commit 25db990bc3a81b78105ec9d6ff1c146d16407d9c Author: yyhuni Date: Fri Dec 12 18:04:57 2025 +0800 Initial commit: Xingrin v1.0.0 diff --git a/.agent/rules/project.md b/.agent/rules/project.md new file mode 100644 index 00000000..c69d49e1 --- /dev/null +++ b/.agent/rules/project.md @@ -0,0 +1,13 @@ +--- +trigger: always_on +--- + +1.后端网页应该是 8888 端口 +2.后端请运行虚拟环境再运行命令,环境在项目根目录~/Desktop/scanner/.venv/bin/python +3.前端所有路由加上末尾斜杠,以匹配 django 的 DRF 规则 +4.网页测试可以用 curl +8.所有前端 api 接口都应该写在@services 中,所有 type 类型都应该写在@types 中 +10.前端的加载等逻辑用 React Query来实现,自动管理 +17.所有业务操作的 toast 都放在 hook 中 +19.目前后端项目,去不用做安全漏洞方面的相关的代码 +23.前端非必要不要采用window.location.href去跳转,而是用Next.js 客户端路由 \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..171a6384 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +# Node modules(前端本地开发产物,Docker 构建时会重新安装) +frontend/node_modules +frontend/.next + +# Python 虚拟环境 +.venv +__pycache__ +*.pyc + +# 日志和临时文件 +*.log +.DS_Store + +# Git +.git +.gitignore + +# IDE +.idea +.vscode + +# Docker 相关(避免嵌套) +docker/.env diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..a6c3df5f --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,84 @@ +name: Build and Push Docker Images + +on: + push: + branches: [main] + paths: + - 'backend/**' + - 'frontend/**' + - 'docker/**' + - '.github/workflows/**' + workflow_dispatch: # 手动触发 + +# 并发控制:同一分支只保留最新的构建,取消之前正在运行的 +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: docker.io + IMAGE_PREFIX: yyhuni + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - image: xingrin-server + dockerfile: docker/server/Dockerfile + context: . + - image: xingrin-frontend + dockerfile: docker/frontend/Dockerfile + context: . + - image: xingrin-worker + dockerfile: docker/worker/Dockerfile + context: . + - image: xingrin-nginx + dockerfile: docker/nginx/Dockerfile + context: . + - image: xingrin-agent + dockerfile: docker/agent/Dockerfile + context: . + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Free disk space (for large builds like worker) + run: | + echo "=== Before cleanup ===" + df -h + # 删除不需要的大型软件包 + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo docker image prune -af + echo "=== After cleanup ===" + df -h + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:latest + ${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6421e6ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +# ============================ +# 操作系统相关文件 +# ============================ +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# ============================ +# 前端 (Next.js/Node.js) 相关 +# ============================ +# 依赖目录 +front-back/node_modules/ +front-back/.pnpm-store/ + +# Next.js 构建产物 +front-back/.next/ +front-back/out/ +front-back/dist/ + +# 环境变量文件 +front-back/.env +front-back/.env.local +front-back/.env.development.local +front-back/.env.test.local +front-back/.env.production.local + +# 运行时和缓存 +front-back/.turbo/ +front-back/.swc/ +front-back/.eslintcache +front-back/.tsbuildinfo + +# ============================ +# 后端 (Python/Django) 相关 +# ============================ +# Python 虚拟环境 +.venv/ +venv/ +env/ +ENV/ + +# Python 编译文件 +*.pyc +*.pyo +*.pyd +__pycache__/ +*.py[cod] +*$py.class + +# Django 相关 +backend/db.sqlite3 +backend/db.sqlite3-journal +backend/media/ +backend/staticfiles/ +backend/.env +backend/.env.local + +# Python 测试和覆盖率 +.pytest_cache/ +.coverage +htmlcov/ +*.cover + +# ============================ +# 后端 (Go) 相关 +# ============================ +# 编译产物 +backend/bin/ +backend/dist/ +backend/*.exe +backend/*.exe~ +backend/*.dll +backend/*.so +backend/*.dylib + +# 测试相关 +backend/*.test +backend/*.out +backend/*.prof + +# Go workspace 文件 +backend/go.work +backend/go.work.sum + +# Go 依赖管理 +backend/vendor/ + +# ============================ +# IDE 和编辑器相关 +# ============================ +.vscode/ +.idea/ +.cursor/ +.claude/ +.playwright-mcp/ +*.swp +*.swo +*~ + +# ============================ +# Docker 相关 +# ============================ +docker/.env +docker/.env.local + +# SSL 证书和私钥(不应提交) +docker/nginx/ssl/*.pem +docker/nginx/ssl/*.key +docker/nginx/ssl/*.crt + +# ============================ +# 日志文件和扫描结果 +# ============================ +*.log +logs/ +results/ + +# 开发脚本运行时文件(进程 ID 和启动日志) +backend/scripts/dev/.pids/ + +# ============================ +# 临时文件 +# ============================ +tmp/ +temp/ +.cache/ + +HGETALL +KEYS diff --git a/.windsurf/rules/backend.md b/.windsurf/rules/backend.md new file mode 100644 index 00000000..c8719109 --- /dev/null +++ b/.windsurf/rules/backend.md @@ -0,0 +1,13 @@ +--- +trigger: always_on +--- + +1.后端网页应该是 8888 端口 +3.前端所有路由加上末尾斜杠,以匹配 django 的 DRF 规则 +4.网页测试可以用 curl +8.所有前端 api 接口都应该写在@services 中,所有 type 类型都应该写在@types 中 +10.前端的加载等逻辑用 React Query来实现,自动管理 +17.所有业务操作的 toast 都放在 hook 中 +19.目前后端项目,去不用做安全漏洞方面的相关的代码 +23.前端非必要不要采用window.location.href去跳转,而是用Next.js 客户端路由 +24.ui相关的都去调用mcp来看看有没有通用组件,美观的组件来实现 \ No newline at end of file diff --git a/.windsurf/rules/code-preview.md b/.windsurf/rules/code-preview.md new file mode 100644 index 00000000..3e2d370c --- /dev/null +++ b/.windsurf/rules/code-preview.md @@ -0,0 +1,85 @@ +--- +trigger: manual +description: 进行代码审查的时候,必须调用这个规则 +--- + +### **0. 逻辑正确性 & Bug 排查** *(最高优先级,必须手动推演)* + +**目标**:不依赖测试,主动发现“代码能跑但结果错”的逻辑错误。 + +1. **手动推演关键路径**: + - 选 2~3 个典型输入(含边界),**在脑中或纸上一步步推演代码执行流程**。 + - 输出是否符合预期?每一步变量变化是否正确? +2. **常见逻辑 bug 检查**: + - **off-by-one**:循环、数组索引、分页 + - **条件逻辑错误**:`and`/`or` 优先级、短路求值误用 + - **状态混乱**:变量未初始化、被意外覆盖 + - **算法偏差**:排序、搜索、二分查找的中点处理 + - **浮点精度**:是否误用 `==` 比较浮点数? +3. **控制流审查**: + - 所有 `if/else` 分支是否都覆盖?有无“不可达代码”? + - `switch`/`match` 是否有 `default`?是否漏 case? + - 异常路径会返回什么?是否遗漏 `finally` 清理? +4. **业务逻辑一致性**: + - 是否符合**业务规则**?(如“订单总额 = 商品价 × 数量 + 运费 - 折扣”) + - 是否遗漏隐含约束?(如“用户只能评价已完成的订单”) + +### **一、功能性 & 正确性** *(阻塞性问题必须修复)* + +1. **需求符合度**:是否100%覆盖需求?遗漏/多余功能点? +2. **边界条件**: + - 输入:`null`、空、极值、非法格式 + - 集合:空、单元素、超大(如10⁶) + - 循环:终止条件、off-by-one +3. **错误处理**: + - 异常捕获全面?失败路径有降级? + - 错误信息清晰?不泄露栈迹? +4. **并发安全**: + - 竞态/死锁风险?共享资源是否同步? + - 使用了`volatile`/`synchronized`/`Lock`/`atomic`? +5. **单元测试**: + - 覆盖率 ≥80%?包含正向/边界/异常用例? + - 测试独立?无外部依赖? + +### **二、代码质量与可读性** + +1. **命名**:见名知意?遵循规范? +2. **函数设计**: + - **单一职责**?参数 ≤4?建议长度 <50行(视语言调整) + - 可提取为工具函数? +3. **结构与复杂度**: + - 无重复代码?圈复杂度 <10? + - 嵌套 ≤3层?使用卫语句提前返回 +4. **注释**:解释**为什么**而非**是什么**?复杂逻辑必注释 +5. **风格一致**:通过`Prettier`/`ESLint`/`Spotless`自动格式化 + +### **三、架构与设计** + +1. **SOLID**:是否符合单一职责、开闭、依赖倒置? +2. **依赖**:是否依赖接口而非实现?无循环依赖? +3. **可测试性**:是否支持依赖注入?避免`new`硬编码 +4. **扩展性**:新增功能是否只需改一处? + +### **四、性能优化** + +- **N+1查询**?循环内IO/日志/分配? +- 算法复杂度合理?(如O(n²)是否可优化) +- 内存:无泄漏?大对象及时释放?缓存有失效? + +### **五、其他** + +1. **可维护性**:日志带上下文?修改后更干净? +2. **兼容性**:API/数据库变更是否向后兼容? +3. **依赖管理**:新库必要?许可证合规? + +--- + +### **审查最佳实践** + +- **小批次审查**:≤200行/次 +- **语气建议**:`“建议将函数拆分以提升可读性”` 而非 `“这个函数太长了”` +- **自动化先行**:风格/空指针/安全扫描 → CI工具 +- **重点分级**: + - 🛑 **阻塞**:功能错、安全漏洞 + - ⚠️ **必须改**:设计缺陷、性能瓶颈 + - 💡 **建议**:风格、命名、可读性 \ No newline at end of file diff --git a/.windsurf/rules/codelayering.md b/.windsurf/rules/codelayering.md new file mode 100644 index 00000000..c9386f6d --- /dev/null +++ b/.windsurf/rules/codelayering.md @@ -0,0 +1,195 @@ +--- +trigger: always_on +--- + +## 标准分层架构调用顺序 + +按照 **DDD(领域驱动设计)和清洁架构**原则,调用顺序应该是: + +``` +HTTP请求 → Views → Tasks → Services → Repositories → Models + +``` + +--- + +### 📊 完整的调用链路图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ HTTP Request (前端) │ +└────────────────────────┬────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Views (HTTP 层) │ +│ - 参数验证 │ +│ - 权限检查 │ +│ - 调用 Tasks/Services │ +│ - 返回 HTTP 响应 │ +└────────────────────────┬────────────────────────────────────┘ + ↓ + ┌────────────────┴────────────────┐ + ↓ (异步) ↓ (同步) +┌──────────────────┐ ┌──────────────────┐ +│ Tasks (任务层) │ │ Services (业务层)│ +│ - 异步执行 │ │ - 业务逻辑 │ +│ - 后台作业 │───────>│ - 事务管理 │ +│ - 通知发送 │ │ - 数据验证 │ +└──────────────────┘ └────────┬─────────┘ + ↓ + ┌──────────────────────┐ + │ Repositories (存储层) │ + │ - 数据访问 │ + │ - 查询封装 │ + │ - 批量操作 │ + └────────┬─────────────┘ + ↓ + ┌──────────────────────┐ + │ Models (模型层) │ + │ - ORM 定义 │ + │ - 数据结构 │ + │ - 关系映射 │ + └──────────────────────┘ + +``` + +--- + +### 🔄 具体调用示例 + +### **场景 1:同步删除(Views → Services → Repositories → Models)** + +```python +# 1. Views 层 (views.py) +def some_sync_delete(self, request): + # 参数验证 + target_ids = request.data.get('ids') + + # 调用 Service 层 + service = TargetService() + result = service.bulk_delete_targets(target_ids) + + # 返回响应 + return Response({'message': 'deleted'}) + +# 2. Services 层 (services/target_service.py) +class TargetService: + def bulk_delete_targets(self, target_ids): + # 业务逻辑验证 + logger.info("准备删除...") + + # 调用 Repository 层 + deleted_count = self.repo.bulk_delete_by_ids(target_ids) + + # 返回结果 + return deleted_count + +# 3. Repositories 层 (repositories/django_target_repository.py) +class DjangoTargetRepository: + def bulk_delete_by_ids(self, target_ids): + # 数据访问操作 + return Target.objects.filter(id__in=target_ids).delete() + +# 4. Models 层 (models.py) +class Target(models.Model): + # ORM 定义 + name = models.CharField(...) + +``` + +--- + +### **场景 2:异步删除(Views → Tasks → Services → Repositories → Models)** + +```python +# 1. Views 层 (views.py) +def destroy(self, request, *args, **kwargs): + target = self.get_object() + + # 调用 Tasks 层(异步) + async_bulk_delete_targets([target.id], [target.name]) + + # 立即返回 202 + return Response(status=202) + +# 2. Tasks 层 (tasks/target_tasks.py) +def async_bulk_delete_targets(target_ids, target_names): + def _delete(): + # 发送通知 + create_notification("删除中...") + + # 调用 Service 层 + service = TargetService() + result = service.bulk_delete_targets(target_ids) + + # 发送完成通知 + create_notification("删除成功") + + # 后台线程执行 + threading.Thread(target=_delete).start() + +# 3. Services 层 (services/target_service.py) +class TargetService: + def bulk_delete_targets(self, target_ids): + # 业务逻辑 + return self.repo.bulk_delete_by_ids(target_ids) + +# 4. Repositories 层 (repositories/django_target_repository.py) +class DjangoTargetRepository: + def bulk_delete_by_ids(self, target_ids): + # 数据访问 + return Target.objects.filter(id__in=target_ids).delete() + +# 5. Models 层 (models.py) +class Target(models.Model): + # ORM 定义 + ... + +``` + +--- + +### 📋 各层职责清单 + +| 层级 | 职责 | 不应该做 | +| --- | --- | --- | +| **Views** | HTTP 请求处理、参数验证、权限检查 | ❌ 直接访问 Models
❌ 业务逻辑 | +| **Tasks** | 异步执行、后台作业、通知发送 | ❌ 直接访问 Models
❌ HTTP 响应 | +| **Services** | 业务逻辑、事务管理、数据验证 | ❌ 直接写 SQL
❌ HTTP 相关 | +| **Repositories** | 数据访问、查询封装、批量操作 | ❌ 业务逻辑
❌ 通知发送 | +| **Models** | ORM 定义、数据结构、关系映射 | ❌ 业务逻辑
❌ 复杂查询 | + +--- + +### ✅ 最佳实践原则 + +1. **单向依赖**:只能向下调用,不能向上调用 + + ``` + Views → Tasks → Services → Repositories → Models + (上层) (下层) + + ``` + +2. **层级隔离**:相邻层交互,禁止跨层 + - ✅ Views → Services + - ✅ Tasks → Services + - ✅ Services → Repositories + - ❌ Views → Repositories(跨层) + - ❌ Tasks → Models(跨层) +3. **依赖注入**:通过构造函数注入依赖 + + ```python + class TargetService: + def __init__(self): + self.repo = DjangoTargetRepository() # 注入 + + ``` + +4. **接口抽象**:使用 Protocol 定义接口 + + ```python + class TargetRepository(Protocol): + def bulk_delete_by_ids(self, ids): ... + + ``` \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ae856995 --- /dev/null +++ b/LICENSE @@ -0,0 +1,131 @@ +# PolyForm Noncommercial License 1.0.0 + + + +## Acceptance + +In order to get any license under these terms, you must agree +to them as both strict obligations and conditions to all +your licenses. + +## Copyright License + +The licensor grants you a copyright license for the +software to do everything you might do with the software +that would otherwise infringe the licensor's copyright +in it for any permitted purpose. However, you may +only distribute the software according to [Distribution +License](#distribution-license) and make changes or new works +based on the software according to [Changes and New Works +License](#changes-and-new-works-license). + +## Distribution License + +The licensor grants you an additional copyright license +to distribute copies of the software. Your license +to distribute covers distributing the software with +changes and new works permitted by [Changes and New Works +License](#changes-and-new-works-license). + +## Notices + +You must ensure that anyone who gets a copy of any part of +the software from you also gets a copy of these terms or the +URL for them above, as well as copies of any plain-text lines +beginning with `Required Notice:` that the licensor provided +with the software. For example: + +> Required Notice: Copyright Yuhang Yang (yyhuni) + +## Changes and New Works License + +The licensor grants you an additional copyright license to +make changes and new works based on the software for any +permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that +covers patent claims the licensor can license, or becomes able +to license, that you would infringe by using the software. + +## Noncommercial Purposes + +Any noncommercial purpose is a permitted purpose. + +## Personal Uses + +Personal use for research, experiment, and testing for +the benefit of public knowledge, personal study, private +entertainment, hobby projects, amateur pursuits, or religious +observance, without any anticipated commercial application, +is use for a permitted purpose. + +## Noncommercial Organizations + +Use by any charitable organization, educational institution, +public research organization, public safety or health +organization, environmental protection organization, +or government institution is use for a permitted purpose +regardless of the source of funding or obligations resulting +from the funding. + +## Fair Use + +You may have "fair use" rights for the software under the +law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of +your licenses to anyone else, or prevent the licensor from +granting licenses to anyone else. These terms do not imply +any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or +contributes to infringement of any patent, your patent license +for the software granted under these terms ends immediately. If +your company makes such a claim, your patent license ends +immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have +violated any of these terms, or done anything with the software +not covered by your licenses, your licenses can nonetheless +continue if you come into full compliance with these terms, +and take practical steps to correct past violations, within +32 days of receiving notice. Otherwise, all your licenses +end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without +any warranty or condition, and the licensor will not be liable +to you for any damages arising out of these terms or the use +or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these +terms, and the **software** is the software the licensor makes +available under these terms. + +**You** refers to the individual or entity agreeing to these +terms. + +**Your company** is any legal entity, sole proprietorship, +or other kind of organization that you work for, plus all +organizations that have control over, are under the control of, +or are under common control with that organization. **Control** +means ownership of substantially all the assets of an entity, +or the power to direct its management and policies by vote, +contract, or otherwise. Control can be direct or indirect. + +**Your licenses** are all the licenses granted to you for the +software under these terms. + +**Use** means anything you do with the software requiring one +of your licenses. diff --git a/README.md b/README.md new file mode 100644 index 00000000..cbbfaf36 --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +

Xingrin - 星环

+ +

+ 一款现代化的企业级漏洞扫描与资产管理平台
+ 提供自动化安全检测、资产发现、漏洞管理等功能 +

+ +--- + +## ✨ 功能特性 + +### 🎯 目标与资产管理 +- **组织管理** - 多层级目标组织,灵活分组 +- **目标管理** - 支持域名、IP、URL 等多种目标类型 +- **资产发现** - 子域名、网站、端点、目录自动发现 +- **资产快照** - 扫描结果快照对比,追踪资产变化 + +### 🔍 漏洞扫描 +- **多引擎支持** - 集成 Nuclei 等主流扫描引擎 +- **自定义流程** - YAML 配置扫描流程,灵活编排 +- **漏洞分级** - 严重/高危/中危/低危 四级分类 +- **定时扫描** - Cron 表达式配置,自动化周期扫描 + +### 🖥️ 分布式架构 +- **Worker 节点** - 支持多节点分布式扫描 +- **本地/远程** - 本地 Docker 节点 + SSH 远程节点 +- **负载均衡** - 自动任务分发与负载监控 +- **实时状态** - WebSocket 实时推送扫描进度 + +### 📊 可视化界面 +- **数据统计** - 资产/漏洞统计仪表盘 +- **实时通知** - WebSocket 消息推送 +- **暗色主题** - 支持明暗主题切换 + +--- + +## 🛠️ 技术栈 + +- **前端**: Next.js + React + TailwindCSS +- **后端**: Django + Django REST Framework +- **数据库**: PostgreSQL + Redis +- **部署**: Docker + Nginx +- **扫描引擎**: Nuclei + +--- + +## 📦 快速开始 + +### 环境要求 + +- Docker 20.10+ +- Docker Compose 2.0+ +- 推荐 2核 4G 内存起步 +- 10GB+ 磁盘空间 + +### 一键安装 + +```bash +# 克隆项目 +git clone https://github.com/yyhuni/xingrin.git +cd xingrin + +# 安装并启动(生产模式) +sudo ./install.sh + +# 开发模式 +sudo ./install.sh --dev +``` + +### 访问服务 + +- **Web 界面**: `https://localhost` 或 `http://localhost` +- **API 接口**: `http://localhost:8888/api/` +- **API 文档**: `http://localhost:8888/swagger/` + +### 常用命令 + +```bash +# 启动服务 +sudo ./start.sh + +# 停止服务 +sudo ./stop.sh + +# 重启服务 +sudo ./restart.sh + +# 卸载 +sudo ./uninstall.sh + +# 更新 +sudo ./update.sh +``` + +## ⚠️ 免责声明 + +**重要:请在使用前仔细阅读** + +1. 本工具仅供**授权的安全测试**和**安全研究**使用 +2. 使用者必须确保已获得目标系统的**合法授权** +3. **严禁**将本工具用于未经授权的渗透测试或攻击行为 +4. 未经授权扫描他人系统属于**违法行为**,可能面临法律责任 +5. 开发者**不对任何滥用行为负责** + +使用本工具即表示您同意: +- 仅在合法授权范围内使用 +- 遵守所在地区的法律法规 +- 承担因滥用产生的一切后果 + +## 📄 许可证 + +本项目采用 [PolyForm Noncommercial License 1.0.0](LICENSE) 许可证。 + +### 允许的用途 + +- ✅ 个人学习和研究 +- ✅ 非商业安全测试 +- ✅ 教育机构使用 +- ✅ 非营利组织使用 + +### 禁止的用途 + +- ❌ **商业用途**(包括但不限于:出售、商业服务、SaaS 等) +- ❌ 未经授权的渗透测试 +- ❌ 任何违法行为 + +如需商业授权,请联系作者。 + +## 🤝 反馈与贡献 + +- 🐛 **发现 Bug?** 欢迎提交 [Issue](https://github.com/yyhuni/xingrin/issues) +- 💡 **有新想法?** 欢迎提交功能建议 +- 🔧 **想参与开发?** 欢迎提交 Pull Request + +## 📧 联系 + +- GitHub: [@yyhuni](https://github.com/yyhuni) +- 微信公众号: **洋洋的小黑屋** + +微信公众号 diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 00000000..b0db71f2 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,51 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ + +# 虚拟环境 +venv/ +env/ +ENV/ + +# Django +*.log +db.sqlite3 +db.sqlite3-journal +/media +/staticfiles + +# 运行时文件(Flower、PID) +/var/* +!/var/.gitkeep +flower.db +pids/ +script/dev/.pids/ + +# 扫描结果和日志(后端数据) +/results/* +!/results/.gitkeep +/logs/* +!/logs/.gitkeep + +# 环境变量(敏感信息) +.env +.env.development +.env.production +.env.staging +# 只提交模板文件:.env.*.example + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# macOS +.DS_Store diff --git a/backend/apps/__init__.py b/backend/apps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/asset/__init__.py b/backend/apps/asset/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/asset/apps.py b/backend/apps/asset/apps.py new file mode 100644 index 00000000..c34b78eb --- /dev/null +++ b/backend/apps/asset/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class AssetConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.asset' + + def ready(self): + # 导入所有模型以确保Django发现并注册 + from . import models diff --git a/backend/apps/asset/dtos/__init__.py b/backend/apps/asset/dtos/__init__.py new file mode 100644 index 00000000..ec9f664c --- /dev/null +++ b/backend/apps/asset/dtos/__init__.py @@ -0,0 +1,28 @@ +"""Asset DTOs - 数据传输对象""" + +# 资产模块 DTOs +from .asset import ( + SubdomainDTO, + WebSiteDTO, + IPAddressDTO, + DirectoryDTO, + PortDTO, + EndpointDTO, +) + +# 快照模块 DTOs +from .snapshot import ( + SubdomainSnapshotDTO, +) + +__all__ = [ + # 资产模块 + 'SubdomainDTO', + 'WebSiteDTO', + 'IPAddressDTO', + 'DirectoryDTO', + 'PortDTO', + 'EndpointDTO', + # 快照模块 + 'SubdomainSnapshotDTO', +] diff --git a/backend/apps/asset/dtos/asset/__init__.py b/backend/apps/asset/dtos/asset/__init__.py new file mode 100644 index 00000000..8a28d729 --- /dev/null +++ b/backend/apps/asset/dtos/asset/__init__.py @@ -0,0 +1,21 @@ +"""Asset DTOs - 数据传输对象""" + +from .subdomain_dto import SubdomainDTO +from .ip_address_dto import IPAddressDTO +from .port_dto import PortDTO +from .website_dto import WebSiteDTO +from .directory_dto import DirectoryDTO +from .host_port_mapping_dto import HostPortMappingDTO +from .endpoint_dto import EndpointDTO +from .vulnerability_dto import VulnerabilityDTO + +__all__ = [ + 'SubdomainDTO', + 'IPAddressDTO', + 'PortDTO', + 'WebSiteDTO', + 'DirectoryDTO', + 'HostPortMappingDTO', + 'EndpointDTO', + 'VulnerabilityDTO', +] diff --git a/backend/apps/asset/dtos/asset/directory_dto.py b/backend/apps/asset/dtos/asset/directory_dto.py new file mode 100644 index 00000000..a1285bac --- /dev/null +++ b/backend/apps/asset/dtos/asset/directory_dto.py @@ -0,0 +1,18 @@ +"""Directory DTO""" + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class DirectoryDTO: + """目录数据传输对象""" + website_id: int + target_id: int + url: str + status: Optional[int] = None + content_length: Optional[int] = None + words: Optional[int] = None + lines: Optional[int] = None + content_type: str = '' + duration: Optional[int] = None diff --git a/backend/apps/asset/dtos/asset/endpoint_dto.py b/backend/apps/asset/dtos/asset/endpoint_dto.py new file mode 100644 index 00000000..f54b90ce --- /dev/null +++ b/backend/apps/asset/dtos/asset/endpoint_dto.py @@ -0,0 +1,28 @@ +"""Endpoint DTO""" + +from dataclasses import dataclass +from typing import Optional, List + + +@dataclass +class EndpointDTO: + """端点 DTO - 资产表数据传输对象""" + target_id: int + url: str + host: Optional[str] = None + title: Optional[str] = None + status_code: Optional[int] = None + content_length: Optional[int] = None + webserver: Optional[str] = None + body_preview: Optional[str] = None + content_type: Optional[str] = None + tech: Optional[List[str]] = None + vhost: Optional[bool] = None + location: Optional[str] = None + matched_gf_patterns: Optional[List[str]] = None + + def __post_init__(self): + if self.tech is None: + self.tech = [] + if self.matched_gf_patterns is None: + self.matched_gf_patterns = [] diff --git a/backend/apps/asset/dtos/asset/host_port_mapping_dto.py b/backend/apps/asset/dtos/asset/host_port_mapping_dto.py new file mode 100644 index 00000000..a0f76345 --- /dev/null +++ b/backend/apps/asset/dtos/asset/host_port_mapping_dto.py @@ -0,0 +1,12 @@ +"""HostPortMapping DTO""" + +from dataclasses import dataclass + + +@dataclass +class HostPortMappingDTO: + """主机端口映射 DTO(资产表)""" + target_id: int + host: str + ip: str + port: int diff --git a/backend/apps/asset/dtos/asset/ip_address_dto.py b/backend/apps/asset/dtos/asset/ip_address_dto.py new file mode 100644 index 00000000..67856400 --- /dev/null +++ b/backend/apps/asset/dtos/asset/ip_address_dto.py @@ -0,0 +1,17 @@ +"""IPAddress DTO""" + +from dataclasses import dataclass + + +@dataclass +class IPAddressDTO: + """ + IP地址数据传输对象 + + 只包含 IP 自身的信息,不包含关联关系。 + 关联关系通过 SubdomainIPAssociationDTO 管理。 + """ + ip: str + protocol_version: str = '' + is_private: bool = False + reverse_pointer: str = '' diff --git a/backend/apps/asset/dtos/asset/port_dto.py b/backend/apps/asset/dtos/asset/port_dto.py new file mode 100644 index 00000000..de8fe6b5 --- /dev/null +++ b/backend/apps/asset/dtos/asset/port_dto.py @@ -0,0 +1,13 @@ +"""Port DTO""" + +from dataclasses import dataclass + + +@dataclass +class PortDTO: + """端口数据传输对象""" + ip_address_id: int + number: int + service_name: str = '' + target_id: int = None + scan_id: int = None diff --git a/backend/apps/asset/dtos/asset/subdomain_dto.py b/backend/apps/asset/dtos/asset/subdomain_dto.py new file mode 100644 index 00000000..22b686c3 --- /dev/null +++ b/backend/apps/asset/dtos/asset/subdomain_dto.py @@ -0,0 +1,15 @@ +"""Subdomain DTO""" + +from dataclasses import dataclass + + +@dataclass +class SubdomainDTO: + """ + 子域名 DTO(纯资产表) + + 用于传递子域名资产数据,只包含资产本身的信息。 + 扫描相关信息存储在快照表中。 + """ + name: str + target_id: int # 必填:子域名必须属于某个目标 diff --git a/backend/apps/asset/dtos/asset/vulnerability_dto.py b/backend/apps/asset/dtos/asset/vulnerability_dto.py new file mode 100644 index 00000000..cefb44f6 --- /dev/null +++ b/backend/apps/asset/dtos/asset/vulnerability_dto.py @@ -0,0 +1,18 @@ +"""Vulnerability DTO""" + +from dataclasses import dataclass, field +from typing import Optional, Dict, Any +from decimal import Decimal + + +@dataclass +class VulnerabilityDTO: + """漏洞数据传输对象(资产表用)""" + target_id: int + url: str + vuln_type: str + severity: str + source: str = "" + cvss_score: Optional[Decimal] = None + description: str = "" + raw_output: Dict[str, Any] = field(default_factory=dict) diff --git a/backend/apps/asset/dtos/asset/website_dto.py b/backend/apps/asset/dtos/asset/website_dto.py new file mode 100644 index 00000000..da38f987 --- /dev/null +++ b/backend/apps/asset/dtos/asset/website_dto.py @@ -0,0 +1,26 @@ +"""WebSite DTO""" + +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class WebSiteDTO: + """网站数据传输对象""" + target_id: int + url: str + host: str + title: str = '' + status_code: Optional[int] = None + content_length: Optional[int] = None + location: str = '' + webserver: str = '' + content_type: str = '' + tech: List[str] = None + body_preview: str = '' + vhost: Optional[bool] = None + created_at: str = None + + def __post_init__(self): + if self.tech is None: + self.tech = [] diff --git a/backend/apps/asset/dtos/snapshot/__init__.py b/backend/apps/asset/dtos/snapshot/__init__.py new file mode 100644 index 00000000..cbd8d6b6 --- /dev/null +++ b/backend/apps/asset/dtos/snapshot/__init__.py @@ -0,0 +1,17 @@ +"""Snapshot DTOs""" + +from .subdomain_snapshot_dto import SubdomainSnapshotDTO +from .host_port_mapping_snapshot_dto import HostPortMappingSnapshotDTO +from .website_snapshot_dto import WebsiteSnapshotDTO +from .directory_snapshot_dto import DirectorySnapshotDTO +from .endpoint_snapshot_dto import EndpointSnapshotDTO +from .vulnerability_snapshot_dto import VulnerabilitySnapshotDTO + +__all__ = [ + 'SubdomainSnapshotDTO', + 'HostPortMappingSnapshotDTO', + 'WebsiteSnapshotDTO', + 'DirectorySnapshotDTO', + 'EndpointSnapshotDTO', + 'VulnerabilitySnapshotDTO', +] diff --git a/backend/apps/asset/dtos/snapshot/directory_snapshot_dto.py b/backend/apps/asset/dtos/snapshot/directory_snapshot_dto.py new file mode 100644 index 00000000..f166a619 --- /dev/null +++ b/backend/apps/asset/dtos/snapshot/directory_snapshot_dto.py @@ -0,0 +1,48 @@ +"""Directory Snapshot DTO""" + +from dataclasses import dataclass +from typing import Optional +from apps.asset.dtos.asset import DirectoryDTO + + +@dataclass +class DirectorySnapshotDTO: + """ + 目录快照数据传输对象 + + 用于保存扫描过程中发现的目录信息到快照表 + + 注意:website_id 和 target_id 只用于传递数据和转换为资产 DTO,不会保存到快照表中。 + 快照只属于 scan。 + """ + scan_id: int + website_id: int # 仅用于传递数据,不保存到数据库 + target_id: int # 仅用于传递数据,不保存到数据库 + url: str + status: Optional[int] = None + content_length: Optional[int] = None + words: Optional[int] = None + lines: Optional[int] = None + content_type: str = '' + duration: Optional[int] = None + + def to_asset_dto(self) -> DirectoryDTO: + """ + 转换为资产 DTO(用于同步到资产表) + + 注意:去除 scan_id 字段,因为资产表不需要 + + Returns: + DirectoryDTO: 资产表 DTO + """ + return DirectoryDTO( + website_id=self.website_id, + target_id=self.target_id, + url=self.url, + status=self.status, + content_length=self.content_length, + words=self.words, + lines=self.lines, + content_type=self.content_type, + duration=self.duration + ) diff --git a/backend/apps/asset/dtos/snapshot/endpoint_snapshot_dto.py b/backend/apps/asset/dtos/snapshot/endpoint_snapshot_dto.py new file mode 100644 index 00000000..f2194f86 --- /dev/null +++ b/backend/apps/asset/dtos/snapshot/endpoint_snapshot_dto.py @@ -0,0 +1,62 @@ +"""EndpointSnapshot DTO""" + +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class EndpointSnapshotDTO: + """ + 端点快照 DTO + + 注意:target_id 只用于传递数据和转换为资产 DTO,不会保存到快照表中。 + 快照只属于 scan。 + """ + scan_id: int + url: str + host: str = '' # 主机名(域名或IP地址) + title: str = '' + status_code: Optional[int] = None + content_length: Optional[int] = None + location: str = '' + webserver: str = '' + content_type: str = '' + tech: List[str] = None + body_preview: str = '' + vhost: Optional[bool] = None + matched_gf_patterns: List[str] = None + target_id: Optional[int] = None # 冗余字段,用于同步到资产表 + + def __post_init__(self): + if self.tech is None: + self.tech = [] + if self.matched_gf_patterns is None: + self.matched_gf_patterns = [] + + def to_asset_dto(self): + """ + 转换为资产 DTO(用于同步到资产表) + + Returns: + EndpointDTO: 资产表 DTO(移除 scan_id) + """ + from apps.asset.dtos.asset import EndpointDTO + + if self.target_id is None: + raise ValueError("target_id 不能为 None,无法同步到资产表") + + return EndpointDTO( + target_id=self.target_id, + url=self.url, + host=self.host, + title=self.title, + status_code=self.status_code, + content_length=self.content_length, + webserver=self.webserver, + body_preview=self.body_preview, + content_type=self.content_type, + tech=self.tech if self.tech else [], + vhost=self.vhost, + location=self.location, + matched_gf_patterns=self.matched_gf_patterns if self.matched_gf_patterns else [] + ) diff --git a/backend/apps/asset/dtos/snapshot/host_port_mapping_snapshot_dto.py b/backend/apps/asset/dtos/snapshot/host_port_mapping_snapshot_dto.py new file mode 100644 index 00000000..e56c4e76 --- /dev/null +++ b/backend/apps/asset/dtos/snapshot/host_port_mapping_snapshot_dto.py @@ -0,0 +1,33 @@ +"""HostPortMappingSnapshot DTO""" + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class HostPortMappingSnapshotDTO: + """主机端口映射快照 DTO""" + scan_id: int + host: str + ip: str + port: int + target_id: Optional[int] = None # 冗余字段,用于同步到资产表 + + def to_asset_dto(self): + """ + 转换为资产 DTO(用于同步到资产表) + + Returns: + HostPortMappingDTO: 资产表 DTO(移除 scan_id) + """ + from apps.asset.dtos.asset import HostPortMappingDTO + + if self.target_id is None: + raise ValueError("target_id 不能为 None,无法同步到资产表") + + return HostPortMappingDTO( + target_id=self.target_id, + host=self.host, + ip=self.ip, + port=self.port + ) diff --git a/backend/apps/asset/dtos/snapshot/subdomain_snapshot_dto.py b/backend/apps/asset/dtos/snapshot/subdomain_snapshot_dto.py new file mode 100644 index 00000000..b0952a53 --- /dev/null +++ b/backend/apps/asset/dtos/snapshot/subdomain_snapshot_dto.py @@ -0,0 +1,34 @@ +"""SubdomainSnapshot DTO""" + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from apps.asset.dtos import SubdomainDTO + + +@dataclass +class SubdomainSnapshotDTO: + """ + 子域名快照 DTO + + 用于传递快照数据,包含完整的业务上下文信息。 + 快照表记录每次扫描的历史数据。 + """ + name: str + scan_id: int # 必填:快照必须关联扫描任务 + target_id: int # 必填:目标ID(用于转换为资产 DTO) + + def to_asset_dto(self) -> 'SubdomainDTO': + """ + 转换为资产 DTO(用于保存到资产表) + + Returns: + SubdomainDTO: 资产 DTO(不包含 scan_id) + + Note: + 资产表只存储核心数据,扫描上下文(scan_id)不保存到资产表。 + target_id 已经包含在 DTO 中,无需额外传参。 + """ + from apps.asset.dtos import SubdomainDTO + return SubdomainDTO(name=self.name, target_id=self.target_id) diff --git a/backend/apps/asset/dtos/snapshot/vulnerability_snapshot_dto.py b/backend/apps/asset/dtos/snapshot/vulnerability_snapshot_dto.py new file mode 100644 index 00000000..dfd1e8b0 --- /dev/null +++ b/backend/apps/asset/dtos/snapshot/vulnerability_snapshot_dto.py @@ -0,0 +1,42 @@ +"""VulnerabilitySnapshot DTO""" + +from dataclasses import dataclass, field +from typing import Optional, Dict, Any +from decimal import Decimal + + +@dataclass +class VulnerabilitySnapshotDTO: + """漏洞快照 DTO + + 对应 VulnerabilitySnapshot 模型,用于在 Service/Task 之间传递漏洞结果数据。 + + 设计与其他快照 DTO 一致: + - scan_id: 只属于快照表 + - target_id: 只用于转换为资产 DTO,不直接存入快照表 + """ + + scan_id: int + target_id: int # 仅用于传递数据和生成资产 DTO,不保存到快照表 + url: str + vuln_type: str + severity: str + source: str = "" + cvss_score: Optional[Decimal] = None + description: str = "" + raw_output: Dict[str, Any] = field(default_factory=dict) + + def to_asset_dto(self): + """转换为漏洞资产 DTO(用于同步到 Vulnerability 表)。""" + from apps.asset.dtos.asset import VulnerabilityDTO + + return VulnerabilityDTO( + target_id=self.target_id, + url=self.url, + vuln_type=self.vuln_type, + severity=self.severity, + source=self.source, + cvss_score=self.cvss_score, + description=self.description, + raw_output=self.raw_output, + ) diff --git a/backend/apps/asset/dtos/snapshot/website_snapshot_dto.py b/backend/apps/asset/dtos/snapshot/website_snapshot_dto.py new file mode 100644 index 00000000..c28c3fd8 --- /dev/null +++ b/backend/apps/asset/dtos/snapshot/website_snapshot_dto.py @@ -0,0 +1,55 @@ +"""WebsiteSnapshot DTO""" + +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class WebsiteSnapshotDTO: + """ + 网站快照 DTO + + 注意:target_id 只用于传递数据和转换为资产 DTO,不会保存到快照表中。 + 快照只属于 scan,target 信息通过 scan.target 获取。 + """ + scan_id: int + target_id: int # 仅用于传递数据,不保存到数据库 + url: str + host: str + title: str = '' + status: Optional[int] = None + content_length: Optional[int] = None + location: str = '' + web_server: str = '' + content_type: str = '' + tech: List[str] = None + body_preview: str = '' + vhost: Optional[bool] = None + + def __post_init__(self): + if self.tech is None: + self.tech = [] + + def to_asset_dto(self): + """ + 转换为资产 DTO(用于同步到资产表) + + Returns: + WebSiteDTO: 资产表 DTO(移除 scan_id) + """ + from apps.asset.dtos.asset import WebSiteDTO + + return WebSiteDTO( + target_id=self.target_id, + url=self.url, + host=self.host, + title=self.title, + status_code=self.status, + content_length=self.content_length, + location=self.location, + webserver=self.web_server, + content_type=self.content_type, + tech=self.tech if self.tech else [], + body_preview=self.body_preview, + vhost=self.vhost + ) diff --git a/backend/apps/asset/migrations/__init__.py b/backend/apps/asset/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/asset/models/__init__.py b/backend/apps/asset/models/__init__.py new file mode 100644 index 00000000..ca0c4955 --- /dev/null +++ b/backend/apps/asset/models/__init__.py @@ -0,0 +1,45 @@ +# 导入所有模型,确保Django能发现它们 + +# 业务模型 +from .asset_models import ( + Subdomain, + WebSite, + Endpoint, + Directory, + HostPortMapping, + Vulnerability, +) + +# 快照模型 +from .snapshot_models import ( + SubdomainSnapshot, + WebsiteSnapshot, + DirectorySnapshot, + HostPortMappingSnapshot, + EndpointSnapshot, + VulnerabilitySnapshot, +) + +# 统计模型 +from .statistics_models import AssetStatistics, StatisticsHistory + +# 导出所有模型供外部导入 +__all__ = [ + # 业务模型 + 'Subdomain', + 'WebSite', + 'Endpoint', + 'Directory', + 'HostPortMapping', + 'Vulnerability', + # 快照模型 + 'SubdomainSnapshot', + 'WebsiteSnapshot', + 'DirectorySnapshot', + 'HostPortMappingSnapshot', + 'EndpointSnapshot', + 'VulnerabilitySnapshot', + # 统计模型 + 'AssetStatistics', + 'StatisticsHistory', +] diff --git a/backend/apps/asset/models/asset_models.py b/backend/apps/asset/models/asset_models.py new file mode 100644 index 00000000..58c59606 --- /dev/null +++ b/backend/apps/asset/models/asset_models.py @@ -0,0 +1,515 @@ + +from django.db import models +from django.contrib.postgres.fields import ArrayField +from django.core.validators import MinValueValidator, MaxValueValidator + + +class SoftDeleteManager(models.Manager): + """软删除管理器:默认只返回未删除的记录""" + + def get_queryset(self): + return super().get_queryset().filter(deleted_at__isnull=True) + + +class Subdomain(models.Model): + """ + 子域名模型(纯资产表) + + 设计特点: + - 只存储子域名资产信息 + - 与其他资产表(IPAddress、Port)无直接关联 + - 扫描历史记录存储在 SubdomainSnapshot 快照表中 + """ + + id = models.AutoField(primary_key=True) + target = models.ForeignKey( + 'targets.Target', # 使用字符串引用避免循环导入 + on_delete=models.CASCADE, + related_name='subdomains', + help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)' + ) + name = models.CharField(max_length=1000, help_text='子域名名称') + discovered_at = models.DateTimeField(auto_now_add=True, help_text='首次发现时间') + + # ==================== 软删除字段 ==================== + deleted_at = models.DateTimeField(null=True, blank=True, db_index=True, help_text='删除时间(NULL表示未删除)') + + # ==================== 管理器 ==================== + objects = SoftDeleteManager() # 默认管理器:只返回未删除的记录 + all_objects = models.Manager() # 全量管理器:包括已删除的记录(用于硬删除) + + class Meta: + db_table = 'subdomain' + verbose_name = '子域名' + verbose_name_plural = '子域名' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['-discovered_at']), + models.Index(fields=['name', 'target']), # 复合索引,优化 get_by_names_and_target_id 批量查询 + models.Index(fields=['target']), # 优化从target_id快速查找下面的子域名 + models.Index(fields=['name']), # 优化从name快速查找子域名,搜索场景 + models.Index(fields=['deleted_at', '-discovered_at']), # 软删除 + 时间索引 + ] + constraints = [ + # 部分唯一约束:只对未删除记录生效 + models.UniqueConstraint( + fields=['name', 'target'], + condition=models.Q(deleted_at__isnull=True), + name='unique_name_target_active' + ) + ] + + def __str__(self): + return str(self.name or f'Subdomain {self.id}') + + +class Endpoint(models.Model): + """端点模型""" + + id = models.AutoField(primary_key=True) + target = models.ForeignKey( + 'targets.Target', # 使用字符串引用 + on_delete=models.CASCADE, + related_name='endpoints', + help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)' + ) + + url = models.CharField(max_length=2000, help_text='最终访问的完整URL') + host = models.CharField( + max_length=253, + blank=True, + default='', + help_text='主机名(域名或IP地址)' + ) + location = models.CharField( + max_length=1000, + blank=True, + default='', + help_text='重定向地址(HTTP 3xx 响应头 Location)' + ) + discovered_at = models.DateTimeField(auto_now_add=True, help_text='发现时间') + title = models.CharField( + max_length=1000, + blank=True, + default='', + help_text='网页标题(HTML 标签内容)' + ) + webserver = models.CharField( + max_length=200, + blank=True, + default='', + help_text='服务器类型(HTTP 响应头 Server 值)' + ) + body_preview = models.CharField( + max_length=1000, + blank=True, + default='', + help_text='响应正文前N个字符(默认100个字符)' + ) + content_type = models.CharField( + max_length=200, + blank=True, + default='', + help_text='响应类型(HTTP Content-Type 响应头)' + ) + tech = ArrayField( + models.CharField(max_length=100), + blank=True, + default=list, + help_text='技术栈(服务器/框架/语言等)' + ) + status_code = models.IntegerField( + null=True, + blank=True, + help_text='HTTP状态码' + ) + content_length = models.IntegerField( + null=True, + blank=True, + help_text='响应体大小(单位字节)' + ) + vhost = models.BooleanField( + null=True, + blank=True, + help_text='是否支持虚拟主机' + ) + matched_gf_patterns = ArrayField( + models.CharField(max_length=100), + blank=True, + default=list, + help_text='匹配的GF模式列表,用于识别敏感端点(如api, debug, config等)' + ) + + # ==================== 软删除字段 ==================== + deleted_at = models.DateTimeField(null=True, blank=True, db_index=True, help_text='删除时间(NULL表示未删除)') + + # ==================== 管理器 ==================== + objects = SoftDeleteManager() # 默认管理器:只返回未删除的记录 + all_objects = models.Manager() # 全量管理器:包括已删除的记录(用于硬删除) + + class Meta: + db_table = 'endpoint' + verbose_name = '端点' + verbose_name_plural = '端点' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['-discovered_at']), + models.Index(fields=['target']), # 优化从target_id快速查找下面的端点(主关联字段) + models.Index(fields=['url']), # URL索引,优化查询性能 + models.Index(fields=['host']), # host索引,优化根据主机名查询 + models.Index(fields=['status_code']), # 状态码索引,优化筛选 + models.Index(fields=['deleted_at', '-discovered_at']), # 软删除 + 时间索引 + ] + constraints = [ + # 部分唯一约束:只对未删除记录生效 + models.UniqueConstraint( + fields=['url', 'target'], + condition=models.Q(deleted_at__isnull=True), + name='unique_endpoint_url_target_active' + ) + ] + + def __str__(self): + return str(self.url or f'Endpoint {self.id}') + + +class WebSite(models.Model): + """站点模型""" + + id = models.AutoField(primary_key=True) + target = models.ForeignKey( + 'targets.Target', # 使用字符串引用 + on_delete=models.CASCADE, + related_name='websites', + help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)' + ) + + url = models.CharField(max_length=2000, help_text='最终访问的完整URL') + host = models.CharField( + max_length=253, + blank=True, + default='', + help_text='主机名(域名或IP地址)' + ) + location = models.CharField( + max_length=1000, + blank=True, + default='', + help_text='重定向地址(HTTP 3xx 响应头 Location)' + ) + discovered_at = models.DateTimeField(auto_now_add=True, help_text='发现时间') + title = models.CharField( + max_length=1000, + blank=True, + default='', + help_text='网页标题(HTML <title> 标签内容)' + ) + webserver = models.CharField( + max_length=200, + blank=True, + default='', + help_text='服务器类型(HTTP 响应头 Server 值)' + ) + body_preview = models.CharField( + max_length=1000, + blank=True, + default='', + help_text='响应正文前N个字符(默认100个字符)' + ) + content_type = models.CharField( + max_length=200, + blank=True, + default='', + help_text='响应类型(HTTP Content-Type 响应头)' + ) + tech = ArrayField( + models.CharField(max_length=100), + blank=True, + default=list, + help_text='技术栈(服务器/框架/语言等)' + ) + status_code = models.IntegerField( + null=True, + blank=True, + help_text='HTTP状态码' + ) + content_length = models.IntegerField( + null=True, + blank=True, + help_text='响应体大小(单位字节)' + ) + vhost = models.BooleanField( + null=True, + blank=True, + help_text='是否支持虚拟主机' + ) + + # ==================== 软删除字段 ==================== + deleted_at = models.DateTimeField(null=True, blank=True, db_index=True, help_text='删除时间(NULL表示未删除)') + + # ==================== 管理器 ==================== + objects = SoftDeleteManager() # 默认管理器:只返回未删除的记录 + all_objects = models.Manager() # 全量管理器:包括已删除的记录(用于硬删除) + + class Meta: + db_table = 'website' + verbose_name = '站点' + verbose_name_plural = '站点' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['-discovered_at']), + models.Index(fields=['url']), # URL索引,优化查询性能 + models.Index(fields=['host']), # host索引,优化根据主机名查询 + models.Index(fields=['target']), # 优化从target_id快速查找下面的站点 + models.Index(fields=['deleted_at', '-discovered_at']), # 软删除 + 时间索引 + ] + constraints = [ + # 部分唯一约束:只对未删除记录生效 + models.UniqueConstraint( + fields=['url', 'target'], + condition=models.Q(deleted_at__isnull=True), + name='unique_website_url_target_active' + ) + ] + + def __str__(self): + return str(self.url or f'Website {self.id}') + + +class Directory(models.Model): + """ + 目录模型 + """ + + id = models.AutoField(primary_key=True) + website = models.ForeignKey( + 'Website', + on_delete=models.CASCADE, + related_name='directories', + help_text='所属的站点(主关联字段,表示所属关系,不能为空)' + ) + target = models.ForeignKey( + 'targets.Target', # 使用字符串引用 + on_delete=models.CASCADE, + related_name='directories', + null=True, + blank=True, + help_text='所属的扫描目标(冗余字段,用于快速查询)' + ) + + url = models.CharField( + null=False, + blank=False, + max_length=2000, + help_text='完整请求 URL' + ) + status = models.IntegerField( + null=True, + blank=True, + help_text='HTTP 响应状态码' + ) + content_length = models.BigIntegerField( + null=True, + blank=True, + help_text='响应体字节大小(Content-Length 或实际长度)' + ) + words = models.IntegerField( + null=True, + blank=True, + help_text='响应体中单词数量(按空格分割)' + ) + lines = models.IntegerField( + null=True, + blank=True, + help_text='响应体行数(按换行符分割)' + ) + content_type = models.CharField( + max_length=200, + blank=True, + default='', + help_text='响应头 Content-Type 值' + ) + duration = models.BigIntegerField( + null=True, + blank=True, + help_text='请求耗时(单位:纳秒)' + ) + + discovered_at = models.DateTimeField(auto_now_add=True, help_text='发现时间') + + # ==================== 软删除字段 ==================== + deleted_at = models.DateTimeField(null=True, blank=True, db_index=True, help_text='删除时间(NULL表示未删除)') + + # ==================== 管理器 ==================== + objects = SoftDeleteManager() # 默认管理器:只返回未删除的记录 + all_objects = models.Manager() # 全量管理器:包括已删除的记录(用于硬删除) + + class Meta: + db_table = 'directory' + verbose_name = '目录' + verbose_name_plural = '目录' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['-discovered_at']), + models.Index(fields=['target']), # 优化从target_id快速查找下面的目录 + models.Index(fields=['url']), # URL索引,优化搜索和唯一约束 + models.Index(fields=['website']), # 站点索引,优化按站点查询 + models.Index(fields=['status']), # 状态码索引,优化筛选 + models.Index(fields=['deleted_at', '-discovered_at']), # 软删除 + 时间索引 + ] + constraints = [ + # 部分唯一约束:只对未删除记录生效 + models.UniqueConstraint( + fields=['website', 'url'], + condition=models.Q(deleted_at__isnull=True), + name='unique_directory_url_website_active' + ), + ] + + def __str__(self): + return str(self.url or f'Directory {self.id}') + + +class HostPortMapping(models.Model): + """ + 主机端口映射表 + + 设计特点: + - 存储主机(host)、IP、端口的三元映射关系 + - 只关联 target_id,不关联其他资产表 + - target + host + ip + port 组成复合唯一约束 + """ + + id = models.AutoField(primary_key=True) + + # ==================== 关联字段 ==================== + target = models.ForeignKey( + 'targets.Target', + on_delete=models.CASCADE, + related_name='host_port_mappings', + help_text='所属的扫描目标' + ) + + # ==================== 核心字段 ==================== + host = models.CharField( + max_length=1000, + blank=False, + help_text='主机名(域名或IP)' + ) + ip = models.GenericIPAddressField( + blank=False, + help_text='IP地址' + ) + port = models.IntegerField( + blank=False, + validators=[ + MinValueValidator(1, message='端口号必须大于等于1'), + MaxValueValidator(65535, message='端口号必须小于等于65535') + ], + help_text='端口号(1-65535)' + ) + + # ==================== 时间字段 ==================== + discovered_at = models.DateTimeField( + auto_now_add=True, + help_text='发现时间' + ) + + # ==================== 软删除字段 ==================== + deleted_at = models.DateTimeField( + null=True, + blank=True, + db_index=True, + help_text='删除时间(NULL表示未删除)' + ) + + # ==================== 管理器 ==================== + objects = SoftDeleteManager() # 默认管理器:只返回未删除的记录 + all_objects = models.Manager() # 全量管理器:包括已删除的记录(用于硬删除) + + class Meta: + db_table = 'host_port_mapping' + verbose_name = '主机端口映射' + verbose_name_plural = '主机端口映射' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['target']), # 优化按目标查询 + models.Index(fields=['host']), # 优化按主机名查询 + models.Index(fields=['ip']), # 优化按IP查询 + models.Index(fields=['port']), # 优化按端口查询 + models.Index(fields=['host', 'ip']), # 优化组合查询 + models.Index(fields=['-discovered_at']), # 优化时间排序 + models.Index(fields=['deleted_at', '-discovered_at']), # 软删除 + 时间索引 + ] + constraints = [ + # 复合唯一约束:target + host + ip + port 组合唯一(只对未删除记录生效) + models.UniqueConstraint( + fields=['target', 'host', 'ip', 'port'], + condition=models.Q(deleted_at__isnull=True), + name='unique_target_host_ip_port_active' + ), + ] + + def __str__(self): + return f'{self.host} ({self.ip}:{self.port})' + + +class Vulnerability(models.Model): + """ + 漏洞模型(资产表) + + 存储发现的漏洞资产,与 Target 关联。 + 扫描历史记录存储在 VulnerabilitySnapshot 快照表中。 + """ + + # 延迟导入避免循环引用 + from apps.common.definitions import VulnSeverity + + id = models.AutoField(primary_key=True) + target = models.ForeignKey( + 'targets.Target', + on_delete=models.CASCADE, + related_name='vulnerabilities', + help_text='所属的扫描目标' + ) + + # ==================== 核心字段 ==================== + url = models.TextField(help_text='漏洞所在的URL') + vuln_type = models.CharField(max_length=100, help_text='漏洞类型(如 xss, sqli)') + severity = models.CharField( + max_length=20, + choices=VulnSeverity.choices, + default=VulnSeverity.UNKNOWN, + help_text='严重性(未知/信息/低/中/高/危急)' + ) + source = models.CharField(max_length=50, blank=True, default='', help_text='来源工具(如 dalfox, nuclei, crlfuzz)') + cvss_score = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True, help_text='CVSS 评分(0.0-10.0)') + description = models.TextField(blank=True, default='', help_text='漏洞描述') + raw_output = models.JSONField(blank=True, default=dict, help_text='工具原始输出') + + # ==================== 时间字段 ==================== + discovered_at = models.DateTimeField(auto_now_add=True, help_text='首次发现时间') + + # ==================== 软删除字段 ==================== + deleted_at = models.DateTimeField(null=True, blank=True, db_index=True, help_text='删除时间(NULL表示未删除)') + + # ==================== 管理器 ==================== + objects = SoftDeleteManager() + all_objects = models.Manager() + + class Meta: + db_table = 'vulnerability' + verbose_name = '漏洞' + verbose_name_plural = '漏洞' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['target']), + models.Index(fields=['vuln_type']), + models.Index(fields=['severity']), + models.Index(fields=['source']), + models.Index(fields=['-discovered_at']), + models.Index(fields=['deleted_at', '-discovered_at']), + ] + + def __str__(self): + return f'{self.vuln_type} - {self.url[:50]}' diff --git a/backend/apps/asset/models/snapshot_models.py b/backend/apps/asset/models/snapshot_models.py new file mode 100644 index 00000000..646573b9 --- /dev/null +++ b/backend/apps/asset/models/snapshot_models.py @@ -0,0 +1,335 @@ +from django.db import models +from django.contrib.postgres.fields import ArrayField +from django.core.validators import MinValueValidator, MaxValueValidator + + +class SubdomainSnapshot(models.Model): + """子域名快照""" + + id = models.AutoField(primary_key=True) + scan = models.ForeignKey( + 'scan.Scan', + on_delete=models.CASCADE, + related_name='subdomain_snapshots', + help_text='所属的扫描任务' + ) + + name = models.CharField(max_length=1000, help_text='子域名名称') + discovered_at = models.DateTimeField(auto_now_add=True, help_text='发现时间') + + class Meta: + db_table = 'subdomain_snapshot' + verbose_name = '子域名快照' + verbose_name_plural = '子域名快照' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['scan']), + models.Index(fields=['name']), + models.Index(fields=['-discovered_at']), + ] + constraints = [ + # 唯一约束:同一次扫描中,同一个子域名只能记录一次 + models.UniqueConstraint( + fields=['scan', 'name'], + name='unique_subdomain_per_scan_snapshot' + ), + ] + + def __str__(self): + return f'{self.name} (Scan #{self.scan_id})' + +class WebsiteSnapshot(models.Model): + """ + 网站快照 + + 记录:某次扫描中发现的网站 + """ + + id = models.AutoField(primary_key=True) + scan = models.ForeignKey( + 'scan.Scan', + on_delete=models.CASCADE, + related_name='website_snapshots', + help_text='所属的扫描任务' + ) + + # 扫描结果数据 + url = models.CharField(max_length=2000, help_text='站点URL') + host = models.CharField(max_length=253, blank=True, default='', help_text='主机名(域名或IP地址)') + title = models.CharField(max_length=500, blank=True, default='', help_text='页面标题') + status = models.IntegerField(null=True, blank=True, help_text='HTTP状态码') + content_length = models.BigIntegerField(null=True, blank=True, help_text='内容长度') + location = models.CharField(max_length=1000, blank=True, default='', help_text='重定向位置') + web_server = models.CharField(max_length=200, blank=True, default='', help_text='Web服务器') + content_type = models.CharField(max_length=200, blank=True, default='', help_text='内容类型') + tech = ArrayField( + models.CharField(max_length=100), + blank=True, + default=list, + help_text='技术栈' + ) + body_preview = models.TextField(blank=True, default='', help_text='响应体预览') + vhost = models.BooleanField(null=True, blank=True, help_text='虚拟主机标志') + discovered_at = models.DateTimeField(auto_now_add=True, help_text='发现时间') + + class Meta: + db_table = 'website_snapshot' + verbose_name = '网站快照' + verbose_name_plural = '网站快照' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['scan']), + models.Index(fields=['url']), + models.Index(fields=['host']), # host索引,优化根据主机名查询 + models.Index(fields=['-discovered_at']), + ] + constraints = [ + # 唯一约束:同一次扫描中,同一个URL只能记录一次 + models.UniqueConstraint( + fields=['scan', 'url'], + name='unique_website_per_scan_snapshot' + ), + ] + + def __str__(self): + return f'{self.url} (Scan #{self.scan_id})' + + +class DirectorySnapshot(models.Model): + """ + 目录快照 + + 记录:某次扫描中发现的目录 + """ + + id = models.AutoField(primary_key=True) + scan = models.ForeignKey( + 'scan.Scan', + on_delete=models.CASCADE, + related_name='directory_snapshots', + help_text='所属的扫描任务' + ) + + # 扫描结果数据 + url = models.CharField(max_length=2000, help_text='目录URL') + status = models.IntegerField(null=True, blank=True, help_text='HTTP状态码') + content_length = models.BigIntegerField(null=True, blank=True, help_text='内容长度') + words = models.IntegerField(null=True, blank=True, help_text='响应体中单词数量(按空格分割)') + lines = models.IntegerField(null=True, blank=True, help_text='响应体行数(按换行符分割)') + content_type = models.CharField(max_length=200, blank=True, default='', help_text='响应头 Content-Type 值') + duration = models.BigIntegerField(null=True, blank=True, help_text='请求耗时(单位:纳秒)') + discovered_at = models.DateTimeField(auto_now_add=True, help_text='发现时间') + + class Meta: + db_table = 'directory_snapshot' + verbose_name = '目录快照' + verbose_name_plural = '目录快照' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['scan']), + models.Index(fields=['url']), + models.Index(fields=['status']), # 状态码索引,优化筛选 + models.Index(fields=['-discovered_at']), + ] + constraints = [ + # 唯一约束:同一次扫描中,同一个目录URL只能记录一次 + models.UniqueConstraint( + fields=['scan', 'url'], + name='unique_directory_per_scan_snapshot' + ), + ] + + def __str__(self): + return f'{self.url} (Scan #{self.scan_id})' + + +class HostPortMappingSnapshot(models.Model): + """ + 主机端口映射快照表 + + 设计特点: + - 存储某次扫描中发现的主机(host)、IP、端口的三元映射关系 + - 主关联 scan_id,记录扫描历史 + - scan + host + ip + port 组成复合唯一约束 + """ + + id = models.AutoField(primary_key=True) + + # ==================== 关联字段 ==================== + scan = models.ForeignKey( + 'scan.Scan', + on_delete=models.CASCADE, + related_name='host_port_mapping_snapshots', + help_text='所属的扫描任务(主关联)' + ) + + # ==================== 核心字段 ==================== + host = models.CharField( + max_length=1000, + blank=False, + help_text='主机名(域名或IP)' + ) + ip = models.GenericIPAddressField( + blank=False, + help_text='IP地址' + ) + port = models.IntegerField( + blank=False, + validators=[ + MinValueValidator(1, message='端口号必须大于等于1'), + MaxValueValidator(65535, message='端口号必须小于等于65535') + ], + help_text='端口号(1-65535)' + ) + + # ==================== 时间字段 ==================== + discovered_at = models.DateTimeField( + auto_now_add=True, + help_text='发现时间' + ) + + class Meta: + db_table = 'host_port_mapping_snapshot' + verbose_name = '主机端口映射快照' + verbose_name_plural = '主机端口映射快照' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['scan']), # 优化按扫描查询 + models.Index(fields=['host']), # 优化按主机名查询 + models.Index(fields=['ip']), # 优化按IP查询 + models.Index(fields=['port']), # 优化按端口查询 + models.Index(fields=['host', 'ip']), # 优化组合查询 + models.Index(fields=['scan', 'host']), # 优化扫描+主机查询 + models.Index(fields=['-discovered_at']), # 优化时间排序 + ] + constraints = [ + # 复合唯一约束:同一次扫描中,scan + host + ip + port 组合唯一 + models.UniqueConstraint( + fields=['scan', 'host', 'ip', 'port'], + name='unique_scan_host_ip_port_snapshot' + ), + ] + + def __str__(self): + return f'{self.host} ({self.ip}:{self.port}) [Scan #{self.scan_id}]' + + +class EndpointSnapshot(models.Model): + """ + 端点快照 + + 记录:某次扫描中发现的端点 + """ + + id = models.AutoField(primary_key=True) + scan = models.ForeignKey( + 'scan.Scan', + on_delete=models.CASCADE, + related_name='endpoint_snapshots', + help_text='所属的扫描任务' + ) + + # 扫描结果数据 + url = models.CharField(max_length=2000, help_text='端点URL') + host = models.CharField( + max_length=253, + blank=True, + default='', + help_text='主机名(域名或IP地址)' + ) + title = models.CharField(max_length=1000, blank=True, default='', help_text='页面标题') + status_code = models.IntegerField(null=True, blank=True, help_text='HTTP状态码') + content_length = models.IntegerField(null=True, blank=True, help_text='内容长度') + location = models.CharField(max_length=1000, blank=True, default='', help_text='重定向位置') + webserver = models.CharField(max_length=200, blank=True, default='', help_text='Web服务器') + content_type = models.CharField(max_length=200, blank=True, default='', help_text='内容类型') + tech = ArrayField( + models.CharField(max_length=100), + blank=True, + default=list, + help_text='技术栈' + ) + body_preview = models.CharField(max_length=1000, blank=True, default='', help_text='响应体预览') + vhost = models.BooleanField(null=True, blank=True, help_text='虚拟主机标志') + matched_gf_patterns = ArrayField( + models.CharField(max_length=100), + blank=True, + default=list, + help_text='匹配的GF模式列表' + ) + discovered_at = models.DateTimeField(auto_now_add=True, help_text='发现时间') + + class Meta: + db_table = 'endpoint_snapshot' + verbose_name = '端点快照' + verbose_name_plural = '端点快照' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['scan']), + models.Index(fields=['url']), + models.Index(fields=['host']), # host索引,优化根据主机名查询 + models.Index(fields=['status_code']), # 状态码索引,优化筛选 + models.Index(fields=['-discovered_at']), + ] + constraints = [ + # 唯一约束:同一次扫描中,同一个URL只能记录一次 + models.UniqueConstraint( + fields=['scan', 'url'], + name='unique_endpoint_per_scan_snapshot' + ), + ] + + def __str__(self): + return f'{self.url} (Scan #{self.scan_id})' + + +class VulnerabilitySnapshot(models.Model): + """ + 漏洞快照 + + 记录:某次扫描中发现的漏洞 + """ + + # 延迟导入避免循环引用 + from apps.common.definitions import VulnSeverity + + id = models.AutoField(primary_key=True) + scan = models.ForeignKey( + 'scan.Scan', + on_delete=models.CASCADE, + related_name='vulnerability_snapshots', + help_text='所属的扫描任务' + ) + + # ==================== 核心字段 ==================== + url = models.TextField(help_text='漏洞所在的URL') + vuln_type = models.CharField(max_length=100, help_text='漏洞类型(如 xss, sqli)') + severity = models.CharField( + max_length=20, + choices=VulnSeverity.choices, + default=VulnSeverity.UNKNOWN, + help_text='严重性(未知/信息/低/中/高/危急)' + ) + source = models.CharField(max_length=50, blank=True, default='', help_text='来源工具(如 dalfox, nuclei, crlfuzz)') + cvss_score = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True, help_text='CVSS 评分(0.0-10.0)') + description = models.TextField(blank=True, default='', help_text='漏洞描述') + raw_output = models.JSONField(blank=True, default=dict, help_text='工具原始输出') + + # ==================== 时间字段 ==================== + discovered_at = models.DateTimeField(auto_now_add=True, help_text='发现时间') + + class Meta: + db_table = 'vulnerability_snapshot' + verbose_name = '漏洞快照' + verbose_name_plural = '漏洞快照' + ordering = ['-discovered_at'] + indexes = [ + models.Index(fields=['scan']), + models.Index(fields=['vuln_type']), + models.Index(fields=['severity']), + models.Index(fields=['source']), + models.Index(fields=['-discovered_at']), + ] + + def __str__(self): + return f'{self.vuln_type} - {self.url[:50]} (Scan #{self.scan_id})' \ No newline at end of file diff --git a/backend/apps/asset/models/statistics_models.py b/backend/apps/asset/models/statistics_models.py new file mode 100644 index 00000000..756da39b --- /dev/null +++ b/backend/apps/asset/models/statistics_models.py @@ -0,0 +1,82 @@ +from django.db import models + + +class AssetStatistics(models.Model): + """ + 资产统计表 + + 存储预聚合的全局统计数据,避免仪表盘实时 COUNT 大表。 + 由定时任务(Prefect Flow)定期刷新。 + """ + + id = models.AutoField(primary_key=True) + + # ==================== 当前统计字段 ==================== + total_targets = models.IntegerField(default=0, help_text='目标总数') + total_subdomains = models.IntegerField(default=0, help_text='子域名总数') + total_ips = models.IntegerField(default=0, help_text='IP地址总数') + total_endpoints = models.IntegerField(default=0, help_text='端点总数') + total_websites = models.IntegerField(default=0, help_text='网站总数') + total_vulns = models.IntegerField(default=0, help_text='漏洞总数') + total_assets = models.IntegerField(default=0, help_text='总资产数(子域名+IP+端点+网站)') + + # ==================== 上次统计字段(用于计算趋势)==================== + prev_targets = models.IntegerField(default=0, help_text='上次目标总数') + prev_subdomains = models.IntegerField(default=0, help_text='上次子域名总数') + prev_ips = models.IntegerField(default=0, help_text='上次IP地址总数') + prev_endpoints = models.IntegerField(default=0, help_text='上次端点总数') + prev_websites = models.IntegerField(default=0, help_text='上次网站总数') + prev_vulns = models.IntegerField(default=0, help_text='上次漏洞总数') + prev_assets = models.IntegerField(default=0, help_text='上次总资产数') + + # ==================== 时间字段 ==================== + updated_at = models.DateTimeField(auto_now=True, help_text='最后更新时间') + + class Meta: + db_table = 'asset_statistics' + verbose_name = '资产统计' + verbose_name_plural = '资产统计' + + def __str__(self): + return f'AssetStatistics (updated: {self.updated_at})' + + @classmethod + def get_or_create_singleton(cls) -> 'AssetStatistics': + """获取或创建单例统计记录""" + obj, _ = cls.objects.get_or_create(pk=1) + return obj + + +class StatisticsHistory(models.Model): + """ + 统计历史表(用于折线图) + + 每天记录一条快照,用于展示趋势。 + 由定时任务在刷新统计时自动写入。 + """ + + date = models.DateField(unique=True, help_text='统计日期') + + # 各类资产数量 + total_targets = models.IntegerField(default=0, help_text='目标总数') + total_subdomains = models.IntegerField(default=0, help_text='子域名总数') + total_ips = models.IntegerField(default=0, help_text='IP地址总数') + total_endpoints = models.IntegerField(default=0, help_text='端点总数') + total_websites = models.IntegerField(default=0, help_text='网站总数') + total_vulns = models.IntegerField(default=0, help_text='漏洞总数') + total_assets = models.IntegerField(default=0, help_text='总资产数') + + created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间') + updated_at = models.DateTimeField(auto_now=True, help_text='更新时间') + + class Meta: + db_table = 'statistics_history' + verbose_name = '统计历史' + verbose_name_plural = '统计历史' + ordering = ['-date'] + indexes = [ + models.Index(fields=['date']), + ] + + def __str__(self): + return f'StatisticsHistory ({self.date})' diff --git a/backend/apps/asset/repositories/__init__.py b/backend/apps/asset/repositories/__init__.py new file mode 100644 index 00000000..c317f62d --- /dev/null +++ b/backend/apps/asset/repositories/__init__.py @@ -0,0 +1,41 @@ +"""Asset Repositories - 数据访问层""" + +# 资产模块 Repositories +from .asset import ( + DjangoSubdomainRepository, + DjangoWebSiteRepository, + DjangoDirectoryRepository, + DjangoHostPortMappingRepository, + DjangoEndpointRepository, +) + +# 快照模块 Repositories +from .snapshot import ( + DjangoSubdomainSnapshotRepository, + DjangoHostPortMappingSnapshotRepository, + DjangoWebsiteSnapshotRepository, + DjangoDirectorySnapshotRepository, + DjangoEndpointSnapshotRepository, +) + +# 统计模块 Repository +from .statistics_repository import AssetStatisticsRepository + +__all__ = [ + # 资产模块 + 'DjangoSubdomainRepository', + 'DjangoWebSiteRepository', + 'DjangoDirectoryRepository', + 'DjangoHostPortMappingRepository', + 'DjangoEndpointRepository', + # 快照模块 + 'DjangoSubdomainSnapshotRepository', + 'DjangoHostPortMappingSnapshotRepository', + 'DjangoWebsiteSnapshotRepository', + 'DjangoDirectorySnapshotRepository', + 'DjangoEndpointSnapshotRepository', + # 统计模块 + 'AssetStatisticsRepository', +] + + diff --git a/backend/apps/asset/repositories/asset/__init__.py b/backend/apps/asset/repositories/asset/__init__.py new file mode 100644 index 00000000..ad7d633b --- /dev/null +++ b/backend/apps/asset/repositories/asset/__init__.py @@ -0,0 +1,15 @@ +"""Asset Repositories - 数据访问层""" + +from .subdomain_repository import DjangoSubdomainRepository +from .website_repository import DjangoWebSiteRepository +from .directory_repository import DjangoDirectoryRepository +from .host_port_mapping_repository import DjangoHostPortMappingRepository +from .endpoint_repository import DjangoEndpointRepository + +__all__ = [ + 'DjangoSubdomainRepository', + 'DjangoWebSiteRepository', + 'DjangoDirectoryRepository', + 'DjangoHostPortMappingRepository', + 'DjangoEndpointRepository', +] diff --git a/backend/apps/asset/repositories/asset/directory_repository.py b/backend/apps/asset/repositories/asset/directory_repository.py new file mode 100644 index 00000000..2c7781ab --- /dev/null +++ b/backend/apps/asset/repositories/asset/directory_repository.py @@ -0,0 +1,249 @@ +""" +Django ORM 实现的 Directory Repository +""" + +import logging +from typing import List, Tuple, Dict, Iterator +from django.db import transaction, IntegrityError, OperationalError, DatabaseError +from django.utils import timezone + +from apps.asset.models.asset_models import Directory +from apps.asset.dtos import DirectoryDTO +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + + +@auto_ensure_db_connection +class DjangoDirectoryRepository: + """Django ORM 实现的 Directory Repository""" + + def bulk_create_ignore_conflicts(self, items: List[DirectoryDTO]) -> int: + """ + 批量创建 Directory,忽略冲突 + + Args: + items: Directory DTO 列表 + + Returns: + int: 实际创建的记录数 + + Raises: + IntegrityError: 数据完整性错误 + OperationalError: 数据库操作错误 + DatabaseError: 数据库错误 + """ + if not items: + return 0 + + try: + # 转换为 Django 模型对象 + directory_objects = [ + Directory( + website_id=item.website_id, + target_id=item.target_id, + url=item.url, + status=item.status, + content_length=item.content_length, + words=item.words, + lines=item.lines, + content_type=item.content_type, + duration=item.duration + ) + for item in items + ] + + with transaction.atomic(): + # 批量插入或忽略冲突 + # 如果 website + url 已存在,忽略冲突 + Directory.objects.bulk_create( + directory_objects, + ignore_conflicts=True + ) + + logger.debug(f"成功处理 {len(items)} 条 Directory 记录") + return len(items) + + except IntegrityError as e: + logger.error( + f"批量插入 Directory 失败 - 数据完整性错误: {e}, " + f"记录数: {len(items)}" + ) + raise + + except OperationalError as e: + logger.error( + f"批量插入 Directory 失败 - 数据库操作错误: {e}, " + f"记录数: {len(items)}" + ) + raise + + except DatabaseError as e: + logger.error( + f"批量插入 Directory 失败 - 数据库错误: {e}, " + f"记录数: {len(items)}" + ) + raise + + except Exception as e: + logger.error( + f"批量插入 Directory 失败 - 未知错误: {e}, " + f"记录数: {len(items)}, " + f"错误类型: {type(e).__name__}", + exc_info=True + ) + raise + + def get_by_website(self, website_id: int) -> List[DirectoryDTO]: + """ + 获取指定站点的所有目录 + + Args: + website_id: 站点 ID + + Returns: + List[DirectoryDTO]: 目录列表 + """ + try: + directories = Directory.objects.filter(website_id=website_id) + return [ + DirectoryDTO( + website_id=d.website_id, + target_id=d.target_id, + url=d.url, + status=d.status, + content_length=d.content_length, + words=d.words, + lines=d.lines, + content_type=d.content_type, + duration=d.duration + ) + for d in directories + ] + + except Exception as e: + logger.error(f"获取目录列表失败 - Website ID: {website_id}, 错误: {e}") + raise + + def count_by_website(self, website_id: int) -> int: + """ + 统计指定站点的目录总数 + + Args: + website_id: 站点 ID + + Returns: + int: 目录总数 + """ + try: + count = Directory.objects.filter(website_id=website_id).count() + logger.debug(f"Website {website_id} 的目录总数: {count}") + return count + + except Exception as e: + logger.error(f"统计目录数量失败 - Website ID: {website_id}, 错误: {e}") + raise + + def get_all(self): + """ + 获取所有目录 + + Returns: + QuerySet: 目录查询集 + """ + return Directory.objects.all() + + def get_by_target(self, target_id: int): + return Directory.objects.filter(target_id=target_id).select_related('website').order_by('-discovered_at') + + def get_urls_for_export(self, target_id: int, batch_size: int = 1000) -> Iterator[str]: + """流式导出目标下的所有目录 URL(只查 url 字段,避免加载多余数据)。""" + try: + queryset = ( + Directory.objects + .filter(target_id=target_id) + .values_list('url', flat=True) + .order_by('url') + .iterator(chunk_size=batch_size) + ) + for url in queryset: + yield url + except Exception as e: + logger.error("流式导出目录 URL 失败 - Target ID: %s, 错误: %s", target_id, e) + raise + + def soft_delete_by_ids(self, directory_ids: List[int]) -> int: + """ + 根据 ID 列表批量软删除Directory + + Args: + directory_ids: Directory ID 列表 + + Returns: + 软删除的记录数 + """ + try: + updated_count = ( + Directory.objects + .filter(id__in=directory_ids) + .update(deleted_at=timezone.now()) + ) + logger.debug( + "批量软删除Directory成功 - Count: %s, 更新记录: %s", + len(directory_ids), + updated_count + ) + return updated_count + except Exception as e: + logger.error( + "批量软删除Directory失败 - IDs: %s, 错误: %s", + directory_ids, + e + ) + raise + + def hard_delete_by_ids(self, directory_ids: List[int]) -> Tuple[int, Dict[str, int]]: + """ + 根据 ID 列表硬删除Directory(使用数据库级 CASCADE) + + Args: + directory_ids: Directory ID 列表 + + Returns: + (删除的记录数, 删除详情字典) + """ + try: + batch_size = 1000 + total_deleted = 0 + + logger.debug(f"开始批量删除 {len(directory_ids)} 个Directory(数据库 CASCADE)...") + + for i in range(0, len(directory_ids), batch_size): + batch_ids = directory_ids[i:i + batch_size] + count, _ = Directory.all_objects.filter(id__in=batch_ids).delete() + total_deleted += count + logger.debug(f"批次删除完成: {len(batch_ids)} 个Directory,删除 {count} 条记录") + + deleted_details = { + 'directories': len(directory_ids), + 'total': total_deleted, + 'note': 'Database CASCADE - detailed stats unavailable' + } + + logger.debug( + "批量硬删除成功(CASCADE)- Directory数: %s, 总删除记录: %s", + len(directory_ids), + total_deleted + ) + + return total_deleted, deleted_details + + except Exception as e: + logger.error( + "批量硬删除失败(CASCADE)- Directory数: %s, 错误: %s", + len(directory_ids), + str(e), + exc_info=True + ) + raise diff --git a/backend/apps/asset/repositories/asset/endpoint_repository.py b/backend/apps/asset/repositories/asset/endpoint_repository.py new file mode 100644 index 00000000..10bfe23d --- /dev/null +++ b/backend/apps/asset/repositories/asset/endpoint_repository.py @@ -0,0 +1,192 @@ +"""Endpoint Repository - Django ORM 实现""" + +import logging +from typing import List, Optional, Tuple, Dict, Any + +from apps.asset.models import Endpoint +from apps.asset.dtos.asset import EndpointDTO +from apps.common.decorators import auto_ensure_db_connection +from django.db import transaction + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoEndpointRepository: + """端点 Repository - 负责端点表的数据访问""" + + def bulk_create_ignore_conflicts(self, items: List[EndpointDTO]) -> int: + """ + 批量创建端点(忽略冲突) + + Args: + items: 端点 DTO 列表 + + Returns: + int: 创建的记录数 + """ + if not items: + return 0 + + try: + endpoints = [] + for item in items: + # Endpoint 模型当前只关联 target,不再依赖 website 外键 + # 这里按照 EndpointDTO 的字段映射构造 Endpoint 实例 + endpoints.append(Endpoint( + target_id=item.target_id, + url=item.url, + host=item.host or '', + title=item.title or '', + status_code=item.status_code, + content_length=item.content_length, + webserver=item.webserver or '', + body_preview=item.body_preview or '', + content_type=item.content_type or '', + tech=item.tech if item.tech else [], + vhost=item.vhost, + location=item.location or '', + matched_gf_patterns=item.matched_gf_patterns if item.matched_gf_patterns else [] + )) + + with transaction.atomic(): + created = Endpoint.objects.bulk_create( + endpoints, + ignore_conflicts=True, + batch_size=1000 + ) + return len(created) + + except Exception as e: + logger.error(f"批量创建端点失败: {e}") + raise + + def get_by_website(self, website_id: int) -> List[EndpointDTO]: + """ + 获取网站下的所有端点 + + Args: + website_id: 网站 ID + + Returns: + List[EndpointDTO]: 端点列表 + """ + endpoints = Endpoint.objects.filter( + website_id=website_id + ).order_by('-discovered_at') + + result = [] + for endpoint in endpoints: + result.append(EndpointDTO( + website_id=endpoint.website_id, + target_id=endpoint.target_id, + url=endpoint.url, + title=endpoint.title, + status_code=endpoint.status_code, + content_length=endpoint.content_length, + webserver=endpoint.webserver, + body_preview=endpoint.body_preview, + content_type=endpoint.content_type, + tech=endpoint.tech, + vhost=endpoint.vhost, + location=endpoint.location, + matched_gf_patterns=endpoint.matched_gf_patterns + )) + + return result + + def get_queryset_by_target(self, target_id: int): + return Endpoint.objects.filter(target_id=target_id).order_by('-discovered_at') + + def get_all(self): + """获取所有端点(全局查询)""" + return Endpoint.objects.all().order_by('-discovered_at') + + def get_by_target(self, target_id: int) -> List[EndpointDTO]: + """ + 获取目标下的所有端点 + + Args: + target_id: 目标 ID + + Returns: + List[EndpointDTO]: 端点列表 + """ + endpoints = Endpoint.objects.filter( + target_id=target_id + ).order_by('-discovered_at') + + result = [] + for endpoint in endpoints: + result.append(EndpointDTO( + website_id=endpoint.website_id, + target_id=endpoint.target_id, + url=endpoint.url, + title=endpoint.title, + status_code=endpoint.status_code, + content_length=endpoint.content_length, + webserver=endpoint.webserver, + body_preview=endpoint.body_preview, + content_type=endpoint.content_type, + tech=endpoint.tech, + vhost=endpoint.vhost, + location=endpoint.location, + matched_gf_patterns=endpoint.matched_gf_patterns + )) + + return result + + def count_by_website(self, website_id: int) -> int: + """ + 统计网站下的端点数量 + + Args: + website_id: 网站 ID + + Returns: + int: 端点数量 + """ + return Endpoint.objects.filter(website_id=website_id).count() + + def count_by_target(self, target_id: int) -> int: + """ + 统计目标下的端点数量 + + Args: + target_id: 目标 ID + + Returns: + int: 端点数量 + """ + return Endpoint.objects.filter(target_id=target_id).count() + + def soft_delete_by_ids(self, ids: List[int]) -> int: + """ + 软删除端点(批量) + + Args: + ids: 端点 ID 列表 + + Returns: + int: 更新的记录数 + """ + from django.utils import timezone + return Endpoint.objects.filter( + id__in=ids + ).update(deleted_at=timezone.now()) + + def hard_delete_by_ids(self, ids: List[int]) -> Tuple[int, Dict[str, int]]: + """ + 硬删除端点(批量) + + Args: + ids: 端点 ID 列表 + + Returns: + Tuple[int, Dict[str, int]]: (删除总数, 详细信息) + """ + deleted_count, details = Endpoint.all_objects.filter( + id__in=ids + ).delete() + + return deleted_count, details diff --git a/backend/apps/asset/repositories/asset/host_port_mapping_repository.py b/backend/apps/asset/repositories/asset/host_port_mapping_repository.py new file mode 100644 index 00000000..7f3b17dd --- /dev/null +++ b/backend/apps/asset/repositories/asset/host_port_mapping_repository.py @@ -0,0 +1,167 @@ +"""HostPortMapping Repository - Django ORM 实现""" + +import logging +from typing import List, Iterator + +from apps.asset.models.asset_models import HostPortMapping +from apps.asset.dtos.asset import HostPortMappingDTO +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoHostPortMappingRepository: + """HostPortMapping Repository - Django ORM 实现""" + + def bulk_create_ignore_conflicts(self, items: List[HostPortMappingDTO]) -> int: + """ + 批量创建主机端口关联(忽略冲突) + + Args: + items: 主机端口关联 DTO 列表 + + Returns: + int: 实际创建的记录数(注意:ignore_conflicts 时可能为 0) + + Note: + - 基于唯一约束 (target + host + ip + port) 自动去重 + - 忽略已存在的记录,不更新 + """ + try: + logger.debug("准备批量创建主机端口关联 - 数量: %d", len(items)) + + if not items: + logger.debug("主机端口关联为空,跳过创建") + return 0 + + # 构建记录对象 + records = [] + for item in items: + records.append(HostPortMapping( + target_id=item.target_id, + host=item.host, + ip=item.ip, + port=item.port + )) + + # 批量创建(忽略冲突,基于唯一约束去重) + created = HostPortMapping.objects.bulk_create( + records, + ignore_conflicts=True + ) + + created_count = len(created) if created else 0 + logger.debug("主机端口关联创建完成 - 数量: %d", created_count) + + return created_count + + except Exception as e: + logger.error( + "批量创建主机端口关联失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def get_for_export(self, target_id: int, batch_size: int = 1000): + queryset = ( + HostPortMapping.objects + .filter(target_id=target_id) + .order_by("host", "port") + .values("host", "port") + .iterator(chunk_size=batch_size) + ) + for item in queryset: + yield item + + def get_ips_for_export(self, target_id: int, batch_size: int = 1000) -> Iterator[str]: + """流式导出目标下的所有唯一 IP 地址。""" + queryset = ( + HostPortMapping.objects + .filter(target_id=target_id) + .values_list("ip", flat=True) + .distinct() + .order_by("ip") + .iterator(chunk_size=batch_size) + ) + for ip in queryset: + yield ip + + def get_ip_aggregation_by_target(self, target_id: int, search: str = None): + from django.db.models import Min + + qs = HostPortMapping.objects.filter(target_id=target_id) + if search: + qs = qs.filter(ip__icontains=search) + + ip_aggregated = ( + qs + .values('ip') + .annotate( + discovered_at=Min('discovered_at') + ) + .order_by('-discovered_at') + ) + + results = [] + for item in ip_aggregated: + ip = item['ip'] + mappings = ( + HostPortMapping.objects + .filter(target_id=target_id, ip=ip) + .values('host', 'port') + .distinct() + ) + + hosts = sorted({m['host'] for m in mappings}) + ports = sorted({m['port'] for m in mappings}) + + results.append({ + 'ip': ip, + 'hosts': hosts, + 'ports': ports, + 'discovered_at': item['discovered_at'], + }) + + return results + + def get_all_ip_aggregation(self, search: str = None): + """获取所有 IP 聚合数据(全局查询)""" + from django.db.models import Min + + qs = HostPortMapping.objects.all() + if search: + qs = qs.filter(ip__icontains=search) + + ip_aggregated = ( + qs + .values('ip') + .annotate( + discovered_at=Min('discovered_at') + ) + .order_by('-discovered_at') + ) + + results = [] + for item in ip_aggregated: + ip = item['ip'] + mappings = ( + HostPortMapping.objects + .filter(ip=ip) + .values('host', 'port') + .distinct() + ) + + hosts = sorted({m['host'] for m in mappings}) + ports = sorted({m['port'] for m in mappings}) + + results.append({ + 'ip': ip, + 'hosts': hosts, + 'ports': ports, + 'discovered_at': item['discovered_at'], + }) + + return results diff --git a/backend/apps/asset/repositories/asset/subdomain_repository.py b/backend/apps/asset/repositories/asset/subdomain_repository.py new file mode 100644 index 00000000..f7e37c3d --- /dev/null +++ b/backend/apps/asset/repositories/asset/subdomain_repository.py @@ -0,0 +1,256 @@ +import logging +from typing import List, Iterator + +from django.db import transaction, IntegrityError, OperationalError, DatabaseError +from django.utils import timezone +from typing import Tuple, Dict + +from apps.asset.models.asset_models import Subdomain +from apps.asset.dtos import SubdomainDTO +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoSubdomainRepository: + """基于 Django ORM 的子域名仓储实现。""" + + def bulk_create_ignore_conflicts(self, items: List[SubdomainDTO]) -> None: + """ + 批量创建子域名,忽略冲突 + + Args: + items: 子域名 DTO 列表 + + Raises: + IntegrityError: 数据完整性错误(如唯一约束冲突) + OperationalError: 数据库操作错误(如连接失败) + DatabaseError: 其他数据库错误 + """ + if not items: + return + + try: + subdomain_objects = [ + Subdomain( + name=item.name, + target_id=item.target_id, + ) + for item in items + ] + + with transaction.atomic(): + # 使用 ignore_conflicts 策略: + # - 新子域名:INSERT 完整记录 + # - 已存在子域名:忽略(不更新,因为没有探测字段数据) + # 注意:ignore_conflicts 无法返回实际创建的数量 + Subdomain.objects.bulk_create( # type: ignore[attr-defined] + subdomain_objects, + ignore_conflicts=True, # 忽略重复记录 + ) + + logger.debug(f"成功处理 {len(items)} 条子域名记录") + + except IntegrityError as e: + logger.error( + f"批量插入子域名失败 - 数据完整性错误: {e}, " + f"记录数: {len(items)}, " + f"示例域名: {items[0].name if items else 'N/A'}" + ) + raise + + except OperationalError as e: + logger.error( + f"批量插入子域名失败 - 数据库操作错误: {e}, " + f"记录数: {len(items)}" + ) + raise + + except DatabaseError as e: + logger.error( + f"批量插入子域名失败 - 数据库错误: {e}, " + f"记录数: {len(items)}" + ) + raise + + except Exception as e: + logger.error( + f"批量插入子域名失败 - 未知错误: {e}, " + f"记录数: {len(items)}, " + f"错误类型: {type(e).__name__}", + exc_info=True + ) + raise + + def get_or_create(self, name: str, target_id: int) -> Tuple[Subdomain, bool]: + """ + 获取或创建子域名 + + Args: + name: 子域名名称 + target_id: 目标 ID + + Returns: + (Subdomain对象, 是否新创建) + """ + return Subdomain.objects.get_or_create( + name=name, + target_id=target_id, + ) + + def get_domains_for_export(self, target_id: int, batch_size: int = 1000) -> Iterator[str]: + """ + 流式导出域名(用于生成扫描工具输入文件) + + 使用 iterator() 进行流式查询,避免一次性加载所有数据到内存 + + Args: + target_id: 目标 ID + batch_size: 每次从数据库读取的行数 + + Yields: + str: 域名 + """ + queryset = Subdomain.objects.filter( + target_id=target_id + ).only('name').iterator(chunk_size=batch_size) + + for subdomain in queryset: + yield subdomain.name + + def get_by_target(self, target_id: int): + return Subdomain.objects.filter(target_id=target_id).order_by('-discovered_at') + + def count_by_target(self, target_id: int) -> int: + """ + 统计目标下的域名数量 + + Args: + target_id: 目标 ID + + Returns: + int: 域名数量 + """ + return Subdomain.objects.filter(target_id=target_id).count() + + def get_by_names_and_target_id(self, names: set, target_id: int) -> dict: + """ + 根据域名列表和目标ID批量查询 Subdomain + + Args: + names: 域名集合 + target_id: 目标 ID + + Returns: + dict: {domain_name: Subdomain对象} + """ + subdomains = Subdomain.objects.filter( + name__in=names, + target_id=target_id + ).only('id', 'name') + + return {sd.name: sd for sd in subdomains} + + def get_all(self): + """ + 获取所有子域名 + + Returns: + QuerySet: 子域名查询集 + """ + return Subdomain.objects.all() + + def soft_delete_by_ids(self, subdomain_ids: List[int]) -> int: + """ + 根据 ID 列表批量软删除子域名 + + Args: + subdomain_ids: 子域名 ID 列表 + + Returns: + 软删除的记录数 + + Note: + - 使用软删除:只标记为已删除,不真正删除数据库记录 + - 保留所有关联数据,可恢复 + """ + try: + updated_count = ( + Subdomain.objects + .filter(id__in=subdomain_ids) + .update(deleted_at=timezone.now()) + ) + logger.debug( + "批量软删除子域名成功 - Count: %s, 更新记录: %s", + len(subdomain_ids), + updated_count + ) + return updated_count + except Exception as e: + logger.error( + "批量软删除子域名失败 - IDs: %s, 错误: %s", + subdomain_ids, + e + ) + raise + + def hard_delete_by_ids(self, subdomain_ids: List[int]) -> Tuple[int, Dict[str, int]]: + """ + 根据 ID 列表硬删除子域名(使用数据库级 CASCADE) + + Args: + subdomain_ids: 子域名 ID 列表 + + Returns: + (删除的记录数, 删除详情字典) + + Strategy: + 使用数据库级 CASCADE 删除,性能最优 + + Note: + - 硬删除:从数据库中永久删除 + - 数据库自动处理所有外键级联删除 + - 不触发 Django 信号(pre_delete/post_delete) + """ + try: + batch_size = 1000 # 每批处理1000个子域名 + total_deleted = 0 + + logger.debug(f"开始批量删除 {len(subdomain_ids)} 个子域名(数据库 CASCADE)...") + + # 分批处理子域名ID,避免单次删除过多 + for i in range(0, len(subdomain_ids), batch_size): + batch_ids = subdomain_ids[i:i + batch_size] + + # 直接删除子域名,数据库自动级联删除所有关联数据 + count, _ = Subdomain.all_objects.filter(id__in=batch_ids).delete() + total_deleted += count + + logger.debug(f"批次删除完成: {len(batch_ids)} 个子域名,删除 {count} 条记录") + + # 由于使用数据库 CASCADE,无法获取详细统计 + deleted_details = { + 'subdomains': len(subdomain_ids), + 'total': total_deleted, + 'note': 'Database CASCADE - detailed stats unavailable' + } + + logger.debug( + "批量硬删除成功(CASCADE)- 子域名数: %s, 总删除记录: %s", + len(subdomain_ids), + total_deleted + ) + + return total_deleted, deleted_details + + except Exception as e: + logger.error( + "批量硬删除失败(CASCADE)- 子域名数: %s, 错误: %s", + len(subdomain_ids), + str(e), + exc_info=True + ) + raise + + diff --git a/backend/apps/asset/repositories/asset/website_repository.py b/backend/apps/asset/repositories/asset/website_repository.py new file mode 100644 index 00000000..845cb87b --- /dev/null +++ b/backend/apps/asset/repositories/asset/website_repository.py @@ -0,0 +1,260 @@ +""" +Django ORM 实现的 WebSite Repository +""" + +import logging +from typing import List, Generator, Tuple, Dict, Optional +from django.db import transaction, IntegrityError, OperationalError, DatabaseError +from django.utils import timezone + +from apps.asset.models.asset_models import WebSite +from apps.asset.dtos import WebSiteDTO +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + + +@auto_ensure_db_connection +class DjangoWebSiteRepository: + """Django ORM 实现的 WebSite Repository""" + + def bulk_create_ignore_conflicts(self, items: List[WebSiteDTO]) -> None: + """ + 批量创建 WebSite,忽略冲突 + + Args: + items: WebSite DTO 列表 + + Raises: + IntegrityError: 数据完整性错误 + OperationalError: 数据库操作错误 + DatabaseError: 数据库错误 + """ + if not items: + return + + try: + # 转换为 Django 模型对象 + website_objects = [ + WebSite( + target_id=item.target_id, + url=item.url, + host=item.host, + location=item.location, + title=item.title, + webserver=item.webserver, + body_preview=item.body_preview, + content_type=item.content_type, + tech=item.tech, + status_code=item.status_code, + content_length=item.content_length, + vhost=item.vhost + ) + for item in items + ] + + with transaction.atomic(): + # 批量插入或更新 + # 如果URL和目标已存在,忽略冲突 + WebSite.objects.bulk_create( + website_objects, + ignore_conflicts=True + ) + + logger.debug(f"成功处理 {len(items)} 条 WebSite 记录") + + except IntegrityError as e: + logger.error( + f"批量插入 WebSite 失败 - 数据完整性错误: {e}, " + f"记录数: {len(items)}" + ) + raise + + except OperationalError as e: + logger.error( + f"批量插入 WebSite 失败 - 数据库操作错误: {e}, " + f"记录数: {len(items)}" + ) + raise + + except DatabaseError as e: + logger.error( + f"批量插入 WebSite 失败 - 数据库错误: {e}, " + f"记录数: {len(items)}" + ) + raise + + except Exception as e: + logger.error( + f"批量插入 WebSite 失败 - 未知错误: {e}, " + f"记录数: {len(items)}, " + f"错误类型: {type(e).__name__}", + exc_info=True + ) + raise + + def get_urls_for_export(self, target_id: int, batch_size: int = 1000) -> Generator[str, None, None]: + """ + 流式导出目标下的所有站点 URL + + Args: + target_id: 目标 ID + batch_size: 批次大小 + + Yields: + str: 站点 URL + """ + try: + # 查询目标下的站点,只选择 URL 字段,避免不必要的数据传输 + queryset = WebSite.objects.filter( + target_id=target_id + ).values_list('url', flat=True).iterator(chunk_size=batch_size) + + for url in queryset: + yield url + except Exception as e: + logger.error(f"流式导出站点 URL 失败 - Target ID: {target_id}, 错误: {e}") + raise + + def get_by_target(self, target_id: int): + return WebSite.objects.filter(target_id=target_id).order_by('-discovered_at') + + def count_by_target(self, target_id: int) -> int: + """ + 统计目标下的站点总数 + + Args: + target_id: 目标 ID + + Returns: + int: 站点总数 + """ + try: + count = WebSite.objects.filter(target_id=target_id).count() + logger.debug(f"Target {target_id} 的站点总数: {count}") + return count + + except Exception as e: + logger.error(f"统计站点数量失败 - Target ID: {target_id}, 错误: {e}") + raise + + def count_by_scan(self, scan_id: int) -> int: + """ + 统计扫描下的站点总数 + """ + try: + count = WebSite.objects.filter(scan_id=scan_id).count() + logger.debug(f"Scan {scan_id} 的站点总数: {count}") + return count + except Exception as e: + logger.error(f"统计站点数量失败 - Scan ID: {scan_id}, 错误: {e}") + raise + + def get_by_url(self, url: str, target_id: int) -> Optional[int]: + """ + 根据 URL 和 target_id 查找站点 ID + + Args: + url: 站点 URL + target_id: 目标 ID + + Returns: + Optional[int]: 站点 ID,如果不存在返回 None + + Raises: + ValueError: 发现多个站点时 + """ + try: + website = WebSite.objects.filter(url=url, target_id=target_id).first() + if website: + return website.id + return None + + except Exception as e: + logger.error(f"查询站点失败 - URL: {url}, Target ID: {target_id}, 错误: {e}") + raise + + def get_all(self): + """ + 获取所有网站 + + Returns: + QuerySet: 网站查询集 + """ + return WebSite.objects.all() + + def soft_delete_by_ids(self, website_ids: List[int]) -> int: + """ + 根据 ID 列表批量软删除WebSite + + Args: + website_ids: WebSite ID 列表 + + Returns: + 软删除的记录数 + """ + try: + updated_count = ( + WebSite.objects + .filter(id__in=website_ids) + .update(deleted_at=timezone.now()) + ) + logger.debug( + "批量软删除WebSite成功 - Count: %s, 更新记录: %s", + len(website_ids), + updated_count + ) + return updated_count + except Exception as e: + logger.error( + "批量软删除WebSite失败 - IDs: %s, 错误: %s", + website_ids, + e + ) + raise + + def hard_delete_by_ids(self, website_ids: List[int]) -> Tuple[int, Dict[str, int]]: + """ + 根据 ID 列表硬删除WebSite(使用数据库级 CASCADE) + + Args: + website_ids: WebSite ID 列表 + + Returns: + (删除的记录数, 删除详情字典) + """ + try: + batch_size = 1000 + total_deleted = 0 + + logger.debug(f"开始批量删除 {len(website_ids)} 个WebSite(数据库 CASCADE)...") + + for i in range(0, len(website_ids), batch_size): + batch_ids = website_ids[i:i + batch_size] + count, _ = WebSite.all_objects.filter(id__in=batch_ids).delete() + total_deleted += count + logger.debug(f"批次删除完成: {len(batch_ids)} 个WebSite,删除 {count} 条记录") + + deleted_details = { + 'websites': len(website_ids), + 'total': total_deleted, + 'note': 'Database CASCADE - detailed stats unavailable' + } + + logger.debug( + "批量硬删除成功(CASCADE)- WebSite数: %s, 总删除记录: %s", + len(website_ids), + total_deleted + ) + + return total_deleted, deleted_details + + except Exception as e: + logger.error( + "批量硬删除失败(CASCADE)- WebSite数: %s, 错误: %s", + len(website_ids), + str(e), + exc_info=True + ) + raise diff --git a/backend/apps/asset/repositories/snapshot/__init__.py b/backend/apps/asset/repositories/snapshot/__init__.py new file mode 100644 index 00000000..38590bf6 --- /dev/null +++ b/backend/apps/asset/repositories/snapshot/__init__.py @@ -0,0 +1,17 @@ +"""Snapshot Repositories - 数据访问层""" + +from .subdomain_snapshot_repository import DjangoSubdomainSnapshotRepository +from .host_port_mapping_snapshot_repository import DjangoHostPortMappingSnapshotRepository +from .website_snapshot_repository import DjangoWebsiteSnapshotRepository +from .directory_snapshot_repository import DjangoDirectorySnapshotRepository +from .endpoint_snapshot_repository import DjangoEndpointSnapshotRepository +from .vulnerability_snapshot_repository import DjangoVulnerabilitySnapshotRepository + +__all__ = [ + 'DjangoSubdomainSnapshotRepository', + 'DjangoHostPortMappingSnapshotRepository', + 'DjangoWebsiteSnapshotRepository', + 'DjangoDirectorySnapshotRepository', + 'DjangoEndpointSnapshotRepository', + 'DjangoVulnerabilitySnapshotRepository', +] diff --git a/backend/apps/asset/repositories/snapshot/directory_snapshot_repository.py b/backend/apps/asset/repositories/snapshot/directory_snapshot_repository.py new file mode 100644 index 00000000..13d2dea4 --- /dev/null +++ b/backend/apps/asset/repositories/snapshot/directory_snapshot_repository.py @@ -0,0 +1,78 @@ +"""Directory Snapshot Repository - 目录快照数据访问层""" + +import logging +from typing import List +from django.db import transaction + +from apps.asset.models import DirectorySnapshot +from apps.asset.dtos.snapshot import DirectorySnapshotDTO +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoDirectorySnapshotRepository: + """ + 目录快照仓储(Django ORM 实现) + + 负责目录快照表的数据访问操作 + """ + + def save_snapshots(self, items: List[DirectorySnapshotDTO]) -> None: + """ + 批量保存目录快照记录 + + 使用 ignore_conflicts 策略,如果快照已存在(相同 scan + url)则跳过 + + Args: + items: 目录快照 DTO 列表 + + Raises: + ValueError: items 为空 + Exception: 数据库操作失败 + """ + if not items: + logger.warning("目录快照列表为空,跳过保存") + return + + try: + # 转换为 Django 模型对象 + snapshot_objects = [ + DirectorySnapshot( + scan_id=item.scan_id, + url=item.url, + status=item.status, + content_length=item.content_length, + words=item.words, + lines=item.lines, + content_type=item.content_type, + duration=item.duration + ) + for item in items + ] + + with transaction.atomic(): + # 批量插入,忽略冲突 + # 如果 scan + url 已存在,跳过 + DirectorySnapshot.objects.bulk_create( + snapshot_objects, + ignore_conflicts=True + ) + + logger.debug("成功保存 %d 条目录快照记录", len(items)) + + except Exception as e: + logger.error( + "批量保存目录快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def get_by_scan(self, scan_id: int): + return DirectorySnapshot.objects.filter(scan_id=scan_id).order_by('-discovered_at') + + def get_all(self): + return DirectorySnapshot.objects.all().order_by('-discovered_at') diff --git a/backend/apps/asset/repositories/snapshot/endpoint_snapshot_repository.py b/backend/apps/asset/repositories/snapshot/endpoint_snapshot_repository.py new file mode 100644 index 00000000..a2adb6de --- /dev/null +++ b/backend/apps/asset/repositories/snapshot/endpoint_snapshot_repository.py @@ -0,0 +1,74 @@ +"""EndpointSnapshot Repository - Django ORM 实现""" + +import logging +from typing import List + +from apps.asset.models.snapshot_models import EndpointSnapshot +from apps.asset.dtos.snapshot import EndpointSnapshotDTO +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoEndpointSnapshotRepository: + """端点快照 Repository - 负责端点快照表的数据访问""" + + def save_snapshots(self, items: List[EndpointSnapshotDTO]) -> None: + """ + 保存端点快照 + + Args: + items: 端点快照 DTO 列表 + + Note: + - 保存完整的快照数据 + - 基于唯一约束 (scan + url) 自动去重 + """ + try: + logger.debug("准备保存端点快照 - 数量: %d", len(items)) + + if not items: + logger.debug("端点快照为空,跳过保存") + return + + # 构建快照对象 + snapshots = [] + for item in items: + snapshots.append(EndpointSnapshot( + scan_id=item.scan_id, + url=item.url, + title=item.title, + status_code=item.status_code, + content_length=item.content_length, + location=item.location, + webserver=item.webserver, + content_type=item.content_type, + tech=item.tech if item.tech else [], + body_preview=item.body_preview, + vhost=item.vhost, + matched_gf_patterns=item.matched_gf_patterns if item.matched_gf_patterns else [] + )) + + # 批量创建(忽略冲突,基于唯一约束去重) + EndpointSnapshot.objects.bulk_create( + snapshots, + ignore_conflicts=True + ) + + logger.debug("端点快照保存成功 - 数量: %d", len(snapshots)) + + except Exception as e: + logger.error( + "保存端点快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def get_by_scan(self, scan_id: int): + return EndpointSnapshot.objects.filter(scan_id=scan_id).order_by('-discovered_at') + + def get_all(self): + return EndpointSnapshot.objects.all().order_by('-discovered_at') diff --git a/backend/apps/asset/repositories/snapshot/host_port_mapping_snapshot_repository.py b/backend/apps/asset/repositories/snapshot/host_port_mapping_snapshot_repository.py new file mode 100644 index 00000000..81fbaec1 --- /dev/null +++ b/backend/apps/asset/repositories/snapshot/host_port_mapping_snapshot_repository.py @@ -0,0 +1,145 @@ +"""HostPortMappingSnapshot Repository - Django ORM 实现""" + +import logging +from typing import List, Iterator + +from apps.asset.models.snapshot_models import HostPortMappingSnapshot +from apps.asset.dtos.snapshot import HostPortMappingSnapshotDTO +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoHostPortMappingSnapshotRepository: + """HostPortMappingSnapshot Repository - Django ORM 实现,负责主机端口映射快照表的数据访问""" + + def save_snapshots(self, items: List[HostPortMappingSnapshotDTO]) -> None: + """ + 保存主机端口关联快照 + + Args: + items: 主机端口关联快照 DTO 列表 + + Note: + - 保存完整的快照数据 + - 基于唯一约束 (scan + host + ip + port) 自动去重 + """ + try: + logger.debug("准备保存主机端口关联快照 - 数量: %d", len(items)) + + if not items: + logger.debug("主机端口关联快照为空,跳过保存") + return + + # 构建快照对象 + snapshots = [] + for item in items: + snapshots.append(HostPortMappingSnapshot( + scan_id=item.scan_id, + host=item.host, + ip=item.ip, + port=item.port + )) + + # 批量创建(忽略冲突,基于唯一约束去重) + HostPortMappingSnapshot.objects.bulk_create( + snapshots, + ignore_conflicts=True + ) + + logger.debug("主机端口关联快照保存成功 - 数量: %d", len(snapshots)) + + except Exception as e: + logger.error( + "保存主机端口关联快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def get_ip_aggregation_by_scan(self, scan_id: int, search: str = None): + from django.db.models import Min + + qs = HostPortMappingSnapshot.objects.filter(scan_id=scan_id) + if search: + qs = qs.filter(ip__icontains=search) + + ip_aggregated = ( + qs + .values('ip') + .annotate( + discovered_at=Min('discovered_at') + ) + .order_by('-discovered_at') + ) + + results = [] + for item in ip_aggregated: + ip = item['ip'] + mappings = ( + HostPortMappingSnapshot.objects + .filter(scan_id=scan_id, ip=ip) + .values('host', 'port') + .distinct() + ) + + hosts = sorted({m['host'] for m in mappings}) + ports = sorted({m['port'] for m in mappings}) + + results.append({ + 'ip': ip, + 'hosts': hosts, + 'ports': ports, + 'discovered_at': item['discovered_at'], + }) + + return results + + def get_all_ip_aggregation(self, search: str = None): + """获取所有 IP 聚合数据""" + from django.db.models import Min + + qs = HostPortMappingSnapshot.objects.all() + if search: + qs = qs.filter(ip__icontains=search) + + ip_aggregated = ( + qs + .values('ip') + .annotate(discovered_at=Min('discovered_at')) + .order_by('-discovered_at') + ) + + results = [] + for item in ip_aggregated: + ip = item['ip'] + mappings = ( + HostPortMappingSnapshot.objects + .filter(ip=ip) + .values('host', 'port') + .distinct() + ) + hosts = sorted({m['host'] for m in mappings}) + ports = sorted({m['port'] for m in mappings}) + results.append({ + 'ip': ip, + 'hosts': hosts, + 'ports': ports, + 'discovered_at': item['discovered_at'], + }) + return results + + def get_ips_for_export(self, scan_id: int, batch_size: int = 1000) -> Iterator[str]: + """流式导出扫描下的所有唯一 IP 地址。""" + queryset = ( + HostPortMappingSnapshot.objects + .filter(scan_id=scan_id) + .values_list("ip", flat=True) + .distinct() + .order_by("ip") + .iterator(chunk_size=batch_size) + ) + for ip in queryset: + yield ip diff --git a/backend/apps/asset/repositories/snapshot/subdomain_snapshot_repository.py b/backend/apps/asset/repositories/snapshot/subdomain_snapshot_repository.py new file mode 100644 index 00000000..77889cf7 --- /dev/null +++ b/backend/apps/asset/repositories/snapshot/subdomain_snapshot_repository.py @@ -0,0 +1,61 @@ +"""Django ORM 实现的 SubdomainSnapshot Repository""" + +import logging +from typing import List + +from apps.asset.models.snapshot_models import SubdomainSnapshot +from apps.asset.dtos import SubdomainSnapshotDTO +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoSubdomainSnapshotRepository: + """子域名快照 Repository - 负责子域名快照表的数据访问""" + + def save_subdomain_snapshots(self, items: List[SubdomainSnapshotDTO]) -> None: + """ + 保存子域名快照 + + Args: + items: 子域名快照 DTO 列表 + + Note: + - 保存完整的快照数据 + - 基于唯一约束自动去重(忽略冲突) + """ + try: + logger.debug("准备保存子域名快照 - 数量: %d", len(items)) + + if not items: + logger.debug("子域名快照为空,跳过保存") + return + + # 构建快照对象 + snapshots = [] + for item in items: + snapshots.append(SubdomainSnapshot( + scan_id=item.scan_id, + name=item.name, + )) + + # 批量创建(忽略冲突,基于唯一约束去重) + SubdomainSnapshot.objects.bulk_create(snapshots, ignore_conflicts=True) + + logger.debug("子域名快照保存成功 - 数量: %d", len(snapshots)) + + except Exception as e: + logger.error( + "保存子域名快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def get_by_scan(self, scan_id: int): + return SubdomainSnapshot.objects.filter(scan_id=scan_id).order_by('-discovered_at') + + def get_all(self): + return SubdomainSnapshot.objects.all().order_by('-discovered_at') diff --git a/backend/apps/asset/repositories/snapshot/vulnerability_snapshot_repository.py b/backend/apps/asset/repositories/snapshot/vulnerability_snapshot_repository.py new file mode 100644 index 00000000..91ec5fc3 --- /dev/null +++ b/backend/apps/asset/repositories/snapshot/vulnerability_snapshot_repository.py @@ -0,0 +1,66 @@ +"""Vulnerability Snapshot Repository - 漏洞快照数据访问层""" + +import logging +from typing import List + +from django.db import transaction + +from apps.asset.models import VulnerabilitySnapshot +from apps.asset.dtos.snapshot import VulnerabilitySnapshotDTO +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoVulnerabilitySnapshotRepository: + """漏洞快照仓储(Django ORM 实现)""" + + def save_snapshots(self, items: List[VulnerabilitySnapshotDTO]) -> None: + """批量保存漏洞快照记录。 + + 使用 ``ignore_conflicts`` 策略,如果快照已存在则跳过。 + 具体唯一约束由数据库模型控制。 + """ + if not items: + logger.warning("漏洞快照列表为空,跳过保存") + return + + try: + snapshot_objects = [ + VulnerabilitySnapshot( + scan_id=item.scan_id, + url=item.url, + vuln_type=item.vuln_type, + severity=item.severity, + source=item.source, + cvss_score=item.cvss_score, + description=item.description, + raw_output=item.raw_output, + ) + for item in items + ] + + with transaction.atomic(): + VulnerabilitySnapshot.objects.bulk_create( + snapshot_objects, + ignore_conflicts=True, + ) + + logger.debug("成功保存 %d 条漏洞快照记录", len(items)) + + except Exception as e: + logger.error( + "批量保存漏洞快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True, + ) + raise + + def get_by_scan(self, scan_id: int): + """按扫描任务获取漏洞快照 QuerySet。""" + return VulnerabilitySnapshot.objects.filter(scan_id=scan_id).order_by("-discovered_at") + + def get_all(self): + return VulnerabilitySnapshot.objects.all().order_by('-discovered_at') diff --git a/backend/apps/asset/repositories/snapshot/website_snapshot_repository.py b/backend/apps/asset/repositories/snapshot/website_snapshot_repository.py new file mode 100644 index 00000000..82e92121 --- /dev/null +++ b/backend/apps/asset/repositories/snapshot/website_snapshot_repository.py @@ -0,0 +1,74 @@ +"""WebsiteSnapshot Repository - Django ORM 实现""" + +import logging +from typing import List + +from apps.asset.models.snapshot_models import WebsiteSnapshot +from apps.asset.dtos.snapshot import WebsiteSnapshotDTO +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoWebsiteSnapshotRepository: + """网站快照 Repository - 负责网站快照表的数据访问""" + + def save_snapshots(self, items: List[WebsiteSnapshotDTO]) -> None: + """ + 保存网站快照 + + Args: + items: 网站快照 DTO 列表 + + Note: + - 保存完整的快照数据 + - 基于唯一约束 (scan + subdomain + url) 自动去重 + """ + try: + logger.debug("准备保存网站快照 - 数量: %d", len(items)) + + if not items: + logger.debug("网站快照为空,跳过保存") + return + + # 构建快照对象 + snapshots = [] + for item in items: + snapshots.append(WebsiteSnapshot( + scan_id=item.scan_id, + url=item.url, + host=item.host, + title=item.title, + status=item.status, + content_length=item.content_length, + location=item.location, + web_server=item.web_server, + content_type=item.content_type, + tech=item.tech if item.tech else [], + body_preview=item.body_preview, + vhost=item.vhost + )) + + # 批量创建(忽略冲突,基于唯一约束去重) + WebsiteSnapshot.objects.bulk_create( + snapshots, + ignore_conflicts=True + ) + + logger.debug("网站快照保存成功 - 数量: %d", len(snapshots)) + + except Exception as e: + logger.error( + "保存网站快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def get_by_scan(self, scan_id: int): + return WebsiteSnapshot.objects.filter(scan_id=scan_id).order_by('-discovered_at') + + def get_all(self): + return WebsiteSnapshot.objects.all().order_by('-discovered_at') diff --git a/backend/apps/asset/repositories/statistics_repository.py b/backend/apps/asset/repositories/statistics_repository.py new file mode 100644 index 00000000..69b38cab --- /dev/null +++ b/backend/apps/asset/repositories/statistics_repository.py @@ -0,0 +1,128 @@ +"""资产统计 Repository""" +import logging +from datetime import date, timedelta +from typing import Optional, List + +from apps.asset.models import AssetStatistics, StatisticsHistory + +logger = logging.getLogger(__name__) + + +class AssetStatisticsRepository: + """ + 资产统计数据访问层 + + 职责: + - 读取/更新预聚合的统计数据 + """ + + def get_statistics(self) -> Optional[AssetStatistics]: + """ + 获取统计数据 + + Returns: + 统计数据对象,不存在则返回 None + """ + return AssetStatistics.objects.first() + + def get_or_create_statistics(self) -> AssetStatistics: + """ + 获取或创建统计数据(单例) + + Returns: + 统计数据对象 + """ + return AssetStatistics.get_or_create_singleton() + + def update_statistics( + self, + total_targets: int, + total_subdomains: int, + total_ips: int, + total_endpoints: int, + total_websites: int, + total_vulns: int, + ) -> AssetStatistics: + """ + 更新统计数据 + + Args: + total_targets: 目标总数 + total_subdomains: 子域名总数 + total_ips: IP 总数 + total_endpoints: 端点总数 + total_websites: 网站总数 + total_vulns: 漏洞总数 + + Returns: + 更新后的统计数据对象 + """ + stats = self.get_or_create_statistics() + + # 1. 保存当前值到 prev_* 字段 + stats.prev_targets = stats.total_targets + stats.prev_subdomains = stats.total_subdomains + stats.prev_ips = stats.total_ips + stats.prev_endpoints = stats.total_endpoints + stats.prev_websites = stats.total_websites + stats.prev_vulns = stats.total_vulns + stats.prev_assets = stats.total_assets + + # 2. 更新当前值 + stats.total_targets = total_targets + stats.total_subdomains = total_subdomains + stats.total_ips = total_ips + stats.total_endpoints = total_endpoints + stats.total_websites = total_websites + stats.total_vulns = total_vulns + stats.total_assets = total_subdomains + total_ips + total_endpoints + total_websites + stats.save() + + logger.info( + "更新资产统计: targets=%d, subdomains=%d, ips=%d, endpoints=%d, websites=%d, vulns=%d, assets=%d", + total_targets, total_subdomains, total_ips, total_endpoints, total_websites, total_vulns, stats.total_assets + ) + return stats + + def save_daily_snapshot(self, stats: AssetStatistics) -> StatisticsHistory: + """ + 保存每日统计快照(幂等,每天只存一条) + + Args: + stats: 当前统计数据 + + Returns: + 历史记录对象 + """ + history, created = StatisticsHistory.objects.update_or_create( + date=date.today(), + defaults={ + 'total_targets': stats.total_targets, + 'total_subdomains': stats.total_subdomains, + 'total_ips': stats.total_ips, + 'total_endpoints': stats.total_endpoints, + 'total_websites': stats.total_websites, + 'total_vulns': stats.total_vulns, + 'total_assets': stats.total_assets, + } + ) + action = "创建" if created else "更新" + logger.info(f"{action}统计快照: date={history.date}, assets={history.total_assets}") + return history + + def get_history(self, days: int = 7) -> List[StatisticsHistory]: + """ + 获取历史统计数据(用于折线图) + + Args: + days: 获取最近多少天的数据,默认 7 天 + + Returns: + 历史记录列表,按日期升序 + """ + start_date = date.today() - timedelta(days=days - 1) + return list( + StatisticsHistory.objects + .filter(date__gte=start_date) + .order_by('date') + ) diff --git a/backend/apps/asset/serializers.py b/backend/apps/asset/serializers.py new file mode 100644 index 00000000..97326ec2 --- /dev/null +++ b/backend/apps/asset/serializers.py @@ -0,0 +1,291 @@ +from rest_framework import serializers +from .models import Subdomain, WebSite, Directory, HostPortMapping, Endpoint, Vulnerability +from .models.snapshot_models import ( + SubdomainSnapshot, + WebsiteSnapshot, + DirectorySnapshot, + EndpointSnapshot, + VulnerabilitySnapshot, +) + + +# 注意:IPAddress 和 Port 模型已被重构为 HostPortMapping +# 以下是基于新架构的序列化器实现 + +# class PortSerializer(serializers.ModelSerializer): +# """端口序列化器""" +# +# class Meta: +# model = Port +# fields = ['number', 'service_name', 'description', 'is_uncommon'] + + +class SubdomainSerializer(serializers.ModelSerializer): + """子域名序列化器""" + + class Meta: + model = Subdomain + fields = [ + 'id', 'name', 'discovered_at', 'target' + ] + read_only_fields = ['id', 'discovered_at'] + + +class SubdomainListSerializer(serializers.ModelSerializer): + """子域名列表序列化器(用于扫描详情)""" + + # 注意:Subdomain 模型已简化,只保留核心字段 + # cname, is_cdn, cdn_name 等字段已移至 SubdomainSnapshot + # ports 和 ip_addresses 关系已被重构为 HostPortMapping + + class Meta: + model = Subdomain + fields = [ + 'id', 'name', 'discovered_at' + ] + read_only_fields = ['id', 'discovered_at'] + + +# class IPAddressListSerializer(serializers.ModelSerializer): +# """IP 地址列表序列化器""" +# +# subdomain = serializers.CharField(source='subdomain.name', allow_blank=True, default='') +# created_at = serializers.DateTimeField(read_only=True) +# ports = PortSerializer(many=True, read_only=True) +# +# class Meta: +# model = IPAddress +# fields = [ +# 'id', +# 'ip', +# 'subdomain', +# 'reverse_pointer', +# 'created_at', +# 'ports', +# ] +# read_only_fields = fields + + +class WebSiteSerializer(serializers.ModelSerializer): + """站点序列化器""" + + subdomain = serializers.CharField(source='subdomain.name', allow_blank=True, default='') + + class Meta: + model = WebSite + fields = [ + 'id', + 'url', + 'location', + 'title', + 'webserver', + 'content_type', + 'status_code', + 'content_length', + 'body_preview', + 'tech', + 'vhost', + 'subdomain', + 'discovered_at', + ] + read_only_fields = fields + + +class VulnerabilitySerializer(serializers.ModelSerializer): + """漏洞资产序列化器(按目标查看漏洞资产)。""" + + class Meta: + model = Vulnerability + fields = [ + 'id', + 'target', + 'url', + 'vuln_type', + 'severity', + 'source', + 'cvss_score', + 'description', + 'raw_output', + 'discovered_at', + ] + read_only_fields = fields + + +class VulnerabilitySnapshotSerializer(serializers.ModelSerializer): + """漏洞快照序列化器(用于扫描历史漏洞列表)。""" + + class Meta: + model = VulnerabilitySnapshot + fields = [ + 'id', + 'url', + 'vuln_type', + 'severity', + 'source', + 'cvss_score', + 'description', + 'raw_output', + 'discovered_at', + ] + read_only_fields = fields + + +class EndpointListSerializer(serializers.ModelSerializer): + """端点列表序列化器(用于目标端点列表页)""" + + # 将 GF 匹配模式映射为前端使用的 tags 字段 + tags = serializers.ListField( + child=serializers.CharField(), + source='matched_gf_patterns', + read_only=True, + ) + + class Meta: + model = Endpoint + fields = [ + 'id', + 'url', + 'location', + 'status_code', + 'title', + 'content_length', + 'content_type', + 'webserver', + 'body_preview', + 'tech', + 'vhost', + 'tags', + 'discovered_at', + ] + read_only_fields = fields + + +class DirectorySerializer(serializers.ModelSerializer): + """目录序列化器""" + + website_url = serializers.CharField(source='website.url', read_only=True) + discovered_at = serializers.DateTimeField(read_only=True) + + class Meta: + model = Directory + fields = [ + 'id', + 'url', + 'status', + 'content_length', + 'words', + 'lines', + 'content_type', + 'duration', + 'website_url', + 'discovered_at', + ] + read_only_fields = fields + + +class IPAddressAggregatedSerializer(serializers.Serializer): + """ + IP 地址聚合序列化器 + + 基于 HostPortMapping 模型,按 IP 聚合显示: + - ip: IP 地址 + - hosts: 该 IP 关联的所有主机名列表 + - ports: 该 IP 关联的所有端口列表 + - discovered_at: 首次发现时间 + """ + ip = serializers.IPAddressField(read_only=True) + hosts = serializers.ListField(child=serializers.CharField(), read_only=True) + ports = serializers.ListField(child=serializers.IntegerField(), read_only=True) + discovered_at = serializers.DateTimeField(read_only=True) + + +# ==================== 快照序列化器 ==================== + +class SubdomainSnapshotSerializer(serializers.ModelSerializer): + """子域名快照序列化器(用于扫描历史)""" + + class Meta: + model = SubdomainSnapshot + fields = ['id', 'name', 'discovered_at'] + read_only_fields = fields + + +class WebsiteSnapshotSerializer(serializers.ModelSerializer): + """网站快照序列化器(用于扫描历史)""" + + subdomain_name = serializers.CharField(source='subdomain.name', read_only=True) + webserver = serializers.CharField(source='web_server', read_only=True) # 映射字段名 + status_code = serializers.IntegerField(source='status', read_only=True) # 映射字段名 + + class Meta: + model = WebsiteSnapshot + fields = [ + 'id', + 'url', + 'location', + 'title', + 'webserver', # 使用映射后的字段名 + 'content_type', + 'status_code', # 使用映射后的字段名 + 'content_length', + 'body_preview', + 'tech', + 'vhost', + 'subdomain_name', + 'discovered_at', + ] + read_only_fields = fields + + +class DirectorySnapshotSerializer(serializers.ModelSerializer): + """目录快照序列化器(用于扫描历史)""" + + # DirectorySnapshot 当前不再关联 Website,这里暂时将 website_url 映射为自身的 url,保证字段兼容 + website_url = serializers.CharField(source='url', read_only=True) + + class Meta: + model = DirectorySnapshot + fields = [ + 'id', + 'url', + 'status', + 'content_length', + 'words', + 'lines', + 'content_type', + 'duration', + 'website_url', + 'discovered_at', + ] + read_only_fields = fields + + +class EndpointSnapshotSerializer(serializers.ModelSerializer): + """端点快照序列化器(用于扫描历史)""" + + # 将 GF 匹配模式映射为前端使用的 tags 字段 + tags = serializers.ListField( + child=serializers.CharField(), + source='matched_gf_patterns', + read_only=True, + ) + + class Meta: + model = EndpointSnapshot + fields = [ + 'id', + 'url', + 'host', + 'location', + 'title', + 'webserver', + 'content_type', + 'status_code', + 'content_length', + 'body_preview', + 'tech', + 'vhost', + 'tags', + 'discovered_at', + ] + read_only_fields = fields diff --git a/backend/apps/asset/services/__init__.py b/backend/apps/asset/services/__init__.py new file mode 100644 index 00000000..5dd95948 --- /dev/null +++ b/backend/apps/asset/services/__init__.py @@ -0,0 +1,43 @@ +"""Asset Services - 业务逻辑层""" + +# 资产模块 Services +from .asset import ( + SubdomainService, + WebSiteService, + DirectoryService, + HostPortMappingService, + EndpointService, + VulnerabilityService, +) + +# 快照模块 Services +from .snapshot import ( + SubdomainSnapshotsService, + HostPortMappingSnapshotsService, + WebsiteSnapshotsService, + DirectorySnapshotsService, + EndpointSnapshotsService, + VulnerabilitySnapshotsService, +) + +# 统计模块 Service +from .statistics_service import AssetStatisticsService + +__all__ = [ + # 资产模块 + 'SubdomainService', + 'WebSiteService', + 'DirectoryService', + 'HostPortMappingService', + 'EndpointService', + 'VulnerabilityService', + # 快照模块 + 'SubdomainSnapshotsService', + 'HostPortMappingSnapshotsService', + 'WebsiteSnapshotsService', + 'DirectorySnapshotsService', + 'EndpointSnapshotsService', + 'VulnerabilitySnapshotsService', + # 统计模块 + 'AssetStatisticsService', +] diff --git a/backend/apps/asset/services/asset/__init__.py b/backend/apps/asset/services/asset/__init__.py new file mode 100644 index 00000000..0d902374 --- /dev/null +++ b/backend/apps/asset/services/asset/__init__.py @@ -0,0 +1,17 @@ +"""Asset Services - 资产模块的业务逻辑层""" + +from .subdomain_service import SubdomainService +from .website_service import WebSiteService +from .directory_service import DirectoryService +from .host_port_mapping_service import HostPortMappingService +from .endpoint_service import EndpointService +from .vulnerability_service import VulnerabilityService + +__all__ = [ + 'SubdomainService', + 'WebSiteService', + 'DirectoryService', + 'HostPortMappingService', + 'EndpointService', + 'VulnerabilityService', +] diff --git a/backend/apps/asset/services/asset/directory_service.py b/backend/apps/asset/services/asset/directory_service.py new file mode 100644 index 00000000..1b4ce667 --- /dev/null +++ b/backend/apps/asset/services/asset/directory_service.py @@ -0,0 +1,55 @@ +import logging +from typing import Tuple, Iterator + +from apps.asset.models.asset_models import Directory +from apps.asset.repositories import DjangoDirectoryRepository + +logger = logging.getLogger(__name__) + + +class DirectoryService: + """目录业务逻辑层""" + + def __init__(self, repository=None): + """ + 初始化目录服务 + + Args: + repository: 目录仓储实例(用于依赖注入) + """ + self.repo = repository or DjangoDirectoryRepository() + + # ==================== 创建操作 ==================== + + def bulk_create_ignore_conflicts(self, directory_dtos: list) -> None: + """ + 批量创建目录记录,忽略冲突(用于扫描任务) + + Args: + directory_dtos: DirectoryDTO 列表 + """ + return self.repo.bulk_create_ignore_conflicts(directory_dtos) + + # ==================== 查询操作 ==================== + + def get_all(self): + """ + 获取所有目录 + + Returns: + QuerySet: 目录查询集 + """ + logger.debug("获取所有目录") + return self.repo.get_all() + + def get_directories_by_target(self, target_id: int): + logger.debug("获取目标下所有目录 - Target ID: %d", target_id) + return self.repo.get_by_target(target_id) + + def iter_directory_urls_by_target(self, target_id: int, chunk_size: int = 1000) -> Iterator[str]: + """流式获取目标下的所有目录 URL,用于导出大批量数据。""" + logger.debug("流式导出目标下目录 URL - Target ID: %d", target_id) + return self.repo.get_urls_for_export(target_id=target_id, batch_size=chunk_size) + + +__all__ = ['DirectoryService'] diff --git a/backend/apps/asset/services/asset/endpoint_service.py b/backend/apps/asset/services/asset/endpoint_service.py new file mode 100644 index 00000000..72f33097 --- /dev/null +++ b/backend/apps/asset/services/asset/endpoint_service.py @@ -0,0 +1,178 @@ +""" +Endpoint 服务层 + +处理 URL/端点相关的业务逻辑 +""" + +import logging +from typing import List, Optional, Dict, Any, Iterator + +from apps.asset.dtos.asset import EndpointDTO +from apps.asset.repositories.asset import DjangoEndpointRepository + +logger = logging.getLogger(__name__) + + +class EndpointService: + """ + Endpoint 服务类 + + 提供 Endpoint(URL/端点)相关的业务逻辑 + """ + + def __init__(self): + """初始化 Endpoint 服务""" + self.repo = DjangoEndpointRepository() + + def bulk_create_endpoints( + self, + endpoints: List[EndpointDTO], + ignore_conflicts: bool = True + ) -> int: + """ + 批量创建端点记录 + + Args: + endpoints: 端点数据列表 + ignore_conflicts: 是否忽略冲突(去重) + + Returns: + int: 创建的记录数 + """ + if not endpoints: + return 0 + + try: + if ignore_conflicts: + return self.repo.bulk_create_ignore_conflicts(endpoints) + else: + # 如果需要非忽略冲突的版本,可以在 repository 中添加 + return self.repo.bulk_create_ignore_conflicts(endpoints) + except Exception as e: + logger.error(f"批量创建端点失败: {e}") + raise + + def get_endpoints_by_website( + self, + website_id: int, + limit: Optional[int] = None + ) -> List[Dict[str, Any]]: + """ + 获取网站下的端点列表 + + Args: + website_id: 网站 ID + limit: 返回数量限制 + + Returns: + List[Dict]: 端点列表 + """ + endpoints_dto = self.repo.get_by_website(website_id) + + if limit: + endpoints_dto = endpoints_dto[:limit] + + endpoints = [] + for dto in endpoints_dto: + endpoints.append({ + 'url': dto.url, + 'title': dto.title, + 'status_code': dto.status_code, + 'content_length': dto.content_length, + 'webserver': dto.webserver + }) + + return endpoints + + def get_endpoints_by_target( + self, + target_id: int, + limit: Optional[int] = None + ) -> List[Dict[str, Any]]: + """ + 获取目标下的端点列表 + + Args: + target_id: 目标 ID + limit: 返回数量限制 + + Returns: + List[Dict]: 端点列表 + """ + endpoints_dto = self.repo.get_by_target(target_id) + + if limit: + endpoints_dto = endpoints_dto[:limit] + + endpoints = [] + for dto in endpoints_dto: + endpoints.append({ + 'url': dto.url, + 'title': dto.title, + 'status_code': dto.status_code, + 'content_length': dto.content_length, + 'webserver': dto.webserver + }) + + return endpoints + + def count_endpoints_by_target(self, target_id: int) -> int: + """ + 统计目标下的端点数量 + + Args: + target_id: 目标 ID + + Returns: + int: 端点数量 + """ + return self.repo.count_by_target(target_id) + + def get_queryset_by_target(self, target_id: int): + return self.repo.get_queryset_by_target(target_id) + + def get_all(self): + """获取所有端点(全局查询)""" + return self.repo.get_all() + + def iter_endpoint_urls_by_target(self, target_id: int, chunk_size: int = 1000) -> Iterator[str]: + """流式获取目标下的所有端点 URL,用于导出。""" + queryset = self.repo.get_queryset_by_target(target_id) + for url in queryset.values_list('url', flat=True).iterator(chunk_size=chunk_size): + yield url + + def count_endpoints_by_website(self, website_id: int) -> int: + """ + 统计网站下的端点数量 + + Args: + website_id: 网站 ID + + Returns: + int: 端点数量 + """ + return self.repo.count_by_website(website_id) + + def soft_delete_endpoints(self, endpoint_ids: List[int]) -> int: + """ + 软删除端点 + + Args: + endpoint_ids: 端点 ID 列表 + + Returns: + int: 更新的数量 + """ + return self.repo.soft_delete_by_ids(endpoint_ids) + + def hard_delete_endpoints(self, endpoint_ids: List[int]) -> tuple: + """ + 硬删除端点 + + Args: + endpoint_ids: 端点 ID 列表 + + Returns: + tuple: (删除总数, 详细信息) + """ + return self.repo.hard_delete_by_ids(endpoint_ids) diff --git a/backend/apps/asset/services/asset/host_port_mapping_service.py b/backend/apps/asset/services/asset/host_port_mapping_service.py new file mode 100644 index 00000000..d6aed8a7 --- /dev/null +++ b/backend/apps/asset/services/asset/host_port_mapping_service.py @@ -0,0 +1,61 @@ +"""HostPortMapping Service - 业务逻辑层""" + +import logging +from typing import List, Iterator + +from apps.asset.repositories.asset import DjangoHostPortMappingRepository +from apps.asset.dtos.asset import HostPortMappingDTO + +logger = logging.getLogger(__name__) + + +class HostPortMappingService: + """主机端口映射服务 - 负责主机端口映射数据的业务逻辑""" + + def __init__(self): + self.repo = DjangoHostPortMappingRepository() + + def bulk_create_ignore_conflicts(self, items: List[HostPortMappingDTO]) -> int: + """ + 批量创建主机端口映射(忽略冲突) + + Args: + items: 主机端口映射 DTO 列表 + + Returns: + int: 实际创建的记录数 + + Note: + 使用数据库唯一约束 + ignore_conflicts 自动去重 + """ + try: + logger.debug("Service: 准备批量创建主机端口映射 - 数量: %d", len(items)) + + created_count = self.repo.bulk_create_ignore_conflicts(items) + + logger.info("Service: 主机端口映射创建成功 - 数量: %d", created_count) + + return created_count + + except Exception as e: + logger.error( + "Service: 批量创建主机端口映射失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def iter_host_port_by_target(self, target_id: int, batch_size: int = 1000): + return self.repo.get_for_export(target_id=target_id, batch_size=batch_size) + + def get_ip_aggregation_by_target(self, target_id: int, search: str = None): + return self.repo.get_ip_aggregation_by_target(target_id, search=search) + + def get_all_ip_aggregation(self, search: str = None): + """获取所有 IP 聚合数据(全局查询)""" + return self.repo.get_all_ip_aggregation(search=search) + + def iter_ips_by_target(self, target_id: int, batch_size: int = 1000) -> Iterator[str]: + """流式获取目标下的所有唯一 IP 地址。""" + return self.repo.get_ips_for_export(target_id=target_id, batch_size=batch_size) diff --git a/backend/apps/asset/services/asset/subdomain_service.py b/backend/apps/asset/services/asset/subdomain_service.py new file mode 100644 index 00000000..33dc55cf --- /dev/null +++ b/backend/apps/asset/services/asset/subdomain_service.py @@ -0,0 +1,123 @@ +import logging +from typing import Tuple, List, Dict + +from apps.asset.repositories import DjangoSubdomainRepository +from apps.asset.dtos import SubdomainDTO + +logger = logging.getLogger(__name__) + + +class SubdomainService: + """子域名业务逻辑层""" + + def __init__(self, repository=None): + """ + 初始化子域名服务 + + Args: + repository: 子域名仓储实例(用于依赖注入) + """ + self.repo = repository or DjangoSubdomainRepository() + + # ==================== 查询操作 ==================== + + def get_all(self): + """ + 获取所有子域名 + + Returns: + QuerySet: 子域名查询集 + """ + logger.debug("获取所有子域名") + return self.repo.get_all() + + # ==================== 创建操作 ==================== + + def get_or_create(self, name: str, target_id: int) -> Tuple[any, bool]: + """ + 获取或创建子域名 + + Args: + name: 子域名名称 + target_id: 目标 ID + + Returns: + (Subdomain对象, 是否新创建) + """ + logger.debug("获取或创建子域名 - Name: %s, Target ID: %d", name, target_id) + return self.repo.get_or_create(name, target_id) + + def bulk_create_ignore_conflicts(self, items: List[SubdomainDTO]) -> None: + """ + 批量创建子域名,忽略冲突 + + Args: + items: 子域名 DTO 列表 + + Note: + 使用 ignore_conflicts 策略,重复记录会被跳过 + """ + logger.debug("批量创建子域名 - 数量: %d", len(items)) + return self.repo.bulk_create_ignore_conflicts(items) + + def get_by_names_and_target_id(self, names: set, target_id: int) -> dict: + """ + 根据域名列表和目标ID批量查询子域名 + + Args: + names: 域名集合 + target_id: 目标 ID + + Returns: + dict: {域名: Subdomain对象} + """ + logger.debug("批量查询子域名 - 数量: %d, Target ID: %d", len(names), target_id) + return self.repo.get_by_names_and_target_id(names, target_id) + + def get_subdomain_names_by_target(self, target_id: int) -> List[str]: + """ + 获取目标下的所有子域名名称 + + Args: + target_id: 目标 ID + + Returns: + List[str]: 子域名名称列表 + """ + logger.debug("获取目标下所有子域名 - Target ID: %d", target_id) + # 通过仓储层统一访问数据库,内部已使用 iterator() 做流式查询 + return list(self.repo.get_domains_for_export(target_id=target_id)) + + def get_subdomains_by_target(self, target_id: int): + return self.repo.get_by_target(target_id) + + def count_subdomains_by_target(self, target_id: int) -> int: + """ + 统计目标下的子域名数量 + + Args: + target_id: 目标 ID + + Returns: + int: 子域名数量 + """ + logger.debug("统计目标下子域名数量 - Target ID: %d", target_id) + return self.repo.count_by_target(target_id) + + def iter_subdomain_names_by_target(self, target_id: int, chunk_size: int = 1000): + """ + 流式获取目标下的所有子域名名称(内存优化) + + Args: + target_id: 目标 ID + chunk_size: 批次大小 + + Yields: + str: 子域名名称 + """ + logger.debug("流式获取目标下所有子域名 - Target ID: %d, 批次大小: %d", target_id, chunk_size) + # 通过仓储层统一访问数据库,内部已使用 iterator() 做流式查询 + return self.repo.get_domains_for_export(target_id=target_id, batch_size=chunk_size) + + +__all__ = ['SubdomainService'] diff --git a/backend/apps/asset/services/asset/vulnerability_service.py b/backend/apps/asset/services/asset/vulnerability_service.py new file mode 100644 index 00000000..8e5e6bef --- /dev/null +++ b/backend/apps/asset/services/asset/vulnerability_service.py @@ -0,0 +1,81 @@ +"""Vulnerability Service - 漏洞资产业务逻辑层""" + +import logging +from typing import List + +from apps.asset.models import Vulnerability +from apps.asset.dtos.asset import VulnerabilityDTO +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class VulnerabilityService: + """漏洞资产服务 + + 当前提供基础的批量创建能力,使用 ignore_conflicts 依赖数据库唯一约束去重。 + """ + + def bulk_create_ignore_conflicts(self, items: List[VulnerabilityDTO]) -> None: + """批量创建漏洞资产记录,忽略冲突。 + + Note: + - 是否去重取决于模型上的唯一/部分唯一约束; + - 当前 Vulnerability 模型未定义唯一约束,因此会保留全部记录。 + """ + if not items: + logger.debug("漏洞资产列表为空,跳过保存") + return + + try: + vulns = [ + Vulnerability( + target_id=item.target_id, + url=item.url, + vuln_type=item.vuln_type, + severity=item.severity, + source=item.source, + cvss_score=item.cvss_score, + description=item.description, + raw_output=item.raw_output, + ) + for item in items + ] + + Vulnerability.objects.bulk_create(vulns, ignore_conflicts=True) + logger.info("漏洞资产保存成功 - 数量: %d", len(vulns)) + + except Exception as e: + logger.error( + "批量保存漏洞资产失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True, + ) + raise + + # ==================== 查询方法 ==================== + + def get_all(self): + """获取所有漏洞 QuerySet(用于全局漏洞列表)。 + + Returns: + QuerySet[Vulnerability]: 所有漏洞,按发现时间倒序 + """ + return Vulnerability.objects.filter(deleted_at__isnull=True).order_by("-discovered_at") + + def get_queryset_by_target(self, target_id: int): + """按目标获取漏洞 QuerySet(用于分页)。 + + Args: + target_id: 目标 ID + + Returns: + QuerySet[Vulnerability]: 目标下的所有漏洞,按发现时间倒序 + """ + return Vulnerability.objects.filter(target_id=target_id).order_by("-discovered_at") + + def count_by_target(self, target_id: int) -> int: + """统计目标下的漏洞数量。""" + return Vulnerability.objects.filter(target_id=target_id).count() diff --git a/backend/apps/asset/services/asset/website_service.py b/backend/apps/asset/services/asset/website_service.py new file mode 100644 index 00000000..83b57170 --- /dev/null +++ b/backend/apps/asset/services/asset/website_service.py @@ -0,0 +1,91 @@ +import logging +from typing import Tuple, List + +from apps.asset.repositories import DjangoWebSiteRepository +from apps.asset.dtos import WebSiteDTO + +logger = logging.getLogger(__name__) + + +class WebSiteService: + """网站业务逻辑层""" + + def __init__(self, repository=None): + """ + 初始化网站服务 + + Args: + repository: 网站仓储实例(用于依赖注入) + """ + self.repo = repository or DjangoWebSiteRepository() + + # ==================== 创建操作 ==================== + + def bulk_create_ignore_conflicts(self, website_dtos: List[WebSiteDTO]) -> None: + """ + 批量创建网站记录,忽略冲突(用于扫描任务) + + Args: + website_dtos: WebSiteDTO 列表 + + Note: + 使用 ignore_conflicts 策略,重复记录会被跳过 + """ + logger.debug("批量创建网站 - 数量: %d", len(website_dtos)) + return self.repo.bulk_create_ignore_conflicts(website_dtos) + + # ==================== 查询操作 ==================== + + def get_by_url(self, url: str, target_id: int) -> int: + """ + 根据 URL 和 target_id 查找网站 ID + + Args: + url: 网站 URL + target_id: 目标 ID + + Returns: + int: 网站 ID,如果不存在返回 None + """ + return self.repo.get_by_url(url=url, target_id=target_id) + + # ==================== 查询操作 ==================== + + def get_all(self): + """ + 获取所有网站 + + Returns: + QuerySet: 网站查询集 + """ + logger.debug("获取所有网站") + return self.repo.get_all() + + def get_websites_by_target(self, target_id: int): + return self.repo.get_by_target(target_id) + + def count_websites_by_scan(self, scan_id: int) -> int: + """ + 统计扫描下的网站数量 + + Args: + scan_id: 扫描 ID + + Returns: + int: 网站数量 + """ + logger.debug("统计扫描下网站数量 - Scan ID: %d", scan_id) + return self.repo.count_by_scan(scan_id) + + def iter_website_urls_by_target(self, target_id: int, chunk_size: int = 1000): + """流式获取目标下的所有站点 URL(内存优化,委托给 Repository 层)""" + logger.debug( + "流式获取目标下所有站点 URL - Target ID: %d, 批次大小: %d", + target_id, + chunk_size, + ) + # 通过仓储层统一访问数据库,避免 Service 直接依赖 ORM + return self.repo.get_urls_for_export(target_id=target_id, batch_size=chunk_size) + + +__all__ = ['WebSiteService'] diff --git a/backend/apps/asset/services/snapshot/__init__.py b/backend/apps/asset/services/snapshot/__init__.py new file mode 100644 index 00000000..afabdeac --- /dev/null +++ b/backend/apps/asset/services/snapshot/__init__.py @@ -0,0 +1,17 @@ +"""Snapshot Services - 快照服务""" + +from .subdomain_snapshots_service import SubdomainSnapshotsService +from .host_port_mapping_snapshots_service import HostPortMappingSnapshotsService +from .website_snapshots_service import WebsiteSnapshotsService +from .directory_snapshots_service import DirectorySnapshotsService +from .endpoint_snapshots_service import EndpointSnapshotsService +from .vulnerability_snapshots_service import VulnerabilitySnapshotsService + +__all__ = [ + 'SubdomainSnapshotsService', + 'HostPortMappingSnapshotsService', + 'WebsiteSnapshotsService', + 'DirectorySnapshotsService', + 'EndpointSnapshotsService', + 'VulnerabilitySnapshotsService', +] diff --git a/backend/apps/asset/services/snapshot/directory_snapshots_service.py b/backend/apps/asset/services/snapshot/directory_snapshots_service.py new file mode 100644 index 00000000..2e8a58d2 --- /dev/null +++ b/backend/apps/asset/services/snapshot/directory_snapshots_service.py @@ -0,0 +1,83 @@ +"""Directory Snapshots Service - 业务逻辑层""" + +import logging +from typing import List, Iterator + +from apps.asset.repositories.snapshot import DjangoDirectorySnapshotRepository +from apps.asset.services.asset import DirectoryService +from apps.asset.dtos.snapshot import DirectorySnapshotDTO + +logger = logging.getLogger(__name__) + + +class DirectorySnapshotsService: + """目录快照服务 - 统一管理快照和资产同步""" + + def __init__(self): + self.snapshot_repo = DjangoDirectorySnapshotRepository() + self.asset_service = DirectoryService() + + def save_and_sync(self, items: List[DirectorySnapshotDTO]) -> None: + """ + 保存目录快照并同步到资产表(统一入口) + + 流程: + 1. 保存到快照表(完整记录,包含 scan_id) + 2. 同步到资产表(去重,不包含 scan_id) + + Args: + items: 目录快照 DTO 列表(必须包含 website_id) + + Raises: + ValueError: 如果 items 中的 website_id 为 None + Exception: 数据库操作失败 + """ + if not items: + return + + # 检查 Scan 是否仍存在(防止删除后竞态写入) + scan_id = items[0].scan_id + from apps.scan.repositories import DjangoScanRepository + if not DjangoScanRepository().exists(scan_id): + logger.warning("Scan 已删除,跳过目录快照保存 - scan_id=%s, 数量=%d", scan_id, len(items)) + return + + try: + logger.debug("保存目录快照并同步到资产表 - 数量: %d", len(items)) + + # 步骤 1: 保存到快照表 + logger.debug("步骤 1: 保存到快照表") + self.snapshot_repo.save_snapshots(items) + + # 步骤 2: 转换为资产 DTO 并保存到资产表 + # 注意:去重是通过数据库的 UNIQUE 约束 + ignore_conflicts 实现的 + # - 新记录:插入资产表 + # - 已存在的记录:自动跳过 + logger.debug("步骤 2: 同步到资产表(通过 Service 层)") + asset_items = [item.to_asset_dto() for item in items] + + self.asset_service.bulk_create_ignore_conflicts(asset_items) + + logger.info("目录快照和资产数据保存成功 - 数量: %d", len(items)) + + except Exception as e: + logger.error( + "保存目录快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def get_by_scan(self, scan_id: int): + return self.snapshot_repo.get_by_scan(scan_id) + + def get_all(self): + """获取所有目录快照""" + return self.snapshot_repo.get_all() + + def iter_directory_urls_by_scan(self, scan_id: int, chunk_size: int = 1000) -> Iterator[str]: + """流式获取某次扫描下的所有目录 URL。""" + queryset = self.snapshot_repo.get_by_scan(scan_id) + for snapshot in queryset.iterator(chunk_size=chunk_size): + yield snapshot.url diff --git a/backend/apps/asset/services/snapshot/endpoint_snapshots_service.py b/backend/apps/asset/services/snapshot/endpoint_snapshots_service.py new file mode 100644 index 00000000..4d8a43c6 --- /dev/null +++ b/backend/apps/asset/services/snapshot/endpoint_snapshots_service.py @@ -0,0 +1,83 @@ +"""Endpoint Snapshots Service - 业务逻辑层""" + +import logging +from typing import List, Iterator + +from apps.asset.repositories.snapshot import DjangoEndpointSnapshotRepository +from apps.asset.services.asset import EndpointService +from apps.asset.dtos.snapshot import EndpointSnapshotDTO + +logger = logging.getLogger(__name__) + + +class EndpointSnapshotsService: + """端点快照服务 - 统一管理快照和资产同步""" + + def __init__(self): + self.snapshot_repo = DjangoEndpointSnapshotRepository() + self.asset_service = EndpointService() + + def save_and_sync(self, items: List[EndpointSnapshotDTO]) -> None: + """ + 保存端点快照并同步到资产表(统一入口) + + 流程: + 1. 保存到快照表(完整记录) + 2. 同步到资产表(去重) + + Args: + items: 端点快照 DTO 列表(必须包含 target_id) + + Raises: + ValueError: 如果 items 中的 target_id 为 None + Exception: 数据库操作失败 + """ + if not items: + return + + # 检查 Scan 是否仍存在(防止删除后竞态写入) + scan_id = items[0].scan_id + from apps.scan.repositories import DjangoScanRepository + if not DjangoScanRepository().exists(scan_id): + logger.warning("Scan 已删除,跳过端点快照保存 - scan_id=%s, 数量=%d", scan_id, len(items)) + return + + try: + logger.debug("保存端点快照并同步到资产表 - 数量: %d", len(items)) + + # 步骤 1: 保存到快照表 + logger.debug("步骤 1: 保存到快照表") + self.snapshot_repo.save_snapshots(items) + + # 步骤 2: 转换为资产 DTO 并保存到资产表 + # 注意:去重是通过数据库的 UNIQUE 约束 + ignore_conflicts 实现的 + # - 新记录:插入资产表 + # - 已存在的记录:自动跳过 + logger.debug("步骤 2: 同步到资产表(通过 Service 层)") + asset_items = [item.to_asset_dto() for item in items] + + self.asset_service.bulk_create_endpoints(asset_items) + + logger.info("端点快照和资产数据保存成功 - 数量: %d", len(items)) + + except Exception as e: + logger.error( + "保存端点快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def get_by_scan(self, scan_id: int): + return self.snapshot_repo.get_by_scan(scan_id) + + def get_all(self): + """获取所有端点快照""" + return self.snapshot_repo.get_all() + + def iter_endpoint_urls_by_scan(self, scan_id: int, chunk_size: int = 1000) -> Iterator[str]: + """流式获取某次扫描下的所有端点 URL。""" + queryset = self.snapshot_repo.get_by_scan(scan_id) + for snapshot in queryset.iterator(chunk_size=chunk_size): + yield snapshot.url diff --git a/backend/apps/asset/services/snapshot/host_port_mapping_snapshots_service.py b/backend/apps/asset/services/snapshot/host_port_mapping_snapshots_service.py new file mode 100644 index 00000000..98acae49 --- /dev/null +++ b/backend/apps/asset/services/snapshot/host_port_mapping_snapshots_service.py @@ -0,0 +1,81 @@ +"""HostPortMapping Snapshots Service - 业务逻辑层""" + +import logging +from typing import List, Iterator + +from apps.asset.repositories.snapshot import DjangoHostPortMappingSnapshotRepository +from apps.asset.services.asset import HostPortMappingService +from apps.asset.dtos.snapshot import HostPortMappingSnapshotDTO + +logger = logging.getLogger(__name__) + + +class HostPortMappingSnapshotsService: + """HostPortMapping Snapshots Service - 统一管理快照和资产同步""" + + def __init__(self): + self.snapshot_repo = DjangoHostPortMappingSnapshotRepository() + self.asset_service = HostPortMappingService() + + def save_and_sync(self, items: List[HostPortMappingSnapshotDTO]) -> None: + """ + 保存主机端口关联快照并同步到资产表(统一入口) + + 流程: + 1. 保存到快照表(完整记录,包含 scan_id) + 2. 同步到资产表(去重,不包含 scan_id) + + Args: + items: 主机端口关联快照 DTO 列表(必须包含 target_id) + + Note: + target_id 已经包含在 DTO 中,无需额外传参。 + """ + logger.debug("保存主机端口关联快照 - 数量: %d", len(items)) + + if not items: + logger.debug("快照数据为空,跳过保存") + return + + # 检查 Scan 是否仍存在(防止删除后竞态写入) + scan_id = items[0].scan_id + from apps.scan.repositories import DjangoScanRepository + if not DjangoScanRepository().exists(scan_id): + logger.warning("Scan 已删除,跳过主机端口快照保存 - scan_id=%s, 数量=%d", scan_id, len(items)) + return + + try: + # 步骤 1: 保存到快照表 + logger.debug("步骤 1: 保存到快照表") + self.snapshot_repo.save_snapshots(items) + + # 步骤 2: 转换为资产 DTO 并保存到资产表 + # 注意:去重是通过数据库的 UNIQUE 约束 + ignore_conflicts 实现的 + # - 新记录:插入资产表 + # - 已存在的记录:自动跳过 + logger.debug("步骤 2: 同步到资产表(通过 Service 层)") + asset_items = [item.to_asset_dto() for item in items] + + self.asset_service.bulk_create_ignore_conflicts(asset_items) + + logger.info("主机端口关联快照和资产数据保存成功 - 数量: %d", len(items)) + + except Exception as e: + logger.error( + "保存主机端口关联快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def get_ip_aggregation_by_scan(self, scan_id: int, search: str = None): + return self.snapshot_repo.get_ip_aggregation_by_scan(scan_id, search=search) + + def get_all_ip_aggregation(self, search: str = None): + """获取所有 IP 聚合数据""" + return self.snapshot_repo.get_all_ip_aggregation(search=search) + + def iter_ips_by_scan(self, scan_id: int, batch_size: int = 1000) -> Iterator[str]: + """流式获取某次扫描下的所有唯一 IP 地址。""" + return self.snapshot_repo.get_ips_for_export(scan_id=scan_id, batch_size=batch_size) diff --git a/backend/apps/asset/services/snapshot/subdomain_snapshots_service.py b/backend/apps/asset/services/snapshot/subdomain_snapshots_service.py new file mode 100644 index 00000000..13481737 --- /dev/null +++ b/backend/apps/asset/services/snapshot/subdomain_snapshots_service.py @@ -0,0 +1,79 @@ +import logging +from typing import List, Iterator + +from apps.asset.dtos import SubdomainSnapshotDTO +from apps.asset.repositories import DjangoSubdomainSnapshotRepository + +logger = logging.getLogger(__name__) + + +class SubdomainSnapshotsService: + """子域名快照服务 - 负责子域名快照数据的业务逻辑""" + + def __init__(self): + self.subdomain_snapshot_repo = DjangoSubdomainSnapshotRepository() + + def save_and_sync(self, items: List[SubdomainSnapshotDTO]) -> None: + """ + 保存子域名快照并同步到资产表(统一入口) + + 流程: + 1. 保存到快照表(完整记录,包含 scan_id) + 2. 同步到资产表(去重,不包含 scan_id) + + Args: + items: 子域名快照 DTO 列表(包含 target_id) + + Note: + target_id 已经包含在 DTO 中,无需额外传参。 + """ + logger.debug("保存子域名快照 - 数量: %d", len(items)) + + if not items: + logger.debug("快照数据为空,跳过保存") + return + + # 检查 Scan 是否仍存在(防止删除后竞态写入) + scan_id = items[0].scan_id + from apps.scan.repositories import DjangoScanRepository + if not DjangoScanRepository().exists(scan_id): + logger.warning("Scan 已删除,跳过快照保存 - scan_id=%s, 数量=%d", scan_id, len(items)) + return + + try: + # 步骤 1: 保存到快照表 + logger.debug("步骤 1: 保存到快照表") + self.subdomain_snapshot_repo.save_subdomain_snapshots(items) + + # 步骤 2: 转换为资产 DTO 并保存到资产表(通过数据库唯一约束自动去重) + # 注意:去重是通过数据库的 UNIQUE 约束 + ignore_conflicts 实现的 + # - 新子域名:插入资产表 + # - 已存在的子域名:自动跳过(不更新,因为资产表只记录核心数据) + asset_items = [item.to_asset_dto() for item in items] + + from apps.asset.services import SubdomainService + subdomain_service = SubdomainService() + subdomain_service.bulk_create_ignore_conflicts(asset_items) + + logger.info("子域名快照和业务数据保存成功 - 数量: %d", len(items)) + + except Exception as e: + logger.error( + "保存子域名快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def get_by_scan(self, scan_id: int): + return self.subdomain_snapshot_repo.get_by_scan(scan_id) + + def get_all(self): + """获取所有子域名快照""" + return self.subdomain_snapshot_repo.get_all() + + def iter_subdomain_names_by_scan(self, scan_id: int, chunk_size: int = 1000) -> Iterator[str]: + queryset = self.subdomain_snapshot_repo.get_by_scan(scan_id) + for snapshot in queryset.iterator(chunk_size=chunk_size): + yield snapshot.name \ No newline at end of file diff --git a/backend/apps/asset/services/snapshot/vulnerability_snapshots_service.py b/backend/apps/asset/services/snapshot/vulnerability_snapshots_service.py new file mode 100644 index 00000000..db76d308 --- /dev/null +++ b/backend/apps/asset/services/snapshot/vulnerability_snapshots_service.py @@ -0,0 +1,81 @@ +"""Vulnerability Snapshots Service - 业务逻辑层""" + +import logging +from typing import List, Iterator + +from apps.asset.repositories.snapshot import DjangoVulnerabilitySnapshotRepository +from apps.asset.services.asset.vulnerability_service import VulnerabilityService +from apps.asset.dtos.snapshot import VulnerabilitySnapshotDTO + +logger = logging.getLogger(__name__) + + +class VulnerabilitySnapshotsService: + """漏洞快照服务 - 统一管理快照和资产同步。 + + 流程与 Website/Directory 等保持一致: + 1. 保存到 VulnerabilitySnapshot 快照表(包含 scan_id) + 2. 转为 VulnerabilityDTO 并同步到 Vulnerability 资产表(基于 target_id) + """ + + def __init__(self): + self.snapshot_repo = DjangoVulnerabilitySnapshotRepository() + self.asset_service = VulnerabilityService() + + def save_and_sync(self, items: List[VulnerabilitySnapshotDTO]) -> None: + """保存漏洞快照并同步到漏洞资产表。""" + if not items: + return + + # 检查 Scan 是否仍存在(防止删除后竞态写入) + scan_id = items[0].scan_id + from apps.scan.repositories import DjangoScanRepository + if not DjangoScanRepository().exists(scan_id): + logger.warning("Scan 已删除,跳过漏洞快照保存 - scan_id=%s, 数量=%d", scan_id, len(items)) + return + + try: + logger.debug("保存漏洞快照并同步到资产表 - 数量: %d", len(items)) + + # 步骤 1: 保存到快照表 + logger.debug("步骤 1: 保存到漏洞快照表") + self.snapshot_repo.save_snapshots(items) + + # 步骤 2: 转换为资产 DTO 并保存到资产表 + logger.debug("步骤 2: 同步到漏洞资产表") + asset_items = [item.to_asset_dto() for item in items] + self.asset_service.bulk_create_ignore_conflicts(asset_items) + + logger.info("漏洞快照和资产数据保存成功 - 数量: %d", len(items)) + + # 步骤 3: 发布漏洞保存信号(通知等模块可监听) + from apps.common.signals import vulnerabilities_saved + vulnerabilities_saved.send( + sender=self.__class__, + items=items, + scan_id=scan_id, + target_id=items[0].target_id if items else None + ) + + except Exception as e: + logger.error( + "保存漏洞快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True, + ) + raise + + def get_by_scan(self, scan_id: int): + """按扫描任务获取所有漏洞快照。""" + return self.snapshot_repo.get_by_scan(scan_id) + + def get_all(self): + """获取所有漏洞快照""" + return self.snapshot_repo.get_all() + + def iter_vuln_urls_by_scan(self, scan_id: int, chunk_size: int = 1000) -> Iterator[str]: + """流式获取某次扫描下的所有漏洞 URL。""" + queryset = self.snapshot_repo.get_by_scan(scan_id) + for snapshot in queryset.iterator(chunk_size=chunk_size): + yield snapshot.url diff --git a/backend/apps/asset/services/snapshot/website_snapshots_service.py b/backend/apps/asset/services/snapshot/website_snapshots_service.py new file mode 100644 index 00000000..19a18fa6 --- /dev/null +++ b/backend/apps/asset/services/snapshot/website_snapshots_service.py @@ -0,0 +1,83 @@ +"""Website Snapshots Service - 业务逻辑层""" + +import logging +from typing import List, Iterator + +from apps.asset.repositories.snapshot import DjangoWebsiteSnapshotRepository +from apps.asset.services.asset import WebSiteService +from apps.asset.dtos.snapshot import WebsiteSnapshotDTO + +logger = logging.getLogger(__name__) + + +class WebsiteSnapshotsService: + """网站快照服务 - 统一管理快照和资产同步""" + + def __init__(self): + self.snapshot_repo = DjangoWebsiteSnapshotRepository() + self.asset_service = WebSiteService() + + def save_and_sync(self, items: List[WebsiteSnapshotDTO]) -> None: + """ + 保存网站快照并同步到资产表(统一入口) + + 流程: + 1. 保存到快照表(完整记录,包含 scan_id) + 2. 同步到资产表(去重,不包含 scan_id) + + Args: + items: 网站快照 DTO 列表(必须包含 target_id) + + Raises: + ValueError: 如果 items 中的 target_id 为 None + Exception: 数据库操作失败 + """ + if not items: + return + + # 检查 Scan 是否仍存在(防止删除后竞态写入) + scan_id = items[0].scan_id + from apps.scan.repositories import DjangoScanRepository + if not DjangoScanRepository().exists(scan_id): + logger.warning("Scan 已删除,跳过网站快照保存 - scan_id=%s, 数量=%d", scan_id, len(items)) + return + + try: + logger.debug("保存网站快照并同步到资产表 - 数量: %d", len(items)) + + # 步骤 1: 保存到快照表 + logger.debug("步骤 1: 保存到快照表") + self.snapshot_repo.save_snapshots(items) + + # 步骤 2: 转换为资产 DTO 并保存到资产表 + # 注意:去重是通过数据库的 UNIQUE 约束 + ignore_conflicts 实现的 + # - 新记录:插入资产表 + # - 已存在的记录:自动跳过 + logger.debug("步骤 2: 同步到资产表(通过 Service 层)") + asset_items = [item.to_asset_dto() for item in items] + + self.asset_service.bulk_create_ignore_conflicts(asset_items) + + logger.info("网站快照和资产数据保存成功 - 数量: %d", len(items)) + + except Exception as e: + logger.error( + "保存网站快照失败 - 数量: %d, 错误: %s", + len(items), + str(e), + exc_info=True + ) + raise + + def get_by_scan(self, scan_id: int): + return self.snapshot_repo.get_by_scan(scan_id) + + def get_all(self): + """获取所有网站快照""" + return self.snapshot_repo.get_all() + + def iter_website_urls_by_scan(self, scan_id: int, chunk_size: int = 1000) -> Iterator[str]: + """流式获取某次扫描下的所有站点 URL(按发现时间倒序)。""" + queryset = self.snapshot_repo.get_by_scan(scan_id) + for snapshot in queryset.iterator(chunk_size=chunk_size): + yield snapshot.url diff --git a/backend/apps/asset/services/statistics_service.py b/backend/apps/asset/services/statistics_service.py new file mode 100644 index 00000000..df8dcb9f --- /dev/null +++ b/backend/apps/asset/services/statistics_service.py @@ -0,0 +1,162 @@ +"""资产统计 Service""" +import logging +from typing import Optional + +from django.db.models import Count + +from apps.asset.repositories import AssetStatisticsRepository +from apps.asset.models import ( + AssetStatistics, + StatisticsHistory, + Subdomain, + WebSite, + Endpoint, + HostPortMapping, + Vulnerability, +) +from apps.targets.models import Target +from apps.scan.models import Scan + +logger = logging.getLogger(__name__) + + +class AssetStatisticsService: + """ + 资产统计服务 + + 职责: + - 获取统计数据 + - 刷新统计数据(供定时任务调用) + """ + + def __init__(self): + self.repo = AssetStatisticsRepository() + + def get_statistics(self) -> dict: + """ + 获取统计数据 + + Returns: + 统计数据字典 + """ + stats = self.repo.get_statistics() + + if stats is None: + # 如果没有统计数据,返回默认值 + return { + 'total_targets': 0, + 'total_subdomains': 0, + 'total_ips': 0, + 'total_endpoints': 0, + 'total_websites': 0, + 'total_vulns': 0, + 'total_assets': 0, + 'running_scans': Scan.objects.filter(status='running').count(), + 'updated_at': None, + # 变化值 + 'change_targets': 0, + 'change_subdomains': 0, + 'change_ips': 0, + 'change_endpoints': 0, + 'change_websites': 0, + 'change_vulns': 0, + 'change_assets': 0, + 'vuln_by_severity': self._get_vuln_by_severity(), + } + + # 运行中的扫描数量实时查询(数量小,无需缓存) + running_scans = Scan.objects.filter(status='running').count() + + return { + 'total_targets': stats.total_targets, + 'total_subdomains': stats.total_subdomains, + 'total_ips': stats.total_ips, + 'total_endpoints': stats.total_endpoints, + 'total_websites': stats.total_websites, + 'total_vulns': stats.total_vulns, + 'total_assets': stats.total_assets, + 'running_scans': running_scans, + 'updated_at': stats.updated_at, + # 变化值 = 当前值 - 上次值 + 'change_targets': stats.total_targets - stats.prev_targets, + 'change_subdomains': stats.total_subdomains - stats.prev_subdomains, + 'change_ips': stats.total_ips - stats.prev_ips, + 'change_endpoints': stats.total_endpoints - stats.prev_endpoints, + 'change_websites': stats.total_websites - stats.prev_websites, + 'change_vulns': stats.total_vulns - stats.prev_vulns, + 'change_assets': stats.total_assets - stats.prev_assets, + # 漏洞严重程度分布 + 'vuln_by_severity': self._get_vuln_by_severity(), + } + + def _get_vuln_by_severity(self) -> dict: + """获取按严重程度统计的漏洞数量""" + result = Vulnerability.objects.values('severity').annotate(count=Count('id')) + severity_map = {item['severity']: item['count'] for item in result} + return { + 'critical': severity_map.get('critical', 0), + 'high': severity_map.get('high', 0), + 'medium': severity_map.get('medium', 0), + 'low': severity_map.get('low', 0), + 'info': severity_map.get('info', 0), + } + + def refresh_statistics(self) -> AssetStatistics: + """ + 刷新统计数据(执行实际 COUNT 查询) + + 供定时任务调用,不建议在接口中直接调用。 + + Returns: + 更新后的统计数据对象 + """ + logger.info("开始刷新资产统计...") + + # 执行 COUNT 查询 + total_targets = Target.objects.filter(deleted_at__isnull=True).count() + total_subdomains = Subdomain.objects.count() + total_ips = HostPortMapping.objects.values('ip').distinct().count() + total_endpoints = Endpoint.objects.count() + total_websites = WebSite.objects.count() + total_vulns = Vulnerability.objects.count() + + # 更新统计表 + stats = self.repo.update_statistics( + total_targets=total_targets, + total_subdomains=total_subdomains, + total_ips=total_ips, + total_endpoints=total_endpoints, + total_websites=total_websites, + total_vulns=total_vulns, + ) + + # 保存每日快照(用于折线图) + self.repo.save_daily_snapshot(stats) + + logger.info("资产统计刷新完成") + return stats + + def get_statistics_history(self, days: int = 7) -> list[dict]: + """ + 获取历史统计数据(用于折线图) + + Args: + days: 获取最近多少天的数据,默认 7 天 + + Returns: + 历史数据列表,每项包含 date 和各统计字段 + """ + history = self.repo.get_history(days=days) + return [ + { + 'date': h.date.isoformat(), + 'totalTargets': h.total_targets, + 'totalSubdomains': h.total_subdomains, + 'totalIps': h.total_ips, + 'totalEndpoints': h.total_endpoints, + 'totalWebsites': h.total_websites, + 'totalVulns': h.total_vulns, + 'totalAssets': h.total_assets, + } + for h in history + ] diff --git a/backend/apps/asset/urls.py b/backend/apps/asset/urls.py new file mode 100644 index 00000000..b178183f --- /dev/null +++ b/backend/apps/asset/urls.py @@ -0,0 +1,28 @@ +""" +Asset 应用 URL 配置 +""" + +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import ( + SubdomainViewSet, + WebSiteViewSet, + DirectoryViewSet, + VulnerabilityViewSet, + AssetStatisticsViewSet, +) + +# 创建 DRF 路由器 +router = DefaultRouter() + +# 注册 ViewSet +# 注意:IPAddress 模型已被重构为 HostPortMapping,相关路由已移除 +router.register(r'subdomains', SubdomainViewSet, basename='subdomain') +router.register(r'websites', WebSiteViewSet, basename='website') +router.register(r'directories', DirectoryViewSet, basename='directory') +router.register(r'vulnerabilities', VulnerabilityViewSet, basename='vulnerability') +router.register(r'statistics', AssetStatisticsViewSet, basename='asset-statistics') + +urlpatterns = [ + path('assets/', include(router.urls)), +] diff --git a/backend/apps/asset/views.py b/backend/apps/asset/views.py new file mode 100644 index 00000000..8edfddf8 --- /dev/null +++ b/backend/apps/asset/views.py @@ -0,0 +1,562 @@ +import logging +from rest_framework import viewsets, status, filters +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.request import Request +from rest_framework.exceptions import NotFound, ValidationError as DRFValidationError +from django.core.exceptions import ValidationError, ObjectDoesNotExist +from django.db import DatabaseError, IntegrityError, OperationalError +from django.http import StreamingHttpResponse + +from .serializers import ( + SubdomainListSerializer, WebSiteSerializer, DirectorySerializer, + VulnerabilitySerializer, EndpointListSerializer, IPAddressAggregatedSerializer, + SubdomainSnapshotSerializer, WebsiteSnapshotSerializer, DirectorySnapshotSerializer, + EndpointSnapshotSerializer, VulnerabilitySnapshotSerializer +) +from .services import ( + SubdomainService, WebSiteService, DirectoryService, + VulnerabilityService, AssetStatisticsService, EndpointService, HostPortMappingService +) +from .services.snapshot import ( + SubdomainSnapshotsService, WebsiteSnapshotsService, DirectorySnapshotsService, + EndpointSnapshotsService, HostPortMappingSnapshotsService, VulnerabilitySnapshotsService +) +from apps.common.pagination import BasePagination + +logger = logging.getLogger(__name__) + + +class AssetStatisticsViewSet(viewsets.ViewSet): + """ + 资产统计 API + + 提供仪表盘所需的统计数据(预聚合,读取缓存表) + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = AssetStatisticsService() + + def list(self, request): + """ + 获取资产统计数据 + + GET /assets/statistics/ + + 返回: + - totalTargets: 目标总数 + - totalSubdomains: 子域名总数 + - totalIps: IP 总数 + - totalEndpoints: 端点总数 + - totalWebsites: 网站总数 + - totalVulns: 漏洞总数 + - totalAssets: 总资产数 + - runningScans: 运行中的扫描数 + - updatedAt: 统计更新时间 + """ + try: + stats = self.service.get_statistics() + return Response({ + 'totalTargets': stats['total_targets'], + 'totalSubdomains': stats['total_subdomains'], + 'totalIps': stats['total_ips'], + 'totalEndpoints': stats['total_endpoints'], + 'totalWebsites': stats['total_websites'], + 'totalVulns': stats['total_vulns'], + 'totalAssets': stats['total_assets'], + 'runningScans': stats['running_scans'], + 'updatedAt': stats['updated_at'], + # 变化值 + 'changeTargets': stats['change_targets'], + 'changeSubdomains': stats['change_subdomains'], + 'changeIps': stats['change_ips'], + 'changeEndpoints': stats['change_endpoints'], + 'changeWebsites': stats['change_websites'], + 'changeVulns': stats['change_vulns'], + 'changeAssets': stats['change_assets'], + # 漏洞严重程度分布 + 'vulnBySeverity': stats['vuln_by_severity'], + }) + except (DatabaseError, OperationalError) as e: + logger.exception("获取资产统计数据失败") + return Response( + {'error': '获取统计数据失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + @action(detail=False, methods=['get'], url_path='history') + def history(self, request: Request): + """ + 获取统计历史数据(用于折线图) + + GET /assets/statistics/history/?days=7 + + Query Parameters: + days: 获取最近多少天的数据,默认 7,最大 90 + + Returns: + 历史数据列表 + """ + try: + days_param = request.query_params.get('days', '7') + try: + days = int(days_param) + except (ValueError, TypeError): + days = 7 + days = min(max(days, 1), 90) # 限制在 1-90 天 + + history = self.service.get_statistics_history(days=days) + return Response(history) + except (DatabaseError, OperationalError) as e: + logger.exception("获取统计历史数据失败") + return Response( + {'error': '获取历史数据失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +# 注意:IPAddress 模型已被重构为 HostPortMapping +# IPAddressViewSet 已删除,需要根据新架构重新实现 + + +class SubdomainViewSet(viewsets.ModelViewSet): + """子域名管理 ViewSet + + 支持两种访问方式: + 1. 嵌套路由:GET /api/targets/{target_pk}/subdomains/ + 2. 独立路由:GET /api/subdomains/(全局查询) + """ + + serializer_class = SubdomainListSerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['name'] + ordering = ['-discovered_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = SubdomainService() + + def get_queryset(self): + """根据是否有 target_pk 参数决定查询范围""" + target_pk = self.kwargs.get('target_pk') + if target_pk: + return self.service.get_subdomains_by_target(target_pk) + return self.service.get_all() + + @action(detail=False, methods=['get'], url_path='export') + def export(self, request): + """导出子域名(纯文本,一行一个)""" + target_pk = self.kwargs.get('target_pk') + if not target_pk: + raise DRFValidationError('必须在目标下导出') + + def line_iterator(): + for name in self.service.iter_subdomain_names_by_target(target_pk): + yield f"{name}\n" + + response = StreamingHttpResponse( + line_iterator(), + content_type='text/plain; charset=utf-8', + ) + response['Content-Disposition'] = f'attachment; filename="target-{target_pk}-subdomains.txt"' + return response + + +class WebSiteViewSet(viewsets.ModelViewSet): + """站点管理 ViewSet + + 支持两种访问方式: + 1. 嵌套路由:GET /api/targets/{target_pk}/websites/ + 2. 独立路由:GET /api/websites/(全局查询) + """ + + serializer_class = WebSiteSerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['host'] + ordering = ['-discovered_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = WebSiteService() + + def get_queryset(self): + """根据是否有 target_pk 参数决定查询范围""" + target_pk = self.kwargs.get('target_pk') + if target_pk: + return self.service.get_websites_by_target(target_pk) + return self.service.get_all() + + @action(detail=False, methods=['get'], url_path='export') + def export(self, request): + """导出站点 URL(纯文本,一行一个)""" + target_pk = self.kwargs.get('target_pk') + if not target_pk: + raise DRFValidationError('必须在目标下导出') + + def line_iterator(): + for url in self.service.iter_website_urls_by_target(target_pk): + yield f"{url}\n" + + response = StreamingHttpResponse( + line_iterator(), + content_type='text/plain; charset=utf-8', + ) + response['Content-Disposition'] = f'attachment; filename="target-{target_pk}-websites.txt"' + return response + + +class DirectoryViewSet(viewsets.ModelViewSet): + """目录管理 ViewSet + + 支持两种访问方式: + 1. 嵌套路由:GET /api/targets/{target_pk}/directories/ + 2. 独立路由:GET /api/directories/(全局查询) + """ + + serializer_class = DirectorySerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['url'] + ordering = ['-discovered_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = DirectoryService() + + def get_queryset(self): + """根据是否有 target_pk 参数决定查询范围""" + target_pk = self.kwargs.get('target_pk') + if target_pk: + return self.service.get_directories_by_target(target_pk) + return self.service.get_all() + + @action(detail=False, methods=['get'], url_path='export') + def export(self, request): + """导出目录 URL(纯文本,一行一个)""" + target_pk = self.kwargs.get('target_pk') + if not target_pk: + raise DRFValidationError('必须在目标下导出') + + def line_iterator(): + for url in self.service.iter_directory_urls_by_target(target_pk): + yield f"{url}\n" + + response = StreamingHttpResponse( + line_iterator(), + content_type='text/plain; charset=utf-8', + ) + response['Content-Disposition'] = f'attachment; filename="target-{target_pk}-directories.txt"' + return response + + +class EndpointViewSet(viewsets.ModelViewSet): + """端点管理 ViewSet + + 支持两种访问方式: + 1. 嵌套路由:GET /api/targets/{target_pk}/endpoints/ + 2. 独立路由:GET /api/endpoints/(全局查询) + """ + + serializer_class = EndpointListSerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['host'] + ordering = ['-discovered_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = EndpointService() + + def get_queryset(self): + """根据是否有 target_pk 参数决定查询范围""" + target_pk = self.kwargs.get('target_pk') + if target_pk: + return self.service.get_queryset_by_target(target_pk) + return self.service.get_all() + + @action(detail=False, methods=['get'], url_path='export') + def export(self, request): + """导出端点 URL(纯文本,一行一个)""" + target_pk = self.kwargs.get('target_pk') + if not target_pk: + raise DRFValidationError('必须在目标下导出') + + def line_iterator(): + for url in self.service.iter_endpoint_urls_by_target(target_pk): + yield f"{url}\n" + + response = StreamingHttpResponse( + line_iterator(), + content_type='text/plain; charset=utf-8', + ) + response['Content-Disposition'] = f'attachment; filename="target-{target_pk}-endpoints.txt"' + return response + + +class HostPortMappingViewSet(viewsets.ModelViewSet): + """主机端口映射管理 ViewSet(IP 地址聚合视图) + + 支持两种访问方式: + 1. 嵌套路由:GET /api/targets/{target_pk}/ip-addresses/ + 2. 独立路由:GET /api/ip-addresses/(全局查询) + + 返回按 IP 聚合的数据,每个 IP 显示其关联的所有 hosts 和 ports + + 注意:由于返回的是聚合数据(字典列表),不支持 DRF SearchFilter + """ + + serializer_class = IPAddressAggregatedSerializer + pagination_class = BasePagination + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = HostPortMappingService() + + def get_queryset(self): + """根据是否有 target_pk 参数决定查询范围,返回按 IP 聚合的数据""" + target_pk = self.kwargs.get('target_pk') + search = self.request.query_params.get('search', None) + if target_pk: + return self.service.get_ip_aggregation_by_target(target_pk, search=search) + return self.service.get_all_ip_aggregation(search=search) + + @action(detail=False, methods=['get'], url_path='export') + def export(self, request): + """导出 IP 地址(纯文本,一行一个)""" + target_pk = self.kwargs.get('target_pk') + if not target_pk: + raise DRFValidationError('必须在目标下导出') + + def line_iterator(): + for ip in self.service.iter_ips_by_target(target_pk): + yield f"{ip}\n" + + response = StreamingHttpResponse( + line_iterator(), + content_type='text/plain; charset=utf-8', + ) + response['Content-Disposition'] = f'attachment; filename="target-{target_pk}-ip-addresses.txt"' + return response + + +class VulnerabilityViewSet(viewsets.ModelViewSet): + """漏洞资产管理 ViewSet(只读) + + 支持两种访问方式: + 1. 嵌套路由:GET /api/targets/{target_pk}/vulnerabilities/ + 2. 独立路由:GET /api/vulnerabilities/(全局查询) + """ + + serializer_class = VulnerabilitySerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['vuln_type'] + ordering = ['-discovered_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = VulnerabilityService() + + def get_queryset(self): + """根据是否有 target_pk 参数决定查询范围""" + target_pk = self.kwargs.get('target_pk') + if target_pk: + return self.service.get_queryset_by_target(target_pk) + return self.service.get_all() + + +# ==================== 快照 ViewSet(Scan 嵌套路由) ==================== + +class SubdomainSnapshotViewSet(viewsets.ModelViewSet): + """子域名快照 ViewSet - 嵌套路由:GET /api/scans/{scan_pk}/subdomains/""" + + serializer_class = SubdomainSnapshotSerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['name'] + ordering_fields = ['name', 'discovered_at'] + ordering = ['-discovered_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = SubdomainSnapshotsService() + + def get_queryset(self): + scan_pk = self.kwargs.get('scan_pk') + if scan_pk: + return self.service.get_by_scan(scan_pk) + return self.service.get_all() + + @action(detail=False, methods=['get'], url_path='export') + def export(self, request): + scan_pk = self.kwargs.get('scan_pk') + if not scan_pk: + raise DRFValidationError('必须在扫描下导出') + + def line_iterator(): + for name in self.service.iter_subdomain_names_by_scan(scan_pk): + yield f"{name}\n" + + response = StreamingHttpResponse(line_iterator(), content_type='text/plain; charset=utf-8') + response['Content-Disposition'] = f'attachment; filename="scan-{scan_pk}-subdomains.txt"' + return response + + +class WebsiteSnapshotViewSet(viewsets.ModelViewSet): + """网站快照 ViewSet - 嵌套路由:GET /api/scans/{scan_pk}/websites/""" + + serializer_class = WebsiteSnapshotSerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['host'] + ordering = ['-discovered_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = WebsiteSnapshotsService() + + def get_queryset(self): + scan_pk = self.kwargs.get('scan_pk') + if scan_pk: + return self.service.get_by_scan(scan_pk) + return self.service.get_all() + + @action(detail=False, methods=['get'], url_path='export') + def export(self, request): + scan_pk = self.kwargs.get('scan_pk') + if not scan_pk: + raise DRFValidationError('必须在扫描下导出') + + def line_iterator(): + for url in self.service.iter_website_urls_by_scan(scan_pk): + yield f"{url}\n" + + response = StreamingHttpResponse(line_iterator(), content_type='text/plain; charset=utf-8') + response['Content-Disposition'] = f'attachment; filename="scan-{scan_pk}-websites.txt"' + return response + + +class DirectorySnapshotViewSet(viewsets.ModelViewSet): + """目录快照 ViewSet - 嵌套路由:GET /api/scans/{scan_pk}/directories/""" + + serializer_class = DirectorySnapshotSerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['url'] + ordering = ['-discovered_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = DirectorySnapshotsService() + + def get_queryset(self): + scan_pk = self.kwargs.get('scan_pk') + if scan_pk: + return self.service.get_by_scan(scan_pk) + return self.service.get_all() + + @action(detail=False, methods=['get'], url_path='export') + def export(self, request): + scan_pk = self.kwargs.get('scan_pk') + if not scan_pk: + raise DRFValidationError('必须在扫描下导出') + + def line_iterator(): + for url in self.service.iter_directory_urls_by_scan(scan_pk): + yield f"{url}\n" + + response = StreamingHttpResponse(line_iterator(), content_type='text/plain; charset=utf-8') + response['Content-Disposition'] = f'attachment; filename="scan-{scan_pk}-directories.txt"' + return response + + +class EndpointSnapshotViewSet(viewsets.ModelViewSet): + """端点快照 ViewSet - 嵌套路由:GET /api/scans/{scan_pk}/endpoints/""" + + serializer_class = EndpointSnapshotSerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['host'] + ordering = ['-discovered_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = EndpointSnapshotsService() + + def get_queryset(self): + scan_pk = self.kwargs.get('scan_pk') + if scan_pk: + return self.service.get_by_scan(scan_pk) + return self.service.get_all() + + @action(detail=False, methods=['get'], url_path='export') + def export(self, request): + scan_pk = self.kwargs.get('scan_pk') + if not scan_pk: + raise DRFValidationError('必须在扫描下导出') + + def line_iterator(): + for url in self.service.iter_endpoint_urls_by_scan(scan_pk): + yield f"{url}\n" + + response = StreamingHttpResponse(line_iterator(), content_type='text/plain; charset=utf-8') + response['Content-Disposition'] = f'attachment; filename="scan-{scan_pk}-endpoints.txt"' + return response + + +class HostPortMappingSnapshotViewSet(viewsets.ModelViewSet): + """主机端口映射快照 ViewSet - 嵌套路由:GET /api/scans/{scan_pk}/ip-addresses/ + + 注意:由于返回的是聚合数据(字典列表),不支持 DRF SearchFilter + """ + + serializer_class = IPAddressAggregatedSerializer + pagination_class = BasePagination + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = HostPortMappingSnapshotsService() + + def get_queryset(self): + scan_pk = self.kwargs.get('scan_pk') + search = self.request.query_params.get('search', None) + if scan_pk: + return self.service.get_ip_aggregation_by_scan(scan_pk, search=search) + return self.service.get_all_ip_aggregation(search=search) + + @action(detail=False, methods=['get'], url_path='export') + def export(self, request): + scan_pk = self.kwargs.get('scan_pk') + if not scan_pk: + raise DRFValidationError('必须在扫描下导出') + + def line_iterator(): + for ip in self.service.iter_ips_by_scan(scan_pk): + yield f"{ip}\n" + + response = StreamingHttpResponse(line_iterator(), content_type='text/plain; charset=utf-8') + response['Content-Disposition'] = f'attachment; filename="scan-{scan_pk}-ip-addresses.txt"' + return response + + +class VulnerabilitySnapshotViewSet(viewsets.ModelViewSet): + """漏洞快照 ViewSet - 嵌套路由:GET /api/scans/{scan_pk}/vulnerabilities/""" + + serializer_class = VulnerabilitySnapshotSerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['vuln_type'] + ordering = ['-discovered_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = VulnerabilitySnapshotsService() + + def get_queryset(self): + scan_pk = self.kwargs.get('scan_pk') + if scan_pk: + return self.service.get_by_scan(scan_pk) + return self.service.get_all() diff --git a/backend/apps/common/__init__.py b/backend/apps/common/__init__.py new file mode 100644 index 00000000..01996fee --- /dev/null +++ b/backend/apps/common/__init__.py @@ -0,0 +1,23 @@ +""" +通用工具模块 + +提供各种共享的工具类和函数 +""" + +from .normalizer import normalize_domain, normalize_ip, normalize_cidr, normalize_target +from .validators import validate_domain, validate_ip, validate_cidr, detect_target_type + +__all__ = [ + # 规范化工具 + 'normalize_domain', + 'normalize_ip', + 'normalize_cidr', + 'normalize_target', + + # 验证器 + 'validate_domain', + 'validate_ip', + 'validate_cidr', + 'detect_target_type', +] + diff --git a/backend/apps/common/apps.py b/backend/apps/common/apps.py new file mode 100644 index 00000000..0ff006a7 --- /dev/null +++ b/backend/apps/common/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class CommonConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.common' # 因为在 apps/ 目录下 + + def ready(self): + """应用就绪时调用""" + pass diff --git a/backend/apps/common/authentication.py b/backend/apps/common/authentication.py new file mode 100644 index 00000000..1f1c1a03 --- /dev/null +++ b/backend/apps/common/authentication.py @@ -0,0 +1,13 @@ +from rest_framework.authentication import SessionAuthentication + + +class CsrfExemptSessionAuthentication(SessionAuthentication): + """ + 前后端分离项目使用的 Session 认证 + 禁用 CSRF 检查,因为 CSRF 主要防护的是同源页面表单提交 + 前后端分离项目通过 CORS 控制跨域访问,不需要 CSRF + """ + + def enforce_csrf(self, request): + # 不执行 CSRF 检查 + return diff --git a/backend/apps/common/container_bootstrap.py b/backend/apps/common/container_bootstrap.py new file mode 100644 index 00000000..27adab29 --- /dev/null +++ b/backend/apps/common/container_bootstrap.py @@ -0,0 +1,66 @@ +""" +容器启动引导模块 + +提供动态任务容器的通用初始化功能: +- 从 Server 配置中心获取配置 +- 设置环境变量 +- 初始化 Django 环境 + +使用方式: + from apps.common.container_bootstrap import fetch_config_and_setup_django + fetch_config_and_setup_django() # 必须在 Django 导入之前调用 +""" +import os +import sys +import requests +import logging + +logger = logging.getLogger(__name__) + + +def fetch_config_and_setup_django(): + """ + 从配置中心获取配置并初始化 Django + + Note: + 必须在 Django 导入之前调用此函数 + """ + server_url = os.environ.get("SERVER_URL") + if not server_url: + print("[ERROR] 缺少 SERVER_URL 环境变量", file=sys.stderr) + sys.exit(1) + + config_url = f"{server_url}/api/workers/config/" + try: + resp = requests.get(config_url, timeout=10) + resp.raise_for_status() + config = resp.json() + + # 数据库配置(必需) + os.environ.setdefault("DB_HOST", config['db']['host']) + os.environ.setdefault("DB_PORT", config['db']['port']) + os.environ.setdefault("DB_NAME", config['db']['name']) + os.environ.setdefault("DB_USER", config['db']['user']) + os.environ.setdefault("DB_PASSWORD", config['db']['password']) + + # Redis 配置 + os.environ.setdefault("REDIS_URL", config['redisUrl']) + + # 日志配置 + os.environ.setdefault("LOG_DIR", config['paths']['logs']) + os.environ.setdefault("LOG_LEVEL", config['logging']['level']) + os.environ.setdefault("ENABLE_COMMAND_LOGGING", str(config['logging']['enableCommandLogging']).lower()) + os.environ.setdefault("DEBUG", str(config['debug'])) + + print(f"[CONFIG] 从配置中心获取配置成功: {config_url}") + + except Exception as e: + print(f"[ERROR] 获取配置失败: {config_url} - {e}", file=sys.stderr) + sys.exit(1) + + # 初始化 Django + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + import django + django.setup() + + return config diff --git a/backend/apps/common/decorators/__init__.py b/backend/apps/common/decorators/__init__.py new file mode 100644 index 00000000..bb5e16eb --- /dev/null +++ b/backend/apps/common/decorators/__init__.py @@ -0,0 +1,19 @@ +""" +通用装饰器模块 + +提供可在整个项目中复用的装饰器 +""" + +from .db_connection import ( + ensure_db_connection, + auto_ensure_db_connection, + async_check_and_reconnect, + ensure_db_connection_async, +) + +__all__ = [ + 'ensure_db_connection', + 'auto_ensure_db_connection', + 'async_check_and_reconnect', + 'ensure_db_connection_async', +] diff --git a/backend/apps/common/decorators/db_connection.py b/backend/apps/common/decorators/db_connection.py new file mode 100644 index 00000000..04964887 --- /dev/null +++ b/backend/apps/common/decorators/db_connection.py @@ -0,0 +1,169 @@ +""" +数据库连接装饰器 + +提供自动数据库连接健康检查的装饰器,确保长时间运行的任务中数据库连接不会失效。 + +主要功能: +- @auto_ensure_db_connection: 类装饰器,自动为所有公共方法添加连接检查 +- @ensure_db_connection: 方法装饰器,单独为某个方法添加连接检查 + +使用场景: +- Repository 层的数据库操作 +- Service 层需要确保数据库连接的操作 +- 任何需要数据库连接健康检查的类或方法 +""" + +import logging +import functools +import time +from django.db import connection +from asgiref.sync import sync_to_async + +logger = logging.getLogger(__name__) + + +def ensure_db_connection(method): + """ + 方法装饰器:自动确保数据库连接健康 + + 在方法执行前自动检查数据库连接,如果连接失效则自动重连。 + + 使用场景: + - 需要单独装饰某个方法时使用 + - 通常建议使用类装饰器 @auto_ensure_db_connection + + 示例: + @ensure_db_connection + def my_method(self): + # 会自动检查连接健康 + ... + """ + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + _check_and_reconnect() + return method(self, *args, **kwargs) + return wrapper + + +def auto_ensure_db_connection(cls): + """ + 类装饰器:自动给所有公共方法添加数据库连接检查 + + 自动为类中所有公共方法(不以 _ 开头的方法)添加 @ensure_db_connection 装饰器。 + + 特性: + - 自动装饰所有公共方法 + - 跳过私有方法(以 _ 开头) + - 跳过类方法和静态方法 + - 跳过已经装饰过的方法 + + 使用方式: + @auto_ensure_db_connection + class MyRepository: + def bulk_create(self, items): + # 自动添加连接检查 + ... + + def query(self, filters): + # 自动添加连接检查 + ... + + def _private_method(self): + # 不会添加装饰器 + ... + + 优势: + - 无需为每个方法手动添加装饰器 + - 减少代码重复 + - 降低遗漏风险 + """ + for attr_name in dir(cls): + # 跳过私有方法和魔术方法 + if attr_name.startswith('_'): + continue + + attr = getattr(cls, attr_name) + + # 只装饰可调用的实例方法 + if callable(attr) and not isinstance(attr, (staticmethod, classmethod)): + # 检查是否已经被装饰过(避免重复装饰) + if not hasattr(attr, '_db_connection_ensured'): + wrapped = ensure_db_connection(attr) + wrapped._db_connection_ensured = True + setattr(cls, attr_name, wrapped) + + return cls + + +def _check_and_reconnect(max_retries=5): + """ + 检查数据库连接健康状态,必要时使用指数退避重新连接 + + 策略: + 1. 尝试执行简单查询测试连接 + 2. 如果失败,使用指数退避策略重试(最多5次) + 3. 每次重试的等待时间:2^attempt 秒 (1s, 2s, 4s, 8s, 16s) + + 异常处理: + - 连接失效时自动重连 + - 记录警告日志和重试信息 + - 忽略关闭连接时的错误 + - 达到最大重试次数后抛出异常 + """ + last_error = None + + for attempt in range(max_retries): + try: + connection.ensure_connection() + with connection.cursor() as cursor: + cursor.execute("SELECT 1") + cursor.fetchone() + + # 连接成功 + if attempt > 0: + logger.info(f"数据库重连成功 (尝试 {attempt + 1}/{max_retries})") + return + + except Exception as e: + last_error = e + logger.warning( + f"数据库连接检查失败 (尝试 {attempt + 1}/{max_retries}): {e}" + ) + + # 关闭失效的连接 + try: + connection.close() + except Exception: + pass # 忽略关闭时的错误 + + # 如果还有重试机会,使用指数退避等待 + if attempt < max_retries - 1: + delay = 2 ** attempt # 指数退避: 1, 2, 4, 8, 16 秒 + logger.info(f"等待 {delay} 秒后重试...") + time.sleep(delay) + else: + # 最后一次尝试也失败,抛出异常 + logger.error( + f"数据库重连失败,已达最大重试次数 ({max_retries})" + ) + raise last_error + + +async def async_check_and_reconnect(max_retries=5): + await sync_to_async(_check_and_reconnect, thread_sensitive=True)(max_retries=max_retries) + + +def ensure_db_connection_async(method): + @functools.wraps(method) + async def wrapper(*args, **kwargs): + await async_check_and_reconnect() + return await method(*args, **kwargs) + return wrapper + + +__all__ = [ + 'ensure_db_connection', + 'auto_ensure_db_connection', + 'async_check_and_reconnect', + 'ensure_db_connection_async', +] diff --git a/backend/apps/common/definitions.py b/backend/apps/common/definitions.py new file mode 100644 index 00000000..d80cbb4e --- /dev/null +++ b/backend/apps/common/definitions.py @@ -0,0 +1,20 @@ +from django.db import models + + +class ScanStatus(models.TextChoices): + """扫描任务状态枚举""" + CANCELLED = 'cancelled', '已取消' + COMPLETED = 'completed', '已完成' + FAILED = 'failed', '失败' + INITIATED = 'initiated', '初始化' + RUNNING = 'running', '运行中' + + +class VulnSeverity(models.TextChoices): + """漏洞严重性枚举""" + UNKNOWN = 'unknown', '未知' + INFO = 'info', '信息' + LOW = 'low', '低' + MEDIUM = 'medium', '中' + HIGH = 'high', '高' + CRITICAL = 'critical', '危急' diff --git a/backend/apps/common/hash_utils.py b/backend/apps/common/hash_utils.py new file mode 100644 index 00000000..6e898a9a --- /dev/null +++ b/backend/apps/common/hash_utils.py @@ -0,0 +1,101 @@ +"""通用文件 hash 计算与校验工具 + +提供 SHA-256 哈希计算和校验功能,用于: +- 字典文件上传时计算 hash +- Worker 侧本地缓存校验 +""" + +import hashlib +import logging +from pathlib import Path +from typing import Optional, BinaryIO + +logger = logging.getLogger(__name__) + +# 默认分块大小:64KB(兼顾内存和性能) +DEFAULT_CHUNK_SIZE = 65536 + + +def calc_file_sha256(file_path: str, chunk_size: int = DEFAULT_CHUNK_SIZE) -> str: + """计算文件的 SHA-256 哈希值 + + Args: + file_path: 文件绝对路径 + chunk_size: 分块读取大小(字节),默认 64KB + + Returns: + str: SHA-256 十六进制字符串(64 字符) + + Raises: + FileNotFoundError: 文件不存在 + OSError: 文件读取失败 + """ + hasher = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + hasher.update(chunk) + return hasher.hexdigest() + + +def calc_stream_sha256(stream: BinaryIO, chunk_size: int = DEFAULT_CHUNK_SIZE) -> str: + """从二进制流计算 SHA-256(用于边写边算) + + Args: + stream: 可读取的二进制流(如 UploadedFile.chunks()) + chunk_size: 分块大小 + + Returns: + str: SHA-256 十六进制字符串 + """ + hasher = hashlib.sha256() + for chunk in iter(lambda: stream.read(chunk_size), b""): + hasher.update(chunk) + return hasher.hexdigest() + + +def safe_calc_file_sha256(file_path: str) -> Optional[str]: + """安全计算文件 SHA-256(异常时返回 None) + + Args: + file_path: 文件绝对路径 + + Returns: + str | None: SHA-256 十六进制字符串,或 None(文件不存在/读取失败) + """ + try: + return calc_file_sha256(file_path) + except FileNotFoundError: + logger.warning("计算 hash 失败:文件不存在 - %s", file_path) + return None + except OSError as exc: + logger.warning("计算 hash 失败:读取错误 - %s: %s", file_path, exc) + return None + + +def is_file_hash_match(file_path: str, expected_hash: str) -> bool: + """校验文件 hash 是否与期望值匹配 + + Args: + file_path: 文件绝对路径 + expected_hash: 期望的 SHA-256 十六进制字符串 + + Returns: + bool: True 表示匹配,False 表示不匹配或计算失败 + """ + if not expected_hash: + # 期望值为空,视为"无法校验",返回 False 让调用方决定是否重新下载 + return False + + actual_hash = safe_calc_file_sha256(file_path) + if actual_hash is None: + return False + + return actual_hash.lower() == expected_hash.lower() + + +__all__ = [ + "calc_file_sha256", + "calc_stream_sha256", + "safe_calc_file_sha256", + "is_file_hash_match", +] diff --git a/backend/apps/common/management/commands/db_health_check.py b/backend/apps/common/management/commands/db_health_check.py new file mode 100644 index 00000000..752853bd --- /dev/null +++ b/backend/apps/common/management/commands/db_health_check.py @@ -0,0 +1,429 @@ +""" +数据库健康检查管理命令 + +使用方法: +python manage.py db_health_check # 基础延迟测试(5次) +python manage.py db_health_check --test-count=10 # 指定测试次数 +python manage.py db_health_check --reconnect # 强制重连后测试 +python manage.py db_health_check --stats # 显示连接统计信息 +python manage.py db_health_check --api-test # 测试实际API查询性能 +python manage.py db_health_check --monitor # 监控数据库服务器性能指标 +python manage.py db_health_check --db=other # 指定数据库别名 +python manage.py db_health_check --api-test --test-count=10 # API性能测试10次 +python manage.py db_health_check --reconnect --api-test # 重连后进行API测试 + +示例: +# 快速延迟检查 +python manage.py db_health_check --test-count=3 + +# 完整性能分析 +python manage.py db_health_check --api-test --stats --test-count=5 + +# 数据库服务器监控 +python manage.py db_health_check --monitor +""" + +import time +import logging +from django.core.management.base import BaseCommand +from django.db import connection, connections +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + """Django管理命令:数据库健康检查""" + + help = '检查数据库连接健康状态' + + def add_arguments(self, parser): + parser.add_argument( + '--reconnect', + action='store_true', + help='强制重新连接数据库', + ) + parser.add_argument( + '--stats', + action='store_true', + help='显示连接统计信息', + ) + parser.add_argument( + '--db', + type=str, + default='default', + help='指定数据库别名(默认: default)', + ) + parser.add_argument( + '--test-count', + type=int, + default=5, + help='延迟测试次数(默认: 5)', + ) + parser.add_argument( + '--api-test', + action='store_true', + help='测试实际API查询性能', + ) + parser.add_argument( + '--monitor', + action='store_true', + help='监控数据库服务器性能指标', + ) + + def handle(self, *args, **options): + db_alias = options['db'] + test_count = options['test_count'] + + self.stdout.write(f"正在测试数据库 '{db_alias}' 连接...") + + # 获取数据库连接 + db_connection = connections[db_alias] + + if options['reconnect']: + self.stdout.write("强制重新连接数据库...") + try: + db_connection.close() + db_connection.ensure_connection() + self.stdout.write(self.style.SUCCESS("✓ 重连成功")) + except Exception as e: + self.stdout.write(self.style.ERROR(f"✗ 重连失败: {e}")) + return + + # 测试数据库延迟 + if options['monitor']: + self.monitor_database_performance(db_connection) + elif options['api_test']: + self.test_api_performance(test_count) + else: + self.test_database_latency(db_connection, test_count) + + if options['stats']: + self.show_connection_stats(db_connection) + + def test_database_latency(self, db_connection, test_count): + """测试数据库延迟""" + self.stdout.write(f"\n开始延迟测试({test_count} 次)...") + + latencies = [] + successful_tests = 0 + connection_times = [] + query_times = [] + + for i in range(test_count): + try: + # 测试连接建立时间 + conn_start = time.time() + db_connection.ensure_connection() + conn_end = time.time() + conn_time = (conn_end - conn_start) * 1000 + connection_times.append(conn_time) + + # 测试查询执行时间 + query_start = time.time() + with db_connection.cursor() as cursor: + cursor.execute("SELECT 1") + result = cursor.fetchone() + query_end = time.time() + query_time = (query_end - query_start) * 1000 + query_times.append(query_time) + + total_time = conn_time + query_time + latencies.append(total_time) + successful_tests += 1 + + self.stdout.write(f" 测试 {i+1}: 总计{total_time:.2f}ms (连接:{conn_time:.2f}ms + 查询:{query_time:.2f}ms) ✓") + + except Exception as e: + self.stdout.write(f" 测试 {i+1}: 失败 - {e}") + + # 计算统计信息 + if latencies: + avg_latency = sum(latencies) / len(latencies) + min_latency = min(latencies) + max_latency = max(latencies) + + avg_conn_time = sum(connection_times) / len(connection_times) + avg_query_time = sum(query_times) / len(query_times) + + self.stdout.write(f"\n延迟统计:") + self.stdout.write(f" 成功测试: {successful_tests}/{test_count}") + self.stdout.write(f" 平均总延迟: {avg_latency:.2f}ms") + self.stdout.write(f" 平均连接时间: {avg_conn_time:.2f}ms") + self.stdout.write(f" 平均查询时间: {avg_query_time:.2f}ms") + self.stdout.write(f" 最小延迟: {min_latency:.2f}ms") + self.stdout.write(f" 最大延迟: {max_latency:.2f}ms") + + # 分析延迟来源 + if avg_conn_time > avg_query_time * 2: + self.stdout.write(self.style.WARNING(" 分析: 连接建立是主要延迟来源")) + elif avg_query_time > avg_conn_time * 2: + self.stdout.write(self.style.WARNING(" 分析: 查询执行是主要延迟来源")) + else: + self.stdout.write(" 分析: 连接和查询延迟相当") + + # 延迟评估 + if avg_latency < 10: + self.stdout.write(self.style.SUCCESS(" 评估: 延迟很低,连接优秀")) + elif avg_latency < 50: + self.stdout.write(self.style.SUCCESS(" 评估: 延迟较低,连接良好")) + elif avg_latency < 200: + self.stdout.write(self.style.WARNING(" 评估: 延迟中等,连接可接受")) + else: + self.stdout.write(self.style.ERROR(" 评估: 延迟较高,可能影响性能")) + else: + self.stdout.write(self.style.ERROR("所有测试都失败了")) + + def test_api_performance(self, test_count): + """测试实际API查询性能""" + self.stdout.write(f"\n开始API性能测试({test_count} 次)...") + + # 导入必要的模块 + from apps.scan.models import Scan + from apps.engine.models import ScanEngine + from apps.targets.models import Target + from django.db.models import Count + + api_latencies = [] + successful_tests = 0 + + for i in range(test_count): + try: + start_time = time.time() + + # 测试多种查询类型 + + # 1. 简单查询 - 引擎列表 + engines = list(ScanEngine.objects.all()[:10]) + + # 2. 复杂查询 - 扫描列表(即使没有数据也会执行复杂的JOIN) + scan_queryset = Scan.objects.select_related( + 'target', 'engine' + ).annotate( + subdomains_count=Count('subdomains', distinct=True), + endpoints_count=Count('endpoints', distinct=True), + ips_count=Count('ip_addresses', distinct=True) + ).order_by('-id')[:10] + scan_list = list(scan_queryset) + + # 3. 目标查询 + targets = list(Target.objects.all()[:10]) + + end_time = time.time() + latency_ms = (end_time - start_time) * 1000 + api_latencies.append(latency_ms) + successful_tests += 1 + + self.stdout.write(f" API测试 {i+1}: {latency_ms:.2f}ms ✓ (引擎:{len(engines)}, 扫描:{len(scan_list)}, 目标:{len(targets)})") + + except Exception as e: + self.stdout.write(f" API测试 {i+1}: 失败 - {e}") + + # 计算API查询统计信息 + if api_latencies: + avg_latency = sum(api_latencies) / len(api_latencies) + min_latency = min(api_latencies) + max_latency = max(api_latencies) + + self.stdout.write(f"\nAPI查询统计:") + self.stdout.write(f" 成功测试: {successful_tests}/{test_count}") + self.stdout.write(f" 平均延迟: {avg_latency:.2f}ms") + self.stdout.write(f" 最小延迟: {min_latency:.2f}ms") + self.stdout.write(f" 最大延迟: {max_latency:.2f}ms") + + # 与简单查询对比 + simple_query_avg = 150 # 基于之前的测试结果 + overhead = avg_latency - simple_query_avg + self.stdout.write(f" 业务逻辑开销: {overhead:.2f}ms") + + # 性能评估 + if avg_latency < 500: + self.stdout.write(self.style.SUCCESS(" 评估: API响应速度良好")) + elif avg_latency < 1000: + self.stdout.write(self.style.WARNING(" 评估: API响应速度一般")) + else: + self.stdout.write(self.style.ERROR(" 评估: API响应速度较慢,需要优化")) + else: + self.stdout.write(self.style.ERROR("所有API测试都失败了")) + + def monitor_database_performance(self, db_connection): + """监控数据库服务器性能指标""" + self.stdout.write(f"\n开始监控数据库性能指标...") + + try: + with db_connection.cursor() as cursor: + # 1. 数据库基本信息 + self.stdout.write(f"\n=== 数据库基本信息 ===") + cursor.execute("SELECT version();") + version = cursor.fetchone()[0] + self.stdout.write(f"PostgreSQL版本: {version}") + + cursor.execute("SELECT current_database();") + db_name = cursor.fetchone()[0] + self.stdout.write(f"当前数据库: {db_name}") + + # 2. 连接信息 + self.stdout.write(f"\n=== 连接状态 ===") + cursor.execute(""" + SELECT count(*) as total_connections, + count(*) FILTER (WHERE state = 'active') as active_connections, + count(*) FILTER (WHERE state = 'idle') as idle_connections + FROM pg_stat_activity; + """) + conn_stats = cursor.fetchone() + self.stdout.write(f"总连接数: {conn_stats[0]}") + self.stdout.write(f"活跃连接: {conn_stats[1]}") + self.stdout.write(f"空闲连接: {conn_stats[2]}") + + # 3. 数据库大小 + self.stdout.write(f"\n=== 数据库大小 ===") + cursor.execute(""" + SELECT pg_size_pretty(pg_database_size(current_database())) as db_size; + """) + db_size = cursor.fetchone()[0] + self.stdout.write(f"数据库大小: {db_size}") + + # 4. 表统计信息 + self.stdout.write(f"\n=== 主要表统计 ===") + cursor.execute(""" + SELECT schemaname, relname, + n_tup_ins as inserts, + n_tup_upd as updates, + n_tup_del as deletes, + n_live_tup as live_rows, + n_dead_tup as dead_rows + FROM pg_stat_user_tables + WHERE schemaname = 'public' + ORDER BY n_live_tup DESC + LIMIT 10; + """) + tables = cursor.fetchall() + if tables: + for table in tables: + self.stdout.write(f" {table[1]}: {table[5]} 行 (插入:{table[2]}, 更新:{table[3]}, 删除:{table[4]}, 死行:{table[6]})") + else: + self.stdout.write(" 暂无表统计数据") + + # 5. 慢查询统计 + self.stdout.write(f"\n=== 查询性能统计 ===") + cursor.execute(""" + SELECT query, + calls, + total_time, + mean_time, + rows + FROM pg_stat_statements + WHERE query NOT LIKE '%pg_stat_statements%' + ORDER BY mean_time DESC + LIMIT 5; + """) + try: + slow_queries = cursor.fetchall() + if slow_queries: + for i, query in enumerate(slow_queries, 1): + self.stdout.write(f" {i}. 平均耗时: {query[3]:.2f}ms, 调用次数: {query[1]}") + self.stdout.write(f" 查询: {query[0][:100]}...") + else: + self.stdout.write(" 未找到查询统计(可能未启用pg_stat_statements扩展)") + except Exception: + self.stdout.write(" 查询统计不可用(需要pg_stat_statements扩展)") + + # 6. 锁信息 + self.stdout.write(f"\n=== 锁状态 ===") + cursor.execute(""" + SELECT mode, count(*) + FROM pg_locks + GROUP BY mode + ORDER BY count(*) DESC; + """) + locks = cursor.fetchall() + total_locks = sum(lock[1] for lock in locks) + self.stdout.write(f"总锁数量: {total_locks}") + for lock in locks: + self.stdout.write(f" {lock[0]}: {lock[1]} 个") + + # 7. 缓存命中率 + self.stdout.write(f"\n=== 缓存性能 ===") + cursor.execute(""" + SELECT + sum(heap_blks_read) as heap_read, + sum(heap_blks_hit) as heap_hit, + sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) * 100 as cache_hit_ratio + FROM pg_statio_user_tables; + """) + cache_stats = cursor.fetchone() + if cache_stats[0] and cache_stats[1]: + self.stdout.write(f"缓存命中率: {cache_stats[2]:.2f}%") + self.stdout.write(f"磁盘读取: {cache_stats[0]} 块") + self.stdout.write(f"缓存命中: {cache_stats[1]} 块") + else: + self.stdout.write("缓存统计: 暂无数据") + + # 8. 检查点和WAL + self.stdout.write(f"\n=== WAL和检查点 ===") + cursor.execute(""" + SELECT + checkpoints_timed, + checkpoints_req, + checkpoint_write_time, + checkpoint_sync_time + FROM pg_stat_bgwriter; + """) + bgwriter = cursor.fetchone() + self.stdout.write(f"定时检查点: {bgwriter[0]}") + self.stdout.write(f"请求检查点: {bgwriter[1]}") + self.stdout.write(f"检查点写入时间: {bgwriter[2]}ms") + self.stdout.write(f"检查点同步时间: {bgwriter[3]}ms") + + # 9. 当前活跃查询 + self.stdout.write(f"\n=== 当前活跃查询 ===") + cursor.execute(""" + SELECT pid, + application_name, + state, + query_start, + now() - query_start as duration, + left(query, 100) as query_preview + FROM pg_stat_activity + WHERE state = 'active' + AND query NOT LIKE '%pg_stat_activity%' + ORDER BY query_start; + """) + active_queries = cursor.fetchall() + if active_queries: + for query in active_queries: + self.stdout.write(f" PID {query[0]} ({query[1]}): 运行 {query[4]}") + self.stdout.write(f" 查询: {query[5]}...") + else: + self.stdout.write(" 当前无活跃查询") + + except Exception as e: + self.stdout.write(self.style.ERROR(f"监控失败: {e}")) + + def show_connection_stats(self, db_connection): + """显示连接统计信息""" + self.stdout.write(f"\n连接信息:") + + # 基本连接信息 + settings_dict = db_connection.settings_dict + self.stdout.write(f" 数据库类型: {db_connection.vendor}") + self.stdout.write(f" 主机: {settings_dict.get('HOST', 'localhost')}") + self.stdout.write(f" 端口: {settings_dict.get('PORT', '5432')}") + self.stdout.write(f" 数据库名: {settings_dict.get('NAME', '')}") + self.stdout.write(f" 用户: {settings_dict.get('USER', '')}") + + # 连接配置 + conn_max_age = settings_dict.get('CONN_MAX_AGE', 0) + self.stdout.write(f" 连接最大存活时间: {conn_max_age}秒") + + # 查询统计 + if hasattr(db_connection, 'queries'): + query_count = len(db_connection.queries) + if query_count > 0: + total_time = sum(float(q['time']) for q in db_connection.queries) + self.stdout.write(f" 查询次数: {query_count}") + self.stdout.write(f" 总查询时间: {total_time:.4f}秒") + + # 连接状态 + is_connected = hasattr(db_connection, 'connection') and db_connection.connection is not None + self.stdout.write(f" 连接状态: {'已连接' if is_connected else '未连接'}") diff --git a/backend/apps/common/management/commands/db_monitor.py b/backend/apps/common/management/commands/db_monitor.py new file mode 100644 index 00000000..3e05f9a7 --- /dev/null +++ b/backend/apps/common/management/commands/db_monitor.py @@ -0,0 +1,164 @@ +""" +简化的数据库性能监控命令 + +专注于可能导致查询延迟的关键指标 +""" + +import time +from django.core.management.base import BaseCommand +from django.db import connections + + +class Command(BaseCommand): + """简化的数据库性能监控""" + + help = '监控数据库性能关键指标' + + def add_arguments(self, parser): + parser.add_argument( + '--interval', + type=int, + default=5, + help='监控间隔(秒,默认: 5)', + ) + parser.add_argument( + '--count', + type=int, + default=3, + help='监控次数(默认: 3)', + ) + + def handle(self, *args, **options): + interval = options['interval'] + count = options['count'] + + self.stdout.write("🔍 数据库性能监控开始...") + + for i in range(count): + if i > 0: + time.sleep(interval) + + self.stdout.write(f"\n=== 第 {i+1} 次监控 ===") + self.monitor_key_metrics() + + def monitor_key_metrics(self): + """监控关键性能指标""" + db_connection = connections['default'] + + try: + with db_connection.cursor() as cursor: + # 1. 连接和活动状态 + cursor.execute(""" + SELECT + count(*) as total_connections, + count(*) FILTER (WHERE state = 'active') as active, + count(*) FILTER (WHERE state = 'idle') as idle, + count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_trans, + count(*) FILTER (WHERE wait_event_type IS NOT NULL) as waiting + FROM pg_stat_activity; + """) + conn_stats = cursor.fetchone() + self.stdout.write(f"连接: 总计{conn_stats[0]} | 活跃{conn_stats[1]} | 空闲{conn_stats[2]} | 事务中{conn_stats[3]} | 等待{conn_stats[4]}") + + # 2. 锁等待情况 + cursor.execute(""" + SELECT + count(*) as total_locks, + count(*) FILTER (WHERE NOT granted) as waiting_locks + FROM pg_locks; + """) + lock_stats = cursor.fetchone() + if lock_stats[1] > 0: + self.stdout.write(self.style.WARNING(f"🔒 锁: 总计{lock_stats[0]} | 等待{lock_stats[1]}")) + else: + self.stdout.write(f"🔒 锁: 总计{lock_stats[0]} | 等待{lock_stats[1]}") + + # 3. 长时间运行的查询 + cursor.execute(""" + SELECT + pid, + application_name, + now() - query_start as duration, + state, + left(query, 60) as query_preview + FROM pg_stat_activity + WHERE state = 'active' + AND query_start < now() - interval '1 second' + AND query NOT LIKE '%pg_stat_activity%' + ORDER BY query_start; + """) + long_queries = cursor.fetchall() + if long_queries: + self.stdout.write(self.style.WARNING(f"⏱️ 长查询 ({len(long_queries)} 个):")) + for query in long_queries: + self.stdout.write(f" PID {query[0]} ({query[1]}): {query[2]} - {query[4]}...") + else: + self.stdout.write("⏱️ 长查询: 无") + + # 4. 缓存命中率 + cursor.execute(""" + SELECT + sum(heap_blks_hit) as cache_hits, + sum(heap_blks_read) as disk_reads, + CASE + WHEN sum(heap_blks_hit) + sum(heap_blks_read) = 0 THEN 0 + ELSE round(sum(heap_blks_hit) * 100.0 / (sum(heap_blks_hit) + sum(heap_blks_read)), 2) + END as hit_ratio + FROM pg_statio_user_tables; + """) + cache_stats = cursor.fetchone() + if cache_stats[0] or cache_stats[1]: + hit_ratio = cache_stats[2] or 0 + if hit_ratio < 95: + self.stdout.write(self.style.WARNING(f"💾 缓存命中率: {hit_ratio}% (缓存:{cache_stats[0]}, 磁盘:{cache_stats[1]})")) + else: + self.stdout.write(f"💾 缓存命中率: {hit_ratio}% (缓存:{cache_stats[0]}, 磁盘:{cache_stats[1]})") + else: + self.stdout.write("💾 缓存: 暂无统计数据") + + # 5. 检查点活动(尝试获取,如果失败则跳过) + try: + cursor.execute("SELECT * FROM pg_stat_bgwriter LIMIT 1;") + bgwriter_cols = [desc[0] for desc in cursor.description] + + if 'checkpoints_timed' in bgwriter_cols: + cursor.execute(""" + SELECT + checkpoints_timed, + checkpoints_req, + checkpoint_write_time, + checkpoint_sync_time + FROM pg_stat_bgwriter; + """) + bgwriter = cursor.fetchone() + total_checkpoints = bgwriter[0] + bgwriter[1] + if bgwriter[2] > 10000 or bgwriter[3] > 5000: + self.stdout.write(self.style.WARNING(f"📝 检查点: 总计{total_checkpoints} | 写入{bgwriter[2]}ms | 同步{bgwriter[3]}ms")) + else: + self.stdout.write(f"📝 检查点: 总计{total_checkpoints} | 写入{bgwriter[2]}ms | 同步{bgwriter[3]}ms") + else: + self.stdout.write("📝 检查点: 统计不可用") + except Exception: + self.stdout.write("📝 检查点: 统计不可用") + + # 6. 数据库大小变化 + cursor.execute("SELECT pg_database_size(current_database());") + db_size = cursor.fetchone()[0] + db_size_mb = round(db_size / 1024 / 1024, 2) + self.stdout.write(f"💿 数据库大小: {db_size_mb} MB") + + # 7. 测试查询延迟 + start_time = time.time() + cursor.execute("SELECT 1") + cursor.fetchone() + query_latency = (time.time() - start_time) * 1000 + + if query_latency > 500: + self.stdout.write(self.style.ERROR(f"⚡ 查询延迟: {query_latency:.2f}ms (高)")) + elif query_latency > 200: + self.stdout.write(self.style.WARNING(f"⚡ 查询延迟: {query_latency:.2f}ms (中)")) + else: + self.stdout.write(f"⚡ 查询延迟: {query_latency:.2f}ms (正常)") + + except Exception as e: + self.stdout.write(self.style.ERROR(f"监控失败: {e}")) diff --git a/backend/apps/common/management/commands/init_admin.py b/backend/apps/common/management/commands/init_admin.py new file mode 100644 index 00000000..65013fbb --- /dev/null +++ b/backend/apps/common/management/commands/init_admin.py @@ -0,0 +1,64 @@ +""" +初始化 admin 用户 +用法: python manage.py init_admin [--password <password>] +""" +import os +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model + +User = get_user_model() + +DEFAULT_USERNAME = 'admin' +DEFAULT_PASSWORD = 'admin' # 默认密码,建议首次登录后修改 + + +class Command(BaseCommand): + help = '初始化 admin 用户' + + def add_arguments(self, parser): + parser.add_argument( + '--password', + type=str, + default=os.getenv('ADMIN_PASSWORD', DEFAULT_PASSWORD), + help='admin 用户密码 (默认: admin 或 ADMIN_PASSWORD 环境变量)' + ) + parser.add_argument( + '--force', + action='store_true', + help='强制重置密码(如果用户已存在)' + ) + + def handle(self, *args, **options): + password = options['password'] + force = options['force'] + + try: + user = User.objects.get(username=DEFAULT_USERNAME) + if force: + user.set_password(password) + user.save() + self.stdout.write( + self.style.SUCCESS(f'✓ admin 用户密码已重置') + ) + else: + self.stdout.write( + self.style.WARNING(f'⚠ admin 用户已存在,跳过创建(使用 --force 重置密码)') + ) + except User.DoesNotExist: + User.objects.create_superuser( + username=DEFAULT_USERNAME, + email='admin@localhost', + password=password + ) + self.stdout.write( + self.style.SUCCESS(f'✓ admin 用户创建成功') + ) + self.stdout.write( + self.style.WARNING(f' 用户名: {DEFAULT_USERNAME}') + ) + self.stdout.write( + self.style.WARNING(f' 密码: {password}') + ) + self.stdout.write( + self.style.WARNING(f' ⚠ 请首次登录后修改密码!') + ) diff --git a/backend/apps/common/normalizer.py b/backend/apps/common/normalizer.py new file mode 100644 index 00000000..f14f65de --- /dev/null +++ b/backend/apps/common/normalizer.py @@ -0,0 +1,106 @@ +import re + +# 预编译正则表达式,避免每次调用时重新编译 +IP_PATTERN = re.compile(r'^[\d.:]+$') + + +def normalize_domain(domain: str) -> str: + """ + 规范化域名 + - 去除首尾空格 + - 转换为小写 + - 移除末尾的点 + + Args: + domain: 原始域名 + + Returns: + 规范化后的域名 + + Raises: + ValueError: 域名为空或只包含空格 + """ + if not domain or not domain.strip(): + raise ValueError("域名不能为空") + + normalized = domain.strip().lower() + + # 移除末尾的点 + if normalized.endswith('.'): + normalized = normalized.rstrip('.') + + return normalized + + +def normalize_ip(ip: str) -> str: + """ + 规范化 IP 地址 + - 去除首尾空格 + - IP 地址不转小写(保持原样) + + Args: + ip: 原始 IP 地址 + + Returns: + 规范化后的 IP 地址 + + Raises: + ValueError: IP 地址为空或只包含空格 + """ + if not ip or not ip.strip(): + raise ValueError("IP 地址不能为空") + + return ip.strip() + + +def normalize_cidr(cidr: str) -> str: + """ + 规范化 CIDR + - 去除首尾空格 + - CIDR 不转小写(保持原样) + + Args: + cidr: 原始 CIDR + + Returns: + 规范化后的 CIDR + + Raises: + ValueError: CIDR 为空或只包含空格 + """ + if not cidr or not cidr.strip(): + raise ValueError("CIDR 不能为空") + + return cidr.strip() + + +def normalize_target(target: str) -> str: + """ + 规范化目标名称(统一入口) + 根据目标格式自动选择合适的规范化函数 + + Args: + target: 原始目标名称 + + Returns: + 规范化后的目标名称 + + Raises: + ValueError: 目标为空或只包含空格 + """ + if not target or not target.strip(): + raise ValueError("目标名称不能为空") + + # 先去除首尾空格 + trimmed = target.strip() + + # 如果包含 /,按 CIDR 处理 + if '/' in trimmed: + return normalize_cidr(trimmed) + + # 如果是纯数字、点、冒号组成,按 IP 处理 + if IP_PATTERN.match(trimmed): + return normalize_ip(trimmed) + + # 否则按域名处理 + return normalize_domain(trimmed) diff --git a/backend/apps/common/pagination.py b/backend/apps/common/pagination.py new file mode 100644 index 00000000..e666e244 --- /dev/null +++ b/backend/apps/common/pagination.py @@ -0,0 +1,34 @@ +""" +自定义分页器,匹配前端响应格式 +""" +from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response + + +class BasePagination(PageNumberPagination): + """ + 基础分页器,统一返回格式 + + 响应格式: + { + "results": [...], + "total": 100, + "page": 1, + "pageSize": 10, + "totalPages": 10 + } + """ + page_size = 10 # 默认每页 10 条 + page_size_query_param = 'pageSize' # 允许客户端自定义每页数量 + max_page_size = 1000 # 最大每页数量限制 + + def get_paginated_response(self, data): + """自定义响应格式""" + return Response({ + 'results': data, # 数据列表 + 'total': self.page.paginator.count, # 总记录数 + 'page': self.page.number, # 当前页码(从 1 开始) + 'page_size': self.page.paginator.per_page, # 实际使用的每页大小 + 'total_pages': self.page.paginator.num_pages # 总页数 + }) + diff --git a/backend/apps/common/prefect_django_setup.py b/backend/apps/common/prefect_django_setup.py new file mode 100644 index 00000000..ba485d31 --- /dev/null +++ b/backend/apps/common/prefect_django_setup.py @@ -0,0 +1,42 @@ +""" +Prefect Flow Django 环境初始化模块 + +在所有 Prefect Flow 文件开头导入此模块即可自动配置 Django 环境 +""" + +import os +import sys + + +def setup_django_for_prefect(): + """ + 为 Prefect Flow 配置 Django 环境 + + 此函数会: + 1. 添加项目根目录到 Python 路径 + 2. 设置 DJANGO_SETTINGS_MODULE 环境变量 + 3. 调用 django.setup() 初始化 Django + + 使用方式: + from apps.common.prefect_django_setup import setup_django_for_prefect + setup_django_for_prefect() + """ + # 获取项目根目录(backend 目录) + current_dir = os.path.dirname(os.path.abspath(__file__)) + backend_dir = os.path.join(current_dir, '../..') + backend_dir = os.path.abspath(backend_dir) + + # 添加到 Python 路径 + if backend_dir not in sys.path: + sys.path.insert(0, backend_dir) + + # 配置 Django + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + + # 初始化 Django + import django + django.setup() + + +# 自动执行初始化(导入即生效) +setup_django_for_prefect() diff --git a/backend/apps/common/signals.py b/backend/apps/common/signals.py new file mode 100644 index 00000000..f8be0abd --- /dev/null +++ b/backend/apps/common/signals.py @@ -0,0 +1,29 @@ +"""通用信号定义 + +定义项目中使用的自定义信号,用于解耦各模块之间的通信。 + +使用方式: +1. 发布信号:signal.send(sender=SomeClass, **kwargs) +2. 接收信号:@receiver(signal) def handler(sender, **kwargs): ... +""" + +from django.dispatch import Signal + + +# ==================== 漏洞相关信号 ==================== + +# 漏洞保存完成信号 +# 参数: +# - items: List[VulnerabilitySnapshotDTO] 保存的漏洞列表 +# - scan_id: int 扫描任务ID +# - target_id: int 目标ID +vulnerabilities_saved = Signal() + + +# ==================== Worker 相关信号 ==================== + +# Worker 删除失败信号(只在失败时发送) +# 参数: +# - worker_name: str Worker 名称 +# - message: str 失败原因 +worker_delete_failed = Signal() diff --git a/backend/apps/common/urls.py b/backend/apps/common/urls.py new file mode 100644 index 00000000..65d4cfde --- /dev/null +++ b/backend/apps/common/urls.py @@ -0,0 +1,12 @@ +""" +通用模块 URL 配置 +""" +from django.urls import path +from .views import LoginView, LogoutView, MeView, ChangePasswordView + +urlpatterns = [ + path('auth/login/', LoginView.as_view(), name='auth-login'), + path('auth/logout/', LogoutView.as_view(), name='auth-logout'), + path('auth/me/', MeView.as_view(), name='auth-me'), + path('auth/change-password/', ChangePasswordView.as_view(), name='auth-change-password'), +] diff --git a/backend/apps/common/validators.py b/backend/apps/common/validators.py new file mode 100644 index 00000000..31793525 --- /dev/null +++ b/backend/apps/common/validators.py @@ -0,0 +1,142 @@ +"""域名、IP、端口和目标验证工具函数""" +import ipaddress +import logging +import validators + +logger = logging.getLogger(__name__) + + +def validate_domain(domain: str) -> None: + """ + 验证域名格式(使用 validators 库) + + Args: + domain: 域名字符串(应该已经规范化) + + Raises: + ValueError: 域名格式无效 + """ + if not domain: + raise ValueError("域名不能为空") + + # 使用 validators 库验证域名格式 + # 支持国际化域名(IDN)和各种边界情况 + if not validators.domain(domain): + raise ValueError(f"域名格式无效: {domain}") + + +def validate_ip(ip: str) -> None: + """ + 验证 IP 地址格式(支持 IPv4 和 IPv6) + + Args: + ip: IP 地址字符串(应该已经规范化) + + Raises: + ValueError: IP 地址格式无效 + """ + if not ip: + raise ValueError("IP 地址不能为空") + + try: + ipaddress.ip_address(ip) + except ValueError: + raise ValueError(f"IP 地址格式无效: {ip}") + + +def validate_cidr(cidr: str) -> None: + """ + 验证 CIDR 格式(支持 IPv4 和 IPv6) + + Args: + cidr: CIDR 字符串(应该已经规范化) + + Raises: + ValueError: CIDR 格式无效 + """ + if not cidr: + raise ValueError("CIDR 不能为空") + + try: + ipaddress.ip_network(cidr, strict=False) + except ValueError: + raise ValueError(f"CIDR 格式无效: {cidr}") + + +def detect_target_type(name: str) -> str: + """ + 检测目标类型(不做规范化,只验证) + + Args: + name: 目标名称(应该已经规范化) + + Returns: + str: 目标类型 ('domain', 'ip', 'cidr') - 使用 Target.TargetType 枚举值 + + Raises: + ValueError: 如果无法识别目标类型 + """ + # 在函数内部导入模型,避免 AppRegistryNotReady 错误 + from apps.targets.models import Target + + if not name: + raise ValueError("目标名称不能为空") + + # 检查是否是 CIDR 格式(包含 /) + if '/' in name: + validate_cidr(name) + return Target.TargetType.CIDR + + # 检查是否是 IP 地址 + try: + validate_ip(name) + return Target.TargetType.IP + except ValueError: + pass + + # 检查是否是合法域名 + try: + validate_domain(name) + return Target.TargetType.DOMAIN + except ValueError: + pass + + # 无法识别的格式 + raise ValueError(f"无法识别的目标格式: {name},必须是域名、IP地址或CIDR范围") + + +def validate_port(port: any) -> tuple[bool, int | None]: + """ + 验证并转换端口号 + + Args: + port: 待验证的端口号(可能是字符串、整数或其他类型) + + Returns: + tuple: (is_valid, port_number) + - is_valid: 端口是否有效 + - port_number: 有效时为整数端口号,无效时为 None + + 验证规则: + 1. 必须能转换为整数 + 2. 必须在 1-65535 范围内 + + 示例: + >>> is_valid, port_num = validate_port(8080) + >>> is_valid, port_num + (True, 8080) + + >>> is_valid, port_num = validate_port("invalid") + >>> is_valid, port_num + (False, None) + """ + try: + port_num = int(port) + if 1 <= port_num <= 65535: + return True, port_num + else: + logger.warning("端口号超出有效范围 (1-65535): %d", port_num) + return False, None + except (ValueError, TypeError): + logger.warning("端口号格式错误,无法转换为整数: %s", port) + return False, None diff --git a/backend/apps/common/views/__init__.py b/backend/apps/common/views/__init__.py new file mode 100644 index 00000000..05115be3 --- /dev/null +++ b/backend/apps/common/views/__init__.py @@ -0,0 +1,3 @@ +from .auth_views import LoginView, LogoutView, MeView, ChangePasswordView + +__all__ = ['LoginView', 'LogoutView', 'MeView', 'ChangePasswordView'] diff --git a/backend/apps/common/views/auth_views.py b/backend/apps/common/views/auth_views.py new file mode 100644 index 00000000..4504d653 --- /dev/null +++ b/backend/apps/common/views/auth_views.py @@ -0,0 +1,173 @@ +""" +认证相关视图 +使用 Django 内置认证系统,支持 Session 认证 +""" +import logging +from django.contrib.auth import authenticate, login, logout, update_session_auth_hash +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator +from rest_framework import status +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import AllowAny, IsAuthenticated + +logger = logging.getLogger(__name__) + + +@method_decorator(csrf_exempt, name='dispatch') +class LoginView(APIView): + """ + 用户登录 + POST /api/auth/login/ + """ + authentication_classes = [] # 禁用认证(绕过 CSRF) + permission_classes = [AllowAny] + + def post(self, request): + username = request.data.get('username') + password = request.data.get('password') + + if not username or not password: + return Response( + {'error': '请提供用户名和密码'}, + status=status.HTTP_400_BAD_REQUEST + ) + + user = authenticate(request, username=username, password=password) + + if user is not None: + login(request, user) + logger.info(f"用户 {username} 登录成功") + return Response({ + 'message': '登录成功', + 'user': { + 'id': user.id, + 'username': user.username, + 'isStaff': user.is_staff, + 'isSuperuser': user.is_superuser, + } + }) + else: + logger.warning(f"用户 {username} 登录失败:用户名或密码错误") + return Response( + {'error': '用户名或密码错误'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + +@method_decorator(csrf_exempt, name='dispatch') +class LogoutView(APIView): + """ + 用户登出 + POST /api/auth/logout/ + """ + authentication_classes = [] # 禁用认证(绕过 CSRF) + permission_classes = [AllowAny] + + def post(self, request): + # 从 session 获取用户名用于日志 + user_id = request.session.get('_auth_user_id') + if user_id: + from django.contrib.auth import get_user_model + User = get_user_model() + try: + user = User.objects.get(pk=user_id) + username = user.username + logout(request) + logger.info(f"用户 {username} 已登出") + except User.DoesNotExist: + logout(request) + else: + logout(request) + return Response({'message': '已登出'}) + + +@method_decorator(csrf_exempt, name='dispatch') +class MeView(APIView): + """ + 获取当前用户信息 + GET /api/auth/me/ + """ + authentication_classes = [] # 禁用认证(绕过 CSRF) + permission_classes = [AllowAny] + + def get(self, request): + # 从 session 获取用户 + from django.contrib.auth import get_user_model + User = get_user_model() + + user_id = request.session.get('_auth_user_id') + if user_id: + try: + user = User.objects.get(pk=user_id) + return Response({ + 'authenticated': True, + 'user': { + 'id': user.id, + 'username': user.username, + 'isStaff': user.is_staff, + 'isSuperuser': user.is_superuser, + } + }) + except User.DoesNotExist: + pass + + return Response({ + 'authenticated': False, + 'user': None + }) + + +@method_decorator(csrf_exempt, name='dispatch') +class ChangePasswordView(APIView): + """ + 修改密码 + POST /api/auth/change-password/ + """ + authentication_classes = [] # 禁用认证(绕过 CSRF) + permission_classes = [AllowAny] # 手动检查登录状态 + + def post(self, request): + # 手动检查登录状态(从 session 获取用户) + from django.contrib.auth import get_user_model + User = get_user_model() + + user_id = request.session.get('_auth_user_id') + if not user_id: + return Response( + {'error': '请先登录'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + try: + user = User.objects.get(pk=user_id) + except User.DoesNotExist: + return Response( + {'error': '用户不存在'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # CamelCaseParser 将 oldPassword -> old_password + old_password = request.data.get('old_password') + new_password = request.data.get('new_password') + + if not old_password or not new_password: + return Response( + {'error': '请提供旧密码和新密码'}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not user.check_password(old_password): + return Response( + {'error': '旧密码错误'}, + status=status.HTTP_400_BAD_REQUEST + ) + + user.set_password(new_password) + user.save() + + # 更新 session,避免用户被登出 + update_session_auth_hash(request, user) + + logger.info(f"用户 {user.username} 已修改密码") + return Response({'message': '密码修改成功'}) diff --git a/backend/apps/engine/__init__.py b/backend/apps/engine/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/engine/apps.py b/backend/apps/engine/apps.py new file mode 100644 index 00000000..68d4658f --- /dev/null +++ b/backend/apps/engine/apps.py @@ -0,0 +1,32 @@ +import os +from django.apps import AppConfig + + +class EngineConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.engine' + verbose_name = '扫描引擎' + + def ready(self): + """应用就绪时启动定时调度器""" + # 只在主进程中启动调度器(避免 autoreload 重复启动) + # 检查是否在 runserver 的 autoreload 子进程中 + if os.environ.get('RUN_MAIN') == 'true' or not self._is_runserver(): + # 只在 Server 容器中启动调度器(Worker 容器不需要) + if not os.environ.get('SERVER_URL'): # Worker 容器有 SERVER_URL + self._start_scheduler() + + def _is_runserver(self): + """检查是否通过 runserver 启动""" + import sys + return 'runserver' in sys.argv + + def _start_scheduler(self): + """启动调度器""" + try: + from apps.engine.scheduler import start_scheduler + start_scheduler() + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.warning(f"调度器启动失败: {e}") diff --git a/backend/apps/engine/consumers/__init__.py b/backend/apps/engine/consumers/__init__.py new file mode 100644 index 00000000..46a3d419 --- /dev/null +++ b/backend/apps/engine/consumers/__init__.py @@ -0,0 +1,6 @@ +""" +Engine WebSocket Consumers +""" +from .worker_deploy_consumer import WorkerDeployConsumer + +__all__ = ['WorkerDeployConsumer'] diff --git a/backend/apps/engine/consumers/worker_deploy_consumer.py b/backend/apps/engine/consumers/worker_deploy_consumer.py new file mode 100644 index 00000000..a9cdd17b --- /dev/null +++ b/backend/apps/engine/consumers/worker_deploy_consumer.py @@ -0,0 +1,454 @@ +""" +WebSocket Consumer - Worker 交互式终端 (使用 PTY) +""" + +import json +import logging +import asyncio +import os +from channels.generic.websocket import AsyncWebsocketConsumer +from asgiref.sync import sync_to_async + +from django.conf import settings + +from apps.engine.services import WorkerService + +logger = logging.getLogger(__name__) + + +class WorkerDeployConsumer(AsyncWebsocketConsumer): + """ + Worker 交互式终端 WebSocket Consumer + + 使用 paramiko invoke_shell 实现真正的交互式终端 + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ssh_client = None + self.shell = None + self.worker = None + self.read_task = None + self.worker_service = WorkerService() + + async def connect(self): + """连接时加入对应 Worker 的组并自动建立 SSH 连接""" + self.worker_id = self.scope['url_route']['kwargs']['worker_id'] + self.group_name = f'worker_deploy_{self.worker_id}' + + await self.channel_layer.group_add(self.group_name, self.channel_name) + await self.accept() + + logger.info(f"终端已连接 - Worker: {self.worker_id}") + + # 自动建立 SSH 连接 + await self._auto_ssh_connect() + + async def disconnect(self, close_code): + """断开时清理资源""" + if self.read_task: + self.read_task.cancel() + if self.shell: + try: + self.shell.close() + except Exception: + pass + if self.ssh_client: + try: + self.ssh_client.close() + except Exception: + pass + + await self.channel_layer.group_discard(self.group_name, self.channel_name) + logger.info(f"终端已断开 - Worker: {self.worker_id}") + + async def receive(self, text_data=None, bytes_data=None): + """接收客户端消息""" + if bytes_data: + # 二进制数据直接发送到 shell + if self.shell: + await asyncio.to_thread(self.shell.send, bytes_data) + return + + if not text_data: + return + + try: + data = json.loads(text_data) + msg_type = data.get('type') + + if msg_type == 'resize': + cols = data.get('cols', 80) + rows = data.get('rows', 24) + if self.shell: + await asyncio.to_thread(self.shell.resize_pty, cols, rows) + + elif msg_type == 'input': + # 终端输入 + if self.shell: + text = data.get('data', '') + await asyncio.to_thread(self.shell.send, text) + + elif msg_type == 'deploy': + # 执行部署脚本(后台运行) + await self._run_deploy_script() + + elif msg_type == 'attach': + # 查看部署进度(attach 到 tmux 会话) + await self._attach_deploy_session() + + elif msg_type == 'uninstall': + # 执行卸载脚本(后台运行) + await self._run_uninstall_script() + + except json.JSONDecodeError: + # 可能是普通文本输入 + if self.shell and text_data: + await asyncio.to_thread(self.shell.send, text_data) + except Exception as e: + logger.error(f"处理消息错误: {e}") + + async def _auto_ssh_connect(self): + """自动从数据库读取密码并连接""" + logger.info(f"[SSH] 开始自动连接 - Worker ID: {self.worker_id}") + # 通过服务层获取 Worker 节点 + # thread_sensitive=False 确保在新线程中运行,避免数据库连接问题 + self.worker = await sync_to_async(self.worker_service.get_worker, thread_sensitive=False)(self.worker_id) + logger.info(f"[SSH] Worker 查询结果: {self.worker}") + + if not self.worker: + await self.send(text_data=json.dumps({ + 'type': 'error', + 'message': 'Worker 不存在' + })) + return + + if not self.worker.password: + await self.send(text_data=json.dumps({ + 'type': 'error', + 'message': '未配置 SSH 密码,请先编辑节点信息' + })) + return + + # 使用默认终端大小 + await self._ssh_connect(self.worker.password, 80, 24) + + async def _ssh_connect(self, password: str, cols: int = 80, rows: int = 24): + """建立 SSH 连接并启动交互式 shell (使用 tmux 持久化会话)""" + try: + import paramiko + except ImportError: + await self.send(text_data=json.dumps({ + 'type': 'error', + 'message': '服务器缺少 paramiko 库' + })) + return + + # self.worker 已在 _auto_ssh_connect 中查询 + try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + await asyncio.to_thread( + ssh.connect, + self.worker.ip_address, + port=self.worker.ssh_port, + username=self.worker.username, + password=password, + timeout=30 + ) + + self.ssh_client = ssh + + # 启动交互式 shell(连接时不做 tmux 安装,仅提供普通 shell) + self.shell = await asyncio.to_thread( + ssh.invoke_shell, + term='xterm-256color', + width=cols, + height=rows + ) + + # 发送连接成功消息 + logger.info(f"[SSH] 准备发送 connected 消息 - Worker: {self.worker_id}") + await self.send(text_data=json.dumps({ + 'type': 'connected' + })) + logger.info(f"[SSH] connected 消息已发送 - Worker: {self.worker_id}") + + # 启动读取任务 + self.read_task = asyncio.create_task(self._read_shell_output()) + + logger.info(f"[SSH] Shell 已连接,读取任务已启动 - Worker: {self.worker_id}") + + except paramiko.AuthenticationException: + logger.error(f"[SSH] 认证失败 - Worker: {self.worker_id}") + await self.send(text_data=json.dumps({ + 'type': 'error', + 'message': '认证失败,密码错误' + })) + except Exception as e: + logger.error(f"[SSH] 连接失败 - Worker: {self.worker_id}, Error: {e}") + await self.send(text_data=json.dumps({ + 'type': 'error', + 'message': f'连接失败: {str(e)}' + })) + + async def _read_shell_output(self): + """持续读取 shell 输出并发送到客户端""" + try: + while self.shell and not self.shell.closed: + if self.shell.recv_ready(): + data = await asyncio.to_thread(self.shell.recv, 4096) + if data: + await self.send(bytes_data=data) + else: + await asyncio.sleep(0.01) + except asyncio.CancelledError: + pass + except Exception as e: + logger.error(f"读取 shell 输出错误: {e}") + + async def _run_deploy_script(self): + """运行部署脚本(在 tmux 会话中执行,支持断线续连) + + 流程: + 1. 通过 SFTP 上传脚本到远程服务器 + 2. 使用 exec_command 静默执行(不在交互式终端回显) + 3. 通过 WebSocket 发送结果到前端显示 + """ + if not self.ssh_client: + return + + from apps.engine.services.deploy_service import ( + get_bootstrap_script, + get_deploy_script, + get_start_agent_script + ) + + # 优先使用 settings 中配置的对外访问主机(PUBLIC_HOST)拼接 Django URL + public_host = getattr(settings, 'PUBLIC_HOST', '').strip() + server_port = getattr(settings, 'SERVER_PORT', '8888') + + if not public_host: + error_msg = ( + "未配置 PUBLIC_HOST,请在 docker/.env 中设置对外访问 IP/域名 " + "(PUBLIC_HOST) 并重启服务后再执行远程部署" + ) + logger.error(error_msg) + await self.send(text_data=json.dumps({ + 'type': 'error', + 'message': error_msg, + })) + return + + django_host = f"{public_host}:{server_port}" # Django / 心跳上报使用 + heartbeat_api_url = f"http://{django_host}" # 基础 URL,agent 会加 /api/... + + session_name = f'xingrin_deploy_{self.worker_id}' + remote_script_path = '/tmp/xingrin_deploy.sh' + + # 获取外置脚本内容 + bootstrap_script = get_bootstrap_script() + deploy_script = get_deploy_script() + start_script = get_start_agent_script( + heartbeat_api_url=heartbeat_api_url, + worker_id=self.worker_id + ) + + # 合并脚本 + combined_script = f"""#!/bin/bash +set -e + +# ==================== 阶段 1: 环境初始化 ==================== +{bootstrap_script} + +# ==================== 阶段 2: 安装 Docker ==================== +{deploy_script} + +# ==================== 阶段 3: 启动 Agent ==================== +{start_script} + +echo "SUCCESS" +""" + + # 更新状态为 deploying + await sync_to_async(self.worker_service.update_status)(self.worker_id, 'deploying') + + # 发送开始提示 + start_msg = "\r\n\033[36m[XingRin] 正在准备部署...\033[0m\r\n" + await self.send(bytes_data=start_msg.encode()) + + try: + # 1. 上传脚本 + sftp = await asyncio.to_thread(self.ssh_client.open_sftp) + with sftp.file(remote_script_path, 'w') as f: + f.write(combined_script) + sftp.chmod(remote_script_path, 0o755) + await asyncio.to_thread(sftp.close) + + # 2. 静默执行部署命令(使用 exec_command,不会回显到终端) + deploy_cmd = f""" +# 确保 tmux 安装 +if ! command -v tmux >/dev/null 2>&1; then + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq tmux >/dev/null 2>&1 + fi +fi + +# 检查脚本是否存在 +if [ ! -f "{remote_script_path}" ]; then + echo "SCRIPT_NOT_FOUND" + exit 1 +fi + +# 启动 tmux 会话 +if command -v tmux >/dev/null 2>&1; then + tmux kill-session -t {session_name} 2>/dev/null || true + # 使用 bash 执行脚本,确保环境正确 + tmux new-session -d -s {session_name} "bash {remote_script_path}; echo '部署完成,按回车退出'; read" + # 验证会话是否创建成功 + sleep 0.5 + if tmux has-session -t {session_name} 2>/dev/null; then + echo "SUCCESS" + else + echo "SESSION_CREATE_FAILED" + fi +else + echo "TMUX_NOT_FOUND" +fi +""" + stdin, stdout, stderr = await asyncio.to_thread( + self.ssh_client.exec_command, deploy_cmd + ) + result = await asyncio.to_thread(stdout.read) + result = result.decode().strip() + + # 3. 发送结果到前端终端显示 + if "SUCCESS" in result: + # 部署任务已在后台启动,保持 deploying 状态 + # 只有当心跳上报成功后才会变成 deployed(通过 heartbeat API 自动更新) + success_msg = ( + "\r\n\033[32m✓ 部署任务已在后台启动\033[0m\r\n" + f"\033[90m 会话: {session_name}\033[0m\r\n" + "\r\n" + "\033[36m点击 [查看进度] 按钮查看部署输出\033[0m\r\n" + f"\033[90m或手动执行: tmux attach -t {session_name}\033[0m\r\n" + "\r\n" + ) + else: + # 获取更多错误信息 + err = await asyncio.to_thread(stderr.read) + err_msg = err.decode().strip() if err else "" + success_msg = f"\r\n\033[31m✗ 部署启动失败\033[0m\r\n\033[90m结果: {result}\r\n错误: {err_msg}\033[0m\r\n" + + await self.send(bytes_data=success_msg.encode()) + + except Exception as e: + error_msg = f"\033[31m✗ 部署失败: {str(e)}\033[0m\r\n" + await self.send(bytes_data=error_msg.encode()) + logger.error(f"部署脚本执行失败: {e}") + + async def _run_uninstall_script(self): + """在远程主机上执行 Worker 卸载脚本 + + 逻辑: + 1. 通过服务层读取本地 worker-uninstall.sh 内容 + 2. 上传到远程 /tmp/xingrin_uninstall.sh 并赋予执行权限 + 3. 使用 exec_command 以 bash 执行脚本 + 4. 将执行结果摘要写回前端终端 + """ + if not self.ssh_client: + return + + from apps.engine.services.deploy_service import get_uninstall_script + + uninstall_script = get_uninstall_script() + remote_script_path = '/tmp/xingrin_uninstall.sh' + + start_msg = "\r\n\033[36m[XingRin] 正在执行 Worker 卸载...\033[0m\r\n" + await self.send(bytes_data=start_msg.encode()) + + try: + # 上传卸载脚本 + sftp = await asyncio.to_thread(self.ssh_client.open_sftp) + with sftp.file(remote_script_path, 'w') as f: + f.write(uninstall_script) + sftp.chmod(remote_script_path, 0o755) + await asyncio.to_thread(sftp.close) + + # 执行卸载脚本 + cmd = f"bash {remote_script_path}" + stdin, stdout, stderr = await asyncio.to_thread( + self.ssh_client.exec_command, cmd + ) + out = await asyncio.to_thread(stdout.read) + err = await asyncio.to_thread(stderr.read) + + # 转换换行符为终端格式 (\n -> \r\n) + output_text = out.decode().strip().replace('\n', '\r\n') if out else "" + error_text = err.decode().strip().replace('\n', '\r\n') if err else "" + + # 简单判断是否成功(退出码 + 关键字) + exit_status = stdout.channel.recv_exit_status() + if exit_status == 0: + # 卸载成功,重置状态为 pending + await sync_to_async(self.worker_service.update_status)(self.worker_id, 'pending') + # 删除 Redis 中的心跳数据 + from apps.engine.services.worker_load_service import worker_load_service + worker_load_service.delete_load(self.worker_id) + # 发送状态更新到前端 + await self.send(text_data=json.dumps({ + 'type': 'status', + 'status': 'pending' # 卸载后变为待部署状态 + })) + msg = "\r\n\033[32m✓ 节点卸载完成\033[0m\r\n" + if output_text: + msg += f"\033[90m{output_text}\033[0m\r\n" + else: + msg = "\r\n\033[31m✗ Worker 卸载失败\033[0m\r\n" + if output_text: + msg += f"\033[90m输出: {output_text}\033[0m\r\n" + if error_text: + msg += f"\033[90m错误: {error_text}\033[0m\r\n" + + await self.send(bytes_data=msg.encode()) + + except Exception as e: + error_msg = f"\033[31m✗ 卸载执行异常: {str(e)}\033[0m\r\n" + await self.send(bytes_data=error_msg.encode()) + logger.error(f"卸载脚本执行失败: {e}") + + async def _attach_deploy_session(self): + """Attach 到部署会话查看进度""" + if not self.shell or not self.ssh_client: + return + + session_name = f'xingrin_deploy_{self.worker_id}' + + # 先静默检查会话是否存在 + check_cmd = f"tmux has-session -t {session_name} 2>/dev/null && echo EXISTS || echo NOT_EXISTS" + stdin, stdout, stderr = await asyncio.to_thread( + self.ssh_client.exec_command, check_cmd + ) + result = await asyncio.to_thread(stdout.read) + result = result.decode().strip() + + if "EXISTS" in result: + # 会话存在,直接 attach + await asyncio.to_thread(self.shell.send, f"tmux attach -t {session_name}\n") + else: + # 会话不存在,发送提示 + msg = "\r\n\033[33m没有正在运行的部署任务\033[0m\r\n\033[90m请先点击 [执行部署] 按钮启动部署\033[0m\r\n\r\n" + await self.send(bytes_data=msg.encode()) + + # Channel Layer 消息处理 + async def terminal_output(self, event): + if self.shell: + await asyncio.to_thread(self.shell.send, event['content']) + + async def deploy_status(self, event): + await self.send(text_data=json.dumps({ + 'type': 'status', + 'status': event['status'], + 'message': event.get('message', '') + })) diff --git a/backend/apps/engine/management/__init__.py b/backend/apps/engine/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/engine/management/commands/__init__.py b/backend/apps/engine/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/engine/management/commands/init_default_engine.py b/backend/apps/engine/management/commands/init_default_engine.py new file mode 100644 index 00000000..08e70c7b --- /dev/null +++ b/backend/apps/engine/management/commands/init_default_engine.py @@ -0,0 +1,112 @@ +""" +初始化默认扫描引擎 + +用法: + python manage.py init_default_engine # 只创建不存在的引擎(不覆盖已有) + python manage.py init_default_engine --force # 强制覆盖所有引擎配置 + + cd /root/my-vulun-scan/docker + docker compose exec server python backend/manage.py init_default_engine --force + +功能: +- 读取 engine_config_example.yaml 作为默认配置 +- 创建 full scan(默认引擎)+ 各扫描类型的子引擎 +- 默认不覆盖已有配置,加 --force 才会覆盖 +""" + +from django.core.management.base import BaseCommand +from pathlib import Path + +import yaml + +from apps.engine.models import ScanEngine + + +class Command(BaseCommand): + help = '初始化默认扫描引擎配置(默认不覆盖已有,加 --force 强制覆盖)' + + def add_arguments(self, parser): + parser.add_argument( + '--force', + action='store_true', + help='强制覆盖已有的引擎配置', + ) + + def handle(self, *args, **options): + force = options.get('force', False) + # 读取默认配置文件 + config_path = Path(__file__).resolve().parent.parent.parent.parent / 'scan' / 'configs' / 'engine_config_example.yaml' + + if not config_path.exists(): + self.stdout.write(self.style.ERROR(f'配置文件不存在: {config_path}')) + return + + with open(config_path, 'r', encoding='utf-8') as f: + default_config = f.read() + + # 解析 YAML 为字典,后续用于生成子引擎配置 + try: + config_dict = yaml.safe_load(default_config) or {} + except yaml.YAMLError as e: + self.stdout.write(self.style.ERROR(f'引擎配置 YAML 解析失败: {e}')) + return + + # 1) full scan:保留完整配置 + engine = ScanEngine.objects.filter(name='full scan').first() + if engine: + if force: + engine.configuration = default_config + engine.save() + self.stdout.write(self.style.SUCCESS(f'✓ 扫描引擎 full scan 配置已更新 (ID: {engine.id})')) + else: + self.stdout.write(self.style.WARNING(f' ⊘ full scan 已存在,跳过(使用 --force 覆盖)')) + else: + engine = ScanEngine.objects.create( + name='full scan', + configuration=default_config, + ) + self.stdout.write(self.style.SUCCESS(f'✓ 扫描引擎 full scan 已创建 (ID: {engine.id})')) + + # 2) 为每个扫描类型生成一个「单一扫描类型」的子引擎 + # 例如:subdomain_discovery, port_scan, ... + from apps.scan.configs.command_templates import get_supported_scan_types + + supported_scan_types = set(get_supported_scan_types()) + + for scan_type, scan_cfg in config_dict.items(): + # 只处理受支持且结构为 {tools: {...}} 的扫描类型 + if scan_type not in supported_scan_types: + continue + if not isinstance(scan_cfg, dict): + continue + # subdomain_discovery 使用 4 阶段新结构(无 tools 字段),其他扫描类型仍要求有 tools + if scan_type != 'subdomain_discovery' and 'tools' not in scan_cfg: + continue + + # 构造只包含当前扫描类型配置的 YAML + single_config = {scan_type: scan_cfg} + try: + single_yaml = yaml.safe_dump( + single_config, + sort_keys=False, + allow_unicode=True, + ) + except yaml.YAMLError as e: + self.stdout.write(self.style.ERROR(f'生成子引擎 {scan_type} 配置失败: {e}')) + continue + + engine_name = f"{scan_type}" + sub_engine = ScanEngine.objects.filter(name=engine_name).first() + if sub_engine: + if force: + sub_engine.configuration = single_yaml + sub_engine.save() + self.stdout.write(self.style.SUCCESS(f' ✓ 子引擎 {engine_name} 配置已更新 (ID: {sub_engine.id})')) + else: + self.stdout.write(self.style.WARNING(f' ⊘ {engine_name} 已存在,跳过(使用 --force 覆盖)')) + else: + sub_engine = ScanEngine.objects.create( + name=engine_name, + configuration=single_yaml, + ) + self.stdout.write(self.style.SUCCESS(f' ✓ 子引擎 {engine_name} 已创建 (ID: {sub_engine.id})')) diff --git a/backend/apps/engine/management/commands/init_nuclei_templates.py b/backend/apps/engine/management/commands/init_nuclei_templates.py new file mode 100644 index 00000000..53b8ecad --- /dev/null +++ b/backend/apps/engine/management/commands/init_nuclei_templates.py @@ -0,0 +1,126 @@ +"""初始化 Nuclei 模板仓库 + +项目安装后执行此命令,自动创建官方模板仓库记录。 + +使用方式: + python manage.py init_nuclei_templates # 只创建记录 + python manage.py init_nuclei_templates --sync # 创建并同步(git clone) +""" + +import logging +from django.core.management.base import BaseCommand + +from apps.engine.models import NucleiTemplateRepo +from apps.engine.services import NucleiTemplateRepoService + +logger = logging.getLogger(__name__) + + +# 默认仓库配置 +DEFAULT_REPOS = [ + { + "name": "nuclei-templates", + "repo_url": "https://github.com/projectdiscovery/nuclei-templates.git", + "description": "Nuclei 官方模板仓库,包含数千个漏洞检测模板", + }, +] + + +class Command(BaseCommand): + help = "初始化 Nuclei 模板仓库(创建官方模板仓库记录)" + + def add_arguments(self, parser): + parser.add_argument( + "--sync", + action="store_true", + help="创建后立即同步(git clone),首次需要较长时间", + ) + parser.add_argument( + "--force", + action="store_true", + help="强制重新创建(删除已存在的同名仓库)", + ) + + def handle(self, *args, **options): + do_sync = options.get("sync", False) + force = options.get("force", False) + + service = NucleiTemplateRepoService() + created = 0 + skipped = 0 + synced = 0 + + for repo_config in DEFAULT_REPOS: + name = repo_config["name"] + repo_url = repo_config["repo_url"] + + # 检查是否已存在 + existing = NucleiTemplateRepo.objects.filter(name=name).first() + + if existing: + if force: + self.stdout.write(self.style.WARNING( + f"[{name}] 强制模式,删除已存在的仓库记录" + )) + service.remove_local_path_dir(existing) + existing.delete() + else: + self.stdout.write(self.style.SUCCESS( + f"[{name}] 已存在,跳过创建" + )) + skipped += 1 + + # 如果需要同步且已存在,也执行同步 + if do_sync and existing.id: + try: + result = service.refresh_repo(existing.id) + self.stdout.write(self.style.SUCCESS( + f"[{name}] 同步完成: {result.get('action', 'unknown')}, " + f"commit={result.get('commitHash', 'N/A')[:8]}" + )) + synced += 1 + except Exception as e: + self.stdout.write(self.style.ERROR( + f"[{name}] 同步失败: {e}" + )) + continue + + # 创建新仓库记录 + try: + repo = NucleiTemplateRepo.objects.create( + name=name, + repo_url=repo_url, + ) + self.stdout.write(self.style.SUCCESS( + f"[{name}] 创建成功: id={repo.id}" + )) + created += 1 + + # 初始化本地路径 + service.ensure_local_path(repo) + + # 如果需要同步 + if do_sync: + try: + self.stdout.write(self.style.WARNING( + f"[{name}] 正在同步(首次可能需要几分钟)..." + )) + result = service.refresh_repo(repo.id) + self.stdout.write(self.style.SUCCESS( + f"[{name}] 同步完成: {result.get('action', 'unknown')}, " + f"commit={result.get('commitHash', 'N/A')[:8]}" + )) + synced += 1 + except Exception as e: + self.stdout.write(self.style.ERROR( + f"[{name}] 同步失败: {e}" + )) + + except Exception as e: + self.stdout.write(self.style.ERROR( + f"[{name}] 创建失败: {e}" + )) + + self.stdout.write(self.style.SUCCESS( + f"\n初始化完成: 创建 {created}, 跳过 {skipped}, 同步 {synced}" + )) diff --git a/backend/apps/engine/management/commands/init_wordlists.py b/backend/apps/engine/management/commands/init_wordlists.py new file mode 100644 index 00000000..85467ab1 --- /dev/null +++ b/backend/apps/engine/management/commands/init_wordlists.py @@ -0,0 +1,148 @@ +"""初始化所有内置字典 Wordlist 记录 + +- 目录扫描默认字典: dir_default.txt -> /app/backend/wordlist/dir_default.txt +- 子域名爆破默认字典: subdomains-top1million-110000.txt -> /app/backend/wordlist/subdomains-top1million-110000.txt + +可重复执行:如果已存在同名记录且文件有效则跳过,只在缺失或文件丢失时创建/修复。 +""" + +import logging +import shutil +from pathlib import Path + +from django.conf import settings +from django.core.management.base import BaseCommand + +from apps.common.hash_utils import safe_calc_file_sha256 +from apps.engine.models import Wordlist + + +logger = logging.getLogger(__name__) + + +DEFAULT_WORDLISTS = [ + { + "name": "dir_default.txt", + "filename": "dir_default.txt", + "description": "内置默认目录字典", + }, + { + "name": "subdomains-top1million-110000.txt", + "filename": "subdomains-top1million-110000.txt", + "description": "内置默认子域名字典", + }, +] + + +class Command(BaseCommand): + help = "初始化所有内置字典 Wordlist 记录" + + def handle(self, *args, **options): + project_base = Path(settings.BASE_DIR).parent # /app/backend -> /app + base_wordlist_dir = project_base / "backend" / "wordlist" + runtime_base_dir = Path(getattr(settings, "WORDLISTS_BASE_PATH", "/opt/xingrin/wordlists")) + runtime_base_dir.mkdir(parents=True, exist_ok=True) + + initialized = 0 + skipped = 0 + failed = 0 + + for item in DEFAULT_WORDLISTS: + name = item["name"] + filename = item["filename"] + description = item["description"] + + existing = Wordlist.objects.filter(name=name).first() + if existing: + file_path = existing.file_path or "" + file_hash = getattr(existing, 'file_hash', '') or '' + if file_path and Path(file_path).exists() and file_hash: + # 记录、文件、hash 都在,直接跳过 + self.stdout.write(self.style.SUCCESS( + f"[{name}] 已存在且文件有效,跳过初始化 (file_path={file_path})" + )) + skipped += 1 + continue + elif file_path and Path(file_path).exists() and not file_hash: + # 文件在但 hash 缺失,需要补算 + self.stdout.write(self.style.WARNING( + f"[{name}] 记录已存在但缺少 file_hash,将补算并更新" + )) + else: + self.stdout.write(self.style.WARNING( + f"[{name}] 记录已存在但物理文件丢失,将重新创建文件路径并修复记录" + )) + + src_path = base_wordlist_dir / filename + dest_path = runtime_base_dir / filename + + if not src_path.exists(): + self.stdout.write(self.style.WARNING( + f"[{name}] 未找到内置字典文件: {src_path},跳过" + )) + failed += 1 + continue + + try: + shutil.copy2(src_path, dest_path) + except OSError as exc: + self.stdout.write(self.style.WARNING( + f"[{name}] 复制内置字典到运行目录失败: {exc}" + )) + failed += 1 + continue + + # 统计文件大小和行数 + try: + file_size = dest_path.stat().st_size + except OSError: + file_size = 0 + + line_count = 0 + try: + with dest_path.open("rb") as f: + for _ in f: + line_count += 1 + except OSError: + logger.warning("统计字典行数失败: %s", src_path) + + # 计算文件 hash + file_hash = safe_calc_file_sha256(str(dest_path)) or "" + + # 如果之前已有记录则更新,否则创建新记录 + if existing: + existing.file_path = str(dest_path) + existing.file_size = file_size + existing.line_count = line_count + existing.file_hash = file_hash + existing.description = existing.description or description + existing.save(update_fields=[ + "file_path", + "file_size", + "line_count", + "file_hash", + "description", + "updated_at", + ]) + wordlist = existing + action = "更新" + else: + wordlist = Wordlist.objects.create( + name=name, + description=description, + file_path=str(dest_path), + file_size=file_size, + line_count=line_count, + file_hash=file_hash, + ) + action = "创建" + + initialized += 1 + hash_preview = (wordlist.file_hash[:16] + "...") if wordlist.file_hash else "N/A" + self.stdout.write(self.style.SUCCESS( + f"[{name}] {action}字典记录成功: id={wordlist.id}, size={wordlist.file_size}, lines={wordlist.line_count}, hash={hash_preview}" + )) + + self.stdout.write(self.style.SUCCESS( + f"初始化完成: 成功 {initialized}, 已存在跳过 {skipped}, 文件缺失 {failed}" + )) diff --git a/backend/apps/engine/migrations/__init__.py b/backend/apps/engine/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/engine/models.py b/backend/apps/engine/models.py new file mode 100644 index 00000000..dbb3eebf --- /dev/null +++ b/backend/apps/engine/models.py @@ -0,0 +1,126 @@ +from django.db import models + + +class WorkerNode(models.Model): + """Worker 节点模型 - 分布式扫描执行器""" + + # 状态选项(前后端统一) + STATUS_CHOICES = [ + ('pending', '待部署'), + ('deploying', '部署中'), + ('online', '在线'), + ('offline', '离线'), + ] + + name = models.CharField(max_length=100, help_text='节点名称') + # 本地节点会自动填入 127.0.0.1 或容器 IP + ip_address = models.GenericIPAddressField(help_text='IP 地址(本地节点为 127.0.0.1)') + ssh_port = models.IntegerField(default=22, help_text='SSH 端口') + username = models.CharField(max_length=50, default='root', help_text='SSH 用户名') + password = models.CharField(max_length=200, blank=True, default='', help_text='SSH 密码') + + # 本地节点标记(Docker 容器内的 Worker) + is_local = models.BooleanField(default=False, help_text='是否为本地节点(Docker 容器内)') + + # 状态(前后端统一) + status = models.CharField( + max_length=20, + choices=STATUS_CHOICES, + default='pending', + help_text='状态: pending/deploying/online/offline' + ) + + # 心跳数据存储在 Redis(worker:load:{id}),不再使用数据库字段 + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'worker_node' + verbose_name = 'Worker 节点' + ordering = ['-created_at'] + constraints = [ + # 远程节点 IP 唯一(本地节点不限制,因为都是 127.0.0.1) + models.UniqueConstraint( + fields=['ip_address'], + condition=models.Q(is_local=False), + name='unique_remote_worker_ip' + ), + # 名称全局唯一 + models.UniqueConstraint( + fields=['name'], + name='unique_worker_name' + ), + ] + + def __str__(self): + if self.is_local: + return f"{self.name} (本地)" + return f"{self.name} ({self.ip_address or '未知'})" + + +class ScanEngine(models.Model): + """扫描引擎模型""" + + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=200, unique=True, help_text='引擎名称') + configuration = models.CharField(max_length=10000, blank=True, default='', help_text='引擎配置,yaml 格式') + created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间') + updated_at = models.DateTimeField(auto_now=True, help_text='更新时间') + + class Meta: + db_table = 'scan_engine' + verbose_name = '扫描引擎' + verbose_name_plural = '扫描引擎' + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['-created_at']), + ] + def __str__(self): + return str(self.name or f'ScanEngine {self.id}') + + +class Wordlist(models.Model): + """字典文件模型""" + + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=200, unique=True, help_text='字典名称,唯一') + description = models.CharField(max_length=200, blank=True, default='', help_text='字典描述') + file_path = models.CharField(max_length=500, help_text='后端保存的字典文件绝对路径') + file_size = models.BigIntegerField(default=0, help_text='文件大小(字节)') + line_count = models.IntegerField(default=0, help_text='字典行数') + file_hash = models.CharField(max_length=64, blank=True, default='', help_text='文件 SHA-256 哈希,用于缓存校验') + created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间') + updated_at = models.DateTimeField(auto_now=True, help_text='更新时间') + + class Meta: + db_table = 'wordlist' + verbose_name = '字典文件' + verbose_name_plural = '字典文件' + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['-created_at']), + ] + + def __str__(self) -> str: + return self.name + + +class NucleiTemplateRepo(models.Model): + """Nuclei 模板 Git 仓库模型(多仓库)""" + + name = models.CharField(max_length=200, unique=True, help_text="仓库名称,用于前端展示和配置引用") + repo_url = models.CharField(max_length=500, help_text="Git 仓库地址") + local_path = models.CharField(max_length=500, blank=True, default='', help_text="本地工作目录绝对路径") + commit_hash = models.CharField(max_length=40, blank=True, default='', help_text="最后同步的 Git commit hash,用于 Worker 版本校验") + last_synced_at = models.DateTimeField(null=True, blank=True, help_text="最后一次成功同步时间") + created_at = models.DateTimeField(auto_now_add=True, help_text="创建时间") + updated_at = models.DateTimeField(auto_now=True, help_text="更新时间") + + class Meta: + db_table = "nuclei_template_repo" + verbose_name = "Nuclei 模板仓库" + verbose_name_plural = "Nuclei 模板仓库" + + def __str__(self) -> str: # pragma: no cover - 简单表示 + return f"NucleiTemplateRepo({self.id}, {self.name})" diff --git a/backend/apps/engine/repositories/__init__.py b/backend/apps/engine/repositories/__init__.py new file mode 100644 index 00000000..9ef38678 --- /dev/null +++ b/backend/apps/engine/repositories/__init__.py @@ -0,0 +1,17 @@ +"""Engine Repositories 模块 + +提供 ScanEngine、WorkerNode、Wordlist、NucleiRepo 等数据访问层实现 +""" + +from .django_engine_repository import DjangoEngineRepository +from .django_worker_repository import DjangoWorkerRepository +from .django_wordlist_repository import DjangoWordlistRepository +from .nuclei_repo_repository import NucleiTemplateRepository, TemplateFileRepository + +__all__ = [ + "DjangoEngineRepository", + "DjangoWorkerRepository", + "DjangoWordlistRepository", + "NucleiTemplateRepository", + "TemplateFileRepository", +] diff --git a/backend/apps/engine/repositories/django_engine_repository.py b/backend/apps/engine/repositories/django_engine_repository.py new file mode 100644 index 00000000..47720460 --- /dev/null +++ b/backend/apps/engine/repositories/django_engine_repository.py @@ -0,0 +1,40 @@ +""" +ScanEngine 数据访问层 Django ORM 实现 + +基于 Django ORM 的 ScanEngine Repository 实现类 +""" + +import logging + +from ..models import ScanEngine +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoEngineRepository: + """基于 Django ORM 的 ScanEngine 数据访问层实现""" + + def get_all(self): + """获取所有扫描引擎查询集""" + return ScanEngine.objects.all().order_by('-created_at') # type: ignore + + def get_by_id(self, engine_id: int) -> ScanEngine | None: + """ + 根据 ID 获取扫描引擎 + + Args: + engine_id: 引擎 ID + + Returns: + ScanEngine 对象或 None + """ + try: + return ScanEngine.objects.get(id=engine_id) # type: ignore + except ScanEngine.DoesNotExist: # type: ignore + logger.warning("ScanEngine 不存在 - Engine ID: %s", engine_id) + return None + + +__all__ = ['DjangoEngineRepository'] diff --git a/backend/apps/engine/repositories/django_wordlist_repository.py b/backend/apps/engine/repositories/django_wordlist_repository.py new file mode 100644 index 00000000..7031d1e3 --- /dev/null +++ b/backend/apps/engine/repositories/django_wordlist_repository.py @@ -0,0 +1,51 @@ +"""Wordlist 数据访问层 Django ORM 实现 + +基于 Django ORM 的 Wordlist Repository 实现类 +""" + +import logging + +from apps.engine.models import Wordlist +from apps.common.decorators import auto_ensure_db_connection + + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoWordlistRepository: + """基于 Django ORM 的 Wordlist 数据访问层实现""" + + def get_queryset(self): + """获取字典查询集""" + return Wordlist.objects.all().order_by("-created_at") + + def get_by_id(self, wordlist_id: int) -> Wordlist | None: + """根据 ID 获取字典""" + try: + return Wordlist.objects.get(id=wordlist_id) + except Wordlist.DoesNotExist: + logger.warning("Wordlist 不存在 - ID: %s", wordlist_id) + return None + + def get_by_name(self, name: str) -> Wordlist | None: + try: + return Wordlist.objects.get(name=name) + except Wordlist.DoesNotExist: + logger.warning("Wordlist 不存在 - 名称: %s", name) + return None + + def create(self, **kwargs) -> Wordlist: + """创建字典记录""" + return Wordlist.objects.create(**kwargs) + + def delete(self, wordlist_id: int) -> bool: + """删除字典记录""" + wordlist = self.get_by_id(wordlist_id) + if not wordlist: + return False + wordlist.delete() + return True + + +__all__ = ["DjangoWordlistRepository"] diff --git a/backend/apps/engine/repositories/django_worker_repository.py b/backend/apps/engine/repositories/django_worker_repository.py new file mode 100644 index 00000000..56b287f2 --- /dev/null +++ b/backend/apps/engine/repositories/django_worker_repository.py @@ -0,0 +1,99 @@ +""" +WorkerNode 数据访问层 Django ORM 实现 + +基于 Django ORM 的 WorkerNode Repository 实现类 +""" + +import logging +from typing import Any + +from django.utils import timezone + +from apps.engine.models import WorkerNode +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoWorkerRepository: + """基于 Django ORM 的 WorkerNode 数据访问层实现""" + + def get_by_id(self, worker_id: int) -> WorkerNode | None: + """根据 ID 获取 Worker 节点""" + try: + return WorkerNode.objects.get(id=worker_id) + except WorkerNode.DoesNotExist: + logger.warning("WorkerNode 不存在 - ID: %s", worker_id) + return None + + def get_all(self): + """获取所有 Worker 节点的查询集""" + return WorkerNode.objects.all().order_by("-created_at") + + def update_status(self, worker_id: int, status: str) -> bool: + """更新 Worker 节点状态""" + worker = self.get_by_id(worker_id) + if not worker: + return False + + worker.status = status + worker.save(update_fields=["status"]) + logger.info("Worker %s 状态更新为: %s", worker_id, status) + return True + + + def delete_by_id(self, worker_id: int) -> bool: + """根据 ID 删除 Worker 节点""" + worker = self.get_by_id(worker_id) + if not worker: + return False + + worker.delete() + logger.info("Worker %s 已删除", worker_id) + return True + + def get_or_create_by_name( + self, + name: str, + is_local: bool = True + ) -> tuple[WorkerNode, bool]: + """ + 根据名称获取或创建 Worker 节点 + + 用于本地 Worker 自注册。 + + Args: + name: Worker 名称 + is_local: 是否为本地节点 + + Returns: + (worker, created) 元组 + """ + import socket + + # 尝试获取本机 IP + try: + hostname = socket.gethostname() + ip_address = socket.gethostbyname(hostname) + except Exception: + ip_address = '127.0.0.1' + + worker, created = WorkerNode.objects.get_or_create( + name=name, + defaults={ + 'ip_address': ip_address, + 'is_local': is_local, + 'status': 'offline', # 等待心跳上报后自动变为 online + } + ) + + if created: + logger.info("本地 Worker 注册成功: %s (IP: %s)", name, ip_address) + else: + logger.debug("本地 Worker 已存在: %s", name) + + return worker, created + + +__all__ = ["DjangoWorkerRepository"] diff --git a/backend/apps/engine/repositories/nuclei_repo_repository.py b/backend/apps/engine/repositories/nuclei_repo_repository.py new file mode 100644 index 00000000..5038ab97 --- /dev/null +++ b/backend/apps/engine/repositories/nuclei_repo_repository.py @@ -0,0 +1,196 @@ +"""Nuclei 模板仓库 Repository 层 + +本模块包含两个 Repository 类,负责数据访问: + +1. NucleiTemplateRepository + - 职责:ORM 操作,按 ID 查询仓库配置 + - 被 Service 层调用,不直接被 View 调用 + +2. TemplateFileRepository + - 职责:文件系统操作,读取模板目录树和文件内容(只读) + - 按仓库的 local_path 构建,一个仓库对应一个实例 + +调用链路: + View → Service → Repository → Model/FileSystem +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Dict, List, Optional + +from apps.common.decorators import auto_ensure_db_connection +from apps.engine.models import NucleiTemplateRepo + + +@auto_ensure_db_connection +class NucleiTemplateRepository: + """Nuclei 模板仓库 ORM Repository + + 负责与 NucleiTemplateRepo 模型交互,提供按 ID 查询功能。 + CRUD 操作由 DRF ModelViewSet 默认实现,这里只保留 Service 需要的查询方法。 + """ + + def get_by_id(self, repo_id: int) -> Optional[NucleiTemplateRepo]: + """根据 ID 获取仓库对象 + + Args: + repo_id: 仓库 ID + + Returns: + NucleiTemplateRepo 对象,不存在返回 None + """ + try: + return NucleiTemplateRepo.objects.get(id=repo_id) + except NucleiTemplateRepo.DoesNotExist: + return None + + +class TemplateFileRepository: + """模板文件系统 Repository(只读) + + 负责读取指定根目录下的 Nuclei 模板文件。 + 每个仓库克隆到本地后,用这个类来读取目录树和文件内容。 + + Attributes: + root: 仓库本地根目录路径(即 git clone 的目标目录) + """ + + def __init__(self, root: Path) -> None: + """初始化文件系统 Repository + + Args: + root: 仓库本地根目录路径(会自动转换为绝对路径) + """ + # 确保存储的是绝对路径 + self.root = root.resolve() + + def get_tree(self) -> List[Dict]: + """获取模板目录树结构 + + 遍历 root 目录,构建树形结构,只包含: + - 文件夹节点 + - .yaml / .yml 文件节点 + + Returns: + 树形结构列表,格式如下: + [ + { + "type": "folder", + "name": "nuclei-templates", + "path": "", + "children": [ + {"type": "folder", "name": "http", "path": "http", "children": [...]}, + {"type": "file", "name": "example.yaml", "path": "http/example.yaml"} + ] + } + ] + """ + # self.root 在 __init__ 中已确保是绝对路径 + root_dir = self.root + + # 根节点 + root_node: Dict = { + "type": "folder", + "name": root_dir.name or "root", + "path": "", + "children": [], + } + + # 目录不存在时返回空树 + if not root_dir.exists() or not root_dir.is_dir(): + return [root_node] + + # 用于快速查找父节点 + path_to_node: Dict[Path, Dict] = {root_dir: root_node} + + # 遍历目录树 + for dirpath, dirnames, filenames in os.walk(root_dir): + current_dir = Path(dirpath) + parent_node = path_to_node.get(current_dir) + if parent_node is None: + continue + + # 添加子目录节点(按名称排序) + for dirname in sorted(dirnames): + child_fs_path = current_dir / dirname + rel = child_fs_path.relative_to(root_dir) + api_path = rel.as_posix() # 使用 POSIX 风格路径(前端友好) + + node: Dict = { + "type": "folder", + "name": dirname, + "path": api_path, + "children": [], + } + parent_node.setdefault("children", []).append(node) + path_to_node[child_fs_path] = node + + # 添加模板文件节点(仅 .yaml / .yml,按名称排序) + for filename in sorted(filenames): + if not (filename.endswith(".yaml") or filename.endswith(".yml")): + continue + + file_fs_path = current_dir / filename + rel = file_fs_path.relative_to(root_dir) + api_path = rel.as_posix() + + file_node: Dict = { + "type": "file", + "name": filename, + "path": api_path, + } + parent_node.setdefault("children", []).append(file_node) + + return [root_node] + + def get_file_content(self, rel_path: str) -> Optional[Dict]: + """根据相对路径获取模板文件内容 + + Args: + rel_path: 相对于 root 的路径,如 "http/cves/CVE-2021-1234.yaml" + + Returns: + 成功时返回: + { + "path": "http/cves/CVE-2021-1234.yaml", + "name": "CVE-2021-1234.yaml", + "content": "id: CVE-2021-1234\ninfo:\n name: ..." + } + 失败时返回 None(路径无效、文件不存在、读取失败等) + """ + # 清理路径 + rel_path = (rel_path or "").strip().lstrip("/") + if not rel_path: + return None + + # self.root 在 __init__ 中已确保是绝对路径 + base_dir = self.root + # 拼接后 resolve() 确保解析 .. 等相对路径符号(防止目录遍历攻击) + target_path = (base_dir / rel_path).resolve() + + # 防止目录遍历攻击:确保目标路径在 base_dir 内 + try: + target_path.relative_to(base_dir) + except ValueError: + return None + + # 检查文件是否存在 + if not target_path.is_file(): + return None + + # 读取文件内容 + try: + content = target_path.read_text(encoding="utf-8", errors="replace") + except OSError: + return None + + return { + "path": rel_path, + "name": target_path.name, + "content": content, + } + + +__all__ = ["NucleiTemplateRepository", "TemplateFileRepository"] diff --git a/backend/apps/engine/routing.py b/backend/apps/engine/routing.py new file mode 100644 index 00000000..3c78f086 --- /dev/null +++ b/backend/apps/engine/routing.py @@ -0,0 +1,10 @@ +""" +Worker WebSocket 路由配置 +""" + +from django.urls import path +from .consumers import WorkerDeployConsumer + +websocket_urlpatterns = [ + path('ws/workers/<int:worker_id>/deploy/', WorkerDeployConsumer.as_asgi()), +] diff --git a/backend/apps/engine/scheduler.py b/backend/apps/engine/scheduler.py new file mode 100644 index 00000000..48de09df --- /dev/null +++ b/backend/apps/engine/scheduler.py @@ -0,0 +1,133 @@ +""" +APScheduler 定时任务调度器 + +替代 Prefect Work Pool,用于触发定时任务。 +实际任务执行通过 task_distributor 分发到各 Worker。 +""" +import logging +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger +from apscheduler.triggers.interval import IntervalTrigger +from django.conf import settings + +logger = logging.getLogger(__name__) + +# 全局调度器实例 +_scheduler: BackgroundScheduler | None = None + + +def get_scheduler() -> BackgroundScheduler: + """获取调度器实例""" + global _scheduler + if _scheduler is None: + _scheduler = BackgroundScheduler( + timezone=settings.TIME_ZONE, + job_defaults={ + 'coalesce': True, # 合并错过的任务 + 'max_instances': 1, # 同一任务最多同时运行1个实例 + 'misfire_grace_time': 60 * 5, # 错过5分钟内仍然执行 + } + ) + return _scheduler + + +def start_scheduler(): + """启动调度器并注册所有定时任务""" + scheduler = get_scheduler() + + if scheduler.running: + logger.info("调度器已在运行") + return + + # 注册定时任务 + _register_scheduled_jobs(scheduler) + + # 启动调度器 + scheduler.start() + logger.info("✓ APScheduler 定时调度器已启动") + + +def shutdown_scheduler(): + """关闭调度器""" + global _scheduler + if _scheduler and _scheduler.running: + _scheduler.shutdown(wait=False) + logger.info("APScheduler 调度器已关闭") + _scheduler = None + + +def _register_scheduled_jobs(scheduler: BackgroundScheduler): + """注册所有定时任务""" + + # 1. 定时扫描任务(检查并执行到期的定时扫描) + scheduler.add_job( + _trigger_scheduled_scans, + trigger=IntervalTrigger(minutes=1), # 每分钟检查并触发到期任务 + id='scheduled_scans', + name='定时扫描任务', + replace_existing=True, + ) + logger.info(" - 已注册: 定时扫描任务(每分钟)") + + # 2. 资产统计刷新(每小时) + scheduler.add_job( + _trigger_statistics_refresh, + trigger=CronTrigger(minute=0), # 每小时整点 + id='statistics_refresh', + name='资产统计刷新', + replace_existing=True, + ) + logger.info(" - 已注册: 资产统计刷新(每小时)") + + # 3. 扫描结果清理(每天凌晨3点) + scheduler.add_job( + _trigger_cleanup, + trigger=CronTrigger(hour=3, minute=0), + id='scan_cleanup', + name='扫描结果清理', + replace_existing=True, + ) + logger.info(" - 已注册: 扫描结果清理(每天 03:00)") + + +def _trigger_scheduled_scans(): + """触发到期的定时扫描任务""" + try: + from apps.scan.services.scheduled_scan_service import ScheduledScanService + + service = ScheduledScanService() + triggered_count = service.trigger_due_scans() + + if triggered_count > 0: + logger.info(f"定时扫描: 已触发 {triggered_count} 个任务") + + except Exception as e: + logger.error(f"定时扫描任务执行失败: {e}", exc_info=True) + + +def _trigger_statistics_refresh(): + """触发资产统计刷新""" + try: + from apps.asset.services.statistics_service import AssetStatisticsService + + service = AssetStatisticsService() + service.refresh_statistics() + + logger.info("资产统计刷新完成") + + except Exception as e: + logger.error(f"资产统计刷新失败: {e}", exc_info=True) + + +def _trigger_cleanup(): + """触发扫描结果清理(分发到各 Worker)""" + try: + from apps.engine.services.task_distributor import TaskDistributor + + distributor = TaskDistributor() + results = distributor.execute_cleanup_on_all_workers() + + logger.info(f"扫描清理任务已分发到 {len(results)} 个 Worker") + + except Exception as e: + logger.error(f"扫描清理任务分发失败: {e}", exc_info=True) diff --git a/backend/apps/engine/serializers/__init__.py b/backend/apps/engine/serializers/__init__.py new file mode 100644 index 00000000..45a16b9d --- /dev/null +++ b/backend/apps/engine/serializers/__init__.py @@ -0,0 +1,14 @@ +""" +Engine Serializers +""" +from .worker_serializer import WorkerNodeSerializer +from .engine_serializer import ScanEngineSerializer +from .wordlist_serializer import WordlistSerializer +from .nuclei_template_repo_serializer import NucleiTemplateRepoSerializer + +__all__ = [ + "WorkerNodeSerializer", + "ScanEngineSerializer", + "WordlistSerializer", + "NucleiTemplateRepoSerializer", +] diff --git a/backend/apps/engine/serializers/engine_serializer.py b/backend/apps/engine/serializers/engine_serializer.py new file mode 100644 index 00000000..84aac64e --- /dev/null +++ b/backend/apps/engine/serializers/engine_serializer.py @@ -0,0 +1,45 @@ +""" +扫描引擎序列化器 +""" +from rest_framework import serializers +from apps.engine.models import ScanEngine + + +class ScanEngineSerializer(serializers.ModelSerializer): + """扫描引擎序列化器""" + + class Meta: + model = ScanEngine + fields = [ + 'id', + 'name', + 'configuration', + 'created_at', + 'updated_at', + ] + read_only_fields = ['id', 'created_at', 'updated_at'] + + def to_representation(self, instance): + """自定义序列化输出""" + data = super().to_representation(instance) + # 确保 configuration 字段存在且不为 null + if data.get('configuration') is None: + data['configuration'] = '' + return data + + def validate_name(self, value): + """验证引擎名称""" + if not value.strip(): + raise serializers.ValidationError("引擎名称不能为空") + return value.strip() + + def validate_configuration(self, value): + """验证 YAML 配置""" + if value: + # 可以在这里添加 YAML 格式验证 + import yaml + try: + yaml.safe_load(value) + except yaml.YAMLError as e: + raise serializers.ValidationError(f"YAML 格式错误: {str(e)}") + return value diff --git a/backend/apps/engine/serializers/nuclei_template_repo_serializer.py b/backend/apps/engine/serializers/nuclei_template_repo_serializer.py new file mode 100644 index 00000000..494de3e0 --- /dev/null +++ b/backend/apps/engine/serializers/nuclei_template_repo_serializer.py @@ -0,0 +1,50 @@ +"""Nuclei 模板仓库序列化器 + +用于 DRF ModelViewSet 的 CRUD 操作,将 NucleiTemplateRepo 模型序列化为 JSON。 + +字段说明: +- id: 仓库 ID(只读,自动生成) +- name: 仓库名称,用于前端展示 +- repo_url: Git 仓库地址,如 https://github.com/projectdiscovery/nuclei-templates.git +- local_path: 本地克隆路径(只读,由后端自动生成) +- last_synced_at: 最后同步时间(只读) +- created_at: 创建时间(只读) +- updated_at: 更新时间(只读) +""" + +from __future__ import annotations + +from rest_framework import serializers + +from apps.engine.models import NucleiTemplateRepo + + +class NucleiTemplateRepoSerializer(serializers.ModelSerializer): + """Nuclei 模板仓库序列化器 + + 用于仓库的 CRUD API 响应。 + """ + + class Meta: + model = NucleiTemplateRepo + fields = [ + "id", # 仓库 ID(只读) + "name", # 仓库名称 + "repo_url", # Git 仓库地址 + "local_path", # 本地克隆路径(只读) + "commit_hash", # 最后同步的 commit hash(只读) + "last_synced_at", # 最后同步时间(只读) + "created_at", # 创建时间(只读) + "updated_at", # 更新时间(只读) + ] + read_only_fields = [ + "id", + "local_path", # 由后端根据 name 自动生成 + "commit_hash", # 由 refresh 操作更新 + "last_synced_at", # 由 refresh 操作更新 + "created_at", + "updated_at", + ] + + +__all__ = ["NucleiTemplateRepoSerializer"] diff --git a/backend/apps/engine/serializers/wordlist_serializer.py b/backend/apps/engine/serializers/wordlist_serializer.py new file mode 100644 index 00000000..ff43f28a --- /dev/null +++ b/backend/apps/engine/serializers/wordlist_serializer.py @@ -0,0 +1,32 @@ +"""字典文件序列化器""" + +from rest_framework import serializers + +from apps.engine.models import Wordlist + + +class WordlistSerializer(serializers.ModelSerializer): + """字典文件序列化器""" + + class Meta: + model = Wordlist + fields = [ + "id", + "name", + "description", + "file_path", + "file_size", + "line_count", + "file_hash", + "created_at", + "updated_at", + ] + read_only_fields = [ + "id", + "file_path", + "file_size", + "line_count", + "file_hash", + "created_at", + "updated_at", + ] diff --git a/backend/apps/engine/serializers/worker_serializer.py b/backend/apps/engine/serializers/worker_serializer.py new file mode 100644 index 00000000..8639bebe --- /dev/null +++ b/backend/apps/engine/serializers/worker_serializer.py @@ -0,0 +1,90 @@ +""" +Worker 节点序列化器 +""" +from rest_framework import serializers +from apps.engine.models import WorkerNode + + +class WorkerNodeSerializer(serializers.ModelSerializer): + """ + Worker 节点序列化器 + + 优化:通过 context['loads'] 传入批量查询的 Redis 数据,避免 N+1 查询 + """ + + # 密码只写(不返回给前端) + password = serializers.CharField(write_only=True, required=False, allow_blank=True) + + # 状态(数据库存储 + Redis 心跳补充判断) + status = serializers.SerializerMethodField() + + # 负载数据(从 Redis 读取) + info = serializers.SerializerMethodField() + + class Meta: + model = WorkerNode + fields = ['id', 'name', 'ip_address', 'ssh_port', 'username', 'status', + 'is_local', 'info', 'created_at', 'updated_at', 'password'] + read_only_fields = ['id', 'status', 'is_local', 'info', 'created_at', 'updated_at'] + + def _get_load_from_context(self, worker_id: int) -> dict | None: + """从 context 获取预加载的负载数据""" + loads = self.context.get('loads', {}) + return loads.get(worker_id) + + def get_status(self, obj) -> str: + """ + 获取状态(前后端统一): + - pending/deploying: 直接返回数据库值 + - online/offline: 通过 Redis 心跳动态判断 + """ + # pending 和 deploying 直接返回 + if obj.status in ('pending', 'deploying'): + return obj.status + + # online/offline 通过 Redis 心跳判断 + # 优先从 context 获取(批量查询) + load = self._get_load_from_context(obj.id) + if load is not None: + return 'online' + + # 回退:单独查询 Redis + from apps.engine.services.worker_load_service import worker_load_service + if worker_load_service.is_online(obj.id): + return 'online' + return 'offline' + + def get_info(self, obj) -> dict | None: + """获取负载数据 + + 注意:返回的字典键名使用 camelCase,因为 djangorestframework_camel_case + 只转换序列化器字段名,不会递归转换 SerializerMethodField 返回的嵌套字典 + """ + # 优先从 context 获取(批量查询) + load = self._get_load_from_context(obj.id) + if load is not None: + return { + 'cpuPercent': load.get('cpu', 0), + 'memoryPercent': load.get('mem', 0), + } + + # 回退:单独查询 Redis + from apps.engine.services.worker_load_service import worker_load_service + load = worker_load_service.get_load(obj.id) + if load: + return { + 'cpuPercent': load.get('cpu', 0), + 'memoryPercent': load.get('mem', 0), + } + return None + + def create(self, validated_data): + """创建时保存密码""" + return super().create(validated_data) + + def update(self, instance, validated_data): + """更新时,如果密码为空则不更新密码""" + password = validated_data.get('password', '') + if not password: + validated_data.pop('password', None) + return super().update(instance, validated_data) diff --git a/backend/apps/engine/services/__init__.py b/backend/apps/engine/services/__init__.py new file mode 100644 index 00000000..79303b80 --- /dev/null +++ b/backend/apps/engine/services/__init__.py @@ -0,0 +1,25 @@ +""" +Engine 服务层 +""" + +from .engine_service import EngineService +from .worker_service import WorkerService +from .wordlist_service import WordlistService +from .nuclei_template_repo_service import NucleiTemplateRepoService +from .deploy_service import ( + get_bootstrap_script, + get_deploy_script, + get_start_agent_script, + get_uninstall_script, +) + +__all__ = [ + "EngineService", + "WorkerService", + "WordlistService", + "NucleiTemplateRepoService", + "get_bootstrap_script", + "get_deploy_script", + "get_start_agent_script", + "get_uninstall_script", +] diff --git a/backend/apps/engine/services/deploy_service.py b/backend/apps/engine/services/deploy_service.py new file mode 100644 index 00000000..f838373d --- /dev/null +++ b/backend/apps/engine/services/deploy_service.py @@ -0,0 +1,62 @@ +""" +远程节点部署脚本服务 + +脚本文件位置:backend/scripts/worker-deploy/ +- bootstrap.sh: 环境初始化(安装基础依赖) +- install.sh: 安装 Docker + 拉取镜像 +- uninstall.sh: 卸载脚本 +- start-agent.sh: 启动 agent 容器 +- agent.sh: 心跳上报(在容器内运行) + +新架构说明: +- 远程节点只需安装 Docker 和运行 agent +- 扫描任务由主服务器通过 SSH docker run 执行 +""" + +from pathlib import Path + +# 脚本目录 +SCRIPTS_DIR = Path(__file__).parent.parent.parent.parent / "scripts" / "worker-deploy" + + +def _read_script(filename: str) -> str: + """读取脚本文件内容""" + script_path = SCRIPTS_DIR / filename + if script_path.exists(): + return script_path.read_text() + else: + raise FileNotFoundError(f"脚本文件不存在: {script_path}") + + +def get_bootstrap_script() -> str: + """获取环境初始化脚本""" + return _read_script("bootstrap.sh") + + +def get_deploy_script() -> str: + """获取安装脚本(安装 Docker + 拉取镜像)""" + return _read_script("install.sh") + + +def get_uninstall_script() -> str: + """获取卸载脚本""" + return _read_script("uninstall.sh") + + +def get_start_agent_script( + heartbeat_api_url: str = None, + worker_id: int = None +) -> str: + """ + 获取 agent 启动脚本 + + :param heartbeat_api_url: 心跳上报地址 + :param worker_id: Worker ID + """ + script = _read_script("start-agent.sh") + + # 只需替换两个变量 + script = script.replace("{{HEARTBEAT_API_URL}}", heartbeat_api_url or '') + script = script.replace("{{WORKER_ID}}", str(worker_id) if worker_id else '') + + return script diff --git a/backend/apps/engine/services/engine_service.py b/backend/apps/engine/services/engine_service.py new file mode 100644 index 00000000..fa5033fd --- /dev/null +++ b/backend/apps/engine/services/engine_service.py @@ -0,0 +1,39 @@ +""" +ScanEngine 业务逻辑服务层(Service) + +负责扫描引擎相关的业务逻辑处理 +""" + +import logging + +from ..models import ScanEngine +from ..repositories import DjangoEngineRepository + +logger = logging.getLogger(__name__) + + +class EngineService: + """ScanEngine 业务逻辑服务""" + + def __init__(self): + """初始化服务,注入 Repository 依赖""" + self.repo = DjangoEngineRepository() + + def get_engine(self, engine_id: int) -> ScanEngine | None: + """ + 获取扫描引擎 + + Args: + engine_id: 引擎 ID + + Returns: + ScanEngine 对象或 None + """ + return self.repo.get_by_id(engine_id) + + def get_all_engines(self): + """获取所有扫描引擎查询集""" + return self.repo.get_all() + + +__all__ = ['EngineService'] diff --git a/backend/apps/engine/services/nuclei_template_repo_service.py b/backend/apps/engine/services/nuclei_template_repo_service.py new file mode 100644 index 00000000..6f837a78 --- /dev/null +++ b/backend/apps/engine/services/nuclei_template_repo_service.py @@ -0,0 +1,303 @@ +"""Nuclei 模板仓库业务 Service 层 + +本模块封装 Nuclei 多仓库的核心业务逻辑: + +1. Git 同步(refresh_repo) + - 首次调用:git clone --depth 1 + - 后续调用:git pull --ff-only + - 自动更新 last_synced_at 和 local_path + +2. 模板只读浏览 + - get_template_tree: 获取目录树结构 + - get_template_content: 获取单个模板文件内容 + +注意:仓库的 CRUD 操作由 DRF ModelViewSet 默认实现,不在 Service 层处理。 + +调用链路: + View.refresh() → Service.refresh_repo() → subprocess(git) + View.templates_tree() → Service.get_template_tree() → Repository.get_tree() + View.templates_content() → Service.get_template_content() → Repository.get_file_content() + +配置项(settings.py): + NUCLEI_TEMPLATES_REPOS_BASE_DIR: 仓库本地存储根目录,默认 /opt/xingrin/nuclei-repos +""" + +from __future__ import annotations + +import logging +import shutil +from pathlib import Path +from typing import Any, Dict, List, Optional + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.utils import timezone + +from apps.engine.repositories import NucleiTemplateRepository, TemplateFileRepository + + +logger = logging.getLogger(__name__) + + +class NucleiTemplateRepoService: + """Nuclei 多仓库业务 Service + + 负责 Git 同步和模板只读浏览逻辑。 + 通过依赖注入 Repository,方便单元测试。 + + Attributes: + repo: NucleiTemplateRepository 实例,用于 ORM 操作 + """ + + def __init__(self, repository: NucleiTemplateRepository | None = None) -> None: + """初始化 Service + + Args: + repository: 可选,注入 NucleiTemplateRepository 实例(用于测试) + """ + self.repo = repository or NucleiTemplateRepository() + + # ==================== 内部辅助方法 ==================== + + def _get_repo_obj(self, repo_id: int): + """获取仓库对象 + + Args: + repo_id: 仓库 ID + + Returns: + NucleiTemplateRepo 对象 + + Raises: + ValidationError: 仓库不存在时抛出 + """ + obj = self.repo.get_by_id(repo_id) + if not obj: + raise ValidationError("仓库不存在") + return obj + + def _get_base_dir(self) -> Path: + """获取仓库本地存储根目录 + + 从 settings.NUCLEI_TEMPLATES_REPOS_BASE_DIR 读取,默认 /opt/xingrin/nuclei-repos。 + 如果目录不存在会自动创建。 + + Returns: + 根目录 Path 对象 + """ + base_dir = getattr(settings, "NUCLEI_TEMPLATES_REPOS_BASE_DIR", "/opt/xingrin/nuclei-repos") + path = Path(base_dir).resolve() + path.mkdir(parents=True, exist_ok=True) + return path + + def remove_local_path_dir(self, repo_obj) -> None: + """删除与仓库关联的本地目录(如果存在) + + 只会删除位于 NUCLEI_TEMPLATES_REPOS_BASE_DIR 下的目录,避免误删其它路径。 + + Args: + repo_obj: NucleiTemplateRepo 实例 + """ + raw = (getattr(repo_obj, "local_path", "") or "").strip() + if not raw: + return + + base_dir = self._get_base_dir() + path = Path(raw).expanduser().resolve() + + # 仅允许删除 base_dir 下的子目录 + try: + path.relative_to(base_dir) + except ValueError: + return + + if not path.exists() or not path.is_dir(): + return + + try: + shutil.rmtree(path) + except OSError: + # 删除失败时记录日志但不阻塞主流程 + logger.warning("删除 nuclei 本地目录失败: %s", path, exc_info=True) + + def ensure_local_path(self, repo_obj) -> Path: + """确保仓库的本地路径存在并返回 Path + + 规则: + - 如果 repo.local_path 已有值: + - 展开 ~ 并 resolve() 为绝对路径 + - 如果尚未设置: + - 使用 baseDir/nameSlug 生成目录,例如: + /opt/xingrin/nuclei-repos/di-san-fang-mo-ban + - 如果 name 不可 slugify,则退化为 repo-<id> + + 任何情况下都会保证目标目录已创建。 + + Args: + repo_obj: NucleiTemplateRepo 实例 + + Returns: + 本地目录的绝对 Path + """ + from django.utils.text import slugify + + # 已有 local_path,直接规范化为绝对路径 + if getattr(repo_obj, "local_path", None): + path = Path(repo_obj.local_path).expanduser().resolve() + else: + base_dir = self._get_base_dir() + # 根据仓库名称生成 slug,避免中文/空格等问题 + raw_name = (repo_obj.name or "").strip() + slug = slugify(raw_name) if raw_name else "" + if not slug: + slug = f"repo-{repo_obj.id}" + path = (base_dir / slug).resolve() + repo_obj.local_path = str(path) + repo_obj.save(update_fields=["local_path"]) + + path.mkdir(parents=True, exist_ok=True) + return path + + # ==================== Git 同步 ==================== + + def refresh_repo(self, repo_id: int) -> Dict[str, Any]: + """同步仓库(Git clone 或 pull) + + 根据 local_path 是否存在 .git 目录判断: + - 不存在:执行 git clone --depth 1(浅克隆,节省空间) + - 存在:执行 git pull --ff-only(快进合并) + + 同步成功后会更新数据库中的 last_synced_at 和 local_path。 + + Args: + repo_id: 仓库 ID + + Returns: + { + "repoId": 1, + "action": "clone" | "pull", + "localPath": "/opt/xingrin/nuclei-repos/my-templates", + "stdout": "...", + "stderr": "..." + } + + Raises: + ValidationError: 仓库不存在 + RuntimeError: Git 命令执行失败 + """ + import subprocess + + obj = self._get_repo_obj(repo_id) + + # 确保本地路径已生成并为绝对路径 + local_path = self.ensure_local_path(obj) + + git_dir = local_path / ".git" + cmd: List[str] + action: str + + # 判断是 clone 还是 pull + if git_dir.is_dir(): + # 已有仓库,执行 pull + cmd = ["git", "-C", str(local_path), "pull", "--ff-only"] + action = "pull" + else: + # 新仓库,执行 clone + if local_path.exists() and not local_path.is_dir(): + raise RuntimeError(f"本地路径已存在且不是目录: {local_path}") + # --depth 1 浅克隆,只获取最新提交,节省空间和时间 + cmd = ["git", "clone", "--depth", "1", obj.repo_url, str(local_path)] + action = "clone" + + # 执行 Git 命令 + result = subprocess.run( + cmd, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + # 检查执行结果 + if result.returncode != 0: + logger.warning("nuclei 模板仓库 %s git %s 失败: %s", obj.id, action, result.stderr.strip()) + raise RuntimeError("Git 同步失败") + + # 获取当前 commit hash + commit_result = subprocess.run( + ["git", "-C", str(local_path), "rev-parse", "HEAD"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + commit_hash = commit_result.stdout.strip() if commit_result.returncode == 0 else "" + + # 同步成功,更新数据库(包含 commit_hash) + obj.last_synced_at = timezone.now() + obj.local_path = str(local_path) + obj.commit_hash = commit_hash + obj.save(update_fields=["last_synced_at", "local_path", "commit_hash"]) + + logger.info( + "nuclei 模板仓库 %s git %s 成功, commit=%s", + obj.id, action, commit_hash[:8] if commit_hash else "N/A" + ) + + return { + "repoId": obj.id, + "action": action, + "localPath": str(local_path), + "commitHash": commit_hash, + "stdout": result.stdout, + "stderr": result.stderr, + } + + # ==================== 模板树与内容(只读) ==================== + + def _get_fs_repo(self, repo_id: int) -> TemplateFileRepository: + """获取文件系统 Repository 实例 + + Args: + repo_id: 仓库 ID + + Returns: + TemplateFileRepository 实例 + + Raises: + ValidationError: 仓库不存在 + """ + obj = self._get_repo_obj(repo_id) + # 确保本地路径已生成并为绝对路径 + root = self.ensure_local_path(obj) + # 传入绝对路径给 Repository + return TemplateFileRepository(root=root) + + def get_template_tree(self, repo_id: int) -> List[Dict[str, Any]]: + """获取仓库的模板目录树 + + Args: + repo_id: 仓库 ID + + Returns: + 目录树结构,详见 TemplateFileRepository.get_tree() + """ + fs_repo = self._get_fs_repo(repo_id) + return fs_repo.get_tree() + + def get_template_content(self, repo_id: int, rel_path: str) -> Optional[Dict[str, Any]]: + """获取单个模板文件内容 + + Args: + repo_id: 仓库 ID + rel_path: 相对路径,如 "http/cves/CVE-2021-1234.yaml" + + Returns: + 文件内容,详见 TemplateFileRepository.get_file_content() + 文件不存在或读取失败返回 None + """ + fs_repo = self._get_fs_repo(repo_id) + return fs_repo.get_file_content(rel_path) + + +__all__ = ["NucleiTemplateRepoService"] diff --git a/backend/apps/engine/services/task_distributor.py b/backend/apps/engine/services/task_distributor.py new file mode 100644 index 00000000..ff49cf1a --- /dev/null +++ b/backend/apps/engine/services/task_distributor.py @@ -0,0 +1,571 @@ +""" +负载感知任务分发器 + +根据 Worker 负载动态分发任务,支持本地和远程 Worker。 + +核心逻辑: +1. 查询所有在线 Worker 的负载(从心跳数据) +2. 选择负载最低的 Worker(可能是本地或远程) +3. 本地 Worker:直接执行 docker run +4. 远程 Worker:通过 SSH 执行 docker run +5. 任务执行完自动销毁容器 + +特点: +- 负载感知:任务优先分发到最空闲的机器 +- 统一调度:本地和远程 Worker 使用相同的选择逻辑 +- 资源隔离:每个任务独立容器 +- 按需创建:空闲时零占用 +""" + +import logging +import time +from typing import Optional, Dict, Any + +import paramiko +from django.conf import settings + +from apps.engine.models import WorkerNode + +logger = logging.getLogger(__name__) + + +class TaskDistributor: + """ + 负载感知任务分发器 + + 根据 Worker 负载自动选择最优节点执行任务。 + - 本地 Worker (is_local=True):直接执行 docker 命令 + - 远程 Worker (is_local=False):通过 SSH 执行 docker 命令 + + 负载均衡策略: + - 心跳间隔:3 秒(Agent 上报到 Redis) + - 任务间隔:6 秒(确保心跳已更新) + - 高负载阈值:85%(CPU 或内存超过则跳过) + - 在线判断:Redis TTL(15秒过期视为离线) + """ + + # 上次任务提交时间(类级别,所有实例共享) + _last_submit_time: float = 0 + + def __init__(self): + self.docker_image = getattr(settings, 'TASK_EXECUTOR_IMAGE', 'yyhuni/xingrin-worker:latest') + self.results_mount = getattr(settings, 'CONTAINER_RESULTS_MOUNT', '/app/backend/results') + self.logs_mount = getattr(settings, 'CONTAINER_LOGS_MOUNT', '/app/backend/logs') + self.submit_interval = getattr(settings, 'TASK_SUBMIT_INTERVAL', 5) + + def get_online_workers(self) -> list[WorkerNode]: + """ + 获取所有在线的 Worker + + 判断条件: + - status in ('online', 'offline') 表示已部署 + - Redis 中有心跳数据(TTL 未过期) + """ + from apps.engine.services.worker_load_service import worker_load_service + + # 1. 获取所有已部署的节点(online/offline 表示已部署) + workers = WorkerNode.objects.filter(status__in=['online', 'offline']) + + # 2. 过滤出 Redis 中有心跳数据的(在线) + online_workers = [] + for worker in workers: + if worker_load_service.is_online(worker.id): + online_workers.append(worker) + + return online_workers + + def select_best_worker(self) -> Optional[WorkerNode]: + """ + 选择负载最低的在线 Worker + + 选择策略: + - 从 Redis 读取实时负载数据 + - CPU 权重 70%,内存权重 30% + - 排除 CPU > 85% 或 内存 > 85% 的机器 + + Returns: + 最优 Worker,如果没有可用的返回 None + """ + from apps.engine.services.worker_load_service import worker_load_service + + workers = self.get_online_workers() + + if not workers: + logger.warning("没有可用的在线 Worker") + return None + + # 从 Redis 批量获取负载数据 + worker_ids = [w.id for w in workers] + loads = worker_load_service.get_all_loads(worker_ids) + + # 计算每个 Worker 的负载分数 + scored_workers = [] + high_load_workers = [] # 高负载 Worker(降级备选) + + for worker in workers: + # 从 Redis 获取负载数据 + load = loads.get(worker.id) + if not load: + # Redis 无数据,跳过该节点(不应该发生,因为 get_online_workers 已过滤) + logger.warning(f"Worker {worker.name} 无负载数据,跳过") + continue + + cpu = load.get('cpu', 0) + mem = load.get('mem', 0) + + # 加权分数(越低越好) + score = cpu * 0.7 + mem * 0.3 + + # 区分正常和高负载(阈值降到 85%,更保守) + if cpu > 85 or mem > 85: + high_load_workers.append((worker, score, cpu, mem)) + logger.debug( + "高负载 Worker: %s (CPU: %.1f%%, MEM: %.1f%%)", + worker.name, cpu, mem + ) + else: + scored_workers.append((worker, score, cpu, mem)) + + # 降级策略:如果没有正常负载的,使用高负载中最低的 + if not scored_workers: + if high_load_workers: + logger.warning("所有 Worker 高负载,降级选择负载最低的") + scored_workers = high_load_workers + else: + logger.warning("没有可用的 Worker") + return None + + # 选择分数最低的 + scored_workers.sort(key=lambda x: x[1]) + best_worker, score, cpu, mem = scored_workers[0] + + logger.info( + "选择 Worker: %s (CPU: %.1f%%, MEM: %.1f%%, Score: %.1f)", + best_worker.name, cpu, mem, score + ) + + return best_worker + + def _wait_for_submit_interval(self): + """ + 等待任务提交间隔(后台线程中执行,不阻塞 API) + + 确保连续任务提交之间有足够的间隔,让心跳有时间更新负载数据。 + 如果距上次提交已超过间隔,则不等待。 + """ + if TaskDistributor._last_submit_time > 0: + elapsed = time.time() - TaskDistributor._last_submit_time + if elapsed < self.submit_interval: + time.sleep(self.submit_interval - elapsed) + TaskDistributor._last_submit_time = time.time() + + def _build_docker_command( + self, + worker: WorkerNode, + script_module: str, + script_args: Dict[str, Any], + ) -> str: + """ + 构建 docker run 命令 + + 容器只需要 SERVER_URL,启动后从配置中心获取完整配置。 + + Args: + worker: 目标 Worker(用于区分本地/远程网络) + script_module: 脚本模块路径(如 apps.scan.scripts.run_initiate_scan) + script_args: 脚本参数(会转换为命令行参数) + + Returns: + 完整的 docker run 命令 + """ + import shlex + + # 根据 Worker 类型确定网络和 Server 地址 + if worker.is_local: + # 本地:加入 Docker 网络,使用内部服务名 + network_arg = f"--network {settings.DOCKER_NETWORK_NAME}" + server_url = f"http://server:{settings.SERVER_PORT}" + else: + # 远程:无需指定网络,使用公网地址 + network_arg = "" + server_url = f"http://{settings.PUBLIC_HOST}:{settings.SERVER_PORT}" + + # 挂载路径(所有节点统一使用固定路径) + host_results_dir = settings.HOST_RESULTS_DIR # /opt/xingrin/results + host_logs_dir = settings.HOST_LOGS_DIR # /opt/xingrin/logs + + # 环境变量:只需 SERVER_URL,其他配置容器启动时从配置中心获取 + env_vars = [f"-e SERVER_URL={shlex.quote(server_url)}"] + + # 挂载卷 + volumes = [ + f"-v {host_results_dir}:{self.results_mount}", + f"-v {host_logs_dir}:{self.logs_mount}", + ] + + # 构建命令行参数 + # 使用 shlex.quote 处理特殊字符,确保参数在 shell 中正确解析 + args_str = " ".join([f"--{k}={shlex.quote(str(v))}" for k, v in script_args.items()]) + + # 日志文件路径(容器内),保留最近 10000 行 + log_file = f"{self.logs_mount}/container_{script_module.split('.')[-1]}.log" + + # 构建内部命令(日志轮转 + 执行脚本) + inner_cmd = f'tail -n 10000 {log_file} > {log_file}.tmp 2>/dev/null; mv {log_file}.tmp {log_file} 2>/dev/null; python -m {script_module} {args_str} >> {log_file} 2>&1' + + # 完整命令(--pull=always 检查并增量更新镜像,确保使用最新版本) + # 使用双引号包裹 sh -c 命令,内部 shlex.quote 生成的单引号参数可正确解析 + cmd = f'''docker run --rm -d --pull=always {network_arg} \ + {' '.join(env_vars)} \ + {' '.join(volumes)} \ + {self.docker_image} \ + sh -c "{inner_cmd}"''' + + return cmd + + def _execute_docker_command( + self, + worker: WorkerNode, + docker_cmd: str, + ) -> tuple[bool, str]: + """ + 在 Worker 上执行 docker run 命令 + + docker run -d 会立即返回容器 ID,无需等待任务完成。 + + Args: + worker: 目标 Worker + docker_cmd: docker run 命令 + + Returns: + (success, container_id) 元组 + """ + logger.debug("准备执行 Docker 命令 - Worker: %s, Local: %s", worker.name, worker.is_local) + logger.debug("Docker 命令: %s", docker_cmd[:200] + '...' if len(docker_cmd) > 200 else docker_cmd) + + if worker.is_local: + return self._execute_local_docker(docker_cmd) + else: + return self._execute_ssh_docker(worker, docker_cmd) + + def _execute_local_docker( + self, + docker_cmd: str, + ) -> tuple[bool, str]: + """ + 在本地执行 docker run 命令 + + docker run -d 立即返回容器 ID。 + """ + import subprocess + logger.info("开始执行本地 Docker 命令...") + try: + result = subprocess.run( + docker_cmd, + shell=True, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + logger.error( + "本地 Docker 执行失败 - Exit: %d, Stderr: %s, Stdout: %s", + result.returncode, result.stderr[:500], result.stdout[:500] + ) + return False, result.stderr + + container_id = result.stdout.strip() + logger.info("本地 Docker 执行成功 - Container ID: %s", container_id[:12] if container_id else 'N/A') + return True, container_id + + except Exception as e: + logger.error("本地 Docker 执行异常: %s", e, exc_info=True) + return False, f"执行异常: {e}" + + def _execute_ssh_docker( + self, + worker: WorkerNode, + docker_cmd: str, + ) -> tuple[bool, str]: + """ + 在远程 Worker 上通过 SSH 执行 docker run 命令 + + docker run -d 立即返回容器 ID,无需长时间等待。 + + Args: + worker: 目标 Worker + docker_cmd: docker run 命令 + + Returns: + (success, container_id) 元组 + """ + ssh = None + logger.info("开始 SSH Docker 执行 - Worker: %s (%s:%d)", worker.name, worker.ip_address, worker.ssh_port) + try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # 连接(SSH 连接超时 10 秒足够) + ssh.connect( + hostname=worker.ip_address, + port=worker.ssh_port, + username=worker.username, + password=worker.password if worker.password else None, + timeout=10, + ) + logger.debug("SSH 连接成功 - Worker: %s", worker.name) + + # 执行 docker run(-d 模式立即返回) + stdin, stdout, stderr = ssh.exec_command(docker_cmd) + exit_code = stdout.channel.recv_exit_status() + + output = stdout.read().decode().strip() + error = stderr.read().decode().strip() + + if exit_code != 0: + logger.error( + "SSH Docker 执行失败 - Worker: %s, Exit: %d, Stderr: %s, Stdout: %s", + worker.name, exit_code, error[:500], output[:500] + ) + return False, error + + logger.info("SSH Docker 执行成功 - Worker: %s, Container ID: %s", worker.name, output[:12] if output else 'N/A') + return True, output + + except paramiko.AuthenticationException as e: + logger.error("SSH 认证失败 - Worker: %s, Error: %s", worker.name, e) + return False, f"认证失败: {e}" + except paramiko.SSHException as e: + logger.error("SSH 连接错误 - Worker: %s, Error: %s", worker.name, e) + return False, f"SSH 错误: {e}" + except Exception as e: + logger.error("SSH Docker 执行异常 - Worker: %s, Error: %s", worker.name, e) + return False, f"执行异常: {e}" + finally: + if ssh: + ssh.close() + + def execute_scan_flow( + self, + scan_id: int, + target_name: str, + target_id: int, + scan_workspace_dir: str, + engine_name: str, + scheduled_scan_name: str | None = None, + ) -> tuple[bool, str, Optional[str], Optional[int]]: + """ + 在远程或本地 Worker 上执行扫描 Flow + + Args: + scan_id: 扫描任务 ID + target_name: 目标名称 + target_id: 目标 ID + scan_workspace_dir: 扫描工作目录 + engine_name: 引擎名称 + scheduled_scan_name: 定时扫描任务名称(可选) + + Returns: + (success, message, container_id, worker_id) 元组 + + Note: + engine_config 由 Flow 内部通过 scan_id 查询数据库获取 + """ + # 1. 等待提交间隔(后台线程执行,不阻塞 API) + self._wait_for_submit_interval() + + # 2. 选择最佳 Worker + worker = self.select_best_worker() + if not worker: + return False, "没有可用的 Worker", None, None + + # 3. 构建 docker run 命令 + script_args = { + 'scan_id': scan_id, + 'target_name': target_name, + 'target_id': target_id, + 'scan_workspace_dir': scan_workspace_dir, + 'engine_name': engine_name, + } + if scheduled_scan_name: + script_args['scheduled_scan_name'] = scheduled_scan_name + + docker_cmd = self._build_docker_command( + worker=worker, + script_module='apps.scan.scripts.run_initiate_scan', + script_args=script_args, + ) + + logger.info( + "提交扫描任务到 Worker: %s - Scan ID: %d, Target: %s", + worker.name, scan_id, target_name + ) + + # 4. 执行 docker run(本地直接执行,远程通过 SSH) + success, output = self._execute_docker_command(worker, docker_cmd) + + if success: + container_id = output[:12] if output else None + logger.info( + "扫描任务已提交 - Scan ID: %d, Worker: %s, Container: %s", + scan_id, worker.name, container_id + ) + return True, f"任务已提交到 {worker.name}", container_id, worker.id + else: + logger.error( + "扫描任务提交失败 - Scan ID: %d, Worker: %s, Error: %s", + scan_id, worker.name, output + ) + return False, output, None, None + + def execute_cleanup_on_all_workers( + self, + retention_days: int = 7, + ) -> list[dict]: + """ + 在所有 Worker 上执行清理任务 + + Args: + retention_days: 保留天数,默认7天 + + Returns: + 各 Worker 的执行结果列表 + """ + results = [] + + # 获取所有在线的 Worker + workers = self.get_online_workers() + if not workers: + logger.warning("没有可用的 Worker 执行清理任务") + return results + + logger.info(f"开始在 {len(workers)} 个 Worker 上执行清理任务") + + for worker in workers: + try: + # 构建 docker run 命令(清理过期扫描结果目录) + script_args = { + 'results_dir': '/app/backend/results', + 'retention_days': retention_days, + } + + docker_cmd = self._build_docker_command( + worker=worker, + script_module='apps.scan.scripts.run_cleanup', + script_args=script_args, + ) + + # 执行清理命令 + success, output = self._execute_docker_command(worker, docker_cmd) + + results.append({ + 'worker_id': worker.id, + 'worker_name': worker.name, + 'success': success, + 'output': output[:500] if output else None, + }) + + if success: + logger.info(f"✓ Worker {worker.name} 清理任务已启动") + else: + logger.warning(f"✗ Worker {worker.name} 清理任务启动失败: {output}") + + except Exception as e: + logger.error(f"Worker {worker.name} 清理任务执行异常: {e}") + results.append({ + 'worker_id': worker.id, + 'worker_name': worker.name, + 'success': False, + 'error': str(e), + }) + + return results + + def execute_delete_task( + self, + task_type: str, + ids: list[int], + ) -> tuple[bool, str, str | None]: + """ + 分发删除任务到最优 Worker + + 统一入口,根据 task_type 选择对应的删除脚本执行。 + + Args: + task_type: 任务类型 ('targets', 'organizations', 'scans') + ids: 要删除的 ID 列表 + + Returns: + (success, message, container_id) 元组 + """ + import json + + # 映射任务类型到脚本 + script_map = { + 'targets': 'apps.targets.scripts.run_delete_targets', + 'organizations': 'apps.targets.scripts.run_delete_organizations', + 'scans': 'apps.scan.scripts.run_delete_scans', + } + + # 映射任务类型到参数名 + param_map = { + 'targets': 'target_ids', + 'organizations': 'organization_ids', + 'scans': 'scan_ids', + } + + if task_type not in script_map: + return False, f"不支持的任务类型: {task_type}", None + + # 选择最佳 Worker + worker = self.select_best_worker() + if not worker: + return False, "没有可用的 Worker", None + + # 构建参数(ID 列表需要 JSON 序列化) + script_args = { + param_map[task_type]: json.dumps(ids), + } + + # 构建 docker run 命令 + docker_cmd = self._build_docker_command( + worker=worker, + script_module=script_map[task_type], + script_args=script_args, + ) + + logger.info( + "分发删除任务 - 类型: %s, 数量: %d, Worker: %s", + task_type, len(ids), worker.name + ) + + # 执行命令 + success, output = self._execute_docker_command(worker, docker_cmd) + + if success: + container_id = output.strip() if output else None + logger.info( + "✓ 删除任务已分发 - 类型: %s, Container: %s", + task_type, container_id + ) + return True, f"任务已提交到 {worker.name}", container_id + else: + logger.error( + "✗ 删除任务分发失败 - 类型: %s, Error: %s", + task_type, output + ) + return False, output, None + + +# 单例 +_distributor: Optional[TaskDistributor] = None + + +def get_task_distributor() -> TaskDistributor: + """获取任务分发器单例""" + global _distributor + if _distributor is None: + _distributor = TaskDistributor() + return _distributor + + diff --git a/backend/apps/engine/services/wordlist_service.py b/backend/apps/engine/services/wordlist_service.py new file mode 100644 index 00000000..abe7e646 --- /dev/null +++ b/backend/apps/engine/services/wordlist_service.py @@ -0,0 +1,184 @@ +"""Wordlist 业务逻辑服务层(Service) + +负责字典文件相关的业务逻辑处理 +""" + +import hashlib +import logging +import os +import time +from typing import Optional + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.core.files.uploadedfile import UploadedFile + +from apps.common.hash_utils import safe_calc_file_sha256 +from apps.engine.models import Wordlist +from apps.engine.repositories import DjangoWordlistRepository + + +logger = logging.getLogger(__name__) + + +class WordlistService: + """字典文件业务逻辑服务""" + + def __init__(self) -> None: + """初始化服务,注入 Repository 依赖""" + self.repo = DjangoWordlistRepository() + + def get_queryset(self): + """获取字典列表查询集""" + return self.repo.get_queryset() + + def get_wordlist(self, wordlist_id: int) -> Optional[Wordlist]: + """根据 ID 获取字典""" + return self.repo.get_by_id(wordlist_id) + + def get_wordlist_by_name(self, name: str) -> Optional[Wordlist]: + name = (name or "").strip() + if not name: + return None + return self.repo.get_by_name(name) + + def create_wordlist( + self, + name: str, + description: str, + uploaded_file: UploadedFile, + ) -> Wordlist: + """创建字典文件记录并保存物理文件""" + + name = (name or "").strip() + if not name: + raise ValidationError("字典名称不能为空") + + if self._exists_by_name(name): + raise ValidationError("已存在同名字典") + + base_dir = getattr(settings, "WORDLISTS_BASE_PATH", "/opt/xingrin/wordlists") + storage_dir = base_dir + os.makedirs(storage_dir, exist_ok=True) + + # 按原始文件名保存(做最小清洗),同名上传时覆盖旧文件 + original_name = os.path.basename(uploaded_file.name or "wordlist.txt") + # 仅清理路径分隔符,保留空格等字符,避免目录穿越 + safe_name = original_name.replace("/", "_").replace("\\", "_") or "wordlist.txt" + # 如果没有扩展名,补一个 .txt,方便识别 + base, ext = os.path.splitext(safe_name) + if not ext: + safe_name = f"{base}.txt" + + full_path = os.path.join(storage_dir, safe_name) + + # 边写边算 hash + hasher = hashlib.sha256() + with open(full_path, "wb+") as dest: + for chunk in uploaded_file.chunks(): + dest.write(chunk) + hasher.update(chunk) + file_hash = hasher.hexdigest() + + try: + file_size = os.path.getsize(full_path) + except OSError: + file_size = 0 + + line_count = 0 + try: + with open(full_path, "rb") as f: + for _ in f: + line_count += 1 + except OSError: + logger.warning("统计字典行数失败: %s", full_path) + + wordlist = self.repo.create( + name=name, + description=description or "", + file_path=full_path, + file_size=file_size, + line_count=line_count, + file_hash=file_hash, + ) + + logger.info( + "创建字典: id=%s, name=%s, size=%s, lines=%s, hash=%s", + wordlist.id, + wordlist.name, + wordlist.file_size, + wordlist.line_count, + wordlist.file_hash[:16] + "..." if wordlist.file_hash else "N/A", + ) + return wordlist + + def delete_wordlist(self, wordlist_id: int) -> bool: + """删除字典记录及对应的物理文件""" + wordlist: Optional[Wordlist] = self.repo.get_by_id(wordlist_id) + if not wordlist: + return False + + file_path = wordlist.file_path + if file_path: + try: + if os.path.exists(file_path): + os.remove(file_path) + except OSError as exc: + logger.warning("删除字典文件失败: %s - %s", file_path, exc) + + return self.repo.delete(wordlist_id) + + def _exists_by_name(self, name: str) -> bool: + """判断是否存在同名的字典""" + return self.repo.get_queryset().filter(name=name).exists() + + def get_wordlist_content(self, wordlist_id: int) -> Optional[str]: + """获取字典文件内容""" + wordlist = self.repo.get_by_id(wordlist_id) + if not wordlist or not wordlist.file_path: + return None + + try: + with open(wordlist.file_path, "r", encoding="utf-8", errors="replace") as f: + return f.read() + except OSError as exc: + logger.warning("读取字典文件失败: %s - %s", wordlist.file_path, exc) + return None + + def update_wordlist_content(self, wordlist_id: int, content: str) -> Optional[Wordlist]: + """更新字典文件内容并重新计算 hash""" + wordlist = self.repo.get_by_id(wordlist_id) + if not wordlist or not wordlist.file_path: + return None + + try: + # 写入新内容 + with open(wordlist.file_path, "w", encoding="utf-8") as f: + f.write(content) + + # 重新计算统计信息 + file_size = os.path.getsize(wordlist.file_path) + line_count = content.count("\n") + (1 if content and not content.endswith("\n") else 0) + file_hash = safe_calc_file_sha256(wordlist.file_path) or "" + + # 更新记录 + wordlist.file_size = file_size + wordlist.line_count = line_count + wordlist.file_hash = file_hash + wordlist.save(update_fields=["file_size", "line_count", "file_hash", "updated_at"]) + + logger.info( + "更新字典内容: id=%s, name=%s, size=%s, lines=%s, hash=%s", + wordlist.id, + wordlist.name, + wordlist.file_size, + wordlist.line_count, + wordlist.file_hash[:16] + "..." if wordlist.file_hash else "N/A", + ) + return wordlist + except OSError as exc: + logger.error("写入字典文件失败: %s - %s", wordlist.file_path, exc) + return None + + +__all__ = ["WordlistService"] diff --git a/backend/apps/engine/services/worker_load_service.py b/backend/apps/engine/services/worker_load_service.py new file mode 100644 index 00000000..04479353 --- /dev/null +++ b/backend/apps/engine/services/worker_load_service.py @@ -0,0 +1,147 @@ +""" +Worker 负载服务(Redis) + +存储结构: +- worker:load:{worker_id} - Hash: {cpu, mem, updated} +- TTL: 60 秒(超时自动清理) +""" + +import logging +from typing import Optional, Dict, Any +from datetime import datetime + +import redis +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class WorkerLoadService: + """Worker 负载数据服务(基于 Redis)""" + + # Key 前缀 + KEY_PREFIX = "worker:load:" + + # 数据过期时间(秒)- 超过此时间未更新视为离线 + # 心跳间隔 3 秒,TTL 设为 15 秒(5 次心跳容错) + TTL_SECONDS = 15 + + def __init__(self): + self._redis: Optional[redis.Redis] = None + + @property + def redis(self) -> redis.Redis: + """懒加载 Redis 连接""" + if self._redis is None: + self._redis = redis.Redis( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + db=settings.REDIS_DB, + decode_responses=True, + ) + return self._redis + + def _key(self, worker_id: int) -> str: + """生成 Redis key""" + return f"{self.KEY_PREFIX}{worker_id}" + + def update_load(self, worker_id: int, cpu_percent: float, memory_percent: float) -> bool: + """ + 更新 Worker 负载数据 + + Args: + worker_id: Worker ID + cpu_percent: CPU 使用率 + memory_percent: 内存使用率 + + Returns: + 是否成功 + """ + try: + key = self._key(worker_id) + data = { + "cpu": cpu_percent, + "mem": memory_percent, + "updated": datetime.now().isoformat(), + } + + # 使用 pipeline 原子操作 + pipe = self.redis.pipeline() + pipe.hset(key, mapping=data) + pipe.expire(key, self.TTL_SECONDS) + pipe.execute() + + return True + except Exception as e: + logger.error(f"更新 Worker 负载失败 - ID: {worker_id}: {e}") + return False + + def get_load(self, worker_id: int) -> Optional[Dict[str, Any]]: + """ + 获取 Worker 负载数据 + + Returns: + {"cpu": float, "mem": float, "updated": str} 或 None + """ + try: + key = self._key(worker_id) + data = self.redis.hgetall(key) + + if not data: + return None + + return { + "cpu": float(data.get("cpu", 0)), + "mem": float(data.get("mem", 0)), + "updated": data.get("updated", ""), + } + except Exception as e: + logger.error(f"获取 Worker 负载失败 - ID: {worker_id}: {e}") + return None + + def get_all_loads(self, worker_ids: list[int]) -> Dict[int, Dict[str, Any]]: + """ + 批量获取 Worker 负载数据 + + Args: + worker_ids: Worker ID 列表 + + Returns: + {worker_id: {"cpu": float, "mem": float}} 字典 + """ + result = {} + + try: + pipe = self.redis.pipeline() + for worker_id in worker_ids: + pipe.hgetall(self._key(worker_id)) + + responses = pipe.execute() + + for worker_id, data in zip(worker_ids, responses): + if data: + result[worker_id] = { + "cpu": float(data.get("cpu", 0)), + "mem": float(data.get("mem", 0)), + } + except Exception as e: + logger.error(f"批量获取 Worker 负载失败: {e}") + + return result + + def delete_load(self, worker_id: int) -> bool: + """删除 Worker 负载数据""" + try: + self.redis.delete(self._key(worker_id)) + return True + except Exception as e: + logger.error(f"删除 Worker 负载失败 - ID: {worker_id}: {e}") + return False + + def is_online(self, worker_id: int) -> bool: + """检查 Worker 是否在线(Redis 中有数据且未过期)""" + return self.redis.exists(self._key(worker_id)) > 0 + + +# 单例 +worker_load_service = WorkerLoadService() diff --git a/backend/apps/engine/services/worker_service.py b/backend/apps/engine/services/worker_service.py new file mode 100644 index 00000000..34fa9219 --- /dev/null +++ b/backend/apps/engine/services/worker_service.py @@ -0,0 +1,138 @@ +""" +WorkerNode 业务逻辑服务层(Service) + +负责 Worker 节点相关的业务逻辑处理 +""" + +import logging +from typing import Any + +from apps.engine.repositories import DjangoWorkerRepository + +logger = logging.getLogger(__name__) + + +class WorkerService: + """Worker 节点业务逻辑服务""" + + def __init__(self) -> None: + """初始化服务,注入 Repository 依赖""" + self.repo = DjangoWorkerRepository() + + # ==================== 查询 ==================== + + def get_worker(self, worker_id: int): + """根据 ID 获取 Worker 节点""" + return self.repo.get_by_id(worker_id) + + def get_all_workers(self): + """获取所有 Worker 节点查询集""" + return self.repo.get_all() + + # ==================== 状态更新 ==================== + + def update_status(self, worker_id: int, status: str) -> bool: + """更新 Worker 节点状态 + + Args: + worker_id: Worker ID + status: 状态 (pending/deploying/online/offline) + """ + return self.repo.update_status(worker_id, status) + + + def delete_worker(self, worker_id: int) -> bool: + """删除 Worker 节点""" + return self.repo.delete_by_id(worker_id) + + # ==================== 自注册 ==================== + + def register_worker(self, name: str, is_local: bool = True): + """ + 注册 Worker 节点(本地 Worker 自注册用) + + 幂等操作:已存在则返回现有节点。 + + Args: + name: Worker 名称 + is_local: 是否为本地节点 + + Returns: + (WorkerNode, created) 元组 + """ + return self.repo.get_or_create_by_name( + name=name, + is_local=is_local + ) + + def remote_uninstall( + self, + worker_id: int, + ip_address: str, + ssh_port: int, + username: str, + password: str | None + ) -> tuple[bool, str]: + """ + 在远程主机上执行卸载脚本 + + Args: + worker_id: Worker ID(仅用于日志) + ip_address: SSH 主机地址 + ssh_port: SSH 端口 + username: SSH 用户名 + password: SSH 密码 + + Returns: + (success, message) 元组 + """ + if not password: + return False, "未配置 SSH 密码,跳过远程卸载" + + try: + import paramiko + from apps.engine.services.deploy_service import get_uninstall_script + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + logger.info(f"[卸载] 正在连接 {ip_address}...") + ssh.connect( + ip_address, + port=ssh_port, + username=username, + password=password, + timeout=30 + ) + + # 上传卸载脚本 + uninstall_script = get_uninstall_script() + remote_script_path = '/tmp/xingrin_uninstall.sh' + + sftp = ssh.open_sftp() + with sftp.file(remote_script_path, 'w') as f: + f.write(uninstall_script) + sftp.chmod(remote_script_path, 0o755) + sftp.close() + + # 执行卸载脚本 + logger.info(f"[卸载] 正在执行卸载脚本...") + stdin, stdout, stderr = ssh.exec_command(f"bash {remote_script_path}") + exit_status = stdout.channel.recv_exit_status() + + ssh.close() + + if exit_status == 0: + logger.info(f"[卸载] Worker {worker_id} 远程卸载成功") + return True, "远程卸载成功" + else: + error = stderr.read().decode().strip() + logger.warning(f"[卸载] Worker {worker_id} 远程卸载失败: {error}") + return False, f"远程卸载失败: {error}" + + except Exception as e: + logger.warning(f"[卸载] Worker {worker_id} 远程卸载异常: {e}") + return False, f"远程卸载异常: {str(e)}" + + +__all__ = ["WorkerService"] diff --git a/backend/apps/engine/urls.py b/backend/apps/engine/urls.py new file mode 100644 index 00000000..46679a24 --- /dev/null +++ b/backend/apps/engine/urls.py @@ -0,0 +1,22 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter + +from .views import ( + ScanEngineViewSet, + WorkerNodeViewSet, + WordlistViewSet, + NucleiTemplateRepoViewSet, +) + + +# 创建路由器 +router = DefaultRouter() +router.register(r"engines", ScanEngineViewSet, basename="engine") +router.register(r"workers", WorkerNodeViewSet, basename="worker") +router.register(r"wordlists", WordlistViewSet, basename="wordlist") +router.register(r"nuclei/repos", NucleiTemplateRepoViewSet, basename="nuclei-repos") + +urlpatterns = [ + path("", include(router.urls)), +] + diff --git a/backend/apps/engine/views/__init__.py b/backend/apps/engine/views/__init__.py new file mode 100644 index 00000000..c360257f --- /dev/null +++ b/backend/apps/engine/views/__init__.py @@ -0,0 +1,12 @@ +"""Engine Views""" +from .worker_views import WorkerNodeViewSet +from .engine_views import ScanEngineViewSet +from .wordlist_views import WordlistViewSet +from .nuclei_template_repo_views import NucleiTemplateRepoViewSet + +__all__ = [ + "WorkerNodeViewSet", + "ScanEngineViewSet", + "WordlistViewSet", + "NucleiTemplateRepoViewSet", +] diff --git a/backend/apps/engine/views/engine_views.py b/backend/apps/engine/views/engine_views.py new file mode 100644 index 00000000..4fdf9e30 --- /dev/null +++ b/backend/apps/engine/views/engine_views.py @@ -0,0 +1,31 @@ +""" +扫描引擎 Views +""" +from rest_framework import viewsets + +from apps.engine.serializers import ScanEngineSerializer +from apps.engine.services import EngineService + + +class ScanEngineViewSet(viewsets.ModelViewSet): + """ + 扫描引擎 ViewSet + + 自动提供完整的 CRUD 操作: + - GET /api/engines/ - 获取引擎列表 + - POST /api/engines/ - 创建新引擎 + - GET /api/engines/{id}/ - 获取引擎详情 + - PUT /api/engines/{id}/ - 更新引擎 + - PATCH /api/engines/{id}/ - 部分更新引擎 + - DELETE /api/engines/{id}/ - 删除引擎 + """ + + serializer_class = ScanEngineSerializer + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.engine_service = EngineService() + + def get_queryset(self): + """通过服务层获取查询集""" + return self.engine_service.get_all_engines() diff --git a/backend/apps/engine/views/nuclei_template_repo_views.py b/backend/apps/engine/views/nuclei_template_repo_views.py new file mode 100644 index 00000000..2b058820 --- /dev/null +++ b/backend/apps/engine/views/nuclei_template_repo_views.py @@ -0,0 +1,196 @@ +"""Nuclei 模板仓库 View 层(HTTP 接口) + +本模块提供 Nuclei 多仓库管理的 REST API,基于 DRF ModelViewSet。 + +API 列表: +========== + +仓库 CRUD(ModelViewSet 默认实现): +- GET /api/nuclei/repos/ 获取仓库列表 +- POST /api/nuclei/repos/ 创建仓库 +- GET /api/nuclei/repos/{id}/ 获取仓库详情 +- PUT /api/nuclei/repos/{id}/ 更新仓库 +- DELETE /api/nuclei/repos/{id}/ 删除仓库 + +自定义 Action: +- POST /api/nuclei/repos/{id}/refresh/ 手动 Git 同步(clone/pull) +- GET /api/nuclei/repos/{id}/templates/tree/ 获取当前本地模板目录树(不自动同步) +- GET /api/nuclei/repos/{id}/templates/content/ 获取单个模板内容(只读) + +调用链路: + HTTP Request → View → Service → Repository → Model/FileSystem +""" + +from __future__ import annotations + +import logging + +from django.core.exceptions import ValidationError +from rest_framework import status, viewsets +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response + +from apps.engine.models import NucleiTemplateRepo +from apps.engine.serializers import NucleiTemplateRepoSerializer +from apps.engine.services import NucleiTemplateRepoService + + +logger = logging.getLogger(__name__) + + +class NucleiTemplateRepoViewSet(viewsets.ModelViewSet): + """Nuclei 模板 Git 仓库 ViewSet + + 继承 ModelViewSet,自动获得 CRUD 能力: + - list: 获取仓库列表 + - create: 创建仓库 + - retrieve: 获取仓库详情 + - update: 更新仓库 + - destroy: 删除仓库 + + 额外提供三个自定义 Action(见下方方法)。 + + Attributes: + queryset: 默认查询集,按创建时间倒序 + serializer_class: 序列化器类 + service: Service 层实例,处理业务逻辑 + """ + + # DRF ModelViewSet 配置 + queryset = NucleiTemplateRepo.objects.all().order_by("-created_at") + serializer_class = NucleiTemplateRepoSerializer + + def __init__(self, *args, **kwargs) -> None: # type: ignore[override] + """初始化 ViewSet,创建 Service 实例""" + super().__init__(*args, **kwargs) + self.service = NucleiTemplateRepoService() + + def perform_create(self, serializer) -> None: # type: ignore[override] + """创建仓库时初始化本地路径目录 + + 设计原则:第一次创建仓库就确定好 local_path,后续所有 Git 拉取和模板读取 + 都复用这个固定目录,避免运行时才临时决定路径。 + """ + instance = serializer.save() + # 初始化并持久化 local_path,同时在文件系统中创建对应目录 + self.service.ensure_local_path(instance) + + def perform_destroy(self, instance: NucleiTemplateRepo) -> None: # type: ignore[override] + """删除仓库时同时清理本地目录 + + 前端在 /tools/nuclei/ 点击删除时: + - 这里会先尝试删除 instance.local_path 对应的目录 + - 然后调用父类逻辑删除数据库记录 + """ + # 清理本地目录(最佳努力,不影响主流程) + self.service.remove_local_path_dir(instance) + super().perform_destroy(instance) + + # ==================== 自定义 Action: Git 同步 ==================== + + @action(detail=True, methods=["post"], url_path="refresh") + def refresh(self, request: Request, pk: str | None = None) -> Response: + """手动触发 Git 同步 + + POST /api/nuclei/repos/{id}/refresh/ + + 执行 git clone(首次)或 git pull(后续)。 + 同步成功后更新 last_synced_at。 + + Returns: + 200: {"message": "刷新成功", "result": {...}} + 400: {"message": "无效的仓库 ID"} 或 {"message": "仓库不存在"} + 500: {"message": "刷新仓库失败"} + """ + # 解析仓库 ID + try: + repo_id = int(pk) if pk is not None else None + except (TypeError, ValueError): + return Response({"message": "无效的仓库 ID"}, status=status.HTTP_400_BAD_REQUEST) + + # 调用 Service 层 + try: + result = self.service.refresh_repo(repo_id) + except ValidationError as exc: + return Response({"message": str(exc)}, status=status.HTTP_400_BAD_REQUEST) + except Exception as exc: # noqa: BLE001 + logger.error("刷新 Nuclei 模板仓库失败: %s", exc, exc_info=True) + return Response({"message": "刷新仓库失败"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({"message": "刷新成功", "result": result}, status=status.HTTP_200_OK) + + # ==================== 自定义 Action: 模板只读浏览 ==================== + + @action(detail=True, methods=["get"], url_path="templates/tree") + def templates_tree(self, request: Request, pk: str | None = None) -> Response: + """获取模板目录树 + + GET /api/nuclei/repos/{id}/templates/tree/ + + 只读取当前本地仓库目录,不主动触发 Git 同步。 + 如需拉取远端最新内容,请先调用 POST /api/nuclei/repos/{id}/refresh/。 + + 返回的树形结构包含所有文件夹和 .yaml/.yml 文件。 + + Returns: + 200: {"roots": [{type, name, path, children}, ...]} + 400: {"message": "无效的仓库 ID"} 或 {"message": "仓库不存在"} + 500: {"message": "获取模板目录树失败"} + """ + # 解析仓库 ID + try: + repo_id = int(pk) if pk is not None else None + except (TypeError, ValueError): + return Response({"message": "无效的仓库 ID"}, status=status.HTTP_400_BAD_REQUEST) + + # 调用 Service 层,仅从当前本地目录读取目录树 + try: + roots = self.service.get_template_tree(repo_id) + except ValidationError as exc: + return Response({"message": str(exc)}, status=status.HTTP_400_BAD_REQUEST) + except Exception as exc: # noqa: BLE001 + logger.error("获取 Nuclei 模板目录树失败: %s", exc, exc_info=True) + return Response({"message": "获取模板目录树失败"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({"roots": roots}) + + @action(detail=True, methods=["get"], url_path="templates/content") + def templates_content(self, request: Request, pk: str | None = None) -> Response: + """获取单个模板文件内容 + + GET /api/nuclei/repos/{id}/templates/content/?path=http/example.yaml + + Query Parameters: + path: 模板相对路径,如 "http/cves/CVE-2021-1234.yaml" + + Returns: + 200: {"path": "...", "name": "...", "content": "..."} + 400: {"message": "无效的仓库 ID"} 或 {"message": "缺少 path 参数"} + 404: {"message": "模板不存在或无法读取"} + 500: {"message": "获取模板内容失败"} + """ + # 解析仓库 ID + try: + repo_id = int(pk) if pk is not None else None + except (TypeError, ValueError): + return Response({"message": "无效的仓库 ID"}, status=status.HTTP_400_BAD_REQUEST) + + # 解析 path 参数 + rel_path = (request.query_params.get("path", "") or "").strip() + if not rel_path: + return Response({"message": "缺少 path 参数"}, status=status.HTTP_400_BAD_REQUEST) + + # 调用 Service 层 + try: + result = self.service.get_template_content(repo_id, rel_path) + except ValidationError as exc: + return Response({"message": str(exc)}, status=status.HTTP_400_BAD_REQUEST) + except Exception as exc: # noqa: BLE001 + logger.error("获取 Nuclei 模板内容失败: %s", exc, exc_info=True) + return Response({"message": "获取模板内容失败"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 文件不存在 + if result is None: + return Response({"message": "模板不存在或无法读取"}, status=status.HTTP_404_NOT_FOUND) + return Response(result) diff --git a/backend/apps/engine/views/wordlist_views.py b/backend/apps/engine/views/wordlist_views.py new file mode 100644 index 00000000..4ac47759 --- /dev/null +++ b/backend/apps/engine/views/wordlist_views.py @@ -0,0 +1,127 @@ +"""字典管理 API Views""" + +import os + +from django.core.exceptions import ValidationError +from django.http import FileResponse +from rest_framework import status, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from apps.common.pagination import BasePagination +from apps.engine.serializers.wordlist_serializer import WordlistSerializer +from apps.engine.services.wordlist_service import WordlistService + + +class WordlistViewSet(viewsets.ViewSet): + """字典管理 ViewSet""" + + pagination_class = BasePagination + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.service = WordlistService() + + @property + def paginator(self): + """懒加载分页器实例""" + if not hasattr(self, "_paginator"): + if self.pagination_class is None: + self._paginator = None + else: + self._paginator = self.pagination_class() + return self._paginator + + def list(self, request): + """获取字典列表""" + queryset = self.service.get_queryset() + page = self.paginator.paginate_queryset(queryset, request, view=self) + serializer = WordlistSerializer(page, many=True) + return self.paginator.get_paginated_response(serializer.data) + + def create(self, request): + """上传并创建字典记录""" + name = (request.data.get("name", "") or "").strip() + description = request.data.get("description", "") or "" + uploaded_file = request.FILES.get("file") + + if not uploaded_file: + return Response({"error": "缺少字典文件"}, status=status.HTTP_400_BAD_REQUEST) + + try: + wordlist = self.service.create_wordlist( + name=name, + description=description, + uploaded_file=uploaded_file, + ) + except ValidationError as exc: + return Response({"error": str(exc)}, status=status.HTTP_400_BAD_REQUEST) + + serializer = WordlistSerializer(wordlist) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def destroy(self, request, pk=None): + """删除字典记录""" + try: + wordlist_id = int(pk) + except (TypeError, ValueError): + return Response({"error": "无效的 ID"}, status=status.HTTP_400_BAD_REQUEST) + + success = self.service.delete_wordlist(wordlist_id) + if not success: + return Response({"error": "字典不存在"}, status=status.HTTP_404_NOT_FOUND) + + return Response(status=status.HTTP_204_NO_CONTENT) + + @action(detail=False, methods=["get"], url_path="download") + def download(self, request): + """通过字典名称下载文件内容 + + Query 参数: + - wordlist: 字典名称,对应 Wordlist.name(唯一) + """ + name = (request.query_params.get("wordlist", "") or "").strip() + if not name: + return Response({"error": "缺少参数 wordlist"}, status=status.HTTP_400_BAD_REQUEST) + + wordlist = self.service.get_wordlist_by_name(name) + if not wordlist: + return Response({"error": "字典不存在"}, status=status.HTTP_404_NOT_FOUND) + + file_path = wordlist.file_path + if not file_path or not os.path.exists(file_path): + return Response({"error": "字典文件不存在"}, status=status.HTTP_404_NOT_FOUND) + + filename = os.path.basename(file_path) + response = FileResponse(open(file_path, "rb"), as_attachment=True, filename=filename) + return response + + @action(detail=True, methods=["get", "put"], url_path="content") + def content(self, request, pk=None): + """获取或更新字典文件内容 + + GET: 返回字典文件的文本内容 + PUT: 更新字典文件内容,重新计算 hash + """ + try: + wordlist_id = int(pk) + except (TypeError, ValueError): + return Response({"error": "无效的 ID"}, status=status.HTTP_400_BAD_REQUEST) + + if request.method == "GET": + content = self.service.get_wordlist_content(wordlist_id) + if content is None: + return Response({"error": "字典不存在或文件无法读取"}, status=status.HTTP_404_NOT_FOUND) + return Response({"content": content}) + + elif request.method == "PUT": + content = request.data.get("content") + if content is None: + return Response({"error": "缺少 content 参数"}, status=status.HTTP_400_BAD_REQUEST) + + wordlist = self.service.update_wordlist_content(wordlist_id, content) + if not wordlist: + return Response({"error": "字典不存在或更新失败"}, status=status.HTTP_404_NOT_FOUND) + + serializer = WordlistSerializer(wordlist) + return Response(serializer.data) diff --git a/backend/apps/engine/views/worker_views.py b/backend/apps/engine/views/worker_views.py new file mode 100644 index 00000000..7a2e0d63 --- /dev/null +++ b/backend/apps/engine/views/worker_views.py @@ -0,0 +1,215 @@ +""" +Worker 节点 Views +""" +import os +import threading +import logging + +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response + +from apps.engine.serializers import WorkerNodeSerializer +from apps.engine.services import WorkerService +from apps.common.signals import worker_delete_failed + +logger = logging.getLogger(__name__) + + +class WorkerNodeViewSet(viewsets.ModelViewSet): + """ + Worker 节点 ViewSet + + HTTP API: + - GET /api/workers/ - 获取节点列表 + - POST /api/workers/ - 创建节点 + - DELETE /api/workers/{id}/ - 删除节点(同时执行远程卸载) + - POST /api/workers/{id}/heartbeat/ - 心跳上报 + + 部署通过 WebSocket 终端进行: + - ws://host/ws/workers/{id}/deploy/ + """ + + serializer_class = WorkerNodeSerializer + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.worker_service = WorkerService() + + def get_queryset(self): + """通过服务层获取 Worker 查询集""" + return self.worker_service.get_all_workers() + + def get_serializer_context(self): + """传入批量查询的 Redis 负载数据,避免 N+1 查询""" + context = super().get_serializer_context() + + # 仅在 list 操作时批量预加载 + if self.action == 'list': + from apps.engine.services.worker_load_service import worker_load_service + queryset = self.get_queryset() + worker_ids = list(queryset.values_list('id', flat=True)) + context['loads'] = worker_load_service.get_all_loads(worker_ids) + + return context + + def destroy(self, request, *args, **kwargs): + """ + 删除 Worker 节点 + + 流程: + 1. 后台线程执行远程卸载脚本 + 2. 卸载完成后删除数据库记录 + 3. 发送通知 + """ + worker = self.get_object() + + # 在主线程中提取所有需要的数据(避免后台线程访问 ORM 对象) + worker_id = worker.id + worker_name = worker.name + ip_address = worker.ip_address + ssh_port = worker.ssh_port + username = worker.username + password = worker.password + + # 1. 删除 Redis 中的负载数据 + from apps.engine.services.worker_load_service import worker_load_service + worker_load_service.delete_load(worker_id) + + # 2. 删除数据库记录(立即生效,前端刷新时不会再看到) + self.worker_service.delete_worker(worker_id) + + def _async_remote_uninstall(): + """后台执行远程卸载""" + try: + success, message = self.worker_service.remote_uninstall( + worker_id=worker_id, + ip_address=ip_address, + ssh_port=ssh_port, + username=username, + password=password + ) + if success: + logger.info(f"Worker {worker_name} 远程卸载成功") + else: + logger.warning(f"Worker {worker_name} 远程卸载: {message}") + # 卸载失败时发送通知 + worker_delete_failed.send( + sender=self.__class__, + worker_name=worker_name, + message=message + ) + except Exception as e: + logger.error(f"Worker {worker_name} 远程卸载失败: {e}") + worker_delete_failed.send( + sender=self.__class__, + worker_name=worker_name, + message=str(e) + ) + + # 2. 后台线程执行远程卸载(不阻塞响应) + threading.Thread(target=_async_remote_uninstall, daemon=True).start() + + # 3. 立即返回成功 + return Response( + {"message": f"节点 {worker_name} 已删除"}, + status=status.HTTP_200_OK + ) + + @action(detail=True, methods=['post']) + def heartbeat(self, request, pk=None): + """接收心跳上报(写 Redis,首次心跳更新部署状态)""" + from apps.engine.services.worker_load_service import worker_load_service + + worker = self.get_object() + info = request.data if request.data else {} + + # 1. 写入 Redis(实时负载数据,TTL=60秒) + cpu = info.get('cpu_percent', 0) + mem = info.get('memory_percent', 0) + worker_load_service.update_load(worker.id, cpu, mem) + + # 2. 首次心跳:更新状态为 online + if worker.status not in ('online', 'offline'): + worker.status = 'online' + worker.save(update_fields=['status']) + + return Response({'status': 'ok'}) + + @action(detail=False, methods=['post']) + def register(self, request): + """ + Worker 自注册 API + + 本地 Worker 启动时调用此接口注册自己。 + 如果同名节点已存在,返回现有记录;否则创建新记录。 + + 请求体: + { + "name": "Local-Scan-Worker", + "is_local": true + } + + 返回: + { + "worker_id": 1, + "name": "Local-Scan-Worker", + "created": false # true 表示新创建,false 表示已存在 + } + """ + name = request.data.get('name') + is_local = request.data.get('is_local', True) + + if not name: + return Response( + {'error': '缺少 name 参数'}, + status=status.HTTP_400_BAD_REQUEST + ) + + worker, created = self.worker_service.register_worker( + name=name, + is_local=is_local + ) + + return Response({ + 'worker_id': worker.id, + 'name': worker.name, + 'created': created + }) + + @action(detail=False, methods=['get']) + def config(self, request): + """ + 获取任务容器配置 + + 任务容器启动时调用此接口获取完整配置, + 实现配置中心化管理,Worker 只需知道 SERVER_URL。 + + 返回: + { + "db": {"host": "...", "port": "...", ...}, + "redisUrl": "...", + "paths": {"results": "...", "logs": "..."} + } + """ + from django.conf import settings + + return Response({ + 'db': { + 'host': getattr(settings, 'WORKER_DB_HOST', settings.DATABASES['default']['HOST']), + 'port': str(settings.DATABASES['default']['PORT']), + 'name': settings.DATABASES['default']['NAME'], + 'user': settings.DATABASES['default']['USER'], + 'password': settings.DATABASES['default']['PASSWORD'], + }, + 'redisUrl': getattr(settings, 'WORKER_REDIS_URL', 'redis://redis:6379/0'), + 'paths': { + 'results': getattr(settings, 'CONTAINER_RESULTS_MOUNT', '/app/backend/results'), + 'logs': getattr(settings, 'CONTAINER_LOGS_MOUNT', '/app/backend/logs'), + }, + 'logging': { + 'level': os.getenv('LOG_LEVEL', 'INFO'), + 'enableCommandLogging': os.getenv('ENABLE_COMMAND_LOGGING', 'true').lower() == 'true', + }, + 'debug': settings.DEBUG + }) diff --git a/backend/apps/scan/__init__.py b/backend/apps/scan/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/scan/apps.py b/backend/apps/scan/apps.py new file mode 100644 index 00000000..2036f91c --- /dev/null +++ b/backend/apps/scan/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + + +class ScanConfig(AppConfig): + """扫描应用配置类""" + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.scan' + + def ready(self): + """应用启动时注册信号接收器""" + # 导入接收器模块以注册信号处理函数 + from apps.scan.notifications import receivers # noqa: F401 \ No newline at end of file diff --git a/backend/apps/scan/configs/command_templates.py b/backend/apps/scan/configs/command_templates.py new file mode 100644 index 00000000..e95998bf --- /dev/null +++ b/backend/apps/scan/configs/command_templates.py @@ -0,0 +1,280 @@ +""" +扫描工具命令模板(简化版,不使用 Jinja2) + +使用 Python 原生字符串格式化,零依赖。 +""" + +from django.conf import settings + +# ==================== 路径配置 ==================== +SCAN_TOOLS_BASE_PATH = getattr(settings, 'SCAN_TOOLS_BASE_PATH', '/opt/xingrin/tools') + +# ==================== 子域名发现 ==================== + +SUBDOMAIN_DISCOVERY_COMMANDS = { + 'subfinder': { + # 默认使用所有数据源(更全面,略慢),并始终开启递归 + # -all 使用所有数据源 + # -recursive 对支持递归的源启用递归枚举(默认开启) + 'base': 'subfinder -d {domain} -all -recursive -o {output_file} -silent', + 'optional': { + 'threads': '-t {threads}', # 控制并发 goroutine 数 + } + }, + + 'amass_passive': { + # 先执行被动枚举,将结果写入 amass 内部数据库,然后从数据库中导出纯域名(names)到 output_file + # -silent 禁用进度条和其他输出 + 'base': 'amass enum -passive -silent -d {domain} && amass subs -names -d {domain} > {output_file}' + }, + + 'amass_active': { + # 先执行主动枚举 + 爆破,将结果写入 amass 内部数据库,然后从数据库中导出纯域名(names)到 output_file + # -silent 禁用进度条和其他输出 + 'base': 'amass enum -active -silent -d {domain} -brute && amass subs -names -d {domain} > {output_file}' + }, + + 'sublist3r': { + 'base': 'python3 {scan_tools_base}/Sublist3r/sublist3r.py -d {domain} -o {output_file}', + 'optional': { + 'threads': '-t {threads}' + } + }, + + 'assetfinder': { + 'base': 'assetfinder --subs-only {domain} > {output_file}', + }, + + # === 主动字典爆破 === + 'subdomain_bruteforce': { + # 使用字典对目标域名进行 DNS 爆破 + # -d 目标域名,-w 字典文件,-o 输出文件 + 'base': 'puredns bruteforce {wordlist} {domain} -r /app/backend/resources/resolvers.txt --write {output_file} --quiet', + 'optional': {}, + }, + + # === DNS 存活验证(最终统一验证)=== + 'subdomain_resolve': { + # 验证子域名是否能解析(存活验证) + # 输入文件为候选子域列表,输出为存活子域列表 + 'base': 'puredns resolve {input_file} -r /app/backend/resources/resolvers.txt --write {output_file} --wildcard-tests 50 --wildcard-batch 1000000 --quiet', + 'optional': {}, + }, + + # === 变异生成 + 存活验证(流式管道,避免 OOM)=== + 'subdomain_permutation_resolve': { + # 流式管道:dnsgen 生成变异域名 | puredns resolve 验证存活 + # 不落盘中间文件,避免内存爆炸;不做通配符过滤 + 'base': 'cat {input_file} | dnsgen - | puredns resolve -r /app/backend/resources/resolvers.txt --write {output_file} --wildcard-tests 50 --wildcard-batch 1000000 --quiet', + 'optional': {}, + }, +} + + +# ==================== 端口扫描 ==================== + +PORT_SCAN_COMMANDS = { + 'naabu_active': { + 'base': 'naabu -exclude-cdn -warm-up-time 5 -verify -list {domains_file} -json -silent', + 'optional': { + 'threads': '-c {threads}', + 'ports': '-p {ports}', + 'top_ports': '-top-ports {top_ports}', + 'rate': '-rate {rate}' + } + }, + + 'naabu_passive': { + 'base': 'naabu -list {domains_file} -passive -json -silent' + }, +} + + +# ==================== 站点扫描 ==================== + +SITE_SCAN_COMMANDS = { + 'httpx': { + 'base': ( + 'httpx -l {url_file} ' + '-status-code -content-type -content-length ' + '-location -title -server -body-preview ' + '-tech-detect -cdn -vhost ' + '-random-agent -no-color -json' + ), + 'optional': { + 'threads': '-threads {threads}', + 'rate_limit': '-rate-limit {rate_limit}', + 'request_timeout': '-timeout {request_timeout}', + 'retries': '-retries {retries}' + } + }, +} + + +# ==================== 目录扫描 ==================== + +DIRECTORY_SCAN_COMMANDS = { + 'ffuf': { + 'base': 'ffuf -u {url}/FUZZ -se -ac -sf -json -w {wordlist}', + 'optional': { + 'delay': '-p {delay}', + 'threads': '-t {threads}', + 'request_timeout': '-timeout {request_timeout}', + 'match_codes': '-mc {match_codes}', + 'rate': '-rate {rate}' + } + }, +} + + +# ==================== URL 获取 ==================== + +URL_FETCH_COMMANDS = { + 'waymore': { + 'base': 'waymore -i {domain_name} -mode U -oU {output_file}', + 'input_type': 'domain_name' + }, + + 'katana': { + 'base': ( + 'katana -list {sites_file} -o {output_file} ' + '-jc ' # 开启 JavaScript 爬取 + 自动解析 .js 文件里的所有端点(最重要) + '-xhr ' # 额外从 JS 中提取 XHR/Fetch 请求的 API 路径(再多挖 10-20% 隐藏接口) + '-kf all ' # 在每个目录下自动 fuzz 所有已知敏感文件(.env、.git、backup、config、ds_store 等 5000+ 条) + '-fs rdn ' # 智能过滤相对重复+噪声路径(分页、?id=1/2/3 这类垃圾全干掉,输出极干净) + '-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" ' # 固定一个正常 UA(Katana 默认会随机,但固定更低调) + '-silent ' # 安静模式(终端不输出进度条,只出 URL) + ), + 'optional': { + 'depth': '-d {depth}', # 爬取最大深度(平衡深度与时间,默认 3,推荐 5) + 'threads': '-c {threads}', # 全局并发数(极低并发最像真人,推荐 10) + 'rate_limit': '-rl {rate_limit}', # 全局硬限速:每秒最多 N 个请求(WAF 几乎不报警,推荐 30) + 'random_delay': '-rd {random_delay}', # 每次请求之间随机延迟 N 秒(再加一层人性化,推荐 1) + 'retry': '-retry {retry}', # 失败请求自动重试次数(网络抖动不丢包,推荐 2) + 'request_timeout': '-timeout {request_timeout}' # 单请求超时秒数(防卡死,推荐 12) + }, + 'input_type': 'sites_file' + }, + + 'uro': { + 'base': 'uro -i {input_file} -o {output_file}', + 'optional': { + 'whitelist': '-w {whitelist}', # 只保留指定扩展名的 URL(空格分隔) + 'blacklist': '-b {blacklist}', # 排除指定扩展名的 URL(空格分隔) + 'filters': '-f {filters}' # 额外的过滤规则(空格分隔) + } + }, + + 'httpx': { + 'base': ( + 'httpx -l {url_file} ' + '-status-code -content-type -content-length ' + '-location -title -server -body-preview ' + '-tech-detect -cdn -vhost ' + '-random-agent -no-color -json' + ), + 'optional': { + 'threads': '-threads {threads}', + 'rate_limit': '-rate-limit {rate_limit}', + 'request_timeout': '-timeout {request_timeout}', + 'retries': '-retries {retries}' + } + }, +} + +VULN_SCAN_COMMANDS = { + 'dalfox_xss': { + 'base': ( + 'dalfox --silence --no-color --no-spinner ' + '--skip-bav ' + 'file {endpoints_file} ' + '--waf-evasion ' + '--format json' + ), + 'optional': { + 'only_poc': '--only-poc {only_poc}', + 'ignore_return': '--ignore-return {ignore_return}', + 'blind_xss_server': '-b {blind_xss_server}', + 'delay': '--delay {delay}', + 'request_timeout': '--timeout {request_timeout}', + # 是否追加 UA 头,由 user_agent 是否存在决定 + 'user_agent': '--user-agent "{user_agent}"', + 'worker': '--worker {worker}', + }, + 'input_type': 'endpoints_file', + }, + 'nuclei': { + # nuclei 漏洞扫描 + # -j: JSON 输出 + # -silent: 静默模式 + # -l: 输入 URL 列表文件 + # -t: 模板目录路径(支持多个仓库,多次 -t 由 template_args 直接拼接) + 'base': 'nuclei -j -silent -l {endpoints_file} {template_args}', + 'optional': { + 'concurrency': '-c {concurrency}', # 并发数(默认 25) + 'rate_limit': '-rl {rate_limit}', # 每秒请求数限制 + 'request_timeout': '-timeout {request_timeout}', # 请求超时秒数 + 'bulk_size': '-bs {bulk_size}', # 批量处理大小 + 'retries': '-retries {retries}', # 重试次数 + 'severity': '-severity {severity}', # 过滤严重性(info,low,medium,high,critical) + 'tags': '-tags {tags}', # 过滤标签 + 'exclude_tags': '-etags {exclude_tags}', # 排除标签 + }, + 'input_type': 'endpoints_file', + }, +} + + +# ==================== 工具映射 ==================== + +COMMAND_TEMPLATES = { + 'subdomain_discovery': SUBDOMAIN_DISCOVERY_COMMANDS, + 'port_scan': PORT_SCAN_COMMANDS, + 'site_scan': SITE_SCAN_COMMANDS, + 'directory_scan': DIRECTORY_SCAN_COMMANDS, + 'url_fetch': URL_FETCH_COMMANDS, + 'vuln_scan': VULN_SCAN_COMMANDS, +} + +# ==================== 扫描类型配置 ==================== + +# 执行阶段定义(按顺序执行) +EXECUTION_STAGES = [ + { + 'mode': 'sequential', + 'flows': ['subdomain_discovery', 'port_scan', 'site_scan'] + }, + { + 'mode': 'parallel', + 'flows': ['url_fetch', 'directory_scan'] + }, + { + 'mode': 'sequential', + 'flows': ['vuln_scan'] + }, +] + + +def get_supported_scan_types(): + """ + 获取支持的扫描类型 + + Returns: + list: 支持的扫描类型列表(从 COMMAND_TEMPLATES 的 keys 获取) + """ + return list(COMMAND_TEMPLATES.keys()) + + +def get_command_template(scan_type: str, tool_name: str) -> dict: + """ + 获取工具的命令模板 + + Args: + scan_type: 扫描类型 + tool_name: 工具名称 + + Returns: + 命令模板字典,如果未找到则返回 None + """ + templates = COMMAND_TEMPLATES.get(scan_type, {}) + return templates.get(tool_name) diff --git a/backend/apps/scan/configs/engine_config_example.yaml b/backend/apps/scan/configs/engine_config_example.yaml new file mode 100644 index 00000000..85ce2a02 --- /dev/null +++ b/backend/apps/scan/configs/engine_config_example.yaml @@ -0,0 +1,224 @@ +# 引擎配置 +# +# ==================== 参数命名规范 ==================== +# 所有参数统一用中划线,如 rate-limit, request-timeout, wordlist-name +# - 贴近 CLI 参数风格,用户更直观 +# - 系统会自动转换为下划线供代码使用 +# +# ==================== 必需参数 ==================== +# - enabled: 是否启用工具(true/false) +# - timeout: 超时时间(秒),工具执行超过此时间会被强制终止 +# +# 使用方式: +# - 在前端创建扫描引擎时,将此配置保存到数据库 +# - 执行扫描时,从数据库读取配置并传递给 Flow +# - 取消注释可选参数即可启用 + +# ==================== 子域名发现 ==================== +# +# 流程说明: +# Stage 1: 被动收集(并行) - 必选,至少启用一个工具 +# Stage 2: 字典爆破(可选) - 使用字典暴力枚举子域名 +# Stage 3: 变异生成 + 验证(可选) - 基于已发现域名生成变异,流式验证存活 +# Stage 4: DNS 存活验证(可选) - 验证所有候选域名是否能解析 +# +# 灵活组合:可以关闭 2/3/4 中的任意阶段,最终结果会根据实际执行的阶段动态决定 +# +subdomain_discovery: + # === Stage 1: 被动收集工具(并行执行)=== + passive_tools: + subfinder: + enabled: true + timeout: 7200 # 2小时 + # threads: 10 # 可选,并发 goroutine 数 + + amass_passive: + enabled: true + timeout: 7200 # 2小时 + + amass_active: + enabled: true # 主动枚举 + 爆破 + timeout: 7200 + + sublist3r: + enabled: true + timeout: 7200 + # threads: 50 # 可选,线程数 + + assetfinder: + enabled: true + timeout: 7200 + + # === Stage 2: 主动字典爆破(可选)=== + bruteforce: + enabled: false # 是否启用字典爆破 + subdomain_bruteforce: + timeout: auto # 自动根据字典行数计算(后续代码中按行数 * 3 秒实现) + wordlist-name: subdomains-top1million-110000.txt # 字典名称,对应「字典管理」中的 Wordlist.name + + # === Stage 3: 变异生成 + 存活验证(可选,流式管道避免 OOM)=== + permutation: + enabled: true # 是否启用变异生成 + subdomain_permutation_resolve: + timeout: 7200 # 2小时(变异量大时需要更长时间) + + # === Stage 4: DNS 存活验证(可选)=== + resolve: + enabled: true # 是否启用存活验证 + subdomain_resolve: + timeout: auto # 自动根据候选子域数量计算(在 Flow 中按行数 * 3 秒实现) + + +# ==================== 端口扫描 ==================== +port_scan: + tools: + naabu_active: + enabled: true + timeout: auto # 自动计算(根据:目标数 × 端口数 × 0.5秒) + # 例如:100个域名 × 100个端口 × 0.5 = 5000秒 + # 10个域名 × 1000个端口 × 0.5 = 5000秒 + # 超时范围:60秒 ~ 2天(172800秒) + # 或者手动指定:timeout: 3600 + threads: 200 # 可选,并发连接数(默认 5) + # ports: 1-65535 # 可选,扫描端口范围(默认 1-65535) + top-ports: 100 # 可选,Scan for nmap top 100 ports(影响 timeout 计算) + rate: 10 # 可选,扫描速率(默认 10) + + naabu_passive: + enabled: true + timeout: auto # 自动计算(被动扫描通常较快,端口数默认为 100) + # 被动扫描,使用被动数据源,无需额外配置 + +# ==================== 站点扫描 ==================== +site_scan: + tools: + httpx: + enabled: true + timeout: auto # 自动计算(根据URL数量,每个URL 1秒) + # 或者手动指定:timeout: 3600 + # threads: 50 # 可选,并发线程数(httpx 默认 50) + # rate-limit: 150 # 可选,每秒发送的请求数量(httpx 默认 150) + # request-timeout: 10 # 可选,单个请求的超时时间(httpx 默认 10)秒 + # retries: 2 # 可选,请求失败重试次数 + +# ==================== 目录扫描 ==================== +directory_scan: + tools: + ffuf: + enabled: true + timeout: auto # 自动计算超时时间(根据字典行数) + # 计算公式:字典行数 × 0.02秒/词 + # 超时范围:60秒 ~ 7200秒(2小时) + # 也可以手动指定固定超时(如 300) + wordlist-name: dir_default.txt # 字典名称(必需),对应「字典管理」中唯一的 Wordlist.name + # 安装时会自动初始化名为 dir_default.txt 的默认目录字典 + # ffuf 会逐行读取字典文件,将每行作为 FUZZ 关键字的替换值 + delay: 0.1-2.0 # Seconds of delay between requests, or a range of random delay + # For example "0.1" or "0.1-2.0" + threads: 10 # Number of concurrent threads (default: 40) + request-timeout: 10 # HTTP request timeout in seconds (default: 10) + match-codes: 200,201,301,302,401,403 # Match HTTP status codes, comma separated + # rate: 0 # Rate of requests per second (default: 0) + +# ==================== URL 获取 ==================== +url_fetch: + tools: + waymore: + enabled: true + timeout: 3600 # 工具级别总超时:固定 3600 秒(按域名 target_name 输入) + # 如果目标较大或希望更快/更慢,可根据需要手动调整秒数 + # 输入类型:domain_name(域名级别,自动去重同域名站点) + + katana: + enabled: true + timeout: auto # 工具级别总超时:自动计算(根据站点数量) + # 或手动指定:timeout: 300 + + # ========== 核心功能参数(已在命令中固定开启) ========== + # -jc: JavaScript 爬取 + 自动解析 .js 文件里的所有端点(最重要) + # -xhr: 从 JS 中提取 XHR/Fetch 请求的 API 路径(再多挖 10-20% 隐藏接口) + # -kf all: 自动 fuzz 所有已知敏感文件(.env、.git、backup、config 等 5000+ 条) + # -fs rdn: 智能过滤重复+噪声路径(分页、?id=1/2/3 全干掉,输出极干净) + + # ========== 可选参数(推荐配置) ========== + depth: 5 # 爬取最大深度(平衡深度与时间,默认 3,推荐 5) + threads: 10 # 全局并发数(极低并发最像真人,推荐 10) + rate-limit: 30 # 全局硬限速:每秒最多 30 个请求(WAF 几乎不报警) + random-delay: 1 # 每次请求之间随机延迟 0.5~1.5 秒(再加一层人性化) + retry: 2 # 失败请求自动重试 2 次(网络抖动不丢包) + request-timeout: 12 # 单请求超时 12 秒(防卡死,katana 参数名是 -timeout) + + # 输入类型:url(站点级别,每个站点单独爬取) + + uro: + enabled: true + timeout: auto # 自动计算(根据 URL 数量,每 100 个约 1 秒) + # 范围:30 秒 ~ 300 秒 + # 或手动指定:timeout: 60 + + # ========== 可选参数 ========== + # whitelist: # 只保留指定扩展名的 URL(如:php,asp,jsp) + # - php + # - asp + # blacklist: # 排除指定扩展名的 URL(静态资源) + # - jpg + # - jpeg + # - png + # - gif + # - svg + # - ico + # - css + # - woff + # - woff2 + # - ttf + # - eot + # - mp4 + # - mp3 + # - pdf + # filters: # 额外的过滤规则,参考 uro 文档 + # - hasparams # 只保留有参数的 URL + # - hasext # 只保留有扩展名的 URL + # - vuln # 只保留可能有漏洞的 URL + + # 用途:清理合并后的 URL 列表,去除冗余和无效 URL + # 输入类型:merged_file(合并后的 URL 文件) + # 输出:清理后的 URL 列表 + + httpx: + enabled: true + timeout: auto # 自动计算(根据 URL 数量,每个 URL 1 秒) + # 或手动指定:timeout: 600 + # threads: 50 # 可选,并发线程数(httpx 默认 50) + # rate-limit: 150 # 可选,每秒发送的请求数量(httpx 默认 150) + # request-timeout: 10 # 可选,单个请求的超时时间(httpx 默认 10)秒 + # retries: 2 # 可选,请求失败重试次数 + + # 用途:判断 URL 存活,过滤无效 URL + # 输入类型:url_file(URL 列表文件) + # 输出:存活的 URL 及其响应信息(status, title, server, tech 等) + +# ==================== 漏洞扫描 ==================== +vuln_scan: + tools: + dalfox_xss: + enabled: true + timeout: auto # 自动计算(根据 endpoints 行数 × 100 秒),或手动指定秒数如 timeout: 600 + request-timeout: 10 # Dalfox 单个请求的超时时间,对应命令行 --timeout + only-poc: r # 只输出 POC 结果(r: 反射型) + ignore-return: "302,404,403" # 忽略这些返回码 + # blind-xss-server: xxx # 可选:盲打 XSS 回连服务地址,需要时再开启 + delay: 100 # Dalfox 扫描内部延迟参数 + worker: 10 # Dalfox worker 数量 + user-agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" # 默认 UA,可根据需要修改 + + nuclei: + enabled: true + timeout: auto # 自动计算(根据 endpoints 行数),或手动指定秒数 + template-repo-names: # 模板仓库列表(必填,数组写法),对应「Nuclei 模板」中的仓库名 + - nuclei-templates # Worker 会自动同步到与 Server 一致的 commit 版本 + # - nuclei-custom # 可追加自定义仓库,按顺序依次 -t 传入 + concurrency: 25 # 并发数(默认 25) + rate-limit: 150 # 每秒请求数限制(默认 150) + request-timeout: 5 # 单个请求超时秒数(默认 5) + severity: medium,high,critical # 只扫描中高危,降低噪音(逗号分隔) + # tags: cve,rce # 可选:只使用指定标签的模板 diff --git a/backend/apps/scan/flows/__init__.py b/backend/apps/scan/flows/__init__.py new file mode 100644 index 00000000..311c3445 --- /dev/null +++ b/backend/apps/scan/flows/__init__.py @@ -0,0 +1,12 @@ +"""Prefect Flows(编排层) + +注意:大部分 Flow 已迁移到 scripts/ 目录作为普通脚本执行 +""" + +from .initiate_scan_flow import initiate_scan_flow +from .subdomain_discovery_flow import subdomain_discovery_flow + +__all__ = [ + 'initiate_scan_flow', + 'subdomain_discovery_flow', +] diff --git a/backend/apps/scan/flows/directory_scan_flow.py b/backend/apps/scan/flows/directory_scan_flow.py new file mode 100644 index 00000000..d6914903 --- /dev/null +++ b/backend/apps/scan/flows/directory_scan_flow.py @@ -0,0 +1,482 @@ +""" +目录扫描 Flow + +负责编排目录扫描的完整流程 + +架构: +- Flow 负责编排多个原子 Task +- 支持串行执行扫描工具(流式处理) +- 每个 Task 可独立重试 +- 配置由 YAML 解析 +""" + +# Django 环境初始化(导入即生效) +from apps.common.prefect_django_setup import setup_django_for_prefect + +from prefect import flow + +import logging +import os +import subprocess +from pathlib import Path + +from apps.scan.tasks.directory_scan import ( + export_sites_task, + run_and_stream_save_directories_task +) +from apps.scan.handlers.scan_flow_handlers import ( + on_scan_flow_running, + on_scan_flow_completed, + on_scan_flow_failed, +) +from apps.scan.utils import config_parser, build_scan_command, ensure_wordlist_local + +logger = logging.getLogger(__name__) + + +def calculate_directory_scan_timeout( + tool_config: dict, + base_per_word: float = 1.0, + min_timeout: int = 60, + max_timeout: int = 7200 +) -> int: + """ + 根据字典行数计算目录扫描超时时间 + + 计算公式:超时时间 = 字典行数 × 每个单词基础时间 + 超时范围:60秒 ~ 2小时(7200秒) + + Args: + tool_config: 工具配置字典,包含 wordlist 路径 + base_per_word: 每个单词的基础时间(秒),默认 1.0秒 + min_timeout: 最小超时时间(秒),默认 60秒 + max_timeout: 最大超时时间(秒),默认 7200秒(2小时) + + Returns: + int: 计算出的超时时间(秒),范围:60 ~ 7200 + + Example: + # 1000行字典 × 1.0秒 = 1000秒 → 限制为7200秒中的 1000秒 + # 10000行字典 × 1.0秒 = 10000秒 → 限制为7200秒(最大值) + timeout = calculate_directory_scan_timeout( + tool_config={'wordlist': '/path/to/wordlist.txt'} + ) + """ + try: + # 从 tool_config 中获取 wordlist 路径 + wordlist_path = tool_config.get('wordlist') + if not wordlist_path: + logger.warning("工具配置中未指定 wordlist,使用默认超时: %d秒", min_timeout) + return min_timeout + + # 展开用户目录(~) + wordlist_path = os.path.expanduser(wordlist_path) + + # 检查文件是否存在 + if not os.path.exists(wordlist_path): + logger.warning("字典文件不存在: %s,使用默认超时: %d秒", wordlist_path, min_timeout) + return min_timeout + + # 使用 wc -l 快速统计字典行数 + result = subprocess.run( + ['wc', '-l', wordlist_path], + capture_output=True, + text=True, + check=True + ) + # wc -l 输出格式:行数 + 空格 + 文件名 + line_count = int(result.stdout.strip().split()[0]) + + # 计算超时时间 + timeout = int(line_count * base_per_word) + + # 设置合理的下限(不再设置上限) + timeout = max(min_timeout, timeout) + + logger.info( + "目录扫描超时计算 - 字典: %s, 行数: %d, 基础时间: %.3f秒/词, 计算超时: %d秒", + wordlist_path, line_count, base_per_word, timeout + ) + + return timeout + + except subprocess.CalledProcessError as e: + logger.error("统计字典行数失败: %s", e) + # 失败时返回默认超时 + return min_timeout + except (ValueError, IndexError) as e: + logger.error("解析字典行数失败: %s", e) + return min_timeout + except Exception as e: + logger.error("计算超时时间异常: %s", e) + return min_timeout + + +def _setup_directory_scan_directory(scan_workspace_dir: str) -> Path: + """ + 创建并验证目录扫描工作目录 + + Args: + scan_workspace_dir: 扫描工作空间目录 + + Returns: + Path: 目录扫描目录路径 + + Raises: + RuntimeError: 目录创建或验证失败 + """ + directory_scan_dir = Path(scan_workspace_dir) / 'directory_scan' + directory_scan_dir.mkdir(parents=True, exist_ok=True) + + if not directory_scan_dir.is_dir(): + raise RuntimeError(f"目录扫描目录创建失败: {directory_scan_dir}") + if not os.access(directory_scan_dir, os.W_OK): + raise RuntimeError(f"目录扫描目录不可写: {directory_scan_dir}") + + return directory_scan_dir + + +def _export_site_urls(target_id: int, directory_scan_dir: Path) -> tuple[str, int]: + """ + 导出目标下的所有站点 URL 到文件 + + Args: + target_id: 目标 ID + directory_scan_dir: 目录扫描目录 + + Returns: + tuple: (sites_file, site_count) + + Raises: + ValueError: 站点数量为 0 + """ + logger.info("Step 1: 导出目标的所有站点 URL") + + sites_file = str(directory_scan_dir / 'sites.txt') + export_result = export_sites_task( + target_id=target_id, + output_file=sites_file, + batch_size=1000 # 每次读取 1000 条,优化内存占用 + ) + + site_count = export_result['total_count'] + + logger.info( + "✓ 站点 URL 导出完成 - 文件: %s, 数量: %d", + export_result['output_file'], + site_count + ) + + if site_count == 0: + logger.warning("目标下没有站点,无法执行目录扫描") + # 不抛出异常,由上层决定如何处理 + # raise ValueError("目标下没有站点,无法执行目录扫描") + + return export_result['output_file'], site_count + + +def _run_scans_sequentially( + enabled_tools: dict, + sites_file: str, + directory_scan_dir: Path, + scan_id: int, + target_id: int, + site_count: int, + target_name: str +) -> tuple[int, int, list]: + """ + 串行执行目录扫描任务(支持多工具) + + Args: + enabled_tools: 启用的工具配置字典 + sites_file: 站点文件路径 + directory_scan_dir: 目录扫描目录 + scan_id: 扫描任务 ID + target_id: 目标 ID + site_count: 站点数量 + target_name: 目标名称(用于错误日志) + + Returns: + tuple: (total_directories, processed_sites, failed_sites) + """ + # 读取站点列表 + sites = [] + with open(sites_file, 'r', encoding='utf-8') as f: + for line in f: + site_url = line.strip() + if site_url: + sites.append(site_url) + + logger.info("准备扫描 %d 个站点,使用工具: %s", len(sites), ', '.join(enabled_tools.keys())) + + total_directories = 0 + processed_sites_set = set() # 使用 set 避免重复计数 + failed_sites = [] + + # 遍历每个工具 + for tool_name, tool_config in enabled_tools.items(): + logger.info("="*60) + logger.info("使用工具: %s", tool_name) + logger.info("="*60) + + # 如果配置了 wordlist_name,则先确保本地存在对应的字典文件(含 hash 校验) + wordlist_name = tool_config.get('wordlist_name') + if wordlist_name: + try: + local_wordlist_path = ensure_wordlist_local(wordlist_name) + tool_config['wordlist'] = local_wordlist_path + except Exception as exc: + logger.error("为工具 %s 准备字典失败: %s", tool_name, exc) + # 当前工具无法执行,将所有站点视为失败,继续下一个工具 + failed_sites.extend(sites) + continue + + # 逐个站点执行扫描 + for idx, site_url in enumerate(sites, 1): + logger.info( + "[%d/%d] 开始扫描站点: %s (工具: %s)", + idx, len(sites), site_url, tool_name + ) + + # 使用统一的命令构建器 + try: + command = build_scan_command( + tool_name=tool_name, + scan_type='directory_scan', + command_params={ + 'url': site_url + }, + tool_config=tool_config + ) + except Exception as e: + logger.error( + "✗ [%d/%d] 构建 %s 命令失败: %s - 站点: %s", + idx, len(sites), tool_name, e, site_url + ) + failed_sites.append(site_url) + continue + + # 单个站点超时:从配置中获取(支持 'auto' 动态计算) + # ffuf 逐个站点扫描,timeout 就是单个站点的超时时间 + site_timeout = tool_config.get('timeout', 300) + if site_timeout == 'auto': + # 动态计算超时时间(基于字典行数) + site_timeout = calculate_directory_scan_timeout(tool_config) + logger.info(f"✓ 工具 {tool_name} 动态计算 timeout: {site_timeout}秒") + + # 生成日志文件路径 + from datetime import datetime + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + log_file = directory_scan_dir / f"{tool_name}_{timestamp}_{idx}.log" + + try: + # 直接调用 task(串行执行) + result = run_and_stream_save_directories_task( + cmd=command, + tool_name=tool_name, # 新增:工具名称 + scan_id=scan_id, + target_id=target_id, + site_url=site_url, + cwd=str(directory_scan_dir), + shell=True, + batch_size=1000, + timeout=site_timeout, + log_file=str(log_file) # 新增:日志文件路径 + ) + + total_directories += result.get('created_directories', 0) + processed_sites_set.add(site_url) # 使用 set 记录成功的站点 + + logger.info( + "✓ [%d/%d] 站点扫描完成: %s - 发现 %d 个目录", + idx, len(sites), site_url, + result.get('created_directories', 0) + ) + + except subprocess.TimeoutExpired as exc: + # 超时异常单独处理 + failed_sites.append(site_url) + logger.warning( + "⚠️ [%d/%d] 站点扫描超时: %s - 超时配置: %d秒\n" + "注意:超时前已解析的目录数据已保存到数据库,但扫描未完全完成。", + idx, len(sites), site_url, site_timeout + ) + except Exception as exc: + # 其他异常 + failed_sites.append(site_url) + logger.error( + "✗ [%d/%d] 站点扫描失败: %s - 错误: %s", + idx, len(sites), site_url, exc + ) + + # 每 10 个站点输出进度 + if idx % 10 == 0: + logger.info( + "进度: %d/%d (%.1f%%) - 已发现 %d 个目录", + idx, len(sites), idx/len(sites)*100, total_directories + ) + + # 计算成功和失败的站点数 + processed_count = len(processed_sites_set) + + if failed_sites: + logger.warning( + "部分站点扫描失败: %d/%d", + len(failed_sites), len(sites) + ) + + logger.info( + "✓ 串行目录扫描执行完成 - 成功: %d/%d, 失败: %d, 总目录数: %d", + processed_count, len(sites), len(failed_sites), total_directories + ) + + return total_directories, processed_count, failed_sites + + +@flow( + name="directory_scan", + log_prints=True, + on_running=[on_scan_flow_running], + on_completion=[on_scan_flow_completed], + on_failure=[on_scan_flow_failed], +) +def directory_scan_flow( + scan_id: int, + target_name: str, + target_id: int, + scan_workspace_dir: str, + enabled_tools: dict +) -> dict: + """ + 目录扫描 Flow + + 主要功能: + 1. 从 target 获取所有站点的 URL + 2. 对每个站点 URL 执行目录扫描(支持 ffuf 等工具) + 3. 流式保存扫描结果到数据库 Directory 表 + + 工作流程: + Step 0: 创建工作目录 + Step 1: 导出站点 URL 列表到文件(供扫描工具使用) + Step 2: 验证工具配置 + Step 3: 串行执行扫描工具并实时保存结果 + + ffuf 输出字段: + - url: 发现的目录/文件 URL + - length: 响应内容长度 + - status: HTTP 状态码 + - words: 响应内容单词数 + - lines: 响应内容行数 + - content_type: 内容类型 + - duration: 请求耗时 + + Args: + scan_id: 扫描任务 ID + target_name: 目标名称 + target_id: 目标 ID + scan_workspace_dir: 扫描工作空间目录 + enabled_tools: 启用的工具配置字典 + + Returns: + dict: { + 'success': bool, + 'scan_id': int, + 'target': str, + 'scan_workspace_dir': str, + 'sites_file': str, + 'site_count': int, + 'total_directories': int, # 发现的总目录数 + 'processed_sites': int, # 成功处理的站点数 + 'failed_sites_count': int, # 失败的站点数 + 'executed_tasks': list + } + + Raises: + ValueError: 参数错误 + RuntimeError: 执行失败 + """ + try: + logger.info( + "="*60 + "\n" + + "开始目录扫描\n" + + f" Scan ID: {scan_id}\n" + + f" Target: {target_name}\n" + + f" Workspace: {scan_workspace_dir}\n" + + "="*60 + ) + + # 参数验证 + if scan_id is None: + raise ValueError("scan_id 不能为空") + if not target_name: + raise ValueError("target_name 不能为空") + if target_id is None: + raise ValueError("target_id 不能为空") + if not scan_workspace_dir: + raise ValueError("scan_workspace_dir 不能为空") + if not enabled_tools: + raise ValueError("enabled_tools 不能为空") + + # Step 0: 创建工作目录 + directory_scan_dir = _setup_directory_scan_directory(scan_workspace_dir) + + # Step 1: 导出站点 URL + sites_file, site_count = _export_site_urls(target_id, directory_scan_dir) + + if site_count == 0: + logger.warning("目标下没有站点,跳过目录扫描") + return { + 'success': True, + 'scan_id': scan_id, + 'target': target_name, + 'scan_workspace_dir': scan_workspace_dir, + 'sites_file': sites_file, + 'site_count': 0, + 'total_directories': 0, + 'processed_sites': 0, + 'failed_sites_count': 0, + 'executed_tasks': ['export_sites'] + } + + # Step 2: 工具配置信息 + logger.info("Step 2: 工具配置信息") + logger.info( + "✓ 启用工具: %s", + ', '.join(enabled_tools.keys()) + ) + + # Step 3: 串行执行扫描工具并实时保存结果 + logger.info("Step 3: 串行执行扫描工具并实时保存结果") + total_directories, processed_sites, failed_sites = _run_scans_sequentially( + enabled_tools=enabled_tools, + sites_file=sites_file, + directory_scan_dir=directory_scan_dir, + scan_id=scan_id, + target_id=target_id, + site_count=site_count, + target_name=target_name + ) + + # 检查是否所有站点都失败 + if processed_sites == 0 and site_count > 0: + logger.warning("所有站点扫描均失败 - 总站点数: %d, 失败数: %d", site_count, len(failed_sites)) + # 不抛出异常,让扫描继续 + + logger.info("="*60 + "\n✓ 目录扫描完成\n" + "="*60) + + return { + 'success': True, + 'scan_id': scan_id, + 'target': target_name, + 'scan_workspace_dir': scan_workspace_dir, + 'sites_file': sites_file, + 'site_count': site_count, + 'total_directories': total_directories, + 'processed_sites': processed_sites, + 'failed_sites_count': len(failed_sites), + 'executed_tasks': ['export_sites', 'run_and_stream_save_directories'] + } + + except Exception as e: + logger.exception("目录扫描失败: %s", e) + raise \ No newline at end of file diff --git a/backend/apps/scan/flows/initiate_scan_flow.py b/backend/apps/scan/flows/initiate_scan_flow.py new file mode 100644 index 00000000..de75ad2c --- /dev/null +++ b/backend/apps/scan/flows/initiate_scan_flow.py @@ -0,0 +1,279 @@ +""" +扫描初始化 Flow + +负责编排扫描任务的初始化流程 + +职责: +- 使用 FlowOrchestrator 解析 YAML 配置 +- 在 Prefect Flow 中执行子 Flow(Subflow) +- 按照 YAML 顺序编排工作流 +- 不包含具体业务逻辑(由 Tasks 和 FlowOrchestrator 实现) + +架构: +- Flow: Prefect 编排层(本文件) +- FlowOrchestrator: 配置解析和执行计划(apps/scan/services/) +- Tasks: 执行层(apps/scan/tasks/) +- Handlers: 状态管理(apps/scan/handlers/) +""" + +# Django 环境初始化(导入即生效) +# 注意:动态扫描容器应使用 run_initiate_scan.py 启动,以便在导入前设置环境变量 +from apps.common.prefect_django_setup import setup_django_for_prefect + +from prefect import flow, task +from pathlib import Path +import logging + +from apps.scan.handlers import ( + on_initiate_scan_flow_running, + on_initiate_scan_flow_completed, + on_initiate_scan_flow_failed, +) +from prefect.futures import wait +from apps.scan.tasks.workspace_tasks import create_scan_workspace_task +from apps.scan.orchestrators import FlowOrchestrator + +logger = logging.getLogger(__name__) + + +@task(name="run_subflow") +def _run_subflow_task(scan_type: str, flow_func, flow_kwargs: dict): + """包装子 Flow 的 Task,用于在并行阶段并发执行子 Flow。""" + logger.info("开始执行子 Flow: %s", scan_type) + return flow_func(**flow_kwargs) + + +@flow( + name='initiate_scan', + description='扫描任务初始化流程', + log_prints=True, + on_running=[on_initiate_scan_flow_running], + on_completion=[on_initiate_scan_flow_completed], + on_failure=[on_initiate_scan_flow_failed], +) +def initiate_scan_flow( + scan_id: int, + target_name: str, + target_id: int, + scan_workspace_dir: str, + engine_name: str, + scheduled_scan_name: str | None = None, +) -> dict: + """ + 初始化扫描任务(动态工作流编排) + + 根据 YAML 配置动态编排工作流: + - 从数据库获取 engine_config (YAML) + - 检测启用的扫描类型 + - 按照定义的阶段执行: + Stage 1: Discovery (顺序执行) + - subdomain_discovery + - port_scan + - site_scan + Stage 2: Analysis (并行执行) + - url_fetch + - directory_scan + + Args: + scan_id: 扫描任务 ID + target_name: 目标名称 + target_id: 目标 ID + scan_workspace_dir: Scan 工作空间目录路径 + engine_name: 引擎名称(用于显示) + scheduled_scan_name: 定时扫描任务名称(可选,用于通知显示) + + Returns: + dict: 执行结果摘要 + + Raises: + ValueError: 参数验证失败或配置无效 + RuntimeError: 执行失败 + """ + try: + # ==================== 参数验证 ==================== + if not scan_id: + raise ValueError("scan_id is required") + if not scan_workspace_dir: + raise ValueError("scan_workspace_dir is required") + if not engine_name: + raise ValueError("engine_name is required") + + + logger.info( + "="*60 + "\n" + + "开始初始化扫描任务\n" + + f" Scan ID: {scan_id}\n" + + f" Target: {target_name}\n" + + f" Engine: {engine_name}\n" + + f" Workspace: {scan_workspace_dir}\n" + + "="*60 + ) + + # ==================== Task 1: 创建 Scan 工作空间 ==================== + scan_workspace_path = create_scan_workspace_task(scan_workspace_dir) + + # ==================== Task 2: 获取引擎配置 ==================== + from apps.scan.models import Scan + scan = Scan.objects.select_related('engine').get(id=scan_id) + engine_config = scan.engine.configuration + + # ==================== Task 3: 解析配置,生成执行计划 ==================== + orchestrator = FlowOrchestrator(engine_config) + + # FlowOrchestrator 已经解析了所有工具配置 + enabled_tools_by_type = orchestrator.enabled_tools_by_type + + logger.info( + f"执行计划生成成功:\n" + f" 扫描类型: {' → '.join(orchestrator.scan_types)}\n" + f" 总共 {len(orchestrator.scan_types)} 个 Flow" + ) + + # ==================== 初始化阶段进度 ==================== + # 在解析完配置后立即初始化,此时已有完整的 scan_types 列表 + from apps.scan.services import ScanService + scan_service = ScanService() + scan_service.init_stage_progress(scan_id, orchestrator.scan_types) + logger.info(f"✓ 初始化阶段进度 - Stages: {orchestrator.scan_types}") + + # ==================== 更新 Target 最后扫描时间 ==================== + # 在开始扫描时更新,表示"最后一次扫描开始时间" + from apps.targets.services import TargetService + target_service = TargetService() + target_service.update_last_scanned_at(target_id) + logger.info(f"✓ 更新 Target 最后扫描时间 - Target ID: {target_id}") + + # ==================== Task 3: 执行 Flow(动态阶段执行)==================== + # 注意:各阶段状态更新由 scan_flow_handlers.py 自动处理(running/completed/failed) + executed_flows = [] + results = {} + + # 通用执行参数 + flow_kwargs = { + 'scan_id': scan_id, + 'target_name': target_name, + 'target_id': target_id, + 'scan_workspace_dir': str(scan_workspace_path) + } + + def record_flow_result(scan_type, result=None, error=None): + """ + 统一的结果记录函数 + + Args: + scan_type: 扫描类型名称 + result: 执行结果(成功时) + error: 异常对象(失败时) + """ + if error: + # 失败处理:记录错误但不抛出异常,让扫描继续执行后续阶段 + error_msg = f"{scan_type} 执行失败: {str(error)}" + logger.warning(error_msg) + executed_flows.append(f"{scan_type} (失败)") + results[scan_type] = {'success': False, 'error': str(error)} + # 不再抛出异常,让扫描继续 + else: + # 成功处理 + executed_flows.append(scan_type) + results[scan_type] = result + logger.info(f"✓ {scan_type} 执行成功") + + def get_valid_flows(flow_names): + """ + 获取有效的 Flow 函数列表,并为每个 Flow 准备专属参数 + + Args: + flow_names: 扫描类型名称列表 + + Returns: + list: [(scan_type, flow_func, flow_specific_kwargs), ...] 有效的函数列表 + """ + valid_flows = [] + for scan_type in flow_names: + flow_func = orchestrator.get_flow_function(scan_type) + if flow_func: + # 为每个 Flow 准备专属的参数(包含对应的 enabled_tools) + flow_specific_kwargs = dict(flow_kwargs) + flow_specific_kwargs['enabled_tools'] = enabled_tools_by_type.get(scan_type, {}) + valid_flows.append((scan_type, flow_func, flow_specific_kwargs)) + else: + logger.warning(f"跳过未实现的 Flow: {scan_type}") + return valid_flows + + # --------------------------------------------------------- + # 动态阶段执行(基于 FlowOrchestrator 定义) + # --------------------------------------------------------- + for mode, enabled_flows in orchestrator.get_execution_stages(): + if mode == 'sequential': + # 顺序执行 + logger.info(f"\n{'='*60}\n顺序执行阶段: {', '.join(enabled_flows)}\n{'='*60}") + for scan_type, flow_func, flow_specific_kwargs in get_valid_flows(enabled_flows): + logger.info(f"\n{'='*60}\n执行 Flow: {scan_type}\n{'='*60}") + try: + result = flow_func(**flow_specific_kwargs) + record_flow_result(scan_type, result=result) + except Exception as e: + record_flow_result(scan_type, error=e) + + elif mode == 'parallel': + # 并行执行阶段:通过 Task 包装子 Flow,并使用 Prefect TaskRunner 并发运行 + logger.info(f"\n{'='*60}\n并行执行阶段: {', '.join(enabled_flows)}\n{'='*60}") + futures = [] + + # 提交所有并行子 Flow 任务 + for scan_type, flow_func, flow_specific_kwargs in get_valid_flows(enabled_flows): + logger.info(f"\n{'='*60}\n提交并行子 Flow 任务: {scan_type}\n{'='*60}") + future = _run_subflow_task.submit( + scan_type=scan_type, + flow_func=flow_func, + flow_kwargs=flow_specific_kwargs, + ) + futures.append((scan_type, future)) + + # 等待所有并行子 Flow 完成 + if futures: + wait([f for _, f in futures]) + + # 检查结果(复用统一的结果处理逻辑) + for scan_type, future in futures: + try: + result = future.result() + record_flow_result(scan_type, result=result) + except Exception as e: + record_flow_result(scan_type, error=e) + + # ==================== 完成 ==================== + logger.info( + "="*60 + "\n" + + "✓ 扫描任务初始化完成\n" + + f" 执行的 Flow: {', '.join(executed_flows)}\n" + + "="*60 + ) + + # ==================== 返回结果 ==================== + return { + 'success': True, + 'scan_id': scan_id, + 'target': target_name, + 'scan_workspace_dir': str(scan_workspace_path), + 'executed_flows': executed_flows, + 'results': results + } + + except ValueError as e: + # 参数错误 + logger.error("参数错误: %s", e) + raise + except RuntimeError as e: + # 执行失败 + logger.error("运行时错误: %s", e) + raise + except OSError as e: + # 文件系统错误(工作空间创建失败) + logger.error("文件系统错误: %s", e) + raise + except Exception as e: + # 其他未预期错误 + logger.exception("初始化扫描任务失败: %s", e) + # 注意:失败状态更新由 Prefect State Handlers 自动处理 + raise diff --git a/backend/apps/scan/flows/port_scan_flow.py b/backend/apps/scan/flows/port_scan_flow.py new file mode 100644 index 00000000..d18b659b --- /dev/null +++ b/backend/apps/scan/flows/port_scan_flow.py @@ -0,0 +1,528 @@ +""" +端口扫描 Flow + +负责编排端口扫描的完整流程 + +架构: +- Flow 负责编排多个原子 Task +- 支持串行执行扫描工具(流式处理) +- 每个 Task 可独立重试 +- 配置由 YAML 解析 +""" + +# Django 环境初始化(导入即生效) +from apps.common.prefect_django_setup import setup_django_for_prefect + +import logging +import os +import subprocess +from pathlib import Path +from typing import Callable +from prefect import flow +from apps.scan.tasks.port_scan import ( + export_scan_targets_task, + run_and_stream_save_ports_task +) +from apps.scan.handlers.scan_flow_handlers import ( + on_scan_flow_running, + on_scan_flow_completed, + on_scan_flow_failed, +) +from apps.scan.utils import config_parser, build_scan_command + +logger = logging.getLogger(__name__) + + +def calculate_port_scan_timeout( + tool_config: dict, + file_path: str, + base_per_pair: float = 0.5 +) -> int: + """ + 根据目标数量和端口数量计算超时时间 + + 计算公式:超时时间 = 目标数 × 端口数 × base_per_pair + 超时范围:60秒 ~ 2天(172800秒) + + Args: + tool_config: 工具配置字典,包含端口配置(ports, top-ports等) + file_path: 目标文件路径(域名/IP列表) + base_per_pair: 每个"端口-目标对"的基础时间(秒),默认 0.5秒 + + Returns: + int: 计算出的超时时间(秒),范围:60 ~ 172800 + + Example: + # 100个目标 × 100个端口 × 0.5秒 = 5000秒 + # 10个目标 × 1000个端口 × 0.5秒 = 5000秒 + timeout = calculate_port_scan_timeout( + tool_config={'top-ports': 100}, + file_path='/path/to/domains.txt' + ) + """ + try: + # 1. 统计目标数量 + result = subprocess.run( + ['wc', '-l', file_path], + capture_output=True, + text=True, + check=True + ) + target_count = int(result.stdout.strip().split()[0]) + + # 2. 解析端口数量 + port_count = _parse_port_count(tool_config) + + # 3. 计算超时时间 + # 总工作量 = 目标数 × 端口数 + total_work = target_count * port_count + timeout = int(total_work * base_per_pair) + + # 4. 设置合理的下限(不再设置上限) + min_timeout = 60 # 最小 60 秒 + timeout = max(min_timeout, timeout) + + logger.info( + f"计算端口扫描 timeout - " + f"目标数: {target_count}, " + f"端口数: {port_count}, " + f"总工作量: {total_work}, " + f"超时: {timeout}秒" + ) + return timeout + + except Exception as e: + logger.warning(f"计算 timeout 失败: {e},使用默认值 600秒") + return 600 + + +def _parse_port_count(tool_config: dict) -> int: + """ + 从工具配置中解析端口数量 + + 优先级: + 1. top-ports: N → 返回 N + 2. ports: "80,443,8080" → 返回逗号分隔的数量 + 3. ports: "1-1000" → 返回范围的大小 + 4. ports: "1-65535" → 返回 65535 + 5. 默认 → 返回 100(naabu 默认扫描 top 100) + + Args: + tool_config: 工具配置字典 + + Returns: + int: 端口数量 + """ + # 1. 检查 top-ports 配置 + if 'top-ports' in tool_config: + top_ports = tool_config['top-ports'] + if isinstance(top_ports, int) and top_ports > 0: + return top_ports + logger.warning(f"top-ports 配置无效: {top_ports},使用默认值") + + # 2. 检查 ports 配置 + if 'ports' in tool_config: + ports_str = str(tool_config['ports']).strip() + + # 2.1 逗号分隔的端口列表:80,443,8080 + if ',' in ports_str: + port_list = [p.strip() for p in ports_str.split(',') if p.strip()] + return len(port_list) + + # 2.2 端口范围:1-1000 + if '-' in ports_str: + try: + start, end = ports_str.split('-', 1) + start_port = int(start.strip()) + end_port = int(end.strip()) + + if 1 <= start_port <= end_port <= 65535: + return end_port - start_port + 1 + logger.warning(f"端口范围无效: {ports_str},使用默认值") + except ValueError: + logger.warning(f"端口范围解析失败: {ports_str},使用默认值") + + # 2.3 单个端口 + try: + port = int(ports_str) + if 1 <= port <= 65535: + return 1 + except ValueError: + logger.warning(f"端口配置解析失败: {ports_str},使用默认值") + + # 3. 默认值:naabu 默认扫描 top 100 端口 + return 100 + + +def _setup_port_scan_directory(scan_workspace_dir: str) -> Path: + """ + 创建并验证端口扫描工作目录 + + Args: + scan_workspace_dir: 扫描工作空间目录 + + Returns: + Path: 端口扫描目录路径 + + Raises: + RuntimeError: 目录创建或验证失败 + """ + port_scan_dir = Path(scan_workspace_dir) / 'port_scan' + port_scan_dir.mkdir(parents=True, exist_ok=True) + + if not port_scan_dir.is_dir(): + raise RuntimeError(f"端口扫描目录创建失败: {port_scan_dir}") + if not os.access(port_scan_dir, os.W_OK): + raise RuntimeError(f"端口扫描目录不可写: {port_scan_dir}") + + return port_scan_dir + + +def _export_scan_targets(target_id: int, port_scan_dir: Path) -> tuple[str, int, str]: + """ + 导出扫描目标到文件 + + 根据 Target 类型自动决定导出内容: + - DOMAIN: 从 Subdomain 表导出子域名 + - IP: 直接写入 target.name + - CIDR: 展开 CIDR 范围内的所有 IP + + Args: + target_id: 目标 ID + port_scan_dir: 端口扫描目录 + + Returns: + tuple: (targets_file, target_count, target_type) + """ + logger.info("Step 1: 导出扫描目标列表") + + targets_file = str(port_scan_dir / 'targets.txt') + export_result = export_scan_targets_task( + target_id=target_id, + output_file=targets_file, + batch_size=1000 # 每次读取 1000 条,优化内存占用 + ) + + target_count = export_result['total_count'] + target_type = export_result.get('target_type', 'unknown') + + logger.info( + "✓ 扫描目标导出完成 - 类型: %s, 文件: %s, 数量: %d", + target_type, + export_result['output_file'], + target_count + ) + + if target_count == 0: + logger.warning("目标下没有可扫描的地址,无法执行端口扫描") + + return export_result['output_file'], target_count, target_type + + +def _run_scans_sequentially( + enabled_tools: dict, + domains_file: str, + port_scan_dir: Path, + scan_id: int, + target_id: int, + target_name: str +) -> tuple[dict, int, list, list]: + """ + 串行执行端口扫描任务 + + Args: + enabled_tools: 已启用的工具配置字典 + domains_file: 域名文件路径 + port_scan_dir: 端口扫描目录 + scan_id: 扫描任务 ID + target_id: 目标 ID + target_name: 目标名称(用于错误日志) + + Returns: + tuple: (tool_stats, processed_records, successful_tool_names, failed_tools) + 注意:端口扫描是流式输出,不生成结果文件 + + Raises: + RuntimeError: 所有工具均失败 + """ + # ==================== 构建命令并串行执行 ==================== + + tool_stats = {} + processed_records = 0 + failed_tools = [] # 记录失败的工具(含原因) + + # for循环执行工具:按顺序串行运行每个启用的端口扫描工具 + for tool_name, tool_config in enabled_tools.items(): + # 1. 构建完整命令(变量替换) + try: + command = build_scan_command( + tool_name=tool_name, + scan_type='port_scan', + command_params={ + 'domains_file': domains_file # 对应 {domains_file} + }, + tool_config=tool_config #yaml的工具配置 + ) + except Exception as e: + reason = f"命令构建失败: {str(e)}" + logger.error(f"构建 {tool_name} 命令失败: {e}") + failed_tools.append({'tool': tool_name, 'reason': reason}) + continue + + # 2. 获取超时时间(支持 'auto' 动态计算) + config_timeout = tool_config['timeout'] + if config_timeout == 'auto': + # 动态计算超时时间 + config_timeout = calculate_port_scan_timeout( + tool_config=tool_config, + file_path=str(domains_file) + ) + logger.info(f"✓ 工具 {tool_name} 动态计算 timeout: {config_timeout}秒") + + # 2.1 生成日志文件路径 + from datetime import datetime + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + log_file = port_scan_dir / f"{tool_name}_{timestamp}.log" + + # 3. 执行扫描任务 + logger.info("开始执行 %s 扫描(超时: %d秒)...", tool_name, config_timeout) + + try: + # 直接调用 task(串行执行) + # 注意:端口扫描是流式输出到 stdout,不使用 output_file + result = run_and_stream_save_ports_task( + cmd=command, + tool_name=tool_name, # 工具名称 + scan_id=scan_id, + target_id=target_id, + cwd=str(port_scan_dir), + shell=True, + batch_size=1000, + timeout=config_timeout, + log_file=str(log_file) # 新增:日志文件路径 + ) + + tool_stats[tool_name] = { + 'command': command, + 'result': result, + 'timeout': config_timeout + } + processed_records += result.get('processed_records', 0) + logger.info( + "✓ 工具 %s 流式处理完成 - 记录数: %d", + tool_name, result.get('processed_records', 0) + ) + + except subprocess.TimeoutExpired as exc: + # 超时异常单独处理 + # 注意:流式处理任务超时时,已解析的数据已保存到数据库 + reason = f"执行超时(配置: {config_timeout}秒)" + failed_tools.append({'tool': tool_name, 'reason': reason}) + logger.warning( + "⚠️ 工具 %s 执行超时 - 超时配置: %d秒\n" + "注意:超时前已解析的端口数据已保存到数据库,但扫描未完全完成。", + tool_name, config_timeout + ) + except Exception as exc: + # 其他异常 + failed_tools.append({'tool': tool_name, 'reason': str(exc)}) + logger.error("工具 %s 执行失败: %s", tool_name, exc, exc_info=True) + + if failed_tools: + logger.warning( + "以下扫描工具执行失败: %s", + ', '.join([f['tool'] for f in failed_tools]) + ) + + if not tool_stats: + error_details = "; ".join([f"{f['tool']}: {f['reason']}" for f in failed_tools]) + logger.warning("所有端口扫描工具均失败 - 目标: %s, 失败工具: %s", target_name, error_details) + # 返回空结果,不抛出异常,让扫描继续 + return {}, 0, [], failed_tools + + # 动态计算成功的工具列表 + successful_tool_names = [name for name in enabled_tools.keys() + if name not in [f['tool'] for f in failed_tools]] + + logger.info( + "✓ 串行端口扫描执行完成 - 成功: %d/%d (成功: %s, 失败: %s)", + len(tool_stats), len(enabled_tools), + ', '.join(successful_tool_names) if successful_tool_names else '无', + ', '.join([f['tool'] for f in failed_tools]) if failed_tools else '无' + ) + + return tool_stats, processed_records, successful_tool_names, failed_tools + + +@flow( + name="port_scan", + log_prints=True, + on_running=[on_scan_flow_running], + on_completion=[on_scan_flow_completed], + on_failure=[on_scan_flow_failed], +) +def port_scan_flow( + scan_id: int, + target_name: str, + target_id: int, + scan_workspace_dir: str, + enabled_tools: dict +) -> dict: + """ + 端口扫描 Flow + + 主要功能: + 1. 扫描目标域名的开放端口(核心目标) + 2. 发现域名对应的 IP 地址(附带产物) + 3. 保存 IP 和端口的关联关系 + + 输出资产: + - Port:开放的端口列表(主要资产) + - IPAddress:域名对应的 IP 地址(附带资产) + + 工作流程: + Step 0: 创建工作目录 + Step 1: 导出域名列表到文件(供扫描工具使用) + Step 2: 解析配置,获取启用的工具 + Step 3: 串行执行扫描工具,运行端口扫描工具并实时解析输出到数据库(Subdomain → IPAddress → Port) + + Args: + scan_id: 扫描任务 ID + target_name: 域名 + target_id: 目标 ID + scan_workspace_dir: Scan 工作空间目录 + enabled_tools: 启用的工具配置字典 + + Returns: + dict: { + 'success': bool, + 'scan_id': int, + 'target': str, + 'scan_workspace_dir': str, + 'domains_file': str, + 'domain_count': int, + 'processed_records': int, + 'executed_tasks': list, + 'tool_stats': { + 'total': int, # 总工具数 + 'successful': int, # 成功工具数 + 'failed': int, # 失败工具数 + 'successful_tools': list[str], # 成功工具列表 ['naabu_active'] + 'failed_tools': list[dict], # 失败工具列表 [{'tool': 'naabu_passive', 'reason': '超时'}] + 'details': dict # 详细执行结果(保留向后兼容) + } + } + + Raises: + ValueError: 配置错误 + RuntimeError: 执行失败 + + Note: + 端口扫描的输出必然包含 IP 信息,因为: + - 扫描工具需要解析域名 → IP + - 端口属于 IP,而不是直接属于域名 + - 同一域名可能对应多个 IP(CDN、负载均衡) + """ + try: + # 参数验证 + if scan_id is None: + raise ValueError("scan_id 不能为空") + if not target_name: + raise ValueError("target_name 不能为空") + if target_id is None: + raise ValueError("target_id 不能为空") + if not scan_workspace_dir: + raise ValueError("scan_workspace_dir 不能为空") + if not enabled_tools: + raise ValueError("enabled_tools 不能为空") + + logger.info( + "="*60 + "\n" + + "开始端口扫描\n" + + f" Scan ID: {scan_id}\n" + + f" Target: {target_name}\n" + + f" Workspace: {scan_workspace_dir}\n" + + "="*60 + ) + + # Step 0: 创建工作目录 + port_scan_dir = _setup_port_scan_directory(scan_workspace_dir) + + # Step 1: 导出扫描目标列表到文件(根据 Target 类型自动决定内容) + targets_file, target_count, target_type = _export_scan_targets(target_id, port_scan_dir) + + if target_count == 0: + logger.warning("目标下没有可扫描的地址,跳过端口扫描") + return { + 'success': True, + 'scan_id': scan_id, + 'target': target_name, + 'scan_workspace_dir': scan_workspace_dir, + 'targets_file': targets_file, + 'target_count': 0, + 'target_type': target_type, + 'processed_records': 0, + 'executed_tasks': ['export_scan_targets'], + 'tool_stats': { + 'total': 0, + 'successful': 0, + 'failed': 0, + 'successful_tools': [], + 'failed_tools': [], + 'details': {} + } + } + + # Step 2: 工具配置信息 + logger.info("Step 2: 工具配置信息") + logger.info( + "✓ 启用工具: %s", + ', '.join(enabled_tools.keys()) + ) + + # Step 3: 串行执行扫描工具 + logger.info("Step 3: 串行执行扫描工具") + tool_stats, processed_records, successful_tool_names, failed_tools = _run_scans_sequentially( + enabled_tools=enabled_tools, + domains_file=targets_file, # 现在是 targets_file,兼容原参数名 + port_scan_dir=port_scan_dir, + scan_id=scan_id, + target_id=target_id, + target_name=target_name + ) + + logger.info("="*60 + "\n✓ 端口扫描完成\n" + "="*60) + + # 动态生成已执行的任务列表 + executed_tasks = ['export_scan_targets', 'parse_config'] + executed_tasks.extend([f'run_and_stream_save_ports ({tool})' for tool in tool_stats.keys()]) + + return { + 'success': True, + 'scan_id': scan_id, + 'target': target_name, + 'scan_workspace_dir': scan_workspace_dir, + 'targets_file': targets_file, + 'target_count': target_count, + 'target_type': target_type, + 'processed_records': processed_records, + 'executed_tasks': executed_tasks, + 'tool_stats': { + 'total': len(tool_stats) + len(failed_tools), + 'successful': len(successful_tool_names), + 'failed': len(failed_tools), + 'successful_tools': successful_tool_names, + 'failed_tools': failed_tools, # [{'tool': 'naabu_active', 'reason': '超时'}] + 'details': tool_stats # 详细结果(保留向后兼容) + } + } + + except ValueError as e: + logger.error("配置错误: %s", e) + raise + except RuntimeError as e: + logger.error("运行时错误: %s", e) + raise + except Exception as e: + logger.exception("端口扫描失败: %s", e) + raise diff --git a/backend/apps/scan/flows/site_scan_flow.py b/backend/apps/scan/flows/site_scan_flow.py new file mode 100644 index 00000000..4eb15c3d --- /dev/null +++ b/backend/apps/scan/flows/site_scan_flow.py @@ -0,0 +1,495 @@ + +""" +站点扫描 Flow + +负责编排站点扫描的完整流程 + +架构: +- Flow 负责编排多个原子 Task +- 支持串行执行扫描工具(流式处理) +- 每个 Task 可独立重试 +- 配置由 YAML 解析 +""" + +# Django 环境初始化(导入即生效) +from apps.common.prefect_django_setup import setup_django_for_prefect + +import logging +import os +import subprocess +from pathlib import Path +from typing import Callable +from prefect import flow +from apps.scan.tasks.site_scan import export_site_urls_task, run_and_stream_save_websites_task +from apps.scan.handlers.scan_flow_handlers import ( + on_scan_flow_running, + on_scan_flow_completed, + on_scan_flow_failed, +) +from apps.scan.utils import config_parser, build_scan_command + +logger = logging.getLogger(__name__) + + +def calculate_timeout_by_line_count( + tool_config: dict, + file_path: str, + base_per_time: int = 1 +) -> int: + """ + 根据文件行数计算 timeout + + 使用 wc -l 统计文件行数,根据行数和每行基础时间计算 timeout + + Args: + tool_config: 工具配置字典(此函数未使用,但保持接口一致性) + file_path: 要统计行数的文件路径 + base_per_time: 每行的基础时间(秒),默认1秒 + + Returns: + int: 计算出的超时时间(秒) + + Example: + timeout = calculate_timeout_by_line_count( + tool_config={}, + file_path='/path/to/urls.txt', + base_per_time=2 + ) + """ + try: + # 使用 wc -l 快速统计行数 + result = subprocess.run( + ['wc', '-l', file_path], + capture_output=True, + text=True, + check=True + ) + # wc -l 输出格式:行数 + 空格 + 文件名 + line_count = int(result.stdout.strip().split()[0]) + + # 计算 timeout:行数 × 每行基础时间 + timeout = line_count * base_per_time + + logger.info( + f"timeout 自动计算: 文件={file_path}, " + f"行数={line_count}, 每行时间={base_per_time}秒, timeout={timeout}秒" + ) + + return timeout + + except Exception as e: + # 如果 wc -l 失败,使用默认值 + logger.warning(f"wc -l 计算行数失败: {e},使用默认 timeout: 600秒") + return 600 + + +def _setup_site_scan_directory(scan_workspace_dir: str) -> Path: + """ + 创建并验证站点扫描工作目录 + + Args: + scan_workspace_dir: 扫描工作空间目录 + + Returns: + Path: 站点扫描目录路径 + + Raises: + RuntimeError: 目录创建或验证失败 + """ + site_scan_dir = Path(scan_workspace_dir) / 'site_scan' + site_scan_dir.mkdir(parents=True, exist_ok=True) + + if not site_scan_dir.is_dir(): + raise RuntimeError(f"站点扫描目录创建失败: {site_scan_dir}") + if not os.access(site_scan_dir, os.W_OK): + raise RuntimeError(f"站点扫描目录不可写: {site_scan_dir}") + + return site_scan_dir + + +def _export_site_urls(target_id: int, site_scan_dir: Path) -> tuple[str, int, int]: + """ + 导出站点 URL 到文件 + + Args: + target_id: 目标 ID + site_scan_dir: 站点扫描目录 + + Returns: + tuple: (urls_file, total_urls, association_count) + + Raises: + ValueError: URL 数量为 0 + """ + logger.info("Step 1: 导出站点URL列表") + + urls_file = str(site_scan_dir / 'site_urls.txt') + export_result = export_site_urls_task( + target_id=target_id, + output_file=urls_file, + batch_size=1000 # 每次处理1000个子域名 + ) + + total_urls = export_result['total_urls'] + association_count = export_result['association_count'] # 主机端口关联数 + + logger.info( + "✓ 站点URL导出完成 - 文件: %s, URL数量: %d, 关联数: %d", + export_result['output_file'], + total_urls, + association_count + ) + + if total_urls == 0: + logger.warning("目标下没有可用的站点URL,无法执行站点扫描") + # 不抛出异常,由上层决定如何处理 + # raise ValueError("目标下没有可用的站点URL,无法执行站点扫描") + + return export_result['output_file'], total_urls, association_count + + +def _run_scans_sequentially( + enabled_tools: dict, + urls_file: str, + total_urls: int, + site_scan_dir: Path, + scan_id: int, + target_id: int, + target_name: str +) -> tuple[dict, int, list, list]: + """ + 串行执行站点扫描任务 + + Args: + enabled_tools: 已启用的工具配置字典 + urls_file: URL 文件路径 + total_urls: URL 总数 + site_scan_dir: 站点扫描目录 + scan_id: 扫描任务 ID + target_id: 目标 ID + target_name: 目标名称(用于错误日志) + + Returns: + tuple: (tool_stats, processed_records, successful_tool_names, failed_tools) + + Raises: + RuntimeError: 所有工具均失败 + """ + tool_stats = {} + processed_records = 0 + failed_tools = [] + + for tool_name, tool_config in enabled_tools.items(): + # 1. 构建完整命令(变量替换) + try: + command = build_scan_command( + tool_name=tool_name, + scan_type='site_scan', + command_params={ + 'url_file': urls_file + }, + tool_config=tool_config + ) + except Exception as e: + reason = f"命令构建失败: {str(e)}" + logger.error(f"构建 {tool_name} 命令失败: {e}") + failed_tools.append({'tool': tool_name, 'reason': reason}) + continue + + # 2. 获取超时时间(支持 'auto' 动态计算) + config_timeout = tool_config.get('timeout', 300) + if config_timeout == 'auto': + # 动态计算超时时间 + timeout = calculate_timeout_by_line_count(tool_config, urls_file, base_per_time=1) + logger.info(f"✓ 工具 {tool_name} 动态计算 timeout: {timeout}秒") + else: + # 使用配置的超时时间和动态计算的较大值 + dynamic_timeout = calculate_timeout_by_line_count(tool_config, urls_file, base_per_time=1) + timeout = max(dynamic_timeout, config_timeout) + + # 2.1 生成日志文件路径(类似端口扫描) + from datetime import datetime + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + log_file = site_scan_dir / f"{tool_name}_{timestamp}.log" + + logger.info( + "开始执行 %s 站点扫描 - URL数: %d, 最终超时: %ds", + tool_name, total_urls, timeout + ) + + # 3. 执行扫描任务 + try: + # 流式执行扫描并实时保存结果 + result = run_and_stream_save_websites_task( + cmd=command, + tool_name=tool_name, # 新增:工具名称 + scan_id=scan_id, + target_id=target_id, + cwd=str(site_scan_dir), + shell=True, + batch_size=1000, + timeout=timeout, + log_file=str(log_file) # 新增:日志文件路径 + ) + + tool_stats[tool_name] = { + 'command': command, + 'result': result, + 'timeout': timeout + } + processed_records += result.get('processed_records', 0) + + logger.info( + "✓ 工具 %s 流式处理完成 - 处理记录: %d, 创建站点: %d, 跳过: %d", + tool_name, + result.get('processed_records', 0), + result.get('created_websites', 0), + result.get('skipped_no_subdomain', 0) + result.get('skipped_failed', 0) + ) + + except subprocess.TimeoutExpired as exc: + # 超时异常单独处理 + reason = f"执行超时(配置: {timeout}秒)" + failed_tools.append({'tool': tool_name, 'reason': reason}) + logger.warning( + "⚠️ 工具 %s 执行超时 - 超时配置: %d秒\n" + "注意:超时前已解析的站点数据已保存到数据库,但扫描未完全完成。", + tool_name, timeout + ) + except Exception as exc: + # 其他异常 + failed_tools.append({'tool': tool_name, 'reason': str(exc)}) + logger.error("工具 %s 执行失败: %s", tool_name, exc, exc_info=True) + + if failed_tools: + logger.warning( + "以下扫描工具执行失败: %s", + ', '.join([f['tool'] for f in failed_tools]) + ) + + if not tool_stats: + error_details = "; ".join([f"{f['tool']}: {f['reason']}" for f in failed_tools]) + logger.warning("所有站点扫描工具均失败 - 目标: %s, 失败工具: %s", target_name, error_details) + # 返回空结果,不抛出异常,让扫描继续 + return {}, 0, [], failed_tools + + # 动态计算成功的工具列表 + successful_tool_names = [name for name in enabled_tools.keys() + if name not in [f['tool'] for f in failed_tools]] + + logger.info( + "✓ 串行站点扫描执行完成 - 成功: %d/%d (成功: %s, 失败: %s)", + len(tool_stats), len(enabled_tools), + ', '.join(successful_tool_names) if successful_tool_names else '无', + ', '.join([f['tool'] for f in failed_tools]) if failed_tools else '无' + ) + + return tool_stats, processed_records, successful_tool_names, failed_tools + + +def calculate_timeout(url_count: int, base: int = 600, per_url: int = 1) -> int: + """ + 根据 URL 数量动态计算扫描超时时间 + + 规则: + - 基础时间:默认 600 秒(10 分钟) + - 每个 URL 额外增加:默认 1 秒 + + Args: + url_count: URL 数量,必须为正整数 + base: 基础超时时间(秒),默认 600 + per_url: 每个 URL 增加的时间(秒),默认 1 + + Returns: + int: 计算得到的超时时间(秒),不超过 max_timeout + + Raises: + ValueError: 当 url_count 为负数或 0 时抛出异常 + """ + if url_count < 0: + raise ValueError(f"URL数量不能为负数: {url_count}") + if url_count == 0: + raise ValueError("URL数量不能为0") + + timeout = base + int(url_count * per_url) + + # 不设置上限,由调用方根据需要控制 + return timeout + + +@flow( + name="site_scan", + log_prints=True, + on_running=[on_scan_flow_running], + on_completion=[on_scan_flow_completed], + on_failure=[on_scan_flow_failed], +) +def site_scan_flow( + scan_id: int, + target_name: str, + target_id: int, + scan_workspace_dir: str, + enabled_tools: dict +) -> dict: + """ + 站点扫描 Flow + + 主要功能: + 1. 从target获取所有子域名与其对应的端口号,拼接成URL写入文件 + 2. 用httpx进行批量请求并实时保存到数据库(流式处理) + + 工作流程: + Step 0: 创建工作目录 + Step 1: 导出站点 URL 列表 + Step 2: 解析配置,获取启用的工具 + Step 3: 串行执行扫描工具并实时保存结果 + + Args: + scan_id: 扫描任务 ID + target_name: 目标名称 + target_id: 目标 ID + scan_workspace_dir: 扫描工作空间目录 + enabled_tools: 启用的工具配置字典 + + Returns: + dict: { + 'success': bool, + 'scan_id': int, + 'target': str, + 'scan_workspace_dir': str, + 'urls_file': str, + 'total_urls': int, + 'association_count': int, + 'processed_records': int, + 'created_websites': int, + 'skipped_no_subdomain': int, + 'skipped_failed': int, + 'executed_tasks': list, + 'tool_stats': { + 'total': int, + 'successful': int, + 'failed': int, + 'successful_tools': list[str], + 'failed_tools': list[dict] + } + } + + Raises: + ValueError: 配置错误 + RuntimeError: 执行失败 + """ + try: + logger.info( + "="*60 + "\n" + + "开始站点扫描\n" + + f" Scan ID: {scan_id}\n" + + f" Target: {target_name}\n" + + f" Workspace: {scan_workspace_dir}\n" + + "="*60 + ) + + # 参数验证 + if scan_id is None: + raise ValueError("scan_id 不能为空") + if not target_name: + raise ValueError("target_name 不能为空") + if target_id is None: + raise ValueError("target_id 不能为空") + if not scan_workspace_dir: + raise ValueError("scan_workspace_dir 不能为空") + + # Step 0: 创建工作目录 + site_scan_dir = _setup_site_scan_directory(scan_workspace_dir) + + # Step 1: 导出站点 URL + urls_file, total_urls, association_count = _export_site_urls( + target_id, site_scan_dir + ) + + if total_urls == 0: + logger.warning("目标下没有可用的站点URL,跳过站点扫描") + return { + 'success': True, + 'scan_id': scan_id, + 'target': target_name, + 'scan_workspace_dir': scan_workspace_dir, + 'urls_file': urls_file, + 'total_urls': 0, + 'association_count': association_count, + 'processed_records': 0, + 'created_websites': 0, + 'skipped_no_subdomain': 0, + 'skipped_failed': 0, + 'executed_tasks': ['export_site_urls'], + 'tool_stats': { + 'total': 0, + 'successful': 0, + 'failed': 0, + 'successful_tools': [], + 'failed_tools': [], + 'details': {} + } + } + + # Step 2: 工具配置信息 + logger.info("Step 2: 工具配置信息") + logger.info( + "✓ 启用工具: %s", + ', '.join(enabled_tools.keys()) + ) + + # Step 3: 串行执行扫描工具 + logger.info("Step 3: 串行执行扫描工具并实时保存结果") + tool_stats, processed_records, successful_tool_names, failed_tools = _run_scans_sequentially( + enabled_tools=enabled_tools, + urls_file=urls_file, + total_urls=total_urls, + site_scan_dir=site_scan_dir, + scan_id=scan_id, + target_id=target_id, + target_name=target_name + ) + + logger.info("="*60 + "\n✓ 站点扫描完成\n" + "="*60) + + # 动态生成已执行的任务列表 + executed_tasks = ['export_site_urls', 'parse_config'] + executed_tasks.extend([f'run_and_stream_save_websites ({tool})' for tool in tool_stats.keys()]) + + # 汇总所有工具的结果 + total_created = sum(stats['result'].get('created_websites', 0) for stats in tool_stats.values()) + total_skipped_no_subdomain = sum(stats['result'].get('skipped_no_subdomain', 0) for stats in tool_stats.values()) + total_skipped_failed = sum(stats['result'].get('skipped_failed', 0) for stats in tool_stats.values()) + + return { + 'success': True, + 'scan_id': scan_id, + 'target': target_name, + 'scan_workspace_dir': scan_workspace_dir, + 'urls_file': urls_file, + 'total_urls': total_urls, + 'association_count': association_count, + 'processed_records': processed_records, + 'created_websites': total_created, + 'skipped_no_subdomain': total_skipped_no_subdomain, + 'skipped_failed': total_skipped_failed, + 'executed_tasks': executed_tasks, + 'tool_stats': { + 'total': len(enabled_tools), + 'successful': len(successful_tool_names), + 'failed': len(failed_tools), + 'successful_tools': successful_tool_names, + 'failed_tools': failed_tools, + 'details': tool_stats + } + } + + except ValueError as e: + logger.error("配置错误: %s", e) + raise + except RuntimeError as e: + logger.error("运行时错误: %s", e) + raise + except Exception as e: + logger.exception("站点扫描失败: %s", e) + raise \ No newline at end of file diff --git a/backend/apps/scan/flows/subdomain_discovery_flow.py b/backend/apps/scan/flows/subdomain_discovery_flow.py new file mode 100644 index 00000000..a3f8f336 --- /dev/null +++ b/backend/apps/scan/flows/subdomain_discovery_flow.py @@ -0,0 +1,750 @@ +""" +子域名发现扫描 Flow(增强版) + +负责编排子域名发现扫描的完整流程 + +架构: +- Flow 负责编排多个原子 Task +- 支持并行执行扫描工具 +- 每个 Task 可独立重试 +- 配置由 YAML 解析 + +增强流程(4 阶段): + Stage 1: 被动收集(并行) - 必选 + Stage 2: 字典爆破(可选) - 子域名字典爆破 + Stage 3: 变异生成 + 验证(可选) - dnsgen + 通用存活验证 + Stage 4: DNS 存活验证(可选) - 通用存活验证 + +各阶段可灵活开关,最终结果根据实际执行的阶段动态决定 +""" + +# Django 环境初始化(导入即生效) +from apps.common.prefect_django_setup import setup_django_for_prefect + +from prefect import flow +from pathlib import Path +import logging +import os +from apps.scan.handlers.scan_flow_handlers import ( + on_scan_flow_running, + on_scan_flow_completed, + on_scan_flow_failed, +) +from apps.scan.utils import build_scan_command, ensure_wordlist_local +from apps.engine.services.wordlist_service import WordlistService +from apps.common.normalizer import normalize_domain +from apps.common.validators import validate_domain +from datetime import datetime +import uuid +import subprocess + +logger = logging.getLogger(__name__) + + +def _setup_subdomain_directory(scan_workspace_dir: str) -> Path: + """ + 创建并验证子域名扫描工作目录 + + Args: + scan_workspace_dir: 扫描工作空间目录 + + Returns: + Path: 子域名扫描目录路径 + + Raises: + RuntimeError: 目录创建或验证失败 + """ + result_dir = Path(scan_workspace_dir) / 'subdomain_discovery' + result_dir.mkdir(parents=True, exist_ok=True) + + if not result_dir.is_dir(): + raise RuntimeError(f"子域名扫描目录创建失败: {result_dir}") + if not os.access(result_dir, os.W_OK): + raise RuntimeError(f"子域名扫描目录不可写: {result_dir}") + + return result_dir + + +def _validate_and_normalize_target(target_name: str) -> str: + """ + 验证并规范化目标域名 + + Args: + target_name: 原始目标域名 + + Returns: + str: 规范化后的域名 + + Raises: + ValueError: 域名无效时抛出异常 + + Example: + >>> _validate_and_normalize_target('EXAMPLE.COM') + 'example.com' + >>> _validate_and_normalize_target('http://example.com') + 'example.com' + """ + try: + normalized_target = normalize_domain(target_name) + validate_domain(normalized_target) + logger.debug("域名验证通过: %s -> %s", target_name, normalized_target) + return normalized_target + except ValueError as e: + error_msg = f"无效的目标域名: {target_name} - {e}" + logger.error(error_msg) + raise ValueError(error_msg) from e + + +def _run_scans_parallel( + enabled_tools: dict, + domain_name: str, + result_dir: Path +) -> tuple[list, list, list]: + """ + 并行运行所有启用的子域名扫描工具 + + Args: + enabled_tools: 启用的工具配置字典 {'tool_name': {'timeout': 600, ...}} + domain_name: 目标域名 + result_dir: 结果输出目录 + + Returns: + tuple: (result_files, failed_tools, successful_tool_names) + + Raises: + RuntimeError: 所有工具均失败 + """ + # 导入任务函数 + from apps.scan.tasks.subdomain_discovery import run_subdomain_discovery_task + + # 生成时间戳(所有工具共用) + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + + # TODO: 接入代理池管理系统 + # from apps.proxy.services import proxy_pool + # proxy_stats = proxy_pool.get_stats() + # logger.info(f"代理池状态: {proxy_stats['healthy']}/{proxy_stats['total']} 可用") + + failures = [] # 记录命令构建失败的工具 + futures = {} + + # 1. 构建命令并提交并行任务 + for tool_name, tool_config in enabled_tools.items(): + # 1.1 生成唯一的输出文件路径(绝对路径) + short_uuid = uuid.uuid4().hex[:4] + output_file = str(result_dir / f"{tool_name}_{timestamp}_{short_uuid}.txt") + + # 1.2 构建完整命令(变量替换) + try: + command = build_scan_command( + tool_name=tool_name, + scan_type='subdomain_discovery', + command_params={ + 'domain': domain_name, # 对应 {domain} + 'output_file': output_file # 对应 {output_file} + }, + tool_config=tool_config + ) + except Exception as e: + failure_msg = f"{tool_name}: 命令构建失败 - {e}" + failures.append(failure_msg) + logger.error(f"构建 {tool_name} 命令失败: {e}") + continue + + # 1.3 获取超时时间(支持 'auto' 动态计算) + timeout = tool_config['timeout'] + if timeout == 'auto': + # 子域名发现工具通常运行时间较长,使用默认值 600 秒 + timeout = 600 + logger.info(f"✓ 工具 {tool_name} 使用默认 timeout: {timeout}秒") + + # 1.4 提交任务 + logger.debug( + f"提交任务 - 工具: {tool_name}, 超时: {timeout}s, 输出: {output_file}" + ) + + future = run_subdomain_discovery_task.submit( + tool=tool_name, + command=command, + timeout=timeout, + output_file=output_file + ) + futures[tool_name] = future + + # 2. 检查是否有任何工具成功提交 + if not futures: + logger.warning( + "所有扫描工具均无法启动 - 目标: %s, 失败详情: %s", + domain_name, "; ".join(failures) + ) + # 返回空结果,不抛出异常,让扫描继续 + return [], [{'tool': 'all', 'reason': '所有工具均无法启动'}], [] + + # 3. 等待并行任务完成,获取结果 + result_files = [] + failed_tools = [] + + for tool_name, future in futures.items(): + try: + result = future.result() # 返回文件路径(字符串)或 ""(失败) + if result: + result_files.append(result) + logger.info("✓ 扫描工具 %s 执行成功: %s", tool_name, result) + else: + failure_msg = f"{tool_name}: 未生成结果文件" + failures.append(failure_msg) + failed_tools.append({'tool': tool_name, 'reason': '未生成结果文件'}) + logger.warning("⚠️ 扫描工具 %s 未生成结果文件", tool_name) + except Exception as e: + failure_msg = f"{tool_name}: {str(e)}" + failures.append(failure_msg) + failed_tools.append({'tool': tool_name, 'reason': str(e)}) + logger.warning("⚠️ 扫描工具 %s 执行失败: %s", tool_name, str(e)) + + # 4. 检查是否有成功的工具 + if not result_files: + logger.warning( + "所有扫描工具均失败 - 目标: %s, 失败详情: %s", + domain_name, "; ".join(failures) + ) + # 返回空结果,不抛出异常,让扫描继续 + return [], failed_tools, [] + + # 5. 动态计算成功的工具列表 + successful_tool_names = [name for name in futures.keys() + if name not in [f['tool'] for f in failed_tools]] + + logger.info( + "✓ 扫描工具并行执行完成 - 成功: %d/%d (成功: %s, 失败: %s)", + len(result_files), len(futures), + ', '.join(successful_tool_names) if successful_tool_names else '无', + ', '.join([f['tool'] for f in failed_tools]) if failed_tools else '无' + ) + + return result_files, failed_tools, successful_tool_names + + +def _run_single_tool( + tool_name: str, + tool_config: dict, + command_params: dict, + result_dir: Path, + scan_type: str = 'subdomain_discovery' +) -> str: + """ + 运行单个扫描工具 + + Args: + tool_name: 工具名称 + tool_config: 工具配置 + command_params: 命令参数 + result_dir: 结果目录 + scan_type: 扫描类型 + + Returns: + str: 输出文件路径,失败返回空字符串 + """ + from apps.scan.tasks.subdomain_discovery import run_subdomain_discovery_task + + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + short_uuid = uuid.uuid4().hex[:4] + output_file = str(result_dir / f"{tool_name}_{timestamp}_{short_uuid}.txt") + + # 添加 output_file 到参数 + command_params['output_file'] = output_file + + try: + command = build_scan_command( + tool_name=tool_name, + scan_type=scan_type, + command_params=command_params, + tool_config=tool_config + ) + except Exception as e: + logger.error(f"构建 {tool_name} 命令失败: {e}") + return "" + + timeout = tool_config.get('timeout', 3600) + if timeout == 'auto': + timeout = 3600 + + logger.info(f"执行 {tool_name}: timeout={timeout}s") + + try: + result = run_subdomain_discovery_task( + tool=tool_name, + command=command, + timeout=timeout, + output_file=output_file + ) + return result if result else "" + except Exception as e: + logger.warning(f"{tool_name} 执行失败: {e}") + return "" + + +def _count_lines(file_path: str) -> int: + """ + 统计文件非空行数 + + Args: + file_path: 文件路径 + + Returns: + int: 非空行数量 + """ + try: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + return sum(1 for line in f if line.strip()) + except Exception as e: + logger.warning(f"统计文件行数失败: {file_path} - {e}") + return 0 + + +def _merge_files(file_list: list, output_file: str) -> str: + """ + 合并多个文件并去重 + + Args: + file_list: 文件路径列表 + output_file: 输出文件路径 + + Returns: + str: 输出文件路径 + """ + domains = set() + for f in file_list: + if f and Path(f).exists(): + with open(f, 'r', encoding='utf-8', errors='ignore') as fp: + for line in fp: + line = line.strip() + if line: + domains.add(line) + + with open(output_file, 'w', encoding='utf-8') as fp: + for domain in sorted(domains): + fp.write(domain + '\n') + + logger.info(f"合并完成: {len(domains)} 个域名 -> {output_file}") + return output_file + + +@flow( + name="subdomain_discovery", + log_prints=True, + on_running=[on_scan_flow_running], + on_completion=[on_scan_flow_completed], + on_failure=[on_scan_flow_failed], +) +def subdomain_discovery_flow( + scan_id: int, + target_name: str, + target_id: int, + scan_workspace_dir: str, + enabled_tools: dict +) -> dict: + """子域名发现扫描流程(增强版) + + 工作流程(4 阶段): + Stage 1: 被动收集(并行) - 必选 + Stage 2: 字典爆破(可选) - 子域名字典爆破 + Stage 3: 变异生成 + 验证(可选) - dnsgen + 通用存活验证 + Stage 4: DNS 存活验证(可选) - 通用存活验证 + Final: 保存到数据库 + + Args: + scan_id: 扫描任务 ID + target_name: 目标名称(域名) + target_id: 目标 ID + scan_workspace_dir: Scan 工作空间目录(由 Service 层创建) + enabled_tools: 扫描配置字典: + { + 'passive_tools': {...}, + 'bruteforce': {...}, + 'permutation': {...}, + 'resolve': {...} + } + + Returns: + dict: 扫描结果 + + Raises: + ValueError: 配置错误 + RuntimeError: 执行失败 + """ + try: + # ==================== 参数验证 ==================== + if scan_id is None: + raise ValueError("scan_id 不能为空") + if target_id is None: + raise ValueError("target_id 不能为空") + if not scan_workspace_dir: + raise ValueError("scan_workspace_dir 不能为空") + if enabled_tools is None: + raise ValueError("enabled_tools 不能为空") + + scan_config = enabled_tools + + # 如果未提供目标域名,跳过扫描 + if not target_name: + logger.warning("未提供目标域名,跳过子域名发现扫描") + return _empty_result(scan_id, '', scan_workspace_dir) + + # 导入任务函数 + from apps.scan.tasks.subdomain_discovery import ( + run_subdomain_discovery_task, + merge_and_validate_task, + save_domains_task + ) + + # Step 0: 准备工作 + result_dir = _setup_subdomain_directory(scan_workspace_dir) + + # 验证并规范化目标域名 + try: + domain_name = _validate_and_normalize_target(target_name) + except ValueError as e: + logger.warning("目标域名无效,跳过子域名发现扫描: %s", e) + return _empty_result(scan_id, target_name, scan_workspace_dir) + + # 验证成功后打印日志 + logger.info( + "="*60 + "\n" + + "开始子域名发现扫描(增强版)\n" + + f" Scan ID: {scan_id}\n" + + f" Domain: {domain_name}\n" + + f" Workspace: {scan_workspace_dir}\n" + + "="*60 + ) + + # 解析配置 + passive_tools = scan_config.get('passive_tools', {}) + bruteforce_config = scan_config.get('bruteforce', {}) + permutation_config = scan_config.get('permutation', {}) + resolve_config = scan_config.get('resolve', {}) + + # 过滤出启用的被动工具 + enabled_passive_tools = { + k: v for k, v in passive_tools.items() + if v.get('enabled', True) + } + + executed_tasks = [] + all_result_files = [] + failed_tools = [] + successful_tool_names = [] + + # ==================== Stage 1: 被动收集(并行)==================== + logger.info("=" * 40) + logger.info("Stage 1: 被动收集(并行)") + logger.info("=" * 40) + + if enabled_passive_tools: + logger.info("启用工具: %s", ', '.join(enabled_passive_tools.keys())) + result_files, stage1_failed, stage1_success = _run_scans_parallel( + enabled_tools=enabled_passive_tools, + domain_name=domain_name, + result_dir=result_dir + ) + all_result_files.extend(result_files) + failed_tools.extend(stage1_failed) + successful_tool_names.extend(stage1_success) + executed_tasks.extend([f'passive ({tool})' for tool in stage1_success]) + else: + logger.warning("未启用任何被动收集工具") + + # 合并 Stage 1 结果 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + current_result = str(result_dir / f"subs_passive_{timestamp}.txt") + if all_result_files: + current_result = _merge_files(all_result_files, current_result) + executed_tasks.append('merge_passive') + else: + # 创建空文件 + Path(current_result).touch() + logger.warning("Stage 1 无结果,创建空文件") + + # ==================== Stage 2: 字典爆破(可选)==================== + bruteforce_enabled = bruteforce_config.get('enabled', False) + if bruteforce_enabled: + logger.info("=" * 40) + logger.info("Stage 2: 字典爆破") + logger.info("=" * 40) + + bruteforce_tool_config = bruteforce_config.get('subdomain_bruteforce', {}) + wordlist_name = bruteforce_tool_config.get('wordlist_name', 'dns_wordlist.txt') + + try: + # 确保本地存在字典文件(含 hash 校验) + local_wordlist_path = ensure_wordlist_local(wordlist_name) + + # 获取字典记录用于计算 timeout + wordlist_service = WordlistService() + wordlist = wordlist_service.get_wordlist_by_name(wordlist_name) + + timeout_value = bruteforce_tool_config.get('timeout', 3600) + if timeout_value == 'auto' and wordlist: + line_count = getattr(wordlist, 'line_count', None) + if line_count is None: + try: + with open(local_wordlist_path, 'rb') as f: + line_count = sum(1 for _ in f) + except OSError: + line_count = 0 + + try: + line_count_int = int(line_count) + except (TypeError, ValueError): + line_count_int = 0 + + timeout_value = line_count_int * 3 if line_count_int > 0 else 3600 + bruteforce_tool_config = { + **bruteforce_tool_config, + 'timeout': timeout_value, + } + logger.info( + "subdomain_bruteforce 使用自动 timeout: %s 秒 (字典行数=%s, 3秒/行)", + timeout_value, + line_count_int, + ) + + brute_output = str(result_dir / f"subs_brute_{timestamp}.txt") + brute_result = _run_single_tool( + tool_name='subdomain_bruteforce', + tool_config=bruteforce_tool_config, + command_params={ + 'domain': domain_name, + 'wordlist': local_wordlist_path, + 'output_file': brute_output + }, + result_dir=result_dir + ) + + if brute_result: + # 合并 Stage 1 + Stage 2 + current_result = _merge_files( + [current_result, brute_result], + str(result_dir / f"subs_merged_{timestamp}.txt") + ) + successful_tool_names.append('subdomain_bruteforce') + executed_tasks.append('bruteforce') + else: + failed_tools.append({'tool': 'subdomain_bruteforce', 'reason': '执行失败'}) + except Exception as exc: + logger.warning("字典准备失败,跳过字典爆破: %s", exc) + failed_tools.append({'tool': 'subdomain_bruteforce', 'reason': str(exc)}) + + # ==================== Stage 3: 变异生成 + 验证(可选)==================== + permutation_enabled = permutation_config.get('enabled', False) + if permutation_enabled: + logger.info("=" * 40) + logger.info("Stage 3: 变异生成 + 存活验证(流式管道)") + logger.info("=" * 40) + + permutation_tool_config = permutation_config.get('subdomain_permutation_resolve', {}) + + # === Step 3.1: 泛解析采样检测 === + # 生成原文件 100 倍的变异样本,检查解析结果是否超过 50 倍 + before_count = _count_lines(current_result) + + # 配置参数 + SAMPLE_MULTIPLIER = 100 # 采样数量 = 原文件 × 100 + EXPANSION_THRESHOLD = 50 # 膨胀阈值 = 原文件 × 50 + SAMPLE_TIMEOUT = 7200 # 采样超时 2 小时 + + sample_size = before_count * SAMPLE_MULTIPLIER + max_allowed = before_count * EXPANSION_THRESHOLD + + sample_output = str(result_dir / f"subs_permuted_sample_{timestamp}.txt") + sample_cmd = ( + f"cat {current_result} | dnsgen - | head -n {sample_size} | " + f"puredns resolve -r /app/backend/resources/resolvers.txt " + f"--write {sample_output} --wildcard-tests 50 --wildcard-batch 1000000 --quiet" + ) + + logger.info( + f"泛解析采样检测: 原文件 {before_count} 个, " + f"采样 {sample_size} 个, 阈值 {max_allowed} 个" + ) + + try: + subprocess.run( + sample_cmd, + shell=True, + timeout=SAMPLE_TIMEOUT, + check=False, + capture_output=True + ) + sample_result_count = _count_lines(sample_output) if Path(sample_output).exists() else 0 + + logger.info( + f"采样结果: {sample_result_count} 个域名存活 " + f"(原文件: {before_count}, 阈值: {max_allowed})" + ) + + if sample_result_count > max_allowed: + # 采样结果超过阈值,说明存在泛解析,跳过完整变异 + ratio = sample_result_count / before_count if before_count > 0 else sample_result_count + logger.warning( + f"跳过变异: 采样检测到泛解析 " + f"({sample_result_count} > {max_allowed}, 膨胀率 {ratio:.1f}x)" + ) + failed_tools.append({ + 'tool': 'subdomain_permutation_resolve', + 'reason': f"采样检测到泛解析 (膨胀率 {ratio:.1f}x)" + }) + else: + # === Step 3.2: 采样通过,执行完整变异 === + logger.info("采样检测通过,执行完整变异...") + + permuted_output = str(result_dir / f"subs_permuted_{timestamp}.txt") + + permuted_result = _run_single_tool( + tool_name='subdomain_permutation_resolve', + tool_config=permutation_tool_config, + command_params={ + 'input_file': current_result, + 'output_file': permuted_output, + }, + result_dir=result_dir + ) + + if permuted_result: + # 合并原结果 + 变异验证结果 + current_result = _merge_files( + [current_result, permuted_result], + str(result_dir / f"subs_with_permuted_{timestamp}.txt") + ) + successful_tool_names.append('subdomain_permutation_resolve') + executed_tasks.append('permutation') + else: + failed_tools.append({'tool': 'subdomain_permutation_resolve', 'reason': '执行失败'}) + + except subprocess.TimeoutExpired: + logger.warning(f"采样检测超时 ({SAMPLE_TIMEOUT}秒),跳过变异") + failed_tools.append({'tool': 'subdomain_permutation_resolve', 'reason': '采样检测超时'}) + except Exception as e: + logger.warning(f"采样检测失败: {e},跳过变异") + failed_tools.append({'tool': 'subdomain_permutation_resolve', 'reason': f'采样检测失败: {e}'}) + + # ==================== Stage 4: DNS 存活验证(可选)==================== + # 无论是否启用 Stage 3,只要 resolve.enabled 为 true 就会执行,对当前所有候选子域做统一 DNS 验证 + resolve_enabled = resolve_config.get('enabled', False) + if resolve_enabled: + logger.info("=" * 40) + logger.info("Stage 4: DNS 存活验证") + logger.info("=" * 40) + + resolve_tool_config = resolve_config.get('subdomain_resolve', {}) + + # 根据当前候选子域数量动态计算 timeout(支持 timeout: auto) + timeout_value = resolve_tool_config.get('timeout', 3600) + if timeout_value == 'auto': + line_count = 0 + try: + with open(current_result, 'rb') as f: + line_count = sum(1 for _ in f) + except OSError: + line_count = 0 + + try: + line_count_int = int(line_count) + except (TypeError, ValueError): + line_count_int = 0 + + timeout_value = line_count_int * 3 if line_count_int > 0 else 3600 + resolve_tool_config = { + **resolve_tool_config, + 'timeout': timeout_value, + } + logger.info( + "subdomain_resolve 使用自动 timeout: %s 秒 (候选子域数=%s, 3秒/域名)", + timeout_value, + line_count_int, + ) + + alive_output = str(result_dir / f"subs_alive_{timestamp}.txt") + + alive_result = _run_single_tool( + tool_name='subdomain_resolve', + tool_config=resolve_tool_config, + command_params={ + 'input_file': current_result, + 'output_file': alive_output, + }, + result_dir=result_dir + ) + + if alive_result: + current_result = alive_result + successful_tool_names.append('subdomain_resolve') + executed_tasks.append('resolve') + else: + failed_tools.append({'tool': 'subdomain_resolve', 'reason': '执行失败'}) + + # ==================== Final: 保存到数据库 ==================== + logger.info("=" * 40) + logger.info("Final: 保存到数据库") + logger.info("=" * 40) + + # 最终验证和保存 + final_file = merge_and_validate_task( + result_files=[current_result], + result_dir=str(result_dir) + ) + + save_result = save_domains_task( + domains_file=final_file, + scan_id=scan_id, + target_id=target_id + ) + processed_domains = save_result.get('processed_records', 0) + executed_tasks.append('save_domains') + + logger.info("="*60 + "\n✓ 子域名发现扫描完成\n" + "="*60) + + return { + 'success': True, + 'scan_id': scan_id, + 'target': domain_name, + 'scan_workspace_dir': scan_workspace_dir, + 'total': processed_domains, + 'executed_tasks': executed_tasks, + 'tool_stats': { + 'total': len(enabled_passive_tools) + (1 if bruteforce_enabled else 0) + + (1 if permutation_enabled else 0) + (1 if resolve_enabled else 0), + 'successful': len(successful_tool_names), + 'failed': len(failed_tools), + 'successful_tools': successful_tool_names, + 'failed_tools': failed_tools + } + } + + except ValueError as e: + logger.error("配置错误: %s", e) + raise + except RuntimeError as e: + logger.error("运行时错误: %s", e) + raise + except Exception as e: + logger.exception("子域名发现扫描失败: %s", e) + raise + + +def _empty_result(scan_id: int, target: str, scan_workspace_dir: str) -> dict: + """返回空结果""" + return { + 'success': True, + 'scan_id': scan_id, + 'target': target, + 'scan_workspace_dir': scan_workspace_dir, + 'total': 0, + 'executed_tasks': [], + 'tool_stats': { + 'total': 0, + 'successful': 0, + 'failed': 0, + 'successful_tools': [], + 'failed_tools': [] + } + } diff --git a/backend/apps/scan/flows/url_fetch/__init__.py b/backend/apps/scan/flows/url_fetch/__init__.py new file mode 100644 index 00000000..8b2d6dda --- /dev/null +++ b/backend/apps/scan/flows/url_fetch/__init__.py @@ -0,0 +1,21 @@ +""" +URL Fetch Flow 模块 + +提供 URL 获取相关的 Flow: +- url_fetch_flow: 主 Flow(按输入类型编排 + 统一后处理) +- domain_name_url_fetch_flow: 基于 domain_name(来自 target_name)输入的 URL 获取子 Flow(如 waymore) +- domains_url_fetch_flow: 基于 domains_file 输入的 URL 获取子 Flow(如 gau、waybackurls) +- sites_url_fetch_flow: 基于 sites_file 输入的 URL 获取子 Flow(如 katana 等爬虫) +""" + +from .main_flow import url_fetch_flow +from .domain_name_url_fetch_flow import domain_name_url_fetch_flow +from .domains_url_fetch_flow import domains_url_fetch_flow +from .sites_url_fetch_flow import sites_url_fetch_flow + +__all__ = [ + 'url_fetch_flow', + 'domain_name_url_fetch_flow', + 'domains_url_fetch_flow', + 'sites_url_fetch_flow', +] diff --git a/backend/apps/scan/flows/url_fetch/domain_name_url_fetch_flow.py b/backend/apps/scan/flows/url_fetch/domain_name_url_fetch_flow.py new file mode 100644 index 00000000..b19b1679 --- /dev/null +++ b/backend/apps/scan/flows/url_fetch/domain_name_url_fetch_flow.py @@ -0,0 +1,169 @@ +""" +基于 domain_name(域名)的 URL 获取 Flow + +主要用于像 waymore 这种按域名输入(input_type = 'domain_name')的工具: +- 直接对目标域名(target_name/domain_name)执行 URL 被动收集 +- 不再依赖 domains_file(子域名列表文件) +""" + +# Django 环境初始化 +from apps.common.prefect_django_setup import setup_django_for_prefect + +import logging +import uuid +from datetime import datetime +from pathlib import Path +from typing import Dict + +from prefect import flow + +from apps.common.validators import validate_domain +from apps.scan.tasks.url_fetch import run_url_fetcher_task +from apps.scan.utils import build_scan_command + + +logger = logging.getLogger(__name__) + + +@flow(name="domain_name_url_fetch_flow", log_prints=True) +def domain_name_url_fetch_flow( + scan_id: int, + target_id: int, + target_name: str, + output_dir: str, + domain_name_tools: Dict[str, dict], +) -> dict: + """ + 基于 target_name/domain_name 域名执行 URL 获取子 Flow(当前主要用于 waymore)。 + + 执行流程: + 1. 校验 target_name 是否为域名 + 2. 使用传入的 domain_name_tools 工具列表 + 3. 为每个工具构建命令并并行执行 + 4. 汇总结果文件列表 + """ + try: + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + # 复用公共域名校验逻辑,确保 target_name 是合法域名 + validate_domain(target_name) + + logger.info( + "开始基于 domain_name 的 URL 获取 - Target: %s, Tools: %s", + target_name, + ", ".join(domain_name_tools.keys()) if domain_name_tools else "无", + ) + + futures: dict[str, object] = {} + failed_tools: list[dict] = [] + + # 提交所有基于域名的 URL 获取任务 + for tool_name, tool_config in domain_name_tools.items(): + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + short_uuid = uuid.uuid4().hex[:4] + output_file = str(output_path / f"{tool_name}_{timestamp}_{short_uuid}.txt") + + command_params = { + "domain_name": target_name, + "output_file": output_file, + } + + try: + command = build_scan_command( + tool_name=tool_name, + scan_type="url_fetch", + command_params=command_params, + tool_config=tool_config, + ) + except Exception as e: + logger.error("构建 %s 命令失败: %s", tool_name, e) + failed_tools.append({"tool": tool_name, "reason": f"命令构建失败: {e}"}) + continue + + # 计算超时时间:domain_name 模式下,没有行数统计,auto 使用固定超时 + raw_timeout = tool_config.get("timeout", 3600) + timeout = 3600 + if isinstance(raw_timeout, str) and raw_timeout == "auto": + timeout = 3600 + logger.info( + "工具 %s 使用固定自动超时: %d 秒 (domain_name 模式)", + tool_name, + timeout, + ) + else: + try: + timeout = int(raw_timeout) + except (TypeError, ValueError): + logger.warning( + "工具 %s 的 timeout 配置无效(%s),将使用默认 3600 秒", + tool_name, + raw_timeout, + ) + timeout = 3600 + + logger.info( + "提交任务 - 工具: %s, domain_name: %s, 超时: %d秒", + tool_name, + target_name, + timeout, + ) + + future = run_url_fetcher_task.submit( + tool_name=tool_name, + command=command, + timeout=timeout, + output_file=output_file, + ) + futures[tool_name] = future + + result_files: list[str] = [] + successful_tools: list[str] = [] + + # 收集执行结果 + for tool_name, future in futures.items(): + try: + result = future.result() + if result and result.get("success"): + result_files.append(result["output_file"]) + successful_tools.append(tool_name) + logger.info( + "✓ 工具 %s 执行成功 - 发现 URL: %d", + tool_name, + result.get("url_count", 0), + ) + else: + failed_tools.append( + { + "tool": tool_name, + "reason": "未生成结果或无有效 URL", + } + ) + logger.warning("⚠️ 工具 %s 未生成有效结果", tool_name) + except Exception as e: + failed_tools.append({"tool": tool_name, "reason": str(e)}) + logger.warning("⚠️ 工具 %s 执行失败: %s", tool_name, e) + + logger.info( + "基于 domain_name 的 URL 获取完成 - 成功工具: %s, 失败工具: %s", + successful_tools or "无", + [f["tool"] for f in failed_tools] or "无", + ) + + return { + "success": True, + "result_files": result_files, + "failed_tools": failed_tools, + "successful_tools": successful_tools, + } + + except Exception as e: + logger.error("domain_name URL 获取失败: %s", e, exc_info=True) + return { + "success": False, + "result_files": [], + "failed_tools": [ + {"tool": "domain_name_url_fetch_flow", "reason": str(e)}, + ], + "successful_tools": [], + } diff --git a/backend/apps/scan/flows/url_fetch/domains_url_fetch_flow.py b/backend/apps/scan/flows/url_fetch/domains_url_fetch_flow.py new file mode 100644 index 00000000..05931287 --- /dev/null +++ b/backend/apps/scan/flows/url_fetch/domains_url_fetch_flow.py @@ -0,0 +1,139 @@ +""" +URL 被动收集 Flow + +从历史归档、搜索引擎等被动来源收集 URL +工具:waymore, gau, waybackurls 等 +输入:domains_file(子域名列表) +""" + +# Django 环境初始化 +from apps.common.prefect_django_setup import setup_django_for_prefect + +import logging +from pathlib import Path + +from prefect import flow + +from .utils import run_tools_parallel + +logger = logging.getLogger(__name__) + + +def _export_domains_file(target_id: int, scan_id: int, output_dir: Path) -> tuple[str, int]: + """ + 导出子域名列表到文件 + + Args: + target_id: 目标 ID + scan_id: 扫描 ID + output_dir: 输出目录 + + Returns: + tuple: (file_path, count) + """ + from apps.scan.tasks.url_fetch import export_target_assets_task + + output_file = str(output_dir / "domains.txt") + result = export_target_assets_task( + output_file=output_file, + target_id=target_id, + scan_id=scan_id, + input_type="domains_file" + ) + + count = result['asset_count'] + if count == 0: + logger.warning("子域名列表为空,被动收集可能无法正常工作") + else: + logger.info("✓ 子域名列表导出完成 - 数量: %d", count) + + return output_file, count + + +@flow(name="domains_url_fetch_flow", log_prints=True) +def domains_url_fetch_flow( + scan_id: int, + target_id: int, + target_name: str, + output_dir: str, + enabled_tools: dict +) -> dict: + """ + URL 被动收集子 Flow + + 执行流程: + 1. 导出子域名列表(domains_file) + 2. 并行执行被动收集工具 + 3. 返回结果文件列表 + + Args: + scan_id: 扫描 ID + target_id: 目标 ID + target_name: 目标名称 + output_dir: 输出目录 + enabled_tools: 启用的被动收集工具配置 + + Returns: + dict: { + 'success': bool, + 'result_files': list, + 'failed_tools': list, + 'successful_tools': list, + 'domains_count': int + } + """ + try: + output_path = Path(output_dir) + + logger.info( + "开始 URL 被动收集 - Target: %s, Tools: %s", + target_name, ', '.join(enabled_tools.keys()) + ) + + # Step 1: 导出子域名列表 + domains_file, domains_count = _export_domains_file( + target_id=target_id, + scan_id=scan_id, + output_dir=output_path + ) + + if domains_count == 0: + logger.warning("没有可用的子域名,跳过被动收集") + return { + 'success': True, + 'result_files': [], + 'failed_tools': [], + 'successful_tools': [], + 'domains_count': 0 + } + + # Step 2: 并行执行被动收集工具 + result_files, failed_tools, successful_tools = run_tools_parallel( + tools=enabled_tools, + input_file=domains_file, + input_type="domains_file", + output_dir=output_path + ) + + logger.info( + "✓ 被动收集完成 - 成功: %d/%d, 结果文件: %d", + len(successful_tools), len(enabled_tools), len(result_files) + ) + + return { + 'success': True, + 'result_files': result_files, + 'failed_tools': failed_tools, + 'successful_tools': successful_tools, + 'domains_count': domains_count + } + + except Exception as e: + logger.error("URL 被动收集失败: %s", e, exc_info=True) + return { + 'success': False, + 'result_files': [], + 'failed_tools': [{'tool': 'domains_url_fetch_flow', 'reason': str(e)}], + 'successful_tools': [], + 'domains_count': 0 + } diff --git a/backend/apps/scan/flows/url_fetch/main_flow.py b/backend/apps/scan/flows/url_fetch/main_flow.py new file mode 100644 index 00000000..4328ceef --- /dev/null +++ b/backend/apps/scan/flows/url_fetch/main_flow.py @@ -0,0 +1,476 @@ +""" +URL Fetch 主 Flow + +负责编排不同输入类型的 URL 获取子 Flow(domain_name / domains_file / sites_file),以及统一的后处理(uro 去重、httpx 验证) + +架构: +- 调用 domain_name_url_fetch_flow(domain_name 输入)、domains_url_fetch_flow(domains_file 输入)和 sites_url_fetch_flow(sites_file 输入) +- 合并多个子 Flow 的结果 +- 统一进行 uro 去重(如果启用) +- 统一进行 httpx 验证(如果启用) +""" + +# Django 环境初始化 +from apps.common.prefect_django_setup import setup_django_for_prefect + +import logging +import os +from pathlib import Path +from datetime import datetime + +from prefect import flow + +from apps.scan.handlers.scan_flow_handlers import ( + on_scan_flow_running, + on_scan_flow_completed, + on_scan_flow_failed, +) + +from .domain_name_url_fetch_flow import domain_name_url_fetch_flow +from .domains_url_fetch_flow import domains_url_fetch_flow +from .sites_url_fetch_flow import sites_url_fetch_flow +from .utils import calculate_timeout_by_line_count + +logger = logging.getLogger(__name__) + + +# ==================== 工具分类配置 ==================== +# 使用 target_name (domain_name) 作为输入的 URL 获取工具 +DOMAIN_NAME_TOOLS = {'waymore'} +# 使用 domains_file 作为输入的 URL 获取工具 +DOMAINS_FILE_TOOLS = {'gau', 'waybackurls'} +# 使用 sites_file 作为输入的 URL 获取工具 +SITES_FILE_TOOLS = {'katana', 'gospider', 'hakrawler'} +# 后处理工具:不参与获取,用于清理和验证 +POST_PROCESS_TOOLS = {'uro', 'httpx'} + + +def _setup_url_fetch_directory(scan_workspace_dir: str) -> Path: + """创建并验证 URL 获取工作目录""" + url_fetch_dir = Path(scan_workspace_dir) / 'url_fetch' + url_fetch_dir.mkdir(parents=True, exist_ok=True) + + if not url_fetch_dir.is_dir(): + raise RuntimeError(f"URL 获取目录创建失败: {url_fetch_dir}") + if not os.access(url_fetch_dir, os.W_OK): + raise RuntimeError(f"URL 获取目录不可写: {url_fetch_dir}") + + return url_fetch_dir + + +def _classify_tools(enabled_tools: dict) -> tuple[dict, dict, dict, dict, dict]: + """ + 将启用的工具按输入类型分类 + + Returns: + tuple: (domain_name_tools, domains_file_tools, sites_file_tools, uro_config, httpx_config) + """ + domain_name_tools: dict = {} + domains_file_tools: dict = {} + sites_file_tools: dict = {} + uro_config = None + httpx_config = None + + for tool_name, tool_config in enabled_tools.items(): + if tool_name in DOMAIN_NAME_TOOLS: + domain_name_tools[tool_name] = tool_config + elif tool_name in DOMAINS_FILE_TOOLS: + domains_file_tools[tool_name] = tool_config + elif tool_name in SITES_FILE_TOOLS: + sites_file_tools[tool_name] = tool_config + elif tool_name == 'uro': + uro_config = tool_config + elif tool_name == 'httpx': + httpx_config = tool_config + else: + logger.warning("未知工具类型: %s,将尝试作为 domains_file 输入的被动收集工具", tool_name) + domains_file_tools[tool_name] = tool_config + + return domain_name_tools, domains_file_tools, sites_file_tools, uro_config, httpx_config + + +def _merge_and_deduplicate_urls(result_files: list, url_fetch_dir: Path) -> tuple[str, int]: + """合并并去重 URL""" + from apps.scan.tasks.url_fetch import merge_and_deduplicate_urls_task + + merged_file = merge_and_deduplicate_urls_task( + result_files=result_files, + result_dir=str(url_fetch_dir) + ) + + # 统计唯一 URL 数量 + unique_url_count = 0 + if Path(merged_file).exists(): + with open(merged_file, 'r') as f: + unique_url_count = sum(1 for line in f if line.strip()) + + logger.info( + "✓ URL 合并去重完成 - 合并文件: %s, 唯一 URL 数: %d", + merged_file, unique_url_count + ) + + return merged_file, unique_url_count + + +def _clean_urls_with_uro( + merged_file: str, + uro_config: dict, + url_fetch_dir: Path +) -> tuple[str, int, int]: + """使用 uro 清理合并后的 URL 列表""" + from apps.scan.tasks.url_fetch import clean_urls_task + + raw_timeout = uro_config.get('timeout', 60) + whitelist = uro_config.get('whitelist') + blacklist = uro_config.get('blacklist') + filters = uro_config.get('filters') + + # 计算超时时间 + if isinstance(raw_timeout, str) and raw_timeout == 'auto': + timeout = calculate_timeout_by_line_count( + tool_config=uro_config, + file_path=merged_file, + base_per_time=1, + ) + timeout = max(30, timeout) + logger.info("uro 自动计算超时时间(按行数,每行 1 秒): %d 秒", timeout) + else: + try: + timeout = int(raw_timeout) + except (TypeError, ValueError): + logger.warning("uro timeout 配置无效(%s),使用默认 60 秒", raw_timeout) + timeout = 60 + + result = clean_urls_task( + input_file=merged_file, + output_dir=str(url_fetch_dir), + timeout=timeout, + whitelist=whitelist, + blacklist=blacklist, + filters=filters + ) + + if result['success']: + return result['output_file'], result['output_count'], result['removed_count'] + else: + logger.warning("uro 清理失败: %s,使用原始合并文件", result.get('error', '未知错误')) + return merged_file, result['input_count'], 0 + + +def _validate_and_stream_save_urls( + merged_file: str, + httpx_config: dict, + url_fetch_dir: Path, + scan_id: int, + target_id: int +) -> int: + """使用 httpx 验证 URL 存活并流式保存到数据库""" + from apps.scan.utils import build_scan_command + from apps.scan.tasks.url_fetch import run_and_stream_save_urls_task + + logger.info("开始使用 httpx 验证 URL 存活状态...") + + # 统计待验证的 URL 数量 + try: + with open(merged_file, 'r') as f: + url_count = sum(1 for _ in f) + logger.info("待验证 URL 数量: %d", url_count) + except Exception as e: + logger.error("读取 URL 文件失败: %s", e) + return 0 + + if url_count == 0: + logger.warning("没有需要验证的 URL") + return 0 + + # 构建 httpx 命令 + command_params = {'url_file': merged_file} + + try: + command = build_scan_command( + tool_name='httpx', + scan_type='url_fetch', + command_params=command_params, + tool_config=httpx_config + ) + except Exception as e: + logger.error("构建 httpx 命令失败: %s", e) + logger.warning("降级处理:将直接保存所有 URL(不验证存活)") + return _save_urls_to_database(merged_file, scan_id, target_id) + + # 计算超时时间 + raw_timeout = httpx_config.get('timeout', 'auto') + timeout = 3600 + if isinstance(raw_timeout, str) and raw_timeout == 'auto': + # 按 URL 行数计算超时时间:每行 3 秒,不设上限 + timeout = url_count * 3 + timeout = max(600, timeout) + logger.info( + "自动计算 httpx 超时时间(按行数,每行 3 秒): url_count=%d, timeout=%d 秒", + url_count, + timeout, + ) + else: + try: + timeout = int(raw_timeout) + except (TypeError, ValueError): + timeout = 3600 + logger.info("使用配置的 httpx 超时时间: %d 秒", timeout) + + # 生成日志文件路径 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + log_file = url_fetch_dir / f"httpx_validation_{timestamp}.log" + + # 流式执行 + try: + result = run_and_stream_save_urls_task( + cmd=command, + tool_name='httpx', + scan_id=scan_id, + target_id=target_id, + cwd=str(url_fetch_dir), + shell=True, + batch_size=500, + timeout=timeout, + log_file=str(log_file) + ) + + saved = result.get('saved_urls', 0) + logger.info( + "✓ httpx 验证完成 - 存活 URL: %d (%.1f%%)", + saved, (saved / url_count * 100) if url_count > 0 else 0 + ) + return saved + + except Exception as e: + logger.error("httpx 流式验证失败: %s", e, exc_info=True) + raise + + +def _save_urls_to_database(merged_file: str, scan_id: int, target_id: int) -> int: + """保存 URL 到数据库(不验证存活)""" + from apps.scan.tasks.url_fetch import save_urls_task + + result = save_urls_task( + urls_file=merged_file, + scan_id=scan_id, + target_id=target_id + ) + + saved_count = result.get('saved_urls', 0) + logger.info("✓ URL 保存完成 - 保存数量: %d", saved_count) + + return saved_count + + +@flow( + name="url_fetch", + log_prints=True, + on_running=[on_scan_flow_running], + on_completion=[on_scan_flow_completed], + on_failure=[on_scan_flow_failed], +) +def url_fetch_flow( + scan_id: int, + target_name: str, + target_id: int, + scan_workspace_dir: str, + enabled_tools: dict +) -> dict: + """ + URL 获取主 Flow + + 执行流程: + 1. 准备工作目录 + 2. 按输入类型分类工具(domain_name / domains_file / sites_file / 后处理) + 3. 并行执行子 Flow + - domain_name_url_fetch_flow: 基于 domain_name(来自 target_name)执行 URL 获取(如 waymore) + - domains_url_fetch_flow: 基于 domains_file 执行 URL 获取(如 gau、waybackurls) + - sites_url_fetch_flow: 基于 sites_file 执行爬虫(如 katana 等) + 4. 合并所有子 Flow 的结果并去重 + 5. uro 去重(如果启用) + 6. httpx 验证(如果启用) + + Args: + scan_id: 扫描 ID + target_name: 目标名称 + target_id: 目标 ID + scan_workspace_dir: 扫描工作目录 + enabled_tools: 启用的工具配置 + + Returns: + dict: 扫描结果 + """ + try: + logger.info( + "="*60 + "\n" + + "开始 URL 获取扫描\n" + + f" Scan ID: {scan_id}\n" + + f" Target: {target_name}\n" + + f" Workspace: {scan_workspace_dir}\n" + + "="*60 + ) + + # Step 1: 准备工作目录 + logger.info("Step 1: 准备工作目录") + url_fetch_dir = _setup_url_fetch_directory(scan_workspace_dir) + + # Step 2: 分类工具(按输入类型) + logger.info("Step 2: 分类工具") + domain_name_tools, domains_file_tools, sites_file_tools, uro_config, httpx_config = _classify_tools(enabled_tools) + + logger.info( + "工具分类 - domain_name: %s, domains_file: %s, sites_file: %s, uro: %s, httpx: %s", + list(domain_name_tools.keys()) or '无', + list(domains_file_tools.keys()) or '无', + list(sites_file_tools.keys()) or '无', + '启用' if uro_config else '未启用', + '启用' if httpx_config else '未启用' + ) + + # 检查是否有获取工具 + if not domain_name_tools and not domains_file_tools and not sites_file_tools: + raise ValueError( + "URL Fetch 流程需要至少启用一个 URL 获取工具(如 waymore, katana)。" + "httpx 和 uro 仅用于后处理,不能单独使用。" + ) + + # Step 3: 并行执行子 Flow + all_result_files = [] + all_failed_tools = [] + all_successful_tools = [] + + # 3a: 基于 domain_name(target_name) 的 URL 被动收集(如 waymore) + if domain_name_tools: + logger.info("Step 3a: 执行基于 domain_name 的 URL 被动收集子 Flow") + tn_result = domain_name_url_fetch_flow( + scan_id=scan_id, + target_id=target_id, + target_name=target_name, + output_dir=str(url_fetch_dir), + domain_name_tools=domain_name_tools, + ) + all_result_files.extend(tn_result.get('result_files', [])) + all_failed_tools.extend(tn_result.get('failed_tools', [])) + all_successful_tools.extend(tn_result.get('successful_tools', [])) + + # 3b: 基于 domains_file 的 URL 被动收集 + if domains_file_tools: + logger.info("Step 3b: 执行基于 domains_file 的 URL 被动收集子 Flow") + passive_result = domains_url_fetch_flow( + scan_id=scan_id, + target_id=target_id, + target_name=target_name, + output_dir=str(url_fetch_dir), + enabled_tools=domains_file_tools, + ) + all_result_files.extend(passive_result.get('result_files', [])) + all_failed_tools.extend(passive_result.get('failed_tools', [])) + all_successful_tools.extend(passive_result.get('successful_tools', [])) + + # 3c: 爬虫(以 sites_file 为输入) + if sites_file_tools: + logger.info("Step 3c: 执行爬虫子 Flow") + crawl_result = sites_url_fetch_flow( + scan_id=scan_id, + target_id=target_id, + target_name=target_name, + output_dir=str(url_fetch_dir), + enabled_tools=sites_file_tools + ) + all_result_files.extend(crawl_result.get('result_files', [])) + all_failed_tools.extend(crawl_result.get('failed_tools', [])) + all_successful_tools.extend(crawl_result.get('successful_tools', [])) + + # 检查是否有成功的工具 + if not all_result_files: + error_details = "; ".join([f"{f['tool']}: {f['reason']}" for f in all_failed_tools]) + logger.warning("所有 URL 获取工具均失败 - 目标: %s, 失败详情: %s", target_name, error_details) + # 返回空结果,不抛出异常,让扫描继续 + return { + 'success': True, + 'scan_id': scan_id, + 'target': target_name, + 'unique_url_count': 0, + 'valid_url_count': 0, + 'failed_tools': all_failed_tools, + 'successful_tools': [], + 'message': '所有 URL 获取工具均无结果' + } + + # Step 4: 合并并去重 URL + logger.info("Step 4: 合并并去重 URL") + merged_file, unique_url_count = _merge_and_deduplicate_urls( + result_files=all_result_files, + url_fetch_dir=url_fetch_dir + ) + + # Step 5: 使用 uro 清理 URL(如果启用) + url_file_for_validation = merged_file + uro_removed_count = 0 + + if uro_config and uro_config.get('enabled', False): + logger.info("Step 5: 使用 uro 清理 URL") + url_file_for_validation, cleaned_count, uro_removed_count = _clean_urls_with_uro( + merged_file=merged_file, + uro_config=uro_config, + url_fetch_dir=url_fetch_dir + ) + else: + logger.info("Step 5: 跳过 uro 清理(未启用)") + + # Step 6: 使用 httpx 验证存活并保存(如果启用) + if httpx_config and httpx_config.get('enabled', False): + logger.info("Step 6: 使用 httpx 验证 URL 存活并流式保存") + saved_count = _validate_and_stream_save_urls( + merged_file=url_file_for_validation, + httpx_config=httpx_config, + url_fetch_dir=url_fetch_dir, + scan_id=scan_id, + target_id=target_id + ) + else: + logger.info("Step 6: 保存到数据库(未启用 httpx 验证)") + saved_count = _save_urls_to_database( + merged_file=url_file_for_validation, + scan_id=scan_id, + target_id=target_id + ) + + logger.info("="*60 + "\n✓ URL 获取扫描完成\n" + "="*60) + + # 构建已执行的任务列表 + executed_tasks = ['setup_directory', 'classify_tools'] + if domain_name_tools: + executed_tasks.append('domain_name_url_fetch_flow') + if domains_file_tools: + executed_tasks.append('domains_url_fetch_flow') + if sites_file_tools: + executed_tasks.append('sites_url_fetch_flow') + executed_tasks.append('merge_and_deduplicate') + if uro_config and uro_config.get('enabled', False): + executed_tasks.append('uro_clean') + if httpx_config and httpx_config.get('enabled', False): + executed_tasks.append('httpx_validation_and_save') + else: + executed_tasks.append('save_urls') + + return { + 'success': True, + 'scan_id': scan_id, + 'target': target_name, + 'scan_workspace_dir': scan_workspace_dir, + 'total': saved_count, + 'executed_tasks': executed_tasks, + 'tool_stats': { + 'total': len(domain_name_tools) + len(domains_file_tools) + len(sites_file_tools), + 'successful': len(all_successful_tools), + 'failed': len(all_failed_tools), + 'successful_tools': all_successful_tools, + 'failed_tools': [f['tool'] for f in all_failed_tools] + } + } + + except Exception as e: + logger.error("URL 获取扫描失败: %s", e, exc_info=True) + raise diff --git a/backend/apps/scan/flows/url_fetch/sites_url_fetch_flow.py b/backend/apps/scan/flows/url_fetch/sites_url_fetch_flow.py new file mode 100644 index 00000000..ea225d72 --- /dev/null +++ b/backend/apps/scan/flows/url_fetch/sites_url_fetch_flow.py @@ -0,0 +1,139 @@ +""" +URL 爬虫 Flow + +主动爬取网站页面,提取 URL 和 JS 端点 +工具:katana, gospider, hakrawler 等 +输入:sites_file(站点 URL 列表) +""" + +# Django 环境初始化 +from apps.common.prefect_django_setup import setup_django_for_prefect + +import logging +from pathlib import Path + +from prefect import flow + +from .utils import run_tools_parallel + +logger = logging.getLogger(__name__) + + +def _export_sites_file(target_id: int, scan_id: int, output_dir: Path) -> tuple[str, int]: + """ + 导出站点 URL 列表到文件 + + Args: + target_id: 目标 ID + scan_id: 扫描 ID + output_dir: 输出目录 + + Returns: + tuple: (file_path, count) + """ + from apps.scan.tasks.url_fetch import export_target_assets_task + + output_file = str(output_dir / "sites.txt") + result = export_target_assets_task( + output_file=output_file, + target_id=target_id, + scan_id=scan_id, + input_type="sites_file" + ) + + count = result['asset_count'] + if count == 0: + logger.warning("站点列表为空,爬虫可能无法正常工作") + else: + logger.info("✓ 站点列表导出完成 - 数量: %d", count) + + return output_file, count + + +@flow(name="sites_url_fetch_flow", log_prints=True) +def sites_url_fetch_flow( + scan_id: int, + target_id: int, + target_name: str, + output_dir: str, + enabled_tools: dict +) -> dict: + """ + URL 爬虫子 Flow + + 执行流程: + 1. 导出站点 URL 列表(sites_file) + 2. 并行执行爬虫工具 + 3. 返回结果文件列表 + + Args: + scan_id: 扫描 ID + target_id: 目标 ID + target_name: 目标名称 + output_dir: 输出目录 + enabled_tools: 启用的爬虫工具配置 + + Returns: + dict: { + 'success': bool, + 'result_files': list, + 'failed_tools': list, + 'successful_tools': list, + 'sites_count': int + } + """ + try: + output_path = Path(output_dir) + + logger.info( + "开始 URL 爬虫 - Target: %s, Tools: %s", + target_name, ', '.join(enabled_tools.keys()) + ) + + # Step 1: 导出站点 URL 列表 + sites_file, sites_count = _export_sites_file( + target_id=target_id, + scan_id=scan_id, + output_dir=output_path + ) + + if sites_count == 0: + logger.warning("没有可用的站点,跳过爬虫") + return { + 'success': True, + 'result_files': [], + 'failed_tools': [], + 'successful_tools': [], + 'sites_count': 0 + } + + # Step 2: 并行执行爬虫工具 + result_files, failed_tools, successful_tools = run_tools_parallel( + tools=enabled_tools, + input_file=sites_file, + input_type="sites_file", + output_dir=output_path + ) + + logger.info( + "✓ 爬虫完成 - 成功: %d/%d, 结果文件: %d", + len(successful_tools), len(enabled_tools), len(result_files) + ) + + return { + 'success': True, + 'result_files': result_files, + 'failed_tools': failed_tools, + 'successful_tools': successful_tools, + 'sites_count': sites_count + } + + except Exception as e: + logger.error("URL 爬虫失败: %s", e, exc_info=True) + return { + 'success': False, + 'result_files': [], + 'failed_tools': [{'tool': 'sites_url_fetch_flow', 'reason': str(e)}], + 'successful_tools': [], + 'sites_count': 0 + } diff --git a/backend/apps/scan/flows/url_fetch/utils.py b/backend/apps/scan/flows/url_fetch/utils.py new file mode 100644 index 00000000..14e5a9ea --- /dev/null +++ b/backend/apps/scan/flows/url_fetch/utils.py @@ -0,0 +1,232 @@ +""" +URL Fetch 共享工具函数 +""" + +import logging +import subprocess +import uuid +from datetime import datetime +from pathlib import Path + +from apps.scan.utils import build_scan_command + +logger = logging.getLogger(__name__) + + +def calculate_timeout_by_line_count( + tool_config: dict, + file_path: str, + base_per_time: int = 1, +) -> int: + """ + 根据文件行数自动计算超时时间 + + Args: + tool_config: 工具配置(保留参数,未来可能用于更复杂的计算) + file_path: 输入文件路径 + base_per_time: 每行的基础时间(秒) + + Returns: + int: 计算出的超时时间(秒) + """ + try: + result = subprocess.run( + ['wc', '-l', file_path], + capture_output=True, + text=True, + check=True, + ) + line_count = int(result.stdout.strip().split()[0]) + timeout = line_count * base_per_time + logger.info( + "timeout 自动计算: 文件=%s, 行数=%d, 每行时间=%d秒, timeout=%d秒", + file_path, + line_count, + base_per_time, + timeout, + ) + return timeout + except Exception as e: + logger.warning("wc -l 计算行数失败: %s,将使用默认 timeout: 600秒", e) + return 600 + + +def prepare_tool_execution( + tool_name: str, + tool_config: dict, + input_file: str, + input_type: str, + output_dir: Path, + scan_type: str = "url_fetch" +) -> dict: + """ + 准备单个工具的执行参数 + + Args: + tool_name: 工具名称 + tool_config: 工具配置 + input_file: 输入文件路径 + input_type: 输入类型(domains_file 或 sites_file) + output_dir: 输出目录 + scan_type: 扫描类型 + + Returns: + dict: 执行参数,包含 command, input_file, output_file, timeout + 或包含 error 键表示失败 + """ + # 1. 统计输入文件行数 + try: + with open(input_file, 'r') as f: + input_count = sum(1 for _ in f) + logger.info("工具 %s - 输入类型: %s, 数量: %d", tool_name, input_type, input_count) + except Exception as e: + return {"error": f"读取输入文件失败: {e}"} + + # 2. 生成输出文件路径(带时间戳和短 UUID 后缀) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + short_uuid = uuid.uuid4().hex[:4] + output_file = str(output_dir / f"{tool_name}_{timestamp}_{short_uuid}.txt") + + # 3. 构建命令 + command_params = { + input_type: input_file, + "output_file": output_file, + } + + try: + command = build_scan_command( + tool_name=tool_name, + scan_type=scan_type, + command_params=command_params, + tool_config=tool_config, + ) + except Exception as e: + logger.error("构建 %s 命令失败: %s", tool_name, e) + return {"error": f"命令构建失败: {e}"} + + # 4. 计算超时时间(支持 auto 和显式整数) + raw_timeout = tool_config.get("timeout", 3600) + timeout = 3600 + + if isinstance(raw_timeout, str) and raw_timeout == "auto": + try: + # katana / waymore 每个站点需要更长时间 + base_per_time = 360 if tool_name in ("katana", "waymore") else 1 + timeout = calculate_timeout_by_line_count( + tool_config=tool_config, + file_path=input_file, + base_per_time=base_per_time, + ) + except Exception as e: + logger.warning( + "工具 %s 自动计算 timeout 失败,将使用默认 3600 秒: %s", + tool_name, + e, + ) + timeout = 3600 + else: + try: + timeout = int(raw_timeout) + except (TypeError, ValueError): + logger.warning( + "工具 %s 的 timeout 配置无效(%s),将使用默认 3600 秒", + tool_name, + raw_timeout, + ) + timeout = 3600 + + # 5. 返回执行参数 + return { + "command": command, + "input_file": input_file, + "input_type": input_type, + "output_file": output_file, + "timeout": timeout, + } + + +def run_tools_parallel( + tools: dict, + input_file: str, + input_type: str, + output_dir: Path +) -> tuple[list, list, list]: + """ + 并行执行工具列表 + + Args: + tools: 工具配置字典 {tool_name: tool_config} + input_file: 输入文件路径 + input_type: 输入类型 + output_dir: 输出目录 + + Returns: + tuple: (result_files, failed_tools, successful_tool_names) + """ + from apps.scan.tasks.url_fetch import run_url_fetcher_task + + futures: dict[str, object] = {} + failed_tools: list[dict] = [] + + # 提交所有工具的并行任务 + for tool_name, tool_config in tools.items(): + exec_params = prepare_tool_execution( + tool_name=tool_name, + tool_config=tool_config, + input_file=input_file, + input_type=input_type, + output_dir=output_dir, + ) + + if "error" in exec_params: + failed_tools.append({"tool": tool_name, "reason": exec_params["error"]}) + continue + + logger.info( + "提交任务 - 工具: %s, 输入: %s, 超时: %d秒", + tool_name, + input_type, + exec_params["timeout"], + ) + + # 提交并行任务 + future = run_url_fetcher_task.submit( + tool_name=tool_name, + command=exec_params["command"], + timeout=exec_params["timeout"], + output_file=exec_params["output_file"], + ) + futures[tool_name] = future + + # 收集执行结果 + result_files = [] + for tool_name, future in futures.items(): + try: + result = future.result() + if result and result['success']: + result_files.append(result['output_file']) + logger.info( + "✓ 工具 %s 执行成功 - 发现 URL: %d", + tool_name, result['url_count'] + ) + else: + failed_tools.append({ + 'tool': tool_name, + 'reason': '未生成结果或无有效URL' + }) + logger.warning("⚠️ 工具 %s 未生成有效结果", tool_name) + except Exception as e: + failed_tools.append({ + 'tool': tool_name, + 'reason': str(e) + }) + logger.warning("⚠️ 工具 %s 执行失败: %s", tool_name, e) + + # 计算成功的工具列表 + failed_tool_names = [f['tool'] for f in failed_tools] + successful_tool_names = [ + name for name in tools.keys() + if name not in failed_tool_names + ] + + return result_files, failed_tools, successful_tool_names diff --git a/backend/apps/scan/flows/vuln_scan/__init__.py b/backend/apps/scan/flows/vuln_scan/__init__.py new file mode 100644 index 00000000..2431205b --- /dev/null +++ b/backend/apps/scan/flows/vuln_scan/__init__.py @@ -0,0 +1,14 @@ +"""vuln_scan Flow 模块 + +包含漏洞扫描相关的 Flow: +- vuln_scan_flow: 主 Flow(编排各类漏洞扫描子 Flow) +- endpoints_vuln_scan_flow: 基于 endpoints_file 的漏洞扫描子 Flow(Dalfox 等) +""" + +from .main_flow import vuln_scan_flow +from .endpoints_vuln_scan_flow import endpoints_vuln_scan_flow + +__all__ = [ + "vuln_scan_flow", + "endpoints_vuln_scan_flow", +] diff --git a/backend/apps/scan/flows/vuln_scan/endpoints_vuln_scan_flow.py b/backend/apps/scan/flows/vuln_scan/endpoints_vuln_scan_flow.py new file mode 100644 index 00000000..9965c42d --- /dev/null +++ b/backend/apps/scan/flows/vuln_scan/endpoints_vuln_scan_flow.py @@ -0,0 +1,242 @@ +from apps.common.prefect_django_setup import setup_django_for_prefect + +import logging +from datetime import datetime +from pathlib import Path +from typing import Dict + +from prefect import flow + +from apps.scan.handlers.scan_flow_handlers import ( + on_scan_flow_running, + on_scan_flow_completed, + on_scan_flow_failed, +) +from apps.scan.utils import build_scan_command, ensure_nuclei_templates_local +from apps.scan.tasks.vuln_scan import ( + export_endpoints_task, + run_vuln_tool_task, + run_and_stream_save_dalfox_vulns_task, + run_and_stream_save_nuclei_vulns_task, +) +from .utils import calculate_timeout_by_line_count + + +logger = logging.getLogger(__name__) + + +def _setup_vuln_scan_directory(scan_workspace_dir: str) -> Path: + vuln_scan_dir = Path(scan_workspace_dir) / "vuln_scan" + vuln_scan_dir.mkdir(parents=True, exist_ok=True) + return vuln_scan_dir + + +@flow( + name="endpoints_vuln_scan_flow", + log_prints=True, +) +def endpoints_vuln_scan_flow( + scan_id: int, + target_name: str, + target_id: int, + scan_workspace_dir: str, + enabled_tools: Dict[str, dict], +) -> dict: + """基于 Endpoint 的漏洞扫描 Flow(串行执行 Dalfox 等工具)。""" + try: + if scan_id is None: + raise ValueError("scan_id 不能为空") + if not target_name: + raise ValueError("target_name 不能为空") + if target_id is None: + raise ValueError("target_id 不能为空") + if not scan_workspace_dir: + raise ValueError("scan_workspace_dir 不能为空") + if not enabled_tools: + raise ValueError("enabled_tools 不能为空") + + vuln_scan_dir = _setup_vuln_scan_directory(scan_workspace_dir) + endpoints_file = vuln_scan_dir / "input_endpoints.txt" + + # Step 1: 导出 Endpoint URL + export_result = export_endpoints_task( + target_id=target_id, + output_file=str(endpoints_file), + ) + total_endpoints = export_result.get("total_count", 0) + + if total_endpoints == 0 or not endpoints_file.exists() or endpoints_file.stat().st_size == 0: + logger.warning("目标下没有可用 Endpoint,跳过漏洞扫描") + return { + "success": True, + "scan_id": scan_id, + "target": target_name, + "scan_workspace_dir": scan_workspace_dir, + "endpoints_file": str(endpoints_file), + "endpoint_count": 0, + "executed_tools": [], + "tool_results": {}, + } + + logger.info("Endpoint 导出完成,共 %d 条,开始执行漏洞扫描", total_endpoints) + + tool_results: Dict[str, dict] = {} + + # Step 2: 并行执行每个漏洞扫描工具(目前主要是 Dalfox) + # 1)先为每个工具 submit Prefect Task,让 Worker 并行调度 + # 2)再统一收集各自的结果,组装成 tool_results + tool_futures: Dict[str, dict] = {} + + for tool_name, tool_config in enabled_tools.items(): + # Nuclei 需要先确保本地模板存在(支持多个模板仓库) + template_args = "" + if tool_name == "nuclei": + repo_names = tool_config.get("template_repo_names") + if not repo_names or not isinstance(repo_names, (list, tuple)): + logger.error("Nuclei 配置缺少 template_repo_names(数组),跳过") + continue + template_paths = [] + try: + for repo_name in repo_names: + path = ensure_nuclei_templates_local(repo_name) + template_paths.append(path) + logger.info("Nuclei 模板路径 [%s]: %s", repo_name, path) + except Exception as e: + logger.error("获取 Nuclei 模板失败: %s,跳过 nuclei 扫描", e) + continue + template_args = " ".join(f"-t {p}" for p in template_paths) + + # 构建命令参数 + command_params = {"endpoints_file": str(endpoints_file)} + if template_args: + command_params["template_args"] = template_args + + command = build_scan_command( + tool_name=tool_name, + scan_type="vuln_scan", + command_params=command_params, + tool_config=tool_config, + ) + + raw_timeout = tool_config.get("timeout", 600) + timeout = 600 + + if isinstance(raw_timeout, str) and raw_timeout == "auto": + # timeout=auto 时,根据 endpoints_file 行数自动计算超时时间 + # Dalfox: 每行 100 秒,Nuclei: 每行 30 秒 + base_per_time = 30 if tool_name == "nuclei" else 100 + timeout = calculate_timeout_by_line_count( + tool_config=tool_config, + file_path=str(endpoints_file), + base_per_time=base_per_time, + ) + else: + try: + timeout = int(raw_timeout) + except (TypeError, ValueError) as e: + # 配置错误应当直接暴露,避免默默使用默认值导致排查困难 + raise ValueError( + f"工具 {tool_name} 的 timeout 配置无效: {raw_timeout!r}" + ) from e + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + log_file = vuln_scan_dir / f"{tool_name}_{timestamp}.log" + + # Dalfox XSS 使用流式任务,一边解析一边保存漏洞结果 + if tool_name == "dalfox_xss": + logger.info("开始执行漏洞扫描工具 %s(流式保存漏洞结果,已提交任务)", tool_name) + future = run_and_stream_save_dalfox_vulns_task.submit( + cmd=command, + tool_name=tool_name, + scan_id=scan_id, + target_id=target_id, + cwd=str(vuln_scan_dir), + shell=True, + batch_size=1, + timeout=timeout, + log_file=str(log_file), + ) + + tool_futures[tool_name] = { + "future": future, + "command": command, + "timeout": timeout, + "log_file": str(log_file), + "mode": "streaming", + } + elif tool_name == "nuclei": + # Nuclei 使用流式任务 + logger.info("开始执行漏洞扫描工具 %s(流式保存漏洞结果,已提交任务)", tool_name) + future = run_and_stream_save_nuclei_vulns_task.submit( + cmd=command, + tool_name=tool_name, + scan_id=scan_id, + target_id=target_id, + cwd=str(vuln_scan_dir), + shell=True, + batch_size=10, + timeout=timeout, + log_file=str(log_file), + ) + + tool_futures[tool_name] = { + "future": future, + "command": command, + "timeout": timeout, + "log_file": str(log_file), + "mode": "streaming", + } + else: + # 其他工具仍使用非流式执行逻辑 + logger.info("开始执行漏洞扫描工具 %s(已提交任务)", tool_name) + future = run_vuln_tool_task.submit( + tool_name=tool_name, + command=command, + timeout=timeout, + log_file=str(log_file), + ) + + tool_futures[tool_name] = { + "future": future, + "command": command, + "timeout": timeout, + "log_file": str(log_file), + "mode": "normal", + } + + # 统一收集所有工具的执行结果 + for tool_name, meta in tool_futures.items(): + future = meta["future"] + result = future.result() + + if meta["mode"] == "streaming": + tool_results[tool_name] = { + "command": meta["command"], + "timeout": meta["timeout"], + "processed_records": result.get("processed_records"), + "created_vulns": result.get("created_vulns"), + "command_log_file": meta["log_file"], + } + else: + tool_results[tool_name] = { + "command": meta["command"], + "timeout": meta["timeout"], + "duration": result.get("duration"), + "returncode": result.get("returncode"), + "command_log_file": result.get("command_log_file"), + } + + return { + "success": True, + "scan_id": scan_id, + "target": target_name, + "scan_workspace_dir": scan_workspace_dir, + "endpoints_file": str(endpoints_file), + "endpoint_count": total_endpoints, + "executed_tools": list(enabled_tools.keys()), + "tool_results": tool_results, + } + + except Exception as e: + logger.exception("Endpoint 漏洞扫描失败: %s", e) + raise diff --git a/backend/apps/scan/flows/vuln_scan/main_flow.py b/backend/apps/scan/flows/vuln_scan/main_flow.py new file mode 100644 index 00000000..31c6b48c --- /dev/null +++ b/backend/apps/scan/flows/vuln_scan/main_flow.py @@ -0,0 +1,107 @@ +from apps.common.prefect_django_setup import setup_django_for_prefect + +import logging +from typing import Dict, Tuple + +from prefect import flow + +from apps.scan.handlers.scan_flow_handlers import ( + on_scan_flow_running, + on_scan_flow_completed, + on_scan_flow_failed, +) +from apps.scan.configs.command_templates import get_command_template +from .endpoints_vuln_scan_flow import endpoints_vuln_scan_flow + + +logger = logging.getLogger(__name__) + + +def _classify_vuln_tools(enabled_tools: Dict[str, dict]) -> Tuple[Dict[str, dict], Dict[str, dict]]: + """根据命令模板中的 input_type 对漏洞扫描工具进行分类。 + + 当前支持: + - endpoints_file: 以端点列表文件为输入(例如 Dalfox XSS) + 预留: + - 其他 input_type 将被归类到 other_tools,暂不处理。 + """ + endpoints_tools: Dict[str, dict] = {} + other_tools: Dict[str, dict] = {} + + for tool_name, tool_config in enabled_tools.items(): + template = get_command_template("vuln_scan", tool_name) or {} + input_type = template.get("input_type", "endpoints_file") + + if input_type == "endpoints_file": + endpoints_tools[tool_name] = tool_config + else: + other_tools[tool_name] = tool_config + + return endpoints_tools, other_tools + + +@flow( + name="vuln_scan", + log_prints=True, + on_running=[on_scan_flow_running], + on_completion=[on_scan_flow_completed], + on_failure=[on_scan_flow_failed], +) +def vuln_scan_flow( + scan_id: int, + target_name: str, + target_id: int, + scan_workspace_dir: str, + enabled_tools: Dict[str, dict], +) -> dict: + """漏洞扫描主 Flow:串行编排各类漏洞扫描子 Flow。 + + 支持工具: + - dalfox_xss: XSS 漏洞扫描(流式保存) + - nuclei: 通用漏洞扫描(流式保存,支持模板 commit hash 同步) + """ + try: + if scan_id is None: + raise ValueError("scan_id 不能为空") + if not target_name: + raise ValueError("target_name 不能为空") + if target_id is None: + raise ValueError("target_id 不能为空") + if not scan_workspace_dir: + raise ValueError("scan_workspace_dir 不能为空") + if not enabled_tools: + raise ValueError("enabled_tools 不能为空") + + # Step 1: 分类工具 + endpoints_tools, other_tools = _classify_vuln_tools(enabled_tools) + + logger.info( + "漏洞扫描工具分类 - endpoints_file: %s, 其他: %s", + list(endpoints_tools.keys()) or "无", + list(other_tools.keys()) or "无", + ) + + if other_tools: + logger.warning( + "存在暂不支持输入类型的漏洞扫描工具,将被忽略: %s", + list(other_tools.keys()), + ) + + if not endpoints_tools: + raise ValueError("漏洞扫描需要至少启用一个以 endpoints_file 为输入的工具(如 dalfox_xss、nuclei)。") + + # Step 2: 执行 Endpoint 漏洞扫描子 Flow(串行) + endpoint_result = endpoints_vuln_scan_flow( + scan_id=scan_id, + target_name=target_name, + target_id=target_id, + scan_workspace_dir=scan_workspace_dir, + enabled_tools=endpoints_tools, + ) + + # 目前只有一个子 Flow,直接返回其结果 + return endpoint_result + + except Exception as e: + logger.exception("漏洞扫描主 Flow 失败: %s", e) + raise diff --git a/backend/apps/scan/flows/vuln_scan/utils.py b/backend/apps/scan/flows/vuln_scan/utils.py new file mode 100644 index 00000000..062a6907 --- /dev/null +++ b/backend/apps/scan/flows/vuln_scan/utils.py @@ -0,0 +1,46 @@ +""" +Vuln Scan 共享工具函数 +""" + +import logging +import subprocess + +logger = logging.getLogger(__name__) + + +def calculate_timeout_by_line_count( + tool_config: dict, + file_path: str, + base_per_time: int = 1, +) -> int: + """ + 根据文件行数自动计算超时时间 + + Args: + tool_config: 工具配置(保留参数,未来可能用于更复杂的计算) + file_path: 输入文件路径 + base_per_time: 每行的基础时间(秒) + + Returns: + int: 计算出的超时时间(秒) + """ + try: + result = subprocess.run( + ["wc", "-l", file_path], + capture_output=True, + text=True, + check=True, + ) + line_count = int(result.stdout.strip().split()[0]) + timeout = line_count * base_per_time + logger.info( + "timeout 自动计算: 文件=%s, 行数=%d, 每行时间=%d秒, timeout=%d秒", + file_path, + line_count, + base_per_time, + timeout, + ) + return timeout + except Exception as e: + logger.error("wc -l 计算行数失败: %s", e) + raise RuntimeError(f"自动计算超时时间失败: {e}") from e diff --git a/backend/apps/scan/handlers/__init__.py b/backend/apps/scan/handlers/__init__.py new file mode 100644 index 00000000..52fe9316 --- /dev/null +++ b/backend/apps/scan/handlers/__init__.py @@ -0,0 +1,28 @@ +"""Prefect Flow 状态处理器 + +当前架构使用 Docker + SSH 执行任务,不使用 Prefect Server。 +docker stop 会触发 on_failure 处理器。 +""" + +from .initiate_scan_flow_handlers import ( + on_initiate_scan_flow_running, + on_initiate_scan_flow_completed, + on_initiate_scan_flow_failed, +) + +from .scan_flow_handlers import ( + on_scan_flow_running, + on_scan_flow_completed, + on_scan_flow_failed, +) + +__all__ = [ + # 初始化扫描流程处理器 + 'on_initiate_scan_flow_running', + 'on_initiate_scan_flow_completed', + 'on_initiate_scan_flow_failed', + # 通用扫描流程处理器 + 'on_scan_flow_running', + 'on_scan_flow_completed', + 'on_scan_flow_failed', +] diff --git a/backend/apps/scan/handlers/initiate_scan_flow_handlers.py b/backend/apps/scan/handlers/initiate_scan_flow_handlers.py new file mode 100644 index 00000000..ef91fac4 --- /dev/null +++ b/backend/apps/scan/handlers/initiate_scan_flow_handlers.py @@ -0,0 +1,277 @@ +""" +initiate_scan_flow 状态处理器 + +负责 initiate_scan_flow 生命周期的状态同步: +- on_running: Flow 开始运行时更新扫描状态为 RUNNING +- on_completion: Flow 成功完成时更新扫描状态为 COMPLETED +- on_failure: Flow 失败时更新扫描状态为 FAILED(包括超时、异常、docker stop 等) + +策略:快速失败(Fail-Fast) +- 任何子任务失败都会导致 Flow 失败 +- Flow 成功 = 所有任务成功 +""" + +import logging +from prefect import Flow +from prefect.client.schemas import FlowRun, State + +logger = logging.getLogger(__name__) + + + +def on_initiate_scan_flow_running(flow: Flow, flow_run: FlowRun, state: State) -> None: + """ + initiate_scan_flow 开始运行时的回调 + + 职责:更新 Scan 状态为 RUNNING + 发送通知 + + 触发时机: + - Prefect Flow 状态变为 Running 时自动触发 + - 在 Flow 函数体执行之前调用 + + Args: + flow: Prefect Flow 对象 + flow_run: Flow 运行实例 + state: Flow 当前状态 + """ + logger.info("🚀 initiate_scan_flow_running 回调开始运行 - Flow Run: %s", flow_run.id) + + scan_id = flow_run.parameters.get('scan_id') + target_name = flow_run.parameters.get('target_name') + engine_name = flow_run.parameters.get('engine_name') + scheduled_scan_name = flow_run.parameters.get('scheduled_scan_name') + + if not scan_id: + logger.warning( + "Flow 参数中缺少 scan_id,跳过状态更新 - Flow Run: %s", + flow_run.id + ) + return + + def _update_running_status(): + from apps.scan.services import ScanService + from apps.common.definitions import ScanStatus + + service = ScanService() + success = service.update_status( + scan_id, + ScanStatus.RUNNING + ) + + if success: + logger.info( + "✓ Flow 状态回调:扫描状态已更新为 RUNNING - Scan ID: %s, Flow Run: %s", + scan_id, + flow_run.id + ) + else: + logger.error( + "✗ Flow 状态回调:更新扫描状态失败 - Scan ID: %s", + scan_id + ) + return success + + # 执行状态更新(Repository 层已有 @auto_ensure_db_connection 保证连接可靠性) + _update_running_status() + + # 发送通知 + logger.info("准备发送扫描开始通知 - Scan ID: %s, Target: %s", scan_id, target_name) + try: + from apps.scan.notifications import create_notification, NotificationLevel, NotificationCategory + + # 根据是否为定时扫描构建不同的标题和消息 + if scheduled_scan_name: + title = f"⏰ {target_name} 扫描开始" + message = f"定时任务:{scheduled_scan_name}\n引擎:{engine_name}" + else: + title = f"{target_name} 扫描开始" + message = f"引擎:{engine_name}" + + create_notification( + title=title, + message=message, + level=NotificationLevel.MEDIUM, + category=NotificationCategory.SCAN + ) + logger.info("✓ 扫描开始通知已发送 - Scan ID: %s, Target: %s", scan_id, target_name) + except Exception as e: + logger.error(f"发送扫描开始通知失败 - Scan ID: {scan_id}: {e}", exc_info=True) + + +def on_initiate_scan_flow_completed(flow: Flow, flow_run: FlowRun, state: State) -> None: + """ + initiate_scan_flow 成功完成时的回调 + + 职责:更新 Scan 状态为 COMPLETED + + 触发时机: + - Prefect Flow 正常执行完成时自动触发 + - 在 Flow 函数体返回之后调用 + + 策略:快速失败(Fail-Fast) + - Flow 成功完成 = 所有任务成功 → COMPLETED + - Flow 执行失败 = 有任务失败 → FAILED (由 on_failed 处理) + + 竞态条件处理: + - 如果用户已手动取消(状态已是 CANCELLED),保持终态,不覆盖 + + Args: + flow: Prefect Flow 对象 + flow_run: Flow 运行实例 + state: Flow 当前状态 + """ + logger.info("✅ initiate_scan_flow_completed 回调开始运行 - Flow Run: %s", flow_run.id) + + scan_id = flow_run.parameters.get('scan_id') + target_name = flow_run.parameters.get('target_name') + engine_name = flow_run.parameters.get('engine_name') + + if not scan_id: + return + + def _update_completed_status(): + from apps.scan.services import ScanService + from apps.common.definitions import ScanStatus + from django.utils import timezone + + service = ScanService() + + # 仅在运行中时更新为 COMPLETED;其他状态保持不变 + completed_updated = service.update_status_if_match( + scan_id=scan_id, + current_status=ScanStatus.RUNNING, + new_status=ScanStatus.COMPLETED, + stopped_at=timezone.now() + ) + + if completed_updated: + logger.info( + "✓ Flow 状态回调:扫描状态已原子更新为 COMPLETED - Scan ID: %s, Flow Run: %s", + scan_id, + flow_run.id + ) + return service.update_cached_stats(scan_id) + else: + logger.info( + "ℹ️ Flow 状态回调:状态未更新(可能已是终态)- Scan ID: %s, Flow Run: %s", + scan_id, + flow_run.id + ) + return None + + # 执行状态更新并获取统计数据 + stats = _update_completed_status() + + # 发送通知(包含统计摘要) + logger.info("准备发送扫描完成通知 - Scan ID: %s, Target: %s", scan_id, target_name) + try: + from apps.scan.notifications import create_notification, NotificationLevel, NotificationCategory + + # 构建通知消息 + message = f"引擎:{engine_name}" + if stats: + results = [] + results.append(f"子域名: {stats.get('subdomains', 0)}") + results.append(f"站点: {stats.get('websites', 0)}") + results.append(f"IP: {stats.get('ips', 0)}") + results.append(f"端点: {stats.get('endpoints', 0)}") + results.append(f"目录: {stats.get('directories', 0)}") + vulns_total = stats.get('vulns_total', 0) + if vulns_total > 0: + results.append(f"漏洞: {vulns_total} (严重:{stats.get('vulns_critical', 0)} 高:{stats.get('vulns_high', 0)} 中:{stats.get('vulns_medium', 0)} 低:{stats.get('vulns_low', 0)})") + else: + results.append("漏洞: 0") + message += f"\n结果:{' | '.join(results)}" + + create_notification( + title=f"{target_name} 扫描完成", + message=message, + level=NotificationLevel.MEDIUM, + category=NotificationCategory.SCAN + ) + logger.info("✓ 扫描完成通知已发送 - Scan ID: %s, Target: %s", scan_id, target_name) + except Exception as e: + logger.error(f"发送扫描完成通知失败 - Scan ID: {scan_id}: {e}", exc_info=True) + + +def on_initiate_scan_flow_failed(flow: Flow, flow_run: FlowRun, state: State) -> None: + """ + initiate_scan_flow 失败时的回调 + + 职责:更新 Scan 状态为 FAILED,并记录错误信息 + + 触发时机: + - Prefect Flow 执行失败或抛出异常时自动触发 + - Flow 超时、任务失败等所有失败场景都会触发此回调 + + 竞态条件处理: + - 如果用户已手动取消(状态已是 CANCELLED),保持终态,不覆盖 + + Args: + flow: Prefect Flow 对象 + flow_run: Flow 运行实例 + state: Flow 当前状态(包含错误信息) + """ + logger.info("❌ initiate_scan_flow_failed 回调开始运行 - Flow Run: %s", flow_run.id) + + scan_id = flow_run.parameters.get('scan_id') + target_name = flow_run.parameters.get('target_name') + engine_name = flow_run.parameters.get('engine_name') + + if not scan_id: + return + + def _update_failed_status(): + from apps.scan.services import ScanService + from apps.common.definitions import ScanStatus + from django.utils import timezone + + service = ScanService() + + # 提取错误信息 + error_message = str(state.message) if state.message else "Flow 执行失败" + + # 仅在运行中时更新为 FAILED;其他状态保持不变 + failed_updated = service.update_status_if_match( + scan_id=scan_id, + current_status=ScanStatus.RUNNING, + new_status=ScanStatus.FAILED, + stopped_at=timezone.now() + ) + + if failed_updated: + # 成功更新(正常失败流程) + logger.error( + "✗ Flow 状态回调:扫描状态已原子更新为 FAILED - Scan ID: %s, Flow Run: %s, 错误: %s", + scan_id, + flow_run.id, + error_message + ) + # 更新缓存统计数据(终态) + service.update_cached_stats(scan_id) + else: + logger.warning( + "⚠️ Flow 状态回调:未更新任何记录(可能已被其他进程处理)- Scan ID: %s, Flow Run: %s", + scan_id, + flow_run.id + ) + return True + + # 执行状态更新 + _update_failed_status() + + # 发送通知 + logger.info("准备发送扫描失败通知 - Scan ID: %s, Target: %s", scan_id, target_name) + try: + from apps.scan.notifications import create_notification, NotificationLevel, NotificationCategory + error_message = str(state.message) if state.message else "未知错误" + message = f"引擎:{engine_name}\n错误:{error_message}" + create_notification( + title=f"{target_name} 扫描失败", + message=message, + level=NotificationLevel.HIGH, + category=NotificationCategory.SCAN + ) + logger.info("✓ 扫描失败通知已发送 - Scan ID: %s, Target: %s", scan_id, target_name) + except Exception as e: + logger.error(f"发送扫描失败通知失败 - Scan ID: {scan_id}: {e}", exc_info=True) diff --git a/backend/apps/scan/handlers/scan_flow_handlers.py b/backend/apps/scan/handlers/scan_flow_handlers.py new file mode 100644 index 00000000..1caf545b --- /dev/null +++ b/backend/apps/scan/handlers/scan_flow_handlers.py @@ -0,0 +1,150 @@ +""" +扫描流程处理器 + +负责处理扫描流程(端口扫描、子域名发现等)的状态变化和通知 + +职责: +- 更新各阶段的进度状态(running/completed/failed) +- 发送扫描阶段的通知 +""" + +import logging +from prefect import Flow +from prefect.client.schemas import FlowRun, State + +logger = logging.getLogger(__name__) + + +def _get_stage_from_flow_name(flow_name: str) -> str | None: + """ + 从 Flow name 获取对应的 stage + + Flow name 直接作为 stage(与 engine_config 的 key 一致) + 排除主 Flow(initiate_scan) + """ + # 排除主 Flow,它不是阶段 Flow + if flow_name == 'initiate_scan': + return None + return flow_name + + +def on_scan_flow_running(flow: Flow, flow_run: FlowRun, state: State) -> None: + """ + 扫描流程开始运行时的回调 + + 职责: + - 更新阶段进度为 running + - 发送扫描开始通知 + + Args: + flow: Prefect Flow 对象 + flow_run: Flow 运行实例 + state: Flow 当前状态 + """ + logger.info("🚀 扫描流程开始运行 - Flow: %s, Run ID: %s", flow.name, flow_run.id) + + # 提取流程参数 + flow_params = flow_run.parameters or {} + scan_id = flow_params.get('scan_id') + target_name = flow_params.get('target_name', 'unknown') + + # 更新阶段进度 + stage = _get_stage_from_flow_name(flow.name) + if scan_id and stage: + try: + from apps.scan.services import ScanService + service = ScanService() + service.start_stage(scan_id, stage) + logger.info(f"✓ 阶段进度已更新为 running - Scan ID: {scan_id}, Stage: {stage}") + except Exception as e: + logger.error(f"更新阶段进度失败 - Scan ID: {scan_id}, Stage: {stage}: {e}") + + +def on_scan_flow_completed(flow: Flow, flow_run: FlowRun, state: State) -> None: + """ + 扫描流程完成时的回调 + + 职责: + - 更新阶段进度为 completed + - 发送扫描完成通知(可选) + + Args: + flow: Prefect Flow 对象 + flow_run: Flow 运行实例 + state: Flow 当前状态 + """ + logger.info("✅ 扫描流程完成 - Flow: %s, Run ID: %s", flow.name, flow_run.id) + + # 提取流程参数 + flow_params = flow_run.parameters or {} + scan_id = flow_params.get('scan_id') + + # 更新阶段进度 + stage = _get_stage_from_flow_name(flow.name) + if scan_id and stage: + try: + from apps.scan.services import ScanService + service = ScanService() + # 从 flow result 中提取 detail(如果有) + result = state.result() if state.result else None + detail = None + if isinstance(result, dict): + detail = result.get('detail') + service.complete_stage(scan_id, stage, detail) + logger.info(f"✓ 阶段进度已更新为 completed - Scan ID: {scan_id}, Stage: {stage}") + # 每个阶段完成后刷新缓存统计,便于前端实时看到增量 + try: + service.update_cached_stats(scan_id) + logger.info("✓ 阶段完成后已刷新缓存统计 - Scan ID: %s", scan_id) + except Exception as e: + logger.error("阶段完成后刷新缓存统计失败 - Scan ID: %s, 错误: %s", scan_id, e) + except Exception as e: + logger.error(f"更新阶段进度失败 - Scan ID: {scan_id}, Stage: {stage}: {e}") + + +def on_scan_flow_failed(flow: Flow, flow_run: FlowRun, state: State) -> None: + """ + 扫描流程失败时的回调 + + 职责: + - 更新阶段进度为 failed + - 发送扫描失败通知 + + Args: + flow: Prefect Flow 对象 + flow_run: Flow 运行实例 + state: Flow 当前状态 + """ + logger.info("❌ 扫描流程失败 - Flow: %s, Run ID: %s", flow.name, flow_run.id) + + # 提取流程参数 + flow_params = flow_run.parameters or {} + scan_id = flow_params.get('scan_id') + target_name = flow_params.get('target_name', 'unknown') + + # 提取错误信息 + error_message = str(state.message) if state.message else "未知错误" + + # 更新阶段进度 + stage = _get_stage_from_flow_name(flow.name) + if scan_id and stage: + try: + from apps.scan.services import ScanService + service = ScanService() + service.fail_stage(scan_id, stage, error_message) + logger.info(f"✓ 阶段进度已更新为 failed - Scan ID: {scan_id}, Stage: {stage}") + except Exception as e: + logger.error(f"更新阶段进度失败 - Scan ID: {scan_id}, Stage: {stage}: {e}") + + # 发送通知 + try: + from apps.scan.notifications import create_notification, NotificationLevel + message = f"任务:{flow.name}\n状态:执行失败\n错误:{error_message}" + create_notification( + title=target_name, + message=message, + level=NotificationLevel.HIGH + ) + logger.error(f"✓ 扫描失败通知已发送 - Target: {target_name}, Flow: {flow.name}, Error: {error_message}") + except Exception as e: + logger.error(f"发送扫描失败通知失败 - Flow: {flow.name}: {e}") diff --git a/backend/apps/scan/management/commands/generate_test_data.py b/backend/apps/scan/management/commands/generate_test_data.py new file mode 100644 index 00000000..d3be330c --- /dev/null +++ b/backend/apps/scan/management/commands/generate_test_data.py @@ -0,0 +1,567 @@ +""" +生成测试数据的管理命令 + +用法: + python manage.py generate_test_data --target example.com --count 100000 + +性能测试: + python manage.py generate_test_data --target example.com --count 10000 --batch-size 500 --benchmark +""" + +import random +import string +import time +from django.core.management.base import BaseCommand +from django.db import transaction, connection +from django.utils import timezone +from apps.targets.models import Target +from apps.scan.models import Scan +from apps.asset.models.asset_models import Subdomain, IPAddress, Port, WebSite, Directory + + +class Command(BaseCommand): + help = '为指定目标生成大量测试数据' + + def add_arguments(self, parser): + parser.add_argument( + '--target', + type=str, + required=True, + help='目标域名(如 example.com)' + ) + parser.add_argument( + '--count', + type=int, + default=100000, + help='每个表生成的记录数(默认 100000)' + ) + parser.add_argument( + '--batch-size', + type=int, + default=1000, + help='批量插入的批次大小(默认 1000)' + ) + parser.add_argument( + '--benchmark', + action='store_true', + help='启用性能基准测试模式(显示详细的性能指标)' + ) + parser.add_argument( + '--test-batch-sizes', + action='store_true', + help='测试不同批次大小的性能(100, 500, 1000, 2000, 5000)' + ) + + def handle(self, *args, **options): + target_name = options['target'] + count = options['count'] + batch_size = options['batch_size'] + benchmark = options['benchmark'] + test_batch_sizes = options['test_batch_sizes'] + + # 如果是测试批次大小模式 + if test_batch_sizes: + self._test_batch_sizes(target_name, count) + return + + self.stdout.write(f'\n{"="*60}') + self.stdout.write(f' 开始生成测试数据') + self.stdout.write(f'{"="*60}\n') + self.stdout.write(f'目标: {target_name}') + self.stdout.write(f'每表记录数: {count:,}') + self.stdout.write(f'批次大小: {batch_size:,}') + if benchmark: + self.stdout.write('模式: 性能基准测试 ⚡') + self._print_db_info() + self.stdout.write('') + + # 记录总开始时间 + total_start_time = time.time() + + # 1. 获取或创建目标 + try: + target = Target.objects.get(name=target_name) + self.stdout.write(self.style.SUCCESS(f'✓ 找到目标: {target.name} (ID: {target.id})')) + except Target.DoesNotExist: + self.stdout.write(self.style.ERROR(f'✗ 目标不存在: {target_name}')) + return + + # 2. 创建新的测试扫描任务 + from apps.engine.models import ScanEngine + engine = ScanEngine.objects.first() + if not engine: + self.stdout.write(self.style.ERROR('✗ 没有可用的扫描引擎')) + return + + scan = Scan.objects.create( + target=target, + engine=engine, + status='completed', + results_dir=f'/tmp/test_{target_name}_{int(time.time())}' + ) + self.stdout.write(self.style.SUCCESS(f'✓ 创建新测试扫描任务 (ID: {scan.id})')) + + # 3. 生成子域名 + self.stdout.write(f'\n[1/5] 生成 {count:,} 个子域名...') + subdomains, stats1 = self._generate_subdomains(target, scan, count, batch_size, benchmark) + + # 4. 生成 IP 地址 + self.stdout.write(f'\n[2/5] 生成 {count:,} 个 IP 地址...') + ips, stats2 = self._generate_ips(target, scan, subdomains, count, batch_size, benchmark) + + # 5. 生成端口 + self.stdout.write(f'\n[3/5] 生成 {count:,} 个端口...') + stats3 = self._generate_ports(scan, ips, subdomains, count, batch_size, benchmark) + + # 6. 生成网站 + self.stdout.write(f'\n[4/5] 生成 {count:,} 个网站...') + websites, stats4 = self._generate_websites(target, scan, subdomains, count, batch_size, benchmark) + + # 7. 生成目录 + self.stdout.write(f'\n[5/5] 生成 {count:,} 个目录...') + stats5 = self._generate_directories(target, scan, websites, count, batch_size, benchmark) + + # 计算总耗时 + total_time = time.time() - total_start_time + + self.stdout.write(f'\n{"="*60}') + self.stdout.write(self.style.SUCCESS(' ✓ 测试数据生成完成!')) + self.stdout.write(f'{"="*60}') + self.stdout.write(f'总耗时: {total_time:.2f} 秒 ({total_time/60:.2f} 分钟)\n') + + if benchmark: + self._print_performance_summary([stats1, stats2, stats3, stats4, stats5]) + + def _generate_subdomains(self, target, scan, count, batch_size, benchmark=False): + """生成子域名""" + subdomains = [] + created_subdomains = [] + start_time = time.time() + batch_times = [] + + for i in range(count): + # 生成唯一的子域名 + subdomain_name = f'test-{i:07d}.{target.name}' + + subdomains.append(Subdomain( + target=target, + scan=scan, + name=subdomain_name, + cname=[], + is_cdn=random.choice([True, False]), + cdn_name=random.choice(['', 'cloudflare', 'akamai', 'fastly']) + )) + + # 批量插入 + if len(subdomains) >= batch_size: + batch_start = time.time() + with transaction.atomic(): + created = Subdomain.objects.bulk_create(subdomains, ignore_conflicts=True) + created_subdomains.extend(created) + batch_time = time.time() - batch_start + batch_times.append(batch_time) + + if benchmark: + speed = len(subdomains) / batch_time + self.stdout.write(f' 插入 {len(subdomains):,} 个 | 耗时: {batch_time:.2f}s | 速度: {speed:.0f} 条/秒') + else: + self.stdout.write(f' 插入 {len(subdomains):,} 个子域名... (进度: {i+1:,}/{count:,})') + subdomains = [] + + # 插入剩余的 + if subdomains: + with transaction.atomic(): + created = Subdomain.objects.bulk_create(subdomains, ignore_conflicts=True) + created_subdomains.extend(created) + self.stdout.write(f' 插入 {len(subdomains):,} 个子域名... (进度: {count:,}/{count:,})') + + total_time = time.time() - start_time + avg_batch_time = sum(batch_times) / len(batch_times) if batch_times else 0 + total_speed = len(created_subdomains) / total_time if total_time > 0 else 0 + + self.stdout.write(self.style.SUCCESS( + f' ✓ 完成!共创建 {len(created_subdomains):,} 个 | ' + f'总耗时: {total_time:.2f}s | ' + f'平均速度: {total_speed:.0f} 条/秒' + )) + + return created_subdomains, { + 'name': '子域名', + 'count': len(created_subdomains), + 'time': total_time, + 'speed': total_speed, + 'avg_batch_time': avg_batch_time + } + + def _generate_ips(self, target, scan, subdomains, count, batch_size, benchmark=False): + """生成 IP 地址""" + # 重新从数据库查询 subdomain,确保有 ID + subdomain_list = list(Subdomain.objects.filter(scan=scan).values_list('id', flat=True)) + + ips = [] + created_ips = [] + start_time = time.time() + batch_times = [] + + for i in range(count): + # 生成随机 IP + ip_addr = f'192.168.{random.randint(0, 255)}.{random.randint(1, 254)}' + subdomain_id = random.choice(subdomain_list) if subdomain_list else None + + if subdomain_id: + ips.append(IPAddress( + target=target, + scan=scan, + subdomain_id=subdomain_id, + ip=f'{ip_addr}-{i}', # 加后缀确保唯一 + protocol_version='IPv4', + is_private=True + )) + + # 批量插入 + if len(ips) >= batch_size: + batch_start = time.time() + with transaction.atomic(): + created = IPAddress.objects.bulk_create(ips, ignore_conflicts=True) + created_ips.extend(created) + batch_time = time.time() - batch_start + batch_times.append(batch_time) + + if benchmark: + speed = len(ips) / batch_time + self.stdout.write(f' 插入 {len(ips):,} 个 | 耗时: {batch_time:.2f}s | 速度: {speed:.0f} 条/秒') + else: + self.stdout.write(f' 插入 {len(ips):,} 个 IP 地址... (进度: {i+1:,}/{count:,})') + ips = [] + + # 插入剩余的 + if ips: + with transaction.atomic(): + created = IPAddress.objects.bulk_create(ips, ignore_conflicts=True) + created_ips.extend(created) + self.stdout.write(f' 插入 {len(ips):,} 个 IP 地址... (进度: {count:,}/{count:,})') + + total_time = time.time() - start_time + avg_batch_time = sum(batch_times) / len(batch_times) if batch_times else 0 + total_speed = len(created_ips) / total_time if total_time > 0 else 0 + + self.stdout.write(self.style.SUCCESS( + f' ✓ 完成!共创建 {len(created_ips):,} 个 | ' + f'总耗时: {total_time:.2f}s | ' + f'平均速度: {total_speed:.0f} 条/秒' + )) + + return created_ips, { + 'name': 'IP地址', + 'count': len(created_ips), + 'time': total_time, + 'speed': total_speed, + 'avg_batch_time': avg_batch_time + } + + def _generate_ports(self, scan, ips, subdomains, count, batch_size, benchmark=False): + """生成端口""" + # 重新查询 IP 和 subdomain 的 ID + ip_list = list(IPAddress.objects.filter(scan=scan).values_list('id', flat=True)) + subdomain_list = list(Subdomain.objects.filter(scan=scan).values_list('id', flat=True)) + + ports = [] + total_created = 0 + start_time = time.time() + batch_times = [] + + for i in range(count): + ip_id = random.choice(ip_list) if ip_list else None + subdomain_id = random.choice(subdomain_list) if subdomain_list else None + + if ip_id: + ports.append(Port( + ip_address_id=ip_id, + subdomain_id=subdomain_id, + number=random.randint(1, 65535), + service_name=random.choice(['http', 'https', 'ssh', 'ftp', 'mysql']), + is_uncommon=random.choice([True, False]) + )) + + # 批量插入 + if len(ports) >= batch_size: + batch_start = time.time() + with transaction.atomic(): + Port.objects.bulk_create(ports, ignore_conflicts=True) + total_created += len(ports) + batch_time = time.time() - batch_start + batch_times.append(batch_time) + + if benchmark: + speed = len(ports) / batch_time + self.stdout.write(f' 插入 {len(ports):,} 个 | 耗时: {batch_time:.2f}s | 速度: {speed:.0f} 条/秒') + else: + self.stdout.write(f' 插入 {len(ports):,} 个端口... (进度: {i+1:,}/{count:,})') + ports = [] + + # 插入剩余的 + if ports: + with transaction.atomic(): + Port.objects.bulk_create(ports, ignore_conflicts=True) + total_created += len(ports) + self.stdout.write(f' 插入 {len(ports):,} 个端口... (进度: {count:,}/{count:,})') + + total_time = time.time() - start_time + avg_batch_time = sum(batch_times) / len(batch_times) if batch_times else 0 + total_speed = total_created / total_time if total_time > 0 else 0 + + self.stdout.write(self.style.SUCCESS( + f' ✓ 完成!共创建 {total_created:,} 个 | ' + f'总耗时: {total_time:.2f}s | ' + f'平均速度: {total_speed:.0f} 条/秒' + )) + + return { + 'name': '端口', + 'count': total_created, + 'time': total_time, + 'speed': total_speed, + 'avg_batch_time': avg_batch_time + } + + def _generate_websites(self, target, scan, subdomains, count, batch_size, benchmark=False): + """生成网站""" + # 重新查询 subdomain 信息 + subdomain_data = list(Subdomain.objects.filter(scan=scan).values('id', 'name')) + + websites = [] + created_websites = [] + start_time = time.time() + batch_times = [] + + for i in range(count): + subdomain = random.choice(subdomain_data) if subdomain_data else None + + if subdomain: + protocol = random.choice(['http', 'https']) + url = f'{protocol}://{subdomain["name"]}' + + websites.append(WebSite( + target=target, + scan=scan, + subdomain_id=subdomain['id'], + url=f'{url}?id={i}', # 加参数确保唯一 + title=f'Test Website {i}', + status_code=random.choice([200, 301, 302, 404, 500]), + content_length=random.randint(1000, 100000), + webserver=random.choice(['nginx', 'apache', 'IIS']), + content_type='text/html', + tech=['Python', 'Django'] if i % 2 == 0 else ['Node.js', 'React'], + vhost=random.choice([True, False, None]) + )) + + # 批量插入 + if len(websites) >= batch_size: + batch_start = time.time() + with transaction.atomic(): + created = WebSite.objects.bulk_create(websites, ignore_conflicts=True) + created_websites.extend(created) + batch_time = time.time() - batch_start + batch_times.append(batch_time) + + if benchmark: + speed = len(websites) / batch_time + self.stdout.write(f' 插入 {len(websites):,} 个 | 耗时: {batch_time:.2f}s | 速度: {speed:.0f} 条/秒') + else: + self.stdout.write(f' 插入 {len(websites):,} 个网站... (进度: {i+1:,}/{count:,})') + websites = [] + + # 插入剩余的 + if websites: + with transaction.atomic(): + created = WebSite.objects.bulk_create(websites, ignore_conflicts=True) + created_websites.extend(created) + self.stdout.write(f' 插入 {len(websites):,} 个网站... (进度: {count:,}/{count:,})') + + total_time = time.time() - start_time + avg_batch_time = sum(batch_times) / len(batch_times) if batch_times else 0 + total_speed = len(created_websites) / total_time if total_time > 0 else 0 + + self.stdout.write(self.style.SUCCESS( + f' ✓ 完成!共创建 {len(created_websites):,} 个 | ' + f'总耗时: {total_time:.2f}s | ' + f'平均速度: {total_speed:.0f} 条/秒' + )) + + return created_websites, { + 'name': '网站', + 'count': len(created_websites), + 'time': total_time, + 'speed': total_speed, + 'avg_batch_time': avg_batch_time + } + + def _generate_directories(self, target, scan, websites, count, batch_size, benchmark=False): + """生成目录""" + # 重新查询 website 信息 + website_data = list(WebSite.objects.filter(scan=scan).values('id', 'url')) + + directories = [] + total_created = 0 + start_time = time.time() + batch_times = [] + + for i in range(count): + website = random.choice(website_data) if website_data else None + + if website: + path = ''.join(random.choices(string.ascii_lowercase, k=10)) + + directories.append(Directory( + target=target, + scan=scan, + website_id=website['id'], + url=f'{website["url"]}/dir/{path}/{i}', # 加后缀确保唯一 + status=random.choice([200, 301, 403, 404]), + length=random.randint(1000, 50000), + words=random.randint(100, 5000), + lines=random.randint(50, 1000), + content_type='text/html' + )) + + # 批量插入 + if len(directories) >= batch_size: + batch_start = time.time() + with transaction.atomic(): + Directory.objects.bulk_create(directories, ignore_conflicts=True) + total_created += len(directories) + batch_time = time.time() - batch_start + batch_times.append(batch_time) + + if benchmark: + speed = len(directories) / batch_time + self.stdout.write(f' 插入 {len(directories):,} 个 | 耗时: {batch_time:.2f}s | 速度: {speed:.0f} 条/秒') + else: + self.stdout.write(f' 插入 {len(directories):,} 个目录... (进度: {i+1:,}/{count:,})') + directories = [] + + # 插入剩余的 + if directories: + with transaction.atomic(): + Directory.objects.bulk_create(directories, ignore_conflicts=True) + total_created += len(directories) + self.stdout.write(f' 插入 {len(directories):,} 个目录... (进度: {count:,}/{count:,})') + + total_time = time.time() - start_time + avg_batch_time = sum(batch_times) / len(batch_times) if batch_times else 0 + total_speed = total_created / total_time if total_time > 0 else 0 + + self.stdout.write(self.style.SUCCESS( + f' ✓ 完成!共创建 {total_created:,} 个 | ' + f'总耗时: {total_time:.2f}s | ' + f'平均速度: {total_speed:.0f} 条/秒' + )) + + return { + 'name': '目录', + 'count': total_created, + 'time': total_time, + 'speed': total_speed, + 'avg_batch_time': avg_batch_time + } + + def _print_db_info(self): + """打印数据库连接信息""" + db_settings = connection.settings_dict + self.stdout.write(f'\n数据库信息:') + self.stdout.write(f' 主机: {db_settings["HOST"]}') + self.stdout.write(f' 端口: {db_settings["PORT"]}') + self.stdout.write(f' 数据库: {db_settings["NAME"]}') + self.stdout.write(f' 引擎: {db_settings["ENGINE"].split(".")[-1]}') + + def _print_performance_summary(self, stats_list): + """打印性能总结""" + self.stdout.write(f'\n{"="*60}') + self.stdout.write(' 性能测试报告') + self.stdout.write(f'{"="*60}\n') + + total_records = sum(s['count'] for s in stats_list) + total_time = sum(s['time'] for s in stats_list) + overall_speed = total_records / total_time if total_time > 0 else 0 + + self.stdout.write(f'{"表名":<12} {"记录数":<12} {"耗时(秒)":<12} {"速度(条/秒)":<15} {"平均批次时间(秒)"}') + self.stdout.write('-' * 65) + + for stats in stats_list: + self.stdout.write( + f'{stats["name"]:<12} ' + f'{stats["count"]:<12,} ' + f'{stats["time"]:<12.2f} ' + f'{stats["speed"]:<15.0f} ' + f'{stats.get("avg_batch_time", 0):<.3f}' + ) + + self.stdout.write('-' * 65) + self.stdout.write( + f'{"总计":<12} ' + f'{total_records:<12,} ' + f'{total_time:<12.2f} ' + f'{overall_speed:<15.0f}' + ) + self.stdout.write('') + + def _test_batch_sizes(self, target_name, count): + """测试不同批次大小的性能""" + batch_sizes = [100, 500, 1000, 2000, 5000] + test_count = min(count, 10000) # 限制测试数据量 + + self.stdout.write(f'\n{"="*60}') + self.stdout.write(f' 批次大小性能测试') + self.stdout.write(f'{"="*60}\n') + self.stdout.write(f'测试数据量: {test_count:,} 条') + self.stdout.write(f'测试批次: {batch_sizes}\n') + + results = [] + + for batch_size in batch_sizes: + self.stdout.write(f'\n测试批次大小: {batch_size}') + self.stdout.write('-' * 40) + + # 这里只测试子域名的插入性能 + try: + target = Target.objects.get(name=target_name) + except Target.DoesNotExist: + self.stdout.write(self.style.ERROR(f'目标不存在: {target_name}')) + return + + scan = Scan.objects.filter(target=target).first() + if not scan: + from apps.engine.models import ScanEngine + engine = ScanEngine.objects.first() + scan = Scan.objects.create( + target=target, + engine=engine, + status='completed', + results_dir=f'/tmp/test_{target_name}' + ) + + _, stats = self._generate_subdomains(target, scan, test_count, batch_size, benchmark=True) + results.append((batch_size, stats)) + + # 清理测试数据 + Subdomain.objects.filter(scan=scan, name__startswith=f'test-').delete() + + # 打印对比结果 + self.stdout.write(f'\n{"="*60}') + self.stdout.write(' 批次大小对比结果') + self.stdout.write(f'{"="*60}\n') + self.stdout.write(f'{"批次大小":<12} {"总耗时(秒)":<15} {"速度(条/秒)":<15} {"平均批次时间(秒)"}') + self.stdout.write('-' * 60) + + for batch_size, stats in results: + self.stdout.write( + f'{batch_size:<12} ' + f'{stats["time"]:<15.2f} ' + f'{stats["speed"]:<15.0f} ' + f'{stats["avg_batch_time"]:<.3f}' + ) + + # 找出最快的批次大小 + fastest = min(results, key=lambda x: x[1]['time']) + self.stdout.write(f'\n推荐批次大小: {fastest[0]} (最快: {fastest[1]["time"]:.2f}秒)') + self.stdout.write('') diff --git a/backend/apps/scan/migrations/__init__.py b/backend/apps/scan/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/scan/models.py b/backend/apps/scan/models.py new file mode 100644 index 00000000..ce193f61 --- /dev/null +++ b/backend/apps/scan/models.py @@ -0,0 +1,180 @@ +from django.db import models +from django.contrib.postgres.fields import ArrayField + +from ..common.definitions import ScanStatus + + + + +class SoftDeleteManager(models.Manager): + """软删除管理器:默认只返回未删除的记录""" + + def get_queryset(self): + return super().get_queryset().filter(deleted_at__isnull=True) + + +class Scan(models.Model): + """扫描任务模型""" + + id = models.AutoField(primary_key=True) + + target = models.ForeignKey('targets.Target', on_delete=models.CASCADE, related_name='scans', help_text='扫描目标') + + engine = models.ForeignKey( + 'engine.ScanEngine', + on_delete=models.CASCADE, + related_name='scans', + help_text='使用的扫描引擎' + ) + + created_at = models.DateTimeField(auto_now_add=True, help_text='任务创建时间') + stopped_at = models.DateTimeField(null=True, blank=True, help_text='扫描结束时间') + + status = models.CharField( + max_length=20, + choices=ScanStatus.choices, + default=ScanStatus.INITIATED, + db_index=True, + help_text='任务状态' + ) + + results_dir = models.CharField(max_length=100, blank=True, default='', help_text='结果存储目录') + + container_ids = ArrayField( + models.CharField(max_length=100), + blank=True, + default=list, + help_text='容器 ID 列表(Docker Container ID)' + ) + + worker = models.ForeignKey( + 'engine.WorkerNode', + on_delete=models.SET_NULL, + related_name='scans', + null=True, + blank=True, + help_text='执行扫描的 Worker 节点' + ) + + error_message = models.CharField(max_length=2000, blank=True, default='', help_text='错误信息') + + # ==================== 软删除字段 ==================== + deleted_at = models.DateTimeField(null=True, blank=True, db_index=True, help_text='删除时间(NULL表示未删除)') + + # ==================== 管理器 ==================== + objects = SoftDeleteManager() # 默认管理器:只返回未删除的记录 + all_objects = models.Manager() # 全量管理器:包括已删除的记录(用于硬删除) + + # ==================== 进度跟踪字段 ==================== + progress = models.IntegerField(default=0, help_text='扫描进度 0-100') + current_stage = models.CharField(max_length=50, blank=True, default='', help_text='当前扫描阶段') + stage_progress = models.JSONField(default=dict, help_text='各阶段进度详情') + + # ==================== 缓存统计字段 ==================== + cached_subdomains_count = models.IntegerField(default=0, help_text='缓存的子域名数量') + cached_websites_count = models.IntegerField(default=0, help_text='缓存的网站数量') + cached_endpoints_count = models.IntegerField(default=0, help_text='缓存的端点数量') + cached_ips_count = models.IntegerField(default=0, help_text='缓存的IP地址数量') + cached_directories_count = models.IntegerField(default=0, help_text='缓存的目录数量') + cached_vulns_total = models.IntegerField(default=0, help_text='缓存的漏洞总数') + cached_vulns_critical = models.IntegerField(default=0, help_text='缓存的严重漏洞数量') + cached_vulns_high = models.IntegerField(default=0, help_text='缓存的高危漏洞数量') + cached_vulns_medium = models.IntegerField(default=0, help_text='缓存的中危漏洞数量') + cached_vulns_low = models.IntegerField(default=0, help_text='缓存的低危漏洞数量') + stats_updated_at = models.DateTimeField(null=True, blank=True, help_text='统计数据最后更新时间') + + class Meta: + db_table = 'scan' + verbose_name = '扫描任务' + verbose_name_plural = '扫描任务' + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['-created_at']), # 优化按创建时间降序排序(list 查询的默认排序) + models.Index(fields=['target']), # 优化按目标查询扫描任务 + models.Index(fields=['deleted_at', '-created_at']), # 软删除 + 时间索引 + ] + + def __str__(self): + return f"Scan #{self.id} - {self.target.name}" + + +class ScheduledScan(models.Model): + """ + 定时扫描任务模型 + + 调度机制: + - APScheduler 每分钟检查 next_run_time + - 到期任务通过 task_distributor 分发到 Worker 执行 + - 支持 cron 表达式进行灵活调度 + + 扫描模式(二选一): + - 组织扫描:设置 organization,执行时动态获取组织下所有目标 + - 目标扫描:设置 target,扫描单个目标 + - organization 优先级高于 target + """ + + id = models.AutoField(primary_key=True) + + # 基本信息 + name = models.CharField(max_length=200, help_text='任务名称') + + # 关联的扫描引擎 + engine = models.ForeignKey( + 'engine.ScanEngine', + on_delete=models.CASCADE, + related_name='scheduled_scans', + help_text='使用的扫描引擎' + ) + + # 关联的组织(组织扫描模式:执行时动态获取组织下所有目标) + organization = models.ForeignKey( + 'targets.Organization', + on_delete=models.CASCADE, + related_name='scheduled_scans', + null=True, + blank=True, + help_text='扫描组织(设置后执行时动态获取组织下所有目标)' + ) + + # 关联的目标(目标扫描模式:扫描单个目标) + target = models.ForeignKey( + 'targets.Target', + on_delete=models.CASCADE, + related_name='scheduled_scans', + null=True, + blank=True, + help_text='扫描单个目标(与 organization 二选一)' + ) + + # 调度配置 - 直接使用 Cron 表达式 + cron_expression = models.CharField( + max_length=100, + default='0 2 * * *', + help_text='Cron 表达式,格式:分 时 日 月 周' + ) + + # 状态 + is_enabled = models.BooleanField(default=True, db_index=True, help_text='是否启用') + + # 执行统计 + run_count = models.IntegerField(default=0, help_text='已执行次数') + last_run_time = models.DateTimeField(null=True, blank=True, help_text='上次执行时间') + next_run_time = models.DateTimeField(null=True, blank=True, help_text='下次执行时间') + + # 时间戳 + created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间') + updated_at = models.DateTimeField(auto_now=True, help_text='更新时间') + + class Meta: + db_table = 'scheduled_scan' + verbose_name = '定时扫描任务' + verbose_name_plural = '定时扫描任务' + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['-created_at']), + models.Index(fields=['is_enabled', '-created_at']), + models.Index(fields=['name']), # 优化 name 搜索 + ] + + def __str__(self): + return f"ScheduledScan #{self.id} - {self.name}" \ No newline at end of file diff --git a/backend/apps/scan/notifications/__init__.py b/backend/apps/scan/notifications/__init__.py new file mode 100644 index 00000000..ca0600e9 --- /dev/null +++ b/backend/apps/scan/notifications/__init__.py @@ -0,0 +1,12 @@ +"""极简通知系统""" + +from .types import NotificationLevel, NotificationCategory +from .models import Notification +from .services import create_notification + +__all__ = [ + 'NotificationLevel', + 'NotificationCategory', + 'Notification', + 'create_notification' +] diff --git a/backend/apps/scan/notifications/consumers.py b/backend/apps/scan/notifications/consumers.py new file mode 100644 index 00000000..e399e2e9 --- /dev/null +++ b/backend/apps/scan/notifications/consumers.py @@ -0,0 +1,144 @@ +""" +WebSocket Consumer - 通知实时推送 +""" + +import json +import logging +import asyncio +from channels.generic.websocket import AsyncWebsocketConsumer + +logger = logging.getLogger(__name__) + + +class NotificationConsumer(AsyncWebsocketConsumer): + """ + 通知 WebSocket Consumer + + 处理客户端连接、断开和通知推送 + 使用 Redis Channel Layer 订阅通知 + 支持心跳保活机制 + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.heartbeat_task = None # 心跳任务 + + async def connect(self): + """ + 客户端连接时调用 + 加入通知广播组 + """ + # 通知组名(所有客户端共享) + self.group_name = 'notifications' + + # 加入组 + await self.channel_layer.group_add( + self.group_name, + self.channel_name + ) + + # 接受 WebSocket 连接 + await self.accept() + + # 发送连接成功消息 + await self.send(text_data=json.dumps({ + 'type': 'connected', + 'message': '连接成功' + }, ensure_ascii=False)) + + # 启动服务端心跳(可选:防止中间件超时) + # self.heartbeat_task = asyncio.create_task(self._heartbeat_loop()) + + logger.debug(f"WebSocket 连接已建立 - Channel: {self.channel_name}") + + async def disconnect(self, close_code): + """ + 客户端断开时调用 + 离开通知广播组 + """ + # 取消心跳任务 + if self.heartbeat_task and not self.heartbeat_task.done(): + self.heartbeat_task.cancel() + try: + await self.heartbeat_task + except asyncio.CancelledError: + pass + + # 离开组 + await self.channel_layer.group_discard( + self.group_name, + self.channel_name + ) + + logger.debug(f"WebSocket 连接已断开 - Channel: {self.channel_name}, Code: {close_code}") + + async def receive(self, text_data): + """ + 接收客户端消息 + 当前实现不需要处理客户端消息,保留以备扩展 + """ + try: + data = json.loads(text_data) + message_type = data.get('type') + + # 心跳响应 + if message_type == 'ping': + await self.send(text_data=json.dumps({ + 'type': 'pong', + 'message': '心跳响应' + }, ensure_ascii=False)) + logger.debug(f"心跳响应 - Channel: {self.channel_name}") + + except json.JSONDecodeError as e: + logger.warning(f"解析客户端消息失败 - Channel: {self.channel_name}: {e}") + except Exception as e: + logger.error(f"处理客户端消息异常 - Channel: {self.channel_name}: {e}", exc_info=True) + + async def notification_message(self, event): + """ + 接收来自 Channel Layer 的通知消息 + 转发给 WebSocket 客户端 + + Args: + event: 消息事件,包含通知数据 + """ + try: + # 构造发送给客户端的消息 + message = { + 'type': 'notification', + 'id': event['id'], + 'category': event.get('category', 'system'), + 'title': event['title'], + 'message': event['message'], + 'level': event['level'], + 'created_at': event['created_at'] + } + + # 发送给客户端 + await self.send(text_data=json.dumps(message, ensure_ascii=False)) + + logger.debug(f"通知已推送 - Channel: {self.channel_name}, ID: {event['id']}") + + except Exception as e: + logger.error(f"推送通知失败 - Channel: {self.channel_name}: {e}", exc_info=True) + + async def _heartbeat_loop(self): + """ + 服务端主动心跳循环(可选) + 定期向客户端发送 ping 消息,保持连接活跃 + 防止中间件或防火墙断开长时间无活动的连接 + + 注意:通常客户端心跳就足够了,这是额外的保险措施 + """ + try: + while True: + await asyncio.sleep(45) # 每 45 秒发送一次心跳 + await self.send(text_data=json.dumps({ + 'type': 'ping', + 'message': '服务端心跳' + }, ensure_ascii=False)) + logger.debug(f"服务端心跳已发送 - Channel: {self.channel_name}") + except asyncio.CancelledError: + logger.debug(f"心跳循环已取消 - Channel: {self.channel_name}") + except Exception as e: + logger.error(f"心跳循环异常 - Channel: {self.channel_name}: {e}", exc_info=True) diff --git a/backend/apps/scan/notifications/models.py b/backend/apps/scan/notifications/models.py new file mode 100644 index 00000000..723c2bcb --- /dev/null +++ b/backend/apps/scan/notifications/models.py @@ -0,0 +1,143 @@ +"""通知系统数据模型""" + +from django.db import models + +from .types import NotificationLevel, NotificationCategory + + +class NotificationSettings(models.Model): + """ + 通知设置(单例模型) + 存储 Discord webhook 配置和各分类的通知开关 + """ + + # Discord 配置 + discord_enabled = models.BooleanField(default=False, help_text='是否启用 Discord 通知') + discord_webhook_url = models.URLField(blank=True, default='', help_text='Discord Webhook URL') + + # 分类开关(使用 JSONField 存储) + categories = models.JSONField( + default=dict, + help_text='各分类通知开关,如 {"scan": true, "vulnerability": true, "asset": true, "system": false}' + ) + + # 时间信息 + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'notification_settings' + verbose_name = '通知设置' + verbose_name_plural = '通知设置' + + def save(self, *args, **kwargs): + # 单例模式:强制只有一条记录 + self.pk = 1 + super().save(*args, **kwargs) + + @classmethod + def get_instance(cls) -> 'NotificationSettings': + """获取或创建单例实例""" + obj, _ = cls.objects.get_or_create( + pk=1, + defaults={ + 'discord_enabled': False, + 'discord_webhook_url': '', + 'categories': { + 'scan': True, + 'vulnerability': True, + 'asset': True, + 'system': False, + } + } + ) + return obj + + def is_category_enabled(self, category: str) -> bool: + """检查指定分类是否启用通知""" + return self.categories.get(category, False) + + +class Notification(models.Model): + """通知模型""" + + id = models.AutoField(primary_key=True) + + # 通知分类 + category = models.CharField( + max_length=20, + choices=NotificationCategory.choices, + default=NotificationCategory.SYSTEM, + db_index=True, + help_text='通知分类' + ) + + # 通知级别 + level = models.CharField( + max_length=20, + choices=NotificationLevel.choices, + default=NotificationLevel.LOW, + db_index=True, + help_text='通知级别' + ) + + title = models.CharField(max_length=200, help_text='通知标题') + message = models.CharField(max_length=2000, help_text='通知内容') + + # 时间信息 + created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间') + + is_read = models.BooleanField(default=False, help_text='是否已读') + read_at = models.DateTimeField(null=True, blank=True, help_text='阅读时间') + + class Meta: + db_table = 'notification' + verbose_name = '通知' + verbose_name_plural = '通知' + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['-created_at']), + models.Index(fields=['category', '-created_at']), + models.Index(fields=['level', '-created_at']), + models.Index(fields=['is_read', '-created_at']), + ] + + def __str__(self): + return f"{self.get_level_display()} - {self.title}" + + @classmethod + def cleanup_old_notifications(cls): + """ + 清理超过15天的旧通知(硬编码) + + Returns: + int: 删除的通知数量 + """ + from datetime import datetime, timedelta + + # 硬编码:只保留最近15天的通知 + cutoff_date = datetime.now() - timedelta(days=15) + delete_result = cls.objects.filter(created_at__lt=cutoff_date).delete() + + return delete_result[0] if delete_result[0] else 0 + + def save(self, *args, **kwargs): + """ + 重写save方法,在创建新通知时自动清理旧通知 + """ + is_new = self.pk is None + super().save(*args, **kwargs) + + # 只在创建新通知时执行清理(自动清理超过15天的通知) + if is_new: + try: + deleted_count = self.__class__.cleanup_old_notifications() + if deleted_count > 0: + import logging + logger = logging.getLogger(__name__) + logger.info(f"自动清理了 {deleted_count} 条超过15天的旧通知") + except Exception as e: + # 清理失败不应影响通知创建 + import logging + logger = logging.getLogger(__name__) + logger.warning(f"通知自动清理失败: {e}") diff --git a/backend/apps/scan/notifications/receivers.py b/backend/apps/scan/notifications/receivers.py new file mode 100644 index 00000000..9d577f99 --- /dev/null +++ b/backend/apps/scan/notifications/receivers.py @@ -0,0 +1,82 @@ +"""信号接收器 - 处理通知相关的信号 + +监听各种信号并发送相应的通知。 +""" + +import logging +from django.dispatch import receiver + +from apps.common.signals import vulnerabilities_saved, worker_delete_failed +from apps.scan.notifications import create_notification, NotificationLevel, NotificationCategory + +logger = logging.getLogger(__name__) + + +@receiver(vulnerabilities_saved) +def on_vulnerabilities_saved(sender, items, scan_id, target_id, **kwargs): + """漏洞保存完成后的通知处理 + + 为每个漏洞发送详细通知 + """ + if not items: + return + + # 获取目标名称 + target_name = "未知目标" + if target_id: + try: + from apps.targets.models import Target + target = Target.objects.filter(id=target_id).first() + if target: + target_name = target.name + except Exception: + pass + + # 严重程度映射 + severity_level_map = { + 'critical': NotificationLevel.CRITICAL, + 'high': NotificationLevel.HIGH, + 'medium': NotificationLevel.MEDIUM, + 'low': NotificationLevel.LOW, + } + + for vuln in items: + try: + severity = vuln.severity or 'unknown' + + # 构建漏洞详情消息 + message = f"漏洞:{vuln.vuln_type}\n" + message += f"程度:{severity}\n" + message += f"目标:{target_name}\n" + message += f"URL:{vuln.url}" + if vuln.source: + message += f"\n来源:{vuln.source}" + if vuln.description: + message += f"\n描述:{vuln.description}" + + # 根据漏洞严重程度设置通知级别 + level = severity_level_map.get(severity.lower(), NotificationLevel.MEDIUM) + + create_notification( + title=f"{vuln.vuln_type}", + message=message, + level=level, + category=NotificationCategory.VULNERABILITY + ) + + except Exception as e: + logger.error("发送漏洞通知失败 - url=%s: %s", vuln.url, e, exc_info=True) + + logger.info("漏洞通知已发送 - scan_id=%s, 数量=%d", scan_id, len(items)) + + +@receiver(worker_delete_failed) +def on_worker_delete_failed(sender, worker_name, message, **kwargs): + """Worker 删除失败时的通知处理""" + create_notification( + title="Worker 删除警告", + message=f"节点 {worker_name} 已从数据库删除,但远程卸载失败: {message}", + level=NotificationLevel.MEDIUM, + category=NotificationCategory.SYSTEM + ) + logger.warning("Worker 删除失败通知已发送 - worker=%s, message=%s", worker_name, message) diff --git a/backend/apps/scan/notifications/repositories.py b/backend/apps/scan/notifications/repositories.py new file mode 100644 index 00000000..cd18dfb5 --- /dev/null +++ b/backend/apps/scan/notifications/repositories.py @@ -0,0 +1,78 @@ +import logging +from typing import TypedDict +from django.utils import timezone + +from apps.common.decorators import auto_ensure_db_connection +from .models import Notification, NotificationSettings + + +logger = logging.getLogger(__name__) + + +class NotificationSettingsData(TypedDict): + """通知设置数据结构""" + discord_enabled: bool + discord_webhook_url: str + categories: dict[str, bool] + + +@auto_ensure_db_connection +class NotificationSettingsRepository: + """通知设置仓储层""" + + def get_settings(self) -> NotificationSettings: + """获取通知设置单例""" + return NotificationSettings.get_instance() + + def update_settings( + self, + discord_enabled: bool, + discord_webhook_url: str, + categories: dict[str, bool] + ) -> NotificationSettings: + """更新通知设置""" + settings = NotificationSettings.get_instance() + settings.discord_enabled = discord_enabled + settings.discord_webhook_url = discord_webhook_url + settings.categories = categories + settings.save() + return settings + + def is_category_enabled(self, category: str) -> bool: + """检查指定分类是否启用""" + settings = self.get_settings() + return settings.is_category_enabled(category) + + +@auto_ensure_db_connection +class DjangoNotificationRepository: + def get_filtered(self, level: str | None = None, unread: bool | None = None): + queryset = Notification.objects.all() + + if level: + queryset = queryset.filter(level=level) + + if unread is True: + queryset = queryset.filter(is_read=False) + elif unread is False: + queryset = queryset.filter(is_read=True) + + return queryset.order_by("-created_at") + + def get_unread_count(self) -> int: + return Notification.objects.filter(is_read=False).count() + + def mark_all_as_read(self) -> int: + updated = Notification.objects.filter(is_read=False).update( + is_read=True, + read_at=timezone.now(), + ) + return updated + + def create(self, title: str, message: str, level: str, category: str = 'system') -> Notification: + return Notification.objects.create( + category=category, + level=level, + title=title, + message=message, + ) diff --git a/backend/apps/scan/notifications/routing.py b/backend/apps/scan/notifications/routing.py new file mode 100644 index 00000000..c0187c78 --- /dev/null +++ b/backend/apps/scan/notifications/routing.py @@ -0,0 +1,12 @@ +""" +WebSocket 路由配置 +""" + +from django.urls import path + +# 延迟导入,避免循环依赖 +from .consumers import NotificationConsumer + +websocket_urlpatterns = [ + path('ws/notifications/', NotificationConsumer.as_asgi()), +] diff --git a/backend/apps/scan/notifications/serializers.py b/backend/apps/scan/notifications/serializers.py new file mode 100644 index 00000000..3bd1054f --- /dev/null +++ b/backend/apps/scan/notifications/serializers.py @@ -0,0 +1,20 @@ +"""通知序列化器""" + +from rest_framework import serializers + +from .models import Notification + + +class NotificationSerializer(serializers.ModelSerializer): + class Meta: + model = Notification + fields = [ + 'id', + 'category', + 'title', + 'message', + 'level', + 'is_read', + 'created_at', + 'read_at', + ] diff --git a/backend/apps/scan/notifications/services.py b/backend/apps/scan/notifications/services.py new file mode 100644 index 00000000..686fb1bf --- /dev/null +++ b/backend/apps/scan/notifications/services.py @@ -0,0 +1,363 @@ +"""通知服务 - 支持数据库存储和 WebSocket 实时推送""" + +import logging +import time +import requests +from .models import Notification, NotificationSettings +from .types import NotificationLevel, NotificationCategory +from .repositories import DjangoNotificationRepository, NotificationSettingsRepository + +logger = logging.getLogger(__name__) + + +# ============================================================ +# 外部推送渠道抽象 +# ============================================================ + +# Discord Embed 颜色映射(使用字符串 key,因为 model 字段存储的是字符串) +DISCORD_COLORS = { + 'low': 0x3498db, # 蓝色 + 'medium': 0xf39c12, # 橙色 + 'high': 0xe74c3c, # 红色 + 'critical': 0x9b59b6, # 紫色 +} + +# 分类 emoji(使用字符串 key) +CATEGORY_EMOJI = { + 'scan': '🔍', + 'vulnerability': '⚠️', + 'asset': '🌐', + 'system': '⚙️', +} + + +def push_to_external_channels(notification: Notification) -> None: + """ + 推送通知到外部渠道(Discord、Slack 等) + + 根据用户设置决定推送到哪些渠道。 + 目前支持:Discord + 未来可扩展:Slack、Telegram、Email 等 + + Args: + notification: 通知对象 + """ + settings = NotificationSettings.get_instance() + + # 检查分类是否启用 + if not settings.is_category_enabled(notification.category): + logger.debug(f"分类 {notification.category} 未启用外部推送") + return + + # Discord 渠道 + if settings.discord_enabled and settings.discord_webhook_url: + try: + _send_discord(notification, settings.discord_webhook_url) + except Exception as e: + logger.warning(f"Discord 推送失败: {e}") + + # 未来扩展:Slack + # if settings.slack_enabled and settings.slack_webhook_url: + # _send_slack(notification, settings.slack_webhook_url) + + # 未来扩展:Telegram + # if settings.telegram_enabled and settings.telegram_bot_token: + # _send_telegram(notification, settings.telegram_chat_id) + + +def _send_discord(notification: Notification, webhook_url: str) -> bool: + """发送到 Discord Webhook""" + try: + color = DISCORD_COLORS.get(notification.level, 0x95a5a6) + emoji = CATEGORY_EMOJI.get(notification.category, '📢') + + embed = { + 'title': f"{emoji} {notification.title}", + 'description': notification.message, + 'color': color, + 'footer': { + 'text': f"级别: {notification.get_level_display()} | 分类: {notification.get_category_display()}" + }, + 'timestamp': notification.created_at.isoformat(), + } + + response = requests.post( + webhook_url, + json={'embeds': [embed]}, + timeout=10 + ) + + if response.status_code in (200, 204): + logger.info(f"Discord 通知发送成功 - {notification.title}") + return True + else: + logger.warning(f"Discord 发送失败 - 状态码: {response.status_code}") + return False + + except requests.RequestException as e: + logger.error(f"Discord 网络错误: {e}") + return False + + +# ============================================================ +# 设置服务 +# ============================================================ + +class NotificationSettingsService: + """通知设置服务""" + + def __init__(self, repository: NotificationSettingsRepository | None = None): + self.repo = repository or NotificationSettingsRepository() + + def get_settings(self) -> dict: + """获取通知设置(前端格式)""" + settings = self.repo.get_settings() + return { + 'discord': { + 'enabled': settings.discord_enabled, + 'webhookUrl': settings.discord_webhook_url, + }, + 'categories': settings.categories, + } + + def update_settings(self, data: dict) -> dict: + """更新通知设置 + + 注意:DRF CamelCaseJSONParser 会将前端的 webhookUrl 转换为 webhook_url + """ + discord_data = data.get('discord', {}) + categories = data.get('categories', {}) + + # CamelCaseJSONParser 转换后的字段名是 webhook_url + webhook_url = discord_data.get('webhook_url', '') + + settings = self.repo.update_settings( + discord_enabled=discord_data.get('enabled', False), + discord_webhook_url=webhook_url, + categories=categories, + ) + + return { + 'discord': { + 'enabled': settings.discord_enabled, + 'webhookUrl': settings.discord_webhook_url, + }, + 'categories': settings.categories, + } + + +class NotificationService: + """通知业务服务,封装常用查询与更新操作""" + + def __init__(self, repository: DjangoNotificationRepository | None = None): + self.repo = repository or DjangoNotificationRepository() + + def get_notifications(self, level: str | None = None, unread: bool | None = None): + return self.repo.get_filtered(level=level, unread=unread) + + def get_unread_count(self) -> int: + return self.repo.get_unread_count() + + def mark_all_as_read(self) -> int: + return self.repo.mark_all_as_read() + + +def create_notification( + title: str, + message: str, + level: NotificationLevel = NotificationLevel.LOW, + category: NotificationCategory = NotificationCategory.SYSTEM +) -> Notification: + """ + 创建通知记录并实时推送 + + 增强的重试机制: + - 最多重试 3 次 + - 每次重试前强制关闭并重建数据库连接 + - 重试间隔:1秒 → 2秒 → 3秒 + - 针对连接错误进行特殊处理 + + Args: + title: 通知标题 + message: 通知消息 + level: 通知级别 + category: 通知分类 + + Returns: + Notification: 创建的通知对象 + + Raises: + Exception: 重试3次后仍然失败 + """ + from django.db import connection + from psycopg2 import OperationalError, InterfaceError + + repo = DjangoNotificationRepository() + + max_retries = 3 + last_exception = None + + for attempt in range(1, max_retries + 1): + try: + # 强制关闭旧连接并重建(每次尝试都重建) + if attempt > 1: + logger.debug(f"重试创建通知 ({attempt}/{max_retries}) - {title}") + + connection.close() + connection.ensure_connection() + + # 测试连接是否真的可用 + connection.cursor().execute("SELECT 1") + + # 1. 写入数据库(通过仓储层统一访问 ORM) + notification = repo.create( + title=title, + message=message, + level=level, + category=category, + ) + + # 2. WebSocket 实时推送(推送失败不影响通知创建) + try: + _push_to_websocket(notification) + except Exception as push_error: + logger.warning(f"WebSocket 推送失败,但通知已创建 - {title}: {push_error}") + + # 3. 外部渠道推送(Discord/Slack 等,推送失败不影响通知创建) + try: + push_to_external_channels(notification) + except Exception as external_error: + logger.warning(f"外部渠道推送失败,但通知已创建 - {title}: {external_error}") + + if attempt > 1: + logger.info(f"✓ 通知创建成功(重试 {attempt-1} 次后) - {title}") + else: + logger.debug(f"通知已创建并推送 - {title}") + + return notification + + except (OperationalError, InterfaceError) as e: + # 数据库连接错误,需要重试 + last_exception = e + error_msg = str(e) + logger.warning( + f"数据库连接错误 ({attempt}/{max_retries}) - {title}: {error_msg[:100]}" + ) + + if attempt < max_retries: + # 指数退避:1秒、2秒、3秒 + sleep_time = attempt + logger.debug(f"等待 {sleep_time} 秒后重试...") + time.sleep(sleep_time) + else: + logger.error( + f"创建通知失败 - 数据库连接问题(已重试 {max_retries} 次) - {title}: {error_msg}" + ) + + except Exception as e: + # 其他错误,不重试直接抛出 + last_exception = e + error_str = str(e).lower() + + if 'connection' in error_str or 'closed' in error_str: + logger.error(f"创建通知失败 - 连接相关错误 - {title}: {e}") + else: + logger.error(f"创建通知失败 - {title}: {e}") + + # 非连接错误,直接抛出不重试 + raise + + # 所有重试都失败了 + error_msg = f"创建通知失败 - 已重试 {max_retries} 次仍然失败 - {title}" + logger.error(error_msg) + raise RuntimeError(error_msg) from last_exception + + +def _push_to_websocket(notification: Notification) -> None: + """ + 推送通知到 WebSocket 客户端 + + - 在 Server 容器中:直接通过 Channel Layer 推送 + - 在 Worker 容器中:通过 API 回调让 Server 推送(因为 Worker 无法访问 Redis) + """ + import os + + # 检测是否在 Worker 容器中(有 SERVER_URL 环境变量) + server_url = os.environ.get("SERVER_URL") + + if server_url: + # Worker 容器:通过 API 回调 + _push_via_api_callback(notification, server_url) + else: + # Server 容器:直接推送 + _push_via_channel_layer(notification) + + +def _push_via_api_callback(notification: Notification, server_url: str) -> None: + """ + 通过 HTTP 回调推送通知(Worker → Server 跨容器通信) + + 注意:这不是同进程内的 service 调用 view,而是 Worker 容器 + 通过 HTTP 请求 Server 容器的 /api/callbacks/notification/ 接口。 + Worker 无法直接访问 Redis,需要由 Server 代为推送 WebSocket。 + """ + import requests + + try: + callback_url = f"{server_url}/api/callbacks/notification/" + data = { + 'id': notification.id, + 'category': notification.category, + 'title': notification.title, + 'message': notification.message, + 'level': notification.level, + 'created_at': notification.created_at.isoformat() + } + + resp = requests.post(callback_url, json=data, timeout=5) + resp.raise_for_status() + + logger.debug(f"通知回调推送成功 - ID: {notification.id}") + + except Exception as e: + logger.warning(f"通知回调推送失败 - ID: {notification.id}: {e}") + + +def _push_via_channel_layer(notification: Notification) -> None: + """通过 Channel Layer 直接推送通知(Server 容器使用)""" + try: + logger.debug(f"开始推送通知到 WebSocket - ID: {notification.id}") + + from asgiref.sync import async_to_sync + from channels.layers import get_channel_layer + + # 获取 Channel Layer + channel_layer = get_channel_layer() + + if channel_layer is None: + logger.warning("Channel Layer 未配置,跳过 WebSocket 推送") + return + + # 构造通知数据 + data = { + 'type': 'notification.message', # 对应 Consumer 的 notification_message 方法 + 'id': notification.id, + 'category': notification.category, + 'title': notification.title, + 'message': notification.message, + 'level': notification.level, + 'created_at': notification.created_at.isoformat() + } + + # 发送到通知组(所有连接的客户端) + async_to_sync(channel_layer.group_send)( + 'notifications', # 组名 + data + ) + + logger.debug(f"通知推送成功 - ID: {notification.id}") + + except ImportError as e: + logger.warning(f"Channels 模块未安装,跳过 WebSocket 推送: {e}") + except Exception as e: + logger.warning(f"WebSocket 推送失败 - ID: {notification.id}: {e}", exc_info=True) diff --git a/backend/apps/scan/notifications/types.py b/backend/apps/scan/notifications/types.py new file mode 100644 index 00000000..b99628c8 --- /dev/null +++ b/backend/apps/scan/notifications/types.py @@ -0,0 +1,19 @@ +"""通知系统类型定义""" + +from django.db import models + + +class NotificationLevel(models.TextChoices): + """通知级别""" + LOW = 'low', '低' + MEDIUM = 'medium', '中' + HIGH = 'high', '高' + CRITICAL = 'critical', '严重' + + +class NotificationCategory(models.TextChoices): + """通知分类""" + SCAN = 'scan', '扫描任务' + VULNERABILITY = 'vulnerability', '漏洞发现' + ASSET = 'asset', '资产发现' + SYSTEM = 'system', '系统消息' diff --git a/backend/apps/scan/notifications/urls.py b/backend/apps/scan/notifications/urls.py new file mode 100644 index 00000000..fe4b8523 --- /dev/null +++ b/backend/apps/scan/notifications/urls.py @@ -0,0 +1,29 @@ +""" +通知系统 URL 配置 +""" + +from django.urls import path +from . import views +from .views import ( + NotificationCollectionView, + NotificationMarkAllAsReadView, + NotificationUnreadCountView, +) + +app_name = 'notifications' + +urlpatterns = [ + # 通知列表 + path('', NotificationCollectionView.as_view(), name='list'), + + # 未读数量 + path('unread-count/', NotificationUnreadCountView.as_view(), name='unread-count'), + + # 标记全部已读 + path('mark-all-as-read/', NotificationMarkAllAsReadView.as_view(), name='mark-all-as-read'), + + # 测试通知 + path('test/', views.notifications_test, name='test'), +] + +# WebSocket 实时通知路由在 routing.py 中定义:ws://host/ws/notifications/ diff --git a/backend/apps/scan/notifications/views.py b/backend/apps/scan/notifications/views.py new file mode 100644 index 00000000..73a7109d --- /dev/null +++ b/backend/apps/scan/notifications/views.py @@ -0,0 +1,294 @@ +""" +通知系统视图 - REST API 和测试接口 +""" + +import logging +from typing import Any +from django.http import JsonResponse +from django.utils import timezone +from rest_framework import status +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from apps.common.pagination import BasePagination +from .models import Notification +from .serializers import NotificationSerializer +from .types import NotificationLevel +from .services import NotificationService, NotificationSettingsService + +logger = logging.getLogger(__name__) + + +def notifications_test(request): + """ + 测试通知推送 + """ + try: + from .services import create_notification + from django.http import JsonResponse + + level_param = request.GET.get('level', NotificationLevel.LOW) + try: + level_choice = NotificationLevel(level_param) + except ValueError: + level_choice = NotificationLevel.LOW + + title = request.GET.get('title') or "测试通知" + message = request.GET.get('message') or "这是一条测试通知消息" + + # 创建测试通知 + notification = create_notification( + title=title, + message=message, + level=level_choice + ) + + return JsonResponse({ + 'success': True, + 'message': '测试通知已发送', + 'notification_id': notification.id + }) + + except Exception as e: + logger.error(f"发送测试通知失败: {e}") + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=500) + + +def build_api_response( + data: Any = None, + *, + message: str = '操作成功', + code: str = '200', + state: str = 'success', + status_code: int = status.HTTP_200_OK +) -> Response: + """构建统一的 API 响应格式 + + Args: + data: 响应数据体(可选) + message: 响应消息 + code: 响应代码 + state: 响应状态(success/error) + status_code: HTTP 状态码 + + Returns: + DRF Response 对象 + """ + payload = { + 'code': code, + 'state': state, + 'message': message, + } + if data is not None: + payload['data'] = data + return Response(payload, status=status_code) + + +def _parse_bool(value: str | None) -> bool | None: + """解析字符串为布尔值 + + Args: + value: 字符串值,支持 '1', 'true', 'yes' 为 True;'0', 'false', 'no' 为 False + + Returns: + 布尔值,或 None(如果无法解析) + """ + if value is None: + return None + value = str(value).strip().lower() + if value in {'1', 'true', 'yes'}: + return True + if value in {'0', 'false', 'no'}: + return False + return None + + + +class NotificationCollectionView(APIView): + """通知列表 + + 支持的方法: + - GET: 获取通知列表(支持分页和过滤) + """ + pagination_class = BasePagination + + def get(self, request: Request) -> Response: + """ + 获取通知列表 + + URL: GET /api/notifications/?page=1&pageSize=20&level=info&unread=true + + 查询参数: + - page: 页码(默认 1) + - pageSize: 每页数量(默认 10,最大 1000) + - level: 通知级别过滤(low/medium/high) + - unread: 是否未读(true/false) + + 返回: + - results: 通知列表 + - total: 总记录数 + - page: 当前页码 + - page_size: 每页大小 + - total_pages: 总页数 + """ + service = NotificationService() + + # 按级别过滤 + level_param = request.query_params.get('level') + level_filter = level_param if level_param in NotificationLevel.values else None + + # 按已读状态过滤 + # unread=true: 仅未读 unread=false: 仅已读 unread=None: 全部 + unread_param = _parse_bool(request.query_params.get('unread')) + + queryset = service.get_notifications(level=level_filter, unread=unread_param) + + # 使用通用分页器 + paginator = self.pagination_class() + page_obj = paginator.paginate_queryset(queryset, request) + serializer = NotificationSerializer(page_obj, many=True) + return paginator.get_paginated_response(serializer.data) + + +class NotificationUnreadCountView(APIView): + """获取未读通知数量 + + URL: GET /api/notifications/unread-count/ + + 功能: + - 返回当前未读通知的数量 + + 返回: + - count: 未读通知数量 + """ + + def get(self, request: Request) -> Response: + """获取未读通知数量""" + service = NotificationService() + count = service.get_unread_count() + return build_api_response({'count': count}, message='获取未读数量成功') + + +class NotificationMarkAllAsReadView(APIView): + """标记全部通知为已读 + + URL: POST /api/notifications/mark-all-as-read/ + + 功能: + - 将所有未读通知标记为已读 + - 更新 read_at 时间戳 + + 返回: + - updated: 更新的通知数量 + """ + + def post(self, request: Request) -> Response: + """标记全部通知为已读""" + service = NotificationService() + updated = service.mark_all_as_read() + return build_api_response({'updated': updated}, message='全部标记已读成功') + + +class NotificationSettingsView(APIView): + """通知设置 API + + URL: /api/settings/notifications/ + + 支持的方法: + - GET: 获取当前通知设置 + - PUT: 更新通知设置 + """ + + def get(self, request: Request) -> Response: + """获取通知设置""" + service = NotificationSettingsService() + settings = service.get_settings() + return Response(settings) + + def put(self, request: Request) -> Response: + """更新通知设置""" + service = NotificationSettingsService() + settings = service.update_settings(request.data) + return Response({'message': '已保存通知设置', **settings}) + + +# ============================================ +# Worker 回调 API +# ============================================ + +@api_view(['POST']) +@permission_classes([AllowAny]) # Worker 容器无认证,可考虑添加 Token 验证 +def notification_callback(request): + """ + 接收 Worker 的通知推送请求 + + Worker 容器无法直接访问 Redis,通过此 API 回调让 Server 推送 WebSocket。 + + POST /api/callbacks/notification/ + { + "id": 1, + "category": "scan", + "title": "扫描开始", + "message": "...", + "level": "info", + "created_at": "2025-01-01T00:00:00" + } + """ + try: + data = request.data + + # 验证必要字段 + required_fields = ['id', 'category', 'title', 'message', 'level', 'created_at'] + for field in required_fields: + if field not in data: + return Response( + {'error': f'缺少字段: {field}'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 推送到 WebSocket + _push_notification_to_websocket(data) + + logger.debug(f"回调通知推送成功 - ID: {data['id']}, Title: {data['title']}") + return Response({'status': 'ok'}) + + except Exception as e: + logger.error(f"回调通知处理失败: {e}", exc_info=True) + return Response( + {'error': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +def _push_notification_to_websocket(data: dict): + """推送通知到 WebSocket(Server 端使用)""" + from asgiref.sync import async_to_sync + from channels.layers import get_channel_layer + + channel_layer = get_channel_layer() + if channel_layer is None: + logger.warning("Channel Layer 未配置,跳过 WebSocket 推送") + return + + # 构造通知数据 + ws_data = { + 'type': 'notification.message', + 'id': data['id'], + 'category': data['category'], + 'title': data['title'], + 'message': data['message'], + 'level': data['level'], + 'created_at': data['created_at'] + } + + # 发送到通知组 + async_to_sync(channel_layer.group_send)( + 'notifications', + ws_data + ) diff --git a/backend/apps/scan/orchestrators/__init__.py b/backend/apps/scan/orchestrators/__init__.py new file mode 100644 index 00000000..bec8ab1b --- /dev/null +++ b/backend/apps/scan/orchestrators/__init__.py @@ -0,0 +1,11 @@ +""" +工作流编排器模块 + +提供工作流编排相关的类和函数 +""" + +from .flow_orchestrator import FlowOrchestrator + +__all__ = [ + 'FlowOrchestrator', +] diff --git a/backend/apps/scan/orchestrators/flow_orchestrator.py b/backend/apps/scan/orchestrators/flow_orchestrator.py new file mode 100644 index 00000000..655a7b5d --- /dev/null +++ b/backend/apps/scan/orchestrators/flow_orchestrator.py @@ -0,0 +1,224 @@ +""" +工作流编排器 + +职责: +- 解析 YAML 配置 +- 检测扫描类型 +- 提供 Flow 函数映射 +- 生成执行计划 + +注意:本类只负责准备和解析,不执行 Flow +Flow 的执行由 initiate_scan_flow (Prefect @flow) 负责 +""" + +import logging +import yaml +from typing import Dict, List, Optional, Callable, Any + +from apps.scan.configs.command_templates import get_supported_scan_types, EXECUTION_STAGES + +logger = logging.getLogger(__name__) + + +class FlowOrchestrator: + """ + 工作流编排器 + + 负责解析 YAML 配置并生成执行计划,不执行具体的 Flow + """ + + def __init__(self, engine_config: str): + """ + 初始化编排器 + + Args: + engine_config: YAML 格式的引擎配置字符串 + + Raises: + ValueError: 配置为空或解析失败 + """ + if not engine_config or not engine_config.strip(): + raise ValueError("引擎配置不能为空") + + try: + self.config = yaml.safe_load(engine_config) or {} + except yaml.YAMLError as e: + logger.error(f"引擎配置解析失败: {e}") + raise ValueError(f"引擎配置解析失败: {e}") + + if not self.config: + raise ValueError("引擎配置为空") + + # 检测启用的扫描类型 + self.scan_types = self._detect_scan_types() + + # 解析所有扫描类型的工具配置 + from apps.scan.utils.config_parser import parse_enabled_tools_from_dict + self.enabled_tools_by_type = {} + for scan_type in self.scan_types: + try: + enabled_tools = parse_enabled_tools_from_dict( + scan_type=scan_type, + parsed_config=self.config + ) + if enabled_tools: + self.enabled_tools_by_type[scan_type] = enabled_tools + logger.debug(f"✓ {scan_type}: {len(enabled_tools)} 个启用工具") + except Exception as e: + logger.error(f"解析 {scan_type} 工具配置失败: {e}") + raise + + logger.info(f"✓ FlowOrchestrator 初始化完成,{len(self.enabled_tools_by_type)} 个扫描类型有启用的工具") + + def _parse_config(self, engine_config: str) -> Dict: + """ + 解析 YAML 配置 + + Args: + engine_config: YAML 格式字符串 + + Returns: + dict: 解析后的配置字典 + + Raises: + ValueError: 配置为空或解析失败 + """ + if not engine_config: + raise ValueError("引擎配置为空,请提供有效的 YAML 配置") + + try: + config = yaml.safe_load(engine_config) + if not config: + raise ValueError("YAML 配置解析结果为空") + + logger.info(f"YAML 配置解析成功,检测到的 key: {list(config.keys())}") + return config + + except yaml.YAMLError as e: + raise ValueError(f"YAML 配置解析失败: {e}") + + def _detect_scan_types(self) -> List[str]: + """ + 检测配置中已启用的扫描类型(按 YAML 顺序) + + Returns: + list: 已启用的扫描类型列表 + + Raises: + ValueError: 未检测到有效的扫描类型 + """ + # 保持 YAML 中的顺序,且只包含已启用的类型 + supported_scan_types = get_supported_scan_types() + scan_types = [ + key for key in self.config.keys() + if key in supported_scan_types and self.is_scan_type_enabled(key) + ] + + if not scan_types: + raise ValueError( + f"未检测到已启用的扫描类型。\n" + f"配置中的 key: {list(self.config.keys())}\n" + f"支持的扫描类型: {supported_scan_types}" + ) + + logger.info(f"检测到已启用的扫描类型(按顺序): {scan_types}") + return scan_types + + def is_scan_type_enabled(self, scan_type: str) -> bool: + """ + 判断指定扫描类型是否启用(存在配置且有启用的工具) + + Args: + scan_type: 扫描类型 + + Returns: + bool: 是否启用 + """ + if scan_type not in self.config: + return False + + scan_config = self.config.get(scan_type, {}) + + # 子域名发现使用 passive_tools 结构 + if scan_type == 'subdomain_discovery': + passive_tools = scan_config.get('passive_tools', {}) + for tool_config in passive_tools.values(): + if isinstance(tool_config, dict) and tool_config.get('enabled', False): + return True + return False + + # 其他扫描类型:检查 tools + tools = scan_config.get('tools', {}) + for tool_config in tools.values(): + if tool_config.get('enabled', False): + return True + + return False + + def get_execution_stages(self): + """ + 迭代器:获取所有执行阶段 + + Yields: + tuple: (mode, enabled_flows) + - mode: 执行模式('sequential' 或 'parallel') + - enabled_flows: 该阶段中已启用的扫描类型列表 + + Example: + for mode, flows in orchestrator.get_execution_stages(): + if mode == 'sequential': + for flow in flows: + execute_flow(flow) + else: # parallel + execute_parallel(flows) + """ + for stage in EXECUTION_STAGES: + # 筛选出已启用的流程 + enabled_flows = [ + flow for flow in stage['flows'] + if flow in self.scan_types + ] + + # 只返回有启用流程的阶段 + if enabled_flows: + logger.debug(f"阶段 {stage['mode']}: {enabled_flows}") + yield stage['mode'], enabled_flows + + def get_flow_function(self, scan_type: str) -> Optional[Callable]: + """ + 获取指定扫描类型的 Flow 函数(延迟导入) + + Args: + scan_type: 扫描类型 + + Returns: + Callable: Flow 函数,如果未实现则返回 None + """ + if scan_type == 'subdomain_discovery': + from apps.scan.flows.subdomain_discovery_flow import subdomain_discovery_flow + return subdomain_discovery_flow + + elif scan_type == 'port_scan': + from apps.scan.flows.port_scan_flow import port_scan_flow + return port_scan_flow + + elif scan_type == 'site_scan': + from apps.scan.flows.site_scan_flow import site_scan_flow + return site_scan_flow + + elif scan_type == 'directory_scan': + from apps.scan.flows.directory_scan_flow import directory_scan_flow + return directory_scan_flow + + elif scan_type == 'url_fetch': + from apps.scan.flows.url_fetch import url_fetch_flow + return url_fetch_flow + + elif scan_type == 'vuln_scan': + from apps.scan.flows.vuln_scan import vuln_scan_flow + return vuln_scan_flow + + else: + logger.warning(f"未实现的扫描类型: {scan_type}") + return None + diff --git a/backend/apps/scan/repositories/__init__.py b/backend/apps/scan/repositories/__init__.py new file mode 100644 index 00000000..3a1ca134 --- /dev/null +++ b/backend/apps/scan/repositories/__init__.py @@ -0,0 +1,23 @@ +""" +Scan Repositories 模块 + +提供 Scan 模型的数据访问层实现 +其他模型的 Repository 应从各自的 app 导入 +""" + +# Django ORM 实现 +from .django_scan_repository import DjangoScanRepository +from .scheduled_scan_repository import DjangoScheduledScanRepository, ScheduledScanDTO + +# 为了向后兼容,保留 ScanRepository 别名 +ScanRepository = DjangoScanRepository + +__all__ = [ + # 实现类 + 'DjangoScanRepository', + 'DjangoScheduledScanRepository', + 'ScheduledScanDTO', + # 向后兼容别名 + 'ScanRepository', +] + diff --git a/backend/apps/scan/repositories/django_scan_repository.py b/backend/apps/scan/repositories/django_scan_repository.py new file mode 100644 index 00000000..5cd3efa6 --- /dev/null +++ b/backend/apps/scan/repositories/django_scan_repository.py @@ -0,0 +1,620 @@ +""" +Scan 数据访问层 Django ORM 实现 + +基于 Django ORM 的 Scan Repository 实现类 +""" + +from __future__ import annotations + +import logging +from typing import List, Tuple, Dict +from datetime import datetime + +from django.db import transaction, DatabaseError +from django.db.models import QuerySet, F, Value, Func, Count +from django.utils import timezone + +from apps.scan.models import Scan +from apps.targets.models import Target +from apps.engine.models import ScanEngine +from apps.common.definitions import ScanStatus +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoScanRepository: + """基于 Django ORM 的 Scan 数据访问层实现""" + + # ==================== 基础 CRUD 操作 ==================== + + + def get_by_id(self, + scan_id: int, + prefetch_relations: bool = False, + for_update: bool = False + ) -> Scan | None: + """ + 根据 ID 获取扫描任务 + + Args: + scan_id: 扫描任务 ID + prefetch_relations: 是否预加载关联对象(engine, target) + 默认 False,只在需要展示关联信息时设为 True + for_update: 是否加锁(用于更新场景) + + Returns: + Scan 对象或 None + """ + try: + # 根据是否需要更新来决定是否加锁 + if for_update: + queryset = Scan.objects.select_for_update() # type: ignore # pylint: disable=no-member + else: + queryset = Scan.objects # type: ignore # pylint: disable=no-member + + # 预加载关联对象(性能优化:默认不加载) + if prefetch_relations: + queryset = queryset.select_related('engine', 'target') + + return queryset.get(id=scan_id) + except Scan.DoesNotExist: # type: ignore # pylint: disable=no-member + logger.warning("Scan 不存在 - Scan ID: %s", scan_id) + return None + + + def get_by_id_for_update(self, scan_id: int) -> Scan | None: + """ + 根据 ID 获取扫描任务(加锁) + + 用于需要更新的场景,避免并发冲突。 + 不预加载关联对象,保持查询最小化,提高加锁性能。 + + Args: + scan_id: 扫描任务 ID + + Returns: + Scan 对象或 None + + Note: + - 使用默认的阻塞模式(等待锁释放) + - 不包含关联对象(engine, target),如需关联对象请使用 get_by_id() + """ + try: + return Scan.objects.select_for_update().get(id=scan_id) # type: ignore # pylint: disable=no-member + except Scan.DoesNotExist: # type: ignore # pylint: disable=no-member + logger.warning("Scan 不存在 - Scan ID: %s", scan_id) + return None + + + def exists(self, scan_id: int) -> bool: + """ + 检查扫描任务是否存在 + + Args: + scan_id: 扫描任务 ID + + Returns: + 是否存在 + """ + return Scan.objects.filter(id=scan_id).exists() + + + def create(self, + target: Target, + engine: ScanEngine, + results_dir: str, + status: ScanStatus = ScanStatus.INITIATED + ) -> Scan: + """ + 创建扫描任务 + + Args: + target: 扫描目标 + engine: 扫描引擎 + results_dir: 结果目录 + status: 初始状态 + + Returns: + 创建的 Scan 对象 + """ + scan = Scan( + target=target, + engine=engine, + results_dir=results_dir, + status=status, + container_ids=[] + ) + scan.save() + logger.debug("创建 Scan - ID: %s, Target: %s", scan.id, target.name) + return scan + + + def bulk_create(self, scans: List[Scan]) -> List[Scan]: + """ + 批量创建扫描任务 + + Args: + scans: Scan 对象列表 + + Returns: + 创建的 Scan 对象列表 + """ + created_scans = Scan.objects.bulk_create(scans) # type: ignore # pylint: disable=no-member + logger.debug("批量创建 Scan - 数量: %d", len(created_scans)) + return created_scans + + + def soft_delete_by_ids(self, scan_ids: List[int]) -> int: + """ + 根据 ID 列表批量软删除 Scan + + Args: + scan_ids: Scan ID 列表 + + Returns: + 软删除的记录数 + """ + try: + updated_count = ( + Scan.objects + .filter(id__in=scan_ids) + .update(deleted_at=timezone.now()) + ) + logger.debug( + "批量软删除 Scan 成功 - Count: %s, 更新记录: %s", + len(scan_ids), + updated_count + ) + return updated_count + except Exception as e: + logger.error( + "批量软删除 Scan 失败 - IDs: %s, 错误: %s", + scan_ids, + e + ) + raise + + def hard_delete_by_ids(self, scan_ids: List[int]) -> Tuple[int, Dict[str, int]]: + """ + 根据 ID 列表硬删除 Scan(使用数据库级 CASCADE) + + Args: + scan_ids: Scan ID 列表 + + Returns: + (删除的记录数, 删除详情字典) + """ + try: + batch_size = 1000 + total_deleted = 0 + + logger.debug(f"开始批量删除 {len(scan_ids)} 个 Scan(数据库 CASCADE)...") + + for i in range(0, len(scan_ids), batch_size): + batch_ids = scan_ids[i:i + batch_size] + count, _ = Scan.all_objects.filter(id__in=batch_ids).delete() + total_deleted += count + logger.debug(f"批次删除完成: {len(batch_ids)} 个 Scan,删除 {count} 条记录") + + deleted_details = { + 'scans': len(scan_ids), + 'total': total_deleted, + 'note': 'Database CASCADE - detailed stats unavailable' + } + + logger.debug( + "批量硬删除成功(CASCADE)- Scan数: %s, 总删除记录: %s", + len(scan_ids), + total_deleted + ) + + return total_deleted, deleted_details + + except Exception as e: + logger.error( + "批量硬删除失败(CASCADE)- Scan数: %s, 错误: %s", + len(scan_ids), + str(e), + exc_info=True + ) + raise + + + + # ==================== 查询操作 ==================== + + + def get_all(self, prefetch_relations: bool = True) -> QuerySet[Scan]: + """ + 获取所有扫描任务 + + Args: + prefetch_relations: 是否预加载关联对象(engine, target) + + Returns: + Scan QuerySet + """ + queryset = Scan.objects.all() # type: ignore # pylint: disable=no-member + if prefetch_relations: + queryset = queryset.select_related('engine', 'target') + return queryset.order_by('-created_at') + + + def get_statistics(self) -> dict: + """ + 获取扫描任务统计数据 + + Returns: + 统计数据字典 + + Note: + 使用缓存字段聚合,性能优异 + """ + from django.db.models import Sum + + # 基础统计 + total_scans = Scan.objects.count() # type: ignore # pylint: disable=no-member + + # 按状态统计 + running_scans = Scan.objects.filter(status='running').count() # type: ignore # pylint: disable=no-member + completed_scans = Scan.objects.filter(status='completed').count() # type: ignore # pylint: disable=no-member + failed_scans = Scan.objects.filter(status='failed').count() # type: ignore # pylint: disable=no-member + + # 使用缓存字段聚合统计(只统计已完成的扫描) + aggregated = Scan.objects.filter(status='completed').aggregate( # type: ignore # pylint: disable=no-member + total_vulns=Sum('cached_vulns_total'), + total_subdomains=Sum('cached_subdomains_count'), + total_endpoints=Sum('cached_endpoints_count'), + total_websites=Sum('cached_websites_count'), + total_ips=Sum('cached_ips_count'), + ) + + total_vulns = aggregated['total_vulns'] or 0 + total_subdomains = aggregated['total_subdomains'] or 0 + total_endpoints = aggregated['total_endpoints'] or 0 + total_websites = aggregated['total_websites'] or 0 + total_ips = aggregated['total_ips'] or 0 + + return { + 'total': total_scans, + 'running': running_scans, + 'completed': completed_scans, + 'failed': failed_scans, + 'total_vulns': total_vulns, + 'total_subdomains': total_subdomains, + 'total_endpoints': total_endpoints, + 'total_websites': total_websites, + 'total_ips': total_ips, + 'total_assets': total_subdomains + total_endpoints + total_websites + total_ips, + } + + + + # ==================== 状态更新操作 ==================== + + + @transaction.atomic + def update_status(self, + scan_id: int, + status: ScanStatus, + error_message: str | None = None, + stopped_at: datetime | None = None + ) -> bool: + """ + 更新扫描任务状态 + + Args: + scan_id: 扫描任务 ID + status: 新状态 + error_message: 错误消息(可选) + stopped_at: 结束时间(可选,由调用方决定是否传递) + + Returns: + 是否更新成功 + + Note: + Repository 层不判断业务状态,只负责数据更新 + created_at 是自动设置的,不需要手动传递 + """ + scan = self.get_by_id_for_update(scan_id) + if not scan: + return False + + scan.status = status + + if error_message: + if len(error_message) > 2000: + scan.error_message = error_message[:1980] + "... (已截断)" + logger.warning( + "错误信息过长(%d 字符),已截断 - Scan ID: %s", + len(error_message), scan_id + ) + else: + scan.error_message = error_message + + # 根据传递的参数更新时间戳(由调用方决定) + if stopped_at is not None: + scan.stopped_at = stopped_at + + scan.save() + logger.debug( + "更新 Scan 状态 - ID: %s, 状态: %s", + scan_id, + ScanStatus(status).label + ) + return True + + + def append_container_id(self, scan_id: int, container_id: str) -> bool: + """ + 追加容器 ID 到 container_ids 数组(并发安全) + + 使用 PostgreSQL 的 array_append 函数在数据库层面进行原子操作, + 避免并发场景下的 Race Condition。 + + Args: + scan_id: 扫描任务 ID + container_id: Docker 容器 ID + + Returns: + 是否追加成功 + + Note: + - 使用 F 表达式和 ArrayAppend 确保并发安全 + - 生成的 SQL: UPDATE scan SET container_ids = array_append(container_ids, ?) + """ + try: + container_field = Scan._meta.get_field('container_ids') + updated_count = Scan.objects.filter(id=scan_id).update( # type: ignore + container_ids=Func( + F('container_ids'), + Value(container_id), + function='ARRAY_APPEND', + output_field=container_field + ) + ) + + if updated_count > 0: + logger.debug("追加容器 ID - Scan ID: %s, Container ID: %s", scan_id, container_id) + return True + else: + logger.warning("Scan 不存在,无法追加容器 ID - Scan ID: %s", scan_id) + return False + except DatabaseError as e: + logger.error("追加容器 ID 失败 - Scan ID: %s, 错误: %s", scan_id, e) + return False + + + def update_worker(self, scan_id: int, worker_id: int) -> bool: + """ + 更新扫描任务的 Worker ID + + Args: + scan_id: 扫描任务 ID + worker_id: Worker 节点 ID + + Returns: + 是否更新成功 + """ + try: + updated_count = Scan.objects.filter(id=scan_id).update(worker_id=worker_id) # type: ignore + + if updated_count > 0: + logger.debug("更新 Worker ID - Scan ID: %s, Worker ID: %s", scan_id, worker_id) + return True + else: + logger.warning("Scan 不存在,无法更新 Worker ID - Scan ID: %s", scan_id) + return False + except DatabaseError as e: + logger.error("更新 Worker ID 失败 - Scan ID: %s, 错误: %s", scan_id, e) + return False + + + def update_cached_stats(self, scan_id: int) -> dict | None: + """ + 更新扫描任务的缓存统计数据 + + 使用 Django ORM 聚合查询,避免原生 SQL,保持数据库抽象 + + Args: + scan_id: 扫描任务 ID + + Returns: + 成功返回统计数据字典,失败返回 None + """ + try: + from apps.asset.models import VulnerabilitySnapshot + + scan = self.get_by_id(scan_id, prefetch_relations=False) + if not scan: + logger.error("Scan 不存在,无法更新缓存统计数据 - Scan ID: %s", scan_id) + return None + + # 统计快照数据(用于扫描历史) + # IP 数量需要按 IP 去重统计 + ips_count = scan.host_port_mapping_snapshots.values('ip').distinct().count() + + # 漏洞统计:按扫描维度基于 VulnerabilitySnapshot 聚合 + vuln_qs = VulnerabilitySnapshot.objects.filter(scan_id=scan_id) + total_vulns = vuln_qs.count() + + severity_stats = { + 'critical': 0, + 'high': 0, + 'medium': 0, + 'low': 0, + } + + for row in vuln_qs.values('severity').annotate(count=Count('id')): + sev = (row.get('severity') or '').lower() + count = row.get('count') or 0 + if sev in severity_stats: + severity_stats[sev] = count + + stats = { + 'subdomains': scan.subdomain_snapshots.count(), + 'websites': scan.website_snapshots.count(), + 'endpoints': scan.endpoint_snapshots.count(), + 'ips': ips_count, + 'directories': scan.directory_snapshots.count(), + 'vulns_total': total_vulns, + 'vulns_critical': severity_stats['critical'], + 'vulns_high': severity_stats['high'], + 'vulns_medium': severity_stats['medium'], + 'vulns_low': severity_stats['low'], + } + + # 批量更新字段(使用 cached_ 前缀的字段名) + cached_stats = { + 'cached_subdomains_count': stats['subdomains'], + 'cached_websites_count': stats['websites'], + 'cached_endpoints_count': stats['endpoints'], + 'cached_ips_count': stats['ips'], + 'cached_directories_count': stats['directories'], + 'cached_vulns_total': stats['vulns_total'], + 'cached_vulns_critical': stats['vulns_critical'], + 'cached_vulns_high': stats['vulns_high'], + 'cached_vulns_medium': stats['vulns_medium'], + 'cached_vulns_low': stats['vulns_low'], + 'stats_updated_at': timezone.now() + } + + for field, value in cached_stats.items(): + setattr(scan, field, value) + + scan.save(update_fields=list(cached_stats.keys())) + + logger.debug("更新缓存统计数据成功 - Scan ID: %s", scan_id) + return stats + except DatabaseError as e: + logger.exception("数据库错误:更新缓存统计数据失败 - Scan ID: %s", scan_id) + return None + except Exception as e: + logger.error("更新缓存统计数据失败 - Scan ID: %s, 错误: %s", scan_id, e) + return None + + + def update_status_if_match(self, + scan_id: int, + current_status: ScanStatus, + new_status: ScanStatus, + stopped_at: datetime = None + ) -> bool: + """ + 条件更新扫描状态(原子操作) + + 仅当扫描状态匹配 current_status 时才更新为 new_status。 + 这是一个原子操作,用于处理并发场景下的状态更新。 + + Args: + scan_id: 扫描ID + current_status: 当前期望的状态 + new_status: 要更新到的新状态 + stopped_at: 停止时间(可选) + + Returns: + bool: 是否更新成功(True=更新了记录,False=未更新) + """ + try: + update_fields = { + 'status': new_status, + } + + if stopped_at: + update_fields['stopped_at'] = stopped_at + + # 原子操作:只有状态匹配时才更新 + updated = Scan.objects.filter( + id=scan_id, + status=current_status + ).update(**update_fields) + + if updated > 0: + logger.debug( + "条件更新扫描状态成功 - Scan ID: %s, %s → %s", + scan_id, + current_status.value, + new_status.value + ) + return True + else: + logger.debug( + "条件更新扫描状态跳过(状态不匹配) - Scan ID: %s, 期望: %s, 目标: %s", + scan_id, + current_status.value, + new_status.value + ) + return False + + except Exception as e: + logger.error( + "条件更新扫描状态失败 - Scan ID: %s, %s → %s, 错误: %s", + scan_id, + current_status.value, + new_status.value, + e + ) + return False + + + def update_progress( + self, + scan_id: int, + progress: int | None = None, + current_stage: str | None = None, + stage_progress: dict | None = None + ) -> bool: + """ + 更新扫描进度信息 + + Args: + scan_id: 扫描任务 ID + progress: 进度百分比 0-100(可选) + current_stage: 当前阶段(可选) + stage_progress: 各阶段详情(可选) + + Returns: + 是否更新成功 + """ + try: + update_fields = {} + + if progress is not None: + update_fields['progress'] = progress + + if current_stage is not None: + update_fields['current_stage'] = current_stage + + if stage_progress is not None: + update_fields['stage_progress'] = stage_progress + + if not update_fields: + return True # 无需更新 + + updated = Scan.objects.filter(id=scan_id).update(**update_fields) + + if updated > 0: + logger.debug( + "更新扫描进度 - Scan ID: %s, progress: %s, stage: %s", + scan_id, + progress, + current_stage + ) + return True + else: + logger.warning("Scan 不存在,无法更新进度 - Scan ID: %s", scan_id) + return False + + except Exception as e: + logger.error( + "更新扫描进度失败 - Scan ID: %s, 错误: %s", + scan_id, + e + ) + return False + + +# 导出接口 +__all__ = ['DjangoScanRepository'] diff --git a/backend/apps/scan/repositories/scheduled_scan_repository.py b/backend/apps/scan/repositories/scheduled_scan_repository.py new file mode 100644 index 00000000..27a9336b --- /dev/null +++ b/backend/apps/scan/repositories/scheduled_scan_repository.py @@ -0,0 +1,194 @@ +""" +定时扫描任务 Repository + +数据访问层:负责 ScheduledScan 模型的 CRUD 操作 +""" +import logging +from typing import List, Optional, Tuple, Dict +from dataclasses import dataclass +from datetime import datetime + +from django.db import transaction +from django.utils import timezone + +from apps.common.decorators import auto_ensure_db_connection +from apps.scan.models import ScheduledScan + + +logger = logging.getLogger(__name__) + + +@dataclass +class ScheduledScanDTO: + """定时扫描 DTO + + 扫描模式(二选一): + - 组织扫描:设置 organization_id,执行时动态获取组织下所有目标 + - 目标扫描:设置 target_id,扫描单个目标 + - organization_id 优先级高于 target_id + """ + id: Optional[int] = None + name: str = '' + engine_id: int = 0 + organization_id: Optional[int] = None # 组织扫描模式 + target_id: Optional[int] = None # 目标扫描模式 + cron_expression: Optional[str] = None + is_enabled: bool = True + run_count: int = 0 + last_run_time: Optional[datetime] = None + next_run_time: Optional[datetime] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + + +@auto_ensure_db_connection +class DjangoScheduledScanRepository: + """ + 定时扫描任务 Repository + + 职责: + - CRUD 操作 + - 查询封装 + - 不包含业务逻辑 + """ + + def get_by_id(self, scheduled_scan_id: int) -> Optional[ScheduledScan]: + """根据 ID 查询定时扫描任务""" + try: + return ScheduledScan.objects.select_related('engine', 'organization', 'target').get(id=scheduled_scan_id) + except ScheduledScan.DoesNotExist: + return None + + def get_queryset(self): + """ + 获取所有定时扫描任务的查询集 + + Returns: + QuerySet + """ + return ScheduledScan.objects.select_related('engine', 'organization', 'target').order_by('-created_at') + + def get_all(self, page: int = 1, page_size: int = 10) -> Tuple[List[ScheduledScan], int]: + """ + 分页查询所有定时扫描任务 + + Returns: + (定时扫描列表, 总数) + """ + queryset = self.get_queryset() + total = queryset.count() + + offset = (page - 1) * page_size + scheduled_scans = list(queryset[offset:offset + page_size]) + + return scheduled_scans, total + + def get_enabled(self) -> List[ScheduledScan]: + """获取所有启用的定时扫描任务""" + return list( + ScheduledScan.objects.select_related('engine', 'target') + .filter(is_enabled=True) + .order_by('-created_at') + ) + + def create(self, dto: ScheduledScanDTO) -> ScheduledScan: + """ + 创建定时扫描任务 + + Args: + dto: 定时扫描 DTO + + Returns: + 创建的 ScheduledScan 对象 + """ + with transaction.atomic(): + scheduled_scan = ScheduledScan.objects.create( + name=dto.name, + engine_id=dto.engine_id, + organization_id=dto.organization_id, # 组织扫描模式 + target_id=dto.target_id if not dto.organization_id else None, # 目标扫描模式 + cron_expression=dto.cron_expression, + is_enabled=dto.is_enabled, + ) + + scan_mode = "organization" if dto.organization_id else "target" + logger.info("创建定时扫描任务 - ID: %s, Name: %s, Mode: %s", scheduled_scan.id, scheduled_scan.name, scan_mode) + return scheduled_scan + + def update(self, scheduled_scan_id: int, dto: ScheduledScanDTO) -> Optional[ScheduledScan]: + """ + 更新定时扫描任务 + + Args: + scheduled_scan_id: 定时扫描 ID + dto: 更新的数据 + + Returns: + 更新后的 ScheduledScan 对象,不存在返回 None + """ + try: + with transaction.atomic(): + scheduled_scan = ScheduledScan.objects.select_for_update().get(id=scheduled_scan_id) + + # 更新基本字段 + if dto.name: + scheduled_scan.name = dto.name + if dto.engine_id: + scheduled_scan.engine_id = dto.engine_id + if dto.cron_expression is not None: + scheduled_scan.cron_expression = dto.cron_expression + if dto.is_enabled is not None: + scheduled_scan.is_enabled = dto.is_enabled + if dto.next_run_time is not None: + scheduled_scan.next_run_time = dto.next_run_time + + # 切换扫描模式 + if dto.organization_id is not None: + # 切换到组织扫描模式 + scheduled_scan.organization_id = dto.organization_id + scheduled_scan.target_id = None # 清空目标 + elif dto.target_id is not None: + # 切换到目标扫描模式 + scheduled_scan.organization_id = None # 清空组织 + scheduled_scan.target_id = dto.target_id + + scheduled_scan.save() + + scan_mode = "organization" if scheduled_scan.organization_id else "target" + logger.info("更新定时扫描任务 - ID: %s, Mode: %s", scheduled_scan_id, scan_mode) + return scheduled_scan + + except ScheduledScan.DoesNotExist: + logger.warning("定时扫描任务不存在 - ID: %s", scheduled_scan_id) + return None + + def update_next_run_time(self, scheduled_scan_id: int, next_run_time: datetime) -> bool: + """更新下次执行时间""" + updated = ScheduledScan.objects.filter(id=scheduled_scan_id).update( + next_run_time=next_run_time + ) + return updated > 0 + + def increment_run_count(self, scheduled_scan_id: int) -> bool: + """增加执行次数并更新上次执行时间""" + from django.db.models import F + updated = ScheduledScan.objects.filter(id=scheduled_scan_id).update( + run_count=F('run_count') + 1, + last_run_time=timezone.now() + ) + return updated > 0 + + def toggle_enabled(self, scheduled_scan_id: int, enabled: bool) -> bool: + """切换启用状态""" + updated = ScheduledScan.objects.filter(id=scheduled_scan_id).update( + is_enabled=enabled + ) + return updated > 0 + + def hard_delete(self, scheduled_scan_id: int) -> bool: + """删除定时扫描任务""" + deleted, _ = ScheduledScan.objects.filter(id=scheduled_scan_id).delete() + if deleted > 0: + logger.info("硬删除定时扫描任务 - ID: %s", scheduled_scan_id) + return deleted > 0 diff --git a/backend/apps/scan/scripts/__init__.py b/backend/apps/scan/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/scan/scripts/run_cleanup.py b/backend/apps/scan/scripts/run_cleanup.py new file mode 100644 index 00000000..89715baf --- /dev/null +++ b/backend/apps/scan/scripts/run_cleanup.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +""" +清理任务脚本 + +用于动态容器执行清理任务,目前支持: +- results: 清理过期的扫描结果目录 + +如需添加其他清理任务,添加对应的 cleanup_xxx() 函数即可。 + +注意:此脚本只做文件清理,不需要 Django 环境。 +""" +import argparse +import shutil +import logging +from datetime import datetime, timedelta +from pathlib import Path + +logging.basicConfig( + level=logging.INFO, + format='[%(asctime)s] [%(levelname)s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) +logger = logging.getLogger(__name__) + + +def cleanup_results(results_dir: str, retention_days: int) -> dict: + """ + 清理过期的扫描结果目录 + + Args: + results_dir: 扫描结果根目录 + retention_days: 保留天数 + + Returns: + 清理统计信息 + """ + results_path = Path(results_dir) + if not results_path.exists(): + logger.warning(f"扫描结果目录不存在: {results_dir}") + return {'deleted': 0, 'failed': 0, 'skipped': 0} + + cutoff_date = datetime.now() - timedelta(days=retention_days) + stats = {'deleted': 0, 'failed': 0, 'skipped': 0, 'freed_bytes': 0} + + logger.info(f"开始清理扫描结果 - 目录: {results_dir}, 保留天数: {retention_days}") + logger.info(f"清理截止时间: {cutoff_date}") + + for item in results_path.iterdir(): + if not item.is_dir(): + continue + + # 只处理 scan_ 开头的目录 + if not item.name.startswith('scan_'): + stats['skipped'] += 1 + continue + + try: + # 获取目录修改时间 + mtime = datetime.fromtimestamp(item.stat().st_mtime) + + if mtime < cutoff_date: + # 计算目录大小 + dir_size = sum(f.stat().st_size for f in item.rglob('*') if f.is_file()) + + # 删除目录 + shutil.rmtree(item) + stats['deleted'] += 1 + stats['freed_bytes'] += dir_size + + logger.info(f" 已删除: {item.name} (修改时间: {mtime}, 大小: {dir_size / 1024 / 1024:.2f} MB)") + else: + stats['skipped'] += 1 + + except Exception as e: + logger.error(f" 删除失败: {item.name} - {e}") + stats['failed'] += 1 + + logger.info(f"清理完成 - 删除: {stats['deleted']}, 失败: {stats['failed']}, 跳过: {stats['skipped']}") + logger.info(f"释放空间: {stats['freed_bytes'] / 1024 / 1024:.2f} MB") + + return stats + + +def main(): + parser = argparse.ArgumentParser(description="清理任务") + parser.add_argument("--results_dir", type=str, default="/app/backend/results", help="扫描结果目录") + parser.add_argument("--retention_days", type=int, default=7, help="保留天数") + + args = parser.parse_args() + + stats = cleanup_results(args.results_dir, args.retention_days) + + print(f"清理完成: {stats}") + + +if __name__ == "__main__": + main() diff --git a/backend/apps/scan/scripts/run_delete_scans.py b/backend/apps/scan/scripts/run_delete_scans.py new file mode 100644 index 00000000..5228d08d --- /dev/null +++ b/backend/apps/scan/scripts/run_delete_scans.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +""" +扫描硬删除脚本 + +用于动态容器执行,硬删除已软删除的扫描及其关联数据。 +""" +import sys +import argparse +import json +import logging +from apps.common.container_bootstrap import fetch_config_and_setup_django + +logging.basicConfig( + level=logging.INFO, + format='[%(asctime)s] [%(levelname)s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) +logger = logging.getLogger(__name__) + + +def hard_delete_scans(scan_ids: list[int]) -> dict: + """ + 硬删除扫描 + + Args: + scan_ids: 扫描 ID 列表 + + Returns: + 删除统计信息 + """ + from apps.scan.services import ScanService + + service = ScanService() + + try: + deleted_count, details = service.hard_delete_scans(scan_ids) + + logger.info(f"✓ 硬删除完成 - 删除数量: {deleted_count}") + logger.info(f" 详情: {details}") + + return { + 'success': True, + 'deleted_count': deleted_count, + 'details': details, + } + + except Exception as e: + logger.error(f"硬删除失败: {e}", exc_info=True) + return { + 'success': False, + 'error': str(e), + } + + +def main(): + parser = argparse.ArgumentParser(description="硬删除扫描") + parser.add_argument("--scan_ids", type=str, required=True, help="扫描 ID 列表 (JSON)") + + args = parser.parse_args() + + # 解析 scan_ids + scan_ids = json.loads(args.scan_ids) + + logger.info(f"开始硬删除 {len(scan_ids)} 个扫描") + + # 获取配置并初始化 Django + fetch_config_and_setup_django() + + # 执行删除 + result = hard_delete_scans(scan_ids) + + print(f"删除完成: {result}") + + if not result.get('success'): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/backend/apps/scan/scripts/run_initiate_scan.py b/backend/apps/scan/scripts/run_initiate_scan.py new file mode 100644 index 00000000..67a52aac --- /dev/null +++ b/backend/apps/scan/scripts/run_initiate_scan.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +""" +扫描任务启动脚本 + +用于动态扫描容器启动时执行。 +必须在 Django 导入之前获取配置并设置环境变量。 +""" +import argparse +from apps.common.container_bootstrap import fetch_config_and_setup_django + + +def main(): + # 1. 从配置中心获取配置并初始化 Django(必须在 Django 导入之前) + fetch_config_and_setup_django() + + # 2. 解析命令行参数 + parser = argparse.ArgumentParser(description="执行扫描初始化 Flow") + parser.add_argument("--scan_id", type=int, required=True, help="扫描任务 ID") + parser.add_argument("--target_name", type=str, required=True, help="目标名称") + parser.add_argument("--target_id", type=int, required=True, help="目标 ID") + parser.add_argument("--scan_workspace_dir", type=str, required=True, help="扫描工作目录") + parser.add_argument("--engine_name", type=str, required=True, help="引擎名称") + parser.add_argument("--scheduled_scan_name", type=str, default=None, help="定时扫描任务名称(可选)") + + args = parser.parse_args() + + # 3. 现在可以安全导入 Django 相关模块 + from apps.scan.flows.initiate_scan_flow import initiate_scan_flow + + # 4. 执行 Flow + result = initiate_scan_flow( + scan_id=args.scan_id, + target_name=args.target_name, + target_id=args.target_id, + scan_workspace_dir=args.scan_workspace_dir, + engine_name=args.engine_name, + scheduled_scan_name=args.scheduled_scan_name, + ) + + print(f"Flow 执行完成: {result}") + + +if __name__ == "__main__": + main() diff --git a/backend/apps/scan/scripts/run_scheduled_scan.py b/backend/apps/scan/scripts/run_scheduled_scan.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/scan/serializers.py b/backend/apps/scan/serializers.py new file mode 100644 index 00000000..0f30c1c7 --- /dev/null +++ b/backend/apps/scan/serializers.py @@ -0,0 +1,245 @@ +from rest_framework import serializers +from django.db.models import Count + +from .models import Scan, ScheduledScan + + +class ScanSerializer(serializers.ModelSerializer): + """扫描任务序列化器""" + target_name = serializers.SerializerMethodField() + engine_name = serializers.SerializerMethodField() + + class Meta: + model = Scan + fields = [ + 'id', 'target', 'target_name', 'engine', 'engine_name', + 'created_at', 'stopped_at', 'status', 'results_dir', + 'container_ids', 'error_message' + ] + read_only_fields = [ + 'id', 'created_at', 'stopped_at', 'results_dir', + 'container_ids', 'error_message', 'status' + ] + + def get_target_name(self, obj): + """获取目标名称""" + return obj.target.name if obj.target else None + + def get_engine_name(self, obj): + """获取引擎名称""" + return obj.engine.name if obj.engine else None + + +class ScanHistorySerializer(serializers.ModelSerializer): + """扫描历史列表专用序列化器 + + 为前端扫描历史页面提供优化的数据格式,包括: + - 扫描汇总统计(子域名、端点、漏洞数量) + - 进度百分比和当前阶段 + """ + + # 字段映射 + target_name = serializers.CharField(source='target.name', read_only=True) + engine_name = serializers.CharField(source='engine.name', read_only=True) + + # 计算字段 + summary = serializers.SerializerMethodField() + + # 进度跟踪字段(直接从模型读取) + progress = serializers.IntegerField(read_only=True) + current_stage = serializers.CharField(read_only=True) + stage_progress = serializers.JSONField(read_only=True) + + class Meta: + model = Scan + fields = [ + 'id', 'target', 'target_name', 'engine', 'engine_name', + 'created_at', 'status', 'summary', 'progress', + 'current_stage', 'stage_progress' + ] + + def get_summary(self, obj): + """获取扫描汇总数据。 + + 设计原则: + - 子域名/网站/端点/IP/目录使用缓存字段(避免实时 COUNT) + - 漏洞统计使用 Scan 上的缓存字段,在扫描结束时统一聚合 + """ + # 1. 使用缓存字段构建基础统计(子域名、网站、端点、IP、目录) + summary = { + 'subdomains': obj.cached_subdomains_count or 0, + 'websites': obj.cached_websites_count or 0, + 'endpoints': obj.cached_endpoints_count or 0, + 'ips': obj.cached_ips_count or 0, + 'directories': obj.cached_directories_count or 0, + } + + # 2. 使用 Scan 模型上的缓存漏洞统计(按严重性聚合) + summary['vulnerabilities'] = { + 'total': obj.cached_vulns_total or 0, + 'critical': obj.cached_vulns_critical or 0, + 'high': obj.cached_vulns_high or 0, + 'medium': obj.cached_vulns_medium or 0, + 'low': obj.cached_vulns_low or 0, + } + + return summary + + +class QuickScanSerializer(serializers.Serializer): + """ + 快速扫描序列化器 + + 功能: + - 接收目标列表和引擎配置 + - 自动创建/获取目标 + - 立即发起扫描 + """ + + # 批量创建的最大数量限制 + MAX_BATCH_SIZE = 1000 + + # 目标列表 + targets = serializers.ListField( + child=serializers.DictField(), + help_text='目标列表,每个目标包含 name 字段' + ) + + # 扫描引擎 ID + engine_id = serializers.IntegerField( + required=True, + help_text='使用的扫描引擎 ID (必填)' + ) + + def validate_targets(self, value): + """验证目标列表""" + if not value: + raise serializers.ValidationError("目标列表不能为空") + + # 检查数量限制,防止服务器过载 + if len(value) > self.MAX_BATCH_SIZE: + raise serializers.ValidationError( + f"快速扫描最多支持 {self.MAX_BATCH_SIZE} 个目标,当前提交了 {len(value)} 个" + ) + + # 验证每个目标的必填字段 + for idx, target in enumerate(value): + if 'name' not in target: + raise serializers.ValidationError(f"第 {idx + 1} 个目标缺少 name 字段") + if not target['name']: + raise serializers.ValidationError(f"第 {idx + 1} 个目标的 name 不能为空") + + return value + + +# ==================== 定时扫描序列化器 ==================== + +class ScheduledScanSerializer(serializers.ModelSerializer): + """定时扫描任务序列化器(用于列表和详情)""" + + # 关联字段 + engine_name = serializers.CharField(source='engine.name', read_only=True) + organization_id = serializers.IntegerField(source='organization.id', read_only=True, allow_null=True) + organization_name = serializers.CharField(source='organization.name', read_only=True, allow_null=True) + target_id = serializers.IntegerField(source='target.id', read_only=True, allow_null=True) + target_name = serializers.CharField(source='target.name', read_only=True, allow_null=True) + scan_mode = serializers.SerializerMethodField() + + class Meta: + model = ScheduledScan + fields = [ + 'id', 'name', + 'engine', 'engine_name', + 'organization_id', 'organization_name', + 'target_id', 'target_name', + 'scan_mode', + 'cron_expression', + 'is_enabled', + 'run_count', 'last_run_time', 'next_run_time', + 'created_at', 'updated_at' + ] + read_only_fields = [ + 'id', 'run_count', + 'last_run_time', 'next_run_time', + 'created_at', 'updated_at' + ] + + def get_scan_mode(self, obj): + """获取扫描模式:organization 或 target""" + return 'organization' if obj.organization_id else 'target' + + +class CreateScheduledScanSerializer(serializers.Serializer): + """创建定时扫描任务序列化器 + + 扫描模式(二选一): + - 组织扫描:提供 organization_id,执行时动态获取组织下所有目标 + - 目标扫描:提供 target_id,扫描单个目标 + """ + + name = serializers.CharField(max_length=200, help_text='任务名称') + engine_id = serializers.IntegerField(help_text='扫描引擎 ID') + + # 组织扫描模式 + organization_id = serializers.IntegerField( + required=False, + allow_null=True, + help_text='组织 ID(组织扫描模式:执行时动态获取组织下所有目标)' + ) + + # 目标扫描模式 + target_id = serializers.IntegerField( + required=False, + allow_null=True, + help_text='目标 ID(目标扫描模式:扫描单个目标)' + ) + + cron_expression = serializers.CharField( + max_length=100, + default='0 2 * * *', + help_text='Cron 表达式,格式:分 时 日 月 周' + ) + is_enabled = serializers.BooleanField(default=True, help_text='是否立即启用') + + def validate(self, data): + """验证 organization_id 和 target_id 互斥""" + organization_id = data.get('organization_id') + target_id = data.get('target_id') + + if not organization_id and not target_id: + raise serializers.ValidationError('必须提供 organization_id 或 target_id 其中之一') + + if organization_id and target_id: + raise serializers.ValidationError('organization_id 和 target_id 只能提供其中之一') + + return data + + +class UpdateScheduledScanSerializer(serializers.Serializer): + """更新定时扫描任务序列化器""" + + name = serializers.CharField(max_length=200, required=False, help_text='任务名称') + engine_id = serializers.IntegerField(required=False, help_text='扫描引擎 ID') + + # 组织扫描模式 + organization_id = serializers.IntegerField( + required=False, + allow_null=True, + help_text='组织 ID(设置后清空 target_id)' + ) + + # 目标扫描模式 + target_id = serializers.IntegerField( + required=False, + allow_null=True, + help_text='目标 ID(设置后清空 organization_id)' + ) + + cron_expression = serializers.CharField(max_length=100, required=False, help_text='Cron 表达式') + is_enabled = serializers.BooleanField(required=False, help_text='是否启用') + + +class ToggleScheduledScanSerializer(serializers.Serializer): + """切换定时扫描启用状态序列化器""" + + is_enabled = serializers.BooleanField(help_text='是否启用') diff --git a/backend/apps/scan/services/__init__.py b/backend/apps/scan/services/__init__.py new file mode 100644 index 00000000..1d02c1d8 --- /dev/null +++ b/backend/apps/scan/services/__init__.py @@ -0,0 +1,29 @@ +""" +扫描服务模块 + +提供各种扫描任务的服务功能 + +架构: +- ScanService: 主服务(协调者) +- ScanCreationService: 创建服务 +- ScanStateService: 状态管理服务 +- ScanControlService: 控制服务 +- ScanStatsService: 统计服务 +""" + +from .scan_service import ScanService +from .scan_creation_service import ScanCreationService +from .scan_state_service import ScanStateService +from .scan_control_service import ScanControlService +from .scan_stats_service import ScanStatsService +from .scheduled_scan_service import ScheduledScanService + +__all__ = [ + 'ScanService', # 主入口(向后兼容) + 'ScanCreationService', + 'ScanStateService', + 'ScanControlService', + 'ScanStatsService', + 'ScheduledScanService', +] + diff --git a/backend/apps/scan/services/scan_control_service.py b/backend/apps/scan/services/scan_control_service.py new file mode 100644 index 00000000..a2f19d89 --- /dev/null +++ b/backend/apps/scan/services/scan_control_service.py @@ -0,0 +1,291 @@ +""" +扫描控制服务 + +职责: +- 停止扫描(docker kill 强制杀死) +- 删除扫描(两阶段删除) +""" + +import logging +import threading +from typing import Dict, List +from django.db import transaction, connection +from django.db.utils import DatabaseError, OperationalError +from django.core.exceptions import ObjectDoesNotExist +from django.utils import timezone + +from apps.common.definitions import ScanStatus +from apps.scan.repositories import DjangoScanRepository + +logger = logging.getLogger(__name__) + + +class ScanControlService: + """ + 扫描控制服务 + + 职责: + - 停止扫描(取消 Flow Run) + - 删除扫描(两阶段删除) + - 批量操作 + """ + + def __init__(self): + """ + 初始化服务 + """ + self.scan_repo = DjangoScanRepository() + + def _stop_containers( + self, + container_ids: List[str], + worker_id: int, + ) -> int: + """ + 在指定 Worker 上停止 Docker 容器 + + Args: + container_ids: 容器 ID 列表 + worker_id: Worker 节点 ID + + Returns: + 成功停止的数量 + """ + if not container_ids: + return 0 + + from apps.engine.models import WorkerNode + + try: + worker = WorkerNode.objects.get(id=worker_id) + except WorkerNode.DoesNotExist: + logger.error(f"Worker 不存在: {worker_id}") + return 0 + + # 构建 docker kill 命令(强制杀死,避免进程不响应 SIGTERM) + container_ids_str = ' '.join(container_ids) + docker_cmd = f"docker kill {container_ids_str} 2>/dev/null || true" + + stopped_count = 0 + + if worker.is_local: + # 本地执行 + import subprocess + try: + result = subprocess.run( + docker_cmd, + shell=True, + capture_output=True, + text=True, + timeout=30 + ) + # 统计成功停止的容器数(输出的每一行是一个成功停止的容器 ID) + if result.stdout: + stopped_count = len(result.stdout.strip().split('\n')) + logger.info(f"本地 docker kill 完成: {stopped_count}/{len(container_ids)}") + except Exception as e: + logger.error(f"本地 docker kill 失败: {e}") + else: + # 远程通过 SSH 执行 + import paramiko + ssh = None + try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect( + hostname=worker.ip_address, + port=worker.ssh_port, + username=worker.username, + password=worker.password if worker.password else None, + timeout=10, + ) + + stdin, stdout, stderr = ssh.exec_command(docker_cmd, timeout=30) + output = stdout.read().decode().strip() + if output: + stopped_count = len(output.split('\n')) + logger.info(f"SSH docker kill 完成 - Worker: {worker.name}, 数量: {stopped_count}/{len(container_ids)}") + except Exception as e: + logger.error(f"SSH docker kill 失败 - Worker: {worker.name}: {e}") + finally: + if ssh: + ssh.close() + + return stopped_count + + def delete_scans_two_phase(self, scan_ids: List[int]) -> dict: + """ + 两阶段删除扫描任务 + + 流程: + 1. 软删除:立即更新 deleted_at 字段(同步,快速) + 2. 后台异步:停止容器 + 分发硬删除任务(不阻塞 API) + + Args: + scan_ids: 扫描任务 ID 列表 + + Returns: + 删除结果统计 + """ + # 1. 获取要删除的 Scan 信息 + scans = list(self.scan_repo.get_all(prefetch_relations=False).filter(id__in=scan_ids)) + if not scans: + raise ValueError("未找到要删除的 Scan") + + scan_names = [f"Scan #{s.id}" for s in scans] + existing_ids = [s.id for s in scans] + + # 2. 收集需要停止的容器信息(同步收集,异步执行) + containers_by_worker: Dict[int, List[str]] = {} + for scan in scans: + if scan.status in [ScanStatus.RUNNING, ScanStatus.INITIATED]: + if scan.container_ids and scan.worker_id: + if scan.worker_id not in containers_by_worker: + containers_by_worker[scan.worker_id] = [] + containers_by_worker[scan.worker_id].extend(scan.container_ids) + + # 3. 第一阶段:软删除(同步,快速) + soft_count = self.scan_repo.soft_delete_by_ids(existing_ids) + logger.info(f"✓ 软删除完成: {soft_count} 个 Scan") + + # 4. 第二阶段:后台异步执行停止容器 + 硬删除(不阻塞 API) + thread = threading.Thread( + target=self._async_cleanup_and_hard_delete, + args=(existing_ids, containers_by_worker), + daemon=True, + ) + thread.start() + + return { + 'soft_deleted_count': soft_count, + 'scan_names': scan_names, + 'hard_delete_scheduled': True, + } + + def _async_cleanup_and_hard_delete( + self, + scan_ids: List[int], + containers_by_worker: Dict[int, List[str]] + ): + """ + 后台线程:停止容器 + 分发硬删除任务 + """ + # 后台线程需要新的数据库连接 + connection.close() + + # 1. 停止容器 + if containers_by_worker: + total_containers = sum(len(c) for c in containers_by_worker.values()) + logger.info(f"🛑 后台停止容器 - Worker 数量: {len(containers_by_worker)}, 容器数量: {total_containers}") + stopped_count = 0 + for worker_id, container_ids in containers_by_worker.items(): + try: + count = self._stop_containers(container_ids, worker_id) + stopped_count += count + except Exception as e: + logger.warning(f"停止容器时出错 - Worker ID {worker_id}: {e}") + logger.info(f"✓ 已停止 {stopped_count}/{total_containers} 个容器") + + # 2. 分发硬删除任务 + try: + from apps.engine.services.task_distributor import get_task_distributor + + distributor = get_task_distributor() + success, message, container_id = distributor.execute_delete_task( + task_type='scans', + ids=scan_ids + ) + + if success: + logger.info(f"✓ 硬删除任务已分发 - Container: {container_id}") + else: + logger.warning(f"硬删除任务分发失败: {message}") + + except Exception as e: + logger.error(f"❌ 分发删除任务失败: {e}", exc_info=True) + + def stop_scan(self, scan_id: int) -> tuple[bool, int]: + """ + 主动停止扫描任务(用户发起) + + 工作流程: + 1. 验证扫描状态(只能停止 RUNNING/INITIATED) + 2. 通过 docker kill 强制终止容器 + 3. 立即更新状态为 CANCELLED(终态) + + Args: + scan_id: 扫描任务 ID + + Returns: + (是否成功, 停止的容器数量) + + 并发安全: + 使用数据库行锁(select_for_update)防止并发修改, + 避免用户重复点击导致的重复操作 + """ + try: + # 1. 在事务内获取扫描对象、检查状态、更新状态(加锁,防止并发) + with transaction.atomic(): + # 使用 select_for_update() 加行锁,防止并发修改 + scan = self.scan_repo.get_by_id_for_update(scan_id) + if not scan: + logger.error("Scan 不存在 - Scan ID: %s", scan_id) + return False, 0 + + # 2. 验证状态(只能停止 RUNNING/INITIATED) + if scan.status not in [ScanStatus.RUNNING, ScanStatus.INITIATED]: + logger.warning( + "无法停止扫描:当前状态为 %s - Scan ID: %s", + ScanStatus(scan.status).label, + scan_id + ) + return False, 0 + + # 3. 获取容器 ID 列表和 Worker ID(在锁内读取,确保数据一致性) + container_ids = scan.container_ids or [] + worker_id = scan.worker_id + + # 4. 立即更新状态为 CANCELLED(终态) + scan.status = ScanStatus.CANCELLED + scan.stopped_at = timezone.now() + scan.error_message = "用户手动取消扫描" + scan.save(update_fields=['status', 'stopped_at', 'error_message']) + logger.info("✓ 已更新状态为 CANCELLED(事务内)- Scan ID: %s", scan_id) + + # 5. 更新阶段进度:running → cancelled, pending → cancelled + from apps.scan.services.scan_state_service import ScanStateService + state_service = ScanStateService() + state_service.cancel_running_stages(scan_id, final_status="cancelled") + + # 事务结束,锁释放 + # 后续耗时操作在事务外执行,避免长时间持有锁 + + # 6. 停止 Docker 容器(通过 SSH/本地执行 docker stop) + stopped_count = 0 + if container_ids and worker_id: + try: + stopped_count = self._stop_containers(container_ids, worker_id) + logger.info( + "✓ 已停止 %d/%d 个容器 - Scan ID: %s", + stopped_count, len(container_ids), scan_id + ) + except Exception as e: + logger.error("停止容器失败: %s", e) + # 容器停止失败不影响取消结果,状态已经更新为 CANCELLED + elif not worker_id: + logger.warning("无 Worker 信息,跳过容器停止 - Scan ID: %s", scan_id) + else: + logger.info("无关联容器需要停止 - Scan ID: %s", scan_id) + + return True, stopped_count + + except (DatabaseError, OperationalError) as e: + logger.exception("数据库错误:停止扫描失败 - Scan ID: %s", scan_id) + raise + except ObjectDoesNotExist: + logger.error("Scan 不存在 - Scan ID: %s", scan_id) + return False, 0 + + +# 导出接口 +__all__ = ['ScanControlService'] diff --git a/backend/apps/scan/services/scan_creation_service.py b/backend/apps/scan/services/scan_creation_service.py new file mode 100644 index 00000000..55ce395f --- /dev/null +++ b/backend/apps/scan/services/scan_creation_service.py @@ -0,0 +1,312 @@ +""" +扫描创建服务 + +职责: +- 准备扫描参数 +- 创建 Scan 记录 +- 通过负载感知分发器在最优 Worker 上执行任务(支持本地和远程) +""" + +import uuid +import logging +import threading +from typing import List +from datetime import datetime +from pathlib import Path +from django.conf import settings +from django.db import transaction, connection +from django.db.utils import DatabaseError, IntegrityError, OperationalError +from django.core.exceptions import ValidationError, ObjectDoesNotExist + +from apps.scan.models import Scan +from apps.scan.repositories import DjangoScanRepository +from apps.targets.repositories import DjangoTargetRepository, DjangoOrganizationRepository +from apps.engine.repositories import DjangoEngineRepository +from apps.targets.models import Target +from apps.engine.models import ScanEngine +from apps.common.definitions import ScanStatus +from apps.engine.services.task_distributor import get_task_distributor + +logger = logging.getLogger(__name__) + + +class ScanCreationService: + """ + 扫描创建服务 + + 职责: + - 准备扫描参数 + - 创建 Scan 记录 + - 通过负载感知分发器在最优 Worker 上执行任务 + - 处理创建过程中的错误 + """ + + def __init__(self): + """ + 初始化服务 + Note: + 移除了依赖注入,因为: + 1. 项目没有单元测试需求 + 2. 不会更换数据库实现 + 3. 所有调用都是直接实例化 + 4. 减少不必要的复杂度 + """ + self.scan_repo = DjangoScanRepository() + self.target_repo = DjangoTargetRepository() + self.organization_repo = DjangoOrganizationRepository() + self.engine_repo = DjangoEngineRepository() + + def prepare_initiate_scan( + self, + organization_id: int | None = None, + target_id: int | None = None, + engine_id: int | None = None + ) -> tuple[List[Target], ScanEngine]: + """ + 准备扫描任务所需的数据 + + 职责: + 1. 参数验证(必填项、互斥参数) + 2. 资源查询(Engine、Organization、Target) + 3. 业务逻辑判断(组织下是否有目标) + 4. 返回准备好的目标列表和扫描引擎 + + Args: + organization_id: 组织ID(可选) + target_id: 目标ID(可选) + engine_id: 扫描引擎ID(必填) + + Returns: + (目标列表, 扫描引擎对象) - 供 create_scans 方法使用 + + Raises: + ValidationError: 参数验证失败或业务规则不满足 + ObjectDoesNotExist: 资源不存在(Organization/Target/ScanEngine) + + Note: + - organization_id 和 target_id 必须二选一 + - 如果提供 organization_id,返回该组织下所有目标 + - 如果提供 target_id,返回单个目标列表 + """ + # 1. 参数验证 + if not engine_id: + raise ValidationError('缺少必填参数: engine_id') + + if not organization_id and not target_id: + raise ValidationError('必须提供 organization_id 或 target_id 其中之一') + + if organization_id and target_id: + raise ValidationError('organization_id 和 target_id 只能提供其中之一') + + # 2. 查询扫描引擎(通过 Repository 层) + engine = self.engine_repo.get_by_id(engine_id) + if not engine: + logger.error("扫描引擎不存在 - Engine ID: %s", engine_id) + raise ObjectDoesNotExist(f'ScanEngine ID {engine_id} 不存在') + + # 3. 根据参数获取目标列表 + targets = [] + + if organization_id: + # 根据组织ID获取所有目标(通过 Repository 层) + organization = self.organization_repo.get_by_id(organization_id) + if not organization: + logger.error("组织不存在 - Organization ID: %s", organization_id) + raise ObjectDoesNotExist(f'Organization ID {organization_id} 不存在') + + targets = self.organization_repo.get_targets(organization_id) + + if not targets: + raise ValidationError(f'组织 ID {organization_id} 下没有目标') + + logger.debug( + "准备发起扫描 - 组织: %s, 目标数量: %d, 引擎: %s", + organization.name, + len(targets), + engine.name + ) + else: + # 根据目标ID获取单个目标(通过 Repository 层) + target = self.target_repo.get_by_id(target_id) + if not target: + logger.error("目标不存在 - Target ID: %s", target_id) + raise ObjectDoesNotExist(f'Target ID {target_id} 不存在') + + targets = [target] + + logger.debug( + "准备发起扫描 - 目标: %s, 引擎: %s", + target.name, + engine.name + ) + + return targets, engine + + def _generate_scan_workspace_dir(self) -> str: + """ + 生成 Scan 工作空间目录路径 + + 职责: + - 生成唯一的 Scan 级别工作空间目录路径字符串 + - 不创建实际目录(由 Flow 层负责) + + Returns: + Scan 工作空间目录路径字符串 + + 格式:{SCAN_RESULTS_DIR}/scan_{timestamp}_{uuid8}/ + 示例:/data/scans/scan_20231104_152030_a3f2b7e9/ + + Raises: + ValueError: 如果 SCAN_RESULTS_DIR 未设置或为空 + + Note: + 使用秒级时间戳 + 8 位 UUID 确保路径唯一性 + 冲突概率:同一秒内创建 1000 个扫描,冲突概率 < 0.01% + """ + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + unique_id = uuid.uuid4().hex[:8] # 8 位十六进制UUID (4,294,967,296 种可能) + + # 从 settings 获取,保持配置统一 + base_dir = getattr(settings, 'SCAN_RESULTS_DIR', None) + if not base_dir: + error_msg = "SCAN_RESULTS_DIR 未设置,无法创建扫描工作空间" + logger.error(error_msg) + raise ValueError(error_msg) + + scan_workspace_dir = str(Path(base_dir) / f"scan_{timestamp}_{unique_id}") + return scan_workspace_dir + + def create_scans( + self, + targets: List[Target], + engine: ScanEngine, + scheduled_scan_name: str | None = None + ) -> List[Scan]: + """ + 为多个目标批量创建扫描任务,后台异步分发到 Worker + + Args: + targets: 目标列表 + engine: 扫描引擎对象 + scheduled_scan_name: 定时扫描任务名称(可选,用于通知显示) + + Returns: + 创建的 Scan 对象列表(立即返回,不等待分发完成) + + 流程: + 1. 同步:批量创建 Scan 记录(快速) + 2. 异步:后台线程通过 TaskDistributor 分发任务到 Workers + """ + # 第一步:准备批量创建的数据 + scans_to_create = [] + + for target in targets: + try: + scan_workspace_dir = self._generate_scan_workspace_dir() + scan = Scan( + target=target, + engine=engine, + results_dir=scan_workspace_dir, + status=ScanStatus.INITIATED, + container_ids=[], + ) + scans_to_create.append(scan) + except (ValidationError, ValueError) as e: + logger.error( + "准备扫描任务数据失败 - Target: %s, 错误: %s", + target.name, e + ) + continue + + if not scans_to_create: + logger.warning("没有需要创建的扫描任务") + return [] + + # 第二步:使用事务批量创建(同步,快速) + created_scans = [] + try: + with transaction.atomic(): + created_scans = self.scan_repo.bulk_create(scans_to_create) + logger.info("批量创建扫描记录成功 - 数量: %d", len(created_scans)) + except (DatabaseError, IntegrityError) as e: + logger.exception("数据库错误:批量创建扫描记录失败 - 错误: %s", e) + return [] + except ValidationError as e: + logger.error("验证错误:扫描任务数据无效 - 错误: %s", e) + return [] + + # 第三步:分发任务到 Workers + scan_data = [ + { + 'scan_id': scan.id, + 'target_name': scan.target.name, + 'target_id': scan.target.id, + 'results_dir': scan.results_dir, + 'engine_name': scan.engine.name, + 'scheduled_scan_name': scheduled_scan_name, + } + for scan in created_scans + ] + + # 后台线程异步分发(不阻塞 API/调度器) + thread = threading.Thread( + target=self._distribute_scans_to_workers, + args=(scan_data,), + daemon=True, + ) + thread.start() + logger.info("扫描任务已创建,后台分发中 - 数量: %d", len(created_scans)) + + return created_scans + + def _distribute_scans_to_workers(self, scan_data: List[dict]): + """ + 后台线程:分发扫描任务到 Workers + + Args: + scan_data: 扫描任务数据列表 + """ + # 后台线程需要新的数据库连接 + connection.close() + + distributor = get_task_distributor() + scan_repo = DjangoScanRepository() + + for data in scan_data: + scan_id = data['scan_id'] + try: + success, message, container_id, worker_id = distributor.execute_scan_flow( + scan_id=scan_id, + target_name=data['target_name'], + target_id=data['target_id'], + scan_workspace_dir=data['results_dir'], + engine_name=data['engine_name'], + scheduled_scan_name=data.get('scheduled_scan_name'), + ) + + if success: + if container_id: + scan_repo.append_container_id(scan_id, container_id) + if worker_id: + scan_repo.update_worker(scan_id, worker_id) + logger.info( + "✓ 扫描任务已提交 - Scan ID: %s, Worker: %s", + scan_id, worker_id + ) + else: + raise Exception(message) + + except Exception as e: + logger.error("提交扫描任务失败 - Scan ID: %s, 错误: %s", scan_id, e) + try: + scan_repo.update_status( + scan_id, + ScanStatus.FAILED, + error_message=f'提交任务失败: {e}', + ) + except (DatabaseError, OperationalError) as save_error: + logger.error("更新状态失败 - Scan ID: %s, 错误: %s", scan_id, save_error) + + +# 导出接口 +__all__ = ['ScanCreationService'] diff --git a/backend/apps/scan/services/scan_service.py b/backend/apps/scan/services/scan_service.py new file mode 100644 index 00000000..27885a4d --- /dev/null +++ b/backend/apps/scan/services/scan_service.py @@ -0,0 +1,179 @@ +""" +扫描任务服务 + +负责 Scan 模型的所有业务逻辑 +""" + +from __future__ import annotations + +import logging +import uuid +from typing import Dict, List, TYPE_CHECKING +from datetime import datetime +from pathlib import Path +from django.conf import settings +from django.db import transaction +from django.db.utils import DatabaseError, IntegrityError, OperationalError +from django.core.exceptions import ValidationError, ObjectDoesNotExist + +from apps.scan.models import Scan +from apps.scan.repositories import DjangoScanRepository +from apps.targets.repositories import DjangoTargetRepository, DjangoOrganizationRepository +from apps.engine.repositories import DjangoEngineRepository +from apps.targets.models import Target +from apps.engine.models import ScanEngine +from apps.common.definitions import ScanStatus + +logger = logging.getLogger(__name__) + + +class ScanService: + """ + 扫描任务服务(协调者) + + 职责: + - 协调各个子服务 + - 提供统一的公共接口 + - 保持向后兼容 + + 注意: + - 具体业务逻辑已拆分到子服务 + - 本类主要负责委托和协调 + """ + + # 终态集合:这些状态一旦设置,不应该被覆盖 + FINAL_STATUSES = { + ScanStatus.COMPLETED, + ScanStatus.FAILED, + ScanStatus.CANCELLED + } + + def __init__(self): + """ + 初始化服务 + """ + # 初始化子服务 + from apps.scan.services.scan_creation_service import ScanCreationService + from apps.scan.services.scan_state_service import ScanStateService + from apps.scan.services.scan_control_service import ScanControlService + from apps.scan.services.scan_stats_service import ScanStatsService + + self.creation_service = ScanCreationService() + self.state_service = ScanStateService() + self.control_service = ScanControlService() + self.stats_service = ScanStatsService() + + # 保留 ScanRepository(用于 get_scan 方法) + self.scan_repo = DjangoScanRepository() + + def get_scan(self, scan_id: int, prefetch_relations: bool) -> Scan | None: + """ + 获取扫描任务(包含关联对象) + + 自动预加载 engine 和 target,避免 N+1 查询问题 + + Args: + scan_id: 扫描任务 ID + + Returns: + Scan 对象(包含 engine 和 target)或 None + """ + return self.scan_repo.get_by_id(scan_id, prefetch_relations) + + def get_all_scans(self, prefetch_relations: bool = True): + return self.scan_repo.get_all(prefetch_relations=prefetch_relations) + + def prepare_initiate_scan( + self, + organization_id: int | None = None, + target_id: int | None = None, + engine_id: int | None = None + ) -> tuple[List[Target], ScanEngine]: + """ + 为创建扫描任务做准备,返回所需的目标列表和扫描引擎 + """ + return self.creation_service.prepare_initiate_scan( + organization_id, target_id, engine_id + ) + + def create_scans( + self, + targets: List[Target], + engine: ScanEngine, + scheduled_scan_name: str | None = None + ) -> List[Scan]: + """批量创建扫描任务(委托给 ScanCreationService)""" + return self.creation_service.create_scans(targets, engine, scheduled_scan_name) + + # ==================== 状态管理方法(委托给 ScanStateService) ==================== + + def update_status( + self, + scan_id: int, + status: ScanStatus, + error_message: str | None = None, + stopped_at: datetime | None = None + ) -> bool: + """更新 Scan 状态(委托给 ScanStateService)""" + return self.state_service.update_status( + scan_id, status, error_message, stopped_at + ) + + def update_status_if_match( + self, + scan_id: int, + current_status: ScanStatus, + new_status: ScanStatus, + stopped_at: datetime | None = None + ) -> bool: + """条件更新 Scan 状态(委托给 ScanStateService)""" + return self.state_service.update_status_if_match( + scan_id, current_status, new_status, stopped_at + ) + + def update_cached_stats(self, scan_id: int) -> dict | None: + """更新缓存统计数据(委托给 ScanStateService),返回统计数据字典""" + return self.state_service.update_cached_stats(scan_id) + + # ==================== 进度跟踪方法(委托给 ScanStateService) ==================== + + def init_stage_progress(self, scan_id: int, stages: list[str]) -> bool: + """初始化阶段进度(委托给 ScanStateService)""" + return self.state_service.init_stage_progress(scan_id, stages) + + def start_stage(self, scan_id: int, stage: str) -> bool: + """开始执行某个阶段(委托给 ScanStateService)""" + return self.state_service.start_stage(scan_id, stage) + + def complete_stage(self, scan_id: int, stage: str, detail: str | None = None) -> bool: + """完成某个阶段(委托给 ScanStateService)""" + return self.state_service.complete_stage(scan_id, stage, detail) + + def fail_stage(self, scan_id: int, stage: str, error: str | None = None) -> bool: + """标记某个阶段失败(委托给 ScanStateService)""" + return self.state_service.fail_stage(scan_id, stage, error) + + def cancel_running_stages(self, scan_id: int, final_status: str = "cancelled") -> bool: + """取消所有正在运行的阶段(委托给 ScanStateService)""" + return self.state_service.cancel_running_stages(scan_id, final_status) + + # ==================== 删除和控制方法(委托给 ScanControlService) ==================== + + def delete_scans_two_phase(self, scan_ids: List[int]) -> dict: + """两阶段删除扫描任务(委托给 ScanControlService)""" + return self.control_service.delete_scans_two_phase(scan_ids) + + def stop_scan(self, scan_id: int) -> tuple[bool, int]: + """停止扫描任务(委托给 ScanControlService)""" + return self.control_service.stop_scan(scan_id) + + # ==================== 统计方法(委托给 ScanStatsService) ==================== + + def get_statistics(self) -> dict: + """获取扫描统计数据(委托给 ScanStatsService)""" + return self.stats_service.get_statistics() + + + +# 导出接口 +__all__ = ['ScanService'] diff --git a/backend/apps/scan/services/scan_state_service.py b/backend/apps/scan/services/scan_state_service.py new file mode 100644 index 00000000..b73e76cf --- /dev/null +++ b/backend/apps/scan/services/scan_state_service.py @@ -0,0 +1,445 @@ +""" +扫描状态管理服务 + +职责: +- 更新扫描状态 +- 条件状态更新(乐观锁) +- 更新缓存统计数据 +""" + +import logging +from datetime import datetime +from django.db.utils import DatabaseError, OperationalError +from django.core.exceptions import ObjectDoesNotExist + +from apps.common.definitions import ScanStatus +from apps.scan.repositories import DjangoScanRepository + +logger = logging.getLogger(__name__) + + +class ScanStateService: + """ + 扫描状态管理服务 + + 职责: + - 更新扫描状态 + - 条件状态更新(乐观锁) + - 更新缓存统计数据 + - 状态验证 + """ + + def __init__(self): + """ + 初始化服务 + """ + self.repo = DjangoScanRepository() + + def update_status( + self, + scan_id: int, + status: ScanStatus, + error_message: str | None = None, + stopped_at: datetime | None = None + ) -> bool: + """ + 更新 Scan 状态 + + Args: + scan_id: 扫描任务 ID + status: 新状态 + error_message: 错误消息(可选) + stopped_at: 结束时间(可选) + + Returns: + 是否更新成功 + + Note: + created_at 是自动设置的,不需要手动传递 + """ + try: + result = self.repo.update_status( + scan_id, + status, + error_message, + stopped_at=stopped_at + ) + if result: + logger.debug( + "更新 Scan 状态成功 - Scan ID: %s, 状态: %s", + scan_id, + ScanStatus(status).label + ) + return result + except (DatabaseError, OperationalError) as e: + logger.exception("数据库错误:更新 Scan 状态失败 - Scan ID: %s", scan_id) + raise # 数据库错误应该向上传播 + except ObjectDoesNotExist: + logger.error("Scan 不存在 - Scan ID: %s", scan_id) + return False + + def update_status_if_match( + self, + scan_id: int, + current_status: ScanStatus, + new_status: ScanStatus, + stopped_at: datetime | None = None + ) -> bool: + """ + 条件更新 Scan 状态(原子操作) + + 仅当扫描状态匹配 current_status 时才更新为 new_status。 + 这是一个原子操作,用于处理并发场景下的状态更新。 + + Args: + scan_id: 扫描任务 ID + current_status: 当前期望的状态 + new_status: 要更新到的新状态 + stopped_at: 结束时间(可选) + + Returns: + 是否更新成功(True=更新了记录,False=未更新或状态不匹配) + + Note: + 此方法通过 Repository 层执行原子操作,适用于需要条件更新的场景 + """ + try: + result = self.repo.update_status_if_match( + scan_id=scan_id, + current_status=current_status, + new_status=new_status, + stopped_at=stopped_at + ) + if result: + logger.debug( + "条件更新 Scan 状态成功 - Scan ID: %s, %s → %s", + scan_id, + current_status.value, + new_status.value + ) + return result + except (DatabaseError, OperationalError) as e: + logger.exception( + "数据库错误:条件更新 Scan 状态失败 - Scan ID: %s", + scan_id + ) + raise + except Exception as e: + logger.error( + "条件更新 Scan 状态失败 - Scan ID: %s, 错误: %s", + scan_id, + e + ) + return False + + def update_cached_stats(self, scan_id: int) -> dict | None: + """ + 更新扫描任务的缓存统计数据 + + 使用 Repository 层进行数据访问,符合分层架构规范 + + Args: + scan_id: 扫描任务 ID + + Returns: + 成功返回统计数据字典,失败返回 None + + Note: + 应该在扫描进入终态时调用,更新缓存的统计字段以提升查询性能 + """ + try: + # 通过 Repository 层更新统计数据 + result = self.repo.update_cached_stats(scan_id) + if result: + logger.debug("更新缓存统计数据成功 - Scan ID: %s", scan_id) + return result + except (DatabaseError, OperationalError) as e: + logger.exception("数据库错误:更新缓存统计数据失败 - Scan ID: %s", scan_id) + return None + except Exception as e: + logger.error("更新缓存统计数据失败 - Scan ID: %s, 错误: %s", scan_id, e) + return None + + def update_progress( + self, + scan_id: int, + progress: int | None = None, + current_stage: str | None = None, + stage_progress: dict | None = None + ) -> bool: + """ + 更新扫描进度信息 + + Args: + scan_id: 扫描任务 ID + progress: 进度百分比 0-100(可选) + current_stage: 当前阶段(可选) + stage_progress: 各阶段详情(可选) + + Returns: + 是否更新成功 + """ + try: + result = self.repo.update_progress( + scan_id, + progress=progress, + current_stage=current_stage, + stage_progress=stage_progress + ) + if result: + logger.debug( + "更新扫描进度成功 - Scan ID: %s, stage: %s", + scan_id, + current_stage + ) + return result + except (DatabaseError, OperationalError) as e: + logger.exception("数据库错误:更新扫描进度失败 - Scan ID: %s", scan_id) + return False + except Exception as e: + logger.error("更新扫描进度失败 - Scan ID: %s, 错误: %s", scan_id, e) + return False + + def init_stage_progress(self, scan_id: int, stages: list[str]) -> bool: + """ + 初始化阶段进度(所有阶段设为 pending) + + Args: + scan_id: 扫描任务 ID + stages: 阶段列表,如 ['subdomain_discovery', 'port_scan', ...] + 顺序与 engine_config 配置和 Flow 执行顺序一致 + + Returns: + 是否初始化成功 + """ + stage_progress = { + stage: {"status": "pending", "order": idx} + for idx, stage in enumerate(stages) + } + return self.update_progress( + scan_id, + progress=0, + stage_progress=stage_progress + ) + + def start_stage(self, scan_id: int, stage: str) -> bool: + """ + 开始执行某个阶段 + + Args: + scan_id: 扫描任务 ID + stage: 阶段名称 + + Returns: + 是否更新成功 + """ + from datetime import datetime + + # 从数据库获取当前进度状态 + scan = self.repo.get_by_id(scan_id) + if not scan: + logger.warning(f"start_stage: Scan not found - ID: {scan_id}") + return False + + stage_progress = scan.stage_progress or {} + + # 保留原有的 order 字段 + existing = stage_progress.get(stage, {}) + order = existing.get("order", 0) + + # 如果阶段已经是 cancelled 状态,不要启动 + if existing.get("status") == "cancelled": + logger.info(f"start_stage: 阶段已取消,跳过 - Scan ID: {scan_id}, Stage: {stage}") + return True + + stage_progress[stage] = { + "status": "running", + "order": order, + "started_at": datetime.now().isoformat() + } + + # 计算进度百分比 + total_stages = len(stage_progress) + completed = sum(1 for s in stage_progress.values() if s.get("status") == "completed") + progress = int((completed / total_stages) * 100) if total_stages > 0 else 0 + + return self.update_progress( + scan_id, + progress=progress, + current_stage=stage, + stage_progress=stage_progress + ) + + def complete_stage( + self, + scan_id: int, + stage: str, + detail: str | None = None + ) -> bool: + """ + 完成某个阶段 + + Args: + scan_id: 扫描任务 ID + stage: 阶段名称 + detail: 完成详情(可选) + + Returns: + 是否更新成功 + """ + from datetime import datetime + + # 从数据库获取当前进度状态 + scan = self.repo.get_by_id(scan_id) + if not scan: + logger.warning(f"complete_stage: Scan not found - ID: {scan_id}") + return False + + stage_progress = scan.stage_progress or {} + + existing = stage_progress.get(stage, {}) + order = existing.get("order", 0) + started_at = existing.get("started_at") + + # 如果阶段已经是 cancelled 状态,不要覆盖为 completed + if existing.get("status") == "cancelled": + logger.info(f"complete_stage: 阶段已取消,跳过 - Scan ID: {scan_id}, Stage: {stage}") + return True + + duration = 0 # 默认 0,避免 null + if started_at: + try: + start_time = datetime.fromisoformat(started_at) + duration = int((datetime.now() - start_time).total_seconds()) + except (ValueError, TypeError): + logger.warning(f"complete_stage: 无法解析 started_at - Stage: {stage}, Value: {started_at}") + else: + logger.error(f"complete_stage: started_at 缺失 - Scan ID: {scan_id}, Stage: {stage}") + + stage_progress[stage] = { + "status": "completed", + "order": order, + "duration": duration, + } + if detail: + stage_progress[stage]["detail"] = detail + + # 计算进度百分比 + total_stages = len(stage_progress) + completed = sum(1 for s in stage_progress.values() if s.get("status") == "completed") + progress = int((completed / total_stages) * 100) if total_stages > 0 else 0 + + # 如果全部完成,进度设为 100 + if completed == total_stages: + progress = 100 + + return self.update_progress( + scan_id, + progress=progress, + current_stage="" if completed == total_stages else stage, + stage_progress=stage_progress + ) + + def fail_stage( + self, + scan_id: int, + stage: str, + error: str | None = None + ) -> bool: + """ + 标记某个阶段失败 + + Args: + scan_id: 扫描任务 ID + stage: 阶段名称 + error: 错误信息(可选) + + Returns: + 是否更新成功 + """ + # 从数据库获取当前进度状态 + scan = self.repo.get_by_id(scan_id) + if not scan: + logger.warning(f"fail_stage: Scan not found - ID: {scan_id}") + return False + + stage_progress = scan.stage_progress or {} + + # 保留原有的 order 字段 + existing = stage_progress.get(stage, {}) + order = existing.get("order", 0) + + # 如果阶段已经是 cancelled 状态,不要覆盖为 failed + # (用户手动停止时会先标记为 cancelled,docker kill 后触发的 on_failed 不应覆盖) + if existing.get("status") == "cancelled": + logger.info(f"fail_stage: 阶段已取消,跳过 - Scan ID: {scan_id}, Stage: {stage}") + return True + + stage_progress[stage] = { + "status": "failed", + "order": order, + "error": error + } + + return self.update_progress( + scan_id, + current_stage=stage, + stage_progress=stage_progress + ) + + def cancel_running_stages(self, scan_id: int, final_status: str = "cancelled") -> bool: + """ + 标记所有未完成的阶段(扫描被取消时调用) + + 将所有 running 状态的阶段标记为 final_status, + 将所有 pending 状态的阶段标记为 skipped + + Args: + scan_id: 扫描任务 ID + final_status: running 阶段的最终状态 + + Returns: + 是否更新成功 + """ + try: + scan = self.repo.get_by_id(scan_id) + if not scan or not scan.stage_progress: + return False + + stage_progress = scan.stage_progress + updated = False + + for stage, info in stage_progress.items(): + status = info.get("status") + order = info.get("order", 0) + + if status == "running": + # 正在运行的阶段标记为 final_status + stage_progress[stage] = { + "status": final_status, + "order": order, + } + updated = True + elif status == "pending": + # 未开始的阶段统一标记为 cancelled + stage_progress[stage] = { + "status": "cancelled", + "order": order, + } + updated = True + + if updated: + self.update_progress( + scan_id, + current_stage="", + stage_progress=stage_progress + ) + + return True + except Exception as e: + logger.error("取消阶段进度失败 - Scan ID: %s, 错误: %s", scan_id, e) + return False + + +# 导出接口 +__all__ = ['ScanStateService'] diff --git a/backend/apps/scan/services/scan_stats_service.py b/backend/apps/scan/services/scan_stats_service.py new file mode 100644 index 00000000..4e8aa755 --- /dev/null +++ b/backend/apps/scan/services/scan_stats_service.py @@ -0,0 +1,55 @@ +""" +扫描统计服务 + +职责: +- 获取扫描统计数据 +- 数据聚合查询 +""" + +import logging +from django.db.utils import DatabaseError, OperationalError + +from apps.scan.repositories import DjangoScanRepository + +logger = logging.getLogger(__name__) + + +class ScanStatsService: + """ + 扫描统计服务 + + 职责: + - 统计数据查询 + - 数据聚合 + """ + + def __init__(self): + """ + 初始化服务 + """ + self.scan_repo = DjangoScanRepository() + + def get_statistics(self) -> dict: + """ + 获取扫描任务统计数据 + + Returns: + 统计数据字典 + + Raises: + DatabaseError: 数据库错误 + + Note: + 使用 Repository 层的聚合查询,性能优异 + """ + try: + statistics = self.scan_repo.get_statistics() + logger.debug("获取扫描统计数据成功 - 总数: %d", statistics['total']) + return statistics + except (DatabaseError, OperationalError) as e: + logger.exception("数据库错误:获取扫描统计数据失败") + raise + + +# 导出接口 +__all__ = ['ScanStatsService'] diff --git a/backend/apps/scan/services/scheduled_scan_service.py b/backend/apps/scan/services/scheduled_scan_service.py new file mode 100644 index 00000000..360a9805 --- /dev/null +++ b/backend/apps/scan/services/scheduled_scan_service.py @@ -0,0 +1,343 @@ +""" +定时扫描任务 Service + +业务逻辑层: +- 管理定时扫描任务的 CRUD +- 计算下次执行时间 +- APScheduler 会每分钟检查 next_run_time,到期任务通过 task_distributor 分发 +""" +import logging +from typing import List, Optional, Tuple +from datetime import datetime + +from django.core.exceptions import ValidationError + +from apps.scan.models import ScheduledScan +from apps.scan.repositories import DjangoScheduledScanRepository, ScheduledScanDTO +from apps.engine.repositories import DjangoEngineRepository +from apps.targets.services import TargetService + + +logger = logging.getLogger(__name__) + + +class ScheduledScanService: + """ + 定时扫描任务服务 + + 职责: + - 定时扫描任务的 CRUD 操作 + - 调度逻辑处理(基于 next_run_time) + """ + + def __init__(self): + self.repo = DjangoScheduledScanRepository() + self.engine_repo = DjangoEngineRepository() + self.target_service = TargetService() + + # ==================== 查询方法 ==================== + + def get_by_id(self, scheduled_scan_id: int) -> Optional[ScheduledScan]: + """根据 ID 获取定时扫描任务""" + return self.repo.get_by_id(scheduled_scan_id) + + def get_queryset(self): + """获取所有定时扫描任务的查询集""" + return self.repo.get_queryset() + + def get_all(self, page: int = 1, page_size: int = 10) -> Tuple[List[ScheduledScan], int]: + """分页获取所有定时扫描任务""" + return self.repo.get_all(page, page_size) + + # ==================== 创建方法 ==================== + + def create(self, dto: ScheduledScanDTO) -> ScheduledScan: + """ + 创建定时扫描任务 + + 流程: + 1. 验证参数 + 2. 创建数据库记录 + 3. 计算并设置 next_run_time + + Args: + dto: 定时扫描 DTO + + Returns: + 创建的 ScheduledScan 对象 + + Raises: + ValidationError: 参数验证失败 + """ + # 1. 验证参数 + self._validate_create_dto(dto) + + # 2. 创建数据库记录 + scheduled_scan = self.repo.create(dto) + + # 3. 如果有 cron 表达式且已启用,计算下次执行时间 + if scheduled_scan.cron_expression and scheduled_scan.is_enabled: + next_run_time = self._calculate_next_run_time(scheduled_scan) + if next_run_time: + self.repo.update_next_run_time(scheduled_scan.id, next_run_time) + scheduled_scan.next_run_time = next_run_time + + logger.info( + "创建定时扫描任务 - ID: %s, 名称: %s, 下次执行: %s", + scheduled_scan.id, scheduled_scan.name, scheduled_scan.next_run_time + ) + + return scheduled_scan + + def _validate_create_dto(self, dto: ScheduledScanDTO) -> None: + """验证创建 DTO""" + from apps.targets.repositories import DjangoOrganizationRepository + + if not dto.name: + raise ValidationError('任务名称不能为空') + + if not dto.engine_id: + raise ValidationError('必须选择扫描引擎') + + if not self.engine_repo.get_by_id(dto.engine_id): + raise ValidationError(f'扫描引擎 ID {dto.engine_id} 不存在') + + # 验证扫描模式(organization_id 和 target_id 互斥) + if not dto.organization_id and not dto.target_id: + raise ValidationError('必须选择组织或扫描目标') + + if dto.organization_id and dto.target_id: + raise ValidationError('组织扫描和目标扫描只能选择其中一种') + + # 组织扫描模式:验证组织是否存在 + if dto.organization_id: + org_repo = DjangoOrganizationRepository() + if not org_repo.get_by_id(dto.organization_id): + raise ValidationError(f'组织 ID {dto.organization_id} 不存在') + + # 目标扫描模式:验证目标是否存在 + if dto.target_id: + if not self.target_service.get_by_id(dto.target_id): + raise ValidationError(f'目标 ID {dto.target_id} 不存在') + + # 验证 cron 表达式格式(简单校验) + if dto.cron_expression: + parts = dto.cron_expression.split() + if len(parts) != 5: + raise ValidationError('Cron 表达式格式错误,需要 5 个部分:分 时 日 月 周') + + # ==================== 更新方法 ==================== + + def update(self, scheduled_scan_id: int, dto: ScheduledScanDTO) -> Optional[ScheduledScan]: + """ + 更新定时扫描任务 + + Args: + scheduled_scan_id: 定时扫描 ID + dto: 更新的数据 + + Returns: + 更新后的 ScheduledScan 对象 + """ + existing = self.repo.get_by_id(scheduled_scan_id) + if not existing: + return None + + # 更新数据库记录 + scheduled_scan = self.repo.update(scheduled_scan_id, dto) + if not scheduled_scan: + return None + + # 如果 cron 表达式或启用状态变化,重新计算 next_run_time + cron_changed = dto.cron_expression is not None and dto.cron_expression != existing.cron_expression + enabled_changed = dto.is_enabled is not None and dto.is_enabled != existing.is_enabled + + if cron_changed or enabled_changed: + if scheduled_scan.is_enabled and scheduled_scan.cron_expression: + next_run_time = self._calculate_next_run_time(scheduled_scan) + self.repo.update_next_run_time(scheduled_scan.id, next_run_time) + scheduled_scan.next_run_time = next_run_time + else: + # 禁用或无 cron 表达式,清空下次执行时间 + self.repo.update_next_run_time(scheduled_scan.id, None) + scheduled_scan.next_run_time = None + + return scheduled_scan + + # ==================== 启用/禁用方法 ==================== + + def toggle_enabled(self, scheduled_scan_id: int, enabled: bool) -> bool: + """ + 切换定时扫描任务的启用状态 + + Args: + scheduled_scan_id: 定时扫描 ID + enabled: 是否启用 + + Returns: + 是否成功 + """ + scheduled_scan = self.repo.get_by_id(scheduled_scan_id) + if not scheduled_scan: + return False + + # 更新数据库 + if not self.repo.toggle_enabled(scheduled_scan_id, enabled): + return False + + # 更新 next_run_time + if enabled and scheduled_scan.cron_expression: + next_run_time = self._calculate_next_run_time(scheduled_scan) + self.repo.update_next_run_time(scheduled_scan_id, next_run_time) + else: + self.repo.update_next_run_time(scheduled_scan_id, None) + + logger.info("切换定时扫描状态 - ID: %s, Enabled: %s", scheduled_scan_id, enabled) + return True + + def record_run(self, scheduled_scan_id: int) -> bool: + """ + 记录一次执行(增加执行次数、更新上次执行时间、计算下次执行时间) + + Args: + scheduled_scan_id: 定时扫描 ID + + Returns: + 是否成功 + """ + # 1. 增加执行次数并更新上次执行时间 + if not self.repo.increment_run_count(scheduled_scan_id): + return False + + # 2. 计算并更新下次执行时间 + scheduled_scan = self.repo.get_by_id(scheduled_scan_id) + if scheduled_scan and scheduled_scan.cron_expression: + next_run_time = self._calculate_next_run_time(scheduled_scan) + if next_run_time: + self.repo.update_next_run_time(scheduled_scan_id, next_run_time) + + return True + + # ==================== 删除方法 ==================== + + def delete(self, scheduled_scan_id: int) -> bool: + """ + 删除定时扫描任务 + + Args: + scheduled_scan_id: 定时扫描 ID + + Returns: + 是否成功 + """ + return self.repo.hard_delete(scheduled_scan_id) + + # ==================== 定时触发(APScheduler 调用)==================== + + def trigger_due_scans(self) -> int: + """ + 检查并触发所有到期的定时扫描任务 + + 由 APScheduler 每分钟调用一次,检查 next_run_time <= now 的任务 + + Returns: + 触发的任务数量 + """ + from django.utils import timezone + from croniter import croniter + + now = timezone.now() + triggered_count = 0 + + # 获取所有启用且到期的定时扫描 + due_scans = ScheduledScan.objects.filter( + is_enabled=True, + next_run_time__lte=now, + ) + + for scheduled_scan in due_scans: + try: + # 触发扫描 + self._trigger_scan_now(scheduled_scan) + + # 更新执行记录 + self.repo.increment_run_count(scheduled_scan.id) + + # 计算并更新下次执行时间 + cron = croniter(scheduled_scan.cron_expression, now) + next_run = cron.get_next(datetime) + + scheduled_scan.last_run_time = now + scheduled_scan.next_run_time = next_run + scheduled_scan.save(update_fields=['last_run_time', 'next_run_time']) + + triggered_count += 1 + logger.info( + "定时扫描已触发 - ID: %s, 名称: %s, 下次执行: %s", + scheduled_scan.id, scheduled_scan.name, next_run + ) + + except Exception as e: + logger.error( + "定时扫描触发失败 - ID: %s, Error: %s", + scheduled_scan.id, e + ) + + return triggered_count + + # ==================== 内部方法 ==================== + + def _trigger_scan_now(self, scheduled_scan: ScheduledScan) -> int: + """ + 立即触发扫描(支持组织扫描和目标扫描两种模式) + + 复用 ScanService 的逻辑,与 API 调用保持一致。 + """ + from apps.scan.services.scan_service import ScanService + + scan_service = ScanService() + + # 1. 准备扫描所需数据(复用 API 的逻辑) + targets, engine = scan_service.prepare_initiate_scan( + organization_id=scheduled_scan.organization_id, + target_id=scheduled_scan.target_id, + engine_id=scheduled_scan.engine_id + ) + + # 2. 创建扫描任务,传递定时扫描名称用于通知显示 + created_scans = scan_service.create_scans( + targets, engine, + scheduled_scan_name=scheduled_scan.name + ) + + logger.info( + "定时扫描已触发 - ScheduledScan ID: %s, 创建扫描数: %d", + scheduled_scan.id, len(created_scans) + ) + return len(created_scans) + + # ==================== 辅助方法 ==================== + + def _calculate_next_run_time(self, scheduled_scan: ScheduledScan) -> Optional[datetime]: + """ + 计算下次执行时间 + + Args: + scheduled_scan: 定时扫描对象 + + Returns: + 下次执行时间,once 类型返回 None + """ + from croniter import croniter + from django.utils import timezone + + cron_expr = scheduled_scan.cron_expression + if not cron_expr: + return None + + try: + cron = croniter(cron_expr, timezone.now()) + return cron.get_next(datetime) + except Exception as e: + logger.error("计算下次执行时间失败: %s", e) + return None diff --git a/backend/apps/scan/tasks/__init__.py b/backend/apps/scan/tasks/__init__.py new file mode 100644 index 00000000..34b9dd86 --- /dev/null +++ b/backend/apps/scan/tasks/__init__.py @@ -0,0 +1,35 @@ +""" +扫描任务模块 + +包含: +- Prefect Tasks: 具体操作的执行单元 + +架构说明: +- Flow(flows/)编排 Tasks(tasks/) +- Tasks 负责具体操作,Flow 负责编排 +""" + +# Prefect Tasks +from .workspace_tasks import create_scan_workspace_task + +# 子域名发现任务(已重构为多个子任务) +from .subdomain_discovery import ( + run_subdomain_discovery_task, + merge_and_validate_task, + save_domains_task, +) + +# 注意: +# - subdomain_discovery_task 已重构为多个子任务(subdomain_discovery/) +# - finalize_scan_task 已废弃(Handler 统一管理状态) +# - initiate_scan_task 已迁移到 flows/initiate_scan_flow.py +# - cleanup_old_scans_task 已迁移到 flows(cleanup_old_scans_flow) + +__all__ = [ + # Prefect Tasks + 'create_scan_workspace_task', + # 子域名发现任务 + 'run_subdomain_discovery_task', + 'merge_and_validate_task', + 'save_domains_task', +] diff --git a/backend/apps/scan/tasks/directory_scan/__init__.py b/backend/apps/scan/tasks/directory_scan/__init__.py new file mode 100644 index 00000000..feccdc26 --- /dev/null +++ b/backend/apps/scan/tasks/directory_scan/__init__.py @@ -0,0 +1,15 @@ +""" +目录扫描任务 + +主要任务: +- export_sites_task:导出站点列表到文件 +- run_and_stream_save_directories_task:流式运行目录扫描并实时保存结果 +""" + +from .export_sites_task import export_sites_task +from .run_and_stream_save_directories_task import run_and_stream_save_directories_task + +__all__ = [ + 'export_sites_task', + 'run_and_stream_save_directories_task', +] diff --git a/backend/apps/scan/tasks/directory_scan/export_sites_task.py b/backend/apps/scan/tasks/directory_scan/export_sites_task.py new file mode 100644 index 00000000..11785082 --- /dev/null +++ b/backend/apps/scan/tasks/directory_scan/export_sites_task.py @@ -0,0 +1,94 @@ +""" +导出站点 URL 到 TXT 文件的 Task + +使用流式处理,避免大量站点导致内存溢出 +""" +import logging +from pathlib import Path +from prefect import task + +from apps.asset.repositories import DjangoWebSiteRepository + +logger = logging.getLogger(__name__) + + +@task(name="export_sites") +def export_sites_task( + target_id: int, + output_file: str, + batch_size: int = 1000 +) -> dict: + """ + 导出目标下的所有站点 URL 到 TXT 文件 + + 使用流式处理,支持大规模数据导出(10万+站点) + + Args: + target_id: 目标 ID + output_file: 输出文件路径(绝对路径) + batch_size: 每次读取的批次大小,默认 1000 + + Returns: + dict: { + 'success': bool, + 'output_file': str, + 'total_count': int + } + + Raises: + ValueError: 参数错误 + IOError: 文件写入失败 + """ + try: + # 初始化 Repository + repository = DjangoWebSiteRepository() + + logger.info("开始导出站点 URL - Target ID: %d, 输出文件: %s", target_id, output_file) + + # 确保输出目录存在 + output_path = Path(output_file) + output_path.parent.mkdir(parents=True, exist_ok=True) + + # 使用 Repository 流式查询站点 URL + url_iterator = repository.get_urls_for_export( + target_id=target_id, + batch_size=batch_size + ) + + # 流式写入文件 + total_count = 0 + with open(output_path, 'w', encoding='utf-8', buffering=8192) as f: + for url in url_iterator: + # 每次只处理一个 URL,边读边写 + f.write(f"{url}\n") + total_count += 1 + + # 每写入 10000 条记录打印一次进度 + if total_count % 10000 == 0: + logger.info("已导出 %d 个站点 URL...", total_count) + + logger.info( + "✓ 站点 URL 导出完成 - 总数: %d, 文件: %s (%.2f KB)", + total_count, + str(output_path), # 使用绝对路径 + output_path.stat().st_size / 1024 + ) + + return { + 'success': True, + 'output_file': str(output_path), + 'total_count': total_count + } + + except FileNotFoundError as e: + logger.error("输出目录不存在: %s", e) + raise + except PermissionError as e: + logger.error("文件写入权限不足: %s", e) + raise + except Exception as e: + logger.exception("导出站点 URL 失败: %s", e) + raise + + + diff --git a/backend/apps/scan/tasks/directory_scan/run_and_stream_save_directories_task.py b/backend/apps/scan/tasks/directory_scan/run_and_stream_save_directories_task.py new file mode 100644 index 00000000..21091250 --- /dev/null +++ b/backend/apps/scan/tasks/directory_scan/run_and_stream_save_directories_task.py @@ -0,0 +1,469 @@ +""" +基于 execute_stream 的流式目录扫描任务 + +主要功能: + 1. 实时执行目录扫描命令(如 ffuf) + 2. 流式处理命令输出,实时解析为 Directory 记录 + 3. 批量保存到数据库 + 4. 避免生成大量临时文件,提高效率 + +数据流向: + 命令执行 → 流式输出 → 实时解析 → 批量保存 → 数据库 + + 输入:扫描命令及参数 + 输出:Directory 记录 + +优化策略: + - 使用 execute_stream 实时处理输出 + - 流式处理避免内存溢出 + - 批量操作减少数据库交互 +""" + +import logging +import json +import subprocess +import time +from pathlib import Path +from prefect import task +from typing import Generator, Optional, TYPE_CHECKING +from django.db import IntegrityError, OperationalError, DatabaseError +from psycopg2 import InterfaceError +from dataclasses import dataclass + +from apps.asset.services import WebSiteService +from apps.asset.dtos.snapshot import DirectorySnapshotDTO +from apps.scan.utils import execute_stream + +# 类型检查时导入,运行时不导入(避免循环依赖) +if TYPE_CHECKING: + from apps.asset.services.snapshot import DirectorySnapshotsService + +logger = logging.getLogger(__name__) + + +@dataclass +class ServiceSet: + """ + Service 集合,用于依赖注入 + + 提供目录扫描所需的 Service 实例,便于测试时注入 Mock 对象 + """ + website: WebSiteService + snapshot: "DirectorySnapshotsService" + + @classmethod + def create_default(cls) -> "ServiceSet": + """创建默认的 Service 集合""" + from apps.asset.services.snapshot import DirectorySnapshotsService + return cls( + website=WebSiteService(), + snapshot=DirectorySnapshotsService() + ) + + +def _parse_and_validate_line(line: str) -> Optional[dict]: + """ + 解析并验证单行 JSON 数据 + + Args: + line: 单行输出数据 + + Returns: + Optional[dict]: 有效的 ffuf 扫描记录,或 None 如果验证失败 + + 验证步骤: + 1. 解析 JSON 格式 + 2. 验证数据类型为字典 + 3. 验证必要字段(url) + """ + try: + # 步骤 1: 解析 JSON + try: + line_data = json.loads(line) + except json.JSONDecodeError: + # logger.debug("跳过非 JSON 格式的行: %s", line[:100]) + return None + + # 步骤 2: 验证数据类型 + if not isinstance(line_data, dict): + logger.warning("解析后的数据不是字典类型,跳过: %s", str(line_data)[:100]) + return None + + # 步骤 3: 验证必要字段 + if not line_data.get('url'): + logger.debug("URL 为空,跳过") + return None + + # 返回有效记录 + return { + 'url': line_data['url'], + 'status': line_data.get('status'), + 'length': line_data.get('length'), + 'words': line_data.get('words'), + 'lines': line_data.get('lines'), + 'content_type': line_data.get('content-type', ''), + 'duration': line_data.get('duration') + } + + except Exception as e: + logger.error("解析行数据异常: %s - 数据: %s", e, line[:100]) + return None + + +def _parse_ffuf_stream_output( + cmd: str, + tool_name: str, + cwd: Optional[str] = None, + shell: bool = False, + timeout: Optional[int] = None, + log_file: Optional[str] = None +) -> Generator[dict, None, None]: + """ + 流式解析 ffuf 目录扫描命令输出 + + 基于 execute_stream 实时处理 ffuf 命令的 stdout,将每行 JSON 输出 + 转换为 Directory 记录格式 + + Args: + cmd: ffuf 目录扫描命令 + cwd: 工作目录 + shell: 是否使用 shell 执行 + timeout: 命令执行超时时间(秒),None 表示不设置超时 + + Yields: + dict: 每次 yield 一条解析后的目录记录 + """ + logger.info("开始流式解析 ffuf 目录扫描命令输出 - 命令: %s", cmd) + + total_lines = 0 + error_lines = 0 + valid_records = 0 + + try: + # 使用 execute_stream 获取实时输出流(带工具名、超时控制和日志文件) + for line in execute_stream(cmd=cmd, tool_name=tool_name, cwd=cwd, shell=shell, timeout=timeout, log_file=log_file): + total_lines += 1 + + # 解析并验证单行数据 + record = _parse_and_validate_line(line) + if record is None: + error_lines += 1 + continue + + valid_records += 1 + # yield 一条有效记录 + yield record + + # 每处理 1000 条记录输出一次进度 + if valid_records % 1000 == 0: + logger.info("已解析 %d 条有效记录...", valid_records) + + except subprocess.TimeoutExpired as e: + # 超时异常:简洁输出,不显示堆栈 + error_msg = f"流式解析命令输出超时 - 命令执行超过 {timeout} 秒" + logger.warning(error_msg) # 超时是可预期的 + raise RuntimeError(error_msg) from e + except Exception as e: + # 其他异常:输出详细堆栈以便调试 + logger.error("流式解析命令输出失败: %s", e, exc_info=True) + raise + + logger.info( + "流式解析完成 - 总行数: %d, 有效记录: %d, 错误行数: %d", + total_lines, valid_records, error_lines + ) + + +def _save_batch_with_retry( + batch: list, + website_id: int, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet, + max_retries: int = 3 +) -> dict: + """ + 保存一个批次的目录扫描结果(带重试机制) + + Args: + batch: 数据批次 + website_id: 站点 ID + scan_id: 扫描任务ID + target_id: 目标ID + batch_num: 批次编号 + services: Service 集合(必须,依赖注入) + max_retries: 最大重试次数 + + Returns: + dict: { + 'success': bool, + 'created_directories': int + } + """ + for attempt in range(max_retries): + try: + count = _save_batch(batch, website_id, scan_id, target_id, batch_num, services) + return { + 'success': True, + 'created_directories': count + } + + except IntegrityError as e: + # 数据完整性错误,不应重试 + logger.error("批次 %d 数据完整性错误,跳过: %s", batch_num, str(e)[:100]) + return { + 'success': False, + 'created_directories': 0 + } + + except (OperationalError, DatabaseError, InterfaceError) as e: + # 数据库连接/操作错误,可重试 + if attempt < max_retries - 1: + wait_time = 2 ** attempt # 指数退避: 1s, 2s, 4s + logger.warning( + "批次 %d 保存失败(第 %d 次尝试),%d秒后重试: %s", + batch_num, attempt + 1, wait_time, str(e)[:100] + ) + time.sleep(wait_time) + else: + logger.error("批次 %d 保存失败(已重试 %d 次): %s", batch_num, max_retries, e) + return { + 'success': False, + 'created_directories': 0 + } + + except Exception as e: + # 其他未知错误 - 检查是否为连接问题 + error_str = str(e).lower() + if 'connection' in error_str and attempt < max_retries - 1: + logger.warning( + "批次 %d 连接相关错误(尝试 %d/%d): %s,Repository 装饰器会自动重连", + batch_num, attempt + 1, max_retries, str(e) + ) + time.sleep(2) + else: + logger.error("批次 %d 未知错误: %s", batch_num, e, exc_info=True) + return { + 'success': False, + 'created_directories': 0 + } + + return { + 'success': False, + 'created_directories': 0 + } + + +def _save_batch( + batch: list, + website_id: int, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet +) -> int: + """ + 保存一个批次的数据到数据库(使用快照 Service) + + 数据关系链: + WebSite (已存在) → DirectorySnapshot (待创建) → Directory (自动同步) + + 处理流程: + 1. 构建 DirectorySnapshotDTO:包含 scan_id 和 target_id + 2. 调用快照 Service:save_and_sync() 自动保存快照并同步到资产表 + + Args: + batch: 数据批次,list of dict + website_id: 站点 ID + scan_id: 扫描任务 ID + target_id: 目标 ID + batch_num: 批次编号(用于日志) + services: Service 集合(依赖注入) + + Returns: + int: 创建的记录数 + """ + if not batch: + logger.debug("批次 %d 为空,跳过处理", batch_num) + return 0 + + # ========== Step 1: 准备 DirectorySnapshot 数据(内存操作,无需事务)========== + snapshot_items = [] + + for record in batch: + # 创建 DirectorySnapshot DTO + snapshot_dto = DirectorySnapshotDTO( + scan_id=scan_id, + website_id=website_id, + target_id=target_id, # 冗余字段,用于同步到资产表 + url=record['url'], + status=record.get('status'), + content_length=record.get('length'), + words=record.get('words'), + lines=record.get('lines'), + content_type=record.get('content_type', ''), + duration=record.get('duration') + ) + + snapshot_items.append(snapshot_dto) + + # ========== Step 2: 保存快照并同步到资产表(通过快照 Service)========== + if snapshot_items: + services.snapshot.save_and_sync(snapshot_items) + + return len(snapshot_items) + + +@task( + name='run_and_stream_save_directories', + retries=0, + log_prints=True +) +def run_and_stream_save_directories_task( + cmd: str, + tool_name: str, + scan_id: int, + target_id: int, + site_url: str, + cwd: Optional[str] = None, + shell: bool = False, + batch_size: int = 1000, + timeout: Optional[int] = None, + log_file: Optional[str] = None +) -> dict: + """ + 执行 ffuf 目录扫描命令并流式保存结果到数据库 + + 该任务将: + 1. 通过 site_url 查找对应的 WebSite 对象 + 2. 流式解析 ffuf 输出(JSON 格式) + 3. 批量保存到 Directory 表 + + Args: + cmd: ffuf 目录扫描命令 + scan_id: 扫描任务 ID + target_id: 目标 ID + site_url: 当前站点 URL + cwd: 工作目录(可选) + shell: 是否使用 shell 执行(默认 False) + batch_size: 批量保存大小(默认1000) + timeout: 命令执行超时时间(秒),None 表示不设置超时 + + Returns: + dict: { + 'processed_records': int, # 处理的记录总数 + 'created_directories': int, # 创建的目录记录数 + 'site_url': str # 当前站点 URL + } + + Raises: + ValueError: 参数验证失败 + RuntimeError: 命令执行或数据库操作失败 + subprocess.TimeoutExpired: 命令执行超时 + """ + logger.info( + "开始执行流式目录扫描任务 - site_url=%s, 超时=%s秒", + site_url, timeout if timeout else '无限制' + ) + + data_generator = None + + try: + # 1. 初始化服务 + services = ServiceSet.create_default() + + # 2. 查找站点(使用 Service) + website_id = services.website.get_by_url(url=site_url, target_id=target_id) + + if website_id is None: + logger.error("站点不存在: %s", site_url) + raise ValueError(f"站点不存在: {site_url}") + + logger.info("找到站点: %s (ID: %d)", site_url, website_id) + + # 3. 初始化资源 + data_generator = _parse_ffuf_stream_output(cmd=cmd, tool_name=tool_name, cwd=cwd, shell=shell, timeout=timeout, log_file=log_file) + + # 4. 流式处理记录并分批保存 + total_records = 0 + batch_num = 0 + failed_batches = [] + batch = [] + total_created = 0 + + for record in data_generator: + batch.append(record) + total_records += 1 + + # 达到批次大小,执行保存 + if len(batch) >= batch_size: + batch_num += 1 + result = _save_batch_with_retry( + batch, website_id, scan_id, target_id, batch_num, services + ) + + total_created += result.get('created_directories', 0) + + if not result['success']: + failed_batches.append(batch_num) + + batch = [] # 清空批次 + + # 每20个批次输出进度 + if batch_num % 20 == 0: + logger.info( + "进度: 已处理 %d 批次,%d 条记录", + batch_num, total_records + ) + + # 保存最后一批 + if batch: + batch_num += 1 + result = _save_batch_with_retry( + batch, website_id, scan_id, target_id, batch_num, services + ) + total_created += result.get('created_directories', 0) + + if not result['success']: + failed_batches.append(batch_num) + + # 检查失败批次 + if failed_batches: + logger.warning( + "部分批次保存失败 - 站点: %s, 失败批次: %s", + site_url, failed_batches + ) + + logger.info( + "✓ 流式保存完成 - 站点: %s, 处理记录: %d(%d 批次),创建目录: %d", + site_url, total_records, batch_num, total_created + ) + + return { + 'processed_records': total_records, + 'created_directories': total_created, + 'site_url': site_url + } + + except subprocess.TimeoutExpired: + # 超时异常直接向上传播,保留异常类型 + logger.warning( + "⚠️ 目录扫描任务超时 - site_url=%s, 超时=%s秒", + site_url, timeout + ) + raise + + except Exception as e: + error_msg = f"流式执行目录扫描任务失败: {e}" + logger.error(error_msg, exc_info=True) + raise RuntimeError(error_msg) from e + + finally: + # 清理资源 + if data_generator is not None: + try: + data_generator.close() + logger.debug("已关闭数据生成器") + except Exception as gen_close_error: + logger.error("关闭生成器时出错: %s", gen_close_error) diff --git a/backend/apps/scan/tasks/port_scan/__init__.py b/backend/apps/scan/tasks/port_scan/__init__.py new file mode 100644 index 00000000..09aa1f4a --- /dev/null +++ b/backend/apps/scan/tasks/port_scan/__init__.py @@ -0,0 +1,15 @@ +""" +端口扫描相关 Tasks + +提供端口扫描流程所需的原子化任务 +""" + +from .export_scan_targets_task import export_scan_targets_task +from .run_and_stream_save_ports_task import run_and_stream_save_ports_task +from .types import PortScanRecord + +__all__ = [ + 'export_scan_targets_task', + 'run_and_stream_save_ports_task', + 'PortScanRecord', +] \ No newline at end of file diff --git a/backend/apps/scan/tasks/port_scan/export_scan_targets_task.py b/backend/apps/scan/tasks/port_scan/export_scan_targets_task.py new file mode 100644 index 00000000..215410ef --- /dev/null +++ b/backend/apps/scan/tasks/port_scan/export_scan_targets_task.py @@ -0,0 +1,189 @@ +""" +导出扫描目标到 TXT 文件的 Task + +根据 Target 类型决定导出内容: +- DOMAIN: 从 Subdomain 表导出子域名 +- IP: 直接写入 target.name +- CIDR: 展开 CIDR 范围内的所有 IP + +使用流式处理,避免大量数据导致内存溢出 +""" +import logging +import ipaddress +from pathlib import Path +from prefect import task + +from apps.asset.services.asset.subdomain_service import SubdomainService +from apps.targets.services import TargetService +from apps.targets.models import Target # 仅用于 TargetType 常量 + +logger = logging.getLogger(__name__) + + +def _export_domains(target_id: int, output_path: Path, batch_size: int) -> int: + """ + 导出域名类型目标的子域名 + + Args: + target_id: 目标 ID + output_path: 输出文件路径 + batch_size: 批次大小 + + Returns: + int: 导出的记录数 + """ + subdomain_service = SubdomainService() + domain_iterator = subdomain_service.iter_subdomain_names_by_target( + target_id=target_id, + chunk_size=batch_size + ) + + total_count = 0 + with open(output_path, 'w', encoding='utf-8', buffering=8192) as f: + for domain_name in domain_iterator: + f.write(f"{domain_name}\n") + total_count += 1 + + if total_count % 10000 == 0: + logger.info("已导出 %d 个域名...", total_count) + + return total_count + + +def _export_ip(target_name: str, output_path: Path) -> int: + """ + 导出 IP 类型目标 + + Args: + target_name: IP 地址 + output_path: 输出文件路径 + + Returns: + int: 导出的记录数(始终为 1) + """ + with open(output_path, 'w', encoding='utf-8') as f: + f.write(f"{target_name}\n") + return 1 + + +def _export_cidr(target_name: str, output_path: Path) -> int: + """ + 导出 CIDR 类型目标,展开为每个 IP + + Args: + target_name: CIDR 范围(如 192.168.1.0/24) + output_path: 输出文件路径 + + Returns: + int: 导出的 IP 数量 + """ + network = ipaddress.ip_network(target_name, strict=False) + total_count = 0 + + with open(output_path, 'w', encoding='utf-8', buffering=8192) as f: + for ip in network.hosts(): # 排除网络地址和广播地址 + f.write(f"{ip}\n") + total_count += 1 + + if total_count % 10000 == 0: + logger.info("已导出 %d 个 IP...", total_count) + + # 如果是 /32 或 /128(单个 IP),hosts() 会为空,需要特殊处理 + if total_count == 0: + with open(output_path, 'w', encoding='utf-8') as f: + f.write(f"{network.network_address}\n") + total_count = 1 + + return total_count + + +@task(name="export_scan_targets") +def export_scan_targets_task( + target_id: int, + output_file: str, + batch_size: int = 1000 +) -> dict: + """ + 导出扫描目标到 TXT 文件 + + 根据 Target 类型自动决定导出内容: + - DOMAIN: 从 Subdomain 表导出子域名(流式处理,支持 10万+ 域名) + - IP: 直接写入 target.name(单个 IP) + - CIDR: 展开 CIDR 范围内的所有可用 IP + + Args: + target_id: 目标 ID + output_file: 输出文件路径(绝对路径) + batch_size: 每次读取的批次大小,默认 1000(仅对 DOMAIN 类型有效) + + Returns: + dict: { + 'success': bool, + 'output_file': str, + 'total_count': int, + 'target_type': str + } + + Raises: + ValueError: Target 不存在 + IOError: 文件写入失败 + """ + try: + # 1. 通过 Service 层获取 Target + target_service = TargetService() + target = target_service.get_target(target_id) + if not target: + raise ValueError(f"Target ID {target_id} 不存在") + + target_type = target.type + target_name = target.name + + logger.info( + "开始导出扫描目标 - Target ID: %d, Name: %s, Type: %s, 输出文件: %s", + target_id, target_name, target_type, output_file + ) + + # 2. 确保输出目录存在 + output_path = Path(output_file) + output_path.parent.mkdir(parents=True, exist_ok=True) + + # 3. 根据类型导出 + if target_type == Target.TargetType.DOMAIN: + total_count = _export_domains(target_id, output_path, batch_size) + type_desc = "域名" + elif target_type == Target.TargetType.IP: + total_count = _export_ip(target_name, output_path) + type_desc = "IP" + elif target_type == Target.TargetType.CIDR: + total_count = _export_cidr(target_name, output_path) + type_desc = "CIDR IP" + else: + raise ValueError(f"不支持的目标类型: {target_type}") + + logger.info( + "✓ 扫描目标导出完成 - 类型: %s, 总数: %d, 文件: %s (%.2f KB)", + type_desc, + total_count, + str(output_path), + output_path.stat().st_size / 1024 + ) + + return { + 'success': True, + 'output_file': str(output_path), + 'total_count': total_count, + 'target_type': target_type + } + + except FileNotFoundError as e: + logger.error("输出目录不存在: %s", e) + raise + except PermissionError as e: + logger.error("文件写入权限不足: %s", e) + raise + except ValueError as e: + logger.error("参数错误: %s", e) + raise + except Exception as e: + logger.exception("导出扫描目标失败: %s", e) + raise diff --git a/backend/apps/scan/tasks/port_scan/run_and_stream_save_ports_task.py b/backend/apps/scan/tasks/port_scan/run_and_stream_save_ports_task.py new file mode 100644 index 00000000..af2e4572 --- /dev/null +++ b/backend/apps/scan/tasks/port_scan/run_and_stream_save_ports_task.py @@ -0,0 +1,698 @@ +""" +基于 stream_command 的流式端口扫描任务(简化版) + +主要功能: + 1. 实时执行端口扫描命令(如 naabu) + 2. 流式处理命令输出,实时解析为 PortScanRecord + 3. 批量保存到数据库(HostPortAssociation + HostPortAssociationSnapshot) + 4. 避免生成大量临时文件,提高效率 + +数据流向: + 命令执行 → 流式输出 → 实时解析 → 批量保存 → 数据库 + + 输入:扫描命令及参数 + 输出:HostPortAssociation(资产表)+ HostPortAssociationSnapshot(快照表) + +优化策略: + - 使用 stream_command 实时处理输出 + - 直接存储 host + ip + port 组合,不维护复杂关系 + - 流式处理避免内存溢出 + - 批量操作减少数据库交互 +""" + +import logging +import json +import subprocess +import time +from asyncio import CancelledError +from pathlib import Path +from prefect import task +from typing import Generator, List, Optional, TYPE_CHECKING +from django.db import IntegrityError, OperationalError, DatabaseError +from psycopg2 import InterfaceError +from dataclasses import dataclass + +from .types import PortScanRecord +from apps.scan.utils import execute_stream +from apps.common.validators import validate_port +from apps.common.definitions import ScanStatus +from apps.scan.models import Scan + +# 类型检查时导入,运行时不导入(避免循环依赖) +if TYPE_CHECKING: + from apps.asset.services.snapshot import HostPortMappingSnapshotsService + +logger = logging.getLogger(__name__) + + +@dataclass +class ServiceSet: + """ + Service 集合,用于依赖注入 + + 提供所有需要的 Service 实例,便于测试时注入 Mock 对象 + """ + snapshot: "HostPortMappingSnapshotsService" + + @classmethod + def create_default(cls) -> "ServiceSet": + """创建默认的 Service 集合""" + from apps.asset.services.snapshot import HostPortMappingSnapshotsService + return cls( + snapshot=HostPortMappingSnapshotsService() + ) + + +def _save_batch_with_retry( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet, + max_retries: int = 3 +) -> dict: + """ + 保存一个批次的端口扫描结果(带重试机制) + + Args: + batch: 数据批次 + scan_id: 扫描任务ID + target_id: 目标ID + batch_num: 批次编号 + services: Service 集合(必须,包含 HostPortAssociationSnapshotsService) + max_retries: 最大重试次数 + + Returns: + dict: {'success': bool} + """ + for attempt in range(max_retries): + try: + result = _save_batch(batch, scan_id, target_id, batch_num, services) + return result # {'success': True} + + except IntegrityError as e: + # 数据完整性错误,不应重试(IntegrityError 是 DatabaseError 的子类,需先处理) + logger.error("批次 %d 数据完整性错误,跳过: %s", batch_num, str(e)[:100]) + return {'success': False} + + except (OperationalError, DatabaseError) as e: + # 数据库连接/操作错误,可重试 + if attempt < max_retries - 1: + wait_time = 2 ** attempt # 指数退避: 1s, 2s, 4s + logger.warning( + "批次 %d 保存失败(第 %d 次尝试),%d秒后重试: %s", + batch_num, attempt + 1, wait_time, str(e)[:100] + ) + time.sleep(wait_time) + else: + logger.error("批次 %d 保存失败(已重试 %d 次): %s", batch_num, max_retries, e) + return {'success': False} + + except Exception as e: + # 其他未知错误 + logger.error("批次 %d 未知错误: %s", batch_num, e, exc_info=True) + return {'success': False} + + return {'success': False} + + +def _save_batch( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet # Service集合(依赖注入) +) -> dict: + """ + 保存一个批次的端口扫描数据到数据库(使用 Service 架构) + + 数据存储: + 使用 HostPortAssociationSnapshotsService.save_and_sync() + 自动保存到快照表并同步到资产表 + + 处理流程: + 1. 构建 HostPortAssociationSnapshotDTO 列表 + 2. 调用 service.save_and_sync() 统一处理 + - 保存到快照表(scan_id) + - 同步到资产表(target_id) + + Args: + batch: 数据批次,list of {'host', 'ip', 'port'} + scan_id: 扫描任务 ID + target_id: 目标 ID + batch_num: 批次编号(用于日志) + services: Service 集合(包含 HostPortAssociationSnapshotsService) + + Returns: + dict: {'success': bool} + + Raises: + TypeError: batch 参数类型错误 + IntegrityError: 数据完整性错误 + OperationalError: 数据库操作错误 + DatabaseError: 其他数据库错误 + + Note: + 此函数不包含重试逻辑,由外层 _save_batch_with_retry 负责重试 + + Strategy: + 使用 bulk_create + ignore_conflicts + - 新记录:插入 + - 重复记录:忽略(不更新) + """ + from apps.asset.dtos.snapshot import HostPortMappingSnapshotDTO + + # 参数验证 + if not isinstance(batch, list): + raise TypeError(f"batch 必须是 list 类型,实际: {type(batch).__name__}") + + if not batch: + logger.debug("批次 %d 为空,跳过处理", batch_num) + return {'success': True} + + # 构建 DTO 列表(包含完整的业务上下文) + items = [ + HostPortMappingSnapshotDTO( + scan_id=scan_id, + target_id=target_id, # 包含 target_id 用于同步到资产表 + host=record['host'], + ip=record['ip'], + port=record['port'] + ) + for record in batch + ] + + # 调用 Service 统一处理(保存快照 + 同步资产) + # DTO 已包含 target_id,无需额外传参 + services.snapshot.save_and_sync(items) + + logger.debug("批次 %d: 已处理 %d 条记录", batch_num, len(batch)) + + return {'success': True} + +def _parse_and_validate_line(line: str) -> Optional[PortScanRecord]: + """ + 解析并验证单行 JSON 数据 + + Args: + line: 单行输出数据 + + Returns: + Optional[PortScanRecord]: 有效的端口扫描记录,或 None 如果验证失败 + + 验证步骤: + 1. 解析 JSON 格式 + 2. 验证数据类型为字典 + 3. 提取必要字段(host, ip, port) + 4. 验证字段不为空 + 5. 验证端口号有效性 + """ + try: + # 步骤 1: 解析 JSON + try: + line_data = json.loads(line) + except json.JSONDecodeError: + # logger.debug("跳过非 JSON 格式的行: %s", line[:100]) + return None + + # 步骤 2: 验证数据类型 + if not isinstance(line_data, dict): + logger.warning("解析后的数据不是字典类型,跳过: %s", str(line_data)[:100]) + return None + + # 步骤 3: 提取必要字段 + host = line_data.get('host', '').strip() + ip = line_data.get('ip', '').strip() + port = line_data.get('port') + + # 步骤 4: 验证字段不为空 + if not host or not ip or port is None: + logger.warning( + "缺少必要字段,跳过: host=%s, ip=%s, port=%s", + host, ip, port + ) + return None + + # 步骤 5: 验证端口号有效性 + is_valid, port_num = validate_port(port) + if not is_valid: + return None + + # 返回有效记录 + return { + 'host': host, + 'ip': ip, + 'port': port_num, + } + + except Exception as e: + logger.error("解析行数据异常: %s - 数据: %s", e, line[:100]) + return None + + +def _parse_naabu_stream_output( + cmd: str, + tool_name: str, + cwd: Optional[str] = None, + shell: bool = False, + timeout: Optional[int] = None, + log_file: Optional[str] = None +) -> Generator[PortScanRecord, None, None]: + """ + 流式解析 naabu 端口扫描命令输出 + + 基于 stream_command 实时处理 naabu 命令的 stdout,将每行 JSON 输出 + 转换为 PortScanRecord 格式,沿用现有字段校验逻辑 + + Args: + cmd: naabu 端口扫描命令(如: "naabu -l domains.txt -json") + tool_name: 工具名称(如: "naabu") + cwd: 工作目录 + shell: 是否使用 shell 执行 + timeout: 命令执行超时时间(秒),None 表示不设置超时 + log_file: 日志文件路径(可选) + + Yields: + PortScanRecord: 每次 yield 一条解析后的端口记录,格式: + { + 'host': str, # 域名 + 'ip': str, # IP地址 + 'port': int, # 端口号 + } + """ + logger.info("开始流式解析 naabu 端口扫描命令输出 - 命令: %s", cmd) + + total_lines = 0 + error_lines = 0 + last_log_time = time.time() # 添加:记录上次日志时间 + + try: + # 使用 execute_stream 获取实时输出流(带工具名、超时控制和日志文件) + for line in execute_stream(cmd=cmd, tool_name=tool_name, cwd=cwd, shell=shell, timeout=timeout, log_file=log_file): + total_lines += 1 + + try: + # 解析并验证单行数据 + record = _parse_and_validate_line(line) + if record is None: + error_lines += 1 + continue + + # yield 一条有效记录 + yield record + + # 添加:每100条记录输出一次处理速度统计 + if total_lines % 100 == 0: + current_time = time.time() + elapsed = current_time - last_log_time + logger.info( + "流式处理进度 - 已处理: %d 行, 有效记录: %d, 错误: %d, 速度: %.1f 行/秒", + total_lines, total_lines - error_lines, error_lines, 100 / elapsed if elapsed > 0 else 0 + ) + last_log_time = current_time + + except (json.JSONDecodeError, ValueError, KeyError) as e: + # 数据解析错误(可恢复):记录警告但继续处理后续数据 + # 这类错误通常是单条数据格式问题,不应影响整体流程 + error_lines += 1 + logger.warning( + "数据解析错误,跳过此行 (行号: %d) - 错误: %s, 原始数据: %s", + total_lines, e, line[:100] # 只记录前100字符避免日志过大 + ) + continue + + except subprocess.TimeoutExpired as e: + # 超时异常:简洁输出,不显示堆栈 + error_msg = f"流式解析命令输出超时 - 命令执行超过 {timeout} 秒" + logger.warning(error_msg) # 超时是可预期的 + raise RuntimeError(error_msg) from e + + except (IOError, OSError) as e: + # IO错误(致命):无法继续读取数据流 + logger.error("流式解析IO错误: %s", e, exc_info=True) + raise RuntimeError(f"流式解析IO错误: {e}") from e + + except (BrokenPipeError, ConnectionError) as e: + # 连接错误(致命):进程异常终止或管道断开 + logger.error("流式解析连接错误(进程可能异常终止): %s", e, exc_info=True) + raise RuntimeError(f"流式解析连接错误: {e}") from e + + except Exception as e: + # 未预期的异常:输出详细堆栈以便调试 + logger.error( + "流式解析命令输出失败(未预期的异常): %s", + e, exc_info=True + ) + raise + + logger.info( + "流式解析完成 - 总行数: %d, 错误行数: %d", + total_lines, error_lines, + ) + + +def _validate_task_parameters(cmd: str, target_id: int, scan_id: int, cwd: Optional[str]) -> None: + """ + 验证任务参数的有效性 + + Args: + cmd: 扫描命令 + target_id: 目标ID + scan_id: 扫描ID + cwd: 工作目录 + + Raises: + ValueError: 参数验证失败 + """ + if not cmd or not cmd.strip(): + raise ValueError("扫描命令不能为空") + + if target_id is None: + raise ValueError("target_id 不能为 None,必须指定目标ID") + + if scan_id is None: + raise ValueError("scan_id 不能为 None,必须指定扫描ID") + + # 验证工作目录(如果指定) + if cwd and not Path(cwd).exists(): + raise ValueError(f"工作目录不存在: {cwd}") + + +def _accumulate_batch_stats(total_stats: dict, batch_result: dict) -> None: + """ + 累加批次统计信息 + + Args: + total_stats: 总统计信息字典 + batch_result: 批次结果字典 + """ + total_stats['created_ips'] += batch_result.get('created_ips', 0) + total_stats['created_ports'] += batch_result.get('created_ports', 0) + total_stats['skipped_no_subdomain'] += batch_result.get('skipped_no_subdomain', 0) + total_stats['skipped_no_ip'] += batch_result.get('skipped_no_ip', 0) + + +def _process_batch( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + total_stats: dict, + failed_batches: list, + services: ServiceSet +) -> None: + """ + 处理单个批次 + + Args: + batch: 数据批次 + scan_id: 扫描ID + target_id: 目标ID + batch_num: 批次编号 + total_stats: 总统计信息 + failed_batches: 失败批次列表 + services: Service 集合(必须,依赖注入) + """ + result = _save_batch_with_retry( + batch, scan_id, target_id, batch_num, services + ) + + # 累计统计信息(失败时可能有部分数据已保存) + _accumulate_batch_stats(total_stats, result) + + if not result['success']: + failed_batches.append(batch_num) + logger.warning( + "批次 %d 保存失败,但已累计统计信息:创建IP=%d, 创建端口=%d", + batch_num, result.get('created_ips', 0), result.get('created_ports', 0) + ) + + +def _process_records_in_batches( + data_generator, + scan_id: int, + target_id: int, + batch_size: int, + services: ServiceSet +) -> dict: + """ + 流式处理记录并分批保存 + + Args: + data_generator: 数据生成器 + scan_id: 扫描ID + target_id: 目标ID + batch_size: 批次大小 + services: Service 集合(必须,依赖注入) + + Returns: + dict: 处理统计信息 + + Raises: + RuntimeError: 存在失败批次时抛出 + subprocess.TimeoutExpired: 命令执行超时(部分数据已保存) + + Note: + 如果发生超时,已处理的数据会被保留在数据库中, + 但扫描任务会被标记为失败。这是预期行为。 + """ + total_records = 0 + batch_num = 0 + failed_batches = [] + batch = [] + cancel_check_interval = 50 # 每处理50条检查一次取消信号 + + # 统计信息 + total_stats = { + 'created_ips': 0, + 'created_ports': 0, + 'skipped_no_subdomain': 0, + 'skipped_no_ip': 0 + } + + # 流式读取生成器并分批保存 + # 注意:如果超时,subprocess.TimeoutExpired 会从 data_generator 中抛出 + # 此时已处理的数据已经保存到数据库 + for record in data_generator: + # 周期性检查取消信号,协作式终止 + if cancel_check_interval > 0 and (total_records % cancel_check_interval == 0): + _raise_if_cancelled(scan_id) + + batch.append(record) + total_records += 1 + + # 达到批次大小,执行保存 + if len(batch) >= batch_size: + batch_num += 1 + _process_batch(batch, scan_id, target_id, batch_num, total_stats, failed_batches, services) + batch = [] # 清空批次 + + # 每20个批次输出进度 + if batch_num % 20 == 0: + logger.info("进度: 已处理 %d 批次,%d 条记录", batch_num, total_records) + + # 保存最后一批 + if batch: + batch_num += 1 + _process_batch(batch, scan_id, target_id, batch_num, total_stats, failed_batches, services) + + # 最后再检查一次取消信号,避免在尾部卡住 + _raise_if_cancelled(scan_id) + + # 检查失败批次 + if failed_batches: + error_msg = ( + f"流式保存端口扫描结果时出现失败批次,处理记录: {total_records}," + f"失败批次: {failed_batches}" + ) + logger.warning(error_msg) # 超时是可预期的 + raise RuntimeError(error_msg) + + return { + 'processed_records': total_records, + 'batch_count': batch_num, + **total_stats + } + + +def _build_final_result(stats: dict) -> dict: + """ + 构建最终结果并输出日志 + + Args: + stats: 处理统计信息 + + Returns: + dict: 最终结果 + """ + logger.info( + "✓ 流式保存完成 - 处理记录: %d(%d 批次),创建IP: %d,创建端口: %d,跳过(无域名): %d,跳过(无IP): %d", + stats['processed_records'], stats['batch_count'], stats['created_ips'], stats['created_ports'], + stats['skipped_no_subdomain'], stats['skipped_no_ip'] + ) + + # 如果没有创建任何记录,给出明确提示 + if stats['created_ips'] == 0 and stats['created_ports'] == 0: + logger.warning( + "⚠️ 没有创建任何记录!可能原因:1) 域名不在数据库中 2) 命令输出格式问题 3) 重复数据被忽略" + ) + + return { + 'processed_records': stats['processed_records'], + 'created_ips': stats['created_ips'], + 'created_ports': stats['created_ports'], + 'skipped_no_subdomain': stats['skipped_no_subdomain'], + 'skipped_no_ip': stats['skipped_no_ip'] + } + + +def _cleanup_resources(data_generator) -> None: + """ + 清理任务资源 + + Args: + data_generator: 数据生成器(可以为 None) + + Note: + 此函数设计为幂等且安全: + - 可以多次调用 + - 接受 None 值 + - 捕获所有异常,不会导致 finally 块失败 + """ + # 确保生成器被正确关闭 + if data_generator is None: + logger.debug("数据生成器为 None,无需清理") + return + + try: + data_generator.close() + logger.debug("✓ 已成功关闭数据生成器") + except StopIteration: + # 生成器已经正常结束,这是预期行为 + logger.debug("数据生成器已正常结束") + except GeneratorExit: + # 生成器已经被关闭,这是预期行为 + logger.debug("数据生成器已被关闭") + except Exception as gen_close_error: + # 未预期的错误:记录但不抛出,避免掩盖原始异常 + logger.error( + "⚠️ 关闭生成器时出错(此错误不会影响任务结果): %s", + gen_close_error, + exc_info=True + ) + + +@task( + name='run_and_stream_save_ports', + retries=0, + log_prints=True +) +def run_and_stream_save_ports_task( + cmd: str, + tool_name: str, + scan_id: int, + target_id: int, + cwd: Optional[str] = None, + shell: bool = False, + batch_size: int = 1000, + timeout: Optional[int] = None, + log_file: Optional[str] = None +) -> dict: + """ + 执行端口扫描命令并流式保存结果到数据库 + + 该任务将: + 1. 验证输入参数 + 2. 初始化资源(缓存、生成器) + 3. 流式处理记录并分批保存 + 4. 构建并返回结果统计 + + Args: + cmd: 端口扫描命令(如: "naabu -l domains.txt -json") + tool_name: 工具名称(如: "naabu") + scan_id: 扫描任务 ID + target_id: 目标 ID + cwd: 工作目录(可选) + shell: 是否使用 shell 执行(默认 False) + batch_size: 批量保存大小(默认1000) + timeout: 命令执行超时时间(秒),None 表示不设置超时 + log_file: 日志文件路径(可选) + + Returns: + dict: { + 'processed_records': int, # 处理的记录总数 + 'created_ips': int, # 创建的IP记录数 + 'created_ports': int, # 创建的端口记录数 + 'skipped_no_subdomain': int, # 因域名不存在跳过的记录数 + 'skipped_no_ip': int, # 因IP不存在跳过的记录数 + } + + Raises: + ValueError: 参数验证失败 + RuntimeError: 命令执行或数据库操作失败 + subprocess.TimeoutExpired: 命令执行超时 + + Performance: + - 流式处理,实时解析命令输出 + - 内存占用恒定(只存储一个 batch) + - 复用现有的批次保存和重试逻辑 + - 使用事务确保数据一致性 + """ + logger.info( + "开始执行流式端口扫描任务 - target_id=%s, 超时=%s秒, 命令: %s", + target_id, timeout if timeout else '无限制', cmd + ) + + data_generator = None + + try: + # 1. 验证参数 + _validate_task_parameters(cmd, target_id, scan_id, cwd) + + # 2. 初始化资源 + data_generator = _parse_naabu_stream_output(cmd, tool_name, cwd, shell, timeout, log_file) + services = ServiceSet.create_default() + + # 3. 流式处理记录并分批保存 + stats = _process_records_in_batches( + data_generator, scan_id, target_id, batch_size, services + ) + + # 4. 构建最终结果 + return _build_final_result(stats) + + except CancelledError: + # Prefect 取消信号:终止任务并标记为取消(让上层 handler 触发状态更新) + logger.warning( + "⚠️ 端口扫描任务检测到取消信号,正在终止 - scan_id=%s, target_id=%s", + scan_id, target_id + ) + raise + + except subprocess.TimeoutExpired: + # 超时异常:部分数据已保存,但扫描未完成 + # 这是预期行为:流式处理会实时保存已解析的数据 + logger.warning( + "⚠️ 端口扫描任务超时 - target_id=%s, 超时=%s秒\n" + "注意:超时前已解析的数据已保存到数据库,但扫描未完全完成。\n" + "建议:增加超时时间或减少扫描目标数量。", + target_id, timeout + ) + raise # 直接重新抛出,保留异常类型 + + except Exception as e: + error_msg = f"流式执行端口扫描任务失败: {e}" + logger.error(error_msg, exc_info=True) + raise RuntimeError(error_msg) from e + + finally: + # 5. 清理资源 + _cleanup_resources(data_generator) + + +def _raise_if_cancelled(scan_id: int) -> None: + """检测扫描是否已请求取消,若已取消则抛出 CancelledError 以触发 Prefect 取消流程。""" + status = Scan.objects.filter(id=scan_id).values_list("status", flat=True).first() + if status == ScanStatus.CANCELLED: + logger.warning("检测到取消信号,终止端口扫描 - scan_id=%s", scan_id) + raise CancelledError() diff --git a/backend/apps/scan/tasks/port_scan/types.py b/backend/apps/scan/tasks/port_scan/types.py new file mode 100644 index 00000000..4418c8b2 --- /dev/null +++ b/backend/apps/scan/tasks/port_scan/types.py @@ -0,0 +1,32 @@ +""" +端口扫描相关类型定义 + +定义端口扫描流程中的数据结构,确保类型安全 +""" + +from typing import TypedDict + + +class PortScanRecord(TypedDict): + """ + 端口扫描记录类型定义 + + 说明: + 这是端口扫描的标准输出格式,包含: + - host: 被扫描的域名 + - ip: 域名解析后的 IP 地址(扫描工具自动解析) + - port: 发现的开放端口 + + 用途: + - 解析器输出:parse_naabu_result_task + - 保存器输入:save_ports_task + + 注意: + IP 是端口扫描的必然产物,因为: + 1. 扫描工具需要先解析域名到 IP + 2. 端口属于 IP,而非域名 + 3. 同一域名可能有多个 IP + """ + host: str # 域名(如:www.example.com) + ip: str # IP 地址(如:1.1.1.1) + port: int # 端口号(如:80, 443, 22) diff --git a/backend/apps/scan/tasks/site_scan/__init__.py b/backend/apps/scan/tasks/site_scan/__init__.py new file mode 100644 index 00000000..01cd8e7b --- /dev/null +++ b/backend/apps/scan/tasks/site_scan/__init__.py @@ -0,0 +1,15 @@ +""" +站点扫描任务模块 + +包含站点扫描相关的所有任务: +- export_site_urls_task: 导出站点URL到文件 +- run_and_stream_save_websites_task: 流式运行httpx扫描并实时保存结果 +""" + +from .export_site_urls_task import export_site_urls_task +from .run_and_stream_save_websites_task import run_and_stream_save_websites_task + +__all__ = [ + 'export_site_urls_task', + 'run_and_stream_save_websites_task', +] diff --git a/backend/apps/scan/tasks/site_scan/export_site_urls_task.py b/backend/apps/scan/tasks/site_scan/export_site_urls_task.py new file mode 100644 index 00000000..fd863395 --- /dev/null +++ b/backend/apps/scan/tasks/site_scan/export_site_urls_task.py @@ -0,0 +1,119 @@ +""" +导出站点URL到文件的Task + +直接使用 HostPortMapping 表查询 host+port 组合,拼接成URL格式写入文件 +""" +import logging +from pathlib import Path +from prefect import task + +from apps.asset.services import HostPortMappingService + +logger = logging.getLogger(__name__) + + +@task(name="export_site_urls") +def export_site_urls_task( + target_id: int, + output_file: str, + batch_size: int = 1000 +) -> dict: + """ + 导出目标下的所有站点URL到文件(基于 HostPortMapping 表) + + 功能: + 1. 从 HostPortMapping 表查询 target 下所有 host+port 组合 + 2. 拼接成URL格式(标准端口80/443将省略端口号) + 3. 写入到指定文件中 + + Args: + target_id: 目标ID + output_file: 输出文件路径(绝对路径) + batch_size: 每次处理的批次大小,默认1000(暂未使用,预留) + + Returns: + dict: { + 'success': bool, + 'output_file': str, + 'total_urls': int, + 'association_count': int # 主机端口关联数量 + } + + Raises: + ValueError: 参数错误 + IOError: 文件写入失败 + """ + try: + logger.info("开始统计站点URL - Target ID: %d, 输出文件: %s", target_id, output_file) + + # 确保输出目录存在 + output_path = Path(output_file) + output_path.parent.mkdir(parents=True, exist_ok=True) + + # 直接查询 HostPortMapping 表,按 host 排序 + service = HostPortMappingService() + associations = service.iter_host_port_by_target( + target_id=target_id, + batch_size=batch_size, + ) + + total_urls = 0 + association_count = 0 + + # 流式写入文件 + with open(output_path, 'w', encoding='utf-8', buffering=8192) as f: + for assoc in associations: + association_count += 1 + host = assoc['host'] + port = assoc['port'] + + # 根据端口号生成URL + # 80 端口:只生成 HTTP URL(省略端口号) + # 443 端口:只生成 HTTPS URL(省略端口号) + # 其他端口:生成 HTTP 和 HTTPS 两个URL(带端口号) + if port == 80: + # HTTP 标准端口,省略端口号 + url = f"http://{host}" + f.write(f"{url}\n") + total_urls += 1 + elif port == 443: + # HTTPS 标准端口,省略端口号 + url = f"https://{host}" + f.write(f"{url}\n") + total_urls += 1 + else: + # 非标准端口,生成 HTTP 和 HTTPS 两个URL + http_url = f"http://{host}:{port}" + https_url = f"https://{host}:{port}" + f.write(f"{http_url}\n") + f.write(f"{https_url}\n") + total_urls += 2 + + # 每处理1000条记录打印一次进度 + if association_count % 1000 == 0: + logger.info("已处理 %d 条关联,生成 %d 个URL...", association_count, total_urls) + + logger.info( + "✓ 站点URL导出完成 - 关联数: %d, 总URL数: %d, 文件: %s (%.2f KB)", + association_count, + total_urls, + str(output_path), + output_path.stat().st_size / 1024 + ) + + return { + 'success': True, + 'output_file': str(output_path), + 'total_urls': total_urls, + 'association_count': association_count + } + + except FileNotFoundError as e: + logger.error("输出目录不存在: %s", e) + raise + except PermissionError as e: + logger.error("文件写入权限不足: %s", e) + raise + except Exception as e: + logger.exception("导出站点URL失败: %s", e) + raise diff --git a/backend/apps/scan/tasks/site_scan/run_and_stream_save_websites_task.py b/backend/apps/scan/tasks/site_scan/run_and_stream_save_websites_task.py new file mode 100644 index 00000000..af277fd3 --- /dev/null +++ b/backend/apps/scan/tasks/site_scan/run_and_stream_save_websites_task.py @@ -0,0 +1,767 @@ +""" +基于 execute_stream 的流式站点扫描任务 + +主要功能: + 1. 实时执行站点扫描命令(httpx) + 2. 流式处理命令输出,实时解析为 HttpxRecord + 3. 批量保存到数据库,复用现有的字段校验与统计逻辑 + 4. 避免生成大量临时文件,提高效率 + +数据流向: + 命令执行 → 流式输出 → 实时解析 → 批量保存 → 数据库 + + 输入:扫描命令及参数 + 输出:WebSite 记录 + +优化策略: + - 使用 execute_stream 实时处理输出 + - 复用现有的 _save_batch_with_retry 逻辑 + - 流式处理避免内存溢出 + - 批量操作减少数据库交互 +""" + +import logging +import json +import subprocess +import time +from pathlib import Path +from prefect import task +from typing import Generator, Optional, Dict, Any, TYPE_CHECKING +from django.db import IntegrityError, OperationalError, DatabaseError +from dataclasses import dataclass +from urllib.parse import urlparse, urlunparse +from dateutil.parser import parse as parse_datetime +from psycopg2 import InterfaceError + +from apps.asset.dtos.snapshot import WebsiteSnapshotDTO + +from apps.scan.utils import execute_stream + +# 类型检查时导入,运行时不导入(避免循环依赖) +if TYPE_CHECKING: + from apps.asset.services.snapshot import WebsiteSnapshotsService + +logger = logging.getLogger(__name__) + + +@dataclass +class ServiceSet: + """ + Service 集合,用于依赖注入 + + 提供所有需要的 Service 实例,便于测试时注入 Mock 对象 + """ + snapshot: "WebsiteSnapshotsService" + + @classmethod + def create_default(cls) -> 'ServiceSet': + """创建默认的 Service 集合""" + from apps.asset.services.snapshot import WebsiteSnapshotsService + return cls( + snapshot=WebsiteSnapshotsService() + ) + + +def normalize_url(url: str) -> str: + """ + 标准化 URL,移除默认端口号 + + 处理规则: + - HTTPS 协议的 443 端口 → 移除端口号 + - HTTP 协议的 80 端口 → 移除端口号 + - 其他端口 → 保留端口号 + + Args: + url: 原始 URL(如 https://www.example.com:443) + + Returns: + str: 标准化后的 URL(如 https://www.example.com) + + Examples: + >>> normalize_url('https://www.example.com:443') + 'https://www.example.com' + >>> normalize_url('http://www.example.com:80') + 'http://www.example.com' + >>> normalize_url('https://www.example.com:8443') + 'https://www.example.com:8443' + """ + try: + parsed = urlparse(url) + + # 检查是否需要移除端口号 + should_remove_port = ( + (parsed.scheme == 'https' and parsed.port == 443) or + (parsed.scheme == 'http' and parsed.port == 80) + ) + + if should_remove_port: + # 重建 URL,不包含端口号 + # netloc 可能是 'domain:port' 格式,需要只保留 hostname + netloc = parsed.hostname or parsed.netloc.split(':')[0] + normalized = urlunparse(( + parsed.scheme, + netloc, + parsed.path, + parsed.params, + parsed.query, + parsed.fragment + )) + return normalized + + # 不需要修改,返回原 URL + return url + + except Exception as e: + # 解析失败,返回原 URL + logger.debug("URL 标准化失败: %s,使用原始 URL: %s", e, url) + return url + + +class HttpxRecord: + """httpx 扫描记录数据类""" + + def __init__(self, data: Dict[str, Any]): + self.url = data.get('url', '') + self.input = data.get('input', '') + self.title = data.get('title', '') + self.status_code = data.get('status_code') + self.content_length = data.get('content_length') + self.content_type = data.get('content_type', '') + self.location = data.get('location', '') + self.webserver = data.get('webserver', '') + self.body_preview = data.get('body_preview', '') + self.tech = data.get('tech', []) + self.vhost = data.get('vhost') + self.failed = data.get('failed', False) + self.timestamp = data.get('timestamp') + + # 从 URL 中提取主机名 + self.host = self._extract_hostname() + + def _extract_hostname(self) -> str: + """ + 从 URL 或 input 字段提取主机名 + + 优先级: + 1. 使用 urlparse 解析 URL 获取 hostname + 2. 从 input 字段提取(处理可能包含协议的情况) + 3. 从 URL 字段手动提取(降级方案) + + Returns: + str: 提取的主机名(小写) + """ + try: + # 方法 1: 使用 urlparse 解析 URL + if self.url: + parsed = urlparse(self.url) + if parsed.hostname: + return parsed.hostname + + # 方法 2: 从 input 字段提取 + if self.input: + host = self.input.strip().lower() + # 移除协议前缀 + if host.startswith(('http://', 'https://')): + host = host.split('//', 1)[1].split('/')[0] + return host + + # 方法 3: 从 URL 手动提取(降级方案) + if self.url: + return self.url.replace('http://', '').replace('https://', '').split('/')[0] + + # 兜底:返回空字符串 + return '' + + except Exception as e: + # 异常处理:尽力从 input 或 URL 提取 + logger.debug("提取主机名失败: %s,使用降级方案", e) + if self.input: + return self.input.strip().lower() + if self.url: + return self.url.replace('http://', '').replace('https://', '').split('/')[0] + return '' + + +def _save_batch_with_retry( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet, + max_retries: int = 3 +) -> dict: + """ + 保存一个批次的数据(带重试机制) + + Args: + batch: 要保存的记录批次 + scan_id: 扫描任务ID + target_id: 目标ID + batch_num: 批次编号 + services: Service 集合(必须,依赖注入) + max_retries: 最大重试次数 + + Returns: + dict: { + 'success': bool, + 'created_websites': int, + 'skipped_failed': int + } + """ + for attempt in range(max_retries): + try: + stats = _save_batch(batch, scan_id, target_id, batch_num, services) + return { + 'success': True, + 'created_websites': stats.get('created_websites', 0), + 'skipped_failed': stats.get('skipped_failed', 0) + } + + except IntegrityError as e: + # 数据完整性错误,不应重试(IntegrityError 是 DatabaseError 的子类,需先处理) + logger.error("批次 %d 数据完整性错误,跳过: %s", batch_num, str(e)[:100]) + return { + 'success': False, + 'created_websites': 0, + 'skipped_failed': 0 + } + + except (OperationalError, DatabaseError, InterfaceError) as e: + # 数据库连接/操作错误,可重试 + if attempt < max_retries - 1: + wait_time = 2 ** attempt # 指数退避: 1s, 2s, 4s + logger.warning( + "批次 %d 保存失败(第 %d 次尝试),%d秒后重试: %s", + batch_num, attempt + 1, wait_time, str(e)[:100] + ) + time.sleep(wait_time) + else: + logger.error("批次 %d 保存失败(已重试 %d 次): %s", batch_num, max_retries, e) + return { + 'success': False, + 'created_websites': 0, + 'skipped_failed': 0 + } + + except Exception as e: + # 其他未知错误 - 检查是否为连接问题 + error_str = str(e).lower() + if 'connection' in error_str and attempt < max_retries - 1: + logger.warning( + "批次 %d 连接相关错误(尝试 %d/%d): %s,Repository 装饰器会自动重连", + batch_num, attempt + 1, max_retries, str(e) + ) + time.sleep(2) + else: + logger.error("批次 %d 未知错误: %s", batch_num, e, exc_info=True) + return { + 'success': False, + 'created_websites': 0, + 'skipped_failed': 0 + } + + return { + 'success': False, + 'created_websites': 0, + 'skipped_failed': 0 + } + + +def _save_batch( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet +) -> dict: + """ + 保存一个批次的数据到数据库(使用快照 Service) + + 数据关系链: + Subdomain (已存在) → WebsiteSnapshot (待创建) → WebSite (自动同步) + + 处理流程: + 1. 查询 Subdomain:根据域名批量查询(LRU缓存) + 2. 构建 WebsiteSnapshotDTO:包含 scan_id 和 target_id + 3. 调用快照 Service:save_and_sync() 自动保存快照并同步到资产表 + + Args: + batch: 数据批次,list of HttpxRecord + scan_id: 扫描任务 ID + target_id: 目标 ID + batch_num: 批次编号(用于日志) + services: Service 集合(依赖注入) + + Returns: + dict: 包含创建和跳过记录的统计信息 + + Raises: + TypeError: batch 参数类型错误 + IntegrityError: 数据完整性错误 + OperationalError: 数据库操作错误 + DatabaseError: 其他数据库错误 + + Note: + 此函数不包含重试逻辑,由外层 _save_batch_with_retry 负责重试 + """ + # 参数验证 + if not isinstance(batch, list): + raise TypeError(f"batch 必须是 list 类型,实际: {type(batch).__name__}") + + if not batch: + logger.debug("批次 %d 为空,跳过处理", batch_num) + return { + 'created_websites': 0, + 'skipped_failed': 0 + } + + # 统计变量 + skipped_failed = 0 + + # ========== Step 1: 准备 WebsiteSnapshot 数据(内存操作,无需事务)========== + snapshot_items = [] + + for record in batch: + # 跳过失败的请求 + if record.failed: + skipped_failed += 1 + continue + + # 解析时间戳 + created_at = None + if hasattr(record, 'timestamp') and record.timestamp: + try: + created_at = parse_datetime(record.timestamp) + except (ValueError, TypeError) as e: + logger.warning(f"无法解析时间戳 {record.timestamp}: {e}") + + # 使用 input 字段(原始扫描的 URL)而不是 url 字段(重定向后的 URL) + # 原因:避免多个不同的输入 URL 重定向到同一个 URL 时产生唯一约束冲突 + # 例如:http://example.com 和 https://example.com 都重定向到 https://example.com + # 如果使用 record.url,两条记录会有相同的 url,导致数据库冲突 + # 如果使用 record.input,两条记录保留原始输入,不会冲突 + normalized_url = normalize_url(record.input) + + # 提取 host 字段(域名或IP地址) + host = record.host if record.host else '' + + # 创建 WebsiteSnapshot DTO + snapshot_dto = WebsiteSnapshotDTO( + scan_id=scan_id, + target_id=target_id, # 主关联字段 + url=normalized_url, # 保存原始输入 URL(归一化后) + host=host, # 主机名(域名或IP地址) + location=record.location, # location 字段保存重定向信息 + title=record.title[:1000] if record.title else '', + web_server=record.webserver[:200] if record.webserver else '', + body_preview=record.body_preview[:1000] if record.body_preview else '', + content_type=record.content_type[:200] if record.content_type else '', + tech=record.tech if isinstance(record.tech, list) else [], + status=record.status_code, + content_length=record.content_length, + vhost=record.vhost + ) + + snapshot_items.append(snapshot_dto) + + # ========== Step 3: 保存快照并同步到资产表(通过快照 Service)========== + if snapshot_items: + services.snapshot.save_and_sync(snapshot_items) + + return { + 'created_websites': len(snapshot_items), + 'skipped_failed': skipped_failed + } + +def _parse_and_validate_line(line: str) -> Optional[HttpxRecord]: + """ + 解析并验证单行 JSON 数据 + + Args: + line: 单行输出数据 + + Returns: + Optional[HttpxRecord]: 有效的 httpx 扫描记录,或 None 如果验证失败 + + 验证步骤: + 1. 解析 JSON 格式 + 2. 验证数据类型为字典 + 3. 创建 HttpxRecord 对象 + 4. 验证必要字段(url) + """ + try: + # 步骤 1: 解析 JSON + try: + line_data = json.loads(line) + except json.JSONDecodeError: + # logger.debug("跳过非 JSON 格式的行: %s", line[:100]) + return None + + # 步骤 2: 验证数据类型 + if not isinstance(line_data, dict): + logger.warning("解析后的数据不是字典类型,跳过: %s", str(line_data)[:100]) + return None + + # 步骤 3: 创建记录 + record = HttpxRecord(line_data) + + # 步骤 4: 验证必要字段 + if not record.url: + logger.debug("URL 为空,跳过") + return None + + # 返回有效记录 + return record + + except Exception as e: + logger.error("解析行数据异常: %s - 数据: %s", e, line[:100]) + return None + + +def _parse_httpx_stream_output( + cmd: str, + tool_name: str, + cwd: Optional[str] = None, + shell: bool = False, + timeout: Optional[int] = None, + log_file: Optional[str] = None +) -> Generator[HttpxRecord, None, None]: + """ + 流式解析 httpx 站点扫描命令输出 + + 基于 execute_stream 实时处理 httpx 命令的 stdout,将每行 JSON 输出 + 转换为 HttpxRecord 格式,沿用现有字段校验逻辑 + + Args: + cmd: httpx 站点扫描命令 + cwd: 工作目录 + shell: 是否使用 shell 执行 + timeout: 命令执行超时时间(秒),None 表示不设置超时 + + Yields: + HttpxRecord: 每次 yield 一条解析后的站点记录 + """ + logger.info("开始流式解析 httpx 站点扫描命令输出 - 命令: %s", cmd) + + total_lines = 0 + error_lines = 0 + valid_records = 0 + + try: + # 使用 execute_stream 获取实时输出流(带工具名、超时控制和日志文件) + for line in execute_stream(cmd=cmd, tool_name=tool_name, cwd=cwd, shell=shell, timeout=timeout, log_file=log_file): + total_lines += 1 + + # 解析并验证单行数据 + record = _parse_and_validate_line(line) + if record is None: + error_lines += 1 + continue + + valid_records += 1 + # yield 一条有效记录 + yield record + + # 每处理 1000 条记录输出一次进度 + if valid_records % 1000 == 0: + logger.info("已解析 %d 条有效记录...", valid_records) + + except subprocess.TimeoutExpired as e: + # 超时异常:简洁输出,不显示堆栈 + error_msg = f"流式解析命令输出超时 - 命令执行超过 {timeout} 秒" + logger.warning(error_msg) # 超时是可预期的 + raise RuntimeError(error_msg) from e + except Exception as e: + # 其他异常:输出详细堆栈以便调试 + logger.error("流式解析命令输出失败: %s", e, exc_info=True) + raise + + logger.info( + "流式解析完成 - 总行数: %d, 有效记录: %d, 错误行数: %d", + total_lines, valid_records, error_lines + ) + + +def _validate_task_parameters(cmd: str, target_id: int, scan_id: int, cwd: Optional[str]) -> None: + """ + 验证任务参数的有效性 + + Args: + cmd: 扫描命令 + target_id: 目标ID + scan_id: 扫描ID + cwd: 工作目录 + + Raises: + ValueError: 参数验证失败 + """ + if not cmd or not cmd.strip(): + raise ValueError("扫描命令不能为空") + + if target_id is None: + raise ValueError("target_id 不能为 None,必须指定目标ID") + + if scan_id is None: + raise ValueError("scan_id 不能为 None,必须指定扫描ID") + + # 验证工作目录(如果指定) + if cwd and not Path(cwd).exists(): + raise ValueError(f"工作目录不存在: {cwd}") + + +def _accumulate_batch_stats(total_stats: dict, batch_result: dict) -> None: + """ + 累加批次统计信息 + + Args: + total_stats: 总统计信息字典 + batch_result: 批次结果字典 + """ + total_stats['created_websites'] += batch_result.get('created_websites', 0) + total_stats['skipped_failed'] += batch_result.get('skipped_failed', 0) + + +def _process_batch( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + total_stats: dict, + failed_batches: list, + services: ServiceSet +) -> None: + """ + 处理单个批次 + + Args: + batch: 数据批次 + scan_id: 扫描ID + target_id: 目标ID + batch_num: 批次编号 + total_stats: 总统计信息 + failed_batches: 失败批次列表 + services: Service 集合(必须,依赖注入) + """ + result = _save_batch_with_retry( + batch, scan_id, target_id, batch_num, services + ) + + # 累计统计信息(失败时可能有部分数据已保存) + _accumulate_batch_stats(total_stats, result) + + if not result['success']: + failed_batches.append(batch_num) + logger.warning( + "批次 %d 保存失败,但已累计统计信息:创建站点=%d", + batch_num, result.get('created_websites', 0) + ) + + +def _process_records_in_batches( + data_generator, + scan_id: int, + target_id: int, + batch_size: int, + services: ServiceSet +) -> dict: + """ + 流式处理记录并分批保存 + + Args: + data_generator: 数据生成器 + scan_id: 扫描ID + target_id: 目标ID + batch_size: 批次大小 + services: Service 集合(必须,依赖注入) + + Returns: + dict: 处理统计信息 + + Raises: + RuntimeError: 存在失败批次时抛出 + """ + total_records = 0 + batch_num = 0 + failed_batches = [] + batch = [] + + # 统计信息 + total_stats = { + 'created_websites': 0, + 'skipped_failed': 0 + } + + # 流式读取生成器并分批保存 + for record in data_generator: + batch.append(record) + total_records += 1 + + # 达到批次大小,执行保存 + if len(batch) >= batch_size: + batch_num += 1 + _process_batch(batch, scan_id, target_id, batch_num, total_stats, failed_batches, services) + batch = [] # 清空批次 + + # 每20个批次输出进度 + if batch_num % 20 == 0: + logger.info("进度: 已处理 %d 批次,%d 条记录", batch_num, total_records) + + # 保存最后一批 + if batch: + batch_num += 1 + _process_batch(batch, scan_id, target_id, batch_num, total_stats, failed_batches, services) + + # 检查失败批次 + if failed_batches: + error_msg = ( + f"流式保存站点扫描结果时出现失败批次,处理记录: {total_records}," + f"失败批次: {failed_batches}" + ) + logger.warning(error_msg) # 超时是可预期的 + raise RuntimeError(error_msg) + + return { + 'processed_records': total_records, + 'batch_count': batch_num, + **total_stats + } + + +def _build_final_result(stats: dict) -> dict: + """ + 构建最终结果并输出日志 + + Args: + stats: 处理统计信息 + + Returns: + dict: 最终结果 + """ + logger.info( + "✓ 流式保存完成 - 处理记录: %d(%d 批次),创建站点: %d,跳过(失败): %d", + stats['processed_records'], stats['batch_count'], stats['created_websites'], + stats['skipped_failed'] + ) + + # 如果没有创建任何记录,给出明确提示 + if stats['created_websites'] == 0: + logger.warning( + "⚠️ 没有创建任何站点记录!可能原因:1) 域名不在数据库中 2) 命令输出格式问题 3) 重复数据被忽略 4) 所有请求都失败" + ) + + return { + 'processed_records': stats['processed_records'], + 'created_websites': stats['created_websites'], + 'skipped_failed': stats['skipped_failed'] + } + + +def _cleanup_resources(data_generator) -> None: + """ + 清理任务资源 + + Args: + data_generator: 数据生成器 + """ + # 注:LRUCache 是局部变量,函数结束时会自动释放,无需手动 clear() + + # 确保生成器被正确关闭 + if data_generator is not None: + try: + data_generator.close() + logger.debug("已关闭数据生成器") + except Exception as gen_close_error: + logger.error("关闭生成器时出错: %s", gen_close_error) + + +@task( + name='run_and_stream_save_websites', + retries=0, + log_prints=True +) +def run_and_stream_save_websites_task( + cmd: str, + tool_name: str, + scan_id: int, + target_id: int, + cwd: Optional[str] = None, + shell: bool = False, + batch_size: int = 1000, + timeout: Optional[int] = None, + log_file: Optional[str] = None +) -> dict: + """ + 执行 httpx 站点扫描命令并流式保存结果到数据库 + + 该任务将: + 1. 验证输入参数 + 2. 初始化资源(缓存、生成器) + 3. 流式处理记录并分批保存 + 4. 构建并返回结果统计 + + Args: + cmd: httpx 站点扫描命令 + scan_id: 扫描任务 ID + target_id: 目标 ID + cwd: 工作目录(可选) + shell: 是否使用 shell 执行(默认 False) + batch_size: 批量保存大小(默认1000) + timeout: 命令执行超时时间(秒),None 表示不设置超时 + + Returns: + dict: { + 'processed_records': int, # 处理的记录总数 + 'created_websites': int, # 创建的站点记录数 + 'skipped_failed': int, # 因请求失败跳过的记录数 + } + + Raises: + ValueError: 参数验证失败 + RuntimeError: 命令执行或数据库操作失败 + subprocess.TimeoutExpired: 命令执行超时 + + Performance: + - 流式处理,实时解析命令输出 + - 内存占用恒定(只存储一个 batch) + - 复用现有的批次保存和重试逻辑 + - 使用事务确保数据一致性 + """ + logger.info( + "开始执行流式站点扫描任务 - target_id=%s, 超时=%s秒, 命令: %s", + target_id, timeout if timeout else '无限制', cmd + ) + + data_generator = None + + try: + # 1. 验证参数 + _validate_task_parameters(cmd, target_id, scan_id, cwd) + + # 2. 初始化资源 + data_generator = _parse_httpx_stream_output(cmd, tool_name, cwd, shell, timeout, log_file) + services = ServiceSet.create_default() + + # 3. 流式处理记录并分批保存 + stats = _process_records_in_batches( + data_generator, scan_id, target_id, batch_size, services + ) + + # 4. 构建最终结果 + return _build_final_result(stats) + + except subprocess.TimeoutExpired: + # 超时异常直接向上传播,保留异常类型 + logger.warning( + "⚠️ 站点扫描任务超时 - target_id=%s, 超时=%s秒", + target_id, timeout + ) + raise # 直接重新抛出,不包装 + + except Exception as e: + error_msg = f"流式执行站点扫描任务失败: {e}" + logger.error(error_msg, exc_info=True) + raise RuntimeError(error_msg) from e + + finally: + # 5. 清理资源 + _cleanup_resources(data_generator) diff --git a/backend/apps/scan/tasks/subdomain_discovery/__init__.py b/backend/apps/scan/tasks/subdomain_discovery/__init__.py new file mode 100644 index 00000000..bde1700a --- /dev/null +++ b/backend/apps/scan/tasks/subdomain_discovery/__init__.py @@ -0,0 +1,24 @@ +""" +子域名发现任务模块 + +包含子域名扫描流程的 Prefect Tasks: +- run_subdomain_discovery_task: 运行单个子域名发现工具(可并行) +- merge_and_validate_task: 合并、解析并验证域名(一体化高性能) +- save_domains_task: 保存到数据库 + +架构优势: +- 每个 task 单一职责,可独立重试 +- 支持并行执行扫描工具 +- 流式处理 + 正则验证,性能提升 50-100 倍 +- Flow 层编排,逻辑清晰 +""" + +from .run_subdomain_discovery_task import run_subdomain_discovery_task +from .merge_and_validate_task import merge_and_validate_task +from .save_domains_task import save_domains_task + +__all__ = [ + 'run_subdomain_discovery_task', + 'merge_and_validate_task', + 'save_domains_task', +] diff --git a/backend/apps/scan/tasks/subdomain_discovery/merge_and_validate_task.py b/backend/apps/scan/tasks/subdomain_discovery/merge_and_validate_task.py new file mode 100644 index 00000000..70bd6027 --- /dev/null +++ b/backend/apps/scan/tasks/subdomain_discovery/merge_and_validate_task.py @@ -0,0 +1,195 @@ +""" +合并并去重域名任务 + +合并 merge + parse + validate 三个步骤,优化性能: +- 单命令实现(LC_ALL=C sort -u) +- C语言级性能,单进程高效 +- 无临时文件,零额外开销 +- 支持千万级数据处理 + +性能优势: +- LC_ALL=C 字节序比较(比locale快20-30%) +- 单进程直接处理多文件(无管道开销) +- 内存占用恒定(~50MB for 50万域名) +- 50万域名处理时间:~0.5秒(相比 Python 提升 ~67%) + +Note: + - 工具(amass/subfinder)输出已标准化(小写,无空行) + - sort -u 自动处理去重和排序 + - 无需额外过滤,性能最优 +""" + +import logging +import uuid +import subprocess +from pathlib import Path +from datetime import datetime +from prefect import task +from typing import List + +logger = logging.getLogger(__name__) + +# 注:使用纯系统命令实现,无需 Python 缓冲区配置 +# 工具(amass/subfinder)输出已是小写且标准化 + +@task( + name='merge_and_deduplicate', + retries=1, + log_prints=True +) +def merge_and_validate_task( + result_files: List[str], + result_dir: str +) -> str: + """ + 合并扫描结果并去重(高性能流式处理) + + 流程: + 1. 使用 LC_ALL=C sort -u 直接处理多文件 + 2. 排序去重一步完成 + 3. 返回去重后的文件路径 + + 命令:LC_ALL=C sort -u file1 file2 file3 -o output + 注:工具输出已标准化(小写,无空行),无需额外处理 + + Args: + result_files: 结果文件路径列表 + result_dir: 结果目录 + + Returns: + str: 去重后的域名文件路径 + + Raises: + RuntimeError: 处理失败 + + Performance: + - 纯系统命令(C语言实现),单进程极简 + - LC_ALL=C: 字节序比较 + - sort -u: 直接处理多文件(无管道开销) + + Design: + - 极简单命令,无冗余处理 + - 单进程直接执行(无管道/重定向开销) + - 内存占用仅在 sort 阶段(外部排序,不会 OOM) + """ + logger.info("开始合并并去重 %d 个结果文件(系统命令优化)", len(result_files)) + + result_path = Path(result_dir) + + # 验证文件存在性 + valid_files = [] + for file_path_str in result_files: + file_path = Path(file_path_str) + if file_path.exists(): + valid_files.append(str(file_path)) + else: + logger.warning("结果文件不存在: %s", file_path) + + if not valid_files: + raise RuntimeError("所有结果文件都不存在") + + # 生成输出文件路径 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + short_uuid = uuid.uuid4().hex[:4] + merged_file = result_path / f"merged_{timestamp}_{short_uuid}.txt" + + try: + # ==================== 使用系统命令一步完成:排序去重 ==================== + # LC_ALL=C: 使用字节序比较(比locale快20-30%) + # sort -u: 直接处理多文件,排序去重 + # -o: 安全输出(比重定向更可靠) + cmd = f"LC_ALL=C sort -u {' '.join(valid_files)} -o {merged_file}" + + logger.debug("执行命令: %s", cmd) + + # 按输入文件总行数动态计算超时时间 + total_lines = 0 + for file_path in valid_files: + try: + line_count_proc = subprocess.run( + ["wc", "-l", file_path], + check=True, + capture_output=True, + text=True, + ) + total_lines += int(line_count_proc.stdout.strip().split()[0]) + except (subprocess.CalledProcessError, ValueError, IndexError): + continue + + timeout = 3600 + if total_lines > 0: + # 按行数线性计算:每行约 0.1 秒 + base_per_line = 0.1 + est = int(total_lines * base_per_line) + timeout = max(600, est) + + logger.info( + "Subdomain 合并去重 timeout 自动计算: 输入总行数=%d, timeout=%d秒", + total_lines, + timeout, + ) + + result = subprocess.run( + cmd, + shell=True, + check=True, + timeout=timeout + ) + + logger.debug("✓ 合并去重完成") + + # ==================== 统计结果 ==================== + if not merged_file.exists(): + raise RuntimeError("合并文件未被创建") + + # 统计行数(使用系统命令提升大文件性能) + try: + line_count_proc = subprocess.run( + ["wc", "-l", str(merged_file)], + check=True, + capture_output=True, + text=True + ) + unique_count = int(line_count_proc.stdout.strip().split()[0]) + except (subprocess.CalledProcessError, ValueError, IndexError) as e: + logger.warning( + "wc -l 统计失败(文件: %s),降级为 Python 逐行统计 - 错误: %s", + merged_file, e + ) + unique_count = 0 + with open(merged_file, 'r', encoding='utf-8') as file_obj: + for _ in file_obj: + unique_count += 1 + + if unique_count == 0: + raise RuntimeError("未找到任何有效域名") + + file_size = merged_file.stat().st_size + + logger.info( + "✓ 合并去重完成 - 去重后: %d 个域名, 文件大小: %.2f KB", + unique_count, + file_size / 1024 + ) + + return str(merged_file) + + except subprocess.TimeoutExpired: + error_msg = "合并去重超时(>60分钟),请检查数据量或系统资源" + logger.warning(error_msg) # 超时是可预期的 + raise RuntimeError(error_msg) + + except subprocess.CalledProcessError as e: + error_msg = f"系统命令执行失败: {e.stderr if e.stderr else str(e)}" + logger.warning(error_msg) # 超时是可预期的 + raise RuntimeError(error_msg) from e + + except IOError as e: + error_msg = f"文件读写失败: {e}" + logger.warning(error_msg) # 超时是可预期的 + raise RuntimeError(error_msg) from e + + except Exception as e: + error_msg = f"合并去重失败: {e}" + logger.error(error_msg, exc_info=True) + raise diff --git a/backend/apps/scan/tasks/subdomain_discovery/run_subdomain_discovery_task.py b/backend/apps/scan/tasks/subdomain_discovery/run_subdomain_discovery_task.py new file mode 100644 index 00000000..61162ca4 --- /dev/null +++ b/backend/apps/scan/tasks/subdomain_discovery/run_subdomain_discovery_task.py @@ -0,0 +1,90 @@ +""" +运行扫描工具任务 + +负责运行单个子域名扫描工具(amass、subfinder 等) +""" + +import logging +from pathlib import Path +from prefect import task +from apps.scan.utils import execute_and_wait + +logger = logging.getLogger(__name__) + + +@task( + name='run_subdomain_discovery', + retries=0, # 显式禁用重试 + log_prints=True +) +def run_subdomain_discovery_task( + tool: str, + command: str, + timeout: int, + output_file: str +) -> str: + """ + 运行单个子域名发现工具 + + Args: + tool: 子域名发现工具名称(用于日志) + command: 完整的扫描命令(已由 Flow 层使用命令构建器生成) + timeout: 命令执行超时时间(秒) + output_file: 输出文件完整路径(由 Flow 层生成,包含目录和文件名) + + Returns: + str: 结果文件路径 + + Raises: + ValueError: 参数验证失败 + RuntimeError: 扫描执行失败 + + Note: + - 扫描结果通过工具的 -o 参数写入结果文件 + - 使用通用的 run_scan_command 函数执行扫描 + - 日志文件统一随 workspace 管理,默认保留 7 天自动清理 + - 文件命名格式:{tool}_{timestamp}_{uuid4}.txt + - 示例:subfinder_20250116_142200_a3f2.txt, subfinder_20250116_142200_a3f2.log + """ + # 准备路径 + output_file_path = Path(output_file) + log_file = str(output_file_path.with_suffix('.log')) + + # 使用通用的扫描命令执行器 + try: + result = execute_and_wait( + tool_name=tool, + command=command, + timeout=timeout, + log_file=log_file # 明确指定日志文件路径 + ) + + # 验证输出文件是否生成 + if not output_file_path.exists(): + logger.warning( + "扫描工具 %s 未生成结果文件: %s (returncode: %d)", + tool, str(output_file_path), result['returncode'] # 强制转换为字符串 + ) + return "" + + # 检查文件大小 + file_size = output_file_path.stat().st_size + if file_size == 0: + logger.warning("扫描工具 %s 生成的结果文件为空: %s", tool, output_file_path) + return "" # 空文件视为无效结果,与未生成文件的行为一致 + + logger.info( + "✓ 扫描完成: %s - 结果文件: %s (%.2f KB)", + tool, str(output_file_path), file_size / 1024 # 使用绝对路径 + ) + + # 返回结果文件路径 + return str(output_file_path) + + except RuntimeError: + # 直接向上抛出(已在 execute_and_wait 中记录日志) + raise + except Exception as e: + error_msg = f"扫描工具 {tool} 执行异常: {e}" + logger.error(error_msg, exc_info=True) + raise diff --git a/backend/apps/scan/tasks/subdomain_discovery/save_domains_task.py b/backend/apps/scan/tasks/subdomain_discovery/save_domains_task.py new file mode 100644 index 00000000..cb11e9fa --- /dev/null +++ b/backend/apps/scan/tasks/subdomain_discovery/save_domains_task.py @@ -0,0 +1,243 @@ +""" +保存域名任务 + +负责将验证后的域名批量保存到数据库 +""" + +import logging +import time +from pathlib import Path +from prefect import task +from typing import List +from dataclasses import dataclass +from django.db import IntegrityError, OperationalError, DatabaseError + +from apps.asset.services.snapshot import SubdomainSnapshotsService +from apps.common.validators import validate_domain + +logger = logging.getLogger(__name__) + + +@dataclass +class ServiceSet: + """ + Service 集合,用于依赖注入 + + 封装所有需要的 Service 实例,便于测试和管理。 + """ + snapshot: SubdomainSnapshotsService + + @classmethod + def create_default(cls) -> 'ServiceSet': + """创建默认的 Service 集合""" + return cls( + snapshot=SubdomainSnapshotsService() + ) + + +@task( + name='save_domains', + retries=0, + log_prints=True +) +def save_domains_task( + domains_file: str, + scan_id: int, + target_id: int = None, + batch_size: int = 1000 +) -> dict: + """ + 流式批量保存域名到数据库 + + Args: + domains_file: 域名文件路径(流式读取) + scan_id: 扫描任务 ID + target_id: 目标 ID(可选) + batch_size: 批量保存大小 + + Returns: + dict: { + 'processed_records': int # 处理的域名总数(不是实际创建数) + } + + Raises: + ValueError: 参数验证失败(target_id为None或路径不是文件) + FileNotFoundError: 域名文件不存在 + RuntimeError: 数据库操作失败 + IOError: 文件读取失败 + + Performance: + - 流式读取文件,边读边保存 + - 内存占用恒定(只存储一个 batch) + - 默认batch_size=1000(平衡性能和内存) + - 批次失败自动重试 + + Note: + 由于使用 ignore_conflicts,无法返回实际创建的数量 + """ + logger.info("开始从文件流式保存域名到数据库: %s", domains_file) + + # 参数验证 + if target_id is None: + raise ValueError("target_id 不能为 None,必须指定目标ID") + + # 文件验证 + file_path = Path(domains_file) + if not file_path.exists(): + raise FileNotFoundError(f"域名文件不存在: {domains_file}") + if not file_path.is_file(): + raise ValueError(f"路径不是文件: {domains_file}") + + batch_num = 0 + failed_batches = [] # 记录失败的批次 + total_domains = 0 # 总域名数 + + # 初始化 Service 集合(依赖注入) + services = ServiceSet.create_default() + + try: + # 流式读取并分批保存 + batch = [] + + with open(domains_file, 'r', encoding='utf-8') as f: + for line in f: + domain = line.strip() + + # 验证域名格式(包含空行检查) + try: + validate_domain(domain) + except ValueError as e: + logger.warning("跳过无效域名: %s - %s", domain, e) + continue + + # 只有通过验证的域名才添加到批次和计数 + batch.append(domain) + total_domains += 1 + + # 达到批次大小,执行保存 + if len(batch) >= batch_size: + batch_num += 1 + result = _save_batch_with_retry(batch, scan_id, target_id, batch_num, services) + if not result['success']: + failed_batches.append(batch_num) + logger.warning("批次 %d 保存失败,已记录", batch_num) + + batch = [] # 清空批次 + + # 每20个批次输出进度(减少日志开销) + if batch_num % 20 == 0: + logger.info("进度: 已处理 %d 批次,%d 个域名", batch_num, total_domains) + + # 保存最后一批(可能不足 batch_size) + if batch: + batch_num += 1 + result = _save_batch_with_retry(batch, scan_id, target_id, batch_num, services) + if not result['success']: + failed_batches.append(batch_num) + + # 输出最终统计 + if failed_batches: + error_msg = ( + f"保存域名时出现失败批次,处理域名: {total_domains}," + f"失败批次: {failed_batches}" + ) + logger.error(error_msg) + raise RuntimeError(error_msg) + + logger.info("✓ 保存完成 - 处理域名: %d(%d 批次)", total_domains, batch_num) + + return { + 'processed_records': total_domains + } + + except (IntegrityError, OperationalError, DatabaseError) as e: + error_msg = f"数据库操作失败: {e}" + logger.error(error_msg) + raise RuntimeError(error_msg) from e + + except IOError as e: + error_msg = f"文件读取失败: {e}" + logger.error(error_msg) + raise RuntimeError(error_msg) from e + + except Exception as e: + error_msg = f"保存域名失败: {e}" + logger.error(error_msg, exc_info=True) + raise + + +def _save_batch_with_retry( + batch: List[str], + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet, + max_retries: int = 3 +) -> dict: + """ + 保存一个批次的域名(带重试机制) + + Args: + batch: 域名批次 + scan_id: 扫描ID + target_id: 目标ID + batch_num: 批次编号 + services: Service 集合(依赖注入) + max_retries: 最大重试次数 + + Returns: + dict: {'success': bool} + + Strategy: + 使用 bulk_create + ignore_conflicts + - 新域名:插入 (INSERT) + - 重复域名:忽略(不更新,因为没有探测数据) + """ + # 调试日志:记录传入的参数 + logger.info(f"[调试] _save_batch_with_retry 接收的参数: scan_id={scan_id}, target_id={target_id}, batch_size={len(batch)}") + + # 使用快照 DTO(包含完整的业务上下文) + from apps.asset.dtos import SubdomainSnapshotDTO + items = [ + SubdomainSnapshotDTO( + name=domain, + scan_id=scan_id, + target_id=target_id # 包含 target_id + ) + for domain in batch + ] + + # 调试日志:记录第一个DTO的内容 + if items: + first_item = items[0] + logger.info(f"[调试] 第一个 SubdomainSnapshotDTO: name={first_item.name}, scan_id={first_item.scan_id}, target_id={first_item.target_id}") + + for attempt in range(max_retries): + try: + # DTO 已包含 target_id,无需额外传参 + services.snapshot.save_and_sync(items) + logger.debug("批次 %d: 已处理 %d 个域名", batch_num, len(batch)) + return {'success': True} + + except (OperationalError, DatabaseError) as e: + # 数据库连接/操作错误,可重试 + if attempt < max_retries - 1: + wait_time = 2 ** attempt # 指数退避: 1s, 2s, 4s + logger.warning("批次 %d 保存失败(第 %d 次尝试),%d秒后重试: %s", + batch_num, attempt + 1, wait_time, str(e)[:100]) + time.sleep(wait_time) + else: + logger.error("批次 %d 保存失败(已重试 %d 次): %s", batch_num, max_retries, e) + return {'success': False} + + except IntegrityError as e: + # 数据完整性错误,不应重试 + logger.error("批次 %d 数据完整性错误,跳过: %s", batch_num, str(e)[:100]) + return {'success': False} + + except Exception as e: + # 其他未知错误 + logger.error("批次 %d 未知错误: %s", batch_num, e, exc_info=True) + return {'success': False} + + return {'success': False} diff --git a/backend/apps/scan/tasks/url_fetch/__init__.py b/backend/apps/scan/tasks/url_fetch/__init__.py new file mode 100644 index 00000000..5399708b --- /dev/null +++ b/backend/apps/scan/tasks/url_fetch/__init__.py @@ -0,0 +1,27 @@ +""" +URL 获取任务模块 + +包含 URL 获取相关的所有原子任务: +- export_target_assets_task: 导出目标资产(域名或站点) +- run_url_fetcher_task: 执行 URL 获取工具 +- merge_and_deduplicate_urls_task: 合并去重 URL +- clean_urls_task: 使用 uro 清理 URL +- save_urls_task: 保存 URL 到数据库 +- run_and_stream_save_urls_task: 流式验证并保存存活的 URL +""" + +from .export_target_assets_task import export_target_assets_task +from .run_url_fetcher_task import run_url_fetcher_task +from .merge_and_deduplicate_urls_task import merge_and_deduplicate_urls_task +from .clean_urls_task import clean_urls_task +from .save_urls_task import save_urls_task +from .run_and_stream_save_urls_task import run_and_stream_save_urls_task + +__all__ = [ + 'export_target_assets_task', + 'run_url_fetcher_task', + 'merge_and_deduplicate_urls_task', + 'clean_urls_task', + 'save_urls_task', + 'run_and_stream_save_urls_task', +] diff --git a/backend/apps/scan/tasks/url_fetch/clean_urls_task.py b/backend/apps/scan/tasks/url_fetch/clean_urls_task.py new file mode 100644 index 00000000..d1483958 --- /dev/null +++ b/backend/apps/scan/tasks/url_fetch/clean_urls_task.py @@ -0,0 +1,205 @@ +""" +URL 清理任务 + +使用 uro 工具清理合并后的 URL 列表: +- 去除重复和相似的 URL +- 根据扩展名过滤(whitelist/blacklist) +- 智能过滤无效 URL +""" + +import logging +import subprocess +from pathlib import Path +from datetime import datetime +from prefect import task +from typing import Optional + +from apps.scan.utils import execute_and_wait + +logger = logging.getLogger(__name__) + + +@task( + name='clean_urls_with_uro', + retries=1, + log_prints=True +) +def clean_urls_task( + input_file: str, + output_dir: str, + timeout: int = 60, + whitelist: Optional[list] = None, + blacklist: Optional[list] = None, + filters: Optional[list] = None +) -> dict: + """ + 使用 uro 清理 URL 列表 + + Args: + input_file: 输入的 URL 文件路径 + output_dir: 输出目录 + timeout: 超时时间(秒) + whitelist: 只保留指定扩展名的 URL + blacklist: 排除指定扩展名的 URL + filters: 额外的过滤规则 + + Returns: + dict: { + 'success': bool, + 'output_file': str, + 'input_count': int, + 'output_count': int, + 'removed_count': int + } + """ + input_path = Path(input_file) + output_path = Path(output_dir) + + # 1. 验证输入文件 + if not input_path.exists(): + logger.error("输入文件不存在: %s", input_file) + return { + 'success': False, + 'output_file': input_file, + 'input_count': 0, + 'output_count': 0, + 'removed_count': 0, + 'error': '输入文件不存在' + } + + # 2. 统计输入 URL 数量 + try: + result = subprocess.run( + ['wc', '-l', str(input_path)], + capture_output=True, + text=True, + check=True + ) + input_count = int(result.stdout.strip().split()[0]) + except Exception as e: + logger.warning("统计输入文件行数失败: %s", e) + input_count = 0 + with open(input_path, 'r') as f: + input_count = sum(1 for line in f if line.strip()) + + if input_count == 0: + logger.warning("输入文件为空,跳过 uro 清理") + return { + 'success': True, + 'output_file': input_file, + 'input_count': 0, + 'output_count': 0, + 'removed_count': 0 + } + + logger.info("开始 uro 清理 - 输入 URL 数: %d", input_count) + + # 3. 生成输出文件路径 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + output_file = output_path / f"urls_cleaned_{timestamp}.txt" + + # 4. 构建 uro 命令 + cmd_parts = ['uro', '-i', str(input_path), '-o', str(output_file)] + + if whitelist: + cmd_parts.extend(['-w'] + [str(w) for w in whitelist]) + + if blacklist: + cmd_parts.extend(['-b'] + [str(b) for b in blacklist]) + + if filters: + cmd_parts.extend(['-f'] + [str(f) for f in filters]) + + # 5. 构建命令字符串 + command = ' '.join(cmd_parts) + log_file = str(output_path / f"uro_{timestamp}.log") + + logger.debug("uro 命令: %s", command) + + # 6. 使用 execute_and_wait 执行(会自动发送通知) + try: + result = execute_and_wait( + tool_name='uro', + command=command, + timeout=timeout, + log_file=log_file + ) + + if result['returncode'] != 0: + logger.warning( + "uro 返回非零状态码: %d", + result['returncode'] + ) + # uro 可能正常完成但返回非零,检查输出文件 + if not output_file.exists(): + return { + 'success': False, + 'output_file': input_file, + 'input_count': input_count, + 'output_count': input_count, + 'removed_count': 0, + 'error': f'uro 执行失败 (returncode: {result["returncode"]})' + } + + except RuntimeError as e: + # execute_and_wait 超时或执行失败会抛出 RuntimeError + logger.error("uro 执行失败: %s", e) + return { + 'success': False, + 'output_file': input_file, + 'input_count': input_count, + 'output_count': input_count, + 'removed_count': 0, + 'error': str(e) + } + except Exception as e: + logger.error("uro 执行异常: %s", e) + return { + 'success': False, + 'output_file': input_file, + 'input_count': input_count, + 'output_count': input_count, + 'removed_count': 0, + 'error': str(e) + } + + # 7. 统计清理后的 URL 数量 + output_count = 0 + if output_file.exists(): + try: + result = subprocess.run( + ['wc', '-l', str(output_file)], + capture_output=True, + text=True, + check=True + ) + output_count = int(result.stdout.strip().split()[0]) + except Exception: + with open(output_file, 'r') as f: + output_count = sum(1 for line in f if line.strip()) + else: + logger.warning("uro 未生成输出文件,使用原始文件") + return { + 'success': False, + 'output_file': input_file, + 'input_count': input_count, + 'output_count': input_count, + 'removed_count': 0, + 'error': '未生成输出文件' + } + + removed_count = input_count - output_count + + logger.info( + "✓ uro 清理完成 - 输入: %d, 输出: %d, 移除: %d (%.1f%%)", + input_count, output_count, removed_count, + (removed_count / input_count * 100) if input_count > 0 else 0 + ) + + return { + 'success': True, + 'output_file': str(output_file), + 'input_count': input_count, + 'output_count': output_count, + 'removed_count': removed_count + } diff --git a/backend/apps/scan/tasks/url_fetch/export_target_assets_task.py b/backend/apps/scan/tasks/url_fetch/export_target_assets_task.py new file mode 100644 index 00000000..85b26d19 --- /dev/null +++ b/backend/apps/scan/tasks/url_fetch/export_target_assets_task.py @@ -0,0 +1,128 @@ +""" +导出目标资产任务 + +根据 input_type 导出不同类型的资产到文件: +- domains_file: 导出子域名列表(用于 waymore 等域名级工具) +- sites_file: 导出站点 URL 列表(用于 katana 等站点级工具) + +使用流式写入,避免内存溢出 +""" + +import logging +from pathlib import Path +from prefect import task +from typing import Optional + +logger = logging.getLogger(__name__) + + +@task( + name='export_target_assets', + retries=1, + log_prints=True +) +def export_target_assets_task( + output_file: str, + target_id: int, + scan_id: int, + input_type: str, + batch_size: int = 1000 +) -> dict: + """ + 根据 input_type 导出目标资产到文件 + + Args: + output_file: 输出文件路径 + target_id: 目标 ID + scan_id: 扫描 ID + input_type: 输入类型 ('domains_file' 或 'sites_file') + batch_size: 批次大小(内存优化) + + Returns: + dict: { + 'output_file': str, # 输出文件路径 + 'asset_count': int, # 资产数量 + 'asset_type': str # 资产类型(domains 或 sites) + } + + Raises: + ValueError: 参数错误 + RuntimeError: 执行失败 + """ + try: + logger.info("开始导出目标资产 - 类型: %s", input_type) + + # 确保输出目录存在 + output_path = Path(output_file) + output_path.parent.mkdir(parents=True, exist_ok=True) + + # 根据 input_type 导出不同的资产 + if input_type == 'domains_file': + # 导出子域名列表 + logger.info("从目标 %d 导出域名列表", target_id) + from apps.asset.services import SubdomainService + + # 使用 Service 层的流式接口 + subdomain_service = SubdomainService() + + # 流式写入文件 + asset_count = 0 + with open(output_path, 'w') as f: + # 使用 Service 层的迭代器进行批量处理 + for domain in subdomain_service.iter_subdomain_names_by_target(target_id, batch_size): + f.write(f"{domain}\n") + asset_count += 1 + + # 每写入一批就刷新缓冲区 + if asset_count % batch_size == 0: + f.flush() + + logger.info("✓ 域名导出完成 - 文件: %s, 数量: %d", output_file, asset_count) + + if asset_count == 0: + logger.warning("目标下没有域名") + + return { + 'output_file': output_file, + 'asset_count': asset_count, + 'asset_type': 'domains' + } + + elif input_type == 'sites_file': + # 导出站点 URL 列表(按目标导出) + logger.info("从目标 %d 导出站点 URL 列表", target_id) + from apps.asset.services import WebSiteService + + # 使用 Service 层的流式接口 + website_service = WebSiteService() + + # 流式写入文件 + asset_count = 0 + with open(output_path, 'w') as f: + # 使用 Service 层的迭代器进行批量处理(按目标) + for url in website_service.iter_website_urls_by_target(target_id, batch_size): + f.write(f"{url}\n") + asset_count += 1 + + # 每写入一批就刷新缓冲区 + if asset_count % batch_size == 0: + f.flush() + + logger.info("✓ 站点 URL 导出完成 - 文件: %s, 数量: %d", output_file, asset_count) + + if asset_count == 0: + logger.warning("扫描下没有站点") + + return { + 'output_file': output_file, + 'asset_count': asset_count, + 'asset_type': 'sites' + } + + else: + # 未知的 input_type + raise ValueError(f"不支持的 input_type: {input_type},必须是 'domains_file' 或 'sites_file'") + + except Exception as e: + logger.error("导出资产失败: %s", e, exc_info=True) + raise RuntimeError(f"导出资产失败: {e}") from e diff --git a/backend/apps/scan/tasks/url_fetch/merge_and_deduplicate_urls_task.py b/backend/apps/scan/tasks/url_fetch/merge_and_deduplicate_urls_task.py new file mode 100644 index 00000000..1784b5b9 --- /dev/null +++ b/backend/apps/scan/tasks/url_fetch/merge_and_deduplicate_urls_task.py @@ -0,0 +1,163 @@ +""" +合并并去重 URL 任务 + +合并多个工具的输出文件,去重并验证 URL 格式 +性能优化:使用系统命令处理大文件 +""" + +import logging +import uuid +import subprocess +from pathlib import Path +from datetime import datetime +from prefect import task +from typing import List + +logger = logging.getLogger(__name__) + + +@task( + name='merge_and_deduplicate_urls', + retries=1, + log_prints=True +) +def merge_and_deduplicate_urls_task( + result_files: List[str], + result_dir: str +) -> str: + """ + 合并扫描结果并去重(高性能流式处理) + + 流程: + 1. 使用 LC_ALL=C sort -u 直接处理多文件 + 2. 排序去重一步完成 + 3. 返回去重后的 URL 文件路径 + + Args: + result_files: 结果文件路径列表 + result_dir: 结果目录 + + Returns: + str: 去重后的 URL 文件路径 + + Raises: + RuntimeError: 处理失败 + """ + logger.info("开始合并并去重 %d 个结果文件(系统命令优化)", len(result_files)) + + result_path = Path(result_dir) + + # 验证文件存在性 + valid_files = [] + for file_path_str in result_files: + file_path = Path(file_path_str) + if file_path.exists(): + valid_files.append(str(file_path)) + else: + logger.warning("结果文件不存在: %s", file_path) + + if not valid_files: + raise RuntimeError("所有结果文件都不存在") + + # 生成输出文件路径 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + short_uuid = uuid.uuid4().hex[:4] + merged_file = result_path / f"merged_{timestamp}_{short_uuid}.txt" + + try: + # 使用系统命令一步完成: 排序去重 + cmd = f"LC_ALL=C sort -u {' '.join(valid_files)} -o {merged_file}" + logger.debug("执行命令: %s", cmd) + + # 按输入文件总行数动态计算超时时间 + total_lines = 0 + for file_path in valid_files: + try: + line_count_proc = subprocess.run( + ["wc", "-l", file_path], + check=True, + capture_output=True, + text=True, + ) + total_lines += int(line_count_proc.stdout.strip().split()[0]) + except (subprocess.CalledProcessError, ValueError, IndexError): + continue + + timeout = 3600 + if total_lines > 0: + # 按行数线性计算:每行约 0.1 秒 + base_per_line = 0.1 + est = int(total_lines * base_per_line) + timeout = max(600, est) + + logger.info( + "URL 合并去重 timeout 自动计算: 输入总行数=%d, timeout=%d秒", + total_lines, + timeout, + ) + + result = subprocess.run( + cmd, + shell=True, + check=True, + timeout=timeout + ) + + logger.debug("✓ 合并去重完成") + + # 统计结果 + if not merged_file.exists(): + raise RuntimeError("合并文件未被创建") + + # 优先使用 wc -l 统计行数,大文件性能更好 + try: + line_count_proc = subprocess.run( + ["wc", "-l", str(merged_file)], + check=True, + capture_output=True, + text=True, + ) + unique_count = int(line_count_proc.stdout.strip().split()[0]) + except (subprocess.CalledProcessError, ValueError, IndexError) as e: + logger.warning( + "wc -l 统计失败(文件: %s),降级为 Python 逐行统计 - 错误: %s", + merged_file, + e, + ) + unique_count = 0 + with open(merged_file, "r", encoding="utf-8") as file_obj: + for _ in file_obj: + unique_count += 1 + + if unique_count == 0: + raise RuntimeError("未找到任何有效 URL") + + file_size = merged_file.stat().st_size + + logger.info( + "✓ 合并去重完成 - 去重后: %d 个 URL, 文件大小: %.2f KB", + unique_count, + file_size / 1024, + ) + + return str(merged_file) + + except subprocess.TimeoutExpired: + error_msg = "合并去重超时(>60分钟),请检查数据量或系统资源" + logger.warning(error_msg) # 超时是可预期的 + raise RuntimeError(error_msg) + + except subprocess.CalledProcessError as e: + error_msg = f"系统命令执行失败: {e.stderr if e.stderr else str(e)}" + logger.error(error_msg) + raise RuntimeError(error_msg) from e + + except IOError as e: + error_msg = f"文件读写失败: {e}" + logger.error(error_msg) + raise RuntimeError(error_msg) from e + + except Exception as e: + error_msg = f"合并去重失败: {e}" + logger.error(error_msg, exc_info=True) + raise diff --git a/backend/apps/scan/tasks/url_fetch/run_and_stream_save_urls_task.py b/backend/apps/scan/tasks/url_fetch/run_and_stream_save_urls_task.py new file mode 100644 index 00000000..3e6f253f --- /dev/null +++ b/backend/apps/scan/tasks/url_fetch/run_and_stream_save_urls_task.py @@ -0,0 +1,479 @@ +""" +基于 execute_stream 的流式 URL 验证任务 + +主要功能: + 1. 实时执行 httpx 命令验证 URL 存活 + 2. 流式处理命令输出,解析存活的 URL + 3. 批量保存到数据库(Endpoint 表) + 4. 避免一次性加载所有 URL 到内存 + +数据流向: + httpx 命令执行 → 流式输出 → 实时解析 → 批量保存 → Endpoint 表 + +优化策略: + - 使用 execute_stream 实时处理输出 + - 流式处理避免内存溢出 + - 批量操作减少数据库交互 + - 只保存存活的 URL(status 2xx/3xx) +""" + +import logging +import json +import subprocess +import time +from pathlib import Path +from prefect import task +from typing import Generator, Optional +from django.db import IntegrityError, OperationalError, DatabaseError +from psycopg2 import InterfaceError +from dataclasses import dataclass + +from apps.asset.services.snapshot import EndpointSnapshotsService +from apps.scan.utils import execute_stream + +logger = logging.getLogger(__name__) + + +@dataclass +class ServiceSet: + """ + Service 集合,用于依赖注入 + + 提供 URL 验证所需的 Service 实例 + """ + snapshot: EndpointSnapshotsService + + @classmethod + def create_default(cls) -> "ServiceSet": + """创建默认的 Service 集合""" + return cls( + snapshot=EndpointSnapshotsService() + ) + + +def _parse_and_validate_line(line: str) -> Optional[dict]: + """ + 解析并验证单行 httpx JSON 输出 + + Args: + line: 单行输出数据 + + Returns: + Optional[dict]: 有效的 httpx 记录,或 None 如果验证失败 + + 只返回存活的 URL(2xx/3xx 状态码) + """ + try: + # 解析 JSON + try: + line_data = json.loads(line) + except json.JSONDecodeError: + # logger.debug("跳过非 JSON 格式的行: %s", line[:100]) + return None + + # 验证数据类型 + if not isinstance(line_data, dict): + logger.warning("解析后的数据不是字典类型,跳过: %s", str(line_data)[:100]) + return None + + # 获取必要字段 + url = line_data.get('url', '').strip() + status_code = line_data.get('status_code') + + if not url: + logger.debug("URL 为空,跳过") + return None + + # 只保存存活的 URL(2xx 或 3xx) + if status_code and (200 <= status_code < 400): + return { + 'url': url, + 'host': line_data.get('host', ''), # 从 httpx 输出中提取 host + 'status_code': status_code, + 'title': line_data.get('title', ''), + 'content_length': line_data.get('content_length', 0), + 'content_type': line_data.get('content_type', ''), + 'webserver': line_data.get('webserver', ''), + 'location': line_data.get('location', ''), + 'tech': line_data.get('tech', []), + 'body_preview': line_data.get('body_preview', ''), + 'vhost': line_data.get('vhost', False), + } + else: + logger.debug("URL 不存活(状态码: %s),跳过: %s", status_code, url) + return None + + except Exception as e: + logger.error("解析行数据异常: %s - 数据: %s", e, line[:100]) + return None + + +def _parse_httpx_stream_output( + cmd: str, + tool_name: str, + cwd: Optional[str] = None, + shell: bool = False, + timeout: Optional[int] = None, + log_file: Optional[str] = None +) -> Generator[dict, None, None]: + """ + 流式解析 httpx 命令输出 + + Args: + cmd: httpx 命令 + tool_name: 工具名称('httpx') + cwd: 工作目录 + shell: 是否使用 shell 执行 + timeout: 命令执行超时时间(秒) + log_file: 日志文件路径 + + Yields: + dict: 每次 yield 一条存活的 URL 记录 + """ + logger.info("开始流式解析 httpx 输出 - 命令: %s", cmd) + + total_lines = 0 + error_lines = 0 + valid_records = 0 + + try: + # 使用 execute_stream 获取实时输出流 + for line in execute_stream( + cmd=cmd, + tool_name=tool_name, + cwd=cwd, + shell=shell, + timeout=timeout, + log_file=log_file + ): + total_lines += 1 + + # 解析并验证单行数据 + record = _parse_and_validate_line(line) + if record is None: + error_lines += 1 + continue + + valid_records += 1 + # yield 一条有效记录(存活的 URL) + yield record + + # 每处理 500 条记录输出一次进度 + if valid_records % 500 == 0: + logger.info("已解析 %d 条存活的 URL...", valid_records) + + except subprocess.TimeoutExpired as e: + error_msg = f"流式解析命令输出超时 - 命令执行超过 {timeout} 秒" + logger.warning(error_msg) # 超时是可预期的,使用 warning 级别 + raise RuntimeError(error_msg) from e + except Exception as e: + logger.error("流式解析命令输出失败: %s", e, exc_info=True) + raise + + logger.info( + "流式解析完成 - 总行数: %d, 存活 URL: %d, 无效/死链: %d", + total_lines, valid_records, error_lines + ) + + +def _save_batch_with_retry( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet, + max_retries: int = 3 +) -> dict: + """ + 保存一个批次的 URL(带重试机制) + + Args: + batch: 数据批次 + scan_id: 扫描任务ID + target_id: 目标ID + batch_num: 批次编号 + services: Service 集合 + max_retries: 最大重试次数 + + Returns: + dict: {'success': bool, 'saved_count': int} + """ + for attempt in range(max_retries): + try: + count = _save_batch(batch, scan_id, target_id, batch_num, services) + return { + 'success': True, + 'saved_count': count + } + + except IntegrityError as e: + # 唯一约束等数据完整性错误通常意味着重复数据,这里记录错误但不让整个扫描失败 + logger.error("批次 %d 数据完整性错误,跳过: %s", batch_num, str(e)[:100]) + return { + 'success': False, + 'saved_count': 0 + } + + except (OperationalError, DatabaseError, InterfaceError) as e: + # 数据库级错误(连接中断、表结构不匹配等):按指数退避重试,最终失败时抛出异常让 Flow 失败 + if attempt < max_retries - 1: + wait_time = 2 ** attempt + logger.warning( + "批次 %d 保存失败(第 %d 次尝试),%d秒后重试: %s", + batch_num, attempt + 1, wait_time, str(e)[:100] + ) + time.sleep(wait_time) + else: + logger.error( + "批次 %d 保存失败(已重试 %d 次),将终止任务: %s", + batch_num, + max_retries, + e, + exc_info=True, + ) + # 让上层 Task 感知失败,从而标记整个扫描为失败 + raise + + except Exception as e: + # 其他未知异常也不再吞掉,直接抛出以便 Flow 标记为失败 + logger.error("批次 %d 未知错误: %s", batch_num, e, exc_info=True) + raise + + # 理论上不会走到这里,保留兜底返回值以满足类型约束 + return { + 'success': False, + 'saved_count': 0 + } + + +def _save_batch( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet +) -> int: + """ + 保存一个批次的数据到数据库 + + Args: + batch: 数据批次,list of dict + scan_id: 扫描任务 ID + target_id: 目标 ID + batch_num: 批次编号 + services: Service 集合 + + Returns: + int: 创建的记录数 + """ + if not batch: + logger.debug("批次 %d 为空,跳过处理", batch_num) + return 0 + + # 批量构造 Endpoint 快照 DTO + from apps.asset.dtos.snapshot import EndpointSnapshotDTO + + snapshots = [] + for record in batch: + try: + dto = EndpointSnapshotDTO( + scan_id=scan_id, + url=record['url'], + host=record.get('host', ''), + title=record.get('title', ''), + status_code=record.get('status_code'), + content_length=record.get('content_length', 0), + location=record.get('location', ''), + webserver=record.get('webserver', ''), + content_type=record.get('content_type', ''), + tech=record.get('tech', []), + body_preview=record.get('body_preview', ''), + vhost=record.get('vhost', False), + matched_gf_patterns=[], + target_id=target_id, + ) + snapshots.append(dto) + except Exception as e: + logger.error("处理记录失败: %s,错误: %s", record.get('url', 'Unknown'), e) + continue + + if snapshots: + try: + # 通过快照服务统一保存快照并同步到资产表 + services.snapshot.save_and_sync(snapshots) + count = len(snapshots) + logger.info( + "批次 %d: 保存了 %d 个存活的 URL(共 %d 个)", + batch_num, count, len(batch) + ) + return count + except Exception as e: + logger.error("批次 %d 批量保存失败: %s", batch_num, e) + raise + + return 0 + + +def _process_records_in_batches( + data_generator, + scan_id: int, + target_id: int, + batch_size: int, + services: ServiceSet +) -> dict: + """ + 分批处理记录并保存到数据库 + + Args: + data_generator: 数据生成器 + scan_id: 扫描ID + target_id: 目标ID + batch_size: 批次大小 + services: Service 集合 + + Returns: + dict: 处理统计结果 + """ + batch = [] + batch_num = 0 + total_records = 0 + total_saved = 0 + failed_batches = [] + + for record in data_generator: + batch.append(record) + total_records += 1 + + # 达到批次大小,执行保存 + if len(batch) >= batch_size: + batch_num += 1 + result = _save_batch_with_retry( + batch, scan_id, target_id, batch_num, services + ) + + if result['success']: + total_saved += result['saved_count'] + else: + failed_batches.append(batch_num) + + batch = [] # 清空批次 + + # 每 10 个批次输出进度 + if batch_num % 10 == 0: + logger.info( + "进度: 已处理 %d 批次,%d 条记录,保存 %d 条", + batch_num, total_records, total_saved + ) + + # 保存最后一批 + if batch: + batch_num += 1 + result = _save_batch_with_retry( + batch, scan_id, target_id, batch_num, services + ) + + if result['success']: + total_saved += result['saved_count'] + else: + failed_batches.append(batch_num) + + return { + 'processed_records': total_records, + 'saved_urls': total_saved, + 'failed_urls': total_records - total_saved, + 'batch_count': batch_num, + 'failed_batches': failed_batches + } + + +@task(name="run_and_stream_save_urls", retries=3, retry_delay_seconds=10) +def run_and_stream_save_urls_task( + cmd: str, + tool_name: str, + scan_id: int, + target_id: int, + cwd: Optional[str] = None, + shell: bool = False, + batch_size: int = 500, + timeout: Optional[int] = None, + log_file: Optional[str] = None +) -> dict: + """ + 执行 httpx 验证并流式保存存活的 URL + + 该任务将: + 1. 执行 httpx 命令验证 URL 存活 + 2. 流式处理输出,实时解析 + 3. 批量保存存活的 URL 到 Endpoint 表 + + Args: + cmd: httpx 命令 + tool_name: 工具名称('httpx') + scan_id: 扫描任务 ID + target_id: 目标 ID + cwd: 工作目录 + shell: 是否使用 shell 执行 + batch_size: 批次大小(默认 500) + timeout: 超时时间(秒) + log_file: 日志文件路径 + + Returns: + dict: { + 'processed_records': int, # 处理的记录总数 + 'saved_urls': int, # 保存的存活 URL 数 + 'failed_urls': int, # 失败/死链数 + 'batch_count': int, # 批次数 + 'failed_batches': list # 失败的批次号 + } + """ + logger.info( + "开始执行流式 URL 验证任务 - target_id=%s, 超时=%s秒, 命令: %s", + target_id, timeout if timeout else '无限制', cmd + ) + + data_generator = None + + try: + # 1. 初始化资源 + data_generator = _parse_httpx_stream_output( + cmd, tool_name, cwd, shell, timeout, log_file + ) + services = ServiceSet.create_default() + + # 2. 流式处理记录并分批保存 + stats = _process_records_in_batches( + data_generator, scan_id, target_id, batch_size, services + ) + + # 3. 输出最终统计 + logger.info( + "✓ URL 验证任务完成 - 处理: %d, 存活: %d, 失败: %d", + stats['processed_records'], + stats['saved_urls'], + stats['failed_urls'] + ) + + return stats + + except subprocess.TimeoutExpired: + logger.warning( + "⚠️ URL 验证任务超时 - target_id=%s, 超时=%s秒", + target_id, timeout + ) + raise + + except Exception as e: + error_msg = f"流式执行 URL 验证任务失败: {e}" + logger.error(error_msg, exc_info=True) + raise RuntimeError(error_msg) from e + + finally: + # 清理资源 + if data_generator is not None: + try: + # 确保生成器被正确关闭 + data_generator.close() + except (GeneratorExit, StopIteration): + pass + except Exception as e: + logger.warning("关闭数据生成器时出错: %s", e) diff --git a/backend/apps/scan/tasks/url_fetch/run_url_fetcher_task.py b/backend/apps/scan/tasks/url_fetch/run_url_fetcher_task.py new file mode 100644 index 00000000..d871079a --- /dev/null +++ b/backend/apps/scan/tasks/url_fetch/run_url_fetcher_task.py @@ -0,0 +1,112 @@ +""" +执行 URL 获取工具任务 + +负责运行单个 URL 获取工具(waymore、katana 等)。 + +注意: +- 输入文件(domains_file 或 sites_file)和命令的构建在 Flow 层完成 +- 任务内部只负责执行已经构建好的命令并校验 / 统计结果 +""" + +import logging +from pathlib import Path +from prefect import task +from apps.scan.utils import execute_and_wait + +logger = logging.getLogger(__name__) + + +@task( + name='run_url_fetcher', + retries=0, # 不重试,工具本身会处理 + log_prints=True +) +def run_url_fetcher_task( + tool_name: str, + command: str, + timeout: int, + output_file: str +) -> dict: + """ + 执行单个 URL 获取工具 + + Args: + tool_name: 工具名称 + command: 完整的命令字符串(由 Flow 层使用命令构建器生成) + timeout: 命令执行超时时间(秒) + output_file: 输出文件完整路径 + + Returns: + dict: { + 'tool': str, # 工具名称 + 'output_file': str, # 输出文件路径 + 'url_count': int, # 发现的 URL 数量 + 'target_count': int, # 处理的目标数量(占位,始终为 1) + 'success': bool + } + """ + output_file_path = Path(output_file) + log_file = str(output_file_path.with_suffix('.log')) + + try: + logger.info("开始执行 URL 获取工具 %s", tool_name) + + # 使用通用命令执行器 + result = execute_and_wait( + tool_name=tool_name, + command=command, + timeout=timeout, + log_file=log_file + ) + + # 验证输出文件是否生成 + if not output_file_path.exists(): + logger.warning( + "URL 获取工具 %s 未生成结果文件: %s (returncode: %d)", + tool_name, str(output_file_path), result['returncode'] + ) + return { + 'tool': tool_name, + 'output_file': output_file, + 'url_count': 0, + 'target_count': 0, + 'success': False + } + + # 检查文件大小 + file_size = output_file_path.stat().st_size + if file_size == 0: + logger.warning("URL 获取工具 %s 生成的结果文件为空: %s", tool_name, output_file_path) + return { + 'tool': tool_name, + 'output_file': output_file, + 'url_count': 0, + 'target_count': 0, + 'success': False + } + + # 统计 URL 数量(不在此处去重,全局去重交由 merge_and_deduplicate_urls_task 处理) + final_count = 0 + with open(output_file, 'r') as f: + final_count = sum(1 for line in f if line.strip()) + + logger.info( + "✓ URL 获取工具 %s 完成 - 结果文件: %s (URL 数量: %d)", + tool_name, str(output_file_path), final_count + ) + + return { + 'tool': tool_name, + 'output_file': output_file, + 'url_count': final_count, + 'target_count': 1, + 'success': final_count > 0 + } + + except RuntimeError: + # 直接向上抛出(execute_and_wait 已记录详细日志) + raise + except Exception as e: + error_msg = f"URL 获取工具 {tool_name} 执行异常: {e}" + logger.error(error_msg, exc_info=True) + raise diff --git a/backend/apps/scan/tasks/url_fetch/save_urls_task.py b/backend/apps/scan/tasks/url_fetch/save_urls_task.py new file mode 100644 index 00000000..01e32e56 --- /dev/null +++ b/backend/apps/scan/tasks/url_fetch/save_urls_task.py @@ -0,0 +1,200 @@ +""" +保存 URL 到数据库任务 + +批量保存发现的 URL 到 Endpoint 表 +支持批量插入和去重 +""" + +import logging +from pathlib import Path +from prefect import task +from typing import List, Optional +from urllib.parse import urlparse +from dataclasses import dataclass + +logger = logging.getLogger(__name__) + + +@dataclass +class ParsedURL: + """解析后的 URL 数据""" + url: str + domain: str + path: str + query: Optional[str] + method: str = 'GET' # 默认方法 + + +def _parse_url(url: str) -> Optional[ParsedURL]: + """ + 解析 URL 提取各个组件 + + Args: + url: 完整 URL + + Returns: + ParsedURL 或 None(如果解析失败) + """ + try: + # 确保有协议 + if not url.startswith(('http://', 'https://')): + url = f'http://{url}' + + parsed = urlparse(url) + + # 提取域名 + domain = parsed.netloc + if not domain: + return None + + # 提取路径(默认为 /) + path = parsed.path if parsed.path else '/' + + # 提取查询参数 + query = parsed.query if parsed.query else None + + # 重建完整 URL(标准化) + scheme = parsed.scheme if parsed.scheme else 'http' + full_url = f"{scheme}://{domain}{path}" + if query: + full_url = f"{full_url}?{query}" + + return ParsedURL( + url=full_url, + domain=domain, + path=path, + query=query + ) + except Exception as e: + logger.debug(f"解析 URL 失败 {url}: {e}") + return None + + +@task( + name='save_urls', + retries=1, + log_prints=True +) +def save_urls_task( + urls_file: str, + scan_id: int, + target_id: int, + batch_size: int = 1000 +) -> dict: + """ + 保存 URL 到数据库 + + Args: + urls_file: URL 文件路径 + scan_id: 扫描 ID + target_id: 目标 ID + batch_size: 批次大小 + + Returns: + dict: { + 'saved_urls': int, # 保存的 URL 数量 + 'total_urls': int, # 总 URL 数量 + 'skipped_urls': int # 跳过的 URL 数量 + } + """ + try: + logger.info(f"开始保存 URL 到数据库 - 扫描ID: {scan_id}, 目标ID: {target_id}") + + # 导入快照服务和 DTO + from apps.asset.services.snapshot import EndpointSnapshotsService + from apps.asset.dtos.snapshot import EndpointSnapshotDTO + + # 创建快照服务(统一负责快照 + 资产双写) + snapshots_service = EndpointSnapshotsService() + + # 按批次流式读取并解析 URL,避免一次性加载全部到内存 + total_urls = 0 + invalid_urls = 0 + valid_urls = 0 + saved_count = 0 + skipped_count = 0 + batch_index = 0 + current_batch: list[EndpointSnapshotDTO] = [] + + with open(urls_file, 'r') as f: + for line in f: + url = line.strip() + if not url: + continue + + total_urls += 1 + + # 解析 URL + parsed = _parse_url(url) + if not parsed: + invalid_urls += 1 + continue + + valid_urls += 1 + current_batch.append( + EndpointSnapshotDTO( + scan_id=scan_id, + url=parsed.url, + host=parsed.domain, # 设置 host 字段 + target_id=target_id, # 用于同步到资产表 + ) + ) + + # 达到批次大小时写入数据库 + if len(current_batch) >= batch_size: + batch_index += 1 + try: + snapshots_service.save_and_sync(current_batch) + created_count = len(current_batch) + saved_count += created_count + logger.debug(f"批次 {batch_index}: 保存 {created_count} 个 URL") + except Exception as e: + logger.error(f"批量保存失败(批次 {batch_index}): {e}") + skipped_count += len(current_batch) + finally: + current_batch = [] + + # 处理最后不足一个批次的 URL + if current_batch: + batch_index += 1 + try: + snapshots_service.save_and_sync(current_batch) + created_count = len(current_batch) + saved_count += created_count + logger.debug(f"批次 {batch_index}: 保存 {created_count} 个 URL") + except Exception as e: + logger.error(f"批量保存失败(批次 {batch_index}): {e}") + skipped_count += len(current_batch) + + if valid_urls == 0: + logger.warning("没有有效的 URL 需要保存") + return { + 'saved_urls': 0, + 'total_urls': total_urls, + 'skipped_urls': invalid_urls, + } + + logger.info( + "准备保存 %d 个有效 URL(总计: %d,无效: %d)", + valid_urls, + total_urls, + invalid_urls, + ) + + # 计算最终跳过的数量(包括无效 URL 和保存失败的 URL) + final_skipped = total_urls - saved_count + + logger.info( + f"✓ URL 保存完成 - 保存: {saved_count}, " + f"跳过: {final_skipped}(包括重复和无效), 总计: {total_urls}" + ) + + return { + 'saved_urls': saved_count, + 'total_urls': total_urls, + 'skipped_urls': final_skipped + } + + except Exception as e: + logger.error(f"保存 URL 失败: {e}", exc_info=True) + raise RuntimeError(f"保存 URL 失败: {e}") from e diff --git a/backend/apps/scan/tasks/vuln_scan/__init__.py b/backend/apps/scan/tasks/vuln_scan/__init__.py new file mode 100644 index 00000000..09c26728 --- /dev/null +++ b/backend/apps/scan/tasks/vuln_scan/__init__.py @@ -0,0 +1,20 @@ +"""漏洞扫描任务模块 + +包含: +- export_endpoints_task: 导出端点 URL 到文件 +- run_vuln_tool_task: 执行漏洞扫描工具(非流式) +- run_and_stream_save_dalfox_vulns_task: Dalfox 流式执行并保存漏洞结果 +- run_and_stream_save_nuclei_vulns_task: Nuclei 流式执行并保存漏洞结果 +""" + +from .export_endpoints_task import export_endpoints_task +from .run_vuln_tool_task import run_vuln_tool_task +from .run_and_stream_save_dalfox_vulns_task import run_and_stream_save_dalfox_vulns_task +from .run_and_stream_save_nuclei_vulns_task import run_and_stream_save_nuclei_vulns_task + +__all__ = [ + "export_endpoints_task", + "run_vuln_tool_task", + "run_and_stream_save_dalfox_vulns_task", + "run_and_stream_save_nuclei_vulns_task", +] diff --git a/backend/apps/scan/tasks/vuln_scan/export_endpoints_task.py b/backend/apps/scan/tasks/vuln_scan/export_endpoints_task.py new file mode 100644 index 00000000..978d2975 --- /dev/null +++ b/backend/apps/scan/tasks/vuln_scan/export_endpoints_task.py @@ -0,0 +1,77 @@ +"""导出 Endpoint URL 到文件的 Task + +基于 EndpointService.iter_endpoint_urls_by_target 按目标流式导出端点 URL, +用于漏洞扫描(如 Dalfox XSS)的输入文件生成。 +""" + +import logging +from pathlib import Path +from typing import Dict + +from prefect import task + +from apps.asset.services import EndpointService + +logger = logging.getLogger(__name__) + + +@task(name="export_endpoints") +def export_endpoints_task( + target_id: int, + output_file: str, + batch_size: int = 1000, +) -> Dict[str, object]: + """导出目标下的所有 Endpoint URL 到文本文件。 + + Args: + target_id: 目标 ID + output_file: 输出文件路径(绝对路径) + batch_size: 每次从数据库迭代的批大小 + + Returns: + dict: { + "success": bool, + "output_file": str, + "total_count": int, + } + """ + try: + logger.info("开始导出 Endpoint URL - Target ID: %d, 输出文件: %s", target_id, output_file) + + output_path = Path(output_file) + output_path.parent.mkdir(parents=True, exist_ok=True) + + service = EndpointService() + url_iterator = service.iter_endpoint_urls_by_target(target_id, chunk_size=batch_size) + + total_count = 0 + with open(output_path, "w", encoding="utf-8", buffering=8192) as f: + for url in url_iterator: + f.write(f"{url}\n") + total_count += 1 + + if total_count % 10000 == 0: + logger.info("已导出 %d 个 Endpoint URL...", total_count) + + logger.info( + "✓ Endpoint URL 导出完成 - 总数: %d, 文件: %s (%.2f KB)", + total_count, + str(output_path), + output_path.stat().st_size / 1024, + ) + + return { + "success": True, + "output_file": str(output_path), + "total_count": total_count, + } + + except FileNotFoundError as e: + logger.error("输出目录不存在: %s", e) + raise + except PermissionError as e: + logger.error("文件写入权限不足: %s", e) + raise + except Exception as e: + logger.exception("导出 Endpoint URL 失败: %s", e) + raise diff --git a/backend/apps/scan/tasks/vuln_scan/run_and_stream_save_dalfox_vulns_task.py b/backend/apps/scan/tasks/vuln_scan/run_and_stream_save_dalfox_vulns_task.py new file mode 100644 index 00000000..35f9d8b4 --- /dev/null +++ b/backend/apps/scan/tasks/vuln_scan/run_and_stream_save_dalfox_vulns_task.py @@ -0,0 +1,475 @@ +"""基于 execute_stream 的 Dalfox XSS 漏洞流式扫描任务 + +主要功能: + 1. 实时执行 Dalfox 漏洞扫描命令 + 2. 流式处理命令输出,解析为统一的漏洞记录 + 3. 批量保存到 VulnerabilitySnapshot 表 + 4. 避免生成大量临时文件,提高效率 + +数据流向: + 命令执行 → 流式输出 → 实时解析 → 批量保存 → 数据库 + +注意: + Dalfox 的 JSON 输出为数组形式:首行为 '[',每条记录一行,行尾带逗号,末行为 ']' + 本任务在解析阶段会: + - 跳过 '['、']' 等控制行 + - 去掉每条记录末尾的逗号,再做 json 解析 +""" + +import logging +import json +import subprocess +import time +from asyncio import CancelledError +from pathlib import Path +from dataclasses import dataclass +from typing import Generator, Optional, TYPE_CHECKING + +from prefect import task +from django.db import IntegrityError, OperationalError, DatabaseError +from psycopg2 import InterfaceError + +from apps.common.definitions import VulnSeverity, ScanStatus +from apps.asset.dtos.snapshot import VulnerabilitySnapshotDTO +from apps.scan.utils import execute_stream +from apps.scan.models import Scan + +if TYPE_CHECKING: + from apps.asset.services.snapshot import VulnerabilitySnapshotsService + +logger = logging.getLogger(__name__) + + +@dataclass +class ServiceSet: + """Service 集合,用于依赖注入 + + 提供漏洞扫描所需的 Service 实例,便于测试时注入 Mock 对象。 + """ + + snapshot: "VulnerabilitySnapshotsService" + + @classmethod + def create_default(cls) -> "ServiceSet": + """创建默认的 Service 集合""" + from apps.asset.services.snapshot import VulnerabilitySnapshotsService + + return cls(snapshot=VulnerabilitySnapshotsService()) + + +def _validate_task_parameters(cmd: str, target_id: int, scan_id: int, cwd: Optional[str]) -> None: + """验证任务参数的有效性。""" + if not cmd or not cmd.strip(): + raise ValueError("扫描命令不能为空") + + if target_id is None: + raise ValueError("target_id 不能为 None,必须指定目标ID") + + if scan_id is None: + raise ValueError("scan_id 不能为 None,必须指定扫描ID") + + if cwd and not Path(cwd).exists(): + raise ValueError(f"工作目录不存在: {cwd}") + + +def _map_severity(raw: Optional[str]) -> str: + """将 Dalfox 的严重性字符串映射为内部 VulnSeverity。""" + value = (raw or "").strip().lower() + mapping = { + "info": VulnSeverity.INFO, + "information": VulnSeverity.INFO, + "low": VulnSeverity.LOW, + "medium": VulnSeverity.MEDIUM, + "med": VulnSeverity.MEDIUM, + "high": VulnSeverity.HIGH, + "critical": VulnSeverity.CRITICAL, + } + return mapping.get(value, VulnSeverity.UNKNOWN) + + +def _parse_and_validate_line(line: str) -> Optional[dict]: + """解析并验证单行 Dalfox JSON 输出。 + + 处理步骤: + 1. 去除首尾空白 + 2. 跳过 '['、']' 等数组控制行 + 3. 去掉行尾逗号 + 4. 解析 JSON 并验证必要字段 + """ + try: + raw = line.strip() + if not raw: + return None + + # 跳过数组控制行 + if raw in ("[", "]", "],"): + return None + + # 去掉尾部逗号 + if raw.endswith(","): + raw = raw[:-1].rstrip() + + try: + data = json.loads(raw) + except json.JSONDecodeError: + # logger.debug("跳过非 JSON 格式的行: %s", raw[:100]) + return None + + if not isinstance(data, dict): + logger.warning("解析后的数据不是字典类型,跳过: %s", str(data)[:100]) + return None + + # Dalfox 输出中的 URL 字段为 data + url = (data.get("data") or "").strip() + if not url: + logger.debug("Dalfox 记录缺少 data(URL) 字段,跳过") + return None + + severity = _map_severity(data.get("severity")) + + # 漏洞类型:根据 type 字段区分 XSS 类型 + # R=Reflected, S=Stored, V=Verified + type_code = data.get("type", "") + type_map = {"R": "xss-reflected", "S": "xss-stored", "V": "xss-verified"} + vuln_type = type_map.get(type_code, "xss") + + # 简化描述:只用 message_str,完整信息在 raw_output + description = data.get("message_str") or "" + + return { + "url": url, + "vuln_type": vuln_type, + "severity": severity, + "source": "dalfox", + "cvss_score": None, + "description": description, + "raw_output": data, # 存储解析后的 dict,而不是原始字符串 + } + + except Exception as e: + logger.error("解析 Dalfox 行数据异常: %s - 数据: %s", e, line[:100]) + return None + + +def _parse_dalfox_stream_output( + cmd: str, + tool_name: str, + cwd: Optional[str] = None, + shell: bool = False, + timeout: Optional[int] = None, + log_file: Optional[str] = None, +) -> Generator[dict, None, None]: + """流式解析 Dalfox 漏洞扫描命令输出。""" + logger.info("开始流式解析 Dalfox 漏洞扫描命令输出 - 命令: %s", cmd) + + total_lines = 0 + error_lines = 0 + valid_records = 0 + + try: + for line in execute_stream( + cmd=cmd, + tool_name=tool_name, + cwd=cwd, + shell=shell, + timeout=timeout, + log_file=log_file, + ): + total_lines += 1 + + record = _parse_and_validate_line(line) + if record is None: + error_lines += 1 + continue + + valid_records += 1 + yield record + + if valid_records % 100 == 0: + logger.info("已解析 %d 条有效漏洞记录...", valid_records) + + except subprocess.TimeoutExpired as e: + error_msg = f"流式解析命令输出超时 - 命令执行超过 {timeout} 秒" + logger.warning(error_msg) + raise RuntimeError(error_msg) from e + except Exception as e: + logger.error("流式解析 Dalfox 命令输出失败: %s", e, exc_info=True) + raise + + logger.info( + "流式解析完成 - 总行数: %d, 有效记录: %d, 错误行数: %d", + total_lines, + valid_records, + error_lines, + ) + + +def _save_batch( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet, +) -> int: + """保存一个批次的漏洞记录到数据库(使用快照 Service,同步到资产表)。""" + if not batch: + logger.debug("批次 %d 为空,跳过处理", batch_num) + return 0 + + snapshot_items = [] + for record in batch: + try: + dto = VulnerabilitySnapshotDTO( + scan_id=scan_id, + target_id=target_id, + url=record["url"], + vuln_type=record["vuln_type"], + severity=str(record["severity"]), + source=record["source"], + cvss_score=record.get("cvss_score"), + description=record.get("description", ""), + raw_output=record.get("raw_output", ""), + ) + snapshot_items.append(dto) + except Exception as e: + logger.error("构建漏洞快照 DTO 失败: %s,记录: %s", e, str(record)[:200]) + continue + + if snapshot_items: + services.snapshot.save_and_sync(snapshot_items) + + logger.info("批次 %d: 保存了 %d 条漏洞记录(共 %d 条)", batch_num, len(snapshot_items), len(batch)) + return len(snapshot_items) + + +def _save_batch_with_retry( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet, + max_retries: int = 3, +) -> dict: + """保存一个批次的漏洞记录(带重试机制)。""" + for attempt in range(max_retries): + try: + created = _save_batch(batch, scan_id, target_id, batch_num, services) + return {"success": True, "created_vulns": created} + + except IntegrityError as e: + logger.error("批次 %d 数据完整性错误,跳过: %s", batch_num, str(e)[:100]) + return {"success": False, "created_vulns": 0} + + except (OperationalError, DatabaseError, InterfaceError) as e: + if attempt < max_retries - 1: + wait_time = 2 ** attempt + logger.warning( + "批次 %d 保存失败(第 %d 次尝试),%d秒后重试: %s", + batch_num, + attempt + 1, + wait_time, + str(e)[:100], + ) + time.sleep(wait_time) + else: + logger.error("批次 %d 保存失败(已重试 %d 次): %s", batch_num, max_retries, e) + return {"success": False, "created_vulns": 0} + + except Exception as e: + logger.error("批次 %d 未知错误: %s", batch_num, e, exc_info=True) + return {"success": False, "created_vulns": 0} + + return {"success": False, "created_vulns": 0} + + +def _accumulate_batch_stats(total_stats: dict, batch_result: dict) -> None: + """累加批次统计信息。""" + total_stats["created_vulns"] += batch_result.get("created_vulns", 0) + + +def _process_batch( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + total_stats: dict, + failed_batches: list, + services: ServiceSet, +) -> None: + """处理单个批次。""" + result = _save_batch_with_retry(batch, scan_id, target_id, batch_num, services) + _accumulate_batch_stats(total_stats, result) + + if not result["success"]: + failed_batches.append(batch_num) + logger.warning( + "批次 %d 保存失败,但已累计统计信息:创建漏洞=%d", + batch_num, + result.get("created_vulns", 0), + ) + + +def _process_records_in_batches( + data_generator, + scan_id: int, + target_id: int, + batch_size: int, + services: ServiceSet, +) -> dict: + """流式处理记录并分批保存。""" + total_records = 0 + batch_num = 0 + failed_batches = [] + batch = [] + cancel_check_interval = 50 # 每处理50条检查一次取消信号 + + total_stats = {"created_vulns": 0} + + for record in data_generator: + if cancel_check_interval > 0 and (total_records % cancel_check_interval == 0): + _raise_if_cancelled(scan_id) + + batch.append(record) + total_records += 1 + + if len(batch) >= batch_size: + batch_num += 1 + _process_batch(batch, scan_id, target_id, batch_num, total_stats, failed_batches, services) + batch = [] + + if batch_num % 20 == 0: + logger.info("进度: 已处理 %d 批次,%d 条记录", batch_num, total_records) + + if batch: + batch_num += 1 + _process_batch(batch, scan_id, target_id, batch_num, total_stats, failed_batches, services) + + _raise_if_cancelled(scan_id) + + if failed_batches: + error_msg = ( + f"流式保存漏洞扫描结果时出现失败批次,处理记录: {total_records}," + f"失败批次: {failed_batches}" + ) + logger.warning(error_msg) + raise RuntimeError(error_msg) + + return { + "processed_records": total_records, + "batch_count": batch_num, + **total_stats, + } + + +def _build_final_result(stats: dict) -> dict: + """构建最终结果并输出日志。""" + logger.info( + "✓ Dalfox 流式保存完成 - 处理记录: %d(%d 批次),创建漏洞: %d", + stats["processed_records"], + stats["batch_count"], + stats["created_vulns"], + ) + + if stats["created_vulns"] == 0: + logger.warning( + "⚠️ 没有创建任何漏洞记录!可能原因:1) Dalfox 未发现漏洞 2) 输出格式问题 3) 重复数据被忽略" + ) + + return { + "processed_records": stats["processed_records"], + "created_vulns": stats["created_vulns"], + } + + +def _cleanup_resources(data_generator) -> None: + """清理任务资源。""" + if data_generator is None: + return + + try: + data_generator.close() + logger.debug("已关闭数据生成器") + except Exception as gen_close_error: + logger.error("关闭生成器时出错: %s", gen_close_error) + + +@task( + name="run_and_stream_save_dalfox_vulns", + retries=0, + log_prints=True, +) +def run_and_stream_save_dalfox_vulns_task( + cmd: str, + tool_name: str, + scan_id: int, + target_id: int, + cwd: Optional[str] = None, + shell: bool = False, + batch_size: int = 1, # Dalfox 漏洞结果本来就不会特别多,可以改小点实时写入 + timeout: Optional[int] = None, + log_file: Optional[str] = None, +) -> dict: + """执行 Dalfox 漏洞扫描命令并流式保存结果到数据库。""" + logger.info( + "开始执行 Dalfox 流式漏洞扫描任务 - target_id=%s, 超时=%s秒, 命令: %s", + target_id, + timeout if timeout else "无限制", + cmd, + ) + + data_generator = None + + try: + _validate_task_parameters(cmd, target_id, scan_id, cwd) + + data_generator = _parse_dalfox_stream_output( + cmd=cmd, + tool_name=tool_name, + cwd=cwd, + shell=shell, + timeout=timeout, + log_file=log_file, + ) + services = ServiceSet.create_default() + + stats = _process_records_in_batches( + data_generator, + scan_id, + target_id, + batch_size, + services, + ) + + return _build_final_result(stats) + + except CancelledError: + logger.warning( + "⚠️ Dalfox 漏洞扫描任务检测到取消信号,正在终止 - scan_id=%s, target_id=%s", + scan_id, + target_id, + ) + raise + + except subprocess.TimeoutExpired: + logger.warning( + "⚠️ Dalfox 漏洞扫描任务超时 - target_id=%s, 超时=%s秒。超时前已解析的数据已保存到数据库。", + target_id, + timeout, + ) + raise + + except Exception as e: + error_msg = f"流式执行 Dalfox 漏洞扫描任务失败: {e}" + logger.error(error_msg, exc_info=True) + raise RuntimeError(error_msg) from e + + finally: + _cleanup_resources(data_generator) + + +def _raise_if_cancelled(scan_id: int) -> None: + """检测扫描是否已请求取消,若是则抛出 CancelledError 以触发 Prefect 取消流程。""" + status = Scan.objects.filter(id=scan_id).values_list("status", flat=True).first() + if status == ScanStatus.CANCELLED: + logger.warning("检测到取消信号,终止 Dalfox 漏洞扫描 - scan_id=%s", scan_id) + raise CancelledError() diff --git a/backend/apps/scan/tasks/vuln_scan/run_and_stream_save_nuclei_vulns_task.py b/backend/apps/scan/tasks/vuln_scan/run_and_stream_save_nuclei_vulns_task.py new file mode 100644 index 00000000..b6c016f7 --- /dev/null +++ b/backend/apps/scan/tasks/vuln_scan/run_and_stream_save_nuclei_vulns_task.py @@ -0,0 +1,474 @@ +"""基于 execute_stream 的 Nuclei 漏洞流式扫描任务 + +主要功能: + 1. 实时执行 Nuclei 漏洞扫描命令 + 2. 流式处理命令输出,解析为统一的漏洞记录 + 3. 批量保存到 VulnerabilitySnapshot 表 + 4. 避免生成大量临时文件,提高效率 + +数据流向: + 命令执行 → 流式输出 → 实时解析 → 批量保存 → 数据库 + +注意: + Nuclei 的 JSON 输出(-j 参数)为每行一条完整 JSON 对象。 +""" + +import logging +import json +import subprocess +import time +from asyncio import CancelledError +from pathlib import Path +from dataclasses import dataclass +from typing import Generator, Optional, TYPE_CHECKING + +from prefect import task +from django.db import IntegrityError, OperationalError, DatabaseError +from psycopg2 import InterfaceError + +from apps.common.definitions import VulnSeverity, ScanStatus +from apps.asset.dtos.snapshot import VulnerabilitySnapshotDTO +from apps.scan.utils import execute_stream +from apps.scan.models import Scan + +if TYPE_CHECKING: + from apps.asset.services.snapshot import VulnerabilitySnapshotsService + +logger = logging.getLogger(__name__) + + +@dataclass +class ServiceSet: + """Service 集合,用于依赖注入""" + + snapshot: "VulnerabilitySnapshotsService" + + @classmethod + def create_default(cls) -> "ServiceSet": + """创建默认的 Service 集合""" + from apps.asset.services.snapshot import VulnerabilitySnapshotsService + + return cls(snapshot=VulnerabilitySnapshotsService()) + + +def _validate_task_parameters(cmd: str, target_id: int, scan_id: int, cwd: Optional[str]) -> None: + """验证任务参数的有效性。""" + if not cmd or not cmd.strip(): + raise ValueError("扫描命令不能为空") + + if target_id is None: + raise ValueError("target_id 不能为 None,必须指定目标ID") + + if scan_id is None: + raise ValueError("scan_id 不能为 None,必须指定扫描ID") + + if cwd and not Path(cwd).exists(): + raise ValueError(f"工作目录不存在: {cwd}") + + +def _map_severity(raw: Optional[str]) -> str: + """将 Nuclei 的严重性字符串映射为内部 VulnSeverity。""" + value = (raw or "").strip().lower() + mapping = { + "info": VulnSeverity.INFO, + "information": VulnSeverity.INFO, + "low": VulnSeverity.LOW, + "medium": VulnSeverity.MEDIUM, + "high": VulnSeverity.HIGH, + "critical": VulnSeverity.CRITICAL, + } + return mapping.get(value, VulnSeverity.UNKNOWN) + + +def _parse_and_validate_line(line: str) -> Optional[dict]: + """解析并验证单行 Nuclei JSON 输出。 + + Nuclei JSON 输出格式(每行一条完整 JSON): + { + "template": "dns/caa-fingerprint.yaml", + "template-id": "caa-fingerprint", + "info": { + "name": "CAA Record", + "severity": "info", + "description": "...", + "tags": ["dns", "caa"], + "classification": {"cve-id": null, "cwe-id": ["cwe-200"]} + }, + "host": "test.yyhuni.rest", + "matched-at": "test.yyhuni.rest", + "type": "dns", + "timestamp": "2025-12-04T17:33:31.903288+08:00", + "matcher-status": true + } + """ + try: + raw = line.strip() + if not raw: + return None + + try: + data = json.loads(raw) + except json.JSONDecodeError: + return None + + if not isinstance(data, dict): + logger.warning("解析后的数据不是字典类型,跳过: %s", str(data)[:100]) + return None + + # 提取 info 字段 + info = data.get("info", {}) + if not isinstance(info, dict): + info = {} + + # URL: 优先用 matched-at,其次用 host + url = data.get("matched-at") or data.get("host") or "" + if not url: + logger.debug("Nuclei 记录缺少 matched-at 或 host 字段,跳过") + return None + + # 严重性 + severity = _map_severity(info.get("severity")) + + # 漏洞类型:使用 template-id 作为类型标识 + vuln_type = data.get("template-id", "unknown") + + # 简化描述:只用 info.name,完整信息在 raw_output + description = info.get("name", "") + + return { + "url": url, + "vuln_type": vuln_type, + "severity": severity, + "source": "nuclei", + "cvss_score": None, + "description": description, + "raw_output": data, # 存储解析后的 dict,而不是原始字符串 + } + + except Exception as e: + logger.error("解析 Nuclei 行数据异常: %s - 数据: %s", e, line[:100]) + return None + + +def _parse_nuclei_stream_output( + cmd: str, + tool_name: str, + cwd: Optional[str] = None, + shell: bool = False, + timeout: Optional[int] = None, + log_file: Optional[str] = None, +) -> Generator[dict, None, None]: + """流式解析 Nuclei 漏洞扫描命令输出。""" + logger.info("开始流式解析 Nuclei 漏洞扫描命令输出 - 命令: %s", cmd) + + total_lines = 0 + error_lines = 0 + valid_records = 0 + + try: + for line in execute_stream( + cmd=cmd, + tool_name=tool_name, + cwd=cwd, + shell=shell, + timeout=timeout, + log_file=log_file, + ): + total_lines += 1 + + record = _parse_and_validate_line(line) + if record is None: + error_lines += 1 + continue + + valid_records += 1 + yield record + + if valid_records % 100 == 0: + logger.info("已解析 %d 条有效漏洞记录...", valid_records) + + except subprocess.TimeoutExpired as e: + error_msg = f"流式解析命令输出超时 - 命令执行超过 {timeout} 秒" + logger.warning(error_msg) + raise RuntimeError(error_msg) from e + except Exception as e: + logger.error("流式解析 Nuclei 命令输出失败: %s", e, exc_info=True) + raise + + logger.info( + "流式解析完成 - 总行数: %d, 有效记录: %d, 错误行数: %d", + total_lines, + valid_records, + error_lines, + ) + + +def _save_batch( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet, +) -> int: + """保存一个批次的漏洞记录到数据库。""" + if not batch: + logger.debug("批次 %d 为空,跳过处理", batch_num) + return 0 + + snapshot_items = [] + for record in batch: + try: + dto = VulnerabilitySnapshotDTO( + scan_id=scan_id, + target_id=target_id, + url=record["url"], + vuln_type=record["vuln_type"], + severity=str(record["severity"]), + source=record["source"], + cvss_score=record.get("cvss_score"), + description=record.get("description", ""), + raw_output=record.get("raw_output", ""), + ) + snapshot_items.append(dto) + except Exception as e: + logger.error("构建漏洞快照 DTO 失败: %s,记录: %s", e, str(record)[:200]) + continue + + if snapshot_items: + services.snapshot.save_and_sync(snapshot_items) + + logger.info("批次 %d: 保存了 %d 条漏洞记录(共 %d 条)", batch_num, len(snapshot_items), len(batch)) + return len(snapshot_items) + + +def _save_batch_with_retry( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + services: ServiceSet, + max_retries: int = 3, +) -> dict: + """保存一个批次的漏洞记录(带重试机制)。""" + for attempt in range(max_retries): + try: + created = _save_batch(batch, scan_id, target_id, batch_num, services) + return {"success": True, "created_vulns": created} + + except IntegrityError as e: + logger.error("批次 %d 数据完整性错误,跳过: %s", batch_num, str(e)[:100]) + return {"success": False, "created_vulns": 0} + + except (OperationalError, DatabaseError, InterfaceError) as e: + if attempt < max_retries - 1: + wait_time = 2 ** attempt + logger.warning( + "批次 %d 保存失败(第 %d 次尝试),%d秒后重试: %s", + batch_num, + attempt + 1, + wait_time, + str(e)[:100], + ) + time.sleep(wait_time) + else: + logger.error("批次 %d 保存失败(已重试 %d 次): %s", batch_num, max_retries, e) + return {"success": False, "created_vulns": 0} + + except Exception as e: + logger.error("批次 %d 未知错误: %s", batch_num, e, exc_info=True) + return {"success": False, "created_vulns": 0} + + return {"success": False, "created_vulns": 0} + + +def _accumulate_batch_stats(total_stats: dict, batch_result: dict) -> None: + """累加批次统计信息。""" + total_stats["created_vulns"] += batch_result.get("created_vulns", 0) + + +def _process_batch( + batch: list, + scan_id: int, + target_id: int, + batch_num: int, + total_stats: dict, + failed_batches: list, + services: ServiceSet, +) -> None: + """处理单个批次。""" + result = _save_batch_with_retry(batch, scan_id, target_id, batch_num, services) + _accumulate_batch_stats(total_stats, result) + + if not result["success"]: + failed_batches.append(batch_num) + logger.warning( + "批次 %d 保存失败,但已累计统计信息:创建漏洞=%d", + batch_num, + result.get("created_vulns", 0), + ) + + +def _process_records_in_batches( + data_generator, + scan_id: int, + target_id: int, + batch_size: int, + services: ServiceSet, +) -> dict: + """流式处理记录并分批保存。""" + total_records = 0 + batch_num = 0 + failed_batches = [] + batch = [] + cancel_check_interval = 50 # 每处理50条检查一次取消信号 + + total_stats = {"created_vulns": 0} + + for record in data_generator: + if cancel_check_interval > 0 and (total_records % cancel_check_interval == 0): + _raise_if_cancelled(scan_id) + + batch.append(record) + total_records += 1 + + if len(batch) >= batch_size: + batch_num += 1 + _process_batch(batch, scan_id, target_id, batch_num, total_stats, failed_batches, services) + batch = [] + + if batch_num % 20 == 0: + logger.info("进度: 已处理 %d 批次,%d 条记录", batch_num, total_records) + + if batch: + batch_num += 1 + _process_batch(batch, scan_id, target_id, batch_num, total_stats, failed_batches, services) + + _raise_if_cancelled(scan_id) + + if failed_batches: + error_msg = ( + f"流式保存漏洞扫描结果时出现失败批次,处理记录: {total_records}," + f"失败批次: {failed_batches}" + ) + logger.warning(error_msg) + raise RuntimeError(error_msg) + + return { + "processed_records": total_records, + "batch_count": batch_num, + **total_stats, + } + + +def _build_final_result(stats: dict) -> dict: + """构建最终结果并输出日志。""" + logger.info( + "✓ Nuclei 流式保存完成 - 处理记录: %d(%d 批次),创建漏洞: %d", + stats["processed_records"], + stats["batch_count"], + stats["created_vulns"], + ) + + if stats["created_vulns"] == 0: + logger.warning( + "⚠️ 没有创建任何漏洞记录!可能原因:1) Nuclei 未发现漏洞 2) 输出格式问题 3) 重复数据被忽略" + ) + + return { + "processed_records": stats["processed_records"], + "created_vulns": stats["created_vulns"], + } + + +def _cleanup_resources(data_generator) -> None: + """清理任务资源。""" + if data_generator is None: + return + + try: + data_generator.close() + logger.debug("已关闭数据生成器") + except Exception as gen_close_error: + logger.error("关闭生成器时出错: %s", gen_close_error) + + +@task( + name="run_and_stream_save_nuclei_vulns", + retries=0, + log_prints=True, +) +def run_and_stream_save_nuclei_vulns_task( + cmd: str, + tool_name: str, + scan_id: int, + target_id: int, + cwd: Optional[str] = None, + shell: bool = False, + batch_size: int = 10, # Nuclei 结果可能较多,适当增大批次 + timeout: Optional[int] = None, + log_file: Optional[str] = None, +) -> dict: + """执行 Nuclei 漏洞扫描命令并流式保存结果到数据库。""" + logger.info( + "开始执行 Nuclei 流式漏洞扫描任务 - target_id=%s, 超时=%s秒, 命令: %s", + target_id, + timeout if timeout else "无限制", + cmd, + ) + + data_generator = None + + try: + _validate_task_parameters(cmd, target_id, scan_id, cwd) + + data_generator = _parse_nuclei_stream_output( + cmd=cmd, + tool_name=tool_name, + cwd=cwd, + shell=shell, + timeout=timeout, + log_file=log_file, + ) + services = ServiceSet.create_default() + + stats = _process_records_in_batches( + data_generator, + scan_id, + target_id, + batch_size, + services, + ) + + return _build_final_result(stats) + + except CancelledError: + logger.warning( + "⚠️ Nuclei 漏洞扫描任务检测到取消信号,正在终止 - scan_id=%s, target_id=%s", + scan_id, + target_id, + ) + raise + + except subprocess.TimeoutExpired: + logger.warning( + "⚠️ Nuclei 漏洞扫描任务超时 - target_id=%s, 超时=%s秒。超时前已解析的数据已保存到数据库。", + target_id, + timeout, + ) + raise + + except Exception as e: + error_msg = f"流式执行 Nuclei 漏洞扫描任务失败: {e}" + logger.error(error_msg, exc_info=True) + raise RuntimeError(error_msg) from e + + finally: + _cleanup_resources(data_generator) + + +def _raise_if_cancelled(scan_id: int) -> None: + """检测扫描是否已请求取消,若是则抛出 CancelledError 以触发 Prefect 取消流程。""" + status = Scan.objects.filter(id=scan_id).values_list("status", flat=True).first() + if status == ScanStatus.CANCELLED: + logger.warning("检测到取消信号,终止 Nuclei 漏洞扫描 - scan_id=%s", scan_id) + raise CancelledError() diff --git a/backend/apps/scan/tasks/vuln_scan/run_vuln_tool_task.py b/backend/apps/scan/tasks/vuln_scan/run_vuln_tool_task.py new file mode 100644 index 00000000..3c1f2091 --- /dev/null +++ b/backend/apps/scan/tasks/vuln_scan/run_vuln_tool_task.py @@ -0,0 +1,64 @@ +"""执行漏洞扫描工具任务 + +负责运行单个漏洞扫描工具(目前主要是 Dalfox XSS)。 + +注意: +- 命令构建在 Flow 层完成,这里只负责执行已经构建好的命令 +- 使用通用 execute_and_wait 统一管理超时和日志 +""" + +import logging +from typing import Dict + +from prefect import task + +from apps.scan.utils import execute_and_wait + +logger = logging.getLogger(__name__) + + +@task( + name="run_vuln_tool", + retries=0, + log_prints=True, +) +def run_vuln_tool_task( + tool_name: str, + command: str, + timeout: int, + log_file: str | None = None, +) -> Dict[str, object]: + """执行单个漏洞扫描工具。 + + Args: + tool_name: 工具名称(如 "dalfox_xss") + command: 完整命令字符串(由 Flow 层构建) + timeout: 命令执行超时时间(秒) + log_file: 日志文件路径 + + Returns: + dict: execute_and_wait 的返回结果字典,并附加 tool 字段。 + """ + try: + logger.info("开始执行漏洞扫描工具 %s", tool_name) + + result = execute_and_wait( + tool_name=tool_name, + command=command, + timeout=timeout, + log_file=log_file, + ) + + # 保持与 execute_and_wait 一致的字段,并额外附加工具名 + return { + "tool": tool_name, + **result, + } + + except RuntimeError: + # execute_and_wait 已经记录详细日志,这里直接向上抛出 + raise + except Exception as e: + error_msg = f"漏洞扫描工具 {tool_name} 执行异常: {e}" + logger.error(error_msg, exc_info=True) + raise diff --git a/backend/apps/scan/tasks/workspace_tasks.py b/backend/apps/scan/tasks/workspace_tasks.py new file mode 100644 index 00000000..15880523 --- /dev/null +++ b/backend/apps/scan/tasks/workspace_tasks.py @@ -0,0 +1,54 @@ +""" +工作空间相关的 Prefect Tasks + +负责扫描工作空间的创建、验证和管理 +""" + +from pathlib import Path +from prefect import task +import logging + +logger = logging.getLogger(__name__) + + +@task( + name="create_scan_workspace", + description="创建并验证 Scan 工作空间目录", + retries=2, + retry_delay_seconds=5 +) +def create_scan_workspace_task(scan_workspace_dir: str) -> Path: + """ + 创建并验证 Scan 工作空间目录 + + Args: + scan_workspace_dir: Scan 工作空间目录路径 + + Returns: + Path: 创建的 Scan 工作空间路径对象 + + Raises: + OSError: 目录创建失败或不可写 + """ + scan_workspace_path = Path(scan_workspace_dir) + + # 创建目录 + try: + scan_workspace_path.mkdir(parents=True, exist_ok=True) + logger.info("✓ Scan 工作空间已创建: %s", scan_workspace_path) + except OSError as e: + logger.error("创建 Scan 工作空间失败: %s - %s", scan_workspace_dir, e) + raise + + # 验证目录是否可写 + test_file = scan_workspace_path / ".test_write" + try: + test_file.touch() + test_file.unlink() + logger.info("✓ Scan 工作空间验证通过(可写): %s", scan_workspace_path) + except OSError as e: + error_msg = f"Scan 工作空间不可写: {scan_workspace_path}" + logger.error(error_msg) + raise OSError(error_msg) from e + + return scan_workspace_path diff --git a/backend/apps/scan/urls.py b/backend/apps/scan/urls.py new file mode 100644 index 00000000..9dc98881 --- /dev/null +++ b/backend/apps/scan/urls.py @@ -0,0 +1,47 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import ScanViewSet, ScheduledScanViewSet +from .notifications.views import notification_callback +from apps.asset.views import ( + SubdomainSnapshotViewSet, WebsiteSnapshotViewSet, DirectorySnapshotViewSet, + EndpointSnapshotViewSet, HostPortMappingSnapshotViewSet, VulnerabilitySnapshotViewSet +) + +# 创建路由器 +router = DefaultRouter() + +# 注册 ViewSet +router.register(r'scans', ScanViewSet, basename='scan') +router.register(r'scheduled-scans', ScheduledScanViewSet, basename='scheduled-scan') + +# Scan 下的嵌套快照路由 +scan_subdomains_list = SubdomainSnapshotViewSet.as_view({'get': 'list'}) +scan_subdomains_export = SubdomainSnapshotViewSet.as_view({'get': 'export'}) +scan_websites_list = WebsiteSnapshotViewSet.as_view({'get': 'list'}) +scan_websites_export = WebsiteSnapshotViewSet.as_view({'get': 'export'}) +scan_directories_list = DirectorySnapshotViewSet.as_view({'get': 'list'}) +scan_directories_export = DirectorySnapshotViewSet.as_view({'get': 'export'}) +scan_endpoints_list = EndpointSnapshotViewSet.as_view({'get': 'list'}) +scan_endpoints_export = EndpointSnapshotViewSet.as_view({'get': 'export'}) +scan_ip_addresses_list = HostPortMappingSnapshotViewSet.as_view({'get': 'list'}) +scan_ip_addresses_export = HostPortMappingSnapshotViewSet.as_view({'get': 'export'}) +scan_vulnerabilities_list = VulnerabilitySnapshotViewSet.as_view({'get': 'list'}) + +urlpatterns = [ + path('', include(router.urls)), + # Worker 回调 API + path('callbacks/notification/', notification_callback, name='notification-callback'), + # 嵌套路由:/api/scans/{scan_pk}/xxx/ + path('scans/<int:scan_pk>/subdomains/', scan_subdomains_list, name='scan-subdomains-list'), + path('scans/<int:scan_pk>/subdomains/export/', scan_subdomains_export, name='scan-subdomains-export'), + path('scans/<int:scan_pk>/websites/', scan_websites_list, name='scan-websites-list'), + path('scans/<int:scan_pk>/websites/export/', scan_websites_export, name='scan-websites-export'), + path('scans/<int:scan_pk>/directories/', scan_directories_list, name='scan-directories-list'), + path('scans/<int:scan_pk>/directories/export/', scan_directories_export, name='scan-directories-export'), + path('scans/<int:scan_pk>/endpoints/', scan_endpoints_list, name='scan-endpoints-list'), + path('scans/<int:scan_pk>/endpoints/export/', scan_endpoints_export, name='scan-endpoints-export'), + path('scans/<int:scan_pk>/ip-addresses/', scan_ip_addresses_list, name='scan-ip-addresses-list'), + path('scans/<int:scan_pk>/ip-addresses/export/', scan_ip_addresses_export, name='scan-ip-addresses-export'), + path('scans/<int:scan_pk>/vulnerabilities/', scan_vulnerabilities_list, name='scan-vulnerabilities-list'), +] + diff --git a/backend/apps/scan/utils/__init__.py b/backend/apps/scan/utils/__init__.py new file mode 100644 index 00000000..194a2e3b --- /dev/null +++ b/backend/apps/scan/utils/__init__.py @@ -0,0 +1,29 @@ +""" +扫描模块工具包 + +提供扫描相关的工具函数。 +""" + +from .directory_cleanup import remove_directory +from .command_builder import build_scan_command +from .command_executor import execute_and_wait, execute_stream +from .wordlist_helpers import ensure_wordlist_local +from .nuclei_helpers import ensure_nuclei_templates_local +from . import config_parser + +__all__ = [ + # 目录清理 + 'remove_directory', + # 命令构建 + 'build_scan_command', # 扫描工具命令构建(基于 f-string) + # 命令执行 + 'execute_and_wait', # 等待式执行(文件输出) + 'execute_stream', # 流式执行(实时处理) + # 字典文件 + 'ensure_wordlist_local', # 确保本地字典文件(含 hash 校验) + # Nuclei 模板 + 'ensure_nuclei_templates_local', # 确保本地模板(含 commit hash 校验) + # 配置解析 + 'config_parser', +] + diff --git a/backend/apps/scan/utils/command_builder.py b/backend/apps/scan/utils/command_builder.py new file mode 100644 index 00000000..7d49fe6f --- /dev/null +++ b/backend/apps/scan/utils/command_builder.py @@ -0,0 +1,106 @@ +""" +简化的命令构建工具 +使用 Python 原生 f-string 和条件拼接,零依赖,性能更好。 +""" + +import logging +from typing import Dict, Any + +logger = logging.getLogger(__name__) + + +def build_scan_command( + tool_name: str, + scan_type: str, + command_params: Dict[str, Any], + tool_config: Dict[str, Any] +) -> str: + """ + 构建扫描工具命令(使用 f-string) + + Args: + tool_name: 工具名称(如 'subfinder') + scan_type: 扫描类型(如 'subdomain_discovery') + command_params: 命令占位符参数 + - domain: 目标域名 + - domains_file: 域名列表文件(用于端口扫描) + - url_file: URL列表文件(用于站点扫描) + - target_file: 目标文件路径(通用) + - output_file: 输出文件路径 + tool_config: 工具配置参数(包含可选参数) + - threads: 线程数 + - timeout: 超时时间(秒) + - 其他可选参数... + + Returns: + 完整的命令字符串 + + Example: + >>> build_scan_command( + ... tool_name='subfinder', + ... scan_type='subdomain_discovery', + ... command_params={'domain': 'example.com', 'output_file': '/tmp/out.txt'}, + ... tool_config={'threads': 10} + ... ) + 'subfinder -d example.com -o /tmp/out.txt -silent -t 10' + """ + from apps.scan.configs.command_templates import get_command_template, SCAN_TOOLS_BASE_PATH + + # 获取命令模板 + template = get_command_template(scan_type, tool_name) + if not template: + raise ValueError(f"未找到工具 {tool_name} 的命令模板(扫描类型: {scan_type})") + + # 合并所有参数,并将中划线统一转成下划线 + # 规范约定: + # - 配置文件(YAML):参数名用中划线,贴近 CLI 原生参数(如 rate-limit, request-timeout) + # - 模板文件(Python):参数名用下划线,符合 str.format() 占位符语法要求 + # - 此处自动转换:rate-limit → rate_limit + def normalize_key(k): + return k.replace('-', '_') if isinstance(k, str) else k + + all_params = { + 'scan_tools_base': SCAN_TOOLS_BASE_PATH, + **{normalize_key(k): v for k, v in command_params.items()}, + **{normalize_key(k): v for k, v in tool_config.items()} + } + + # nuclei 特殊处理:要求 template_args 必填(支持多 -t),避免格式化缺失 + if tool_name == "nuclei": + if not all_params.get("template_args"): + raise ValueError("nuclei 命令构建缺少 template_args(请检查模板仓库列表配置)") + + try: + # 1. 构建基础命令 + base_command = template['base'].format(**all_params) + + # 2. 拼接可选参数 + optional_parts = [] + for param_name, flag_template in template.get('optional', {}).items(): + # 检查参数是否存在且有值 + if param_name in all_params and all_params[param_name]: + optional_parts.append(flag_template.format(**all_params)) + + # 3. 组合完整命令 + full_command = base_command + if optional_parts: + full_command += ' ' + ' '.join(optional_parts) + + # 4. 清理多余空白 + import re + cleaned_command = re.sub(r'\s+', ' ', full_command).strip() + + return cleaned_command + + except KeyError as e: + raise ValueError( + f"命令构建失败:缺少必需参数 {e}\n" + f"模板: {template}\n" + f"提供的参数: {list(all_params.keys())}" + ) + except Exception as e: + raise ValueError( + f"命令构建失败: {e}\n" + f"模板: {template}\n" + f"提供的参数: {list(all_params.keys())}" + ) diff --git a/backend/apps/scan/utils/command_executor.py b/backend/apps/scan/utils/command_executor.py new file mode 100644 index 00000000..01d84214 --- /dev/null +++ b/backend/apps/scan/utils/command_executor.py @@ -0,0 +1,677 @@ +""" +命令执行器 + +统一管理所有命令执行方式: +- execute_and_wait(): 等待式执行,适合输出到文件的工具 +- execute_stream(): 流式执行,适合实时处理输出的工具 +""" + +import logging +import os +from django.conf import settings +import re +import signal +import subprocess +import threading +import time +from datetime import datetime +from pathlib import Path +from typing import Dict, Any, Optional, Generator + +try: + # 可选依赖:用于根据 CPU / 内存负载做动态并发控制 + import psutil +except ImportError: # 运行环境缺少 psutil 时降级为无动态负载控制 + psutil = None + +logger = logging.getLogger(__name__) + +# 常量定义 +GRACEFUL_SHUTDOWN_TIMEOUT = 5 # 进程优雅退出的超时时间(秒) +MAX_LOG_TAIL_LINES = 1000 # 日志文件读取的最大行数 + +# 命令日志配置(从环境变量读取) +# ENABLE_COMMAND_LOGGING=true: 输出所有内容(命令输出+错误)到log_file_path +# ENABLE_COMMAND_LOGGING=false: 只输出错误到log_file_path +ENABLE_COMMAND_LOGGING = getattr(settings, 'ENABLE_COMMAND_LOGGING', True) + +# 动态并发控制阈值(可在 Django settings 中覆盖) +SCAN_CPU_HIGH = getattr(settings, 'SCAN_CPU_HIGH', 85.0) # CPU 高水位(百分比) +SCAN_MEM_HIGH = getattr(settings, 'SCAN_MEM_HIGH', 85.0) # 内存高水位(百分比) +SCAN_LOAD_CHECK_INTERVAL = getattr(settings, 'SCAN_LOAD_CHECK_INTERVAL', 30) # 负载检查间隔(秒) +SCAN_COMMAND_STARTUP_DELAY = getattr(settings, 'SCAN_COMMAND_STARTUP_DELAY', 5) # 命令启动前等待(秒) + +_ACTIVE_COMMANDS = 0 +_ACTIVE_COMMANDS_LOCK = threading.Lock() + + +def _wait_for_system_load() -> None: + """根据当前机器 CPU/内存负载,决定是否暂缓启动新的外部命令。""" + + # 1. 先强制等待,让之前启动的命令有时间消耗资源,避免并发启动导致延迟OOM + if SCAN_COMMAND_STARTUP_DELAY > 0: + time.sleep(SCAN_COMMAND_STARTUP_DELAY) + + # 2. 再检查系统负载 + if psutil is None: + raise ImportError("psutil 未安装,无法进行负载感知控制") + + while True: + cpu = psutil.cpu_percent(interval=0.5) + mem = psutil.virtual_memory().percent + + if cpu < SCAN_CPU_HIGH and mem < SCAN_MEM_HIGH: + return + + logger.info( + "系统负载较高,暂缓启动: cpu=%.1f%% (阈值 %.1f%%), mem=%.1f%% (阈值 %.1f%%)", + cpu, + SCAN_CPU_HIGH, + mem, + SCAN_MEM_HIGH, + ) + time.sleep(SCAN_LOAD_CHECK_INTERVAL) + + +class CommandExecutor: + """ + 统一的命令执行器 + + 提供两种执行模式: + 1. execute_and_wait() - 等待式执行(适合文件输出) + 2. execute_stream() - 流式执行(适合实时处理) + """ + + def _write_command_start_header(self, log_file: Path, tool_name: str, command: str, timeout: Optional[int] = None): + """ + 在命令开始时写入头部信息 + """ + if not ENABLE_COMMAND_LOGGING: + return + + try: + with open(log_file, 'w', encoding='utf-8') as f: + f.write(f"$ {command}\n") + f.write(f"{'='*60}\n") + f.write(f"# 工具: {tool_name}\n") + f.write(f"# 开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + if timeout is not None: + f.write(f"# 超时限制: {timeout}秒\n") + f.write(f"# 状态: 执行中...\n") + f.write(f"{'='*60}\n\n") + except Exception as e: + logger.error(f"写入命令开始信息失败: {e}") + + def _write_command_end_footer(self, log_file: Path, tool_name: str, duration: float, returncode: int, success: bool): + """ + 在命令结束时追加尾部信息 + """ + if not ENABLE_COMMAND_LOGGING: + return + + try: + with open(log_file, 'a', encoding='utf-8') as f: + f.write(f"\n{'='*60}\n") + f.write(f"# 结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"# 执行耗时: {duration:.2f}秒\n") + f.write(f"# 退出码: {returncode}\n") + f.write(f"# 状态: {'✓ 成功' if success else '✗ 失败'}\n") + f.write(f"{'='*60}\n") + + logger.info(f"📝 {tool_name} 日志: {log_file} (耗时: {duration:.2f}秒)") + except Exception as e: + logger.error(f"写入命令结束信息失败: {e}") + + def _kill_process_tree(self, process: subprocess.Popen) -> None: + """ + 强制终止进程树 + + 当使用 shell=True 时,process.pid 是 shell 的 PID。 + 如果不杀掉整个进程组,shell 的子进程(实际工具)会变成孤儿进程继续运行。 + """ + if process.poll() is not None: + return + + try: + # 尝试杀掉进程组(需要进程启动时设置 start_new_session=True) + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + logger.debug(f"已终止进程组: PGID={process.pid}") + except ProcessLookupError: + pass # 进程已不存在 + except Exception as e: + logger.warning(f"终止进程组失败 ({e}),尝试普通 kill") + try: + process.kill() + except: + pass + + def execute_and_wait( + self, + tool_name: str, + command: str, + timeout: int, + log_file: Optional[str] = None + ) -> Dict[str, Any]: + """ + 等待式执行:启动命令并等待完成 + + 适用场景:工具输出到文件(如 subfinder -o output.txt) + + Args: + tool_name: 工具名称(用于日志) + command: 完整的扫描命令(包含输出文件参数) + timeout: 超时时间(秒) + log_file: 日志文件路径(可选,None 表示丢弃 stderr) + + Returns: + dict: { + 'success': bool, # 命令是否成功执行(returncode == 0) + 'returncode': int, # 命令退出码 + 'log_file': str | None # 日志文件路径 + } + + Raises: + ValueError: 参数验证失败 + RuntimeError: 执行失败或超时 + """ + global _ACTIVE_COMMANDS + + # 验证参数 + if not tool_name: + raise ValueError("工具名称不能为空") + if not command: + raise ValueError("扫描命令不能为空") + if timeout <= 0: + raise ValueError(f"超时时间必须大于0: {timeout}") + + + logger.info("开始运行扫描工具: %s", tool_name) + + # 准备日志文件 + log_file_path = Path(log_file) if log_file else None + + # 记录开始时间(用于计算执行时间) + start_time = datetime.now() + + process = None + log_file_handle = None + acquired_slot = False # 标记是否已增加全局活动命令计数 + + try: + # 在启动新的外部命令之前,先根据 CPU/内存负载判断是否需要等待 + _wait_for_system_load() + + acquired_slot = True + if _ACTIVE_COMMANDS_LOCK: + with _ACTIVE_COMMANDS_LOCK: + _ACTIVE_COMMANDS += 1 + current_active = _ACTIVE_COMMANDS + else: + current_active = 0 + logger.info( + "登记活动命令计数: tool=%s, active=%d", + tool_name, + current_active, + ) + + logger.debug("执行命令: %s", command) + if log_file_path: + logger.debug("日志文件: %s", log_file_path) + else: + logger.debug("日志输出: 丢弃") + + # 准备输出流 + stdout_target = subprocess.DEVNULL + stderr_target = subprocess.DEVNULL + + if log_file_path: + # 先写入命令开始信息 + if ENABLE_COMMAND_LOGGING: + self._write_command_start_header(log_file_path, tool_name, command, timeout) + + # 以追加模式打开日志文件 + log_file_handle = open(log_file_path, 'a', encoding='utf-8', buffering=1) + if ENABLE_COMMAND_LOGGING: + stdout_target = log_file_handle + stderr_target = subprocess.STDOUT + else: + stderr_target = log_file_handle + + # 启动进程 + # 使用 start_new_session=True 创建新会话,使子进程成为新进程组的首领 + # 这样我们可以通过 killpg 杀掉整个进程树 + process = subprocess.Popen( + command, + stdin=subprocess.DEVNULL, + shell=True, + stdout=stdout_target, + stderr=stderr_target, + text=True, + start_new_session=True + ) + + # 等待完成 + process.communicate(timeout=timeout) + + # 检查执行结果 + returncode = process.returncode + success = (returncode == 0) + + # 计算执行时间 + duration = (datetime.now() - start_time).total_seconds() + + # 追加命令结束信息(如果开启且有日志文件) + if log_file_path and ENABLE_COMMAND_LOGGING: + self._write_command_end_footer(log_file_path, tool_name, duration, returncode, success) + command_log_file = str(log_file_path) if log_file_path else None + + if not success: + # 命令执行失败,尝试读取错误日志 + error_output = "" + if log_file_path: + error_output = self._read_log_tail(log_file_path, max_lines=MAX_LOG_TAIL_LINES) + logger.warning( + "扫描工具 %s 返回非零状态码: %d (执行时间: %.2f秒)%s", + tool_name, returncode, duration, + f"\n错误输出:\n{error_output}" if error_output else "" + ) + else: + logger.info("✓ 扫描工具 %s 执行完成 (执行时间: %.2f秒)", tool_name, duration) + + return { + 'success': success, + 'returncode': returncode, + 'log_file': str(log_file_path) if log_file_path else None, + 'command_log_file': command_log_file, + 'duration': duration + } + + except subprocess.TimeoutExpired as e: + # 计算超时时的执行时间 + duration = (datetime.now() - start_time).total_seconds() + + # 追加超时结束信息 + if log_file_path and ENABLE_COMMAND_LOGGING: + self._write_command_end_footer(log_file_path, tool_name, duration, -1, False) + + error_msg = f"扫描工具 {tool_name} 执行超时({timeout}秒,实际执行: {duration:.2f}秒)" + logger.error(error_msg) + if log_file_path and log_file_path.exists(): + logger.debug("超时日志已保存: %s", log_file_path) + raise RuntimeError(error_msg) from e + + except subprocess.SubprocessError as e: + error_msg = f"扫描工具 {tool_name} 执行失败: {e}" + logger.error(error_msg) + raise RuntimeError(error_msg) from e + + except Exception as e: + # 捕获所有异常(包括 Prefect 取消引发的 CancelledError 等) + # 确保在 finally 块中清理进程 + error_msg = f"扫描工具 {tool_name} 执行异常(可能是被中断): {e}" + logger.error(error_msg, exc_info=True) + raise + + finally: + # 关键修复:确保进程树被清理 + if process: + self._kill_process_tree(process) + + # 关闭文件句柄 + if log_file_handle: + try: + log_file_handle.close() + except: + pass + + if acquired_slot: + if _ACTIVE_COMMANDS_LOCK: + with _ACTIVE_COMMANDS_LOCK: + if _ACTIVE_COMMANDS > 0: + _ACTIVE_COMMANDS -= 1 + current_active = _ACTIVE_COMMANDS + else: + current_active = 0 + logger.info( + "释放活动命令计数: tool=%s, active=%d", + tool_name, + current_active, + ) + + def execute_stream( + self, + cmd: str, + tool_name: str, + cwd: Optional[str] = None, + shell: bool = False, + encoding: str = 'utf-8', + suffix_char: Optional[str] = None, + timeout: Optional[int] = None, + log_file: Optional[str] = None + ) -> Generator[str, None, None]: + """ + 流式执行:逐行返回输出 + + 适用场景:工具流式输出 JSON(如 naabu -json) + + Args: + cmd: 要执行的命令 + tool_name: 工具名称(用于日志记录) + cwd: 工作目录 + shell: 是否使用 shell 执行 + encoding: 编码格式 + suffix_char: 末尾后缀字符(用于移除) + timeout: 命令执行超时时间(秒),None 表示不设置超时 + log_file: 日志文件路径(可选) + + Yields: + str: 每行输出的内容(已处理:去空白、去ANSI、去后缀) + + Raises: + subprocess.TimeoutExpired: 命令执行超时 + """ + + global _ACTIVE_COMMANDS + + # 记录开始时间(用于命令日志) + start_time = datetime.now() + acquired_slot = False + + # 准备日志文件路径 + log_file_path = Path(log_file) if log_file else None + if log_file_path: + logger.debug(f"日志文件: {log_file_path}") + else: + logger.debug("日志输出: 丢弃") + + # 根据是否使用shell来格式化命令 + command = cmd if shell else cmd.split() + + # 日志文件句柄 + log_file_handle = None + + # 启动子进程,根据日志策略决定输出方向 + if log_file_path: + # 先写入命令开始信息 + if ENABLE_COMMAND_LOGGING: + self._write_command_start_header(log_file_path, tool_name, cmd, timeout) + + # 以追加模式打开日志文件(开始信息已写入) + log_file_handle = open(log_file_path, 'a', encoding='utf-8', buffering=1) + + stdout_target = subprocess.PIPE + stderr_target = log_file_handle + if ENABLE_COMMAND_LOGGING: + stderr_target = subprocess.STDOUT + + if not acquired_slot: + # 日志模式下,在真正启动进程前做一次负载检查,并登记活动命令计数 + _wait_for_system_load() + acquired_slot = True + if _ACTIVE_COMMANDS_LOCK: + with _ACTIVE_COMMANDS_LOCK: + _ACTIVE_COMMANDS += 1 + current_active = _ACTIVE_COMMANDS + else: + current_active = 0 + logger.info( + "登记活动命令计数: tool=%s, active=%d", + tool_name, + current_active, + ) + + process = subprocess.Popen( + command, + stdin=subprocess.DEVNULL, + stdout=stdout_target, + stderr=stderr_target, + cwd=cwd, + universal_newlines=True, + encoding=encoding, + shell=shell, + start_new_session=True # 关键:创建新进程组 + ) + else: + # 无日志文件:正常流式输出 + if not acquired_slot: + # 非日志模式,同样在启动进程前做一次负载检查,并登记活动命令计数 + _wait_for_system_load() + acquired_slot = True + if _ACTIVE_COMMANDS_LOCK: + with _ACTIVE_COMMANDS_LOCK: + _ACTIVE_COMMANDS += 1 + current_active = _ACTIVE_COMMANDS + else: + current_active = 0 + logger.info( + "登记活动命令计数: tool=%s, active=%d", + tool_name, + current_active, + ) + + process = subprocess.Popen( + command, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=cwd, + universal_newlines=True, + encoding=encoding, + shell=shell, + start_new_session=True # 关键:创建新进程组 + ) + + # 超时控制:使用 Timer 在指定时间后终止进程 + timed_out_event = threading.Event() + + def _kill_when_timeout(): + timed_out_event.set() + if process.poll() is None: # 进程还在运行 + logger.warning(f"命令执行超时({timeout}秒),正在终止进程: {cmd}") + self._kill_process_tree(process) # 使用新的终止方法 + + timer = None + if timeout is not None: + timer = threading.Timer(timeout, _kill_when_timeout) + timer.start() + + try: + # 逐行读取进程输出 + stdout = process.stdout + assert stdout is not None, "stdout should not be None when stdout=PIPE" + + for line in iter(lambda: stdout.readline(), ''): + if not line: + break + + # 去除行首尾的空白字符 + line = line.strip() + # 跳过空行 + if not line: + continue + + # 移除ANSI转义序列(颜色、格式等控制字符) + ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + line = ansi_escape.sub('', line) + # 处理Windows风格的换行符 + line = line.replace('\\x0d\\x0a', '\n') + + # 如果指定了后缀字符,移除末尾的后缀字符 + if suffix_char and line.endswith(suffix_char): + line = line[:-1] + + # 如果开启命令日志且有日志文件,同时写入日志文件 + if log_file_handle and ENABLE_COMMAND_LOGGING: + log_file_handle.write(line + '\n') + log_file_handle.flush() + + # 直接返回行内容,由调用者负责解析 + yield line + + finally: + # 1. 停止定时器(如果还没触发) + if timer: + timer.cancel() + timer.join(timeout=0.1) # 等待 timer 线程完全结束,避免悬挂 + + # 2. 清理进程资源 + exit_code = None + + if timed_out_event.is_set(): + # 超时情况:定时器已经处理了进程终止,只需获取退出码 + logger.debug("进程已被超时定时器终止,等待进程结束") + try: + exit_code = process.wait(timeout=1.0) # 等待进程完全退出 + except subprocess.TimeoutExpired: + logger.warning("进程在超时后仍未退出,强制终止") + self._kill_process_tree(process) + exit_code = -1 + else: + # 正常结束:等待进程自然结束 + # 如果是被外部中断(如 CancelledError),poll() 应为 None,需要 kill + if process.poll() is None: + logger.info(f"流式执行被中断,清理进程: {tool_name}") + self._kill_process_tree(process) + + try: + exit_code = process.wait(timeout=GRACEFUL_SHUTDOWN_TIMEOUT) + except subprocess.TimeoutExpired: + logger.warning( + "程序未能在%d秒内自然结束,强制终止: %s", + GRACEFUL_SHUTDOWN_TIMEOUT, cmd + ) + self._kill_process_tree(process) + exit_code = -2 + + # 3. 关闭进程流 + if process.stdout: + process.stdout.close() + if process.stderr: + process.stderr.close() + + # 4. 关闭日志文件句柄 + if log_file_handle: + log_file_handle.close() + + # 5. 追加命令结束信息(如果开启且有日志文件) + if log_file_path and ENABLE_COMMAND_LOGGING: + duration = (datetime.now() - start_time).total_seconds() + success = not timed_out_event.is_set() and (exit_code == 0 if exit_code is not None else True) + + # 追加结束信息到日志文件末尾 + self._write_command_end_footer(log_file_path, tool_name, duration, exit_code or 0, success) + + if acquired_slot: + if _ACTIVE_COMMANDS_LOCK: + with _ACTIVE_COMMANDS_LOCK: + if _ACTIVE_COMMANDS > 0: + _ACTIVE_COMMANDS -= 1 + current_active = _ACTIVE_COMMANDS + else: + current_active = 0 + logger.info( + "释放活动命令计数: tool=%s, active=%d", + tool_name, + current_active, + ) + + def _read_log_tail(self, log_file: Path, max_lines: int = MAX_LOG_TAIL_LINES) -> str: + """ + 读取日志文件的末尾部分 + + Args: + log_file: 日志文件路径 + max_lines: 最大读取行数 + + Returns: + 日志内容(字符串),读取失败返回错误提示 + """ + if not log_file.exists(): + logger.debug("日志文件不存在: %s", log_file) + return "" + + if log_file.stat().st_size == 0: + logger.debug("日志文件为空: %s", log_file) + return "" + + try: + with open(log_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + return ''.join(lines[-max_lines:] if len(lines) > max_lines else lines) + except UnicodeDecodeError as e: + logger.warning("日志文件编码错误 (%s): %s", log_file, e) + return f"(无法读取日志文件: 编码错误 - {e})" + except PermissionError as e: + logger.warning("日志文件权限不足 (%s): %s", log_file, e) + return f"(无法读取日志文件: 权限不足)" + except IOError as e: + logger.warning("日志文件读取IO错误 (%s): %s", log_file, e) + return f"(无法读取日志文件: IO错误 - {e})" + except Exception as e: + logger.warning("读取日志文件失败 (%s): %s", log_file, e, exc_info=True) + return f"(无法读取日志文件: {type(e).__name__} - {e})" + + +# 单例实例 +_executor = CommandExecutor() + + +# 快捷函数 +def execute_and_wait( + tool_name: str, + command: str, + timeout: int, + log_file: Optional[str] = None +) -> Dict[str, Any]: + """ + 等待式执行命令(快捷函数) + + 适用场景:工具输出到文件(如 subfinder -o output.txt) + + Args: + tool_name: 工具名称 + command: 扫描命令(包含输出文件参数) + timeout: 超时时间(秒) + log_file: 日志文件路径(可选) + + Returns: + 执行结果字典(包含 duration 字段) + + Raises: + RuntimeError: 执行失败或超时 + """ + return _executor.execute_and_wait(tool_name, command, timeout, log_file) + + +def execute_stream( + cmd: str, + tool_name: str, + cwd: Optional[str] = None, + shell: bool = False, + encoding: str = 'utf-8', + suffix_char: Optional[str] = None, + timeout: Optional[int] = None, + log_file: Optional[str] = None +) -> Generator[str, None, None]: + """ + 流式执行命令(快捷函数) + + 适用场景:工具流式输出 JSON(如 naabu -json) + + Args: + cmd: 要执行的命令 + tool_name: 工具名称 + cwd: 工作目录 + shell: 是否使用 shell 执行 + encoding: 编码格式 + suffix_char: 末尾后缀字符 + timeout: 命令执行超时时间(秒) + log_file: 日志文件路径(可选) + + Yields: + str: 每行输出的内容 + + Raises: + subprocess.TimeoutExpired: 命令执行超时 + """ + return _executor.execute_stream(cmd, tool_name, cwd, shell, encoding, suffix_char, timeout, log_file) diff --git a/backend/apps/scan/utils/config_parser.py b/backend/apps/scan/utils/config_parser.py new file mode 100644 index 00000000..54986e32 --- /dev/null +++ b/backend/apps/scan/utils/config_parser.py @@ -0,0 +1,195 @@ +""" +配置解析器 + +负责解析引擎配置(YAML)并提取启用的工具及其配置。 + +架构说明: +- 命令模板:在 command_templates.py 中定义(基础命令 + 可选参数映射) +- 工具配置:从引擎配置(engine_config YAML 字符串)读取 +- 无默认配置文件:所有配置必须在引擎配置中提供 + +核心函数: +- parse_enabled_tools_from_dict(): 解析并过滤启用的工具,返回工具配置字典 + +返回格式: +- {'subfinder': {'enabled': True, 'threads': 10, 'timeout': 600}} +- timeout 是必需参数,支持整数或 'auto'(由具体 Flow 处理) +""" + +import logging +from typing import Dict, Any + +logger = logging.getLogger(__name__) + + +def _normalize_config_keys(config: Dict[str, Any]) -> Dict[str, Any]: + """ + 将配置字典的 key 中划线转换为下划线 + + 规范约定: + - 配置文件统一用中划线(贴近 CLI 参数风格) + - 代码里统一用下划线(Python 标识符规范) + - 此处自动转换:rate-limit → rate_limit + + Args: + config: 原始配置字典 + + Returns: + key 已转换的新字典 + """ + return { + k.replace('-', '_') if isinstance(k, str) else k: v + for k, v in config.items() + } + + +def _parse_subdomain_discovery_config(scan_config: Dict[str, Any]) -> Dict[str, Any]: + """ + 解析子域名发现配置(4阶段流程) + + 配置格式: + { + 'passive_tools': {'subfinder': {...}, ...}, + 'bruteforce': {'enabled': True, 'subdomain_bruteforce': {...}}, + 'permutation': {'enabled': True, 'subdomain_permutation_resolve': {...}}, + 'resolve': {'enabled': True, 'subdomain_resolve': {...}} + } + + Args: + scan_config: subdomain_discovery 的配置字典 + + Returns: + 配置字典,供 Flow 使用 + """ + if 'passive_tools' not in scan_config: + logger.warning("子域名发现配置缺少 passive_tools") + return {} + + result = {} + + # Stage 1: 被动收集工具 + passive_tools = scan_config.get('passive_tools', {}) + enabled_passive = {} + for name, config in passive_tools.items(): + if isinstance(config, dict) and config.get('enabled', False): + enabled_passive[name] = _normalize_config_keys(config) + result['passive_tools'] = enabled_passive + + # Stage 2: 字典爆破(可选) + bruteforce = scan_config.get('bruteforce', {}) + if bruteforce.get('enabled', False): + # 转换内部工具配置的 key + normalized_bruteforce = _normalize_config_keys(bruteforce) + if 'subdomain_bruteforce' in normalized_bruteforce: + normalized_bruteforce['subdomain_bruteforce'] = _normalize_config_keys( + normalized_bruteforce['subdomain_bruteforce'] + ) + result['bruteforce'] = normalized_bruteforce + + # Stage 3: 变异生成(可选) + permutation = scan_config.get('permutation', {}) + if permutation.get('enabled', False): + normalized_permutation = _normalize_config_keys(permutation) + if 'subdomain_permutation_resolve' in normalized_permutation: + normalized_permutation['subdomain_permutation_resolve'] = _normalize_config_keys( + normalized_permutation['subdomain_permutation_resolve'] + ) + result['permutation'] = normalized_permutation + + # Stage 4: 存活验证(可选) + resolve = scan_config.get('resolve', {}) + if resolve.get('enabled', False): + normalized_resolve = _normalize_config_keys(resolve) + if 'subdomain_resolve' in normalized_resolve: + normalized_resolve['subdomain_resolve'] = _normalize_config_keys( + normalized_resolve['subdomain_resolve'] + ) + result['resolve'] = normalized_resolve + + logger.info( + f"子域名发现: passive={len(enabled_passive)}, " + f"bruteforce={'bruteforce' in result}, " + f"permutation={'permutation' in result}, " + f"resolve={'resolve' in result}" + ) + return result + + +def parse_enabled_tools_from_dict( + scan_type: str, + parsed_config: Dict[str, Any] +) -> Dict[str, Dict[str, Any]]: + """ + 从解析后的配置字典中获取启用的工具及其配置 + + Args: + scan_type: 扫描类型 (subdomain_discovery, port_scan, site_scan, directory_scan) + parsed_config: 已解析的配置字典 + + Returns: + 启用的工具配置字典 {tool_name: tool_config} + 对于 subdomain_discovery,返回完整的配置结构(支持4阶段增强流程) + + Raises: + ValueError: 配置格式错误或必需参数缺失/无效时抛出 + """ + if not parsed_config: + logger.warning(f"配置字典为空 - scan_type: {scan_type}") + return {} + + if scan_type not in parsed_config: + logger.warning(f"配置中未找到扫描类型: {scan_type}") + return {} + + scan_config = parsed_config[scan_type] + + # 子域名发现支持增强配置格式(4阶段) + if scan_type == 'subdomain_discovery': + return _parse_subdomain_discovery_config(scan_config) + + if 'tools' not in scan_config: + logger.warning(f"扫描类型 {scan_type} 未配置任何工具") + return {} + + tools = scan_config['tools'] + + # 过滤出启用的工具 + enabled_tools = {} + for name, config in tools.items(): + if not isinstance(config, dict): + raise ValueError(f"工具 {name} 配置格式错误:期望 dict,实际 {type(config).__name__}") + + # 检查是否启用(默认为 False) + enabled_value = config.get('enabled', False) + + # 验证 enabled 字段类型 + if not isinstance(enabled_value, bool): + raise ValueError( + f"工具 {name} 的 enabled 字段类型错误:期望 bool,实际 {type(enabled_value).__name__}" + ) + + if enabled_value: + # 检查 timeout 必需参数 + if 'timeout' not in config: + raise ValueError(f"工具 {name} 缺少必需参数 'timeout'") + + # 验证 timeout 值的有效性 + timeout_value = config['timeout'] + + if timeout_value == 'auto': + # 允许 'auto',由具体 Flow 处理 + pass + elif isinstance(timeout_value, int): + if timeout_value <= 0: + raise ValueError(f"工具 {name} 的 timeout 参数无效({timeout_value}),必须大于0") + else: + raise ValueError( + f"工具 {name} 的 timeout 参数类型错误:期望 int 或 'auto',实际 {type(timeout_value).__name__}" + ) + + # 将配置 key 中划线转为下划线,统一给下游代码使用 + enabled_tools[name] = _normalize_config_keys(config) + + logger.info(f"扫描类型: {scan_type}, 启用工具: {len(enabled_tools)}/{len(tools)}") + + return enabled_tools diff --git a/backend/apps/scan/utils/directory_cleanup.py b/backend/apps/scan/utils/directory_cleanup.py new file mode 100644 index 00000000..c8995631 --- /dev/null +++ b/backend/apps/scan/utils/directory_cleanup.py @@ -0,0 +1,60 @@ +""" +目录清理工具模块 + +提供通用的目录清理功能 +""" + +import logging +import shutil +from pathlib import Path + +logger = logging.getLogger(__name__) + + +def remove_directory(directory: str) -> bool: + """ + 删除目录及其所有内容 + + Args: + directory: 目录路径 + + Returns: + 是否删除成功 + + Warning: + 此函数会永久删除目录及其所有内容,请谨慎使用! + + Example: + >>> remove_directory('/path/to/directory') + True + """ + if not directory: + logger.warning("目录路径为空,跳过删除") + return False + + try: + dir_path = Path(directory) + + if not dir_path.exists(): + logger.warning("目录不存在,无需删除 - Path: %s", directory) + return True + + # 删除整个目录 + shutil.rmtree(dir_path) + + logger.info("✓ 目录已删除 - Path: %s", directory) + return True + + except PermissionError as e: + logger.error("权限不足,无法删除目录 - Path: %s, 错误: %s", directory, e) + return False + + except Exception as e: # noqa: BLE001 + logger.exception("删除目录失败 - Path: %s, 错误: %s", directory, e) + return False + + +__all__ = [ + 'remove_directory', +] + diff --git a/backend/apps/scan/utils/nuclei_helpers.py b/backend/apps/scan/utils/nuclei_helpers.py new file mode 100644 index 00000000..79cea346 --- /dev/null +++ b/backend/apps/scan/utils/nuclei_helpers.py @@ -0,0 +1,181 @@ +"""Nuclei 模板 Worker 侧工具函数 + +提供 Worker 侧确保本地模板与 Server 版本一致的功能。 + +使用 Git commit hash 做版本校验: +- 从数据库获取 Server 的 commit_hash +- 检查本地仓库的 commit hash 是否一致 +- 不一致则 git fetch + git checkout 到指定 commit + +调用示例: + template_path = ensure_nuclei_templates_local("nuclei-templates") + # 返回本地模板目录路径,可直接用于 nuclei -t 参数 +""" + +import logging +import subprocess +from pathlib import Path +from typing import Optional + +from django.conf import settings + +from apps.engine.models import NucleiTemplateRepo + +logger = logging.getLogger(__name__) + + +def get_local_commit_hash(local_path: Path) -> Optional[str]: + """获取本地 Git 仓库的当前 commit hash + + Args: + local_path: 本地仓库路径 + + Returns: + commit hash 字符串,失败返回 None + """ + if not (local_path / ".git").is_dir(): + return None + + result = subprocess.run( + ["git", "-C", str(local_path), "rev-parse", "HEAD"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode == 0: + return result.stdout.strip() + return None + + +def git_clone(repo_url: str, local_path: Path) -> bool: + """Git clone 仓库 + + Args: + repo_url: 仓库 URL + local_path: 本地路径 + + Returns: + 是否成功 + """ + logger.info("正在 clone 模板仓库: %s -> %s", repo_url, local_path) + result = subprocess.run( + ["git", "clone", "--depth", "1", repo_url, str(local_path)], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode != 0: + logger.error("git clone 失败: %s", result.stderr.strip()) + return False + return True + + +def git_fetch_and_checkout(local_path: Path, commit_hash: str) -> bool: + """Git fetch 并 checkout 到指定 commit + + Args: + local_path: 本地仓库路径 + commit_hash: 目标 commit hash + + Returns: + 是否成功 + """ + logger.info("正在同步模板到 commit: %s", commit_hash[:8]) + + # 先 unshallow(如果是浅克隆) + subprocess.run( + ["git", "-C", str(local_path), "fetch", "--unshallow"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # fetch origin + fetch_result = subprocess.run( + ["git", "-C", str(local_path), "fetch", "origin"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if fetch_result.returncode != 0: + logger.error("git fetch 失败: %s", fetch_result.stderr.strip()) + return False + + # checkout 到指定 commit + checkout_result = subprocess.run( + ["git", "-C", str(local_path), "checkout", commit_hash], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if checkout_result.returncode != 0: + logger.error("git checkout 失败: %s", checkout_result.stderr.strip()) + return False + + return True + + +def ensure_nuclei_templates_local(repo_name: str) -> str: + """确保 Worker 本地模板与 Server 版本一致 + + 根据仓库名称查询数据库,获取 repo_url 和 commit_hash, + 然后确保本地仓库存在且版本与 Server 一致。 + + Args: + repo_name: 模板仓库名称,对应 NucleiTemplateRepo.name + + Returns: + 本地模板目录的绝对路径 + + Raises: + ValueError: 仓库不存在 + RuntimeError: Git 操作失败 + """ + # 从数据库查询仓库记录 + repo = NucleiTemplateRepo.objects.filter(name=repo_name).first() + if not repo: + raise ValueError(f"未找到模板仓库: {repo_name},请先在「Nuclei 模板」中添加并同步") + + repo_url = repo.repo_url + expected_hash = repo.commit_hash + + if not repo_url: + raise ValueError(f"模板仓库 {repo_name} 缺少 repo_url") + + # 本地存储路径 + base_dir = getattr(settings, "NUCLEI_TEMPLATES_REPOS_BASE_DIR", "/opt/xingrin/nuclei-repos") + local_path = Path(base_dir) / repo_name.replace(" ", "-").lower() + local_path.mkdir(parents=True, exist_ok=True) + + # 检查本地是否有 .git 目录 + if not (local_path / ".git").is_dir(): + # 首次:git clone + if not git_clone(repo_url, local_path): + raise RuntimeError(f"无法 clone 模板仓库: {repo_name}") + else: + # 已有仓库:检查 commit hash + local_hash = get_local_commit_hash(local_path) + + if expected_hash and local_hash != expected_hash: + # commit 不一致:同步到 Server 版本 + logger.info( + "本地模板版本不一致: local=%s, server=%s", + (local_hash or "N/A")[:8], + expected_hash[:8], + ) + if not git_fetch_and_checkout(local_path, expected_hash): + raise RuntimeError(f"无法同步模板仓库到指定版本: {repo_name}") + elif not expected_hash: + # Server 没有 commit_hash(未同步过),保持本地版本 + logger.warning("模板仓库 %s 在 Server 端未同步,使用本地版本", repo_name) + else: + logger.info("本地模板版本一致: %s", local_hash[:8] if local_hash else "N/A") + + return str(local_path) + + +__all__ = ["ensure_nuclei_templates_local"] diff --git a/backend/apps/scan/utils/wordlist_helpers.py b/backend/apps/scan/utils/wordlist_helpers.py new file mode 100644 index 00000000..32156e0c --- /dev/null +++ b/backend/apps/scan/utils/wordlist_helpers.py @@ -0,0 +1,107 @@ +"""字典文件本地缓存与校验工具 + +提供 worker 侧的字典文件下载和 hash 校验功能,用于: +- 目录扫描 (directory_scan_flow) +- 子域名爆破 (subdomain_discovery_flow) +""" + +import logging +import os +from pathlib import Path +from urllib import request as urllib_request +from urllib import parse as urllib_parse + +from django.conf import settings + +from apps.common.hash_utils import is_file_hash_match +from apps.engine.services import WordlistService + +logger = logging.getLogger(__name__) + + +def ensure_wordlist_local(wordlist_name: str) -> str: + """确保本地存在指定字典文件,并返回本地路径 + + 流程: + 1. 从 DB 查询 Wordlist 记录 + 2. 计算本地缓存路径 + 3. 如果本地文件存在且 hash 匹配,直接返回路径 + 4. 否则从后端 API 下载最新文件 + + Args: + wordlist_name: 字典名称(对应 Wordlist.name) + + Returns: + str: 本地字典文件绝对路径 + + Raises: + ValueError: 字典不存在或参数无效 + RuntimeError: 下载失败 + """ + if not wordlist_name: + raise ValueError("wordlist_name 不能为空") + + service = WordlistService() + wordlist = service.get_wordlist_by_name(wordlist_name) + if not wordlist: + raise ValueError(f"未找到名称为 '{wordlist_name}' 的字典,请在「字典管理」中先创建") + + # 计算本地缓存路径 + backend_path = Path(wordlist.file_path) + base_dir = getattr(settings, 'WORDLISTS_BASE_PATH', '/opt/xingrin/wordlists') + storage_dir = Path(base_dir) + storage_dir.mkdir(parents=True, exist_ok=True) + local_path = storage_dir / backend_path.name + + # 获取期望的 hash(可能为空,表示老数据) + expected_hash = getattr(wordlist, 'file_hash', '') or '' + + # 如果本地文件存在,进行 hash 校验 + if local_path.exists(): + if expected_hash: + # 有 hash,进行校验 + if is_file_hash_match(str(local_path), expected_hash): + logger.info("本地字典文件有效(hash 匹配): %s", local_path) + return str(local_path) + else: + logger.info("本地字典文件 hash 不匹配,将重新下载: %s", local_path) + else: + # 无 hash(老数据),保持旧逻辑:直接复用 + logger.info("本地已存在字典文件(无 hash 校验): %s", local_path) + return str(local_path) + + # 从后端下载字典 + # 优先使用 SERVER_URL 环境变量(动态容器中传递),否则使用 settings 配置 + server_url = os.getenv('SERVER_URL', '').strip() + if server_url: + api_base = f"{server_url.rstrip('/')}/api" + else: + public_host = getattr(settings, 'PUBLIC_HOST', '').strip() + if not public_host: + raise RuntimeError( + "无法确定 Django API 地址:请配置 SERVER_URL 或 PUBLIC_HOST 环境变量" + ) + server_port = getattr(settings, 'SERVER_PORT', '8888') + api_base = f"http://{public_host}:{server_port}/api" + query = urllib_parse.urlencode({'wordlist': wordlist_name}) + download_url = f"{api_base.rstrip('/')}/wordlists/download/?{query}" + + logger.info("从后端下载字典: %s -> %s", download_url, local_path) + + try: + with urllib_request.urlopen(download_url) as resp: + if resp.status != 200: + raise RuntimeError(f"下载字典失败,HTTP {resp.status}") + data = resp.read() + except Exception as exc: + logger.error("下载字典失败: %s", exc) + raise RuntimeError(f"下载字典失败: {exc}") from exc + + with open(local_path, 'wb') as f: + f.write(data) + + logger.info("字典下载完成并保存到: %s", local_path) + return str(local_path) + + +__all__ = ["ensure_wordlist_local"] diff --git a/backend/apps/scan/views/__init__.py b/backend/apps/scan/views/__init__.py new file mode 100644 index 00000000..1c0e4721 --- /dev/null +++ b/backend/apps/scan/views/__init__.py @@ -0,0 +1,9 @@ +"""Scan Views - 统一导出""" + +from .scan_views import ScanViewSet +from .scheduled_scan_views import ScheduledScanViewSet + +__all__ = [ + 'ScanViewSet', + 'ScheduledScanViewSet', +] diff --git a/backend/apps/scan/views/scan_views.py b/backend/apps/scan/views/scan_views.py new file mode 100644 index 00000000..41ff8a21 --- /dev/null +++ b/backend/apps/scan/views/scan_views.py @@ -0,0 +1,421 @@ +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.exceptions import NotFound, APIException +from rest_framework.filters import SearchFilter +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.db.utils import DatabaseError, IntegrityError, OperationalError +import logging + +logger = logging.getLogger(__name__) + +from ..models import Scan, ScheduledScan +from ..serializers import ( + ScanSerializer, ScanHistorySerializer, QuickScanSerializer, + ScheduledScanSerializer, CreateScheduledScanSerializer, + UpdateScheduledScanSerializer, ToggleScheduledScanSerializer +) +from ..services.scan_service import ScanService +from ..services.scheduled_scan_service import ScheduledScanService +from ..repositories import ScheduledScanDTO +from apps.targets.services.target_service import TargetService +from apps.targets.services.organization_service import OrganizationService +from apps.engine.services.engine_service import EngineService +from apps.common.definitions import ScanStatus +from apps.common.pagination import BasePagination + + +class ScanViewSet(viewsets.ModelViewSet): + """扫描任务视图集""" + serializer_class = ScanSerializer + pagination_class = BasePagination + filter_backends = [SearchFilter] + search_fields = ['target__name'] # 按目标名称搜索 + + def get_queryset(self): + """优化查询集,提升API性能 + + 查询优化策略: + - select_related: 预加载 target 和 engine(一对一/多对一关系,使用 JOIN) + - 移除 prefetch_related: 避免加载大量资产数据到内存 + - order_by: 按创建时间降序排列(最新创建的任务排在最前面) + + 性能优化原理: + - 列表页:使用缓存统计字段(cached_*_count),避免实时 COUNT 查询 + - 序列化器:严格验证缓存字段,确保数据一致性 + - 分页场景:每页只显示10条记录,查询高效 + - 避免大数据加载:不再预加载所有关联的资产数据 + """ + # 只保留必要的 select_related,移除所有 prefetch_related + scan_service = ScanService() + queryset = scan_service.get_all_scans(prefetch_relations=True) + + return queryset + + def get_serializer_class(self): + """根据不同的 action 返回不同的序列化器 + + - list action: 使用 ScanHistorySerializer(包含 summary 和 progress) + - retrieve action: 使用 ScanHistorySerializer(包含 summary 和 progress) + - 其他 action: 使用标准的 ScanSerializer + """ + if self.action in ['list', 'retrieve']: + return ScanHistorySerializer + return ScanSerializer + + def destroy(self, request, *args, **kwargs): + """ + 删除单个扫描任务(两阶段删除) + + 1. 软删除:立即对用户不可见 + 2. 硬删除:后台异步执行 + """ + try: + scan = self.get_object() + scan_service = ScanService() + result = scan_service.delete_scans_two_phase([scan.id]) + + return Response({ + 'message': f'已删除扫描任务: Scan #{scan.id}', + 'scanId': scan.id, + 'deletedCount': result['soft_deleted_count'], + 'deletedScans': result['scan_names'], + 'detail': { + 'phase1': '软删除完成,用户已看不到数据', + 'phase2': '硬删除任务已分发,将在后台执行' + } + }, status=status.HTTP_200_OK) + + except Scan.DoesNotExist: + raise NotFound('扫描任务不存在') + except ValueError as e: + raise NotFound(str(e)) + except Exception as e: + logger.exception("删除扫描任务时发生错误") + raise APIException('服务器错误,请稍后重试') + + @action(detail=False, methods=['post']) + def quick(self, request): + """ + 快速扫描接口 + + 功能: + 1. 接收目标列表和引擎配置 + 2. 自动批量创建/获取目标 + 3. 立即发起批量扫描 + + 请求参数: + { + "targets": [{"name": "example.com"}, {"name": "1.1.1.1"}], + "engine_id": 1 + } + """ + serializer = QuickScanSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + targets_data = serializer.validated_data['targets'] + engine_id = serializer.validated_data.get('engine_id') + + try: + # 1. 批量创建/获取目标 + target_service = TargetService() + batch_result = target_service.batch_create_targets( + targets_data=targets_data, + organization_id=None # 快速扫描不关联组织 + ) + + # 收集所有目标对象(包括新创建和已存在的) + # batch_create_targets 返回的是统计信息,我们需要获取目标对象列表 + # 这里重新查询刚刚创建/获取的目标 + target_names = [t['name'] for t in targets_data] + targets = target_service.get_targets_by_names(target_names) + + if not targets: + return Response( + {'error': '没有有效的目标可供扫描'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. 获取扫描引擎 + engine_service = EngineService() + engine = engine_service.get_engine(engine_id) + if not engine: + raise ValidationError(f'扫描引擎 ID {engine_id} 不存在') + + # 3. 批量发起扫描 + scan_service = ScanService() + created_scans = scan_service.create_scans( + targets=targets, + engine=engine + ) + + # 序列化返回结果 + scan_serializer = ScanSerializer(created_scans, many=True) + + return Response({ + 'message': f'快速扫描已启动:{len(created_scans)} 个任务', + 'target_stats': { + 'created': batch_result['created_count'], + 'failed': batch_result['failed_count'] + }, + 'scans': scan_serializer.data + }, status=status.HTTP_201_CREATED) + + except ValidationError as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.exception("快速扫描启动失败") + return Response( + {'error': '服务器内部错误,请稍后重试'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + @action(detail=False, methods=['post']) + def initiate(self, request): + """ + 发起扫描任务 + + 请求参数: + - organization_id: 组织ID (int, 可选) + - target_id: 目标ID (int, 可选) + - engine_id: 扫描引擎ID (int, 必填) + + 注意: organization_id 和 target_id 二选一 + + 返回: + - 扫描任务详情(单个或多个) + """ + # 获取请求数据 + organization_id = request.data.get('organization_id') + target_id = request.data.get('target_id') + engine_id = request.data.get('engine_id') + + try: + # 步骤1:准备扫描所需的数据(验证参数、查询资源、返回目标列表和引擎) + scan_service = ScanService() + targets, engine = scan_service.prepare_initiate_scan( + organization_id=organization_id, + target_id=target_id, + engine_id=engine_id + ) + + # 步骤2:批量创建扫描记录并分发扫描任务 + created_scans = scan_service.create_scans( + targets=targets, + engine=engine + ) + + # 序列化返回结果 + scan_serializer = ScanSerializer(created_scans, many=True) + + return Response( + { + 'message': f'已成功发起 {len(created_scans)} 个扫描任务', + 'count': len(created_scans), + 'scans': scan_serializer.data + }, + status=status.HTTP_201_CREATED + ) + + except ObjectDoesNotExist as e: + # 资源不存在错误(由 service 层抛出) + error_msg = str(e) + return Response( + {'error': error_msg}, + status=status.HTTP_404_NOT_FOUND + ) + + except ValidationError as e: + # 参数验证错误(由 service 层抛出) + return Response( + {'error': str(e)}, + status=status.HTTP_400_BAD_REQUEST + ) + + except (DatabaseError, IntegrityError, OperationalError): + # 数据库错误 + return Response( + {'error': '数据库错误,请稍后重试'}, + status=status.HTTP_503_SERVICE_UNAVAILABLE + ) + + # 所有快照相关的 action 和 export 已迁移到 asset/views.py 中的快照 ViewSet + # GET /api/scans/{id}/subdomains/ -> SubdomainSnapshotViewSet + # GET /api/scans/{id}/subdomains/export/ -> SubdomainSnapshotViewSet.export + # GET /api/scans/{id}/websites/ -> WebsiteSnapshotViewSet + # GET /api/scans/{id}/websites/export/ -> WebsiteSnapshotViewSet.export + # GET /api/scans/{id}/directories/ -> DirectorySnapshotViewSet + # GET /api/scans/{id}/directories/export/ -> DirectorySnapshotViewSet.export + # GET /api/scans/{id}/endpoints/ -> EndpointSnapshotViewSet + # GET /api/scans/{id}/endpoints/export/ -> EndpointSnapshotViewSet.export + # GET /api/scans/{id}/ip-addresses/ -> HostPortMappingSnapshotViewSet + # GET /api/scans/{id}/ip-addresses/export/ -> HostPortMappingSnapshotViewSet.export + # GET /api/scans/{id}/vulnerabilities/ -> VulnerabilitySnapshotViewSet + + @action(detail=False, methods=['post', 'delete'], url_path='bulk-delete') + def bulk_delete(self, request): + """ + 批量删除扫描记录 + + 请求参数: + - ids: 扫描ID列表 (list[int], 必填) + + 示例请求: + POST /api/scans/bulk-delete/ + { + "ids": [1, 2, 3] + } + + 返回: + - message: 成功消息 + - deletedCount: 实际删除的记录数 + + 注意: + - 使用级联删除,会同时删除关联的子域名、端点等数据 + - 只删除存在的记录,不存在的ID会被忽略 + """ + ids = request.data.get('ids', []) + + # 参数验证 + if not ids: + return Response( + {'error': '缺少必填参数: ids'}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not isinstance(ids, list): + return Response( + {'error': 'ids 必须是数组'}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not all(isinstance(i, int) for i in ids): + return Response( + {'error': 'ids 数组中的所有元素必须是整数'}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + # 使用 Service 层批量删除(两阶段删除) + scan_service = ScanService() + result = scan_service.delete_scans_two_phase(ids) + + return Response({ + 'message': f"已删除 {result['soft_deleted_count']} 个扫描任务", + 'deletedCount': result['soft_deleted_count'], + 'deletedScans': result['scan_names'], + 'detail': { + 'phase1': '软删除完成,用户已看不到数据', + 'phase2': '硬删除任务已分发,将在后台执行' + } + }, status=status.HTTP_200_OK) + + except ValueError as e: + # 未找到记录 + raise NotFound(str(e)) + + except Exception as e: + logger.exception("批量删除扫描任务时发生错误") + raise APIException('服务器错误,请稍后重试') + + @action(detail=False, methods=['get']) + def statistics(self, request): + """ + 获取扫描统计数据 + + 返回扫描任务的汇总统计信息,用于仪表板和扫描历史页面。 + 使用缓存字段聚合查询,性能优异。 + + 返回: + - total: 总扫描次数 + - running: 运行中的扫描数量 + - completed: 已完成的扫描数量 + - failed: 失败的扫描数量 + - totalVulns: 总共发现的漏洞数量 + - totalSubdomains: 总共发现的子域名数量 + - totalEndpoints: 总共发现的端点数量 + - totalAssets: 总资产数 + """ + try: + # 使用 Service 层获取统计数据 + scan_service = ScanService() + stats = scan_service.get_statistics() + + return Response({ + 'total': stats['total'], + 'running': stats['running'], + 'completed': stats['completed'], + 'failed': stats['failed'], + 'totalVulns': stats['total_vulns'], + 'totalSubdomains': stats['total_subdomains'], + 'totalEndpoints': stats['total_endpoints'], + 'totalWebsites': stats['total_websites'], + 'totalAssets': stats['total_assets'], + }) + + except (DatabaseError, OperationalError): + return Response( + {'error': '数据库错误,请稍后重试'}, + status=status.HTTP_503_SERVICE_UNAVAILABLE + ) + + @action(detail=True, methods=['post']) + def stop(self, request, pk=None): # pylint: disable=unused-argument + """ + 停止扫描任务 + + URL: POST /api/scans/{id}/stop/ + + 功能: + - 终止正在运行或初始化的扫描任务 + - 更新扫描状态为 CANCELLED + + 状态限制: + - 只能停止 RUNNING 或 INITIATED 状态的扫描 + - 已完成、失败或取消的扫描无法停止 + + 返回: + - message: 成功消息 + - revokedTaskCount: 取消的 Flow Run 数量 + """ + try: + # 使用 Service 层处理停止逻辑 + scan_service = ScanService() + success, revoked_count = scan_service.stop_scan(scan_id=pk) + + if not success: + # 检查是否是状态不允许的问题 + scan = scan_service.get_scan(scan_id=pk, prefetch_relations=False) + if scan and scan.status not in [ScanStatus.RUNNING, ScanStatus.INITIATED]: + return Response( + { + 'error': f'无法停止扫描:当前状态为 {ScanStatus(scan.status).label}', + 'detail': '只能停止运行中或初始化状态的扫描' + }, + status=status.HTTP_400_BAD_REQUEST + ) + # 其他失败原因 + return Response( + {'error': '停止扫描失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + return Response( + { + 'message': f'扫描已停止,已撤销 {revoked_count} 个任务', + 'revokedTaskCount': revoked_count + }, + status=status.HTTP_200_OK + ) + + except ObjectDoesNotExist: + return Response( + {'error': f'扫描 ID {pk} 不存在'}, + status=status.HTTP_404_NOT_FOUND + ) + + except (DatabaseError, IntegrityError, OperationalError): + return Response( + {'error': '数据库错误,请稍后重试'}, + status=status.HTTP_503_SERVICE_UNAVAILABLE + ) diff --git a/backend/apps/scan/views/scheduled_scan_views.py b/backend/apps/scan/views/scheduled_scan_views.py new file mode 100644 index 00000000..900ab8eb --- /dev/null +++ b/backend/apps/scan/views/scheduled_scan_views.py @@ -0,0 +1,149 @@ +""" +定时扫描任务视图集 + +独立文件,避免 views.py 文件过大 +""" +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.filters import SearchFilter +from django.core.exceptions import ValidationError +import logging + +from ..models import ScheduledScan +from ..serializers import ( + ScheduledScanSerializer, CreateScheduledScanSerializer, + UpdateScheduledScanSerializer, ToggleScheduledScanSerializer +) +from ..services.scheduled_scan_service import ScheduledScanService +from ..repositories import ScheduledScanDTO +from apps.common.pagination import BasePagination + + +logger = logging.getLogger(__name__) + + +class ScheduledScanViewSet(viewsets.ModelViewSet): + """ + 定时扫描任务视图集 + + API 端点: + - GET /scheduled-scans/ 获取定时扫描列表 + - POST /scheduled-scans/ 创建定时扫描 + - GET /scheduled-scans/{id}/ 获取定时扫描详情 + - PUT /scheduled-scans/{id}/ 更新定时扫描 + - DELETE /scheduled-scans/{id}/ 删除定时扫描 + - POST /scheduled-scans/{id}/toggle/ 切换启用状态 + """ + + queryset = ScheduledScan.objects.all().order_by('-created_at') + serializer_class = ScheduledScanSerializer + pagination_class = BasePagination + filter_backends = [SearchFilter] + search_fields = ['name'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.service = ScheduledScanService() + + def get_serializer_class(self): + """根据 action 返回不同的序列化器""" + if self.action == 'create': + return CreateScheduledScanSerializer + elif self.action in ['update', 'partial_update']: + return UpdateScheduledScanSerializer + elif self.action == 'toggle': + return ToggleScheduledScanSerializer + return ScheduledScanSerializer + + def create(self, request, *args, **kwargs): + """创建定时扫描任务""" + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + try: + data = serializer.validated_data + dto = ScheduledScanDTO( + name=data['name'], + engine_id=data['engine_id'], + organization_id=data.get('organization_id'), + target_id=data.get('target_id'), + cron_expression=data.get('cron_expression', '0 2 * * *'), + is_enabled=data.get('is_enabled', True), + ) + + scheduled_scan = self.service.create(dto) + response_serializer = ScheduledScanSerializer(scheduled_scan) + + return Response( + { + 'message': f'创建定时扫描任务成功: {scheduled_scan.name}', + 'scheduled_scan': response_serializer.data + }, + status=status.HTTP_201_CREATED + ) + except ValidationError as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + def update(self, request, *args, **kwargs): + """更新定时扫描任务""" + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + try: + data = serializer.validated_data + dto = ScheduledScanDTO( + name=data.get('name'), + engine_id=data.get('engine_id'), + organization_id=data.get('organization_id'), + target_id=data.get('target_id'), + cron_expression=data.get('cron_expression'), + is_enabled=data.get('is_enabled'), + ) + + scheduled_scan = self.service.update(instance.id, dto) + response_serializer = ScheduledScanSerializer(scheduled_scan) + + return Response({ + 'message': f'更新定时扫描任务成功: {scheduled_scan.name}', + 'scheduled_scan': response_serializer.data + }) + except ValidationError as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, *args, **kwargs): + """删除定时扫描任务""" + instance = self.get_object() + name = instance.name + + if self.service.delete(instance.id): + return Response({ + 'message': f'删除定时扫描任务成功: {name}', + 'id': instance.id + }) + return Response({'error': '删除失败'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=True, methods=['post']) + def toggle(self, request, pk=None): + """切换定时扫描任务的启用状态""" + serializer = ToggleScheduledScanSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + is_enabled = serializer.validated_data['is_enabled'] + + if self.service.toggle_enabled(int(pk), is_enabled): + scheduled_scan = self.get_object() + response_serializer = ScheduledScanSerializer(scheduled_scan) + + status_text = '启用' if is_enabled else '禁用' + return Response({ + 'message': f'已{status_text}定时扫描任务', + 'scheduled_scan': response_serializer.data + }) + + return Response( + {'error': f'定时扫描任务 ID {pk} 不存在或操作失败'}, + status=status.HTTP_404_NOT_FOUND + ) + diff --git a/backend/apps/targets/__init__.py b/backend/apps/targets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/targets/apps.py b/backend/apps/targets/apps.py new file mode 100644 index 00000000..1696d84b --- /dev/null +++ b/backend/apps/targets/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class TargetsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.targets' + verbose_name = '扫描目标管理' diff --git a/backend/apps/targets/migrations/__init__.py b/backend/apps/targets/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/targets/models.py b/backend/apps/targets/models.py new file mode 100644 index 00000000..be04bbad --- /dev/null +++ b/backend/apps/targets/models.py @@ -0,0 +1,115 @@ +from django.db import models +from django.utils import timezone + + +class SoftDeleteManager(models.Manager): + """软删除管理器:默认只返回未删除的记录""" + + def get_queryset(self): + return super().get_queryset().filter(deleted_at__isnull=True) + + +class Organization(models.Model): + """组织模型""" + + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=300, blank=True, default='', help_text='组织名称') + description = models.CharField(max_length=1000, blank=True, default='', help_text='组织描述') + created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间') + + # ==================== 软删除字段 ==================== + deleted_at = models.DateTimeField(null=True, blank=True, db_index=True, help_text='删除时间(NULL表示未删除)') + + targets = models.ManyToManyField( + 'Target', + related_name='organizations', + blank=True, + help_text='所属目标列表' + ) + + # ==================== 管理器 ==================== + objects = SoftDeleteManager() # 默认管理器:只返回未删除的记录 + all_objects = models.Manager() # 全量管理器:包括已删除的记录(用于硬删除) + + class Meta: + db_table = 'organization' + verbose_name = '组织' + verbose_name_plural = '组织' + ordering = ['-created_at'] + # 部分唯一约束:只对未删除记录生效 + constraints = [ + models.UniqueConstraint( + fields=['name'], + condition=models.Q(deleted_at__isnull=True), + name='unique_organization_name_active' + ), + ] + indexes = [ + models.Index(fields=['-created_at']), + models.Index(fields=['deleted_at', '-created_at']), # 软删除 + 时间索引 + models.Index(fields=['name']), # 优化 name 搜索 + ] + + def __str__(self): + return str(self.name or f'Organization {self.id}') + + +class Target(models.Model): + """扫描目标模型 + + 核心模型,存储要扫描的目标信息。 + 支持多种类型:域名、IP地址、CIDR范围等。 + """ + + # ==================== 类型定义 ==================== + class TargetType(models.TextChoices): + DOMAIN = 'domain', '域名' + IP = 'ip', 'IP地址' + CIDR = 'cidr', 'CIDR范围' + + # ==================== 基本字段 ==================== + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=300, blank=True, default='', help_text='目标标识(域名/IP/CIDR)') + + type = models.CharField( + max_length=20, + choices=TargetType.choices, + default=TargetType.DOMAIN, + db_index=True, + help_text='目标类型' + ) + + # ==================== 时间戳 ==================== + created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间') + last_scanned_at = models.DateTimeField(null=True, blank=True, help_text='最后扫描时间') + + # ==================== 软删除字段 ==================== + deleted_at = models.DateTimeField(null=True, blank=True, db_index=True, help_text='删除时间(NULL表示未删除)') + + # ==================== 管理器 ==================== + objects = SoftDeleteManager() # 默认管理器:只返回未删除的记录 + all_objects = models.Manager() # 全量管理器:包括已删除的记录(用于硬删除) + + class Meta: + db_table = 'target' + verbose_name = '扫描目标' + verbose_name_plural = '扫描目标' + ordering = ['-created_at'] + # 部分唯一约束:只对未删除记录生效 + constraints = [ + models.UniqueConstraint( + fields=['name'], + condition=models.Q(deleted_at__isnull=True), + name='unique_target_name_active' + ), + ] + indexes = [ + models.Index(fields=['type']), + models.Index(fields=['-created_at']), + models.Index(fields=['deleted_at', '-created_at']), # 软删除 + 时间索引 + models.Index(fields=['deleted_at', 'type']), # 软删除 + 类型索引 + models.Index(fields=['name']), # 优化 name 搜索 + ] + + def __str__(self): + return str(self.name or f'Target {self.id}') diff --git a/backend/apps/targets/repositories/__init__.py b/backend/apps/targets/repositories/__init__.py new file mode 100644 index 00000000..968bfe9f --- /dev/null +++ b/backend/apps/targets/repositories/__init__.py @@ -0,0 +1,13 @@ +""" +Target Repositories 模块 + +提供 Target 和 Organization 数据访问层 +""" + +from .django_target_repository import DjangoTargetRepository +from .django_organization_repository import DjangoOrganizationRepository + +__all__ = [ + 'DjangoTargetRepository', + 'DjangoOrganizationRepository', +] diff --git a/backend/apps/targets/repositories/django_organization_repository.py b/backend/apps/targets/repositories/django_organization_repository.py new file mode 100644 index 00000000..7d87bc66 --- /dev/null +++ b/backend/apps/targets/repositories/django_organization_repository.py @@ -0,0 +1,204 @@ +""" +Organization Django ORM 仓储实现 + +使用 Django ORM 实现组织数据访问 +""" + +import logging +from typing import List, Tuple, Dict +from django.db.models import Count +from django.utils import timezone + +from ..models import Organization, Target +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoOrganizationRepository: + """Organization Django ORM 仓储实现""" + + def bulk_add_targets(self, organization_id: int, targets: List[Target]) -> None: + """ + 批量添加目标到组织 + + Args: + organization_id: 组织 ID + targets: Target 对象列表 + """ + if not targets: + return + + # 使用 through model 批量插入,避免 N 次 add() + ThroughModel = Organization.targets.through + relations = [ + ThroughModel(organization_id=organization_id, target_id=t.id) + for t in targets + ] + + try: + # 使用 ignore_conflicts 忽略已存在的关联 + ThroughModel.objects.bulk_create(relations, ignore_conflicts=True) + except Exception as e: + logger.error(f"批量关联目标失败: {e}") + raise + + def get_by_id(self, organization_id: int) -> Organization | None: + """ + 根据 ID 获取组织 + + Args: + organization_id: 组织 ID + + Returns: + Organization 对象或 None + """ + try: + return Organization.objects.get(id=organization_id) + except Organization.DoesNotExist: + logger.warning("组织不存在 - Organization ID: %s", organization_id) + return None + + def get_names_by_ids(self, organization_ids: List[int]) -> List[Tuple[int, str]]: + """ + 根据 ID 列表获取组织的 ID 和名称 + + Args: + organization_ids: 组织 ID 列表 + + Returns: + [(id, name), ...] 元组列表 + """ + return list( + Organization.objects + .filter(id__in=organization_ids) + .values_list('id', 'name') + ) + + def soft_delete_by_ids(self, organization_ids: List[int]) -> int: + """ + 根据 ID 列表批量软删除组织 + + Args: + organization_ids: 组织 ID 列表 + + Returns: + 软删除的记录数 + + Note: + - 使用软删除:只标记为已删除,不真正删除数据库记录 + - 保留所有关联数据,可恢复 + - 不会影响关联的目标(多对多关系保持不变) + """ + try: + updated_count = ( + Organization.objects + .filter(id__in=organization_ids) + .update(deleted_at=timezone.now()) + ) + logger.debug( + "批量软删除组织成功 - Count: %s, 更新记录: %s", + len(organization_ids), + updated_count + ) + return updated_count + except Exception as e: + logger.error( + "批量软删除组织失败 - IDs: %s, 错误: %s", + organization_ids, + e + ) + raise + + def get_targets(self, organization_id: int) -> List[Target]: + """ + 获取组织下的所有目标 + + Args: + organization_id: 组织 ID + + Returns: + Target 对象列表 + """ + organization = self.get_by_id(organization_id) + if not organization: + return [] + return list(organization.targets.all()) + + def get_all(self): + """ + 获取所有组织 + + Returns: + QuerySet: 组织查询集 + """ + return Organization.objects.all() + + def get_all_with_stats(self): + """ + 获取所有组织并预计算目标数量 + + Returns: + QuerySet: 带统计信息的组织查询集 + """ + return ( + Organization.objects + .annotate(target_count=Count('targets')) + .order_by('-created_at') + ) + + def get_by_ids(self, organization_ids: List[int]) -> List[Organization]: + """ + 根据 ID 列表获取组织(只返回未删除的) + + Args: + organization_ids: 组织 ID 列表 + + Returns: + Organization 对象列表 + """ + return list(Organization.objects.filter(id__in=organization_ids)) + + def hard_delete_by_ids(self, organization_ids: List[int]) -> Tuple[int, Dict[str, int]]: + """ + 根据 ID 列表硬删除组织(真正删除数据) + + Args: + organization_ids: 组织 ID 列表 + + Returns: + (删除的记录数, 删除详情字典) + + Note: + - 硬删除:从数据库中永久删除 + - 使用 Django CASCADE 自动删除中间表 organization_targets 的关联记录 + - 不会删除关联的 Target(多对多关系) + - ⚠️ 不可恢复 + - @auto_ensure_db_connection 自动重试数据库连接失败 + """ + try: + # 使用 all_objects 管理器,可以删除已软删除的记录 + deleted_count, deleted_details = ( + Organization.all_objects + .filter(id__in=organization_ids) + .delete() + ) + + logger.debug( + "硬删除组织成功 - Count: %s, 删除记录数: %s, 详情: %s", + len(organization_ids), + deleted_count, + deleted_details + ) + + return deleted_count, deleted_details + + except Exception as e: + logger.error( + "硬删除组织失败 - IDs: %s, 错误: %s", + organization_ids, + e + ) + raise + diff --git a/backend/apps/targets/repositories/django_target_repository.py b/backend/apps/targets/repositories/django_target_repository.py new file mode 100644 index 00000000..f3610689 --- /dev/null +++ b/backend/apps/targets/repositories/django_target_repository.py @@ -0,0 +1,257 @@ +""" +Target Django ORM 仓储实现 + +使用 Django ORM 实现目标数据访问 +""" + +import logging +from typing import List, Tuple, Dict +from django.db import transaction, IntegrityError, OperationalError, DatabaseError +from django.utils import timezone + +from ..models import Target +from apps.common.decorators import auto_ensure_db_connection + +logger = logging.getLogger(__name__) + + +@auto_ensure_db_connection +class DjangoTargetRepository: + """Target Django ORM 仓储实现""" + + def count_by_ids(self, target_ids: List[int]) -> int: + """ + 统计给定 ID 列表中存在的目标数量 + + Args: + target_ids: 目标 ID 列表 + + Returns: + 存在的目标数量 + """ + if not target_ids: + return 0 + return Target.objects.filter(id__in=target_ids).count() + + def get_by_ids(self, target_ids: List[int]) -> List[Target]: + """ + 根据 ID 列表批量获取目标 + + Args: + target_ids: 目标 ID 列表 + + Returns: + Target 对象列表 + """ + if not target_ids: + return [] + return list(Target.objects.filter(id__in=target_ids)) + + def bulk_create_ignore_conflicts(self, targets: List[Target]) -> None: + """ + 批量创建目标,忽略冲突 + + Args: + targets: Target 对象列表 + """ + if not targets: + return + + try: + Target.objects.bulk_create(targets, ignore_conflicts=True) + except Exception as e: + logger.error(f"批量创建目标失败: {e}") + raise + + def get_by_names(self, names: List[str]) -> List[Target]: + """ + 根据名称列表批量获取目标 + + Args: + names: 目标名称列表 + + Returns: + Target 对象列表 + """ + if not names: + return [] + return list(Target.objects.filter(name__in=names)) + + def get_by_id(self, target_id: int) -> Target | None: + """ + 根据 ID 获取目标 + + Args: + target_id: 目标 ID + + Returns: + Target 对象或 None + """ + try: + return Target.objects.get(id=target_id) + except Target.DoesNotExist: + logger.warning("目标不存在 - Target ID: %s", target_id) + return None + + def get_names_by_ids(self, target_ids: List[int]) -> List[Tuple[int, str]]: + """ + 根据 ID 列表获取目标的 ID 和名称 + + Args: + target_ids: 目标 ID 列表 + + Returns: + [(id, name), ...] 元组列表 + """ + return list( + Target.objects + .filter(id__in=target_ids) + .values_list('id', 'name') + ) + + def soft_delete_by_ids(self, target_ids: List[int]) -> int: + """ + 根据 ID 列表批量软删除目标 + + Args: + target_ids: 目标 ID 列表 + + Returns: + 软删除的记录数 + + Note: + - 使用软删除:只标记为已删除,不真正删除数据库记录 + - 保留所有关联数据,可恢复 + """ + try: + updated_count = ( + Target.objects + .filter(id__in=target_ids) + .update(deleted_at=timezone.now()) + ) + logger.debug( + "批量软删除目标成功 - Count: %s, 更新记录: %s", + len(target_ids), + updated_count + ) + return updated_count + except Exception as e: + logger.error( + "批量软删除目标失败 - IDs: %s, 错误: %s", + target_ids, + e + ) + raise + + def get_all(self): + """ + 获取所有目标 + + Returns: + QuerySet: 目标查询集 + """ + return Target.objects.prefetch_related('organizations').all() + + def get_or_create(self, name: str, target_type: str): + """ + 获取或创建目标 + + Args: + name: 目标名称 + target_type: 目标类型 + + Returns: + (Target对象, 是否新创建的布尔值) + """ + return Target.objects.get_or_create( + name=name, + defaults={'type': target_type} + ) + + def get_by_ids(self, target_ids: List[int]) -> List[Target]: + """ + 根据 ID 列表获取目标(只返回未删除的) + + Args: + target_ids: 目标 ID 列表 + + Returns: + Target 对象列表 + """ + return list(Target.objects.filter(id__in=target_ids)) + + def hard_delete_by_ids(self, target_ids: List[int]) -> Tuple[int, Dict[str, int]]: + """ + 根据 ID 列表硬删除目标(使用数据库级 CASCADE) + + Args: + target_ids: 目标 ID 列表 + + Returns: + (删除的记录数, 删除详情字典) + + Strategy: + 使用数据库级 CASCADE 删除,性能最优 + + Note: + - 硬删除:从数据库中永久删除 + - 数据库自动处理所有外键级联删除 + - 不触发 Django 信号(pre_delete/post_delete) + """ + try: + batch_size = 1000 # 每批处理1000个目标 + total_deleted = 0 + + logger.debug(f"开始批量删除 {len(target_ids)} 个目标(数据库 CASCADE)...") + + # 分批处理目标ID,避免单次删除过多 + for i in range(0, len(target_ids), batch_size): + batch_ids = target_ids[i:i + batch_size] + + # 直接删除目标,数据库自动级联删除所有关联数据 + count, _ = Target.all_objects.filter(id__in=batch_ids).delete() + total_deleted += count + + logger.debug(f"批次删除完成: {len(batch_ids)} 个目标,删除 {count} 条记录") + + # 由于使用数据库 CASCADE,无法获取详细统计 + deleted_details = { + 'targets': len(target_ids), + 'total': total_deleted, + 'note': 'Database CASCADE - detailed stats unavailable' + } + + logger.debug( + "批量硬删除成功(CASCADE)- 目标数: %s, 总删除记录: %s", + len(target_ids), + total_deleted + ) + + return total_deleted, deleted_details + + except Exception as e: + logger.error( + "批量硬删除失败(CASCADE)- 目标数: %s, 错误: %s", + len(target_ids), + str(e), + exc_info=True + ) + raise + + def update_last_scanned_at(self, target_id: int, scanned_at) -> bool: + """ + 更新目标的最后扫描时间 + + Args: + target_id: 目标 ID + scanned_at: 扫描时间 + + Returns: + 是否更新成功 + """ + try: + updated = Target.objects.filter(id=target_id).update(last_scanned_at=scanned_at) + return updated > 0 + except Exception as e: + logger.error("更新最后扫描时间失败 - Target ID: %s, 错误: %s", target_id, e) + return False diff --git a/backend/apps/targets/scripts/__init__.py b/backend/apps/targets/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/targets/scripts/run_delete_organizations.py b/backend/apps/targets/scripts/run_delete_organizations.py new file mode 100644 index 00000000..ed33b0d5 --- /dev/null +++ b/backend/apps/targets/scripts/run_delete_organizations.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +""" +组织硬删除脚本 + +用于动态容器执行,硬删除已软删除的组织及其关联数据。 +""" +import sys +import argparse +import json +import logging +from apps.common.container_bootstrap import fetch_config_and_setup_django + +logging.basicConfig( + level=logging.INFO, + format='[%(asctime)s] [%(levelname)s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) +logger = logging.getLogger(__name__) + + +def hard_delete_organizations(organization_ids: list[int]) -> dict: + """ + 硬删除组织 + + Args: + organization_ids: 组织 ID 列表 + + Returns: + 删除统计信息 + """ + from apps.targets.services import OrganizationService + + service = OrganizationService() + + try: + deleted_count, details = service.hard_delete_organizations(organization_ids) + + logger.info(f"✓ 硬删除完成 - 删除数量: {deleted_count}") + logger.info(f" 详情: {details}") + + return { + 'success': True, + 'deleted_count': deleted_count, + 'details': details, + } + + except Exception as e: + logger.error(f"硬删除失败: {e}", exc_info=True) + return { + 'success': False, + 'error': str(e), + } + + +def main(): + parser = argparse.ArgumentParser(description="硬删除组织") + parser.add_argument("--organization_ids", type=str, required=True, help="组织 ID 列表 (JSON)") + + args = parser.parse_args() + + # 解析 organization_ids + organization_ids = json.loads(args.organization_ids) + + logger.info(f"开始硬删除 {len(organization_ids)} 个组织") + + # 获取配置并初始化 Django + fetch_config_and_setup_django() + + # 执行删除 + result = hard_delete_organizations(organization_ids) + + print(f"删除完成: {result}") + + if not result.get('success'): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/backend/apps/targets/scripts/run_delete_targets.py b/backend/apps/targets/scripts/run_delete_targets.py new file mode 100644 index 00000000..ab4cdabb --- /dev/null +++ b/backend/apps/targets/scripts/run_delete_targets.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +""" +目标硬删除脚本 + +用于动态容器执行,硬删除已软删除的目标。 +""" +import sys +import argparse +import json +import logging +from apps.common.container_bootstrap import fetch_config_and_setup_django + +logging.basicConfig( + level=logging.INFO, + format='[%(asctime)s] [%(levelname)s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) +logger = logging.getLogger(__name__) + + +def hard_delete_targets(target_ids: list[int]) -> dict: + """ + 硬删除目标 + + Args: + target_ids: 目标 ID 列表 + + Returns: + 删除统计信息 + """ + from apps.targets.services import TargetService + + service = TargetService() + + try: + deleted_count, details = service.hard_delete_targets(target_ids) + + logger.info(f"✓ 硬删除完成 - 删除数量: {deleted_count}") + logger.info(f" 详情: {details}") + + return { + 'success': True, + 'deleted_count': deleted_count, + 'details': details, + } + + except Exception as e: + logger.error(f"硬删除失败: {e}", exc_info=True) + return { + 'success': False, + 'error': str(e), + } + + +def main(): + parser = argparse.ArgumentParser(description="硬删除目标") + parser.add_argument("--target_ids", type=str, required=True, help="目标 ID 列表 (JSON)") + + args = parser.parse_args() + + # 解析 target_ids + target_ids = json.loads(args.target_ids) + + logger.info(f"开始硬删除 {len(target_ids)} 个目标") + + # 获取配置并初始化 Django + fetch_config_and_setup_django() + + # 执行删除 + result = hard_delete_targets(target_ids) + + print(f"删除完成: {result}") + + if not result.get('success'): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/backend/apps/targets/serializers.py b/backend/apps/targets/serializers.py new file mode 100644 index 00000000..fb3df617 --- /dev/null +++ b/backend/apps/targets/serializers.py @@ -0,0 +1,224 @@ +from rest_framework import serializers +from django.db import IntegrityError +from django.db.models import Count +from .models import Organization, Target +from apps.common.normalizer import normalize_target +from apps.common.validators import detect_target_type +from apps.asset.models import Vulnerability + + +class SimpleOrganizationSerializer(serializers.ModelSerializer): + """ + 简化版组织序列化器 - 用于嵌套在其他序列化器中 + + 注意事项: + 1. 只包含基本字段 (id, name),不嵌套 targets + 2. 避免循环引用:Organization ↔ Target 是多对多关系 + 如果双向嵌套会导致无限递归 + 3. 适用场景: + - 在 TargetSerializer 中显示所属组织列表 + - 在其他需要显示组织基本信息的地方 + """ + class Meta: + model = Organization + fields = ['id', 'name'] + + +class TargetSerializer(serializers.ModelSerializer): + """ + 目标序列化器 + + 性能优化说明: + 1. 使用嵌套序列化器 SimpleOrganizationSerializer 显示关联的组织 + 2. ⚠️ 重要:ViewSet 必须使用 prefetch_related('organizations') + 否则会产生 N+1 查询问题: + - 没有预加载:100 个目标 = 1 + 100 = 101 次查询 + - 正确预加载:100 个目标 = 1 + 1 = 2 次查询 + + 已优化的视图: + - TargetViewSet: queryset = Target.objects.prefetch_related('organizations') + - OrganizationViewSet.targets(): queryset.prefetch_related('organizations') + """ + organizations = SimpleOrganizationSerializer(many=True, read_only=True) + + class Meta: + model = Target + fields = ['id', 'name', 'type', 'created_at', 'last_scanned_at', 'organizations'] + read_only_fields = ['id', 'created_at', 'type'] + + def create(self, validated_data): + """创建目标时自动规范化、检测目标类型""" + name = validated_data.get('name', '') + try: + # 1. 规范化 + normalized_name = normalize_target(name) + # 2. 验证并检测类型 + target_type = detect_target_type(normalized_name) + # 3. 写入 + validated_data['name'] = normalized_name + validated_data['type'] = target_type + + return super().create(validated_data) + except ValueError as e: + raise serializers.ValidationError({'name': str(e)}) + except IntegrityError: + # 处理唯一性约束冲突 + raise serializers.ValidationError({ + 'name': f'目标 "{normalized_name}" 已存在' + }) + + def update(self, instance, validated_data): + """更新目标时,如果 name 变化则重新规范化和检测类型""" + # 如果 name 发生变化,重新规范化和检测类型 + if 'name' in validated_data and validated_data['name'] != instance.name: + try: + # 1. 规范化 + normalized_name = normalize_target(validated_data['name']) + # 2. 验证并检测类型 + target_type = detect_target_type(normalized_name) + # 3. 写入 + validated_data['name'] = normalized_name + validated_data['type'] = target_type + except ValueError as e: + raise serializers.ValidationError({'name': str(e)}) + + try: + return super().update(instance, validated_data) + except IntegrityError: + # 处理唯一性约束冲突 + raise serializers.ValidationError({ + 'name': f'目标 "{validated_data.get("name", instance.name)}" 已存在' + }) + + +class TargetDetailSerializer(serializers.ModelSerializer): + """ + 目标详情序列化器 - 包含统计数据 + + 用于单个目标详情页面(只读),包含各类资产的统计数量 + + Note: + - 此序列化器只用于 retrieve action(只读操作) + - 不包含 create/update 方法,因为详情页不需要修改功能 + - 所有字段都是只读的,包括 name + """ + organizations = SimpleOrganizationSerializer(many=True, read_only=True) + summary = serializers.SerializerMethodField() + + class Meta: + model = Target + fields = ['id', 'name', 'type', 'created_at', 'last_scanned_at', 'organizations', 'summary'] + read_only_fields = ['id', 'name', 'type', 'created_at', 'last_scanned_at', 'summary'] + + def get_summary(self, obj): + """计算目标资产统计数据 + + 统计该目标下的资产数量: + - subdomains: 子域名数量 + - websites: 网站数量 + - endpoints: 端点数量 + - ips: IP地址数量 + - directories: 目录数量 + - vulnerabilities: 漏洞统计(暂时返回 0,待后续实现) + + 性能说明: + - 使用 .count() 查询获取统计数据 + - 每个统计字段执行一次数据库查询 + - 不使用 annotate 预聚合的原因:多个 Count(distinct=True) 在大数据量时性能较差 + - 对于详情页单条记录,直接 .count() 查询性能可接受 + - ips 统计使用 distinct() 去重,因为 HostPortMapping 中同一 IP 可能有多个端口 + """ + # 基础资产统计(直接使用关联关系 count) + subdomains_count = obj.subdomains.count() + websites_count = obj.websites.count() + endpoints_count = obj.endpoints.count() + ips_count = obj.host_port_mappings.values('ip').distinct().count() + directories_count = obj.directories.count() + + # 漏洞统计:按目标维度实时统计 Vulnerability 资产表 + vuln_qs = obj.vulnerabilities.all() + + total = vuln_qs.count() + + severity_stats = { + 'critical': 0, + 'high': 0, + 'medium': 0, + 'low': 0, + } + + for row in vuln_qs.values('severity').annotate(count=Count('id')): + sev = row['severity'] or '' + count = row['count'] or 0 + if sev in severity_stats: + severity_stats[sev] = count + + return { + 'subdomains': subdomains_count, + 'websites': websites_count, + 'endpoints': endpoints_count, + 'ips': ips_count, + 'directories': directories_count, + 'vulnerabilities': { + 'total': total, + **severity_stats, + } + } + + +class OrganizationSerializer(serializers.ModelSerializer): + # 使用 IntegerField 接收由 annotate 预计算的 target_count + # 避免 N+1 查询问题(在 ViewSet 的 get_queryset 中使用 annotate 预计算) + target_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Organization + fields = ['id', 'name', 'description', 'created_at', 'target_count'] + read_only_fields = ['id', 'created_at', 'target_count'] + + +class BatchCreateTargetSerializer(serializers.Serializer): + """ + 批量创建目标的序列化器 + + 安全限制: + - 最多支持 1000 个目标的批量创建 + - 防止恶意用户提交大量数据导致服务器过载 + """ + + # 批量创建的最大数量限制 + MAX_BATCH_SIZE = 1000 + + # 目标列表 + targets = serializers.ListField( + child=serializers.DictField(), + help_text='目标列表,每个目标包含 name 字段(type 会自动检测)' + ) + + # 可选:关联的组织ID + organization_id = serializers.IntegerField( + required=False, + allow_null=True, + help_text='可选:关联到指定组织的ID' + ) + + def validate_targets(self, value): + """验证目标列表""" + if not value: + raise serializers.ValidationError("目标列表不能为空") + + # 检查数量限制,防止服务器过载 + if len(value) > self.MAX_BATCH_SIZE: + raise serializers.ValidationError( + f"批量创建最多支持 {self.MAX_BATCH_SIZE} 个目标,当前提交了 {len(value)} 个" + ) + + # 验证每个目标的必填字段 + for idx, target in enumerate(value): + if 'name' not in target: + raise serializers.ValidationError(f"第 {idx + 1} 个目标缺少 name 字段") + if not target['name']: + raise serializers.ValidationError(f"第 {idx + 1} 个目标的 name 不能为空") + + return value + diff --git a/backend/apps/targets/services/__init__.py b/backend/apps/targets/services/__init__.py new file mode 100644 index 00000000..da716790 --- /dev/null +++ b/backend/apps/targets/services/__init__.py @@ -0,0 +1,5 @@ +"""Target Services""" + +from .target_service import TargetService + +__all__ = ['TargetService'] diff --git a/backend/apps/targets/services/organization_service.py b/backend/apps/targets/services/organization_service.py new file mode 100644 index 00000000..de62e33c --- /dev/null +++ b/backend/apps/targets/services/organization_service.py @@ -0,0 +1,172 @@ +""" +Organization 业务逻辑服务层(Service) + +负责组织相关的业务逻辑处理 +""" + +import logging +from typing import List, Tuple, Dict + +from ..models import Organization +from ..repositories.django_organization_repository import DjangoOrganizationRepository + +logger = logging.getLogger(__name__) + + +class OrganizationService: + """Organization 业务逻辑服务""" + + def __init__(self): + """初始化服务,注入 Repository 依赖""" + self.repo = DjangoOrganizationRepository() + + # ==================== 查询操作 ==================== + + def get_organization(self, organization_id: int) -> Organization | None: + """ + 获取组织 + + Args: + organization_id: 组织 ID + + Returns: + Organization 对象或 None + """ + return self.repo.get_by_id(organization_id) + + + def get_all(self): + """ + 获取所有组织 + + Returns: + QuerySet: 组织查询集 + """ + return self.repo.get_all() + + def get_all_with_stats(self): + """ + 获取所有组织(带统计信息) + + Returns: + QuerySet: 带统计信息的组织查询集 + """ + return self.repo.get_all_with_stats() + + # ==================== 创建操作 ==================== + + def bulk_add_targets(self, organization_id: int, targets: List) -> None: + """ + 批量添加目标到组织 + + Args: + organization_id: 组织 ID + targets: Target 对象列表 + """ + logger.debug("批量关联目标到组织 - Org ID: %s, Targets: %s", organization_id, len(targets)) + self.repo.bulk_add_targets(organization_id, targets) + + # ==================== 删除操作 ==================== + + def delete_organizations_two_phase(self, organization_ids: List[int]) -> Dict: + """ + 两阶段删除组织(业务方法) + + Args: + organization_ids: 组织 ID 列表 + + Returns: + { + 'soft_deleted_count': int, + 'hard_delete_scheduled': bool + } + + Raises: + ValueError: 未找到要删除的组织 + + Note: + - 阶段 1:软删除(立即),用户立即看不到数据 + - 阶段 2:硬删除(后台),真正删除数据和中间表 + """ + + # 1. 软删除(如果 ID 不存在,update 返回 0) + soft_count = self.soft_delete_organizations(organization_ids) + + # 2. 检查是否有记录被删除 + if soft_count == 0: + raise ValueError("未找到要删除的组织") + + logger.info(f"✓ 软删除完成: {soft_count} 个组织") + + # 3. 使用 task_distributor 分发硬删除任务到 Worker + try: + from apps.engine.services.task_distributor import get_task_distributor + + distributor = get_task_distributor() + success, message, container_id = distributor.execute_delete_task( + task_type='organizations', + ids=organization_ids + ) + + if success: + logger.info(f"✓ 硬删除任务已分发 - Container: {container_id}") + else: + logger.warning(f"硬删除任务分发失败: {message}") + + except Exception as e: + logger.error(f"❌ 分发删除任务失败: {e}", exc_info=True) + logger.warning("硬删除可能未成功提交,请检查 Worker 状态") + + return { + 'soft_deleted_count': soft_count, + 'hard_delete_scheduled': True + } + + def soft_delete_organizations(self, organization_ids: List[int]) -> int: + """ + 软删除组织 + + Args: + organization_ids: 组织 ID 列表 + + Returns: + 软删除的记录数 + + Note: + - 返回值是实际更新的记录数,不是传入的 ID 数量 + - 如果某些 ID 不存在,返回值会小于传入的 ID 数量 + """ + logger.info("软删除 %d 个组织", len(organization_ids)) + + try: + deleted_count = self.repo.soft_delete_by_ids(organization_ids) + logger.info("✓ 软删除成功 - 数量: %d", deleted_count) + return deleted_count + except Exception as e: + logger.error("软删除失败: %s", e) + raise + + def hard_delete_organizations(self, organization_ids: List[int]) -> Tuple[int, Dict[str, int]]: + """ + 硬删除组织(真正删除数据,使用 Django CASCADE) + + Args: + organization_ids: 组织 ID 列表 + + Returns: + (删除的记录数, 删除详情字典) + + Note: + - 从数据库中永久删除 + - Django CASCADE 自动删除 organization_targets 中间表记录 + - 不会删除关联的 Target(多对多) + """ + logger.info("硬删除 %d 个组织", len(organization_ids)) + + try: + deleted_count, deleted_details = self.repo.hard_delete_by_ids(organization_ids) + logger.info("✓ 硬删除成功 - 数量: %d, 删除记录数: %d", len(organization_ids), deleted_count) + return deleted_count, deleted_details + except Exception as e: + logger.error("❌ 硬删除失败 - IDs: %s, 错误: %s", organization_ids, e) + raise diff --git a/backend/apps/targets/services/target_service.py b/backend/apps/targets/services/target_service.py new file mode 100644 index 00000000..d576bbdd --- /dev/null +++ b/backend/apps/targets/services/target_service.py @@ -0,0 +1,338 @@ +""" +Target 业务逻辑服务层(Service) + +负责目标相关的业务逻辑处理 +""" + +import logging +from typing import List, Tuple, Dict, Any, Optional + +from django.db import transaction + +from ..models import Target +from ..repositories.django_target_repository import DjangoTargetRepository + +logger = logging.getLogger(__name__) + + +class TargetService: + """Target 业务逻辑服务""" + + def __init__(self): + """初始化服务,注入 Repository 依赖""" + self.repo = DjangoTargetRepository() + + # ==================== 查询方法 ==================== + + def count_existing_ids(self, target_ids: List[int]) -> int: + """ + 统计给定 ID 列表中实际存在的目标数量 + + Args: + target_ids: 目标 ID 列表 + + Returns: + 存在的目标数量 + """ + return self.repo.count_by_ids(target_ids) + + # ==================== 查询操作 ==================== + + def get_target(self, target_id: int) -> Target | None: + """ + 获取目标 + + Args: + target_id: 目标 ID + + Returns: + Target 对象或 None + """ + return self.repo.get_by_id(target_id) + + def get_by_id(self, target_id: int) -> Target | None: + """ + 根据 ID 获取目标(get_target 别名) + + Args: + target_id: 目标 ID + + Returns: + Target 对象或 None + """ + return self.repo.get_by_id(target_id) + + + def get_all(self): + """ + 获取所有目标 + + Returns: + QuerySet: 目标查询集 + """ + return self.repo.get_all() + + def get_targets_by_names(self, names: List[str]) -> List[Target]: + """ + 根据名称批量获取目标 + + Args: + names: 目标名称列表 + + Returns: + Target 对象列表 + """ + return self.repo.get_by_names(names) + + def update_last_scanned_at(self, target_id: int) -> bool: + """ + 更新目标的最后扫描时间 + + Args: + target_id: 目标 ID + + Returns: + 是否更新成功 + """ + from django.utils import timezone + return self.repo.update_last_scanned_at(target_id, timezone.now()) + + # ==================== 创建操作 ==================== + + def create_or_get_target( + self, + name: str, + target_type: str + ) -> Tuple[Target, bool]: + """ + 创建或获取目标 + + Args: + name: 目标名称 + target_type: 目标类型 + + Returns: + (Target对象, 是否新创建) + """ + logger.debug("创建或获取目标 - Name: %s, Type: %s", name, target_type) + target, created = self.repo.get_or_create(name, target_type) + + if created: + logger.info("创建新目标 - ID: %s, Name: %s", target.id, name) + else: + logger.debug("目标已存在 - ID: %s, Name: %s", target.id, name) + + return target, created + + def batch_create_targets( + self, + targets_data: List[Dict[str, Any]], + organization_id: Optional[int] = None + ) -> Dict[str, Any]: + """ + 批量创建目标(高性能优化版) + + Args: + targets_data: 目标数据列表,每个元素包含 name 字段 + organization_id: 可选,关联到指定组织的 ID + + Returns: + { + 'created_count': int, # 成功处理的总数(包括复用) + 'failed_count': int, + 'failed_targets': List[Dict], + 'message': str + } + + Performance: + 使用 bulk_create 替代逐个创建,大幅减少数据库交互次数。 + 1000个目标:~100ms (优化前 ~2s) + """ + from apps.asset.services.asset.subdomain_service import SubdomainService + from apps.asset.dtos import SubdomainDTO + from apps.targets.models import Target + from apps.common.normalizer import normalize_target + from apps.common.validators import detect_target_type + from .organization_service import OrganizationService + + # 1. 预处理数据:规范化 + 类型检测 + # 使用字典去重,key为规范化后的名称 + valid_targets_map = {} # {name: type} + failed_targets = [] + + for data in targets_data: + name = data.get('name') + if not name: + continue + + try: + norm_name = normalize_target(name) + t_type = detect_target_type(norm_name) + valid_targets_map[norm_name] = t_type + except ValueError as e: + failed_targets.append({'name': name, 'reason': str(e)}) + + if not valid_targets_map: + return { + 'created_count': 0, + 'failed_count': len(failed_targets), + 'failed_targets': failed_targets, + 'message': "没有有效的目标" + } + + # 验证组织是否存在 + if organization_id: + org_service = OrganizationService() + organization = org_service.get_organization(organization_id) + if not organization: + raise ValueError(f'组织 ID {organization_id} 不存在') + + with transaction.atomic(): + # 2. 批量创建 Target (使用 Repository) + target_objs = [ + Target(name=name, type=t_type) + for name, t_type in valid_targets_map.items() + ] + self.repo.bulk_create_ignore_conflicts(target_objs) + + # 3. 重新查询获取所有涉及的 Target 对象(含 ID)(使用 Repository) + all_targets = self.repo.get_by_names(list(valid_targets_map.keys())) + + # 4. 处理关联组织 (使用 OrganizationService) + if organization_id: + org_service = OrganizationService() + org_service.bulk_add_targets(organization_id, all_targets) + + # 5. 处理 Subdomain 创建 (使用 SubdomainService) + domain_targets = [t for t in all_targets if t.type == Target.TargetType.DOMAIN] + if domain_targets: + subdomain_dtos = [ + SubdomainDTO(name=t.name, target_id=t.id) + for t in domain_targets + ] + subdomain_service = SubdomainService() + subdomain_service.bulk_create_ignore_conflicts(subdomain_dtos) + + success_count = len(all_targets) + + logger.info( + "批量创建目标完成 (Bulk) - 成功处理: %d, 失败: %d", + success_count, len(failed_targets) + ) + + return { + 'created_count': success_count, + 'failed_count': len(failed_targets), + 'failed_targets': failed_targets, + 'message': f"成功处理 {success_count} 个目标" + } + + # ==================== 删除操作 ==================== + + def delete_targets_two_phase(self, target_ids: List[int]) -> Dict: + """ + 两阶段删除目标(业务方法) + + Args: + target_ids: 目标 ID 列表 + + Returns: + { + 'soft_deleted_count': int, + 'hard_delete_scheduled': bool + } + + Raises: + ValueError: 未找到要删除的目标 + + Note: + - 阶段 1:软删除(立即),用户立即看不到数据 + - 阶段 2:硬删除(后台),真正删除数据和关联 + """ + + # 1. 软删除(如果 ID 不存在,update 返回 0) + soft_count = self.soft_delete_targets(target_ids) + + # 2. 检查是否有记录被删除 + if soft_count == 0: + raise ValueError("未找到要删除的目标") + + logger.info(f"✓ 软删除完成: {soft_count} 个目标") + + # 3. 使用 task_distributor 分发硬删除任务到 Worker + try: + from apps.engine.services.task_distributor import get_task_distributor + + distributor = get_task_distributor() + success, message, container_id = distributor.execute_delete_task( + task_type='targets', + ids=target_ids + ) + + if success: + logger.info(f"✓ 硬删除任务已分发 - Container: {container_id}") + else: + logger.warning(f"硬删除任务分发失败: {message}") + + except Exception as e: + logger.error(f"❌ 分发删除任务失败: {e}", exc_info=True) + logger.warning("硬删除可能未成功提交,请检查 Worker 状态") + + return { + 'soft_deleted_count': soft_count, + 'hard_delete_scheduled': True + } + + def soft_delete_targets(self, target_ids: List[int]) -> int: + """ + 软删除目标 + + Args: + target_ids: 目标 ID 列表 + + Returns: + 软删除的记录数 + + Note: + - 返回值是实际更新的记录数,不是传入的 ID 数量 + - 如果某些 ID 不存在,返回值会小于传入的 ID 数量 + """ + logger.info("软删除 %d 个目标", len(target_ids)) + + try: + deleted_count = self.repo.soft_delete_by_ids(target_ids) + logger.info("✓ 软删除成功 - 数量: %d", deleted_count) + return deleted_count + except Exception as e: + logger.error("软删除失败: %s", e) + raise + + def hard_delete_targets(self, target_ids: List[int]) -> Tuple[int, Dict[str, int]]: + """ + 硬删除目标(真正删除数据)- 使用数据库级 CASCADE + + Args: + target_ids: 目标 ID 列表 + + Returns: + (删除的记录数, 删除详情字典) + + Strategy: + 使用数据库级 CASCADE 删除,性能最优 + + Note: + - 硬删除:从数据库中永久删除 + - 数据库自动级联删除所有关联数据 + - 不触发 Django 信号(pre_delete/post_delete) + """ + logger.debug("准备硬删除目标(CASCADE)- Count: %s, IDs: %s", len(target_ids), target_ids) + + deleted_count, details = self.repo.hard_delete_by_ids(target_ids) + + logger.info( + "硬删除目标成功(CASCADE)- Count: %s, 删除记录数: %s", + len(target_ids), + deleted_count + ) + + return deleted_count, details diff --git a/backend/apps/targets/urls.py b/backend/apps/targets/urls.py new file mode 100644 index 00000000..b586a503 --- /dev/null +++ b/backend/apps/targets/urls.py @@ -0,0 +1,43 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import OrganizationViewSet, TargetViewSet +from apps.asset.views import ( + SubdomainViewSet, WebSiteViewSet, DirectoryViewSet, + EndpointViewSet, HostPortMappingViewSet, VulnerabilityViewSet +) + +# 创建路由器 +router = DefaultRouter() + +# 注册 ViewSet +router.register(r'organizations', OrganizationViewSet, basename='organization') +router.register(r'targets', TargetViewSet, basename='target') + +# Target 下的嵌套资产路由 +target_subdomains_list = SubdomainViewSet.as_view({'get': 'list'}) +target_subdomains_export = SubdomainViewSet.as_view({'get': 'export'}) +target_websites_list = WebSiteViewSet.as_view({'get': 'list'}) +target_websites_export = WebSiteViewSet.as_view({'get': 'export'}) +target_directories_list = DirectoryViewSet.as_view({'get': 'list'}) +target_directories_export = DirectoryViewSet.as_view({'get': 'export'}) +target_endpoints_list = EndpointViewSet.as_view({'get': 'list'}) +target_endpoints_export = EndpointViewSet.as_view({'get': 'export'}) +target_ip_addresses_list = HostPortMappingViewSet.as_view({'get': 'list'}) +target_ip_addresses_export = HostPortMappingViewSet.as_view({'get': 'export'}) +target_vulnerabilities_list = VulnerabilityViewSet.as_view({'get': 'list'}) + +urlpatterns = [ + path('', include(router.urls)), + # 嵌套路由:/api/targets/{target_pk}/xxx/ + path('targets/<int:target_pk>/subdomains/', target_subdomains_list, name='target-subdomains-list'), + path('targets/<int:target_pk>/subdomains/export/', target_subdomains_export, name='target-subdomains-export'), + path('targets/<int:target_pk>/websites/', target_websites_list, name='target-websites-list'), + path('targets/<int:target_pk>/websites/export/', target_websites_export, name='target-websites-export'), + path('targets/<int:target_pk>/directories/', target_directories_list, name='target-directories-list'), + path('targets/<int:target_pk>/directories/export/', target_directories_export, name='target-directories-export'), + path('targets/<int:target_pk>/endpoints/', target_endpoints_list, name='target-endpoints-list'), + path('targets/<int:target_pk>/endpoints/export/', target_endpoints_export, name='target-endpoints-export'), + path('targets/<int:target_pk>/ip-addresses/', target_ip_addresses_list, name='target-ip-addresses-list'), + path('targets/<int:target_pk>/ip-addresses/export/', target_ip_addresses_export, name='target-ip-addresses-export'), + path('targets/<int:target_pk>/vulnerabilities/', target_vulnerabilities_list, name='target-vulnerabilities-list'), +] diff --git a/backend/apps/targets/views.py b/backend/apps/targets/views.py new file mode 100644 index 00000000..46f71639 --- /dev/null +++ b/backend/apps/targets/views.py @@ -0,0 +1,428 @@ +import logging +from rest_framework import viewsets, status, filters +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.exceptions import ValidationError, NotFound, APIException +from django.db import transaction +from django.db.models import Count +from .models import Organization, Target +from .serializers import OrganizationSerializer, TargetSerializer, TargetDetailSerializer, BatchCreateTargetSerializer +from .services.target_service import TargetService +from .services.organization_service import OrganizationService +from apps.common.pagination import BasePagination + +logger = logging.getLogger(__name__) + + +class OrganizationViewSet(viewsets.ModelViewSet): + """组织管理 - 增删改查""" + serializer_class = OrganizationSerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['name'] + ordering = ['-created_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.org_service = OrganizationService() + + def get_queryset(self): + """优化查询,预计算目标数量,避免 N+1 查询""" + return self.org_service.get_all_with_stats() + + @action(detail=True, methods=['get']) + def targets(self, request, pk=None): + """ + 获取组织的目标列表 + GET /api/organizations/{id}/targets/?page=1&pageSize=10 + """ + organization = self.get_object() + + # 获取组织的目标(优化:使用 prefetch_related 预加载 organizations,避免 N+1 查询) + queryset = organization.targets.prefetch_related('organizations').all() + + # 使用分页器 + paginator = self.paginator + page = paginator.paginate_queryset(queryset, request, view=self) + + if page is not None: + serializer = TargetSerializer(page, many=True) + return paginator.get_paginated_response(serializer.data) + + # 如果没有分页参数,抛出异常 + raise ValidationError('必须提供分页参数 page 和 pageSize') + + @action(detail=True, methods=['post']) + def unlink_targets(self, request, pk=None): + """ + 解除组织与目标的关联 + POST /api/organizations/{id}/unlink_targets/ + + 请求格式: + { + "target_ids": [1, 2, 3] + } + + 返回: + { + "unlinked_count": 3, + "message": "成功解除 3 个目标的关联" + } + + 注意:此操作只解除关联关系,不会删除目标本身 + """ + organization = self.get_object() + target_ids = request.data.get('target_ids', []) + + if not target_ids: + raise ValidationError('目标ID列表不能为空') + + if not isinstance(target_ids, list): + raise ValidationError('target_ids 必须是数组') + + # 使用事务保护 + with transaction.atomic(): + # 验证目标是否存在且属于该组织(只查询 ID,避免加载完整对象) + existing_target_ids = list( + organization.targets.filter(id__in=target_ids).values_list('id', flat=True) + ) + existing_count = len(existing_target_ids) + + if existing_count == 0: + raise ValidationError('未找到要解除关联的目标') + + # 批量解除关联(直接使用 ID,避免查询对象) + organization.targets.remove(*existing_target_ids) + + return Response({ + 'unlinked_count': existing_count, + 'message': f'成功解除 {existing_count} 个目标的关联' + }) + + def destroy(self, request, *args, **kwargs): + """ + 删除单个组织(复用批量删除逻辑) + + DELETE /api/organizations/{id}/ + + 功能: + - 复用 bulk_delete 的两阶段删除逻辑 + - 立即返回 200 OK,软删除完成,硬删除在后台执行 + + 返回: + - 200 OK: 软删除完成,硬删除已在后台启动 + - 404 Not Found: 组织不存在 + + 注意: + - 两阶段删除:软删除(立即)+ 硬删除(后台任务) + - 硬删除会清理 organization_targets 中间表 + - 不会删除关联的 Target(多对多关系) + """ + try: + organization = self.get_object() + + # 直接调用 Service 层的业务方法(软删除 + 分发硬删除任务) + result = self.org_service.delete_organizations_two_phase([organization.id]) + + return Response({ + 'message': f'已删除组织: {organization.name}', + 'organizationId': organization.id, + 'organizationName': organization.name, + 'deletedCount': result['soft_deleted_count'], + 'deletedOrganizations': result['organization_names'], + 'detail': { + 'phase1': '软删除完成,用户已看不到数据', + 'phase2': '硬删除任务已分发,将在后台执行' + } + }, status=200) + + except Organization.DoesNotExist: + raise NotFound('组织不存在') + except ValueError as e: + raise NotFound(str(e)) + except Exception as e: + logger.exception("删除组织时发生错误") + raise APIException('服务器错误,请稍后重试') + + @action(detail=False, methods=['post', 'delete'], url_path='bulk-delete') + def bulk_delete(self, request): + """ + 批量删除组织(两阶段删除) + + POST/DELETE /api/organizations/bulk-delete/ + + 请求格式: + { + "ids": [1, 2, 3] + } + + 功能: + - 阶段 1:立即软删除(用户立即看不到数据) + - 阶段 2:后台硬删除(真正删除数据和中间表) + - 立即返回 200 OK,硬删除任务已分发 + + 返回: + - 200 OK: 软删除完成,硬删除任务已分发 + - 400 Bad Request: 参数错误 + - 404 Not Found: 未找到要删除的组织 + + 注意: + - 软删除:用户立即看不到 + - 硬删除:清理数据库和 organization_targets 中间表 + - 不会删除关联的 Target(多对多关系) + - 硬删除任务通过 task_distributor 分发到动态容器执行 + """ + ids = request.data.get('ids', []) + + # 参数验证 + if not ids: + raise ValidationError('缺少必填参数: ids') + if not isinstance(ids, list): + raise ValidationError('ids 必须是数组') + if not all(isinstance(i, int) for i in ids): + raise ValidationError('ids 数组中的所有元素必须是整数') + + try: + # 调用 Service 层的业务方法(软删除 + 分发硬删除任务) + result = self.org_service.delete_organizations_two_phase(ids) + + return Response({ + 'message': f"已删除 {result['soft_deleted_count']} 个组织", + 'deletedCount': result['soft_deleted_count'], + 'deletedOrganizations': result['organization_names'], + 'detail': { + 'phase1': '软删除完成,用户已看不到数据', + 'phase2': '硬删除任务已分发,将在后台执行' + } + }, status=200) + + except ValueError as e: + raise NotFound(str(e)) + except Exception as e: + logger.exception("删除组织时发生错误") + raise APIException('服务器错误,请稍后重试') + + +class TargetViewSet(viewsets.ModelViewSet): + """ + 目标管理 - 增删改查 + + 性能优化说明: + 1. 使用 prefetch_related('organizations') 预加载关联的组织 + 2. 配合 TargetSerializer 中的嵌套序列化器 SimpleOrganizationSerializer + 3. 避免 N+1 查询问题: + - 优化前:100 个目标 = 1 + 100 = 101 次查询 + - 优化后:100 个目标 = 1 + 1 = 2 次查询 + + ⚠️ 重要:如果在其他地方使用 TargetSerializer,必须确保查询时使用了 + prefetch_related('organizations'),否则仍会产生 N+1 查询 + """ + serializer_class = TargetSerializer + pagination_class = BasePagination + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['name'] + ordering = ['-created_at'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.target_service = TargetService() + + def get_queryset(self): + """获取目标查询集 + + 注意:不在这里使用 .annotate() 预聚合统计数据 + + 原因: + - 列表页(list action):需要分页 + 高性能统计 + - 详情页(retrieve action):只需要一条记录的统计 + + 统计策略: + - 列表页:在 serializer 中用 .count() 单次查询(高性能) + - 详情页:同样用 .count() 单次查询 + + ⚠️ 为什么不用 .annotate(): + - 原因:多个 Count(distinct=True) 在大数据量时很慢(特别是目录数据) + """ + # 列表和详情都使用相同的查询集(详情页的统计交给 serializer 用 .count()) + return self.target_service.get_all() + + def get_serializer_class(self): + """根据不同的 action 返回不同的序列化器 + + - retrieve action: 使用 TargetDetailSerializer(包含 summary 统计数据) + - 其他 action: 使用标准的 TargetSerializer + """ + if self.action == 'retrieve': + return TargetDetailSerializer + return TargetSerializer + + def destroy(self, request, *args, **kwargs): + """ + 删除单个目标(复用批量删除逻辑) + + DELETE /api/targets/{id}/ + + 功能: + - 复用 bulk_delete 的两阶段删除逻辑 + - 立即返回 200 OK,软删除完成,硬删除在后台执行 + + 返回: + - 200 OK: 软删除完成,硬删除已在后台启动 + - 404 Not Found: 目标不存在 + + 注意: + - 两阶段删除:软删除(立即)+ 硬删除(后台任务) + - 硬删除会使用分批删除策略处理大数据量 + """ + try: + target = self.get_object() + + # 直接调用 Service 层的业务方法(软删除 + 分发硬删除任务) + result = self.target_service.delete_targets_two_phase([target.id]) + + return Response({ + 'message': f'已删除目标: {target.name}', + 'targetId': target.id, + 'targetName': target.name, + 'deletedCount': result['soft_deleted_count'], + 'detail': { + 'phase1': '软删除完成,用户已看不到数据', + 'phase2': '硬删除任务已分发,将在后台执行' + } + }, status=200) + + except Target.DoesNotExist: + raise NotFound('目标不存在') + except ValueError as e: + raise NotFound(str(e)) + except Exception as e: + logger.exception("删除目标时发生错误") + raise APIException('服务器错误,请稍后重试') + + @action(detail=False, methods=['post', 'delete'], url_path='bulk-delete') + def bulk_delete(self, request): + """ + 批量删除目标(两阶段删除策略) + + POST/DELETE /api/targets/bulk-delete/ + + 请求格式: + { + "ids": [1, 2, 3] + } + + 两阶段删除策略: + 1. 阶段 1(立即):软删除目标,用户立即看不到数据 + 2. 阶段 2(后台):硬删除任务,真正清理数据 + + 功能: + - 立即软删除:用户立即看不到数据(响应快) + - 后台硬删除:使用分批删除策略处理大数据量 + + 返回: + - 200 OK: 软删除成功,硬删除任务已分发 + - 400 Bad Request: 参数错误 + - 404 Not Found: 未找到目标 + + 注意: + - 软删除:数据可恢复(deleted_at 不为 NULL) + - 硬删除:数据不可恢复(真正从数据库删除) + - 硬删除任务通过 task_distributor 分发到动态容器执行 + """ + ids = request.data.get('ids', []) + + # 参数验证 + if not ids: + raise ValidationError('缺少必填参数: ids') + if not isinstance(ids, list): + raise ValidationError('ids 必须是数组') + if not all(isinstance(i, int) for i in ids): + raise ValidationError('ids 数组中的所有元素必须是整数') + + try: + # 调用 Service 层的业务方法(软删除 + 分发硬删除任务) + result = self.target_service.delete_targets_two_phase(ids) + + return Response({ + 'message': f"已删除 {result['soft_deleted_count']} 个目标", + 'deletedCount': result['soft_deleted_count'], + 'deletedTargets': result['target_names'], + 'detail': { + 'phase1': '软删除完成,用户已看不到数据', + 'phase2': '硬删除任务已分发,将在后台执行' + } + }, status=200) + + except ValueError as e: + raise NotFound(str(e)) + except Exception as e: + logger.exception("删除目标时发生错误") + raise APIException('服务器错误,请稍后重试') + + @action(detail=False, methods=['post']) + def batch_create(self, request): + """ + 批量创建目标 + POST /api/targets/batch_create/ + + 请求格式: + { + "targets": [ + {"name": "example.com"}, + {"name": "192.168.1.1"}, + {"name": "192.168.1.0/24"} + ], + "organization_id": 1 // 可选,关联到指定组织 + } + + 限制: + - 最多支持 1000 个目标的批量创建 + - type 会根据 name 自动检测(域名/IP/CIDR) + + 返回: + { + "created_count": 2, + "failed_count": 0, + "failed_targets": [ + {"name": "xxx", "reason": "无法识别的目标格式"} + ], + "message": "成功创建 2 个目标" + } + """ + # 1. 参数验证 + serializer = BatchCreateTargetSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + targets_data = serializer.validated_data['targets'] + organization_id = serializer.validated_data.get('organization_id') + + # 2. 调用 Service 层处理业务逻辑 + try: + result = self.target_service.batch_create_targets( + targets_data=targets_data, + organization_id=organization_id + ) + except ValueError as e: + raise ValidationError(str(e)) + + # 3. 返回响应 + return Response(result, status=status.HTTP_201_CREATED) + + # subdomains action 已迁移到 SubdomainViewSet 嵌套路由 + # GET /api/targets/{id}/subdomains/ -> SubdomainViewSet + + # vulnerabilities action 已迁移到 VulnerabilityViewSet 嵌套路由 + # GET /api/targets/{id}/vulnerabilities/ -> VulnerabilityViewSet + + # 所有资产相关的 action 和 export 已迁移到 asset/views.py 中的各 ViewSet + # GET /api/targets/{id}/subdomains/ -> SubdomainViewSet + # GET /api/targets/{id}/subdomains/export/ -> SubdomainViewSet.export + # GET /api/targets/{id}/websites/ -> WebSiteViewSet + # GET /api/targets/{id}/websites/export/ -> WebSiteViewSet.export + # GET /api/targets/{id}/endpoints/ -> EndpointViewSet + # GET /api/targets/{id}/endpoints/export/ -> EndpointViewSet.export + # GET /api/targets/{id}/directories/ -> DirectoryViewSet + # GET /api/targets/{id}/directories/export/ -> DirectoryViewSet.export + # GET /api/targets/{id}/ip-addresses/ -> HostPortMappingViewSet + # GET /api/targets/{id}/ip-addresses/export/ -> HostPortMappingViewSet.export + # GET /api/targets/{id}/vulnerabilities/ -> VulnerabilityViewSet diff --git a/backend/config/__init__.py b/backend/config/__init__.py new file mode 100644 index 00000000..9b6a31f5 --- /dev/null +++ b/backend/config/__init__.py @@ -0,0 +1,11 @@ +""" +配置包初始化 + +确保 Prefect 配置在 Django 启动时被加载 +""" + +# 延迟导入,避免在 ASGI 启动时出现循环依赖 +# configure_prefect() 会在 Django 应用就绪时自动调用 + +__all__ = ('configure_prefect',) + diff --git a/backend/config/asgi.py b/backend/config/asgi.py new file mode 100644 index 00000000..5cd1e55b --- /dev/null +++ b/backend/config/asgi.py @@ -0,0 +1,30 @@ +""" +ASGI config for config project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +# 初始化 Django ASGI 应用(必须在导入路由之前) +django_asgi_app = get_asgi_application() + +# 导入 WebSocket 路由 +from apps.scan.notifications.routing import websocket_urlpatterns as notification_ws +from apps.engine.routing import websocket_urlpatterns as worker_ws + +application = ProtocolTypeRouter({ + 'http': django_asgi_app, + 'websocket': AuthMiddlewareStack( + URLRouter(notification_ws + worker_ws) + ), +}) diff --git a/backend/config/logging_config.py b/backend/config/logging_config.py new file mode 100644 index 00000000..0af0fb43 --- /dev/null +++ b/backend/config/logging_config.py @@ -0,0 +1,328 @@ +""" +日志配置模块(改进版 v2) + +根据环境(开发/生产)和环境变量配置 Django 日志系统 + +改进内容: +1. ✅ 结构化日志 - JSON 格式便于日志分析和监控 +2. ✅ 性能指标日志 - 专门记录性能相关信息 +3. ⚠️ 异步日志处理 - 需要额外配置(见下方说明) + +环境变量: +- LOG_LEVEL: 全局日志级别 (DEBUG/INFO/WARNING/ERROR/CRITICAL) +- LOG_DIR: 日志文件目录(留空则不输出文件) + +开发环境特性: +- 默认 DEBUG 级别 +- 控制台彩色输出 +- 可选文件输出 + +生产环境特性: +- 默认 INFO 级别 +- 控制台 + 文件输出(配置 LOG_DIR) +- 文件自动轮转(10MB,保留5个备份) +- JSON 结构化日志 +- 性能指标日志 + +依赖安装: +- pip install python-json-logger # JSON 格式化器 + +异步日志说明: +- 当前使用标准 RotatingFileHandler(同步写入) +- 如需异步处理,可使用 logging_config_new.py 中的 QueueHandler 方案 +- 异步方案需要额外的 QueueListener 配置和生命周期管理 + +设计说明: +- 直接从环境变量读取配置,避免与 settings.py 循环依赖 +- settings.py 在加载时会调用此模块,此时 settings 对象尚未完全初始化 +- 这是 Django 配置模块的常见模式 +""" + +import os +from pathlib import Path + + +def get_logging_config(debug: bool = False): + """ + 获取日志配置字典 + + Args: + debug: 是否为 DEBUG 模式 + + Returns: + dict: Django LOGGING 配置字典 + """ + # 获取日志配置 + log_level = os.getenv('LOG_LEVEL', 'DEBUG' if debug else 'INFO') + log_dir = os.getenv('LOG_DIR', '') + + # 构建 handlers 配置 + log_handlers = ['console'] + logging_handlers = { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'colored' if debug else 'standard', + 'stream': 'ext://sys.stdout', + } + } + + # 如果配置了日志目录,添加文件 handler + if log_dir: + log_path = Path(log_dir) + log_path.mkdir(parents=True, exist_ok=True) + + # 标准文件日志 + log_handlers.append('file') + logging_handlers['file'] = { + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'standard', + 'filename': str(log_path / 'xingrin.log'), + 'maxBytes': 100 * 1024 * 1024, # 100MB + 'backupCount': 5, + 'encoding': 'utf-8', + } + + # 错误日志单独记录 + log_handlers.append('error_file') + logging_handlers['error_file'] = { + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'standard', + 'filename': str(log_path / 'xingrin_error.log'), + 'maxBytes': 100 * 1024 * 1024, # 100MB + 'backupCount': 5, + 'encoding': 'utf-8', + 'level': 'ERROR', # 只记录 ERROR 及以上级别 + } + + # JSON 结构化日志(便于日志分析和监控) + log_handlers.append('json_file') + logging_handlers['json_file'] = { + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'json', + 'filename': str(log_path / 'xingrin_json.log'), + 'maxBytes': 100 * 1024 * 1024, # 100MB + 'backupCount': 5, + 'encoding': 'utf-8', + } + + # 性能指标日志(专门记录性能相关信息) + logging_handlers['performance_file'] = { + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'json', + 'filename': str(log_path / 'performance.log'), + 'maxBytes': 100 * 1024 * 1024, # 100MB + 'backupCount': 5, + 'encoding': 'utf-8', + } + + # 构建完整的 LOGGING 配置 + logging_config = { + 'version': 1, + 'disable_existing_loggers': False, + + # 格式化器 + 'formatters': { + 'standard': { + 'format': '[%(asctime)s] [%(levelname)s] [%(name)s:%(lineno)d] %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S', + }, + 'colored': { + 'format': '%(log_color)s[%(asctime)s] [%(levelname)s] [%(name)s:%(lineno)d]%(reset)s %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S', + '()': 'colorlog.ColoredFormatter', + 'log_colors': { + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red,bg_white', + }, + }, + # JSON 格式化器(结构化日志) + 'json': { + '()': 'pythonjsonlogger.jsonlogger.JsonFormatter', + 'format': '%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d', + 'datefmt': '%Y-%m-%d %H:%M:%S', + }, + }, + + # 处理器 + 'handlers': logging_handlers, + + # 日志记录器 + 'loggers': { + # Django 核心日志 + 'django': { + 'handlers': log_handlers, + 'level': 'INFO', # Django 框架日志,通常不需要 DEBUG + 'propagate': False, + }, + 'django.request': { + 'handlers': log_handlers, + 'level': 'WARNING', # 请求错误日志 + 'propagate': False, + }, + 'django.server': { + 'handlers': log_handlers, + 'level': 'WARNING', # Django 开发服务器日志 + 'propagate': False, + }, + 'django.db.backends': { + 'handlers': log_handlers, + 'level': 'WARNING' if not debug else 'DEBUG', # SQL 查询日志(开发环境可启用) + 'propagate': False, + }, + + + + # 应用日志 - 扫描模块 + 'apps.scan': { + 'handlers': log_handlers, + 'level': log_level, + 'propagate': False, + }, + + # 应用日志 - 其他模块(统一级别) + 'apps.asset': { + 'handlers': log_handlers, + 'level': log_level, + 'propagate': False, + }, + 'apps.targets': { + 'handlers': log_handlers, + 'level': log_level, + 'propagate': False, + }, + 'apps.engine': { + 'handlers': log_handlers, + 'level': log_level, + 'propagate': False, + }, + 'apps.common': { + 'handlers': log_handlers, + 'level': log_level, + 'propagate': False, + }, + + # 第三方库日志控制 + 'websockets': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭 WebSocket 的 DEBUG/INFO 日志 + 'propagate': False, + }, + 'websockets.client': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭 WebSocket 客户端的调试日志 + 'propagate': False, + }, + 'httpx': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭 HTTP 客户端的详细日志 + 'propagate': False, + }, + 'httpcore': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭 HTTP 核心库的调试日志 + 'propagate': False, + }, + 'httpcore.connection': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭 HTTP 连接的调试日志 + 'propagate': False, + }, + 'httpcore.http11': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭 HTTP/1.1 协议的调试日志 + 'propagate': False, + }, + 'prefect': { + 'handlers': log_handlers, + 'level': 'INFO', # Prefect 框架日志保持 INFO 级别 + 'propagate': False, + }, + 'apscheduler': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭定时任务的 INFO 日志(每分钟执行) + 'propagate': False, + }, + 'apscheduler.scheduler': { + 'handlers': log_handlers, + 'level': 'WARNING', + 'propagate': False, + }, + 'apscheduler.executors': { + 'handlers': log_handlers, + 'level': 'WARNING', + 'propagate': False, + }, + 'graphviz': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭 graphviz 的 DEBUG 日志 + 'propagate': False, + }, + 'graphviz._tools': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭 graphviz._tools 的 DEBUG 日志 + 'propagate': False, + }, + + # Django 框架日志控制 + 'django.db.backends': { + 'handlers': log_handlers, + 'level': 'INFO', # 关闭数据库查询的 DEBUG 日志 + 'propagate': False, + }, + 'django.db.backends.schema': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭数据库模式的 DEBUG 日志 + 'propagate': False, + }, + 'django.utils.autoreload': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭自动重载的 DEBUG 日志 + 'propagate': False, + }, + 'django.request': { + 'handlers': log_handlers, + 'level': 'WARNING', # 只记录 WARNING 以上的请求日志(错误请求) + 'propagate': False, + }, + 'django.server': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭服务器的 INFO 日志(如访问日志) + 'propagate': False, + }, + + # 其他第三方库日志控制 + 'asyncio': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭 asyncio 的 DEBUG 日志 + 'propagate': False, + }, + 'urllib3': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭 urllib3 的详细日志 + 'propagate': False, + }, + 'urllib3.connectionpool': { + 'handlers': log_handlers, + 'level': 'WARNING', # 关闭连接池的详细日志 + 'propagate': False, + }, + + # 性能指标日志(专门记录性能相关信息) + 'performance': { + 'handlers': ['performance_file'] if log_dir else ['console'], + 'level': 'INFO', + 'propagate': False, + }, + }, + + # 根日志记录器(兜底配置) + 'root': { + 'level': log_level, + 'handlers': log_handlers, + }, + } + + return logging_config diff --git a/backend/config/settings.py b/backend/config/settings.py new file mode 100644 index 00000000..d1feb1e7 --- /dev/null +++ b/backend/config/settings.py @@ -0,0 +1,332 @@ +""" +Django settings for config project. + +Generated by 'django-admin startproject' using Django 5.2.7. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path +import os +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + + +def get_bool_env(key: str, default: bool = False) -> bool: + """获取布尔值环境变量,兼容多种写法(true/True/TRUE/1/yes)""" + value = os.getenv(key, str(default)).lower() + return value in ('true', '1', 'yes', 'on') + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-default-key') + +# SECURITY WARNING: don't run with debug turned on in production! +# 安全优先:默认为 False,开发环境需显式设置 DEBUG=True +DEBUG = get_bool_env('DEBUG', False) + +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # 第三方应用 + 'rest_framework', + 'drf_yasg', + 'corsheaders', + 'channels', # WebSocket 支持 + # 业务应用 + 'apps.common', # 通用工具 + 'apps.targets', # 扫描目标管理 + 'apps.scan', # 扫描任务管理 + 'apps.engine', # 扫描引擎管理 + 'apps.asset', # 资产管理 +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', # CORS 中间件(必须在 CommonMiddleware 之前) + 'django.middleware.common.CommonMiddleware', + # 'django.middleware.csrf.CsrfViewMiddleware', # 已禁用 CSRF 校验 + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], # 添加自定义模板目录 + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + +# ASGI 应用配置(用于 WebSocket) +ASGI_APPLICATION = 'config.asgi.application' + + +# Database - PostgreSQL 配置 +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': os.getenv('DB_ENGINE', 'django.db.backends.postgresql'), + 'NAME': os.getenv('DB_NAME', 'xingrin'), + 'USER': os.getenv('DB_USER', 'postgres'), + 'PASSWORD': os.getenv('DB_PASSWORD', ''), + 'HOST': os.getenv('DB_HOST', 'localhost'), + 'PORT': os.getenv('DB_PORT', '5432'), + + # 连接池配置:针对长时间扫描任务优化 + # 说明: + # - 0: 每次请求后关闭(适合不稳定的远程连接) + # - 60-120: 推荐值(平衡性能和资源占用) + # - 300+: 适合长时间任务(减少连接重建开销) + # - None: 永久连接(仅适合专用连接池,不推荐) + 'CONN_MAX_AGE': int(os.getenv('DB_CONN_MAX_AGE', '300')), # 远程数据库使用 300 秒,减少重连开销 + + # PostgreSQL 特定选项 - 针对远程数据库优化 + 'OPTIONS': { + 'connect_timeout': 30, # 连接超时 30 秒(远程数据库需要更长时间) + 'options': '-c statement_timeout=60000 -c idle_in_transaction_session_timeout=300000', # SQL 语句超时 60 秒,事务空闲超时 5 分钟 + 'keepalives_idle': 600, # TCP keepalive 空闲时间 10 分钟 + 'keepalives_interval': 30, # TCP keepalive 间隔 30 秒 + 'keepalives_count': 3, # TCP keepalive 重试次数 + # 性能优化参数 + 'sslmode': 'disable', # 禁用SSL以减少连接延迟(如果网络安全可控) + 'application_name': 'xingrin_scanner', # 标识应用名称,便于监控 + } + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = os.getenv('LANGUAGE_CODE', 'zh-hans') + +TIME_ZONE = os.getenv('TIME_ZONE', 'Asia/Shanghai') + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# ==================== REST Framework 配置 ==================== +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'apps.common.pagination.BasePagination', # 使用基础分页器 + + # Session 认证(禁用 CSRF,前后端分离项目通过 CORS 控制跨域) + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'apps.common.authentication.CsrfExemptSessionAuthentication', + ], + + # JSON 命名格式转换:后端 snake_case ↔ 前端 camelCase + 'DEFAULT_RENDERER_CLASSES': ( + 'djangorestframework_camel_case.render.CamelCaseJSONRenderer', # 响应数据转换为 camelCase + 'djangorestframework_camel_case.render.CamelCaseBrowsableAPIRenderer', # 浏览器 API 也使用 camelCase + ), + 'DEFAULT_PARSER_CLASSES': ( + 'djangorestframework_camel_case.parser.CamelCaseJSONParser', # 请求数据从 camelCase 转换为 snake_case + 'djangorestframework_camel_case.parser.CamelCaseFormParser', # 表单数据也支持转换 + 'djangorestframework_camel_case.parser.CamelCaseMultiPartParser', # 文件上传支持转换 + ), + + # 转换配置 + 'JSON_UNDERSCOREIZE': { + 'no_underscore_before_number': True, # 数字前不加下划线 (field1 不会变成 field_1) + }, +} + +# ==================== CORS 配置 ==================== +# 允许所有来源(前后端分离项目,安全性由认证系统保障) +CORS_ALLOW_ALL_ORIGINS = os.getenv('CORS_ALLOW_ALL_ORIGINS', 'True').lower() == 'true' +CORS_ALLOW_CREDENTIALS = True + +# ==================== CSRF 配置 ==================== +CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://127.0.0.1:3000').split(',') + +# ==================== Session 配置 ==================== +SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 # 7 天 +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SAMESITE = 'Lax' # 跨站请求时发送 cookie + +# ==================== 扫描结果存储和清理配置 ==================== + +# 扫描结果存储目录(智能路径配置) +_scan_results_dir_env = os.getenv('SCAN_RESULTS_DIR') +if _scan_results_dir_env: + # 使用环境变量指定的路径(支持相对和绝对路径) + if os.path.isabs(_scan_results_dir_env): + SCAN_RESULTS_DIR = _scan_results_dir_env # 绝对路径 + else: + SCAN_RESULTS_DIR = str(BASE_DIR / _scan_results_dir_env) # 相对路径转绝对路径 +else: + # 默认使用项目目录下的 results 文件夹 + SCAN_RESULTS_DIR = str(BASE_DIR / 'results') + +# 扫描结果保留时间(单位:天) +SCAN_RESULTS_RETENTION_DAYS = int(os.getenv('SCAN_RETENTION_DAYS', '3')) + + +# ==================== Redis 配置 ==================== +# Redis 配置(用于 WebSocket Channel Layer) +REDIS_HOST = os.getenv('REDIS_HOST', 'localhost') +REDIS_PORT = int(os.getenv('REDIS_PORT', 6379)) +REDIS_DB = int(os.getenv('REDIS_DB', 0)) + +# Channels Layer 配置(WebSocket 后端) +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': { + 'hosts': [(REDIS_HOST, REDIS_PORT)], + 'capacity': 1500, # 单个通道最大消息数 + 'expiry': 10, # 消息过期时间(秒) + }, + }, +} + +# ==================== 日志配置 ==================== +# 日志配置说明: +# 1. 开发环境(DEBUG=True): +# - 默认 DEBUG 级别,输出详细调试信息 +# - 控制台彩色输出,便于调试 +# - 可选文件输出(配置 LOG_DIR) +# +# 2. 生产环境(DEBUG=False): +# - 默认 INFO 级别,只输出关键信息 +# - 同时输出到控制台和文件(配置 LOG_DIR) +# - 文件自动轮转,避免日志文件过大 +# +# 3. 环境变量控制: +# - LOG_LEVEL: 全局日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL) +# - LOG_DIR: 日志文件目录(留空则不输出文件) +# +from config.logging_config import get_logging_config + +LOGGING = get_logging_config(debug=DEBUG) + +# 命令执行日志开关(供 apps.scan.utils.command_executor 使用) +ENABLE_COMMAND_LOGGING = get_bool_env('ENABLE_COMMAND_LOGGING', True) + +# 扫描工具基础路径(后端和 Worker 统一使用该路径前缀存放三方工具等文件) +SCAN_TOOLS_BASE_PATH = os.getenv('SCAN_TOOLS_PATH', '/opt/xingrin/tools') + +# 字典文件基础路径(后端和 Worker 统一使用该路径前缀存放字典文件) +WORDLISTS_BASE_PATH = os.getenv('WORDLISTS_PATH', '/opt/xingrin/wordlists') + +# Nuclei 模板基础路径(custom / public 两类模板目录) +NUCLEI_CUSTOM_TEMPLATES_DIR = os.getenv('NUCLEI_CUSTOM_TEMPLATES_DIR', '/opt/xingrin/nuclei-templates/custom') +NUCLEI_PUBLIC_TEMPLATES_DIR = os.getenv('NUCLEI_PUBLIC_TEMPLATES_DIR', '/opt/xingrin/nuclei-templates/public') + +# Nuclei 官方模板仓库地址 +NUCLEI_TEMPLATES_REPO_URL = os.getenv('NUCLEI_TEMPLATES_REPO_URL', 'https://github.com/projectdiscovery/nuclei-templates.git') + +# 对外访问主机与端口(供 Worker 访问 Django 使用) +PUBLIC_HOST = os.getenv('PUBLIC_HOST', 'localhost').strip() +SERVER_PORT = os.getenv('SERVER_PORT', '8888') + +# ============================================ +# 任务分发器配置(负载感知调度) +# ============================================ +# 任务容器使用的 Docker 镜像 +TASK_EXECUTOR_IMAGE = os.getenv('TASK_EXECUTOR_IMAGE', 'yyhuni/xingrin-worker:latest') + +# 任务提交间隔(秒)- 防止短时间内重复分配到同一节点 +# 应大于心跳间隔(3秒),确保负载数据已更新 +TASK_SUBMIT_INTERVAL = int(os.getenv('TASK_SUBMIT_INTERVAL', '6')) + +# 本地 Worker Docker 网络名称(与 docker-compose.yml 中定义的一致) +DOCKER_NETWORK_NAME = os.getenv('DOCKER_NETWORK_NAME', 'xingrin_network') + +# 宿主机挂载源路径(所有节点统一使用固定路径) +# 部署前需创建:mkdir -p /opt/xingrin/{results,logs} +HOST_RESULTS_DIR = '/opt/xingrin/results' +HOST_LOGS_DIR = '/opt/xingrin/logs' + +# ============================================ +# Worker 配置中心(任务容器从 /api/workers/config/ 获取) +# ============================================ +# 远程 Worker 访问数据库的地址(自动推导) +# - 如果 DB_HOST 是外部 IP → 使用 DB_HOST(远程 PostgreSQL 场景) +# - 如果 DB_HOST 是 Docker 内部名 → 使用 PUBLIC_HOST(本地 PostgreSQL 场景) +_db_host = DATABASES['default']['HOST'] +_is_internal_db = _db_host in ('postgres', 'localhost', '127.0.0.1') +WORKER_DB_HOST = os.getenv( + 'WORKER_DB_HOST', + PUBLIC_HOST if _is_internal_db else _db_host +) + +# 远程 Worker 访问 Redis 的地址(自动推导) +# - 如果 PUBLIC_HOST 是外部 IP → 使用 PUBLIC_HOST +# - 如果 PUBLIC_HOST 是 Docker 内部名 → 使用 redis(本地部署) +_is_internal_public = PUBLIC_HOST in ('server', 'localhost', '127.0.0.1') +WORKER_REDIS_URL = os.getenv( + 'WORKER_REDIS_URL', + 'redis://redis:6379/0' if _is_internal_public else f'redis://{PUBLIC_HOST}:6379/0' +) + +# 容器内挂载目标路径(固定值,不需要修改) +CONTAINER_RESULTS_MOUNT = '/app/backend/results' +CONTAINER_LOGS_MOUNT = '/app/backend/logs' diff --git a/backend/config/urls.py b/backend/config/urls.py new file mode 100644 index 00000000..2f9d2529 --- /dev/null +++ b/backend/config/urls.py @@ -0,0 +1,64 @@ +""" +URL configuration for config project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +from apps.scan.notifications.views import NotificationSettingsView + +# API 文档配置 +schema_view = get_schema_view( + openapi.Info( + title="XingRin API", + default_version='v1', + description="Web 应用侦察工具 API 文档", + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + +urlpatterns = [ + # Django 后台管理 + path('admin/', admin.site.urls), + + # API 文档 + path('api/swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='swagger'), + path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='redoc'), + + # 业务 API(包含 organizations 和 targets) + path('api/', include('apps.targets.urls')), + + # 扫描 API + path('api/', include('apps.scan.urls')), + + # 引擎 & Worker API + path('api/', include('apps.engine.urls')), + + # 资产 API + path('api/', include('apps.asset.urls')), + + # 通知 API + path('api/notifications/', include('apps.scan.notifications.urls')), + + # 通知设置 API + path('api/settings/notifications/', NotificationSettingsView.as_view(), name='notification-settings'), + + # 认证 API + path('api/', include('apps.common.urls')), +] diff --git a/backend/manage.py b/backend/manage.py new file mode 100755 index 00000000..8e7ac79b --- /dev/null +++ b/backend/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 00000000..52685700 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,27 @@ +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "config.settings" +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +testpaths = ["apps"] +addopts = "-v --reuse-db" + +[tool.pylint] +django-settings-module = "config.settings" +load-plugins = "pylint_django" + +[tool.pylint.messages_control] +disable = [ + "missing-docstring", + "invalid-name", + "too-few-public-methods", + "no-member", + "import-error", + "no-name-in-module", +] + +[tool.pylint.format] +max-line-length = 120 + +[tool.pylint.basic] +good-names = ["i", "j", "k", "ex", "Run", "_", "id", "pk", "ip", "url", "db", "qs"] diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 00000000..e82032bd --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,47 @@ +# Django 核心 +Django==5.2.7 +djangorestframework==3.15.2 +djangorestframework-camel-case==1.4.2 +psycopg2-binary==2.9.10 + +# API 文档 +drf-yasg==1.21.7 +setuptools==75.6.0 + +# CORS 支持 +django-cors-headers==4.3.1 + +# 环境变量管理 +python-dotenv==1.0.1 + +# 异步任务和工作流编排 +prefect==3.4.25 +fastapi==0.115.5 # 锁定版本,0.123+ 与 Prefect 不兼容 +redis==5.0.3 # 可选:用于缓存 +APScheduler>=3.10.0 # 定时任务调度器 + +# WebSocket 支持 +channels==4.0.0 +channels-redis==4.2.0 +uvicorn[standard]==0.30.1 + +# SSH & 远程部署 +paramiko>=3.0.0 + +# 测试框架 +pytest==8.0.0 +pytest-django==4.7.0 + +# 工具库 +python-dateutil==2.9.0 +pytz==2024.1 +validators==0.22.0 +PyYAML==6.0.1 +colorlog==6.8.2 # 彩色日志输出 +python-json-logger==2.0.7 # JSON 结构化日志 +Jinja2>=3.1.6 # 命令模板引擎 +croniter>=2.0.0 # Cron 表达式解析(定时扫描) +psutil>=5.9.0 + +# 缓存 +cachetools>=5.3.0 diff --git a/backend/resources/resolvers.txt b/backend/resources/resolvers.txt new file mode 100644 index 00000000..29046c04 --- /dev/null +++ b/backend/resources/resolvers.txt @@ -0,0 +1,19142 @@ +99.99.99.193 +99.93.24.105 +99.92.207.51 +99.87.255.60 +99.83.147.72 +99.79.50.252 +99.79.182.170 +99.70.82.25 +99.42.199.193 +99.227.243.184 +98.28.219.202 +98.26.23.128 +98.213.193.14 +98.213.192.240 +98.213.192.223 +98.213.192.217 +98.191.57.131 +98.189.213.222 +98.186.137.171 +98.185.36.75 +98.175.83.14 +98.175.116.85 +98.171.255.37 +98.164.41.54 +98.154.40.78 +98.154.21.253 +98.153.167.29 +98.153.113.3 +98.152.244.50 +98.152.244.34 +98.13.82.63 +98.124.65.10 +98.116.5.254 +98.114.102.244 +98.102.44.156 +98.102.248.54 +98.102.153.162 +98.101.59.106 +98.101.240.164 +98.101.194.137 +98.100.232.5 +97.94.219.213 +97.90.61.98 +97.87.21.84 +97.84.39.242 +97.79.42.154 +97.78.76.116 +97.78.189.245 +97.75.162.89 +97.74.87.35 +97.74.86.124 +97.68.236.229 +97.68.173.14 +97.65.200.94 +97.64.128.163 +97.105.100.20 +96.95.223.105 +96.95.146.25 +96.93.138.5 +96.92.154.9 +96.88.179.101 +96.85.5.153 +96.82.69.210 +96.82.121.113 +96.81.30.194 +96.80.250.221 +96.8.17.30 +96.8.17.29 +96.8.17.26 +96.8.17.2 +96.8.17.142 +96.8.17.139 +96.8.17.131 +96.8.17.130 +96.76.85.122 +96.76.237.249 +96.74.213.233 +96.73.116.75 +96.73.116.29 +96.72.81.89 +96.72.43.162 +96.71.85.74 +96.71.165.73 +96.70.61.241 +96.70.191.213 +96.7.139.2 +96.7.139.1 +96.7.138.2 +96.7.137.92 +96.7.137.90 +96.7.137.82 +96.7.137.78 +96.7.137.64 +96.7.137.63 +96.7.137.32 +96.7.137.31 +96.7.137.250 +96.7.137.249 +96.7.137.227 +96.7.137.21 +96.7.137.208 +96.7.137.188 +96.7.137.184 +96.7.137.171 +96.7.137.169 +96.7.137.162 +96.7.137.150 +96.7.137.15 +96.7.137.142 +96.7.137.140 +96.7.137.137 +96.7.137.13 +96.7.137.12 +96.7.137.102 +96.7.137.0 +96.7.136.90 +96.7.136.86 +96.7.136.77 +96.7.136.63 +96.7.136.44 +96.7.136.41 +96.7.136.4 +96.7.136.254 +96.7.136.249 +96.7.136.242 +96.7.136.227 +96.7.136.216 +96.7.136.2 +96.7.136.199 +96.7.136.188 +96.7.136.169 +96.7.136.161 +96.7.136.160 +96.7.136.159 +96.7.136.146 +96.7.136.144 +96.7.136.140 +96.7.136.134 +96.7.136.123 +96.7.136.115 +96.7.136.114 +96.7.136.111 +96.7.136.107 +96.69.146.137 +96.68.29.97 +96.68.207.177 +96.68.152.180 +96.65.169.149 +96.56.97.5 +96.56.235.235 +96.5.131.84 +96.45.46.46 +96.45.45.45 +96.39.26.82 +96.36.248.12 +96.36.19.25 +96.35.169.218 +96.30.79.13 +96.30.126.36 +96.250.208.198 +96.235.35.5 +96.231.106.122 +96.127.154.165 +96.11.55.179 +96.10.55.75 +96.10.247.60 +96.10.218.162 +96.10.126.38 +95.97.79.58 +95.97.77.138 +95.97.113.102 +95.87.216.107 +95.86.164.205 +95.85.95.85 +95.85.86.194 +95.84.198.236 +95.84.192.62 +95.84.162.246 +95.80.93.89 +95.80.217.164 +95.80.104.128 +95.77.99.62 +95.77.96.79 +95.77.96.73 +95.73.60.213 +95.71.9.162 +95.71.2.54 +95.71.127.75 +95.68.245.145 +95.66.165.93 +95.66.164.102 +95.66.148.208 +95.66.142.11 +95.66.132.26 +95.65.9.171 +95.64.212.6 +95.64.205.42 +95.64.204.142 +95.64.202.178 +95.64.190.122 +95.64.186.94 +95.64.180.2 +95.64.167.46 +95.64.146.206 +95.64.141.190 +95.64.138.174 +95.64.134.98 +95.61.156.70 +95.58.145.254 +95.57.155.125 +95.54.192.158 +95.52.240.151 +95.50.82.150 +95.50.138.20 +95.47.59.113 +95.47.183.189 +95.47.180.171 +95.47.167.149 +95.47.164.83 +95.47.164.204 +95.47.148.218 +95.47.146.28 +95.47.140.179 +95.47.103.3 +95.47.103.2 +95.46.73.236 +95.46.6.43 +95.46.6.17 +95.46.141.236 +95.45.222.78 +95.43.241.218 +95.43.228.188 +95.43.223.164 +95.43.211.168 +95.43.125.251 +95.43.124.241 +95.43.101.183 +95.42.221.245 +95.42.22.30 +95.31.8.45 +95.31.52.10 +95.31.43.164 +95.31.40.207 +95.31.32.102 +95.31.233.13 +95.31.219.189 +95.31.208.124 +95.31.130.43 +95.30.250.9 +95.30.250.76 +95.30.250.69 +95.30.222.92 +95.30.216.187 +95.30.216.186 +95.255.67.120 +95.22.2.114 +95.216.72.9 +95.216.141.159 +95.216.12.41 +95.215.230.195 +95.215.19.53 +95.214.62.26 +95.214.106.135 +95.213.248.111 +95.213.208.251 +95.213.208.250 +95.213.146.181 +95.209.148.227 +95.190.199.158 +95.189.105.37 +95.189.104.1 +95.188.64.22 +95.183.55.91 +95.183.46.13 +95.182.107.243 +95.181.49.102 +95.181.143.56 +95.181.131.198 +95.180.218.10 +95.179.217.24 +95.179.143.35 +95.174.99.75 +95.174.99.34 +95.174.113.47 +95.174.111.27 +95.174.108.33 +95.174.101.212 +95.173.184.115 +95.172.87.14 +95.172.55.54 +95.172.52.114 +95.171.21.182 +95.171.117.141 +95.171.11.125 +95.168.224.139 +95.168.210.20 +95.168.195.130 +95.167.29.50 +95.167.216.17 +95.167.214.59 +95.167.214.38 +95.167.16.237 +95.167.150.28 +95.166.165.146 +95.165.90.159 +95.165.33.44 +95.165.255.106 +95.165.25.118 +95.165.200.74 +95.165.192.206 +95.165.192.190 +95.165.171.143 +95.165.167.48 +95.165.166.215 +95.165.162.79 +95.165.158.193 +95.165.146.162 +95.165.134.141 +95.165.12.86 +95.164.66.78 +95.164.65.17 +95.163.155.83 +95.161.228.110 +95.161.199.82 +95.161.198.130 +95.161.181.66 +95.161.166.46 +95.160.8.158 +95.160.31.73 +95.160.31.126 +95.160.230.148 +95.160.117.90 +95.160.117.47 +95.160.116.6 +95.160.116.42 +95.160.116.38 +95.160.116.34 +95.158.68.162 +95.158.37.247 +95.158.170.43 +95.158.146.166 +95.158.129.2 +95.158.128.2 +95.157.76.5 +95.156.4.114 +95.154.88.78 +95.154.88.72 +95.154.85.199 +95.154.80.145 +95.154.79.17 +95.154.78.70 +95.154.75.70 +95.154.72.135 +95.154.66.69 +95.154.180.65 +95.154.178.63 +95.154.177.60 +95.154.145.151 +95.154.110.85 +95.154.106.156 +95.154.103.131 +95.153.244.248 +95.143.242.138 +95.143.12.246 +95.142.88.177 +95.142.217.29 +95.141.82.202 +95.141.40.109 +95.141.23.145 +95.140.30.33 +95.140.203.92 +95.140.18.241 +95.140.17.94 +95.130.177.82 +95.129.147.156 +95.129.137.3 +95.128.72.180 +95.128.137.166 +95.124.245.102 +95.111.255.50 +95.111.252.8 +95.111.248.242 +95.111.239.88 +95.111.234.149 +95.111.231.0 +95.111.230.53 +95.111.226.67 +95.110.222.190 +95.110.208.189 +95.110.191.26 +95.110.185.167 +95.110.166.62 +95.110.154.72 +95.110.144.85 +95.110.135.8 +95.110.134.114 +95.110.133.36 +95.110.132.25 +95.110.131.72 +95.110.131.139 +95.107.53.22 +95.105.89.98 +95.105.89.28 +95.104.194.159 +95.104.194.108 +95.104.126.235 +94.79.33.27 +94.76.206.195 +94.76.203.216 +94.75.71.1 +94.75.110.66 +94.73.244.135 +94.73.231.219 +94.73.223.67 +94.73.216.61 +94.73.166.124 +94.72.61.194 +94.72.28.16 +94.72.10.94 +94.70.86.230 +94.70.224.14 +94.70.174.128 +94.68.84.250 +94.61.250.150 +94.51.107.254 +94.46.21.66 +94.46.175.93 +94.45.222.178 +94.43.99.154 +94.42.194.28 +94.41.86.120 +94.41.108.33 +94.40.112.196 +94.32.106.53 +94.30.9.110 +94.30.76.241 +94.29.75.49 +94.29.72.176 +94.26.96.84 +94.26.215.242 +94.26.147.16 +94.26.129.214 +94.255.145.226 +94.254.249.29 +94.253.9.230 +94.253.9.221 +94.253.15.114 +94.253.14.211 +94.253.13.82 +94.253.13.38 +94.253.12.85 +94.253.12.232 +94.249.192.20 +94.247.62.77 +94.247.61.239 +94.247.43.254 +94.247.208.90 +94.247.208.128 +94.243.61.94 +94.243.219.86 +94.243.131.250 +94.241.90.71 +94.240.43.117 +94.240.35.102 +94.24.54.252 +94.24.235.114 +94.24.107.161 +94.237.98.149 +94.236.210.227 +94.236.164.81 +94.233.73.10 +94.233.27.82 +94.232.62.66 +94.232.62.253 +94.232.62.169 +94.232.24.10 +94.232.217.81 +94.232.217.1 +94.232.216.81 +94.232.216.60 +94.232.216.217 +94.232.216.113 +94.232.11.178 +94.231.251.5 +94.231.212.65 +94.231.164.11 +94.231.164.10 +94.230.242.146 +94.230.241.186 +94.230.190.34 +94.230.190.226 +94.230.140.123 +94.230.130.151 +94.230.130.111 +94.23.81.37 +94.23.53.47 +94.23.205.212 +94.229.82.148 +94.229.237.174 +94.228.237.1 +94.228.207.117 +94.228.200.88 +94.228.198.76 +94.228.198.137 +94.228.187.149 +94.21.91.28 +94.21.91.189 +94.21.243.35 +94.21.243.250 +94.21.228.198 +94.209.111.121 +94.206.40.206 +94.206.40.14 +94.205.51.210 +94.205.48.98 +94.205.48.254 +94.203.136.10 +94.200.81.142 +94.200.80.94 +94.200.134.30 +94.200.113.250 +94.200.113.238 +94.20.88.35 +94.20.81.90 +94.20.68.142 +94.20.230.180 +94.20.230.175 +94.20.20.20 +94.20.142.202 +94.199.79.178 +94.199.75.180 +94.199.48.245 +94.199.48.243 +94.199.193.138 +94.198.55.64 +94.190.231.72 +94.190.216.177 +94.190.214.173 +94.190.213.131 +94.190.209.180 +94.190.206.66 +94.187.158.243 +94.18.210.70 +94.177.240.27 +94.177.203.192 +94.177.186.211 +94.174.238.188 +94.168.100.234 +94.158.157.142 +94.155.241.3 +94.154.86.60 +94.154.57.51 +94.154.27.222 +94.154.220.93 +94.153.241.134 +94.152.184.178 +94.141.135.186 +94.140.193.187 +94.140.15.15 +94.140.14.59 +94.140.14.49 +94.140.14.141 +94.140.14.140 +94.140.14.14 +94.140.104.242 +94.139.240.121 +94.139.221.136 +94.139.206.143 +94.138.81.162 +94.137.233.220 +94.135.228.220 +94.127.63.9 +94.127.63.6 +94.127.63.11 +94.127.59.8 +94.127.59.6 +94.127.59.14 +94.127.59.12 +94.127.59.11 +94.127.59.10 +94.127.135.212 +94.126.25.130 +94.124.199.113 +94.124.152.158 +94.102.6.142 +94.101.54.44 +94.101.234.229 +94.101.200.165 +94.100.65.225 +94.100.56.157 +94.100.11.64 +93.99.54.195 +93.97.214.4 +93.97.212.7 +93.95.99.250 +93.95.98.79 +93.95.103.70 +93.94.223.233 +93.93.56.1 +93.92.92.2 +93.92.65.2 +93.92.203.96 +93.92.202.187 +93.92.202.178 +93.92.196.238 +93.91.173.158 +93.91.153.178 +93.91.119.90 +93.91.119.230 +93.91.118.90 +93.91.116.100 +93.89.202.139 +93.89.111.13 +93.89.111.12 +93.88.78.116 +93.88.76.113 +93.88.195.34 +93.88.136.186 +93.87.72.62 +93.87.43.248 +93.87.43.227 +93.87.43.226 +93.87.42.218 +93.87.28.142 +93.87.244.161 +93.87.12.184 +93.87.119.92 +93.86.62.127 +93.86.51.162 +93.86.255.186 +93.85.92.173 +93.85.92.171 +93.85.241.30 +93.84.101.34 +93.83.47.82 +93.81.246.24 +93.81.243.138 +93.64.47.86 +93.64.222.78 +93.63.254.50 +93.63.229.132 +93.63.130.2 +93.62.185.99 +93.62.185.102 +93.62.185.101 +93.56.53.221 +93.56.53.218 +93.56.39.106 +93.56.11.92 +93.55.84.115 +93.51.241.141 +93.47.71.126 +93.46.55.233 +93.46.196.158 +93.46.163.31 +93.46.163.27 +93.43.95.134 +93.43.7.68 +93.42.199.134 +93.39.79.221 +93.34.4.222 +93.240.63.136 +93.240.228.186 +93.240.221.83 +93.240.174.186 +93.191.59.50 +93.191.14.176 +93.190.240.140 +93.190.224.140 +93.190.111.55 +93.189.251.101 +93.189.145.8 +93.187.183.52 +93.186.252.89 +93.186.245.247 +93.184.104.17 +93.183.204.1 +93.180.185.63 +93.180.179.93 +93.179.83.20 +93.179.254.253 +93.177.158.73 +93.177.126.4 +93.177.126.189 +93.175.245.14 +93.175.204.4 +93.174.79.88 +93.174.37.17 +93.174.241.154 +93.171.79.160 +93.171.4.66 +93.171.4.60 +93.171.4.51 +93.171.4.45 +93.171.4.43 +93.171.4.32 +93.171.4.25 +93.171.214.135 +93.171.182.249 +93.171.165.39 +93.170.94.8 +93.170.59.59 +93.170.5.238 +93.170.218.216 +93.170.209.77 +93.170.200.216 +93.170.190.130 +93.170.131.133 +93.170.118.246 +93.170.118.123 +93.170.116.147 +93.170.114.86 +93.170.114.62 +93.159.183.102 +93.159.156.82 +93.159.141.38 +93.158.35.34 +93.158.35.221 +93.158.35.220 +93.158.35.210 +93.158.35.197 +93.158.35.192 +93.158.35.189 +93.158.35.188 +93.158.35.174 +93.158.235.219 +93.158.228.243 +93.158.218.249 +93.157.46.181 +93.157.235.27 +93.157.168.222 +93.157.150.14 +93.153.229.38 +93.153.201.154 +93.153.170.202 +93.153.146.242 +93.150.45.238 +93.150.23.67 +93.150.21.150 +93.147.190.83 +93.147.179.161 +93.147.164.56 +93.145.96.230 +93.145.79.188 +93.145.22.2 +93.145.138.138 +93.126.113.230 +93.125.3.22 +93.125.121.128 +93.123.98.74 +93.123.96.46 +93.123.186.60 +93.123.16.124 +93.122.164.198 +93.122.145.238 +93.120.228.226 +93.115.28.114 +93.115.25.103 +93.115.138.251 +93.115.138.250 +93.115.136.236 +93.109.251.146 +93.109.243.17 +93.109.234.70 +93.109.228.14 +93.105.11.226 +93.104.240.206 +93.104.195.2 +93.103.221.171 +93.103.220.170 +92.92.218.109 +92.87.237.111 +92.80.101.210 +92.79.86.172 +92.67.160.10 +92.62.72.147 +92.62.65.237 +92.62.65.147 +92.62.65.138 +92.61.44.7 +92.60.50.40 +92.60.48.3 +92.59.185.58 +92.55.97.23 +92.55.56.232 +92.55.37.189 +92.55.27.172 +92.55.22.35 +92.53.124.134 +92.53.104.56 +92.53.101.219 +92.51.17.17 +92.51.12.60 +92.50.144.230 +92.50.144.154 +92.50.143.122 +92.50.141.22 +92.50.131.67 +92.49.159.116 +92.47.64.90 +92.45.23.168 +92.43.37.236 +92.43.224.1 +92.42.8.23 +92.42.210.43 +92.42.209.138 +92.42.127.220 +92.42.120.232 +92.39.93.72 +92.39.78.230 +92.39.169.159 +92.39.107.106 +92.38.43.2 +92.38.133.221 +92.33.1.1 +92.27.159.59 +92.26.84.139 +92.255.97.70 +92.255.95.138 +92.255.12.62 +92.252.243.93 +92.252.240.59 +92.249.219.159 +92.249.148.88 +92.249.143.119 +92.246.205.155 +92.245.30.25 +92.245.149.15 +92.245.109.24 +92.244.225.211 +92.243.164.125 +92.242.70.219 +92.242.54.211 +92.241.87.74 +92.241.86.42 +92.241.86.102 +92.241.8.225 +92.241.72.162 +92.241.69.226 +92.241.248.123 +92.241.19.83 +92.241.19.26 +92.241.17.234 +92.241.15.201 +92.241.143.147 +92.241.12.152 +92.241.111.8 +92.241.108.142 +92.241.102.13 +92.241.100.47 +92.241.100.200 +92.241.100.101 +92.240.251.48 +92.223.145.93 +92.223.105.162 +92.222.227.224 +92.222.221.29 +92.222.117.114 +92.205.63.50 +92.205.26.69 +92.205.21.96 +92.205.110.190 +92.204.241.140 +92.204.170.2 +92.203.123.237 +92.203.123.235 +92.198.57.36 +92.198.57.34 +92.198.20.43 +92.190.137.96 +92.182.99.142 +92.182.5.167 +92.182.38.172 +92.182.113.155 +92.182.111.64 +92.174.145.165 +92.174.128.197 +92.173.103.29 +92.154.25.213 +92.126.197.178 +92.124.99.58 +92.124.195.22 +92.124.156.129 +92.111.221.246 +92.101.192.202 +91.90.216.190 +91.90.190.58 +91.90.184.66 +91.90.179.50 +91.82.202.62 +91.81.60.206 +91.80.163.136 +91.74.90.206 +91.74.88.86 +91.74.66.94 +91.73.236.195 +91.73.167.42 +91.72.205.67 +91.72.185.26 +91.25.225.252 +91.25.225.251 +91.25.225.250 +91.249.242.34 +91.249.242.218 +91.249.238.157 +91.249.142.66 +91.247.250.178 +91.247.249.171 +91.247.234.78 +91.246.213.201 +91.246.213.161 +91.246.108.44 +91.245.159.2 +91.245.157.39 +91.244.230.115 +91.244.221.14 +91.244.172.167 +91.244.171.96 +91.244.113.72 +91.241.237.42 +91.240.86.14 +91.240.45.182 +91.240.211.83 +91.240.209.25 +91.240.103.2 +91.239.8.150 +91.239.237.150 +91.238.56.251 +91.238.56.20 +91.238.28.48 +91.238.234.38 +91.238.234.26 +91.238.234.226 +91.238.233.122 +91.237.183.242 +91.237.183.241 +91.237.183.225 +91.237.183.15 +91.237.183.1 +91.237.182.203 +91.237.109.151 +91.236.52.154 +91.236.238.91 +91.236.200.2 +91.236.176.148 +91.236.140.23 +91.236.137.82 +91.236.133.228 +91.236.114.66 +91.236.112.226 +91.235.253.248 +91.235.195.221 +91.235.100.31 +91.233.95.74 +91.233.7.209 +91.233.25.196 +91.233.25.138 +91.233.237.201 +91.233.176.248 +91.233.173.30 +91.233.173.28 +91.233.172.225 +91.232.188.7 +91.232.146.7 +91.232.141.91 +91.232.141.157 +91.232.105.138 +91.230.86.124 +91.230.68.158 +91.230.136.135 +91.229.62.10 +91.229.59.172 +91.229.216.3 +91.229.136.106 +91.228.32.110 +91.228.178.92 +91.227.23.66 +91.227.217.230 +91.227.216.11 +91.226.8.87 +91.226.8.182 +91.226.237.97 +91.226.223.235 +91.226.178.2 +91.226.172.139 +91.225.229.207 +91.225.228.86 +91.225.228.170 +91.224.61.250 +91.224.230.93 +91.224.206.30 +91.224.125.219 +91.224.124.184 +91.223.89.202 +91.223.74.126 +91.223.37.203 +91.223.224.254 +91.223.224.249 +91.223.120.25 +91.223.106.229 +91.221.57.203 +91.219.98.69 +91.219.83.40 +91.219.7.202 +91.219.7.166 +91.219.6.228 +91.219.6.226 +91.219.245.27 +91.219.203.237 +91.219.201.10 +91.218.210.19 +91.218.169.69 +91.218.100.11 +91.217.86.4 +91.217.22.56 +91.217.187.1 +91.217.109.128 +91.215.65.184 +91.215.227.18 +91.215.192.9 +91.215.150.74 +91.212.60.162 +91.212.228.198 +91.211.26.206 +91.211.142.138 +91.211.124.53 +91.210.86.199 +91.210.47.115 +91.210.25.81 +91.210.206.13 +91.210.179.88 +91.210.151.72 +91.209.174.71 +91.209.128.173 +91.208.20.17 +91.207.7.47 +91.206.231.155 +91.206.18.77 +91.205.72.51 +91.205.69.100 +91.205.3.65 +91.205.209.93 +91.205.208.46 +91.205.166.15 +91.205.144.27 +91.205.130.120 +91.205.129.144 +91.205.128.73 +91.204.96.164 +91.204.189.39 +91.204.165.174 +91.204.109.203 +91.203.82.231 +91.203.70.201 +91.203.36.75 +91.203.239.179 +91.203.195.253 +91.203.11.189 +91.202.26.198 +91.202.26.149 +91.201.235.186 +91.201.22.58 +91.201.19.243 +91.201.19.242 +91.201.17.47 +91.201.17.132 +91.201.17.117 +91.201.16.93 +91.201.16.81 +91.201.16.64 +91.201.136.36 +91.201.122.40 +91.200.67.156 +91.200.46.180 +91.200.45.108 +91.200.160.30 +91.200.0.15 +91.199.93.119 +91.197.209.222 +91.197.207.50 +91.197.204.2 +91.197.172.86 +91.197.135.24 +91.195.86.213 +91.195.71.1 +91.194.247.193 +91.194.246.45 +91.194.246.223 +91.194.22.2 +91.194.19.109 +91.193.12.196 +91.192.73.137 +91.192.68.66 +91.192.2.111 +91.192.196.131 +91.192.195.13 +91.192.194.207 +91.192.194.196 +91.192.168.212 +91.192.168.1 +91.191.251.122 +91.191.248.98 +91.191.14.15 +91.190.234.94 +91.190.234.87 +91.190.234.73 +91.190.234.69 +91.190.233.78 +91.190.142.200 +91.189.39.69 +91.189.27.242 +91.188.6.194 +91.188.188.64 +91.187.75.18 +91.187.59.84 +91.187.218.50 +91.185.238.152 +91.185.236.127 +91.183.7.14 +91.183.182.210 +91.183.104.102 +91.151.226.102 +91.150.93.128 +91.150.92.232 +91.150.126.174 +91.149.142.24 +91.148.109.139 +91.147.41.34 +91.147.40.118 +91.144.22.198 +91.143.58.198 +91.143.50.231 +91.143.44.36 +91.143.38.218 +91.138.253.87 +91.138.161.1 +91.137.131.25 +91.137.13.186 +91.135.7.77 +91.135.212.85 +91.135.212.62 +91.134.235.49 +91.134.159.109 +91.134.145.62 +91.133.83.97 +91.126.71.82 +91.126.217.123 +91.126.216.87 +91.126.187.228 +91.122.35.209 +91.122.198.34 +91.121.62.94 +91.121.59.192 +91.121.59.147 +91.121.52.36 +91.121.41.140 +91.121.33.31 +91.121.132.141 +91.121.114.217 +91.121.102.104 +91.120.22.182 +91.117.205.251 +91.109.95.22 +91.109.21.163 +91.109.192.227 +91.108.26.215 +91.103.29.245 +91.100.17.202 +90.84.225.87 +90.83.44.89 +90.63.223.17 +90.63.218.76 +90.63.218.189 +90.63.179.152 +90.24.152.179 +90.189.165.119 +90.189.113.130 +90.188.56.152 +90.188.38.38 +90.183.151.107 +90.183.151.106 +90.182.181.18 +90.176.22.74 +90.170.174.67 +90.163.132.67 +90.161.204.178 +90.161.140.133 +90.160.60.166 +90.160.60.162 +90.160.60.156 +90.160.60.154 +90.160.136.117 +90.160.105.117 +90.158.200.15 +90.154.63.26 +90.154.124.189 +90.151.106.86 +90.150.224.94 +90.150.180.71 +90.150.16.56 +90.115.109.242 +9.9.9.9 +9.9.9.13 +9.9.9.12 +9.9.9.11 +9.9.9.10 +89.91.15.9 +89.47.58.30 +89.45.183.165 +89.43.33.100 +89.43.16.202 +89.42.28.193 +89.42.219.139 +89.42.219.106 +89.42.218.22 +89.42.218.20 +89.40.227.132 +89.40.184.9 +89.39.246.235 +89.39.0.125 +89.38.58.131 +89.38.139.180 +89.37.108.2 +89.35.91.201 +89.35.117.216 +89.33.238.214 +89.32.32.32 +89.31.33.207 +89.29.229.12 +89.28.65.201 +89.28.12.62 +89.26.62.250 +89.26.124.93 +89.26.115.34 +89.252.129.2 +89.251.59.190 +89.251.146.145 +89.250.27.165 +89.250.27.162 +89.250.213.174 +89.250.197.225 +89.250.193.23 +89.250.185.137 +89.25.83.84 +89.25.247.114 +89.25.240.230 +89.25.190.187 +89.25.183.149 +89.248.196.6 +89.247.227.57 +89.246.181.158 +89.244.135.203 +89.24.22.210 +89.24.21.237 +89.24.206.253 +89.239.69.44 +89.238.255.150 +89.238.246.6 +89.238.206.66 +89.236.235.54 +89.236.106.156 +89.234.141.66 +89.233.118.207 +89.232.194.185 +89.232.194.184 +89.232.156.238 +89.232.144.82 +89.232.134.178 +89.231.26.73 +89.231.11.170 +89.228.9.42 +89.222.216.104 +89.222.200.210 +89.222.168.18 +89.222.168.162 +89.221.247.15 +89.221.160.202 +89.22.66.217 +89.22.66.209 +89.22.55.179 +89.22.54.83 +89.22.54.136 +89.22.182.5 +89.22.174.177 +89.22.17.167 +89.22.166.122 +89.22.103.79 +89.218.142.170 +89.216.62.4 +89.216.40.254 +89.216.39.214 +89.216.35.131 +89.216.27.28 +89.216.25.76 +89.216.118.84 +89.216.118.100 +89.216.116.23 +89.216.114.121 +89.216.107.221 +89.216.107.102 +89.216.103.45 +89.216.103.191 +89.215.246.3 +89.212.52.44 +89.212.4.181 +89.21.204.117 +89.208.74.74 +89.208.34.12 +89.208.115.14 +89.207.68.206 +89.207.66.38 +89.207.66.174 +89.207.219.73 +89.202.181.139 +89.201.3.8 +89.201.137.2 +89.20.45.88 +89.20.45.22 +89.20.42.122 +89.20.200.142 +89.190.66.2 +89.190.65.200 +89.190.48.201 +89.190.253.60 +89.19.178.21 +89.19.174.179 +89.19.14.82 +89.189.185.67 +89.189.180.151 +89.189.152.147 +89.189.132.22 +89.189.128.3 +89.189.10.50 +89.188.170.40 +89.188.126.31 +89.188.124.178 +89.188.117.36 +89.187.43.146 +89.187.129.97 +89.186.17.222 +89.186.11.171 +89.186.11.169 +89.186.11.168 +89.185.94.144 +89.185.93.88 +89.185.93.238 +89.185.93.143 +89.185.93.131 +89.185.75.83 +89.185.75.117 +89.185.121.250 +89.18.44.47 +89.179.78.108 +89.179.242.179 +89.175.90.58 +89.175.5.69 +89.175.4.66 +89.175.23.236 +89.175.224.214 +89.175.218.191 +89.175.205.42 +89.175.193.150 +89.175.123.174 +89.175.116.90 +89.175.115.74 +89.174.71.249 +89.174.53.66 +89.174.39.102 +89.17.54.132 +89.17.54.131 +89.165.142.251 +89.163.155.202 +89.154.1.233 +89.151.167.214 +89.151.139.149 +89.151.137.246 +89.151.134.157 +89.151.133.30 +89.148.55.251 +89.147.252.172 +89.147.205.66 +89.147.204.226 +89.147.203.186 +89.147.200.35 +89.145.240.121 +89.145.240.120 +89.145.220.205 +89.144.19.55 +89.143.244.210 +89.143.228.230 +89.143.12.246 +89.140.186.3 +89.135.52.29 +89.135.247.130 +89.134.183.233 +89.134.183.229 +89.134.150.125 +89.133.95.173 +89.133.95.169 +89.133.95.163 +89.133.8.25 +89.133.217.138 +89.133.217.137 +89.132.179.89 +89.121.250.206 +89.113.112.4 +89.111.245.17 +89.111.205.26 +89.111.112.10 +89.109.54.73 +89.109.54.251 +89.109.54.171 +89.109.41.125 +89.109.40.32 +89.109.34.95 +89.109.34.207 +89.109.255.92 +89.109.238.209 +89.109.237.52 +89.109.232.83 +89.109.22.129 +89.109.13.45 +89.109.1.202 +89.108.93.9 +89.108.122.128 +89.108.120.212 +89.108.118.228 +89.108.109.72 +89.108.108.208 +89.108.108.131 +89.107.210.249 +89.107.125.18 +89.107.122.68 +89.104.102.252 +89.100.164.115 +88.99.201.228 +88.97.71.154 +88.87.181.244 +88.87.139.12 +88.86.80.161 +88.86.126.179 +88.86.116.45 +88.85.160.175 +88.84.222.101 +88.84.213.98 +88.80.187.145 +88.80.113.55 +88.80.113.35 +88.30.53.40 +88.30.40.21 +88.30.33.64 +88.30.12.135 +88.28.202.213 +88.28.201.36 +88.28.198.59 +88.221.163.91 +88.221.163.90 +88.221.163.79 +88.221.163.67 +88.221.163.65 +88.221.163.61 +88.221.163.52 +88.221.163.45 +88.221.163.42 +88.221.163.4 +88.221.163.39 +88.221.163.37 +88.221.163.33 +88.221.163.32 +88.221.163.28 +88.221.163.255 +88.221.163.240 +88.221.163.24 +88.221.163.239 +88.221.163.238 +88.221.163.235 +88.221.163.230 +88.221.163.228 +88.221.163.227 +88.221.163.220 +88.221.163.22 +88.221.163.207 +88.221.163.205 +88.221.163.204 +88.221.163.203 +88.221.163.200 +88.221.163.196 +88.221.163.193 +88.221.163.190 +88.221.163.156 +88.221.163.146 +88.221.163.141 +88.221.163.129 +88.221.163.127 +88.221.163.120 +88.221.163.109 +88.221.162.94 +88.221.162.72 +88.221.162.66 +88.221.162.55 +88.221.162.37 +88.221.162.33 +88.221.162.32 +88.221.162.28 +88.221.162.27 +88.221.162.253 +88.221.162.233 +88.221.162.231 +88.221.162.229 +88.221.162.228 +88.221.162.223 +88.221.162.222 +88.221.162.204 +88.221.162.203 +88.221.162.199 +88.221.162.196 +88.221.162.194 +88.221.162.193 +88.221.162.191 +88.221.162.18 +88.221.162.179 +88.221.162.172 +88.221.162.157 +88.221.162.153 +88.221.162.146 +88.221.162.141 +88.221.162.140 +88.221.162.14 +88.221.162.122 +88.221.162.101 +88.221.162.0 +88.220.66.252 +88.220.134.11 +88.216.116.47 +88.214.176.182 +88.210.29.61 +88.208.244.225 +88.208.212.37 +88.208.192.73 +88.205.234.250 +88.200.204.5 +88.200.135.94 +88.199.63.137 +88.198.92.222 +88.198.45.136 +88.157.98.214 +88.157.68.26 +88.157.226.242 +88.157.154.134 +88.157.148.18 +88.157.103.191 +88.156.167.50 +88.156.167.34 +88.156.164.210 +88.156.164.122 +88.150.229.18 +88.149.212.184 +88.149.205.195 +88.149.203.57 +88.148.19.98 +88.147.210.10 +88.147.189.85 +88.147.188.243 +88.147.146.27 +88.147.142.131 +88.140.102.107 +88.135.187.88 +88.135.187.80 +88.135.187.70 +88.135.187.68 +88.135.187.66 +88.135.187.64 +88.135.187.62 +88.135.187.59 +88.135.187.39 +88.135.187.3 +88.135.187.231 +88.135.187.14 +88.135.187.120 +88.133.216.180 +88.132.48.28 +88.130.189.222 +88.130.169.13 +88.119.95.68 +88.119.94.177 +88.119.87.88 +88.119.249.83 +88.119.203.210 +88.119.203.196 +88.119.189.16 +88.119.189.110 +88.119.185.209 +88.119.153.69 +88.119.143.195 +88.119.142.115 +88.119.136.7 +88.119.135.155 +88.116.60.98 +88.116.159.130 +88.116.117.118 +87.98.254.244 +87.98.184.129 +87.98.167.47 +87.98.165.97 +87.98.153.70 +87.98.141.221 +87.98.137.178 +87.98.134.58 +87.98.128.51 +87.97.60.156 +87.76.14.207 +87.76.11.24 +87.76.1.77 +87.62.97.71 +87.61.108.250 +87.56.54.35 +87.54.6.134 +87.54.39.142 +87.49.54.222 +87.48.141.126 +87.27.112.23 +87.255.27.168 +87.255.13.196 +87.255.11.36 +87.255.0.242 +87.253.77.111 +87.252.169.6 +87.251.254.116 +87.251.125.156 +87.251.100.100 +87.250.61.31 +87.249.8.246 +87.249.8.245 +87.249.51.90 +87.249.34.186 +87.249.10.110 +87.249.10.108 +87.247.68.60 +87.246.232.231 +87.246.232.229 +87.245.23.57 +87.245.172.166 +87.244.9.206 +87.244.9.204 +87.244.225.200 +87.244.225.197 +87.244.16.6 +87.242.131.238 +87.241.174.184 +87.241.140.133 +87.239.246.167 +87.239.128.25 +87.238.232.158 +87.238.210.155 +87.238.187.6 +87.238.187.5 +87.237.42.29 +87.237.236.154 +87.237.212.36 +87.236.233.117 +87.236.232.5 +87.235.254.129 +87.234.200.113 +87.229.99.1 +87.229.85.250 +87.229.238.241 +87.229.232.219 +87.228.230.243 +87.226.222.210 +87.225.111.250 +87.225.109.218 +87.224.37.77 +87.224.138.249 +87.213.72.41 +87.213.68.86 +87.213.68.130 +87.213.16.123 +87.213.100.113 +87.204.28.12 +87.204.158.27 +87.201.133.14 +87.197.176.58 +87.197.175.186 +87.197.128.229 +87.197.127.219 +87.197.114.194 +87.193.62.211 +87.193.220.18 +87.193.184.254 +87.140.52.172 +87.139.53.21 +87.129.62.99 +87.123.215.162 +87.123.213.137 +87.121.157.130 +87.117.8.47 +87.117.6.229 +87.117.45.162 +87.117.38.34 +87.117.25.95 +87.117.176.149 +87.116.28.8 +87.105.251.234 +87.103.198.142 +86.96.197.235 +86.65.9.129 +86.63.79.174 +86.63.69.86 +86.63.216.70 +86.63.181.75 +86.63.125.7 +86.63.124.78 +86.63.124.46 +86.57.236.90 +86.57.198.13 +86.49.188.146 +86.48.27.73 +86.47.80.46 +86.47.80.38 +86.43.125.181 +86.35.6.75 +86.35.6.74 +86.32.3.225 +86.32.120.135 +86.32.120.134 +86.32.120.133 +86.188.131.209 +86.127.67.242 +86.126.7.188 +86.125.39.49 +86.124.63.76 +86.124.61.214 +86.123.161.217 +86.122.94.69 +86.122.61.3 +86.122.55.7 +86.122.48.169 +86.122.28.141 +86.122.185.167 +86.121.221.240 +86.120.45.162 +86.120.155.37 +86.120.115.227 +86.110.30.24 +86.109.195.190 +86.107.76.135 +86.107.237.94 +86.107.197.206 +86.107.187.184 +86.106.9.201 +86.102.89.26 +86.102.57.190 +86.102.49.170 +86.102.182.10 +86.102.157.118 +86.102.116.54 +86.101.76.5 +86.101.76.1 +86.101.57.229 +86.100.63.150 +85.95.169.250 +85.95.168.122 +85.94.179.243 +85.94.179.20 +85.93.46.25 +85.93.43.178 +85.90.160.69 +85.90.160.238 +85.9.133.77 +85.9.129.38 +85.9.129.36 +85.9.128.229 +85.89.169.24 +85.88.213.221 +85.88.213.199 +85.8.42.33 +85.72.141.81 +85.62.89.32 +85.62.5.74 +85.62.34.221 +85.62.209.5 +85.62.190.161 +85.62.110.170 +85.61.75.220 +85.60.12.49 +85.58.120.134 +85.53.144.232 +85.51.224.115 +85.51.202.225 +85.51.13.115 +85.50.252.66 +85.50.124.34 +85.31.250.103 +85.30.24.11 +85.30.235.216 +85.30.214.16 +85.30.195.133 +85.28.166.179 +85.27.252.136 +85.27.249.19 +85.26.239.136 +85.26.236.33 +85.26.230.60 +85.26.227.200 +85.26.175.138 +85.255.161.120 +85.255.160.106 +85.25.194.140 +85.24.222.227 +85.238.35.29 +85.238.106.185 +85.238.100.178 +85.237.62.128 +85.237.61.88 +85.237.61.86 +85.237.54.68 +85.237.46.168 +85.237.43.89 +85.237.43.237 +85.237.33.65 +85.236.26.130 +85.236.163.74 +85.235.62.212 +85.235.59.11 +85.235.49.25 +85.235.237.9 +85.235.197.66 +85.235.170.130 +85.235.167.178 +85.234.143.236 +85.234.128.209 +85.234.122.73 +85.234.10.120 +85.222.94.42 +85.222.68.162 +85.222.191.214 +85.222.111.74 +85.222.104.150 +85.221.249.126 +85.221.238.146 +85.221.211.26 +85.221.196.198 +85.221.191.177 +85.221.172.30 +85.221.172.18 +85.221.162.46 +85.220.213.165 +85.22.58.63 +85.22.147.21 +85.217.207.230 +85.217.158.4 +85.215.89.8 +85.215.212.48 +85.215.208.231 +85.215.204.9 +85.215.169.96 +85.215.167.110 +85.215.165.82 +85.215.163.63 +85.215.118.202 +85.214.96.146 +85.214.88.66 +85.214.86.130 +85.214.84.167 +85.214.71.38 +85.214.60.94 +85.214.52.216 +85.214.252.174 +85.214.24.14 +85.214.225.217 +85.214.217.89 +85.214.214.223 +85.214.205.60 +85.214.163.21 +85.214.152.133 +85.214.133.14 +85.214.130.83 +85.214.128.36 +85.214.121.235 +85.214.107.212 +85.214.107.154 +85.21.235.183 +85.21.210.86 +85.21.144.107 +85.209.89.231 +85.209.124.82 +85.209.124.182 +85.206.69.149 +85.206.44.91 +85.206.44.78 +85.206.44.68 +85.206.28.132 +85.206.27.22 +85.206.25.100 +85.206.161.70 +85.206.12.136 +85.204.172.66 +85.203.37.2 +85.203.37.1 +85.202.4.174 +85.202.236.50 +85.202.13.79 +85.200.209.58 +85.198.247.2 +85.195.99.211 +85.195.86.247 +85.195.75.106 +85.195.233.252 +85.195.226.210 +85.194.70.174 +85.194.70.173 +85.194.245.75 +85.193.195.73 +85.192.34.191 +85.192.166.185 +85.192.134.38 +85.19.64.212 +85.187.97.75 +85.187.87.204 +85.187.42.39 +85.187.42.112 +85.187.224.74 +85.187.221.8 +85.187.202.75 +85.187.202.45 +85.187.183.107 +85.187.10.222 +85.183.150.165 +85.18.102.42 +85.175.46.130 +85.175.46.122 +85.175.4.212 +85.175.227.77 +85.175.111.150 +85.173.252.9 +85.173.252.39 +85.173.252.16 +85.173.252.14 +85.173.246.147 +85.173.245.32 +85.173.244.203 +85.173.244.123 +85.173.114.248 +85.172.96.75 +85.172.67.34 +85.172.190.146 +85.172.189.154 +85.172.15.126 +85.172.12.190 +85.172.11.139 +85.172.11.115 +85.172.105.63 +85.172.1.34 +85.169.94.218 +85.169.120.6 +85.163.40.146 +85.163.138.114 +85.159.211.160 +85.159.110.163 +85.152.8.29 +85.15.189.108 +85.15.176.243 +85.15.140.68 +85.146.233.162 +85.146.204.98 +85.143.250.146 +85.143.24.62 +85.143.207.42 +85.143.206.18 +85.143.177.66 +85.143.164.154 +85.143.104.195 +85.140.36.44 +85.140.127.182 +85.14.5.203 +85.14.121.2 +85.14.100.154 +85.132.85.85 +85.132.8.70 +85.132.179.206 +85.132.12.178 +85.132.110.243 +85.132.110.193 +85.132.110.152 +85.132.110.126 +85.12.32.49 +85.119.74.82 +85.119.44.134 +85.117.63.10 +85.117.62.190 +85.117.36.140 +85.116.125.177 +85.116.106.42 +85.115.130.4 +85.114.55.238 +85.114.40.50 +85.113.9.73 +85.113.7.94 +85.113.7.225 +85.113.6.113 +85.113.26.54 +85.113.24.209 +85.113.219.162 +85.113.141.159 +85.11.126.108 +85.11.126.105 +85.11.125.123 +84.96.26.139 +84.96.26.129 +84.95.251.42 +84.95.207.132 +84.94.193.66 +84.92.240.109 +84.79.235.2 +84.7.212.177 +84.55.163.147 +84.54.64.35 +84.54.234.98 +84.54.226.50 +84.54.222.125 +84.54.131.65 +84.54.130.98 +84.54.115.40 +84.54.115.253 +84.54.115.248 +84.53.243.78 +84.53.239.236 +84.53.217.83 +84.53.206.212 +84.53.206.210 +84.53.201.130 +84.52.118.82 +84.47.118.5 +84.47.118.21 +84.46.91.66 +84.43.207.80 +84.43.207.18 +84.42.50.126 +84.42.124.162 +84.41.105.242 +84.40.91.18 +84.40.106.218 +84.38.95.223 +84.33.97.60 +84.33.95.44 +84.33.86.63 +84.255.30.153 +84.255.255.191 +84.255.244.10 +84.255.237.64 +84.255.173.210 +84.254.63.214 +84.254.54.243 +84.254.51.145 +84.254.40.4 +84.253.60.42 +84.253.52.126 +84.253.140.132 +84.248.207.134 +84.247.233.18 +84.247.225.173 +84.245.81.62 +84.245.80.52 +84.243.56.254 +84.239.39.125 +84.236.142.130 +84.233.182.251 +84.232.73.171 +84.232.69.179 +84.232.61.2 +84.232.101.210 +84.23.33.186 +84.22.63.117 +84.22.58.134 +84.22.51.168 +84.22.47.218 +84.22.46.238 +84.22.46.163 +84.22.46.162 +84.22.43.34 +84.22.43.190 +84.22.40.29 +84.22.158.239 +84.22.154.182 +84.22.139.144 +84.22.139.104 +84.21.227.234 +84.205.98.5 +84.205.98.130 +84.205.24.55 +84.204.8.164 +84.204.41.194 +84.204.40.166 +84.204.40.14 +84.203.162.227 +84.201.165.116 +84.201.138.148 +84.200.70.40 +84.200.69.80 +84.20.68.210 +84.20.37.78 +84.199.93.50 +84.199.239.165 +84.19.90.72 +84.17.232.22 +84.15.57.196 +84.15.45.178 +84.15.104.40 +84.14.48.246 +84.14.219.62 +84.14.115.173 +84.1.26.208 +83.99.195.157 +83.98.38.147 +83.97.105.48 +83.96.104.7 +83.87.202.7 +83.85.40.128 +83.71.132.188 +83.69.248.59 +83.69.216.149 +83.69.193.2 +83.69.10.10 +83.68.83.76 +83.48.80.205 +83.48.48.80 +83.48.43.148 +83.48.103.9 +83.246.141.45 +83.244.182.72 +83.244.182.58 +83.243.37.53 +83.243.33.204 +83.242.250.110 +83.242.104.195 +83.242.104.151 +83.240.244.121 +83.239.77.86 +83.239.75.46 +83.239.72.114 +83.239.62.214 +83.239.57.166 +83.239.56.218 +83.239.229.237 +83.239.229.164 +83.239.20.238 +83.239.117.66 +83.239.108.174 +83.235.34.161 +83.235.210.35 +83.235.105.234 +83.234.160.138 +83.233.23.208 +83.230.38.148 +83.229.85.77 +83.229.82.70 +83.229.82.151 +83.229.72.62 +83.229.68.13 +83.229.5.227 +83.229.5.216 +83.229.2.224 +83.228.107.193 +83.228.106.130 +83.223.146.120 +83.221.208.66 +83.221.207.123 +83.221.202.188 +83.220.53.218 +83.220.168.61 +83.220.162.43 +83.219.238.103 +83.219.1.220 +83.218.69.232 +83.212.12.67 +83.211.85.27 +83.211.248.38 +83.211.204.182 +83.206.50.49 +83.19.198.69 +83.174.235.52 +83.174.227.213 +83.174.226.183 +83.174.225.95 +83.174.224.244 +83.174.223.134 +83.174.220.149 +83.174.213.133 +83.174.212.101 +83.174.208.218 +83.173.203.174 +83.172.181.67 +83.172.181.64 +83.172.181.252 +83.172.181.251 +83.171.99.165 +83.171.88.30 +83.171.70.185 +83.171.127.235 +83.171.123.93 +83.171.114.10 +83.171.113.239 +83.171.113.17 +83.171.113.14 +83.171.110.201 +83.171.107.214 +83.171.106.177 +83.17.231.90 +83.169.247.254 +83.169.247.130 +83.169.217.22 +83.167.221.46 +83.167.101.179 +83.151.232.66 +83.151.14.204 +83.15.45.148 +83.149.125.95 +83.145.86.8 +83.145.86.7 +83.145.153.112 +83.145.133.2 +83.144.119.70 +83.143.8.249 +83.142.194.92 +83.142.189.68 +83.142.189.67 +83.142.127.39 +83.142.126.151 +83.142.116.148 +83.142.105.67 +83.14.136.180 +83.139.173.210 +83.137.41.9 +83.137.41.8 +83.137.218.126 +83.136.95.13 +83.136.95.12 +83.135.16.46 +83.13.214.38 +83.13.105.212 +83.110.139.58 +83.1.88.194 +83.1.221.110 +83.1.216.170 +83.1.215.1 +83.1.213.1 +83.1.191.242 +83.1.184.198 +83.1.108.26 +83.1.102.78 +83.0.57.50 +83.0.124.106 +83.0.114.235 +82.98.169.6 +82.97.198.50 +82.97.198.122 +82.97.19.9 +82.96.65.2 +82.96.64.2 +82.81.243.65 +82.80.232.24 +82.80.219.220 +82.80.207.94 +82.80.207.92 +82.79.60.33 +82.79.151.215 +82.78.94.101 +82.78.175.232 +82.78.168.19 +82.77.63.193 +82.77.48.243 +82.77.28.143 +82.77.137.111 +82.77.100.255 +82.76.32.233 +82.69.88.225 +82.69.53.72 +82.68.135.30 +82.66.221.168 +82.66.200.96 +82.66.181.223 +82.66.112.123 +82.65.161.62 +82.65.126.5 +82.64.73.173 +82.64.52.182 +82.64.35.106 +82.64.26.100 +82.64.206.162 +82.64.199.74 +82.64.136.152 +82.64.134.25 +82.64.128.90 +82.64.108.41 +82.3.55.75 +82.223.43.222 +82.223.210.127 +82.223.149.133 +82.223.107.139 +82.222.49.18 +82.220.99.203 +82.214.84.133 +82.214.162.54 +82.214.135.118 +82.214.100.214 +82.213.32.29 +82.210.34.38 +82.209.223.144 +82.209.203.56 +82.208.72.214 +82.208.146.164 +82.208.137.137 +82.204.207.198 +82.204.202.66 +82.204.200.246 +82.204.199.7 +82.204.198.50 +82.204.197.153 +82.204.175.198 +82.204.163.234 +82.204.138.46 +82.202.247.47 +82.202.215.178 +82.200.29.130 +82.198.78.140 +82.196.13.196 +82.195.16.29 +82.195.16.13 +82.194.244.140 +82.194.19.99 +82.194.19.147 +82.194.17.111 +82.193.241.125 +82.193.211.186 +82.192.169.38 +82.179.33.70 +82.177.14.166 +82.177.113.62 +82.165.76.185 +82.165.65.153 +82.165.26.30 +82.163.117.181 +82.162.58.236 +82.162.34.19 +82.152.37.235 +82.151.97.230 +82.151.90.1 +82.151.70.84 +82.151.127.188 +82.151.122.19 +82.151.115.85 +82.151.114.180 +82.151.114.140 +82.151.114.130 +82.149.209.86 +82.149.203.82 +82.148.69.71 +82.146.40.201 +82.146.26.2 +82.144.139.147 +82.143.81.146 +82.142.67.86 +82.142.156.186 +82.142.135.213 +82.140.97.198 +82.140.114.98 +82.140.114.174 +82.139.9.113 +82.138.82.214 +82.138.35.46 +82.138.31.164 +82.138.117.102 +82.137.27.130 +82.137.245.41 +82.135.26.24 +82.135.255.112 +82.135.215.108 +82.135.203.178 +82.135.197.108 +82.135.137.106 +82.135.112.130 +82.134.43.3 +82.127.85.3 +82.127.6.40 +82.119.154.40 +82.119.145.126 +82.118.243.190 +82.118.243.189 +82.118.129.158 +82.117.67.57 +82.117.219.138 +82.117.212.246 +82.117.211.18 +82.117.201.6 +82.117.196.42 +82.116.54.164 +82.116.39.182 +82.116.37.214 +82.115.76.160 +82.115.131.47 +82.114.85.25 +82.114.79.146 +82.114.67.150 +82.114.56.37 +82.114.240.34 +82.114.240.195 +82.114.240.112 +82.114.240.101 +82.112.49.97 +82.112.48.94 +82.112.48.182 +82.112.44.81 +82.112.40.153 +82.112.189.177 +82.103.129.240 +82.102.44.137 +82.102.41.117 +82.102.188.168 +82.102.147.34 +82.100.87.86 +81.95.207.16 +81.95.132.94 +81.95.130.202 +81.95.121.214 +81.95.113.2 +81.94.65.196 +81.94.164.237 +81.94.152.38 +81.94.134.205 +81.93.99.230 +81.93.89.116 +81.93.71.254 +81.93.185.19 +81.93.141.162 +81.9.198.217 +81.9.198.206 +81.9.198.202 +81.9.198.12 +81.9.198.118 +81.9.122.9 +81.89.79.245 +81.88.220.59 +81.88.197.178 +81.88.118.201 +81.83.4.18 +81.83.13.159 +81.82.232.125 +81.82.201.45 +81.7.94.177 +81.7.94.169 +81.7.16.249 +81.7.16.240 +81.7.104.226 +81.63.169.142 +81.62.235.182 +81.5.20.254 +81.5.1.54 +81.45.78.156 +81.45.43.133 +81.45.174.150 +81.45.138.117 +81.43.68.191 +81.42.248.139 +81.42.230.57 +81.4.149.154 +81.4.128.130 +81.4.126.70 +81.4.104.170 +81.4.100.121 +81.30.254.133 +81.30.219.213 +81.30.217.41 +81.30.213.34 +81.30.212.189 +81.30.209.107 +81.30.200.197 +81.30.197.178 +81.30.177.172 +81.3.27.54 +81.29.139.76 +81.29.132.3 +81.28.173.204 +81.28.164.44 +81.27.217.7 +81.27.212.130 +81.27.162.100 +81.26.23.193 +81.26.23.192 +81.255.9.173 +81.250.247.144 +81.250.234.100 +81.248.42.55 +81.248.237.119 +81.248.236.204 +81.248.1.164 +81.247.30.57 +81.246.99.1 +81.246.95.66 +81.246.88.101 +81.24.82.71 +81.24.122.118 +81.23.193.28 +81.23.180.70 +81.23.178.14 +81.23.153.203 +81.23.124.190 +81.23.120.122 +81.221.24.181 +81.221.123.173 +81.22.26.6 +81.219.64.202 +81.218.223.79 +81.218.223.76 +81.218.223.12 +81.218.223.112 +81.218.222.1 +81.216.9.94 +81.216.9.110 +81.211.98.234 +81.211.72.146 +81.211.5.194 +81.211.115.142 +81.21.82.86 +81.21.104.102 +81.201.51.140 +81.201.125.236 +81.200.28.216 +81.200.251.111 +81.200.18.145 +81.20.87.21 +81.20.82.131 +81.20.203.120 +81.199.16.1 +81.199.137.188 +81.198.81.165 +81.198.65.163 +81.198.189.160 +81.196.169.226 +81.192.193.134 +81.19.62.62 +81.183.253.121 +81.183.248.213 +81.183.234.98 +81.183.232.240 +81.183.230.143 +81.183.227.40 +81.183.224.48 +81.183.223.33 +81.183.223.225 +81.183.216.211 +81.183.215.141 +81.183.212.194 +81.183.211.172 +81.182.250.40 +81.182.246.69 +81.182.245.238 +81.182.244.148 +81.182.243.24 +81.181.130.105 +81.181.130.101 +81.18.218.218 +81.177.48.216 +81.177.255.67 +81.177.25.15 +81.177.166.162 +81.177.136.80 +81.176.229.165 +81.174.11.225 +81.173.126.239 +81.171.5.222 +81.170.150.234 +81.17.94.238 +81.17.232.196 +81.17.232.194 +81.169.244.26 +81.169.239.124 +81.169.233.218 +81.169.226.26 +81.169.214.46 +81.169.201.113 +81.169.179.146 +81.169.174.224 +81.169.172.226 +81.169.136.222 +81.169.135.209 +81.167.4.243 +81.167.0.163 +81.163.61.183 +81.163.61.170 +81.162.208.105 +81.162.111.11 +81.16.9.2 +81.16.8.46 +81.16.18.228 +81.150.98.206 +81.150.164.193 +81.150.126.65 +81.15.197.242 +81.15.180.110 +81.15.162.218 +81.15.133.90 +81.149.151.43 +81.147.49.247 +81.147.49.246 +81.147.49.229 +81.144.94.247 +81.144.94.246 +81.144.94.245 +81.133.149.153 +81.130.148.128 +81.130.132.247 +81.128.152.229 +81.0.77.6 +81.0.56.180 +81.0.220.118 +80.97.254.218 +80.96.4.194 +80.96.177.217 +80.94.225.100 +80.94.22.200 +80.93.37.202 +80.93.254.202 +80.93.251.162 +80.92.211.80 +80.91.22.151 +80.91.17.230 +80.91.17.200 +80.91.17.101 +80.90.132.107 +80.89.196.225 +80.89.145.83 +80.87.39.34 +80.87.33.242 +80.87.187.82 +80.87.146.13 +80.87.144.80 +80.87.144.184 +80.87.144.17 +80.87.144.111 +80.86.231.136 +80.86.230.58 +80.86.157.100 +80.83.239.194 +80.83.137.135 +80.82.55.71 +80.82.36.158 +80.82.20.42 +80.81.232.202 +80.81.2.50 +80.81.147.82 +80.80.98.8 +80.80.98.68 +80.80.218.218 +80.80.127.135 +80.80.108.12 +80.80.104.7 +80.79.245.138 +80.79.179.2 +80.79.176.2 +80.78.134.11 +80.78.132.79 +80.78.132.65 +80.77.53.58 +80.76.229.62 +80.76.184.6 +80.73.90.54 +80.73.88.21 +80.73.71.4 +80.73.66.90 +80.73.206.205 +80.72.64.99 +80.72.64.108 +80.72.30.35 +80.72.30.129 +80.72.179.29 +80.72.179.141 +80.72.178.140 +80.72.178.139 +80.72.17.248 +80.70.99.200 +80.67.6.98 +80.67.213.14 +80.67.105.163 +80.66.156.98 +80.65.83.104 +80.65.80.230 +80.64.162.43 +80.58.157.223 +80.58.155.32 +80.55.205.229 +80.54.51.106 +80.54.26.254 +80.54.219.186 +80.54.119.66 +80.53.75.34 +80.53.158.122 +80.52.60.2 +80.50.138.234 +80.50.129.22 +80.50.128.70 +80.48.33.33 +80.48.212.83 +80.48.178.106 +80.48.173.212 +80.48.126.12 +80.27.3.5 +80.27.3.4 +80.27.3.17 +80.27.2.228 +80.27.2.222 +80.27.2.221 +80.27.0.158 +80.254.123.70 +80.253.17.46 +80.252.177.227 +80.252.177.226 +80.252.156.90 +80.252.145.154 +80.252.145.132 +80.252.145.130 +80.252.144.228 +80.252.140.198 +80.250.210.218 +80.250.174.254 +80.249.81.151 +80.249.3.204 +80.249.188.53 +80.249.168.172 +80.248.57.28 +80.248.55.14 +80.248.51.229 +80.248.155.94 +80.247.35.20 +80.246.23.3 +80.246.15.4 +80.245.54.153 +80.245.226.219 +80.244.174.192 +80.242.237.178 +80.242.196.55 +80.241.248.162 +80.241.241.226 +80.241.208.210 +80.240.26.135 +80.240.253.76 +80.240.216.153 +80.240.21.21 +80.234.37.46 +80.234.32.136 +80.233.254.186 +80.233.254.181 +80.233.149.67 +80.232.240.91 +80.232.239.44 +80.232.219.35 +80.232.217.137 +80.229.31.179 +80.228.76.154 +80.228.231.48 +80.228.231.122 +80.228.226.132 +80.227.67.106 +80.227.38.26 +80.220.131.128 +80.211.51.57 +80.211.45.12 +80.211.36.243 +80.211.21.112 +80.211.208.74 +80.211.179.15 +80.209.71.197 +80.209.176.170 +80.194.118.138 +80.186.145.164 +80.179.255.238 +80.179.183.9 +80.179.160.8 +80.178.255.122 +80.178.170.176 +80.158.47.193 +80.156.145.201 +80.155.181.190 +80.154.108.233 +80.153.45.254 +80.152.176.239 +80.151.70.192 +80.151.54.36 +80.151.237.57 +80.151.224.157 +80.151.165.101 +80.151.11.49 +80.150.109.197 +80.15.204.212 +80.15.18.130 +80.15.128.201 +80.15.123.60 +80.147.95.112 +80.147.47.252 +80.147.198.229 +80.147.187.89 +80.147.181.115 +80.147.16.148 +80.147.151.133 +80.147.145.111 +80.14.71.233 +80.14.67.41 +80.13.98.191 +80.13.86.74 +80.13.34.124 +80.13.229.72 +80.13.138.211 +80.13.108.61 +80.125.41.162 +80.125.21.2 +80.125.0.246 +80.123.196.122 +80.118.230.106 +80.118.142.178 +80.113.19.90 +80.110.42.36 +80.11.47.121 +80.11.245.33 +8.8.8.8 +8.8.50.1 +8.8.4.4 +8.47.17.249 +8.43.56.34 +8.42.68.81 +8.42.68.209 +8.42.146.74 +8.41.124.193 +8.38.73.60 +8.38.18.201 +8.36.152.1 +8.36.139.129 +8.36.139.1 +8.35.35.35 +8.34.34.34 +8.33.239.244 +8.33.239.235 +8.33.239.234 +8.33.1.11 +8.33.1.10 +8.30.39.40 +8.30.39.39 +8.30.39.37 +8.30.39.35 +8.30.36.94 +8.30.192.171 +8.30.101.125 +8.30.101.124 +8.30.101.123 +8.30.101.118 +8.29.64.122 +8.29.3.77 +8.29.3.76 +8.29.3.75 +8.29.3.74 +8.29.3.67 +8.29.3.66 +8.29.3.37 +8.29.3.36 +8.29.3.34 +8.29.3.228 +8.29.3.227 +8.29.3.226 +8.29.3.221 +8.29.3.219 +8.29.3.213 +8.29.3.211 +8.29.3.140 +8.29.3.139 +8.29.3.138 +8.29.3.133 +8.29.3.132 +8.29.3.131 +8.29.2.139 +8.29.2.133 +8.29.2.132 +8.29.2.130 +8.28.113.202 +8.28.109.99 +8.28.109.85 +8.28.109.84 +8.28.109.83 +8.28.109.82 +8.28.109.77 +8.28.109.75 +8.28.109.74 +8.28.109.70 +8.28.109.69 +8.28.109.68 +8.28.109.62 +8.28.109.60 +8.28.109.6 +8.28.109.5 +8.28.109.46 +8.28.109.44 +8.28.109.42 +8.28.109.4 +8.28.109.3 +8.28.109.254 +8.28.109.252 +8.28.109.251 +8.28.109.247 +8.28.109.246 +8.28.109.244 +8.28.109.237 +8.28.109.235 +8.28.109.233 +8.28.109.231 +8.28.109.230 +8.28.109.229 +8.28.109.228 +8.28.109.226 +8.28.109.13 +8.28.109.126 +8.28.109.125 +8.28.109.124 +8.28.109.123 +8.28.109.122 +8.28.109.117 +8.28.109.116 +8.28.109.115 +8.28.109.114 +8.28.109.110 +8.28.109.11 +8.28.109.109 +8.28.109.106 +8.28.109.102 +8.28.109.101 +8.28.109.10 +8.26.56.99 +8.26.56.97 +8.26.56.96 +8.26.56.95 +8.26.56.94 +8.26.56.93 +8.26.56.92 +8.26.56.90 +8.26.56.89 +8.26.56.88 +8.26.56.87 +8.26.56.86 +8.26.56.85 +8.26.56.84 +8.26.56.83 +8.26.56.82 +8.26.56.80 +8.26.56.8 +8.26.56.78 +8.26.56.77 +8.26.56.74 +8.26.56.73 +8.26.56.72 +8.26.56.70 +8.26.56.7 +8.26.56.68 +8.26.56.65 +8.26.56.64 +8.26.56.63 +8.26.56.62 +8.26.56.6 +8.26.56.58 +8.26.56.57 +8.26.56.56 +8.26.56.54 +8.26.56.53 +8.26.56.52 +8.26.56.51 +8.26.56.48 +8.26.56.46 +8.26.56.45 +8.26.56.44 +8.26.56.42 +8.26.56.41 +8.26.56.40 +8.26.56.39 +8.26.56.38 +8.26.56.37 +8.26.56.36 +8.26.56.35 +8.26.56.34 +8.26.56.33 +8.26.56.31 +8.26.56.30 +8.26.56.3 +8.26.56.29 +8.26.56.28 +8.26.56.27 +8.26.56.26 +8.26.56.255 +8.26.56.252 +8.26.56.250 +8.26.56.25 +8.26.56.249 +8.26.56.247 +8.26.56.246 +8.26.56.245 +8.26.56.244 +8.26.56.243 +8.26.56.242 +8.26.56.241 +8.26.56.240 +8.26.56.24 +8.26.56.238 +8.26.56.237 +8.26.56.236 +8.26.56.233 +8.26.56.232 +8.26.56.230 +8.26.56.23 +8.26.56.228 +8.26.56.227 +8.26.56.226 +8.26.56.223 +8.26.56.222 +8.26.56.221 +8.26.56.22 +8.26.56.219 +8.26.56.218 +8.26.56.217 +8.26.56.216 +8.26.56.214 +8.26.56.213 +8.26.56.212 +8.26.56.211 +8.26.56.210 +8.26.56.209 +8.26.56.208 +8.26.56.206 +8.26.56.205 +8.26.56.204 +8.26.56.203 +8.26.56.202 +8.26.56.201 +8.26.56.20 +8.26.56.199 +8.26.56.198 +8.26.56.197 +8.26.56.196 +8.26.56.195 +8.26.56.194 +8.26.56.193 +8.26.56.192 +8.26.56.190 +8.26.56.19 +8.26.56.186 +8.26.56.184 +8.26.56.183 +8.26.56.182 +8.26.56.180 +8.26.56.18 +8.26.56.179 +8.26.56.178 +8.26.56.175 +8.26.56.174 +8.26.56.173 +8.26.56.171 +8.26.56.17 +8.26.56.169 +8.26.56.168 +8.26.56.167 +8.26.56.164 +8.26.56.163 +8.26.56.162 +8.26.56.161 +8.26.56.160 +8.26.56.16 +8.26.56.159 +8.26.56.158 +8.26.56.157 +8.26.56.156 +8.26.56.155 +8.26.56.154 +8.26.56.153 +8.26.56.151 +8.26.56.150 +8.26.56.15 +8.26.56.149 +8.26.56.148 +8.26.56.147 +8.26.56.146 +8.26.56.144 +8.26.56.143 +8.26.56.142 +8.26.56.141 +8.26.56.139 +8.26.56.137 +8.26.56.136 +8.26.56.135 +8.26.56.134 +8.26.56.133 +8.26.56.131 +8.26.56.13 +8.26.56.129 +8.26.56.127 +8.26.56.126 +8.26.56.125 +8.26.56.124 +8.26.56.123 +8.26.56.122 +8.26.56.12 +8.26.56.119 +8.26.56.118 +8.26.56.117 +8.26.56.116 +8.26.56.115 +8.26.56.114 +8.26.56.113 +8.26.56.112 +8.26.56.111 +8.26.56.110 +8.26.56.11 +8.26.56.109 +8.26.56.108 +8.26.56.107 +8.26.56.106 +8.26.56.105 +8.26.56.104 +8.26.56.103 +8.26.56.102 +8.26.56.101 +8.26.56.100 +8.26.56.10 +8.26.56.0 +8.25.185.132 +8.25.185.131 +8.25.184.254 +8.25.184.253 +8.25.184.252 +8.25.184.251 +8.25.184.107 +8.243.126.9 +8.243.126.19 +8.243.126.18 +8.243.126.14 +8.243.126.127 +8.243.126.126 +8.243.126.123 +8.243.126.122 +8.243.126.120 +8.243.126.11 +8.243.126.10 +8.243.113.189 +8.242.73.84 +8.242.6.242 +8.242.201.150 +8.242.153.171 +8.242.153.169 +8.242.153.166 +8.242.153.165 +8.242.153.162 +8.242.144.203 +8.24.104.109 +8.23.82.186 +8.224.34.74 +8.215.30.242 +8.213.0.112 +8.213.0.111 +8.212.10.42 +8.210.22.68 +8.209.66.67 +8.209.2.129 +8.208.2.65 +8.20.37.37 +8.20.37.35 +8.20.247.98 +8.20.247.97 +8.20.247.95 +8.20.247.94 +8.20.247.93 +8.20.247.92 +8.20.247.91 +8.20.247.90 +8.20.247.9 +8.20.247.89 +8.20.247.88 +8.20.247.87 +8.20.247.86 +8.20.247.85 +8.20.247.81 +8.20.247.80 +8.20.247.8 +8.20.247.79 +8.20.247.78 +8.20.247.77 +8.20.247.76 +8.20.247.74 +8.20.247.72 +8.20.247.71 +8.20.247.70 +8.20.247.7 +8.20.247.69 +8.20.247.67 +8.20.247.66 +8.20.247.65 +8.20.247.64 +8.20.247.63 +8.20.247.62 +8.20.247.61 +8.20.247.60 +8.20.247.6 +8.20.247.59 +8.20.247.58 +8.20.247.57 +8.20.247.56 +8.20.247.55 +8.20.247.54 +8.20.247.53 +8.20.247.52 +8.20.247.51 +8.20.247.5 +8.20.247.49 +8.20.247.47 +8.20.247.45 +8.20.247.43 +8.20.247.42 +8.20.247.41 +8.20.247.4 +8.20.247.39 +8.20.247.38 +8.20.247.37 +8.20.247.36 +8.20.247.35 +8.20.247.34 +8.20.247.33 +8.20.247.31 +8.20.247.30 +8.20.247.3 +8.20.247.26 +8.20.247.254 +8.20.247.251 +8.20.247.250 +8.20.247.25 +8.20.247.249 +8.20.247.247 +8.20.247.246 +8.20.247.245 +8.20.247.242 +8.20.247.241 +8.20.247.240 +8.20.247.239 +8.20.247.238 +8.20.247.236 +8.20.247.235 +8.20.247.234 +8.20.247.233 +8.20.247.231 +8.20.247.230 +8.20.247.23 +8.20.247.227 +8.20.247.225 +8.20.247.224 +8.20.247.223 +8.20.247.222 +8.20.247.221 +8.20.247.220 +8.20.247.219 +8.20.247.218 +8.20.247.217 +8.20.247.215 +8.20.247.214 +8.20.247.213 +8.20.247.212 +8.20.247.211 +8.20.247.210 +8.20.247.21 +8.20.247.207 +8.20.247.205 +8.20.247.202 +8.20.247.201 +8.20.247.200 +8.20.247.20 +8.20.247.2 +8.20.247.199 +8.20.247.197 +8.20.247.196 +8.20.247.193 +8.20.247.192 +8.20.247.191 +8.20.247.190 +8.20.247.19 +8.20.247.189 +8.20.247.188 +8.20.247.187 +8.20.247.185 +8.20.247.184 +8.20.247.183 +8.20.247.182 +8.20.247.18 +8.20.247.179 +8.20.247.178 +8.20.247.177 +8.20.247.176 +8.20.247.175 +8.20.247.174 +8.20.247.172 +8.20.247.171 +8.20.247.170 +8.20.247.17 +8.20.247.169 +8.20.247.168 +8.20.247.166 +8.20.247.165 +8.20.247.164 +8.20.247.163 +8.20.247.161 +8.20.247.16 +8.20.247.159 +8.20.247.157 +8.20.247.155 +8.20.247.154 +8.20.247.153 +8.20.247.152 +8.20.247.150 +8.20.247.149 +8.20.247.148 +8.20.247.146 +8.20.247.142 +8.20.247.141 +8.20.247.140 +8.20.247.139 +8.20.247.138 +8.20.247.137 +8.20.247.134 +8.20.247.133 +8.20.247.132 +8.20.247.13 +8.20.247.129 +8.20.247.128 +8.20.247.127 +8.20.247.126 +8.20.247.125 +8.20.247.124 +8.20.247.121 +8.20.247.120 +8.20.247.12 +8.20.247.119 +8.20.247.118 +8.20.247.115 +8.20.247.113 +8.20.247.112 +8.20.247.110 +8.20.247.11 +8.20.247.109 +8.20.247.108 +8.20.247.107 +8.20.247.106 +8.20.247.105 +8.20.247.103 +8.20.247.102 +8.20.247.101 +8.20.247.10 +8.20.247.0 +8.20.207.157 +8.20.207.156 +8.20.207.155 +8.20.207.154 +8.20.207.150 +8.20.207.149 +8.20.207.148 +8.20.207.146 +8.20.205.206 +8.20.205.205 +8.20.205.198 +8.20.205.196 +8.20.205.194 +8.19.63.58 +8.19.225.254 +8.18.4.20 +8.18.4.19 +8.17.40.98 +8.17.40.109 +8.17.40.108 +8.17.40.107 +8.17.40.106 +8.17.40.102 +8.17.40.101 +8.17.40.100 +8.17.30.61 +8.14.63.94 +8.14.63.93 +8.14.63.91 +8.14.63.90 +8.14.63.85 +8.14.63.82 +8.14.62.70 +8.14.62.69 +8.14.62.68 +8.14.62.67 +8.14.172.177 +8.0.7.0 +8.0.6.128 +8.0.41.16 +8.0.11.0 +79.98.222.23 +79.98.147.146 +79.98.138.6 +79.77.56.123 +79.61.138.153 +79.190.241.168 +79.188.180.106 +79.187.201.181 +79.187.201.179 +79.175.7.7 +79.174.52.122 +79.174.36.83 +79.174.187.207 +79.173.96.37 +79.173.81.152 +79.173.251.155 +79.170.252.236 +79.170.252.235 +79.170.252.233 +79.170.252.226 +79.170.189.235 +79.170.184.201 +79.170.160.62 +79.163.66.142 +79.163.65.188 +79.162.243.32 +79.162.211.34 +79.162.192.189 +79.161.9.164 +79.161.154.90 +79.143.72.246 +79.143.177.243 +79.143.160.90 +79.143.148.8 +79.142.84.242 +79.142.50.30 +79.141.82.250 +79.141.81.250 +79.140.29.116 +79.140.22.126 +79.140.190.191 +79.140.19.164 +79.140.180.219 +79.139.31.188 +79.138.160.93 +79.137.66.87 +79.137.66.85 +79.137.66.229 +79.137.190.174 +79.137.181.102 +79.137.110.166 +79.136.81.112 +79.136.81.111 +79.136.18.131 +79.135.61.9 +79.135.61.7 +79.135.61.11 +79.134.56.10 +79.134.54.190 +79.133.62.62 +79.131.108.227 +79.129.93.46 +79.129.62.57 +79.129.52.33 +79.129.35.6 +79.129.204.120 +79.129.100.212 +79.129.1.120 +79.125.163.222 +79.125.163.149 +79.124.50.245 +79.120.79.54 +79.120.79.194 +79.120.7.126 +79.120.54.186 +79.120.33.122 +79.120.32.170 +79.110.205.107 +79.110.204.58 +79.110.203.165 +79.110.199.229 +79.110.198.178 +79.106.24.13 +79.106.231.170 +79.106.172.70 +79.104.8.10 +79.104.30.170 +79.104.3.158 +79.104.26.58 +79.104.18.118 +79.104.17.30 +79.101.62.46 +79.101.46.218 +79.101.39.65 +79.101.38.164 +79.0.73.35 +78.96.119.60 +78.9.110.22 +78.88.188.98 +78.88.188.206 +78.85.41.38 +78.85.33.152 +78.85.33.129 +78.85.32.234 +78.85.241.59 +78.85.24.193 +78.85.22.141 +78.85.21.175 +78.83.183.104 +78.83.11.170 +78.80.47.119 +78.46.80.82 +78.46.173.177 +78.41.171.147 +78.40.107.141 +78.36.4.51 +78.36.203.181 +78.36.202.75 +78.36.196.87 +78.31.85.18 +78.31.67.99 +78.31.64.193 +78.31.59.12 +78.31.150.6 +78.31.100.237 +78.31.100.115 +78.30.249.249 +78.30.243.152 +78.30.217.153 +78.30.197.31 +78.30.194.253 +78.30.194.13 +78.29.28.10 +78.29.19.18 +78.28.158.220 +78.26.148.136 +78.25.86.148 +78.25.155.61 +78.25.155.56 +78.25.155.101 +78.25.128.130 +78.199.59.111 +78.159.157.89 +78.157.163.69 +78.157.162.147 +78.156.255.42 +78.155.252.220 +78.155.23.143 +78.155.172.11 +78.153.224.243 +78.153.138.70 +78.153.137.234 +78.142.85.182 +78.142.37.74 +78.142.234.75 +78.142.234.4 +78.142.233.245 +78.142.233.163 +78.142.232.70 +78.142.232.35 +78.140.59.25 +78.140.56.107 +78.140.4.85 +78.140.27.177 +78.140.24.168 +78.140.205.14 +78.140.12.87 +78.140.10.3 +78.136.108.192 +78.134.68.101 +78.134.63.198 +78.134.212.20 +78.134.210.176 +78.132.142.171 +78.131.88.3 +78.131.87.208 +78.131.12.155 +78.131.12.126 +78.131.11.248 +78.131.11.118 +78.130.65.38 +78.130.38.68 +78.130.38.64 +78.130.231.66 +78.130.147.3 +78.130.147.134 +78.130.147.109 +78.129.243.107 +78.129.243.105 +78.129.231.117 +78.129.138.112 +78.111.144.27 +78.111.121.102 +78.111.115.198 +78.110.160.53 +78.110.157.178 +78.110.151.199 +78.109.98.132 +78.109.191.90 +78.109.131.76 +78.108.77.75 +78.108.70.133 +78.108.164.58 +78.108.164.125 +78.108.160.207 +78.108.102.212 +78.108.101.23 +78.107.63.165 +78.107.31.203 +78.107.30.33 +78.107.235.83 +78.107.234.187 +77.95.52.251 +77.94.97.150 +77.94.205.70 +77.94.124.18 +77.93.193.202 +77.92.245.50 +77.92.138.166 +77.91.245.76 +77.91.21.106 +77.89.4.114 +77.89.204.186 +77.89.180.166 +77.88.8.88 +77.88.8.8 +77.88.8.7 +77.88.8.3 +77.88.8.2 +77.88.8.1 +77.88.105.215 +77.87.86.196 +77.87.85.126 +77.87.103.181 +77.87.100.52 +77.85.204.242 +77.85.195.234 +77.85.194.207 +77.85.170.99 +77.79.248.43 +77.79.248.249 +77.79.227.107 +77.79.191.49 +77.79.184.131 +77.79.134.94 +77.79.133.19 +77.79.132.7 +77.78.205.175 +77.78.201.59 +77.78.201.103 +77.78.196.3 +77.78.159.210 +77.78.154.9 +77.78.153.198 +77.78.148.30 +77.77.24.10 +77.77.215.98 +77.76.190.71 +77.76.188.247 +77.76.174.200 +77.76.148.204 +77.75.93.86 +77.75.8.229 +77.75.35.139 +77.75.35.138 +77.74.31.171 +77.73.41.121 +77.72.195.10 +77.71.9.243 +77.71.69.6 +77.71.66.144 +77.71.62.125 +77.71.54.253 +77.71.28.137 +77.71.21.188 +77.69.37.70 +77.69.141.243 +77.68.94.230 +77.68.79.87 +77.68.240.102 +77.68.240.101 +77.68.23.47 +77.68.127.184 +77.66.201.215 +77.66.179.73 +77.66.178.208 +77.66.177.235 +77.66.176.135 +77.65.6.210 +77.65.52.106 +77.65.46.150 +77.65.43.187 +77.65.36.222 +77.65.19.202 +77.65.117.250 +77.55.224.248 +77.55.208.167 +77.51.209.243 +77.51.209.226 +77.51.204.184 +77.51.203.241 +77.51.190.77 +77.51.186.203 +77.50.152.126 +77.50.132.62 +77.48.45.234 +77.48.176.3 +77.48.159.46 +77.46.152.99 +77.46.136.160 +77.45.125.84 +77.45.111.74 +77.45.111.51 +77.45.110.84 +77.44.72.68 +77.43.73.228 +77.43.58.56 +77.42.253.91 +77.42.220.110 +77.41.161.202 +77.41.159.250 +77.41.155.86 +77.41.150.30 +77.40.51.241 +77.40.49.9 +77.39.99.247 +77.39.63.143 +77.39.233.134 +77.39.229.161 +77.38.204.210 +77.37.196.136 +77.37.164.143 +77.37.158.108 +77.37.146.43 +77.34.74.185 +77.34.49.120 +77.34.2.94 +77.32.126.208 +77.28.96.25 +77.28.114.147 +77.26.214.15 +77.26.196.60 +77.246.57.132 +77.246.236.114 +77.245.114.45 +77.245.112.174 +77.244.76.153 +77.244.223.130 +77.243.98.99 +77.243.4.154 +77.243.112.230 +77.242.96.158 +77.242.186.206 +77.242.18.69 +77.242.142.123 +77.242.138.149 +77.242.108.167 +77.242.107.172 +77.242.107.110 +77.238.209.242 +77.238.129.116 +77.237.8.175 +77.237.15.126 +77.236.65.19 +77.235.8.83 +77.235.29.159 +77.235.27.18 +77.235.19.147 +77.233.6.226 +77.233.192.110 +77.233.10.104 +77.232.26.104 +77.232.167.89 +77.232.167.27 +77.232.167.181 +77.232.167.127 +77.232.162.90 +77.232.160.249 +77.232.160.229 +77.231.171.44 +77.230.66.172 +77.230.19.121 +77.222.170.103 +77.221.90.86 +77.220.187.242 +77.220.136.22 +77.198.214.82 +77.158.164.186 +77.158.163.134 +77.158.145.234 +77.156.224.84 +77.137.26.185 +77.137.26.184 +77.129.200.109 +77.129.181.101 +77.129.160.137 +77.121.33.221 +77.109.21.186 +77.108.97.105 +77.108.81.236 +77.108.30.218 +77.106.201.102 +77.106.181.58 +77.105.0.17 +77.104.252.137 +77.104.247.101 +77.104.247.100 +76.9.245.205 +76.85.68.107 +76.80.58.102 +76.80.221.14 +76.79.232.163 +76.76.2.5 +76.76.2.44 +76.76.2.4 +76.76.2.38 +76.76.2.37 +76.76.2.36 +76.76.2.35 +76.76.2.34 +76.76.2.33 +76.76.2.32 +76.76.2.2 +76.76.2.11 +76.76.2.1 +76.76.2.0 +76.76.10.5 +76.76.10.4 +76.76.10.38 +76.76.10.37 +76.76.10.36 +76.76.10.35 +76.76.10.34 +76.76.10.33 +76.76.10.32 +76.76.10.2 +76.76.10.155 +76.76.10.1 +76.76.10.0 +76.72.141.20 +76.240.86.154 +76.190.97.130 +76.181.207.75 +76.16.127.159 +76.144.87.168 +76.122.251.76 +75.60.238.51 +75.2.53.153 +75.190.205.136 +75.150.23.1 +75.150.197.154 +75.148.63.169 +75.148.33.61 +75.146.225.151 +75.144.168.25 +75.127.62.190 +75.10.63.140 +75.10.57.38 +74.95.17.85 +74.93.8.236 +74.93.61.233 +74.9.229.243 +74.87.93.226 +74.87.71.130 +74.87.171.129 +74.85.157.195 +74.84.156.35 +74.82.42.42 +74.75.115.158 +74.63.199.2 +74.62.175.3 +74.220.250.149 +74.220.244.105 +74.219.86.186 +74.214.190.148 +74.214.183.94 +74.208.27.110 +74.208.229.185 +74.208.217.104 +74.208.207.175 +74.208.191.19 +74.203.184.12 +74.202.142.162 +74.174.200.5 +74.143.124.121 +74.142.66.246 +74.142.233.195 +74.124.54.13 +74.123.230.1 +74.122.68.33 +74.122.134.108 +74.121.124.153 +74.120.41.197 +74.120.30.97 +74.120.29.129 +74.120.249.24 +74.120.24.97 +74.120.24.129 +74.117.241.12 +74.114.234.146 +74.113.40.2 +74.113.101.251 +74.112.48.13 +74.112.164.55 +73.76.100.239 +73.232.202.74 +73.213.55.199 +73.198.65.77 +73.181.42.153 +73.130.2.237 +73.121.146.21 +73.111.103.128 +73.100.143.241 +72.95.12.221 +72.69.161.7 +72.45.194.226 +72.45.186.19 +72.45.131.66 +72.44.21.239 +72.44.21.223 +72.28.117.69 +72.28.117.68 +72.255.245.68 +72.255.242.63 +72.255.226.226 +72.253.176.39 +72.24.115.38 +72.237.212.21 +72.237.212.20 +72.234.100.183 +72.223.30.166 +72.215.205.36 +72.211.20.43 +72.211.20.35 +72.20.142.226 +72.19.3.8 +72.19.20.12 +72.19.20.10 +72.18.200.5 +72.18.200.12 +72.167.54.236 +72.167.42.99 +72.167.39.214 +72.167.39.167 +72.14.186.159 +72.14.11.161 +72.131.206.67 +72.129.184.111 +72.128.132.211 +72.10.3.46 +71.93.47.214 +71.9.227.214 +71.86.3.190 +71.82.235.38 +71.78.93.66 +71.78.178.237 +71.76.82.147 +71.67.219.165 +71.67.195.27 +71.67.154.107 +71.66.18.58 +71.6.132.177 +71.6.132.173 +71.45.244.59 +71.41.254.5 +71.41.252.75 +71.4.247.50 +71.4.247.100 +71.19.251.34 +71.167.28.213 +71.162.144.157 +71.14.165.4 +71.13.68.78 +71.10.184.57 +70.97.122.111 +70.95.157.116 +70.94.34.23 +70.94.151.84 +70.91.107.93 +70.90.215.209 +70.89.219.49 +70.88.50.197 +70.63.90.202 +70.61.254.130 +70.61.240.4 +70.44.71.55 +70.35.134.178 +70.232.178.32 +70.184.213.142 +70.180.24.59 +70.180.24.58 +70.117.56.250 +70.114.18.4 +69.75.213.219 +69.70.21.150 +69.67.97.2 +69.67.97.18 +69.64.54.93 +69.64.54.66 +69.63.73.234 +69.63.64.12 +69.60.160.203 +69.60.160.196 +69.46.63.34 +69.30.174.72 +69.28.54.203 +69.229.5.233 +69.229.1.41 +69.194.131.163 +69.179.102.1 +69.169.190.211 +69.167.171.210 +69.163.32.8 +69.131.56.8 +69.13.210.208 +69.11.6.154 +69.10.42.239 +69.1.129.3 +68.89.132.250 +68.234.119.76 +68.225.2.46 +68.188.22.22 +68.183.179.157 +68.177.59.111 +68.175.51.49 +68.171.221.3 +68.171.15.196 +68.171.15.195 +68.15.188.99 +68.132.214.45 +67.97.247.52 +67.97.247.50 +67.79.47.3 +67.79.250.226 +67.78.98.178 +67.72.109.56 +67.60.177.71 +67.242.6.250 +67.231.37.113 +67.227.132.89 +67.225.137.83 +67.222.123.6 +67.219.112.206 +67.212.188.107 +67.210.146.50 +67.21.80.164 +67.21.172.197 +67.207.87.107 +67.206.237.78 +67.206.161.69 +67.204.8.226 +67.203.4.92 +67.200.141.228 +67.20.140.233 +67.192.111.202 +67.17.215.133 +67.17.215.132 +67.148.21.250 +67.135.161.240 +67.134.197.97 +67.134.102.128 +67.132.197.48 +67.130.9.121 +67.130.66.225 +67.128.201.121 +67.128.146.151 +67.128.146.144 +66.93.87.2 +66.92.64.2 +66.92.224.2 +66.92.159.2 +66.85.129.172 +66.85.121.14 +66.82.4.8 +66.82.4.12 +66.76.212.177 +66.45.254.11 +66.42.98.203 +66.28.0.61 +66.28.0.45 +66.251.35.130 +66.249.99.130 +66.246.119.145 +66.244.65.197 +66.243.10.180 +66.23.227.204 +66.223.231.66 +66.219.255.23 +66.216.47.29 +66.212.60.103 +66.209.73.153 +66.207.46.201 +66.206.167.2 +66.206.166.2 +66.199.10.93 +66.199.10.92 +66.199.10.80 +66.198.220.119 +66.198.193.78 +66.193.38.100 +66.193.240.4 +66.181.166.164 +66.175.212.65 +66.175.203.202 +66.175.146.133 +66.173.0.3 +66.163.0.173 +66.163.0.161 +66.162.85.79 +66.162.113.119 +66.161.231.242 +66.155.216.122 +66.135.47.183 +66.111.127.99 +66.11.107.97 +66.109.229.6 +66.109.229.4 +65.75.74.124 +65.75.74.123 +65.56.156.249 +65.56.156.242 +65.49.37.195 +65.39.166.201 +65.39.166.199 +65.39.166.198 +65.39.166.197 +65.39.166.134 +65.39.166.132 +65.39.166.131 +65.255.197.147 +65.246.64.217 +65.246.64.210 +65.244.0.14 +65.220.42.38 +65.220.15.2 +65.203.131.152 +65.199.61.96 +65.184.247.241 +65.172.241.13 +65.158.43.95 +65.158.116.106 +65.157.28.247 +65.155.70.65 +65.155.237.199 +65.155.232.151 +65.155.226.137 +65.155.222.14 +65.155.206.25 +65.155.188.22 +65.155.159.33 +65.155.15.167 +65.155.149.95 +65.155.105.152 +65.154.177.78 +65.154.130.142 +65.153.59.41 +65.152.254.167 +65.144.5.126 +65.144.155.7 +65.144.141.168 +65.144.134.146 +65.144.127.56 +65.141.190.234 +65.141.175.23 +65.141.101.136 +65.140.26.130 +65.140.231.106 +65.140.174.97 +65.133.71.9 +65.133.11.222 +65.133.11.219 +65.132.219.239 +65.127.10.47 +65.124.240.88 +65.123.108.7 +65.123.108.6 +65.121.81.162 +65.115.176.233 +65.115.175.241 +65.114.81.96 +65.114.195.96 +65.113.9.39 +65.112.207.1 +65.112.204.79 +65.109.93.205 +65.108.81.207 +65.108.195.182 +65.108.192.36 +64.90.83.57 +64.9.50.67 +64.81.79.2 +64.81.45.2 +64.81.159.2 +64.81.127.2 +64.80.255.251 +64.80.255.240 +64.80.203.194 +64.76.23.84 +64.76.163.165 +64.72.212.20 +64.6.65.6 +64.6.64.6 +64.6.128.62 +64.6.128.59 +64.50.242.202 +64.50.241.167 +64.50.241.166 +64.50.232.30 +64.50.232.2 +64.5.130.2 +64.45.190.98 +64.45.185.87 +64.31.5.1 +64.29.77.253 +64.237.97.69 +64.237.97.185 +64.237.68.238 +64.235.48.80 +64.233.219.99 +64.233.217.2 +64.233.207.2 +64.233.207.16 +64.233.206.99 +64.227.2.211 +64.223.246.26 +64.222.176.152 +64.215.98.149 +64.215.98.148 +64.212.76.178 +64.212.106.85 +64.212.106.84 +64.201.233.59 +64.20.37.186 +64.191.214.29 +64.19.105.113 +64.183.6.54 +64.17.20.1 +64.16.44.102 +64.157.242.118 +64.156.223.254 +64.146.144.50 +64.146.142.186 +64.145.73.4 +64.139.97.3 +64.132.94.250 +64.132.113.16 +64.130.151.75 +64.129.67.113 +64.129.104.46 +64.121.65.187 +64.119.80.100 +64.119.128.166 +64.107.45.5 +63.73.11.2 +63.47.189.218 +63.45.211.2 +63.45.209.164 +63.45.164.170 +63.42.55.88 +63.41.58.251 +63.40.220.21 +63.40.18.84 +63.40.15.230 +63.250.57.141 +63.246.224.30 +63.239.222.155 +63.238.99.81 +63.237.240.226 +63.234.98.153 +63.234.229.208 +63.232.89.67 +63.232.110.166 +63.226.4.97 +63.226.4.1 +63.225.136.120 +63.218.191.114 +63.215.98.144 +63.209.139.112 +63.197.115.100 +63.156.24.246 +63.151.93.198 +63.151.67.7 +63.151.57.183 +63.149.93.185 +63.149.147.193 +63.148.180.7 +63.146.221.254 +63.146.216.50 +63.145.180.22 +63.143.98.90 +62.96.215.140 +62.94.243.1 +62.93.33.21 +62.91.19.67 +62.89.15.179 +62.89.15.136 +62.89.15.131 +62.77.232.226 +62.77.122.168 +62.76.90.254 +62.76.76.62 +62.76.62.76 +62.76.30.148 +62.74.229.107 +62.74.216.213 +62.74.209.241 +62.64.89.42 +62.55.249.49 +62.55.249.17 +62.55.232.201 +62.55.207.217 +62.54.21.85 +62.54.20.179 +62.54.20.107 +62.48.163.166 +62.38.34.118 +62.36.9.107 +62.32.70.134 +62.29.140.29 +62.28.68.66 +62.28.181.242 +62.28.12.234 +62.28.102.188 +62.255.208.69 +62.245.225.55 +62.245.225.225 +62.233.128.21 +62.233.128.18 +62.233.128.17 +62.232.138.94 +62.231.172.178 +62.23.35.22 +62.23.170.194 +62.225.15.253 +62.221.253.107 +62.219.67.76 +62.217.77.45 +62.214.3.13 +62.210.137.184 +62.210.136.158 +62.210.122.43 +62.210.111.63 +62.201.217.194 +62.20.83.209 +62.2.213.120 +62.193.72.151 +62.182.223.11 +62.176.126.3 +62.173.181.238 +62.171.167.67 +62.168.30.102 +62.168.248.82 +62.168.242.110 +62.168.116.44 +62.161.245.137 +62.159.231.11 +62.157.169.104 +62.154.160.3 +62.153.165.107 +62.153.122.2 +62.152.53.218 +62.150.77.106 +62.150.254.82 +62.149.23.91 +62.149.183.45 +62.149.132.2 +62.149.128.4 +62.149.128.2 +62.146.202.2 +62.146.2.48 +62.140.239.1 +62.14.234.233 +62.122.99.198 +62.122.102.98 +62.12.114.10 +62.117.91.149 +62.112.116.86 +62.106.59.147 +62.105.17.234 +61.98.131.212 +61.97.13.38 +61.93.197.46 +61.85.98.198 +61.83.186.56 +61.83.152.140 +61.82.49.33 +61.82.39.88 +61.82.108.138 +61.8.255.8 +61.8.0.113 +61.79.82.241 +61.79.146.73 +61.76.83.69 +61.72.206.211 +61.7.197.1 +61.58.64.6 +61.5.134.35 +61.42.23.140 +61.41.17.6 +61.41.17.16 +61.39.168.66 +61.35.33.5 +61.34.243.102 +61.32.254.2 +61.255.135.199 +61.254.39.30 +61.251.111.227 +61.250.196.36 +61.244.186.189 +61.244.186.188 +61.244.14.62 +61.238.97.254 +61.222.77.165 +61.221.26.25 +61.219.97.68 +61.219.41.109 +61.219.165.25 +61.213.137.58 +61.213.119.145 +61.206.159.194 +61.206.115.16 +61.199.203.240 +61.199.193.74 +61.193.129.2 +61.19.42.34 +61.19.108.46 +61.127.14.130 +61.120.9.219 +61.111.22.3 +61.111.22.2 +61.108.103.134 +61.100.13.50 +61.1.106.16 +60.56.215.73 +60.45.155.205 +60.32.232.49 +60.32.220.90 +60.32.160.57 +60.32.149.50 +60.32.107.162 +60.251.238.89 +60.250.41.183 +60.250.235.122 +60.250.159.76 +60.250.134.27 +60.250.134.25 +60.249.212.149 +60.249.174.18 +60.249.11.234 +60.248.84.140 +60.248.55.151 +60.248.242.133 +60.248.225.123 +60.248.191.21 +60.248.191.20 +60.248.142.203 +60.248.142.199 +60.248.10.164 +60.244.121.98 +60.242.246.234 +60.240.176.183 +59.9.198.232 +59.31.156.154 +59.28.197.231 +59.28.162.183 +59.28.140.6 +59.27.124.209 +59.2.117.160 +59.190.126.235 +59.188.10.22 +59.18.227.35 +59.18.227.32 +59.18.227.14 +59.159.170.34 +59.158.8.83 +59.152.238.147 +59.152.206.146 +59.148.14.33 +59.144.177.217 +59.127.80.104 +59.127.40.140 +59.127.38.136 +59.127.244.123 +59.125.99.157 +59.125.83.54 +59.125.7.96 +59.125.43.223 +59.125.246.99 +59.125.229.34 +59.125.209.82 +59.125.155.62 +59.124.72.210 +59.124.69.19 +59.124.62.3 +59.124.6.82 +59.124.50.36 +59.124.26.210 +59.124.229.165 +59.124.17.180 +59.124.141.174 +59.124.122.196 +59.120.84.217 +59.120.52.200 +59.120.186.158 +59.120.143.151 +59.120.141.160 +59.120.125.129 +59.120.1.176 +59.120.1.175 +59.12.193.91 +59.12.146.87 +59.12.136.7 +59.103.231.38 +59.10.116.10 +59.1.58.227 +58.96.20.1 +58.82.152.44 +58.79.26.85 +58.72.252.65 +58.71.19.113 +58.71.125.1 +58.69.9.25 +58.69.88.193 +58.69.78.37 +58.69.7.42 +58.69.41.17 +58.69.29.58 +58.69.224.103 +58.69.21.74 +58.69.174.39 +58.69.17.105 +58.69.125.152 +58.27.196.114 +58.26.163.10 +58.26.121.130 +58.229.253.16 +58.228.61.2 +58.227.193.227 +58.191.61.42 +58.185.89.201 +58.185.71.113 +58.185.58.129 +58.185.54.17 +58.185.165.125 +58.181.52.190 +58.181.206.82 +58.180.176.3 +58.177.179.58 +58.177.158.254 +58.162.206.206 +58.147.189.90 +58.138.143.62 +58.137.148.83 +58.120.226.2 +54.94.175.250 +54.37.30.59 +54.252.183.4 +54.251.190.247 +54.214.161.74 +54.174.40.213 +52.6.246.21 +52.3.100.184 +52.29.2.17 +52.24.103.199 +52.233.186.232 +52.23.148.124 +52.229.172.73 +52.171.61.133 +52.119.88.204 +51.91.22.145 +51.91.130.132 +51.91.124.225 +51.83.13.211 +51.79.99.90 +51.79.144.245 +51.77.158.23 +51.75.131.205 +51.68.81.95 +51.68.227.204 +51.68.206.207 +51.68.199.234 +51.68.141.96 +51.68.128.120 +51.38.62.213 +51.38.148.132 +51.38.134.82 +51.38.125.60 +51.38.125.153 +51.255.94.135 +51.255.84.177 +51.255.43.23 +51.255.23.2 +51.255.128.121 +51.255.128.114 +51.254.5.245 +51.250.66.51 +51.210.86.116 +51.210.122.169 +51.178.84.208 +51.178.56.235 +51.178.20.235 +51.159.77.56 +51.15.183.246 +51.15.183.212 +51.15.174.81 +51.11.226.128 +51.104.34.175 +50.86.78.84 +50.78.97.124 +50.78.224.131 +50.78.108.5 +50.77.74.201 +50.76.207.170 +50.65.169.147 +50.49.246.2 +50.47.32.116 +50.45.183.18 +50.29.128.137 +50.28.52.102 +50.27.156.170 +50.249.22.169 +50.247.134.226 +50.246.249.43 +50.242.61.85 +50.237.34.1 +50.237.114.76 +50.236.212.9 +50.235.228.46 +50.235.153.225 +50.235.131.130 +50.234.132.241 +50.234.125.182 +50.231.115.22 +50.230.4.242 +50.229.170.233 +50.226.171.1 +50.226.170.222 +50.225.95.30 +50.225.71.126 +50.223.23.54 +50.223.162.29 +50.223.140.253 +50.220.47.51 +50.220.130.1 +50.218.46.1 +50.217.25.205 +50.217.25.204 +50.217.25.200 +50.215.49.13 +50.215.184.91 +50.213.203.33 +50.208.220.49 +50.207.85.66 +50.207.237.130 +50.206.9.62 +50.206.226.80 +50.206.226.42 +50.204.174.98 +50.204.174.58 +50.203.148.198 +50.198.91.121 +50.194.130.97 +50.192.13.172 +50.175.159.202 +50.170.46.89 +50.168.185.113 +50.125.234.126 +50.117.0.170 +50.115.205.166 +5.96.198.66 +5.9.94.16 +5.9.72.202 +5.9.44.83 +5.89.30.10 +5.83.75.10 +5.8.9.2 +5.79.101.123 +5.63.164.75 +5.63.163.69 +5.63.111.235 +5.63.106.250 +5.61.8.20 +5.61.203.112 +5.59.137.234 +5.58.131.22 +5.53.244.8 +5.53.122.54 +5.45.103.22 +5.42.248.31 +5.39.78.104 +5.39.71.50 +5.39.65.110 +5.39.38.184 +5.32.90.30 +5.32.72.134 +5.32.55.10 +5.32.183.251 +5.32.132.85 +5.28.131.139 +5.28.131.134 +5.255.9.232 +5.255.29.101 +5.255.27.54 +5.255.26.65 +5.255.24.99 +5.255.20.15 +5.255.172.42 +5.255.15.16 +5.255.13.115 +5.255.12.215 +5.255.12.13 +5.255.11.205 +5.254.198.142 +5.254.197.224 +5.254.196.61 +5.253.253.215 +5.253.252.232 +5.253.252.130 +5.253.146.177 +5.252.171.157 +5.252.170.88 +5.252.170.127 +5.249.141.46 +5.23.103.11 +5.227.66.64 +5.226.78.34 +5.21.5.130 +5.21.242.81 +5.21.242.111 +5.21.242.103 +5.21.241.87 +5.21.241.85 +5.206.235.55 +5.200.50.249 +5.200.42.113 +5.200.38.113 +5.2.33.111 +5.2.207.124 +5.2.197.180 +5.2.196.93 +5.199.141.5 +5.199.141.30 +5.199.130.141 +5.196.43.50 +5.196.186.81 +5.189.181.251 +5.189.172.127 +5.189.161.18 +5.189.160.125 +5.189.150.220 +5.189.149.51 +5.189.148.106 +5.189.128.74 +5.188.65.196 +5.188.64.139 +5.188.59.211 +5.188.139.235 +5.188.137.182 +5.187.78.146 +5.185.96.232 +5.185.94.131 +5.185.73.91 +5.185.32.122 +5.185.250.26 +5.185.245.69 +5.185.243.40 +5.185.243.38 +5.185.243.32 +5.185.243.27 +5.185.241.212 +5.185.240.128 +5.185.21.53 +5.185.2.96 +5.181.210.184 +5.180.183.129 +5.180.1.52 +5.178.82.106 +5.178.76.172 +5.178.114.78 +5.175.26.208 +5.172.188.118 +5.17.92.172 +5.17.89.68 +5.164.31.60 +5.164.27.185 +5.164.26.24 +5.161.16.131 +5.161.118.243 +5.158.124.92 +5.158.119.176 +5.154.120.75 +5.152.206.180 +5.151.63.237 +5.151.62.227 +5.149.91.66 +5.149.222.186 +5.149.221.236 +5.149.210.54 +5.149.206.139 +5.149.141.82 +5.148.5.210 +5.148.1.21 +5.143.233.106 +5.141.87.218 +5.141.29.82 +5.140.233.239 +5.140.233.185 +5.140.212.239 +5.140.162.52 +5.135.61.142 +5.135.221.207 +5.135.221.204 +5.135.221.201 +5.135.221.200 +5.135.166.77 +5.135.148.143 +5.135.137.224 +5.135.1.165 +5.134.48.218 +5.134.218.78 +5.130.61.37 +5.129.34.75 +5.128.66.56 +5.128.37.237 +5.128.120.3 +5.11.39.22 +5.11.11.5 +5.11.11.11 +5.104.166.26 +5.102.81.98 +5.101.79.57 +5.101.76.193 +5.101.76.185 +5.101.73.58 +5.101.200.62 +5.101.11.191 +5.100.252.54 +5.100.249.231 +5.10.25.83 +5.10.138.214 +5.1.66.255 +5.1.38.155 +49.50.70.170 +49.255.4.22 +49.255.194.141 +49.254.144.223 +49.254.144.195 +49.248.155.86 +49.248.102.209 +49.248.101.78 +49.231.237.181 +49.231.229.117 +49.229.56.21 +49.229.158.61 +49.229.100.162 +49.212.149.131 +49.156.53.166 +49.143.189.55 +49.1.168.71 +49.0.90.4 +49.0.82.21 +49.0.64.179 +47.57.147.61 +47.49.148.38 +47.48.67.238 +47.47.144.22 +47.44.3.69 +47.254.217.105 +47.253.24.2 +47.207.31.201 +47.206.64.128 +47.181.203.175 +47.180.199.94 +47.176.183.12 +47.176.153.236 +46.8.33.200 +46.8.252.136 +46.8.249.194 +46.8.105.12 +46.55.253.26 +46.55.223.223 +46.55.168.3 +46.47.127.251 +46.45.32.82 +46.45.19.35 +46.44.171.105 +46.44.1.11 +46.44.0.68 +46.42.19.218 +46.42.19.104 +46.40.245.69 +46.40.244.249 +46.40.244.137 +46.40.227.57 +46.40.219.164 +46.40.218.15 +46.40.214.43 +46.40.213.53 +46.40.2.237 +46.40.0.5 +46.4.70.20 +46.4.53.12 +46.37.4.202 +46.36.27.78 +46.30.172.121 +46.29.78.51 +46.29.169.6 +46.29.12.142 +46.254.130.47 +46.249.39.8 +46.246.29.68 +46.245.253.5 +46.243.181.193 +46.243.181.161 +46.243.179.123 +46.242.28.69 +46.242.130.233 +46.238.93.169 +46.235.85.142 +46.234.101.153 +46.233.57.170 +46.231.76.240 +46.231.72.212 +46.228.93.118 +46.227.67.134 +46.227.200.9 +46.226.143.86 +46.226.143.83 +46.221.5.123 +46.22.99.226 +46.216.182.238 +46.21.250.23 +46.21.171.193 +46.21.128.118 +46.199.82.115 +46.199.76.130 +46.196.212.8 +46.19.103.248 +46.188.53.34 +46.188.47.18 +46.188.22.50 +46.185.139.130 +46.183.220.64 +46.182.19.48 +46.174.0.245 +46.172.192.236 +46.171.144.226 +46.167.233.65 +46.166.189.67 +46.165.54.24 +46.160.173.208 +46.16.226.250 +46.16.226.116 +46.149.86.139 +46.148.26.40 +46.148.26.231 +46.140.228.158 +46.13.5.26 +46.107.231.70 +46.107.228.164 +46.101.65.164 +45.90.31.94 +45.90.31.82 +45.90.31.75 +45.90.31.74 +45.90.31.68 +45.90.31.6 +45.90.31.55 +45.90.31.5 +45.90.31.41 +45.90.31.4 +45.90.31.3 +45.90.31.250 +45.90.31.219 +45.90.31.217 +45.90.31.204 +45.90.31.2 +45.90.31.199 +45.90.31.194 +45.90.31.191 +45.90.31.189 +45.90.31.185 +45.90.31.183 +45.90.31.182 +45.90.31.179 +45.90.31.168 +45.90.31.167 +45.90.31.163 +45.90.31.16 +45.90.31.148 +45.90.31.144 +45.90.31.137 +45.90.31.136 +45.90.31.135 +45.90.31.128 +45.90.31.124 +45.90.31.117 +45.90.31.114 +45.90.31.112 +45.90.31.1 +45.90.30.99 +45.90.30.98 +45.90.30.97 +45.90.30.96 +45.90.30.95 +45.90.30.94 +45.90.30.93 +45.90.30.92 +45.90.30.91 +45.90.30.90 +45.90.30.9 +45.90.30.89 +45.90.30.88 +45.90.30.87 +45.90.30.86 +45.90.30.85 +45.90.30.84 +45.90.30.83 +45.90.30.82 +45.90.30.81 +45.90.30.80 +45.90.30.8 +45.90.30.79 +45.90.30.78 +45.90.30.77 +45.90.30.76 +45.90.30.75 +45.90.30.74 +45.90.30.73 +45.90.30.72 +45.90.30.71 +45.90.30.70 +45.90.30.7 +45.90.30.69 +45.90.30.68 +45.90.30.67 +45.90.30.66 +45.90.30.65 +45.90.30.64 +45.90.30.63 +45.90.30.62 +45.90.30.61 +45.90.30.60 +45.90.30.6 +45.90.30.59 +45.90.30.58 +45.90.30.57 +45.90.30.56 +45.90.30.55 +45.90.30.54 +45.90.30.53 +45.90.30.52 +45.90.30.51 +45.90.30.50 +45.90.30.5 +45.90.30.49 +45.90.30.48 +45.90.30.47 +45.90.30.46 +45.90.30.45 +45.90.30.44 +45.90.30.43 +45.90.30.42 +45.90.30.41 +45.90.30.40 +45.90.30.39 +45.90.30.38 +45.90.30.37 +45.90.30.36 +45.90.30.35 +45.90.30.34 +45.90.30.33 +45.90.30.32 +45.90.30.31 +45.90.30.30 +45.90.30.3 +45.90.30.29 +45.90.30.28 +45.90.30.27 +45.90.30.26 +45.90.30.254 +45.90.30.253 +45.90.30.252 +45.90.30.251 +45.90.30.250 +45.90.30.25 +45.90.30.249 +45.90.30.248 +45.90.30.247 +45.90.30.246 +45.90.30.245 +45.90.30.244 +45.90.30.243 +45.90.30.242 +45.90.30.241 +45.90.30.240 +45.90.30.24 +45.90.30.239 +45.90.30.238 +45.90.30.237 +45.90.30.236 +45.90.30.235 +45.90.30.234 +45.90.30.233 +45.90.30.232 +45.90.30.231 +45.90.30.230 +45.90.30.23 +45.90.30.229 +45.90.30.228 +45.90.30.227 +45.90.30.226 +45.90.30.225 +45.90.30.224 +45.90.30.223 +45.90.30.222 +45.90.30.221 +45.90.30.220 +45.90.30.22 +45.90.30.219 +45.90.30.218 +45.90.30.217 +45.90.30.216 +45.90.30.215 +45.90.30.214 +45.90.30.213 +45.90.30.212 +45.90.30.211 +45.90.30.210 +45.90.30.21 +45.90.30.209 +45.90.30.208 +45.90.30.207 +45.90.30.206 +45.90.30.205 +45.90.30.204 +45.90.30.203 +45.90.30.202 +45.90.30.201 +45.90.30.200 +45.90.30.20 +45.90.30.2 +45.90.30.199 +45.90.30.198 +45.90.30.197 +45.90.30.196 +45.90.30.195 +45.90.30.194 +45.90.30.193 +45.90.30.192 +45.90.30.191 +45.90.30.190 +45.90.30.19 +45.90.30.189 +45.90.30.188 +45.90.30.187 +45.90.30.186 +45.90.30.185 +45.90.30.184 +45.90.30.183 +45.90.30.182 +45.90.30.181 +45.90.30.180 +45.90.30.18 +45.90.30.179 +45.90.30.178 +45.90.30.177 +45.90.30.176 +45.90.30.175 +45.90.30.174 +45.90.30.173 +45.90.30.172 +45.90.30.171 +45.90.30.170 +45.90.30.17 +45.90.30.169 +45.90.30.168 +45.90.30.167 +45.90.30.166 +45.90.30.165 +45.90.30.164 +45.90.30.163 +45.90.30.162 +45.90.30.161 +45.90.30.160 +45.90.30.16 +45.90.30.159 +45.90.30.158 +45.90.30.157 +45.90.30.156 +45.90.30.155 +45.90.30.154 +45.90.30.153 +45.90.30.152 +45.90.30.151 +45.90.30.150 +45.90.30.15 +45.90.30.149 +45.90.30.148 +45.90.30.147 +45.90.30.146 +45.90.30.145 +45.90.30.144 +45.90.30.143 +45.90.30.142 +45.90.30.141 +45.90.30.140 +45.90.30.14 +45.90.30.139 +45.90.30.138 +45.90.30.137 +45.90.30.136 +45.90.30.135 +45.90.30.134 +45.90.30.133 +45.90.30.132 +45.90.30.131 +45.90.30.130 +45.90.30.13 +45.90.30.129 +45.90.30.128 +45.90.30.127 +45.90.30.126 +45.90.30.125 +45.90.30.124 +45.90.30.123 +45.90.30.122 +45.90.30.121 +45.90.30.120 +45.90.30.12 +45.90.30.119 +45.90.30.118 +45.90.30.117 +45.90.30.116 +45.90.30.115 +45.90.30.114 +45.90.30.113 +45.90.30.112 +45.90.30.111 +45.90.30.110 +45.90.30.11 +45.90.30.109 +45.90.30.108 +45.90.30.107 +45.90.30.106 +45.90.30.105 +45.90.30.104 +45.90.30.103 +45.90.30.102 +45.90.30.101 +45.90.30.100 +45.90.30.10 +45.90.30.1 +45.90.30.0 +45.90.29.99 +45.90.29.98 +45.90.29.97 +45.90.29.95 +45.90.29.94 +45.90.29.93 +45.90.29.91 +45.90.29.89 +45.90.29.87 +45.90.29.86 +45.90.29.84 +45.90.29.83 +45.90.29.82 +45.90.29.81 +45.90.29.77 +45.90.29.75 +45.90.29.74 +45.90.29.73 +45.90.29.72 +45.90.29.69 +45.90.29.68 +45.90.29.67 +45.90.29.66 +45.90.29.64 +45.90.29.63 +45.90.29.59 +45.90.29.57 +45.90.29.254 +45.90.29.253 +45.90.29.252 +45.90.29.251 +45.90.29.249 +45.90.29.248 +45.90.29.244 +45.90.29.242 +45.90.29.241 +45.90.29.240 +45.90.29.239 +45.90.29.236 +45.90.29.235 +45.90.29.233 +45.90.29.230 +45.90.29.226 +45.90.29.225 +45.90.29.223 +45.90.29.222 +45.90.29.221 +45.90.29.220 +45.90.29.22 +45.90.29.218 +45.90.29.216 +45.90.29.215 +45.90.29.214 +45.90.29.213 +45.90.29.212 +45.90.29.211 +45.90.29.209 +45.90.29.208 +45.90.29.207 +45.90.29.206 +45.90.29.204 +45.90.29.203 +45.90.29.201 +45.90.29.198 +45.90.29.196 +45.90.29.193 +45.90.29.189 +45.90.29.187 +45.90.29.186 +45.90.29.185 +45.90.29.184 +45.90.29.183 +45.90.29.182 +45.90.29.180 +45.90.29.179 +45.90.29.178 +45.90.29.177 +45.90.29.176 +45.90.29.173 +45.90.29.172 +45.90.29.170 +45.90.29.168 +45.90.29.166 +45.90.29.163 +45.90.29.162 +45.90.29.161 +45.90.29.159 +45.90.29.157 +45.90.29.156 +45.90.29.155 +45.90.29.154 +45.90.29.152 +45.90.29.151 +45.90.29.149 +45.90.29.148 +45.90.29.147 +45.90.29.146 +45.90.29.142 +45.90.29.139 +45.90.29.134 +45.90.29.133 +45.90.29.132 +45.90.29.130 +45.90.29.128 +45.90.29.125 +45.90.29.123 +45.90.29.122 +45.90.29.120 +45.90.29.119 +45.90.29.118 +45.90.29.117 +45.90.29.114 +45.90.29.112 +45.90.29.111 +45.90.29.108 +45.90.29.107 +45.90.29.103 +45.90.29.10 +45.90.28.96 +45.90.28.95 +45.90.28.91 +45.90.28.9 +45.90.28.89 +45.90.28.86 +45.90.28.85 +45.90.28.83 +45.90.28.82 +45.90.28.81 +45.90.28.80 +45.90.28.8 +45.90.28.78 +45.90.28.75 +45.90.28.74 +45.90.28.73 +45.90.28.71 +45.90.28.68 +45.90.28.67 +45.90.28.65 +45.90.28.64 +45.90.28.60 +45.90.28.6 +45.90.28.59 +45.90.28.58 +45.90.28.57 +45.90.28.52 +45.90.28.51 +45.90.28.50 +45.90.28.5 +45.90.28.44 +45.90.28.43 +45.90.28.42 +45.90.28.41 +45.90.28.36 +45.90.28.35 +45.90.28.33 +45.90.28.31 +45.90.28.30 +45.90.28.3 +45.90.28.26 +45.90.28.253 +45.90.28.252 +45.90.28.251 +45.90.28.250 +45.90.28.25 +45.90.28.247 +45.90.28.246 +45.90.28.245 +45.90.28.244 +45.90.28.241 +45.90.28.240 +45.90.28.238 +45.90.28.236 +45.90.28.234 +45.90.28.233 +45.90.28.232 +45.90.28.230 +45.90.28.229 +45.90.28.227 +45.90.28.226 +45.90.28.225 +45.90.28.224 +45.90.28.221 +45.90.28.22 +45.90.28.217 +45.90.28.216 +45.90.28.215 +45.90.28.214 +45.90.28.213 +45.90.28.211 +45.90.28.210 +45.90.28.21 +45.90.28.208 +45.90.28.202 +45.90.28.201 +45.90.28.200 +45.90.28.20 +45.90.28.197 +45.90.28.196 +45.90.28.195 +45.90.28.194 +45.90.28.192 +45.90.28.191 +45.90.28.19 +45.90.28.186 +45.90.28.184 +45.90.28.183 +45.90.28.182 +45.90.28.18 +45.90.28.177 +45.90.28.175 +45.90.28.174 +45.90.28.170 +45.90.28.169 +45.90.28.167 +45.90.28.165 +45.90.28.164 +45.90.28.161 +45.90.28.16 +45.90.28.159 +45.90.28.158 +45.90.28.157 +45.90.28.156 +45.90.28.154 +45.90.28.152 +45.90.28.151 +45.90.28.15 +45.90.28.149 +45.90.28.148 +45.90.28.147 +45.90.28.144 +45.90.28.143 +45.90.28.138 +45.90.28.136 +45.90.28.135 +45.90.28.131 +45.90.28.130 +45.90.28.13 +45.90.28.129 +45.90.28.128 +45.90.28.125 +45.90.28.124 +45.90.28.123 +45.90.28.121 +45.90.28.120 +45.90.28.12 +45.90.28.118 +45.90.28.117 +45.90.28.116 +45.90.28.113 +45.90.28.112 +45.90.28.111 +45.90.28.11 +45.90.28.109 +45.90.28.107 +45.90.28.106 +45.90.28.105 +45.90.28.104 +45.90.28.102 +45.87.235.187 +45.84.187.6 +45.84.169.143 +45.83.43.103 +45.83.40.44 +45.80.220.213 +45.77.236.229 +45.77.198.113 +45.76.91.140 +45.76.90.190 +45.76.35.187 +45.76.155.194 +45.76.115.6 +45.74.89.59 +45.73.0.118 +45.71.203.144 +45.7.228.232 +45.67.33.48 +45.65.225.220 +45.65.225.202 +45.65.225.196 +45.65.173.96 +45.65.173.105 +45.65.172.15 +45.64.173.74 +45.63.86.216 +45.63.105.236 +45.62.200.26 +45.6.136.231 +45.58.8.74 +45.58.38.166 +45.58.11.132 +45.55.112.11 +45.5.94.206 +45.5.94.178 +45.5.241.225 +45.4.97.49 +45.32.238.248 +45.32.202.187 +45.255.126.214 +45.249.122.82 +45.249.120.60 +45.248.78.99 +45.248.138.158 +45.239.239.130 +45.238.20.140 +45.236.169.151 +45.232.108.30 +45.231.221.1 +45.231.154.65 +45.230.252.238 +45.229.28.239 +45.228.32.139 +45.228.19.18 +45.228.181.252 +45.228.181.248 +45.226.50.27 +45.224.96.67 +45.224.22.25 +45.224.149.238 +45.224.148.188 +45.224.148.180 +45.224.148.177 +45.224.148.172 +45.224.148.169 +45.221.84.210 +45.191.104.200 +45.191.104.170 +45.186.99.45 +45.183.172.129 +45.180.114.21 +45.178.74.107 +45.177.74.202 +45.177.211.188 +45.176.96.42 +45.176.227.244 +45.175.237.66 +45.174.92.172 +45.174.70.10 +45.174.240.139 +45.173.140.190 +45.173.12.139 +45.172.89.11 +45.172.247.174 +45.172.222.69 +45.172.152.13 +45.171.180.81 +45.170.224.187 +45.168.133.38 +45.167.95.160 +45.167.92.103 +45.167.181.34 +45.167.112.2 +45.166.56.6 +45.164.175.174 +45.163.40.82 +45.163.220.58 +45.163.147.122 +45.162.100.248 +45.160.188.222 +45.159.18.99 +45.157.214.92 +45.152.120.51 +45.147.122.36 +45.143.120.250 +45.141.58.191 +45.14.48.185 +45.131.108.160 +45.129.19.20 +45.126.21.60 +45.125.211.11 +45.125.208.8 +45.123.201.235 +45.120.54.191 +45.118.217.3 +45.117.80.109 +45.117.63.81 +45.117.63.75 +45.117.63.46 +45.117.63.42 +45.117.63.26 +45.117.63.224 +45.117.63.221 +45.117.63.205 +45.117.63.192 +45.117.63.107 +45.117.63.105 +45.11.45.11 +43.252.88.130 +43.252.244.118 +43.251.253.202 +43.251.159.130 +43.249.65.207 +43.249.227.14 +43.249.112.98 +43.247.16.2 +43.240.226.242 +43.231.234.17 +43.230.201.109 +43.229.54.31 +43.228.229.130 +43.154.235.48 +43.132.205.118 +42.82.57.6 +42.61.73.150 +42.61.42.249 +42.61.19.26 +42.61.19.254 +42.61.107.9 +42.200.91.189 +42.200.196.122 +42.2.230.61 +42.116.255.185 +42.116.255.182 +42.116.255.181 +42.116.255.180 +42.116.255.179 +41.87.158.170 +41.87.158.142 +41.87.147.158 +41.87.144.169 +41.85.252.37 +41.84.143.206 +41.79.47.6 +41.76.241.135 +41.76.215.94 +41.76.101.174 +41.76.100.130 +41.72.216.234 +41.63.249.222 +41.63.244.89 +41.63.240.79 +41.60.129.80 +41.59.200.123 +41.59.197.135 +41.58.130.18 +41.57.120.177 +41.33.166.19 +41.231.62.166 +41.23.99.30 +41.23.99.247 +41.23.98.218 +41.23.253.80 +41.23.253.31 +41.23.253.134 +41.23.240.216 +41.23.240.123 +41.23.234.88 +41.23.234.37 +41.23.234.129 +41.23.234.114 +41.23.216.154 +41.23.216.150 +41.23.190.90 +41.23.185.218 +41.23.184.40 +41.23.184.250 +41.23.184.235 +41.23.184.193 +41.23.184.182 +41.23.184.151 +41.23.184.125 +41.23.184.111 +41.23.115.51 +41.23.114.62 +41.23.114.42 +41.23.114.41 +41.23.114.186 +41.23.113.90 +41.23.113.86 +41.23.113.210 +41.23.113.130 +41.228.23.167 +41.228.23.165 +41.228.23.137 +41.221.203.153 +41.221.192.167 +41.221.192.166 +41.218.90.154 +41.217.204.165 +41.216.159.6 +41.216.125.179 +41.215.248.143 +41.215.243.43 +41.214.150.122 +41.214.138.238 +41.211.125.242 +41.208.73.31 +41.205.195.162 +41.204.85.34 +41.203.87.157 +41.203.62.146 +41.191.220.77 +41.188.183.82 +41.185.21.252 +41.185.10.153 +41.184.254.94 +41.184.212.17 +41.184.174.151 +41.184.151.186 +41.174.182.214 +41.174.104.223 +41.173.23.150 +41.165.19.186 +41.162.120.85 +41.139.226.83 +41.139.206.39 +41.139.202.86 +41.139.147.86 +41.138.133.123 +41.138.101.251 +41.128.225.226 +41.111.204.186 +41.111.135.226 +41.0.170.154 +40.70.19.156 +40.127.12.40 +40.122.215.219 +40.114.69.73 +40.114.125.120 +4.79.244.118 +4.79.132.219 +4.7.220.241 +4.7.175.6 +4.59.81.39 +4.53.9.50 +4.53.160.75 +4.53.133.131 +4.4.211.46 +4.4.204.130 +4.4.178.34 +4.35.168.46 +4.34.37.186 +4.31.24.192 +4.31.189.137 +4.30.165.222 +4.26.151.242 +4.26.143.250 +4.26.143.210 +4.26.137.250 +4.2.2.6 +4.2.2.5 +4.2.2.4 +4.2.2.3 +4.2.2.2 +4.2.2.1 +4.16.3.86 +4.1.67.166 +4.1.67.145 +4.1.37.10 +4.1.240.35 +4.1.131.250 +4.0.0.53 +39.125.55.218 +39.110.226.206 +39.110.209.44 +39.110.201.118 +38.80.70.1 +38.74.61.221 +38.52.221.126 +38.51.48.158 +38.242.133.215 +38.142.225.131 +38.141.25.244 +38.135.49.152 +38.132.106.139 +38.125.161.1 +38.124.242.186 +38.122.24.30 +38.111.185.57 +37.99.224.30 +37.97.93.166 +37.9.57.142 +37.9.170.176 +37.79.202.114 +37.79.150.141 +37.59.83.245 +37.49.218.38 +37.44.45.238 +37.32.79.106 +37.29.96.183 +37.28.156.34 +37.26.170.60 +37.251.160.252 +37.25.36.181 +37.235.213.241 +37.235.174.43 +37.235.128.87 +37.232.70.214 +37.232.65.230 +37.232.47.14 +37.230.223.159 +37.228.93.107 +37.220.215.242 +37.208.127.214 +37.208.124.37 +37.202.49.2 +37.194.251.191 +37.187.55.202 +37.187.155.58 +37.187.133.168 +37.186.124.135 +37.186.115.160 +37.18.74.136 +37.18.29.33 +37.18.29.217 +37.18.26.2 +37.18.255.71 +37.18.105.149 +37.17.53.108 +37.157.177.192 +37.148.235.164 +37.128.95.86 +37.120.172.191 +37.114.62.26 +37.114.34.106 +37.111.142.222 +37.0.70.118 +36.50.50.50 +35.199.20.182 +35.167.25.37 +35.155.221.215 +34.94.84.140 +34.80.2.188 +34.76.9.111 +32.141.0.226 +31.7.37.37 +31.7.36.36 +31.46.42.149 +31.45.234.58 +31.44.247.221 +31.42.31.2 +31.42.179.75 +31.41.226.9 +31.40.97.86 +31.40.113.204 +31.40.100.86 +31.3.135.232 +31.28.113.156 +31.28.0.248 +31.221.33.250 +31.22.0.186 +31.210.69.163 +31.210.218.84 +31.206.52.78 +31.202.16.125 +31.192.98.158 +31.182.37.74 +31.182.36.26 +31.182.124.131 +31.173.244.149 +31.173.203.246 +31.173.165.18 +31.172.250.244 +31.172.133.253 +31.171.110.240 +31.170.113.126 +31.163.200.36 +31.163.192.96 +31.162.165.69 +31.156.55.106 +31.154.3.161 +31.148.48.205 +31.148.127.100 +31.146.83.206 +31.146.5.166 +31.146.216.174 +31.146.161.194 +31.135.92.50 +31.135.16.194 +31.128.75.116 +31.11.204.241 +31.11.132.161 +31.11.132.129 +31.0.162.2 +3.214.203.108 +3.20.144.103 +3.14.242.170 +3.139.141.82 +3.136.88.129 +27.72.58.161 +27.7.175.204 +27.54.166.204 +27.33.60.66 +27.32.252.176 +27.32.174.151 +27.174.14.71 +27.174.13.64 +27.174.10.206 +27.160.23.132 +27.147.48.26 +27.147.164.86 +27.135.204.65 +27.131.191.90 +27.131.162.193 +27.126.4.14 +27.126.155.4 +27.126.155.1 +27.124.6.225 +27.122.254.83 +27.113.241.18 +27.111.32.42 +27.110.245.97 +27.110.239.50 +27.110.232.161 +27.110.231.42 +27.110.185.10 +27.110.176.225 +27.110.163.162 +27.110.152.250 +27.110.148.225 +27.110.146.233 +27.110.139.78 +24.56.102.20 +24.56.100.20 +24.48.255.252 +24.39.127.26 +24.39.107.174 +24.37.245.42 +24.32.73.75 +24.244.219.147 +24.237.135.25 +24.230.77.131 +24.209.241.222 +24.182.239.198 +24.182.14.205 +24.172.82.94 +24.172.34.250 +24.152.48.66 +24.152.37.40 +24.149.93.51 +24.124.66.35 +24.123.203.111 +24.119.69.70 +24.116.92.101 +24.116.195.164 +24.105.237.170 +24.104.140.255 +24.104.140.229 +24.104.140.222 +24.104.140.212 +24.104.140.206 +24.104.140.201 +24.100.136.104 +23.92.212.178 +23.81.140.234 +23.59.249.67 +23.59.249.61 +23.59.249.226 +23.59.248.248 +23.59.248.226 +23.59.248.171 +23.59.248.145 +23.59.248.128 +23.56.161.88 +23.56.161.53 +23.56.161.5 +23.56.161.255 +23.56.161.246 +23.56.161.240 +23.56.161.23 +23.56.161.213 +23.56.161.18 +23.56.161.167 +23.56.161.147 +23.56.161.13 +23.56.161.12 +23.56.161.107 +23.56.160.79 +23.56.160.36 +23.56.160.252 +23.56.160.232 +23.56.160.213 +23.56.160.195 +23.56.160.15 +23.56.160.14 +23.56.160.116 +23.31.51.242 +23.30.232.82 +23.253.230.65 +23.25.183.97 +23.246.82.186 +23.242.22.236 +23.216.53.97 +23.216.53.83 +23.216.53.70 +23.216.53.58 +23.216.53.46 +23.216.53.34 +23.216.53.230 +23.216.53.229 +23.216.53.222 +23.216.53.194 +23.216.53.155 +23.216.53.152 +23.216.53.151 +23.216.53.15 +23.216.53.14 +23.216.53.128 +23.216.53.106 +23.216.52.80 +23.216.52.42 +23.216.52.34 +23.216.52.29 +23.216.52.228 +23.216.52.188 +23.216.52.163 +23.216.52.159 +23.216.52.154 +23.216.52.138 +23.216.52.128 +23.216.52.127 +23.216.52.102 +23.189.48.79 +23.115.93.66 +23.115.125.217 +23.111.184.9 +223.6.6.82 +223.6.6.81 +223.6.6.72 +223.6.6.68 +223.6.6.6 +223.6.6.56 +223.6.6.48 +223.6.6.46 +223.6.6.43 +223.6.6.41 +223.6.6.34 +223.6.6.253 +223.6.6.245 +223.6.6.241 +223.6.6.237 +223.6.6.232 +223.6.6.204 +223.6.6.199 +223.6.6.198 +223.6.6.196 +223.6.6.195 +223.6.6.191 +223.6.6.184 +223.6.6.183 +223.6.6.171 +223.6.6.170 +223.6.6.17 +223.6.6.169 +223.6.6.151 +223.6.6.150 +223.6.6.147 +223.6.6.143 +223.6.6.141 +223.6.6.139 +223.6.6.133 +223.6.6.127 +223.6.6.0 +223.5.5.93 +223.5.5.91 +223.5.5.84 +223.5.5.82 +223.5.5.79 +223.5.5.78 +223.5.5.70 +223.5.5.64 +223.5.5.59 +223.5.5.51 +223.5.5.50 +223.5.5.5 +223.5.5.47 +223.5.5.45 +223.5.5.41 +223.5.5.4 +223.5.5.38 +223.5.5.31 +223.5.5.252 +223.5.5.248 +223.5.5.241 +223.5.5.231 +223.5.5.23 +223.5.5.228 +223.5.5.224 +223.5.5.219 +223.5.5.204 +223.5.5.203 +223.5.5.200 +223.5.5.190 +223.5.5.19 +223.5.5.187 +223.5.5.170 +223.5.5.17 +223.5.5.168 +223.5.5.163 +223.5.5.159 +223.5.5.157 +223.5.5.151 +223.5.5.15 +223.5.5.148 +223.5.5.136 +223.5.5.13 +223.5.5.129 +223.5.5.124 +223.5.5.123 +223.5.5.112 +223.5.5.111 +223.5.5.110 +223.5.5.0 +223.29.52.106 +223.255.176.196 +223.255.176.195 +223.25.232.2 +223.22.243.241 +223.197.185.46 +223.171.73.95 +222.99.168.132 +222.98.43.84 +222.98.187.134 +222.98.115.251 +222.98.115.219 +222.97.189.7 +222.255.237.207 +222.255.206.232 +222.255.167.61 +222.255.144.115 +222.253.48.106 +222.252.14.239 +222.239.76.18 +222.239.221.250 +222.239.110.51 +222.235.176.47 +222.231.63.77 +222.231.61.196 +222.230.138.105 +222.228.150.127 +222.151.203.57 +222.127.7.54 +222.127.61.166 +222.127.25.226 +222.127.168.25 +222.127.151.3 +222.127.140.178 +222.122.82.24 +222.122.21.3 +222.122.166.141 +222.122.149.60 +222.121.55.99 +222.119.29.168 +222.118.225.33 +222.110.103.39 +222.107.91.130 +222.106.70.250 +222.104.22.126 +222.104.139.4 +222.103.228.194 +222.101.9.213 +222.101.9.212 +221.186.19.122 +221.163.194.3 +221.163.155.133 +221.162.247.60 +221.162.19.50 +221.162.19.157 +221.159.225.51 +221.156.92.194 +221.154.98.205 +221.154.146.204 +221.151.144.34 +221.151.144.12 +221.150.72.183 +221.143.46.205 +221.143.46.154 +221.143.216.58 +221.143.20.131 +221.139.13.130 +221.132.89.147 +221.125.130.98 +220.90.96.3 +220.90.201.8 +220.88.49.223 +220.83.87.2 +220.81.64.1 +220.78.162.11 +220.77.228.2 +220.73.162.155 +220.73.162.132 +220.73.139.8 +220.245.192.186 +220.241.227.4 +220.158.164.243 +220.149.56.99 +220.135.28.237 +220.135.20.47 +220.135.105.31 +220.134.170.52 +220.133.235.235 +220.132.88.194 +220.132.221.129 +220.130.55.196 +220.130.130.141 +220.130.12.85 +220.130.12.22 +220.130.10.250 +220.125.183.29 +220.119.186.100 +220.117.241.194 +220.110.160.2 +219.87.164.152 +219.87.162.222 +219.85.163.52 +219.252.2.100 +219.252.1.100 +219.252.0.1 +219.250.36.130 +219.166.235.130 +219.166.16.122 +219.166.11.146 +219.166.109.122 +219.163.11.226 +219.127.86.6 +219.118.161.57 +219.117.237.252 +219.106.241.34 +219.103.63.145 +218.44.251.210 +218.44.234.170 +218.44.187.8 +218.38.136.62 +218.36.68.254 +218.32.222.5 +218.255.18.2 +218.236.85.1 +218.236.49.18 +218.236.241.132 +218.235.251.3 +218.235.251.2 +218.234.23.53 +218.234.19.131 +218.234.18.181 +218.223.34.93 +218.216.72.194 +218.208.115.68 +218.208.114.28 +218.189.82.194 +218.188.52.18 +218.188.223.182 +218.188.217.219 +218.188.210.196 +218.161.68.137 +218.161.40.163 +218.159.112.240 +218.159.112.239 +218.159.100.172 +218.158.8.12 +218.158.8.11 +218.156.163.51 +218.152.207.197 +218.151.83.209 +218.151.134.214 +218.149.84.50 +218.146.34.200 +218.146.255.235 +218.145.31.139 +218.145.31.132 +218.102.13.133 +217.97.139.128 +217.79.247.238 +217.79.18.121 +217.78.177.110 +217.76.33.82 +217.73.31.10 +217.7.81.136 +217.7.80.40 +217.7.63.1 +217.69.228.142 +217.69.169.25 +217.67.190.98 +217.67.184.254 +217.65.3.81 +217.64.148.33 +217.64.136.254 +217.40.46.142 +217.38.149.236 +217.33.153.162 +217.30.250.167 +217.30.163.159 +217.28.153.27 +217.27.219.122 +217.27.148.125 +217.27.123.226 +217.27.116.145 +217.27.116.144 +217.26.19.166 +217.26.171.146 +217.25.237.141 +217.25.228.98 +217.244.13.14 +217.24.190.106 +217.23.199.77 +217.23.146.60 +217.223.161.177 +217.22.164.126 +217.218.155.155 +217.218.127.127 +217.21.43.3 +217.196.23.122 +217.194.215.91 +217.194.213.253 +217.19.153.18 +217.182.200.38 +217.182.192.218 +217.182.192.123 +217.182.158.123 +217.182.137.224 +217.182.112.181 +217.18.206.22 +217.18.206.12 +217.174.14.101 +217.173.202.194 +217.170.128.27 +217.17.48.11 +217.17.41.146 +217.17.34.68 +217.17.34.10 +217.168.130.197 +217.164.255.37 +217.164.255.35 +217.160.70.42 +217.16.79.246 +217.150.58.17 +217.150.35.129 +217.15.202.106 +217.15.146.124 +217.145.86.12 +217.145.54.100 +217.145.49.185 +217.14.178.67 +217.14.143.18 +217.13.80.252 +217.13.211.2 +217.12.214.244 +217.119.160.99 +217.119.126.221 +217.119.121.146 +217.116.61.153 +217.114.181.99 +217.113.195.244 +217.113.125.26 +217.111.39.134 +217.111.148.201 +217.111.114.4 +217.109.45.177 +217.107.102.103 +217.100.187.236 +217.0.43.50 +216.98.4.252 +216.98.109.133 +216.85.168.28 +216.80.113.2 +216.75.21.70 +216.55.99.220 +216.55.100.220 +216.54.204.186 +216.47.224.66 +216.33.116.102 +216.27.175.2 +216.254.95.2 +216.250.141.137 +216.241.25.83 +216.239.34.40 +216.239.32.40 +216.238.98.60 +216.231.41.2 +216.229.0.25 +216.228.104.3 +216.222.152.16 +216.222.134.250 +216.218.31.74 +216.218.245.200 +216.218.215.117 +216.21.169.147 +216.21.129.22 +216.21.128.22 +216.207.95.191 +216.207.40.128 +216.199.54.9 +216.199.46.11 +216.197.192.203 +216.194.29.204 +216.194.28.69 +216.194.28.42 +216.194.28.33 +216.175.203.51 +216.171.253.13 +216.171.184.243 +216.170.153.146 +216.169.160.23 +216.169.160.2 +216.165.129.158 +216.165.129.157 +216.165.128.161 +216.160.246.151 +216.160.226.49 +216.16.136.146 +216.155.92.110 +216.155.91.251 +216.152.244.77 +216.146.36.36 +216.146.35.35 +216.137.13.22 +216.136.95.83 +216.136.95.82 +216.136.95.67 +216.136.95.35 +216.136.95.34 +216.136.95.3 +216.136.95.2 +216.136.95.19 +216.136.82.113 +216.136.33.82 +216.135.79.125 +216.135.79.118 +216.135.79.115 +216.135.0.10 +216.130.230.144 +216.126.220.4 +216.120.247.231 +216.12.255.2 +216.12.255.1 +216.12.254.250 +216.108.238.70 +213.91.195.139 +213.91.143.246 +213.90.6.14 +213.85.168.57 +213.85.143.246 +213.79.89.138 +213.76.155.132 +213.76.114.107 +213.74.223.74 +213.74.195.52 +213.73.3.164 +213.68.194.51 +213.6.77.30 +213.6.20.57 +213.59.128.41 +213.57.91.138 +213.5.71.47 +213.5.120.3 +213.4.82.7 +213.39.74.43 +213.34.140.206 +213.33.237.106 +213.33.158.134 +213.32.252.91 +213.3.42.193 +213.26.25.3 +213.251.61.42 +213.251.57.217 +213.251.48.193 +213.25.71.2 +213.249.20.207 +213.248.45.60 +213.246.55.57 +213.241.67.226 +213.241.37.244 +213.241.37.234 +213.240.24.242 +213.234.30.118 +213.234.17.74 +213.230.97.216 +213.230.91.58 +213.230.67.186 +213.230.125.148 +213.230.111.191 +213.230.108.161 +213.226.186.10 +213.226.11.210 +213.223.36.242 +213.222.25.130 +213.221.37.215 +213.217.16.106 +213.216.64.102 +213.214.30.78 +213.212.145.1 +213.211.50.2 +213.211.50.1 +213.211.33.58 +213.21.225.67 +213.208.183.59 +213.207.46.220 +213.207.46.218 +213.207.187.249 +213.207.158.201 +213.207.141.97 +213.207.132.189 +213.207.131.97 +213.205.80.82 +213.202.255.96 +213.201.78.48 +213.200.218.61 +213.200.211.110 +213.194.123.26 +213.193.122.202 +213.191.222.21 +213.180.53.103 +213.180.182.246 +213.178.54.62 +213.176.242.166 +213.171.205.61 +213.171.192.34 +213.166.247.100 +213.163.127.229 +213.163.120.82 +213.16.110.117 +213.16.110.112 +213.157.57.133 +213.157.50.130 +213.156.44.184 +213.155.160.240 +213.154.80.203 +213.154.18.59 +213.153.223.21 +213.149.177.25 +213.145.3.44 +213.145.151.190 +213.145.137.102 +213.142.215.190 +213.140.41.194 +213.14.11.162 +213.136.88.86 +213.136.84.5 +213.136.75.64 +213.136.71.68 +213.136.36.155 +213.133.91.249 +213.133.116.14 +213.131.40.230 +213.129.114.98 +213.124.39.21 +213.115.174.71 +213.111.121.178 +213.108.174.218 +213.108.172.182 +213.108.160.205 +212.98.146.97 +212.97.32.2 +212.97.129.35 +212.97.129.34 +212.96.1.70 +212.93.29.112 +212.93.121.72 +212.93.121.60 +212.93.118.21 +212.93.117.80 +212.93.111.192 +212.92.200.165 +212.91.246.11 +212.87.243.194 +212.87.243.120 +212.83.168.230 +212.78.216.109 +212.74.192.40 +212.73.221.107 +212.73.221.104 +212.73.198.88 +212.73.140.66 +212.73.138.38 +212.73.128.182 +212.72.130.21 +212.72.130.20 +212.70.156.160 +212.67.71.84 +212.65.208.193 +212.58.12.245 +212.56.212.194 +212.56.209.18 +212.5.221.202 +212.5.128.228 +212.48.52.94 +212.48.153.245 +212.48.152.160 +212.47.12.21 +212.46.205.34 +212.44.128.18 +212.41.1.87 +212.4.138.238 +212.4.121.10 +212.39.106.154 +212.38.26.132 +212.38.2.130 +212.34.245.179 +212.34.232.70 +212.33.190.215 +212.32.252.131 +212.3.223.82 +212.3.154.18 +212.3.131.158 +212.3.101.130 +212.26.7.10 +212.26.144.58 +212.251.32.202 +212.247.80.50 +212.247.176.202 +212.247.102.181 +212.244.210.91 +212.237.127.127 +212.237.125.216 +212.236.18.126 +212.233.125.20 +212.230.255.129 +212.230.255.1 +212.225.218.80 +212.224.71.71 +212.220.115.146 +212.22.128.20 +212.210.2.2 +212.210.2.15 +212.210.2.14 +212.203.109.67 +212.200.34.37 +212.200.169.182 +212.200.115.156 +212.2.230.69 +212.199.114.12 +212.199.114.11 +212.191.1.46 +212.186.200.123 +212.185.196.10 +212.185.180.3 +212.184.191.2 +212.184.191.193 +212.184.191.100 +212.182.95.26 +212.160.242.185 +212.160.162.67 +212.160.147.19 +212.154.131.115 +212.150.211.99 +212.150.211.114 +212.146.97.154 +212.143.39.243 +212.14.18.110 +212.139.214.147 +212.13.82.7 +212.126.102.138 +212.124.35.25 +212.122.53.126 +212.122.52.11 +212.122.4.2 +212.121.229.233 +212.12.28.30 +212.12.28.126 +212.12.27.29 +212.12.18.113 +212.12.15.128 +212.12.14.122 +212.118.12.22 +212.118.0.2 +212.118.0.1 +212.117.148.54 +212.116.122.201 +212.113.0.3 +212.112.180.131 +212.111.192.246 +212.105.78.7 +212.105.78.6 +212.105.78.3 +212.104.43.3 +212.104.43.2 +212.102.46.89 +212.102.46.84 +212.100.159.226 +212.0.208.189 +211.9.37.1 +211.76.112.60 +211.76.112.59 +211.75.76.173 +211.75.48.98 +211.75.165.163 +211.75.15.177 +211.75.139.191 +211.72.124.193 +211.63.16.5 +211.62.56.125 +211.60.96.137 +211.60.199.66 +211.59.242.36 +211.54.182.7 +211.53.64.18 +211.53.40.133 +211.51.150.131 +211.5.228.210 +211.5.228.209 +211.5.209.194 +211.47.188.226 +211.46.120.1 +211.44.250.215 +211.41.128.71 +211.40.52.5 +211.40.52.2 +211.35.76.100 +211.35.20.27 +211.34.118.2 +211.33.136.94 +211.33.121.235 +211.254.92.254 +211.252.106.130 +211.251.24.241 +211.245.239.40 +211.24.100.198 +211.239.127.57 +211.236.20.2 +211.234.111.74 +211.233.65.13 +211.233.50.4 +211.233.50.3 +211.232.158.254 +211.232.116.3 +211.232.110.114 +211.23.246.116 +211.23.233.180 +211.23.144.240 +211.228.195.162 +211.228.100.115 +211.226.88.54 +211.226.180.60 +211.226.143.43 +211.223.193.200 +211.222.204.5 +211.220.224.12 +211.220.194.152 +211.22.52.82 +211.22.146.115 +211.219.86.1 +211.219.83.226 +211.217.195.226 +211.215.23.77 +211.21.220.171 +211.21.138.151 +211.21.133.123 +211.203.71.5 +211.203.18.17 +211.202.2.28 +211.20.146.109 +211.199.149.131 +211.198.50.2 +211.194.164.158 +211.194.140.3 +211.193.204.3 +211.193.134.134 +211.193.106.59 +211.192.139.216 +211.188.180.22 +211.188.180.21 +211.184.196.130 +211.182.233.3 +211.182.233.2 +211.181.222.135 +211.177.101.116 +211.170.243.20 +211.170.122.12 +211.169.2.100 +211.129.155.175 +211.126.202.126 +211.123.77.97 +211.121.135.181 +211.12.254.10 +211.119.47.110 +211.116.138.5 +211.115.66.175 +211.115.194.5 +211.115.194.4 +211.115.194.3 +211.115.194.2 +211.115.194.1 +211.114.53.85 +211.114.145.7 +211.111.172.72 +211.111.172.71 +211.110.10.36 +211.105.7.5 +211.104.6.1 +211.10.168.1 +210.99.77.210 +210.99.180.194 +210.98.146.2 +210.94.0.73 +210.94.0.7 +210.93.0.2 +210.91.32.187 +210.91.32.171 +210.90.197.1 +210.87.253.60 +210.87.250.156 +210.87.250.154 +210.8.28.194 +210.79.34.137 +210.61.97.241 +210.61.48.168 +210.59.209.19 +210.56.14.146 +210.5.98.46 +210.5.98.130 +210.5.92.6 +210.5.89.193 +210.5.87.146 +210.5.72.33 +210.5.72.30 +210.5.72.24 +210.5.72.21 +210.5.72.2 +210.5.72.12 +210.5.67.241 +210.5.64.10 +210.5.56.146 +210.5.56.145 +210.5.125.6 +210.5.124.161 +210.5.104.28 +210.5.101.242 +210.3.252.27 +210.3.239.131 +210.3.143.206 +210.3.138.237 +210.254.50.20 +210.251.126.97 +210.245.87.16 +210.245.8.9 +210.245.31.102 +210.245.21.123 +210.245.21.102 +210.245.111.195 +210.243.121.155 +210.233.117.1 +210.230.193.68 +210.230.193.66 +210.23.129.34 +210.228.80.253 +210.224.86.126 +210.222.176.66 +210.220.163.82 +210.220.16.6 +210.220.16.2 +210.219.173.18 +210.216.217.254 +210.213.75.161 +210.213.74.41 +210.213.74.129 +210.213.65.118 +210.213.242.242 +210.213.218.233 +210.213.213.206 +210.213.192.241 +210.213.126.161 +210.212.210.91 +210.211.20.225 +210.211.16.230 +210.207.236.2 +210.206.183.39 +210.206.162.2 +210.205.247.4 +210.2.138.68 +210.196.68.174 +210.196.251.66 +210.193.195.118 +210.190.25.80 +210.190.105.66 +210.19.157.131 +210.187.50.146 +210.181.4.25 +210.181.1.24 +210.180.98.69 +210.18.214.38 +210.178.75.16 +210.176.210.4 +210.172.81.186 +210.172.23.114 +210.172.103.18 +210.172.1.251 +210.171.38.41 +210.168.248.242 +210.163.158.224 +210.162.99.34 +210.161.166.69 +210.16.67.138 +210.141.99.89 +210.14.13.209 +210.14.12.50 +210.14.12.22 +210.134.0.130 +210.127.61.151 +210.127.253.120 +210.125.136.7 +210.123.136.39 +210.121.229.1 +210.114.225.223 +210.114.175.182 +210.107.84.3 +210.107.84.2 +210.107.239.132 +210.107.239.131 +210.104.21.1 +210.104.203.3 +210.104.203.2 +210.103.63.70 +210.102.252.3 +210.102.248.37 +210.100.192.2 +210.10.238.133 +210.1.94.54 +210.1.88.137 +210.1.86.1 +210.1.83.201 +210.1.81.40 +210.0.255.216 +210.0.255.144 +210.0.128.251 +210.0.128.250 +210.0.128.242 +210.0.128.241 +209.90.160.221 +209.87.64.70 +209.84.253.11 +209.80.159.21 +209.51.161.14 +209.45.48.205 +209.37.92.138 +209.33.101.220 +209.3.124.160 +209.253.113.2 +209.253.113.10 +209.250.128.6 +209.244.0.53 +209.244.0.4 +209.244.0.3 +209.239.11.98 +209.234.196.12 +209.232.116.6 +209.23.9.76 +209.222.125.10 +209.216.160.2 +209.216.160.131 +209.216.129.4 +209.209.217.168 +209.201.40.112 +209.201.3.13 +209.181.232.128 +209.164.189.56 +209.164.189.55 +209.164.189.54 +209.160.252.138 +209.159.152.77 +209.150.154.1 +209.144.50.123 +209.143.0.10 +209.137.239.50 +209.130.139.2 +209.130.136.2 +209.126.106.217 +208.99.243.17 +208.93.60.20 +208.93.158.1 +208.91.112.53 +208.91.112.52 +208.91.112.220 +208.89.96.20 +208.89.96.11 +208.89.131.200 +208.87.98.94 +208.84.156.150 +208.84.156.137 +208.79.56.204 +208.74.71.20 +208.74.69.5 +208.72.160.67 +208.72.120.204 +208.70.255.220 +208.68.92.38 +208.67.29.251 +208.67.222.222 +208.67.222.220 +208.67.222.2 +208.67.220.222 +208.67.220.220 +208.67.220.2 +208.67.220.120 +208.48.253.106 +208.46.96.168 +208.38.65.35 +208.38.26.33 +208.254.148.100 +208.253.57.5 +208.249.244.2 +208.249.244.1 +208.180.0.251 +208.180.0.250 +208.157.146.14 +208.125.139.6 +208.123.219.155 +208.118.69.226 +208.113.128.45 +208.111.1.2 +208.111.1.1 +208.103.33.22 +208.100.13.10 +207.99.46.118 +207.99.12.105 +207.91.5.32 +207.59.153.242 +207.53.228.67 +207.254.17.115 +207.248.57.11 +207.248.236.85 +207.248.224.72 +207.248.224.71 +207.248.111.242 +207.246.98.123 +207.243.201.24 +207.243.150.98 +207.236.186.187 +207.230.75.50 +207.230.65.98 +207.2.105.196 +207.195.35.143 +207.191.51.250 +207.191.50.250 +207.191.50.10 +207.191.1.10 +207.180.217.214 +207.180.212.30 +207.177.83.1 +207.177.61.103 +207.172.157.201 +207.17.190.5 +207.162.218.5 +207.159.121.241 +207.159.104.10 +207.154.212.113 +207.144.37.198 +207.140.152.71 +207.109.67.32 +207.108.84.1 +207.108.220.16 +206.84.63.62 +206.81.207.41 +206.81.195.82 +206.78.19.8 +206.51.143.55 +206.41.4.36 +206.253.33.131 +206.253.33.130 +206.252.232.95 +206.222.97.94 +206.222.97.82 +206.222.97.50 +206.222.107.70 +206.222.107.38 +206.222.107.34 +206.222.107.130 +206.221.178.134 +206.196.97.89 +206.189.92.228 +206.189.238.147 +206.180.165.149 +206.170.79.50 +206.169.200.135 +206.169.151.40 +206.165.6.12 +206.165.6.11 +206.15.226.2 +206.138.18.20 +206.125.134.19 +205.243.120.83 +205.233.14.44 +205.171.3.66 +205.171.3.65 +205.171.3.26 +205.171.202.66 +205.171.202.166 +205.171.2.65 +205.171.2.25 +205.170.181.146 +205.169.93.95 +205.168.62.216 +205.168.234.48 +205.168.155.0 +204.95.160.2 +204.9.215.185 +204.9.144.70 +204.9.109.57 +204.88.31.246 +204.85.36.31 +204.74.67.99 +204.74.67.98 +204.74.67.97 +204.74.67.94 +204.74.67.92 +204.74.67.89 +204.74.67.88 +204.74.67.87 +204.74.67.86 +204.74.67.85 +204.74.67.83 +204.74.67.8 +204.74.67.78 +204.74.67.77 +204.74.67.74 +204.74.67.73 +204.74.67.70 +204.74.67.7 +204.74.67.69 +204.74.67.67 +204.74.67.66 +204.74.67.65 +204.74.67.64 +204.74.67.63 +204.74.67.62 +204.74.67.61 +204.74.67.58 +204.74.67.57 +204.74.67.56 +204.74.67.52 +204.74.67.51 +204.74.67.50 +204.74.67.5 +204.74.67.48 +204.74.67.46 +204.74.67.45 +204.74.67.44 +204.74.67.43 +204.74.67.42 +204.74.67.40 +204.74.67.37 +204.74.67.34 +204.74.67.31 +204.74.67.30 +204.74.67.3 +204.74.67.28 +204.74.67.26 +204.74.67.253 +204.74.67.252 +204.74.67.251 +204.74.67.250 +204.74.67.247 +204.74.67.246 +204.74.67.245 +204.74.67.244 +204.74.67.243 +204.74.67.241 +204.74.67.239 +204.74.67.236 +204.74.67.235 +204.74.67.233 +204.74.67.232 +204.74.67.231 +204.74.67.229 +204.74.67.226 +204.74.67.225 +204.74.67.224 +204.74.67.223 +204.74.67.221 +204.74.67.220 +204.74.67.219 +204.74.67.217 +204.74.67.215 +204.74.67.213 +204.74.67.212 +204.74.67.210 +204.74.67.209 +204.74.67.207 +204.74.67.205 +204.74.67.203 +204.74.67.201 +204.74.67.20 +204.74.67.2 +204.74.67.198 +204.74.67.197 +204.74.67.195 +204.74.67.194 +204.74.67.192 +204.74.67.19 +204.74.67.189 +204.74.67.187 +204.74.67.185 +204.74.67.184 +204.74.67.183 +204.74.67.181 +204.74.67.180 +204.74.67.18 +204.74.67.176 +204.74.67.174 +204.74.67.173 +204.74.67.17 +204.74.67.169 +204.74.67.167 +204.74.67.166 +204.74.67.165 +204.74.67.163 +204.74.67.162 +204.74.67.161 +204.74.67.160 +204.74.67.159 +204.74.67.154 +204.74.67.152 +204.74.67.151 +204.74.67.149 +204.74.67.146 +204.74.67.145 +204.74.67.144 +204.74.67.141 +204.74.67.139 +204.74.67.137 +204.74.67.135 +204.74.67.134 +204.74.67.132 +204.74.67.130 +204.74.67.13 +204.74.67.127 +204.74.67.125 +204.74.67.124 +204.74.67.122 +204.74.67.121 +204.74.67.120 +204.74.67.12 +204.74.67.118 +204.74.67.117 +204.74.67.116 +204.74.67.114 +204.74.67.111 +204.74.67.110 +204.74.67.11 +204.74.67.107 +204.74.67.106 +204.74.67.105 +204.74.67.102 +204.74.67.101 +204.74.67.10 +204.74.67.1 +204.74.66.99 +204.74.66.98 +204.74.66.97 +204.74.66.95 +204.74.66.93 +204.74.66.92 +204.74.66.91 +204.74.66.90 +204.74.66.83 +204.74.66.82 +204.74.66.80 +204.74.66.8 +204.74.66.77 +204.74.66.76 +204.74.66.73 +204.74.66.72 +204.74.66.69 +204.74.66.68 +204.74.66.66 +204.74.66.64 +204.74.66.61 +204.74.66.58 +204.74.66.57 +204.74.66.56 +204.74.66.55 +204.74.66.54 +204.74.66.53 +204.74.66.52 +204.74.66.50 +204.74.66.49 +204.74.66.48 +204.74.66.47 +204.74.66.45 +204.74.66.41 +204.74.66.40 +204.74.66.38 +204.74.66.36 +204.74.66.35 +204.74.66.34 +204.74.66.33 +204.74.66.32 +204.74.66.31 +204.74.66.27 +204.74.66.26 +204.74.66.253 +204.74.66.251 +204.74.66.250 +204.74.66.247 +204.74.66.246 +204.74.66.245 +204.74.66.242 +204.74.66.241 +204.74.66.24 +204.74.66.239 +204.74.66.238 +204.74.66.237 +204.74.66.233 +204.74.66.23 +204.74.66.229 +204.74.66.227 +204.74.66.225 +204.74.66.224 +204.74.66.223 +204.74.66.222 +204.74.66.22 +204.74.66.215 +204.74.66.214 +204.74.66.213 +204.74.66.210 +204.74.66.208 +204.74.66.206 +204.74.66.204 +204.74.66.200 +204.74.66.2 +204.74.66.199 +204.74.66.198 +204.74.66.197 +204.74.66.195 +204.74.66.194 +204.74.66.193 +204.74.66.192 +204.74.66.191 +204.74.66.19 +204.74.66.189 +204.74.66.187 +204.74.66.186 +204.74.66.185 +204.74.66.183 +204.74.66.182 +204.74.66.18 +204.74.66.179 +204.74.66.178 +204.74.66.177 +204.74.66.176 +204.74.66.175 +204.74.66.174 +204.74.66.173 +204.74.66.172 +204.74.66.171 +204.74.66.170 +204.74.66.165 +204.74.66.164 +204.74.66.161 +204.74.66.160 +204.74.66.159 +204.74.66.158 +204.74.66.157 +204.74.66.156 +204.74.66.155 +204.74.66.152 +204.74.66.151 +204.74.66.149 +204.74.66.147 +204.74.66.146 +204.74.66.144 +204.74.66.143 +204.74.66.139 +204.74.66.138 +204.74.66.137 +204.74.66.134 +204.74.66.133 +204.74.66.131 +204.74.66.130 +204.74.66.13 +204.74.66.129 +204.74.66.128 +204.74.66.127 +204.74.66.126 +204.74.66.122 +204.74.66.121 +204.74.66.117 +204.74.66.115 +204.74.66.114 +204.74.66.113 +204.74.66.112 +204.74.66.110 +204.74.66.11 +204.74.66.109 +204.74.66.107 +204.74.66.106 +204.74.66.105 +204.74.66.104 +204.74.66.103 +204.74.66.102 +204.74.66.10 +204.74.66.1 +204.74.115.99 +204.74.115.98 +204.74.115.91 +204.74.115.90 +204.74.115.9 +204.74.115.88 +204.74.115.87 +204.74.115.86 +204.74.115.84 +204.74.115.83 +204.74.115.82 +204.74.115.81 +204.74.115.80 +204.74.115.8 +204.74.115.79 +204.74.115.78 +204.74.115.77 +204.74.115.76 +204.74.115.72 +204.74.115.71 +204.74.115.70 +204.74.115.69 +204.74.115.68 +204.74.115.67 +204.74.115.65 +204.74.115.63 +204.74.115.6 +204.74.115.59 +204.74.115.55 +204.74.115.54 +204.74.115.52 +204.74.115.5 +204.74.115.49 +204.74.115.45 +204.74.115.44 +204.74.115.42 +204.74.115.41 +204.74.115.40 +204.74.115.4 +204.74.115.39 +204.74.115.38 +204.74.115.37 +204.74.115.36 +204.74.115.35 +204.74.115.34 +204.74.115.33 +204.74.115.32 +204.74.115.31 +204.74.115.3 +204.74.115.28 +204.74.115.27 +204.74.115.26 +204.74.115.254 +204.74.115.25 +204.74.115.248 +204.74.115.246 +204.74.115.245 +204.74.115.244 +204.74.115.243 +204.74.115.242 +204.74.115.240 +204.74.115.24 +204.74.115.239 +204.74.115.236 +204.74.115.234 +204.74.115.233 +204.74.115.231 +204.74.115.229 +204.74.115.225 +204.74.115.223 +204.74.115.221 +204.74.115.220 +204.74.115.219 +204.74.115.216 +204.74.115.213 +204.74.115.212 +204.74.115.211 +204.74.115.210 +204.74.115.21 +204.74.115.208 +204.74.115.207 +204.74.115.205 +204.74.115.204 +204.74.115.203 +204.74.115.201 +204.74.115.2 +204.74.115.199 +204.74.115.197 +204.74.115.196 +204.74.115.193 +204.74.115.192 +204.74.115.190 +204.74.115.189 +204.74.115.186 +204.74.115.184 +204.74.115.182 +204.74.115.181 +204.74.115.18 +204.74.115.179 +204.74.115.176 +204.74.115.174 +204.74.115.173 +204.74.115.171 +204.74.115.169 +204.74.115.168 +204.74.115.166 +204.74.115.165 +204.74.115.164 +204.74.115.163 +204.74.115.161 +204.74.115.160 +204.74.115.159 +204.74.115.158 +204.74.115.157 +204.74.115.156 +204.74.115.152 +204.74.115.151 +204.74.115.150 +204.74.115.15 +204.74.115.149 +204.74.115.148 +204.74.115.147 +204.74.115.145 +204.74.115.144 +204.74.115.141 +204.74.115.140 +204.74.115.138 +204.74.115.136 +204.74.115.134 +204.74.115.133 +204.74.115.132 +204.74.115.131 +204.74.115.128 +204.74.115.127 +204.74.115.124 +204.74.115.120 +204.74.115.118 +204.74.115.116 +204.74.115.115 +204.74.115.114 +204.74.115.112 +204.74.115.11 +204.74.115.109 +204.74.115.108 +204.74.115.106 +204.74.115.104 +204.74.114.98 +204.74.114.97 +204.74.114.96 +204.74.114.95 +204.74.114.93 +204.74.114.90 +204.74.114.9 +204.74.114.89 +204.74.114.86 +204.74.114.78 +204.74.114.77 +204.74.114.69 +204.74.114.64 +204.74.114.62 +204.74.114.61 +204.74.114.58 +204.74.114.55 +204.74.114.54 +204.74.114.51 +204.74.114.48 +204.74.114.47 +204.74.114.46 +204.74.114.45 +204.74.114.44 +204.74.114.43 +204.74.114.39 +204.74.114.34 +204.74.114.33 +204.74.114.30 +204.74.114.3 +204.74.114.28 +204.74.114.253 +204.74.114.252 +204.74.114.246 +204.74.114.243 +204.74.114.241 +204.74.114.238 +204.74.114.236 +204.74.114.235 +204.74.114.231 +204.74.114.229 +204.74.114.227 +204.74.114.224 +204.74.114.223 +204.74.114.222 +204.74.114.221 +204.74.114.218 +204.74.114.213 +204.74.114.205 +204.74.114.202 +204.74.114.201 +204.74.114.200 +204.74.114.20 +204.74.114.199 +204.74.114.198 +204.74.114.197 +204.74.114.190 +204.74.114.187 +204.74.114.180 +204.74.114.178 +204.74.114.176 +204.74.114.175 +204.74.114.174 +204.74.114.172 +204.74.114.170 +204.74.114.17 +204.74.114.169 +204.74.114.165 +204.74.114.163 +204.74.114.160 +204.74.114.16 +204.74.114.159 +204.74.114.155 +204.74.114.152 +204.74.114.151 +204.74.114.150 +204.74.114.147 +204.74.114.144 +204.74.114.143 +204.74.114.142 +204.74.114.141 +204.74.114.140 +204.74.114.14 +204.74.114.139 +204.74.114.135 +204.74.114.134 +204.74.114.132 +204.74.114.129 +204.74.114.126 +204.74.114.125 +204.74.114.124 +204.74.114.123 +204.74.114.120 +204.74.114.117 +204.74.114.116 +204.74.114.113 +204.74.114.111 +204.74.114.110 +204.74.114.11 +204.74.114.108 +204.74.114.105 +204.74.114.104 +204.74.111.99 +204.74.111.96 +204.74.111.95 +204.74.111.94 +204.74.111.93 +204.74.111.91 +204.74.111.90 +204.74.111.9 +204.74.111.87 +204.74.111.86 +204.74.111.85 +204.74.111.82 +204.74.111.81 +204.74.111.80 +204.74.111.8 +204.74.111.79 +204.74.111.74 +204.74.111.73 +204.74.111.71 +204.74.111.68 +204.74.111.65 +204.74.111.64 +204.74.111.61 +204.74.111.60 +204.74.111.59 +204.74.111.58 +204.74.111.57 +204.74.111.56 +204.74.111.55 +204.74.111.54 +204.74.111.53 +204.74.111.52 +204.74.111.5 +204.74.111.49 +204.74.111.47 +204.74.111.46 +204.74.111.45 +204.74.111.41 +204.74.111.40 +204.74.111.38 +204.74.111.37 +204.74.111.36 +204.74.111.35 +204.74.111.33 +204.74.111.32 +204.74.111.3 +204.74.111.29 +204.74.111.27 +204.74.111.253 +204.74.111.252 +204.74.111.251 +204.74.111.250 +204.74.111.249 +204.74.111.247 +204.74.111.246 +204.74.111.245 +204.74.111.241 +204.74.111.238 +204.74.111.236 +204.74.111.235 +204.74.111.234 +204.74.111.233 +204.74.111.232 +204.74.111.23 +204.74.111.229 +204.74.111.227 +204.74.111.225 +204.74.111.223 +204.74.111.220 +204.74.111.22 +204.74.111.219 +204.74.111.218 +204.74.111.217 +204.74.111.216 +204.74.111.215 +204.74.111.214 +204.74.111.212 +204.74.111.211 +204.74.111.21 +204.74.111.208 +204.74.111.207 +204.74.111.205 +204.74.111.204 +204.74.111.203 +204.74.111.20 +204.74.111.2 +204.74.111.199 +204.74.111.197 +204.74.111.196 +204.74.111.194 +204.74.111.192 +204.74.111.191 +204.74.111.189 +204.74.111.188 +204.74.111.186 +204.74.111.185 +204.74.111.182 +204.74.111.181 +204.74.111.18 +204.74.111.179 +204.74.111.178 +204.74.111.176 +204.74.111.175 +204.74.111.173 +204.74.111.172 +204.74.111.171 +204.74.111.169 +204.74.111.168 +204.74.111.166 +204.74.111.164 +204.74.111.163 +204.74.111.162 +204.74.111.16 +204.74.111.158 +204.74.111.157 +204.74.111.156 +204.74.111.155 +204.74.111.154 +204.74.111.152 +204.74.111.150 +204.74.111.146 +204.74.111.145 +204.74.111.144 +204.74.111.143 +204.74.111.14 +204.74.111.139 +204.74.111.138 +204.74.111.137 +204.74.111.136 +204.74.111.133 +204.74.111.132 +204.74.111.13 +204.74.111.125 +204.74.111.124 +204.74.111.12 +204.74.111.118 +204.74.111.117 +204.74.111.115 +204.74.111.111 +204.74.111.11 +204.74.111.107 +204.74.111.106 +204.74.111.104 +204.74.111.103 +204.74.111.100 +204.74.111.10 +204.74.111.1 +204.74.110.96 +204.74.110.95 +204.74.110.93 +204.74.110.90 +204.74.110.89 +204.74.110.84 +204.74.110.83 +204.74.110.81 +204.74.110.80 +204.74.110.78 +204.74.110.77 +204.74.110.74 +204.74.110.72 +204.74.110.7 +204.74.110.66 +204.74.110.63 +204.74.110.62 +204.74.110.6 +204.74.110.58 +204.74.110.56 +204.74.110.50 +204.74.110.5 +204.74.110.47 +204.74.110.44 +204.74.110.43 +204.74.110.42 +204.74.110.35 +204.74.110.34 +204.74.110.33 +204.74.110.30 +204.74.110.3 +204.74.110.29 +204.74.110.28 +204.74.110.254 +204.74.110.252 +204.74.110.25 +204.74.110.249 +204.74.110.245 +204.74.110.244 +204.74.110.242 +204.74.110.241 +204.74.110.24 +204.74.110.238 +204.74.110.236 +204.74.110.235 +204.74.110.224 +204.74.110.223 +204.74.110.221 +204.74.110.220 +204.74.110.216 +204.74.110.211 +204.74.110.210 +204.74.110.21 +204.74.110.209 +204.74.110.203 +204.74.110.200 +204.74.110.195 +204.74.110.193 +204.74.110.192 +204.74.110.19 +204.74.110.188 +204.74.110.182 +204.74.110.180 +204.74.110.18 +204.74.110.176 +204.74.110.173 +204.74.110.172 +204.74.110.166 +204.74.110.165 +204.74.110.164 +204.74.110.163 +204.74.110.162 +204.74.110.161 +204.74.110.16 +204.74.110.156 +204.74.110.154 +204.74.110.153 +204.74.110.151 +204.74.110.150 +204.74.110.15 +204.74.110.149 +204.74.110.148 +204.74.110.147 +204.74.110.138 +204.74.110.137 +204.74.110.134 +204.74.110.131 +204.74.110.128 +204.74.110.127 +204.74.110.124 +204.74.110.121 +204.74.110.120 +204.74.110.119 +204.74.110.116 +204.74.110.111 +204.74.110.108 +204.74.110.103 +204.74.110.1 +204.74.109.99 +204.74.109.98 +204.74.109.97 +204.74.109.96 +204.74.109.95 +204.74.109.94 +204.74.109.90 +204.74.109.89 +204.74.109.88 +204.74.109.86 +204.74.109.84 +204.74.109.83 +204.74.109.81 +204.74.109.8 +204.74.109.76 +204.74.109.74 +204.74.109.73 +204.74.109.72 +204.74.109.71 +204.74.109.70 +204.74.109.69 +204.74.109.68 +204.74.109.65 +204.74.109.63 +204.74.109.62 +204.74.109.6 +204.74.109.59 +204.74.109.57 +204.74.109.52 +204.74.109.51 +204.74.109.48 +204.74.109.46 +204.74.109.45 +204.74.109.43 +204.74.109.41 +204.74.109.40 +204.74.109.4 +204.74.109.39 +204.74.109.38 +204.74.109.37 +204.74.109.35 +204.74.109.34 +204.74.109.33 +204.74.109.31 +204.74.109.30 +204.74.109.3 +204.74.109.29 +204.74.109.28 +204.74.109.254 +204.74.109.252 +204.74.109.247 +204.74.109.246 +204.74.109.244 +204.74.109.243 +204.74.109.242 +204.74.109.24 +204.74.109.239 +204.74.109.237 +204.74.109.236 +204.74.109.235 +204.74.109.233 +204.74.109.232 +204.74.109.228 +204.74.109.227 +204.74.109.221 +204.74.109.219 +204.74.109.218 +204.74.109.216 +204.74.109.215 +204.74.109.214 +204.74.109.213 +204.74.109.211 +204.74.109.210 +204.74.109.207 +204.74.109.206 +204.74.109.204 +204.74.109.203 +204.74.109.202 +204.74.109.201 +204.74.109.200 +204.74.109.198 +204.74.109.197 +204.74.109.193 +204.74.109.192 +204.74.109.191 +204.74.109.19 +204.74.109.189 +204.74.109.186 +204.74.109.185 +204.74.109.184 +204.74.109.183 +204.74.109.181 +204.74.109.180 +204.74.109.179 +204.74.109.175 +204.74.109.174 +204.74.109.172 +204.74.109.171 +204.74.109.170 +204.74.109.17 +204.74.109.169 +204.74.109.168 +204.74.109.167 +204.74.109.166 +204.74.109.165 +204.74.109.164 +204.74.109.161 +204.74.109.16 +204.74.109.159 +204.74.109.158 +204.74.109.155 +204.74.109.154 +204.74.109.152 +204.74.109.150 +204.74.109.149 +204.74.109.148 +204.74.109.147 +204.74.109.146 +204.74.109.145 +204.74.109.143 +204.74.109.141 +204.74.109.140 +204.74.109.138 +204.74.109.137 +204.74.109.136 +204.74.109.133 +204.74.109.132 +204.74.109.131 +204.74.109.130 +204.74.109.129 +204.74.109.128 +204.74.109.127 +204.74.109.126 +204.74.109.125 +204.74.109.124 +204.74.109.122 +204.74.109.121 +204.74.109.119 +204.74.109.118 +204.74.109.117 +204.74.109.116 +204.74.109.114 +204.74.109.113 +204.74.109.112 +204.74.109.111 +204.74.109.110 +204.74.109.11 +204.74.109.105 +204.74.109.104 +204.74.109.103 +204.74.109.102 +204.74.109.100 +204.74.109.10 +204.74.108.97 +204.74.108.96 +204.74.108.95 +204.74.108.94 +204.74.108.91 +204.74.108.90 +204.74.108.89 +204.74.108.85 +204.74.108.84 +204.74.108.82 +204.74.108.80 +204.74.108.8 +204.74.108.79 +204.74.108.78 +204.74.108.77 +204.74.108.72 +204.74.108.71 +204.74.108.7 +204.74.108.69 +204.74.108.68 +204.74.108.67 +204.74.108.63 +204.74.108.62 +204.74.108.6 +204.74.108.55 +204.74.108.52 +204.74.108.51 +204.74.108.49 +204.74.108.48 +204.74.108.45 +204.74.108.43 +204.74.108.41 +204.74.108.39 +204.74.108.38 +204.74.108.37 +204.74.108.36 +204.74.108.34 +204.74.108.33 +204.74.108.32 +204.74.108.30 +204.74.108.28 +204.74.108.252 +204.74.108.251 +204.74.108.249 +204.74.108.248 +204.74.108.247 +204.74.108.246 +204.74.108.244 +204.74.108.242 +204.74.108.238 +204.74.108.237 +204.74.108.234 +204.74.108.23 +204.74.108.229 +204.74.108.228 +204.74.108.227 +204.74.108.225 +204.74.108.224 +204.74.108.220 +204.74.108.22 +204.74.108.219 +204.74.108.218 +204.74.108.217 +204.74.108.215 +204.74.108.213 +204.74.108.211 +204.74.108.208 +204.74.108.207 +204.74.108.206 +204.74.108.203 +204.74.108.201 +204.74.108.200 +204.74.108.20 +204.74.108.2 +204.74.108.199 +204.74.108.198 +204.74.108.197 +204.74.108.196 +204.74.108.194 +204.74.108.193 +204.74.108.192 +204.74.108.191 +204.74.108.190 +204.74.108.19 +204.74.108.189 +204.74.108.187 +204.74.108.185 +204.74.108.183 +204.74.108.180 +204.74.108.18 +204.74.108.178 +204.74.108.176 +204.74.108.174 +204.74.108.171 +204.74.108.169 +204.74.108.167 +204.74.108.164 +204.74.108.163 +204.74.108.162 +204.74.108.161 +204.74.108.160 +204.74.108.16 +204.74.108.159 +204.74.108.156 +204.74.108.155 +204.74.108.152 +204.74.108.151 +204.74.108.150 +204.74.108.149 +204.74.108.147 +204.74.108.144 +204.74.108.142 +204.74.108.141 +204.74.108.140 +204.74.108.139 +204.74.108.138 +204.74.108.137 +204.74.108.133 +204.74.108.132 +204.74.108.131 +204.74.108.130 +204.74.108.124 +204.74.108.123 +204.74.108.122 +204.74.108.121 +204.74.108.12 +204.74.108.118 +204.74.108.115 +204.74.108.114 +204.74.108.113 +204.74.108.109 +204.74.108.105 +204.74.108.103 +204.74.108.102 +204.74.108.100 +204.74.101.99 +204.74.101.98 +204.74.101.97 +204.74.101.96 +204.74.101.95 +204.74.101.94 +204.74.101.93 +204.74.101.92 +204.74.101.91 +204.74.101.90 +204.74.101.89 +204.74.101.87 +204.74.101.86 +204.74.101.82 +204.74.101.81 +204.74.101.80 +204.74.101.79 +204.74.101.78 +204.74.101.77 +204.74.101.76 +204.74.101.75 +204.74.101.74 +204.74.101.73 +204.74.101.72 +204.74.101.71 +204.74.101.7 +204.74.101.69 +204.74.101.67 +204.74.101.64 +204.74.101.63 +204.74.101.62 +204.74.101.61 +204.74.101.60 +204.74.101.6 +204.74.101.59 +204.74.101.58 +204.74.101.57 +204.74.101.56 +204.74.101.55 +204.74.101.53 +204.74.101.51 +204.74.101.49 +204.74.101.48 +204.74.101.47 +204.74.101.45 +204.74.101.40 +204.74.101.39 +204.74.101.38 +204.74.101.37 +204.74.101.36 +204.74.101.35 +204.74.101.34 +204.74.101.33 +204.74.101.31 +204.74.101.29 +204.74.101.254 +204.74.101.251 +204.74.101.250 +204.74.101.25 +204.74.101.249 +204.74.101.248 +204.74.101.246 +204.74.101.244 +204.74.101.243 +204.74.101.242 +204.74.101.241 +204.74.101.24 +204.74.101.238 +204.74.101.237 +204.74.101.236 +204.74.101.235 +204.74.101.233 +204.74.101.232 +204.74.101.231 +204.74.101.23 +204.74.101.228 +204.74.101.227 +204.74.101.225 +204.74.101.224 +204.74.101.223 +204.74.101.222 +204.74.101.220 +204.74.101.22 +204.74.101.219 +204.74.101.217 +204.74.101.214 +204.74.101.213 +204.74.101.209 +204.74.101.207 +204.74.101.205 +204.74.101.203 +204.74.101.200 +204.74.101.20 +204.74.101.2 +204.74.101.197 +204.74.101.196 +204.74.101.195 +204.74.101.192 +204.74.101.191 +204.74.101.19 +204.74.101.186 +204.74.101.185 +204.74.101.182 +204.74.101.180 +204.74.101.18 +204.74.101.179 +204.74.101.178 +204.74.101.175 +204.74.101.173 +204.74.101.169 +204.74.101.166 +204.74.101.164 +204.74.101.162 +204.74.101.16 +204.74.101.153 +204.74.101.151 +204.74.101.150 +204.74.101.149 +204.74.101.148 +204.74.101.144 +204.74.101.143 +204.74.101.140 +204.74.101.137 +204.74.101.136 +204.74.101.135 +204.74.101.133 +204.74.101.132 +204.74.101.131 +204.74.101.13 +204.74.101.128 +204.74.101.125 +204.74.101.122 +204.74.101.121 +204.74.101.12 +204.74.101.118 +204.74.101.117 +204.74.101.115 +204.74.101.113 +204.74.101.111 +204.74.101.11 +204.74.101.109 +204.74.101.107 +204.74.101.106 +204.74.101.105 +204.74.101.104 +204.74.101.101 +204.74.101.10 +204.70.127.128 +204.70.127.127 +204.69.234.99 +204.69.234.98 +204.69.234.96 +204.69.234.95 +204.69.234.93 +204.69.234.91 +204.69.234.90 +204.69.234.9 +204.69.234.89 +204.69.234.88 +204.69.234.87 +204.69.234.86 +204.69.234.84 +204.69.234.83 +204.69.234.82 +204.69.234.81 +204.69.234.80 +204.69.234.79 +204.69.234.78 +204.69.234.77 +204.69.234.72 +204.69.234.71 +204.69.234.70 +204.69.234.7 +204.69.234.69 +204.69.234.68 +204.69.234.67 +204.69.234.64 +204.69.234.62 +204.69.234.61 +204.69.234.60 +204.69.234.59 +204.69.234.58 +204.69.234.57 +204.69.234.54 +204.69.234.53 +204.69.234.51 +204.69.234.50 +204.69.234.49 +204.69.234.48 +204.69.234.47 +204.69.234.46 +204.69.234.44 +204.69.234.42 +204.69.234.41 +204.69.234.40 +204.69.234.4 +204.69.234.39 +204.69.234.38 +204.69.234.33 +204.69.234.32 +204.69.234.31 +204.69.234.28 +204.69.234.27 +204.69.234.26 +204.69.234.253 +204.69.234.252 +204.69.234.25 +204.69.234.249 +204.69.234.246 +204.69.234.245 +204.69.234.244 +204.69.234.243 +204.69.234.242 +204.69.234.241 +204.69.234.240 +204.69.234.24 +204.69.234.238 +204.69.234.237 +204.69.234.232 +204.69.234.231 +204.69.234.230 +204.69.234.23 +204.69.234.229 +204.69.234.228 +204.69.234.227 +204.69.234.225 +204.69.234.224 +204.69.234.221 +204.69.234.220 +204.69.234.219 +204.69.234.218 +204.69.234.217 +204.69.234.216 +204.69.234.215 +204.69.234.214 +204.69.234.210 +204.69.234.21 +204.69.234.207 +204.69.234.206 +204.69.234.205 +204.69.234.203 +204.69.234.202 +204.69.234.201 +204.69.234.200 +204.69.234.199 +204.69.234.198 +204.69.234.197 +204.69.234.196 +204.69.234.195 +204.69.234.193 +204.69.234.192 +204.69.234.191 +204.69.234.190 +204.69.234.19 +204.69.234.189 +204.69.234.188 +204.69.234.187 +204.69.234.185 +204.69.234.181 +204.69.234.180 +204.69.234.18 +204.69.234.178 +204.69.234.177 +204.69.234.176 +204.69.234.175 +204.69.234.174 +204.69.234.173 +204.69.234.172 +204.69.234.171 +204.69.234.17 +204.69.234.168 +204.69.234.166 +204.69.234.165 +204.69.234.164 +204.69.234.163 +204.69.234.162 +204.69.234.160 +204.69.234.16 +204.69.234.158 +204.69.234.157 +204.69.234.155 +204.69.234.154 +204.69.234.152 +204.69.234.151 +204.69.234.150 +204.69.234.148 +204.69.234.145 +204.69.234.143 +204.69.234.142 +204.69.234.14 +204.69.234.139 +204.69.234.137 +204.69.234.135 +204.69.234.133 +204.69.234.130 +204.69.234.129 +204.69.234.127 +204.69.234.125 +204.69.234.124 +204.69.234.123 +204.69.234.122 +204.69.234.121 +204.69.234.120 +204.69.234.12 +204.69.234.118 +204.69.234.117 +204.69.234.116 +204.69.234.115 +204.69.234.113 +204.69.234.112 +204.69.234.111 +204.69.234.110 +204.69.234.107 +204.69.234.106 +204.69.234.105 +204.69.234.103 +204.69.234.102 +204.69.234.100 +204.69.234.1 +204.62.22.103 +204.57.66.2 +204.50.102.243 +204.48.24.104 +204.48.22.68 +204.246.56.12 +204.246.56.100 +204.246.1.36 +204.238.24.10 +204.225.44.3 +204.209.20.154 +204.2.152.19 +204.199.129.38 +204.199.103.180 +204.194.234.200 +204.194.232.200 +204.191.10.5 +204.186.80.193 +204.156.192.68 +204.15.148.186 +204.131.92.115 +204.131.88.138 +204.131.229.119 +204.116.57.2 +204.111.39.9 +204.106.240.4 +203.96.180.18 +203.89.132.4 +203.86.200.253 +203.85.128.252 +203.82.42.180 +203.8.201.11 +203.8.201.10 +203.78.194.179 +203.69.232.60 +203.66.57.148 +203.59.131.91 +203.54.212.126 +203.52.58.169 +203.52.58.161 +203.50.2.71 +203.47.150.183 +203.47.150.176 +203.39.3.133 +203.38.224.193 +203.36.134.238 +203.253.64.1 +203.253.179.3 +203.253.179.2 +203.251.201.1 +203.251.183.5 +203.249.171.2 +203.249.161.2 +203.249.112.101 +203.248.252.2 +203.248.116.42 +203.246.40.2 +203.242.200.6 +203.242.200.5 +203.240.193.11 +203.239.133.223 +203.239.131.1 +203.239.130.3 +203.239.130.1 +203.236.20.11 +203.236.123.10 +203.236.120.80 +203.236.1.12 +203.232.27.11 +203.232.186.58 +203.232.166.2 +203.232.148.3 +203.230.220.2 +203.229.206.20 +203.228.22.223 +203.227.168.2 +203.225.255.11 +203.225.255.10 +203.215.186.37 +203.215.181.201 +203.213.96.4 +203.212.200.209 +203.209.181.154 +203.202.248.5 +203.201.60.5 +203.198.214.41 +203.198.167.39 +203.198.161.89 +203.190.43.46 +203.190.43.107 +203.190.27.235 +203.190.27.234 +203.190.254.3 +203.188.242.211 +203.186.102.170 +203.178.136.36 +203.177.88.250 +203.177.84.2 +203.177.215.1 +203.177.190.130 +203.177.0.158 +203.176.237.36 +203.176.102.68 +203.174.48.83 +203.173.163.89 +203.169.48.8 +203.169.4.1 +203.167.92.254 +203.162.39.66 +203.159.77.77 +203.158.9.5 +203.158.15.67 +203.154.91.1 +203.154.62.1 +203.154.58.1 +203.154.177.8 +203.153.41.205 +203.151.59.20 +203.150.48.128 +203.150.37.11 +203.150.37.10 +203.150.199.17 +203.150.197.136 +203.150.167.19 +203.150.128.133 +203.147.94.71 +203.147.6.30 +203.144.139.244 +203.144.13.250 +203.141.198.193 +203.135.31.114 +203.131.211.181 +203.129.31.67 +203.129.25.39 +203.128.16.2 +203.127.160.67 +203.127.160.66 +203.127.112.132 +203.126.30.39 +203.126.118.38 +203.125.83.122 +203.125.208.78 +203.121.145.77 +203.119.8.106 +203.115.130.74 +203.114.39.110 +203.113.148.12 +203.113.135.28 +203.113.135.26 +202.90.137.75 +202.89.125.8 +202.87.214.253 +202.87.213.253 +202.86.8.100 +202.86.149.20 +202.86.149.18 +202.84.37.99 +202.84.37.100 +202.83.175.188 +202.83.175.187 +202.83.175.182 +202.82.66.142 +202.82.121.113 +202.80.247.34 +202.78.224.130 +202.78.224.129 +202.73.30.98 +202.72.201.46 +202.70.77.101 +202.70.76.13 +202.69.50.49 +202.69.50.161 +202.67.10.108 +202.64.161.118 +202.63.197.114 +202.62.77.2 +202.62.222.222 +202.62.222.220 +202.61.251.9 +202.60.193.137 +202.6.96.4 +202.58.18.18 +202.57.32.1 +202.55.176.11 +202.55.176.10 +202.5.200.8 +202.5.192.9 +202.46.34.75 +202.46.34.74 +202.46.33.250 +202.44.52.1 +202.43.108.1 +202.41.213.33 +202.30.143.44 +202.30.143.41 +202.30.143.11 +202.30.0.11 +202.3.229.2 +202.29.9.46 +202.29.53.4 +202.29.51.130 +202.29.33.51 +202.29.242.222 +202.29.240.110 +202.29.239.134 +202.29.236.57 +202.29.232.113 +202.29.228.142 +202.29.225.26 +202.29.22.4 +202.29.219.154 +202.29.218.142 +202.29.218.138 +202.29.216.54 +202.29.214.86 +202.29.214.22 +202.29.173.61 +202.28.66.3 +202.248.37.74 +202.248.20.133 +202.248.175.138 +202.238.55.204 +202.233.9.234 +202.229.255.8 +202.219.63.253 +202.213.33.133 +202.210.190.99 +202.188.124.58 +202.186.1.57 +202.184.80.21 +202.183.195.162 +202.183.153.140 +202.182.0.2 +202.182.0.1 +202.181.242.131 +202.181.233.243 +202.181.178.163 +202.175.86.206 +202.175.66.122 +202.175.45.2 +202.175.113.124 +202.173.208.2 +202.173.208.1 +202.166.161.107 +202.165.94.226 +202.164.153.87 +202.164.153.108 +202.164.150.220 +202.164.140.95 +202.164.140.93 +202.164.140.88 +202.164.140.67 +202.164.140.31 +202.164.140.203 +202.163.110.163 +202.157.177.4 +202.155.222.251 +202.155.202.75 +202.155.197.230 +202.153.110.47 +202.151.81.209 +202.149.205.59 +202.149.205.15 +202.149.204.207 +202.146.34.251 +202.143.118.53 +202.142.189.98 +202.142.179.66 +202.142.133.126 +202.14.14.99 +202.14.14.97 +202.138.73.149 +202.136.180.57 +202.136.120.187 +202.133.101.116 +202.131.73.38 +202.130.97.66 +202.130.97.65 +202.129.59.69 +202.129.207.29 +202.129.207.28 +202.129.206.237 +202.129.196.242 +202.129.1.186 +202.129.0.27 +202.126.100.157 +202.125.84.197 +202.124.192.10 +202.124.128.3 +202.123.179.205 +201.96.47.101 +201.96.27.129 +201.76.162.156 +201.76.161.162 +201.6.254.155 +201.54.224.134 +201.48.9.5 +201.48.9.4 +201.253.120.219 +201.251.135.168 +201.251.135.167 +201.247.112.236 +201.238.243.245 +201.238.128.199 +201.234.210.179 +201.234.186.234 +201.234.186.225 +201.234.138.252 +201.229.68.14 +201.222.53.130 +201.222.50.30 +201.219.218.130 +201.219.193.1 +201.219.154.75 +201.218.223.138 +201.218.219.210 +201.218.125.251 +201.217.57.148 +201.216.230.100 +201.21.195.129 +201.200.130.36 +201.200.130.35 +201.194.193.31 +201.190.178.156 +201.190.11.254 +201.184.60.178 +201.184.237.218 +201.184.225.210 +201.184.224.50 +201.184.187.98 +201.184.180.58 +201.184.175.242 +201.184.167.2 +201.184.126.218 +201.182.70.106 +201.182.66.37 +201.182.22.253 +201.174.80.34 +201.174.80.178 +201.174.235.68 +201.171.254.115 +201.170.249.229 +201.165.54.242 +201.164.60.34 +201.164.154.202 +201.163.94.65 +201.163.81.90 +201.16.253.25 +201.16.214.164 +201.156.1.226 +201.151.40.54 +201.151.196.110 +201.151.196.107 +201.150.35.124 +201.149.94.61 +201.148.95.234 +201.148.17.116 +201.148.17.110 +201.148.107.70 +201.148.107.14 +201.147.242.124 +201.147.242.119 +201.144.40.97 +201.143.181.110 +201.140.157.33 +201.140.114.161 +201.132.162.254 +201.130.73.234 +201.101.2.38 +200.95.184.33 +200.95.184.22 +200.95.144.3 +200.94.26.115 +200.94.26.114 +200.94.21.29 +200.94.156.193 +200.92.202.26 +200.91.34.20 +200.91.28.35 +200.9.155.203 +200.89.142.74 +200.89.134.202 +200.85.7.28 +200.85.61.90 +200.85.61.34 +200.85.39.94 +200.80.232.12 +200.80.203.75 +200.78.242.11 +200.76.52.60 +200.76.5.147 +200.75.8.110 +200.74.203.116 +200.73.113.158 +200.69.79.50 +200.69.226.98 +200.69.212.177 +200.68.46.21 +200.62.147.66 +200.6.253.74 +200.58.84.94 +200.58.182.186 +200.58.180.50 +200.57.7.61 +200.56.98.145 +200.56.98.113 +200.56.224.11 +200.56.117.25 +200.55.59.102 +200.55.59.101 +200.55.248.253 +200.54.9.138 +200.54.51.6 +200.54.22.74 +200.52.80.60 +200.5.90.102 +200.5.119.178 +200.5.115.26 +200.49.1.8 +200.45.14.178 +200.44.190.134 +200.41.174.20 +200.41.102.254 +200.39.23.4 +200.37.71.3 +200.37.203.90 +200.35.94.41 +200.35.79.41 +200.35.110.9 +200.33.3.123 +200.29.255.3 +200.29.255.2 +200.29.109.112 +200.29.108.64 +200.25.254.134 +200.248.178.54 +200.24.131.97 +200.229.252.65 +200.229.252.196 +200.229.252.171 +200.229.252.17 +200.221.11.101 +200.221.11.100 +200.219.224.172 +200.214.186.3 +200.21.227.130 +200.207.143.93 +200.201.191.91 +200.2.116.245 +200.195.170.186 +200.195.154.122 +200.19.203.1 +200.186.194.135 +200.186.1.135 +200.182.63.96 +200.171.40.210 +200.169.8.1 +200.169.2.2 +200.150.103.60 +200.142.106.130 +200.125.171.220 +200.125.171.219 +200.125.171.171 +200.125.171.170 +200.125.171.117 +200.124.124.190 +200.123.208.126 +200.12.251.226 +200.119.222.242 +200.116.231.27 +200.114.96.11 +200.114.113.67 +200.113.10.155 +200.112.143.5 +200.111.82.197 +200.111.47.43 +200.111.143.146 +200.110.219.225 +200.110.219.162 +200.110.168.42 +200.11.52.202 +200.11.138.12 +200.11.138.11 +200.108.46.242 +200.108.139.254 +200.108.131.206 +200.106.167.196 +200.106.167.195 +200.105.96.57 +200.105.192.6 +200.10.231.110 +200.10.221.16 +200.10.156.62 +200.1.104.36 +200.1.104.35 +20.93.144.220 +20.92.135.165 +20.71.128.65 +20.65.24.202 +20.46.112.146 +20.44.248.67 +20.44.201.37 +20.43.154.54 +20.38.170.198 +20.36.30.173 +20.242.194.111 +20.220.73.158 +20.212.94.189 +20.203.40.214 +20.203.17.58 +20.193.27.225 +20.14.83.169 +20.112.30.229 +20.106.145.8 +2.85.181.242 +2.78.57.194 +2.63.220.22 +2.63.188.10 +2.63.175.22 +2.60.245.18 +2.60.117.118 +2.59.241.54 +2.59.241.250 +2.59.241.23 +2.59.135.250 +2.56.220.2 +2.56.182.37 +2.55.72.107 +2.55.127.105 +2.52.252.252 +2.47.138.139 +2.40.63.222 +2.40.45.218 +2.40.119.162 +2.32.175.178 +2.229.72.225 +2.229.16.101 +2.207.170.66 +2.189.44.44 +2.188.21.130 +2.136.93.224 +2.136.37.48 +2.119.131.34 +2.105.69.220 +199.88.158.1 +199.85.127.30 +199.85.127.20 +199.85.127.10 +199.85.126.30 +199.85.126.20 +199.85.126.10 +199.76.39.72 +199.7.69.99 +199.7.69.98 +199.7.69.96 +199.7.69.94 +199.7.69.93 +199.7.69.91 +199.7.69.90 +199.7.69.9 +199.7.69.88 +199.7.69.87 +199.7.69.85 +199.7.69.83 +199.7.69.82 +199.7.69.81 +199.7.69.8 +199.7.69.79 +199.7.69.78 +199.7.69.73 +199.7.69.72 +199.7.69.71 +199.7.69.7 +199.7.69.68 +199.7.69.66 +199.7.69.65 +199.7.69.64 +199.7.69.62 +199.7.69.6 +199.7.69.58 +199.7.69.56 +199.7.69.54 +199.7.69.53 +199.7.69.49 +199.7.69.46 +199.7.69.45 +199.7.69.44 +199.7.69.43 +199.7.69.36 +199.7.69.34 +199.7.69.32 +199.7.69.31 +199.7.69.3 +199.7.69.29 +199.7.69.26 +199.7.69.254 +199.7.69.251 +199.7.69.250 +199.7.69.25 +199.7.69.249 +199.7.69.248 +199.7.69.246 +199.7.69.243 +199.7.69.242 +199.7.69.24 +199.7.69.239 +199.7.69.238 +199.7.69.234 +199.7.69.231 +199.7.69.230 +199.7.69.229 +199.7.69.226 +199.7.69.225 +199.7.69.221 +199.7.69.214 +199.7.69.213 +199.7.69.212 +199.7.69.210 +199.7.69.208 +199.7.69.201 +199.7.69.200 +199.7.69.20 +199.7.69.2 +199.7.69.198 +199.7.69.189 +199.7.69.188 +199.7.69.184 +199.7.69.181 +199.7.69.179 +199.7.69.178 +199.7.69.176 +199.7.69.174 +199.7.69.171 +199.7.69.170 +199.7.69.17 +199.7.69.167 +199.7.69.164 +199.7.69.162 +199.7.69.161 +199.7.69.160 +199.7.69.16 +199.7.69.159 +199.7.69.156 +199.7.69.154 +199.7.69.151 +199.7.69.15 +199.7.69.147 +199.7.69.145 +199.7.69.144 +199.7.69.143 +199.7.69.141 +199.7.69.140 +199.7.69.139 +199.7.69.137 +199.7.69.136 +199.7.69.134 +199.7.69.133 +199.7.69.131 +199.7.69.13 +199.7.69.126 +199.7.69.125 +199.7.69.124 +199.7.69.123 +199.7.69.121 +199.7.69.12 +199.7.69.119 +199.7.69.117 +199.7.69.115 +199.7.69.113 +199.7.69.112 +199.7.69.109 +199.7.69.108 +199.7.69.106 +199.7.69.103 +199.7.69.100 +199.7.69.10 +199.7.69.1 +199.7.68.99 +199.7.68.97 +199.7.68.93 +199.7.68.92 +199.7.68.91 +199.7.68.9 +199.7.68.89 +199.7.68.88 +199.7.68.86 +199.7.68.85 +199.7.68.83 +199.7.68.82 +199.7.68.77 +199.7.68.76 +199.7.68.75 +199.7.68.73 +199.7.68.72 +199.7.68.71 +199.7.68.70 +199.7.68.69 +199.7.68.68 +199.7.68.66 +199.7.68.63 +199.7.68.62 +199.7.68.56 +199.7.68.53 +199.7.68.52 +199.7.68.51 +199.7.68.49 +199.7.68.48 +199.7.68.45 +199.7.68.44 +199.7.68.43 +199.7.68.41 +199.7.68.4 +199.7.68.38 +199.7.68.36 +199.7.68.35 +199.7.68.34 +199.7.68.32 +199.7.68.29 +199.7.68.27 +199.7.68.26 +199.7.68.251 +199.7.68.250 +199.7.68.25 +199.7.68.248 +199.7.68.244 +199.7.68.243 +199.7.68.240 +199.7.68.24 +199.7.68.239 +199.7.68.237 +199.7.68.236 +199.7.68.234 +199.7.68.233 +199.7.68.231 +199.7.68.23 +199.7.68.229 +199.7.68.227 +199.7.68.226 +199.7.68.225 +199.7.68.223 +199.7.68.222 +199.7.68.221 +199.7.68.219 +199.7.68.217 +199.7.68.216 +199.7.68.215 +199.7.68.214 +199.7.68.213 +199.7.68.212 +199.7.68.211 +199.7.68.210 +199.7.68.21 +199.7.68.209 +199.7.68.208 +199.7.68.207 +199.7.68.204 +199.7.68.203 +199.7.68.202 +199.7.68.201 +199.7.68.200 +199.7.68.20 +199.7.68.198 +199.7.68.197 +199.7.68.195 +199.7.68.192 +199.7.68.188 +199.7.68.185 +199.7.68.184 +199.7.68.181 +199.7.68.18 +199.7.68.179 +199.7.68.175 +199.7.68.173 +199.7.68.172 +199.7.68.171 +199.7.68.17 +199.7.68.168 +199.7.68.167 +199.7.68.166 +199.7.68.165 +199.7.68.161 +199.7.68.160 +199.7.68.159 +199.7.68.158 +199.7.68.157 +199.7.68.156 +199.7.68.153 +199.7.68.152 +199.7.68.149 +199.7.68.148 +199.7.68.145 +199.7.68.144 +199.7.68.143 +199.7.68.141 +199.7.68.140 +199.7.68.14 +199.7.68.139 +199.7.68.137 +199.7.68.134 +199.7.68.133 +199.7.68.132 +199.7.68.131 +199.7.68.13 +199.7.68.128 +199.7.68.127 +199.7.68.124 +199.7.68.123 +199.7.68.121 +199.7.68.119 +199.7.68.118 +199.7.68.115 +199.7.68.113 +199.7.68.110 +199.7.68.109 +199.7.68.108 +199.7.68.104 +199.7.68.102 +199.7.68.101 +199.7.68.100 +199.7.68.10 +199.58.81.218 +199.44.194.3 +199.44.194.2 +199.243.95.1 +199.241.172.22 +199.218.114.27 +199.203.56.218 +199.195.54.137 +199.193.83.137 +199.193.80.46 +199.193.74.45 +199.193.74.42 +199.192.161.86 +199.192.161.23 +199.192.160.102 +199.166.6.62 +199.166.6.2 +199.127.219.254 +199.117.92.73 +199.116.57.181 +199.101.48.17 +198.99.193.2 +198.99.193.1 +198.82.247.98 +198.82.247.66 +198.82.247.34 +198.71.62.239 +198.71.117.66 +198.60.22.22 +198.60.22.2 +198.54.117.11 +198.54.117.10 +198.52.242.90 +198.41.223.98 +198.41.223.93 +198.41.223.83 +198.41.223.73 +198.41.223.64 +198.41.223.63 +198.41.223.61 +198.41.223.55 +198.41.223.42 +198.41.223.40 +198.41.223.38 +198.41.223.251 +198.41.223.250 +198.41.223.245 +198.41.223.238 +198.41.223.224 +198.41.223.215 +198.41.223.210 +198.41.223.207 +198.41.223.204 +198.41.223.200 +198.41.223.189 +198.41.223.177 +198.41.223.127 +198.41.223.124 +198.41.223.112 +198.41.222.87 +198.41.222.8 +198.41.222.75 +198.41.222.67 +198.41.222.6 +198.41.222.57 +198.41.222.49 +198.41.222.42 +198.41.222.36 +198.41.222.32 +198.41.222.249 +198.41.222.24 +198.41.222.239 +198.41.222.237 +198.41.222.236 +198.41.222.233 +198.41.222.230 +198.41.222.229 +198.41.222.227 +198.41.222.226 +198.41.222.221 +198.41.222.220 +198.41.222.205 +198.41.222.198 +198.41.222.197 +198.41.222.194 +198.41.222.188 +198.41.222.186 +198.41.222.172 +198.41.222.170 +198.41.222.169 +198.41.222.167 +198.41.222.159 +198.41.222.150 +198.41.222.15 +198.41.222.147 +198.41.222.145 +198.41.222.142 +198.41.222.141 +198.41.222.14 +198.41.222.128 +198.41.222.120 +198.41.222.116 +198.41.222.110 +198.41.222.107 +198.41.222.101 +198.251.100.2 +198.245.51.147 +198.243.48.167 +198.233.215.56 +198.190.195.80 +198.180.225.180 +198.175.228.44 +198.175.228.33 +198.153.194.60 +198.153.194.50 +198.153.194.40 +198.153.194.210 +198.153.194.200 +198.153.194.140 +198.153.194.130 +198.153.194.120 +198.153.194.1 +198.153.192.50 +198.153.192.40 +198.153.192.32 +198.153.192.210 +198.153.192.140 +198.153.192.1 +198.136.58.194 +198.135.221.2 +197.91.185.58 +197.91.174.198 +197.91.170.214 +197.90.203.87 +197.90.203.50 +197.253.36.34 +197.251.239.245 +197.251.204.34 +197.248.131.203 +197.248.125.181 +197.248.0.34 +197.243.90.62 +197.235.15.47 +197.232.66.154 +197.232.52.61 +197.232.21.96 +197.232.155.47 +197.232.124.205 +197.231.180.83 +197.230.97.29 +197.230.92.90 +197.230.84.1 +197.230.250.150 +197.230.245.130 +197.230.189.218 +197.230.188.74 +197.230.174.153 +197.230.162.89 +197.230.161.193 +197.230.161.122 +197.230.15.206 +197.230.145.10 +197.230.103.202 +197.221.82.226 +197.221.82.2 +197.215.217.251 +197.214.248.66 +197.210.211.1 +197.159.180.2 +197.159.180.1 +197.155.92.21 +197.155.92.20 +197.155.72.149 +197.155.71.114 +197.155.230.206 +197.148.74.19 +197.148.74.18 +197.13.5.134 +196.61.20.249 +196.43.199.61 +196.43.199.60 +196.33.103.134 +196.31.251.146 +196.3.132.154 +196.3.132.153 +196.29.199.76 +196.28.84.126 +196.28.244.3 +196.27.106.21 +196.251.156.82 +196.25.155.102 +196.216.252.65 +196.216.15.56 +196.216.134.71 +196.21.186.253 +196.203.86.4 +196.203.125.131 +196.201.244.7 +196.201.228.22 +196.200.176.2 +196.179.250.95 +196.179.250.93 +196.179.250.91 +196.179.250.89 +196.179.250.50 +196.179.250.11 +196.179.250.10 +196.179.196.177 +196.178.99.16 +196.178.97.91 +196.178.97.85 +196.178.97.84 +196.178.97.52 +196.178.97.42 +196.178.97.250 +196.178.97.193 +196.178.97.16 +196.178.100.224 +196.15.211.117 +196.13.243.20 +196.13.158.51 +196.13.141.10 +195.99.66.220 +195.98.85.66 +195.97.74.14 +195.96.250.194 +195.93.149.199 +195.90.183.90 +195.9.94.122 +195.9.190.22 +195.9.166.86 +195.88.74.2 +195.88.223.73 +195.80.119.99 +195.80.119.101 +195.70.113.203 +195.69.65.98 +195.69.65.1 +195.69.217.130 +195.68.174.244 +195.63.61.189 +195.63.103.144 +195.62.19.125 +195.62.19.119 +195.60.71.123 +195.46.7.225 +195.46.39.40 +195.46.39.39 +195.46.39.151 +195.46.39.103 +195.46.20.65 +195.43.12.163 +195.4.138.12 +195.34.234.228 +195.34.193.16 +195.3.204.225 +195.3.169.67 +195.3.135.101 +195.29.76.12 +195.27.1.1 +195.251.19.1 +195.250.72.134 +195.250.39.16 +195.25.89.202 +195.248.65.72 +195.246.42.210 +195.245.237.35 +195.243.214.4 +195.239.39.250 +195.239.230.40 +195.239.138.198 +195.238.40.45 +195.235.225.10 +195.230.115.3 +195.230.115.2 +195.23.75.151 +195.23.236.90 +195.23.100.180 +195.228.81.117 +195.228.39.166 +195.228.230.148 +195.226.187.130 +195.226.148.211 +195.225.49.20 +195.225.49.131 +195.225.48.193 +195.225.48.17 +195.224.45.182 +195.224.191.221 +195.224.148.52 +195.222.45.135 +195.22.77.99 +195.22.237.106 +195.22.131.230 +195.219.98.40 +195.216.58.18 +195.216.58.16 +195.214.240.136 +195.211.85.70 +195.211.219.141 +195.211.101.223 +195.210.172.46 +195.210.172.43 +195.21.58.113 +195.21.54.113 +195.21.137.153 +195.21.13.234 +195.209.144.228 +195.208.5.1 +195.208.4.1 +195.208.108.75 +195.205.9.65 +195.205.39.18 +195.201.246.253 +195.201.192.29 +195.201.100.11 +195.200.176.2 +195.20.154.230 +195.192.9.141 +195.192.86.170 +195.191.13.87 +195.19.40.237 +195.19.4.32 +195.19.102.93 +195.186.4.192 +195.186.4.162 +195.186.4.111 +195.186.4.110 +195.186.1.162 +195.186.1.111 +195.186.1.110 +195.182.147.74 +195.18.16.140 +195.178.56.180 +195.178.56.179 +195.178.33.46 +195.167.86.34 +195.167.136.57 +195.167.123.245 +195.166.180.239 +195.162.81.125 +195.16.47.53 +195.158.87.192 +195.158.82.92 +195.158.8.30 +195.158.250.101 +195.158.0.5 +195.158.0.3 +195.154.57.169 +195.154.31.170 +195.140.195.21 +195.14.49.27 +195.138.90.226 +195.138.88.134 +195.138.79.162 +195.136.206.181 +195.136.206.159 +195.136.206.155 +195.136.206.152 +195.136.163.57 +195.135.30.64 +195.133.157.159 +195.133.149.106 +195.117.218.206 +195.116.52.251 +195.116.242.247 +195.116.242.240 +195.114.7.252 +195.112.128.222 +195.110.25.248 +195.110.24.248 +195.11.179.142 +195.11.179.140 +195.10.195.195 +194.93.2.70 +194.88.93.22 +194.88.54.158 +194.88.245.142 +194.88.153.197 +194.77.8.1 +194.74.6.42 +194.72.35.195 +194.72.35.194 +194.70.90.162 +194.69.195.2 +194.69.194.3 +194.68.95.4 +194.67.40.47 +194.67.38.7 +194.65.30.2 +194.61.59.25 +194.61.232.18 +194.6.227.60 +194.6.227.180 +194.6.227.18 +194.6.227.150 +194.6.227.15 +194.55.140.138 +194.50.50.3 +194.48.151.213 +194.48.151.200 +194.48.151.145 +194.48.151.132 +194.48.151.130 +194.44.67.95 +194.44.45.242 +194.44.216.58 +194.44.216.10 +194.44.211.78 +194.44.139.88 +194.44.138.10 +194.36.144.87 +194.33.76.1 +194.31.5.18 +194.31.153.61 +194.30.254.138 +194.29.10.6 +194.28.61.114 +194.250.242.105 +194.25.0.68 +194.25.0.62 +194.25.0.60 +194.25.0.52 +194.249.67.20 +194.247.184.53 +194.242.217.239 +194.236.230.132 +194.236.230.131 +194.233.86.28 +194.228.54.34 +194.228.165.168 +194.224.216.170 +194.22.51.35 +194.209.90.8 +194.209.225.15 +194.204.225.18 +194.204.223.244 +194.2.0.50 +194.2.0.20 +194.190.84.137 +194.190.43.87 +194.190.175.17 +194.187.242.10 +194.187.240.10 +194.187.150.71 +194.186.90.74 +194.186.44.69 +194.186.232.214 +194.186.215.150 +194.177.56.1 +194.177.50.125 +194.177.210.210 +194.177.199.1 +194.172.160.4 +194.168.8.123 +194.168.4.123 +194.158.78.137 +194.150.118.100 +194.149.145.132 +194.146.249.146 +194.146.24.191 +194.145.241.6 +194.145.240.7 +194.145.240.6 +194.141.30.177 +194.135.45.87 +194.135.230.86 +194.135.11.210 +194.126.183.208 +194.126.167.2 +194.125.133.10 +194.12.15.222 +193.95.93.77 +193.95.93.243 +193.95.221.141 +193.93.237.230 +193.85.30.34 +193.77.25.50 +193.77.216.22 +193.58.251.251 +193.58.251.105 +193.58.251.104 +193.58.251.103 +193.58.251.102 +193.58.251.101 +193.57.73.100 +193.56.149.89 +193.56.148.228 +193.53.252.195 +193.47.83.251 +193.42.159.2 +193.42.153.26 +193.42.153.107 +193.39.71.5 +193.39.71.4 +193.39.71.3 +193.33.100.206 +193.253.234.127 +193.252.209.127 +193.248.131.101 +193.243.138.50 +193.242.177.92 +193.242.151.45 +193.242.107.8 +193.238.33.131 +193.238.102.55 +193.233.153.193 +193.232.36.242 +193.230.247.195 +193.228.134.195 +193.227.50.3 +193.227.29.241 +193.227.29.10 +193.226.61.1 +193.225.126.61 +193.219.27.222 +193.219.27.220 +193.219.102.62 +193.215.26.18 +193.215.179.34 +193.214.73.78 +193.202.121.50 +193.202.111.250 +193.200.151.69 +193.200.144.35 +193.2.246.9 +193.192.37.178 +193.192.113.146 +193.19.64.88 +193.19.253.254 +193.19.103.4 +193.186.170.50 +193.17.47.1 +193.168.243.5 +193.165.122.190 +193.165.116.70 +193.159.181.250 +193.141.116.163 +193.138.92.130 +193.138.92.129 +193.135.142.198 +193.115.248.226 +193.115.217.24 +193.111.200.191 +193.110.81.9 +193.110.81.0 +193.106.58.211 +193.106.192.9 +193.106.192.6 +193.106.192.14 +193.104.79.138 +192.77.22.74 +192.71.166.92 +192.69.77.66 +192.248.191.138 +192.248.176.219 +192.227.71.86 +192.221.177.0 +192.221.176.16 +192.221.176.0 +192.221.142.128 +192.221.142.0 +192.221.139.0 +192.221.138.0 +192.221.135.0 +192.210.16.184 +192.203.138.17 +192.203.138.11 +192.198.0.2 +192.182.146.202 +192.172.250.8 +192.169.154.227 +192.166.218.28 +192.166.218.146 +192.166.144.12 +192.165.9.158 +192.165.9.157 +192.165.252.20 +192.162.85.48 +192.162.240.66 +192.162.237.50 +192.162.233.20 +192.141.106.20 +192.140.40.253 +192.133.129.2 +192.12.111.30 +192.119.70.155 +192.116.91.229 +192.109.241.6 +192.100.164.125 +192.100.159.34 +191.97.9.99 +191.97.9.225 +191.97.53.229 +191.97.47.93 +191.6.138.137 +191.5.179.151 +191.37.23.73 +191.36.234.58 +191.36.234.143 +191.36.234.135 +191.36.233.57 +191.36.233.100 +191.33.230.226 +191.241.245.108 +191.241.161.70 +191.240.254.238 +191.209.29.122 +191.189.30.99 +191.182.203.8 +191.13.135.23 +191.102.89.6 +191.102.89.2 +191.102.82.83 +191.102.57.58 +191.102.56.57 +191.102.107.237 +191.100.20.116 +190.96.94.98 +190.96.93.74 +190.94.212.10 +190.93.189.30 +190.93.189.28 +190.93.176.126 +190.90.21.101 +190.89.8.146 +190.89.142.141 +190.89.142.129 +190.86.205.194 +190.85.118.18 +190.85.117.140 +190.82.70.214 +190.81.47.205 +190.71.49.122 +190.63.160.34 +190.60.84.243 +190.60.81.50 +190.60.67.138 +190.60.37.226 +190.6.31.100 +190.6.200.161 +190.58.23.133 +190.57.234.194 +190.56.148.54 +190.5.81.230 +190.43.92.243 +190.4.48.3 +190.4.18.218 +190.255.35.60 +190.25.241.210 +190.249.170.32 +190.249.168.212 +190.249.158.58 +190.248.67.34 +190.248.67.206 +190.248.143.210 +190.24.142.107 +190.223.55.37 +190.217.155.118 +190.216.56.107 +190.215.115.214 +190.211.104.94 +190.211.104.93 +190.210.255.132 +190.210.245.247 +190.210.127.98 +190.208.41.11 +190.202.135.58 +190.195.158.55 +190.187.243.222 +190.187.201.179 +190.187.200.243 +190.186.41.66 +190.186.131.245 +190.186.1.46 +190.184.224.206 +190.183.128.74 +190.181.63.242 +190.181.33.58 +190.171.26.34 +190.167.220.185 +190.152.219.135 +190.151.78.115 +190.151.76.90 +190.151.144.21 +190.151.104.178 +190.151.10.179 +190.15.205.212 +190.15.193.168 +190.149.193.229 +190.148.235.254 +190.148.193.146 +190.145.65.197 +190.145.196.114 +190.145.164.130 +190.145.136.210 +190.144.90.226 +190.144.123.122 +190.14.224.45 +190.14.154.78 +190.13.210.157 +190.13.210.148 +190.13.146.123 +190.128.225.58 +190.128.224.237 +190.128.224.236 +190.128.224.234 +190.128.214.90 +190.128.194.46 +190.124.39.34 +190.124.166.45 +190.123.85.89 +190.123.85.117 +190.121.4.63 +190.121.144.48 +190.120.188.98 +190.12.95.170 +190.12.67.210 +190.119.186.205 +190.119.105.85 +190.116.37.110 +190.113.88.165 +190.113.190.162 +190.113.172.24 +190.113.125.182 +190.111.246.169 +190.111.246.128 +190.111.207.83 +190.11.225.2 +190.109.64.49 +190.109.224.227 +190.109.2.245 +190.108.72.6 +190.107.20.164 +190.106.26.6 +190.105.214.36 +190.104.247.202 +190.104.243.44 +190.104.173.150 +190.104.168.155 +190.104.134.90 +190.103.31.90 +190.102.109.41 +190.0.62.118 +190.0.59.102 +190.0.32.94 +190.0.236.22 +190.0.14.18 +189.90.138.250 +189.90.114.109 +189.9.55.9 +189.8.80.34 +189.8.108.104 +189.7.49.85 +189.56.123.82 +189.51.118.34 +189.50.97.37 +189.4.83.33 +189.254.40.179 +189.254.225.213 +189.223.164.93 +189.223.164.9 +189.223.164.81 +189.22.227.194 +189.206.248.177 +189.206.218.22 +189.206.141.193 +189.206.125.227 +189.204.6.253 +189.204.240.235 +189.203.66.130 +189.203.141.69 +189.202.244.234 +189.196.91.198 +189.196.47.87 +189.196.17.222 +189.195.30.67 +189.194.63.25 +189.19.254.196 +189.16.248.21 +189.126.93.129 +189.126.192.4 +189.125.208.154 +189.112.160.165 +189.10.242.138 +188.94.227.38 +188.93.235.3 +188.93.135.180 +188.92.214.1 +188.92.209.129 +188.92.208.46 +188.75.33.210 +188.75.186.152 +188.69.227.55 +188.69.227.53 +188.69.130.183 +188.6.165.9 +188.6.164.43 +188.6.161.26 +188.43.239.146 +188.40.239.99 +188.40.205.205 +188.40.138.230 +188.36.126.131 +188.26.217.1 +188.254.49.206 +188.254.47.154 +188.227.136.44 +188.227.135.6 +188.226.64.126 +188.225.225.25 +188.207.12.98 +188.191.165.58 +188.191.161.121 +188.18.145.68 +188.18.141.247 +188.173.163.40 +188.170.5.117 +188.170.248.157 +188.170.130.91 +188.166.243.215 +188.165.254.29 +188.165.250.96 +188.165.220.211 +188.165.204.74 +188.165.135.96 +188.165.135.209 +188.163.170.130 +188.16.13.79 +188.135.60.42 +188.135.50.178 +188.135.50.138 +188.135.49.95 +188.135.14.80 +188.135.12.158 +188.126.60.67 +188.124.226.58 +188.122.4.78 +188.122.24.142 +188.122.212.56 +188.120.252.242 +188.117.151.126 +188.117.137.97 +188.117.137.81 +188.117.137.71 +188.117.137.37 +188.0.190.47 +188.0.190.35 +188.0.167.35 +188.0.166.185 +187.95.236.236 +187.95.184.142 +187.95.18.51 +187.95.125.180 +187.93.73.138 +187.93.105.85 +187.92.139.86 +187.87.224.3 +187.87.139.51 +187.85.179.189 +187.85.179.186 +187.84.81.62 +187.72.231.113 +187.63.156.236 +187.60.217.204 +187.6.84.178 +187.51.127.93 +187.49.77.178 +187.45.96.90 +187.45.127.228 +187.45.101.123 +187.44.188.134 +187.44.162.196 +187.44.0.18 +187.33.253.130 +187.32.90.61 +187.32.81.223 +187.32.81.194 +187.28.39.146 +187.251.227.50 +187.251.130.25 +187.218.44.161 +187.216.86.65 +187.216.41.205 +187.216.100.162 +187.190.50.24 +187.190.112.108 +187.19.101.65 +187.189.99.168 +187.189.23.177 +187.188.98.54 +187.188.57.147 +187.188.199.38 +187.188.150.41 +187.188.112.16 +187.18.156.188 +187.174.97.82 +187.174.213.18 +187.174.134.212 +187.157.84.101 +187.141.99.33 +187.141.176.81 +187.141.169.250 +187.141.133.236 +187.130.63.137 +187.120.173.2 +187.111.31.78 +187.11.242.42 +187.103.15.117 +187.1.163.30 +186.97.218.42 +186.97.208.18 +186.97.195.122 +186.97.194.162 +186.97.169.125 +186.97.130.130 +186.97.102.226 +186.96.98.138 +186.96.53.86 +186.96.145.231 +186.96.11.240 +186.75.32.30 +186.68.87.82 +186.67.26.198 +186.67.172.51 +186.56.57.194 +186.4.212.236 +186.4.206.188 +186.4.115.64 +186.38.33.98 +186.38.32.139 +186.251.103.3 +186.251.103.10 +186.250.118.34 +186.248.66.34 +186.24.9.1 +186.238.18.99 +186.236.102.88 +186.225.10.4 +186.216.162.70 +186.215.192.243 +186.215.137.186 +186.208.112.202 +186.200.32.84 +186.195.225.250 +186.194.160.118 +186.192.255.36 +186.190.237.140 +186.190.236.11 +186.190.228.83 +186.182.51.17 +186.179.241.146 +186.177.77.238 +186.177.211.202 +186.177.204.203 +186.177.203.161 +186.177.193.132 +186.176.200.35 +186.166.202.49 +186.159.4.66 +186.159.15.86 +186.154.241.226 +186.151.152.210 +186.150.201.138 +186.147.249.90 +186.124.22.114 +186.121.214.98 +186.116.6.75 +186.113.2.86 +186.103.175.74 +186.103.167.190 +186.10.94.57 +186.10.6.226 +186.1.41.92 +186.0.171.147 +185.99.89.250 +185.99.77.92 +185.97.204.135 +185.96.87.18 +185.95.67.151 +185.93.240.188 +185.93.180.140 +185.93.180.131 +185.90.73.17 +185.84.19.163 +185.84.19.115 +185.81.9.44 +185.8.221.20 +185.74.85.200 +185.74.5.5 +185.74.5.1 +185.70.182.166 +185.67.94.146 +185.66.9.142 +185.66.131.108 +185.65.175.161 +185.65.122.90 +185.61.93.2 +185.60.178.253 +185.56.191.2 +185.55.65.48 +185.53.233.178 +185.52.46.209 +185.51.10.213 +185.50.209.49 +185.49.20.20 +185.49.111.249 +185.49.108.48 +185.48.176.53 +185.47.209.34 +185.46.96.38 +185.46.197.124 +185.45.244.221 +185.44.24.27 +185.44.216.221 +185.43.135.1 +185.42.96.217 +185.42.192.114 +185.38.226.18 +185.38.208.200 +185.34.23.23 +185.34.23.139 +185.34.21.133 +185.32.5.65 +185.31.160.26 +185.3.69.37 +185.3.52.19 +185.254.7.169 +185.249.92.4 +185.248.172.8 +185.247.225.17 +185.246.188.51 +185.244.217.202 +185.243.19.67 +185.242.177.8 +185.242.177.7 +185.242.113.232 +185.24.81.66 +185.24.196.118 +185.24.122.178 +185.234.52.80 +185.234.228.65 +185.23.114.146 +185.229.101.28 +185.228.169.9 +185.228.168.9 +185.228.168.12 +185.228.141.154 +185.226.160.76 +185.222.222.222 +185.221.30.35 +185.220.182.179 +185.22.235.137 +185.214.10.175 +185.21.80.54 +185.21.66.253 +185.200.144.27 +185.192.244.202 +185.190.40.87 +185.190.105.61 +185.189.216.21 +185.189.126.185 +185.189.103.195 +185.189.100.92 +185.188.218.86 +185.188.216.101 +185.186.80.122 +185.184.233.210 +185.183.242.130 +185.182.107.236 +185.181.61.24 +185.180.41.221 +185.180.34.180 +185.18.7.7 +185.177.127.100 +185.175.11.110 +185.174.211.155 +185.171.208.153 +185.170.35.69 +185.170.35.60 +185.170.35.232 +185.170.35.229 +185.170.32.31 +185.17.133.88 +185.165.96.225 +185.160.39.186 +185.158.67.203 +185.156.198.183 +185.155.89.187 +185.153.92.223 +185.153.92.221 +185.153.92.211 +185.153.92.199 +185.153.92.197 +185.153.92.1 +185.150.99.255 +185.15.63.136 +185.15.210.98 +185.15.191.31 +185.147.71.84 +185.147.58.134 +185.146.215.230 +185.145.126.191 +185.144.201.195 +185.142.30.40 +185.14.234.242 +185.14.214.60 +185.14.150.6 +185.139.125.150 +185.136.78.181 +185.135.180.60 +185.134.232.35 +185.133.208.32 +185.132.201.202 +185.132.1.221 +185.130.44.20 +185.130.153.92 +185.129.115.86 +185.127.27.251 +185.127.227.98 +185.123.194.28 +185.123.188.23 +185.121.110.228 +185.12.71.241 +185.12.227.99 +185.12.2.210 +185.117.243.18 +185.116.229.115 +185.113.49.23 +185.112.224.247 +185.110.21.141 +185.11.49.180 +185.109.169.66 +185.108.215.229 +185.108.21.6 +185.108.21.45 +185.106.131.224 +185.106.131.141 +185.105.171.128 +185.101.185.149 +184.95.49.172 +184.68.102.2 +184.184.114.202 +184.183.89.198 +184.177.9.34 +184.164.96.251 +184.154.158.134 +184.149.25.55 +183.99.226.197 +183.98.206.253 +183.91.3.242 +183.178.69.52 +183.178.58.215 +183.178.110.4 +183.177.101.51 +183.107.20.90 +183.107.106.231 +183.104.61.35 +183.104.157.72 +183.100.49.21 +182.93.25.98 +182.93.25.100 +182.78.166.110 +182.78.164.254 +182.78.137.21 +182.75.205.90 +182.75.174.228 +182.73.54.193 +182.71.68.161 +182.71.61.207 +182.71.145.153 +182.58.134.111 +182.48.251.137 +182.239.78.192 +182.237.214.37 +182.237.16.7 +182.23.44.5 +182.211.210.55 +182.19.95.98 +182.176.111.188 +182.171.70.161 +182.171.233.145 +182.171.231.25 +182.162.73.17 +182.162.73.16 +182.16.171.164 +182.158.74.175 +182.158.73.179 +182.156.93.102 +182.156.242.178 +182.156.155.26 +182.156.155.142 +182.156.155.14 +182.156.154.98 +182.156.154.12 +182.156.153.39 +182.156.153.209 +182.156.153.111 +182.156.152.94 +182.156.152.89 +182.156.152.247 +182.156.152.240 +182.156.152.209 +182.156.152.2 +182.156.152.130 +182.156.152.119 +182.156.152.115 +181.94.247.163 +181.94.246.48 +181.94.245.137 +181.94.197.197 +181.80.17.44 +181.67.191.133 +181.57.149.82 +181.49.102.254 +181.48.92.218 +181.48.218.42 +181.48.196.182 +181.48.195.159 +181.48.195.157 +181.40.92.86 +181.40.122.102 +181.229.1.116 +181.224.225.3 +181.212.39.92 +181.210.92.7 +181.209.95.2 +181.209.91.154 +181.209.89.29 +181.209.86.250 +181.209.82.154 +181.209.78.67 +181.209.72.90 +181.209.194.198 +181.209.119.114 +181.209.111.149 +181.209.109.210 +181.209.105.158 +181.209.105.156 +181.209.105.154 +181.209.104.234 +181.205.83.202 +181.205.72.18 +181.205.7.26 +181.205.65.194 +181.205.61.122 +181.205.60.162 +181.205.57.154 +181.205.47.219 +181.205.34.122 +181.205.28.162 +181.205.252.242 +181.205.224.98 +181.205.219.202 +181.205.206.162 +181.205.204.163 +181.205.199.2 +181.205.178.122 +181.205.146.74 +181.205.142.178 +181.205.129.210 +181.205.124.50 +181.205.0.122 +181.204.9.106 +181.204.80.106 +181.204.8.66 +181.204.77.250 +181.204.75.178 +181.204.73.182 +181.204.72.122 +181.204.36.42 +181.204.232.2 +181.204.214.146 +181.204.20.138 +181.204.185.18 +181.204.183.74 +181.204.15.250 +181.204.14.218 +181.191.223.83 +181.191.223.59 +181.189.219.251 +181.188.148.18 +181.177.141.190 +181.171.232.10 +181.15.193.19 +181.143.66.122 +181.143.37.202 +181.143.27.98 +181.143.215.50 +181.143.204.98 +181.143.20.186 +181.143.196.83 +181.129.74.58 +181.129.70.106 +181.129.69.226 +181.129.57.146 +181.129.48.10 +181.129.42.138 +181.129.36.242 +181.129.31.210 +181.129.225.26 +181.129.14.3 +181.129.138.114 +181.129.121.42 +181.129.117.226 +181.12.158.108 +181.119.105.29 +181.118.92.14 +181.118.176.23 +181.118.167.110 +181.118.148.20 +181.118.148.189 +181.115.204.122 +181.115.203.138 +181.115.184.75 +181.115.184.142 +181.114.62.1 +181.114.60.225 +181.114.59.245 +181.114.5.150 +181.114.217.3 +181.114.212.34 +181.114.118.242 +181.112.60.126 +181.110.241.74 +181.105.122.76 +181.105.122.62 +181.105.122.196 +181.105.122.135 +181.105.121.99 +181.10.155.250 +180.94.94.195 +180.94.94.194 +180.92.170.100 +180.69.75.37 +180.69.254.143 +180.69.214.252 +180.64.246.59 +180.43.164.18 +180.42.15.9 +180.255.64.234 +180.255.3.49 +180.232.96.162 +180.232.81.98 +180.211.183.206 +180.193.221.81 +180.193.189.154 +180.193.184.97 +180.193.184.137 +180.193.183.230 +180.193.179.26 +180.193.179.18 +180.193.179.138 +180.193.170.33 +180.189.167.34 +180.180.244.53 +180.178.139.210 +180.150.51.109 +180.150.42.169 +180.150.13.108 +180.150.119.18 +180.150.105.246 +18.254.96.167 +18.163.103.200 +179.96.29.230 +179.93.80.111 +179.61.90.22 +179.60.244.53 +179.60.235.209 +179.60.235.153 +179.60.232.14 +179.60.232.10 +179.51.237.31 +179.43.97.147 +179.228.250.125 +179.228.207.216 +179.191.66.250 +179.189.226.125 +179.189.21.60 +179.184.102.46 +179.111.216.102 +179.1.133.89 +178.77.243.102 +178.75.220.2 +178.73.210.182 +178.72.73.23 +178.70.70.151 +178.69.14.158 +178.63.25.202 +178.62.197.147 +178.54.198.71 +178.49.184.32 +178.47.34.77 +178.46.160.85 +178.46.159.220 +178.46.158.13 +178.46.128.150 +178.35.238.134 +178.34.180.229 +178.34.159.215 +178.33.45.223 +178.33.250.245 +178.33.164.91 +178.32.107.33 +178.255.79.70 +178.255.191.166 +178.252.114.250 +178.249.64.22 +178.248.211.216 +178.248.211.177 +178.248.211.174 +178.248.208.20 +178.248.151.131 +178.239.225.58 +178.239.224.161 +178.238.28.70 +178.235.148.35 +178.222.250.29 +178.222.249.245 +178.220.230.54 +178.219.174.62 +178.219.174.3 +178.219.173.190 +178.219.163.177 +178.219.161.211 +178.219.149.47 +178.218.244.86 +178.217.140.7 +178.216.163.13 +178.216.111.85 +178.214.241.150 +178.213.114.193 +178.212.222.102 +178.212.102.76 +178.210.129.161 +178.205.108.200 +178.183.131.200 +178.176.63.46 +178.176.24.185 +178.172.225.2 +178.170.166.98 +178.17.127.129 +178.160.198.130 +178.16.32.129 +178.158.234.89 +178.155.72.98 +178.151.205.106 +178.134.36.178 +178.134.27.51 +178.134.155.82 +178.130.94.122 +178.128.46.208 +177.99.206.131 +177.99.161.122 +177.93.45.236 +177.93.1.250 +177.92.18.202 +177.92.123.158 +177.91.75.139 +177.87.96.4 +177.87.57.15 +177.84.120.203 +177.8.173.164 +177.8.163.162 +177.8.162.132 +177.75.74.0 +177.73.160.206 +177.69.127.41 +177.66.0.46 +177.52.247.201 +177.52.151.132 +177.47.128.2 +177.43.124.234 +177.43.102.34 +177.39.102.189 +177.36.241.38 +177.36.214.1 +177.36.196.106 +177.244.25.118 +177.242.151.222 +177.241.250.42 +177.241.245.222 +177.240.8.182 +177.240.18.182 +177.234.226.92 +177.234.209.111 +177.234.132.8 +177.229.223.74 +177.223.234.119 +177.223.107.235 +177.221.41.221 +177.220.156.202 +177.220.153.162 +177.220.149.10 +177.22.38.165 +177.22.203.220 +177.21.15.122 +177.200.69.231 +177.200.196.85 +177.20.183.3 +177.190.222.139 +177.190.199.35 +177.19.150.218 +177.19.145.82 +177.184.176.5 +177.174.112.253 +177.159.101.225 +177.152.93.246 +177.152.52.99 +177.152.159.199 +177.152.104.139 +177.144.128.64 +177.131.29.211 +177.131.29.209 +177.128.24.188 +177.104.64.2 +177.101.35.197 +177.10.162.82 +176.99.5.15 +176.98.80.97 +176.98.80.149 +176.96.240.86 +176.9.93.198 +176.9.54.219 +176.9.29.119 +176.9.204.129 +176.9.163.161 +176.9.100.254 +176.9.1.117 +176.65.63.119 +176.62.79.18 +176.62.189.246 +176.58.119.151 +176.58.113.172 +176.53.10.136 +176.33.142.139 +176.31.248.107 +176.31.103.216 +176.28.250.122 +176.28.107.12 +176.241.192.31 +176.241.110.51 +176.235.135.204 +176.197.7.54 +176.197.228.243 +176.196.53.70 +176.193.76.23 +176.192.123.190 +176.124.144.35 +176.122.71.107 +176.122.24.201 +176.122.21.137 +176.121.9.144 +176.120.203.38 +176.12.122.226 +176.118.26.2 +176.114.228.63 +176.114.128.30 +176.108.36.129 +176.107.131.32 +176.107.118.206 +176.106.252.22 +176.105.213.126 +176.105.207.93 +176.103.72.103 +176.102.137.49 +176.102.128.154 +176.101.247.53 +176.100.76.240 +175.45.16.253 +175.213.232.126 +175.213.132.85 +175.213.132.56 +175.209.22.21 +175.208.49.10 +175.208.229.187 +175.207.242.72 +175.202.234.243 +175.199.45.229 +175.144.214.180 +175.143.98.42 +175.139.176.60 +175.138.229.245 +175.138.182.115 +175.126.106.69 +175.117.145.35 +175.110.54.135 +175.101.18.20 +175.101.18.18 +175.101.132.109 +174.71.211.178 +174.69.43.159 +174.47.194.76 +174.141.219.114 +174.141.212.61 +174.138.21.128 +174.138.185.110 +174.138.182.182 +173.9.160.113 +173.27.123.175 +173.251.21.58 +173.249.48.6 +173.249.41.233 +173.249.10.12 +173.248.232.249 +173.248.155.77 +173.246.249.1 +173.245.59.99 +173.245.59.88 +173.245.59.87 +173.245.59.82 +173.245.59.59 +173.245.59.56 +173.245.59.55 +173.245.59.52 +173.245.59.49 +173.245.59.25 +173.245.59.237 +173.245.59.230 +173.245.59.225 +173.245.59.208 +173.245.59.203 +173.245.59.186 +173.245.59.172 +173.245.59.17 +173.245.59.167 +173.245.59.165 +173.245.59.163 +173.245.59.156 +173.245.59.147 +173.245.59.146 +173.245.59.139 +173.245.59.131 +173.245.59.127 +173.245.59.123 +173.245.59.122 +173.245.59.12 +173.245.59.117 +173.245.59.102 +173.245.59.10 +173.245.58.95 +173.245.58.93 +173.245.58.89 +173.245.58.86 +173.245.58.74 +173.245.58.67 +173.245.58.6 +173.245.58.49 +173.245.58.39 +173.245.58.237 +173.245.58.223 +173.245.58.215 +173.245.58.201 +173.245.58.184 +173.245.58.180 +173.245.58.18 +173.245.58.177 +173.245.58.167 +173.245.58.137 +173.245.58.136 +173.245.58.127 +173.245.58.126 +173.245.58.118 +173.245.58.113 +173.245.58.106 +173.241.228.10 +173.239.57.92 +173.239.23.84 +173.227.163.200 +173.226.143.254 +173.223.99.98 +173.223.99.92 +173.223.99.83 +173.223.99.81 +173.223.99.71 +173.223.99.66 +173.223.99.3 +173.223.99.26 +173.223.99.250 +173.223.99.226 +173.223.99.220 +173.223.99.201 +173.223.99.194 +173.223.99.175 +173.223.99.172 +173.223.99.168 +173.223.99.15 +173.223.99.124 +173.223.99.115 +173.223.99.112 +173.223.99.103 +173.223.98.53 +173.223.98.49 +173.223.98.29 +173.223.98.244 +173.223.98.213 +173.223.98.205 +173.223.98.198 +173.223.98.191 +173.223.98.181 +173.223.98.180 +173.223.98.174 +173.223.98.168 +173.223.98.137 +173.223.98.118 +173.223.98.117 +173.223.98.112 +173.223.101.9 +173.223.101.48 +173.223.101.46 +173.223.100.9 +173.223.100.50 +173.223.100.10 +173.219.2.16 +173.212.6.5 +173.212.243.123 +173.212.242.89 +173.212.241.35 +173.212.239.87 +173.212.228.2 +173.197.161.35 +173.166.181.209 +173.163.101.137 +173.161.65.201 +173.161.248.250 +173.15.132.155 +173.13.186.37 +173.12.123.37 +173.0.43.38 +172.64.47.93 +172.64.47.91 +172.64.47.9 +172.64.47.85 +172.64.47.75 +172.64.47.67 +172.64.47.50 +172.64.47.45 +172.64.47.44 +172.64.47.29 +172.64.47.254 +172.64.47.250 +172.64.47.242 +172.64.47.227 +172.64.47.226 +172.64.47.224 +172.64.47.221 +172.64.47.216 +172.64.47.210 +172.64.47.204 +172.64.47.200 +172.64.47.195 +172.64.47.186 +172.64.47.181 +172.64.47.180 +172.64.47.18 +172.64.47.178 +172.64.47.174 +172.64.47.171 +172.64.47.170 +172.64.47.168 +172.64.47.167 +172.64.47.166 +172.64.47.158 +172.64.47.154 +172.64.47.153 +172.64.47.147 +172.64.47.143 +172.64.47.133 +172.64.47.124 +172.64.47.12 +172.64.47.113 +172.64.47.110 +172.64.47.107 +172.64.47.106 +172.64.47.104 +172.64.47.103 +172.64.47.102 +172.64.47.10 +172.64.46.9 +172.64.46.84 +172.64.46.83 +172.64.46.80 +172.64.46.72 +172.64.46.66 +172.64.46.62 +172.64.46.53 +172.64.46.52 +172.64.46.50 +172.64.46.47 +172.64.46.46 +172.64.46.45 +172.64.46.42 +172.64.46.36 +172.64.46.35 +172.64.46.34 +172.64.46.31 +172.64.46.29 +172.64.46.28 +172.64.46.27 +172.64.46.255 +172.64.46.253 +172.64.46.252 +172.64.46.243 +172.64.46.236 +172.64.46.230 +172.64.46.229 +172.64.46.227 +172.64.46.22 +172.64.46.217 +172.64.46.213 +172.64.46.211 +172.64.46.209 +172.64.46.203 +172.64.46.202 +172.64.46.200 +172.64.46.198 +172.64.46.192 +172.64.46.191 +172.64.46.179 +172.64.46.177 +172.64.46.176 +172.64.46.173 +172.64.46.17 +172.64.46.161 +172.64.46.160 +172.64.46.159 +172.64.46.144 +172.64.46.142 +172.64.46.137 +172.64.46.13 +172.64.46.127 +172.64.46.124 +172.64.46.111 +172.64.46.109 +172.64.46.106 +172.64.46.103 +172.64.38.95 +172.64.38.84 +172.64.38.79 +172.64.38.71 +172.64.38.56 +172.64.38.47 +172.64.38.30 +172.64.38.28 +172.64.38.241 +172.64.38.232 +172.64.38.227 +172.64.38.220 +172.64.38.215 +172.64.38.211 +172.64.38.205 +172.64.38.198 +172.64.38.197 +172.64.38.195 +172.64.38.190 +172.64.38.182 +172.64.38.179 +172.64.38.177 +172.64.38.175 +172.64.38.169 +172.64.38.155 +172.64.38.14 +172.64.38.128 +172.64.38.12 +172.64.38.10 +172.64.38.1 +172.64.37.99 +172.64.37.98 +172.64.37.97 +172.64.37.96 +172.64.37.95 +172.64.37.94 +172.64.37.93 +172.64.37.92 +172.64.37.91 +172.64.37.90 +172.64.37.9 +172.64.37.89 +172.64.37.88 +172.64.37.87 +172.64.37.86 +172.64.37.85 +172.64.37.84 +172.64.37.83 +172.64.37.82 +172.64.37.81 +172.64.37.80 +172.64.37.8 +172.64.37.79 +172.64.37.78 +172.64.37.77 +172.64.37.76 +172.64.37.75 +172.64.37.74 +172.64.37.73 +172.64.37.72 +172.64.37.71 +172.64.37.70 +172.64.37.7 +172.64.37.69 +172.64.37.68 +172.64.37.66 +172.64.37.65 +172.64.37.64 +172.64.37.63 +172.64.37.62 +172.64.37.61 +172.64.37.60 +172.64.37.6 +172.64.37.59 +172.64.37.58 +172.64.37.57 +172.64.37.56 +172.64.37.55 +172.64.37.54 +172.64.37.53 +172.64.37.52 +172.64.37.51 +172.64.37.50 +172.64.37.5 +172.64.37.49 +172.64.37.48 +172.64.37.47 +172.64.37.46 +172.64.37.45 +172.64.37.44 +172.64.37.43 +172.64.37.42 +172.64.37.41 +172.64.37.40 +172.64.37.4 +172.64.37.39 +172.64.37.38 +172.64.37.37 +172.64.37.35 +172.64.37.34 +172.64.37.33 +172.64.37.32 +172.64.37.31 +172.64.37.30 +172.64.37.3 +172.64.37.29 +172.64.37.28 +172.64.37.27 +172.64.37.26 +172.64.37.254 +172.64.37.253 +172.64.37.252 +172.64.37.251 +172.64.37.250 +172.64.37.25 +172.64.37.249 +172.64.37.248 +172.64.37.247 +172.64.37.246 +172.64.37.245 +172.64.37.244 +172.64.37.243 +172.64.37.242 +172.64.37.241 +172.64.37.240 +172.64.37.24 +172.64.37.239 +172.64.37.238 +172.64.37.237 +172.64.37.236 +172.64.37.235 +172.64.37.234 +172.64.37.232 +172.64.37.231 +172.64.37.230 +172.64.37.23 +172.64.37.229 +172.64.37.228 +172.64.37.227 +172.64.37.226 +172.64.37.225 +172.64.37.224 +172.64.37.223 +172.64.37.222 +172.64.37.221 +172.64.37.220 +172.64.37.22 +172.64.37.219 +172.64.37.218 +172.64.37.217 +172.64.37.216 +172.64.37.215 +172.64.37.214 +172.64.37.213 +172.64.37.212 +172.64.37.211 +172.64.37.210 +172.64.37.21 +172.64.37.209 +172.64.37.208 +172.64.37.207 +172.64.37.206 +172.64.37.205 +172.64.37.204 +172.64.37.203 +172.64.37.201 +172.64.37.200 +172.64.37.20 +172.64.37.2 +172.64.37.199 +172.64.37.198 +172.64.37.197 +172.64.37.196 +172.64.37.195 +172.64.37.194 +172.64.37.193 +172.64.37.192 +172.64.37.191 +172.64.37.190 +172.64.37.19 +172.64.37.189 +172.64.37.188 +172.64.37.187 +172.64.37.186 +172.64.37.185 +172.64.37.184 +172.64.37.183 +172.64.37.182 +172.64.37.181 +172.64.37.180 +172.64.37.18 +172.64.37.179 +172.64.37.178 +172.64.37.177 +172.64.37.176 +172.64.37.175 +172.64.37.174 +172.64.37.173 +172.64.37.172 +172.64.37.171 +172.64.37.170 +172.64.37.17 +172.64.37.169 +172.64.37.168 +172.64.37.167 +172.64.37.166 +172.64.37.165 +172.64.37.164 +172.64.37.163 +172.64.37.162 +172.64.37.161 +172.64.37.160 +172.64.37.16 +172.64.37.159 +172.64.37.157 +172.64.37.156 +172.64.37.155 +172.64.37.154 +172.64.37.153 +172.64.37.152 +172.64.37.151 +172.64.37.150 +172.64.37.15 +172.64.37.149 +172.64.37.148 +172.64.37.147 +172.64.37.146 +172.64.37.145 +172.64.37.144 +172.64.37.143 +172.64.37.142 +172.64.37.141 +172.64.37.140 +172.64.37.14 +172.64.37.139 +172.64.37.138 +172.64.37.137 +172.64.37.136 +172.64.37.135 +172.64.37.134 +172.64.37.133 +172.64.37.132 +172.64.37.131 +172.64.37.130 +172.64.37.13 +172.64.37.129 +172.64.37.128 +172.64.37.127 +172.64.37.126 +172.64.37.125 +172.64.37.124 +172.64.37.123 +172.64.37.122 +172.64.37.121 +172.64.37.120 +172.64.37.12 +172.64.37.119 +172.64.37.118 +172.64.37.117 +172.64.37.116 +172.64.37.115 +172.64.37.114 +172.64.37.113 +172.64.37.112 +172.64.37.111 +172.64.37.110 +172.64.37.11 +172.64.37.109 +172.64.37.108 +172.64.37.107 +172.64.37.106 +172.64.37.105 +172.64.37.104 +172.64.37.103 +172.64.37.102 +172.64.37.101 +172.64.37.100 +172.64.37.10 +172.64.37.1 +172.64.37.0 +172.64.36.99 +172.64.36.98 +172.64.36.97 +172.64.36.96 +172.64.36.95 +172.64.36.94 +172.64.36.93 +172.64.36.92 +172.64.36.91 +172.64.36.90 +172.64.36.9 +172.64.36.89 +172.64.36.88 +172.64.36.87 +172.64.36.86 +172.64.36.85 +172.64.36.84 +172.64.36.83 +172.64.36.82 +172.64.36.81 +172.64.36.80 +172.64.36.8 +172.64.36.79 +172.64.36.78 +172.64.36.77 +172.64.36.76 +172.64.36.75 +172.64.36.74 +172.64.36.73 +172.64.36.72 +172.64.36.71 +172.64.36.70 +172.64.36.7 +172.64.36.69 +172.64.36.68 +172.64.36.67 +172.64.36.66 +172.64.36.65 +172.64.36.64 +172.64.36.63 +172.64.36.62 +172.64.36.61 +172.64.36.60 +172.64.36.6 +172.64.36.59 +172.64.36.58 +172.64.36.57 +172.64.36.56 +172.64.36.55 +172.64.36.54 +172.64.36.53 +172.64.36.52 +172.64.36.51 +172.64.36.50 +172.64.36.5 +172.64.36.49 +172.64.36.48 +172.64.36.47 +172.64.36.46 +172.64.36.45 +172.64.36.44 +172.64.36.43 +172.64.36.42 +172.64.36.41 +172.64.36.40 +172.64.36.4 +172.64.36.39 +172.64.36.38 +172.64.36.37 +172.64.36.36 +172.64.36.35 +172.64.36.34 +172.64.36.33 +172.64.36.32 +172.64.36.31 +172.64.36.30 +172.64.36.3 +172.64.36.29 +172.64.36.28 +172.64.36.27 +172.64.36.26 +172.64.36.255 +172.64.36.254 +172.64.36.253 +172.64.36.252 +172.64.36.251 +172.64.36.250 +172.64.36.25 +172.64.36.249 +172.64.36.248 +172.64.36.247 +172.64.36.246 +172.64.36.245 +172.64.36.244 +172.64.36.243 +172.64.36.241 +172.64.36.240 +172.64.36.24 +172.64.36.239 +172.64.36.238 +172.64.36.237 +172.64.36.236 +172.64.36.235 +172.64.36.233 +172.64.36.232 +172.64.36.231 +172.64.36.230 +172.64.36.23 +172.64.36.229 +172.64.36.228 +172.64.36.227 +172.64.36.226 +172.64.36.225 +172.64.36.224 +172.64.36.223 +172.64.36.222 +172.64.36.221 +172.64.36.22 +172.64.36.219 +172.64.36.218 +172.64.36.217 +172.64.36.216 +172.64.36.215 +172.64.36.214 +172.64.36.213 +172.64.36.212 +172.64.36.211 +172.64.36.210 +172.64.36.21 +172.64.36.209 +172.64.36.208 +172.64.36.207 +172.64.36.206 +172.64.36.205 +172.64.36.204 +172.64.36.203 +172.64.36.202 +172.64.36.201 +172.64.36.200 +172.64.36.20 +172.64.36.2 +172.64.36.199 +172.64.36.198 +172.64.36.197 +172.64.36.196 +172.64.36.195 +172.64.36.194 +172.64.36.193 +172.64.36.192 +172.64.36.191 +172.64.36.190 +172.64.36.19 +172.64.36.189 +172.64.36.188 +172.64.36.187 +172.64.36.186 +172.64.36.185 +172.64.36.184 +172.64.36.183 +172.64.36.182 +172.64.36.181 +172.64.36.180 +172.64.36.18 +172.64.36.179 +172.64.36.178 +172.64.36.177 +172.64.36.176 +172.64.36.175 +172.64.36.174 +172.64.36.173 +172.64.36.172 +172.64.36.171 +172.64.36.170 +172.64.36.17 +172.64.36.169 +172.64.36.168 +172.64.36.167 +172.64.36.166 +172.64.36.165 +172.64.36.164 +172.64.36.163 +172.64.36.162 +172.64.36.161 +172.64.36.160 +172.64.36.16 +172.64.36.159 +172.64.36.158 +172.64.36.157 +172.64.36.156 +172.64.36.155 +172.64.36.154 +172.64.36.153 +172.64.36.152 +172.64.36.151 +172.64.36.150 +172.64.36.15 +172.64.36.149 +172.64.36.148 +172.64.36.147 +172.64.36.146 +172.64.36.145 +172.64.36.144 +172.64.36.143 +172.64.36.142 +172.64.36.141 +172.64.36.140 +172.64.36.14 +172.64.36.139 +172.64.36.138 +172.64.36.137 +172.64.36.136 +172.64.36.135 +172.64.36.134 +172.64.36.133 +172.64.36.132 +172.64.36.131 +172.64.36.130 +172.64.36.13 +172.64.36.129 +172.64.36.128 +172.64.36.127 +172.64.36.126 +172.64.36.125 +172.64.36.124 +172.64.36.123 +172.64.36.122 +172.64.36.121 +172.64.36.120 +172.64.36.12 +172.64.36.119 +172.64.36.118 +172.64.36.117 +172.64.36.116 +172.64.36.115 +172.64.36.114 +172.64.36.113 +172.64.36.112 +172.64.36.111 +172.64.36.110 +172.64.36.11 +172.64.36.109 +172.64.36.108 +172.64.36.107 +172.64.36.106 +172.64.36.105 +172.64.36.104 +172.64.36.103 +172.64.36.102 +172.64.36.101 +172.64.36.100 +172.64.36.1 +172.64.36.0 +172.64.35.92 +172.64.35.87 +172.64.35.84 +172.64.35.73 +172.64.35.72 +172.64.35.71 +172.64.35.64 +172.64.35.48 +172.64.35.43 +172.64.35.42 +172.64.35.40 +172.64.35.26 +172.64.35.252 +172.64.35.230 +172.64.35.228 +172.64.35.204 +172.64.35.192 +172.64.35.17 +172.64.35.168 +172.64.35.166 +172.64.35.153 +172.64.35.150 +172.64.35.146 +172.64.35.130 +172.64.35.124 +172.64.35.111 +172.64.35.11 +172.64.35.108 +172.64.35.106 +172.64.34.88 +172.64.34.75 +172.64.34.74 +172.64.34.66 +172.64.34.56 +172.64.34.50 +172.64.34.49 +172.64.34.42 +172.64.34.33 +172.64.34.244 +172.64.34.242 +172.64.34.241 +172.64.34.235 +172.64.34.231 +172.64.34.206 +172.64.34.204 +172.64.34.202 +172.64.34.201 +172.64.34.195 +172.64.34.159 +172.64.34.151 +172.64.34.129 +172.64.34.124 +172.64.34.123 +172.64.34.121 +172.64.34.117 +172.64.33.90 +172.64.33.81 +172.64.33.76 +172.64.33.74 +172.64.33.57 +172.64.33.49 +172.64.33.47 +172.64.33.44 +172.64.33.33 +172.64.33.252 +172.64.33.25 +172.64.33.245 +172.64.33.243 +172.64.33.225 +172.64.33.218 +172.64.33.217 +172.64.33.213 +172.64.33.207 +172.64.33.206 +172.64.33.20 +172.64.33.2 +172.64.33.188 +172.64.33.18 +172.64.33.179 +172.64.33.178 +172.64.33.176 +172.64.33.144 +172.64.33.124 +172.64.33.121 +172.64.33.116 +172.64.33.107 +172.64.33.103 +172.64.33.0 +172.64.32.99 +172.64.32.58 +172.64.32.56 +172.64.32.54 +172.64.32.52 +172.64.32.5 +172.64.32.49 +172.64.32.46 +172.64.32.44 +172.64.32.39 +172.64.32.254 +172.64.32.253 +172.64.32.250 +172.64.32.247 +172.64.32.243 +172.64.32.240 +172.64.32.239 +172.64.32.238 +172.64.32.216 +172.64.32.209 +172.64.32.191 +172.64.32.179 +172.64.32.178 +172.64.32.163 +172.64.32.153 +172.64.32.135 +172.64.32.131 +172.64.32.129 +172.64.32.127 +172.64.32.119 +172.64.32.105 +172.64.32.104 +172.2.219.18 +172.109.185.34 +172.109.128.250 +172.105.152.133 +172.104.93.80 +172.104.57.181 +172.104.29.247 +171.33.152.31 +171.25.251.148 +171.244.23.49 +171.224.241.161 +170.84.108.11 +170.83.240.248 +170.81.9.4 +170.39.180.34 +170.249.203.131 +170.247.198.22 +170.246.105.242 +170.244.57.8 +170.244.57.7 +170.239.207.95 +170.239.207.211 +170.239.206.88 +170.239.206.125 +170.239.204.247 +170.239.204.239 +170.239.204.231 +170.239.204.230 +170.239.204.181 +170.239.204.175 +170.239.204.168 +170.239.204.156 +170.239.204.148 +170.239.144.20 +170.238.239.67 +170.238.212.154 +170.238.117.68 +170.238.10.65 +170.233.74.158 +170.231.205.55 +170.231.205.49 +170.231.205.44 +170.231.205.43 +170.231.205.22 +170.210.83.34 +170.150.222.243 +170.150.155.85 +170.0.15.49 +169.55.51.86 +169.55.102.246 +169.53.182.124 +169.255.135.218 +169.239.80.214 +169.239.236.101 +169.237.229.88 +168.95.192.1 +168.95.1.1 +168.93.88.114 +168.9.36.114 +168.61.172.232 +168.243.48.33 +168.235.75.84 +168.232.20.58 +168.228.51.197 +168.228.232.251 +168.227.102.42 +168.215.210.50 +168.205.124.9 +168.196.78.22 +168.196.78.18 +168.196.144.214 +168.195.135.71 +168.195.135.67 +168.181.87.38 +168.181.247.94 +168.181.247.54 +168.181.247.33 +168.181.247.29 +168.181.247.27 +168.181.247.20 +168.181.247.2 +168.181.247.124 +168.181.247.115 +168.181.247.10 +168.181.161.2 +168.154.245.252 +168.154.224.50 +168.154.160.5 +168.154.160.4 +168.126.63.2 +168.126.63.1 +168.126.246.2 +168.121.97.42 +168.121.97.36 +168.100.172.1 +167.99.168.38 +167.98.87.164 +167.98.253.194 +167.98.191.45 +167.98.176.51 +167.98.174.81 +167.98.171.242 +167.98.161.42 +167.98.161.41 +167.86.119.212 +167.86.109.163 +167.71.34.203 +167.250.99.85 +167.249.249.250 +167.235.59.243 +167.235.247.108 +167.224.103.4 +167.224.103.3 +167.172.60.99 +167.157.20.2 +167.128.4.101 +166.252.14.91 +166.203.165.254 +166.203.128.183 +166.200.113.63 +166.200.113.59 +166.200.113.122 +166.168.39.152 +166.146.42.193 +166.130.64.24 +166.102.165.32 +166.102.165.13 +166.102.165.11 +165.87.201.244 +165.87.201.242 +165.87.194.244 +165.87.13.129 +165.84.188.244 +165.73.82.119 +165.73.132.203 +165.246.10.2 +165.21.13.90 +165.166.159.198 +165.166.159.147 +165.16.68.129 +165.16.68.1 +165.16.58.124 +165.16.116.172 +165.156.20.90 +165.156.20.9 +165.156.20.80 +165.156.20.72 +165.156.20.7 +165.156.20.68 +165.156.20.65 +165.156.20.64 +165.156.20.62 +165.156.20.60 +165.156.20.6 +165.156.20.59 +165.156.20.56 +165.156.20.52 +165.156.20.49 +165.156.20.46 +165.156.20.44 +165.156.20.4 +165.156.20.30 +165.156.20.28 +165.156.20.26 +165.156.20.252 +165.156.20.243 +165.156.20.242 +165.156.20.235 +165.156.20.221 +165.156.20.217 +165.156.20.213 +165.156.20.208 +165.156.20.205 +165.156.20.203 +165.156.20.196 +165.156.20.192 +165.156.20.186 +165.156.20.174 +165.156.20.171 +165.156.20.158 +165.156.20.157 +165.156.20.155 +165.156.20.154 +165.156.20.152 +165.156.20.145 +165.156.20.144 +165.156.20.14 +165.156.20.131 +165.156.20.130 +165.156.20.129 +165.156.20.127 +165.156.20.124 +165.156.20.123 +165.156.20.121 +165.156.20.12 +165.156.20.103 +165.156.18.99 +165.156.18.98 +165.156.18.97 +165.156.18.96 +165.156.18.94 +165.156.18.93 +165.156.18.91 +165.156.18.9 +165.156.18.88 +165.156.18.87 +165.156.18.86 +165.156.18.85 +165.156.18.84 +165.156.18.83 +165.156.18.82 +165.156.18.80 +165.156.18.8 +165.156.18.78 +165.156.18.77 +165.156.18.76 +165.156.18.75 +165.156.18.74 +165.156.18.73 +165.156.18.71 +165.156.18.70 +165.156.18.7 +165.156.18.69 +165.156.18.68 +165.156.18.67 +165.156.18.64 +165.156.18.63 +165.156.18.62 +165.156.18.60 +165.156.18.6 +165.156.18.59 +165.156.18.57 +165.156.18.54 +165.156.18.52 +165.156.18.51 +165.156.18.50 +165.156.18.49 +165.156.18.46 +165.156.18.44 +165.156.18.43 +165.156.18.41 +165.156.18.4 +165.156.18.38 +165.156.18.37 +165.156.18.36 +165.156.18.32 +165.156.18.30 +165.156.18.27 +165.156.18.26 +165.156.18.254 +165.156.18.253 +165.156.18.251 +165.156.18.250 +165.156.18.25 +165.156.18.249 +165.156.18.244 +165.156.18.242 +165.156.18.24 +165.156.18.237 +165.156.18.236 +165.156.18.235 +165.156.18.232 +165.156.18.231 +165.156.18.230 +165.156.18.23 +165.156.18.228 +165.156.18.227 +165.156.18.223 +165.156.18.222 +165.156.18.220 +165.156.18.22 +165.156.18.213 +165.156.18.21 +165.156.18.209 +165.156.18.208 +165.156.18.207 +165.156.18.206 +165.156.18.203 +165.156.18.202 +165.156.18.201 +165.156.18.200 +165.156.18.20 +165.156.18.2 +165.156.18.198 +165.156.18.195 +165.156.18.192 +165.156.18.191 +165.156.18.190 +165.156.18.19 +165.156.18.187 +165.156.18.186 +165.156.18.185 +165.156.18.182 +165.156.18.181 +165.156.18.18 +165.156.18.177 +165.156.18.176 +165.156.18.171 +165.156.18.17 +165.156.18.169 +165.156.18.167 +165.156.18.166 +165.156.18.165 +165.156.18.164 +165.156.18.163 +165.156.18.162 +165.156.18.160 +165.156.18.16 +165.156.18.159 +165.156.18.158 +165.156.18.157 +165.156.18.156 +165.156.18.155 +165.156.18.154 +165.156.18.152 +165.156.18.15 +165.156.18.147 +165.156.18.145 +165.156.18.144 +165.156.18.143 +165.156.18.141 +165.156.18.140 +165.156.18.14 +165.156.18.137 +165.156.18.136 +165.156.18.135 +165.156.18.134 +165.156.18.133 +165.156.18.131 +165.156.18.130 +165.156.18.128 +165.156.18.127 +165.156.18.126 +165.156.18.125 +165.156.18.124 +165.156.18.122 +165.156.18.120 +165.156.18.119 +165.156.18.118 +165.156.18.116 +165.156.18.115 +165.156.18.114 +165.156.18.113 +165.156.18.111 +165.156.18.11 +165.156.18.108 +165.156.18.106 +165.156.18.105 +165.156.18.104 +165.156.18.102 +165.156.18.101 +165.156.18.100 +165.156.18.10 +165.156.18.1 +165.156.17.99 +165.156.17.98 +165.156.17.96 +165.156.17.95 +165.156.17.94 +165.156.17.93 +165.156.17.92 +165.156.17.91 +165.156.17.90 +165.156.17.88 +165.156.17.87 +165.156.17.86 +165.156.17.84 +165.156.17.83 +165.156.17.82 +165.156.17.81 +165.156.17.80 +165.156.17.8 +165.156.17.79 +165.156.17.78 +165.156.17.77 +165.156.17.76 +165.156.17.75 +165.156.17.74 +165.156.17.73 +165.156.17.72 +165.156.17.71 +165.156.17.70 +165.156.17.7 +165.156.17.69 +165.156.17.68 +165.156.17.66 +165.156.17.65 +165.156.17.64 +165.156.17.63 +165.156.17.61 +165.156.17.59 +165.156.17.58 +165.156.17.57 +165.156.17.55 +165.156.17.54 +165.156.17.53 +165.156.17.50 +165.156.17.5 +165.156.17.47 +165.156.17.46 +165.156.17.45 +165.156.17.44 +165.156.17.42 +165.156.17.41 +165.156.17.4 +165.156.17.39 +165.156.17.37 +165.156.17.36 +165.156.17.34 +165.156.17.33 +165.156.17.32 +165.156.17.31 +165.156.17.3 +165.156.17.28 +165.156.17.27 +165.156.17.26 +165.156.17.252 +165.156.17.251 +165.156.17.250 +165.156.17.25 +165.156.17.248 +165.156.17.247 +165.156.17.246 +165.156.17.244 +165.156.17.243 +165.156.17.242 +165.156.17.241 +165.156.17.240 +165.156.17.24 +165.156.17.239 +165.156.17.236 +165.156.17.235 +165.156.17.234 +165.156.17.233 +165.156.17.232 +165.156.17.231 +165.156.17.23 +165.156.17.229 +165.156.17.228 +165.156.17.227 +165.156.17.225 +165.156.17.224 +165.156.17.223 +165.156.17.222 +165.156.17.220 +165.156.17.22 +165.156.17.219 +165.156.17.218 +165.156.17.216 +165.156.17.215 +165.156.17.214 +165.156.17.212 +165.156.17.211 +165.156.17.210 +165.156.17.21 +165.156.17.206 +165.156.17.205 +165.156.17.202 +165.156.17.20 +165.156.17.2 +165.156.17.197 +165.156.17.196 +165.156.17.195 +165.156.17.194 +165.156.17.193 +165.156.17.192 +165.156.17.190 +165.156.17.19 +165.156.17.189 +165.156.17.188 +165.156.17.187 +165.156.17.185 +165.156.17.184 +165.156.17.183 +165.156.17.182 +165.156.17.181 +165.156.17.180 +165.156.17.18 +165.156.17.178 +165.156.17.177 +165.156.17.176 +165.156.17.174 +165.156.17.173 +165.156.17.172 +165.156.17.171 +165.156.17.170 +165.156.17.17 +165.156.17.169 +165.156.17.168 +165.156.17.167 +165.156.17.166 +165.156.17.163 +165.156.17.161 +165.156.17.160 +165.156.17.159 +165.156.17.158 +165.156.17.155 +165.156.17.153 +165.156.17.152 +165.156.17.15 +165.156.17.148 +165.156.17.147 +165.156.17.146 +165.156.17.145 +165.156.17.144 +165.156.17.143 +165.156.17.142 +165.156.17.141 +165.156.17.140 +165.156.17.14 +165.156.17.138 +165.156.17.137 +165.156.17.136 +165.156.17.131 +165.156.17.130 +165.156.17.13 +165.156.17.129 +165.156.17.128 +165.156.17.127 +165.156.17.126 +165.156.17.124 +165.156.17.123 +165.156.17.122 +165.156.17.121 +165.156.17.120 +165.156.17.119 +165.156.17.117 +165.156.17.116 +165.156.17.115 +165.156.17.114 +165.156.17.112 +165.156.17.110 +165.156.17.11 +165.156.17.109 +165.156.17.108 +165.156.17.107 +165.156.17.105 +165.156.17.104 +165.156.17.103 +165.156.17.102 +165.156.17.101 +165.156.17.100 +165.156.17.10 +165.156.17.1 +165.156.16.97 +165.156.16.95 +165.156.16.93 +165.156.16.92 +165.156.16.86 +165.156.16.85 +165.156.16.83 +165.156.16.82 +165.156.16.81 +165.156.16.80 +165.156.16.8 +165.156.16.78 +165.156.16.75 +165.156.16.73 +165.156.16.72 +165.156.16.71 +165.156.16.70 +165.156.16.7 +165.156.16.68 +165.156.16.67 +165.156.16.64 +165.156.16.62 +165.156.16.61 +165.156.16.51 +165.156.16.50 +165.156.16.5 +165.156.16.49 +165.156.16.48 +165.156.16.45 +165.156.16.44 +165.156.16.41 +165.156.16.40 +165.156.16.35 +165.156.16.32 +165.156.16.31 +165.156.16.3 +165.156.16.29 +165.156.16.28 +165.156.16.26 +165.156.16.254 +165.156.16.253 +165.156.16.251 +165.156.16.250 +165.156.16.25 +165.156.16.248 +165.156.16.247 +165.156.16.241 +165.156.16.240 +165.156.16.232 +165.156.16.228 +165.156.16.227 +165.156.16.226 +165.156.16.224 +165.156.16.222 +165.156.16.220 +165.156.16.218 +165.156.16.216 +165.156.16.215 +165.156.16.214 +165.156.16.213 +165.156.16.212 +165.156.16.208 +165.156.16.207 +165.156.16.206 +165.156.16.205 +165.156.16.204 +165.156.16.203 +165.156.16.202 +165.156.16.20 +165.156.16.2 +165.156.16.198 +165.156.16.197 +165.156.16.196 +165.156.16.192 +165.156.16.191 +165.156.16.19 +165.156.16.189 +165.156.16.186 +165.156.16.184 +165.156.16.183 +165.156.16.18 +165.156.16.178 +165.156.16.177 +165.156.16.176 +165.156.16.175 +165.156.16.173 +165.156.16.172 +165.156.16.168 +165.156.16.165 +165.156.16.163 +165.156.16.160 +165.156.16.16 +165.156.16.159 +165.156.16.158 +165.156.16.157 +165.156.16.156 +165.156.16.154 +165.156.16.151 +165.156.16.149 +165.156.16.148 +165.156.16.147 +165.156.16.143 +165.156.16.142 +165.156.16.141 +165.156.16.140 +165.156.16.14 +165.156.16.133 +165.156.16.131 +165.156.16.130 +165.156.16.13 +165.156.16.129 +165.156.16.127 +165.156.16.126 +165.156.16.124 +165.156.16.122 +165.156.16.121 +165.156.16.120 +165.156.16.119 +165.156.16.118 +165.156.16.116 +165.156.16.112 +165.156.16.109 +165.156.16.108 +165.156.16.104 +165.156.16.102 +165.156.16.101 +165.156.16.100 +165.156.16.10 +165.140.185.34 +165.140.185.254 +164.77.156.235 +164.77.129.37 +164.68.108.7 +164.68.108.101 +164.163.74.82 +164.163.133.21 +164.163.1.90 +164.132.210.88 +164.132.170.198 +164.132.167.189 +164.124.107.9 +164.124.101.2 +163.47.202.150 +163.44.49.226 +163.182.174.241 +163.172.31.111 +162.75.12.97 +162.75.12.201 +162.253.133.97 +162.251.82.99 +162.251.82.98 +162.251.82.97 +162.251.82.96 +162.251.82.95 +162.251.82.94 +162.251.82.93 +162.251.82.92 +162.251.82.91 +162.251.82.90 +162.251.82.9 +162.251.82.89 +162.251.82.88 +162.251.82.87 +162.251.82.86 +162.251.82.85 +162.251.82.84 +162.251.82.83 +162.251.82.82 +162.251.82.81 +162.251.82.80 +162.251.82.8 +162.251.82.79 +162.251.82.78 +162.251.82.77 +162.251.82.76 +162.251.82.75 +162.251.82.74 +162.251.82.73 +162.251.82.72 +162.251.82.71 +162.251.82.7 +162.251.82.69 +162.251.82.68 +162.251.82.67 +162.251.82.66 +162.251.82.65 +162.251.82.64 +162.251.82.63 +162.251.82.62 +162.251.82.61 +162.251.82.60 +162.251.82.6 +162.251.82.59 +162.251.82.58 +162.251.82.57 +162.251.82.56 +162.251.82.55 +162.251.82.54 +162.251.82.53 +162.251.82.52 +162.251.82.51 +162.251.82.50 +162.251.82.5 +162.251.82.49 +162.251.82.48 +162.251.82.47 +162.251.82.46 +162.251.82.45 +162.251.82.44 +162.251.82.42 +162.251.82.41 +162.251.82.40 +162.251.82.4 +162.251.82.39 +162.251.82.37 +162.251.82.36 +162.251.82.35 +162.251.82.34 +162.251.82.33 +162.251.82.32 +162.251.82.31 +162.251.82.30 +162.251.82.3 +162.251.82.29 +162.251.82.28 +162.251.82.27 +162.251.82.25 +162.251.82.245 +162.251.82.243 +162.251.82.242 +162.251.82.241 +162.251.82.240 +162.251.82.24 +162.251.82.239 +162.251.82.238 +162.251.82.237 +162.251.82.236 +162.251.82.235 +162.251.82.234 +162.251.82.233 +162.251.82.232 +162.251.82.231 +162.251.82.230 +162.251.82.23 +162.251.82.229 +162.251.82.228 +162.251.82.227 +162.251.82.226 +162.251.82.225 +162.251.82.224 +162.251.82.223 +162.251.82.222 +162.251.82.221 +162.251.82.220 +162.251.82.22 +162.251.82.219 +162.251.82.218 +162.251.82.217 +162.251.82.216 +162.251.82.215 +162.251.82.214 +162.251.82.213 +162.251.82.212 +162.251.82.211 +162.251.82.210 +162.251.82.21 +162.251.82.209 +162.251.82.208 +162.251.82.207 +162.251.82.206 +162.251.82.205 +162.251.82.204 +162.251.82.203 +162.251.82.202 +162.251.82.201 +162.251.82.200 +162.251.82.20 +162.251.82.199 +162.251.82.198 +162.251.82.197 +162.251.82.196 +162.251.82.195 +162.251.82.194 +162.251.82.193 +162.251.82.192 +162.251.82.191 +162.251.82.190 +162.251.82.19 +162.251.82.189 +162.251.82.188 +162.251.82.187 +162.251.82.186 +162.251.82.185 +162.251.82.184 +162.251.82.183 +162.251.82.182 +162.251.82.181 +162.251.82.180 +162.251.82.18 +162.251.82.179 +162.251.82.178 +162.251.82.177 +162.251.82.176 +162.251.82.175 +162.251.82.174 +162.251.82.173 +162.251.82.172 +162.251.82.171 +162.251.82.170 +162.251.82.17 +162.251.82.169 +162.251.82.168 +162.251.82.167 +162.251.82.166 +162.251.82.165 +162.251.82.164 +162.251.82.163 +162.251.82.162 +162.251.82.161 +162.251.82.160 +162.251.82.16 +162.251.82.159 +162.251.82.158 +162.251.82.157 +162.251.82.156 +162.251.82.155 +162.251.82.154 +162.251.82.153 +162.251.82.152 +162.251.82.151 +162.251.82.150 +162.251.82.15 +162.251.82.149 +162.251.82.148 +162.251.82.147 +162.251.82.146 +162.251.82.145 +162.251.82.144 +162.251.82.143 +162.251.82.142 +162.251.82.141 +162.251.82.140 +162.251.82.14 +162.251.82.139 +162.251.82.138 +162.251.82.137 +162.251.82.136 +162.251.82.135 +162.251.82.134 +162.251.82.133 +162.251.82.132 +162.251.82.131 +162.251.82.130 +162.251.82.13 +162.251.82.129 +162.251.82.128 +162.251.82.127 +162.251.82.126 +162.251.82.12 +162.251.82.117 +162.251.82.116 +162.251.82.115 +162.251.82.114 +162.251.82.113 +162.251.82.112 +162.251.82.111 +162.251.82.110 +162.251.82.11 +162.251.82.109 +162.251.82.108 +162.251.82.107 +162.251.82.106 +162.251.82.105 +162.251.82.104 +162.251.82.103 +162.251.82.102 +162.251.82.101 +162.251.82.100 +162.251.82.10 +162.251.82.1 +162.251.82.0 +162.251.158.88 +162.251.146.91 +162.247.183.205 +162.246.127.108 +162.243.172.61 +162.241.132.129 +162.223.90.104 +162.221.187.229 +162.218.154.2 +162.212.19.235 +162.211.33.243 +162.210.104.17 +162.191.88.204 +162.191.201.179 +162.19.92.206 +162.19.58.10 +162.17.81.57 +162.159.9.87 +162.159.9.83 +162.159.9.78 +162.159.9.68 +162.159.9.66 +162.159.9.53 +162.159.9.46 +162.159.9.4 +162.159.9.31 +162.159.9.29 +162.159.9.250 +162.159.9.248 +162.159.9.24 +162.159.9.238 +162.159.9.233 +162.159.9.229 +162.159.9.219 +162.159.9.216 +162.159.9.214 +162.159.9.211 +162.159.9.21 +162.159.9.204 +162.159.9.185 +162.159.9.167 +162.159.9.160 +162.159.9.159 +162.159.9.155 +162.159.9.147 +162.159.9.142 +162.159.9.140 +162.159.9.135 +162.159.9.119 +162.159.9.105 +162.159.9.103 +162.159.8.92 +162.159.8.82 +162.159.8.80 +162.159.8.76 +162.159.8.46 +162.159.8.45 +162.159.8.38 +162.159.8.36 +162.159.8.32 +162.159.8.29 +162.159.8.27 +162.159.8.254 +162.159.8.252 +162.159.8.241 +162.159.8.232 +162.159.8.215 +162.159.8.21 +162.159.8.194 +162.159.8.192 +162.159.8.175 +162.159.8.159 +162.159.8.146 +162.159.8.136 +162.159.8.128 +162.159.8.118 +162.159.8.114 +162.159.8.111 +162.159.8.100 +162.159.7.99 +162.159.7.89 +162.159.7.85 +162.159.7.84 +162.159.7.79 +162.159.7.77 +162.159.7.248 +162.159.7.247 +162.159.7.215 +162.159.7.211 +162.159.7.201 +162.159.7.199 +162.159.7.188 +162.159.7.183 +162.159.7.18 +162.159.7.164 +162.159.7.163 +162.159.7.159 +162.159.7.149 +162.159.7.140 +162.159.7.139 +162.159.7.129 +162.159.7.126 +162.159.7.119 +162.159.7.113 +162.159.7.101 +162.159.7.10 +162.159.7.1 +162.159.6.99 +162.159.6.64 +162.159.6.62 +162.159.6.45 +162.159.6.35 +162.159.6.28 +162.159.6.254 +162.159.6.252 +162.159.6.250 +162.159.6.229 +162.159.6.215 +162.159.6.211 +162.159.6.203 +162.159.6.189 +162.159.6.178 +162.159.6.177 +162.159.6.175 +162.159.6.169 +162.159.6.138 +162.159.6.134 +162.159.6.123 +162.159.6.107 +162.159.58.98 +162.159.58.94 +162.159.58.92 +162.159.58.89 +162.159.58.86 +162.159.58.37 +162.159.58.251 +162.159.58.223 +162.159.58.219 +162.159.58.217 +162.159.58.214 +162.159.58.201 +162.159.58.198 +162.159.58.188 +162.159.58.186 +162.159.58.185 +162.159.58.183 +162.159.58.179 +162.159.58.166 +162.159.58.156 +162.159.58.14 +162.159.58.133 +162.159.58.131 +162.159.58.125 +162.159.58.119 +162.159.58.115 +162.159.58.111 +162.159.58.110 +162.159.58.102 +162.159.56.86 +162.159.56.84 +162.159.56.75 +162.159.56.59 +162.159.56.47 +162.159.56.43 +162.159.56.18 +162.159.50.79 +162.159.50.74 +162.159.50.4 +162.159.50.3 +162.159.50.27 +162.159.5.73 +162.159.5.71 +162.159.5.64 +162.159.5.49 +162.159.5.44 +162.159.5.31 +162.159.5.228 +162.159.5.226 +162.159.5.22 +162.159.5.211 +162.159.5.206 +162.159.5.20 +162.159.5.194 +162.159.5.18 +162.159.5.171 +162.159.5.170 +162.159.5.165 +162.159.5.161 +162.159.5.152 +162.159.5.149 +162.159.5.147 +162.159.5.137 +162.159.5.125 +162.159.5.123 +162.159.5.118 +162.159.5.104 +162.159.5.101 +162.159.46.92 +162.159.46.90 +162.159.46.8 +162.159.46.73 +162.159.46.71 +162.159.46.70 +162.159.46.56 +162.159.46.55 +162.159.46.53 +162.159.46.51 +162.159.46.48 +162.159.46.47 +162.159.46.42 +162.159.46.38 +162.159.46.28 +162.159.46.26 +162.159.46.250 +162.159.46.249 +162.159.46.247 +162.159.46.239 +162.159.46.232 +162.159.46.23 +162.159.46.224 +162.159.46.223 +162.159.46.221 +162.159.46.219 +162.159.46.218 +162.159.46.214 +162.159.46.202 +162.159.46.197 +162.159.46.194 +162.159.46.190 +162.159.46.185 +162.159.46.182 +162.159.46.18 +162.159.46.177 +162.159.46.175 +162.159.46.172 +162.159.46.167 +162.159.46.166 +162.159.46.165 +162.159.46.161 +162.159.46.151 +162.159.46.15 +162.159.46.147 +162.159.46.144 +162.159.46.134 +162.159.46.120 +162.159.46.119 +162.159.46.117 +162.159.46.115 +162.159.46.1 +162.159.46.0 +162.159.45.99 +162.159.45.98 +162.159.45.97 +162.159.45.96 +162.159.45.95 +162.159.45.94 +162.159.45.93 +162.159.45.92 +162.159.45.91 +162.159.45.90 +162.159.45.9 +162.159.45.89 +162.159.45.88 +162.159.45.87 +162.159.45.86 +162.159.45.85 +162.159.45.84 +162.159.45.83 +162.159.45.82 +162.159.45.81 +162.159.45.80 +162.159.45.8 +162.159.45.79 +162.159.45.78 +162.159.45.77 +162.159.45.76 +162.159.45.75 +162.159.45.74 +162.159.45.73 +162.159.45.72 +162.159.45.71 +162.159.45.70 +162.159.45.7 +162.159.45.69 +162.159.45.68 +162.159.45.67 +162.159.45.66 +162.159.45.65 +162.159.45.64 +162.159.45.63 +162.159.45.62 +162.159.45.61 +162.159.45.60 +162.159.45.6 +162.159.45.59 +162.159.45.58 +162.159.45.57 +162.159.45.56 +162.159.45.55 +162.159.45.54 +162.159.45.53 +162.159.45.52 +162.159.45.51 +162.159.45.50 +162.159.45.5 +162.159.45.49 +162.159.45.48 +162.159.45.47 +162.159.45.46 +162.159.45.45 +162.159.45.44 +162.159.45.43 +162.159.45.42 +162.159.45.41 +162.159.45.40 +162.159.45.4 +162.159.45.39 +162.159.45.38 +162.159.45.37 +162.159.45.36 +162.159.45.35 +162.159.45.34 +162.159.45.33 +162.159.45.32 +162.159.45.31 +162.159.45.30 +162.159.45.3 +162.159.45.29 +162.159.45.28 +162.159.45.27 +162.159.45.26 +162.159.45.254 +162.159.45.253 +162.159.45.252 +162.159.45.251 +162.159.45.250 +162.159.45.25 +162.159.45.249 +162.159.45.248 +162.159.45.247 +162.159.45.246 +162.159.45.245 +162.159.45.244 +162.159.45.243 +162.159.45.242 +162.159.45.241 +162.159.45.240 +162.159.45.24 +162.159.45.239 +162.159.45.238 +162.159.45.237 +162.159.45.236 +162.159.45.235 +162.159.45.234 +162.159.45.233 +162.159.45.232 +162.159.45.231 +162.159.45.230 +162.159.45.23 +162.159.45.229 +162.159.45.228 +162.159.45.227 +162.159.45.226 +162.159.45.225 +162.159.45.224 +162.159.45.223 +162.159.45.222 +162.159.45.221 +162.159.45.220 +162.159.45.22 +162.159.45.219 +162.159.45.218 +162.159.45.217 +162.159.45.216 +162.159.45.215 +162.159.45.214 +162.159.45.213 +162.159.45.212 +162.159.45.211 +162.159.45.210 +162.159.45.21 +162.159.45.209 +162.159.45.208 +162.159.45.207 +162.159.45.206 +162.159.45.205 +162.159.45.204 +162.159.45.203 +162.159.45.202 +162.159.45.201 +162.159.45.200 +162.159.45.20 +162.159.45.2 +162.159.45.199 +162.159.45.198 +162.159.45.197 +162.159.45.196 +162.159.45.195 +162.159.45.194 +162.159.45.193 +162.159.45.192 +162.159.45.191 +162.159.45.190 +162.159.45.19 +162.159.45.189 +162.159.45.188 +162.159.45.187 +162.159.45.186 +162.159.45.185 +162.159.45.184 +162.159.45.183 +162.159.45.182 +162.159.45.181 +162.159.45.180 +162.159.45.18 +162.159.45.179 +162.159.45.178 +162.159.45.177 +162.159.45.176 +162.159.45.175 +162.159.45.174 +162.159.45.173 +162.159.45.172 +162.159.45.171 +162.159.45.170 +162.159.45.17 +162.159.45.169 +162.159.45.168 +162.159.45.167 +162.159.45.166 +162.159.45.165 +162.159.45.164 +162.159.45.163 +162.159.45.162 +162.159.45.161 +162.159.45.160 +162.159.45.16 +162.159.45.159 +162.159.45.158 +162.159.45.157 +162.159.45.156 +162.159.45.155 +162.159.45.154 +162.159.45.153 +162.159.45.152 +162.159.45.151 +162.159.45.150 +162.159.45.15 +162.159.45.149 +162.159.45.148 +162.159.45.147 +162.159.45.146 +162.159.45.145 +162.159.45.144 +162.159.45.143 +162.159.45.142 +162.159.45.141 +162.159.45.140 +162.159.45.14 +162.159.45.139 +162.159.45.138 +162.159.45.137 +162.159.45.136 +162.159.45.135 +162.159.45.134 +162.159.45.133 +162.159.45.132 +162.159.45.131 +162.159.45.130 +162.159.45.13 +162.159.45.129 +162.159.45.128 +162.159.45.127 +162.159.45.126 +162.159.45.125 +162.159.45.124 +162.159.45.123 +162.159.45.122 +162.159.45.121 +162.159.45.120 +162.159.45.12 +162.159.45.119 +162.159.45.118 +162.159.45.117 +162.159.45.116 +162.159.45.115 +162.159.45.114 +162.159.45.113 +162.159.45.112 +162.159.45.111 +162.159.45.110 +162.159.45.11 +162.159.45.109 +162.159.45.108 +162.159.45.107 +162.159.45.106 +162.159.45.105 +162.159.45.104 +162.159.45.103 +162.159.45.102 +162.159.45.101 +162.159.45.100 +162.159.45.10 +162.159.45.1 +162.159.45.0 +162.159.44.99 +162.159.44.98 +162.159.44.97 +162.159.44.96 +162.159.44.95 +162.159.44.94 +162.159.44.93 +162.159.44.92 +162.159.44.91 +162.159.44.90 +162.159.44.9 +162.159.44.89 +162.159.44.88 +162.159.44.87 +162.159.44.86 +162.159.44.85 +162.159.44.84 +162.159.44.83 +162.159.44.82 +162.159.44.81 +162.159.44.80 +162.159.44.8 +162.159.44.79 +162.159.44.78 +162.159.44.77 +162.159.44.76 +162.159.44.75 +162.159.44.74 +162.159.44.73 +162.159.44.72 +162.159.44.71 +162.159.44.70 +162.159.44.7 +162.159.44.69 +162.159.44.68 +162.159.44.67 +162.159.44.66 +162.159.44.65 +162.159.44.64 +162.159.44.63 +162.159.44.62 +162.159.44.61 +162.159.44.60 +162.159.44.6 +162.159.44.59 +162.159.44.58 +162.159.44.57 +162.159.44.56 +162.159.44.55 +162.159.44.54 +162.159.44.53 +162.159.44.52 +162.159.44.51 +162.159.44.50 +162.159.44.5 +162.159.44.49 +162.159.44.48 +162.159.44.47 +162.159.44.46 +162.159.44.45 +162.159.44.44 +162.159.44.43 +162.159.44.42 +162.159.44.41 +162.159.44.40 +162.159.44.4 +162.159.44.39 +162.159.44.38 +162.159.44.37 +162.159.44.36 +162.159.44.35 +162.159.44.34 +162.159.44.33 +162.159.44.32 +162.159.44.31 +162.159.44.30 +162.159.44.3 +162.159.44.29 +162.159.44.28 +162.159.44.27 +162.159.44.26 +162.159.44.255 +162.159.44.254 +162.159.44.253 +162.159.44.252 +162.159.44.251 +162.159.44.250 +162.159.44.25 +162.159.44.249 +162.159.44.248 +162.159.44.247 +162.159.44.246 +162.159.44.245 +162.159.44.244 +162.159.44.243 +162.159.44.242 +162.159.44.241 +162.159.44.240 +162.159.44.24 +162.159.44.239 +162.159.44.238 +162.159.44.237 +162.159.44.236 +162.159.44.235 +162.159.44.234 +162.159.44.233 +162.159.44.232 +162.159.44.231 +162.159.44.230 +162.159.44.23 +162.159.44.229 +162.159.44.228 +162.159.44.227 +162.159.44.226 +162.159.44.225 +162.159.44.224 +162.159.44.223 +162.159.44.222 +162.159.44.221 +162.159.44.220 +162.159.44.22 +162.159.44.219 +162.159.44.218 +162.159.44.217 +162.159.44.216 +162.159.44.215 +162.159.44.214 +162.159.44.213 +162.159.44.212 +162.159.44.211 +162.159.44.210 +162.159.44.21 +162.159.44.209 +162.159.44.208 +162.159.44.207 +162.159.44.206 +162.159.44.205 +162.159.44.204 +162.159.44.203 +162.159.44.202 +162.159.44.201 +162.159.44.200 +162.159.44.20 +162.159.44.2 +162.159.44.199 +162.159.44.198 +162.159.44.197 +162.159.44.196 +162.159.44.195 +162.159.44.194 +162.159.44.193 +162.159.44.192 +162.159.44.191 +162.159.44.190 +162.159.44.19 +162.159.44.189 +162.159.44.188 +162.159.44.187 +162.159.44.186 +162.159.44.185 +162.159.44.184 +162.159.44.183 +162.159.44.182 +162.159.44.181 +162.159.44.180 +162.159.44.18 +162.159.44.179 +162.159.44.178 +162.159.44.177 +162.159.44.176 +162.159.44.175 +162.159.44.174 +162.159.44.173 +162.159.44.172 +162.159.44.171 +162.159.44.170 +162.159.44.17 +162.159.44.169 +162.159.44.168 +162.159.44.167 +162.159.44.166 +162.159.44.165 +162.159.44.164 +162.159.44.163 +162.159.44.162 +162.159.44.161 +162.159.44.160 +162.159.44.16 +162.159.44.159 +162.159.44.158 +162.159.44.157 +162.159.44.156 +162.159.44.155 +162.159.44.154 +162.159.44.153 +162.159.44.152 +162.159.44.151 +162.159.44.150 +162.159.44.15 +162.159.44.149 +162.159.44.148 +162.159.44.147 +162.159.44.146 +162.159.44.145 +162.159.44.144 +162.159.44.143 +162.159.44.142 +162.159.44.141 +162.159.44.140 +162.159.44.14 +162.159.44.139 +162.159.44.138 +162.159.44.137 +162.159.44.136 +162.159.44.135 +162.159.44.134 +162.159.44.133 +162.159.44.132 +162.159.44.131 +162.159.44.130 +162.159.44.13 +162.159.44.129 +162.159.44.128 +162.159.44.127 +162.159.44.126 +162.159.44.125 +162.159.44.124 +162.159.44.123 +162.159.44.122 +162.159.44.121 +162.159.44.120 +162.159.44.12 +162.159.44.119 +162.159.44.118 +162.159.44.117 +162.159.44.116 +162.159.44.115 +162.159.44.114 +162.159.44.113 +162.159.44.112 +162.159.44.111 +162.159.44.110 +162.159.44.11 +162.159.44.109 +162.159.44.108 +162.159.44.107 +162.159.44.106 +162.159.44.105 +162.159.44.104 +162.159.44.103 +162.159.44.102 +162.159.44.101 +162.159.44.100 +162.159.44.10 +162.159.44.1 +162.159.44.0 +162.159.43.99 +162.159.43.98 +162.159.43.97 +162.159.43.96 +162.159.43.95 +162.159.43.94 +162.159.43.93 +162.159.43.92 +162.159.43.91 +162.159.43.90 +162.159.43.9 +162.159.43.89 +162.159.43.88 +162.159.43.87 +162.159.43.86 +162.159.43.85 +162.159.43.84 +162.159.43.83 +162.159.43.82 +162.159.43.81 +162.159.43.80 +162.159.43.8 +162.159.43.79 +162.159.43.78 +162.159.43.77 +162.159.43.76 +162.159.43.75 +162.159.43.74 +162.159.43.73 +162.159.43.72 +162.159.43.71 +162.159.43.70 +162.159.43.7 +162.159.43.69 +162.159.43.68 +162.159.43.67 +162.159.43.66 +162.159.43.65 +162.159.43.64 +162.159.43.63 +162.159.43.62 +162.159.43.61 +162.159.43.60 +162.159.43.6 +162.159.43.59 +162.159.43.58 +162.159.43.57 +162.159.43.56 +162.159.43.55 +162.159.43.54 +162.159.43.53 +162.159.43.52 +162.159.43.51 +162.159.43.50 +162.159.43.5 +162.159.43.49 +162.159.43.48 +162.159.43.47 +162.159.43.46 +162.159.43.45 +162.159.43.44 +162.159.43.43 +162.159.43.42 +162.159.43.41 +162.159.43.40 +162.159.43.4 +162.159.43.39 +162.159.43.38 +162.159.43.37 +162.159.43.36 +162.159.43.35 +162.159.43.34 +162.159.43.33 +162.159.43.32 +162.159.43.31 +162.159.43.30 +162.159.43.3 +162.159.43.29 +162.159.43.28 +162.159.43.27 +162.159.43.26 +162.159.43.254 +162.159.43.253 +162.159.43.252 +162.159.43.251 +162.159.43.250 +162.159.43.25 +162.159.43.249 +162.159.43.248 +162.159.43.247 +162.159.43.246 +162.159.43.245 +162.159.43.244 +162.159.43.243 +162.159.43.242 +162.159.43.241 +162.159.43.240 +162.159.43.24 +162.159.43.239 +162.159.43.238 +162.159.43.237 +162.159.43.236 +162.159.43.235 +162.159.43.234 +162.159.43.233 +162.159.43.232 +162.159.43.231 +162.159.43.230 +162.159.43.23 +162.159.43.229 +162.159.43.228 +162.159.43.227 +162.159.43.226 +162.159.43.225 +162.159.43.224 +162.159.43.223 +162.159.43.222 +162.159.43.221 +162.159.43.220 +162.159.43.22 +162.159.43.219 +162.159.43.218 +162.159.43.217 +162.159.43.216 +162.159.43.215 +162.159.43.214 +162.159.43.213 +162.159.43.212 +162.159.43.211 +162.159.43.210 +162.159.43.21 +162.159.43.209 +162.159.43.208 +162.159.43.207 +162.159.43.206 +162.159.43.205 +162.159.43.204 +162.159.43.203 +162.159.43.202 +162.159.43.201 +162.159.43.200 +162.159.43.20 +162.159.43.2 +162.159.43.199 +162.159.43.198 +162.159.43.197 +162.159.43.196 +162.159.43.195 +162.159.43.194 +162.159.43.193 +162.159.43.192 +162.159.43.191 +162.159.43.190 +162.159.43.19 +162.159.43.189 +162.159.43.188 +162.159.43.187 +162.159.43.186 +162.159.43.185 +162.159.43.184 +162.159.43.183 +162.159.43.182 +162.159.43.181 +162.159.43.180 +162.159.43.18 +162.159.43.179 +162.159.43.178 +162.159.43.177 +162.159.43.176 +162.159.43.175 +162.159.43.174 +162.159.43.173 +162.159.43.172 +162.159.43.171 +162.159.43.170 +162.159.43.17 +162.159.43.169 +162.159.43.168 +162.159.43.167 +162.159.43.166 +162.159.43.165 +162.159.43.164 +162.159.43.163 +162.159.43.162 +162.159.43.161 +162.159.43.160 +162.159.43.16 +162.159.43.159 +162.159.43.158 +162.159.43.157 +162.159.43.156 +162.159.43.155 +162.159.43.154 +162.159.43.153 +162.159.43.152 +162.159.43.151 +162.159.43.150 +162.159.43.15 +162.159.43.149 +162.159.43.148 +162.159.43.147 +162.159.43.146 +162.159.43.145 +162.159.43.144 +162.159.43.143 +162.159.43.142 +162.159.43.141 +162.159.43.140 +162.159.43.14 +162.159.43.139 +162.159.43.138 +162.159.43.137 +162.159.43.136 +162.159.43.135 +162.159.43.134 +162.159.43.133 +162.159.43.132 +162.159.43.131 +162.159.43.130 +162.159.43.13 +162.159.43.129 +162.159.43.128 +162.159.43.127 +162.159.43.126 +162.159.43.125 +162.159.43.124 +162.159.43.123 +162.159.43.122 +162.159.43.121 +162.159.43.120 +162.159.43.12 +162.159.43.119 +162.159.43.118 +162.159.43.117 +162.159.43.116 +162.159.43.115 +162.159.43.114 +162.159.43.113 +162.159.43.112 +162.159.43.111 +162.159.43.110 +162.159.43.11 +162.159.43.109 +162.159.43.108 +162.159.43.107 +162.159.43.106 +162.159.43.105 +162.159.43.104 +162.159.43.103 +162.159.43.102 +162.159.43.101 +162.159.43.100 +162.159.43.10 +162.159.43.1 +162.159.43.0 +162.159.42.99 +162.159.42.98 +162.159.42.97 +162.159.42.96 +162.159.42.95 +162.159.42.94 +162.159.42.93 +162.159.42.92 +162.159.42.91 +162.159.42.90 +162.159.42.9 +162.159.42.89 +162.159.42.88 +162.159.42.87 +162.159.42.86 +162.159.42.85 +162.159.42.84 +162.159.42.83 +162.159.42.82 +162.159.42.81 +162.159.42.80 +162.159.42.8 +162.159.42.79 +162.159.42.78 +162.159.42.77 +162.159.42.76 +162.159.42.75 +162.159.42.74 +162.159.42.73 +162.159.42.72 +162.159.42.71 +162.159.42.70 +162.159.42.7 +162.159.42.69 +162.159.42.68 +162.159.42.67 +162.159.42.66 +162.159.42.65 +162.159.42.64 +162.159.42.63 +162.159.42.62 +162.159.42.61 +162.159.42.60 +162.159.42.6 +162.159.42.59 +162.159.42.58 +162.159.42.57 +162.159.42.56 +162.159.42.55 +162.159.42.54 +162.159.42.53 +162.159.42.52 +162.159.42.51 +162.159.42.50 +162.159.42.5 +162.159.42.49 +162.159.42.48 +162.159.42.47 +162.159.42.46 +162.159.42.45 +162.159.42.44 +162.159.42.43 +162.159.42.42 +162.159.42.41 +162.159.42.40 +162.159.42.4 +162.159.42.39 +162.159.42.38 +162.159.42.37 +162.159.42.36 +162.159.42.35 +162.159.42.34 +162.159.42.33 +162.159.42.32 +162.159.42.31 +162.159.42.30 +162.159.42.3 +162.159.42.29 +162.159.42.28 +162.159.42.27 +162.159.42.26 +162.159.42.255 +162.159.42.254 +162.159.42.253 +162.159.42.252 +162.159.42.251 +162.159.42.250 +162.159.42.25 +162.159.42.249 +162.159.42.248 +162.159.42.247 +162.159.42.246 +162.159.42.245 +162.159.42.244 +162.159.42.243 +162.159.42.242 +162.159.42.241 +162.159.42.240 +162.159.42.24 +162.159.42.239 +162.159.42.238 +162.159.42.237 +162.159.42.236 +162.159.42.235 +162.159.42.234 +162.159.42.233 +162.159.42.232 +162.159.42.231 +162.159.42.230 +162.159.42.23 +162.159.42.229 +162.159.42.228 +162.159.42.227 +162.159.42.226 +162.159.42.225 +162.159.42.224 +162.159.42.223 +162.159.42.222 +162.159.42.221 +162.159.42.220 +162.159.42.22 +162.159.42.219 +162.159.42.218 +162.159.42.217 +162.159.42.216 +162.159.42.215 +162.159.42.214 +162.159.42.213 +162.159.42.212 +162.159.42.211 +162.159.42.210 +162.159.42.21 +162.159.42.209 +162.159.42.208 +162.159.42.207 +162.159.42.206 +162.159.42.205 +162.159.42.204 +162.159.42.203 +162.159.42.202 +162.159.42.201 +162.159.42.200 +162.159.42.20 +162.159.42.2 +162.159.42.199 +162.159.42.198 +162.159.42.197 +162.159.42.196 +162.159.42.195 +162.159.42.194 +162.159.42.193 +162.159.42.192 +162.159.42.191 +162.159.42.190 +162.159.42.19 +162.159.42.189 +162.159.42.188 +162.159.42.187 +162.159.42.186 +162.159.42.185 +162.159.42.184 +162.159.42.183 +162.159.42.182 +162.159.42.181 +162.159.42.180 +162.159.42.18 +162.159.42.179 +162.159.42.178 +162.159.42.177 +162.159.42.176 +162.159.42.175 +162.159.42.174 +162.159.42.173 +162.159.42.172 +162.159.42.171 +162.159.42.170 +162.159.42.17 +162.159.42.169 +162.159.42.168 +162.159.42.167 +162.159.42.166 +162.159.42.165 +162.159.42.164 +162.159.42.163 +162.159.42.162 +162.159.42.161 +162.159.42.160 +162.159.42.16 +162.159.42.159 +162.159.42.158 +162.159.42.157 +162.159.42.156 +162.159.42.155 +162.159.42.154 +162.159.42.153 +162.159.42.152 +162.159.42.151 +162.159.42.150 +162.159.42.15 +162.159.42.149 +162.159.42.148 +162.159.42.147 +162.159.42.146 +162.159.42.145 +162.159.42.144 +162.159.42.143 +162.159.42.142 +162.159.42.141 +162.159.42.140 +162.159.42.14 +162.159.42.139 +162.159.42.138 +162.159.42.137 +162.159.42.136 +162.159.42.135 +162.159.42.134 +162.159.42.133 +162.159.42.132 +162.159.42.131 +162.159.42.130 +162.159.42.13 +162.159.42.129 +162.159.42.128 +162.159.42.127 +162.159.42.126 +162.159.42.125 +162.159.42.124 +162.159.42.123 +162.159.42.122 +162.159.42.121 +162.159.42.120 +162.159.42.12 +162.159.42.119 +162.159.42.118 +162.159.42.117 +162.159.42.116 +162.159.42.115 +162.159.42.114 +162.159.42.113 +162.159.42.112 +162.159.42.111 +162.159.42.110 +162.159.42.11 +162.159.42.109 +162.159.42.108 +162.159.42.107 +162.159.42.106 +162.159.42.105 +162.159.42.104 +162.159.42.103 +162.159.42.102 +162.159.42.101 +162.159.42.100 +162.159.42.10 +162.159.42.1 +162.159.42.0 +162.159.41.84 +162.159.41.77 +162.159.41.75 +162.159.41.70 +162.159.41.65 +162.159.41.64 +162.159.41.59 +162.159.41.42 +162.159.41.36 +162.159.41.35 +162.159.41.34 +162.159.41.32 +162.159.41.28 +162.159.41.27 +162.159.41.235 +162.159.41.223 +162.159.41.218 +162.159.41.202 +162.159.41.201 +162.159.41.191 +162.159.41.182 +162.159.41.18 +162.159.41.170 +162.159.41.17 +162.159.41.158 +162.159.41.151 +162.159.41.147 +162.159.41.134 +162.159.41.110 +162.159.41.107 +162.159.41.104 +162.159.41.1 +162.159.41.0 +162.159.40.91 +162.159.40.86 +162.159.40.83 +162.159.40.74 +162.159.40.42 +162.159.40.27 +162.159.40.229 +162.159.40.226 +162.159.40.215 +162.159.40.209 +162.159.40.207 +162.159.40.203 +162.159.40.202 +162.159.40.189 +162.159.40.186 +162.159.40.184 +162.159.40.183 +162.159.40.182 +162.159.40.172 +162.159.40.170 +162.159.40.165 +162.159.40.15 +162.159.40.1 +162.159.4.95 +162.159.4.86 +162.159.4.81 +162.159.4.71 +162.159.4.70 +162.159.4.66 +162.159.4.62 +162.159.4.6 +162.159.4.59 +162.159.4.57 +162.159.4.56 +162.159.4.45 +162.159.4.42 +162.159.4.37 +162.159.4.3 +162.159.4.29 +162.159.4.27 +162.159.4.25 +162.159.4.248 +162.159.4.244 +162.159.4.243 +162.159.4.227 +162.159.4.220 +162.159.4.208 +162.159.4.198 +162.159.4.193 +162.159.4.188 +162.159.4.16 +162.159.4.158 +162.159.4.147 +162.159.4.142 +162.159.4.135 +162.159.4.123 +162.159.4.111 +162.159.4.106 +162.159.4.100 +162.159.39.99 +162.159.39.98 +162.159.39.97 +162.159.39.96 +162.159.39.95 +162.159.39.94 +162.159.39.93 +162.159.39.92 +162.159.39.91 +162.159.39.90 +162.159.39.9 +162.159.39.89 +162.159.39.88 +162.159.39.87 +162.159.39.86 +162.159.39.85 +162.159.39.84 +162.159.39.83 +162.159.39.82 +162.159.39.81 +162.159.39.80 +162.159.39.8 +162.159.39.79 +162.159.39.78 +162.159.39.77 +162.159.39.76 +162.159.39.75 +162.159.39.74 +162.159.39.73 +162.159.39.72 +162.159.39.71 +162.159.39.70 +162.159.39.7 +162.159.39.69 +162.159.39.67 +162.159.39.66 +162.159.39.65 +162.159.39.64 +162.159.39.63 +162.159.39.62 +162.159.39.61 +162.159.39.60 +162.159.39.6 +162.159.39.59 +162.159.39.58 +162.159.39.57 +162.159.39.56 +162.159.39.55 +162.159.39.54 +162.159.39.53 +162.159.39.52 +162.159.39.51 +162.159.39.50 +162.159.39.5 +162.159.39.49 +162.159.39.48 +162.159.39.47 +162.159.39.46 +162.159.39.45 +162.159.39.44 +162.159.39.43 +162.159.39.42 +162.159.39.41 +162.159.39.40 +162.159.39.4 +162.159.39.39 +162.159.39.38 +162.159.39.37 +162.159.39.36 +162.159.39.35 +162.159.39.34 +162.159.39.33 +162.159.39.32 +162.159.39.31 +162.159.39.30 +162.159.39.3 +162.159.39.29 +162.159.39.28 +162.159.39.27 +162.159.39.26 +162.159.39.254 +162.159.39.253 +162.159.39.252 +162.159.39.251 +162.159.39.250 +162.159.39.25 +162.159.39.249 +162.159.39.248 +162.159.39.247 +162.159.39.246 +162.159.39.245 +162.159.39.244 +162.159.39.243 +162.159.39.242 +162.159.39.241 +162.159.39.240 +162.159.39.24 +162.159.39.239 +162.159.39.238 +162.159.39.237 +162.159.39.236 +162.159.39.235 +162.159.39.234 +162.159.39.233 +162.159.39.232 +162.159.39.231 +162.159.39.230 +162.159.39.23 +162.159.39.229 +162.159.39.228 +162.159.39.227 +162.159.39.226 +162.159.39.225 +162.159.39.224 +162.159.39.223 +162.159.39.222 +162.159.39.221 +162.159.39.220 +162.159.39.22 +162.159.39.219 +162.159.39.218 +162.159.39.217 +162.159.39.216 +162.159.39.215 +162.159.39.214 +162.159.39.213 +162.159.39.212 +162.159.39.211 +162.159.39.210 +162.159.39.21 +162.159.39.209 +162.159.39.208 +162.159.39.207 +162.159.39.206 +162.159.39.205 +162.159.39.204 +162.159.39.203 +162.159.39.202 +162.159.39.201 +162.159.39.200 +162.159.39.20 +162.159.39.2 +162.159.39.199 +162.159.39.198 +162.159.39.197 +162.159.39.196 +162.159.39.195 +162.159.39.194 +162.159.39.193 +162.159.39.192 +162.159.39.191 +162.159.39.190 +162.159.39.19 +162.159.39.189 +162.159.39.188 +162.159.39.187 +162.159.39.186 +162.159.39.184 +162.159.39.183 +162.159.39.182 +162.159.39.181 +162.159.39.180 +162.159.39.18 +162.159.39.179 +162.159.39.178 +162.159.39.177 +162.159.39.176 +162.159.39.175 +162.159.39.174 +162.159.39.173 +162.159.39.172 +162.159.39.171 +162.159.39.170 +162.159.39.17 +162.159.39.169 +162.159.39.168 +162.159.39.167 +162.159.39.166 +162.159.39.165 +162.159.39.164 +162.159.39.163 +162.159.39.162 +162.159.39.161 +162.159.39.160 +162.159.39.16 +162.159.39.159 +162.159.39.158 +162.159.39.157 +162.159.39.156 +162.159.39.155 +162.159.39.154 +162.159.39.153 +162.159.39.152 +162.159.39.151 +162.159.39.150 +162.159.39.15 +162.159.39.149 +162.159.39.148 +162.159.39.147 +162.159.39.146 +162.159.39.145 +162.159.39.144 +162.159.39.143 +162.159.39.142 +162.159.39.141 +162.159.39.140 +162.159.39.14 +162.159.39.139 +162.159.39.138 +162.159.39.137 +162.159.39.136 +162.159.39.135 +162.159.39.134 +162.159.39.133 +162.159.39.132 +162.159.39.131 +162.159.39.130 +162.159.39.13 +162.159.39.129 +162.159.39.128 +162.159.39.127 +162.159.39.126 +162.159.39.125 +162.159.39.124 +162.159.39.123 +162.159.39.122 +162.159.39.121 +162.159.39.120 +162.159.39.12 +162.159.39.119 +162.159.39.118 +162.159.39.117 +162.159.39.116 +162.159.39.115 +162.159.39.114 +162.159.39.113 +162.159.39.112 +162.159.39.111 +162.159.39.110 +162.159.39.11 +162.159.39.109 +162.159.39.108 +162.159.39.107 +162.159.39.106 +162.159.39.105 +162.159.39.104 +162.159.39.103 +162.159.39.102 +162.159.39.101 +162.159.39.100 +162.159.39.10 +162.159.39.1 +162.159.39.0 +162.159.38.99 +162.159.38.98 +162.159.38.97 +162.159.38.96 +162.159.38.95 +162.159.38.94 +162.159.38.93 +162.159.38.92 +162.159.38.91 +162.159.38.90 +162.159.38.9 +162.159.38.89 +162.159.38.88 +162.159.38.87 +162.159.38.86 +162.159.38.85 +162.159.38.84 +162.159.38.83 +162.159.38.82 +162.159.38.81 +162.159.38.80 +162.159.38.8 +162.159.38.79 +162.159.38.78 +162.159.38.77 +162.159.38.76 +162.159.38.75 +162.159.38.74 +162.159.38.73 +162.159.38.72 +162.159.38.71 +162.159.38.70 +162.159.38.7 +162.159.38.69 +162.159.38.68 +162.159.38.67 +162.159.38.66 +162.159.38.65 +162.159.38.64 +162.159.38.63 +162.159.38.62 +162.159.38.61 +162.159.38.60 +162.159.38.6 +162.159.38.59 +162.159.38.58 +162.159.38.57 +162.159.38.56 +162.159.38.55 +162.159.38.54 +162.159.38.53 +162.159.38.52 +162.159.38.51 +162.159.38.50 +162.159.38.5 +162.159.38.49 +162.159.38.48 +162.159.38.47 +162.159.38.46 +162.159.38.45 +162.159.38.44 +162.159.38.43 +162.159.38.42 +162.159.38.41 +162.159.38.40 +162.159.38.4 +162.159.38.39 +162.159.38.38 +162.159.38.37 +162.159.38.36 +162.159.38.35 +162.159.38.34 +162.159.38.33 +162.159.38.32 +162.159.38.31 +162.159.38.30 +162.159.38.3 +162.159.38.29 +162.159.38.28 +162.159.38.27 +162.159.38.26 +162.159.38.255 +162.159.38.254 +162.159.38.253 +162.159.38.252 +162.159.38.251 +162.159.38.250 +162.159.38.25 +162.159.38.249 +162.159.38.248 +162.159.38.247 +162.159.38.245 +162.159.38.244 +162.159.38.243 +162.159.38.242 +162.159.38.241 +162.159.38.240 +162.159.38.24 +162.159.38.239 +162.159.38.238 +162.159.38.237 +162.159.38.236 +162.159.38.235 +162.159.38.234 +162.159.38.233 +162.159.38.232 +162.159.38.231 +162.159.38.230 +162.159.38.23 +162.159.38.229 +162.159.38.228 +162.159.38.227 +162.159.38.226 +162.159.38.225 +162.159.38.224 +162.159.38.223 +162.159.38.222 +162.159.38.221 +162.159.38.220 +162.159.38.22 +162.159.38.219 +162.159.38.218 +162.159.38.217 +162.159.38.216 +162.159.38.215 +162.159.38.214 +162.159.38.213 +162.159.38.212 +162.159.38.211 +162.159.38.210 +162.159.38.21 +162.159.38.209 +162.159.38.208 +162.159.38.207 +162.159.38.206 +162.159.38.205 +162.159.38.204 +162.159.38.203 +162.159.38.202 +162.159.38.201 +162.159.38.200 +162.159.38.20 +162.159.38.2 +162.159.38.199 +162.159.38.198 +162.159.38.197 +162.159.38.196 +162.159.38.195 +162.159.38.194 +162.159.38.193 +162.159.38.192 +162.159.38.191 +162.159.38.190 +162.159.38.19 +162.159.38.189 +162.159.38.188 +162.159.38.187 +162.159.38.186 +162.159.38.185 +162.159.38.184 +162.159.38.183 +162.159.38.182 +162.159.38.181 +162.159.38.180 +162.159.38.18 +162.159.38.179 +162.159.38.178 +162.159.38.177 +162.159.38.176 +162.159.38.175 +162.159.38.174 +162.159.38.173 +162.159.38.172 +162.159.38.171 +162.159.38.170 +162.159.38.17 +162.159.38.169 +162.159.38.168 +162.159.38.167 +162.159.38.166 +162.159.38.165 +162.159.38.164 +162.159.38.163 +162.159.38.162 +162.159.38.161 +162.159.38.160 +162.159.38.16 +162.159.38.159 +162.159.38.158 +162.159.38.157 +162.159.38.156 +162.159.38.155 +162.159.38.154 +162.159.38.153 +162.159.38.152 +162.159.38.151 +162.159.38.150 +162.159.38.15 +162.159.38.149 +162.159.38.148 +162.159.38.147 +162.159.38.146 +162.159.38.145 +162.159.38.144 +162.159.38.143 +162.159.38.142 +162.159.38.141 +162.159.38.140 +162.159.38.14 +162.159.38.139 +162.159.38.138 +162.159.38.137 +162.159.38.136 +162.159.38.135 +162.159.38.134 +162.159.38.133 +162.159.38.132 +162.159.38.131 +162.159.38.130 +162.159.38.13 +162.159.38.129 +162.159.38.128 +162.159.38.127 +162.159.38.126 +162.159.38.125 +162.159.38.124 +162.159.38.123 +162.159.38.122 +162.159.38.121 +162.159.38.120 +162.159.38.12 +162.159.38.119 +162.159.38.118 +162.159.38.117 +162.159.38.116 +162.159.38.115 +162.159.38.114 +162.159.38.113 +162.159.38.112 +162.159.38.111 +162.159.38.110 +162.159.38.11 +162.159.38.109 +162.159.38.108 +162.159.38.107 +162.159.38.106 +162.159.38.105 +162.159.38.104 +162.159.38.103 +162.159.38.102 +162.159.38.101 +162.159.38.100 +162.159.38.10 +162.159.38.1 +162.159.38.0 +162.159.36.96 +162.159.36.86 +162.159.36.7 +162.159.36.64 +162.159.36.61 +162.159.36.6 +162.159.36.58 +162.159.36.46 +162.159.36.43 +162.159.36.36 +162.159.36.253 +162.159.36.252 +162.159.36.25 +162.159.36.249 +162.159.36.247 +162.159.36.243 +162.159.36.240 +162.159.36.237 +162.159.36.230 +162.159.36.227 +162.159.36.226 +162.159.36.224 +162.159.36.220 +162.159.36.216 +162.159.36.199 +162.159.36.190 +162.159.36.185 +162.159.36.181 +162.159.36.175 +162.159.36.158 +162.159.36.152 +162.159.36.141 +162.159.36.139 +162.159.36.136 +162.159.36.134 +162.159.36.132 +162.159.36.126 +162.159.36.125 +162.159.36.123 +162.159.36.115 +162.159.36.114 +162.159.36.110 +162.159.36.11 +162.159.36.104 +162.159.36.1 +162.159.35.99 +162.159.35.98 +162.159.35.97 +162.159.35.96 +162.159.35.95 +162.159.35.94 +162.159.35.93 +162.159.35.92 +162.159.35.91 +162.159.35.90 +162.159.35.9 +162.159.35.89 +162.159.35.88 +162.159.35.87 +162.159.35.86 +162.159.35.85 +162.159.35.84 +162.159.35.83 +162.159.35.82 +162.159.35.81 +162.159.35.80 +162.159.35.8 +162.159.35.79 +162.159.35.78 +162.159.35.77 +162.159.35.76 +162.159.35.75 +162.159.35.74 +162.159.35.73 +162.159.35.72 +162.159.35.71 +162.159.35.70 +162.159.35.7 +162.159.35.69 +162.159.35.68 +162.159.35.67 +162.159.35.66 +162.159.35.65 +162.159.35.64 +162.159.35.63 +162.159.35.62 +162.159.35.61 +162.159.35.60 +162.159.35.6 +162.159.35.59 +162.159.35.58 +162.159.35.57 +162.159.35.56 +162.159.35.55 +162.159.35.54 +162.159.35.53 +162.159.35.52 +162.159.35.51 +162.159.35.50 +162.159.35.5 +162.159.35.49 +162.159.35.48 +162.159.35.47 +162.159.35.46 +162.159.35.45 +162.159.35.44 +162.159.35.43 +162.159.35.42 +162.159.35.41 +162.159.35.40 +162.159.35.4 +162.159.35.39 +162.159.35.38 +162.159.35.37 +162.159.35.36 +162.159.35.35 +162.159.35.34 +162.159.35.33 +162.159.35.32 +162.159.35.31 +162.159.35.30 +162.159.35.3 +162.159.35.29 +162.159.35.28 +162.159.35.27 +162.159.35.26 +162.159.35.254 +162.159.35.253 +162.159.35.251 +162.159.35.250 +162.159.35.25 +162.159.35.249 +162.159.35.248 +162.159.35.247 +162.159.35.246 +162.159.35.245 +162.159.35.244 +162.159.35.243 +162.159.35.242 +162.159.35.241 +162.159.35.240 +162.159.35.24 +162.159.35.239 +162.159.35.238 +162.159.35.237 +162.159.35.236 +162.159.35.235 +162.159.35.234 +162.159.35.233 +162.159.35.232 +162.159.35.231 +162.159.35.230 +162.159.35.23 +162.159.35.229 +162.159.35.228 +162.159.35.227 +162.159.35.226 +162.159.35.225 +162.159.35.224 +162.159.35.223 +162.159.35.222 +162.159.35.221 +162.159.35.220 +162.159.35.22 +162.159.35.219 +162.159.35.218 +162.159.35.217 +162.159.35.216 +162.159.35.215 +162.159.35.214 +162.159.35.213 +162.159.35.212 +162.159.35.211 +162.159.35.210 +162.159.35.21 +162.159.35.209 +162.159.35.208 +162.159.35.207 +162.159.35.206 +162.159.35.205 +162.159.35.204 +162.159.35.203 +162.159.35.202 +162.159.35.201 +162.159.35.200 +162.159.35.20 +162.159.35.2 +162.159.35.199 +162.159.35.198 +162.159.35.197 +162.159.35.196 +162.159.35.195 +162.159.35.194 +162.159.35.193 +162.159.35.192 +162.159.35.191 +162.159.35.190 +162.159.35.19 +162.159.35.189 +162.159.35.188 +162.159.35.187 +162.159.35.186 +162.159.35.185 +162.159.35.184 +162.159.35.183 +162.159.35.182 +162.159.35.181 +162.159.35.180 +162.159.35.18 +162.159.35.179 +162.159.35.178 +162.159.35.177 +162.159.35.176 +162.159.35.175 +162.159.35.174 +162.159.35.173 +162.159.35.172 +162.159.35.171 +162.159.35.170 +162.159.35.17 +162.159.35.169 +162.159.35.168 +162.159.35.167 +162.159.35.166 +162.159.35.165 +162.159.35.164 +162.159.35.163 +162.159.35.162 +162.159.35.161 +162.159.35.160 +162.159.35.16 +162.159.35.159 +162.159.35.158 +162.159.35.157 +162.159.35.156 +162.159.35.155 +162.159.35.154 +162.159.35.153 +162.159.35.152 +162.159.35.151 +162.159.35.150 +162.159.35.15 +162.159.35.149 +162.159.35.148 +162.159.35.147 +162.159.35.146 +162.159.35.145 +162.159.35.144 +162.159.35.143 +162.159.35.142 +162.159.35.141 +162.159.35.140 +162.159.35.14 +162.159.35.139 +162.159.35.138 +162.159.35.137 +162.159.35.136 +162.159.35.135 +162.159.35.134 +162.159.35.133 +162.159.35.132 +162.159.35.131 +162.159.35.130 +162.159.35.13 +162.159.35.129 +162.159.35.128 +162.159.35.127 +162.159.35.126 +162.159.35.125 +162.159.35.124 +162.159.35.123 +162.159.35.122 +162.159.35.121 +162.159.35.120 +162.159.35.12 +162.159.35.119 +162.159.35.118 +162.159.35.117 +162.159.35.116 +162.159.35.115 +162.159.35.114 +162.159.35.113 +162.159.35.112 +162.159.35.111 +162.159.35.110 +162.159.35.11 +162.159.35.109 +162.159.35.108 +162.159.35.107 +162.159.35.106 +162.159.35.105 +162.159.35.104 +162.159.35.103 +162.159.35.102 +162.159.35.101 +162.159.35.100 +162.159.35.10 +162.159.35.1 +162.159.35.0 +162.159.34.99 +162.159.34.98 +162.159.34.97 +162.159.34.96 +162.159.34.95 +162.159.34.94 +162.159.34.93 +162.159.34.92 +162.159.34.91 +162.159.34.90 +162.159.34.9 +162.159.34.89 +162.159.34.88 +162.159.34.87 +162.159.34.86 +162.159.34.85 +162.159.34.84 +162.159.34.83 +162.159.34.82 +162.159.34.81 +162.159.34.80 +162.159.34.8 +162.159.34.79 +162.159.34.78 +162.159.34.77 +162.159.34.76 +162.159.34.75 +162.159.34.74 +162.159.34.73 +162.159.34.72 +162.159.34.71 +162.159.34.70 +162.159.34.7 +162.159.34.69 +162.159.34.68 +162.159.34.67 +162.159.34.66 +162.159.34.65 +162.159.34.64 +162.159.34.63 +162.159.34.62 +162.159.34.61 +162.159.34.60 +162.159.34.6 +162.159.34.59 +162.159.34.58 +162.159.34.57 +162.159.34.56 +162.159.34.55 +162.159.34.54 +162.159.34.53 +162.159.34.52 +162.159.34.51 +162.159.34.50 +162.159.34.5 +162.159.34.49 +162.159.34.48 +162.159.34.47 +162.159.34.46 +162.159.34.45 +162.159.34.44 +162.159.34.43 +162.159.34.42 +162.159.34.41 +162.159.34.40 +162.159.34.4 +162.159.34.39 +162.159.34.38 +162.159.34.37 +162.159.34.36 +162.159.34.35 +162.159.34.34 +162.159.34.33 +162.159.34.32 +162.159.34.31 +162.159.34.30 +162.159.34.3 +162.159.34.29 +162.159.34.28 +162.159.34.27 +162.159.34.26 +162.159.34.255 +162.159.34.254 +162.159.34.253 +162.159.34.252 +162.159.34.251 +162.159.34.250 +162.159.34.25 +162.159.34.249 +162.159.34.248 +162.159.34.247 +162.159.34.246 +162.159.34.245 +162.159.34.244 +162.159.34.243 +162.159.34.242 +162.159.34.241 +162.159.34.240 +162.159.34.24 +162.159.34.239 +162.159.34.238 +162.159.34.237 +162.159.34.236 +162.159.34.235 +162.159.34.234 +162.159.34.233 +162.159.34.232 +162.159.34.231 +162.159.34.230 +162.159.34.23 +162.159.34.229 +162.159.34.228 +162.159.34.227 +162.159.34.226 +162.159.34.225 +162.159.34.224 +162.159.34.223 +162.159.34.222 +162.159.34.221 +162.159.34.220 +162.159.34.22 +162.159.34.219 +162.159.34.218 +162.159.34.217 +162.159.34.216 +162.159.34.215 +162.159.34.214 +162.159.34.213 +162.159.34.212 +162.159.34.211 +162.159.34.210 +162.159.34.21 +162.159.34.209 +162.159.34.208 +162.159.34.207 +162.159.34.206 +162.159.34.205 +162.159.34.204 +162.159.34.203 +162.159.34.202 +162.159.34.201 +162.159.34.200 +162.159.34.20 +162.159.34.2 +162.159.34.199 +162.159.34.198 +162.159.34.197 +162.159.34.196 +162.159.34.195 +162.159.34.194 +162.159.34.193 +162.159.34.192 +162.159.34.191 +162.159.34.190 +162.159.34.19 +162.159.34.189 +162.159.34.188 +162.159.34.187 +162.159.34.186 +162.159.34.185 +162.159.34.184 +162.159.34.183 +162.159.34.182 +162.159.34.181 +162.159.34.180 +162.159.34.18 +162.159.34.179 +162.159.34.178 +162.159.34.177 +162.159.34.176 +162.159.34.175 +162.159.34.174 +162.159.34.173 +162.159.34.172 +162.159.34.171 +162.159.34.170 +162.159.34.17 +162.159.34.169 +162.159.34.168 +162.159.34.167 +162.159.34.166 +162.159.34.165 +162.159.34.164 +162.159.34.163 +162.159.34.162 +162.159.34.161 +162.159.34.160 +162.159.34.16 +162.159.34.159 +162.159.34.158 +162.159.34.157 +162.159.34.156 +162.159.34.155 +162.159.34.154 +162.159.34.153 +162.159.34.152 +162.159.34.151 +162.159.34.150 +162.159.34.15 +162.159.34.149 +162.159.34.148 +162.159.34.147 +162.159.34.146 +162.159.34.145 +162.159.34.144 +162.159.34.143 +162.159.34.142 +162.159.34.141 +162.159.34.140 +162.159.34.14 +162.159.34.139 +162.159.34.138 +162.159.34.137 +162.159.34.136 +162.159.34.135 +162.159.34.134 +162.159.34.133 +162.159.34.132 +162.159.34.131 +162.159.34.130 +162.159.34.13 +162.159.34.129 +162.159.34.128 +162.159.34.127 +162.159.34.126 +162.159.34.125 +162.159.34.124 +162.159.34.123 +162.159.34.122 +162.159.34.121 +162.159.34.120 +162.159.34.12 +162.159.34.119 +162.159.34.118 +162.159.34.117 +162.159.34.116 +162.159.34.115 +162.159.34.114 +162.159.34.113 +162.159.34.112 +162.159.34.111 +162.159.34.110 +162.159.34.11 +162.159.34.109 +162.159.34.108 +162.159.34.107 +162.159.34.106 +162.159.34.105 +162.159.34.104 +162.159.34.103 +162.159.34.102 +162.159.34.101 +162.159.34.100 +162.159.34.10 +162.159.34.1 +162.159.34.0 +162.159.33.93 +162.159.33.92 +162.159.33.83 +162.159.33.74 +162.159.33.68 +162.159.33.59 +162.159.33.57 +162.159.33.54 +162.159.33.251 +162.159.33.247 +162.159.33.238 +162.159.33.234 +162.159.33.23 +162.159.33.227 +162.159.33.224 +162.159.33.22 +162.159.33.217 +162.159.33.216 +162.159.33.211 +162.159.33.204 +162.159.33.197 +162.159.33.186 +162.159.33.17 +162.159.33.165 +162.159.33.154 +162.159.33.152 +162.159.33.143 +162.159.33.141 +162.159.33.130 +162.159.33.127 +162.159.33.121 +162.159.33.119 +162.159.33.115 +162.159.33.110 +162.159.32.99 +162.159.32.95 +162.159.32.90 +162.159.32.72 +162.159.32.7 +162.159.32.50 +162.159.32.49 +162.159.32.46 +162.159.32.34 +162.159.32.26 +162.159.32.255 +162.159.32.252 +162.159.32.249 +162.159.32.238 +162.159.32.23 +162.159.32.211 +162.159.32.210 +162.159.32.206 +162.159.32.204 +162.159.32.177 +162.159.32.175 +162.159.32.174 +162.159.32.169 +162.159.32.155 +162.159.32.153 +162.159.32.139 +162.159.32.135 +162.159.32.119 +162.159.32.118 +162.159.32.109 +162.159.3.98 +162.159.3.93 +162.159.3.91 +162.159.3.72 +162.159.3.71 +162.159.3.68 +162.159.3.65 +162.159.3.61 +162.159.3.27 +162.159.3.246 +162.159.3.245 +162.159.3.235 +162.159.3.234 +162.159.3.233 +162.159.3.229 +162.159.3.224 +162.159.3.190 +162.159.3.183 +162.159.3.182 +162.159.3.179 +162.159.3.176 +162.159.3.169 +162.159.3.157 +162.159.3.156 +162.159.3.136 +162.159.3.135 +162.159.3.131 +162.159.3.123 +162.159.3.121 +162.159.3.120 +162.159.3.118 +162.159.3.117 +162.159.3.104 +162.159.27.173 +162.159.27.162 +162.159.26.49 +162.159.25.85 +162.159.25.84 +162.159.25.206 +162.159.25.167 +162.159.24.87 +162.159.24.85 +162.159.24.69 +162.159.24.244 +162.159.24.197 +162.159.24.129 +162.159.23.93 +162.159.23.84 +162.159.23.82 +162.159.23.80 +162.159.23.66 +162.159.23.41 +162.159.23.39 +162.159.23.37 +162.159.23.240 +162.159.23.236 +162.159.23.230 +162.159.23.205 +162.159.23.194 +162.159.23.184 +162.159.23.181 +162.159.23.16 +162.159.23.158 +162.159.23.154 +162.159.23.145 +162.159.23.144 +162.159.23.138 +162.159.23.137 +162.159.23.13 +162.159.23.124 +162.159.23.122 +162.159.23.120 +162.159.23.118 +162.159.23.111 +162.159.23.106 +162.159.23.102 +162.159.22.95 +162.159.22.92 +162.159.22.90 +162.159.22.79 +162.159.22.73 +162.159.22.71 +162.159.22.66 +162.159.22.65 +162.159.22.38 +162.159.22.37 +162.159.22.28 +162.159.22.226 +162.159.22.221 +162.159.22.219 +162.159.22.214 +162.159.22.212 +162.159.22.211 +162.159.22.209 +162.159.22.207 +162.159.22.196 +162.159.22.193 +162.159.22.192 +162.159.22.186 +162.159.22.185 +162.159.22.183 +162.159.22.180 +162.159.22.161 +162.159.22.16 +162.159.22.159 +162.159.22.154 +162.159.22.150 +162.159.22.149 +162.159.22.144 +162.159.22.132 +162.159.22.131 +162.159.22.125 +162.159.22.123 +162.159.22.121 +162.159.22.115 +162.159.21.96 +162.159.21.94 +162.159.21.91 +162.159.21.86 +162.159.21.84 +162.159.21.83 +162.159.21.74 +162.159.21.73 +162.159.21.64 +162.159.21.5 +162.159.21.33 +162.159.21.244 +162.159.21.231 +162.159.21.221 +162.159.21.22 +162.159.21.218 +162.159.21.184 +162.159.21.18 +162.159.21.178 +162.159.21.167 +162.159.21.16 +162.159.21.159 +162.159.21.153 +162.159.21.147 +162.159.21.127 +162.159.21.118 +162.159.21.11 +162.159.21.108 +162.159.20.79 +162.159.20.73 +162.159.20.7 +162.159.20.5 +162.159.20.42 +162.159.20.28 +162.159.20.247 +162.159.20.243 +162.159.20.232 +162.159.20.23 +162.159.20.224 +162.159.20.218 +162.159.20.211 +162.159.20.208 +162.159.20.206 +162.159.20.194 +162.159.20.192 +162.159.20.191 +162.159.20.170 +162.159.20.169 +162.159.20.161 +162.159.20.155 +162.159.20.15 +162.159.20.139 +162.159.20.137 +162.159.20.134 +162.159.20.132 +162.159.20.129 +162.159.20.121 +162.159.20.108 +162.159.20.106 +162.159.20.104 +162.159.2.99 +162.159.2.97 +162.159.2.96 +162.159.2.94 +162.159.2.89 +162.159.2.88 +162.159.2.85 +162.159.2.73 +162.159.2.62 +162.159.2.61 +162.159.2.57 +162.159.2.48 +162.159.2.46 +162.159.2.32 +162.159.2.28 +162.159.2.253 +162.159.2.25 +162.159.2.243 +162.159.2.218 +162.159.2.215 +162.159.2.210 +162.159.2.190 +162.159.2.183 +162.159.2.181 +162.159.2.180 +162.159.2.18 +162.159.2.178 +162.159.2.174 +162.159.2.166 +162.159.2.161 +162.159.2.152 +162.159.2.140 +162.159.2.14 +162.159.2.139 +162.159.2.138 +162.159.2.136 +162.159.2.129 +162.159.2.121 +162.159.2.112 +162.159.2.110 +162.159.2.11 +162.159.2.106 +162.159.2.1 +162.159.19.80 +162.159.19.7 +162.159.19.68 +162.159.19.62 +162.159.19.35 +162.159.19.3 +162.159.19.26 +162.159.19.25 +162.159.19.248 +162.159.19.244 +162.159.19.235 +162.159.19.21 +162.159.19.206 +162.159.19.204 +162.159.19.203 +162.159.19.201 +162.159.19.199 +162.159.19.173 +162.159.19.170 +162.159.19.168 +162.159.19.165 +162.159.19.163 +162.159.19.159 +162.159.19.147 +162.159.19.139 +162.159.19.13 +162.159.19.125 +162.159.18.79 +162.159.18.72 +162.159.18.64 +162.159.18.44 +162.159.18.38 +162.159.18.32 +162.159.18.252 +162.159.18.233 +162.159.18.224 +162.159.18.215 +162.159.18.208 +162.159.18.200 +162.159.18.2 +162.159.18.196 +162.159.18.192 +162.159.18.191 +162.159.18.172 +162.159.18.167 +162.159.18.162 +162.159.18.151 +162.159.18.150 +162.159.18.145 +162.159.18.143 +162.159.18.140 +162.159.18.139 +162.159.18.136 +162.159.18.133 +162.159.18.128 +162.159.18.126 +162.159.18.12 +162.159.18.114 +162.159.18.112 +162.159.18.111 +162.159.18.106 +162.159.17.95 +162.159.17.85 +162.159.17.82 +162.159.17.79 +162.159.17.75 +162.159.17.70 +162.159.17.51 +162.159.17.49 +162.159.17.43 +162.159.17.32 +162.159.17.29 +162.159.17.24 +162.159.17.234 +162.159.17.232 +162.159.17.221 +162.159.17.205 +162.159.17.194 +162.159.17.183 +162.159.17.182 +162.159.17.178 +162.159.17.172 +162.159.17.165 +162.159.17.152 +162.159.17.146 +162.159.17.139 +162.159.17.134 +162.159.17.127 +162.159.17.122 +162.159.17.117 +162.159.16.90 +162.159.16.83 +162.159.16.58 +162.159.16.47 +162.159.16.43 +162.159.16.34 +162.159.16.240 +162.159.16.225 +162.159.16.212 +162.159.16.201 +162.159.16.200 +162.159.16.195 +162.159.16.180 +162.159.16.166 +162.159.16.159 +162.159.16.153 +162.159.16.146 +162.159.16.140 +162.159.16.127 +162.159.16.123 +162.159.16.117 +162.159.16.102 +162.159.15.95 +162.159.15.75 +162.159.15.71 +162.159.15.36 +162.159.15.33 +162.159.15.3 +162.159.15.29 +162.159.15.26 +162.159.15.244 +162.159.15.235 +162.159.15.22 +162.159.15.216 +162.159.15.21 +162.159.15.206 +162.159.15.197 +162.159.15.182 +162.159.15.178 +162.159.15.166 +162.159.15.161 +162.159.15.16 +162.159.15.157 +162.159.15.154 +162.159.15.148 +162.159.15.132 +162.159.15.131 +162.159.15.121 +162.159.15.119 +162.159.15.117 +162.159.15.112 +162.159.15.110 +162.159.15.106 +162.159.15.103 +162.159.14.94 +162.159.14.91 +162.159.14.87 +162.159.14.73 +162.159.14.72 +162.159.14.66 +162.159.14.60 +162.159.14.4 +162.159.14.28 +162.159.14.26 +162.159.14.251 +162.159.14.247 +162.159.14.21 +162.159.14.201 +162.159.14.2 +162.159.14.197 +162.159.14.192 +162.159.14.189 +162.159.14.134 +162.159.14.131 +162.159.14.130 +162.159.14.13 +162.159.14.125 +162.159.14.12 +162.159.13.98 +162.159.13.85 +162.159.13.77 +162.159.13.74 +162.159.13.58 +162.159.13.55 +162.159.13.50 +162.159.13.47 +162.159.13.41 +162.159.13.39 +162.159.13.35 +162.159.13.3 +162.159.13.253 +162.159.13.246 +162.159.13.244 +162.159.13.223 +162.159.13.198 +162.159.13.194 +162.159.13.19 +162.159.13.171 +162.159.13.168 +162.159.13.165 +162.159.13.153 +162.159.13.151 +162.159.13.15 +162.159.13.126 +162.159.13.123 +162.159.13.114 +162.159.13.106 +162.159.13.104 +162.159.12.69 +162.159.12.65 +162.159.12.55 +162.159.12.5 +162.159.12.42 +162.159.12.39 +162.159.12.36 +162.159.12.254 +162.159.12.247 +162.159.12.228 +162.159.12.223 +162.159.12.207 +162.159.12.199 +162.159.12.195 +162.159.12.172 +162.159.12.157 +162.159.12.13 +162.159.12.125 +162.159.12.123 +162.159.12.111 +162.159.12.108 +162.159.12.10 +162.159.11.99 +162.159.11.91 +162.159.11.87 +162.159.11.84 +162.159.11.78 +162.159.11.75 +162.159.11.26 +162.159.11.247 +162.159.11.235 +162.159.11.232 +162.159.11.208 +162.159.11.198 +162.159.11.192 +162.159.11.173 +162.159.11.168 +162.159.11.167 +162.159.11.162 +162.159.11.157 +162.159.11.139 +162.159.11.135 +162.159.11.130 +162.159.11.13 +162.159.11.122 +162.159.11.111 +162.159.11.106 +162.159.11.10 +162.159.10.96 +162.159.10.9 +162.159.10.69 +162.159.10.65 +162.159.10.58 +162.159.10.55 +162.159.10.49 +162.159.10.38 +162.159.10.254 +162.159.10.249 +162.159.10.247 +162.159.10.221 +162.159.10.207 +162.159.10.203 +162.159.10.199 +162.159.10.195 +162.159.10.181 +162.159.10.178 +162.159.10.161 +162.159.10.155 +162.159.10.154 +162.159.10.145 +162.159.10.143 +162.159.10.141 +162.159.10.14 +162.159.10.138 +162.159.10.134 +162.159.10.133 +162.159.10.13 +162.159.10.120 +162.159.10.109 +162.159.10.105 +162.159.10.103 +162.159.10.100 +162.159.1.97 +162.159.1.75 +162.159.1.67 +162.159.1.62 +162.159.1.60 +162.159.1.48 +162.159.1.36 +162.159.1.35 +162.159.1.32 +162.159.1.243 +162.159.1.24 +162.159.1.238 +162.159.1.235 +162.159.1.226 +162.159.1.221 +162.159.1.20 +162.159.1.194 +162.159.1.185 +162.159.1.170 +162.159.1.154 +162.159.1.136 +162.159.1.122 +162.159.1.120 +162.159.1.1 +162.159.0.97 +162.159.0.93 +162.159.0.88 +162.159.0.80 +162.159.0.76 +162.159.0.73 +162.159.0.70 +162.159.0.7 +162.159.0.69 +162.159.0.63 +162.159.0.6 +162.159.0.44 +162.159.0.31 +162.159.0.28 +162.159.0.250 +162.159.0.245 +162.159.0.230 +162.159.0.228 +162.159.0.226 +162.159.0.210 +162.159.0.208 +162.159.0.195 +162.159.0.188 +162.159.0.15 +162.159.0.149 +162.159.0.137 +162.159.0.132 +162.159.0.118 +162.159.0.110 +162.159.0.11 +162.159.0.106 +162.159.0.10 +162.155.248.54 +162.142.63.110 +162.142.28.10 +162.142.105.146 +162.14.21.56 +162.14.21.178 +161.97.85.60 +161.97.149.103 +161.82.184.70 +161.49.226.194 +161.49.215.50 +161.49.215.28 +161.35.196.248 +161.230.203.238 +161.200.96.9 +161.11.228.11 +161.11.226.170 +161.0.62.217 +160.86.109.33 +160.72.33.26 +160.19.99.98 +160.16.111.164 +160.153.251.73 +160.119.101.224 +160.0.192.36 +16.62.171.163 +159.69.72.157 +159.69.68.181 +159.69.114.157 +159.255.187.250 +159.255.183.217 +159.203.125.234 +159.196.50.130 +159.196.204.14 +159.192.97.99 +159.192.142.225 +159.192.139.224 +159.192.104.150 +158.75.195.29 +158.69.31.60 +158.51.134.53 +158.46.37.156 +158.247.210.85 +158.174.73.94 +157.55.83.218 +157.25.58.57 +157.245.139.171 +157.231.209.248 +157.185.70.98 +157.175.242.142 +157.157.166.191 +157.157.138.227 +157.119.201.134 +157.100.63.48 +157.100.58.252 +157.100.55.226 +156.67.54.58 +156.154.71.56 +156.154.71.50 +156.154.71.5 +156.154.71.46 +156.154.71.44 +156.154.71.43 +156.154.71.42 +156.154.71.40 +156.154.71.33 +156.154.71.29 +156.154.71.26 +156.154.71.25 +156.154.71.22 +156.154.71.20 +156.154.71.2 +156.154.71.18 +156.154.71.13 +156.154.71.12 +156.154.71.11 +156.154.71.1 +156.154.70.7 +156.154.70.64 +156.154.70.60 +156.154.70.57 +156.154.70.5 +156.154.70.44 +156.154.70.43 +156.154.70.42 +156.154.70.40 +156.154.70.33 +156.154.70.29 +156.154.70.26 +156.154.70.25 +156.154.70.22 +156.154.70.2 +156.154.70.18 +156.154.70.16 +156.154.70.14 +156.154.70.13 +156.154.70.12 +156.154.70.11 +156.154.70.10 +156.154.70.1 +156.154.69.99 +156.154.69.96 +156.154.69.95 +156.154.69.94 +156.154.69.93 +156.154.69.91 +156.154.69.9 +156.154.69.88 +156.154.69.87 +156.154.69.86 +156.154.69.85 +156.154.69.84 +156.154.69.83 +156.154.69.82 +156.154.69.81 +156.154.69.80 +156.154.69.79 +156.154.69.77 +156.154.69.76 +156.154.69.75 +156.154.69.73 +156.154.69.72 +156.154.69.70 +156.154.69.69 +156.154.69.66 +156.154.69.64 +156.154.69.63 +156.154.69.62 +156.154.69.60 +156.154.69.6 +156.154.69.58 +156.154.69.53 +156.154.69.52 +156.154.69.50 +156.154.69.5 +156.154.69.49 +156.154.69.45 +156.154.69.44 +156.154.69.41 +156.154.69.4 +156.154.69.39 +156.154.69.36 +156.154.69.35 +156.154.69.34 +156.154.69.33 +156.154.69.31 +156.154.69.30 +156.154.69.29 +156.154.69.28 +156.154.69.253 +156.154.69.252 +156.154.69.251 +156.154.69.250 +156.154.69.25 +156.154.69.249 +156.154.69.247 +156.154.69.246 +156.154.69.243 +156.154.69.241 +156.154.69.239 +156.154.69.237 +156.154.69.236 +156.154.69.235 +156.154.69.234 +156.154.69.232 +156.154.69.230 +156.154.69.23 +156.154.69.229 +156.154.69.227 +156.154.69.224 +156.154.69.223 +156.154.69.222 +156.154.69.220 +156.154.69.219 +156.154.69.218 +156.154.69.214 +156.154.69.213 +156.154.69.211 +156.154.69.210 +156.154.69.21 +156.154.69.209 +156.154.69.207 +156.154.69.206 +156.154.69.205 +156.154.69.203 +156.154.69.201 +156.154.69.200 +156.154.69.2 +156.154.69.199 +156.154.69.198 +156.154.69.197 +156.154.69.196 +156.154.69.195 +156.154.69.194 +156.154.69.193 +156.154.69.192 +156.154.69.191 +156.154.69.190 +156.154.69.19 +156.154.69.189 +156.154.69.188 +156.154.69.187 +156.154.69.183 +156.154.69.182 +156.154.69.178 +156.154.69.177 +156.154.69.176 +156.154.69.174 +156.154.69.169 +156.154.69.168 +156.154.69.166 +156.154.69.165 +156.154.69.164 +156.154.69.161 +156.154.69.160 +156.154.69.16 +156.154.69.159 +156.154.69.158 +156.154.69.157 +156.154.69.156 +156.154.69.155 +156.154.69.154 +156.154.69.153 +156.154.69.152 +156.154.69.151 +156.154.69.149 +156.154.69.148 +156.154.69.147 +156.154.69.146 +156.154.69.145 +156.154.69.144 +156.154.69.143 +156.154.69.138 +156.154.69.137 +156.154.69.135 +156.154.69.134 +156.154.69.133 +156.154.69.132 +156.154.69.131 +156.154.69.13 +156.154.69.128 +156.154.69.127 +156.154.69.121 +156.154.69.12 +156.154.69.119 +156.154.69.117 +156.154.69.115 +156.154.69.114 +156.154.69.112 +156.154.69.111 +156.154.69.110 +156.154.69.11 +156.154.69.109 +156.154.69.108 +156.154.69.105 +156.154.69.104 +156.154.69.103 +156.154.69.102 +156.154.69.100 +156.154.69.10 +156.154.68.99 +156.154.68.98 +156.154.68.97 +156.154.68.96 +156.154.68.95 +156.154.68.94 +156.154.68.93 +156.154.68.92 +156.154.68.91 +156.154.68.90 +156.154.68.9 +156.154.68.89 +156.154.68.88 +156.154.68.87 +156.154.68.86 +156.154.68.85 +156.154.68.84 +156.154.68.82 +156.154.68.81 +156.154.68.8 +156.154.68.79 +156.154.68.78 +156.154.68.77 +156.154.68.76 +156.154.68.75 +156.154.68.74 +156.154.68.73 +156.154.68.72 +156.154.68.71 +156.154.68.70 +156.154.68.7 +156.154.68.69 +156.154.68.68 +156.154.68.67 +156.154.68.66 +156.154.68.64 +156.154.68.62 +156.154.68.61 +156.154.68.60 +156.154.68.6 +156.154.68.59 +156.154.68.58 +156.154.68.57 +156.154.68.55 +156.154.68.53 +156.154.68.52 +156.154.68.51 +156.154.68.50 +156.154.68.5 +156.154.68.49 +156.154.68.48 +156.154.68.47 +156.154.68.46 +156.154.68.45 +156.154.68.44 +156.154.68.43 +156.154.68.42 +156.154.68.41 +156.154.68.40 +156.154.68.4 +156.154.68.39 +156.154.68.38 +156.154.68.37 +156.154.68.36 +156.154.68.35 +156.154.68.34 +156.154.68.33 +156.154.68.32 +156.154.68.31 +156.154.68.30 +156.154.68.3 +156.154.68.29 +156.154.68.28 +156.154.68.27 +156.154.68.26 +156.154.68.254 +156.154.68.253 +156.154.68.252 +156.154.68.251 +156.154.68.250 +156.154.68.25 +156.154.68.249 +156.154.68.248 +156.154.68.247 +156.154.68.245 +156.154.68.244 +156.154.68.243 +156.154.68.242 +156.154.68.241 +156.154.68.240 +156.154.68.24 +156.154.68.239 +156.154.68.237 +156.154.68.236 +156.154.68.235 +156.154.68.233 +156.154.68.232 +156.154.68.231 +156.154.68.230 +156.154.68.23 +156.154.68.229 +156.154.68.228 +156.154.68.227 +156.154.68.226 +156.154.68.225 +156.154.68.224 +156.154.68.223 +156.154.68.222 +156.154.68.221 +156.154.68.220 +156.154.68.22 +156.154.68.219 +156.154.68.218 +156.154.68.217 +156.154.68.216 +156.154.68.214 +156.154.68.213 +156.154.68.212 +156.154.68.211 +156.154.68.210 +156.154.68.21 +156.154.68.209 +156.154.68.207 +156.154.68.206 +156.154.68.205 +156.154.68.204 +156.154.68.203 +156.154.68.201 +156.154.68.200 +156.154.68.20 +156.154.68.2 +156.154.68.199 +156.154.68.198 +156.154.68.197 +156.154.68.195 +156.154.68.194 +156.154.68.193 +156.154.68.192 +156.154.68.191 +156.154.68.19 +156.154.68.189 +156.154.68.188 +156.154.68.187 +156.154.68.186 +156.154.68.185 +156.154.68.184 +156.154.68.183 +156.154.68.182 +156.154.68.181 +156.154.68.180 +156.154.68.18 +156.154.68.179 +156.154.68.178 +156.154.68.176 +156.154.68.175 +156.154.68.174 +156.154.68.173 +156.154.68.172 +156.154.68.171 +156.154.68.170 +156.154.68.17 +156.154.68.169 +156.154.68.168 +156.154.68.167 +156.154.68.166 +156.154.68.164 +156.154.68.163 +156.154.68.162 +156.154.68.161 +156.154.68.160 +156.154.68.16 +156.154.68.159 +156.154.68.158 +156.154.68.156 +156.154.68.155 +156.154.68.154 +156.154.68.153 +156.154.68.152 +156.154.68.151 +156.154.68.150 +156.154.68.15 +156.154.68.149 +156.154.68.148 +156.154.68.147 +156.154.68.146 +156.154.68.145 +156.154.68.143 +156.154.68.142 +156.154.68.141 +156.154.68.140 +156.154.68.139 +156.154.68.138 +156.154.68.137 +156.154.68.136 +156.154.68.135 +156.154.68.134 +156.154.68.133 +156.154.68.132 +156.154.68.131 +156.154.68.130 +156.154.68.13 +156.154.68.129 +156.154.68.128 +156.154.68.127 +156.154.68.126 +156.154.68.125 +156.154.68.124 +156.154.68.123 +156.154.68.122 +156.154.68.121 +156.154.68.120 +156.154.68.12 +156.154.68.119 +156.154.68.118 +156.154.68.117 +156.154.68.116 +156.154.68.115 +156.154.68.114 +156.154.68.113 +156.154.68.112 +156.154.68.111 +156.154.68.110 +156.154.68.11 +156.154.68.109 +156.154.68.108 +156.154.68.107 +156.154.68.106 +156.154.68.105 +156.154.68.104 +156.154.68.103 +156.154.68.102 +156.154.68.101 +156.154.68.100 +156.154.68.10 +156.154.68.1 +156.154.67.99 +156.154.67.97 +156.154.67.96 +156.154.67.95 +156.154.67.94 +156.154.67.93 +156.154.67.92 +156.154.67.9 +156.154.67.88 +156.154.67.86 +156.154.67.85 +156.154.67.84 +156.154.67.83 +156.154.67.82 +156.154.67.80 +156.154.67.8 +156.154.67.79 +156.154.67.78 +156.154.67.76 +156.154.67.74 +156.154.67.73 +156.154.67.72 +156.154.67.70 +156.154.67.69 +156.154.67.67 +156.154.67.66 +156.154.67.65 +156.154.67.63 +156.154.67.62 +156.154.67.6 +156.154.67.59 +156.154.67.56 +156.154.67.55 +156.154.67.54 +156.154.67.53 +156.154.67.5 +156.154.67.49 +156.154.67.48 +156.154.67.45 +156.154.67.43 +156.154.67.42 +156.154.67.40 +156.154.67.4 +156.154.67.39 +156.154.67.34 +156.154.67.33 +156.154.67.3 +156.154.67.29 +156.154.67.28 +156.154.67.27 +156.154.67.26 +156.154.67.253 +156.154.67.252 +156.154.67.251 +156.154.67.250 +156.154.67.249 +156.154.67.248 +156.154.67.245 +156.154.67.244 +156.154.67.242 +156.154.67.241 +156.154.67.240 +156.154.67.24 +156.154.67.238 +156.154.67.237 +156.154.67.236 +156.154.67.235 +156.154.67.234 +156.154.67.230 +156.154.67.23 +156.154.67.229 +156.154.67.227 +156.154.67.226 +156.154.67.225 +156.154.67.224 +156.154.67.223 +156.154.67.221 +156.154.67.220 +156.154.67.22 +156.154.67.219 +156.154.67.217 +156.154.67.216 +156.154.67.215 +156.154.67.213 +156.154.67.212 +156.154.67.211 +156.154.67.209 +156.154.67.208 +156.154.67.207 +156.154.67.206 +156.154.67.201 +156.154.67.20 +156.154.67.199 +156.154.67.198 +156.154.67.197 +156.154.67.195 +156.154.67.193 +156.154.67.191 +156.154.67.189 +156.154.67.182 +156.154.67.180 +156.154.67.18 +156.154.67.179 +156.154.67.178 +156.154.67.177 +156.154.67.176 +156.154.67.173 +156.154.67.170 +156.154.67.169 +156.154.67.168 +156.154.67.167 +156.154.67.164 +156.154.67.163 +156.154.67.161 +156.154.67.160 +156.154.67.16 +156.154.67.159 +156.154.67.157 +156.154.67.155 +156.154.67.154 +156.154.67.153 +156.154.67.151 +156.154.67.15 +156.154.67.148 +156.154.67.147 +156.154.67.144 +156.154.67.143 +156.154.67.142 +156.154.67.140 +156.154.67.139 +156.154.67.137 +156.154.67.135 +156.154.67.131 +156.154.67.127 +156.154.67.126 +156.154.67.125 +156.154.67.124 +156.154.67.123 +156.154.67.122 +156.154.67.120 +156.154.67.119 +156.154.67.115 +156.154.67.113 +156.154.67.109 +156.154.67.106 +156.154.67.104 +156.154.67.103 +156.154.67.102 +156.154.67.10 +156.154.66.99 +156.154.66.97 +156.154.66.96 +156.154.66.91 +156.154.66.90 +156.154.66.9 +156.154.66.89 +156.154.66.88 +156.154.66.87 +156.154.66.86 +156.154.66.85 +156.154.66.81 +156.154.66.8 +156.154.66.78 +156.154.66.75 +156.154.66.74 +156.154.66.73 +156.154.66.72 +156.154.66.7 +156.154.66.68 +156.154.66.67 +156.154.66.66 +156.154.66.65 +156.154.66.64 +156.154.66.63 +156.154.66.62 +156.154.66.61 +156.154.66.58 +156.154.66.57 +156.154.66.56 +156.154.66.54 +156.154.66.53 +156.154.66.52 +156.154.66.51 +156.154.66.50 +156.154.66.5 +156.154.66.49 +156.154.66.48 +156.154.66.47 +156.154.66.46 +156.154.66.45 +156.154.66.44 +156.154.66.42 +156.154.66.41 +156.154.66.35 +156.154.66.34 +156.154.66.32 +156.154.66.3 +156.154.66.29 +156.154.66.28 +156.154.66.254 +156.154.66.253 +156.154.66.251 +156.154.66.248 +156.154.66.247 +156.154.66.245 +156.154.66.244 +156.154.66.243 +156.154.66.241 +156.154.66.239 +156.154.66.237 +156.154.66.236 +156.154.66.235 +156.154.66.234 +156.154.66.233 +156.154.66.230 +156.154.66.23 +156.154.66.229 +156.154.66.228 +156.154.66.227 +156.154.66.224 +156.154.66.223 +156.154.66.222 +156.154.66.221 +156.154.66.220 +156.154.66.22 +156.154.66.218 +156.154.66.215 +156.154.66.214 +156.154.66.210 +156.154.66.21 +156.154.66.208 +156.154.66.206 +156.154.66.205 +156.154.66.203 +156.154.66.202 +156.154.66.20 +156.154.66.199 +156.154.66.198 +156.154.66.197 +156.154.66.195 +156.154.66.194 +156.154.66.193 +156.154.66.192 +156.154.66.190 +156.154.66.19 +156.154.66.189 +156.154.66.188 +156.154.66.187 +156.154.66.185 +156.154.66.184 +156.154.66.183 +156.154.66.181 +156.154.66.180 +156.154.66.179 +156.154.66.178 +156.154.66.175 +156.154.66.173 +156.154.66.172 +156.154.66.170 +156.154.66.17 +156.154.66.168 +156.154.66.167 +156.154.66.165 +156.154.66.162 +156.154.66.161 +156.154.66.16 +156.154.66.159 +156.154.66.157 +156.154.66.156 +156.154.66.152 +156.154.66.150 +156.154.66.15 +156.154.66.149 +156.154.66.145 +156.154.66.143 +156.154.66.141 +156.154.66.14 +156.154.66.139 +156.154.66.138 +156.154.66.137 +156.154.66.136 +156.154.66.135 +156.154.66.131 +156.154.66.130 +156.154.66.13 +156.154.66.127 +156.154.66.125 +156.154.66.123 +156.154.66.122 +156.154.66.121 +156.154.66.120 +156.154.66.12 +156.154.66.119 +156.154.66.118 +156.154.66.116 +156.154.66.115 +156.154.66.113 +156.154.66.112 +156.154.66.11 +156.154.66.109 +156.154.66.106 +156.154.66.105 +156.154.66.103 +156.154.66.102 +156.154.66.101 +156.154.66.10 +156.154.66.1 +156.154.65.98 +156.154.65.97 +156.154.65.96 +156.154.65.95 +156.154.65.93 +156.154.65.92 +156.154.65.91 +156.154.65.9 +156.154.65.89 +156.154.65.88 +156.154.65.86 +156.154.65.84 +156.154.65.83 +156.154.65.82 +156.154.65.81 +156.154.65.80 +156.154.65.8 +156.154.65.78 +156.154.65.76 +156.154.65.75 +156.154.65.74 +156.154.65.73 +156.154.65.71 +156.154.65.7 +156.154.65.68 +156.154.65.67 +156.154.65.66 +156.154.65.64 +156.154.65.63 +156.154.65.62 +156.154.65.60 +156.154.65.6 +156.154.65.59 +156.154.65.58 +156.154.65.57 +156.154.65.56 +156.154.65.55 +156.154.65.53 +156.154.65.52 +156.154.65.50 +156.154.65.5 +156.154.65.49 +156.154.65.48 +156.154.65.47 +156.154.65.44 +156.154.65.43 +156.154.65.42 +156.154.65.41 +156.154.65.40 +156.154.65.39 +156.154.65.38 +156.154.65.37 +156.154.65.36 +156.154.65.35 +156.154.65.34 +156.154.65.33 +156.154.65.32 +156.154.65.31 +156.154.65.30 +156.154.65.3 +156.154.65.29 +156.154.65.28 +156.154.65.254 +156.154.65.253 +156.154.65.252 +156.154.65.251 +156.154.65.250 +156.154.65.25 +156.154.65.249 +156.154.65.248 +156.154.65.243 +156.154.65.242 +156.154.65.241 +156.154.65.240 +156.154.65.24 +156.154.65.239 +156.154.65.235 +156.154.65.234 +156.154.65.233 +156.154.65.232 +156.154.65.231 +156.154.65.23 +156.154.65.228 +156.154.65.227 +156.154.65.226 +156.154.65.225 +156.154.65.224 +156.154.65.223 +156.154.65.220 +156.154.65.22 +156.154.65.219 +156.154.65.218 +156.154.65.216 +156.154.65.215 +156.154.65.214 +156.154.65.213 +156.154.65.212 +156.154.65.211 +156.154.65.210 +156.154.65.21 +156.154.65.207 +156.154.65.206 +156.154.65.204 +156.154.65.203 +156.154.65.200 +156.154.65.20 +156.154.65.2 +156.154.65.199 +156.154.65.198 +156.154.65.196 +156.154.65.195 +156.154.65.193 +156.154.65.192 +156.154.65.191 +156.154.65.190 +156.154.65.189 +156.154.65.188 +156.154.65.187 +156.154.65.185 +156.154.65.184 +156.154.65.183 +156.154.65.182 +156.154.65.181 +156.154.65.180 +156.154.65.18 +156.154.65.179 +156.154.65.178 +156.154.65.176 +156.154.65.175 +156.154.65.174 +156.154.65.173 +156.154.65.172 +156.154.65.171 +156.154.65.17 +156.154.65.168 +156.154.65.167 +156.154.65.166 +156.154.65.163 +156.154.65.162 +156.154.65.161 +156.154.65.160 +156.154.65.159 +156.154.65.158 +156.154.65.157 +156.154.65.155 +156.154.65.153 +156.154.65.152 +156.154.65.150 +156.154.65.149 +156.154.65.148 +156.154.65.147 +156.154.65.146 +156.154.65.145 +156.154.65.144 +156.154.65.142 +156.154.65.141 +156.154.65.140 +156.154.65.14 +156.154.65.139 +156.154.65.138 +156.154.65.136 +156.154.65.135 +156.154.65.134 +156.154.65.133 +156.154.65.132 +156.154.65.131 +156.154.65.130 +156.154.65.129 +156.154.65.128 +156.154.65.127 +156.154.65.125 +156.154.65.124 +156.154.65.123 +156.154.65.121 +156.154.65.120 +156.154.65.12 +156.154.65.119 +156.154.65.117 +156.154.65.116 +156.154.65.115 +156.154.65.114 +156.154.65.113 +156.154.65.111 +156.154.65.110 +156.154.65.11 +156.154.65.108 +156.154.65.107 +156.154.65.106 +156.154.65.105 +156.154.65.103 +156.154.65.102 +156.154.65.101 +156.154.65.10 +156.154.65.1 +156.154.64.99 +156.154.64.98 +156.154.64.97 +156.154.64.96 +156.154.64.93 +156.154.64.90 +156.154.64.9 +156.154.64.88 +156.154.64.85 +156.154.64.84 +156.154.64.82 +156.154.64.81 +156.154.64.8 +156.154.64.79 +156.154.64.78 +156.154.64.77 +156.154.64.75 +156.154.64.74 +156.154.64.72 +156.154.64.70 +156.154.64.68 +156.154.64.66 +156.154.64.65 +156.154.64.63 +156.154.64.60 +156.154.64.6 +156.154.64.59 +156.154.64.58 +156.154.64.56 +156.154.64.55 +156.154.64.54 +156.154.64.52 +156.154.64.51 +156.154.64.5 +156.154.64.49 +156.154.64.48 +156.154.64.47 +156.154.64.46 +156.154.64.43 +156.154.64.42 +156.154.64.41 +156.154.64.38 +156.154.64.36 +156.154.64.35 +156.154.64.32 +156.154.64.31 +156.154.64.30 +156.154.64.27 +156.154.64.254 +156.154.64.253 +156.154.64.252 +156.154.64.251 +156.154.64.250 +156.154.64.25 +156.154.64.249 +156.154.64.248 +156.154.64.247 +156.154.64.246 +156.154.64.245 +156.154.64.243 +156.154.64.242 +156.154.64.241 +156.154.64.24 +156.154.64.239 +156.154.64.238 +156.154.64.237 +156.154.64.236 +156.154.64.234 +156.154.64.233 +156.154.64.231 +156.154.64.230 +156.154.64.228 +156.154.64.227 +156.154.64.226 +156.154.64.225 +156.154.64.223 +156.154.64.22 +156.154.64.219 +156.154.64.218 +156.154.64.216 +156.154.64.215 +156.154.64.213 +156.154.64.212 +156.154.64.211 +156.154.64.210 +156.154.64.21 +156.154.64.209 +156.154.64.208 +156.154.64.207 +156.154.64.206 +156.154.64.205 +156.154.64.204 +156.154.64.202 +156.154.64.201 +156.154.64.200 +156.154.64.199 +156.154.64.197 +156.154.64.196 +156.154.64.195 +156.154.64.194 +156.154.64.191 +156.154.64.19 +156.154.64.189 +156.154.64.188 +156.154.64.183 +156.154.64.182 +156.154.64.181 +156.154.64.18 +156.154.64.177 +156.154.64.176 +156.154.64.175 +156.154.64.174 +156.154.64.173 +156.154.64.172 +156.154.64.170 +156.154.64.17 +156.154.64.168 +156.154.64.167 +156.154.64.166 +156.154.64.165 +156.154.64.162 +156.154.64.157 +156.154.64.156 +156.154.64.155 +156.154.64.154 +156.154.64.153 +156.154.64.151 +156.154.64.150 +156.154.64.15 +156.154.64.148 +156.154.64.146 +156.154.64.142 +156.154.64.14 +156.154.64.139 +156.154.64.138 +156.154.64.137 +156.154.64.136 +156.154.64.135 +156.154.64.134 +156.154.64.133 +156.154.64.131 +156.154.64.130 +156.154.64.13 +156.154.64.129 +156.154.64.128 +156.154.64.127 +156.154.64.123 +156.154.64.121 +156.154.64.120 +156.154.64.119 +156.154.64.118 +156.154.64.117 +156.154.64.116 +156.154.64.115 +156.154.64.114 +156.154.64.112 +156.154.64.111 +156.154.64.11 +156.154.64.109 +156.154.64.108 +156.154.64.107 +156.154.64.104 +156.154.64.103 +156.154.64.102 +156.154.64.100 +156.154.64.1 +156.154.55.15 +156.154.55.1 +156.154.54.8 +156.154.54.15 +156.154.54.13 +156.154.143.99 +156.154.143.98 +156.154.143.92 +156.154.143.91 +156.154.143.9 +156.154.143.89 +156.154.143.87 +156.154.143.82 +156.154.143.79 +156.154.143.78 +156.154.143.77 +156.154.143.76 +156.154.143.73 +156.154.143.72 +156.154.143.71 +156.154.143.68 +156.154.143.64 +156.154.143.60 +156.154.143.55 +156.154.143.53 +156.154.143.51 +156.154.143.48 +156.154.143.46 +156.154.143.45 +156.154.143.44 +156.154.143.43 +156.154.143.42 +156.154.143.41 +156.154.143.39 +156.154.143.38 +156.154.143.37 +156.154.143.34 +156.154.143.32 +156.154.143.31 +156.154.143.3 +156.154.143.29 +156.154.143.28 +156.154.143.254 +156.154.143.253 +156.154.143.252 +156.154.143.250 +156.154.143.248 +156.154.143.247 +156.154.143.245 +156.154.143.243 +156.154.143.242 +156.154.143.241 +156.154.143.240 +156.154.143.237 +156.154.143.235 +156.154.143.232 +156.154.143.230 +156.154.143.23 +156.154.143.226 +156.154.143.225 +156.154.143.224 +156.154.143.223 +156.154.143.222 +156.154.143.221 +156.154.143.22 +156.154.143.218 +156.154.143.217 +156.154.143.216 +156.154.143.215 +156.154.143.213 +156.154.143.212 +156.154.143.211 +156.154.143.210 +156.154.143.207 +156.154.143.206 +156.154.143.204 +156.154.143.20 +156.154.143.199 +156.154.143.198 +156.154.143.196 +156.154.143.194 +156.154.143.191 +156.154.143.190 +156.154.143.189 +156.154.143.188 +156.154.143.184 +156.154.143.182 +156.154.143.181 +156.154.143.178 +156.154.143.173 +156.154.143.171 +156.154.143.170 +156.154.143.165 +156.154.143.164 +156.154.143.160 +156.154.143.159 +156.154.143.151 +156.154.143.150 +156.154.143.15 +156.154.143.148 +156.154.143.146 +156.154.143.145 +156.154.143.140 +156.154.143.14 +156.154.143.136 +156.154.143.135 +156.154.143.134 +156.154.143.133 +156.154.143.131 +156.154.143.130 +156.154.143.128 +156.154.143.120 +156.154.143.12 +156.154.143.119 +156.154.143.117 +156.154.143.113 +156.154.143.112 +156.154.143.106 +156.154.143.105 +156.154.143.103 +156.154.143.10 +156.154.142.86 +156.154.142.84 +156.154.142.79 +156.154.142.71 +156.154.142.7 +156.154.142.61 +156.154.142.59 +156.154.142.53 +156.154.142.5 +156.154.142.49 +156.154.142.46 +156.154.142.43 +156.154.142.41 +156.154.142.4 +156.154.142.36 +156.154.142.31 +156.154.142.28 +156.154.142.23 +156.154.142.225 +156.154.142.213 +156.154.142.203 +156.154.142.201 +156.154.142.200 +156.154.142.195 +156.154.142.179 +156.154.142.169 +156.154.142.166 +156.154.142.165 +156.154.142.164 +156.154.142.16 +156.154.142.156 +156.154.142.152 +156.154.142.150 +156.154.142.149 +156.154.142.142 +156.154.142.14 +156.154.142.133 +156.154.142.132 +156.154.142.128 +156.154.142.119 +156.154.142.112 +156.154.142.102 +156.154.141.91 +156.154.141.9 +156.154.141.88 +156.154.141.85 +156.154.141.82 +156.154.141.81 +156.154.141.71 +156.154.141.69 +156.154.141.46 +156.154.141.254 +156.154.141.236 +156.154.141.235 +156.154.141.230 +156.154.141.229 +156.154.141.227 +156.154.141.217 +156.154.141.213 +156.154.141.203 +156.154.141.201 +156.154.141.198 +156.154.141.19 +156.154.141.188 +156.154.141.185 +156.154.141.176 +156.154.141.159 +156.154.141.153 +156.154.141.148 +156.154.141.146 +156.154.141.138 +156.154.141.127 +156.154.141.120 +156.154.140.98 +156.154.140.96 +156.154.140.94 +156.154.140.93 +156.154.140.9 +156.154.140.89 +156.154.140.87 +156.154.140.81 +156.154.140.76 +156.154.140.75 +156.154.140.74 +156.154.140.72 +156.154.140.71 +156.154.140.63 +156.154.140.60 +156.154.140.50 +156.154.140.41 +156.154.140.4 +156.154.140.37 +156.154.140.36 +156.154.140.33 +156.154.140.28 +156.154.140.252 +156.154.140.251 +156.154.140.250 +156.154.140.248 +156.154.140.246 +156.154.140.244 +156.154.140.243 +156.154.140.242 +156.154.140.238 +156.154.140.237 +156.154.140.234 +156.154.140.233 +156.154.140.23 +156.154.140.229 +156.154.140.227 +156.154.140.224 +156.154.140.220 +156.154.140.215 +156.154.140.212 +156.154.140.203 +156.154.140.201 +156.154.140.199 +156.154.140.198 +156.154.140.196 +156.154.140.195 +156.154.140.193 +156.154.140.188 +156.154.140.183 +156.154.140.180 +156.154.140.179 +156.154.140.178 +156.154.140.172 +156.154.140.169 +156.154.140.168 +156.154.140.165 +156.154.140.163 +156.154.140.161 +156.154.140.156 +156.154.140.153 +156.154.140.152 +156.154.140.145 +156.154.140.144 +156.154.140.14 +156.154.140.134 +156.154.140.131 +156.154.140.130 +156.154.140.129 +156.154.140.127 +156.154.140.120 +156.154.140.119 +156.154.140.117 +156.154.140.116 +156.154.140.111 +156.154.140.108 +156.154.140.107 +156.154.140.1 +155.4.163.233 +155.33.195.106 +155.133.119.188 +154.72.69.26 +154.70.151.53 +154.70.149.42 +154.66.240.30 +154.65.61.1 +154.63.11.0 +154.61.69.45 +154.44.130.160 +154.27.66.183 +154.236.179.226 +154.160.70.6 +154.159.248.54 +154.14.16.251 +154.127.234.250 +154.126.92.178 +154.126.209.249 +154.118.190.94 +154.113.65.122 +154.10.6.11 +153.246.64.54 +153.246.37.6 +153.19.89.89 +153.19.161.6 +153.19.105.120 +153.156.93.5 +153.153.150.28 +153.150.154.214 +153.120.88.148 +152.26.59.3 +152.231.78.50 +152.230.47.28 +152.230.113.98 +152.230.112.34 +152.228.172.176 +152.200.186.94 +152.165.116.5 +152.158.36.48 +152.149.40.2 +152.149.135.10 +152.117.254.193 +152.101.123.194 +151.80.42.104 +151.80.28.5 +151.80.28.48 +151.80.25.178 +151.80.238.97 +151.80.19.146 +151.80.177.171 +151.80.140.68 +151.8.37.148 +151.237.86.81 +151.237.55.39 +151.237.141.109 +151.237.111.135 +151.192.63.2 +151.192.60.134 +151.192.44.238 +151.14.208.163 +151.1.212.6 +151.1.165.85 +151.0.52.85 +150.254.190.122 +150.240.97.236 +150.240.97.199 +150.240.97.192 +150.220.181.150 +15.236.150.25 +15.235.33.251 +15.204.157.214 +15.188.45.248 +149.91.3.9 +149.76.67.57 +149.62.241.50 +149.255.28.62 +149.202.84.49 +149.202.82.130 +149.202.81.7 +149.202.36.53 +149.202.157.139 +149.202.145.242 +149.154.159.92 +149.129.119.137 +149.126.23.226 +149.112.149.112 +149.112.122.30 +149.112.122.20 +149.112.122.10 +149.112.121.30 +149.112.121.20 +149.112.121.10 +149.112.112.9 +149.112.112.13 +149.112.112.12 +149.112.112.112 +149.112.112.11 +149.112.112.10 +149.106.172.178 +148.81.188.146 +148.78.200.7 +148.76.114.50 +148.72.246.219 +148.72.144.60 +148.251.24.48 +148.251.202.168 +148.251.133.165 +148.247.156.27 +148.245.196.68 +148.244.91.61 +148.244.89.212 +148.244.211.136 +148.244.179.157 +148.244.114.30 +148.244.108.33 +148.243.227.67 +148.243.195.206 +148.243.126.229 +148.235.82.66 +148.235.148.162 +148.233.136.82 +148.205.40.11 +148.205.228.17 +148.205.228.11 +148.102.51.97 +148.102.50.30 +147.50.47.189 +147.50.47.185 +147.50.47.181 +147.50.36.138 +147.50.18.146 +147.135.207.73 +147.135.131.46 +147.135.130.233 +146.88.67.162 +146.88.38.231 +146.71.84.161 +146.71.76.129 +146.59.15.173 +146.196.40.203 +146.158.30.143 +146.155.226.12 +146.120.19.126 +146.112.41.5 +146.112.41.4 +146.112.41.2 +146.0.23.67 +145.255.5.183 +145.255.31.145 +145.239.94.164 +145.239.196.217 +145.239.186.86 +145.236.190.166 +145.131.24.226 +145.131.142.83 +144.91.97.18 +144.91.69.91 +144.91.123.182 +144.91.122.26 +144.91.107.114 +144.48.115.72 +144.48.109.239 +144.48.109.231 +144.202.20.89 +144.139.9.180 +144.137.210.182 +144.129.45.42 +144.126.129.147 +143.244.162.177 +143.244.136.178 +143.233.233.142 +143.208.182.103 +143.208.0.14 +143.0.226.116 +143.0.111.78 +142.44.192.51 +142.190.83.129 +142.11.217.5 +142.11.215.140 +142.11.213.209 +142.11.200.36 +141.95.65.106 +141.95.155.209 +141.94.143.35 +141.195.95.131 +141.193.223.166 +141.136.112.107 +141.101.234.131 +141.1.27.249 +141.1.1.1 +141.0.102.130 +140.227.38.198 +14.97.85.129 +14.97.106.65 +14.96.97.193 +14.96.97.162 +14.96.97.160 +14.96.106.114 +14.63.225.15 +14.63.217.237 +14.63.217.12 +14.63.196.128 +14.56.77.168 +14.54.45.37 +14.52.27.10 +14.51.109.49 +14.50.118.125 +14.47.217.72 +14.43.82.31 +14.42.230.214 +14.41.60.11 +14.41.60.10 +14.36.32.8 +14.35.201.12 +14.33.27.243 +14.33.27.240 +14.241.35.56 +14.241.236.220 +14.241.225.212 +14.225.246.17 +14.225.16.66 +14.224.129.15 +14.200.94.202 +14.200.249.18 +14.198.169.236 +14.198.168.140 +14.192.25.141 +14.178.144.53 +14.161.47.108 +14.161.36.134 +14.161.252.185 +14.160.87.86 +14.140.206.30 +14.139.234.242 +14.128.21.30 +139.91.235.18 +139.64.162.129 +139.18.25.34 +139.18.25.33 +139.162.33.25 +139.162.122.189 +139.162.107.204 +139.135.130.148 +139.134.5.51 +139.134.2.190 +139.130.4.4 +139.130.124.242 +138.99.205.1 +138.97.177.27 +138.94.56.4 +138.91.252.187 +138.91.167.7 +138.68.74.166 +138.255.167.106 +138.255.108.121 +138.219.50.74 +138.219.249.221 +138.204.240.26 +138.201.225.189 +138.186.166.164 +138.185.2.206 +138.185.16.6 +138.128.241.232 +138.122.4.226 +138.121.114.217 +138.121.114.185 +138.0.89.142 +138.0.120.199 +137.74.226.27 +137.66.16.204 +137.220.33.124 +137.184.216.0 +137.118.1.32 +137.118.1.106 +137.103.64.198 +136.244.97.119 +136.243.93.187 +136.243.170.238 +136.243.151.59 +136.243.103.8 +135.181.20.96 +134.90.140.29 +134.75.195.7 +134.75.122.2 +134.35.6.220 +134.35.192.16 +134.35.188.122 +134.35.185.31 +134.35.172.170 +134.255.247.23 +134.236.245.30 +134.209.17.114 +134.195.4.2 +134.17.4.251 +134.122.47.67 +134.122.23.38 +134.119.184.220 +134.119.184.218 +133.18.71.5 +133.167.104.150 +132.255.228.168 +132.148.83.193 +132.148.78.54 +132.148.18.164 +132.145.89.151 +131.255.137.57 +131.255.136.10 +131.221.81.1 +131.196.220.10 +131.196.22.222 +131.196.115.45 +131.196.115.39 +131.196.115.190 +131.148.181.242 +131.100.51.65 +131.100.48.73 +130.93.54.181 +130.93.191.141 +130.93.184.27 +130.61.69.123 +130.61.64.122 +130.255.159.152 +130.255.153.137 +130.255.145.195 +130.255.121.9 +130.244.126.99 +130.105.145.84 +13.95.90.89 +13.76.130.172 +13.67.61.31 +13.49.107.2 +13.237.33.153 +129.7.81.41 +129.7.81.40 +129.7.234.150 +129.250.35.251 +129.250.35.250 +129.153.52.146 +129.126.84.102 +129.126.63.122 +129.126.150.172 +129.126.119.238 +128.92.65.181 +128.92.36.51 +128.238.2.38 +128.134.180.196 +128.134.135.200 +128.127.90.39 +128.127.17.20 +126.249.83.70 +126.249.64.222 +126.249.167.193 +126.249.167.161 +125.7.139.15 +125.63.106.202 +125.61.60.151 +125.61.60.133 +125.61.60.132 +125.254.55.83 +125.254.55.66 +125.235.11.66 +125.234.238.3 +125.234.121.6 +125.23.227.44 +125.229.59.173 +125.229.181.204 +125.229.101.233 +125.228.97.10 +125.228.209.195 +125.228.169.101 +125.228.144.5 +125.227.89.44 +125.227.77.87 +125.227.128.229 +125.214.90.2 +125.213.255.4 +125.209.80.106 +125.209.101.172 +125.206.226.66 +125.206.220.10 +125.206.218.49 +125.20.46.206 +125.19.19.84 +125.18.241.103 +125.16.252.73 +125.143.136.8 +125.141.226.248 +125.141.226.247 +125.141.196.236 +125.140.123.200 +125.138.119.126 +125.134.180.69 +125.130.169.142 +124.9.53.99 +124.82.2.186 +124.40.249.202 +124.35.154.159 +124.35.115.150 +124.33.8.69 +124.32.115.205 +124.254.74.246 +124.248.191.83 +124.155.197.234 +124.146.185.97 +124.137.207.115 +124.107.131.17 +124.107.126.1 +124.107.105.97 +124.106.99.238 +124.106.234.45 +124.106.13.147 +124.106.103.113 +124.105.219.222 +124.105.219.198 +124.105.219.106 +124.105.217.62 +124.105.217.50 +124.105.214.82 +124.105.214.174 +124.105.214.110 +124.105.212.198 +124.105.212.194 +124.105.212.158 +124.105.212.146 +124.105.209.234 +124.105.157.110 +124.105.154.90 +124.105.154.238 +124.105.151.86 +124.105.150.94 +123.51.244.77 +123.49.39.106 +123.30.27.24 +123.30.210.134 +123.30.187.240 +123.30.184.141 +123.30.175.83 +123.30.175.82 +123.30.116.18 +123.252.254.74 +123.252.254.26 +123.252.254.22 +123.252.215.25 +123.252.214.45 +123.252.213.83 +123.25.15.217 +123.25.129.97 +123.248.242.21 +123.243.55.86 +123.24.206.120 +123.226.235.68 +123.215.198.209 +123.205.156.16 +123.200.168.132 +123.200.11.90 +123.176.98.140 +123.176.4.39 +123.176.4.227 +123.176.4.224 +123.176.4.2 +123.176.31.226 +123.176.27.94 +123.142.124.146 +123.140.194.2 +123.109.246.249 +122.55.80.210 +122.55.4.89 +122.55.34.214 +122.55.31.181 +122.55.242.174 +122.55.236.166 +122.55.216.174 +122.55.204.1 +122.55.203.38 +122.55.16.1 +122.55.159.150 +122.55.158.66 +122.55.158.162 +122.55.145.178 +122.55.145.150 +122.55.143.226 +122.55.143.170 +122.55.142.178 +122.55.142.154 +122.55.111.98 +122.55.102.170 +122.54.95.78 +122.54.86.193 +122.54.86.1 +122.54.78.230 +122.54.69.14 +122.54.69.130 +122.54.62.137 +122.54.51.2 +122.54.23.49 +122.54.21.218 +122.54.17.114 +122.53.95.26 +122.53.95.126 +122.53.221.170 +122.53.218.12 +122.53.214.65 +122.53.213.74 +122.53.191.170 +122.53.190.38 +122.53.184.177 +122.53.179.133 +122.53.148.218 +122.53.148.193 +122.53.122.234 +122.52.251.163 +122.52.229.125 +122.52.117.51 +122.3.232.38 +122.3.22.133 +122.3.205.241 +122.252.241.166 +122.252.222.157 +122.214.1.141 +122.211.89.209 +122.210.62.171 +122.2.5.42 +122.2.29.66 +122.187.48.102 +122.186.116.209 +122.185.99.78 +122.185.99.106 +122.185.251.225 +122.185.198.242 +122.185.135.135 +122.185.113.244 +122.18.242.92 +122.18.232.147 +122.160.254.158 +122.155.37.109 +122.155.1.72 +122.154.136.56 +122.15.192.21 +122.147.248.147 +122.117.187.15 +122.117.151.111 +122.116.8.109 +122.11.249.26 +122.1.33.1 +121.91.48.192 +121.78.116.111 +121.58.248.195 +121.58.203.4 +121.50.200.15 +121.4.4.41 +121.4.4.246 +121.254.193.196 +121.254.171.228 +121.254.134.99 +121.202.148.156 +121.189.15.8 +121.188.121.59 +121.184.236.110 +121.183.253.122 +121.183.146.212 +121.182.244.180 +121.182.199.17 +121.182.194.189 +121.181.11.82 +121.180.180.71 +121.180.117.234 +121.176.16.144 +121.174.253.216 +121.174.236.100 +121.166.56.200 +121.166.237.218 +121.166.237.204 +121.166.157.11 +121.156.65.149 +121.156.120.240 +121.156.104.183 +121.156.104.182 +121.155.94.225 +121.153.88.212 +121.152.181.152 +121.151.111.5 +121.146.2.10 +121.139.218.165 +121.133.171.50 +121.131.194.203 +121.129.56.60 +121.128.176.211 +121.125.71.40 +121.125.71.213 +121.125.68.67 +121.122.55.58 +121.119.138.109 +120.88.120.137 +120.72.85.169 +120.72.85.167 +120.72.106.125 +120.57.113.146 +120.53.53.84 +120.53.53.198 +120.53.53.137 +120.53.53.116 +120.50.42.142 +120.28.196.176 +120.28.194.84 +120.138.27.84 +120.138.22.174 +12.97.172.197 +12.71.198.244 +12.71.108.154 +12.68.237.194 +12.55.50.170 +12.51.21.245 +12.40.39.6 +12.40.39.5 +12.28.98.3 +12.221.3.59 +12.221.135.162 +12.218.209.130 +12.216.90.50 +12.216.22.17 +12.200.123.164 +12.20.121.22 +12.189.150.34 +12.186.153.128 +12.181.159.78 +12.171.191.58 +12.148.208.86 +12.127.17.72 +12.127.17.71 +12.127.16.77 +12.127.16.67 +12.12.131.134 +12.109.212.19 +12.107.114.42 +12.0.210.194 +119.93.121.146 +119.92.80.57 +119.92.65.97 +119.92.223.242 +119.92.197.6 +119.92.192.241 +119.92.191.34 +119.92.187.97 +119.92.163.10 +119.92.150.133 +119.92.117.102 +119.9.73.44 +119.75.28.242 +119.73.184.241 +119.73.138.17 +119.73.105.7 +119.68.137.99 +119.59.117.3 +119.59.113.138 +119.207.62.155 +119.205.209.167 +119.204.80.25 +119.203.236.251 +119.201.211.21 +119.201.108.71 +119.199.215.81 +119.198.142.165 +119.197.120.3 +119.18.36.102 +119.17.75.70 +119.156.24.134 +119.152.243.157 +119.110.212.115 +119.10.181.138 +118.99.210.36 +118.98.223.17 +118.91.129.65 +118.91.10.17 +118.70.203.68 +118.70.177.223 +118.69.65.41 +118.69.246.104 +118.69.197.82 +118.69.157.184 +118.69.134.83 +118.69.109.45 +118.47.242.4 +118.45.48.177 +118.40.29.112 +118.38.9.207 +118.37.195.251 +118.3.227.163 +118.26.111.53 +118.238.203.252 +118.238.11.132 +118.233.57.133 +118.232.137.224 +118.220.172.122 +118.220.16.99 +118.218.6.137 +118.217.180.182 +118.21.162.12 +118.201.56.26 +118.201.53.209 +118.201.211.82 +118.201.187.122 +118.179.84.158 +118.175.16.66 +118.173.197.174 +118.163.7.54 +118.163.40.163 +118.143.97.54 +118.127.62.178 +118.103.239.9 +118.103.239.33 +117.55.243.14 +117.54.3.237 +117.53.152.76 +117.52.99.143 +117.4.91.86 +117.206.156.235 +117.20.67.244 +117.20.54.245 +117.20.54.242 +117.2.80.119 +117.2.18.50 +117.16.191.7 +117.122.125.106 +117.121.215.99 +117.121.215.101 +117.103.228.101 +117.102.214.93 +116.91.115.190 +116.82.248.104 +116.73.110.135 +116.71.135.74 +116.68.126.100 +116.58.187.189 +116.50.230.138 +116.50.180.210 +116.48.144.195 +116.48.132.6 +116.250.217.39 +116.212.100.98 +116.203.32.217 +116.203.181.6 +116.203.180.236 +116.202.21.65 +116.125.157.67 +116.125.124.79 +116.122.39.197 +116.121.52.43 +116.121.27.10 +116.120.11.6 +116.12.215.137 +116.12.206.109 +116.12.172.241 +116.118.119.167 +116.100.88.123 +115.99.215.16 +115.99.213.7 +115.99.212.118 +115.99.187.128 +115.99.102.41 +115.96.208.210 +115.96.149.199 +115.95.56.126 +115.84.79.71 +115.79.7.63 +115.79.5.78 +115.73.220.183 +115.70.225.190 +115.69.240.6 +115.68.110.146 +115.42.222.113 +115.42.210.81 +115.42.204.234 +115.31.133.178 +115.23.219.218 +115.178.53.146 +115.160.160.146 +115.147.21.134 +115.146.254.38 +115.146.252.198 +115.146.250.193 +115.146.249.49 +115.146.199.242 +115.146.192.90 +115.146.192.218 +115.146.189.170 +115.146.174.102 +115.146.127.181 +115.146.120.140 +115.126.20.15 +114.7.120.14 +114.6.46.153 +114.34.176.73 +114.33.89.166 +114.33.53.151 +114.33.41.219 +114.32.23.229 +114.199.226.206 +114.198.144.44 +114.179.13.90 +114.160.59.146 +114.160.203.201 +114.160.194.67 +114.156.146.116 +114.143.91.103 +114.143.88.197 +114.143.37.233 +114.143.34.54 +114.143.34.228 +114.143.34.218 +114.130.5.6 +114.130.5.5 +114.114.115.115 +114.114.114.114 +114.108.141.187 +113.61.224.12 +113.52.197.14 +113.42.99.133 +113.28.94.231 +113.28.71.243 +113.28.67.147 +113.28.67.105 +113.255.5.34 +113.22.113.64 +113.198.254.2 +113.196.55.130 +113.190.253.229 +113.176.7.202 +113.174.246.243 +113.165.96.215 +113.165.94.135 +113.164.80.4 +113.163.222.44 +113.162.247.163 +113.161.76.34 +113.161.71.132 +113.161.30.2 +113.161.230.20 +113.161.230.19 +113.161.208.18 +113.161.196.15 +113.161.180.214 +113.161.18.44 +113.161.169.161 +113.161.163.169 +113.161.116.150 +113.160.250.45 +113.160.232.172 +113.160.227.246 +113.160.226.179 +113.160.155.57 +113.160.116.252 +113.149.254.97 +112.76.169.12 +112.76.132.49 +112.220.81.150 +112.216.251.98 +112.216.19.69 +112.216.19.68 +112.216.138.50 +112.199.115.40 +112.198.179.23 +112.196.19.243 +112.186.103.96 +112.175.232.142 +112.173.44.139 +112.172.7.207 +112.169.182.143 +112.157.117.247 +112.154.101.76 +112.133.241.244 +112.133.198.54 +112.133.114.222 +112.121.184.34 +111.92.96.96 +111.92.96.19 +111.92.85.233 +111.92.189.105 +111.92.106.148 +111.92.105.39 +111.92.100.55 +111.68.108.215 +111.68.108.200 +111.125.72.190 +111.118.223.243 +111.118.147.236 +110.9.165.5 +110.78.18.44 +110.77.149.172 +110.76.152.20 +110.49.95.45 +110.49.78.20 +110.49.144.179 +110.49.124.74 +110.49.124.73 +110.49.123.26 +110.49.123.187 +110.49.11.170 +110.45.182.88 +110.44.123.48 +110.4.40.214 +110.174.24.94 +110.170.140.105 +110.164.95.66 +110.164.71.170 +110.164.241.236 +110.164.193.206 +110.164.151.194 +110.164.139.66 +110.164.139.186 +110.15.182.99 +110.15.182.145 +110.145.166.222 +110.145.154.62 +110.143.26.239 +110.143.148.183 +110.142.40.60 +109.96.66.14 +109.92.27.159 +109.92.133.78 +109.75.45.3 +109.75.41.201 +109.73.42.171 +109.73.39.154 +109.72.239.247 +109.69.6.30 +109.68.15.212 +109.68.14.61 +109.5.33.66 +109.248.212.9 +109.248.2.1 +109.248.157.46 +109.248.157.118 +109.245.230.145 +109.241.116.68 +109.238.224.178 +109.237.94.186 +109.235.216.5 +109.234.249.10 +109.234.248.10 +109.233.192.72 +109.232.88.4 +109.232.88.3 +109.228.8.84 +109.228.8.83 +109.228.22.126 +109.228.21.223 +109.228.16.144 +109.228.1.132 +109.228.0.238 +109.226.199.197 +109.224.233.190 +109.224.233.174 +109.203.100.192 +109.202.11.6 +109.200.180.226 +109.199.77.76 +109.199.253.44 +109.197.71.34 +109.197.107.104 +109.195.146.245 +109.194.118.24 +109.168.65.86 +109.163.232.228 +109.160.96.238 +109.125.204.16 +109.117.16.197 +109.111.9.14 +109.111.8.0 +109.111.75.114 +109.111.117.212 +109.111.112.58 +109.110.44.12 +109.110.40.41 +109.110.40.214 +109.110.238.65 +109.105.55.39 +109.105.45.31 +109.105.45.30 +109.105.40.34 +108.41.102.212 +108.162.196.93 +108.162.196.91 +108.162.196.79 +108.162.196.77 +108.162.196.75 +108.162.196.67 +108.162.196.62 +108.162.196.3 +108.162.196.245 +108.162.196.238 +108.162.196.229 +108.162.196.219 +108.162.196.212 +108.162.196.21 +108.162.196.205 +108.162.196.20 +108.162.196.199 +108.162.196.180 +108.162.196.173 +108.162.196.17 +108.162.196.16 +108.162.196.15 +108.162.196.129 +108.162.196.12 +108.162.195.95 +108.162.195.94 +108.162.195.93 +108.162.195.91 +108.162.195.8 +108.162.195.7 +108.162.195.67 +108.162.195.65 +108.162.195.45 +108.162.195.42 +108.162.195.36 +108.162.195.31 +108.162.195.30 +108.162.195.29 +108.162.195.247 +108.162.195.246 +108.162.195.220 +108.162.195.208 +108.162.195.202 +108.162.195.197 +108.162.195.183 +108.162.195.181 +108.162.195.172 +108.162.195.167 +108.162.195.163 +108.162.195.137 +108.162.195.135 +108.162.195.132 +108.162.195.127 +108.162.195.118 +108.162.195.113 +108.162.194.93 +108.162.194.87 +108.162.194.85 +108.162.194.78 +108.162.194.64 +108.162.194.6 +108.162.194.56 +108.162.194.5 +108.162.194.40 +108.162.194.36 +108.162.194.35 +108.162.194.32 +108.162.194.3 +108.162.194.252 +108.162.194.251 +108.162.194.25 +108.162.194.249 +108.162.194.24 +108.162.194.23 +108.162.194.229 +108.162.194.227 +108.162.194.215 +108.162.194.213 +108.162.194.201 +108.162.194.193 +108.162.194.186 +108.162.194.182 +108.162.194.180 +108.162.194.177 +108.162.194.159 +108.162.194.153 +108.162.194.152 +108.162.194.150 +108.162.194.141 +108.162.194.127 +108.162.194.120 +108.162.194.112 +108.162.194.11 +108.162.194.105 +108.162.194.103 +108.162.194.10 +108.162.193.99 +108.162.193.94 +108.162.193.83 +108.162.193.70 +108.162.193.37 +108.162.193.33 +108.162.193.32 +108.162.193.250 +108.162.193.242 +108.162.193.227 +108.162.193.226 +108.162.193.225 +108.162.193.224 +108.162.193.221 +108.162.193.207 +108.162.193.201 +108.162.193.198 +108.162.193.181 +108.162.193.161 +108.162.193.154 +108.162.193.151 +108.162.193.139 +108.162.193.130 +108.162.193.112 +108.162.193.111 +108.162.193.108 +108.162.193.106 +108.162.193.104 +108.162.192.8 +108.162.192.79 +108.162.192.66 +108.162.192.65 +108.162.192.45 +108.162.192.35 +108.162.192.3 +108.162.192.27 +108.162.192.254 +108.162.192.248 +108.162.192.238 +108.162.192.237 +108.162.192.234 +108.162.192.208 +108.162.192.200 +108.162.192.19 +108.162.192.185 +108.162.192.183 +108.162.192.164 +108.162.192.162 +108.162.192.149 +108.162.192.146 +108.162.192.140 +108.162.192.14 +108.162.192.136 +108.162.192.134 +108.162.192.127 +108.162.192.124 +108.161.133.137 +107.80.51.240 +107.80.230.195 +107.80.230.178 +107.241.236.99 +107.241.236.106 +107.241.170.173 +107.189.31.10 +107.182.193.69 +107.170.102.45 +107.144.121.130 +107.130.51.137 +107.125.177.89 +107.0.218.126 +106.0.61.250 +105.30.247.93 +105.29.89.225 +105.255.121.94 +105.247.182.102 +105.244.179.78 +105.244.179.225 +105.243.201.129 +105.243.179.28 +105.243.179.199 +105.243.179.17 +105.243.178.87 +105.242.63.54 +104.60.85.131 +104.245.55.17 +104.245.144.98 +104.232.6.1 +104.207.130.197 +104.187.69.92 +104.168.159.220 +104.155.237.225 +104.131.163.103 +103.99.150.10 +103.99.110.113 +103.95.148.6 +103.93.150.184 +103.90.162.6 +103.9.88.154 +103.86.99.100 +103.86.96.100 +103.86.135.166 +103.86.103.29 +103.84.96.6 +103.84.132.2 +103.84.119.230 +103.84.119.226 +103.84.119.192 +103.84.119.172 +103.83.33.13 +103.82.242.153 +103.82.241.138 +103.80.1.193 +103.79.74.1 +103.78.35.229 +103.77.227.162 +103.77.188.19 +103.77.188.18 +103.74.230.32 +103.74.228.89 +103.74.228.39 +103.74.228.177 +103.7.172.8 +103.7.172.7 +103.68.156.122 +103.67.152.193 +103.65.240.147 +103.59.52.3 +103.59.176.154 +103.58.120.120 +103.57.71.89 +103.57.71.38 +103.57.71.2 +103.57.71.156 +103.57.71.145 +103.57.71.140 +103.57.71.137 +103.57.71.120 +103.57.71.102 +103.57.70.98 +103.57.70.62 +103.57.70.34 +103.57.70.220 +103.57.70.103 +103.53.228.24 +103.51.144.216 +103.51.144.214 +103.51.144.212 +103.51.139.51 +103.50.215.236 +103.5.148.99 +103.5.148.100 +103.48.78.157 +103.48.78.156 +103.48.207.78 +103.35.140.52 +103.35.140.43 +103.30.245.97 +103.28.39.63 +103.28.37.218 +103.28.114.57 +103.26.170.103 +103.251.105.188 +103.250.28.6 +103.249.33.206 +103.247.156.200 +103.246.244.143 +103.242.58.167 +103.242.58.166 +103.242.124.7 +103.241.181.28 +103.239.32.81 +103.239.32.36 +103.239.165.34 +103.237.97.164 +103.237.147.46 +103.237.127.20 +103.232.32.246 +103.23.223.249 +103.228.35.43 +103.225.36.238 +103.225.36.226 +103.22.245.50 +103.22.181.122 +103.217.216.122 +103.215.16.230 +103.213.202.166 +103.211.26.243 +103.211.153.41 +103.209.199.8 +103.209.199.6 +103.209.199.114 +103.209.199.107 +103.206.247.194 +103.205.178.9 +103.204.68.85 +103.204.68.48 +103.204.68.219 +103.204.68.165 +103.204.246.241 +103.202.221.88 +103.202.221.69 +103.202.221.53 +103.202.221.40 +103.202.221.34 +103.202.221.224 +103.202.221.210 +103.202.221.200 +103.202.221.178 +103.202.221.163 +103.202.221.149 +103.202.221.12 +103.202.221.107 +103.200.218.78 +103.200.218.77 +103.20.28.2 +103.20.184.97 +103.20.184.81 +103.20.152.11 +103.198.19.2 +103.196.38.8 +103.196.38.39 +103.196.38.38 +103.196.16.2 +103.196.136.7 +103.191.85.74 +103.191.85.56 +103.191.85.50 +103.191.85.46 +103.191.85.45 +103.191.85.168 +103.191.85.164 +103.191.84.84 +103.191.84.54 +103.191.84.50 +103.191.84.28 +103.191.84.186 +103.191.84.112 +103.191.84.10 +103.190.43.95 +103.190.43.24 +103.190.43.227 +103.190.43.148 +103.190.178.190 +103.190.169.4 +103.186.253.5 +103.186.253.244 +103.186.253.243 +103.186.253.240 +103.186.253.185 +103.186.253.114 +103.186.253.112 +103.186.253.109 +103.186.252.56 +103.186.252.29 +103.186.252.218 +103.185.25.167 +103.184.180.241 +103.183.16.25 +103.181.123.94 +103.181.123.9 +103.181.123.86 +103.181.123.83 +103.181.123.76 +103.181.123.68 +103.181.123.65 +103.181.123.5 +103.181.123.36 +103.181.123.241 +103.181.123.231 +103.181.123.212 +103.181.123.196 +103.181.123.19 +103.181.123.188 +103.181.123.141 +103.181.123.134 +103.181.123.109 +103.181.122.92 +103.181.122.6 +103.181.122.249 +103.181.122.241 +103.181.122.236 +103.181.122.17 +103.181.122.16 +103.181.122.158 +103.181.122.148 +103.181.122.131 +103.181.122.108 +103.181.122.105 +103.181.122.101 +103.18.138.20 +103.179.182.47 +103.179.182.237 +103.178.86.41 +103.178.194.131 +103.177.234.10 +103.176.159.18 +103.175.2.235 +103.174.224.9 +103.174.224.87 +103.174.224.84 +103.174.224.70 +103.174.224.50 +103.174.224.41 +103.174.224.39 +103.174.224.38 +103.174.224.247 +103.174.224.241 +103.174.224.225 +103.174.224.222 +103.174.224.22 +103.174.224.21 +103.174.224.208 +103.174.224.184 +103.174.224.151 +103.174.224.140 +103.174.224.124 +103.174.224.119 +103.174.224.117 +103.174.224.10 +103.174.224.0 +103.174.102.61 +103.173.173.173 +103.173.152.233 +103.172.197.43 +103.172.172.51 +103.171.182.73 +103.171.180.48 +103.170.90.121 +103.170.172.95 +103.170.172.79 +103.170.172.77 +103.170.172.73 +103.170.172.61 +103.170.172.50 +103.170.172.38 +103.170.172.3 +103.170.172.254 +103.170.172.252 +103.170.172.249 +103.170.172.246 +103.170.172.232 +103.170.172.221 +103.170.172.220 +103.170.172.203 +103.170.172.199 +103.170.172.185 +103.170.172.182 +103.170.172.175 +103.170.172.164 +103.170.172.163 +103.170.172.162 +103.170.172.157 +103.170.172.151 +103.170.172.148 +103.170.172.147 +103.170.172.138 +103.170.172.127 +103.170.172.113 +103.170.172.111 +103.17.176.84 +103.17.176.81 +103.17.176.73 +103.17.176.32 +103.17.176.31 +103.17.176.3 +103.17.176.251 +103.17.176.240 +103.17.176.231 +103.17.176.215 +103.17.176.214 +103.17.176.182 +103.17.176.181 +103.17.176.175 +103.17.176.173 +103.17.176.146 +103.17.176.133 +103.17.176.128 +103.17.176.117 +103.17.176.115 +103.169.187.67 +103.168.29.185 +103.168.177.230 +103.166.171.69 +103.165.4.82 +103.165.39.50 +103.165.165.68 +103.165.154.1 +103.165.118.76 +103.165.118.179 +103.164.116.180 +103.163.117.61 +103.163.117.223 +103.162.57.37 +103.161.42.2 +103.161.31.137 +103.161.128.130 +103.161.128.128 +103.16.63.166 +103.16.25.47 +103.16.25.46 +103.16.118.11 +103.159.251.209 +103.159.195.40 +103.159.195.234 +103.159.195.156 +103.157.152.6 +103.156.86.78 +103.156.86.29 +103.156.75.132 +103.156.66.254 +103.156.239.193 +103.156.239.106 +103.155.64.208 +103.155.42.204 +103.155.42.150 +103.155.42.130 +103.155.199.45 +103.155.198.142 +103.155.174.87 +103.155.174.85 +103.155.174.68 +103.155.174.113 +103.155.174.109 +103.154.49.74 +103.154.241.252 +103.154.2.93 +103.154.16.58 +103.154.16.5 +103.154.16.34 +103.154.16.32 +103.154.16.30 +103.154.16.255 +103.154.16.25 +103.154.16.238 +103.154.16.228 +103.154.16.217 +103.154.16.210 +103.154.16.208 +103.154.16.177 +103.154.16.148 +103.154.16.136 +103.154.16.128 +103.154.16.115 +103.154.16.101 +103.153.49.82 +103.153.49.41 +103.153.49.251 +103.153.48.83 +103.153.48.27 +103.153.48.25 +103.153.191.42 +103.153.190.78 +103.153.190.238 +103.152.143.244 +103.152.101.135 +103.151.246.46 +103.151.226.62 +103.151.226.42 +103.151.155.21 +103.15.246.182 +103.149.165.83 +103.149.165.162 +103.148.178.80 +103.148.178.77 +103.148.178.7 +103.148.178.63 +103.148.178.59 +103.148.178.54 +103.148.178.50 +103.148.178.35 +103.148.178.3 +103.148.178.20 +103.148.178.179 +103.148.178.161 +103.148.178.104 +103.148.130.112 +103.148.112.146 +103.146.55.82 +103.146.55.67 +103.146.55.61 +103.146.55.53 +103.146.55.49 +103.146.55.48 +103.146.55.39 +103.146.55.27 +103.146.55.250 +103.146.55.247 +103.146.55.243 +103.146.55.171 +103.146.55.157 +103.146.55.153 +103.146.55.142 +103.146.55.14 +103.146.55.137 +103.146.55.133 +103.146.55.131 +103.146.55.124 +103.146.55.108 +103.146.54.25 +103.146.54.247 +103.146.54.244 +103.146.54.24 +103.146.54.218 +103.146.54.217 +103.146.54.112 +103.146.54.104 +103.145.36.151 +103.145.165.91 +103.145.165.7 +103.145.165.59 +103.145.165.42 +103.145.165.235 +103.145.165.233 +103.145.165.226 +103.145.165.213 +103.145.165.206 +103.145.165.204 +103.145.165.196 +103.145.165.172 +103.145.165.155 +103.145.165.116 +103.145.164.99 +103.145.164.94 +103.145.164.85 +103.145.164.81 +103.145.164.77 +103.145.164.70 +103.145.164.57 +103.145.164.52 +103.145.164.51 +103.145.164.50 +103.145.164.5 +103.145.164.39 +103.145.164.32 +103.145.164.247 +103.145.164.239 +103.145.164.234 +103.145.164.231 +103.145.164.226 +103.145.164.221 +103.145.164.218 +103.145.164.206 +103.145.164.203 +103.145.164.186 +103.145.164.18 +103.145.164.178 +103.145.164.162 +103.145.164.153 +103.145.164.141 +103.145.164.14 +103.145.164.113 +103.144.144.58 +103.143.237.9 +103.143.237.85 +103.143.237.82 +103.143.237.40 +103.143.237.206 +103.143.237.172 +103.143.237.165 +103.143.237.140 +103.143.237.136 +103.143.237.10 +103.143.236.96 +103.143.236.89 +103.143.236.37 +103.143.236.233 +103.143.236.219 +103.143.236.203 +103.143.236.200 +103.143.236.181 +103.143.236.142 +103.143.236.105 +103.142.147.14 +103.141.200.98 +103.141.200.97 +103.141.200.75 +103.141.200.12 +103.140.25.69 +103.140.25.43 +103.140.25.36 +103.140.25.223 +103.140.25.191 +103.140.25.121 +103.140.25.115 +103.140.25.100 +103.140.24.95 +103.140.24.75 +103.140.24.248 +103.140.24.244 +103.140.24.209 +103.140.24.201 +103.140.24.175 +103.140.24.158 +103.140.24.148 +103.140.24.14 +103.140.24.130 +103.140.24.125 +103.140.24.124 +103.140.24.115 +103.140.24.105 +103.140.24.102 +103.140.17.242 +103.139.14.2 +103.138.51.141 +103.138.175.58 +103.138.175.22 +103.137.156.3 +103.137.10.193 +103.135.172.78 +103.135.172.73 +103.135.172.60 +103.135.172.37 +103.135.172.31 +103.135.172.28 +103.135.172.199 +103.135.172.197 +103.135.172.191 +103.135.172.141 +103.135.172.117 +103.135.138.6 +103.135.135.64 +103.134.44.222 +103.133.122.148 +103.132.52.32 +103.132.52.31 +103.132.242.59 +103.130.4.82 +103.130.4.8 +103.13.41.2 +103.13.123.16 +103.13.112.251 +103.129.211.142 +103.129.211.13 +103.126.87.123 +103.126.201.33 +103.126.201.129 +103.126.201.1 +103.125.163.241 +103.124.225.75 +103.123.226.10 +103.123.225.10 +103.122.65.97 +103.122.29.129 +103.122.252.82 +103.122.252.81 +103.121.228.5 +103.121.228.1 +103.120.111.2 +103.12.31.83 +103.12.2.11 +103.119.146.46 +103.119.126.68 +103.119.109.162 +103.119.109.160 +103.118.178.18 +103.117.63.67 +103.115.119.41 +103.115.118.210 +103.115.118.201 +103.115.118.197 +103.115.118.113 +103.112.207.159 +103.112.204.37 +103.112.12.214 +103.111.122.2 +103.109.39.79 +103.109.239.243 +103.109.239.242 +103.108.9.217 +103.107.186.1 +103.106.219.117 +103.105.78.207 +103.105.64.163 +103.105.212.98 +103.103.88.39 +103.103.127.169 +103.103.126.6 +103.103.126.110 +103.103.125.253 +103.103.125.247 +103.103.125.133 +103.103.125.130 +103.103.124.70 +103.103.124.247 +103.103.124.212 +103.103.124.200 +103.103.124.144 +103.103.124.120 +103.102.250.37 +103.102.250.209 +103.102.250.127 +103.102.250.102 +103.102.136.102 +103.10.171.230 +102.91.8.126 +102.68.79.77 +102.67.139.204 +102.64.76.49 +102.50.253.95 +102.33.47.78 +102.33.46.203 +102.33.45.23 +102.33.45.152 +102.221.92.53 +102.221.244.1 +102.221.12.46 +102.22.81.43 +102.22.81.114 +102.22.72.24 +102.22.108.2 +102.219.209.155 +102.218.172.222 +102.217.29.78 +102.217.123.98 +102.216.69.18 +102.216.223.14 +102.212.226.177 +102.176.81.182 +102.164.255.149 +102.16.68.12 +102.141.244.134 +102.133.139.27 +101.255.119.209 +101.198.198.198 +101.110.40.25 +101.102.196.118 +101.102.103.104 +101.101.101.101 +101.0.97.70 +101.0.6.195 +1.9.70.93 +1.9.70.86 +1.9.70.84 +1.9.165.210 +1.9.111.99 +1.9.111.97 +1.4.203.13 +1.4.155.85 +1.38.3.52 +1.38.3.168 +1.38.3.167 +1.34.94.130 +1.34.58.188 +1.34.189.116 +1.33.204.121 +1.33.199.58 +1.33.199.57 +1.33.129.233 +1.255.134.74 +1.253.233.47 +1.253.233.42 +1.252.22.37 +1.251.44.10 +1.250.66.91 +1.250.66.88 +1.250.246.15 +1.249.43.20 +1.249.207.225 +1.248.31.204 +1.247.32.84 +1.246.219.171 +1.246.201.9 +1.246.201.53 +1.246.201.38 +1.246.201.12 +1.245.51.74 +1.245.143.166 +1.242.141.19 +1.241.255.203 +1.238.6.146 +1.237.46.97 +1.237.229.208 +1.236.11.126 +1.236.11.121 +1.235.89.75 +1.235.89.4 +1.235.186.131 +1.234.79.82 +1.234.72.207 +1.234.72.165 +1.234.72.146 +1.234.66.81 +1.234.4.150 +1.234.31.2 +1.234.178.2 +1.233.8.222 +1.231.163.67 +1.229.87.167 +1.229.87.157 +1.229.87.138 +1.229.79.96 +1.229.71.72 +1.229.184.91 +1.228.42.28 +1.228.32.93 +1.228.224.139 +1.228.122.57 +1.227.9.73 +1.227.9.134 +1.227.56.61 +1.226.193.136 +1.226.190.17 +1.225.165.101 +1.224.187.102 +1.221.232.82 +1.21.10.81 +1.209.148.129 +1.179.228.10 +1.179.166.229 +1.179.153.125 +1.12.13.53 +1.11.251.105 +1.10.10.10 +1.1.1.3 +1.1.1.2 +1.1.1.1 +1.0.241.30 +1.0.161.80 +1.0.158.183 +1.0.153.150 +1.0.0.3 +1.0.0.2 +1.0.0.19 +1.0.0.1 \ No newline at end of file diff --git a/backend/scripts/performance/monitor_pg_performance.sh b/backend/scripts/performance/monitor_pg_performance.sh new file mode 100755 index 00000000..389521d6 --- /dev/null +++ b/backend/scripts/performance/monitor_pg_performance.sh @@ -0,0 +1,146 @@ +#!/bin/bash + +# PostgreSQL 性能监控脚本 +# 用法:./monitor_pg_performance.sh [数据库名] [间隔秒数] + +# 尝试从 .env 加载数据库配置 +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +if [ -f "$PROJECT_DIR/.env" ]; then + export PGHOST=$(grep "^DB_HOST=" "$PROJECT_DIR/.env" | cut -d '=' -f2) + export PGPORT=$(grep "^DB_PORT=" "$PROJECT_DIR/.env" | cut -d '=' -f2) + export PGUSER=$(grep "^DB_USER=" "$PROJECT_DIR/.env" | cut -d '=' -f2) + export PGPASSWORD=$(grep "^DB_PASSWORD=" "$PROJECT_DIR/.env" | cut -d '=' -f2) + PGDATABASE=$(grep "^DB_NAME=" "$PROJECT_DIR/.env" | cut -d '=' -f2) +fi + +DB_NAME=${1:-${PGDATABASE:-xingrin}} +INTERVAL=${2:-2} + +echo "========================================" +echo " PostgreSQL 性能监控" +echo "========================================" +echo "数据库: $DB_NAME" +echo "刷新间隔: ${INTERVAL}秒" +echo "按 Ctrl+C 停止监控" +echo "========================================" +echo "" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +while true; do + clear + echo -e "${BLUE}========================================" + echo " PostgreSQL 实时性能监控" + echo "========================================${NC}" + echo "时间: $(date '+%Y-%m-%d %H:%M:%S')" + echo "" + + # 1. 数据库连接数 + echo -e "${GREEN}[1] 连接数统计${NC}" + psql -d $DB_NAME -c " + SELECT + count(*) as total_connections, + count(*) FILTER (WHERE state = 'active') as active, + count(*) FILTER (WHERE state = 'idle') as idle, + count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction + FROM pg_stat_activity + WHERE datname = '$DB_NAME'; + " -t + + # 2. 当前活跃查询 + echo -e "${GREEN}[2] 活跃查询 (Top 5)${NC}" + psql -d $DB_NAME -c " + SELECT + pid, + usename, + application_name, + state, + EXTRACT(EPOCH FROM (now() - query_start))::int as duration_sec, + LEFT(query, 60) as query_preview + FROM pg_stat_activity + WHERE datname = '$DB_NAME' + AND state != 'idle' + AND query NOT LIKE '%pg_stat_activity%' + ORDER BY query_start + LIMIT 5; + " + + # 3. 表级统计(插入速度) + echo -e "${GREEN}[3] 表插入统计${NC}" + psql -d $DB_NAME -c " + SELECT + schemaname || '.' || relname as table_name, + n_tup_ins as inserts, + n_tup_upd as updates, + n_tup_del as deletes, + n_live_tup as live_rows + FROM pg_stat_user_tables + WHERE schemaname = 'public' + AND relname IN ('subdomain', 'ip_address', 'port', 'website', 'endpoint', 'directory') + ORDER BY n_tup_ins DESC; + " + + # 4. 锁等待 + echo -e "${GREEN}[4] 锁等待情况${NC}" + psql -d $DB_NAME -c " + SELECT + COUNT(*) as waiting_queries + FROM pg_stat_activity + WHERE wait_event_type = 'Lock' + AND datname = '$DB_NAME'; + " -t + + # 5. 数据库大小 + echo -e "${GREEN}[5] 数据库大小${NC}" + psql -d $DB_NAME -c " + SELECT + pg_size_pretty(pg_database_size('$DB_NAME')) as database_size; + " -t + + # 6. 缓存命中率 + echo -e "${GREEN}[6] 缓存命中率${NC}" + psql -d $DB_NAME -c " + SELECT + sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) * 100 as cache_hit_ratio + FROM pg_statio_user_tables; + " -t + + # 7. IO 统计 + echo -e "${GREEN}[7] 磁盘 IO 统计${NC}" + psql -d $DB_NAME -c " + SELECT + sum(heap_blks_read) as blocks_read, + sum(heap_blks_hit) as blocks_hit, + sum(idx_blks_read) as idx_blocks_read, + sum(idx_blks_hit) as idx_blocks_hit + FROM pg_statio_user_tables; + " -t + + # 8. 长时间运行的事务 + echo -e "${YELLOW}[8] 长时间运行事务 (>30秒)${NC}" + psql -d $DB_NAME -c " + SELECT + pid, + usename, + EXTRACT(EPOCH FROM (now() - xact_start))::int as transaction_duration_sec, + state, + LEFT(query, 50) as query_preview + FROM pg_stat_activity + WHERE datname = '$DB_NAME' + AND state != 'idle' + AND xact_start IS NOT NULL + AND EXTRACT(EPOCH FROM (now() - xact_start)) > 30 + ORDER BY xact_start; + " + + echo "" + echo -e "${BLUE}下次刷新: ${INTERVAL}秒后...${NC}" + sleep $INTERVAL +done diff --git a/backend/scripts/performance/pg_stats_after_test.sql b/backend/scripts/performance/pg_stats_after_test.sql new file mode 100644 index 00000000..1d959488 --- /dev/null +++ b/backend/scripts/performance/pg_stats_after_test.sql @@ -0,0 +1,114 @@ +-- 测试后记录对比数据 +-- 用法:psql -d xingrin -f pg_stats_after_test.sql > stats_after.txt + +\echo '========================================' +\echo ' PostgreSQL 测试后统计数据' +\echo '========================================' +\echo '' + +\echo '当前时间:' +SELECT now(); + +\echo '' +\echo '表记录数(测试后):' +SELECT + schemaname || '.' || relname as table_name, + n_live_tup as row_count, + n_dead_tup as dead_rows, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as total_size, + last_vacuum, + last_autovacuum, + last_analyze, + last_autoanalyze +FROM pg_stat_user_tables +WHERE schemaname = 'public' + AND relname IN ('subdomain', 'ip_address', 'port', 'website', 'endpoint', 'directory', 'scan') +ORDER BY n_live_tup DESC; + +\echo '' +\echo '表膨胀检查:' +SELECT + schemaname || '.' || relname as table_name, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as total_size, + n_live_tup as live_rows, + n_dead_tup as dead_rows, + CASE + WHEN n_live_tup = 0 THEN 0 + ELSE round((n_dead_tup::numeric / n_live_tup) * 100, 2) + END as dead_ratio_percent +FROM pg_stat_user_tables +WHERE schemaname = 'public' + AND relname IN ('subdomain', 'ip_address', 'port', 'website', 'endpoint', 'directory') +ORDER BY n_dead_tup DESC; + +\echo '' +\echo '索引使用情况:' +SELECT + schemaname || '.' || relname as table_name, + indexname, + idx_scan as index_scans, + idx_tup_read as rows_read, + idx_tup_fetch as rows_fetched, + pg_size_pretty(pg_relation_size(indexname::regclass)) as index_size +FROM pg_stat_user_indexes +WHERE schemaname = 'public' + AND relname IN ('subdomain', 'ip_address', 'port', 'website', 'endpoint', 'directory') +ORDER BY idx_scan DESC; + +\echo '' +\echo '表统计增量(本次测试产生):' +SELECT + schemaname || '.' || relname as table_name, + n_tup_ins as total_inserts, + n_tup_upd as total_updates, + n_tup_del as total_deletes, + n_tup_hot_upd as hot_updates +FROM pg_stat_user_tables +WHERE schemaname = 'public' + AND relname IN ('subdomain', 'ip_address', 'port', 'website', 'endpoint', 'directory') +ORDER BY n_tup_ins DESC; + +\echo '' +\echo '缓存命中率(测试后):' +SELECT + sum(heap_blks_hit) as heap_blocks_hit, + sum(heap_blks_read) as heap_blocks_read, + CASE + WHEN sum(heap_blks_hit) + sum(heap_blks_read) = 0 THEN 0 + ELSE round((sum(heap_blks_hit)::numeric / (sum(heap_blks_hit) + sum(heap_blks_read))) * 100, 2) + END as cache_hit_ratio_percent +FROM pg_statio_user_tables +WHERE schemaname = 'public'; + +\echo '' +\echo 'IO 统计:' +SELECT + sum(heap_blks_read) as heap_blocks_read_from_disk, + sum(heap_blks_hit) as heap_blocks_hit_in_cache, + sum(idx_blks_read) as index_blocks_read_from_disk, + sum(idx_blks_hit) as index_blocks_hit_in_cache +FROM pg_statio_user_tables +WHERE schemaname = 'public'; + +\echo '' +\echo '长时间运行查询:' +SELECT + pid, + usename, + application_name, + state, + EXTRACT(EPOCH FROM (now() - query_start))::int as duration_seconds, + query +FROM pg_stat_activity +WHERE datname = current_database() + AND state != 'idle' + AND query NOT LIKE '%pg_stat_activity%' +ORDER BY query_start; + +\echo '' +\echo '数据库总大小:' +SELECT + pg_size_pretty(pg_database_size(current_database())) as database_size; + +\echo '' +\echo '========================================' diff --git a/backend/scripts/performance/pg_stats_before_test.sql b/backend/scripts/performance/pg_stats_before_test.sql new file mode 100644 index 00000000..02d7c93f --- /dev/null +++ b/backend/scripts/performance/pg_stats_before_test.sql @@ -0,0 +1,79 @@ +-- 测试前记录基准数据 +-- 用法:psql -d xingrin -f pg_stats_before_test.sql > stats_before.txt + +\echo '========================================' +\echo ' PostgreSQL 测试前基准数据' +\echo '========================================' +\echo '' + +\echo '当前时间:' +SELECT now(); + +\echo '' +\echo '数据库连接配置:' +SHOW max_connections; +SHOW shared_buffers; +SHOW work_mem; +SHOW maintenance_work_mem; + +\echo '' +\echo '表记录数(测试前):' +SELECT + schemaname || '.' || relname as table_name, + n_live_tup as row_count, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as total_size +FROM pg_stat_user_tables +WHERE schemaname = 'public' + AND relname IN ('subdomain', 'ip_address', 'port', 'website', 'endpoint', 'directory', 'scan') +ORDER BY n_live_tup DESC; + +\echo '' +\echo '索引信息:' +SELECT + schemaname || '.' || tablename as table_name, + indexname, + pg_size_pretty(pg_relation_size(indexname::regclass)) as index_size +FROM pg_indexes +WHERE schemaname = 'public' + AND tablename IN ('subdomain', 'ip_address', 'port', 'website', 'endpoint', 'directory') +ORDER BY tablename, indexname; + +\echo '' +\echo '当前活跃连接数:' +SELECT + count(*) as total, + count(*) FILTER (WHERE state = 'active') as active, + count(*) FILTER (WHERE state = 'idle') as idle +FROM pg_stat_activity +WHERE datname = current_database(); + +\echo '' +\echo '表统计信息(累计):' +SELECT + schemaname || '.' || relname as table_name, + seq_scan as sequential_scans, + seq_tup_read as seq_rows_read, + idx_scan as index_scans, + idx_tup_fetch as idx_rows_fetched, + n_tup_ins as inserts, + n_tup_upd as updates, + n_tup_del as deletes +FROM pg_stat_user_tables +WHERE schemaname = 'public' + AND relname IN ('subdomain', 'ip_address', 'port', 'website', 'endpoint', 'directory') +ORDER BY relname; + +\echo '' +\echo '缓存命中率:' +SELECT + sum(heap_blks_hit) as heap_blocks_hit, + sum(heap_blks_read) as heap_blocks_read, + CASE + WHEN sum(heap_blks_hit) + sum(heap_blks_read) = 0 THEN 0 + ELSE round((sum(heap_blks_hit)::numeric / (sum(heap_blks_hit) + sum(heap_blks_read))) * 100, 2) + END as cache_hit_ratio_percent +FROM pg_statio_user_tables +WHERE schemaname = 'public'; + +\echo '' +\echo '========================================' diff --git a/backend/scripts/performance/start_performance_test.sh b/backend/scripts/performance/start_performance_test.sh new file mode 100755 index 00000000..05977dc2 --- /dev/null +++ b/backend/scripts/performance/start_performance_test.sh @@ -0,0 +1,425 @@ +#!/bin/bash + +# 性能测试快速启动脚本 +# 用法:./start_performance_test.sh + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" + +echo "========================================" +echo " PostgreSQL 性能测试工具" +echo "========================================" +echo "" + +# 加载 .env 文件中的数据库配置 +if [ -f "$PROJECT_DIR/.env" ]; then + echo "加载数据库配置..." + export PGHOST=$(grep "^DB_HOST=" "$PROJECT_DIR/.env" | cut -d '=' -f2) + export PGPORT=$(grep "^DB_PORT=" "$PROJECT_DIR/.env" | cut -d '=' -f2) + export PGUSER=$(grep "^DB_USER=" "$PROJECT_DIR/.env" | cut -d '=' -f2) + export PGPASSWORD=$(grep "^DB_PASSWORD=" "$PROJECT_DIR/.env" | cut -d '=' -f2) + export PGDATABASE=$(grep "^DB_NAME=" "$PROJECT_DIR/.env" | cut -d '=' -f2) + + echo " 主机: $PGHOST:$PGPORT" + echo " 用户: $PGUSER" + echo " 数据库: $PGDATABASE" +else + echo "[WARN] 未找到 .env 文件" +fi +echo "" + +# 检查 PostgreSQL 连接 +echo "检查数据库连接..." +cd "$PROJECT_DIR" +if ! source "$PROJECT_DIR/../.venv/bin/activate" && python -c " +import os +import django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() +from django.db import connection +try: + with connection.cursor() as cursor: + cursor.execute('SELECT 1') + print('✓ 数据库连接正常') +except Exception as e: + print(f'[ERROR] 数据库连接失败: {e}') + exit(1) +" > /dev/null 2>&1; then + echo "[ERROR] 无法连接到数据库 $PGDATABASE" + echo "" + echo "请检查:" + echo " 1. VPS 防火墙是否开放 5432 端口" + echo " 2. PostgreSQL 的 pg_hba.conf 是否允许远程连接" + echo " 3. .env 文件中的数据库配置是否正确" + echo " 4. Django 设置是否正确" + echo "" + echo "手动测试连接:" + echo " cd $PROJECT_DIR && source $PROJECT_DIR/../.venv/bin/activate && python manage.py dbshell" + exit 1 +fi +echo "✓ 数据库连接正常" +echo "" + +# 菜单选择 +echo "请选择操作:" +echo " 1) 测试批次大小(推荐先执行)" +echo " 2) 生成 1 万条测试数据" +echo " 3) 生成 10 万条测试数据" +echo " 4) 生成 100 万条测试数据" +echo " 5) 启动实时监控(独立运行)" +echo " 6) 查看测试前基准数据" +echo " 7) 查看测试后统计数据" +echo " 8) 完整测试流程(自动化)" +echo " 0) 退出" +echo "" +read -p "请输入选项 (0-8): " choice + +case $choice in + 1) + echo "" + echo "开始测试批次大小..." + cd "$PROJECT_DIR" + source "$PROJECT_DIR/../.venv/bin/activate" + + # 自动创建测试目标 + echo "创建测试目标..." + python -c " +import os +import django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() +from apps.targets.models import Target, Organization + +# 创建默认组织 +org, _ = Organization.objects.get_or_create( + name='测试组织', + defaults={'description': '性能测试专用组织'} +) + +# 创建测试目标 +for i in range(1, 4): + target_name = f'test{i}.com' + target, created = Target.objects.get_or_create( + name=target_name + ) + if created: + # 将目标添加到组织 + org.targets.add(target) + print(f'✓ 创建目标: {target_name}') + else: + print(f'✓ 目标已存在: {target_name}') +" + echo "" + + python manage.py generate_test_data --target test1.com --count 10000 --test-batch-sizes + ;; + 2) + echo "" + echo "使用默认批次大小: 5000" + echo "开始生成 1 万条数据..." + cd "$PROJECT_DIR" + source "$PROJECT_DIR/../.venv/bin/activate" + + # 自动创建测试目标 + echo "创建测试目标..." + python -c " +import os +import django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() +from apps.targets.models import Target, Organization + +# 创建默认组织 +org, _ = Organization.objects.get_or_create( + name='测试组织', + defaults={'description': '性能测试专用组织'} +) + +# 创建测试目标 +for i in range(1, 4): + target_name = f'test{i}.com' + target, created = Target.objects.get_or_create( + name=target_name + ) + if created: + # 将目标添加到组织 + org.targets.add(target) + print(f'✓ 创建目标: {target_name}') + else: + print(f'✓ 目标已存在: {target_name}') +" + echo "" + + python manage.py generate_test_data \ + --target test1.com \ + --count 10000 \ + --batch-size 5000 \ + --benchmark + ;; + 3) + echo "" + echo "使用默认批次大小: 5000" + echo "开始生成 10 万条数据..." + cd "$PROJECT_DIR" + source "$PROJECT_DIR/../.venv/bin/activate" + + # 自动创建测试目标 + echo "创建测试目标..." + python -c " +import os +import django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() +from apps.targets.models import Target, Organization + +# 创建默认组织 +org, _ = Organization.objects.get_or_create( + name='测试组织', + defaults={'description': '性能测试专用组织'} +) + +# 创建测试目标 +for i in range(1, 4): + target_name = f'test{i}.com' + target, created = Target.objects.get_or_create( + name=target_name + ) + if created: + # 将目标添加到组织 + org.targets.add(target) + print(f'✓ 创建目标: {target_name}') + else: + print(f'✓ 目标已存在: {target_name}') +" + echo "" + + python manage.py generate_test_data \ + --target test2.com \ + --count 100000 \ + --batch-size 5000 \ + --benchmark + ;; + 4) + echo "" + echo "使用默认批次大小: 5000" + echo "[WARN] 警告:这将生成 100 万条数据,可能需要 2-4 小时" + read -p "确认继续? (y/N): " confirm + if [[ $confirm == [yY] ]]; then + echo "" + echo "开始生成 100 万条数据..." + cd "$PROJECT_DIR" + source "$PROJECT_DIR/../.venv/bin/activate" + + # 自动创建测试目标 + echo "创建测试目标..." + python -c " +import os +import django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() +from apps.targets.models import Target, Organization + +# 创建默认组织 +org, _ = Organization.objects.get_or_create( + name='测试组织', + defaults={'description': '性能测试专用组织'} +) + +# 创建测试目标 +for i in range(1, 4): + target_name = f'test{i}.com' + target, created = Target.objects.get_or_create( + name=target_name + ) + if created: + # 将目标添加到组织 + org.targets.add(target) + print(f'✓ 创建目标: {target_name}') + else: + print(f'✓ 目标已存在: {target_name}') +" + echo "" + + python manage.py generate_test_data \ + --target test3.com \ + --count 1000000 \ + --batch-size 5000 \ + --benchmark + fi + ;; + 5) + echo "" + read -p "刷新间隔(秒,推荐 2-5): " interval + interval=${interval:-2} + echo "" + echo "启动 PostgreSQL 实时监控..." + echo "按 Ctrl+C 停止监控" + sleep 2 + "$SCRIPT_DIR/monitor_pg_performance.sh" xingrin "$interval" + ;; + 6) + echo "" + echo "记录测试前基准数据..." + mkdir -p "$PROJECT_DIR/logs" + psql -d "$PGDATABASE" -f "$SCRIPT_DIR/pg_stats_before_test.sql" > "$PROJECT_DIR/logs/stats_before.txt" + echo "✓ 已保存到: $PROJECT_DIR/logs/stats_before.txt" + echo "" + read -p "是否查看内容? (y/N): " view + if [[ $view == [yY] ]]; then + less "$PROJECT_DIR/logs/stats_before.txt" + fi + ;; + 7) + echo "" + echo "记录测试后统计数据..." + mkdir -p "$PROJECT_DIR/logs" + psql -d "$PGDATABASE" -f "$SCRIPT_DIR/pg_stats_after_test.sql" > "$PROJECT_DIR/logs/stats_after.txt" + echo "✓ 已保存到: $PROJECT_DIR/logs/stats_after.txt" + echo "" + read -p "是否对比测试前后? (y/N): " compare + if [[ $compare == [yY] ]] && [ -f "$PROJECT_DIR/logs/stats_before.txt" ]; then + echo "" + echo "差异对比:" + diff "$PROJECT_DIR/logs/stats_before.txt" "$PROJECT_DIR/logs/stats_after.txt" || true + fi + ;; + 8) + echo "" + echo "========================================" + echo " 完整自动化测试流程" + echo "========================================" + echo "" + echo "步骤 1:创建测试目标" + cd "$PROJECT_DIR" + source "$PROJECT_DIR/../.venv/bin/activate" + python -c " +import os +import django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() +from apps.targets.models import Target, Organization + +# 创建默认组织 +org, _ = Organization.objects.get_or_create( + name='测试组织', + defaults={'description': '性能测试专用组织'} +) + +# 创建测试目标 +for i in range(1, 4): + target_name = f'test{i}.com' + target, created = Target.objects.get_or_create( + name=target_name + ) + if created: + # 将目标添加到组织 + org.targets.add(target) + print(f'✓ 创建目标: {target_name}') + else: + print(f'✓ 目标已存在: {target_name}') +" + echo "✓ 完成" + echo "" + + echo "步骤 2:记录测试前基准数据" + mkdir -p "$PROJECT_DIR/logs" + psql -d "$PGDATABASE" -f "$SCRIPT_DIR/pg_stats_before_test.sql" > "$PROJECT_DIR/logs/stats_before.txt" 2>&1 + echo "✓ 完成" + echo "" + + echo "步骤 3:测试批次大小" + python manage.py generate_test_data --target test1.com --count 10000 --test-batch-sizes | tee "$PROJECT_DIR/logs/batch_size_test.txt" + echo "" + + # 自动提取最优批次大小,如果没有找到则使用默认值5000 + optimal_batch=$(grep "推荐批次大小:" "$PROJECT_DIR/logs/batch_size_test.txt" | awk '{print $2}' || echo "5000") + optimal_batch=${optimal_batch:-5000} + echo "✓ 自动选择最优批次: $optimal_batch" + echo "" + + echo "步骤 4:生成 10 万条数据" + python manage.py generate_test_data \ + --target test3.com \ + --count 100000 \ + --batch-size "$optimal_batch" \ + --benchmark | tee "$PROJECT_DIR/logs/test_results.txt" + echo "" + + echo "步骤 5:记录测试后统计" + psql -d "$PGDATABASE" -f "$SCRIPT_DIR/pg_stats_after_test.sql" > "$PROJECT_DIR/logs/stats_after.txt" 2>&1 + echo "✓ 完成" + echo "" + + # 生成性能报告 + echo "步骤 6:生成性能报告" + report_file="$PROJECT_DIR/logs/performance_report.txt" + + echo "========================================" > "$report_file" + echo " XingRin 性能测试报告" >> "$report_file" + echo "========================================" >> "$report_file" + echo "" >> "$report_file" + echo "测试时间: $(date '+%Y-%m-%d %H:%M:%S')" >> "$report_file" + echo "数据库: $PGHOST:$PGPORT/$PGDATABASE" >> "$report_file" + echo "最优批次: $optimal_batch" >> "$report_file" + echo "" >> "$report_file" + + # 提取批次测试结果 + echo "========================================" >> "$report_file" + echo " 批次大小性能对比" >> "$report_file" + echo "========================================" >> "$report_file" + grep -A 10 "批次大小对比结果" "$PROJECT_DIR/logs/batch_size_test.txt" | tail -n +4 >> "$report_file" + echo "" >> "$report_file" + + # 提取详细测试结果 + echo "========================================" >> "$report_file" + echo " 10万条数据生成性能" >> "$report_file" + echo "========================================" >> "$report_file" + grep "性能测试报告" -A 20 "$PROJECT_DIR/logs/test_results.txt" | tail -n +3 >> "$report_file" + echo "" >> "$report_file" + + # 提取总耗时 + total_time=$(grep "总耗时:" "$PROJECT_DIR/logs/test_results.txt" | head -1) + echo "========================================" >> "$report_file" + echo " 总体性能" >> "$report_file" + echo "========================================" >> "$report_file" + echo "$total_time" >> "$report_file" + echo "" >> "$report_file" + + echo "========================================" >> "$report_file" + echo " 测试文件" >> "$report_file" + echo "========================================" >> "$report_file" + echo "- 测试前基准: logs/stats_before.txt" >> "$report_file" + echo "- 批次测试: logs/batch_size_test.txt" >> "$report_file" + echo "- 性能结果: logs/test_results.txt" >> "$report_file" + echo "- 测试后统计: logs/stats_after.txt" >> "$report_file" + echo "- 性能报告: logs/performance_report.txt" >> "$report_file" + echo "" >> "$report_file" + + echo "✓ 性能报告已生成" + echo "" + + echo "========================================" + echo " ✓ 自动化测试完成!" + echo "========================================" + echo "" + + # 显示性能报告 + cat "$report_file" + echo "" + echo "详细报告已保存到: $report_file" + echo "" + ;; + 0) + echo "退出" + exit 0 + ;; + *) + echo "无效选项" + exit 1 + ;; +esac + +echo "" +echo "完成!" diff --git a/backend/scripts/worker-deploy/agent.sh b/backend/scripts/worker-deploy/agent.sh new file mode 100755 index 00000000..51c7a5fe --- /dev/null +++ b/backend/scripts/worker-deploy/agent.sh @@ -0,0 +1,182 @@ +#!/bin/bash +# ============================================ +# XingRin Agent +# 用途:心跳上报 + 负载监控 +# 适用:远程 VPS 或 Docker 容器内 +# ============================================ + +# 检查是否禁用 Agent +if [ "${AGENT_DISABLED:-false}" = "true" ]; then + echo "[AGENT] 已禁用,跳过启动" + exit 0 +fi + +# 配置 +MARKER_DIR="/opt/xingrin" +SRC_DIR="${MARKER_DIR}/src" +ENV_FILE="${SRC_DIR}/backend/.env" +INTERVAL=${AGENT_INTERVAL:-3} + +# 颜色定义 +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' + +log() { + echo -e "[$(date +'%Y-%m-%d %H:%M:%S')] [AGENT] $1" +} + +# 检测运行模式:容器内 or 远程 VPS +# 如果 /.dockerenv 存在,说明在容器内 +if [ -f "/.dockerenv" ]; then + RUN_MODE="container" + log "运行模式: Docker 容器内" +else + RUN_MODE="remote" + log "运行模式: 远程 VPS" + + # 远程模式:检测 Docker 命令 + if docker info >/dev/null 2>&1; then + DOCKER_CMD="docker" + else + DOCKER_CMD="sudo docker" + fi +fi + +# 加载环境变量(远程模式从文件,容器模式从环境变量) +if [ "$RUN_MODE" = "remote" ] && [ -f "$ENV_FILE" ]; then + set -a + source "$ENV_FILE" + set +a +fi + +# 获取配置 +# SERVER_URL: 后端 API 地址(容器内用 http://server:8888,远程用公网地址) +API_URL="${HEARTBEAT_API_URL:-${SERVER_URL:-}}" +WORKER_NAME="${WORKER_NAME:-}" +IS_LOCAL="${IS_LOCAL:-false}" + +# 容器模式默认标记为本地节点 +if [ "$RUN_MODE" = "container" ]; then + IS_LOCAL="true" +fi + +log "${GREEN}Agent 启动...${NC}" +log "心跳间隔: ${INTERVAL}s" + +if [ -z "$API_URL" ]; then + log "${RED}错误: 未配置 API 地址 (HEARTBEAT_API_URL 或 SERVER_URL)${NC}" + exit 1 +fi + +log "API 地址: ${API_URL}" + +# ============================================ +# 自注册功能(如果 WORKER_ID 未设置) +# ============================================ +register_worker() { + if [ -z "$WORKER_NAME" ]; then + WORKER_NAME="Worker-$(hostname)" + fi + + log "注册 Worker: ${WORKER_NAME}..." + + REGISTER_DATA=$(cat <<EOF +{ + "name": "$WORKER_NAME", + "is_local": $IS_LOCAL +} +EOF +) + + RESPONSE=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + -d "$REGISTER_DATA" \ + "${API_URL}/api/workers/register/" 2>/dev/null) + + if [ $? -eq 0 ]; then + # 解析返回的 workerId(API 使用 camelCase) + WORKER_ID=$(echo "$RESPONSE" | grep -oE '"workerId":\s*[0-9]+' | grep -oE '[0-9]+') + if [ -n "$WORKER_ID" ]; then + log "${GREEN}注册成功: ${WORKER_NAME} (ID: ${WORKER_ID})${NC}" + return 0 + fi + fi + + log "${RED}注册失败: ${RESPONSE}${NC}" + return 1 +} + +# 如果没有 WORKER_ID,执行自注册 +if [ -z "$WORKER_ID" ]; then + # 等待 Server 就绪 + log "等待 Server 就绪..." + for i in $(seq 1 30); do + if curl -s "${API_URL}/api/" > /dev/null 2>&1; then + log "${GREEN}Server 已就绪${NC}" + break + fi + log "Server 未就绪,等待... ($i/30)" + sleep 5 + done + + # 注册 + while ! register_worker; do + log "${YELLOW}注册失败,5 秒后重试...${NC}" + sleep 5 + done +fi + +log "Worker ID: ${WORKER_ID}" + +# ============================================ +# 心跳循环 +# Agent 独立运行,始终发送心跳 +# 主服务器根据心跳数据选择负载最低的节点分发任务 +# ============================================ +while true; do + # 收集系统负载(CPU + 内存) + # 容器内使用挂载的 /host/proc 获取宿主机数据 + if [ -d "/host/proc" ]; then + PROC_DIR="/host/proc" + else + PROC_DIR="/proc" + fi + + # CPU 使用率(百分比数值) + CPU_PERCENT=$(grep 'cpu ' ${PROC_DIR}/stat | awk '{usage=($2+$4)*100/($2+$4+$5)} END {printf "%.1f", usage}') + + # 内存使用率(百分比数值) + if [ -d "/host/proc" ]; then + # 从 /host/proc/meminfo 读取 + MEM_TOTAL=$(grep 'MemTotal' ${PROC_DIR}/meminfo | awk '{print $2}') + MEM_AVAILABLE=$(grep 'MemAvailable' ${PROC_DIR}/meminfo | awk '{print $2}') + MEM_PERCENT=$(awk "BEGIN {printf \"%.1f\", 100 - ($MEM_AVAILABLE / $MEM_TOTAL * 100)}") + else + # 使用 free 命令 + MEM_PERCENT=$(free | grep Mem | awk '{printf "%.1f", $3/$2 * 100}') + fi + + # 构建 JSON 数据(使用数值而非字符串,便于比较和排序) + JSON_DATA=$(cat <<EOF +{ + "cpu_percent": $CPU_PERCENT, + "memory_percent": $MEM_PERCENT +} +EOF +) + + # 发送心跳 + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + -d "$JSON_DATA" \ + "${API_URL}/api/workers/${WORKER_ID}/heartbeat/" 2>/dev/null || echo "000") + + if [ "$RESPONSE" != "200" ] && [ "$RESPONSE" != "201" ]; then + log "${YELLOW}心跳发送失败 (HTTP $RESPONSE)${NC}" + fi + + # 休眠 + sleep $INTERVAL +done diff --git a/backend/scripts/worker-deploy/bootstrap.sh b/backend/scripts/worker-deploy/bootstrap.sh new file mode 100755 index 00000000..1362302b --- /dev/null +++ b/backend/scripts/worker-deploy/bootstrap.sh @@ -0,0 +1,157 @@ +#!/bin/bash +# ============================================ +# XingRin 环境初始化脚本 (通用) +# 用途:安装基础依赖(git, tmux, curl 等) +# 支持:Ubuntu / Debian +# 适用:主机 & Worker VPS +# 特点:幂等执行,重复运行不会重复安装 +# ============================================ + +set -e + +# 版本标记(修改此版本号会触发重新安装) +BOOTSTRAP_VERSION="v1" +MARKER_DIR="/opt/xingrin" +MARKER_FILE="${MARKER_DIR}/.bootstrap_done_${BOOTSTRAP_VERSION}" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { + echo -e "${BLUE}[XingRin]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[XingRin]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[XingRin]${NC} $1" +} + +log_error() { + echo -e "${RED}[XingRin]${NC} $1" +} + +# 等待 apt 锁释放(最多等待 60 秒) +wait_for_apt_lock() { + local max_wait=60 + local waited=0 + while sudo fuser /var/lib/apt/lists/lock >/dev/null 2>&1 || \ + sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1 || \ + sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do + if [ $waited -eq 0 ]; then + log_info "等待 apt 锁释放..." + fi + sleep 2 + waited=$((waited + 2)) + if [ $waited -ge $max_wait ]; then + log_warn "等待 apt 锁超时,继续尝试..." + break + fi + done +} + +# 检查是否已完成初始化(返回 0 表示已完成,返回 1 表示需要初始化) +check_already_done() { + if [ -f "$MARKER_FILE" ]; then + log_success "环境已初始化 (${BOOTSTRAP_VERSION}),跳过" + return 0 # 不要 exit,让后续脚本继续执行 + fi + return 1 +} + +# 检查操作系统 +check_os() { + if ! command -v apt-get &> /dev/null; then + log_error "仅支持 Ubuntu/Debian 系统" + exit 1 + fi + log_info "检测到 Ubuntu/Debian 系统" +} + +# 安装基础依赖 +install_dependencies() { + log_info "安装基础依赖..." + + # 等待 apt 锁释放 + wait_for_apt_lock + + # 更新包索引 + sudo apt-get update -qq 2>/dev/null || true + + # 安装 git(必须) + if ! command -v git &> /dev/null; then + log_info " - 安装 git..." + sudo apt-get install -y -qq git >/dev/null 2>&1 + else + log_info " - git 已安装" + fi + + # 安装 tmux(会话持久化) + if ! command -v tmux &> /dev/null; then + log_info " - 安装 tmux..." + sudo apt-get install -y -qq tmux >/dev/null 2>&1 + else + log_info " - tmux 已安装" + fi + + # 安装 curl(网络请求) + if ! command -v curl &> /dev/null; then + log_info " - 安装 curl..." + sudo apt-get install -y -qq curl >/dev/null 2>&1 + else + log_info " - curl 已安装" + fi + + # 安装 jq(JSON 处理,可选) + if ! command -v jq &> /dev/null; then + log_info " - 安装 jq..." + sudo apt-get install -y -qq jq >/dev/null 2>&1 + else + log_info " - jq 已安装" + fi +} + +# 创建工作目录 +create_directories() { + log_info "创建工作目录..." + sudo mkdir -p "$MARKER_DIR" + sudo mkdir -p "${MARKER_DIR}/logs" + sudo mkdir -p "${MARKER_DIR}/data" + sudo chmod 755 "$MARKER_DIR" + sudo chown -R $USER:$USER "$MARKER_DIR" +} + +# 写入完成标记 +write_marker() { + echo "Bootstrap completed at $(date)" | sudo tee "$MARKER_FILE" > /dev/null + log_success "环境初始化完成" +} + +# 主流程 +main() { + log_info "==========================================" + log_info " XingRin 环境初始化" + log_info "==========================================" + + # 检查是否已初始化,如果已初始化则跳过初始化步骤(但不退出,让后续部署脚本继续执行) + if check_already_done; then + return 0 # 跳过初始化,继续执行后续脚本(Docker 部署、启动容器等) + fi + + check_os + create_directories + install_dependencies + write_marker + + log_info "==========================================" + log_success " ✓ 初始化完成" + log_info "==========================================" +} + +main "$@" diff --git a/backend/scripts/worker-deploy/install.sh b/backend/scripts/worker-deploy/install.sh new file mode 100644 index 00000000..88cc7ab3 --- /dev/null +++ b/backend/scripts/worker-deploy/install.sh @@ -0,0 +1,151 @@ +#!/bin/bash +# ============================================ +# XingRin 远程节点安装脚本 +# 用途:安装 Docker 环境 +# 支持:Ubuntu / Debian +# +# 新架构说明: +# - 只需安装 Docker +# - agent 通过 docker run 启动 +# - 扫描任务由主服务器 SSH docker run 执行 +# ============================================ + +set -e + +MARKER_DIR="/opt/xingrin" +DOCKER_MARKER="${MARKER_DIR}/.docker_installed" + +# 颜色定义 +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[XingRin]${NC} $1"; } +log_success() { echo -e "${GREEN}[XingRin]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[XingRin]${NC} $1"; } +log_error() { echo -e "${RED}[XingRin]${NC} $1"; } + +# 等待 apt 锁释放 +wait_for_apt_lock() { + local max_wait=60 + local waited=0 + while sudo fuser /var/lib/apt/lists/lock >/dev/null 2>&1 || \ + sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1 || \ + sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do + if [ $waited -eq 0 ]; then + log_info "等待 apt 锁释放..." + fi + sleep 2 + waited=$((waited + 2)) + if [ $waited -ge $max_wait ]; then + log_warn "等待 apt 锁超时,继续尝试..." + break + fi + done +} + +# 检测操作系统 +detect_os() { + if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$ID + else + log_error "无法检测操作系统" + exit 1 + fi + + if [[ "$OS" != "ubuntu" && "$OS" != "debian" ]]; then + log_error "仅支持 Ubuntu/Debian 系统" + exit 1 + fi +} + +# 安装 Docker +install_docker() { + if command -v docker &> /dev/null; then + log_info "Docker 已安装: $(docker --version)" + return 0 + fi + + log_info "安装 Docker..." + + wait_for_apt_lock + + # 安装依赖 + sudo apt-get update -qq + sudo apt-get install -y -qq ca-certificates curl gnupg lsb-release >/dev/null 2>&1 + + # 添加 Docker GPG key + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/${OS}/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>/dev/null + + # 添加 Docker 源 + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${OS} $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + + # 安装 Docker + sudo apt-get update -qq + sudo apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1 + + # 启动 Docker + sudo systemctl enable docker >/dev/null 2>&1 || true + sudo systemctl start docker >/dev/null 2>&1 || true + + # 添加当前用户到 docker 组 + sudo usermod -aG docker $USER 2>/dev/null || true + + log_success "Docker 安装完成" +} + +# 创建数据目录 +create_dirs() { + log_info "创建数据目录..." + sudo mkdir -p "${MARKER_DIR}/results" + sudo mkdir -p "${MARKER_DIR}/logs" + sudo chmod -R 755 "${MARKER_DIR}" + log_success "数据目录已创建" +} + +# 清理旧容器 +cleanup_old_containers() { + log_info "清理旧容器..." + + # 停止并删除旧的 agent 容器 + docker stop xingrin-agent 2>/dev/null || true + docker rm xingrin-agent 2>/dev/null || true + + # 兼容旧名称 + docker stop xingrin-watchdog 2>/dev/null || true + docker rm xingrin-watchdog 2>/dev/null || true + + log_success "旧容器已清理" +} + +# 拉取镜像 +pull_image() { + log_info "拉取 Worker 镜像..." + sudo docker pull yyhuni/xingrin-worker:latest + log_success "镜像拉取完成" +} + +# 主流程 +main() { + log_info "==========================================" + log_info " XingRin 节点安装" + log_info "==========================================" + + detect_os + install_docker + cleanup_old_containers + create_dirs + pull_image + + touch "$DOCKER_MARKER" + + log_success "==========================================" + log_success " ✓ 安装完成" + log_success "==========================================" +} + +main "$@" diff --git a/backend/scripts/worker-deploy/start-agent.sh b/backend/scripts/worker-deploy/start-agent.sh new file mode 100755 index 00000000..4e8f1531 --- /dev/null +++ b/backend/scripts/worker-deploy/start-agent.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# ============================================ +# XingRin Agent 启动脚本 +# 用途:启动 agent 容器(心跳上报) +# +# 新架构说明: +# - 使用 docker run 直接启动 +# - 不需要 docker-compose +# - 扫描任务由主服务器 SSH docker run 执行 +# ============================================ + +set -e + +MARKER_DIR="/opt/xingrin" +CONTAINER_NAME="xingrin-agent" +# 使用轻量 agent 镜像(~30MB),仅包含心跳上报功能 +IMAGE="yyhuni/xingrin-agent:latest" + +# 预设变量(远程部署时由 deploy_service.py 替换) +PRESET_SERVER_URL="{{HEARTBEAT_API_URL}}" +PRESET_WORKER_ID="{{WORKER_ID}}" + +# 颜色定义 +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[XingRin]${NC} $1"; } +log_success() { echo -e "${GREEN}[XingRin]${NC} $1"; } + +# 停止旧容器 +stop_old() { + if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + log_info "停止旧的 agent 容器..." + docker stop ${CONTAINER_NAME} 2>/dev/null || true + docker rm ${CONTAINER_NAME} 2>/dev/null || true + fi + # 兼容旧名称 + if docker ps -a --format '{{.Names}}' | grep -q "^xingrin-watchdog$"; then + docker stop xingrin-watchdog 2>/dev/null || true + docker rm xingrin-watchdog 2>/dev/null || true + fi +} + +# 启动 agent +start_agent() { + log_info "==========================================" + log_info " XingRin Agent 启动" + log_info "==========================================" + + log_info "启动 agent 容器..." + # --pull=always 确保使用最新镜像,已是最新则跳过下载 + docker run -d --pull=always \ + --name ${CONTAINER_NAME} \ + --restart always \ + -e SERVER_URL="${PRESET_SERVER_URL}" \ + -e WORKER_ID="${PRESET_WORKER_ID}" \ + -v /proc:/host/proc:ro \ + ${IMAGE} + + log_success "Agent 已启动" +} + +# 显示完成信息 +show_completion() { + echo "" + log_success "==========================================" + log_success " ✓ Agent 已启动" + log_success "==========================================" + echo "" + log_info "管理命令:" + echo " - 查看日志: docker logs -f ${CONTAINER_NAME}" + echo " - 重启: docker restart ${CONTAINER_NAME}" + echo " - 停止: docker stop ${CONTAINER_NAME}" + echo "" +} + +# 主流程 +main() { + stop_old + start_agent + show_completion +} + +main "$@" diff --git a/backend/scripts/worker-deploy/uninstall.sh b/backend/scripts/worker-deploy/uninstall.sh new file mode 100644 index 00000000..8dcdbae6 --- /dev/null +++ b/backend/scripts/worker-deploy/uninstall.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# ============================================ +# XingRin 远程节点卸载脚本 +# 用途:停止 agent 容器并清理环境 +# 支持:Ubuntu / Debian +# ============================================ + +set -e + +MARKER_DIR="/opt/xingrin" + +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[XingRin]${NC} $1"; } +log_success() { echo -e "${GREEN}[XingRin]${NC} $1"; } + +# 停止 agent 容器 +stop_agent() { + log_info "停止 agent 容器..." + + # 停止新名称容器 + docker stop xingrin-agent 2>/dev/null || true + docker rm xingrin-agent 2>/dev/null || true + + # 兼容旧名称 + docker stop xingrin-watchdog 2>/dev/null || true + docker rm xingrin-watchdog 2>/dev/null || true + + log_success "Agent 已停止" +} + +# 清理数据目录 +cleanup_data() { + log_info "清理数据目录..." + + if [ -d "${MARKER_DIR}" ]; then + sudo rm -rf "${MARKER_DIR}" + log_success "数据目录已清理" + fi +} + +# 显示完成信息 +show_completion() { + echo "" + log_success "==========================================" + log_success " ✓ 卸载完成" + log_success "==========================================" + echo "" + log_info "注意:Docker 未卸载,如需卸载请手动执行" +} + +# 主流程 +main() { + log_info "==========================================" + log_info " XingRin 节点卸载" + log_info "==========================================" + + stop_agent + cleanup_data + show_completion +} + +main "$@" diff --git a/backend/wordlist/dir_default.txt b/backend/wordlist/dir_default.txt new file mode 100644 index 00000000..3e717f65 --- /dev/null +++ b/backend/wordlist/dir_default.txt @@ -0,0 +1,158 @@ +/api/admin/v1/users/all +/registerSuccess.do +/getALLUsers +/swagger/static/index.html +/swagger-ui/index.html +/dubbo-provider/distv2/index.html +/user/swagger-ui.html +/swagger-dubbo/api-docs +/distv2/index.html +/static/swagger.json +/api/index.html +/v2/swagger.json +/actuator/hystrix.stream +/intergrationgraph +/druid +/swagger-ui/html +/api/v2/api-docs +/swagger/codes +/template/swagger-ui.html +/spring-security-rest/api/swagger-ui.html +/spring-security-oauth-resource/swagger-ui.html +/v2/api-docs +/api.html +/sw/swagger-ui.html +/~www +/~xfs +/~uucp +/~user5 +/~web +/~user4 +/~user3 +/~user +/~user1 +/~toor +/~user2 +/~sync +/~system +/~testuser +/~test +/~shutdown +/~sql +/~root +/~staff +/~rpc +/~reception +/~rpcuser +/~operator +/~office +/~nscd +/~postmaster +/~pop +/~nobody +/~news +/~mailnull +/~mail +/~lp +/~ident +/~http +/~helpdesk +/~help +/~halt +/~gdm +/~gopher +/~guest +/~games +/~fwuser +/~fwadmin +/~firewall +/~fw +/~db +/~database +/~backup +/~ftp +/~data +/~daemon +/~bin +/~apache +/~admin/ +/~/ +/~anonymous +/~administrator +/zipkin/ +/~admin +/~adm +/zimbra +/zf_backend.php +/zone-h.php +/zimbra/ +/yonetim.php +/zeroclipboard.swf +/zebra.conf +/zehir.php +/zabbix/ +/yum.log +/yonetim.html +/yonetici +/ylwrap +/yonetici.html +/yonetici.php +/yonetim +/yarn.lock +/yarn-error.log +/yarn-debug.log +/yaml_cron.log +/xw.php +/xx.php +/yaml.log +/xw1.php +/xsql/lib/XSQLConfig.xml +/xslt/ +/xsl/common.xsl +/xsl/ +/xsl/_common.xsl +/xsql/ +/xshell.php +/xmlrpc.php +/xphpMyAdmin/ +/xprober.php +/xmlrpc_server.php +/xphperrors.log +/xmlrpc +/xml/_common.xml +/xml/common.xml +/xml/ +/xls/ +/xml +/xcuserdata/ +/xlogin/ +/xiaoma.php +/xferlog +/xd.php +/xampp/phpmyadmin/scripts/setup.php +/xampp/phpmyadmin/index.php +/xampp/phpmyadmin/ +/xampp/ +/x.php +/wwwstats.htm +/wwwstat +/wwwroot.zip +/wwwroot.tgz +/wwwroot.tar.bz2 +/wwwroot.tar.gz +/wwwlog +/wwwroot.tar +/wwwroot.7z +/wwwroot.sql +/wwwroot.rar +/wwwboard/passwd.txt +/wwwboard/ +/www.zip +/www/phpMyAdmin/index.php +/www-error.log +/www.tgz +/www.tar.bz2 +/www-test/ +/www.tar +/www.rar +/www.tar.gz \ No newline at end of file diff --git a/backend/wordlist/subdomains-top1million-110000.txt b/backend/wordlist/subdomains-top1million-110000.txt new file mode 100644 index 00000000..d96a9f07 --- /dev/null +++ b/backend/wordlist/subdomains-top1million-110000.txt @@ -0,0 +1,114442 @@ +www +mail +ftp +localhost +webmail +smtp +webdisk +pop +cpanel +whm +ns1 +ns2 +autodiscover +autoconfig +ns +test +m +blog +dev +www2 +ns3 +pop3 +forum +admin +mail2 +vpn +mx +imap +old +new +mobile +mysql +beta +support +cp +secure +shop +demo +dns2 +ns4 +dns1 +static +lists +web +www1 +img +news +portal +server +wiki +api +media +images +www.blog +backup +dns +sql +intranet +www.forum +www.test +stats +host +video +mail1 +mx1 +www3 +staging +www.m +sip +chat +search +crm +mx2 +ads +ipv4 +remote +email +my +wap +svn +store +cms +download +proxy +www.dev +mssql +apps +dns3 +exchange +mail3 +forums +ns5 +db +office +live +files +info +owa +monitor +helpdesk +panel +sms +newsletter +ftp2 +web1 +web2 +upload +home +bbs +login +app +en +blogs +it +cdn +stage +gw +dns4 +www.demo +ssl +cn +smtp2 +vps +ns6 +relay +online +service +test2 +radio +ntp +library +help +www4 +members +tv +www.shop +extranet +hosting +ldap +services +webdisk.blog +s1 +i +survey +s +www.mail +www.new +c-n7k-v03-01.rz +data +docs +c-n7k-n04-01.rz +ad +legacy +router +de +meet +cs +av +sftp +server1 +stat +moodle +facebook +test1 +photo +partner +nagios +mrtg +s2 +mailadmin +dev2 +ts +autoconfig.blog +autodiscover.blog +games +jobs +image +host2 +gateway +preview +www.support +im +ssh +correo +control +ns0 +vpn2 +cloud +archive +citrix +webdisk.m +voip +connect +game +smtp1 +access +lib +www5 +gallery +redmine +es +irc +stream +qa +dl +billing +construtor +lyncdiscover +painel +fr +projects +a +pgsql +mail4 +tools +iphone +server2 +dbadmin +manage +jabber +music +webmail2 +www.beta +mailer +phpmyadmin +t +reports +rss +pgadmin +images2 +mx3 +www.webmail +ws +content +sv +web3 +community +poczta +www.mobile +ftp1 +dialin +us +sp +panelstats +vip +cacti +s3 +alpha +videos +ns7 +promo +testing +sharepoint +marketing +sitedefender +member +webdisk.dev +emkt +training +edu +autoconfig.m +git +autodiscover.m +catalog +webdisk.test +job +ww2 +www.news +sandbox +elearning +fb +webmail.cp +downloads +speedtest +design +staff +master +panelstatsmail +v2 +db1 +mailserver +builder.cp +travel +mirror +ca +sso +tickets +alumni +sitebuilder +www.admin +auth +jira +ns8 +partners +ml +list +images1 +club +business +update +fw +devel +local +wp +streaming +zeus +images3 +adm +img2 +gate +pay +file +seo +status +share +maps +zimbra +webdisk.forum +trac +oa +sales +post +events +project +xml +wordpress +images4 +main +english +e +img1 +db2 +time +redirect +go +bugs +direct +www6 +social +www.old +development +calendar +www.forums +ru +www.wiki +monitoring +hermes +photos +bb +mx01 +mail5 +temp +map +ns10 +tracker +sport +uk +hr +autodiscover.test +conference +free +autoconfig.test +client +vpn1 +autodiscover.dev +b2b +autoconfig.dev +noc +webconf +ww +payment +firewall +intra +rt +v +clients +www.store +gis +m2 +event +origin +site +domain +barracuda +link +ns11 +internal +dc +smtp3 +zabbix +mdm +assets +images6 +www.ads +mars +mail01 +pda +images5 +c +ns01 +tech +ms +images7 +autoconfig.forum +public +css +autodiscover.forum +webservices +www.video +web4 +orion +pm +fs +w3 +student +www.chat +domains +book +lab +o1.email +server3 +img3 +kb +faq +health +in +board +vod +www.my +cache +atlas +php +images8 +wwww +voip750101.pg6.sip +cas +origin-www +cisco +banner +mercury +w +directory +mailhost +test3 +shopping +webdisk.demo +ip +market +pbx +careers +auto +idp +ticket +js +ns9 +outlook +foto +www.en +pro +mantis +spam +movie +s4 +lync +jupiter +dev1 +erp +register +adv +b +corp +sc +ns12 +images0 +enet1 +mobil +lms +net +storage +ss +ns02 +work +webcam +www7 +report +admin2 +p +nl +love +pt +manager +d +cc +android +linux +reseller +agent +web01 +sslvpn +n +thumbs +links +mailing +hotel +pma +press +venus +finance +uesgh2x +nms +ds +joomla +doc +flash +research +dashboard +track +www.img +x +rs +edge +deliver +sync +oldmail +da +order +eng +testbrvps +user +radius +star +labs +top +srv1 +mailers +mail6 +pub +host3 +reg +lb +log +books +phoenix +drupal +affiliate +www.wap +webdisk.support +www.secure +cvs +st +wksta1 +saturn +logos +preprod +m1 +backup2 +opac +core +vc +mailgw +pluto +ar +software +jp +srv +newsite +www.members +openx +otrs +titan +soft +analytics +code +mp3 +sports +stg +whois +apollo +web5 +ftp3 +www.download +mm +art +host1 +www8 +www.radio +demo2 +click +smail +w2 +feeds +g +education +affiliates +kvm +sites +mx4 +autoconfig.demo +controlpanel +autodiscover.demo +tr +ebook +www.crm +hn +black +mcp +adserver +www.staging +static1 +webservice +f +develop +sa +katalog +as +smart +pr +account +mon +munin +www.games +www.media +cam +school +r +mc +id +network +www.live +forms +math +mb +maintenance +pic +agk +phone +bt +sm +demo1 +ns13 +tw +ps +dev3 +tracking +green +users +int +athena +www.static +www.info +security +mx02 +prod +1 +team +transfer +www.facebook +www10 +v1 +google +proxy2 +feedback +vpgk +auction +view +biz +vpproxy +secure2 +www.it +newmail +sh +mobi +wm +mailgate +dms +11192521404255 +autoconfig.support +play +11192521403954 +start +life +autodiscover.support +antispam +cm +booking +iris +www.portal +hq +gc._msdcs +neptune +terminal +vm +pool +gold +gaia +internet +sklep +ares +poseidon +relay2 +up +resources +is +mall +traffic +webdisk.mail +www.api +join +smtp4 +www9 +w1 +upl +ci +gw2 +open +audio +fax +alfa +www.images +alex +spb +xxx +ac +edm +mailout +webtest +nfs01.jc +me +sun +virtual +spokes +ns14 +webserver +mysql2 +tour +igk +wifi +pre +abc +corporate +adfs +srv2 +delta +loopback +magento +br +campus +law +global +s5 +web6 +orange +awstats +static2 +learning +www.seo +china +gs +www.gallery +tmp +ezproxy +darwin +bi +best +mail02 +studio +sd +signup +dir +server4 +archives +golf +omega +vps2 +sg +ns15 +win +real +www.stats +c1 +eshop +piwik +geo +mis +proxy1 +web02 +pascal +lb1 +app1 +mms +apple +confluence +sns +learn +classifieds +pics +gw1 +www.cdn +rp +matrix +repository +updates +se +developer +meeting +twitter +artemis +au +cat +system +ce +ecommerce +sys +ra +orders +sugar +ir +wwwtest +bugzilla +listserv +www.tv +vote +webmaster +webdev +sam +www.de +vps1 +contact +galleries +history +journal +hotels +www.newsletter +podcast +dating +sub +www.jobs +www.intranet +www.email +mt +science +counter +dns5 +2 +people +ww3 +www.es +ntp1 +vcenter +test5 +radius1 +ocs +power +pg +pl +magazine +sts +fms +customer +wsus +bill +www.hosting +vega +nat +sirius +lg +11285521401250 +sb +hades +students +uat +conf +ap +uxr4 +eu +moon +www.search +checksrv +hydra +usa +digital +wireless +banners +md +mysite +webmail1 +windows +traveler +www.poczta +hrm +database +mysql1 +inside +debian +pc +ask +backend +cz +mx0 +mini +autodiscover.mail +rb +webdisk.shop +mba +www.help +www.sms +test4 +dm +subscribe +sf +passport +red +video2 +ag +autoconfig.mail +all.edge +registration +ns16 +camera +myadmin +ns20 +uxr3 +mta +beauty +fw1 +epaper +central +cert +backoffice +biblioteca +mob +about +space +movies +u +ms1 +ec +forum2 +server5 +money +radius2 +print +ns18 +thunder +nas +ww1 +webdisk.webmail +edit +www.music +planet +m3 +vstagingnew +app2 +repo +prueba +house +ntp2 +dragon +pandora +stock +form +pp +www.sport +physics +food +groups +antivirus +profile +www.online +stream2 +hp +d1 +nhko1111 +logs +eagle +v3 +mail7 +gamma +career +vpn3 +ipad +dom +webdisk.store +iptv +www.promo +hd +mag +box +talk +hera +f1 +www.katalog +syslog +fashion +t1 +2012 +soporte +teste +scripts +welcome +hk +paris +www.game +multimedia +neo +beta2 +msg +io +portal2 +sky +webdisk.beta +web7 +exam +cluster +webdisk.new +img4 +surveys +webmail.controlpanel +error +private +bo +kids +card +vmail +switch +messenger +cal +plus +cars +management +feed +xmpp +ns51 +premium +www.apps +backup1 +asp +ns52 +website +pos +lb2 +www.foto +ws1 +domino +mailman +asterisk +weather +max +ma +node1 +webapps +white +ns17 +cdn2 +dealer +pms +tg +gps +www.travel +listas +chelyabinsk-rnoc-rr02.backbone +hub +demo3 +minecraft +ns22 +hw70f395eb456e +dns01 +wpad +nm +ch +www.catalog +ns21 +web03 +www.videos +rc +www.web +gemini +bm +lp +pdf +webapp +noticias +myaccount +sql1 +hercules +ct +fc +mail11 +pptp +contest +www.us +msk +widget +study +11290521402560 +posta +ee +realestate +out +galaxy +kms +thor +world +webdisk.mobile +www.test2 +base +cd +relay1 +taurus +cgi +www0 +res +d2 +intern +c2 +webdav +mail10 +robot +vcs +am +dns02 +group +silver +www.dl +adsl +ids +ex +ariel +i2 +trade +ims +king +www.fr +sistemas +ecard +themes +builder.controlpanel +blue +z +securemail +www-test +wmail +123 +sonic +netflow +enterprise +extra +webdesign +reporting +libguides +oldsite +autodiscover.secure +check +webdisk.secure +luna +www11 +down +odin +ent +web10 +international +fw2 +leo +pegasus +mailbox +aaa +com +acs +vdi +inventory +simple +e-learning +fire +cb +edi +rsc +yellow +www.sklep +www.social +webmail.cpanel +act +bc +portfolio +hb +smtp01 +cafe +nexus +www.edu +ping +movil +as2 +builder.control +autoconfig.secure +payments +cdn1 +srv3 +openvpn +tm +cisco-capwap-controller +dolphin +webmail3 +minerva +co +wwwold +hotspot +super +products +nova +r1 +blackberry +mike +pe +acc +lion +tp +tiger +stream1 +www12 +admin1 +mx5 +server01 +webdisk.forums +notes +suporte +focus +km +speed +rd +lyncweb +builder.cpanel +pa +mx10 +www.files +fi +konkurs +broadcast +a1 +build +earth +webhost +www.blogs +aurora +review +mg +license +homer +servicedesk +webcon +db01 +dns6 +cfd297 +spider +expo +newsletters +h +ems +city +lotus +fun +autoconfig.webmail +statistics +ams +all.videocdn +autodiscover.shop +autoconfig.shop +tfs +www.billing +happy +cl +sigma +jwc +dream +sv2 +wms +one +ls +europa +ldap2 +a4 +merlin +buy +web11 +dk +autodiscover.webmail +ro +widgets +sql2 +mysql3 +gmail +selfservice +sdc +tt +mailrelay +a.ns +ns19 +webstats +plesk +nsk +test6 +class +agenda +adam +german +www.v2 +renew +car +correio +bk +db3 +voice +sentry +alt +demeter +www.projects +mail8 +bounce +tc +oldwww +www.directory +uploads +carbon +all +mark +bbb +eco +3g +testmail +ms2 +node2 +template +andromeda +www.photo +media2 +articles +yoda +sec +active +nemesis +autoconfig.new +autodiscover.new +push +enews +advertising +mail9 +api2 +david +source +kino +prime +o +vb +testsite +fm +c4anvn3 +samara +reklama +made.by +sis +q +mp +newton +elearn +autodiscover.beta +cursos +filter +autoconfig.beta +news2 +mf +ubuntu +ed +zs +a.mx +center +www.sandbox +img5 +translate +webmail.control +mail0 +smtp02 +s6 +dallas +bob +autoconfig.store +stu +recruit +mailtest +reviews +autodiscover.store +2011 +www.iphone +fp +d3 +rdp +www.design +test7 +bg +console +outbound +jpkc +ext +invest +web8 +testvb +vm1 +family +insurance +atlanta +aqua +film +dp +ws2 +webdisk.cdn +www.wordpress +webdisk.news +at +ocean +dr +yahoo +s8 +host2123 +libra +rose +cloud1 +album +3 +antares +www.a +ipv6 +bridge +demos +cabinet +crl +old2 +angel +cis +www.panel +isis +s7 +guide +webinar +pop2 +cdn101 +company +express +special +loki +accounts +video1 +expert +clientes +p1 +loja +blog2 +img6 +l +mail12 +style +hcm +s11 +mobile2 +triton +s12 +kr +www.links +s13 +friends +www.office +shadow +mymail +autoconfig.forums +ns03 +neu +autodiscover.forums +www.home +root +upgrade +puppet +storm +www.service +isp +get +foro +mytest +test10 +desktop +po +mac +www.member +ph +blackboard +dspace +dev01 +ftp4 +testwww +presse +ldap1 +rock +wow +sw +msn +mas +scm +its +vision +tms +www.wp +hyperion +nic +html +sale +isp-caledon.cit +www.go +do +media1 +web9 +ua +energy +helios +chicago +webftp +i1 +commerce +www.ru +union +netmon +audit +vm2 +mailx +web12 +painelstats +sol +z-hn.nhac +kvm2 +chris +www.board +apache +tube +marvin +bug +external +pki +viper +webadmin +production +r2 +win2 +vpstun +mx03 +ios +www.uk +smile +www.fb +aa +www13 +trinity +www.upload +www.testing +amazon +hosting2 +bip +mw +www.health +india +web04 +rainbow +cisco-lwapp-controller +uranus +qr +domaindnszones +editor +www.stage +manual +nice +robin +gandalf +j +buzz +password +autoconfig.mobile +gb +idea +eva +www.i +server6 +www.job +results +www.test1 +maya +pix +www.cn +gz +th +www.lib +autodiscover.mobile +b1 +horus +zero +sv1 +wptest +cart +brain +mbox +bd +tester +fotos +ess +ns31 +blogx.dev +ceres +gatekeeper +csr +www.cs +sakura +chef +parking +idc +desarrollo +mirrors +sunny +kvm1 +prtg +mo +dns0 +chaos +avatar +alice +task +www.app +dev4 +sl +sugarcrm +youtube +ic-vss6509-gw +simon +m4 +dexter +crystal +terra +fa +server7 +journals +iron +uc +pruebas +magic +ead +www.helpdesk +4 +server10 +computer +galileo +delivery +aff +aries +www.development +el +livechat +host4 +static3 +www.free +sk +puma +coffee +gh +java +fish +templates +tarbaby +mtest +light +www.link +sas +poll +director +destiny +aquarius +vps3 +bravo +freedom +boutique +lite +ns25 +shop2 +ic +foundation +cw +ras +park +next +diana +secure1 +k +euro +managedomain +castor +www-old +charon +nas1 +la +jw +s10 +web13 +mxbackup2 +europe +oasis +donate +s9 +ftps +falcon +depot +genesis +mysql4 +rms +ns30 +www.drupal +wholesale +forestdnszones +www.alumni +marketplace +tesla +statistik +country +imap4 +brand +gift +shell +www.dev2 +apply +nc +kronos +epsilon +testserver +smtp-out +pictures +autos +org +mysql5 +france +shared +cf +sos +stun +channel +2013 +moto +pw +oc.pool +eu.pool +na.pool +cams +www.auto +pi +image2 +test8 +hi +casino +magazin +wwwhost-roe001 +z-hcm.nhac +trial +cam1 +victor +sig +ctrl +wwwhost-ox001 +weblog +rds +first +farm +whatsup +panda +dummy +stream.origin +canada +wc +flv +www.top +emerald +sim +ace +sap +ga +bank +et +soap +guest +mdev +www.client +www.partner +easy +st1 +webvpn +baby +s14 +delivery.a +wwwhost-port001 +hideip +graphics +webshop +catalogue +tom +rm +perm +www.ad +ad1 +mail03 +www.sports +water +intranet2 +autodiscover.news +bj +nsb +charge +export +testweb +sample +quit +proxy3 +email2 +b2 +servicios +novo +new2 +meta +secure3 +ajax +autoconfig.news +ghost +www.cp +good +bookstore +kiwi +ft +demo4 +www.archive +squid +publish +west +football +printer +cv +ny +boss +smtp5 +rsync +sip2 +ks +leon +a3 +mta1 +epay +tst +mgmt +deals +dropbox +www.books +2010 +torrent +webdisk.ads +mx6 +www.art +chem +iproxy +www.pay +anime +ccc +anna +ns23 +hs +cg +acm +pollux +lt +meteo +owncloud +andrew +v4 +www-dev +oxygen +jaguar +panther +personal +ab +dcp +med +www.joomla +john +watson +motor +mails +kiev +asia +campaign +win1 +cards +fantasy +tj +martin +helium +nfs +ads2 +script +anubis +imail +cp2 +mk +bw +em +creative +www.elearning +ad2 +stars +discovery +friend +reservations +buffalo +cdp +uxs2r +atom +cosmos +www.business +a2 +xcb +allegro +om +ufa +dw +cool +files2 +webdisk.chat +ford +oma +zzb +staging2 +texas +ib +cwc +aphrodite +re +spark +www.ftp +oscar +atlantis +osiris +os +m5 +dl1 +www.shopping +ice +beta1 +mcu +inter +interface +gm +kiosk +so +dss +www.survey +customers +fx +nsa +csg +mi +url +dl2 +show +www.classifieds +mexico +knowledge +frank +tests +accounting +krasnodar +um +hc +www.nl +echo +property +gms +london +www.clients +academy +cyber +www.english +museum +poker +www.downloads +gp +cr +arch +gd +virgo +si +smtp-relay +ipc +gay +gg +oracle +ruby +grid +web05 +i3 +tool +bulk +jazz +price +pan +webdisk.admin +agora +w4 +mv +www.moodle +phantom +web14 +radius.auth +voyager +mint +einstein +wedding +sqladmin +cam2 +autodiscover.chat +trans +che +bp +dsl +kazan +autoconfig.chat +al +pearl +transport +lm +h1 +condor +homes +air +stargate +ai +www.www2 +hot +paul +np +kp +engine +ts3 +nano +testtest +sss +james +gk +ep +ox +tomcat +ns32 +sametime +tornado +e1 +s16 +quantum +slave +shark +autoconfig.cdn +www.love +backup3 +webdisk.wiki +altair +youth +keys +site2 +server11 +phobos +common +autodiscover.cdn +key +test9 +core2 +snoopy +lisa +soccer +tld +biblio +sex +fast +train +www.software +credit +p2 +cbf1 +ns24 +mailin +dj +www.community +www-a +www-b +smtps +victoria +www.docs +cherry +cisl-murcia.cit +border +test11 +nemo +pass +mta2 +911 +xen +hg +be +wa +web16 +biologie +bes +fred +turbo +biology +indigo +plan +www.stat +hosting1 +pilot +www.club +diamond +www.vip +cp1 +ics +www.library +autoconfig.admin +japan +autodiscover.admin +quiz +laptop +todo +cdc +mkt +mu +dhcp.pilsnet +dot +xenon +csr21.net +horizon +vp +centos +inf +wolf +mr +fusion +retail +logo +line +11 +sr +shorturl +speedy +webct +omsk +dns7 +ebooks +apc +rus +landing +pluton +www.pda +w5 +san +course +aws +uxs1r +spirit +ts2 +srv4 +classic +webdisk.staging +g1 +ops +comm +bs +sage +innovation +dynamic +www.www +resellers +resource +colo +test01 +swift +bms +metro +s15 +vn +callcenter +www.in +scc +jerry +site1 +profiles +penguin +sps +mail13 +portail +faculty +eis +rr +mh +count +psi +florida +mango +maple +ssltest +cloud2 +general +www.tickets +maxwell +web15 +familiar +arc +axis +ng +admissions +dedicated +cash +nsc +www.qa +tea +tpmsqr01 +rnd +jocuri +office2 +mario +xen2 +mradm.letter +cwa +ninja +amur +core1 +miami +www.sales +cerberus +ixhash +ie +action +daisy +spf +p3 +junior +oss +pw.openvpn +alt-host +fromwl +nobl +isphosts +ns26 +helomatch +test123 +tftp +webaccess +tienda +hostkarma +lv +freemaildomains +sbc +testbed +bart +ironport +server8 +dh +crm2 +watch +skynet +miss +dante +www.affiliates +legal +www.ip +telecom +dt +blog1 +webdisk.email +ip-us +pixel +www.t +dnswl +korea +insight +dd +www.rss +testbl +www01 +auth-hack +www.cms +abuse-report +pb +casa +eval +bio +app3 +cobra +www.ar +solo +wall +oc +dc1 +beast +george +eureka +sit +demo5 +holiday +webhosting +srv01 +router2 +ssp +server9 +quotes +eclipse +entertainment +kc +m0 +af +cpa +pc.jura-gw1 +fox +deal +dav +www.training +webdisk.old +host5 +mix +vendor +uni +mypage +spa +soa +aura +ref +arm +dam +config +austin +aproxy +developers +cms2 +www15 +women +wwwcache +abs +testportal +inet +gt +testshop +g2 +www.ca +pinnacle +support2 +sunrise +snake +www-new +patch +lk +sv3 +b.ns +python +starwars +cube +sj +s0 +gc +stud +micro +webstore +coupon +perseus +maestro +router1 +hawk +pf +h2 +www.soft +dns8 +fly +unicorn +sat +na +xyz +df +lynx +activate +sitemap +t2 +cats +mmm +volgograd +test12 +sendmail +hardware +ara +import +ces +cinema +arena +text +a5 +astro +doctor +casper +smc +voronezh +eric +agency +wf +avia +platinum +butler +yjs +hospital +nursing +admin3 +pd +safety +teszt +tk +s20 +moscow +karen +cse +messages +www.adserver +asa +eros +www.server +player +raptor +documents +srv5 +www.photos +xb +example +culture +demo6 +dev5 +jc +ict +back +p2p +stuff +wb +ccs +su +webinars +kt +hope +http +try +tel +m9 +newyork +gov +www.marketing +relax +setup +fileserver +moodle2 +courses +annuaire +fresh +www.status +rpc +zeta +ibank +helm +autodiscover.ads +mailgateway +integration +viking +metrics +c.ns.e +webdisk.video +www.host +tasks +monster +firefly +icq +saratov +www.book +smtp-out-01 +tourism +dz +zt +daniel +roundcube +paper +24 +sus +splash +zzz +10 +chat2 +autoconfig.ads +mailhub +neon +message +seattle +ftp5 +port +solutions +offers +seth +server02 +peter +ns29 +maillist +www.konkurs +d.ns.e +toto +guides +ae +healthcare +ssc +mproxy +metis +estore +mailsrv +singapore +hm +medusa +bl +bz +i5 +dan +thomas +exchbhlan5 +alert +www.spb +st2 +www.tools +rigel +e.ns.e +kvm3 +astun +trk +www.law +qavgatekeeper +collab +styx +webboard +cag +www.student +galeria +checkout +gestion +mailgate2 +draco +n2 +berlin +touch +seminar +olympus +qavmgk +f.ns.e +intl +stats2 +plato +send +idm +m7 +mx7 +m6 +coco +denver +s32 +toronto +abuse +dn +sophos +bear +logistics +cancer +s24 +r25 +s22 +install +istun +itc +oberon +cps +paypal +7 +mail-out +portal1 +case +hideip-usa +f3 +pcstun +ip-usa +warehouse +webcast +ds1 +bn +rest +logger +marina +tula +vebstage3 +webdisk.static +infinity +polaris +koko +praca +fl +packages +mstun +www.staff +sunshine +mirror1 +jeff +mailservers +jenkins +administration +mlr-all +blade +qagatekeeper +cdn3 +aria +vulcan +party +fz +luke +stc +mds +advance +andy +subversion +deco +99 +diemthi +liberty +read +smtprelayout +fitness +vs +dhcp.zmml +tsg +www.pt +win3 +davinci +two +stella +itsupport +az +ns27 +hyper +m10 +drm +vhost +mir +webspace +mail.test +argon +hamster +livehelp +2009 +bwc +man +ada +exp +metal +pk +msp +hotline +article +twiki +gl +hybrid +www.login +cbf8 +sandy +anywhere +sorry +enter +east +islam +www.map +quote +op +tb +zh +euro2012 +hestia +rwhois +mail04 +schedule +ww5 +servidor +ivan +serenity +dave +mobile1 +ok +lc +synergy +myspace +sipexternal +marc +bird +rio +www.1 +debug +houston +pdc +www.xxx +news1 +ha +mirage +fe +jade +roger +ava +topaz +a.ns.e +madrid +kh +charlotte +download2 +elite +tenders +pacs +cap +fs1 +myweb +calvin +extreme +typo3 +dealers +cds +grace +webchat +comet +www.maps +ranking +hawaii +postoffice +arts +b.ns.e +president +matrixstats +www.s +eden +com-services-vip +www.pics +il +solar +www.loja +gr +ns50 +svc +backups +sq +pinky +jwgl +controller +www.up +sn +medical +spamfilter +prova +membership +dc2 +www.press +csc +gry +drweb +web17 +f2 +nora +monitor1 +calypso +nebula +lyris +penarth.cit +www.mp3 +ssl1 +ns34 +ns35 +mel +as1 +www.x +cricket +ns2.cl +georgia +callisto +exch +s21 +eip +cctv +lucy +bmw +s23 +sem +mira +search2 +ftp.blog +realty +ftp.m +www.hrm +patrick +find +tcs +ts1 +smtp6 +lan +image1 +csi +nissan +sjc +sme +stone +model +gitlab +spanish +michael +remote2 +www.pro +s17 +m.dev +www.soporte +checkrelay +dino +woman +aragorn +index +zj +documentation +felix +www.events +www.au +adult +coupons +imp +oz +www.themes +charlie +rostov +smtpout +www.faq +ff +fortune +vm3 +vms +sbs +stores +teamspeak +w6 +jason +tennis +nt +shine +pad +www.mobil +s25 +woody +technology +cj +visio +renewal +www.c +webdisk.es +secret +host6 +www.fun +polls +web06 +turkey +www.hotel +ecom +tours +product +www.reseller +indiana +mercedes +target +load +area +mysqladmin +don +dodo +sentinel +webdisk.img +websites +www.dir +honey +asdf +spring +tag +astra +monkey +ns28 +ben +www22 +www.journal +eas +www.tw +tor +page +www.bugs +medias +www17 +toledo +vip2 +land +sistema +win4 +dell +unsubscribe +gsa +spot +fin +sapphire +ul-cat6506-gw +www.ns1 +bell +cod +lady +www.eng +click3 +pps +c3 +registrar +websrv +database2 +prometheus +atm +www.samara +api1 +edison +mega +cobalt +eos +db02 +sympa +dv +webdisk.games +coop +50 +blackhole +3d +cma +ehr +db5 +etc +www14 +opera +zoom +realmedia +french +cmc +shanghai +ns33 +batman +ifolder +ns61 +alexander +song +proto +cs2 +homologacao +ips +vanilla +legend +webmail.hosting +chat1 +www.mx +coral +tim +maxim +admission +iso +psy +progress +shms2 +monitor2 +lp2 +thankyou +issues +cultura +xyh +speedtest2 +dirac +www.research +webs +e2 +save +deploy +emarketing +jm +nn +alfresco +chronos +pisces +database1 +reservation +xena +des +directorio +shms1 +pet +sauron +ups +www.feedback +www.usa +teacher +www.magento +nis +ftp01 +baza +kjc +roma +contests +delphi +purple +oak +win5 +violet +www.newsite +deportes +www.work +musica +s29 +autoconfig.es +identity +www.fashion +forest +flr-all +www.german +lead +front +rabota +mysql7 +jack +vladimir +search1 +ns3.cl +promotion +plaza +devtest +cookie +eris +webdisk.images +atc +autodiscover.es +lucky +juno +brown +rs2 +www16 +bpm +www.director +victory +fenix +rich +tokyo +ns36 +src +12 +milk +ssl2 +notify +no +livestream +pink +sony +vps4 +scan +wwws +ovpn +deimos +smokeping +va +n7pdjh4 +lyncav +webdisk.directory +interactive +request +apt +partnerapi +albert +cs1 +ns62 +bus +young +sina +police +workflow +asset +lasvegas +saga +p4 +www.image +dag +crazy +colorado +webtrends +buscador +hongkong +rank +reserve +autoconfig.wiki +autodiscover.wiki +nginx +hu +melbourne +zm +toolbar +cx +samsung +bender +safe +nb +jjc +dps +ap1 +win7 +wl +diendan +www.preview +vt +kalender +testforum +exmail +wizard +qq +www.film +xxgk +www.gold +irkutsk +dis +zenoss +wine +data1 +remus +kelly +stalker +autoconfig.old +everest +ftp.test +spain +autodiscover.old +obs +ocw +icare +ideas +mozart +willow +demo7 +compass +japanese +octopus +prestige +dash +argos +forum1 +img7 +webdisk.download +mysql01 +joe +flex +redir +viva +ge +mod +postfix +www.p +imagine +moss +whmcs +quicktime +rtr +ds2 +future +y +sv4 +opt +mse +selene +mail21 +dns11 +server12 +invoice +clicks +imgs +xen1 +mail14 +www20 +cit +web08 +gw3 +mysql6 +zp +www.life +leads +cnc +bonus +web18 +sia +flowers +diary +s30 +proton +s28 +puzzle +s27 +r2d2 +orel +eo +toyota +front2 +www.pl +descargas +msa +esx2 +challenge +turing +emma +mailgw2 +elections +www.education +relay3 +s31 +www.mba +postfixadmin +ged +scorpion +hollywood +foo +holly +bamboo +civil +vita +lincoln +webdisk.media +story +ht +adonis +serv +voicemail +ef +mx11 +picard +c3po +helix +apis +housing +uptime +bet +phpbb +contents +rent +www.hk +vela +surf +summer +csr11.net +beijing +bingo +www.jp +edocs +mailserver2 +chip +static4 +ecology +engineering +tomsk +iss +csr12.net +s26 +utility +pac +ky +visa +ta +web22 +ernie +fis +content2 +eduroam +youraccount +playground +paradise +server22 +rad +domaincp +ppc +autodiscover.video +date +f5 +openfire +mail.blog +i4 +www.reklama +etools +ftptest +default +kaluga +shop1 +mmc +1c +server15 +autoconfig.video +ve +www21 +impact +laura +qmail +fuji +csr31.net +archer +robo +shiva +tps +www.eu +ivr +foros +ebay +www.dom +lime +mail20 +b3 +wss +vietnam +cable +webdisk.crm +x1 +sochi +vsp +www.partners +polladmin +maia +fund +asterix +c4 +www.articles +fwallow +all-nodes +mcs +esp +helena +doors +atrium +www.school +popo +myhome +www.demo2 +s18 +autoconfig.email +columbus +autodiscover.email +ns60 +abo +classified +sphinx +kg +gate2 +xg +cronos +chemistry +navi +arwen +parts +comics +www.movies +www.services +sad +krasnoyarsk +h3 +virus +hasp +bid +step +reklam +bruno +w7 +cleveland +toko +cruise +p80.pool +agri +leonardo +hokkaido +pages +rental +www.jocuri +fs2 +ipv4.pool +wise +ha.pool +routernet +leopard +mumbai +canvas +cq +m8 +mercurio +www.br +subset.pool +cake +vivaldi +graph +ld +rec +www.temp +bach +melody +cygnus +www.charge +mercure +program +beer +scorpio +upload2 +siemens +lipetsk +barnaul +dialup +mssql2 +eve +moe +nyc +www.s1 +mailgw1 +student1 +universe +dhcp1 +lp1 +builder +bacula +ww4 +www.movil +ns42 +assist +microsoft +www.careers +rex +dhcp +automotive +edgar +designer +servers +spock +jose +webdisk.projects +err +arthur +nike +frog +stocks +pns +ns41 +dbs +scanner +hunter +vk +communication +donald +power1 +wcm +esx1 +hal +salsa +mst +seed +sz +nz +proba +yx +smp +bot +eee +solr +by +face +hydrogen +contacts +ars +samples +newweb +eprints +ctx +noname +portaltest +door +kim +v28 +wcs +ats +zakaz +polycom +chelyabinsk +host7 +www.b2b +xray +td +ttt +secure4 +recruitment +molly +humor +sexy +care +vr +cyclops +bar +newserver +desk +rogue +linux2 +ns40 +alerts +dvd +bsc +mec +20 +m.test +eye +www.monitor +solaris +webportal +goto +kappa +lifestyle +miki +maria +www.site +catalogo +2008 +empire +satellite +losangeles +radar +img01 +n1 +ais +www.hotels +wlan +romulus +vader +odyssey +bali +night +c5 +wave +soul +nimbus +rachel +proyectos +jy +submit +hosting3 +server13 +d7 +extras +australia +filme +tutor +fileshare +heart +kirov +www.android +hosted +jojo +tango +janus +vesta +www18 +new1 +webdisk.radio +comunidad +xy +candy +smg +pai +tuan +gauss +ao +yaroslavl +alma +lpse +hyundai +ja +genius +ti +ski +asgard +www.id +rh +imagenes +kerberos +www.d +peru +mcq-media-01.iutnb +azmoon +srv6 +ig +frodo +afisha +25 +factory +winter +harmony +netlab +chance +sca +arabic +hack +raven +mobility +naruto +alba +anunturi +obelix +libproxy +forward +tts +autodiscover.static +bookmark +www.galeria +subs +ba +testblog +apex +sante +dora +construction +wolverine +autoconfig.static +ofertas +call +lds +ns45 +www.project +gogo +russia +vc1 +chemie +h4 +15 +dvr +tunnel +5 +kepler +ant +indonesia +dnn +picture +encuestas +vl +discover +lotto +swf +ash +pride +web21 +www.ask +dev-www +uma +cluster1 +ring +novosibirsk +mailold +extern +tutorials +mobilemail +www.2 +kultur +hacker +imc +www.contact +rsa +mailer1 +cupid +member2 +testy +systems +add +mail.m +dnstest +webdisk.facebook +mama +hello +phil +ns101 +bh +sasa +pc1 +nana +owa2 +www.cd +compras +webdisk.en +corona +vista +awards +sp1 +mz +iota +elvis +cross +audi +test02 +murmansk +www.demos +gta +autoconfig.directory +argo +dhcp2 +www.db +www.php +diy +ws3 +mediaserver +autodiscover.directory +ncc +www.nsk +present +tgp +itv +investor +pps00 +jakarta +boston +www.bb +spare +if +sar +win11 +rhea +conferences +inbox +videoconf +tsweb +www.xml +twr1 +jx +apps2 +glass +monit +pets +server20 +wap2 +s35 +anketa +www.dav75.users +anhth +montana +sierracharlie.users +sp2 +parents +evolution +anthony +www.noc +yeni +nokia +www.sa +gobbit.users +ns2a +za +www.domains +ultra +rebecca.users +dmz +orca +dav75.users +std +ev +firmware +ece +primary +sao +mina +web23 +ast +sms2 +www.hfccourse.users +www.v28 +formacion +web20 +ist +wind +opensource +www.test2.users +e3 +clifford.users +xsc +sw1 +www.play +www.tech +dns12 +offline +vds +xhtml +steve +mail.forum +www.rebecca.users +hobbit +marge +www.sierracharlie.users +dart +samba +core3 +devil +server18 +lbtest +mail05 +sara +alex.users +www.demwunz.users +www23 +vegas +italia +ez +gollum +test2.users +hfccourse.users +ana +prof +www.pluslatex.users +mxs +dance +avalon +pidlabelling.users +dubious.users +webdisk.search +query +clientweb +www.voodoodigital.users +pharmacy +denis +chi +seven +animal +cas1 +s19 +di +autoconfig.images +www.speedtest +yes +autodiscover.images +www.galleries +econ +www.flash +www.clifford.users +ln +origin-images +www.adrian.users +snow +cad +voyage +www.pidlabelling.users +cameras +volga +wallace +guardian +rpm +mpa +flower +prince +exodus +mine +mailings +cbf3 +www.gsgou.users +wellness +tank +vip1 +name +bigbrother +forex +rugby +webdisk.sms +graduate +webdisk.videos +adrian +mic +13 +firma +www.dubious.users +windu +hit +www.alex.users +dcc +wagner +launch +gizmo +d4 +rma +betterday.users +yamato +bee +pcgk +gifts +home1 +www.team +cms1 +www.gobbit.users +skyline +ogloszenia +www.betterday.users +www.data +river +eproc +acme +demwunz.users +nyx +cloudflare-resolve-to +you +sci +virtual2 +drive +sh2 +toolbox +lemon +hans +psp +goofy +fsimg +lambda +ns55 +vancouver +hkps.pool +adrian.users +ns39 +voodoodigital.users +kz +ns1a +delivery.b +turismo +cactus +pluslatex.users +lithium +euclid +quality +gsgou.users +onyx +db4 +www.domain +persephone +validclick +elibrary +www.ts +panama +www.wholesale +ui +rpg +www.ssl +xenapp +exit +marcus +phd +l2tp-us +cas2 +rapid +advert +malotedigital +bluesky +fortuna +chief +streamer +salud +web19 +stage2 +members2 +www.sc +alaska +spectrum +broker +oxford +jb +jim +cheetah +sofia +webdisk.client +nero +rain +crux +mls +mrtg2 +repair +meteor +samurai +kvm4 +ural +destek +pcs +mig +unity +reporter +ftp-eu +cache2 +van +smtp10 +nod +chocolate +collections +kitchen +rocky +pedro +sophia +st3 +nelson +ak +jl +slim +wap1 +sora +migration +www.india +ns04 +ns37 +ums +www.labs +blah +adimg +yp +db6 +xtreme +groupware +collection +blackbox +sender +t4 +college +kevin +vd +eventos +tags +us2 +macduff +wwwnew +publicapi +web24 +jasper +vladivostok +tender +premier +tele +wwwdev +www.pr +postmaster +haber +zen +nj +rap +planning +domain2 +veronica +isa +www.vb +lamp +goldmine +www.geo +www.math +mcc +www.ua +vera +nav +nas2 +autoconfig.staging +s33 +boards +thumb +autodiscover.staging +carmen +ferrari +jordan +quatro +gazeta +www.test3 +manga +techno +vm0 +vector +hiphop +www.bbs +rootservers +dean +www.ms +win12 +dreamer +alexandra +smtp03 +jackson +wing +ldap3 +www.webmaster +hobby +men +cook +ns70 +olivia +tampa +kiss +nevada +live2 +computers +tina +festival +bunny +jump +military +fj +kira +pacific +gonzo +ftp.dev +svpn +serial +webster +www.pe +s204 +romania +gamers +guru +sh1 +lewis +pablo +yoshi +lego +divine +italy +wallpapers +nd +myfiles +neptun +www.world +convert +www.cloud +proteus +medicine +bak +lista +dy +rhino +dione +sip1 +california +100 +cosmic +electronics +openid +csm +adm2 +soleil +disco +www.pp +xmail +www.movie +pioneer +phplist +elephant +ftp6 +depo +icon +www.ns2 +www.youtube +ota +capacitacion +mailfilter +switch1 +ryazan +auth2 +paynow +webtv +pas +www.v3 +storage1 +rs1 +sakai +pim +vcse +ko +oem +theme +tumblr +smtp0 +server14 +lala +storage2 +k2 +ecm +moo +can +imode +webdisk.gallery +webdisk.jobs +howard +mes +eservices +noah +support1 +soc +gamer +ekb +marco +information +heaven +ty +kursk +wilson +webdisk.wp +freebsd +phones +void +esx3 +empleo +aida +s01 +apc1 +mysites +www.kazan +calc +barney +prohome +fd +kenny +www.filme +ebill +d6 +era +big +goodluck +rdns2 +everything +ns43 +monty +bib +clip +alf +quran +aim +logon +wg +rabbit +ntp3 +upc +www.stream +www.ogloszenia +abcd +autodiscover.en +blogger +pepper +autoconfig.en +stat1 +jf +smtp7 +video3 +eposta +cache1 +ekaterinburg +talent +jewelry +ecs +beta3 +www.proxy +zsb +44 +ww6 +nautilus +angels +servicos +smpp +we +siga +magnolia +smt +maverick +franchise +dev.m +webdisk.info +penza +shrek +faraday +s123 +aleph +vnc +chinese +glpi +unix +leto +win10 +answers +att +webtools +sunset +extranet2 +kirk +mitsubishi +ppp +cargo +comercial +balancer +aire +karma +emergency +zy +dtc +asb +win8 +walker +cougar +autodiscover.videos +bugtracker +autoconfig.videos +icm +tap +nuevo +ganymede +cell +www02 +ticketing +nature +brazil +www.alex +troy +avatars +aspire +custom +www.mm +ebiz +www.twitter +kong +beagle +chess +ilias +codex +camel +crc +microsite +mlm +autoconfig.crm +o2 +human +ken +sonicwall +biznes +pec +flow +autoreply +tips +little +autodiscover.crm +hardcore +egypt +ryan +doska +mumble +s34 +pds +platon +demo8 +total +ug +das +gx +just +tec +archiv +ul +craft +franklin +speedtest1 +rep +supplier +crime +mail-relay +luigi +saruman +defiant +rome +tempo +sr2 +tempest +azure +horse +pliki +barracuda2 +www.gis +cuba +adslnat-curridabat-128 +aw +test13 +box1 +aaaa +x2 +exchbhlan3 +sv6 +disk +enquete +eta +vm4 +deep +mx12 +s111 +budget +arizona +autodiscover.media +ya +webmin +fisto +orbit +bean +mail07 +autoconfig.media +berry +jg +www.money +store1 +sydney +kraken +author +diablo +wwwww +word +www.gmail +www.tienda +samp +golden +travian +www.cat +www.biz +54 +demo10 +bambi +ivanovo +big5 +egitim +he +unregistered.zmc +amanda +orchid +kit +rmr1 +richard +offer +edge1 +germany +tristan +seguro +kyc +maths +columbia +steven +wings +www.sg +ns38 +grand +tver +natasha +r3 +www.tour +pdns +m11 +dweb +nurse +dsp +www.market +meme +www.food +moda +ns44 +mps +jgdw +m.stage +bdsm +mech +rosa +sx +tardis +domreg +eugene +home2 +vpn01 +scott +excel +lyncdiscoverinternal +ncs +pagos +recovery +bastion +wwwx +spectre +static.origin +quizadmin +www.abc +ulyanovsk +test-www +deneb +www.learn +nagano +bronx +ils +mother +defender +stavropol +g3 +lol +nf +caldera +cfd185 +tommy +think +thebest +girls +consulting +owl +newsroom +us.m +hpc +ss1 +dist +valentine +9 +pumpkin +queens +watchdog +serv1 +web07 +pmo +gsm +spam1 +geoip +test03 +ftp.forum +server19 +www.update +tac +vlad +saprouter +lions +lider +zion +c6 +palm +ukr +amsterdam +html5 +wd +estadisticas +blast +phys +rsm +70 +vvv +kris +agro +msn-smtp-out +labor +universal +gapps +futbol +baltimore +wt +avto +workshop +www.ufa +boom +autodiscover.jobs +unknown +alliance +www.svn +duke +kita +tic +killer +ip176-194 +millenium +garfield +assets2 +auctions +point +russian +suzuki +clinic +lyncedge +www.tr +la2 +oldwebmail +shipping +informatica +age +gfx +ipsec +lina +autoconfig.jobs +zoo +splunk +sy +urban +fornax +www.dating +clock +balder +steam +ut +zz +washington +lightning +fiona +im2 +enigma +fdc +zx +sami +eg +cyclone +acacia +yb +nps +update2 +loco +discuss +s50 +kurgan +smith +plant +lux +www.kino +www.extranet +gas +psychologie +01 +s02 +cy +modem +station +www.reg +zip +boa +www.co +mx04 +openerp +bounces +dodge +paula +meetings +firmy +web26 +xz +utm +s40 +panorama +photon +vas +war +marte +gateway2 +tss +anton +hirlevel +winner +fbapps +vologda +arcadia +www.cc +util +16 +tyumen +desire +perl +princess +papa +like +matt +sgs +datacenter +atlantic +maine +tech1 +ias +vintage +linux1 +gzs +cip +keith +carpediem +serv2 +dreams +front1 +lyncaccess +fh +mailer2 +www.chem +natural +student2 +sailing +radio1 +models +evo +tcm +bike +bancuri +baseball +manuals +img8 +imap1 +oldweb +smtpgw +pulsar +reader +will +stream3 +oliver +mail15 +lulu +dyn +bandwidth +messaging +us1 +ibm +idaho +camping +verify +seg +vs1 +autodiscover.sms +blade1 +blade2 +leda +mail17 +horo +testdrive +diet +www.start +mp1 +claims +te +gcc +www.whois +nieuwsbrief +xeon +eternity +greetings +data2 +asf +autoconfig.sms +kemerovo +olga +haha +ecc +prestashop +rps +img0 +olimp +biotech +qa1 +swan +bsd +webdisk.sandbox +sanantonio +dental +www.acc +zmail +statics +ns102 +39 +idb +h5 +connect2 +jd +christian +luxury +ten +bbtest +blogtest +self +www.green +forumtest +olive +www.lab +ns63 +freebies +ns64 +www.g +jake +www.plus +ejournal +letter +works +peach +spoon +sie +lx +aol +baobab +tv2 +edge2 +sign +webdisk.help +www.mobi +php5 +webdata +award +gf +rg +lily +ricky +pico +nod32 +opus +sandiego +emploi +sfa +application +comment +autodiscover.search +www.se +recherche +africa +webdisk.members +multi +wood +xx +fan +reverse +missouri +zinc +brutus +lolo +imap2 +www.windows +aaron +webdisk.wordpress +create +bis +aps +xp +outlet +www.cpanel +bloom +6 +ni +www.vestibular +webdisk.billing +roman +myshop +joyce +qb +walter +www.hr +fisher +daily +webdisk.files +michelle +musik +sic +taiwan +jewel +inbound +trio +mts +dog +mustang +specials +www.forms +crew +tes +www.med +elib +testes +richmond +autodiscover.travel +mccoy +aquila +www.saratov +bts +hornet +election +test22 +kaliningrad +listes +tx +webdisk.travel +onepiece +bryan +saas +opel +florence +blacklist +skin +workspace +theta +notebook +freddy +elmo +www.webdesign +autoconfig.travel +sql3 +faith +cody +nuke +memphis +chrome +douglas +www24 +autoconfig.search +www.analytics +forge +gloria +harry +birmingham +zebra +www.123 +laguna +lamour +igor +brs +polar +lancaster +webdisk.portal +autoconfig.img +autodiscover.img +other +www19 +srs +gala +crown +v5 +fbl +sherlock +remedy +gw-ndh +mushroom +mysql8 +sv5 +csp +marathon +kent +critical +dls +capricorn +standby +test15 +www.portfolio +savannah +img13 +veritas +move +rating +sound +zephyr +download1 +www.ticket +exchange-imap.its +b5 +andrea +dds +epm +banana +smartphone +nicolas +phpadmin +www.subscribe +prototype +experts +mgk +newforum +result +www.prueba +cbf2 +s114 +spp +trident +mirror2 +s112 +sonia +nnov +www.china +alabama +photogallery +blackjack +lex +hathor +inc +xmas +tulip +and +common-sw1 +betty +vo +www.msk +pc2 +schools +s102 +pittsburgh +s101 +rw +ozone +common-sw2 +ragnarok +venezuela +ntp0 +osaka +wx +the +www.register +wh +common-sw +privacy +promos +prov2 +c.ns +88 +oyun +alexandria +second +router-b +kentucky +nickel +www.physics +wsb +bruce +www.connect +cc1 +www.history +bert +graphite +nina +ck +kq +cmts1-all.gw +mickey +goods +was +ramses +teach +on +helen +mng +dotnet +amir +ptc +nucleus +prm +pogoda +frontend +rails +liga +outgoing +thumbnails +ins +ggg +listen +scs +dark +sav +redaktion +viewer +files1 +parker +shib +chandra +mapa +cartoon +admin.test +mad +mail25 +webdisk.www2 +crossroads +webserver2 +www.file +da2 +gratis +upd +momo +lost +vps5 +chelsea +ironman +hive +gadget +cfd307 +alan +sm1 +kansas +stat2 +morpheus +mail18 +bleach +joy +solomon +imgup-lb +jk +hammer +ea +honda +omar +trust +nino +img9 +webmasters +mona +imaps +www.backup +wsp +registro +cooper +uniform +q3 +betav2 +magellan +ris +poetry +clio +metropolis +teen +phonebook +app5 +www.bank +brilliant +underground +hero +s51 +amber +www.f +orlando +autodiscover.wp +server21 +autoconfig.games +pop1 +sean +autoconfig.wp +forever +ism +www.studio +app4 +yum +fermat +demosite +sea +celebrity +autodiscover.games +testadmin +les +www.realestate +demo01 +msm +mediacenter +jxjy +holidays +ahmed +stlouis +bilbo +coupang4 +fb12 +wlan-switch +21 +offsite +fluffy +joker +arcade +cielo +17 +server16 +mss +wonder +smolensk +dg +esc +w8 +www.aa +none +breeze +nba +toys +fakalipit-mbp.cit +nss +gen +tmg +www.perm +fishing +ldapauth +cup +dhl +www.join +eps +dove +tuning +conference.jabber +liste +smtptest +webstat +www.beauty +files3 +resolver1 +revolution +jacksonville +www.aff +pv +webdisk.tv +ia +fog +mason +odessa +www.kb +webdisk.newsletter +im1 +iweb +tower +memo +emperor +financial +stm +newwww +chel +supernova +c8 +rai +hannibal +lava +www.manager +caesar +ssb +www.az +ftp7 +itunes +julia +worldcup +whatever +alpha1 +tablet +grad +tony +14 +18 +memory +jeu +anuncios +smtp11 +colocation +clean +anh +crash +ppm +www.ct +www.cards +sti +est +goat +sg1 +etherpad +37 +aplicaciones +www.webinar +thai +iceman +mass +hqjt +region +itech +1234 +demo11 +www.ic +orenburg +cron +autoconfig.info +autodiscover.info +reset +amis +optimus +electra +bitrix +bolt +mrs +look +thanatos +wowza +istanbul +www.banners +https +timesheet +www.s2 +ibs +lupus +nutrition +return +www.ph +s36 +www.ir +projetos +america +cirrus +tax +trash +msc +cep +www.control +da1 +api-test +www.bt +adams +xserve +www.dealer +orient +retro +www.krasnodar +your +anderson +www.internet +gts +hits +pat +payroll +oblivion +notice +andre +dany +portland +applications +mailin11mx +www.google +nr +photography +xxxx +concept +masters +c.ns.email +startrek +mailin10mx +l2 +host11 +alpha2 +vmailin02mx +cic +d.ns.email +pomoc +melon +provisioning +gx2 +egov +ranger +pod +csr41.net +otto +pj +godzilla +www.house +mgw +web30 +mail.demo +spc +univer +eweb +beacon +merchant +exclusive +sensor +imagens +bu +pathfinder +oops +tnt +srv11 +mage +fernando +urchin +detroit +cetus +daemon +irk +seneca +summit +chimera +nadia +disney +crane +cleo +sahara +cartman +b.mx +hls +px +warren +spam2 +scooter +mailin13mx +e.ns.email +smarthost +tlc +vmailin01mx +mailin16mx +onix +kite +jeep +www.internal +www.b +ax +torrents +mailin15mx +mailserver1 +totem +anh-mobileth +ttc +polo +w10 +otp +mailin14mx +ojs +ksp +webdisk.apps +kyoto +university +academico +pension +www.remote +cast +ns91 +mailin12mx +www.h +cbs +facilities +ads1 +ns92 +publisher +lunar +esd +trip +sac +ot +william +serwis +stk +oj +dragonfly +b.ns.email +a.ns.email +dsa +advertise +s45 +yz +www.lists +resume +t3 +s47 +redesign +toy +pelican +popgate +www.ap +plasma +rocket +patty +srv8 +pizza +dmt +asd +srv7 +bulgaria +svn2 +drivers +ventas +www.pc +animation +monica +santiago +tucson +mary +wm2 +salem +linda +tamil +armstrong +79 +norman +quartz +scheduler +socrates +regist +server24 +campusvirtual +ip4 +alien +www.dev3 +www.vps +ip1 +misc +capella +www.mike +www.pruebas +sion +testdb +nat2 +www.am +anc +mapas +zombie +cac +nikita +freestyle +dude +rail +rea +ran +s103 +s104 +sarah +webm +mazda +claire +esx4 +mail22 +paste +hy +s106 +nh +elara +mail23 +vod2 +autodiscover.projects +lineage +s107 +f.ns.email +egw +apollon +s108 +s109 +cyrus +recruiter +autoconfig.projects +mahara +chopin +fat +emp +titanium +www.bip +chili +cumulus +blues +u2 +iam +donna +delivery.swid +amy +campaigns +wstest +cms3 +webeoc +basic +uag +vip3 +xl +roberto +karriere +pirates +helpme +economy +www.moto +www.corp +nirvana +35 +iklan +commercial +rooster +cbf7 +bkp +ns53 +webdisk.iphone +canon +test.www +www.super +dts +gforge +jam +adtest +cedar +wns1 +superman +autoconfig.facebook +ns66 +esx +tv1 +karta +chile +dotproject +ted +usuarios +relaunch +ismtp +49 +israel +www.click +s110 +www.st +www.teste +images.a +official +autodiscover.facebook +hentai +bss +dali +sparky +www.car +cosmo +emm +digit +landmark +crs +s208 +www.com +voipa075 +voipa019 +standard +myworld +brasil +voipa062 +megatron +voipa04a +groupwise +voipa07e +ns72 +byron +voipa03f +img02 +voipa029 +amos +voipa079 +s125 +voipa04d +bam +voipa017 +ns58 +voipa03d +s124 +voipa03c +colossus +oregon +filemaker +amethyst +wp1 +webdisk.member +voipa03a +projekt +opa +n1.eu.cdn +www-origin +tattoo +driver +voipa038 +rdns1 +s121 +voipa031 +voipa035 +voipa02f +solution +freehost +s119 +mx20 +robert +s116 +queen +www.magazin +acesso +voipa040 +riot +temp2 +voipa05e +www.sale +www.praca +voipa039 +taylor +www.bm +grs +aruba-master +voipa047 +s113 +yoyo +flora +www.voronezh +verdi +yc +euler +pooh +voipa02e +gy +smtp8 +voipa02d +voipa02c +iec +114 +voipa037 +quest +mail30 +www.vpn +j2 +mail26 +voipa02a +origen-www +server17 +voip1 +ws4 +voipa04c +voipa036 +browser +j1 +voipa073 +release +voipa072 +s105 +voipa048 +voipa071 +mail16 +koala +server23 +voipa01f +srilanka +voipa04e +soma +ws-lon-oauth1 +voipa01d +voipa049 +voipa04f +f4 +blitz +cine +host8 +voipa05a +zb +voipa060 +eportal +voipa034 +h6 +voipa033 +voipa032 +digi +voipa030 +service3 +joshua +carlos +projets +kitty +cloud9 +mailinglist +moonlight +webdisk.link +voipa05b +www25 +ina +discount +irc.sac +voipa028 +csa +stories +voipa05c +parfum +voipa06a +voipa01c +www.local +voipa01b +voipa06c +voipa027 +nag +www.sl +robin.exseed +voipa06d +voipa06e +voipa026 +voipa06f +www.magazine +wis +voipa07a +voipa025 +benny +rcs +minsk +voipa064 +vps7 +stash +image3 +noc2 +www.canada +smi +voipa059 +voipa065 +webdisk.classifieds +note +voipa024 +maggie +planetarium +luis +voipa01a +socialmedia +voipa023 +sweet +rmt +cmt +serena +collaboration +ftpmini +esxi +www.advertising +webadvisor +m.demo +psychology +graphs +ly +ppa +voipa063 +networks +s48 +pub2 +power2 +greece +xoap +sib +carla +voipa061 +rts +voipa058 +branch +mediawiki +clark +twin +b4 +web25 +pty11165b +lighthouse +voipa066 +voipa057 +webmeeting +brian +ircip +www.conference +web27 +ocsp +uranium +autodiscover.billing +marley +correoweb +fc2 +fiesta +velocity +sanatate +ac2 +dentist +u1 +techsupport +endpoint +vestibular +voipa022 +clone +frontpage +www.turystyka +samuel +aws-smail +gabriel +bookings +webdisk.stage +b7 +enroll +wmt +anonymous +ali +yukon +gw.bnsc +wikitest +bv +tutorial +zaphod +voipa056 +voipa067 +maint +voipa01e +tau +voipa055 +ren +atl +nat-pool +voipa021 +voipa054 +turystyka +voipa020 +comic +voipa053 +voipa052 +infonet +she +as400 +autoconfig.billing +voipa070 +babylon +voipa018 +lee +www.trade +badger +nospam +srv12 +www.kr +chase +srvc67 +icc +moderator +stark +voipa074 +mail-2 +henry +m-test +oud +vincent +lyra +skinner +guard +sphere +balance +voipa016 +lara +srvc52 +dogs +voipa051 +voipa02b +antonio +silicon +srvc47 +olympic +kings +activesync +triumph +www.freedom +lena +solarwinds +voipa015 +xerox +voipa014 +riverside +gx4 +cdb +to +voipa013 +vault +fisheye +tron +29 +chevrolet +square +srvc42 +bbs1 +dollar +adnet +voipa012 +voipa011 +south +ccm +hamilton +srvc57 +prepaid +voipa010 +kairos +intel +login2 +creditcard +eportfolio +rproxy +alfred +sce +nat1 +riga +blogdev +voipa076 +itchy +newsletter2 +voipa041 +gx3 +gx1 +www.tmp +voipa050 +romeo +nara +legolas +pol +ical +christmas +webmailtest +vw +voipa07b +portals +envios +sandbox2 +amateur +autoconfig.www2 +voipa07c +voipa077 +emily +umwelt +shops +starnet +www.mc +elena +s03 +bnet +srvc62 +lazarus +daphne +www.investor +autodiscover.www2 +voipa042 +illusion +ah +newlife +www.th +equinox +www.agent +tz +milano +presence +autoconfig.tv +voipa078 +novi +pretty +basil +dcs +agencias +voipa03b +venom +erato +ata +voipa03e +sipac +programs +myftp +testdns +gray +autodiscover.tv +horde +hideip-uk +d.ns +manuel +www.adv +voipa046 +thailand +www.women +arnold +demo12 +styles +frost +voipa04b +therapists +apc2 +hugo +epp +gal +gin +wlc +autodiscover.members +nevis +mart +voipa045 +nitrogen +autoconfig.members +lxy +zone +voipa068 +s201 +ibook +aprisostg +validation +voipa043 +tpm +www.tula +bluebird +www.access +0 +voipa069 +death +8 +justin +www.innovation +faust +www.banner +www.md +gals +staging.secure +int.www +int.api +pn +www.share +mylife +ipod +piano +wns2 +pulse +voipa05d +ltx +voipa07f +lj +jwxt +19 +klm +voipa05f +cie +voipa044 +c7 +voipa06b +1000 +smtp12 +liquid +collector +jokes +evasys +emailmarketing +voipa07d +royal +observium +node3 +vis +iks +www.affiliate +inferno +drac +bella +ieee +fran +comp +warszawa +async +stl +wpb +nagios2 +linkedin +mars2 +kei +geography +www.david +apolo +razor +infinite +lucifer +w9 +48 +bgs +tzb +dennis +cs3 +sls +fhg +qs +gina +boris +hps +randy +catalyst +random +www.soccer +con +ani +players +troll +ruben +amg +immigration +vanessa +synapse +izhevsk +hikari +pri +bryansk +lw +calcium +gsc +nashville +nor +pskov +chita +img11 +turtle +philadelphia +scoreboard +loghost +redes +ws01 +prov +akira +uy +malaysia +lovely +bond +yuri +prism +jun +goldfish +brandon +steel +www.review +ora +ami +corpmail +demo9 +romance +www.sex +www.track +mmp +fk +mentor +butterfly +communications +nao +www.talk +mem +short +www.anunturi +mssql3 +s53 +jennifer +tito +stitch +www.ss +ods +bigbang +www.intra +sdo +moa +streams +kav +room +gastro +mat +barbara +epo +morris +jabba +dl3 +peace +win6 +bologna +alpine +benjamin +experience +mtg +srv9 +www.ecommerce +indian +wilma +photoshop +teens +er +www.e +pine +mortgage +espace +wish +ob +darkstar +winwin +nx +cam3 +dota +b12 +color +marie +www.happy +server27 +architecture +okinawa +jess +itest +ns48 +xj +fine +admins +flux +basket +profiler +athens +nest +bison +roadrunner +mobileapp +neko +img170 +charity +file2 +apptest +showroom +lima +www.gry +zoe +arrakis +rss0 +howto +aikido +vps6 +operator +rv +sasuke +modules +sniper +www.pm +armani +webdisk.dev2 +sms1 +www.wm +ddd +vtiger +yam +employment +sir +paintball +proj +mgt +soso +aldebaran +bim +loto +ron +xml2 +oslo +pic2 +snap +msdnaa +promotions +devadmin +alta-gsw +viajes +ram +agents +bash +memberpbp +api3 +taxi +frontier +yuyu +34 +reading +vm02 +venture +beheer +hz +tf +sierra-db +hulk +plugin +ns05 +www.science +samson +espanol +arsenal +cpanel2 +vadim +lord +trend +brest +lesbian +avs +empresas +xavier +flamingo +nas3 +alive +cname +jss +amd +terminator +newworld +cpe +professional +visit +www.ee +spm +presta +yellowpages +block +rosemary +ns65 +goblin +educ +piter +crow +zenith +46 +sabrina +voip2 +jet +img14 +nebraska +i0 +adidas +afrodita +i6 +gimli +bara +treehouse +solid +51 +valiant +vm5 +michigan +embed +limesurvey +sc2 +rossi +www.friends +xoxo +meetingplace +god +www.family +s122 +img03 +licensing +petra +s118 +www.traffic +www.ford +s117 +see +trunk +mystery +www.golf +s115 +mail19 +els +mail33 +crimea +x3 +informer +publicidad +www.clientes +birthday +livesupport +trance +www.biblioteca +mail24 +ms3 +bbm +lcs +abraham +jonas +stephanie +salam +sws +www.tm +juan +rage +battle +rdc +timeclock +kat +dna +bit +force +winnie +liverpool +static5 +beaker +lit +service2 +spica +advertiser +salon +yo +fichiers +prov1 +ecards +autodiscover.wordpress +publishing +captcha +podcasts +org-www +orc +uploader +web33 +ek-cat6506-gw +krang +dani +fotografia +orb +sitesearch +livestats +www.ro +pantera +www.ac +autoconfig.wordpress +milan +classes +neutron +dcms +www30 +beethoven +mail36 +accommodation +macbook +ap2 +testa +webprint +dewey +crmdev +qc +society +psycho +jacob +knowledgebase +vg +cem +s221 +s216 +raovat +tara +lea +observer +andrei +elsa +css1 +chs +homepage +www.ec +aloha +spartan +cs16 +zdrowie +dual +spin +iis +ec2 +trace +compare +photo2 +ica +badboy +gourmet +obsidian +cpc +mode +april +yuki +onlineshop +www.volgograd +umfrage +admin.dev +siteadmin +phptest +som +mani +atendimento +pagerank +olivier +www.gay +fbapp +www.redmine +o2.email +newdesign +s207 +ssd +suppliers +helsinki +cheese +test19 +www.as +s203 +27 +autodiscover.radio +ne +financeiro +www.sp +autoconfig.radio +phpmyadmin2 +saransk +tyr +vic +cluster2 +dev6 +xs +bliss +60 +tatiana +mature +babel +26 +xinli +pustaka +mydesktop +www.n +carter +22 +kobe +testing2 +my2 +90 +explorer +wy +ftp9 +aovivo +army +dx +kiki +phoebe +clasificados +survey2 +ravi +origin-cdn +dial +www.legacy +ftp8 +wz +www-c +nws +s202 +80 +bgr01swd +voltage-pp-0000 +itm +im.rtpete +23 +assets1 +johnny +street +dev7 +ban +ip-uk +weightloss +lpm +iraq +paradox +fermi +vino +oban +test14 +musa +perpustakaan +radius3 +rtpeteim +game2 +pro-oh +regions +hcm.m +dns10 +smx +mans +tns +pozycjonowanie +gonghui +muller +nick +church +services2 +hana +imperial +porno +hama +showcase +sputnik +www.stock +skywalker +www.tomsk +storefront +crater +chan +localhost.m +chloe +pharm +pavel +national +barcelona +silvia +remoteaccess +webdisk.seo +srv02 +jt +recim +alc +fear +aulavirtual +prog +timer +kana +cardinal +hn.m +m12 +timetable +dev.www +maxi +cyan +www.customer +ids1 +ric +lucas +ganesh +mik +member1 +31 +mali +noel +ero +pack +dba +reza +papillon +kps +politics +s222 +navigator +host12 +designs +car40.net +elc +lp3 +sta +csr21.arch +pallas +nostromo +carl +nlp +terry +cmts2-all.gw +pyramid +monk +keeper +magpie +spike +wolves +consumer +jay +mediakit +topics +infosys +lolita +www.pozycjonowanie +pr1 +oldftp +ritz +www-1 +pastebin +nowy +poland +tds +rami +mami +mybook +topsites +statistic +66 +gomez +pamela +listings +only +webdisk.my +speak +kl-cat4900-gw +media3 +original +admintest +preview2 +game1 +videoconferencia +academic +vdp +autoconfig.iphone +teachers +flame +my1 +newage +mx05 +sofa +www.smart +dwcloudorigin +autodiscover.iphone +www.templates +sorigin +tama +cde +c21 +fw01 +ross +onlinegames +cfd264 +sell +teddy +bos +ftp.cp +edwin +mapsorigin +sync1 +fbm +cshm-sbsc01.v10.csngok.ok +warez +wwworigin +dwiorigin +www.mob +wxdataorigin +justice +maporigin +morigin +lira +old1 +kbox +legion +klub +hurricane +fcgi +may +xxxxx +golestan +dworigin +torigin +nvpgk1 +dataorigin +sed +mp2 +www.islam +nvpgk +filter2 +mandarin +staging.www +mwiorigin +tl +soon +omni +www.adm +lc1 +anders +icinga +wawa +questionnaire +dynamics +bia +www.km +kf +cognos +pmb +sslorigin +jana +nw1 +fedora +www.devel +myportal +gromit +www.finance +today +prelive +kermit +p5 +s219 +lancelot +jura +cyc +epi +s206 +penelope +newdev +detox +simba +www26 +www.wedding +wisconsin +philippines +fad +girl +www.novo +apps3 +stb +consulta +dingo +cmail +67 +saba +fairy +bluemoon +auth1 +athos +guia +songs +siam +novelty +tera +www.eshop +s205 +clarity +pdu1 +elias +lawrence +sds +web0 +srv20 +fireball +www.list +sv8 +s100 +cambridge +mission +kamera +atest +ns69 +rtpqaim +fair +c-asa5580-v03-01.rz +s42 +beyond +demoshop +horoscope +puck +egroupware +40 +sup +sv7 +three +option +ozzy +mail06 +mhs +pasca +wps +53 +postit +wii +smf +spitfire +cstrike +utopia +vm01 +vi +dms1 +52 +citrix2 +mxbackup +vm6 +zeon +s126 +classroom +webalizer +halo +s131 +illiad +s133 +archivio +s134 +cns +belgorod +ldapclient +klient +batch +fabio +s211 +s214 +phaim22 +sfs +giporigin +s215 +melissa +s213 +s120 +abel +cow +y2k +s130 +gem +goliath +demo15 +tang +ftpserver +www.kaluga +kia +clips +ham +silence +quad +webinfo +plugins +www.article +volvo +mb1 +cris +ayuda +kingdom +juegos +ns82 +i10 +autodiscover.portal +autoconfig.portal +ts01 +ns81 +caramel +zc +circle +ipplan +automation +rob +twister +poznan +c9 +moskva +ns71 +redhat +secured +rr1 +morgan +str +academia +researcher +ns59 +muse +www.monitoring +mei +ns56 +meridian +wendy +ns46 +brains +bla +autoconfig.sandbox +traf +autodiscover.sandbox +vma +nieruchomosci +simpsons +ark +dbase +bulldog +lyon +kkk +design2 +sequoia +centro +pro-ky +eternal +www.kids +jasmin +tyb +newspaper +rtpclientim +argentina +www.net +nancy +ajuda +bosch +vpnc +magnitogorsk +colombia +cws +mee +convergence +tech2 +scully +deneme +rudy +cab +day +monalisa +blade7 +galeri +acer +qwerty +as.iso +hsp +proof +3c +www.gs +host01 +indy +paolo +ns49 +blade5 +harris +gw4 +select +webdisk.reseller +weber +wxy +dictionary +dmedia-g +info1 +verify.apple +sandra +b2btest +pic1 +strong +suny +clientftp +sml +emba +www.allegro +tmc +galadriel +sun1 +gary +medios +andromede +statistiche +mail.99 +eat +cdn4 +vps8 +sloth +ray +electro +oms +archangel +www.s3 +im.rtpqa +bible +www.alpha +lovers +economics +sma +electric +ip2 +nene +planner +nw +anita +www.ws +homolog +myown +rtpim +firewallix +traveller +bulletin +www.demo1 +benchmark +whisper +ann +greg +host25 +marshall +spiderman +crowd +sprite +tot +harvey +trs +gtest +shuttle +modern +judas +backstage +deti +sterling +ss2 +coconut +xlzx +win13 +scarlet +www.sistemas +ebs +argus +lh +maryland +yn +server29 +relay4 +sexshop +futaba +historia +b11 +b10 +markets +xc +www.av +santafe +usedcars +presentation +cpm +norway +bcs +krishna +castle +rewards +alexa +sonata +formation +www.assets +radon +zelda +autoconfig.loja +wyoming +fate +panel2 +imap3 +cm2 +autodiscover.gallery +mssqladmin +autoconfig.gallery +www.gps +autodiscover.loja +smtp9 +wakeup +d5 +independent +julie +stiri +selenium +www.archives +platform +daisuke +dc3 +ernesto +www.ps +fes +www.pb +d9 +porn +atomic +www.correo +chatter +rbs +emto277627 +tdb +milwaukee +tintin +www.cl +astral +lottery +paint +comments +thegame +foryou +truba +mozilla +borg +node +vps9 +worker +wiki2 +outdoor +monaco +mimosa +sid +body +stardust +devserver +egresados +seagull +server44 +webdisk.host +cp3 +swansea.cit +chicken +api.test +server03 +mssql4 +lucia +nfc +vs2 +vale +imss +s41 +s43 +projekty +picasso +blossom +eleven +taobao +papyrus +pharma +laila +autodiscover.it +evans +ngs +failover +rajesh +profit +enlace +podarok +amira +louis +reboot +planeta +owner +www.blackberry +response +server30 +pil +del +geyser +mtc +vanguard +cec +blackcat +prezenty +clubs +yun +primus +www.2012 +apollo2 +www.corporate +dubai +devapi +finanse +autoconfig.music +autodiscover.music +phenix +madison +tambov +bcc +vpnssl +wp2 +www.hc +webdisk.music +mambo +www.r +www.europe +roy +apartment +www.memberpbp +hod +server41 +mugen +primula +goodlife +server25 +evil +idp2 +www.memberall +b15 +mx9 +memberall +blade3 +www.pic +unreal +b13 +112 +acp +haru +mailservice +no1 +www.irc +tpl +weekly +webmail.forum +testapi +ironport2 +free2 +brothers +blade6 +bayern +daedalus +cincinnati +www.aurora +wi +avon +nmc +season +zorro +www.at +fruit +mx-1 +magneto +atmail +wicked +webmail4 +sanfrancisco +www.central +surgut +adwords +esl +salah +cmp +mania +mebel +aviator +chennai +ser +tccgalleries +blogg +jj +jh +smtp04 +www.op +www.tracker +gui.m +someone +imac +tanya +drew +ns112 +kai +andrey +ion +plum +aplus +weekend +baker +ews +qp +moodle1 +theater +www.phoenix +educacion +parser +limbo +mak +ns54 +profil +arg +freemail +ns57 +42 +shara +opal +www.css +mil +storex +download3 +www.apple +nil +mssql1 +records +v6 +vine +ecuador +webdisk.health +webdisk.social +bones +popup +i24 +philosophy +barry +amadeus +www.yaroslavl +bluebell +45 +smtp13 +www.tutorial +drop +www.cars +ud +sql02 +smtp14 +www.meteo +viktor +taz +www.calendar +partner2 +h7 +twilight +bat +emo +realtime +demo13 +sasha +toshiba +deli +mq +www.todo +adel +47 +drake +info2 +mktg +webzone +certificate +s212 +themis +newchat +s218 +s217 +music1 +yoyaku +shibboleth +s139 +gordon +i7 +employee +havoc +cs01 +lb01 +s138 +blueberry +mobile3 +adelaide +s137 +i8 +s136 +i9 +s135 +webdisk.it +ptt +zippy +camp +fnc +m2m +s132 +gaming +darius +lapis +netstorage +s129 +www.singapore +hunting +maker +win9 +ssh2 +north +label +cjc +oneway +kuba +sapporo +lin +full +bodybuilding +www.phpmyadmin +popular +voodoo +portal3 +wildcat +lucius +project2 +sumire +mn +testm +britney +magma +bilder +asian +an +s58 +www.cinema +passion +vds1 +sklad +eform +devdb +www.test4 +61 +www.like +s224 +andres +sunflower +update1 +gbs +basij +pavlov +fancy +locator +bmail +thalia +tip +kaiser +dsc +sv9 +success +invite +wellbeing +emailadmin +ldap01 +srv21 +mstage +www.booking +xen3 +asg +strike +unique +titus +uran +led +webdisk.us +69 +juniper +shams +repos +cerbere +www.tracking +wwwstg +hair +sulu +file1 +www.australia +opsview +origin-static +appdev +www.open +bursa +net1 +weddings +www.org +s210 +just4fun +halley +s144 +jimmy +wanda +test1234 +s143 +s209 +ipac +webview +gcs +amazing +pubs +demon +utah +gls +hertz +www.wwww +sipinternal +lua +www.exchange +myblog +pic3 +happylife +xiaobao +knight +papercut +timothy +rns1 +77 +shin +primrose +dep +administrator +filer2 +sharon +kayako +redaccion +tsunami +belle +pokemon +sleep +mail40 +apl +srv10 +environment +adc +avedge +top10 +saint +svm +sonar +butters +warning +used +jeux +chouchou +www.learning +long +firewall2 +demo02 +credito +wallpaper +aeon +billing2 +anal +ns-2 +furniture +titania +elmer +wwu +autodiscover.files +karaoke +glory +autoconfig.files +deai +gamez +cristal +sgm +gates +gregory +acorn +rice +venice +kid +fiat +geek +mail27 +media4 +afp +servicetest +pje +adp +www.hn +seminars +sql01 +b6 +sama +remax +vortex +sharing +mox +vince +pts +rrr +mimi +mca +concours +hehe +web28 +phi +pirate +trent +bpa +js1 +xszz +pipe +glacier +bacchus +puffin +webim +chatbox +charles +element +www.students +sana +ibrahim +apidev +nnn +webcache +autodiscover.help +lili +autoconfig.help +shaman +s227 +remont +lexus +ftp.demo +www.pomoc +qm +eddy +32 +absolute +kan +espresso +indra +mweb +rama +colibri +anti +a8 +windowsupdate +inspire +cmstest +rive +now +nini +annunci +elrond +heron +lineage2 +kenzo +feng +envy +abc123 +personel +rides +d8 +lust +360 +karim +sims +nats +nash +alumnos +stop +bk1 +obiwan +www.feeds +arquivos +store2 +www.futbol +lexington +hardy +infocenter +pxe +edu2 +evaluation +www.foro +trading +tiny +www.biznes +autodiscover.helpdesk +larry +muzik +autoconfig.client +volleyball +kultura +eman +autoconfig.download +autodiscover.download +itadmin +ultra1 +yamaha +57 +must +newman +63 +mail-gw +autodiscover.client +bbs2 +topsite +workplace +mari +mailgate1 +mysql10 +publications +ka +devsite +report1 +student3 +yy +autoconfig.helpdesk +www.ww +lang +masaki +costarica +set +labo +oriflame +www.noticias +devwww +30 +www.festival +tpc +net-xb.ohx +features +bgp +www.georgia +webdisk.loja +www.kaliningrad +azerty +www.chelyabinsk +novgorod +camfrog +dig +anyserver +hiroshima +zend +www.sites +carrie +76 +olap +dc4 +binary +www.24 +colors +mynet +salary +judo +webdisk.tickets +gravity +webdisk.design +aviation +rst +94 +boxer +hilbert +herbalife +carrier +64 +nexgen +intranet1 +willie +api.staging +siena +doom +record +admin.m +l2tp +mail.dev +ariadne +www.transport +alaa +area51 +webmail.demo +www.reviews +cantor +webdisk.links +autoconfig.member +test17 +autodiscover.member +s05 +mail250 +gateway1 +smb +web29 +scrubs +transit +chewbacca +web34 +koha +properties +tori +vc2 +mail37 +mail38 +css2 +mail39 +foxtrot +printing +bigben +neworleans +www.dms +vns +teams +writers +cmdb +muenchen +oldforum +111 +libweb +esx5 +benefits +www.asia +scl +pws +esx6 +28 +gutenberg +django +caldav +var +tracker2 +mov +lumiere +tracker1 +33 +manhattan +kaku +maga +kumi +kesc-vpn +dns9 +kelvin +insider +www.car-line +mastermind +sw2 +ns80 +wildersol1 +dns14 +ns75 +avasin +dns.class +webdisk.server +handy +ns68 +ns67 +seco +trinidad +puppetmaster +immobilien +regina +nantes +wm1 +ns47 +41 +citrix1 +citron +zw +dialog +ns90 +ns111 +bomgar +www.doc +discountfinder +lb02 +tao +psg +www.website +resim +www.sm +resolver2 +ns120 +wwb +101 +patriot +portugal +porsche +treinamento +ns110 +marilyn +l2tp-uk +aladin +zim +sophie +francisco +quebec +depot1 +msw +onlyyou +thu +parrot +www.ces +interior +wins +hh +sr1 +ll +tf2 +tallow.cit +sv10 +bigmac +lock +ri +vtest +www.products +mus +bewerbung +www.international +moc +tata +srm-atlas-2.gridpp +bane +wwwc +cfg +building +linux.pp +dev-api +printserver +autodiscover.online +autoconfig.online +gw.pp +pierre +cnr +pressroom +cox +fmc +amin +vtp.data +anis +srm-atlas.gridpp +dhs +legacymail +ws6 +fig +devel2 +dia +maximus +heritage +smoke +lo.vip +163 +santa +popeye +prefs.vip +asc +s04 +lingua +amc +203 +dnsadmin +jsj +s66 +www.toko +etoile +s49 +trafic +circus +orientation +www.im +lsg +harold +666 +email3 +virtual1 +ww8 +rs3 +server33 +server28 +ii +dialer +eds +isatap +npc +creditbank +perfume +garden +cream +kuku +florian +phy +icq.jabber +pop3s +snort +tiki +right +lounge +great +www.best +kato +slc +wj +www.delivery.a +mind +cover +or +adx +pasteur +chitchat +inspiration +kanji +hari +ideal +socrate +mc2 +winchester +www.sanatate +www.bancuri +chen +galois +sgd +recipe +countdown +editorial +hitech +365 +field +retracker +strider +fleur +isaac +signin +testcms +cbc +s140 +marwan +bobo +eda +contribute +www.directorio +moldova +www.gift +kura +s226 +dolly +psa +volunteer +relatorio +draft +iowa +s127 +s128 +maat +canary +norton +s141 +www.resources +s142 +backup5 +xbox360 +s156 +s225 +diego +www.order +s220 +thayer +sacramento +gap +nac +kassa +xbox +user1 +nm2 +misty +carina +ethics +sundance +person +charm +confirm +value +infoweb +reportes +diane +atenea +serene +www.omsk +asdfg +oral +cmd +adobe +ahmad +irving +theia +www.vladivostok +m19 +fatima +millennium +avenger +freechat +webdemo +movie2 +anand +www.sub +franky +cleaning +arhangelsk +artem +barcode +blink +orion2 +euterpe +wfa +encuesta +walking +capa +ape +ayoub +sftp3 +danny +xa +squirrel +gwmail +coins +servis +kd +webhard +scylla +coleman +weblink +doris +drama +apc4 +wip +mistral +prisma +elisa +outage +kangaroo +mpr +term +hakim +concord +pear +emailing +running +s230 +scrapbook +caroline +distance +www.sf +flight +ecampus +host10 +www.la +airport +viola +cbt +www.dp +www.ci +nds +ill +ids2 +catering +user2 +up1 +up2 +www.pliki +impulse +theseus +mcafee +flc +lvs2 +myphp +for +forums2 +phillip +master1 +saturno +cowboy +rebel +burbank +lenta +wellington +icarus +www.football +midnight +mafia +lis +cosign +whiterose +calliope +penny +geology +webdisk.api +mamba +mit +ole +joseph +rcp +subscriptions +mfs +racoon +maroc +fg +gra +tsgw +spravka +sda +cai +abacus +freegift +delicious +mail-old +titanic +www03 +igra +uno +plm +clc +eko +umbrella +cpan +prod2 +cdl +pebbles +globe +nightlife +helper +champions +joel +li +yumi +tuanwei +flirt +scholar +jon +angela +recette +rahul +potato +hlrdap +app6 +tree +baku +per +superstar +tops +eu.edge +bcm +adminmail +autoconfig.classifieds +nec +managed +autodiscover.classifieds +ronny +rover +ttalk +valentina +boletines +ithelp +ida +edoc +partenaires +restore +punk +excellent +owen +www.premium +tcc +www.2011 +emmy +remotesupport +gama +bulkmail +md1 +gera +mailout2 +rbl +db0 +alta +osc +testdomain +email1 +nasa +mika +redwood +agata +voltage-ps-0000 +willy +srv13 +www.phone +leaf +sga +nitro +webdb +b16 +santabarbara +issue +env +pma2 +erwin +kungfu +cadillac +antony +sfx +fury +calls +typo +www.js +restaurant +cheers +ait +sirsi +dust +elec +esther +webcom +www.suporte +activation +cassini +dots +sally +spacewalk +selfcare +pia +ocelot +fic +cute +proxy5 +ps1 +dice +www.cm +ek +archiwum +nguyen +webdisk.archive +cel +virginia +webmailx +www.mail2 +repositorio +krypton +ftp.new +urano +whitelabel +pure +mundo +walnut +trillian +mail32 +billy +sof +friendship +tlt +mail09 +webcam1 +st4 +nico +muzica +www.card +policy +anon +mia +remix +aviva +laplace +dos +shs +shout +fsproxyhn.kis +inscription +hsl +mypc +paco +extend +www.mysql +icms +magnum +sp4 +fsproxyst.kis +bcst-xb.ohx +sebastian +mobiletest +mrm +ies +campus2 +rtr-xb.ohx +itservicedesk +spss +villa +epost +reports2 +zozo +tomo +miracle +ultimate +proxy4 +www.cultura +senator +cdr +werbung +chelyabinsk-rnoc-rr02.backbone.urc.ac.ru +www.moda +rosetta +smhecpsc01-v60.ok +f6 +hrms +assets3 +oas +pgsql2 +pgsql1 +hell +star2 +dprhensimmta +nothing +ffm +xq +www.manage +jin +www.do +rohan +mx8 +canoe +www.dc +eclass +hotthiscodecs +kn +codecsworld +megamediadm +symphony +kea +bestmediafiles +enjoymediafile +easymediadm +devblog +www.cf +livedigitaldownloads +downloadmediadm +admin01 +allstar +bestlivecodecs +www.ls +lib2 +s52 +time2 +www.security +pow +searchdigitalcodecs +teaching +siri +thezone +findfreecodecs +bestdigitalcodecs +luggage +cu +jj-cat4900-gw +www.realty +txt +enjoythiscodecs +honeymoon +www.tourism +tomato +www.computer +findmymediafiles +newmediacodecs +hj-cat4900-gw +mydigitalcodecs +flat +optima +asso +ariane +pie +tuna +gtm1 +mediacodecsworld +aurelia +nestor +fastprodownloads +srm +freedigitalcodecs +delivery.platform +sgr +megamediadownloads +copyright +timon +ldc +languages +fundraising +fastmediadm +vidar +getthiscodecs +linux3 +py +gis1 +webdisk.office +livepromanager +networking +silica +fastdigitaldownloads +newdigitalcodecs +mythiscodecs +skype +dod +rrd +azalea +backupmx +weibo +superprodownloads +fukuoka +webdisk.x +practice +muffin +mystic +www.germany +xerxes +globus +freedownload +als +assistance +lada +freemediadownloads +gsk +wha +www.vietnam +downloaddigitaldownloads +fastmediamanager +livedigitaldm +gaston +megaprodownloads +internship +liveprocodecs +arte +megadigitalmanager +downloadpromanager +meg +sow +cherokee +easydigitaldm +freemediadm +easymediamanager +supervision +varnish +hn.ipad +ressources +paiement +slm +livemediacodecs +thethiscodecs +sql4 +chum +1trmst2hn +www.post +vlg +www.erp +www.bd +times +newdigitalmanager +dddd +irina +deer +leech +newprocodecs +laser +www.orders +lukasz +gan +nascar +ceo +dataservices +access2 +control2 +esf +searchmediafilesinc +joke +getmediacodecs +themediacodecs +freehdcodecs +sifa +ringo +thenewcodecs +freeprodownloads +finddigitalcodecs +back2 +tolkien +puskom +stage1 +bestfreecodecs +supermediamanager +freedigitalmanager +sudan +www.zdrowie +mendel +ico +digilib +apunts2 +js.hindi +hospitality +vod3 +newprodm +enet +www.laptop +hostel +jing +www.e-learning +joan +megamediamanager +tibia +searchlivecodecs +b14 +www.insurance +pesquisa +mymediacodecs +boo +liveprodownloads +juli +newmediadownloads +bestmediafilesinc +freeprodm +gotcha +searchmediafiles +lien +dreamteam +lilo +wsc +sysmon +rbt +resolver +loli +www.mt +staf +garant +findthiscodecs +clienti +way +fastprodm +pronto +champion +terms +data3 +www.global +pr2 +callback +sede +erbium +madmax +ku +nono +pkg +formula1 +vodafone +www.11 +evision +cp01 +cosme +darkness +www.kursk +opportunity +webdisk.joomla +www.zabbix +raja +dumbo +sogo +xfiles +antispam2 +clover +freemediamanager +webdisk.blogs +autoconfig.blogs +marcopolo +autodiscover.blogs +sierra +filer +dana +happiness +webconnect +icp +www.zp +shanti +superdigitaldm +mynewcodecs +www.notes +webapi +easyprodm +webconference +astrahan +taos +promociones +supermediadownloads +www-staging +dickson +livemediamanager +newdigitaldm +kostroma +777 +jpk +ldap-test +megadigitaldm +logan +airsoft +fastmediacodecs +teal +ipam +advanced +app7 +switch2 +hidden +united +underdog +yaya +www.system +pwa +lib1 +finder +yoga +lz +www.podcast +hobbes +hani +findmymediafileinc +york +bars +www.fx +skoda +mysql02 +nueva +tyler +pdm +wander +ns00 +fastdigitaldm +valencia +dar +mns +easypromanager +www.afisha +megapromanager +fastprocodecs +superdigitalmanager +synd +wes +surabaya +comcast +demo14 +bestmediafileinc +mouse +profesionales +xgb +real-estate +tad +rl +recreation +www.cz +dmc +bestdeal +fastpromanager +frey +eldorado +pepsi +dmg +oldman +merak +www.planet +raw +livedigitalcodecs +marta +findmediafileinc +megadigitaldownloads +sft +findmediafilesinc +hotlivecodecs +www.musica +mtn +gondor +spy +www.dz +pdb +cracker +www.digital +downloadhdcodecs +freepromanager +warrior +bestthiscodecs +searchmediafileinc +mailmx +www.mini +www.kiev +kizuna +enjoylivecodecs +mmedia +idefix +searchmediacodecs +rdg +pigeon +webdisk.testing +searchfreecodecs +eb +enjoymediafilesinc +fit +telefon +points +pla +eli +freedigitaldownloads +paranormal +ms4 +hotdigitalcodecs +awa +jesse +enjoymediafiles +limited +sgw +12345 +worldwide +aga +getlivecodecs +www.gb +www.he +findnewcodecs +easymediadownloads +connection +ns2.hosting +gucci +ns1.hosting +www.rc +mojo +freya +timeline +signal +met +pmt +uk2 +www.expo +hasan +rambo +eca +mylivecodecs +poisk +fasthdcodecs +s233 +s236 +apk +menu +skipper +s237 +twins +s239 +mgm +eski +grass +starlight +ns2b +www.rent +ismail +echelon +kitten +bollywood +enjoymediafileinc +fastdigitalcodecs +downloaddigitaldm +downloadprocodecs +motion +pax +lalala +livemediadownloads +jonathan +arcturus +www.poker +s238 +newshop +bonjour +accent +win14 +encore +raphael +downloadprodownloads +tarik +donkey +findmediacodecs +hudson +freedigitaldm +bauer +newprodownloads +safari +advokat +hotmediacodecs +mfr +bubba +easydigitalmanager +api-dev +qa-partner-portal +freeprocodecs +ilearn +livehdcodecs +plusone +newhdcodecs +thelivecodecs +brisbane +midas +newdigitaldownloads +fantasia +tas +superprodm +devon +blaze +findmediafile +easydigitaldownloads +downloaddigitalcodecs +findmymediafile +livedigitalmanager +kw +enq +downloadmediamanager +origami +www.lipetsk +mongo +swallow +emotion +megaprodm +qarvip +am1 +getdigitalcodecs +www.star +brother +searchnewcodecs +infotech +performance +newpromanager +enjoymediacodecs +honduras +eowyn +qa-verio-portal +www.insight +www.script +proxy01 +baron +kif +freemediacodecs +jurnal +google1 +hotfreecodecs +livemediadm +cheboksary +www.multimedia +webapps2 +win17 +hannah +www.rostov +entrepreneurs +www.mag +tarot +findlivecodecs +rambler +win16 +iridium +win18 +www.al +enjoyfreecodecs +inform +trackit +asher +www.sd +secmail +qa.legacy +superpromanager +mobiledev +prod.tools +prod.new +www.newyork +rejestracja +enjoycodecs +132 +searchthiscodecs +ferry +findcodecs +cwcx +findmediafiles +susan +www.dashboard +insomnia +hotnewcodecs +ocadmin +cfd +bestnewcodecs +coder +porter +superdigitaldownloads +sep +getfreecodecs +blood +bestmediacodecs +supermediadm +downloadmediadownloads +theone +kpi +netman +epic +searchmediafile +fastmediadownloads +prospect +matilda +ronaldo +enjoynewcodecs +love1 +myfreecodecs +esxi03 +shire +esxi02 +esxi01 +old-www +geronimo +configurator +downloadprodm +www.zoo +getnewcodecs +pepito +of +fo +wms1 +newmediamanager +island +mensa +challenger +www.ds +www.stiri +findmymediafilesinc +centre +chaplin +onlyone +malcolm +thedigitalcodecs +easyprodownloads +cra +members3 +members1 +lookatme +mailbackup +test07 +getcodecs +downloadmediacodecs +copper +group4 +ginza +dsf +concurso +bright +irc2 +delhi +ground +sdp +raspberry +newmediadm +legendary +why +karina +ganesha +liveprodm +enjoydigitalcodecs +d10 +trevor +tri +bestmediafile +czat +bestcodecs +azrael +twinkle +josh +lvs1 +laos +downloaddigitalmanager +spo +thefreecodecs +www.pos +autodiscover.stage +mta01-40-auultimo +mta02-60-auultimo +mta01-bpo-80-auultimo +mta02-bpo-80-auultimo +mta01-50-auultimo +adriana +mta02-70-auultimo +dcm +mta01-bpo-10-auultimo +nts +ip5 +mta02-bpo-10-auultimo +billing1 +mta01-bpo-90-auultimo +mta02-bpo-90-auultimo +mta01-60-auultimo +mta02-80-auultimo +mta01-bpo-20-auultimo +mta02-bpo-20-auultimo +mta01-70-auultimo +mta02-90-auultimo +autoconfig.stage +fastdigitalmanager +citroen +popcorn +mta01-bpo-30-auultimo +crm1 +memberold +mta02-bpo-30-auultimo +mta02-20-auultimo +mta01-80-auultimo +mta01-bpo-40-auultimo +intro +iq +mta02-bpo-40-auultimo +mta01-10-auultimo +mta02-30-auultimo +rk +mta01-90-auultimo +mta02-10-auultimo +mta01-bpo-50-auultimo +mta02-bpo-50-auultimo +vmc +mobileapps +www41 +mta01-20-auultimo +mta02-40-auultimo +freeman +nox +mta01-bpo-60-auultimo +mta02-bpo-60-auultimo +adsense +studios +mta01-30-auultimo +mta02-50-auultimo +mta01-bpo-70-auultimo +mta02-bpo-70-auultimo +net2 +ankiety +baran +kami +kutuphane +kk +acl +kmc +smarty +m.m +s63 +sh3 +analysis +asi +capital +hrd +heracles +webcalendar +infra +mks +i75 +servizi +supra +i74 +zixvpm +asetus1 +i72 +asetus3 +mail.85st +smtp-in +asetus2 +btc +mail.shop +marconi +mrtg3 +fleet +montreal +sm2 +xyy +www.christian +esa +ctp +z3950 +db8 +www.vhs +bscw +jive +scope +cri +szkolenia +85st +aya +smtpout2 +organic +bdc +w0 +minnesota +rita +illinois +mada +louisiana +ito +mail.eyny +eyny +ses +cloud3 +gs1 +mie +albatros +dieta +cisl-plaisir.cit +exams +albatross +www.prod +aims +qaweb1 +qaweb2 +www.atlanta +img10 +hx +ns100 +logout +tbs +sif +arthouse +p7 +vid +www.pa +unifi +wtf +geoportal +blade4 +monarch +smithers +dakota +gladiator +place +krakow +crm3 +recipes +adi +ho +aka +ispace +abyss +archivo +ns95 +wina +henri +trixbox +inv +athletics +edo +cobbler +newdemo +morningstar +43 +sava +ulysse +ns73 +ns74 +autodiscover.server +autoconfig.server +ns77 +monet +webdisk.site +alchemy +baobao +his +dns22 +nida +moms +120 +nu +ming +dns21 +terre +monitor3 +quark +wcp +mtv +ns-1 +ns-3 +indus +soho +ucenter +kdc +www.www1 +www.main +i14 +alexandre +i12 +iwww +stable +i11 +min +display +webdisk.helpdesk +ebuy +vendors +vmware +tick +ges +tsa +floyd +madonna +replay +mail46 +saa +entrepreneur +mail43 +s38 +aero +aslan +byte +gerald +www.webstore +ftp12 +whoson +roa +web31 +giving +mail08 +spamd +vconf +axel +news01 +gems +snmp +sweden +tsc +acdc +test20 +aroma +www.minecraft +deborah +bronze +web101 +domain3 +insite +shoptest +sec1 +login1 +rochester +hf +sight +www.openx +apitest +www.trk +dispatch +downloader +supply +mj +secure5 +65 +pythagoras +jr +soulmate +dump +hao +ns.forum +www.myspace +opencart +resolve +car40.eng +www.4 +hound +peggy +www27 +www.3 +webdisk.v2 +reborn +netra +quasar +zipcode +moria +akashi +eoffice +iportal +rescue +mail34 +stream4 +hamza +seal +btp +surya +ik +tui +achilles +ibis +bazar +www.w +instant +imperia +easter +imagehost +boleto +office1 +galerie +ricardo +complaints +lark +www.manual +87 +cc2 +exchange01 +avg +osprey +backup01 +daa +serg +89 +bor +91 +www.fotografia +diesel +lynch +kestrel +www.ekaterinburg +netbackup +rafael +webdev1 +tunisie +xvideos +71 +fuck +lens +dominus +anakin +vhs +iw +mywebsite +ukraina +pyatigorsk +58 +remoto +ssl-vpn +56 +screenshot +worldmusic +test18 +domaincontrol +test16 +55 +www.16 +aras +giovanni +webdisk.development +cca +hussein +www001 +cdi +rancid +filetransfer +andi +autodiscover.reseller +logistic +gib +beatles +webdisk.sports +snapshot +autoconfig.business +autodiscover.sports +autoconfig.reseller +hyouon32 +val +autodiscover.business +autoconfig.sports +sdf +webdisk.business +#www +webdisk.fb +shp +winfm +gorod +vserver +gss +auriga +mrb +giant +nix +muonline +webserver1 +kunden +www.ti +ns99 +spec +jen +hale +designfd +aldan +sip3 +test.m +webdisk.hosting +newcom +monmon +freeweb +crm-dev +backbone +salad +www.tester +trc +newport +collaborate +asp2 +davis +yang +captain +tintuc +ns103 +ns104 +sd1 +pmp +artefact +kss +ns123 +www.accounts +stingray +wwa +ns121 +tweb +www.sip +tees +www.energy +origin.fhg3 +secureauth +networld +cxzy +erasmus +ottawa +username +origin.fhg +origin.fhg2 +maxx +acrux +emoney +www61 +web36 +pusher +nsm +iloveyou +takeoff +pnd +wwwt +zeropia +magnus +mud +autodiscover.us +autoconfig.us +psm +corvus +volans +firme +aris +webdisk.wholesale +im3 +msx +mail.tw +kom +builder.hosting +essen +zulu +www.ak +teknik +www-spd +bbc +cam4 +bap +bay +webdisk.online +www.nieruchomosci +aoa +bhm +poems +tcl +annonces +moj +www.bg +gtm +dct +s4357 +bibliotheque +www.finanse +www.prezenty +eie +ksi +vu +dnc +ego +cpp +bugtrack +avl +aso +porthos +z1 +paginasamarillas +h14 +204 +handmade +charts +h12 +afs +s37 +sa2 +kanri +costa +hebe +ssotest +server45 +msi +server42 +milo +web32 +clic +stargazer +pm1 +web35 +leia +www.mms +omg +ooo +zhaosheng +nagi +baki +charger +seer +www.arm +stan +m.staging +teleworker +gis2 +run +tux +flickr +vin +fds +kane +aquarium +psn +www.redirect +love2 +aramis +jweb +pmx +convention +vdc +pele +bangkok +www.voip +www.profiles +webdisk.clients +sentinelle +hartford +rwxy +i19 +edu1 +c-asa5550-v03-03.rz +sita +osi +c-asa5580-v03-02.rz +autoconfig.director +massive +autodiscover.director +www.biotech +lenny +ovh +galactica +idata +tesco +elma +mayak +esse +massachusetts +edmonton +sv01 +milton +hapi +hats +naples +ori +virgil +inmobiliarias +midwest +slice +part +belarus +mysql11 +mysql9 +classificados +brahms +mailb +purchasing +channels +i16 +etech +vod1 +transparencia +pdi +i20 +murakami +windowsts +hagrid +juice +hosts +estate +mxout +bordeaux +mico +celular +fotki +audrey +i23 +marx +terminalservices +petrozavodsk +plone +www.fl +nauka +continuum +i25 +i26 +i27 +nomad +b8 +b9 +eservice +i28 +webdisk.movil +bsf +parked +correu +joom +quick +poligon +entest +serv3 +mailhost2 +safein +asus +res1 +redbox +karate +gzc +mom +mitchell +loyalty +gea +sapi +javier +park2 +park1 +news3 +s234 +s229 +s228 +mail-1 +shampoo +rss2 +courier +asterisk2 +zarzadzanie +savenow +toulouse +s235 +s231 +emall +s232 +staging1 +nagasaki +prosper +rideofthemonth +hideki +imedia +www.groups +plastics +wetter +bin +aos-creative +cs02 +trailer +mops +investigacion +ankieta +livestreamfiold.videocdn +angus +uss +sunday +startup +yuva +windows2008r2 +mid +sharp +webdisk.i +simix +radios +sct +ontime +www.sh +devmail +www.tyumen +www.10 +www.ml +103 +scrap +mailex +www-uat +ekonomi +aster +bouncer +fms1 +isg +wms2 +www.mkt +league +srv15 +www.comics +dbserver +musicman +hosting01 +off +sparrow +srv14 +abhi +www.fenix +router-h +strawberry +swordfish +windows7 +lims +frozen +www.2013 +ys +superhero +risk +kansascity +louisville +flint +www.v1 +joanna +epayment +jesus +hep +carme +gewinnspiel +saturne +gum +gerard +crypton +110 +rsvp +ans +realestate2 +autoconfig.archive +ldap02 +autodiscover.archive +vs3 +secureftp +clothing +sql5 +www.ebooks +bull +www.group +rocks +seoul +faxserver +heineken +ams2 +webauth +philippe +mailboxes +www.russia +agile +facturacion +kimchi +www.japan +iran +bck +selena +incoming +scout +tsm +nigeria +marble +bom +yara +ns1.ha +ns2.ha +autoconfig.it +www.florida +galatea +roku +vip7 +federation +vaio +bazaar +www.mu +mailserv +marry +sigrh +wizzard +cls +www.ae +topic +www.by +www.hi +www.eventos +webdisk.fr +ipv4.forum +www.ka +xm +webdisk.cn +marks +s64 +chromakey +s57 +s56 +poc +hun +bds +www.sql +newunse +www.ok +hammerfest-gsw +verona +underworld +seti +landscape +trek +certification +hemera +xw +massage +www.space +ic-asa5520-vpn-fw +cstest +autoconfig.link +404 +autodiscover.link +toast +www.15 +gwia +hector +religion +www.sk +pc-cat4900-gw +kj +www.mailer +yl +piranha +asap +token +ktv +granada +iws +truck +www.tt +ebisu +ssss +ul-asa5520-vpn-fw +www.europa +autodiscover.seo +arun +reload +higgs +autoconfig.seo +cgp +windmill +wotan +rmail +habarovsk +promote +bass +www.zakaz +www.msf +ubs +elektro +vixen +nsq +path +www.hawaii +sylvester +bbq +noor +laptops +cottage +lighting +rina +bang +nona +duck +c11 +travis +protect +nowhere +akatsuki +mura +rac2 +nimble +kosmos +crmtest +ktc +json +magix +sponsor +freelance +vip5 +cci +lbs +sro +kaitori +mail31 +winston +skc +socket +shi +sei +top1 +bono +webmarketing +toad +sole +fanclub +cos +pipeline +ima +www.fm +copy +karin +techweb +didi +host9 +b19 +mci +reda +agriculture +doit +ip6 +demo20 +prosfores +jpn +vkontakte +fake +miguel +boxoffice +dung +dbd +aplikasi +process +sunil +scp +irene +noir +hanoi +gigi +yusuf +autoconfig.newsletter +shakira +autodiscover.newsletter +www.pms +edc +www.plan +serve +125 +jean +temp1 +filer1 +pcgames +metc +assistenza +make +zcc +vbulletin +change +ibanking +backup4 +vps102 +submitimages +awesome +presto +miel +www.ea +nada +marcel +imvu +tn +manado +svs +incom +www.linux +www.base +string +maurice +oil +oscommerce +vivian +lynn +gundam +goodtimes +123456 +sword +escape +placement +nuri +kumar +working +xml-gw-host +glow +turner +ginger +nuovo +brad +shaggy +yesterday +rrhh +dedi +salt +www.management +vip8 +sand +uhspo +scom +iris2 +masa +dada +www.eco +hms +nataly +smart1 +inb +websvn +personals +sola +twist +suri +punto +ting +wonderland +akari +vh +genetics +prophet +www.invest +www.master +www.ksp +mailsv +www.journals +marcos +polit +www.event +srv24 +ingenieria +xsh +grey +bogota +retete +informa +dracula +a6 +o1.mail +www.bc +mateo +mylove +provision +dominios +tvonline +ccp +vir +xpress +mgr +murat +demo16 +demo18 +pocket +bulten +www.crimea +bumblebee +abcde +hanna +lb3 +cynthia +snowflake +nap +se2 +ibc +bulksms +beeline +wa1 +www.power +www.rnd +www.ma +cathy +master2 +jeremy +lsrp +anything +ps3 +www.luna +honolulu +filter1 +ege +hellokitty +acad +swiss +eschool +ari +mio +slf +ira +raman +mammoth +ons +jsp +eroom +smiles +mail.de +ramon +jessica +checkpoint +dawn +s153 +lvs +host13 +kerio +pin +agnes +globo +garage +box2 +take +star4 +sparkle +s190 +rdm +spotlight +s176 +cedric +nut +testaccount +budapest +frame +unico +tamara +fas +ignite +zodiac +stuart +kasper +webapps1 +start2 +ced +cpn +stp +newyear +beach +varuna +leap +tigers +hotmail +web09 +imaginary +connections +ss4 +mein +ehsan +mssql5 +sayac +cbf4 +goddess +mailcheck +scotty +referat +cecilia +baco +atelier +online1 +www.turismo +weaver +cbf5 +marian +s223 +exchange1 +dorado +iserver +barracuda1 +prada +streaming2 +gisweb +psd +daffy +musicbox +ralph +acid +roland +a7 +shelly +pikachu +mailstore +tecnologia +advice +s186 +s167 +astrology +lm1 +cocoa +secrets +miranda +webstar +alone +awc +younes +crawler +adele +shamrock +primavera +ned +webdisk.login +autoconfig.login +autodiscover.login +cp5 +spiral +syktyvkar +fuzzy +jessie +i90 +makemoney +telnet +danger +dollars +proc +as3 +aaaaa +industrial +www.network +webdisk.portfolio +serv83 +overflow +www.kirov +boron +birds +behzad +faces +webdisk.live +tamer +www-2 +lilac +devcms +warranty +mcq-indus-01.iutnb +supreme +hangman +cancel +mcq-projet-01.iutnb +srv0 +slash +child +geoweb +nowa +conrad +webdisk.free +indesign +86 +sbe +curtis +myforum +infra1 +univ +web4004 +rune +81 +duplo +mail35 +wam +skins +himalaya +beatrice +krs +finland +harrier +fw02 +perfect +goose +genealogy +erik +marriage +heimdall +autocad +pony +stranger +hilda +advisor +75 +win15 +kaizen +ns150 +regulus +adler +zakaria +pay2 +www.reports +cpanel1 +74 +patricia +i80 +www28 +www.kemerovo +www.stud +highway +securelogin +www.l +insane +inti +68 +www.barnaul +tomtom +jedi +speech +filebox +rdns3 +noda +integra +elan +kingkong +akash +www.irkutsk +62 +seeker +keyword +log1 +vtc +www.report +agape +mara +responsive +wan +ipv4.demo +bca +test23 +96 +moments +amp +www.12 +www.travian +escrow +nights +www.13 +dev10 +po2 +ssh1 +www.man +w11 +webdisk.upload +www.20 +surat +toro +go2 +wassup +pleiades +conan +alef +caravan +amjad +smtp.mail +www.smolensk +canopus +www.9 +tsgateway +colt +02 +cpt +www.mama +redsun +dac +fdm +cdn5 +myip +www.stavropol +puertorico +isc +zhaopin +www.esp +turizm +eticket +assets4 +thewall +adserver2 +img05 +autodiscover.main +win20 +doi +www.developer +portale +khorshid +counters +prs +psc +romans +222 +oneclick +cheap +img165 +neuron +trigger +secure10 +nobel +dakar +www.dvd +secure11 +www.demo3 +savebig +taka +sdns +rhythm +adagio +www.property +noproxy +mrp +sou +paygate +sailor +#mail +billpay +itp +volta +hris +sinope +doodle +drc +xchange +shield +rdns +hubble +predator +www.url +turan +murphy +voting +boletim +collins +progamers +ns97 +ns96 +host03 +pct +watt +orinoco +sa1 +secureweb +pharos +nota +picnic +eduardo +congo +ns78 +ns76 +aquamarine +www44 +sot +padma +mosaic +hw +x4 +rocker +fathi +converter +derek +fullmoon +rns2 +persia +t5 +murray +vps104 +bsm +util01 +barbados +essence +main2 +pcworld +tis +mailsvr +kirakira +amigo +ns79 +smash +cassiopeia +fairytale +ns105 +jnp +www.puzzle +mulberry +solusvm +bfm +ns117 +mclaren +mx13 +ns122 +technet +demo19 +local.api +www.account +smtp-out-02 +sonet +vol +jinx +damian +eminem +photobook +www.wd +vps107 +atlant +hamid +bambino +bismarck +secdns +fcs +www.piwik +kkkk +jackpot +excelsior +tootoo +alani +spi +vids +amal +newhost +pingpong +mail-in +origin.www +mister +www.deals +korean +dinosaur +kristine +ccl +empty +shining +ww9 +rays +autoconfig.links +autodiscover.wholesale +www.vc +clay +sch +nagoya +minmin +phs +www.shadow +yuan +pmc +autodiscover.links +smiley +rews +olsztyn +mot +bioinfo +osm +macho +mime +glamour +otter +kx +imran +autoconfig.wholesale +www.mailing +85cc +www.prestige +profi +avril +mail.fc2 +bb1 +corner +legends +bongda +sobek +asta +prep +sogox +8591 +alcatraz +waffle +idiots +mail.8591 +mail.77p2p +spawn +valerie +ass +emailer +filmy +cho +bsa +www.irk +vip4 +cristian +aj +access1 +council +den +elysium +gsf +www.msn +vcon +drp +emg +cme +gcm +www.firmy +workstation +5278 +gadgets +mail.5278 +tapety +holocaust +mail.sogox +mobiles +jorge +m18 +object +apps1 +mediasite +spectro +lister +os2 +hcp +228 +gos +s46 +amar +www.template +cet +kor +q10 +dsadmin +mocha +kiran +lps +blago +marin +sparc +host02 +bellatrix +curiosity +mail.85cc +biuro +oursogo +mail.oursogo +mahdi +femdom +merida +language +alto +www.delivery +kopia +malik +dave1 +eburg +web52 +web51 +server47 +senior +server46 +mvp +clan +domaincontrolpanel +referral +www.develop +halloween +fee +medea +robotics +mehdi +server32 +mssql01 +jiuye +server26 +trauma +desert +tu +harrison +box11 +miass +darklord +77p2p +soluciones +osd +slk +av8d +dentistry +mx-2 +monroe +mail.av8d +simorgh +www.telecom +kerala +wuhan +moody +erc +santander +sharefile +niobe +aca +orangecounty +m01 +pta +mail41 +vc3 +www.7 +eleanor +hvac +assassin +sacs +mex +tales +webdisk.go +autodiscover.exchange +mail44 +mail.news +mail45 +urania +www.photography +hamlet +freebox +bianca +hadron +vcd +blake +sync2 +pdu2 +konvict +lobo +fw3 +smtp-gw +nhac1 +mail.corp +as4 +omicron +www.black +ngo +www.autos +trends +tweety +kinder +ttl +celeste +pitbull +zxc +exchange2 +groovy +www.bridge +oglasi +desa +zara +oplata +www.ece +srv22 +blizzard +iti +ems1 +wintermute +groove +srv23 +pearson +www.ebook +109 +rtc +handbook +vitrin +ws02 +cdm +adv2 +bugatti +www.int +www.rec +www.tk +postal +chou +montgomery +priem +bailey +www.tender +fileupload +bestseller +dongwon +comet2 +leviathan +poze +mac1 +ebusiness +www.vitrin +concursos +merry +129 +cso +nsp +www.profile +mowgli +chewie +alla +annapolis +preston +nos +ets +kv +leasing +test007 +apricot +ykt +flog +slx +algerie +indicadores +excellence +leslie +fresno +freeworld +moby +gestalt +webdisk.jocuri +zakupki +edu3 +marion +thalassa +autoconfig.jocuri +jefferson +tp1 +romi +mpe +stuttgart +www.fotos +timmy +takaki +www.bug +mnemosyne +artist +matador +autodiscover.jocuri +desi +encrypt +bulkemail +i13 +association +esupport +algeria +www.elite +kennedy +yedek +tires +remo +www.motoryzacja +motoryzacja +mystore +aula +pakistan +socialwork +ihome +dept +raid +deepblue +reserved +www.dream +caracas +tsp +wvpn +triplex +jobsearch +sushi +ontario +www.losangeles +szb +shoutcast +mga +fart +vito +vmtest +squeeze +experiment +tal +mos +blocked +newsfeed +lc2 +webdisk.newsite +darling +imageserver +vpn4 +creme +esmeralda +american +pstest +tabletennis +www.virtual +host23 +www.counter +mb2 +yar +vrn +www-demo +pc11 +global2 +www.destek +bysj +quarantine +www.ga +chandler +evp +reed +a0 +www.dreams +helpdesk2 +titans +dq +termin +mota +kamel +newtech +128 +tttt +red5 +inews +manchester +e10 +cyberspace +saigon +www.users +srv03 +www.bali +lojas +filosofia +operations +www.program +regional +authors +malibu +ghosthunter +pacman +ladolcevita +www.style +www.andy +www.mr +electron +www.cert +webmail10 +babbage +giga +g6 +pmm +dixie +bea +stamp +nmail +kage +trials +eforms +content6 +markov +sw3 +watches +snowy +marvel +content7 +maru +woodstock +mano +para +markus +mako +srv16 +srv17 +srv18 +csweb +www.chinese +www.casino +mail42 +mail47 +levi +www.arabic +student4 +www.prestashop +lana +domini +kamikaze +openmeetings +ftpadmin +christianity +iep +emc +labrador +poly +xuebao +redline +tiamat +aq +boc +silk +infos +xweb +msite +thehub +rainbow2 +bola +andreas +jane +www.ny +webdisk.novo +bsh +item +francis +georges +rainbow3 +schulen +katy +hoster +garuda +fsm +datasync +greatdeal +i33 +gamezone +lawyer +marcelo +sl1 +consult +hoge +ellie +hlj +pussy +jersey +violette +kagoshima +generator +webdisk.book +sal +databases +questions +www.mantis +zeus1 +web10656 +centurion +nika +six +primer +roche +barra +www.scripts +merkur +www.spa +radyo +alvis +coa +cch +ilo +clear +www.mark +bells +www.mars +wikis +autoconfig.i +thot +freeze +sahil +quattro +www.klient +live1 +rtg +tinker +autodiscover.i +pera +mirror3 +www.z +bc1 +eon +www.poznan +communities +mfc +rem +www.hp +simg +d22 +saber +planck +smr +joey +salesforce +wired +kernel +qatar +itsm +dima +h8 +unlimited +www.nice +vodka +sud +testlink +www.dental +threads +inca +null +nate +blade9 +lifeline +paf +105 +saman +b18 +b17 +hachi +stick +mta3 +prashant +pavo +server51 +coc +sonny +apus +ibiza +www.wallpapers +lukas +thera +fmipa +blade10 +zxcv +xf +ym +zd +oklahoma +espana +kool +baba +errors +sable +www.victoria +tokens +igate +webdisk.web +www.mercedes +instyle +aion +warcraft +crawl +mrc +orion1 +emu +cisl-gijon.cit +opros +teamwork +ppt +kang +score +integral +stealth +lo +supervisor +tempus +c13 +vmware2 +bubbles +chiba +sorbete +mido +webdisk.docs +porky +autodiscover.health +sha +ivory +true +lfs +vtour +aha +host21 +nato +wild +christopher +papaya +ohio +permits +cct +heroes +autoconfig.health +untitled +xnet +xian +www.rsc +www.alfa +bilet +jas +118 +paragon +bem +mordor +fe1 +hdr +www.tc +customersupport +million +ipcam +www.hub +ns.demo +jms +pro2 +s76 +lts +www.donate +ddi +colorful +olymp +www.about +i61 +longevity +appstore +sra +tortuga +www.vn +sigadmin +juliet +aml +si1d +rates +s55 +www.lady +pres +kos +whale +mal +ew +fender +autodiscover.live +solus +testvpn +caos +autoconfig.live +www.gg +fans +noob +autodiscover.cn +tdc +wheat +agua +pineapple +silva +anne +webdisk.de +nour +conferencia +s68 +antiques +www.me +revista +cs4 +shepherd +ramazan +zena +s54 +lsh +dnp +ctc +taha +doku +mail.in +sm3 +boole +sssss +oceanus +proactive +zakon +kobayashi +www.photogallery +hook +srd +stor1 +arhiva +preproduccion +omid +wp3 +sse +webmail01 +spiker +www.no +avalanche +adds +crl2 +playboy +contador +sela +blessing +mijn +luther +stephen +gj +ananke +komi +www.auction +dionysos +www.king +trix +tomate +www.center +luck +arif +almighty +climate +vz2 +www.bio +documentos +techblog +zuzu +www.invoice +leader +chevy +dune +www.linkedin +www.shared +biyou +coach +literature +amazone +terranova +arion +loulou +typhoon +supersite +gtm2 +webmail.test +nets +funny +vienna +bf +s1103 +rick +forza +sergio +cdn103 +interno +sdi +i35 +neumann +www.va +moca +i67 +nepal +dme +purgatory +axiom +invision +sylar +rape +rams +reference +cdn102 +said +i69 +tino +medi +www.et +oh +dauphin +webdisk.books +www.galaxy +techinfo +i71 +lyncwebconf +minnie +ectest +www.newsletters +i66 +tomy +i73 +kst +race +fiction +sala +sbb +exec +jester +fix +www.nissan +nishi +hummer +matrix2 +limelight +http2 +warp +sunlight +kar +mapy +distributor +scratchy +xk +lola +cashing +pgp +sirena +b30 +racks +wash +www.ko +taganrog +gpm +www.mb +cha +omer +valhalla +merci +vz +cache3 +sucre +waptest +sexo +kani +iad +i76 +scr +breezy +appcgi +dangan +ws11 +www.pk +www.pg +i65 +tennessee +www.ns +foot +lams +csng.ok +www.auctions +i64 +training2 +abf +obninsk +cjxy +i63 +pcsupport.bnsc +pcb +aussie +buddy +web6400 +i77 +s67 +d0 +linkin +star9 +translation +www.gamers +flseok +sota +gpu +candle +autoconfig.de +bae +www.img1 +www.ig +lic +elegant +mymoney +s73 +www.fh +chrysler +hime +myname +autodiscover.de +gu +s77 +bigtits +autoconfig.cn +ilab +claude +i78 +s78 +children +streaming1 +i79 +beryl +cosmetic +s06 +yummy +guitar +vpngw +lcgbdii.gridpp +impuls +i62 +baghdad +ssrs +backpack +fenrir +kurs +myapps +mkc +derecho +anil +games2 +119 +teamo +hts +mail.newsletter +lpta001.itd +lpta009.itd +ad3 +i81 +zurich +trex +peso +secure6 +cambodia +gw.nd +gmt +irm +magnet +ras2 +vpn02 +gw.ag +scholarships +estrella +ataman +hoanganh +cf165.conf +mailhub.kis +www.hiphop +rtr-xa.ohx +aldo +lastminute +niki +shadows +catalogs +shuzai +caca +net-xa.ohx +monavie +ulises +kaspersky +cf195.conf +western +bcst-xa.ohx +cf175.conf +emprego +i82 +yutaka +cf185.conf +reddot +xing +cf155.conf +regis +rtr-oa.ohx +wat +usub +cmi +wartung +net-oa.ohx +sur +lfc-atlas.gridpp +bcst-oa.ohx +sever +marcom +mon2 +static0 +mock +bmb +autodiscover.x +maillists +autoconfig.x +mody +i60 +macos +veeam +brett +brazzers +main1 +i58 +mbs +cda +esi +panfs2-nfs.esc +lila +pptp01 +e-mail +i57 +northstar +mail29 +racer +endeavour +www.fs +webdisk.articles +camper +exercise +gost +intrepid +zaki +theworld +i83 +www.france +www.israel +handball +stephane +hanson +plug +pmd +adsl2 +i56 +sweets +pivot +i55 +asp-winterville.cit +mail28 +www.crazy +autodiscover.soporte +rcc +dragonnew +c64 +blacksun +solaria +www.hardware +karo +continental +boon +mce +luxor +kawaii +ultra2 +tan +compunet +caro +maxime +internetr-all +m.beta +hcm.ipad +ei +i54 +z-diemthi +rac +bomber +reiki +img.e +ciao +i53 +ddns +changes +smog +mask +dmca +service1 +www.plgto.edu +pepe +alis +bk2 +hindi +bk3 +i84 +medicina +alibaba +ntc +www.easy +gymnastics +hq2 +kaka +i52 +sdr +ork +zf +natura +racine +quake +i85 +xmen +entertain +cocoon +bubu +www.pets +prensa +stat4 +www.server2 +hst +lexi +www.resellers +mailhub2 +garnet +pain +belka +morena +coca +rz +un121101225938 +sagittarius +b33 +pci +start.ru +abcdef +plgto.edu +i51 +b32 +b31 +ich +myway +wts +macedonia +m-dev +www.m2 +i86 +citibank +explore +junk +cupcake +pixie +test.shop +212 +abdo +b26 +automail +ewa +hunters +b20 +imagini +121 +palermo +frink +b23 +smtp-in-01.mx-fs2 +frida +i50 +nstest +replica +hj +arctest +thulium +b21 +i48 +smtp-in-03.mx-fs2 +investment +i47 +i46 +compton +smtp-in-02.mx-fs2 +unused.aa2 +home.stage +g5 +cheyenne +hostmaster +webdisk.soporte +g4 +callme +i87 +robby +www.lider +vds4 +i88 +i45 +zahir +cp4 +i44 +dojo +industry +bandung +tumen +admin5 +pba +hideip-europe +i89 +jaka +ora-placeholder-ps-db.srv +fab +clara +oks +cor +fish1 +bomba +formosa +mailweb +e6 +autoconfig.soporte +i43 +i42 +rad1 +www.v +natalia +digitalmedia +i41 +webserver01 +hermes2 +i40 +e4 +seas +nicole +webdisk.analytics +hackers +jungle +i91 +www.holiday +iva +i38 +complete +node01 +i92 +fifa +healing +i37 +abiturient +gato +sh7 +jv +creator +sote +infiniti +nit +smsc +mstudio +four +appli +i93 +secure7 +i36 +ivy +valeria +smtp16 +rohit +konto +www.fz +liberte +pti +video4 +protech +vss +kygl +mp4 +most +horoscop +www34 +hino +im4 +opinion +ipo +pingu +www.door +stellar +cro +www.horoscop +i94 +reunion +xion +i39 +atropos +training1 +i30 +yw +innova +tatooine +dr-www +austria +webdisk.bugs +i34 +hood +shop3 +www.dr +chester +hora +i96 +kenobi +www.xy +radius4 +datenschutz +i97 +vod4 +canopy +dop +host20 +i98 +static-mal-g-in-g01-s +sisko +eol +wahlen +i99 +126 +issa +pig +auth3 +leela +tva +uk1 +mta01 +alp +onion +dle +mlp +liza +khan +mxmail +i32 +mobileiron +lian +minos +www.worker +tsb +mld +www.build +kick +rtx +vds3 +sammy +vet +chatting +maplestory +i31 +play1 +maki +reyes +skala +mail49 +addicted +autodiscover.movil +mailgateway3 +closed +i29 +doctors +autoconfig.movil +calgary +pdb2 +mach +silverstar +old3 +michiko +bkm +kl +arctic +utils +vulcano +madi +test-m +d28 +d27 +horses +registry +win22 +www.code +ishop +www.tenders +www.dd +horror +classico +autodiscover.dev2 +cmsdev +bbss +wide +prima +autoconfig.dev2 +carpenter +mymusic +jelly +moga +www.gd +oswald +whiteboard +smallbusiness +www.mp +cfs +rcm +www.mv +ben10 +www.conf +neuro +verwaltung +www.cabinet +www.ng +tw.blog +www.rr +iman +olympia +s241 +dolce +loko +auc +m.pool +partner1 +www.spanish +i22 +moka +dev.admin +www.rus +cdn01 +mws +malaga +mono +www.firme +mineral +i21 +dipsy +xd +thebe +www-hold +winxp +www.payment +i18 +i17 +porto +www.hf +homeless +well +guangzhou +www.sis +small +www-stg +canal +www.cats +sab +i15 +tsi +discus +hay +slides +starlife +trader +o1.sendgrid +ipphone +hungary +topstar +3w +cairo +www.dns +ts.kmf +nuts +oper +sitemanager +jang +vpn5 +dionysus +asdzxc +gss1 +tetris +incubator +oren +newhaven +cuda2 +pty13213b +www.date +estonia +mrtg1 +wroclaw +greenapple +thumbs.origin +domination +c2c +creation +kawagoe +myhost +pong +vts +faperta +ts.fef +lebanon +grapher +000 +vestnik +lip +myrtle +ts.ydyo +psbfarm +newww +romantic +syria +pergamum +tpi +dede +edms +catfish +www.o +egcdn +www.author +discuz +106 +www.hobby +statystyki +contrib +research1 +charleston +katowice +animals +seraph +darknight +nasty +sari +chronicle +stats1 +vz1 +gibbs +doll +via +11091521400593 +observatorio +spice +sokol +emails +www.act +khalid +tucker +match +counterstrike +testing1 +webdisk.director +www.print +fatih +odie +rush +epro +belize +samer +ripe +c-asa5550-v03-01.rz +ortho +c-asa5550-v03-02.rz +barrie +ikaros +imobiliare +concurs +cypress +cgs +ashley +shu +ticker +teleservices +lover +sitelife +lhr +www.max +ffl +gooogle +www.lms +listsrv +www.city +www.discovery +ncp +wiwi +doc2 +omi +guestbook +ke +justme +town +comet1 +ignition +phim +testonly +ryder +fobos +lobby +yahya +eucalyptus +bmc +fps +vivo +230 +www.ops +abba +ringtones +webd +webmail-original +mailrelay2 +isv +srv19 +www.storm +ultima +naughty +webproxy +priya +app8 +www.vladimir +yoko +tinkerbell +southpark +julius +reach +peridot +www.pre +www.private +yachts +beehive +ati +viejo +hanybal +hamm +hn.nhac +www.mirror +bangbang +asha +eragon +probe +mysql03 +emmanuel +it1 +luka +chillax +kanto +ipmonitor +webworld +www.evolution +chetan +sun2 +oauth +web100004 +tmb +api.dev +vh2 +sph +dmm +sod +hsi +oficina +marcin +toni +gilda +offcampus +nightmare +something +cas3 +redbull +vtb +i49 +tam +tbc +axa +coyote +avm +eventum +albion +nanda +fivestar +qwe +ip3 +www.ava +rev +234 +pinger +resort +bdog +pcc +testpage +wombat +aplicativos +audition +www.lan +nhce +webplus +wyx +www.cdn2 +blackandwhite +ericsson +webdisk.home +mikey +arirang +mst3k +republic +pf1 +www.alaska +eiger +websearch +vegetarian +localhost.blog +input +www.forum2 +avcome +dat154 +www.sim +front3 +pbs +lords +siva +odc +reverseproxy +dys +tomahawk +se1 +server43 +marko +nrg +pooky +marek +chilli +testuser +miko +080 +lys +mobile-test +kaito +nine +shelter +hoteles +mail.xvdieos +mail.avcome +praha +albany +pss +advertisers +host04 +host05 +cotton +p8 +mysqltest +webdisk.webdesign +cbr +herakles +i59 +h10 +serendipity +autoconfig.my +s39 +ftp.shop +foxy +233 +www.moscow +lv121101224239 +emilia +h11 +autodiscover.my +s59 +jcc +dev.shop +s62 +piglet +damdam +h13 +tigger +duncan +s44 +h15 +madagascar +sportal +hank +placeholder +170 +www.k +ing +amt +beth +iii +judith +www.notebook +server35 +rdweb +cacti1 +joshi +videoserver +xvdieos +hey +genie +www.nano +alt.relay +newmedia +kamil +yugioh +digisys +martialarts +www.adrian +mailing2 +sfr +sm01 +m20 +www.wroclaw +oddbanner +webgis +s4242 +proposal +smpp2 +wwwadmin +sss2 +dio +cul +alvarez +hs1 +fen +eso +encoder +evm +artur +major +mcm +nils +sohbet +webdisk.marketing +mobile4 +malta +samar +aaaaaa +alborz +sks +centaurus +citadel +www.password +cdrom +mariana +da17 +isengard +angelina +http1 +ipp +mermaid +margaret +ama +img2081 +hawthorn +mkg +roxy +web002 +ki +www.mak +icpmupdate +enquetes +illuminati +host101 +bubble +approval +dfp +octans +www.uat +uruguay +eagles +distributors +mail50 +msl +trailers +aks +img142 +marianne +enjoylife +nsi +ears +boomer +omail +s157 +franco +mysmis +s159 +marine +micros +crafts +gogle +holy +snowball +www.fin +webpro +mx.mse4 +simply +kali +bogdan +mx.mse3 +jules +addons +appel +s173 +reality +newt +tatsumi +wrestling +sr3 +un121101224723 +www.gm +mx.mse5 +rsync1 +rsync2 +hansa +www.cde +ssi +sssttt +klara +sudoku +doraemon +s175 +mx.mse21 +www51 +bps +shino +img181 +miyabi +elk +mx.cs +bluerain +ver +www.cps +ward +photo1 +blacklabel +www.people +tandem +webhost2 +abc1 +mail.mse4 +ns119 +virgin +mail.mse3 +panzer +vds2 +www.cv +router11v06.zdv +wxdatasecure +sachin +wws +rack10u24 +imwxsecure +tutos +blade8 +creater +xmlsecure +ssl3 +marktwain +islamic +ocsweb +ns116 +rtp +ecshop +corp2 +cure +origin-api +isf +zina +asr +v6.staging +jang3572 +mail.mse21 +www.om +burn +www.wifi +clickme +floor +makalu +corvette +api.ext +bon +exchbhorl2 +www.updates +ns115 +ns114 +cploginky +www.tea +ns113 +haven +hilfe +ns109 +ns108 +test.secure +darkfire +ns107 +ns106 +vpn-uk +logging +api.int +yd +august +dulich +murdock +peanuts +autoplataforma +salman +deb +safa +umbriel +midian +cim +bru +design3 +cploginoh +pmi +vps11 +frankfurt +elaine +ssl4 +dem +pontus +jalal +macbeth +tet +supernatural +mmoem +westside +cp01int +linode +vps103 +blackpearl +proje +www.theme +queue +iceland +vivi +gdi +nixon +esxi04 +icpmdirectory +emr +yu +gif +redrose +gti +cucumber +smtp-ha +dogma +newlook +ns83 +moi +aspera +www.projekty +feri +novorossiysk +online2 +mail-mobile +sinsei +videochat +ns98 +outside +hoover +biblioteka +eddie +csf +freeland +harmonia +kas +www.aaa +camera3 +mustafa +paz +analog +mirkwood +geonetwork +diddy +mobile9 +ngw +sounds +ian +mgs +hawkingdialinrouterport1 +newwebmail +jigsaw +un +blade13 +cookbook +pal +www33 +sven +lapin +fargo +planb +elpaso +www.lol +ptn +smtp.mse21 +massmail +fresco +sooreh +fraise +sgp +nstri +ahwp +freeforall +servicenet +vicky +www.avia +hip +directories +kart +img06 +parto +stt +r0 +dingorio +peppermint +mon3 +gio +www.threads +itcenter +hermes1 +ycg +www.mir +turism +star3 +www.turism +www.pop +www.avto +bac +isee +celine +savings +conquest +myth +brave +s185 +rta +fptest +supporter +surgery +excalibur +service4 +mail.mse5 +www.19 +kenshin +innovate +adms +rhodes +webda +sting +www.cep +web156 +sug +pete +sangsang +ftp10 +nam +mxm +dingdong +www.25 +www.logo +web151 +93 +s200 +postgre +ftp14 +alvin +border-odd.nntp.priv +britneyspears +lancer +ftp15 +www.kiss +border-even.nntp.priv +ddns1 +salina +r7 +www.18 +www.17 +ub +recycle +test04 +attendance +smsgw +scholarship +cristina +abe +mobile.dev +joinus +intermapper +indi-web130 +nk +97 +contract +walk +coke +betaa +camera2 +hussain +mahachkala +www.gt +endeavor +esales +sb1 +freezone +ldaptest +dns-2 +blacky +internetmarketing +ecomm1 +59 +oneman +platypus +akita +ts17 +sagan +apu +suspended +qms +amigos +dharma +95 +tachibana +beautiful +barton +aladdin +finn +i68 +etna +vmscanus +vbox +gfstest +ange +hall +jerome +cdo +monitoring2 +hsbc +temp01 +cweb +i95 +www.president +72 +charmed +36 +blog3 +alina +www.clasificados +panasonic +csr31.eng +car21.net +bba +adnan +www.coupon +auk +cub +koyo +rizzo +escobar +www.murmansk +stamps +moha +more +cristi +flyers +sagar +anger +peek +novokuznetsk +secondary +pimp +mayur +www-org +digg +ite +asahi +www31 +backup02 +www32 +www36 +amaranth +www37 +striker +buddha +extension +faceboook +rise +compaq +intertest +holding +www.penza +murdoch +mypictures +www.montana +helloworld +canna +swati +www.tvonline +carlo +seba +ophelia +adserv +katrina +i70 +tobi +winupdate +freetime +www.translate +ptrmedia +tolyatti +goran +thesis +burns +reservas +blend +occ +blade11 +risingsun +web5516 +web3423 +web3424 +web3425 +mx06 +web18328 +oman +web3426 +web3427 +web18327 +web18770 +web3430 +web3422 +web3431 +infotec +web18326 +web18325 +web3421 +web3432 +web18324 +web3433 +web3434 +web18323 +web18322 +www.salon +web3435 +topgun +web3436 +web18321 +web44 +web3437 +web3438 +web3440 +web3420 +sco +web18319 +web3441 +ddp +web18318 +ddl +web3442 +web18317 +infra2 +web3443 +web18316 +web18315 +web88 +web18314 +web3444 +web90 +web3418 +web3445 +www.statystyki +chaitanya +rf +web18313 +web3446 +web3417 +web3447 +web3448 +web3416 +cdserver +web3450 +studyabroad +web3451 +greendog +web3452 +web3415 +tomas +web3453 +web3454 +web3414 +web18312 +web3455 +ccd +web3413 +name1 +web18311 +kyokushin +web18310 +br1 +web3412 +web3456 +web3411 +web18298 +web3457 +ws9 +web18297 +web3410 +web3458 +web18330 +web3461 +web3462 +web18331 +circe +web3463 +web3408 +web3464 +web3407 +silent +web3466 +web3467 +web18776 +web3406 +web3470 +web3405 +web3404 +supersonic +judy +web3403 +web3402 +web3471 +web3472 +web3473 +web3474 +web3401 +web3475 +web3476 +web3388 +cakes +web3477 +web3387 +73 +web3478 +92 +web3480 +web3386 +web3481 +web3482 +web3483 +mh2 +web3484 +web3485 +web3385 +hinata +web3486 +web3384 +web3383 +web18296 +web3487 +web3382 +casas +seema +web18779 +web18332 +advancement +web3500 +web18333 +essai +web3501 +web3380 +web3502 +web3503 +web3504 +web3378 +web18295 +allstars +web3505 +web18294 +web18293 +web3506 +web3507 +web3508 +web3510 +web3511 +web3512 +web3377 +web18292 +web3376 +web3513 +web3514 +web3515 +web3516 +web3517 +web3375 +web18783 +web3520 +web3521 +web18291 +web3522 +ria +web3523 +web3524 +web3374 +web3525 +web18334 +web3373 +web3372 +web3526 +greenfox +web3371 +web3527 +rms2 +web3370 +web3528 +web3531 +web3532 +web3368 +web18290 +web3533 +web3534 +web18335 +web3535 +web3536 +web18288 +clans +student5 +mgmg +pif +amon +web3537 +web3367 +web3538 +web3540 +web18287 +www.rd +web3541 +web110 +web3366 +web3542 +web18286 +isi +web3543 +chantal +web3544 +web3545 +web3546 +ident +web3547 +web3365 +web18285 +www.oasis +web18284 +web3548 +secure.dev +web111 +web18283 +web3550 +web3554 +web126 +web18790 +earn +keyboard +web18791 +www.oregon +crema +web18792 +cryo +www.prince +web18803 +web18804 +testonline +web18805 +web3364 +web18796 +muzyka +web3611 +faster +web3612 +web3613 +web3363 +web3614 +web18807 +web3362 +web3617 +web18282 +web3618 +web18281 +web18279 +web3361 +web18278 +web3620 +web18336 +web3360 +wiki.dev +web18759 +web3621 +web18808 +web18277 +forum5 +web3623 +web18276 +web18275 +web3624 +web3625 +web18274 +web18273 +web3626 +web18337 +web18809 +web3630 +metallica +ftp13 +web3631 +skunk +web3632 +web3633 +web3634 +web3635 +danube +kylie +alim +iceberg +web3636 +ariana +web3357 +cae +web3637 +tgn +flo +web18338 +web3638 +web3640 +zeppelin +web3641 +web18272 +web3642 +web3643 +atb +db9 +web3644 +web18271 +web3645 +ccb +web3646 +web3648 +web3650 +web3651 +www.oriflame +web3652 +web3356 +web3653 +web3654 +web3655 +web18269 +web18339 +cea +web3656 +web3657 +nsr2 +web18268 +aza +web18341 +brief +web3355 +chm +web3658 +web3661 +diabolo +web3662 +web3354 +web3353 +web3352 +03 +web3663 +agus +web3664 +web3665 +web3666 +web3351 +web3350 +manta +web3667 +web3668 +web3670 +web3671 +dev0 +web3672 +longisland +web3673 +web3348 +web18267 +web3347 +web3674 +det +web3346 +web3675 +web18342 +web3345 +web3676 +web3344 +web18266 +web3677 +web3678 +web3680 +web18265 +web17080 +elton +web18264 +genome +web18263 +win19 +web3343 +xgc +web3682 +cloud4 +autodiscover-redirect +web3683 +web18262 +emd +web3342 +elo +kcc +web18261 +web3684 +blondie +web18259 +merlot +web3685 +web3341 +isle +web3686 +web3687 +web18819 +web3340 +web3700 +web3338 +web3701 +web3702 +web3337 +web3703 +medu +web3704 +web3336 +web18258 +web18257 +gci +webdisk.main +web3705 +web18256 +web3706 +web3707 +web18343 +sok +web3708 +web3711 +web3712 +web3713 +web3714 +wmv +wisdom +web3715 +web3335 +web3334 +web18255 +web3716 +web3333 +web3717 +pgames +web3718 +web3720 +web18254 +web3721 +web3332 +web3331 +www.sss +netscape +web3722 +web3723 +web3328 +autoconfig.main +web3724 +web3725 +web18253 +web3726 +web3327 +web3727 +web18252 +web3728 +web3731 +qazwsx +lastchance +web3732 +web3326 +web3325 +web3324 +rol +web3322 +web3321 +web3733 +web3734 +web3735 +web3736 +web3737 +web18251 +web18249 +web3320 +web3738 +web3740 +web3741 +hofman +web3318 +web3317 +web3742 +crunch +web3743 +web3315 +mailgate3 +web3314 +web3744 +web3745 +ganges +web3746 +thehouse +web3747 +ns141 +web3313 +web3748 +uninews +web18248 +web3750 +web3752 +web3312 +skl +web3311 +web18749 +web18740 +inno +web3753 +web3754 +sae +web18344 +web3755 +bookshop +web3756 +web3757 +web18247 +web18729 +web18720 +web18246 +web18716 +www.solutions +web18245 +web10679 +web18829 +teste1 +achieve +mym +web18650 +web3760 +web3761 +web3762 +web3763 +web18640 +derby +web18630 +web18619 +web4894 +web3764 +xi +web18244 +web3765 +web3766 +web3767 +web10669 +web3768 +uds +web3770 +ns94 +web3771 +mississippi +web3772 +web18243 +web3773 +ns89 +web3774 +kaltura +ns88 +web3775 +ns87 +ns86 +web3776 +ns85 +web3777 +ns84 +web3778 +web3780 +web18613 +web18242 +web3781 +web3782 +web18609 +web18598 +web3783 +web3784 +web18597 +web18241 +web3785 +web18606 +web18595 +alcyone +web3786 +web18240 +web18209 +web18594 +roamer +web18593 +web18592 +projet +web18238 +web18591 +web18237 +watcher +orz +web3787 +web18236 +web18235 +pip +web18234 +web3788 +web18345 +web18600 +web18580 +web18346 +web18569 +web3801 +web18563 +web3802 +suport +web18347 +ns93 +web3803 +time1 +web18233 +web3804 +web18562 +durga +web3805 +web18561 +web3807 +xmlrpc +web3808 +web3810 +web18559 +web18557 +web18348 +web18232 +web18231 +web18556 +web18555 +web3811 +web3812 +www.dictionary +www.campaign +web18554 +spr +38 +joomla25 +web18229 +gt2 +fcc +web18228 +web3459 +web18549 +web18189 +ns124 +web3814 +web18540 +web5933 +web3815 +web18530 +ns118 +web5932 +web18520 +web18509 +others +web18507 +web3816 +webnews +web18504 +web3817 +csd +web3818 +web18503 +web3820 +web18502 +web3821 +web18350 +www.sia +web3822 +web18227 +web3823 +web3824 +web18501 +web3825 +web3826 +web18226 +web16917 +chalet +web3827 +web3939 +cgm +web18839 +web3830 +web3831 +web3832 +web3833 +web3834 +web3835 +web3836 +web3837 +web18480 +web3838 +web3841 +lucca +web3842 +web18225 +web6023 +web5923 +sidon +web3843 +web18470 +web3844 +web3845 +myoffice +web18450 +web3846 +learn2 +web18224 +web3847 +web6689 +web18223 +web3848 +web5915 +web3850 +sen +web18222 +web7439 +www52 +consultant +web18221 +web18846 +hobart +web18430 +vns1 +web6100 +web18219 +web16918 +web18850 +www.baby +web18420 +web4000 +web5924 +web18218 +slot +web4001 +hcc +web4002 +web18217 +ds6 +web4005 +lanka +web18216 +web7409 +web7407 +web5898 +web7396 +web18215 +ds3 +web7395 +navarro +web4006 +thc +nata +web7394 +web5897 +web18214 +web16919 +web4007 +web7391 +web7376 +server31 +web7400 +web4008 +www.sys +web5896 +web4011 +web7385 +www.chevrolet +www.lg +web4012 +programas +web4013 +web4014 +web4015 +darkorbit +web4017 +web18853 +wwe +web4020 +afm +web5895 +etv +artwork +web4021 +web4022 +web7379 +web4024 +web4025 +web7378 +web18213 +web5894 +web4026 +web7446 +web7369 +web4027 +wpc +web18212 +drogo +web4028 +web4031 +web4032 +web5903 +web4033 +web4034 +ezadmin +web5941 +orac +web4035 +web18211 +web4036 +web4037 +web7445 +syd +web18210 +web4038 +web5892 +web4040 +web6699 +web7361 +web4041 +srl +web4042 +web4043 +web5891 +blackbird +web7357 +web16921 +web18349 +web5890 +web4044 +ds10 +web18198 +web4045 +web4046 +s1012 +web4047 +web18197 +web4048 +web7352 +web7350 +web4050 +gs2 +web18196 +host122 +web4051 +mail.kuku +web4052 +ag1 +web4053 +web18195 +web18340 +web4054 +web7339 +rmc +web5886 +web18329 +imt +web4055 +web4056 +reseller2 +web5926 +web18194 +fist +web18193 +ott +web7330 +web7325 +web4057 +web18320 +webdisk.gmail +smm +web7323 +web4060 +web4061 +web16922 +web18192 +anas +orl +gene +web7319 +web4062 +web4063 +web4064 +web4065 +web4066 +web5927 +web4067 +host120 +web7318 +web4068 +vhost2 +sdm +web4070 +web4071 +img04 +yy568 +web4072 +web7316 +web4073 +voyeur +web4074 +web18191 +web4075 +twc +mila +web18190 +www.red +web18299 +web4076 +web4077 +web4078 +engels +web4080 +web18308 +web4081 +web18307 +web4082 +web18306 +666av +srp +web7311 +testapp +web16923 +web18305 +web18188 +web4084 +web4085 +web4086 +web4088 +ldp +ius +web4100 +web5928 +dof +amb +web7310 +web18304 +www.kh +smpp1 +web16924 +web18303 +web18187 +web4101 +web4102 +web4103 +eoe +web4104 +web4105 +web7297 +fatality +web18186 +dpt +vv +web18302 +web4106 +ipn +web4107 +jbc +web18185 +exo +web18866 +web6030 +download4 +web4110 +web7296 +web18411 +ger +web18184 +web4111 +web18183 +web18182 +web18301 +activity +web18181 +web4112 +epg +web4113 +web4114 +web4115 +web7295 +web4116 +web4117 +web4118 +web18300 +fir +www.scc +web4120 +edelweiss +web7304 +m13 +server40 +web18179 +web18178 +web4121 +web4122 +web7303 +web18177 +web4123 +web5880 +camaras +web18412 +web18413 +web7292 +web4124 +web4125 +web18414 +web4127 +web7291 +web7290 +web18870 +web4130 +annualreport +web3729 +web18176 +jiaowu +web5789 +web18175 +truyen +web4132 +tf1 +web18415 +kds +h21 +web7286 +web18174 +web18173 +scd +web18416 +kir +web18280 +web7284 +web7281 +web7280 +web4133 +web4134 +www.malaysia +205 +web18270 +web18172 +web7273 +web7271 +adadmin +web7269 +mae +web4135 +web18417 +freebooks +web6696 +web7266 +ktm +grd +cyprus +web4136 +web4137 +web4138 +recon +web4141 +web18171 +web18418 +gore +web18260 +web4142 +vitrine +hektor +elijah +arp +famous +web10719 +p6 +web7260 +web18170 +web4143 +web5873 +any +web3959 +web4144 +web4145 +web4146 +web18168 +web4147 +ckarea +web4148 +web18167 +e-commerce +web4150 +web6095 +mail.yy568 +server49 +www.informatica +msd +server48 +www.thumbs +web18250 +slpda +web18166 +frm +web18165 +mail.666av +aic +web4429 +1111 +mail.080 +pizzahut +web7253 +web10698 +web18880 +web7251 +web6096 +web7249 +web10695 +web4211 +web4212 +jpadult +web4213 +web4214 +mail.jpadult +web4215 +web4216 +web4217 +www.orel +web4218 +web18419 +mail.ckarea +web10694 +web4220 +spor +ost +web4221 +web7246 +web4222 +web18164 +web4223 +alpha5 +web10692 +web18421 +web18239 +prd +www.memberlite +web4224 +web4225 +web10691 +web4226 +web4227 +web4228 +web4230 +web4231 +esports +web5869 +web18163 +acca +volterra +web4232 +web4233 +web4234 +web18422 +web18162 +www.s4 +web7242 +virt +web7312 +web7240 +web18161 +web7421 +web4235 +web4237 +web10682 +web18230 +web18886 +sgt +web7233 +web4240 +web4241 +web18159 +web4242 +web4243 +web7219 +235 +antigo +web18158 +web18157 +arsenic +web4244 +gallium +web7229 +web4245 +web18156 +web18155 +web7226 +je +web5866 +firefox +web4246 +sw5 +web18423 +hl +web18220 +web10670 +vh1 +web18424 +web18154 +web18153 +web10668 +web4247 +web4248 +web18152 +wr +web3710 +ufo +web6039 +web4250 +web4251 +ns201 +web7220 +www.tennis +web7217 +out1 +ns.blog +web18199 +rq +web18425 +web4252 +web18208 +web7213 +vio +ns161 +darkside +dict +web18151 +web18149 +gear +nalog +web18207 +web18206 +web18148 +covers +web18147 +web18146 +web4253 +web18145 +web18205 +zee +web4254 +web18144 +vasco +web4255 +web18204 +web4256 +web4257 +crt +web18426 +web18900 +frontdoor +web4260 +web-02 +web18143 +web18142 +gaspar +autoconfig.fb +autodiscover.fb +web4261 +web4262 +dns13 +web18203 +itec +web18427 +web18428 +web4263 +web4264 +web18202 +web18201 +web18891 +web18141 +web-01 +web4266 +ness +wishlist +web18200 +web4267 +web18139 +autoconfig.lists +autodiscover.lists +webcam2 +sumy +studentmail +web4419 +logic +web10649 +web4268 +kfree +web4270 +protector +web18138 +web4271 +juventus +web18892 +web18137 +web18136 +centennial +web18429 +web18135 +web4273 +web10642 +www.teacher +csj +web4274 +autodiscover.student +web18431 +www.inventory +web18180 +mac2 +web4275 +webclasseur +web10639 +notes1 +web4276 +web18432 +web4277 +claymore +web18903 +thetis +web18134 +web4280 +autodiscover.clients +web3698 +noise +web18133 +www.anime +autoconfig.clients +web4281 +web18132 +d101 +web5860 +web6693 +web4282 +web18131 +web18169 +libanswers +web10629 +web7399 +web10622 +web18129 +niagara +attach +michal +web18128 +banner2 +web18127 +web4283 +web4284 +www.audio +musique +web7398 +named +odds +webdisk.teste +web18126 +web18894 +naomi +redcross +web4286 +www.orange +web18125 +paprika +www.ajax +web4287 +web4288 +web4301 +web18124 +web18123 +lance +web18895 +hahaha +web18160 +calendario +www.www3 +knox +fafa +web4303 +web10619 +web18121 +jquery +web4304 +web4305 +web18119 +www.sso +web7397 +web18118 +web18117 +web6739 +web4306 +web18116 +kokoro +web10613 +web18115 +web18150 +web18896 +jimbo +web18114 +takumi +web4310 +web3697 +web3970 +web4311 +fisip +web18113 +pdfs +web18112 +september +web4312 +broadway +fkip +web4313 +web18140 +web4399 +underwear +nelly +web4314 +web18111 +perfil +web17999 +web7393 +web17998 +web18907 +web17997 +web4316 +web4317 +web17996 +web17995 +pari +web18130 +web5849 +web17994 +web4318 +web4320 +web17993 +arie +web7392 +moran +web18122 +allen +web4321 +www.2010 +web3696 +web17992 +web17991 +slide +marlboro +web18120 +origin-blog +niche +web17990 +web4395 +leeds +comsci +web7390 +web18898 +web4323 +web17988 +web4324 +web3958 +web4325 +web4326 +web17987 +web4392 +web3692 +web18910 +web4330 +web17986 +web4331 +mascot +web4332 +web4333 +web17989 +web18433 +web17985 +web17984 +web4334 +myphotos +www.denver +web4335 +web4336 +web17983 +web18434 +gfs +web4337 +web18435 +web4338 +fortmyers +ctl +web4340 +web17982 +web4341 +web17980 +web4342 +www.quran +web4389 +web4343 +web17981 +web4344 +web17979 +web4345 +web6700 +web17978 +module +web17977 +web4346 +cpl +www.rp +www.sb +web4347 +web4348 +web4350 +web17970 +web4351 +web17960 +web4352 +ptr +downtown +web3999 +web4899 +web18436 +web4353 +web4354 +www.focus +web4355 +frederick +web4356 +web17976 +web4357 +adder +web5938 +web4358 +www.foros +lidia +web3694 +web4360 +web18050 +web4361 +fgc +web17975 +web18048 +web18047 +web18046 +diffusion +web18045 +web18044 +nsd +www.ghost +web18437 +aukcje +web4362 +web4363 +raki +web17974 +web4364 +crimson +www.glass +web17973 +site5 +rival +web17972 +web17971 +lst +vtls +web4365 +web4366 +gjc +web18043 +web4367 +otc +riker +www.che +web17969 +panic +web4368 +tallahassee +web17968 +web17967 +web4370 +web4371 +arcgis +document +web4372 +wen +web18438 +web4373 +web4374 +surrey +web4375 +www.inter +res2 +ddh +web18042 +web4376 +web4377 +web4378 +web18041 +web4380 +web18040 +myjob +periodismo +www.livehelp +wsus2 +base2 +web17966 +www.keith +web4381 +web18038 +ubezpieczenia +www.ubezpieczenia +web18037 +redstone +web18440 +web18036 +web18035 +web4382 +web4383 +web4384 +dma +web18034 +web18033 +lmc +web18032 +web18031 +hosting4 +web17929 +web18028 +web18027 +s242 +web18026 +web18025 +web17965 +web17964 +web17963 +graffiti +web18024 +www.miami +web18023 +eu1 +validate +web18441 +web4385 +singer +seis +web18442 +web5831 +web18022 +web18021 +web17962 +ddc +web17961 +yalta +web4386 +jiwei +test001 +web17959 +web4387 +cs5 +test111 +www.digi +mailserver3 +babyface +test333 +arjuna +web18019 +web17958 +gjs +web18018 +kalendarz +ehealth +web18017 +web18919 +hicham +web4400 +web5829 +mdc +web4401 +web18016 +abood +web17915 +web4402 +web17914 +cmcc +web18013 +web18012 +web4403 +web18443 +www.wj +hikaru +www.sn +duster +vcm +www.mg +nv +web18444 +web17957 +webmail.admin +builder.admin +web17956 +web18445 +web18011 +web17899 +web18008 +andorra +web18007 +web4404 +web18006 +www.secret +web17955 +rodeo +web18446 +brainstorm +web18005 +extensions +bks +concorde +web4405 +polly +www.ve +web4406 +web17954 +web17953 +web4407 +outbound1 +web17952 +web17951 +web4408 +porta +web18004 +web4410 +web18003 +web18447 +web18049 +web4411 +web4412 +podolsk +web18002 +web4413 +confirmation +web18001 +web4414 +harley +web4415 +www.nn +s253 +web17890 +web10449 +web4416 +web4417 +web17880 +web10439 +tivi +web10434 +snr +web4418 +web4420 +web17870 +matematika +zabawki +web17948 +web18448 +fms2 +web6151 +web4421 +web18449 +cptest +swww +vle +web17947 +web17946 +www.zabawki +web17945 +web4422 +web3691 +web17860 +web5819 +web17944 +web4423 +web17943 +web17850 +crl1 +web17942 +web17941 +web4424 +web7359 +informatika +keystone +web18039 +web17938 +web6850 +mana +edesign +web4425 +web6149 +web17840 +base1 +sla +web6843 +web4426 +web3792 +web6842 +www.cpa +web4427 +doberman +web3690 +web6836 +sexuality +web17830 +web6830 +web17820 +web6823 +web4428 +web17937 +web4909 +web4431 +web6819 +web17936 +bebe +web17935 +www.greetings +wikileaks +web6816 +web5809 +por +web4432 +web4433 +revelation +web4434 +web4435 +precious +autoconfig.web +web4436 +web4437 +web5937 +web3688 +web18451 +web6809 +web4438 +web17934 +wm3 +zing +web6798 +quetzal +web4440 +web6797 +web5798 +web4441 +web4442 +web18452 +web6796 +web4443 +web4444 +web17933 +web4445 +autodiscover.web +web17932 +ex2 +web17931 +web17930 +web6795 +stages +niko +www.vologda +web4446 +web4447 +web17928 +web6794 +web18453 +web6803 +katana +hippo +web4448 +web17927 +web6792 +web5797 +ksu +web6791 +web4450 +web18929 +www.onlinegames +web5899 +web17926 +sh4 +web17925 +warm +web17924 +web18454 +web7349 +gorilla +web18455 +web18456 +web6790 +web5796 +web6780 +www.songs +web17923 +web18457 +web18458 +web5794 +web6773 +web4511 +web17922 +inventario +ws191 +web4512 +web5239 +ws182 +ws201 +web4513 +web6769 +web5793 +web6768 +ws102 +ws101 +yogi +web6766 +ws192 +kodak +web18460 +c0 +vive +web4349 +web4514 +web18461 +web5792 +web6760 +rondo +netsys +combo +web4515 +web4516 +mychart +web5791 +web4517 +web18462 +visualbasic +web17750 +bbt +web5949 +web6753 +shady +parkour +web17921 +web18463 +hiho +rooms +web5790 +web4518 +www.indonesia +web18464 +web18020 +web4520 +web4521 +web17918 +web4522 +web6749 +ftpweb +web4523 +holidayoffer +web17917 +web4524 +web18465 +web6746 +web18466 +qt +web4525 +web4526 +smtp.out +web4527 +web18939 +web17916 +web17740 +web6743 +web4530 +web6740 +web4531 +web4532 +www.weather +tehran +web17030 +web17730 +web6729 +web4533 +web4534 +web18467 +web18015 +web17949 +web4535 +web18014 +web6726 +memories +web17720 +web4536 +dic +mailmx2 +mailmx1 +web4537 +web6720 +web4538 +fpa +web17699 +web4339 +web17913 +web4540 +web17912 +web4541 +web17911 +web6713 +web4542 +sly +web4543 +web4544 +web17910 +web17707 +web17706 +web4545 +smtp15 +web4546 +web4547 +web17705 +jgxy +web4548 +web6709 +web4550 +web17898 +php54 +web4551 +fe2 +web17907 +web4552 +web4553 +web17906 +web17704 +web6698 +alles +web18468 +web17703 +web6697 +web17702 +web4554 +web17905 +manufacturing +revistas +web17904 +web18469 +nasc +web17893 +tvr +www.mall +web4555 +letsgo +web6706 +web4556 +web4557 +swap +web4558 +web17701 +www.daniel +web6695 +security2 +web4561 +web4562 +web17690 +sagitta +web6694 +web4563 +camus +web18471 +web4564 +web6703 +web6692 +ita +web17892 +web4565 +web4567 +web6691 +web4568 +browse +web4570 +web18909 +web4571 +web4572 +web4574 +web4575 +tiga +web4576 +betablog +web4577 +web17680 +web17891 +web4578 +web4581 +web4582 +tetra +web17900 +web4583 +traders +srt +web6683 +web7329 +sklep2 +web4584 +otaku +web17888 +web17887 +web17886 +vm11 +web4585 +web4587 +web17885 +dev02 +web6416 +web6679 +web18949 +web4600 +web17884 +masoud +tmn +s74 +www.yes +s72 +web6676 +web17883 +web17669 +web6670 +web17939 +nms2 +soto +web17882 +nir +www.men +web17881 +web17660 +ciscoworks +web6663 +ricette +web17879 +graduation +www.upgrade +web4601 +s65 +vds22 +web6659 +teck +web17878 +yearbook +web17649 +web17877 +web17876 +qwertyuiop +web4602 +web4329 +web5936 +web17875 +web6650 +uslugi +keitai +pixels +web4604 +sisa +web4605 +lars +web17874 +web4328 +facts +rdb +web17873 +web17872 +web4606 +fsa +anaconda +stack +web17871 +sire +web4607 +web4608 +web4611 +web4612 +exile +web4613 +web17869 +gallery2 +web4614 +h120 +web4615 +web4617 +web17868 +web17867 +web17866 +web4618 +web4620 +web18472 +web17865 +web17640 +web6644 +web6643 +adminpc +web18473 +web4327 +web6639 +web17864 +testvps +web17863 +web4621 +web17862 +web17861 +jaime +web4622 +shane +web6636 +nnm +web4624 +web17630 +seat +cpnew +kmm +unplugged +web6630 +web17620 +web6623 +swamp +jury +web4626 +web18030 +chacha +web4627 +web4322 +riza +web17599 +www.player +web18474 +sondage +www.agora +web4628 +polycom1 +web17859 +web18475 +q2 +web4631 +web17608 +web17607 +web4632 +o1 +web17606 +web17605 +web17604 +web4633 +web17858 +redirector +web17857 +web17856 +zeit +antigua +froggy +ecp +larch +web17603 +web18476 +naoki +vs4 +web17602 +bluesea +web4634 +klaus +web17855 +www.ben +web17854 +vpn-test +web4635 +chromium +web17601 +web18477 +web4636 +web4637 +web17590 +web17853 +web4638 +estates +web17852 +web17851 +web17849 +web17848 +web4319 +zia +web18478 +host41 +web17847 +web17846 +web18479 +web4641 +riad +vertex +web3681 +web17580 +web17845 +web7429 +web17844 +web17843 +web17570 +web4642 +web4643 +fileproxy +www.entertainment +web17842 +dce +rana +remote1 +web4315 +web7309 +web17560 +paola +nutri +web17841 +web4644 +printer2 +web17839 +web17838 +mdb +web7298 +web4645 +funzone +web17837 +web17836 +web4599 +web17835 +web4647 +web17834 +web3679 +web17550 +xmlfeed +web4648 +web17833 +f11 +web4650 +web6043 +camera1 +web17832 +nozaki +web4651 +web4652 +web4653 +st01 +web4654 +web4655 +tpe +web6549 +web17540 +web4656 +web17831 +rumba +messagerie +web4657 +web4309 +web7294 +www.painel +yamada +web18481 +web4658 +bill2 +webmail5 +webdisk.drupal +autoresponder +web18482 +web4660 +soledad +web18483 +netserv1 +web6539 +web4661 +web4308 +web4662 +web17829 +ns.math +web17828 +web17827 +web18484 +tracks +cisco1 +ashi +d16 +www.tours +sf2 +web17826 +web4663 +d15 +web7293 +web4664 +web4665 +d14 +d13 +d12 +d11 +www.amazon +web4666 +cesar +randall +web4667 +terri +web17530 +web4668 +almaty +mab +cameron +web4670 +calipso +web3790 +web4671 +web18485 +postbox +pap +nord +tls +web17825 +web4672 +web4673 +web18486 +web4674 +web17824 +web18487 +web4675 +web18488 +meganet +web4307 +tomita +web18489 +web4676 +web17823 +www.dj +web17822 +web4677 +webdisk.magento +web4678 +web4680 +test55 +web6530 +tsubasa +web4681 +web4682 +sprint +web4296 +web17821 +web17819 +web4684 +web4685 +ovs +web4686 +bcp +rehab +web17520 +comodo +web17818 +tierra +www.lite +www.bulk +web4687 +smbc +babe +bada +web4295 +web4688 +c12 +web17817 +web4294 +web18491 +www.silver +web4293 +web17816 +web4701 +web6509 +web18009 +web4702 +web4703 +rosebud +amor +web6498 +web6497 +web18492 +web18493 +web6496 +web4704 +web17815 +web17814 +chin +web6495 +web4705 +web4706 +www.mo +daybyday +web17813 +web4707 +web4302 +web5935 +www.antiques +web4708 +web17812 +web6494 +web18494 +web4710 +baito +sunpower +web17811 +web17908 +wraith +web6493 +web5199 +web4029 +site4 +web6492 +cliente +web6491 +naboo +mon1 +web17749 +web4711 +web4712 +web6500 +web17748 +web17747 +naps +spl +toaster +ogre +web17746 +web3949 +mimo +web17745 +mica +hayato +iproxy1 +web17744 +web17897 +ppi +otr +web17743 +web4714 +hispania +itnet +web17742 +web4715 +web6485 +www.torun +game5 +web17741 +web4716 +web4300 +webdisk.dating +web17739 +web18495 +quentin +web4717 +obmen +universum +web17738 +web17896 +julian +web18496 +web17737 +web18497 +dogbert +web17736 +web4718 +web4720 +web4721 +web17735 +cdms +web6479 +jns +miller +plesk1 +web4722 +web17895 +web17894 +www.tube +web4723 +web4724 +web18498 +www-backup +kolo +triple +libopac +web6469 +web17734 +web17733 +roza +web17732 +coin +come +web17731 +mag1 +coms +web4725 +web4726 +web16940 +lantern +happytime +mailmaster +web4893 +nsmaster +web18511 +web17903 +web4285 +web4727 +web17902 +diva +web17729 +www.example +wpdemo +web6459 +b22 +web17901 +sona +writer +web4728 +web17728 +web6449 +web18512 +web17727 +web4730 +mmk +web4731 +imaging +kerr +web4732 +web18000 +fe01 +web6439 +web4733 +seller +web6438 +web4734 +web4735 +taichi +mighty +web7275 +e5 +web18513 +indira +www.technology +a9 +web4736 +web3758 +web4737 +web4738 +web4279 +web4740 +web6669 +olya +web17726 +web4690 +midget +web4741 +web4278 +web4692 +web6049 +web4742 +adsrv +web17725 +web4292 +web4744 +web3759 +oleg +web4745 +remove +web4900 +web4746 +web4747 +web17724 +web17723 +web4939 +web17722 +web7389 +web3969 +pancho +web17721 +web4713 +web6409 +vsa +web6398 +web17719 +web17718 +relay01 +web5919 +web3489 +nippon +web6397 +web6430 +web6396 +web4719 +relay02 +web18514 +web4748 +web4750 +web4099 +www.herbalife +web6394 +web5279 +hash +web17717 +www.time +web17716 +web5945 +web17715 +gman +web4812 +railway +fai +web17714 +web4813 +web4814 +web4815 +web4816 +web17713 +gong +web4817 +web4818 +web4820 +web4821 +hide +humanresources +web4822 +web4823 +web4824 +web6393 +web10699 +web4219 +hist +web4825 +web4827 +soi +shredder +muzika +web4828 +web4831 +web4832 +web4833 +web4834 +zvezda +web4835 +web5950 +web4836 +web17712 +hola +fed +gip +web18515 +web4837 +web4838 +web6390 +web4840 +youcef +web17711 +web4841 +web4842 +web4843 +duma +jain +web18439 +web4272 +ftp02 +web4844 +web4890 +web4845 +radikal +web4846 +web17029 +web4847 +web17710 +web3929 +lori +raymond +web17698 +web4848 +web17909 +web4850 +web4269 +211 +web4829 +web6360 +lizard +web4851 +web4852 +web6099 +web17697 +forum-test +web4853 +web17696 +web5946 +web4854 +web4855 +web4856 +web4857 +web6489 +web3399 +web4858 +web4860 +web4861 +web4862 +web4863 +web6349 +web4864 +web17009 +kari +web4865 +web4098 +web4866 +web4867 +web5934 +web18516 +web4868 +web4920 +web6339 +asterisk1 +mydomain +pkm +www.cart +web4870 +k1 +web4871 +web4872 +lazy +web6329 +web4873 +june +web4874 +web17695 +alberto +web16911 +web17694 +web17693 +web16912 +web18517 +quack +web4259 +web16913 +web4875 +kepegawaian +web4258 +koti +web5920 +web4876 +web16914 +web16915 +web5299 +web4877 +web4878 +d37 +d36 +web17016 +www.cool +web4906 +web4880 +d34 +limo +d32 +switch3 +web4881 +d31 +male +web7299 +web18459 +web6129 +web17692 +persona +web3769 +web4882 +web4883 +web4884 +web3669 +web17691 +web4885 +web4949 +web4886 +web17700 +web4887 +maze +web4888 +web5001 +web17688 +web18800 +kuma +web5002 +web4249 +web17687 +web5003 +webdisk.team +web5004 +web5005 +spruce +web6391 +web5007 +mess +shooter +publiker +web5008 +kyle +server08 +zabbix2 +web6249 +visions +sw4 +web17103 +web5010 +www.kz +web5011 +web17686 +web17019 +web5012 +miku +web17685 +web5013 +web100000 +web5014 +web5015 +web5016 +miso +web5017 +web5018 +web7239 +swanson +composite +monika +web17684 +carnival +web6239 +web100001 +web5020 +web5021 +web100002 +web5022 +www.test6 +web5023 +web5024 +web5025 +web5026 +cpd +aukro +bullet +web18518 +web6230 +web17683 +de1 +web5027 +web5028 +rosie +web4039 +web5030 +maximum +ican +web5031 +mohsen +web5032 +web17682 +web5033 +web18519 +web18521 +web4239 +web17681 +web5034 +web5035 +tunisia +sidious +web17679 +web5036 +web18522 +web5037 +web17678 +web5038 +web5040 +web5041 +workfromhome +cw01host9 +cw01host8 +web17677 +web17676 +nathan +cw01host7 +web17675 +web5042 +web5323 +web5043 +web5044 +web17674 +web5045 +web17673 +web5046 +web3329 +glad +web5047 +retailer +web5048 +web5050 +web5729 +web4951 +web4952 +web4953 +web17672 +web4954 +web4955 +web17671 +web4956 +web4957 +web4958 +web16925 +web4962 +foru +web4963 +low +web4964 +web17670 +web4965 +nore +xo +web17668 +web4966 +programming +mx00 +ichi +rapids +mpi +pana +web16926 +app9 +web4967 +web4968 +web16927 +web17667 +web17666 +web17665 +web4970 +chihiro +web16928 +web17664 +web17663 +gunther +web16930 +cw01host6 +shemale +web4971 +web17662 +web4972 +web4973 +web4974 +cw01host5 +web4975 +web4976 +web4977 +web17661 +web4978 +meetme +cw01host4 +web4980 +web18523 +web4981 +megara +web4982 +web4983 +beta.admin +web4984 +www.profesionales +cw01host3 +web3499 +web17659 +web18524 +web4985 +numbers +web16931 +web5859 +web3799 +web4987 +web4990 +cw01host2 +web16932 +web18525 +autoconfig.joomla +warriors +autodiscover.joomla +wushu +web16933 +pola +web16934 +preview1 +minus +web4991 +privat +web16935 +cw01host1 +radio2 +ireland +web17089 +web4993 +pull +web6209 +beekeeping +web4994 +pobeda +web4995 +benz +cost +web17658 +web17657 +zhang +web4996 +deva +rt1 +web18526 +web18527 +web17656 +thanh +dominio +web4997 +web6198 +philips +web4998 +web4999 +web5111 +web5112 +web17655 +web17036 +web5113 +web18528 +web3709 +web16937 +web18529 +ogame +web17654 +web18531 +yasin +web17653 +popmail +web17652 +web5114 +web16938 +web5115 +web5116 +web5117 +web5118 +web5120 +web4238 +web17040 +web5121 +web6197 +web5122 +web16941 +web16942 +web6196 +web6194 +web16943 +web5123 +web6193 +devils +www.ch +web5124 +web5125 +web5126 +web6191 +web5127 +web6190 +web17651 +web17650 +web5128 +web5131 +marino +web5132 +web5133 +web5589 +web5134 +web5135 +web5136 +web5137 +tma +ns.test +web17648 +web5138 +web5140 +web5141 +web4236 +web5142 +web16944 +web6180 +web5143 +web5144 +web6173 +web5145 +web17647 +web5146 +web5147 +web5148 +web5150 +web5151 +web6169 +final +nadya +web17646 +web6167 +web5152 +web5153 +www.mi +web5154 +web5155 +web5156 +gus +web16945 +web16946 +combat +web5157 +web5158 +roeder +web4579 +web17645 +web18490 +web6160 +web5161 +web16947 +towa +web17039 +web17150 +web17644 +web5162 +web17643 +callpilot +web5163 +lp4 +web5164 +mailto +web17642 +web5165 +web5166 +postman +friendly +web5167 +web5168 +web5170 +web5591 +web5171 +web5172 +web4229 +web18532 +web5173 +web5174 +web16950 +zona +web5175 +web5176 +web5177 +topdog +web16951 +web16952 +web18505 +web18506 +web4299 +web17139 +web5178 +web17641 +web6139 +web5180 +web17129 +web6131 +web17639 +vital +myplace +vermeer +web4922 +web5181 +web5182 +palmsprings +web5184 +web5185 +web16954 +web17119 +web17638 +boost +web5329 +web18508 +web5186 +web5187 +scream +woow +web17637 +web5188 +www.torrent +web5201 +web18950 +web17636 +web18948 +web5202 +web18947 +web16956 +web5203 +a01 +web18946 +web3349 +web5204 +web5205 +web5206 +web18945 +web5207 +web5208 +web5210 +web5211 +web18944 +web5212 +web5213 +web18533 +web5214 +www.bi +web18510 +nick2 +web18943 +web16957 +web5215 +web16958 +web17635 +web17634 +web18942 +web17060 +web18941 +web5216 +web16961 +web17633 +web17632 +web3359 +web5217 +web17631 +web3779 +web17629 +tiamo +web5218 +web7419 +web5220 +web5221 +web17069 +web17072 +web18534 +web5222 +web18940 +backupserver +web18938 +hihihi +ttk +web18937 +web16974 +web4819 +www.mega +web16995 +web5223 +buck +dex +web16975 +web16976 +web5224 +bestway +web5225 +web18936 +web3369 +advertisement +ptest +star7 +web16977 +web18935 +prod1 +syzx +web18934 +web17628 +web5226 +web5227 +monitoreo +yellowstone +web18933 +web5228 +web18932 +demo17 +newstest +demo21 +web17627 +web18535 +web18931 +web16978 +web5230 +web5231 +web5195 +marius +web17626 +web5232 +hadi +www.boutique +web5233 +arslan +web17079 +web5234 +web18930 +web4826 +fullhouse +web17625 +www.all +web5235 +web5236 +ironport1 +web5237 +web18928 +web18927 +web5238 +web6369 +web17624 +web16984 +web5241 +beta4 +rusty +web18536 +web16985 +rod +web18926 +web5242 +origin.m +web18925 +web18924 +web18537 +web5243 +oldadmin +site3 +web5244 +shaka +web16986 +hecate +web5245 +web5246 +web17623 +web5247 +web5248 +192 +web5250 +hptest +web5251 +web5252 +web18923 +web5253 +web5254 +web3379 +web18500 +mystyle +web17622 +www.cod +web5255 +checkmate +web5256 +web16987 +web3381 +web18538 +web5257 +web5258 +se3 +web18289 +web16988 +web18539 +web5262 +web3739 +web5264 +sheldon +web5265 +web5267 +web18922 +web18921 +web5268 +web5270 +autoconfig.api +web18920 +sajan +web5271 +web17090 +notifications +web5272 +web5274 +web18918 +web5275 +web18917 +web18916 +web5276 +web5277 +web17621 +autodiscover.api +web5278 +web5281 +web17619 +pkd +web5282 +web17101 +web5283 +web5019 +web17618 +kon +web5285 +web5287 +web5419 +web5288 +web5301 +skills +web16992 +web5302 +web18915 +web18541 +koa +web18914 +web17617 +web5304 +web18913 +web5305 +web18542 +autodiscover.testing +web6379 +politik +comunity +web5306 +sqlserver +web17616 +autoconfig.testing +web18912 +web5307 +immortal +web5308 +web16948 +web18911 +web17093 +web17110 +web6779 +web17615 +web17614 +web5311 +web18908 +web17613 +web17612 +mall1 +casting +web5312 +web5313 +www.contest +web5314 +web5315 +web5317 +web5318 +web17611 +web17609 +web18897 +web5320 +web5321 +www.sync +web18906 +web5324 +web17094 +web5325 +go4it +web18905 +s240 +zcgl +web5326 +slave1 +osama +web5327 +layout +web18904 +wsa +omkar +web5328 +web5331 +patel +web5332 +salim +web5333 +web18543 +web5334 +web4839 +web5335 +basel +web5336 +dei +web5338 +web17598 +web5340 +web18893 +web5341 +www.models +noe +web5342 +wanderer +web18544 +web17597 +vidyo +web5343 +web17596 +web17105 +web17595 +web17594 +web5344 +web17593 +web5345 +web18902 +web17592 +web5346 +web18901 +paraguay +license1 +arrow +web18889 +web17591 +web5347 +web17589 +web18888 +web5348 +web18887 +web17588 +web17587 +web5350 +web3693 +web17106 +web3400 +web16997 +web17586 +bad +fortran +web4586 +web5411 +web17107 +web18885 +web18884 +web3391 +web18883 +web5412 +web5413 +pse +web5414 +web17585 +web5415 +tin +web18882 +web18881 +web17050 +web17108 +b99 +necro +web18879 +web18878 +web17584 +web18877 +web5416 +web17583 +headhunter +web5417 +web18876 +web5420 +web5421 +web5422 +borabora +web5423 +web5424 +web3392 +web5425 +www.nnov +web5426 +web17582 +web3439 +web5427 +web17099 +web5428 +web5430 +web5431 +web17581 +web5432 +web3393 +apteka +rector +pegas +web3394 +liebe +web5433 +web5434 +web5435 +web17579 +web5436 +web17578 +web5437 +web6389 +web3395 +kapital +web16996 +web5438 +web17577 +lolol +web5440 +web3396 +web18875 +web5441 +clubhouse +web5442 +kraft +web5443 +web18874 +web5444 +web18873 +jxcg +web6392 +web5445 +web17576 +web4849 +web5446 +window +klimt +web3397 +web5447 +web17575 +mur +dsi +web5448 +dedicado +web17574 +www.mail1 +web5450 +web18545 +web18872 +web5451 +web18871 +web5452 +web3398 +web5453 +web5454 +marly +web17573 +crayon +web13129 +web18546 +goodfeel +web18869 +web17572 +kuban +web5455 +web18868 +sanctuary +baloo +web6519 +web3409 +web18560 +web6395 +web5456 +web5457 +bsd1 +web17571 +web5458 +web13139 +web10690 +web17569 +web5461 +web17568 +medium +web5462 +web5463 +web13149 +web5464 +web5465 +mayor +web5466 +lucky7 +zlatoust +web5467 +web18867 +web13152 +web17567 +web17104 +win21 +web13157 +web5468 +web5470 +web5471 +psms +torun +web5473 +web5474 +evergreen +leila +yume +cuda +maher +web13159 +web18865 +web5475 +web17566 +oe +web5476 +web5477 +web6399 +salama +web5478 +web18864 +web17565 +web18863 +web5480 +web5481 +limon +gaga +web18862 +www.america +libre +lithuania +web13163 +sancho +www.saransk +onelove +web17564 +web5482 +www.quotes +web17563 +web3990 +web17562 +web5483 +jumbo +web5484 +julio +web13168 +metamorphosis +web5485 +khalil +web5486 +web5487 +web13169 +web5488 +paranoia +khaled +bigdog +web5500 +web7428 +web18547 +web4589 +web5502 +web4859 +web5503 +web13179 +web13182 +web3419 +web5504 +maven +web5505 +web3800 +web13190 +web18548 +web5506 +web5507 +web17561 +web18861 +openemm +web5508 +web18550 +bilal +web17559 +nicaragua +web5510 +tif +web18860 +web17558 +web5511 +web13191 +lcc +admini +ding +web5512 +web13192 +mcb +web5513 +web13193 +web5514 +web5515 +web17557 +web13194 +jolly +web18858 +issam +artis +web5517 +web17556 +web13195 +web17555 +web13196 +alisa +web5518 +redtube +web18857 +colgate +web13197 +web5520 +democracy +web13198 +web13209 +plesk2 +web18856 +web18855 +web13212 +web5521 +web18551 +web5522 +web13213 +liliana +web5523 +bookman +vf +web5524 +web13216 +web13217 +web5525 +web5526 +web5527 +web17554 +web17553 +web17552 +web13219 +web6419 +web13229 +web5912 +web5528 +web18854 +grants +web5531 +web4869 +web10693 +web13238 +web5532 +web5533 +web5534 +web3428 +web13239 +web17551 +c10 +zoot +web4289 +web3989 +web3429 +web5535 +web13245 +ripley +web5536 +web5537 +rockon +web5538 +rawan +web17092 +web13250 +web3529 +web4699 +rasta +web5540 +web5541 +web5799 +web5542 +onlineworld +web17549 +web17548 +web5879 +web5543 +web4879 +jimo +web5544 +web5545 +staging.shop +ns129 +web5546 +web18552 +rogers +web5547 +web5548 +rodrigo +web18499 +web18589 +web17547 +web3794 +ns128 +web17546 +web18553 +web10696 +epage +web6789 +web5550 +web18558 +ns126 +ns125 +web5551 +web5552 +web5553 +web5554 +temporal +jamie +web5000 +web3795 +terence +web18564 +web4901 +web4902 +web5555 +tecnica +web17545 +web3449 +jamal +testing123 +web5556 +web10697 +www.california +web5557 +web18599 +web4903 +web5558 +web16960 +web5560 +web4904 +web16949 +web4889 +web4905 +www.tlc +web5006 +web4907 +igloo +web4908 +dreamland +hosam +web17544 +web4911 +web4897 +web4912 +web5561 +asdfghjkl +devsecure +prize +web3460 +web5562 +web18565 +web4913 +web16998 +web17543 +merpati +web4914 +web5563 +web5564 +web5565 +web6457 +admin6 +gca +perso +web4915 +web17542 +web17541 +web5566 +web4592 +web6793 +web17539 +web17538 +web5568 +endymion +web4916 +web4917 +funky +web5572 +webdisk.photos +web5573 +web5574 +web17537 +web3465 +web18566 +dns03 +sawyer +web5575 +web5576 +web5577 +web4918 +web4921 +web3468 +web3469 +web5578 +haris +web3809 +videocenter +moncompte +web4896 +web5580 +web6429 +racktables +redondo +web17536 +web17535 +web3479 +isidore +web18567 +web18639 +web17534 +web5581 +web17533 +pradeep +shouji +web5909 +web4935 +web5889 +web5582 +web5583 +web4940 +web5584 +dock +web5585 +web3488 +vps106 +web10715 +web7443 +magenta +web3490 +nakamura +gadmin +habbo +web3930 +web5586 +web3491 +web5587 +web5588 +traffic2 +web3492 +spambox +chaotic +2006 +web3493 +ntv +web18309 +web17532 +forte +web5602 +web5603 +web3494 +web5604 +web6490 +web3660 +web18568 +web18570 +vps115 +web5605 +web5606 +isabel +file01 +web5607 +web3496 +web5608 +web5610 +web4593 +web5612 +web6799 +web5613 +lacoste +web5614 +eidos +web17531 +www.public +web4950 +accelerator +web3497 +web5615 +web5616 +web5617 +web3498 +web5618 +web5620 +web17529 +web10689 +inlove +web3509 +web5622 +od +web5623 +web5624 +web5625 +web5893 +filex +web5626 +cw07web01 +vps108 +web6499 +web4959 +web5627 +web3518 +web5628 +web6759 +web5630 +web5631 +mohamed +web3519 +elegance +web3998 +web5632 +webalbum +web5633 +web6520 +web3530 +web3819 +cpanel3 +web6059 +web18571 +web7449 +web18572 +web6529 +web18789 +melinda +simpletest +proxy02 +web3979 +web5634 +web5635 +web3539 +web4895 +web5259 +web3549 +web5637 +web5910 +web5638 +web5911 +web4992 +web5942 +web5795 +web6839 +web5640 +web5913 +web5929 +prove +web5641 +web5642 +hangout +web5643 +web18852 +darkman +web16980 +refresh +web5644 +web5645 +web17920 +web5646 +web5647 +web5648 +web5914 +209 +web5119 +satan +angie +web6119 +web5650 +web18730 +web4595 +web5711 +web7289 +web5712 +annex +web10729 +web5713 +web17889 +web5714 +web5715 +web5716 +web5717 +web4139 +web17528 +web5718 +web5720 +bauhaus +web5721 +web5722 +web6016 +rudolf +web5129 +web5723 +web5724 +web5725 +web5726 +angola +web5917 +web5727 +unavailable +web18810 +webdisk.partners +web5918 +web5728 +web5731 +gi +web6019 +web5732 +web5733 +web6849 +cw03host1 +web5734 +whynot +web5735 +cw03host2 +web5921 +animes +web18851 +web3719 +web18849 +web5922 +web3615 +web3616 +web6829 +web5159 +web5736 +mercator +web3619 +web16990 +web17527 +web6616 +publica +ejournals +web5737 +web3622 +web5738 +web5740 +externo +web3316 +web3319 +web17526 +web5741 +autodiscover.host +highland +web18573 +web17525 +autoconfig.host +web5742 +web5743 +web5744 +web17524 +web17523 +web5745 +web5746 +www.cam +web5747 +web5750 +drago +test002 +web5751 +web17522 +web5752 +web5753 +web3323 +imk +web6619 +spaces +web3628 +web5754 +web3629 +web3358 +web5755 +www.bill +web5757 +dofus +web5758 +web5760 +web18010 +web5761 +web18574 +web18848 +web3495 +edson +web5762 +web5763 +web5764 +web5765 +web5219 +web5766 +web5767 +web17521 +web5768 +web18847 +web5770 +web5771 +web5772 +divya +web5773 +web16999 +gigabyte +realmadrid +tiago +web17919 +drumandbass +web5774 +web17091 +web18845 +web5775 +web18575 +web5776 +web3806 +web3798 +web6719 +web5777 +calculus +web18576 +web18890 +web5778 +web18029 +web3943 +reb +web3944 +ebank +web3945 +web17519 +web5780 +web3948 +web5781 +web5782 +web4049 +web5783 +web5290 +web3953 +fso +web5784 +www.acs +web17518 +web4059 +fortress +web5785 +philip +www.ams +web17517 +milkyway +live3 +web17516 +web5786 +web5787 +web5788 +web3961 +web3963 +icom +web5801 +web5802 +web5803 +web5804 +web18899 +www.ict +web5805 +web17515 +web5806 +euro2008 +web5807 +web5808 +mms2 +www.cis +web5810 +web5811 +terror +web3965 +web5812 +web17514 +web3966 +web5813 +web3975 +web4079 +web5814 +web3981 +web3984 +web5815 +problem +web5816 +web5817 +deuce +web17513 +web3985 +web5818 +web4087 +web5820 +web5821 +web5822 +web17512 +web5823 +web3988 +web5824 +clare +web4093 +web4097 +web5825 +web3330 +web10709 +web3791 +web4291 +web5826 +web5827 +web3793 +web18577 +web17940 +web3796 +web3797 +web17511 +web5800 +web3813 +web5779 +web5931 +nazgul +web5828 +web5830 +web4609 +craig +web5832 +web5833 +web5834 +web4359 +web4369 +web5835 +web4379 +web4919 +web5836 +webtech +web5837 +asdasd +web4089 +guava +web5838 +web5841 +web6649 +enzo +aztec +web5842 +web4388 +web5843 +web4390 +chill +web5844 +web4391 +web5845 +s155 +web5846 +web5769 +web5847 +ashish +web18578 +web5848 +web4393 +web4394 +web5621 +web18844 +web5850 +web5851 +web4396 +web4397 +web5852 +web5853 +web18843 +web5854 +web4398 +web5855 +lab1 +web5856 +web5857 +desperado +web5858 +web5861 +web5862 +web5863 +web5864 +web5865 +web5867 +bandar +web18842 +bk01 +web4409 +web5868 +web5870 +web5871 +web5872 +web5874 +doggy +web5875 +web3689 +web5876 +web5877 +web5878 +dolls +aymen +newmoon +web4430 +web4439 +web4290 +web5619 +web3828 +web5759 +kagami +web5881 +tournament +web5882 +web5883 +web5884 +web5885 +web5756 +web5749 +web5887 +respect +xanadu +terminus +web18579 +web5888 +web5900 +web5901 +web5902 +web5748 +web5904 +web5905 +blazer +web5906 +drift +web4449 +web18841 +web5907 +web5908 +farmer +web6011 +web18581 +elis +web6012 +web3829 +web5739 +web6013 +web6014 +web6015 +web3339 +web6017 +web6018 +web6020 +web3983 +web6021 +web6022 +bills +web6024 +web4519 +web6025 +web18582 +web3840 +www.sochi +web18583 +annie +saffron +alter +web4528 +web18584 +web6026 +web18840 +simplex +web4539 +web18585 +web18586 +web18838 +web6027 +web3991 +web4549 +web6028 +web3389 +web18837 +web4560 +web3849 +web18587 +amity +web4566 +testphp +web6031 +web18836 +web6032 +web6033 +s169 +web18588 +celcom +web6034 +tmm +web6035 +tania +web4569 +web6036 +freely +cyberzone +web6037 +rascal +vampire +web18835 +web6038 +daum +web4573 +web18834 +web17149 +eplus +web4580 +web6040 +web6041 +web6042 +web6044 +web6045 +web6046 +web16953 +web4929 +dzone +erica +erika +gaban +web17148 +s158 +web6047 +web6048 +web6050 +web17147 +www.krasnoyarsk +web17950 +web6051 +web4898 +web18833 +web6052 +web6053 +web18601 +web6054 +web17146 +web6055 +mountainbike +web4588 +web6056 +survey1 +entry +web4590 +keira +mybaby +web17145 +web18832 +pra +web4591 +web18602 +rti +web6057 +web6058 +web6060 +web18831 +web4603 +web18830 +web4594 +web6061 +web18603 +web6062 +web17144 +web4596 +gears +web6063 +web4597 +web6064 +web6065 +bikini +armada +videobox +web6066 +web17143 +www.td +web6067 +wsi +web4598 +web6068 +web6070 +web18828 +web6071 +web4610 +web17142 +web18827 +smstest +web6072 +web6073 +web6074 +ens +web17141 +bns +web17140 +web5944 +www.ulyanovsk +diamante +web6075 +web6076 +web10356 +web18604 +web6077 +web17138 +power4 +depression +web6078 +web6080 +web17137 +web6081 +web4623 +web6082 +web4625 +web6083 +web6084 +ftp.secure +web4630 +web10431 +web53 +web6085 +web6086 +web6087 +web18826 +web10432 +ginny +web54 +web17136 +shortcuts +web10433 +web18825 +web10435 +village +web43 +web6088 +web10436 +www.izhevsk +web6101 +web6102 +mylive +web6103 +web18824 +web6104 +hermit +web10437 +web17135 +shh +web18823 +rinrin +web6105 +web18822 +web10438 +web6106 +web10440 +web6108 +web6110 +web17134 +web10441 +web6111 +web6112 +headlines +web6113 +web6114 +web18821 +web6115 +web10442 +web18820 +web10443 +web6116 +web6117 +web10444 +web6118 +web6120 +fairtrade +spartacus +web18605 +web17133 +web10445 +web6121 +web6122 +www.stu +web6123 +web10446 +web6124 +webdisk.club +web17132 +web18818 +web18817 +web17131 +web6125 +fadi +web18596 +web10447 +web10448 +web18607 +web6126 +web10450 +web18816 +web18815 +yokohama +web10451 +exporter +web6127 +web6128 +web18814 +web6130 +web4640 +nightwing +web6132 +web6133 +spectra +bread +web4646 +web6134 +web4649 +web6135 +web6136 +gort +web6137 +web4659 +web10611 +web18813 +web17130 +web10612 +web18859 +rekrutacja +www.rekrutacja +web6138 +web18812 +web10614 +forum3 +thekey +web6140 +web10615 +web18811 +web6141 +web6142 +web6143 +web10616 +web10617 +web10618 +web17128 +web10620 +web10621 +web6144 +mydev +web6145 +web6146 +web18799 +web6147 +astronomy +domi +web6148 +web10623 +rtmp +web6150 +web4616 +bappeda +web6152 +web18608 +web10624 +web18610 +web6153 +web10625 +web6154 +web6155 +web6156 +www.mdm +cnet +goodies +web18611 +web18612 +happy123 +web18798 +web16955 +web6157 +web6158 +web18614 +web18797 +eedition +web17127 +web18615 +radium +web10626 +web6161 +web17126 +www.testsite +web18616 +web6162 +portalweb +web18806 +web6163 +web10627 +mandrake +web6164 +web6165 +web18795 +web18794 +web10628 +web6166 +web18793 +oam +web10630 +web5839 +web10631 +web10632 +web10633 +web10634 +web10635 +web10636 +web10637 +easymoney +bomb +bangbros +web10638 +web18617 +web6168 +web10640 +web10641 +server07 +web10643 +backup6 +web10644 +jenny +server06 +web18802 +web18618 +hshs +web10645 +web17125 +web6170 +web10646 +onlinetest +web6171 +web10647 +web10648 +web10650 +web10651 +web10652 +web18801 +tpp +web6172 +tunis +web6174 +web6175 +web10653 +web16983 +web18620 +web6176 +freeads +swim +web10654 +web10655 +muzic +web10657 +mofos +web10658 +web10660 +web18788 +web10661 +web6177 +web10662 +web18621 +web6178 +realitykings +bhc +web10663 +web10664 +web18622 +web10665 +web10666 +web10667 +web4669 +web6159 +web10671 +web10672 +iservice +smurf +web6181 +web18787 +www.oc +ide +web18786 +armageddon +web6182 +web17124 +web18785 +web6183 +web6184 +web10673 +web10674 +web6185 +web6186 +kain +ssl7 +web10675 +web6187 +web10676 +web6188 +web10677 +web18623 +web18624 +web10678 +web10680 +web6200 +web6201 +web10681 +web10683 +web6202 +web6203 +web10684 +web18784 +itservices +web6204 +web17123 +alterego +web16982 +web10685 +web18782 +web10686 +pinetree +web18625 +web6205 +web6206 +web6207 +temple +web10687 +web10688 +d21 +web6208 +web6210 +web6211 +web10700 +web10701 +web18626 +web6212 +web6213 +web6214 +web18781 +web6215 +web17122 +web18627 +web18780 +bas +web10702 +web6216 +web18628 +web10703 +dbs1 +web6217 +web6218 +web17121 +loves +prado +web18778 +web6220 +goya +web6221 +web6222 +web6223 +web10704 +web6224 +web10705 +iftp +web18629 +hoken +web6225 +web6226 +reform +easydns2 +web17120 +easydns1 +web6227 +web10706 +web10707 +web6228 +web10708 +web17118 +webdisk.foro +web17117 +web18631 +web10710 +web6231 +web18632 +web10711 +web10712 +daugia +web10713 +web10714 +dev-admin +web6232 +odp +dl5 +web17116 +web17115 +web18633 +web6233 +web10716 +sergey +web10717 +web6234 +web6235 +web6236 +web6237 +minotaur +web6238 +web18777 +web6240 +web6241 +web16981 +web10718 +buu +web6242 +iraqi +web17114 +web17113 +web6243 +bowling +web17112 +web6244 +web18634 +web18635 +web18636 +web18637 +web18775 +web6245 +web6246 +web6247 +web18774 +web18638 +web6248 +web10720 +web17111 +web18773 +ns131 +web17109 +web17059 +nessus +web6250 +web17098 +web17097 +web10721 +www.designer +web10722 +web18641 +aziz +web10723 +melpomene +echidna +polish +ixion +web18642 +sanat +www.ventas +web18643 +web10724 +web17096 +malabar +web18772 +web17095 +web16994 +protocolo +web4619 +web10725 +who +web6311 +web16993 +web17102 +web6312 +web6313 +web6314 +jak +web10726 +web18644 +web6315 +tottori +web6316 +web6317 +web16991 +web10727 +www.fis +web6318 +web6320 +web6321 +web10728 +web17100 +web6322 +web6323 +web18771 +web6324 +web18645 +web6325 +web6326 +web17088 +web6327 +web6328 +strauss +web6330 +web17087 +vm03 +vspace +web10730 +web4679 +web6331 +web6332 +web18769 +web6333 +www.agro +web17086 +web6334 +web17085 +web4683 +web6335 +web6336 +web6337 +web6338 +new3 +web6340 +olm +web4700 +lyncext +web6341 +web6342 +web6343 +web3992 +web17084 +cgc +web4693 +web4694 +web6344 +www.cams +www.casa +web6345 +web4695 +web6346 +web4696 +web4697 +web17083 +web6347 +sm4 +web18768 +arda +web18767 +bnc +web6348 +web17082 +web6350 +web6351 +www.chef +web6352 +web6353 +cjy +web6354 +web6355 +ghc +web6356 +web17081 +web6357 +web6358 +web6361 +web6362 +web6363 +web6364 +web4698 +web4709 +web18766 +www.buzz +web6365 +web6366 +web6367 +ns140 +www.core +web6368 +web6370 +ecdl +web18765 +arab +web6371 +web6372 +web6373 +web16979 +web17078 +web6374 +web6375 +abdullah +web6376 +web6377 +deepak +web6378 +beny +web4891 +web4729 +web17077 +web6380 +weblync +web17076 +www003 +web17075 +web17074 +web18590 +web6381 +web3730 +web18764 +web4739 +web4743 +web6382 +www.plant +web5009 +web17073 +logserver +web18763 +web6383 +web16972 +web18762 +web17600 +web17071 +web6384 +web16970 +web18761 +web6385 +web6386 +web6387 +web17068 +web6388 +discussion +web4749 +web17067 +web6401 +web6402 +web6403 +web18760 +web6404 +web6405 +web18758 +web4529 +web4297 +web17610 +web4003 +web6406 +web18757 +jericho +web6407 +web18756 +web6408 +web6410 +qv +web4811 +web4010 +web17066 +web17065 +join2 +web6411 +web6412 +web3911 +web18755 +web6413 +web17064 +web18754 +web6414 +web3912 +memoria +web16963 +sigam +web3913 +arkansas +web17062 +web18753 +web3914 +web6415 +web6417 +web18752 +117 +web6418 +web17061 +advocate +web6420 +web18751 +web16959 +web18750 +web6421 +web3915 +web6422 +www.washington +web6423 +web17058 +web6424 +hassan +web17057 +web6425 +web18748 +web4016 +web17056 +web18747 +web6426 +web5459 +web6427 +web6428 +harper +web6431 +web6432 +web18746 +web18745 +bits +web6433 +web18744 +files4 +web6434 +web6435 +web17055 +web6436 +web17054 +vscan +web6437 +web3917 +web6440 +web4018 +tesoreria +web6441 +rentals +web17053 +web4019 +web17052 +web3921 +web17051 +web17049 +web17048 +web17047 +web6442 +bacon +web3627 +web3922 +web5925 +web6443 +web6444 +web17046 +kochi +web4023 +web6445 +web6446 +web17045 +web4923 +web17044 +web6447 +casanova +web18743 +web4924 +web6448 +web17043 +www.the +web6450 +web6451 +web3924 +web4925 +web6452 +web4926 +web6453 +web4927 +beam +web4928 +kawaji +optics +midgard +web5029 +130 +web17042 +bayside.cit +web17041 +web6454 +web3925 +diamant +web16939 +web17038 +web17037 +web5940 +web16936 +web4931 +web6455 +web6456 +web17035 +web4933 +web4934 +web6458 +web3926 +ceramics +web6460 +web4936 +web4937 +radioweb +web17034 +web17033 +web17032 +web17031 +web6461 +web5939 +web6462 +web6463 +musicworld +web6464 +web4938 +wwwa +wwwb +web6465 +web5039 +grassroots +web16929 +web10659 +web6466 +web18742 +web6467 +web5309 +web17028 +web4941 +web3927 +web6468 +web6470 +web4942 +web6471 +web4943 +web18741 +jz +web4944 +yh +chat4 +web6472 +adis +web18739 +web4945 +web4946 +web6473 +web3928 +web4947 +loadtest +web4948 +locate +vpbx +ssr +web6229 +web5049 +web6474 +web4030 +dtk +web13189 +www.quality +web3931 +web17027 +web4960 +web4961 +javascript +micco +micos +web6475 +web3932 +web6476 +web6477 +web6478 +web3933 +web6480 +web4969 +www.html +web6481 +web17026 +web17025 +web6079 +web18738 +web6482 +web18737 +web6483 +web3934 +web7259 +web4979 +web6484 +web4910 +web17024 +web6486 +web3935 +faktury +web4265 +web3936 +web6487 +web6488 +web17023 +web6501 +web6502 +web6503 +web4986 +web4988 +web6504 +web4989 +web3937 +web3938 +web3994 +subscriber +web6505 +survivors +web18736 +web13199 +web18735 +web6506 +web3941 +web3942 +sparta +web6507 +pgsql3 +web5130 +web6097 +web13125 +web17020 +web13126 +web6508 +web13127 +web6510 +web6511 +web13128 +veterinaria +web6512 +web17022 +web13130 +web6513 +vlc +web17021 +web13131 +web6514 +www.mta +web18734 +babes +web13132 +web6515 +turf +web6516 +web6517 +tres +ldaps +web6518 +web16920 +web6521 +web13133 +web6522 +web6523 +web13134 +web6524 +web13135 +web17018 +web6525 +web18733 +runner +web13136 +web17017 +web6526 +wfb +web6527 +web6528 +renoir +web18732 +web18731 +web16973 +reka +web13137 +web13138 +web13140 +forwarding +web6089 +web16916 +web17015 +web17014 +web6531 +web13141 +web13142 +web17013 +web17012 +web13143 +web6532 +web6533 +web6534 +web6535 +inex +web6219 +web6536 +lc3 +web6537 +ots +web17011 +web6538 +web13144 +santosh +web6540 +web6541 +web13145 +web18728 +web5189 +web6542 +web13146 +web18727 +web202 +web6543 +cannes +web6544 +web18726 +blog-dev +web13147 +web13148 +web18725 +web13150 +web18724 +web13151 +web6545 +web17010 +web6546 +web201 +web17008 +web18723 +web6547 +web18722 +cl1 +web18721 +web13153 +blanco +web13154 +talos +web6548 +web6550 +web4629 +web6611 +web6612 +web6613 +web13155 +web13156 +web6614 +web18719 +web13158 +web6615 +web6617 +web17007 +cw01host10 +web5948 +web13160 +web17006 +web6618 +web17005 +web6620 +web13161 +web13162 +web13164 +web6621 +web18718 +web6622 +web13165 +web17004 +web6624 +web13166 +web6625 +web13167 +web6626 +web6627 +suche +web5139 +web17003 +backyard +web6628 +web17002 +opendata +web13170 +nita +web3919 +web6631 +web17001 +web6632 +web18717 +web6633 +web16989 +web6634 +inout +web16971 +web6635 +web6637 +fastcash +ftp.staging +web6319 +web17000 +web6640 +web6641 +web5599 +web6642 +web5289 +web6645 +web6199 +web13171 +web6646 +web13172 +naif +jackass +web13173 +web13174 +web13175 +web5840 +web6647 +web6648 +web6651 +web6652 +web6653 +alms +web13176 +dragons +iina +web13177 +www.tibia +web6654 +web18715 +web6655 +web4932 +backlink +web13178 +web4559 +web13180 +web13181 +web13183 +web13184 +thegallery +web6656 +007 +st6 +web4298 +web6657 +web18714 +nlb +web6658 +iview +web18713 +web18712 +web4149 +web18711 +web6660 +web6661 +web6662 +feedme +web13185 +web5592 +web17070 +web6664 +web13186 +web6665 +wargames +earnmoney +web16968 +edu4 +web13187 +web13188 +web6666 +web16967 +www.test5 +web6667 +web6668 +web16966 +web13200 +web13201 +web3390 +web6671 +web6672 +web6673 +web6674 +web6675 +web6677 +www.ld +web3699 +web13202 +web5590 +web6678 +imagegallery +web6680 +web5492 +web6681 +web16965 +web13203 +web13204 +web3923 +web5649 +web13205 +web13206 +web6682 +web6684 +www.fan +web13207 +web6685 +webdisk.movies +web13208 +mountain +joko +dmx +web5639 +web13210 +web6069 +web13211 +web16964 +web6686 +web6195 +web6687 +web17063 +web6688 +blik +kala +web5719 +web6701 +web6702 +web6704 +www.gov +web16962 +web6705 +sociology +web6707 +web13214 +web13215 +web3946 +web13218 +web13220 +web13221 +web6708 +web6710 +web13222 +web7450 +web13223 +web6711 +web7448 +holland +web13224 +web13225 +web6712 +web7447 +web13226 +web6714 +web6715 +web6716 +web7279 +web6717 +web6718 +web13227 +web5636 +web13228 +web13230 +web6109 +web13231 +web6721 +web13232 +ebi +web4830 +web6029 +web6722 +web6723 +web5629 +web6724 +web6725 +web3649 +iapps +web7444 +web5192 +web6727 +web6728 +web6730 +web6731 +web7442 +web5916 +web6732 +web4140 +web6733 +web6734 +web7441 +web7440 +web6735 +web5611 +web18646 +natal +web13233 +web5609 +web6736 +web5598 +web5597 +web7438 +web7437 +web6737 +web6738 +diaspora +web6741 +web6098 +web13234 +web18647 +web13235 +web13236 +web5596 +web7436 +web5595 +web5594 +web13237 +web5149 +web13240 +web7435 +web7434 +bydgoszcz +web13241 +web5593 +web7433 +web6742 +web7432 +web7431 +lloyd +web6744 +web6745 +web6747 +web7430 +web6748 +web13242 +web6750 +web6751 +web6752 +web6754 +web6755 +web7427 +web4892 +web5601 +web7426 +web5600 +web13243 +web7425 +web7424 +web6756 +web6757 +web13244 +web6758 +engage +web6761 +web6762 +web5489 +test.support +web6763 +web13246 +web7423 +web13247 +relais +web6764 +web7422 +web6765 +web6767 +web13248 +web7420 +web6770 +web6771 +web7418 +web7417 +web7416 +web7415 +web6772 +x22 +ever +web5579 +web6774 +web3947 +web5571 +web5491 +web5570 +web5160 +web3950 +web6775 +web6776 +web7414 +web6777 +web18648 +web6778 +web6781 +web7413 +endor +web6782 +gaza +web6107 +webdisk.app +figaro +web5567 +web7412 +web3647 +web6783 +web7411 +web7410 +web6784 +web6785 +msuperserv +web7408 +web3695 +salix +web3951 +web7406 +web6786 +www.webstats +web5190 +cdf +web4131 +web6787 +web6788 +web6800 +web7405 +web6801 +webdisk.community +web6802 +web5169 +web6804 +web6805 +web6806 +web6807 +web5494 +web3952 +web5495 +web6808 +web6810 +web6811 +web3920 +web4691 +web6812 +web5179 +palembang +web6813 +ajs +web7404 +web7403 +smtp05 +ecr +web6814 +web7402 +web7401 +finch +tdr +web4129 +web6815 +web6817 +ien +bedroom +web5183 +web6818 +hre +web6820 +web6821 +web3954 +web7388 +web5200 +web6822 +web7387 +web6824 +web5549 +deve +web3955 +web4128 +web5569 +web5191 +web5539 +web6825 +web7386 +web6826 +web4126 +web6827 +innovo +web5193 +web6828 +web7384 +web7383 +web6831 +adrms +web5943 +web6832 +web6833 +web5194 +web6834 +web6835 +web6837 +web3956 +web7382 +web6838 +web5196 +web5530 +web6840 +web7381 +web6841 +web5947 +web3918 +web6844 +web5197 +web6845 +web7380 +web5509 +web5498 +tiens +web6846 +xen4 +web6847 +web6848 +chicco +sgb +web7377 +web6179 +easyway +web3659 +web5198 +web5209 +deedee +web4639 +web5519 +web5499 +pwc +web18649 +b161 +web3749 +web5497 +web4930 +web5496 +b123 +jellyfish +web-hosting +web5493 +web6690 +web7211 +web4119 +web5501 +fukushima +web5490 +nebo +web5559 +web3957 +web7212 +web7214 +web5479 +web7215 +web7216 +web4058 +web7218 +web7221 +selly +web6094 +bindu +web7375 +web7222 +web7223 +web7224 +web7225 +web6629 +web7227 +adil +web7374 +web7373 +web7228 +web5472 +web7372 +web5469 +web3916 +web7230 +web7371 +web7370 +web7368 +blaster +web6638 +web3960 +web7231 +web7367 +starweb +web5229 +web5460 +web16969 +web3962 +web7232 +web5240 +web5449 +web3964 +web3789 +web5249 +web7366 +web7234 +web3839 +le +web7235 +web6093 +elgg +web7365 +web4109 +hud +eset +web7236 +web7237 +web3995 +asp1 +kingston +web5260 +web5261 +ntt +samho +webdisk.ip +web3967 +web7364 +web7238 +web7363 +faisal +singh +web7362 +web7360 +web5263 +web5439 +web7358 +web7241 +fabian +web4108 +web7243 +web5266 +web7244 +web7245 +web3968 +web5269 +www29 +web4069 +www39 +www35 +web7247 +web3997 +web5273 +web6192 +web4009 +web5429 +web7248 +web3971 +web7250 +web7356 +web5280 +web7355 +web7354 +web7252 +web5730 +web6359 +web4096 +web7254 +tmp7 +darkknight +web3972 +web7353 +answer +web4095 +web5284 +mail.pics +web7255 +web7256 +web7257 +web6092 +web7351 +web5930 +web7258 +ver2 +web7348 +web7261 +web7262 +web5286 +web7347 +web5418 +137 +web7263 +sysadmin +web7346 +web3996 +web7345 +web7344 +web7264 +web7343 +web4094 +web7265 +web3973 +web7342 +akasaka +groupon +web5300 +web5291 +web3993 +web7341 +web7340 +web7267 +web5292 +web7268 +web5303 +web5294 +oyster +web7270 +seabird +docman +web3974 +web5295 +web17709 +web3940 +web5296 +web7272 +web4689 +web7338 +web5297 +web4092 +www.6 +www.5 +web5298 +web7274 +web7276 +web7277 +hmc +web17708 +web7278 +web7337 +web4091 +web5310 +web4090 +web7336 +web6189 +web6091 +web7282 +web3976 +tableau +web3987 +web3986 +firebird +web7335 +visual +web7334 +webpay +hoth +www.bo +web7333 +web5316 +web5319 +vishnu +web7283 +reisen +web7285 +web7287 +web7288 +web7300 +web7301 +web7332 +web7331 +web5529 +web6090 +web3977 +web5349 +cosanostra +web7328 +rat +web5322 +ws02qa000 +web7327 +web7302 +web7326 +web5293 +walrus +web7305 +ws02qa001 +scooby +skylight +velma +ws02qa002 +web3751 +web7324 +ws02qa003 +ws02qa004 +web3978 +web5330 +web7306 +sleepy +sandbox1 +morton +web3980 +web7322 +www.novosibirsk +mathematics +web7307 +web13249 +croatia +sst +web5337 +web7321 +web4083 +web7320 +web5339 +web3982 +web7317 +web17689 +reps +web7308 +homeschooling +web7315 +web7314 +web7313 +web3639 +memberlite +testmobile +b.i61 +orbital +scrapbooking +b.i59 +b.i62 +b.i58 +therock +abcdefg +b.i57 +myinfo +b.i63 +b.i64 +b.i65 +b.i56 +b.i55 +b.i66 +b.i67 +devforum +b.i54 +smpt +b.i53 +drupaltest +b.i52 +venkat +kimoto +b.i68 +b.i69 +b.i51 +b.i49 +faceebook +b.i48 +eac +vhosts +b.i47 +b.i46 +www.uy +b.i71 +b.i45 +b.i44 +b.i43 +b.i42 +b.i41 +b.i72 +www.14 +b.i40 +b.i38 +b.i37 +b.i36 +b.i73 +youssef +b.i35 +b.i74 +b.i75 +b.i34 +cuckoo +xink +b.i33 +b.i32 +169 +b.i31 +237 +b.i29 +ohyes +b.i76 +b.i77 +b.i28 +b.i27 +timemachine +resimler +b.i78 +autodiscover.design +b.i26 +b.i25 +b.i24 +pylon +b.i79 +www.financial +retailers +b.i81 +momen +b.i82 +autoconfig.design +fsc +b.i23 +b.i22 +b.i21 +guideline +131 +reef +134 +h2media +funnyman +b.i83 +afshin +choose +www.ffm +162 +eforce +storm2 +openvz +b.i84 +b.i20 +bestcar +b.i18 +milkbar +b.i85 +b.i17 +punjabi +logiciel +b.i86 +dreamz +clk +b.i16 +autodiscover.tickets +b.i15 +b.i87 +autoconfig.tickets +huygens +thales +jason1 +alertus +invent +b.i14 +kopenhagen +b.i13 +b.i12 +b.i88 +t10 +b.i11 +b.i89 +b.i10 +b.i91 +geotech +b.i92 +d.i40 +d.i91 +d.i90 +hamburg +marie1 +schubert +whiterabbit +janey +r230.i90 +d.i86 +d.i85 +contractor +d.i80 +b.i93 +qa.secure +qa.www +staffs +b.i94 +jambo +uws +build.www +ak47 +b.i95 +splayer +b.i96 +r230.i80 +translator +qa-lohika.www +elnino +freesoft +local.www +b.i97 +local.secure +anilkumar +b.i98 +build-lohika.www +d.i70 +usertest +b.i0 +rolando +kath +build.secure +rotor +polychrome +imhere +opmanager +r230.i69 +courrier +dn2 +shinbus +masq +d.i59 +anto +b117 +mayrose +tribuna +b148 +mtb2000 +r230.i59 +servicecenter +fastnet +a1234567 +hayden +d.i49 +anarchy +hbf +redwing +brew +connector +fishbook +www.phys +idp-test +smart2 +d.i99 +d.i98 +d.i97 +d.i96 +qweasd +d.i95 +funfunfun +d.i94 +d.i93 +amoozesh +b.i1 +comedy +craiova +www.sante +daesin +d.i92 +b.i2 +r230.i50 +d.i89 +b.i3 +zoidberg +farhangi +d.i88 +d.i87 +ebm +lilith +i-origin +logbook +b.i4 +d.i46 +ielts +ww7 +imis +d.i84 +barlow +gestao +backlinks +d.i83 +ateam +algol +denebola +d.i82 +b.i5 +b.i6 +d.i81 +b.i7 +d.i79 +fs5 +_domainkey +webdisk.card +d.i78 +autodiscover.app +b.i8 +b.i9 +autoconfig.app +garm +gava +www.shop2 +d.i77 +camilla +ptah +mcd-www2 +d.i76 +x10 +x11 +gareth +d.i75 +autodiscover.v2 +autoconfig.v2 +d.i74 +d.i73 +version1 +av1 +d.i72 +qh +mansour +d.i71 +d.i69 +d.i68 +d.i67 +julliet +drupal7 +kepa +d.i66 +safer +d.i65 +textile +mf1 +ispadmin +d.i64 +d.i63 +fuel +spooky +gobo +aoi +www.new1 +krsk +d.i62 +d.i61 +d.i60 +autoconfig.webdesign +roo +d.i58 +d.i57 +d.i56 +dns18 +d.i55 +d.i54 +dns20 +autodiscover.webdesign +d.i53 +d.i52 +screen +context +dns19 +webdisk.labs +mafiawars +serv4 +d.i51 +d.i50 +d.i48 +cleverskincare +d.i47 +rapidleech +hideip-canada +garcia +d.i39 +d.i45 +wedge +flames +d.i44 +csm-nat-10 +d.i43 +d.i42 +d.i41 +d.i38 +itd +boky +gautam +www.afaceri +cpw +miyazaki +ip-ca +ici +pclab +autodiscover.movies +hideip-hongkong +autoconfig.movies +webclient +dame +ip-hk +slipknot +ip-it +www012 +mysql41 +www.imagegallery +mapz +mall49 +kota +l2tp-ca +dcode +midori +l2tp-hk +highschool +l2tp-it +gapi +whisky +flores +gmax +gogl +medo +gshf +hideip-italy +loke +gardena +www.zero +windows1 +fap +baikal +driss +juridico +goldman +lemur +joen +dk2 +ouroboros +mathews +mathias +ql +psyche +syed +nikolai +www.study +maruwa +render +www.zend +www.xtreme +banane +ocio +edu10 +asgadmin +topcat +fs3 +fs4 +amail +shivam +j3 +kurihara +md5 +vie +reni +hairy +styleguide +raza +syndication +sotm +artlove +afd +sinbad +bypass +jsd +achille +greenhouse +gmc +dimension +fervor +smtpmail +wisla +www.advert +arquivo +l1 +dayton +p1-all1 +nirwana +beian +mutation +office3 +filter3 +www.mlm +104 +lyncsip +vhost1 +sis2 +seek +competitions +iperf +neotest +hifoods +dwarf +chat3 +supergirl +dt1 +lklp1 +lklp2 +lklp3 +lklp4 +lklp5 +rflp4 +rflp1 +rflp2 +inuyasha +rflp3 +www.estore +rflp5 +st0 +st7 +vpn11 +msdesign +ausbildung +ghost2 +comtax +jrhms +molotok +wakayama +tokushima +madan +baja +savoy +www.p2p +pace +aucc +efile +spamfilter2 +kitahara +shenyang +spamtest +www.porn +malak +photo4 +cass +www.kansas +www.webservice +www.park +onlinekatalog +www.louisiana +coding +www.next +www.utah +delaware +gameover +pennsylvania +www.maine +gambit +www.lost +alok +bongo +www100 +mpc +www.liga +zaid +chandan +apollo-v +d.i0 +d.i1 +havok +warta +banshee +ws21 +www011 +d.i2 +www-hac +ws22 +d.i3 +mys +d.i4 +visage +webdisk.central +www.ecom +tarek +creativemind +azur +d.i5 +opr +info7 +www.clip +bcr +edf +www.boom +ns130 +sm11 +www.cr +par +urlaub +nadir +asad +d.i6 +www.mailbox +d.i7 +callum +www.tasks +ucupdates-r2 +sptest +www.aaaa +kal +vm04 +openhouse +bans +d.i8 +thiago +d.i9 +vm05 +scuttle +search3 +web50 +www.hu +lucky777 +arno +inblue +domino2 +pegase +osp +hichem +pc10 +ipade +bk15 +www.fa +landau +w3c +kvm01 +lares +bounty +jsw +yassine +valley +www.relax +hephaistos +sesame +eole +irce.sac +irco.sac +love4ever +dsj +aliraqi +zono +wsj +correo2 +dawood +saddam +davido +haydar +archivos +ouranos +tns21 +www.u +b.i70 +webdisk.webinar +www.test123 +comfort +webtrack +afaceri +tribal +thelord +kauai +pnj +webdisk.noticias +e-shop +lazaro +www.alt +atos +blop +zabava +webdisk.ask +duckbill +www.gc +ume +135 +www.fishing +test008 +everyday +www.spam +hacked +rp1 +battery +www.um +skate +ssl8 +ecos +arco +flashgame +myproject +www.il +myfriend +cadastro +nicky +chucky +cot +lollipop +krystal +ando +mdt +www.bw +coolweb +122 +lab2 +hyena +cal02 +cal01 +127 +rad01 +bots +picpost +ddos +farhad +s154 +www.messenger +mort +jasmine +lcr +origen +management-uat +fernanda +cpmail +zixgateway02 +www.eedition +havefun +hotsex +ftp.mail +cina +pcserver2 +davy +mmsc +studenti +shit +boletin +iem +prague +parceiros +nereid +webtrac +outcast +escort +micasa +168 +nitrox +202 +www.anuncios +999 +www.honduras +doremi +r80.i0 +www.magnitogorsk +r80.i1 +r80.i2 +stylist +veda +verity +cisco-capwap-controller.net +janice +gekoo +mellow +surfing +r80.i3 +vivek +cuc +apophis +paulo +shah +2000 +idiomas +furni +www.lifestyle +golf2 +skkk22 +nomade +material +sophosav +seeit +lsmb01c.lsdf +rentacar +power3 +r80.i4 +bigcity +www.pedro +sonic3 +imanager +r80.i5 +acura +lsmb02.lsdf +r80.i6 +www.deal +raiderz +r80.i7 +r80.i8 +afroz +r80.i9 +found +members4 +qatest1 +prestige2 +contenidos +mp7 +www.nokia +mp26 +r230.i0 +r230.i1 +dnd +r230.i2 +www.argentina +r230.i3 +loadbalancer +zxcvbnm +dpa +sonic4 +r230.i4 +sonic2 +lucian +s170 +www.holidays +box13 +lorena +mrelay +r230.i5 +sgmail +chihuahua +tmd +webdev2 +melkor +box3 +vermont +stevens +r230.i6 +myprofile +protein +ovz1 +ap3 +myweb20 +spamserv +newgeneration +onapp +www.ssp +andri +belal +dahlia +betta +egis +fetish +xe +dominion +drugs +ashok +drone +testtesttest +january +elly +montada +francois +portia +mongoose +emil +chang +draconis +esxi05 +bisnis +ashraf +myrose +chico +hotplace +reflex +www.hot +s198 +vantage +crisp +bnat +ash977 +r230.i7 +autoconfig.novo +dario +autodiscover.novo +webzine +asd123 +aisha +spanky +aiolos +dejan +ton +www.box +reg1 +zixgateway01 +www.admission +www.wa +www.bid +live4 +www.arts +dimas +cawaii +dandy +verio +www.ada +www.ace +extmail +nsw +assess +cna +spamfilter1 +brics +techie +chips +r230.i8 +r230.i9 +netlog +johnson +www.imagens +r230.i70 +drlee +www.nic +r230.i10 +chara +axion +blade14 +vps024 +cyril +tsl +myserver +uae +vps100 +vps101 +www.euro2012 +farah +edtech +netshop +r230.i11 +bcd +r230.i12 +cardiology +reynolds +carol +devapps +r230.i13 +rosso +vps105 +hp2 +api.new +resnet +r230.i14 +arora +kakashi +kalyan +hibu-portal +fendi +casablanca +rvip +megaupload +tkd +esoft +whsil +blackfriday +filestore +vps119 +r4 +r230.i15 +rajiv +bassem +movie1 +bassam +verio-portal +r230.i16 +2007 +rit +hml +haker +worldwar +playgames +r230.i17 +package +baraka +vps109 +r230.i18 +munna +chipmunk +uh +webmail.panel +nyc2 +raj +builder.panel +kaylee +vps114 +hawai +basketball +apac +metric +deadly +guess +r230.i19 +enrique +darkangel +entourage +filemanager +www.camp +r230.i21 +monolith +tiffany +partner-portal +hoang +aspirin +r230.i22 +apollo1 +wonderful +hours +googleservice +r230.i23 +shabaz +holistic +tab +ronald +elink +exp1 +folkart +iherb +ebanking +taekwondo +madness +avp +ns127 +atlantida +mii +pil.qa +ukraine +jackbauer +homework +pilqa +pnp +benben +saerom +severodvinsk +oac +possible +lv121101224503 +library2 +iwillbe +flute +karel +cc3 +theking +petrus +adolfo +r230.i24 +katja +83 +hadar +office-gw +las +r230.i25 +domain.control-panel +wecare +amore +amara +besmart +r230.i26 +thoth +mf3 +r230.i27 +r1soft +webdataadmin +www.mails +dreamers +pdu4 +www.chel +overdrive +allianz +r230.i28 +apc5 +billion +santos +r230.i30 +teta +mizar +project1 +singularity +simolly +asma +sakshi +r230.i31 +r230.i32 +serv01 +host14 +fim +majid +www.mybook +wakaba +router11v13.zdv +manny +r230.i33 +lviv +proxmox +sena +suncity +staff2 +seguridad +www.museum +location +r230.i34 +cnki +ths +wlddy129 +www.rainbow +daesung +www.catalogue +oracle1 +iframe +r230.i35 +smaug +r230.i36 +www.east +portuguese +m360 +abcxyz +ss3 +lavender +micky +uv +r230.i37 +lewis.ucs +naser +rajawali +navid +galactus +invoices +jamestest +r230.i38 +recursos +www.warriors +cdnet +r230.i40 +brownie +mohan +fax2 +hss +nameless +zerocool +akram +r230.i41 +basement +rik +shopping1 +r230.i42 +zin +bohr +ringtone +roundtable +podarki +sleipnir +finaid +harem +r230.i43 +castest +bst +loving +www.kitchen +bci +mybox +pdu3 +blogs1 +blackrock +dol +abcabc +r230.i44 +r230.i45 +nibbler +koka +dangban +artgallery +flyhigh +piero +post2 +r230.i46 +jdc +tta +test123456789 +musicvideo +r230.i47 +slave2 +vitality +enomoto +r230.i48 +cts +solidworks +renata +fra +spokane +investors +r230.i49 +learnenglish +cms01 +r230.i51 +r230.i52 +rafik +companion +www.merlin +r230.i53 +r230.i54 +r230.i55 +tahiti +phototheque +rakuen +r230.i56 +snoop +jaya +www.action +thea +slider +norbert +yasser +affiliation +notification +webmail.blog +123456789 +rowdy +mostwanted +r230.i57 +www.gls +listmail +r230.i58 +flashback +roach +ronin +vz10 +iakas +buzon +algebra +plesktest +www.ccc +rim +sportscards +humanrights +www.ash +gate5 +a10 +orwell +shibby +youyou +spade +asdasdasd +mail.new +vam +shenzhen +freetv +imperium +crocodile +yosep +r230.i60 +r230.i61 +demo22 +r230.i62 +mcr +r230.i63 +video01 +question +dejavu +r230.i64 +r230.i65 +ped +cookies +java1 +tmail +r230.i66 +heath +medic +sumit +www.enterprise +suraj +skincare +habbocoins +www.view +moment +raymond1 +siren +mma +afl +yomi +speedup +tiara +a11 +actividades +yjsh +mercy +www.exam +pc01 +wake +complex +asptest +srv25 +vpndr +tyrex +imarketing +toolkit +sunray +what +mmail +wawan +r230.i67 +www.aqua +minimalist +r230.i68 +fantasio +rockstar +flor +b.i19 +r230.i71 +r230.i72 +makeup +r230.i73 +www.yo +hacks +trivia +djh +www.os +newtest +tong +busca +toma +www.mk +sugi +abdellah +titi +vitamins +r230.i74 +mangos +www.hm +osx +r230.i75 +yah00 +r230.i76 +kumquat +dania +admin.staging +in-discountvouchers +marios +in-v4 +gds +reserv +r230.i77 +r230.i78 +norma +bibliotecas +www.rich +goga +vilnius +exim +limit +www.warszawa +turquoise +cascade +starworld +thumper +infamous +www.empresas +capture +centenario +sv0 +arius +vini +r230.i79 +pon +r230.i81 +rema +rn +column +alfredo +webmail.extend +hex +r230.i82 +r230.i83 +svn3 +r230.i84 +kona +lms2 +psicologia +sweeps +bangalore +adweb +r230.i85 +firepass +hiroyuki +wlan-controller +crucible +poem +r230.i86 +ella +otrs2 +upload3 +jdih +futsal +talktalk +sai +lu +r230.i87 +de.test +mental +private.search +www.pandora +actus +hc2 +www.station +hg2 +geonet +pauline +vodoley +gugu +elsalvador +blueteam +suzhou +bene +hobo +http3 +smsapi +evelyn +flair +motd +market1 +lol123 +cleopatra +r230.i88 +jordan23 +www.klub +simbridge +www.lang +moro +websoft +medya +nasser +swp +vps01 +myvideos +asl +healthline +r230.i89 +blueocean +keywords +moma +fozzie +pmis +admindev +clever +herb +grandfantasia +imad +edition +listserver +gram +santacruz +speaker +ru1 +elizabeth +degreeworks +fr2 +www.espanol +pkp +nani +uz +mile +gratuit +r230.i91 +cisco5 +server09 +michaelm +xyz1 +www.dk +r230.i92 +autoshow +buzzard +lppm +flashchat +mate +mann +louise +sprinter +sportsmedicine +less +ipkvm +corp1 +version2 +koda +r230.i93 +munich +r230.i94 +dns2.inf +mydream +www.free2 +k3 +bse +r230.i95 +r230.i96 +gym +urp +stumail +greencard +baike +illu +alcohol +r230.i97 +r230.i98 +vnet +lionel +mysql55 +freegames +gk1 +hoya +muslim +bradesco +znakomstva +alex123 +www.face +hong +r230.i99 +b.i30 +rincon +aviso +gorgon +nsr +b.i39 +freeway +high +r80.i20 +b.i50 +r80.i30 +arb +autodiscover.newsletters +v8 +head +paginas +happyhour +www.advance +autoconfig.newsletters +b.i60 +dalian +r80.i10 +glam +alireza +shura +onlineshopping +www.hentai +oldbbs +toa +skif +yjsb +robson +ipa +zlgc +www.fusion +gggg +pankaj +sleeper +fm1 +ryohchan +gamp +five +peer +offshore +renegade +pic4 +back1 +symfonia +slogan +artek +violetta +ramen +kiko +mabs +www.hacker +test1111 +www.juegos +b24 +r80.i11 +r80.i12 +readers +r80.i13 +usvpn +dca +b25 +micro2 +rudolph +trabajo +fsp +r80.i14 +b28 +b29 +cona +etax +kenko +www.admissions +www.campus +koks +r80.i15 +modx +mail99 +healthy +pl2 +kec +r80.i16 +korn +b39 +ps2 +activities +disability +edukacja +devweb +webline +dale +tema +megaplan +lionking +manu +donation +r80.i17 +travelworld +b49 +metall +mash +kuki +r80.i18 +mero +palma +yavin +rapport +www.webhosting +r80.i19 +masha +r80.i21 +loly +institut +fleo +r80.i22 +katia +tanit +automobiles +ourspace +webdisk.tutorial +nejm +caronte +www.tp +cara +www.rs +arti +tsukasa +www.nz +r80.i23 +storage3 +asem +moza +www.mz +es1 +pissing +admin.demo +umc +yuka +routing +anni +starscream +www.lp +mip +rel +r80.i24 +gardening +lille +r80.i25 +mylib +edt +purchase +r80.i26 +tomasz +cta +iga +know +effect +rand +www.fj +sono +aeris +www.el +mount +r80.i27 +choice +autodiscover.magento +r80.i28 +autoconfig.magento +r80.i29 +accord +r80.i31 +netoffice +webstyle +r80.i32 +tupper +hotjobs +dreamseed +c36 +last +sedna +r80.i33 +quimica +kasai +brutal +mailout1 +kill +vlast +r80.i34 +r80.i35 +www.try +d17 +r80.i36 +d18 +www.empleo +nyc1 +cisco2 +fotograf +paysites +mora +kenya +r80.i37 +hdd +r80.i38 +d23 +r80.i39 +d24 +r80.i41 +tvs +r80.i42 +r80.i43 +r80.i40 +r80.i45 +r80.i46 +tribe +d25 +mods +newsupport +r80.i47 +r80.i48 +r80.i49 +d26 +d30 +sublime +design1 +r80.i51 +susu +tenis +backend2 +pvc +smartnet +r80.i52 +r80.i53 +r80.i54 +addc +r80.i55 +r80.i56 +r80.i57 +r80.i58 +r80.i59 +r80.i61 +sears +posh +pochta +r80.i62 +svi +hostgator +r80.i63 +buffy +e22 +contracts +content-test +r80.i64 +nat4 +spongebob +synchro +citrus +staffmail +r80.i65 +r80.i66 +yves +prem +lonewolf +herbal +standards +autodiscover.books +autoconfig.books +toucan +kora +sql6 +host29 +sup1 +fw-ext +host31 +nat3 +chubby +kbs +webdisk.legacy +vmware1 +pool-node +r80.i67 +www.filosofia +www.systems +fpk +b151 +ball +canberra +www.cursos +zebulon +simona +boca +christ +bluestar +cosmin +avdesk +webdisk.shopping +syrup +214 +4ever +mtk +www.logos +sago +ns.m +ebe +r80.i68 +antenna +demoweb +r80.i69 +abl +ects +mssql7 +tj200 +mssql6 +borderless +bremen +kater +woo +pu +q4 +fop +r80.i44 +atv +blabla +saki +webservices2 +mayhem +r80.i72 +tuk +c20 +r80.i73 +cpk +d20 +r80.i74 +r80.i75 +d29 +igame +r80.i76 +educatie +wikipedia +r80.i77 +templar +r80.i78 +sue +kvm6 +f10 +semi +wmail2 +sera +r80.i79 +grc +shon +habitat +skt +r80.i81 +r80.i82 +still +r80.i83 +smsgateway +miroslav +scuba +rip +star1 +owa1 +slate +r80.i84 +dandelion +livetest +ols +www.bj +meetingplace2 +webdisk.auctions +vds7 +warlords +r80.i85 +chichi +vds15 +hqc +workshops +vds16 +newportal +vds10 +vds12 +mrx +me2 +webdisk.hotels +droid +crack +vds18 +s71 +eight +vds5 +vds6 +sour +autoconfig.fr +vds8 +autodiscover.fr +r80.i86 +rowing +loveyou +veranstaltungen +vm10 +35114 +cocos +36114 +fr1 +s91 +tico +dnsmaster +arh +kefu +ksc +ilove +resultats +kao +ubnt +r80.i87 +sule +r80.i88 +pcgame +snail +vds9 +r80.i89 +lyrics +idol +dlp +r80.i91 +s75 +tris +gnu +waf +turk +paulus +waps +cir +r80.i92 +r80.i93 +wsn +r80.i94 +r80.i95 +panoramix +iflow +www.ai +old.nrelate +momo1 +www.maths +xeno +ontheroad +r80.i96 +actu +internalmailrelay +sgc +www.mech +mystique +r80.i97 +r80.i98 +btb +spe +autoconfig.articles +xone +autodiscover.uk +oskar +www.horoscope +harace +www.miss +pathway +www.brazil +aki +autodiscover.articles +webdisk.uk +r80.i99 +help1 +www.revolution +r80.i50 +b.i80 +www.korea +creatives +kep +dynamic1 +r80.i60 +st5 +cons +b.i90 +shake +ingrid +impala +ramadan +bolivia +strategy +uspeh +bestfriend +app02 +r80.i70 +sanjay +cmax +r80.i71 +www.webcam +wed +mail-3 +mens +resonance +www.form +charming +eld +ncaa +fairytail +www.outlet +a.ext +ranma +r80.i80 +image4 +fiorekorea +r80.i90 +enlighten +oscars +ems2 +webdisk.suporte +websitepanel +rubin +zola +obi +monopoly +belinda +giochi +darko +sunjoy +dealclick +interhost +ex1 +r230.i20 +d.i19 +webdisk.themes +autodiscover.themes +autoconfig.themes +td3xgamma +reddragon +unic +www.cas +gdc +r230.i29 +adhara +www.cma +zoro +ecms +hao123 +m.qa +torque +d.i29 +d.i31 +stupid +techwiki +trustees +d.i10 +www.eva +lookup +d.i11 +d.i12 +webdisk.contact +quan +adam3 +kobi +www.stone +d.i13 +asti +www.stars +adam2 +mailhub1 +d.i14 +invisible +www.owa +nfs1 +www.onepiece +d.i15 +granite +abnormal +parliament +adventure +obm +enoch +www.salud +props +www.cb +ghost1 +energia +precision +core4 +sbaweb +rybinsk +websystem +autodiscover.analytics +www.tumen +jumper +hse +tone +concierge +www.est +udp +serv5 +autodiscover.social +autoconfig.usa +piggy +webdisk.usa +mailscan +restaurants +autoconfig.analytics +webdisk.track +zenit +campbell +autodiscover.usa +thomson +noreply +grafik +d.i16 +coal +d.i17 +sadewa +reebok +avrora +wac +maila +czech +fip +classics +igre +feynman +konferencje +d.i18 +ala +d.i20 +www.be +elm +nori +online-test +d.i21 +pushmail +d.i22 +d.i23 +karolina +waterloo +goethe +www.eb +d.i24 +oferta +cepheus +s243 +www.fish +ksk +d.i25 +qd +spoc +wizards +mam +www.seed +www.rap +arms +d.i26 +admin.new +d.i27 +windsor +webdisk.s +d.i28 +d.i30 +tur +r230.i39 +www.3d +nuage +spx +www.houston +bode +cloud5 +pchan +d.i32 +comtest +newblog +d.i33 +tribute +nweb +format +clinton +kb1 +qzlx +abram +aruba +www.historia +able +www.clinic +typer +vacation +xxh +cancer.ucs +cntest +bohemia +www.berlin +southwest +www.starwars +c-asa5550-v04-02.rz +www.adidas +podcasting +aristotle +www.mh +unicom +dev9 +d.i34 +vaughan +gamebox +c-3640-v03-02.rz +zamani +rencontre +merkury +atmos +fibonacci +c-4402-v03-01.rz +c-asa5550-v04-01.rz +worcester +greenville +montage +rx +b75 +ketban +robinson +b73 +pczone +black1 +sri +olddev +dublin +othello +sitio +etraining +akron +professor +lucky13 +prom +harvest +www.backoffice +www.golden +flv1 +autodiscover.tools +autoconfig.tools +flash1 +bht +doc1 +chibi +aman +senat +nou +fourier +gcweb +kgb +demotest +d102 +www.ayuda +abby +wn +mycroft +www.mca +www.tb +russell +vcenter5 +dl4 +ldap4 +intouch +autoconfig.teste +dirsync +autodiscover.teste +cmsadmin +pune +zigzag +xboxadmin +redapple +kenneth +micah +webdesigner +d.i35 +www.alice +niel +d.i36 +nhac +poste +merc +vpscp +fairfield +dev.mobile +secondlife +jeeves +av2 +publicftp +wild10 +d.i37 +d100 +www.chicago +bangladesh +cardiff +renaissance +iblog +leadership +kimjm +ofis +www.smtp +afterimage +webmail.manage +ombudsman +dla +fibo +fort +grenoble +aoc +nieuw +screenshots +zillion +testservice +xxl +minfin +www.gap +zan +samo +irc1 +ernest +eh +prospero +phoenixguild +wiz +gv +www.oxford +hspc +jo +ws122 +net3 +dpr +m.media +ns171 +ns181 +rowlf +cockpit +autodiscover.go +zpanel +nofear +ucc +cena +ws202 +bayes +autoconfig.go +pyrite +mail003 +sysaid +pm2 +sni +tokiohotel +sng +andante +236 +shy +www.s5 +www.surveys +catapult +www64 +peugeot +mct +aap +powerdns +qqq +acr +upi +zerone +metin +elohim +saab +osa +server34 +www.halloween +server37 +fausto +server39 +vcenter2 +imghosting +server55 +dining +apa +mlb +rhubarb +lsj +lsd +matin +giuseppe +nas4 +salut +ceti +dido +mft +lpg +hogar +medved +llama +h70 +www.college +m15 +h16 +h17 +h18 +lf +h19 +jks +ere +denial +www.wow +firenze +ddr +jhc +jap +esx8 +s60 +paddy +dmp +ngwnameserver2 +mycampus +drs +dsm +imgmulti +newwebsite +partizan +usa2 +constellation +www.production +etm +www.v5 +evg +han +gep +webdisk.panel +teststore +gil +ggs +webdisk.erp +promo1 +autoconfig.erp +misaki +autodiscover.erp +ipt +elt +civicrm +programy +dks +sigma2 +lap +operation +bookit +din +mail.sex +chj +ngwnameserver +www.its +boy +www.tapety +inmotion +cbm +saeed +extrem +h2h +hustler +xcite +ossec +bch +videodemo +inhouse +www.ftptest +raghu +bb2 +host112 +sheila +arhiv +verne +loan +origin-m +x5 +webui +bigone +smtpauth +gilbert +mamo +www.bib +sv51 +maca +vpc +cadmium +dago +soar +teo +music2 +wot +onenet +congress +bunker +abaco +lazer +webqa +gaby +kkkkk +serveur +www71 +www66 +www49 +www74 +www54 +www62 +ds7 +aron +july +www63 +noodle +www65 +www67 +toot +www56 +nc1 +www55 +www.imagine +redirection +greenday +www48 +host100 +adb +www53 +host103 +mda +vik +everyone +aberdeen +ooc +latitude +subdomain +www.direct +loans +www.omega +tmt +stv +distribution +seb +tsw +eam +web2008 +dnt +gbp +hns +kdl +esg +bpi +annapurna +staging3 +testwebsite +cs7 +curie +sdx +mx.dev +vertigo +sandie +ghs +host114 +autodiscover.dir +gtp +ptm +webdisk.dir +bluebox +shinbodenki +host115 +itf +webwork +pdg +tigris +autoconfig.dir +sccm +simka +autodiscover.hosting +qam +monitor-dev +autoconfig.hosting +msf +angeldesign +host118 +www.properties +ol +msweb +wu +nonprofit +flying +phuket +mna +kobold +shx +uucp +weixin +pam +mdp +pit +lota +pinnacle2 +s150 +ns151 +dhcp3 +zhuanti +netinfo +julien +kundencenter +fortis +liveupdate +savremote +bishop +sii +fotoservice +tgs +fmp +svn1 +polis +fil +vb4 +harrypotter +phpmyadmin1 +win29 +bistro +win26 +win25 +win24 +crm2011 +newhome +windows2 +curso +mack +shoes +win01 +betelgeuse +cavalier +outils +esolutions +arena1 +dylan +michaeljackson +grab +09 +band +cle +alpha3 +pumba +gunsnroses +mysoft +web155 +www.offers +betting +comps +web153 +db7 +beko +db10 +www101 +4all +web152 +priv +autodiscover.development +flyfishing +himawari +autoconfig.development +w12 +bowie +portable +robbie +dhcp.zfn +ftp16 +gail +erptest +ftp17 +biblos +aragon +provider +gemini2 +web134 +kakaku +test08 +web112 +cornerstone +www.genesis +cp03 +corex +capecod +drag +mm1 +staging-www +web103 +emax +cp14 +www.biology +psearch +pyro +web100 +darshan +over +ws111 +angeles +pagamento +ftp20 +informatik +dns-1 +sba +analyzer +terminal2 +banking +ship +se4 +elsword +ital +ws112 +joda +kila +nsf +ws121 +temp3 +ws131 +nizhnevartovsk +ws132 +www.gfx +softball +pano +theory +web211 +ws141 +exch1 +78 +prefect +www43 +ws142 +clyde +ws151 +ws152 +alexx +geo2 +ws161 +ws162 +enjoy +ginga +coma +remi +ilan +ws171 +web81 +web55 +82 +zimbra1 +ws172 +siti +web37 +filr +moses +85 +ws181 +static6 +webdisk.services +ws211 +www.jb +tess +enlaces +sge +financialaid +84 +webapp1 +ws212 +websvr1 +tabi +indianapolis +connect3 +cp16 +cp11 +cp05 +promise +historico.vestibular +devshop +fencing +vmhost1 +dbtest1 +sc1 +precios +www.extreme +mvs +hubbard +manabi +straylight +planetlab2 +planetlab1 +gw8 +webserv +payonline +www.j +appgatecl +jimi +pdns5 +khayam +stu1 +su1 +dena +sahand +atena +acme2 +axle +scar +resize +acceso +quince +redirect1 +net4 +mx-4 +sdfs +belgique +pier +juggler +pepo +onex +dell1 +allinone +blackshark +cmf +movi +srinivas +mylink +d112 +mcdata +forma +hcm.nhac +sessions +us.nhac +intranets +dev8 +angarsk +vm-jorum-live +ulanude +sanger +www.katalogi +katalogi +shaw +kitkat +kidney +yew +weboffice +dms2 +pwd +www.everything +imgg +imax +valentin +yankee +hexa +here +account2 +metalib +mensagens +ws-payment +www.czat +bipolar +beneficios +cs13 +aac +edd +www.50 +www.po +saravana +win03 +iba +s10145 +host40 +host37 +benten +ups1 +www.dh +caf +ssmtp +spd +vs6 +host27 +santamaria +argent +host26 +dcm2 +tyros +carreras +so1 +cyrene +nathaniel +workgroup +terpsichore +mail001 +elecciones +interact +seshat +arek +callofduty +libcatalog +torch +www.lego +bosei.goto +images.platform +dco +dec +fuze +interracial +santalucia +folderman +blag +dom2 +acache +host24 +ecn +antiguo +ccache +hiring +fern +host50 +www.chatbox +professionals +host34 +dpe +www.sami +preview-domain +host32 +divinity +fet +www.database +selma +karnage +ficheros +cl01 +www.hack +www.killer +emeeting +womenshealth +majestic +domainadmin +mxi +mxo +lohas +knowhow +www.miass +msu +jag +vyborg +origins +northwest +vps128 +vps127 +muhammed +jst +cloudcomputing +vps020 +webdisk.www +www.signup +vps110 +vps034 +media-1 +pics1 +pics2 +mcserver +mvm +diz +dga +www.arhangelsk +ulan-ude +www.police +sankt-peterburg +www.ivanovo +epub +hiraoka +lbc +khb +matsunaga +seto +poplar +adachi +ssl6 +ssl11 +dalton +ioc +ilm +camille +jan +solon +djinn +rko +muppet +venise +moorea +budo +test.api +iif +italian +zazcloud3 +zazcloud2 +zazcloud1 +ishare +tik +gad +fer +casual +fec +uo +www.si +fca +fao +dta +libftp +grendel +e-learn +ntp01 +mantenimiento +onecard +ermis +beautystyle +ew54384r9c9hyy +company1 +freespace +nfe +rw2 +rw1 +tos +ibk +mftp +bragg +test003 +test005 +amd97 +test0429 +netowl008 +oldhost +nbc +tubo0626 +arthritis +painting +cce +ecomm +charis +umar +itil +bahamas +wwwstage +www06 +www05 +bigfoot +messageboards +www07 +stage01 +www.answers +countrymusic +vox +vweb1 +worms +zep +admiral +k4 +brenda +lemlit +www.ranking +www.harmony +biologi +proxy6 +gw01 +quitsmoking +destination +partnersite +experimental +cmsdemo +wincp +xs2 +lll +rc1 +elms +www.secrets +saurabh +domainmanager +www.bolivia +concrete +bsi +valera +www.filmy +www.muzica +ksiazki +rda +www.interface +www.millenium +departments +112233 +tse +jail +localhost.test +nwoclan +www.roma +www.ptc +usp +zn +zappa +haunter +gs3 +aspnet +sym +sv55 +sv33 +sv32 +sv12 +sv11 +www.dn +sv14 +corina +kurdistan +malina +kvm5 +www.bulgaria +oto +gods +hala +dmo3 +xymon +tlp +www.emperor +tribalwars +sitestudio +wesam +comprar +hutch +wilkesbarre +mtm +aozora +mrt +cristy +bench +musicians +starz +monitor4 +sysy +www.key +fiatlux +ralf +animax +chapel +www.les +www.belgorod +sadik +reign +6arab +ctn +edn +www.neu +ena +kaizer +epk +buyandsell +kaz +ese +ocm +witnesses +web2011 +web2009 +11111 +aspect +inthebox +santamonica +parody +benji +admon +zmm +mx.blog +grandcoteau +gi5 +229 +cours +mypics +fem +alucard +ftp.media +calidad +www.sexshop +gradius +7oob +tai +www.egresados +acj +vbtest +angel2 +nail +potter +akb48 +honeypot +csl +minimal +tvguide +tv3 +coe +bolton +disclaimer +cmg +webdocs +vacancy +bro +citrine +mprod +awp +praxis +cate +gb2 +pixfirewall +boardroom +coolstuff +applyonline +f8 +modelo +asm +capitol +newbbs +weblab +kettler +aigle +rainier +apn +www.dell +www.surf +kmv +bron +amu +azc +sanok +sustainability +indo +h24 +ale +acs3 +ftp.ads +localhost.new +vcp +amie +s925 +sturm +mt4 +www.sanok +e-academy +itmail +signature +peanut +www.bydgoszcz +szczecin +mail-in2 +aces +adm1 +www.olsztyn +kay +faye +mxrelay +activa +www.xbox360 +sig2 +tda +whistler +roses +chao +bta +devsupport +ethel +mcdonalds +recall +allison +arsiv +yi +mikolajki +lsaccess +pila +ike +www.underground +violeta +oce +xara +iz +xfer +gag +ntserver +polling +vitesse +124 +lemmy +115 +www.psd +108 +3e +102 +internacional +107 +cyrille +mt2 +webedi +vungtau +buff +othman +vpnex +agw +dst +darin +antivir +inventor +ssg +mist +kurt +client2 +jona +stanley +espero +raul +smooth +mum +volt +reuters +db03 +www.deportes +rpl +storehouse +miri +alex1 +2pac +alternativa +secim +wee +onetwothree +reese +intermed +kolkata +tus +loc +www.arena +paranoid +www.bingo +kobra +prazdnik +chaser +dasher +www.tambov +mony +www.tms +dpstar +hos +sev +neptuno +campfire +www.chaos +msg2 +wm4 +www.brand +www.konferencje +loop +entrance +spider1 +omserver-iscsi1.srv +miner +bux +cop +serv11 +nw2 +energo +st16 +plotki +st10 +st13 +aip +whitepapers +www.denis +fw4 +www.intl +www.princess +www.dot +rets +ims1 +theboss +kes +aspen +www.olympic +chemical +oid +manila +www.annuaire +hiburan +salomon +pland +pbx2 +rave +cob +banco +saul +out2 +carnage +doug +jtest +hilal +maintain +rptest +salo +www.cyber +lwp +sabre +jjxy +autoconfig.wap +webdisk.wap +autodiscover.wap +wfs +mink +dstest +sstest +avantgarde +mote +talon +fulcrum +luckystar +www.first +webmoney +yana +achinsk +oceanos +ariadna +www.popup +freehosting +zaza +www.girls +gluttony +auckland +mme +www.ed +doin +libcat +tuananh +carto +makassar +aann +adda +misa +addy +badr +alix +dim +ally +bmw1 +guanli +beat +bebo +amun +idp1 +ewb +lp-infracom +manis +lp-interbusiness +bibo +caci +asif +ashu +arya +www.stk +amway +appa +vladikavkaz +nsl +gaz +kolomna +hooligan +mole +webdisk.hr +tut +bold +boot +azad +chiz +dano +davi +buro +acct +bk4 +www.kevin +ehab +carte +cccam +seotools +www.beta2 +bod +hajj +residentevil +hosting0 +box4 +martins +lilly +khabarovsk +tycho +clement +dilbert +sipfed +agni +fulton +web70 +shelby +greene +bertha +mail.plus +www.basket +bigstar +kabu +miledi +vili +gapp +www.motor +vineyard +citrixmobile +graham +maks +ord +www.politics +godlike +kampanj +peixun +jsb +200 +kontakt +gertrude +qks +please +luisa +magia +agm +gauguin +glenn +kristina +furious +osk +gmp +ipb +ipd +sintra +lav +www.andrew +foxx +ome +mail.staging +tat +kxfz +bwch +vit +overlook +admintools +fito +pc02 +sitebuilder2 +wolfe +tati +universo +gola +delo +dies +gora +edem +svet +www.rabota +oscar1 +skorpion +hina +hinh +rtrarccore +muaythai +systeminfo +neruda +scruffy +ehra +jago +elektra +hieu +imas +da3 +sibir +imghost +iter +facility +skill +forfree +radiomaster +mail.office +matan +dnepr +tashkent +ania +www.doska +mirror4 +gamedev +expresso +mail.info +kely +www.sonic +www.texas +mmr +nep +portrait +together +starstyle +mta4 +www.diamond +pop.m +smtp.m +ftp.shopping +dnscheck +webdisk.stream +maas +lips +ak1 +kuro +www.vegas +gerardo +mich +cmo +division +neha +mrak +nikki +engel +homeloans +lyncws +kras +www.special +lnx +media01 +www.avatar +web2010 +pein +rico +peng +media03 +opia +pos1 +www.promote +contingencia +outmail +pop01 +redo +kks +195 +fst +pelops +4arab +raa +ron2 +crypt +phorum +lombard +shar +webdisk.traffic +simi +simo +wlw +iweb1 +tami +freemusic +stormwater +souk +autodiscoverredirect +www.asp +webdisk.pt +stfu +lsm +usmail +vulkan +cnmail +tobe +tuba +netapp +flexmaster +senna +wala +xdsl +aloe +worldnews +fallback.preprod +inw +autoconfig.vietnam +abner +kuwait +www.mock +www.wetter +www.mexico +autodiscover.vietnam +www.mmm +autoconfig.uk +www.egypt +lecture +ss01 +mbeta +denise +juju +www.kerala +sharepoint2010 +pbi +prasad +server81 +www.smf +bc2 +wg1 +www.webmail2 +tm1 +webdisk.events +seo1 +triad +krasnogorsk +www.44 +www-neu +www.vl +cms4 +webprod +www.pagerank +www.kurgan +tourist +emag +rmm +simpeg +psinfo +kariera +openvas +odysseus +flag +www.jamie.users +www.eozkural.users +fwwilson.users +www.angelware.users +davard.users +www.malkara.users +markufo.users +www.mg4rci4.users +citrix3 +www.trial-4e2df4.users +zsjy +jessicagrehan.users +jonathanmann88.users +ldapserver +helensoraya.users +timesheets +www.sls +www.kruse.users +adm3 +www.trial-14d203.users +www.lukestuff +chltlahs +www.finntimberhomes.co.uk.users +arias +www.trial-f40c2e.users +www.rubber-facts.users +www.johnmcmanus.users +www.test1.users +www.premieredance.users +www.securehost2044.users +www.moonrakers.users +bleronuka.users +www.test.andi.users +www.humble.users +db12 +db11 +decor +vm14 +vm13 +robtest1.users +conta +skippy +www.moonjam.users +dhcp02 +www.tpj.users +www.mariajane.users +www.bohemia +lovejoy +vds20 +vds19 +starmusa.users +drink +www.sunrise +pcbscott.users +vds11 +vds17 +minneapolis +www.alexmountford.users +www.alexb.users +www.carly.users +www.cyanideshock.users +vds24 +vds23 +www.a1 +vds21 +jezz.users +kata69.users +sweepstakes +molibi.users +newmarket +trial-4e2df4.users +michael999.users +vm9 +vm8 +toupiao +mushroomgod.users +macau +stuckey.users +gomobile +yandex +buka +tadimeti.users +beasiswa +www.ukbikerz.users +www.omoikitte.users +flore +flori +www.cardigan.users +socialengine +brp +przemo +banks +freud +www.jessicagrehan.users +fireworxstore.users +mikepower.users +bex.users +mst2 +bertrand387.users +publicshare.users +www.mrmarkmountford.users +alutto.users +newchurch +www.plants1966.users +www.slider69gdw.users +phpmyadmin01 +paulmasters.users +poptest +blackcat.users +new.shop +backup1-10 +www.msfbiz.users +guido +www.alex3410.users +rouge.users +phone3 +www.artsutorus.users +laboratorio +hoshi +lmarsden2.users +www.c-electrical.users +chrischarlton.users +pasadena +www.php54 +www.onlinedatingguru.users +www.fireworxstore.users +www.lyonsqc.users +www.europa108.users +trial-14d203.users +backup1-1 +backup1-2 +backup1-3 +backup1-4 +backup1-5 +backup1-6 +backup1-7 +backup1-8 +backup1-9 +www.trial-37e040.users +khainestar.users +trial-38af15.users +www.forasf.users +jinerenco.users +redhotme.users +www.reumatologia.users +www.magento.sapin.users +cartridgeworld.users +www.robtest4.users +www.dynamic +o3 +dhingli.users +ivana +hindsjohn2.users +www.kandyug.users +www.takeley.users +itube +forsale +svc1 +www.chrischarlton.users +kerry +ac1 +mockingbird +www.mfarry.users +thebeach.users +reumatologia.users +futurewasp.users +node4 +assistlink.users +www.thepropertyjungle.users +www.blackbeard.users +rfine.users +cat2 +eai +www.binary.users +samus +dna-decals.users +www.digitalfilmmedia.users +magna +m.apps +securehost2044.users +www.paulie.users +www.timhoverd.users +mclean +www.custom +lb4 +www.asb +www.razzz.users +www.sina +www.invertedmonkey.users +www.dsimkin.users +finntimberhomes.co.uk.users +newengland +www.talos.users +www.scottcook.users +noobs +www.rsmith1.users +cfos.users +lr +magali +vip6 +www.kata69.users +www.jackson +php1 +www.dtc +lfm +www.tumpin.users +www.bertrand387.users +chrismartin60.users +www.publicshare.users +webdisk.sales +hobe1.users +moonrakers.users +mlsw.users +ermm2 +php4 +www.imagines.jinerenco.users +www.davemountjoy.users +www.text +handjob +dbutler.users +thetwistshow.users +www.stuckey.users +christophe +www.gingenious.users +osh +www.kitay-na-dom.users +www.jayvanbuiten.users +napa +robtest2.users +test.jmarnold.users +sql5-replicat +la-tardiviere.users +www.educacion +www.elskitchen.users +save1 +quito +shiki +longbeach +www.can.users +squid1 +darranstewart.users +www.bendidit.users +www.g4axx.users +adamcrohill.users +blog.pcbscott.users +lukewhiston.users +www.chrismartin60.users +video.etools +egysoft +edicion +senate +www.testt3.typo3gardens.users +latex-facts.users +www.woman +anilaurie.users +msfbiz.users +test99 +wvagc.users +mailrelay1 +rocio +epic2 +pgu +georgesbigshed.users +esra +economica +beef.users +laurencepeacock.users +www.elle.users +moneyonline +rui +artsutorus.users +www.jquery +cinar +sfera +otsukaru.users +vs10 +binary.users +www.clean-wheels.users +bullets.users +addy.users +beartrio.users +kate.users +www.cgt.users +places +grevstad.users +www.trial-38af15.users +rainbowmassage.users +forasf.users +radu +www.darranstewart.users +admanager +www.q4nobody.users +www.nfenn.users +digitalfilmmedia.users +c-electrical.users +www.techit.users +banda +www.unicorn +www.victory +can.users +calculator +www.wordpress.typo3gardens.users +sketchbook.users +www.de-jay.users +autoconfig.home +trrocket.users +johnmcmanus.users +autodiscover.home +sforum +tanga +testing.another.users +www.blueleaf.users +www.lexicon.users +www.latex-facts.users +www.cpmotors.users +www.beacon.users +inspectoriguana.users +mail.666 +www.paminfo.users +mfarry.users +attorney +ukbikerz.users +robtest3.users +onecandle.users +eozkural.users +www.imaginary +tpb +server69 +www.tmedwaysmith.users +www.philcain.users +admin-dev +www.html5 +s61 +heatsinkbikes.users +paulie.users +blackbeard.users +www.zaphod +star5 +autoconfig.office +cgt.users +www.heaven.users +s82 +autodiscover.office +carly.users +www.thetwistshow.users +www.timmargh.users +s69 +www.bex.users +www.paulg.users +tumpin.users +test.andi.users +wims +ssa +www.szkolenia +across +badminton +www.lmarsden999.users +kmail +www.adamcrohill.users +www.eleven +wako +www.veritas +eggs +www.lukewhiston.users +www.jezz.users +ccr +mail.dora +thien +www.albarnes.users +adserve +www.buy +www.pingpong +imsandy +recorder +www.davidives.users +www.weightloss +jackdavies.users +igrey.users +tkk +okami +rwd +premieredance.users +communication.users +crossfire +hvc +www.dhingli.users +goa +sander +dsk +apo +www.medicine +7stars +write +qw +dns147 +dns148 +brianna +webserver3 +www.smash +www.heatsinkbikes.users +web83 +www.nagios +web77 +shankar +nsx +kxfzg +bih +ftp21 +server04 +web183 +web182 +www.ks +cardigan.users +tpj.users +lb0 +video6 +alexatack.users +www.jasonthain.users +www.lv +uni-regensburg +sarahedwards.users +video5 +www.nm +caoshea.users +elskitchen.users +web170 +www.thebeach.users +www.cyberwhelk.users +stand +ishtar +www.flameboy.users +www.helensoraya.users +leeanderton.users +nab +www.wolf +thepropertyjungle.users +blade01 +g3las.users +navajo +www.mushroomgod.users +institutii +hesham +maiden +alex3410.users +www.masters +ldapintern +web157 +fleetwoodmac +buttercup +comms +dtest +mining +web150 +www.noapacherestart +web148 +www.knowledge +web147 +web146 +www.purple +efi +web145 +names +www.genius +homeworkhelp +web143 +web142 +web141 +web140 +www.institutii +webdisk.catalog +www.worldspan.users +march-dmz +libros +my3 +supporttest +web138 +web136 +web133 +web132 +web123 +correio2 +www.cse +pup +co.users +misocial.users +sterlitamak +imob +abakan +www.communication.users +incest +ras1 +nfenn.users +web130 +web214 +web212 +ntp4 +macro +winners +phi11ip.users +www.q +web207 +web206 +www.tadimeti.users +magento.sapin.users +daleel +web204 +eleulma +hi-tech +web203 +rpa +www47 +paloma +web217 +web216 +m.store +sigaa +pokeworld +nusantara +web215 +bottletop.users +webdisk.ns2 +www.jonathanmann88.users +www.millwood.users +wc1 +web87 +angelo.users +web86 +web82 +web80 +mynews +milka +web78 +www.csr +nanas +airtel +greyhound +www.lmarsden.users +web137 +chrismadin2000.users +jacquimarsden.users +userweb +mychat +web135 +web131 +web129 +web118 +hayabusa +web205 +web99 +techit.users +mx07 +autoconfig.services +noelle +jammer +ichat +www.lista +mariko +stw +radioclub +autodiscover.services +public1 +www.tax +travel2 +public2 +imb +autodiscover.apps +autoconfig.apps +gerenciador +demon-gw +toplist +sayuri +www.xceed.users +happydays +irmucka.users +hastane +stream5 +iibf +ursula +tblern-scan +blern-scan +habbohotel +de-jay.users +crab +webdisk.demo1 +rac-scan +konstantin +sidekick +tblern1-scan +claudia +secondary006 +bra +csr12.crsc +csr11.sci +gupta +car13.net +mytime +car31.net +strm +indi +amepop +www.dbutler.users +jayvanbuiten.users +bookmarks +www.paris +cird +blitzkrieg +leos +threadgoldj.users +millwood.users +www.chrismadin2000.users +szablony +www.asfdgh.users +robtest4.users +sh5 +sh10 +sh11 +nirmal +dev03 +pushingarrows.users +ise +cdns1 +authentication +webdisk.kb +test05 +igri +tptest +farmasi +www.bleronuka.users +platforma +ftp.www +smail1 +seguranca +drucker +orig +intra2 +unica +principal +jamie.users +takeaway +www.tomfox.users +walid +smtp-1 +reminder +wordpress.typo3gardens.users +rov +desenvolvimento +dimdim +www.sarahedwards.users +beacon.users +heaven.users +www.df +stuart.users +www.cfos.users +www.la-tardiviere.users +vijay +www.fc +backdoor +kruse.users +corpvpn +www.ff +padang +secure01 +monicabatsukh.users +blackmamba +www.fi +host181 +avc +web-prod +milestone +mailgate4 +magnesium +s148 +skat +sh13 +ioa +www.cmc +cosmology +memorial +futurama +utc +mail.uk +host127 +host123 +host117 +echo360 +sh6 +handle +mosta +freenas +mmorpg +www.webshop +mekong +ex01 +www.freestyle +www.w2 +webhost1 +traktor +chart +technologie +tupac +screensaver +csu +centreon +www.ica +sp3 +attpos +freelancer +mariano +keti +vns2 +bkk +cdp1 +www81 +www77 +www80 +pix2 +rsync3 +archie +eyeos +narnia +sh12 +intranet3 +postino +archon +cashier +moodle3 +test33 +gruber +www.meme +undead +telephone +wallflower +fuckyou +maha +tracy +standup +loginlive +inmail +gecko +tethys +gator +sombra +unnamed +wirtschaft +medien +host126 +host121 +acn +at1 +toker +vhost3 +host119 +www.peru +tug +autoconfig.drupal +autodiscover.drupal +testw +buc +idtest +ctm +sunflowers +smpp4 +ene +smpp3 +mando +go3 +hbs +rtb +census +www.sexy +h20 +jumpers +m16 +m14 +www.drupal.publicshare.users +h79 +h78 +citycenter +h77 +h76 +h75 +converse +www.candy +vdr +h74 +um-mailsafe-00 +um-mailsafe-01 +h73 +h72 +medica +h71 +laboutique +sm07 +h69 +h62 +www.test.jmarnold.users +esx7 +ink +jes +europa108.users +test1.users +iri +isu +msupdate +h23 +smtp06 +h80 +www.bullets.users +freepbx +www.rouge.users +www.fwwilson.users +punisher +adverts +installer +akademik +www.pushingarrows.users +earnonline +nex +movistar +mill +openfiler +sin +webdisk.forum2 +oob +lexicon.users +cims +foobar +rgb +babyboy +authenticate +yemen +wink +say +karsten +m02 +handel +shree +tnc +ns202 +ns162 +ns191 +badboyz +irc3 +navier +zap +maroon +ysj +marlon +yuu +marseille +mecha +markos +www.ce +www.cg +number +babar +www.dsp +webdisk.lists +lovestyle +theboy +marche +stest +crdp +socks +nomercy +boardportal +zephir +kirei +mangas +kms1 +milky +facstaff +listserv2 +mindy +315 +natty +fff +www.rb +paradis +reckless +hannover +kin +icecream +ladder +clarion +win32 +mmmmm +mobel +kimo +zizo +petunia +nexon +moral +kansai +milou +braveheart +www.yar +athome +arianna +u3 +carrot +kiseki +n218 +pika +blackmoon +vremea +www.vremea +sada +wug +woodland +knockout +pronet +qk +literatura +blackrose +mservices +aji +www.presentation +attack +iadmin +omani +www.1c +parks +version +gossipgirl +yaka +www.ck +www.ly +sega +picka +tweet +helpcenter +under +tanto +renshi +houqin +colombo +referencement +kusanagi +umail +sponsors +nid +dfs +mtu +hcl +komik +varun +irma +hughes +admin8 +visionadmin +admin4 +teste2 +www.kronos +roller +vero +delete +turnkey +svr1 +jiu +tucows +leonard +wapmail +addiction +vdb +www.thebest +vgrp1 +platina +free1 +nintendods +journey +superb +welcometo +lunaris +sense +spook +www.luxury +www.servicos +oa1 +bonbon +macha +greenlight +rodin +talbot +ans3 +breakdown +www.ant +serge +letras +rules +autoconfig.docs +du +autodiscover.docs +ade +sheng +www.pma +www.out +dth +tammy +cia +ads.be +iavfatnfoopio.be +siham +zhidao +imhn +nagatacho +bistrooz +www.cit +gnt +nobody +sexxx +deki +stadia +np13 +kth +umegari +teebo +iml +whd +www.csi +squirrelmail +satya +ksm +qy +ij +notepad +santi +nes +nickelodeon +autobahn +neverdie +fls +www.developers +pis +hacker1 +voda +www.ideas +kento +sajad +iit +www.synergy +www.sci +snp +www.koko +sor +satoshi +umk +wet +wmc +doctorwho +studio5 +guesswho +synth +sysop +ezone +joris +maize +husky +yamanaka +cachalot +thesaurus +haruka +collect +rajat +shikaku +sylvan +foxnet +peterpan +kogao +camelot +d35 +d33 +praga +graphic +rudeboy +msdn +anyconnect +a001 +d19 +free123 +scorpius +aska +yakutsk +license2 +undercover +salmon +mackay +lakeshore +qwer +darlings +sowhat +scf +www.mirage +udec +formula +aid +www.camera +psychic +session +zgloszenia +lorenzo +elf +ipv4.blog +c26 +eagleeye +smap +ucs +leonidas +kagura +alban +akiko +asas +devi +befriend +pandy +be1 +windy +salvation +uto +cain +poweradmin +karas +amol +b83 +b77 +piracy +aton +b71 +flp +ohana +default-mx +esta +pso +munch +psf +qed +b59 +testns +crosslink +rising +b52 +b51 +choi +a-hm-3107-diemthi +dala +mail.beta +www.mlsw.users +www.blog.pcbscott.users +mfl +momo2 +alexb.users +dann +spaziocloud.users +www.leeanderton.users +www.jstebbings.users +lac +tmedwaysmith.users +net4u +www.khainestar.users +www.imperial +los +www.jacquimarsden.users +chezzer.users +www.robtest.users +lukestuff +www.madmax +www.foobaz.users +www.marlin2001.users +www.robtest1.users +www.monicabatsukh.users +naren +www.cartridgeworld.users +www.hobe1.users +motorola +www.stuart.users +bendidit.users +mayyubiradar.users +cult +www.dkproperty.users +h9 +ema +nabil +onlinedatingguru.users +efe +mihai +cwj +e7 +misato +imagines.jinerenco.users +timhoverd.users +louie +www.jinerenco.users +scottcook.users +jiji +faro +nishiyama +dusk +cyberwhelk.users +feel +www.ermm2 +metas +prisca +retry +aci +livetv +www.beef.users +kunal +marlin2000.users +mazen +wendensambo.users +erin +rubber-facts.users +marty +www.electro +marsa +www.mikepower.users +riverside.users +jasonthain.users +noapacherestart +www.threadgoldj.users +www.addy.users +petehowells.users +www.kate.users +www.phi11ip.users +invertedmonkey.users +www.blackcat.users +cyanideshock.users +www.anilaurie12.users +malkara.users +hada +mg4rci4.users +vacuum +gol +vw1 +bsdb-cluster +lmarsden999.users +autoconfig.book +q4nobody.users +hhhh +www.aln.users +autodiscover.book +c2i +www.wvagc.users +krazy +www.angelo.users +www.rfine.users +www.kiding.users +www.markufo.users +koora +igo +gweb +levis +blueleaf.users +kokon +pro01 +dsimkin.users +razzz.users +lenin +asfdgh.users +cpmotors.users +micho +moonjam.users +juicy +www.caoshea.users +www.automation +playstation +qa3 +lamar +cronica +mail.out +talos.users +maillog +yf +philcain.users +tomfox.users +bestgames +yyxy +ns8-l2 +typo3.heaven.users +myanmar +ns4-l2 +keen +www.spaziocloud.users +www.bottletop.users +joint +climax +peristilo.users +timmargh.users +aln.users +angelware.users +trial-37e040.users +gingenious.users +omoikitte.users +www.georgesbigshed.users +robtest.users +www.davard.users +plants1966.users +www.robtest2.users +albarnes.users +www.anilaurie.users +www.irmucka.users +juni +www.redhotme.users +www.rainbowmassage.users +www.wendensambo.users +foobaz.users +jstebbings.users +putra +lyonsqc.users +rgmcdermott.users +www.laurencepeacock.users +mrmarkmountford.users +www.petehowells.users +marlin2001.users +www.michael999.users +jessy +www.testing.another.users +thethink.users +humble.users +g4axx.users +preview02 +www.thethink.users +www.jackdavies.users +hukum +www.lmarsden2.users +cheater +flameboy.users +boise +www.igrey.users +www.molibi.users +dkproperty.users +www.sketchbook.users +kandyug.users +takeley.users +lin1 +anilaurie12.users +www.riverside.users +jairo +www.misocial.users +www.alutto.users +smtpa +drupal.publicshare.users +www.inspectoriguana.users +www.g3las.users +maro +davemountjoy.users +s330 +aaaaaaaaaa +www.marlin2000.users +lemonade +lmarsden.users +maul +mayo +www.tomfender.users +humas +coba +www.simpletest +kitay-na-dom.users +gross +kiding.users +rsmith1.users +theo +greek +greg1 +www.typo3.heaven.users +gracia +rockers +www.otsukaru.users +elle.users +www.chezzer.users +virtual4 +www.grace +bitacora +www.baseball +www.beartrio.users +hazem +hayat +www.clienti +www.grevstad.users +slider69gdw.users +www.retro +paminfo.users +www.indiana +trial-f40c2e.users +www.onecandle.users +tomfender.users +www.pcbscott.users +www.robtest3.users +hanif +www.hindsjohn2.users +debugger +xceed.users +hallo +hamad +www.trrocket.users +worldspan.users +mariajane.users +www.mayyubiradar.users +www.futurewasp.users +www.assistlink.users +clean-wheels.users +www.peristilo.users +eplans +www.starmusa.users +freak +www.co.users +www.pool +www.test7 +www.alexatack.users +www.dna-decals.users +alexmountford.users +nar +wells +davidives.users +echoes +testt3.typo3gardens.users +evita +paulg.users +mmmm +luxe +smail01 +www.paulmasters.users +ska +neno +nera +987654321 +www.rgmcdermott.users +daniela +www.kit +sli +ganga +ervin +wa2 +www.recruitment +eslam +webco +films +mmg +mori +epoch +moya +corps +msis +image165 +test0 +image163 +ramona +noma +qingdao +tlm +uranus2 +torr +one1 +postgresql +akbar +fatma +vip99 +orso +tyx +productos +webdisk.demos +evolve +ppms +hs2 +qqqq +print1 +sang +gearsofwar +diabetes +lostandfound +shak +foa +unm +daidalos +soya +origin1 +sw93 +vesti +sbm +hummingbird +triathlon +toki +shekinah +toku +toyo +trap +ibrands +fcr +xworld +jisan +unas +www.bologna +wait +pre-prod +want +www.career +webx +debate +monique +wwwe +endless +armenia +yohan +zero1 +zeros +shadowz +spamd2 +spamd1 +inmed +tibet +youxi +www.ccs +accelerando +pub1 +furax +togo +hotaru +www.dan +gmi +itproject +blackadder +coda +faceb00k +daver +aether +rost +locked +www.don +facbook +cabaret +game3 +roadshow +esb +zzzzz +lsty +autodiscover.scripts +webdisk.scripts +autoconfig.scripts +brock +mywedding +ensemble +renato +ayumi +wolfgang +www.jen +veg +www.samsung +www.isp +hosting02 +www.cash +include +www.joe +www.gifts +hibiscus +kasumi +revolt +mch +grid1 +cte +november +maxxx +sweetie +sunfire +www.psych +regi +mycp +ivc +musical +gamingzone +westend +athan +imagination +daydreamer +dementia +grades +aslam +agg +home01 +armin +www.sam +fountain +arief +pasha +jobsite +archi +isabella +www2011 +www.ek +energizer +revproxy +slh +sabine +thinker +daisuki +dutch +justforfun +www.sky +lfg +knights +flexible +kahori +schoolnet +riseup +ptp +wanted +www.ssd +andes +saleem +da18 +redirect2 +www.ver +wdc +gentry +denmark +centaur +naos +autodiscover.iklan +autoconfig.iklan +ameer +apc3 +aamir +thefactory +pyxis +vps12 +cdp2 +test-mail +amour +realize +is2 +basma +www.nightlife +satsat +lockerz +phpmailer +jis +nathalie +babak +bdg +fantastico +starnew +rebecca +group12 +www.naruto +asx +skyview +imusic +tra +www.jewelry +maui +admis +user3 +fati +cisco-pv2 +googlemail +bora +livescore +homebusiness +gangster +bigbluebutton +abdul +abdou +abbas +dotcom +infoline +probando +surface +sensation +favorites +bahrain +jeevan +dreamgirl +msb +blackfox +umzug +imagestest +vivid +mlsrv +muzica9 +ceit +inra +shanks +cooking +shiraz +soundbox +erepublik +loading +robocop +dream1 +onestop +imagem +webdisk.android +smartgroup +autodiscover.photos +mema +adonai +crazyman +autoconfig.photos +crypto +dns04 +gamenet +stronghold +blogshop +postcode +revolver +starpower +srvc +www.spider +bromo +simulation +mptest +radiomix +pop.mail +hangame +rabbits +adstat +dever +mydns +dandi +academics +webacc +buggy +sa01 +burgerking +kant +celtic +nightingale +homebase +imagelibrary +batista +hobbies +bbbbb +jacksparrow +webos +m.new +almaz +zoltan +www.proyectos +fn +deutsch +devcenter +www.street +mentalhealth +freelife +photoart +www.zeus +tiptop +hifriends +ftp.us +support-test +galena +www.arizona +autodiscover.portfolio +autoconfig.portfolio +cabin +cacao +frankenstein +catch +appserver +host53 +ankush +shopper +sunbeam +www.tds +devin +fcbarcelona +vogue +mailint +iskandar +blade12 +mailscanner +stic +magritte +physical +eoa +anniversary +netgear +break +personnel +choco +lovegame +theghost +fluorine +bobtail +www.legal +www.pd +www.umwelt +bhakti +cindy +sd2 +quota +de2 +apple1 +dokuwiki +www.arch +hansol +www.educ +silkroad +shalom +www.audit +reg2 +s1013 +s1011 +jny +s184 +persian +pcdoctor +crazyworld +creativity +spiele +www.hg +alegria +employ +ela +thienan +s199 +hakunamatata +exposed +pch +hogwarts +kabuto +merino +cosmopolitan +thames +www.vision +merian +emilio +arvind +bratsk +ichigo +echos +cydia +moore +eurovision +linkinpark +margie +castro +pcsupport +fatal +tomodachi +s197 +states +friendster +nightwolf +ftpw +s196 +hitman +chameleon +ravindra +testman +www.cdc +guatemala +www.cec +www.cem +testowa +gustavo +yousef +www.sugar +prolife +redstorm +daredevil +s166 +cybertron +dancemusic +shushu +s160 +s194 +s165 +supermario +www.cartoon +dear +grigor +s193 +alicia +natsu +mata +elham +syntaxerror +suresh +alvaro +veteran +faithless +nightfall +ethan +thehacker +mikes +fall +www.delivery.b +emailserver +silverlight +mp6 +mp5 +nomore +vm12 +itools +lbmaster +franz +bigboss +renew2 +alam +lbslave +front5 +front4 +westcoast +selling +hydro +dudu +dresden +kalki +tarzan +erebus +gana +gazette +instinct +cluster3 +hiden +palette +mysql15 +govind +greed +mysql12 +www.cine +arabtimes +brands +blacksheep +kam +top100 +gill +uganda +www.ra +toip +linux5 +theearth +ether +bobcat +s163 +www.fear +smile2 +zi +instrumental +fwd +myroom +www.content +jessi +hypnos +jupiter2 +scarlett +sirocco +vpdn +destroyer +arar +warlord +buh +restricted +tka +photonics +lyceum +fs10 +cipher +kaze +aist +fond +cardio +a-dtap.kalender +t-dtap.kalender +d-dtap.kalender +file4 +pass3 +games4all +lvov +www.c1 +santarita +futuro +cccc +asturias +www.ivan +armand +horizons +sufian +mohammad +itb +testi +www.check +scipio +davide +tjj +registrasi +webcheck +metalhead +tellus +click2 +fgw +afghan +saravanan +samantha +chatbook +ns134 +ns135 +ns137 +vagina +miniclip +dumbledore +pc13 +burhan +harman +naim +bamse +www.read +zarabotok +log2 +www.sara +logger1 +pc5 +anzeigen +sm10 +www.wolves +searchengine +yxy +xgxt +forum7 +haitam +www.promotion +twit +ajaykumar +ws7 +preguntas +newdb +piramida +ws12 +amicus +mourad +cordelia +tc2 +words +autoweb +etic +pmail +suma +krasota +mmi +smtpout1 +www.ssc +ediweb +email02 +sout +komachi +archiver +ms01 +ms02 +photo3 +nethack +vpn10 +www.mci +cont +gifu +okayama +www.questions +shashank +yamagata +oec +portaldev +cmusic +notifier +tns1 +www.oklahoma +www.michigan +annarbor +tulsa +mybb +pmg +kabas +bizcenter +zl +yj +postgres +antivirus2 +daemyung +dent +secure-test +helpnet +devcrm +thinking +www.recipes +mobileconnect +euq +clube +salesadmin +is1 +samy +yess +www.flowers +tstore +vial +sysm +bibliotheek +www.tom +vesper +ux +megaman +www.la2 +rainbow4 +rainbow1 +likelike +www.yahoo +file5 +brittany +poas +j25 +class2 +google2 +www.pai +edu11 +newb +www.sommeraktion +crg +historico.concurso +sommeraktion +autoconfig.community +autodiscover.community +sa3 +linkproof1 +router3 +www.gameover +moschino +koolstuff +beef +test25 +technical +imai +mailsecure +testftp +iaso +gown +twp +vadmin +healthlife +server38 +searay +licai +backup60 +kamakura +peliculas +www.nicaragua +noa +yukari +eq +halle +testcrm +nima +ste +infoview +www.nod32 +gena +babygreen +dbgs +sigahu +webmail45 +www.blue +www.nas +brn +starmax +webmail40 +tamara3 +webtera +campus1 +lsi +hcs +becas +www.ie +chois +ching +nadi +webmail43 +webmail42 +www.sme +webmail47 +martini +webmail46 +webmail44 +webmail41 +mena +webmail39 +webmail38 +webmail37 +webmail36 +webmail17 +lando +jango +boba +conimg +webdisk.galeria +host227 +referrals +starmaroc +ctxweb +mf4 +anket +eob +www.tas +cwrumtas +kess +qaupl +mesa-gnu01 +mesv-transit01 +mesv-transit02 +www.venezuela +mesv-transit03 +mesv-transit04 +koma +mesv-transit05 +www.chocolate +mesv-transit06 +mesv-transit07 +mesv-transit08 +mesa-para4 +newmusic +vpnslc +eng-core +slc-para-poc2 +slc-para-poc3 +slc-para-poc4 +mvrscorea +slc-wad01.sorensoncomm +yacine +mesva-vp-para3 +vcmessagea +econ-upl-vip +mesa-ios-para1 +mesa-ios-para2 +mesa-ios-para3 +ecov-mo-para1 +isma +yama +ecov-mo-para2 +ecov-mo-para3 +flavio +jimm +ecov-mo-para4 +ecov-mo-para5 +slc-para4 +econ-gnu01 +econ-gnu02 +econ-gnu03 +econ-gnu04 +www.tamil +surprise +econ-gnu05 +econ-gnu06 +econ-gnu07 +jeje +econ-gnu08 +hi5 +ecov-transit01 +ecov-transit02 +web001 +ecov-transit03 +ecov-transit05 +ecov-transit06 +ecov-transit07 +ecov-transit08 +sc-email01 +mesa-para3 +con-wad02 +diemchuan2009 +francesco +slccorpweb +dns15 +dns16 +dns17 +nguyenvong2009 +slc-cuda03 +grim +mesa-para5 +chold +con-sql-sign01 +hydrus +mvrsmessagea +smi-gurgle +hrftp +con-tux01 +con-tux02 +con-tux03 +absolut +contactus +con-tux04 +naka +slcv-ts-para1 +spanking +slc-para1 +mesa-mo-para1 +mesa-mo-para2 +mesa-mo-para3 +mesa-mo-para4 +serverdesk +mesa-sql-sign +xnxx +qava-para1 +qava-para2 +ext2 +dev.api +qava-para3 +qava-para4 +slc-para-poc1 +dr-sojo-cuda +onyxlb +listman +mvrs-core-staging +econ-vmscuda1 +econ-vmscuda2 +sogtest +realtunnel +woohoo +slc-tux01 +slc-tux02 +slc-tux03 +slc-tux04 +liber +mesa-upl-vip +drs-tux-01 +dbm +drs-tux-02 +drs-tux-03 +drs-tux-04 +mesa-vmscuda1 +mesva-ios-para6 +econ-cuda01 +vpnmes +econ-sql-sign1 +mesacorpweb +cuda05 +mvrs-statenotify-staging +sc-email02 +urc +familymedicine +dboardiprelay +mesva-vp-para1 +werock +mesva-vp-para2 +obchod +con-nimbus +flasher +mesva-vp-para4 +mesb-hold7 +slc-para2 +slc-para3 +landscaping +alejandro +con-ldap01 +con-ldap02 +vrsiilms +message3 +ecov-pc-para1 +ecov-pc-para2 +ecov-pc-para3 +ecov-pc-para4 +ecov-pc-para5 +mesva-ios-para4 +mesva-ios-para5 +patent +nci +econ-tux1 +andy1 +econ-tux2 +econ-tux3 +econ-tux4 +qadownload +ecov-vp-para1 +ecov-vp-para2 +ecov-vp-para3 +ecov-vp-para4 +ecov-vp-para5 +mesva-ios-para7 +blackdahlia +mvrsstatenotifya +cuda6 +bourbon +symantec +cuda7 +mesa-ldap2 +mesa-ns2 +saltwapolycom +atik +mesa-nimbus1 +mvrs-message-staging +bluehost +ntouchftp +production-www +bastet +mesa-tux1 +comunidade +mesa-tux2 +mesa-tux3 +mesa-tux4 +mesa-para1 +vccorea +felipe +wonderdesk +statenotify3 +chenfeng +kate +qa-auth +ecov-transit04 +reddwarf +ecov-ios-para1 +ecov-ios-para2 +ecov-ios-para3 +ecov-ios-para4 +ecov-ios-para5 +ecov-ios-para6 +ecov-ios-para7 +ecov-ios-para8 +mesa-para2 +ecov-vp-para6 +slc-syslog01 +slcv-exedge01 +engftp +mesa-ldap1 +www.cdf +ue +ww0 +mataram +oi +newtimes +spud +usage +wwx +webdisk.ar +kirari +mainweb +pajero +lunch +flagship +fisica +devang +dwalker +daejin +amiad +admin02 +gfactory +sslvpn2 +zihu +tranny +choral +tomson +boeken +widi +toons +hull +dars +farzan +millionaire +bol +competition +freewatch +bilgi +launchpad +b319 +b318 +b314 +b312 +b320 +planet1 +b122 +b120 +b115 +schneider +webdisk.toko +shair +roma1 +summerschool +senal +sejin +hummel +shinya +bombit +www.vote +www.sitemap +timehost +autoconfig.ip +autodiscover.ip +ecoplus +gst +koeln +stylen +subnow +reweb +host08 +gwsync +img44 +193 +www40 +stat3 +hitachi +zencart +207 +167 +inet2 +151 +sadmin +stlike +133 +moodletest +chester3 +admin-remote +img15 +img16 +webdisk.tools +nteam +kcp +verygood +www.22 +buch +mrbig +nice9 +colin +biyori +greenhands +diff +communicator +felicity +scrappy +postcard +habby +vender +rot +nextgen +dev-blog +www.novgorod +devs +barak +optical +facebook1 +facebook2 +webdisk.ns1 +softzone +sandbox4 +sandbox3 +katsuya +hamsa +ad01 +spor0 +secim0 +w13 +artisan +vulture +fotogaleri0 +wasabi +priyanka +debbie +ows +sondakika0 +resident +fileserver1 +rudi +win36 +gannet +lafayette +asw01swd +turkish +netuno +firdaus +adminservice +battlefield +wattle +edward +www.img2 +jcraft +mallard +wassim +shaun +www.darwin +stork +honeymoons +waseem +bf2 +qatest3 +map1 +cw12 +subekan +technics +cw11 +graphicdesign +cp07 +belmont +waleed +wren +belleza +sybil +web11301 +vishal +w01 +web11111 +caladan +web11101 +web10205 +jmf +n6564321 +chatroom +www.paula +db05 +walmart +topgames +bowser +oneness +www.hep +vbc +sff +lse +secureserve +snap-scheduler.round-robin +crying +masseyanywhere +helal +king89 +autoproxy +snap-event-sink.round-robin +flw +ebdaa3 +tur-ldap +amnesia +getmyip +devstore +sportcom +alb-ldap +ete +snap-developer.round-robin +pregnancy +fight +adoption +fatherhood +google3 +mailstore1 +snap-docs.round-robin +bidding +snap-scheduler.site +snap-event-sink.site +rapide-web.class +webpages +icprovision.cic +mu-mailbe +parth +snap-server.round-robin +www.pogoda +cyborg +schmidt +newgames +okc +rosie-glow.co.uk +api-web.class +bobbins +ttest +snap-home.round-robin +lak +windward +drcorna-bms +biomass +account1 +hollander +eskandari +ts08 +celestial +eslami +turnip +jongho7410 +tmdrbs2 +gw6 +tecnico +pricing +pcsuw020 +dhcpfa1a +dhcpfa3d +nuvola +dhcpfa3a +hyejin24 +dhcpfa2f +armode21 +maen2002 +www.mytest +dhcpfa1f +newel +madinah +dhcpfa39 +phase4 +dhcpfa31 +dhcpfa30 +www.responsive +bluesee710 +dhcpfa27 +halifax +genx +dhcpfa23 +vision1 +zangbie +fff327 +myspace2 +dhcpfa21 +rsj +staging01 +tur-cache +choigoda +dhcpfa19 +heylis98 +dhcpfa16 +jong7188 +vico +dhcpfa14 +dodream +dhcpfa12 +qorthd +andriy +watch1 +dhcpfa10 +gachimaker +www.sv +www.21 +mousavi +fololo +mushi +www.23 +pc-hwell-10 +likekid +casinoonline +brandlab +connect-test +wel-ldap +dhcpfa3e +egotrip +nasr +dhcpfa3c +olympics +goldencontent +tinies +nonos +uas +www.32 +kimaa79 +dhcpfa3b +klmwook1 +pdns1 +blackroid +pdns2 +docentes +pdns6 +jinakim +dhcpfa2e +dhcpfa2d +khaiser +dhcpfa2c +v1002 +sungdong +dhcpfa2b +dhcpfa2a +crossover +dhcpfa1e +phoenixadmin +samwootech +madhatter +dhcpfa1d +sesp +dhcpfa1c +nursery +sunwoo1 +mave +hsj234 +parsons +matome +dhcpfa1b +dhcpfa38 +goung4242 +tur-print +kjj8101 +uploadtest +dhcpfa37 +dhcpfa35 +opendoor +gid045 +webdisk.entertainment +juese1 +dhcpfa34 +dhcpfa33 +dhcpfa32 +emad +m.mobile +jia1728 +gogogogo +cherrypink +dhcpfa28 +ssooal +dunya +asal +ryuhyuna +arak +junseok +sjpeach +dhcpfa26 +kurosaki +best-life +dhcpfa25 +dhcpfa24 +hellsing +dhcpfa22 +navision +dhcpfa20 +img24 +derkuss0706 +juli45 +domato +dragos +eflow1 +136 +donati +139 +dhcpfa18 +andys +dhcpfa17 +acer12 +dongui +bestpeople +150 +dhcpfa15 +sucre7 +dhcpfa13 +dhcpfa11 +bryce +webchin9 +www.teszt +puff +pc-hwell-9 +haken +fishinggear +partyhouse +bigeye +swvpngw-ssbcom +ppp2 +ppp3 +201 +planetx +chelm +contabilidad +hanbok +www.wf +web11121 +monik +web10198 +robyn +webct4 +www-4 +www42 +jjodash +auracom +ehouse +multicare +zcs1 +husain +linuxadmin +alberta +poincare +zapf +gravityfree +nun +drkeyn-voip +webposrt +ipro +master.ldap +wlc1-ap-mgr5 +maebong +klein +okey +gameboy +vostok +dnv +worf +adrastea +misery +webdisk.survey +elumitec +dhcp120 +dhcp116 +zenwsimport +web10169 +dhcp117 +dhcp118 +tkts01 +aspdemo +salewa +jyoon +dhcp119 +leed20 +evergreen2 +vibe +dhcp121 +bartok +iac +pepin +samjin +t15 +evecare +redmoonpo +acomma +news07 +luciano +dhcp122 +matematicas +www.virginia +cmr +habiba +dhcp123 +moohan +pcadmin +dhcp124 +jayeon +dhcp125 +smartplace +njell249 +host225 +kgoodtime +comeback +naty +host226 +tamar +elmasry +coffee01 +hmmedical +dhcpfa36 +dhcpfa29 +oldenburg +gate.ocn +gate.so-net +yahooblog +webdisk.whois +c-4402-v03-02.rz +ajh0381004 +kook +www.cleveland +gate.yahoo +faheem +comunicacion +gate.biglobe +eadmin +saksham +hekate +check001 +promoman +v6.ext +wycieczki +devnull +studio3 +rosehill +gate.yahoopremium +gilead +keko +gate.nifty +yahia +carshop +server112 +edexcel +shyduke1 +drcornb-bms +forestry +test0000 +gangsta +pongdang +www.cai +rosse +supportdesk +ksm0759 +johndoe +happyh +jmac +imgsrc +ernst +drcorn-bms +nefarious +drmrala-suep +heon1567 +www.nautilus +drkeyn-swmgmt +sinbi +pc7misc059 +navinavi +amo +tequila +altmediaadmin +b152 +www.phs +sstp +www.uni +bosque +gre +it3000 +mgcp-cgoon01ca +pots-cgoon01ptc +asm-cgolab01ain +pots-cgolab01ptc +sim-cgolab01ca +kokomo +charleskim1 +songhee +shspa85 +asm-cgoon01ain +deathknight +steph +farhan +auctionboard +sia-cgolab01ca +niche2012 +toastmasters +tjy0514 +darknet +sim-cgoon01ca +mgcp-cgolab01ca +ithelpdesk +sia-cgoon01ca +webdisk.adserver +sia-cgoon02ca +ajkzz429 +psh4637 +sung3moon +dksxodhr +greeneyes +negative +vclub +mauritius +boomtime +heungwon +der +www.colocation +fuse +onlyu +pawpaw +idp-dev +www.kings +bprock +yjs9535 +chuck +ssiso +elba +payment-callback +delivery-ng +industrycert +pw01 +mstg +pw02 +foxit +geos +gogopro +eduforum +nikkip +baige111 +eurekasa +dagon +facebook-callback +db-test +microsites +kryptonite +vmware-controller +inner2 +seafood1 +scurve +contratti +pvcs +kees +ip214 +ip225 +ws-partner +ip227 +dolarge17 +innerlight +mineco21 +testocn2 +3ring +tory1 +kaiser1 +ip231 +seesun +ip232 +ip233 +crashdump +facebook-log +september9 +sip-fw +ufficio-old +ip234 +ip235 +ip236 +apm1010 +smtp-local +ip237 +coolingmusic +ip238 +bbang +redjinah +builder.hcp +clean123 +vacanze +cocomong +godqhrgotdj +ip240 +webmail.hcp +etec +nutricion +prometeo +barron +casadeaur +ip248 +newtemplate +ip250 +midia +tricolor +webconf.um +access.um +gkthdud9 +sip.um +ip202 +kwonsusan +av.um +www.cacti +ameli +bcode +www.mia +www.translator +cinbui +uncle +tae056666 +fina +cs03 +dongin99 +homeandgarden +bata +template1 +projectpier +www.msc +mckerli +mlg +ftpbackup +netwatch +test9d +autodiscover.hotels +employees +hyung0502 +mihosubir +elit +ip213 +test9c +ip230 +ip249 +test9b +stoc +dbservice +0000 +syd-gw2 +besttimes +autoconfig.hotels +autodiscover.ar +tr-tn-0002-gsw +vs7 +test9a +queenie +d205.dev +vs8 +d101.dev +vufind +d213.dev +d209.dev +wwp +autoconfig.ar +d221.dev +d222.dev +tr-tn-0001-gsw +d228.dev +test8d +host45 +community2 +lenovo +kaoyan +imoplataforma +web11142 +d207.dev +poster +test8c +terminalserver +hb-gw3 +d215.dev +www.publiker +endang +fondation +d210.dev +antena +d223.dev +pcservice +dinamica +d217.dev +ipmi +colaboracion +brok +cod4 +252 +chuy +253 +uruk +bobi +www.kls +test8b +ceco +test8a +powered +testwp +test7b +consultoria +test7a +test6c +agn +test6b +test6a +test5f +web11858 +www.webster +cba +peacock +test5e +test5d +kmp +mssql02 +livia +yoshimura +www.computers +handyman +ya-ali +sakamoto +cee +srv05 +amazonia +work4 +webdisk.image +www.edukacja +mx14 +seven7 +flv2 +thoitrang +aide +autoconfig.proposal +repositoriocemabe +youandme +werner +webdisk.proposal +www.proposal +autodiscover.proposal +rockadmin +nudist +mx1.mail +elab +tims +acti +acsi +takepic +bcache +nereus +adops +wlan-switch.hist +wlan-switch.khm +wlan-switch.svet +hme +firework +wlan-switch.soch +ahmedali +wlan-switch.circle +kabin +ear +wlan-switch.saco +egao +linux01 +vvvvvv +en2 +kamome +mandalay +top2 +wlan-switch.plan +wlan-switch.teol +abhijit +sm02 +wlan-switch.kult +karem +historico +mta001 +wlan-switch.psychology +wlan-switch.igsh +wlan-switch.esss +evan +wlan-switch.ekol +autodiscover.manage +wlan-switch.bygg +wlan-switch.cait +jenkins1 +wlan-switch.botmus +sirio +wlan-switch.guesthouse +photographer +moviestar +www-tt +directaccess +wlan-switch.hum.sol +wlan-switch.lundakarnevalen +cdn.origin +rss.origin +wlan-switch.sambib +images.origin +gian +www.iwww +wlan-switch.lunet +wlan-switch.lumes +mailrcv +mc1 +wlan-switch.gerdahallen +wlan-switch.li.sol +mail.bb +wlan-switch.kongresscentrum +wlan-switch.englund +moviezone +ggw +login.omv +gamecenter +websrv3 +parsian +dns30 +wlan-switch.konferens +dns2-br +dns31 +wlan-switch.rektor +tsnsql17 +b3ta +royalty +wlan-switch.iiiee +amavis +tsnsql18 +sportnet +www2-spd +gamesx +micro1 +wlan-switch.ub +wlan-switch.pi +wlan-switch.upv +wlan-switch.stu +x86 +webdisk.ticket +mentoring +debica +karthik +gwweb +mail2.pics +wlan-switch.srv +biyoloji +wlan-switch.sol +wlan-switch.soc +wlan-switch.net +wlan-switch.lub +publishers +wlan-switch.mhm +www.vancouver +qtss +q123 +etime +wlan-switch.ldc +www.oldsite +oita +wlan-switch.fpi +gaurav +kumamoto +wlan-switch.etn +dpi +iwan +exhub +wlan-switch.fil +wlan-switch.ced +wlan-switch.ark +kip +wlan-switch.adk +wlan-switch.kansliht +www.bma +wlan-switch.oresund +wlan-switch.botan +coaching +raleigh +wlan-switch.pedagog +vreme +podpora +wlan-switch.kultur +wlan-switch.astro +client1 +muaban +wlan-switch.evaluat +wlan-switch.luinnovation +soraya +dreamweaver +webservice2 +autodiscover.legacy +autoconfig.legacy +contentx +kearney +thinktank +millhouse +ile +kls +mailservices +leroymerlin +tomek +www.internetmarketing +netstats +ukvpn +gut +tnp +adios +host22 +waza +k2000 +okna +a12 +lior +syt +host19 +pager +aida01 +host18 +smtprelay +stor +api4 +abbey +idximg01 +lito +eii +s321 +host17 +moulin +videoblog +www.baza +gdb +spiceworks +oa2 +vitrin.vitrin +gofla010 +etu +damavand +vpn-server +generali +archimedes +cust +siberia +5star +host15 +betawww +gate1 +ntagil +address +autoconfig.labs +autodiscover.labs +gpr +rivne +jiratest +ibe +www.lviv +v6.int +pashmina +padme +kharkov +vblog +assessment +socialnetwork +erevan +oola +godang +kirovograd +ifc +limbo1 +limbo2 +hansolo +rom +rok +tallinn +ikar +www.mid +clearance +tree76 +tpvlfh +bcaisp +lhc +swatcher +www.celular +parati +gdcaisp +pvp +jpg +www.platinum +shcaisp +vps118 +vps113 +vps111 +wbt +bergman +1983 +vps029 +ndh8134 +jscaisp +woohaha121 +constructor +png +3000 +lovebug +chon1 +lmb +vps117 +lalala3 +dnglobal +loncapa +bbnb +hema +vps116 +vps112 +lss +ip20 +pics3 +noi +mte +zcaisp +vps015 +xms +wwwmail +jal +sukien +ffs +starfox +ssm +car4 +media7 +www2dev +damko +junauto +vdi2 +oldnews +gsp +www.orenburg +new4 +www.chita +mongolia +www.tver +lte +pak +perseo +bigstar19 +www.inspiration +fallout +newzealand +sanchit +registr +pruebas2 +doa0614 +kiso +kubota +maruya +vitruvius +lin2 +magokoro +swingers +bwby +realclub +ansa +interfaces +kusakabe +oko +perro +pho +www.euro +online3 +noilly +gold777 +www.bz +cozy +bstore +oceanblue +www.nt +ssl16 +plt +sandeep +mx21 +vcenter01 +www.watches +paros +sassafras +sandesh +montenegro +farma +hht +preprod.m +hca +gkc +enciclopedia +canit +mahler +mainz +sandman +dolf +czen +mirabelle +sdb +raoul +catalpa +sfc +smile2233 +goldline +ev12 +foshan +fuzhou +www.giftshop +lettledyr +dialogic +dread +176 +reward +etp +fkm +gat +web154 +baekse +emi +pub3 +web104 +gongji +hideip-france +001 +010 +hideip-india +hideip-germany +l2tp-in +l2tp-de +cals2579 +ip-in +dev98 +ip-de +mantienilatuaprivacy +ip-fr +video-italy +webreport +certificados +dsb +redrock +bonsai +applet +cheonji +oldschool +gook +tlb +onthespot +funnystuff +tantra +www.boa +libtest +gpnp +dht +animezone +kshare +chem1 +bys +whitewing1 +baesunhappy +jin123 +lje1265 +mtp +dgp +jdev +girlbygirl +igc +inni +ipas +iotv +jiae +qmailadmin +yell-sandbox +d220.dev +jkbn +d225.dev +bbmy486 +airljs +d201.dev +adforms4yell +d204.dev +d100.dev +oneorzero +myschool +d212.dev +d219.dev +d211.dev +allabout +intra1 +baccharis +pond +d227.dev +mushi3 +point2 +lockwood +d229.dev +internal2 +linkproof2 +d206.dev +d214.dev +makk +d208.dev +hostings +d216.dev +paulsmith +d224.dev +sandbox4yell +werkstatttest +ict4 +adforms +leadservice +mail.fis +daten +jjibbong +d202.dev +d203.dev +koko1234 +www.werkstatttest +proxya +www.pet +d218.dev +rawl +eyecandy +d226.dev +time119 +l-ukicalifia.it +yeslee +joule3700.its +kagayaki +edu21 +edu26 +l-s-a000307.it +unicef +e-uskj5y59e.eps +ballo001 +v100 +gymiin +sq2 +cl5 +old4 +www.adult +webdb1 +tkr +hwaya0952 +videoteca +jinhwa +siegen +gprs +pingu313 +erb +tmobile +playerint +main39 +ndc +bcast +adoc +puny +bc01 +bc02 +sqltest +chr +holycow +necco +babysoo +webserver02 +playlist +cid +www.catalogo +sban +autoconfig.properties +tennshoku +awe +autodiscover.properties +energie +icepeach +hosting11 +ceb +gesundheit +cumall +proxyb +verbraucherschutz +womenshistory +empresa +institucional +m-relay +gsd +oda +philosophie +transact +em2 +ihm +bartar +flanagan +woohyun +netsecurity +mjjproduct +siemens1 +studium +addison +astro1 +sl2 +sl3 +8888 +testapps +crossstitch +milenio +trackandfield +187-122.owo +sv126 +farm2 +northernnj +blk +www.webboard +marcas +mooncho5 +privado +sdca +atr +iphonerepair +koras +saml +autoracing +mutualfunds +rmsnlf2140 +silctl +vpn-dev +bie +casinogambling +usedcar +onedrink +threestar +imhotep +worldsoccer +vpntest +bfg +p1-all2 +sfl +zk +api-staging +usnews +as6 +theotherside +zata +gouk +hra +globalfood +roadtrips +farming +sinjin +fallingstar +sytkfkdgo3 +slave4 +bosfood +economia +specialed +m1234 +lss0918 +www.opencart +amr +sv125 +cplus +calendar2 +lacrosse +originals +nasan +neuroscience +zyxw +beta5 +commons +moviles +bellavista +symccloud +www.sato +trgovina +bookclub +bandits +mka +qa-www +proskate +artcom +boardgames +kasina +alking +artdesign +www.clothing +vpn0 +cage +cookware +magaza +italianfood +ccap +replicant +mani671 +xpressconnect +k11 +k12 +3com +slartibartfast +azteca +webdb2 +gq +bhs +abra +webtesting +kredyt +adsonline +did8535 +e4life +thezoo +hjs +pman +jcs +lhs +medizone +fbs +thesimpsons +qmc +www.lineage +shoutbox +wax +wjdtjs3460 +writing +forecast +dagger +pharmacology +www.columbus +americanhistory +pledge +www.so +kaczor +st8 +mundomagico +smsgate +www.dennis +www.user +lax +allure +cor1 +www.los +pediatrics +collegefootball +insp +zzttfg +tamiky +wap.naujas +rtfm +menace +lovej +fenris +www.naujas +www.courses +touch.test +wap.test +bobae524 +m.naujas +touch.naujas +changpo +arkadia +feelers +www.mapa +shan +hen +frugalliving +shimane +muzeum +whiskey +fukui +yamaguchi +frenchfood +www.dev01 +preprod2 +www.dev02 +kittysh1 +www.play1 +freemind +smax +www.www02 +wmail1 +statistika +sonda +www.aukcje +szkola +www.typer +demonstration +adminnt1004.admin +lcgfts3.gridpp +dcap.pp +eblp14.ebl +lpta097.admin +lcgft-atlas.gridpp +lpta153.itd +tcom6-pc.cc +burton +dgs +lpta142.ebw +kurumsal +mc5 +srm-lhcb2.gridpp +oomnamoo +it2 +phoneplaza +binghwa +prop +newhampshire +srm-superb.gridpp +www.tennessee +pipipsrv +one-test2.gridpp +srm-ilc.gridpp +castorns.ads +zak +icsm +srm-preprod.gridpp +newjersey +burger +cernvmfs.gridpp +ghosty +www.documentation +lpta117.admin +lpta006.admin +www.wyoming +pca370.ebw +nero.cc +www.office365 +atlas-squid.gridpp +www.pennsylvania +srm-dteam.gridpp +srm-gen.gridpp +dml +connecticut +cypher +srm-t2k.gridpp +pca103.itd +pca240.itd +www.ohio +atlassquid.pp +www.kentucky +aiv-emc01.ag +srm-mice.gridpp +www.missouri +srm-cms.gridpp +srm-hone.gridpp +www.maryland +haste +www.iowa +outbox-og +srm-cert.gridpp +lpta141.ebw +cfi +gw.inf +lcgft-atlas-test.gridpp +cms-squid.gridpp +www.ldp +rider +opennms +touched +srm-na62.gridpp +babylove +venz +rack7u39 +lpta100.admin +linux4.pp +l26 +linux5.pp +hepwin2008p.pp +rack7u38 +barbie +gw.cc +rack7u28 +xat +lpta141.admin +srm-cms-2.gridpp +lfc.gridpp +rack7u13 +osman +srm-minos.gridpp +cndlaptop.clf +pca150.itd +richman +www.debug +scnt92.sci +pca095.admin +srm-biomed.gridpp +a3obulogon.itd +harmoni +site-bdii.gridpp +atlas.pp +pakiti.gridpp +coches +stewart +rack7u29 +lcgfts.gridpp +gengar +vww +www.bin +lhcb-lfc.gridpp +lpta131.admin +umbraco +scnt97.sci +sv30 +sv35 +lpta142.admin +outbox-mx +srm-snoplus.gridpp +habbot +www.asf +sv54 +srm-lhcb.gridpp +srm-cms-disk.gridpp +sv53 +sv52 +rapidshare +srm-alice.gridpp +caramel1 +mongo-tuk-c0 +www.flex +sv50 +sv34 +tmk +sv31 +publicity +echo.sc +za-switch +kbn2430 +sv16 +sv15 +www.atm +media101 +www.bmw +gallery1 +pnr +kirkenes-gsw +www.sirius +rbc +mofo +www009 +www008 +www007 +www006 +www005 +www004 +thumbs3 +www002 +www010 +www013 +econet +designfactory +www.truyen +www.roberto +gigic +www.topsites +rack10u20 +doa +moritz +osb +otm +eben +rack22u36 +rack22u12 +bristol +rep1 +newtop +daftpunk +tournesol +olx +waka +dens +ws5 +vans +odo +dow +www.servers +fzgh +metradar +haianh +d1-1 +ceg +vice +webradio +mylinks +ns200 +handson +mtt +sm5 +milforce +busted +playstation3 +nsr1 +biolab +pang +cyberwarrior +appweb +m.es +apns +dillon +phongvu +kemper +jyzx +kdk +thierry +www.ural +www.memoria +haohao +irs +capita +pcx +desarrolloweb +www.ols +isp2 +www.mel +jbo +xmltest +pc4 +pc3 +tek +www.dw +www.cel +spectacle +adman +mailmaga +nadmin +www.soul +vm06 +search4 +www.mix +www.evo +hoy +hoa +www.ngs +ludo +palladium +otis +pc12 +alternativeenergy +ifr +p01 +web2020 +spidernet +smartmaru +kirey +kkami +sheriff1 +ns160 +www.nwr +spacetech +surgery1 +kamis +www.osp +cherryb +jmp +lexcorp +vulcain +www.psi +www.san +ns139 +gam +refer +ns136 +hyderabad +wildstyle +lovesome +dws +accel +localhost.media +click1 +trojan +axs +eung32 +nausicaa +pre-a +kingmotors +leepd +adam123 +eunhee +laurent +xxb +redmay +promethee +glecor +multisam +startimes +davids +www.networks +dsg +s18-254-fi800 +m.videos +freestuff +arcas +deporte +yolanda +dse +cmail2 +cmail1 +www.lolo +haidar +makemoneyonline +mapit +test.cg.vip +silo +www.serwis +www.mary +webdisk.update +finger +cg.vip +arttech +locg.vip +wo1 +armor +boapi.vip +wxaut.d3s.ili +peri +wxstor.d3s.ili +test.lott.vip +keuangan +test.tt.vip +trendy +pokermail +lott.vip +akshay +www.winter +www.mart +coach6new +autoconfig.foro +byt +topsalesclub +jpa +laith +authwsop +authwsop.vip +smartfund +ciscovpn +test.locg.vip +dgi +col +osol +ftp.allegro +tolga +casio +upload1 +autodiscover.foro +www.teszt2 +cov +generica +immobilier +staging40 +zeko +lms1 +bibliotecadigital +abcdefgh +photomania +ado +sbt +m.staging.apps +mbi +caxton +www.sitebuilder +filip +www.sharepoint +www.48 +levon +fizika +w2p +hmm +biologia +infomedia +fotoboek +www.guia +aoladmin +liriklagu +asig +murdoc +pantyhose +onlinemarketing +gamezer +www.poseidon +tct +bios +blackbook +bink +wcms +msds +novel +webdesk +eatingdisorders +ads4 +partytime +just4u +www.gina +www.exit +optout +fs11 +bmi +hrselfservice +atf +itsme +mecatronica +web189 +akg +www.fire +kingfisher +hemant +pg2 +fs12 +fs13 +educar +www-t +chop +alps +isweb +fs14 +fs15 +f7 +fs16 +xwiki +mimas +karol +itservice +fs6 +ckp +kaori +cybozu +ate +tomorrow +violin +tonton +fs7 +fs8 +hendry +controlescolar +globaltrade +war3 +toontown +kairo +www.microsoft +thisisatest +janette +dom1 +cvo +space2 +inco +neve +mobilesync +proveedores +rikardo +shinigami +vm-dns +www.bmi +aos +kenkou +informatique +localhost.demo +hurley +herman +vz6 +oceania +www.aspire +cinnamon +rainyday +livescores +www.eric +educa +webdisk.staff +abd +copernico +astarte +hache +exc +envious +espacio +pkpk87111 +amh +ghqks1203 +www.szczecin +nettuno +bbf +jeunesse +agt +soil +bitcoin +irbis +www.ef +agb +newyahoo +wilk +dsr +wmp +isp1 +wwwmobile +mt1 +afc +mail.mail +afa +kimkim +tjdwh18 +www.elk +thermal +bebero +shyduke +wjdthal1 +itshop +elvira +s149 +botany +uis +folder +onlinemoney +tungsten +www.lancut +alexia +sipav +fedex +boromir +reaper +scratch +aisa +generations +angkor +sudheer +young7197 +mytree +yosemite +pdu8 +www.belchatow +goodhope1 +bildung +pleasure +173 +aprs +ferret +oldpop +ej +888 +4x4 +radiance +praise +ambition +radom +www.pokeworld +megan +dead +clearwater +yk +carpet +wo +airwatch +simmons +accolade +onlinedating +woozoo +rj +lancut +belchatow +amelia +bcl +yvonne +eeee +ufl +palas +goldap +pha +www.aria +insa +bree +manda +www.error +www.nevada +gn +matterhorn +kwon +tum +vips +bachelor +grisu +datastore +erotic +113 +collector1 +exotica +redemption +ezine +local-www +namaste +flex1 +vpne +gh0st +subzero +kamino +highvoltage +boomboom +mscanus +jiminy +deviance +www.rea +webteam +dcweb +skintech +bosna +golf6 +starone +iptbai +content3 +truol.parcerias +infoma +vpn1-uk +www.gearsofwar +rubens +backup7 +nsd2 +truol.parceiros +shequ +autopay +staging.admin +vengeance +acadia +ftp.moodle +bk5 +prx +myself1 +www.karaoke +imag +newdns +140 +bimbo +details +webmarket +charly +aiadmin +kakao +facedog +kulinar +hack1 +pd5 +gagushow +ictc +webmaker +habin +adriano +prema +martian +toros +earl +deepee +apm +dez +penta +dvb +beibet +freyr +a12345 +coolboy +mail.um +jen0615 +supertop +wire +turn +seif +kwons +bkbfate +maz +lucent +www.pub +vm7 +diab +ujjwal +prestige1 +ur +rjsgml5694 +genom +lamy +securelab +vchat +www.liliana +tdm +amelie +dentoo09 +encoder2 +satanas +dynamo +mp8 +mp9 +mp20 +createadmin +mp28 +mp30 +mp10 +mp11 +mp12 +mp13 +mp14 +mp15 +mp16 +mp17 +mp18 +mp19 +mp21 +mp22 +mp24 +mp25 +mp23 +mp27 +mp29 +mp31 +shuffle +dailies +maac +yourworld +wtc +mulan +asasas +ipv4.test +www.pure +www.ahmed +silicium +lobster +ferrum +nok +ihb +ester +faw +hill +bkr +techshare +roxas +www.anita +siwa +araba +mobo +mili +s191 +takamiya +cometogether +islamona +torrentz +greenbee +mercadolibre +www.bells +khenzi +gurgaon +champ +floria +nonsensical +smdesign +sg108 +wdh0517 +www.extra +jinny1004 +luffy +humantech +plf +pd4 +simsim +nie +humberto +ire +kankan +loveparty +fateh +vdp1 +funtime +vps36.dc1 +bnt +surfin +s168 +s171 +uchiha +bf3 +ds4 +www.fv +thunderbirds +www.nato +www.att +etravel +star8 +papi +kamyshin +sanjose +sudhir +rustam +aldrin +avh +huron +test888 +dasha +spammer +test-it +test009 +cima +akhilesh +imba +s177 +sars +bloody +jyx +cco +betatest +www.atc +www.tao +nobile +coolboys +jn +backup-server +s178 +testtravel +ghvpn +www.bola +winmail +listserv1 +ts.inf +ts.mmf +ts.eef +ts.labktp +misslee +ts.iif +ts.ist +ts.myo +ts.mkf +webmedia +ts.laby +codec +thuong +times2 +js2 +showme +autoconfig.contact +shutter +autodiscover.contact +lipstick +asthma +www.brain +wup +black2 +risa +tingting +www.coop +thejoker +www.spark +gpa +realworld +suboffer +sanluis +updown +netmovies24.edge +thienphuc +tia +beni +lee002200 +hugoboss +alfa2 +www.bike +show-tmp +domenapanel +leather +doc3 +syzran +ruslan +mediaserv01 +www.death +st02 +st18 +snowvalley +carina00v +hojung +vpn03 +www.doit +arthas +kniga +moren +exhibition +phpbb3 +monjali +lift +cstool +www.aol +amsp +www.sra +s180 +www.iic +s188 +dizzy +mayg193 +benebene +www.communication +j123456781234567 +www.coins +ciberlynx-wsvn-web1 +rtm +iphone5 +na1 +tice +asdfgh +stafford +www.panda +msadmin +www.is +adma +origin2 +en97ea4c +tmp3 +ogrod +mcbox +rpi +doktoranci +www.edc +corn +www.vas +royals +sherry +forbidden +s183 +estimate +sagitarius +gsearch +builder.extend +handsome +kinomania +libro +vp01 +smartcard +selenagomez +q1w2e3 +motaz +strm3 +forall +sviluppo +s181 +lsb +bestof +webdisk.ayuda +newsearch +cali +www.eagle +s192 +soup +ssbtest +servidores +kandydat +gps2 +amulya +quercus +cmos +chiko +nahyun +kaizoku +germanium +telechargement +physik +asean +sourabh +rc2 +clown +mhm +www.idc +ticket2 +irish +ssltest2 +arch1 +londres +s1018 +starsoft +www.pdf +dip +advisortrac +aileen +blacknight +vcr +sips +autodiscover.ns1 +danco +prashanth +www.cyberspace +autoconfig.ns1 +iprint +n6 +manualidades +orgs +pancake +freaks +corsair +brook +hoop +lifetime +omega2 +www.gamma +ns155 +kissmin +farida +www.firma +mojtaba +messiah +viki +lifes +delight +kangdy777 +sweetdona +rkd2885 +www.thegame +dlfmaekdns +codysale +webdisk.vip +anjongbok +teflon +gksghktjs +yumi89choi +inmobiliaria +taehwa5 +butiroom +comtech +beryllium +eodks +ddok1213 +customerservice +hjs0997 +zeeshan +heeseung +kaustubh +autoconfig.vb +kamzi80 +cheuk +cheng +ooccc +autodiscover.vb +webdisk.vb +inchan21 +checkmate3 +ndnasd +qkswlenro +ujjwweu12 +www.comunidad +rarakk2 +ansari +cips +pippo +kdoy3 +brand94 +telius +sangzero +bnshdj267 +newdata +archiv11 +ya54 +bblues +xmaseves +lty5229 +www.worldcup +ccudi68 +dapin +sinhk71 +girlsunit +angelia22 +ffdesign +update3 +verified +nicekim72 +lovely1st +eloah +ruddls333 +mrcteoqkr +skumar +bless +anta182 +jinwomall +beyourself +blank +keerthi +ihappy +doremi3652 +enkistar +bbonamall +dum24 +stweb +blair +vnfmadid +www.ppc +propose +chinatown +jsmith +twee +agencia +webdisk.downloads +tkagmd +friends4ever +rena1730 +adslcolor +ac3513 +tencent +image670 +ttbe +rachman +ppp725 +e-business +bmw2120 +wapp +bluemint +constructii +okadam +irusy +formulare +guy2me +dkflfkd77 +test2007 +nojom +matthias +jejewa +rng002 +sondari1 +gongg777 +taufik +animo +www.yoga +easysdh +hp1 +stwvudtod +ocnswjfs +wjdakfdn +bs1973 +sadness917 +a1231a +rs3beta +zzezze +srkjh +thsoj +www.prova +eventhouse1234454 +bmw0520 +funstyler +hooraing +freefree +deathstar +myohan72 +enurizone +bshtheone +tarlan +ev0726 +cyzoo +sarapul +procyon +manyenalda +xvshebnjw +damien +paws +amac +johiok +avenue +bhvf547 +synjy00 +kuoemyhws +good0353 +sj8253 +da6atelier +kopnwx +rainbowaa +ts04 +pianojs +mstore +zyn3103 +fnqltjs +qkr3584 +inobell +kimjk1191001 +kwonhi21 +amalia +whoami +ohygtk +fontane +kettle +k9180 +hesse +alison +neofrom +poorinbag +whispers +kdmsekdnsr +fabrics +adimg2 +klpkorea +am2 +anlion +nanna022 +smisslee +comp2020 +ksaraki +countzero +gaga2525 +knife +hamsat +jcjong21 +namiezuki +jahanzeb +team8club +culamoto +sorro +edwards +gg1477 +inix3039 +inha35 +jintoy +icolor +ivwss2 +izziban +jm2k7 +babymam +b127 +tangerine +eyevee +zemnmn +autoconfig.backup +cjhmylove +hyun0011 +factorial +muzzy417 +autodiscover.backup +wiseyjy +wnsgml3370 +wooritoy +moonmih +bdiweb +pp77 +yeosi +albums +freaky +kjy102938 +webdisk.backup +fastguy75 +wnetworks +dollhero +pubftp +romario +wtest +epc +mf2 +rlaendus11 +gadeuk +progress1 +lscompany +gospel81 +flyinghorse +gospel80 +nonfiction +jinjin8858 +temin78 +www.union +wahid +mbbs +tnlrdl +traffic1 +autoconfig.upload +kimmyungsoo +autodiscover.upload +tntworld +next700 +ato6193 +ggamigirl +windowsxp +h4x0r +polter +taken +r-pa1 +mta001.kmm.mobile +feature +bashar +rtsp +pk62 +youngwar85 +youungs +happyyim +iidong +aryan +skawnddl84 +sms0656 +goodloan24 +banned +beaver +basri0310001 +www.gamezone +photobucket +kgb212 +uiuiui +sj4322 +allonrigs +surajit +hamo +psw2024 +gowcaizer +any4love +rlagusrl39 +obus +sjansjan +s333ss +item119 +cassandra +edgestyle +cjstnsdjq +m.order +membres +schatz +bestfood +iloveu +sy3381 +redirects +teatime +pearlngem +sunytest +quenya +vitadolce +eilkuk +presupuestos +musumm +service5 +limkorea +korea8585 +narasimha +www.monster +savior +pbk4959 +ksczerny +univision7 +dgfshhw +gar119 +theplace +dicamp +kulkulku +ruru57 +j7 +winners09 +starlove +aaabbbccc +mbap +sarangme +gang5064 +me09 +fitbow +www.mirror1 +greenf5 +zizibe0316 +jhy914 +sj1062 +guinea +mephisto +ohontaek +newkissq +ares2 +sulgi0566 +rancho +love83js +wshow +apollo4 +apollo5 +lohasbank +kim3929 +testdev +somerset +pczoom +vicious +shakti +shaker +hiphopjr +madammoa +vanda +inis +nono099 +gauthier +sohosoho +oaoqnfakd +karma01 +etisalat +kj49 +rack +msbig +www.newhome +sjsj825 +blackice +kyh90100 +dea8520 +mycampus-315-admin +inotes +vudiwbjsw +ccmjbr +mycampus-256-admin +exp2 +iface +jin987 +p725 +newbie +mbc +shapping +megastar +aboutme78 +gywns20000 +sakuma +wjdduqdl12002 +wjdduqdl12001 +m28 +m27 +rlawngus71 +pnpink +chamomile +botanyadmin +system32 +cw8989 +frogmeat +ceid +myth0505 +mr8032 +juju8598 +royday +smoothguy +igii +niceuni +diamonds +adamas +acc2 +qkrrjsals +ripple +ygfactory +grolsch +interview +nongae75 +kaban +soogi3333 +in001 +heat +hulkmall +scarecrow +china1230 +orangesky +student6 +adamo +as5 +adapt +glimpse +mstr +fanfiction +ubersmith +judy8055 +namissam +bowlpark +in7041 +upm +www.images2 +juju7236 +qweqwe +test18501w +www.forex +www.images4 +gogreen +mabelmari +inline +thesun +thgml884 +kjhlhj0313002 +ourworld +codeigniter +kjhlhj0313001 +unik +mtr +panini +newvision +bizhanna +anpara +chun7436 +memento +mahmood +jungjbk +mnmnq25 +cony +cinfo +zangmanenc +realtop +boogug +ryusuc +heytaehoon +forum4 +destroy +aubade001 +night0070 +nm1 +dnc103 +medmsal +younocke +hoya811218 +accent70 +caligula +dufjk232 +vaco27 +sade +yks4267 +accessories +mungushop7 +savage +zzeshop +yameyuco +zeeman +servlet +ksongha +enbe +nothingjh +designers +naturally +delirium +nectar +sado911 +luxury2304 +dragonse +alclswlfkf +joa282 +wangenni59 +theggun +swe +kds221 +www.hamza +ckdvmfh11 +skssso +largo +bewithme +whoops12 +paranlp +alcor +sathya +leibniz +dmtest +enjdhf238 +nocturne +lucie +regor +homeworks +gember +zealor +kkkkkyta +wlr79 +choi3241 +bestop +tkthcjswo +rulrulru99 +alias +qkstjr1107 +skydriver +dbsak +zrnmebdzq +trustbike +pporori83 +pporori80 +tate +smtp.forum +pop.forum +mamababa1 +astronaut +allan +daacc56 +scfan +mskgreen +webdisk.iklan +gjdjdrmfl0 +www.sakura +www.wwe +topsecret +www.iklan +gucci8596 +www.woo +amine +www.war +ritter +frutas +ljs4394 +dasf +qwerty123 +www.poland +tem +skycity +ebmzone +mail.lists +skyblue +becky +xoghzkb +jps +mailsender +s2handi +prune +sajjad +aardvark +zaqwe9713 +duo +sbboa +angle +anika +greenroom +october +hayanpibu +modelboa +bdr +baksu74 +beowulf +fallback +paradigm +revive +killerjjh +kbsbond +amrit +safran +artiman +safdar +mdesign +demo23 +inbmall +yagudosa +gsb +gsh +ankit +hyunsoo1 +ankur +ird +au11 +ahmedabad +lovesukjun +camden +dnddlek81 +bywoong +designart +momkids +usldskel +router1v105.zdv +mkk9894 +t1234 +bess-proxy +nexzen +hothot +letstalk +pgm +sadhu +havana +kjhlhj0313 +anoop +amaranda +tulipe +grn01 +todytody +evangelion +planner001 +hyu21ni +dnflkskfk +sky4005 +bebequ +usb52614 +starshop +heyckim +shyness37 +user01 +wbg +urologyadmin +truegirl +eboya4 +cardiary +sluggo +qnrrudrud +kyois +akai +suny58 +cochise +anwar +zxweb +mypopstyle +qqbox +hellowyp +arash +superior +hellboy +vosko +qkdl0922 +starsbc +hombrecokr +arman +sudafun +moja +kim389 +www.sad +alfi +alin +msproject +codibank +zyz +oakland +asoka +kathmandu +momsbebe +ames +mcse +comuriji +gotsodrl +busdayrim +moonyelf +seinz +carka +leninache +bbre5241 +giri +mailcleaner +ssailor +khdesign +styleshop +uf +coffeehao +frc +www.progamers +static1-org +simu +iop +sherif +casta +www.pow +apcompany +autoconfig.az +temporary +autodiscover.az +webdisk.az +fiorekorea001 +proyecto +kingmaker +www.one +loveyou1 +haemir +welook +sky0958 +bme +i486yhs +hjnm34 +blanca +infoshare +benisjw +polymer +edp +sungjo2001 +mintshop +gsncom +bombom +bess +gmrao +pulleaum +sang248 +ct1 +nomad21 +tonic +247 +dsh +sang247 +www.violet +wedro +allot +allah +theshadow +cunsung +eod +ottomall +www.enigma +mollym +parkhunuk +nmg +neture +dreamworld +evenewyork +arka +hlsports +sunytest001 +www.trans +mcoup +qatest +alger +fungo +deo3000 +thermo +hss7333 +dikfmj28 +essential +pueblo +fufififi +biscuit +krisberry +elita +therapy +hyymlove +misskar +crochet +grid2 +chanz +netnews +badboyshop +ushng02 +www.lit +assi +cokokk +rubychi +guardians +www.srilanka +arkorea1 +www.idaho +pcportal +gimminhyun +brokers +sonemart +autopro +qopnbvevb +bobkjd +stasis +kyung2299 +ayman +autoparts +byhappy365 +ehdgoanrh +koopreme +hightech +han2gage +aaa123 +jisuandj +tachikawa +cheesecake +shinagawa +emnbxjkst +believe +enjdfm323 +hanax +shoesbal +www.irs +www.ips +jiyugaoka +cf2 +hanab +meysam +nfl +seriat +lomcehia1 +abcd1234 +kai202 +goonis21 +www.agents +meo1973 +sunwoojin1 +zerosumz +nahyenmom001 +qlzlsl +shababcool +www.gtm +benedict +webkey1 +kbs3749 +azalia1020 +amiltd +monkey202 +abracadabra +onedirection +alas +dlfwhago +goodr39 +kbi3229 +kct3000 +www.submit +divertimen +state +starhome +min0501 +hajimeru +hongsham +paparazzi +hamuske +repairman +soaps +cogygud +t18503 +whqudgus81 +hotel4989 +mfe +ayan +mangojelly +gdguy15 +jun0970 +c52 +thehill +grp +buffer +www.epi +tbi +pporf +oyesloan +xxxl80 +apple10cme +busstop1 +www.pix +comeon +meteor76 +alstjd0001 +ampere +as77as +wp-test +instrument +mogi01 +redcap +yayoi +sysweb +wins20 +lifelike +nostalgia +eccube +dugian +inquiry +yanstory +cocochi +sahra +hpcc +unqpuio +halogen +outsourcing +hot2012 +rnxgwmuor +mondayshow +inschrijven +gastory +bran +bris +melowyelow +driving +llomn365 +dldms06 +tundra +chyi87 +www.btc +rino54 +geocoder +jeh0907 +cold +rad02 +disc +facebookadmin +naco3535 +wk +dbslzhs +magicworld +fashionadmin +acn-net-cojp +ovirt +mindf-jp +bma +pc001 +pc002 +suny2858 +bluehorizon +mycampus-314-admin +ajtgm-info +alice5 +eriana-jp +loveae99 +maxmobile +osorymall +guilherme +gabinetevirtual +controle +autodiscover.vip +delfin +czar +darkworld +www.cds +sitelifestage +z2 +staging02 +hwjj1004 +m7043 +rk5558 +virt2 +jinpw73 +deves +ilovez001 +mailproxy +comon +eunseong +kiki0705 +euniii +users2 +clstyle +cariere +eventhouse +inneo +zip1 +zico +mmnxringk +redfly +eagle9753 +yeunddang +freemu +hslove80 +poli2003 +ramarama +rlaxotjd +tidgodl +hami0323 +gndo00 +kooragoo +timeleft +ilearning +us3502 +snowman +ccyulim +fame +arabian +qnubenhs23 +hsh7933 +lj4100 +www.fp +dnfl1206 +iworld +ifdesign +hjs7985 +eyoung2003 +anydaum +webdisk.todo +www.econ +katze1004 +s0319y +dadasa2 +riugombo +yann +yagi +l33650 +as011 +ju8646 +wow1 +kjy1823 +cestlavie +etoss +mssql2005 +holylove +dand1135 +rlekfuwlp +augusta +azul +sechuna1 +autodiscover.pms +ww12 +autoconfig.pms +spyro +wg12 +www.pictures +silkworm +autodiscover.todo +webdisk.pms +kado2 +pokeradmin +kimjr1941 +evrika +burnhorn123 +nso +kingchoon001 +www.rms +storyone +hma5400 +comune +trud +cjb +nami000 +saytool +vatek +do504005 +vava +autoconfig.todo +crush +primo +do504004 +npart11 +dlink +dbsdngus +ehfdkrksms +tory +800 +googleadmin +leglong77 +esx01 +cocktails +parosa +hczerny +sure +qboouy890 +ksy3151 +suny2858002 +dox +xboxlive +verde +kilo +disaster +virus0316 +tonga +isshoe +www.light +nextel +jimmys +timo +jaca +runews +station1 +tian +yci2000 +hubert +nmshdjsu78 +www.mango +choichino +doniworld +www.porno +sirent +ttu264288 +weeds2251 +jina2493 +oden +en.test +vertrieb +test-ssl +test27 +www.maxim +bigshan +puddles +joyongkore +pp725 +inet1 +tada +corporativo +mc277668 +shot +cotacao +shoesadmin +aomyunswbs +gujecat +justfun +hellyhs +growing +car040404 +dub +toilfox +ika +cap1122 +pilot83 +lachy +sejinilove +kws1388 +noyoung +zixvpm01 +r0921 +sham +hwa4394 +feel701 +rota +yahho +bebezzang +roof +yiwutc +control1 +spamcontrol +ald1034 +goodfeel64 +road +oratest +yws +yeoli9 +garrison +sare +amf +skola +bluelucky +muledeer +bulkflow +sake +ondemand +epicprintservice +laylie +riko +donau +tok2580 +fajar +www.adams +xixi +brooks +ssonda +yozme +reim +l9051 +vrs +clouds +dedi21 +dudwnls10 +dnjsdl79 +rayo +pixel3 +khn1212 +www.tlt +inerjjang +www.anthony +dept2 +archi80 +qhrhvmssu +adonis9966 +beijin2783 +take1001 +answn0240 +dgdz +autoconfig.bugs +piyo +autodiscover.bugs +farid +ackbar +www-prod +archive1 +tpa +kcs0713 +alden +serbia +klink +signups +distribuidor +galls +www.solaris +yenim +peep +bestoffer +isfahan +terrier +marlene +alwin +tohoku +balsa +ha1 +vmp +kyushu +ns001 +sica +instructor +fm2 +myfirstsite +redman +veterans +telefonia +ladybird +milos +vesna +adina +dcjark2 +musicstore +zhengzhou +kristian +basset +www.te +ddb +mxb +mortel +www.party +dazzlers +programmer +larissa +elise +attachments +tkm +homeloan +flip +www.jf +aer +radion +serialkiller +rtr-cadre +bal +elements +tww +securetest +emran +reio +oksana +stress +raido +blackstars +survivor +mundial +eka +ning +www.andrea +iip +socios +jpp +123123 +vgw +providers +soe +gaara +wit +asser +moni +pmr +lil +www.payroll +gajah +mone +aston +www.otaku +butik +rin +www.cdn1 +ptl +netmotion +www.karta +rgp +ap30 +oldstats +tch +parabola +caritas +www.animal +awww +www.sbt +www2012 +mail.nsk +testserver1 +vmk +voz +orbita +blimeyl +www.bird +angelica +www.kg +kassem +oldham +imperio +hellen +mladen +fmail +dolcevita +honest +hams +hard +linden +ru2 +pulp +adriaan +sankyo +wineadmin +alternative +confidence +sidney +newstyle +www.prada +stile +lune +kazama +dare +vybor +ebony +shannon +milksugar +haya +abhishek +arma +yourhealth +deus +goli +hackz +helptest +vipmaster +ftp.web +vds13 +comenius +pdu7 +pdu6 +pdu5 +adela +aeolus +ekat +sesam +rag +chimp +laurence +github +hatim +faza +collectd +niconico +gren +vektor +cerium +ambrosia +www.cap +glas +www.animals +hava +niranjan +brucelee +jaan +midi +jaja +muneer +chet +onlinecasinos +averell +wunder +tizer +graf +r2.reboot +illy +mesa +inda +jeet +margarita +strona +s276 +lakshmi +living +gtaiv +www.protocolo +astana +lexx +mstar +s425 +karl +iro +rabat +mmo +www.titans +logistik +koleso +web369 +hotro +mup +leszek +blogspot +s247 +s245 +jacko +welcom +www-d +beetle +ohashi +www.speed +jacky +dogwood +ftp.ask +aruaru +forlife +ip14 +windylion +jove +neweb +www.sound +flyaway +dtp +kivi +jafar +lapa +niv +d54 +celsius +wds +videogames +powerschool +klon +jahan +mta6 +mafalda +mta5 +zoya +liam +janet +pdu +lexa +propane +uchi +mail.main +kore +monterey +jatin +dreamhouse +koro +romanos +mala +lapcooked.com +trantor +extweb +www.automotive +thessaloniki +ovz +mailgateway2 +marg +mats +www82 +www.ultra +meka +intranetdev +ipg +mesi +harvard +giorgios +mygroup +miao +www.venom +avanti +nama +luan +nccs +neda +buster +nena +mcg +veni +jung +karak +pawel +neto +karam +bonanza +mao +myhealth +elina +nilo +markanthony +cluj +pagan +rac3 +century +dinara +zel +rackspace +narod +cauchy +volgodonsk +nunu +dlib +iguana +nintendo +tproxy +org2 +pion +chemlab +pma1 +loja2 +tattooadmin +cesantia +prix +radi +cameroon +vsmtp +solicitarclave +keng +cbg +mlab +kenta +rasa +rawr +kee +renz +kata +www.mohamed +saeb +genesys +189 +kamo +www.bugtracker +188 +sark +proshop +rclwp791749 +ikarus +154 +strat +shun +hyip +vertical +intact +static.dev +www.makemoney +programa +autoconfig.auctions +bbstest +schedules +esms +autodiscover.auctions +ugc +zg +sonu +backupmail +jsoft +sosa +kaseya +remoteapp +scot +spro +animeworld +imagen +artists +iserv +stil +arr +balthazar +musictv +jamesbond +support-ru +uday +rayan +holz +susi +masterdb +kondor +kolik +www-cache-all +rotary +webdisk.deals +gunz +messageboard +testforums +cumbia +wapes +www.jump +ufos +regalo +wsam +wireless2 +samorzadstudencki +eic +tutu +undo +hihi +www.pro.glass +theclub +speedtest.nic-west.cy +patton +yjsgl +amen +wael +justtesting +www.chalet +www.chance +pro.glass +www.illusion +www.mypage +alonso +keaton +hand +sima +takeshi +in01 +maket +ftpmaster +vieclam +maniac +bostonadmin +bomail +hearing +mini2 +papers +www.myblog +xman +exia +www.welcome +kemahasiswaan +marianna +evol +manel +hro +wwwn +autoconfig.indonesia +www.spravka +www.horo +www.vd +www.fair +mariam +brightside +www.italy +bonilla +dynamic2 +magazines +ascom +machi +www.elektro +www.philippines +rci +gage +autodiscover.indonesia +webdisk.vietnam +systec +webdisk.indonesia +htmltest +dynasty +s4104 +machida +thumbweb +officespace +psu +mehul +adj +heri +lopes +lopez +farshad +bconley.com.inbound +wasf-law.com.inbound +redmine2 +ds01 +topten +lemonde +izolda +zita +www.alexander +drug +www.martin +kato1 +assoc +lotfi +fact +lou +bud +frisbee +cmb +dee +webdisk.pay +fabi +www.freebies +verification +relay5 +e8 +e9 +akademi +aig +secure8 +woodruffsweitzer.com.inbound +midwestglove.com.inbound +dome +oci +server61 +modental.org.inbound +hannibalbpw.org.inbound +login3 +sife +ophthalmology +dini +misha +najme +toluca +middleware +komenmidmissouri.org.inbound +edge01 +wg2 +testr +eigo +communitybankmarshall.com.inbound +webstage +aic.org.inbound +theatre +ibf +iap +msma.org.inbound +fbc-columbia.org.inbound +kaiteki +hsa +b35 +auta +dayz +www.odessa +b37 +tix +daffodil +dfm +gerke.com.inbound +freezer +b38 +www.fe +mohak +dr-mail +withersradio.net.inbound +www.kostroma +astrakhan +mail.oyun +mikekehoe.com.inbound +b41 +kse +ns.oyun +maison +b43 +kmfc.com.inbound +ipv4.oyun +mgn +kopn.org.inbound +webmail.oyun +mosab +autoconfig.play +krcg.com.inbound +mustapha +b47 +thedoctorshelper.com.inbound +b48 +vampire1 +mpl +checkmail1 +daw +cafw +mri +checkmail2 +kpo +nidal +interconnect +autodiscover.play +emac +pcm +webdisk.play +rahmat +www.sie +nikos +technica +hsd +autoconfig.social +moberlymonitor.com.inbound +danielboonell.org.inbound +rce +v21 +techops +blogi +joycebremer.com.inbound +klik +gsr +bln-stpt +gardenia +cbofmo.com.inbound +manitoba +www.lc +www.zzb +dementor +edhardy +douga +da4 +freesms +m23 +www.cw +sslgate +cao +shilohranch.org.inbound +mfaoil.com.inbound +hidamari +fblmo.com.inbound +executiveadvantagellc.com.inbound +notas +atoz +newmy +leestirecompany.com.inbound +loveallrv.com.inbound +www.integration +cpps-ofallon.org.inbound +goriley.com.inbound +barsa +moroni +takahashi +leave +cano +sinergia +cana +www.fantasy +www.marketplace +prb +prl +tanaka +valery +avila +zdh +leaves +pop4 +bolsa +countryside +linus +aod +sahar +www.nuovo +jxgc +webdisk.preview +phe +ltc +bibi +wwwakamai +smartpc +zjc +gi-6-1.edge-r.fra.de +te-1-4.core-r.lar.cy +tactics +lpdns +bras +webdisk.t +zch +plymouth +webdisk.g +anta +cny +perforce +roll +bioinformatics +stalin +forrest +colour +armadillo +autoconfig.orders +www.nj +deuxface +hokuto +www.rm +mini1 +libero +epica +pentest +www.podarok +autodiscover.orders +webdisk.orders +mj289 +www.camfrog +www.coffee +smk +www.dubai +www.hits +riv +greenfield +allergies +c14 +www.distance +fianet.xml +www.boston +partenariats +c15 +gelen +kazuma +c16 +c17 +webdisk.press +newcity +hiro +freetalk +c18 +mail.support +c19 +robotic +alexey +cadremploi +c22 +partxml +gogogo +back.partxml +img12 +c23 +c24 +helio +gomel +brooklyn +engineer +c25 +patches +anek +c27 +hiper +hooloo +www.crafts +smtp-2 +saqib +www.peliculas +older +runrun +c28 +perry +stat7 +westpalmbeach +c29 +equipment +stat6 +seventh +sace +lx1 +c31 +negocios +c32 +c33 +q35 +q21 +hq3 +kaden +raf +q8 +www.urban +karlo +c34 +watanabe +c35 +liming +fortworth +wilkinson +boulder +www.behzad +www.melbourne +lunchbox +nevertheless +q1 +www.membership +c37 +wojtek +speedway +www.span +c45 +callobserver +homewood +output +www.origami +hrnet +medina +oaw +hajar +www.limited +amakusa +anaheim +iron2 +www.haj +mixer +yuriy +johan +www.present +levelup +fritz +www.orlando +holyspirit +squid2 +pauli +waltz +www.bodybuilding +tegrity +pooya +cbk +laurel +davidjones +greenwich +blackpool +botamedi +providence +memcache1 +anshin +caelum +lionheart +www.gsc +plane +php2 +www.gta +www.ali +vip11 +pnc +www.tim +bushido +ethereal +e12 +mythos +lifeup +www.dragon +sevilla +d3.files +toplevel +kuperkorea +e24 +www.good +d1.files +saif +aram +scary +d2.files +tototo +dell2 +vip10 +e107 +gameserver +www.pol +31sumai +resona-gr +webdisk.form +daybreak +zenrosai +pcw.istmhd +pcw1.cyahd +raouf +autoconfig.form +keele-nnw-leis-mc-leis.net +www.nyc +autodiscover.form +replicas.ldap +autodiscover.galeria +autoconfig.galeria +lib-ht-2.net +r-es-1.net +lib-ht-1.net +xtermsrvr.gradsch +bcst-rs.hor +downloads4 +www.wpb +recfs.kis +kuwahara +rtr-t.lin +lib-ln-1.net +61-e.lin +blakout +ucb +soaptest +r-ch-1.net +pcw1.cechd +autodiscover.dating +autoconfig.dating +openathens +www.mic +rtr-rs.hor +huda +gnat +sarasa +wc00300.wifi192 +net-x.bar +yamaneko +bootstrap +builder.manage +www.seminars +muzee +anapa +treat +pcw1.ugmhd +deniz +net-w.bar +ncom +net-s.lin +shares +wc00300.wifi160 +nfreya.net +trail +rlaguswndl2 +fujisan +pcw.cyahd +afs.cyahd +afs.pmed +www.ken +r-dw-1.net +bcst-ob.ohx +wc00300.wifi96 +mail-backup +net-e.lin +www.moj +mitsu +liens +afs.medx +cmstore +xtermsrvr.netwshop +rtr-x.bar +bcst-cta.lin +cabal +rtr-w.bar +lib-oa-2.net +rtr-s.lin +net-ob.ohx +darkdream +www.formula1 +sn2 +net-t.lin +rtr-m.lin +refah +pcw.cechd +agentfox +zeal +win27 +afs.istmhd +afs.cechd +rtr-cta.lin +be2.server.twtmail +win23 +r-ht-2.net +r-ht-1.net +icand +xtermsrvr.kopen +webdisk.intranet +toddy +pcsrc.kis +www.aikido +danesh +www.cake +gara +magda +h101 +www.onix +pcw.ugmhd +afs.ugmhd +clubtest +www.vle +www.dev1 +forsaken +be1.server.twtmail +afs.kis +emea +lexmark +psb +anson +freya.net +61-cta.lin +be3.server.twtmail +lib-ln-2.net +misterx +minimax +rtr-ob.ohx +b40 +xtermsrvr.temp +b50 +efc +c30 +e20 +scimte +hitec +rtr-e.lin +fluxus +mail.hi99 +stfafs.kis +xtermsrvr.lect +r-oa-2.net +mil5500 +hwbgz01 +demo-webconfa +sjm +so4 +hi99 +www.karin +r-oa-1.net +shore +bcst-hj.hor +gboard +tibor +pcw.pmed +r-hx-1.net +volcano +builtin +bcst-x.bar +amoxicillin +chiron +aiken +mikyung3422 +newsms +pun +mail.av9 +server56 +server57 +mohammed +pcw.medx +bcst-w.bar +vpngate +uni-netebas +itunesu +szxmam01-sen +pcw1.libhd +bcst-t.lin +bcst-s.lin +net-hj.hor +lib-oa-1.net +salar +tr2 +sameh +inner +pcw1.istmhd +lkpf-dx +reestr +everdream +print-mo.net +bcst-m.lin +xtermsrvr.lib +xtermsrvr.kpa +nov +s70 +www.new2 +rtr-hj.hor +tivoli +szxmam02-sen +spas +samir +net-rs.hor +bcst-e.lin +sjpostad +km7007 +starsky +stuafs.kis +mail.int +mun +updates.kis +ijeltz +bb.vle +xtermsrvr.plroom +mvc +pcw1.pmed +s81 +salto +sanda +r-is-1-2.net +dlv +s83 +saran +bighand +kookoo +spill +lsf +pcw1.medx +lsc +pagamentos +s07 +sauce +mbb +wc00300.wifi64 +solis +s09 +www.joom +sayed +s87 +www.ijeltz +kik +net-cta.lin +av9 +whitelist +kcl +sobee +haj +www.testy +localhost.cc +r-ln-2.net +erp2 +www.det +ankitjain +myhosting +straight +bustup +fr.test +senha +skydive +mizu +demo123 +proxima +pcw.libhd +www.cnr +r-ln-1.net +gpt +www.programy +shani +oceans +rowan +mail.av9898 +afs.libhd +staffprintcluster.kis +eys +vpspanel +cs-utils-rtr.net +av9898 +w-htgb-a.net +dad +creo +gbc +unite +shona +uriel +darts +4test +sinfo +galerias +equilibrium +business2 +garp +inst +www.eos +optin5 +idisk +www.amb +optin10 +econtent +shyam +optin1 +optin2 +optin3 +optin4 +optin6 +optin7 +optin8 +optin9 +harlem +heavensgate +coffeeshop +webdisk.assets +www.wb +qzone +harami +ebenezer +tasha +webeoc2 +autodiscover.realestate +autoconfig.realestate +momiji +shift +szmail +appserv +webdisk.realestate +yas +rudra +sheep +www.ni +b2bqa +esprit +makeawish +www.christmas +fc1 +www.paysites +www.empire +osl +slick +boogie +pdl +app10 +lop +ans1 +ans2 +bellevue +kirara +booboo +altemis +sober +cinderella +synnexdns +blu +ghs.google.com +fcp +glxy +syslog1 +b46 +margot +ecity +ryan1 +arsenalfc +fts +videoman +nolimits +saleh +chb +r8 +www.ab +lunatic +fonts +autoconfig.suporte +sammi +autodiscover.suporte +www.af +cntt +www.ag +mainserver +catia +dns117 +www.theater +snowflakes +proofing +www.dv +root2 +tigre +timex +assassins +cluster4 +www.ha +nastik +ns4a +lovestory +ns3a +www.hb +plastic +web3d +cosplay +designme +www.gr +web47 +gtb +web42 +web40 +dmi +ftp.video +smarterstats +videogame +appletree +puppy +mserver +ftp27 +ftp26 +mail.edu +ftp25 +www.marcus +shib-idp +ftp24 +pmu +ftp23 +mysql51 +psyco +bite +stunt +ravin +ftp22 +www.jv +info3 +lpc +ns142 +iwin +ftp19 +ftp18 +phiphi +w23 +ftp11 +dnsmanager +www.presse +www.mf +www.lt +mercurial +avail +ankara +pshop +soldier +sodium +indie +pdc1 +accept +pc110 +www.oz +ftp.crm +sotestapi +www.nw +chatterbox +www.africa +cookiemonster +pos1234.netcologne-mw +litchfield +c-kurs +www.panama +thistle +hermes3 +conferencing +archeage +blade02 +freshy +www.doors +somi +ktel +myradio +denny +mousika +antallages +worldofwarcraft +xartis +sport2 +www.drivers +columbo +www.xmas +thetikienergeia +www.viva +travels +www.michaeljackson +qmail2 +bonjovi +pluss +pl1 +staffweb +chopper +reliance +www.ecology +rural +xpam +plus1 +aggelies +plaisir +ptolemaida +shirley +chouaib +umair +gw5 +mag2 +gw7 +many +rd1 +www.interactive +firstline +pars +traveler1 +mim +www.mysite +hosanna +painkiller +sweethome +web128 +web127 +www.nirvana +web113 +www.fotograf +aurore +pinto +pinta +svn01 +dummy2 +intruder +thevoid +web108 +web107 +naijatuale.com +web106 +www.jx +web105 +techtest +vgame +www.ys +advantage +web102 +monitoramento +netcom +apocalypse +salavat +www.hassan +perla +urdu +yamayama +superuser +iplist +pecan +nak +voltage +www.testshop +scn +rehan +celebrate +spirou +gamblers +www.adsense +yancancook.net +web209 +jatt007 +panta +www.gazeta +taimoor +lom +tudou +tello +sirius1 +www.deathnote +www.hospital +apple123 +www.sco +pandawa +bnp +matsuzaki +kaa +ilikeit +br2 +backup03 +adarsh +gutschein +alkelaa +web62 +demo-imeetinga +saikat +madoka +mail-ext +www.junior +noisy +web208 +autoconfig.ns2 +autodiscover.ns2 +web98 +math2 +ramadhan +free-software +dailynews +geoffrey +claudio +web49 +a123456 +web46 +web41 +rek +web39 +zags +walia +web38 +rockwell +dnevnik +waqas +hellraiser +home3 +emarket +kb2 +xatka +static8 +posgraduacao +wasup +leisure +mx09 +mx08 +lampung +sip.abas +merchants +msoft +lilili +b74 +www.enciclopedia +matthew +rivers +startimes2 +singles +neverland +mail.mobile +bengal +www.egitim +aubade +gok +n62 +sakata +farzana +sacred +mozzi +netsoft +www.myworld +dni +linlin +indico +mangesh +hackermaster +stream6 +monia +never +blackmarket +kareem +web-2 +demo-reg-hostingconfa +duality +www.clickbank +revival +dragonball +runescapebeta +test12345 +s520 +reddevil +yms +cc4 +fira +muneeb +sireg +aco +mas3 +michail +vertikal +positron +goodwin +xlab +webdisk.2011 +birth +chair +mukesh +rio2 +mahmud +fakultas +dph +packet +canaan +milad +mail.mse17 +cherepovec +www.ufo +nalchik +webstaff +missworld +habbomusic +nesa +jkt +pcclub +gaokao +likewater +jk2 +duffman +nass +ludus +docs.dev +www.lj +jameson +shaheen +graveyard +autodiscover.aff +www.load +diag +webdisk.aff +autoconfig.aff +csn +eastwood +pieter +marcela +pppp +mikki +msgs +shaimaa +egg +www.anti +files5 +manuka +mail.mse7 +sh8 +webapps-test +longitude +xiang +www-qa +serveradmin +sh9 +bicycle +www.ares +freecoins +mail.mse20 +www.hd +slovenia +www.ke +bbs7 +mail.mse8 +uzair +shaper +medialab +edetail +test47 +ips.vds +punch +webdisk.au +www.cq +webmailnew +cdns2 +test09 +mx.mse12 +goal +dwp +mail.mse9 +news02 +www.bh +montpellier +ftp33 +martha +cdn161 +sarg +pile +workorder +www.olympus +nicholas +ftp30 +vsv +iwt +masumi +components +miya +lake +helpdesk1 +una +prisonbreak +thanks +branding +sjbluecn +mobileworld +ns211 +syb +ns231 +telefonica +rampart +himki +seychelles +ssk +corleone +www.boss +smu +pm3 +mx.mse20 +asa2 +overland +ftp29 +dila +kickoff +ftp28 +cas4 +winweb01 +m03 +m04 +ctb +shc +sgg +elwood +mail.www +wuxin +mx.mse22 +mh1 +smtp-test +dn1 +konoha +ethos +www.eda +renault +thesource +win31 +cavin +mailhost1 +flare +win34 +mice +cavuit +paraiso +pml +oceanic +nyhetsbrev +url-server-cn-3 +webdisk.love +opx +xiaoban +www09 +dev40 +snickers +bdb +autodiscover.love +cftv +lexikon +win30 +win35 +autodiscover.filme +pf2 +host-1 +melina +unicreditsim.investor +snx +autoconfig.filme +akademie +nns +webdisk.filme +vinny +smtp.mse17 +smtp.mse18 +genealogie +smtp.mse19 +smtp.mse23 +smtp.mse22 +leblanc +facebooklogin +dynamite +ilink +id2 +www.fitness +no8 +mow +webbank +mnm +mnk +crtrieste.investor +autoconfig.love +www.em +nan +sgi +zaurus +retriever +newtechadmin +vpnb +xelion.investor +mo2 +kym +mfa +affinity +airfrance +dhcp4 +snapper +kdm +bancacrt.investor +host196 +barman +host193 +smtp.mse20 +h26 +www.ib +secure13 +www.streaming +autismadmin +sciences +naveed +artykuly +ordini +www.pi +cassamarca.investor +ksl +ksg +harare +lch +www.investimenti +ricerca-ac +lay +kkn +cariverona.investor +fhm +jsk +caritro.investor +esmtp +smtp07 +smtp08 +ntb +h22 +daniels +www.short +h25 +htd +www.ug +march +dnsb +jjj +shop4 +favorit +www.lineage2 +www.call +googlemini +esk +ppl +host131 +proxytest +host124 +scores +rho +biochem +shibidp +admin.mail +chaka +runescape +host116 +crick +rsport +eliza +cityweb +gabrielle +www.bloom +mailuk +host113 +hancock +uu +myface +bigred +yamuna +glitter +mail.mse10 +mlc +renegades +myspace-login +mail.mse11 +mail.mse12 +staging.administration +host111 +ifb +mail.mse14 +mail.mse15 +mail.mse16 +autoconfig.sales +host109 +autodiscover.sales +mail.mse18 +mail.mse19 +www.if +zubin +syllabus +chutiya +mail.mse22 +m17 +mail.mse2 +www.hope +parse +ksoft +nagios3 +gwa +mailsystem +serversupport +www.dolphin +host108 +wowinfo +aut +hy.lhzs +stg.www +host107 +vlab +citizen +host105 +host104 +www.healthcare +csis +timekeeper +has +duluth +schsmtp +statler +facebok +fre +download5 +turnir +asa1 +www.gct +flashtest +mx.mse7 +www.just4fun +ktech +project7 +cher +testwiki +mhsmtp +logins +rlp +kisa +gct +eet +host102 +cum +pontos +spenden +ephraim +stockholm +geodns +kitt +reno +alexis +mail.mse23 +mail.mse6 +vigi +smeagol +bpk +mx.mse10 +mx.mse11 +evaftp +renaud +mx.mse14 +vhost4 +unibanking-test +smasb2b01 +mailspam02 +apif +www58 +www57 +autoconfig.gmail +email2003 +mail-out02 +ane +autodiscover.gmail +mx.mse15 +webdisk.calendar +mx.mse16 +bbv +evabid +webdisk.moodle +host106 +bb9 +mx.mse17 +cargoappmsg +mail-out01 +hifisweb +mx.mse18 +mx.mse19 +eva-rms +classicrock +host110 +flighttrace01 +flighttrace02 +unibid +myegsc +myeva +curry +hacking +linna +weblogs +www.replay +av01 +immeet +www73 +cargoecdvp +hail +mx.mse23 +host137 +myforas +www.wirtschaft +www75 +mki +elc01 +fisnet +myeva3 +library1 +parque +myeva2 +tosh +gibson +evapm +www.album +fiswebservice +cmscpbs +kenken +gogoeva +host125 +transformers +layer +fpsweb +elearnqa +mx.mse8 +fisoem +spsowa +www68 +naresh +mailspam01 +real2 +shivani +ambsweb +ladybug +mx.mse9 +evaflow +santhosh +evawt3 +mx.mse6 +www.leon +gcstest +correos +arkan +comercio +test31 +apifweb +myegat +smtp20 +smtp17 +smtp18 +smtp19 +soldat +imextabs +shinobi +evakpi +mx.mse1 +epos +mx.mse2 +slartibartfast.itd +s80.as +diagnostics +pastel +s105.as +kram +host170 +archives2 +motoki +www.akatsuki +mado +ansar +namnam +s3.svr.tdzs +s13.as +host171 +host173 +trainer +host176 +test24 +mail.europe +pejman +fod +configure +san2 +smalltalk +s104.as +www.akira +host177 +oujda +host188 +the-best +host150 +truestory +dystopia +migrate +s190.as +www.tournament +s106.as +fujifilm +snooze +host130 +s1004 +s1125 +freedownloads +host133 +mabel +isca +dragonzone +alumnitest +b92 +s103.as +s134.as +host135 +s107.as +autoclub +www.arc +euphoria +nsg +n5 +zpg +bb8 +ame +s108.as +weareone +mexicanfood +s17.as +h2o +winvps +scorpions +bli +fourseasons +pradnya +vanna +sidebar +paulina +dev-web +which +retailtest +belfast +www.sidebar +thegrove +sangam +s110.as +crazychat +kawasaki +s102.as +ziggy +metin2 +sabra +punta +loginfacebook +marengo +mesbah +lacie +botnet +portabilidad +urbanstyle +exeter +l23 +bloodlust +www.seth +nimes +eno +marryme +s111.as +cerebrum +www.ad2 +ruch +w2w +cdntest +mima +zynga +www.whmcs +autoconfig.panel +autodiscover.panel +s19.as +grafika +fattony +s101.as +darkangels +s5.tdzs +nairobi +zagreb +cercetare +mcbain +lukman +gwb +strategia +s112.as +admitere +hic +www.mec +ded +hig +montero +tickers +paramore +dalibor +gri +kul +arlequin +gup +s100.as +martine +redis +homologa +s21.as +s97.as +h88 +h87 +news5 +h86 +h85 +h84 +mobs +s4.tdzs +h83 +h82 +h81 +hoteltest +mehmet +www.april +mietwagen +cooltech +wishmaster +webdisk.mob +magnacarta +dico +zombi +hamdy +andros +iapetus +bats +h67 +h66 +hoo +s113.as +mailus +hotman +zizou +h57 +phorcys +potomac +h56 +h55 +epimetheus +hsk +coldwater +coltrane +htv +www.hoteles +s60.as +admin.mysql +h51 +momos +h49 +s22.as +www.cuba +parana +www.yoyo +asavpn +islamway +elbe +upk +zidan +vali +h43 +h41 +mgc +upsilon +www.gaby +h38 +efiling +h37 +h36 +h35 +geoserver +h34 +yosef +host132 +h29 +spv +www.coupons +host134 +gangstas +h27 +view2 +prt +s513 +third +s114.as +adeline +zgame +banca +mq01 +s23.as +www.tg +host172 +s322 +h90 +anjali +www.rt +host174 +host175 +www.sj +h68 +gamerboy +h50 +gss2 +h48 +h30 +www.jl +h28 +ktf +nakatomi +host194 +raquel +mdl +host199 +wyse +karman +soyokaze +gamepark +secure04 +s115.as +secure03 +cau +mater +www.affinity +www.ep +cooldude +luz +magnetic +radioadmin +pc252 +updater +xtrem +shoping +www.eg +mrv +obl +mug +s116.as +id1 +www.umfrage +listados +autoconfig.forum2 +pc104 +anca +liu +myst +s25.as +bugreport +xx163xx +usagi +cruiser +berserk +wtv +conteudo +autodiscover.forum2 +op2 +zabbo +prepress +livecams +hotsite +festa +ore +s117.as +mayank +www08 +po1 +dev143 +duty +www.emc +norilsk +abit +waldo +xserve2 +powers +maximo +keep +habboretro +alpha4 +maxis +cib +elab1 +encyclopedia +syscom +qlikview +officemail +yemin +saw +win02 +s118.as +usu +www.cce +www.musicworld +www.bla +guppy +icons +alka +mcd +sax +www.dany +htc +popa +glossary +posta2 +tb1 +s27.as +jafari +gamefree +tak +slg +tcp +kstyle +pm4 +pm5 +wins01 +sum +www.kinder +www.seychelles +maumau +s120.as +diverse +tre +ns182 +awm +ras03 +olivos +www.farmasi +webshare +servo +ns172 +cmsweb +logica +esxi06 +ns192 +tde +manager2 +nikhil +northside +audio2 +wbs +vov +moomin +prtest +cache01 +marthe +idpdev +asimov +clockwork +wsf +secure9 +surveyor +mtech +mall6 +yyy +lync-edge +s121.as +bath +www.bk +s1.svr.tdzs +erina +normande +uos +test21 +tile +www.aris +king3 +welcomeback +homme +test28 +webcontrol +clematis +marcia +mobile.news +littlesister +www.bangladesh +chefkoch +internet2 +evi +andra +openwebmail +spccore-router +momoiro +lotos +ssearch +extvideo +s122.as +olimpo +stf +proxynp +s8.tdzs +flashmedia +wallet +carousel +publinet +www.gw +files7 +s31.as +www.szablony +blog.dev +mbr +prestyle +xavi +mslogin +miley +www.ht +pubnet +datafeed +www.kc +dwb +s123.as +jhb +milhouse +chrisss +amefirew +bomberman +kodos +sideshowbob +sound9 +jinzai +sandbox.api +richie +zixun +petshop +primequizzes +mstyle +lakhdar +www.hobbies +jacobs +ichigoichie +ijs +mailru +shimada +rotaract +quanghuy +nnovgorod +fool +odm +detectiveconan +jack2566 +mymy +megha +imager +kailash +td1 +clicker +contra +crus +irecruit +lwbsb +secure-mail +s124.as +gatekeeper2 +old-mail +myself +mth +darkshadow +s64.as +genero +saya +funnyhaha +google-search +oktober +medvedev +s3.tdzs +hikaku +s33.as +mimic +bober +undernet +backmail +speedtest3 +www.trial +seasonal +easynet +hifive +hemali +buenosaires +laperla +plo +mariposa +catharina +expertise +cerber +primetech +linkbox +ctv +spiritchapel +oriental +felicita +bidb +serious +uygulama +blackboardtest +infra3 +fishy +musicon +maildr +nude +administrador +madura +s125.as +www.maya +eapps +www.staf +empleos +www.legend +satelite +time3 +www.splash +ed2 +faccebook +ogrenci +www90 +www.legion +nazuna +special1 +ecommerceadmin +wesele +b72 +simpson +pac2 +www.gratis +oreo +web56 +tgc +pina +monsoon +andesite +s126.as +www.budget +anduril +comp1 +hyde +web96 +vladi +admin11 +fastdownload +brora +gladius +thewarriors +svod +newfacebook +s35.as +harshit +in1 +web181 +fletcher +usoft +loveandpeace +av99 +www.techno +naman +go2av +web163 +web48 +existenz +lamejor +web75 +web76 +novice +mail.z +wc2 +s8.svr.tdzs +web85 +mail.go2av +u6 +aadhaar +gameon +mail.plus28 +autodiscover.clientes +www203 +manish +web91 +web92 +web93 +web95 +web97 +plus28 +web178 +web180 +web224 +webdisk.clientes +klas +autoconfig.clientes +mail.99770 +mail.av99 +vipul +web210 +mail.9son +9son +myweb1 +momus +web213 +mail.adiscuz +mywebs +kunde +mycom +s36.as +mail.tudou +ljm +web65 +adiscuz +exchange07 +ms13 +exotic +99770 +dummy0 +web225 +viral +web226 +web227 +web229 +mss1 +kcb +web231 +blackstar +ap5 +web232 +www.liriklagu +online-casino +vinod +hotmeil +ravel +sldss +mail90 +venere +autodiscover.s +www.asus +www.heaven +triangle +autoconfig.s +brainbox +nose +dinesh +nsz +rmr +mxbackup1 +walle +www.oil +temp4 +yr +halcyon +lauren +se8 +acms +www.consulta +www.yp +pinocchio +captiva +testserver2 +webpr +naat +tekno +petit +biysk +se6 +maykop +s129.as +smtp170 +se5 +orsk +vicki +www.ky +www.armageddon +vps0 +vpsa +taylorswift +scouts +new.test +expo2010 +vpsb +apolon +dummy1 +s2.svr.tdzs +s38.as +venta +s204.as +yourspace +oo +www.watch +karachi +s131.as +zq +web115 +web116 +web117 +web120 +web121 +web122 +www.ecuador +web125 +itnews +baa +pitta +stage-admin +www.supermario +neonet +ddns2 +www.libros +tanis +shambhala +conexao +s39.as +gemini1 +autoconfig.catalog +autodiscover.catalog +mdmc +mail2sms +fasttrack +narutoworld +www.exodus +flowershop +statusquo +yjsc +rns +s132.as +topper +allergan +networth +fellows +punjab +www.eternity +kazekage +www.word +mallorca +ffmpeg +drluke +pardis +www.crazyworld +s41.as +animales +pinkfloyd +www102 +cyberhacker +undertow +amuse +matsu +enchanted +www.statusquo +www124 +www123 +gim +itsolution +web158 +web160 +web161 +rapunzel +hendrix +www.domeny +www.uto +web162 +olddb +domeny +web165 +www.fiesta +sshot +brc +truth +web166 +puccini +s133.as +s119.as +shoe +oes +eager +curriculum +duranduran +tgate +web167 +web168 +fiji +hackerpro +zone1 +zombies +w50 +bsk +vpnx +svr +xplay +cre +prana +trung +percussion +web171 +www.nr +test999 +web172 +kannon +tehnika +admin7 +web173 +sercom +www.nh +www.ne +jdm +web175 +sharktech +artesia +web176 +po3 +web177 +bx +wms3 +sumi +video7 +ns251 +www.makeup +newstore +tonto +www.listas +video8 +ctt +video9 +ns241 +www.kq +www.hongkong +rooney +www.bangkok +edge3 +celina +sharks +kostas +tomi +bassel +s135.as +tora +webdisk.updates +mea +ftp.news +allnews +fb1 +gilson +fairyland +web230 +web233 +scutum +remember +web45 +www.jr +maximizer +periodicos +www.registrar +stacy +matematica +mt5 +nsy +www.guru +headstart +yantai +daniel1 +freire +montecarlo +twig +bok +www.dm +monza +www.bp +dkp +testws +www.myforum +leona +www.bl +nickname +www.an +s136.as +mediatheque +pea +corazon +mays +keiba +www.picture +sanae +www.spotlight +dev.services +www.madison +vignesh +alb +www.medicina +autodiscover.test1 +smtp.test +iodine +edm2 +dob +ftm +sqmail +git2 +supplierportal +wrx +s205.as +fsg +nmp +yakitori +www.gadget +shifa +jmk +ryo +consigna +virtualserver +azmusic +www.anp +www.d3 +lca +lcf +www.records +pav +app01 +pst +tcg +seaside +www.bia +s46.as +moderato +okada +soyuz +tow +vpp +ravenous +lethal +tasya +hongha +www.holy +drawing +dienthoai +kevindev +pop.out +ald +ramune +www.proto +omc +shinhan +ftp.out +webeoc1 +bmr +magica +www.asi +swimming +myapp +www.mn +harima +cog +www.dragonballz +demoserver +www.ars +www.dsa +intramail +www.nms +talia +www.eminem +www.cci +www.goa +business1 +tvm +doh +fte +psych +www.nsr +www.seg +noavaran +mjc +www.costarica +processmaker +sitemail +chanel +dev-mobile +www.writers +cyn +s139.as +mfm +testsite1 +automobile +manas +cln +chatchat +yosi +s48.as +regal +www.cdl +love520 +menslife +hat +vosges +windstar +nguyenhoang +sahoo +www.cytaty +lifeisbeautiful +www.das +grg +saudi +noz +dreamweb +www.openid +fundacion +s141.as +iea +sergo +hoi +zoeken +mybill +songoku +shash +secretpage +i4u +mostafa +teksty +segar +www.get +dns201 +localhost.cs +www.humor +dns202 +cytaty +mail.students +bloodlines +encounter +exoticpets +matchup +sofie +sethi +blueprint +volkswagen +webmail.pec +s08 +s86 +s85 +sasan +www.famous +s84 +s51.as +santy +nable +sando +wonko +krew +s79 +bbdb +bizadm +imation +ctd +ismp +2005 +mwe +sam2 +rgs +jeltz +my-test +staty +stage.api +www.09 +vks +femme +samin +s206.as +www.fly +dedecms +elog +vs01 +tams +www.dienthoai +mymeeting +b149 +palmbeachgardens +time4 +s144.as +earthquake +b139 +span +wrt +h206 +ombre +saira +h205 +www.gym +reddevils +anan +sadia +vs12 +qna +unreality +wolfpack +server53 +h204 +h203 +h202 +h201 +int1 +edv +ongame +lynda +ipadmin +keine +www.jam +tmms +qqqqq +amana +www.adel +www.ipc +richa +detudoumpouco +www.psychology +www.sib +www.opt +sheepdog +billie +ttr +www.ipp +s146.as +www.trailers +h126 +ftp-dev +sculpture +marc1 +supportadmin +ozelders +swing +woe +www.kai +wol +s55.as +h116 +talks +www.esports +exorcist +bluehat +linde +h100 +f113 +www.ist +wst +www.startrek +speakout +marka +e178 +clearing +saria +s147.as +c251 +daotao +shape +c250 +ymd +c247 +c123 +ddv +s56.as +ready +tonny +c117 +lebron +www.lop +narutofan +novosti +tooth +www.itv +b158 +josephine +nosferatu +sadeghi +b157 +expert1 +siteantigo +www.dss +b156 +www.mahdi +s148.as +www.poetry +b155 +dina +macserver +liveon +webha +itacademy +government +colosseum +havilah +www.formation +rafting +nguyentu +b154 +s57.as +www.log +daiwa +winkel +showbiz +www.stocks +mirai +traum +bulletproof +b153 +secur +rezerv +lalaland +b147 +procon +keiko +cancun +www.moo +s149.as +www.msi +kasa +b146 +localhost.lib +vvp +maiko +pentaho +kotobuki +medaka +s50.as +counter2 +www.pec +b145 +roble +yunus +downloads2 +belair +b144 +s7.tdzs +www.thai +s58.as +com01 +perth +iconi +b143 +giftforyou +anet +b142 +chuchu +s207.as +ostrov +www.c2 +www.savannah +margate +b140 +b138 +okapi +b137 +b136 +b129 +tp2 +b111 +bakersfield +b108 +dialogue +b102 +towers +h91 +rajan +lonestar +www.rds +s59.as +timeout +www.sic +yanagi +fantastic +e23 +crazyboys +wyd +dslab +albuquerque +taiyo +www.ecc +rando +falah +www.stp +dnsbl +inge +andover +d67 +www.colorado +www.dallas +browny +d61 +plati +d56 +upset +vernon +rosario +sssssss +mashup +merrick +starstruck +memcache2 +gonzalo +gekiyasu +www.colombia +micronet +www.trabajo +exchange-test +www.dollar +wildwest +chistes +pooja +upskirt +live5 +serv206 +ss8 +oikos +archivepro +bridge-sp +noble +www.zs +www.aim +reclamos +myheart +plog +ourschool +s153.as +moros +www.toronto +tog +www.ryan +nettv +daikokuya +m.news +telcel +fotoweb +c128 +morrow +andrews +chestnut +newhosting +psql +lilium +stms +static-test +networker +s7.svr.tdzs +merch +agassi +trips +newads +www.mining +de.dev +s62.as +showa +es.dev +southdakota +fr.dev +castlerock +globalsoft +www.kent +drmail +www.servicios +cgi-bin +martinez +www.site1 +rnt +c44 +aidan +allyes +princeton +q60 +asu +drt +weba +webstory +d001 +www.proxy1 +redstar +microlab +pialadunia +nouveau +itcom +rivera +protocollo +hq1 +imga +plutus +graceful +toda +q77 +mn1 +lei +beatz +intec +restaurante +cam5 +cam7 +www.publichealth +seashell +zaiko +mx001 +hugin +prova1 +smtpin +eastern +sonora +kendall +v12 +www.zgloszenia +westgate +www.learnenglish +peric +s155.as +phone1 +reslife +dreamcatcher +dnscache1 +dnscache2 +www.guide +lavoro +kensington +kvm7 +rahimi +gofree +fergus +studiofun +sandi +speed2 +aabb +qa2 +abc2 +www.cosmos +pass2 +yoshi3 +gongyi +idrive +www.diablo +herring +acha +peterborough +www.regal +shopshop +pawan +autodiscover.press +latest +autoconfig.press +collect2 +www.future +s156.as +unet +metropolitan +hima +bandb +www.storage +pavan +berkay +www.venus +mavericks +www.veronica +perpus +davenport +bbms +s65.as +tape +preview-m +bahonar +apparel +sdk +b98 +autodiscover.register +allo +www.bloodlines +pcbbs +www.ilove +kiemtien +warrock +www.esf +plc +snack +thietkeweb +penrith +mxserv1 +webdisk.domain +b97 +autoconfig.register +trademark +webdisk.register +ddm +s157.as +ns.dev +clink +kamila +aimages +pz +botan +philly +eelab +prod3 +dsweb +www.riverside +s69.as +b95 +xgzx +gamestation +medialink +www.delta +falco +parag +webdisk.stats +s66.as +beto +codetest +aone +dcadmin +b94 +cpadmin +b93 +server0 +fearless +aran +h4ck +jimbob +eclub +comex +db14 +clima +macon +ravage +b91 +vaevictis +forumweb +www.adwords +www.pmb +b89 +anakonda +watashi +miyake +hys +gmailservice +b86 +play-online +boing +salvador +sugimoto +bogus +bobba +netkuu +nihongo +raijin +psion +anfro2580 +yuyuyuyu +www.george +families +s158.as +librarians +b85 +b84 +webdisk.fa +beton +comunidades +noone +nonon +bayer +maildb +b79 +mnr +ghostrider +b76 +fujiyama +texte +egress +witch +serwer +b69 +b67 +musashi +acceptatie +b66 +isolde +tomioka +webmailold +sbi +buyersguide +b65 +archibus +voltaire +nikka +b63 +invictus +chem2 +b61 +animemanga +helpline +vtech +arl +fufu +izumi +lusitania +stylus +bbp +www.trinity +clerk +www.leads +advantage1 +wloclawek +enum +nimex +imm +saglik +s68.as +b57 +s4.svr.tdzs +b55 +compatible +www.emag +azar +www.offer +fuchsia +b53 +boys +nip +btest +hebrew +www.universum +s143.as +mytischi +aimhigh +netcafe +thanhnhan +b45 +s70.as +shinsei +www.newlife +dbms +raghav +chain +www.ryazan +dax +rad2 +hossein +mohit +cms5 +ser2 +mail.cn +www.descargas +olahraga +wukong +danu +s162.as +elex +www.myweb +paw +deutschland +plume +sasi +www.auta +www.region +autoconfig.md +wb1 +wb2 +acoustic +autodiscover.md +ahxxxhot +mikrotik +seo3 +gto +ldgateway +demi +seo2 +ipi +eiko +s163.as +simoon +ns1.vps +ns2.vps +b27 +www.weblog +habbomix +zope +s72.as +drec +crea +zsys +bundle +conversion +dive +nails +doan +sucuri +docu +pratap +tp3 +server71 +harish +mini8 +airworks +prints +s164.as +g7 +ipad2 +lambert +zubi +dlc +yuma +poisson +michi +gudanggaram +www.sanane +loser +candidats +mamami +moving +yudi +takayama +www.bihar +testserver01 +zima +roh +balzac +mychoice +bihar +edl +s90.as +s165.as +www.survivors +zevs +www.academia +m-sta +elife +rasputin +yong +esmf +m-qa +mobile-preview +mycolors +digimon +www.nigeria +ptw +massi +kuber +zaxc +primaria +tuktuk +heavymetal +www.lada +tedu +www.nebraska +zain +agama +www.nofear +test2013 +www.turkey +radiology +manly +neocorp +garu +globalbusiness +isilon +gcg +webdisk.china +autoconfig.china +zack +autodiscover.china +www.iran +cve +s1.tdzs +mamun +s166.as +dw1 +rumah +manji +yeye +manik +autotrader +embedded +ibaraki +maman +lowie +maxima +wic +jiaoyou +www.turkteam +yami +s75.as +jjw +whj +imtech +microwave +vvvv +www.skyline +finances +www.360 +weka +ezio +caiwu +down2 +tv4 +hbc +www.agenda +wayne +www.seeker +passat +wap3 +webdisk.list +funy +patria +v9 +dolores +webdisk.dvd +t8 +spel +tryout +manolo +maddy +onlineweb +www.lh +s76.as +madar +autodiscover.list +consul +s1234 +hastings +autoconfig.list +onlinepr +kriss +granit +greenboy +tota +libya +hip-hop +nade +igm +empik +suzu +vlounge +s168.as +muhammad +finanzas +suna +randevu +talisker +hopi +device +s77.as +temis +stav +cfengine +stag +tink +www.index +www.fatal +diler +hrc +medianet +ip10 +huawei +fathers +tecno +s6.svr.tdzs +jace +register2 +biztositas +www.biztositas +lon +sharingan +mra +tere +typhon +haifa +runa +sandro +ucakbileti +www.vanilla +www.academy +intranett +www.celcom +matius +kinks +www.darkness +iut +jens +insu +anmeldung +autoconfig.traffic +autodiscover.traffic +morning +www.prog +208 +cd-cat3750-sw +health2 +mabo +rony +todofutbol +roni +jiin +livingstone +ursa +s78.as +155 +bkzs +nejc +156 +moons +www.mailadmin +qube +165 +sniffer +mame +sany +samm +182 +185 +khang +www.merc +191 +jkim +197 +lamia +rino +198 +saed +kasi +vpn6 +fcserver +www.austin +resh +hearts +sources +plants +taotao +jonah +webmail.stage +razi +pierrot +rara +fav +ftpsearch +rani +webdisk.test1 +kenji +vc-cat3560-gw +s171.as +ragu +scala +dike +tmp5 +webmail.haber +christine +ns.haber +www.blackjack +www.challenge +hospedagem +ipv4.haber +poke +kika +kimi +mail.haber +www.meeting +kenan +devnet +s79.as +mitsuba +dragonballz +kebab +jmark +dods +www.bleach +kkok +newtracker +netcommunity +okajima +landings +autodiscover.id +payback +jokers +www.icm +autoconfig.id +chinook +ssl26 +vdns1 +www.tecnologia +tomiko +feliz +www.carter +sasaki +db-master +ssl28 +valinor +ssl24 +ssl22 +ssl20 +stijn +ssl18 +oradea +webcat +mail.omsk +nise +kassi +weal +niit +ellen +itsmylife +luca +moyo +www.religion +taty +llc +www.wmw +nt4 +momi +s81.as +currency +kapil +nadeshiko +kamar +hotrod +luyi +kamal +lmd +supermarket +nath +ishan +mizo +nard +www.bellavista +smak +albina +bestshop +www.katowice +s173.as +kabul +smsservice +webman +ironhide +loll +d148 +znaki +mer +inova +mfe1 +kush +indir +maxy +belyaeva +preview01 +nt1 +www.y +jure +s82.as +rate +autoconfig.monitor +webdisk.monitor +newstar +mugs +autodiscover.monitor +mlf +www.tutor +iknow +tais +webmail20 +www.asgard +kopi +javad +s2.tdzs +shoggoth +juguetes +installation +webmail26 +janez +tack +kpop +s270 +uroda +lele +www.tribe +janem +s310 +www.sunny +jandk +tenki +rebelion +leah +jupi +d49 +s145.as +d41 +d40 +celtics +d55 +d53 +rammy +klds +makh +magical +d51 +funeral +d50 +d48 +kink +mp33 +duplicate +d47 +d46 +d45 +atempo +evrm +s83.as +fits +zarafa +d44 +s200.as +d43 +d42 +d39 +smtp-in1 +www.rugby +www.shine +joao +maj +smtp-out1 +shinsekai +maka +webirc +jul +web911 +www.sears +reiya +mirror5 +www.pixels +vdns2 +emiliano +s255 +wealth +s256 +practicas +worldgame +miva +jman +kyiv +itsa +designcom +www.gamestation +s258 +nomura +login.cqgd +taipei +freebook +s262 +oneworld +s84.as +s264 +s265 +s266 +gunit +www.ragnarok +poison +akadem +torus +isra +s271 +hiren +isni +stronger +inna +kabo +s272 +arsen +www.tizer +sheva +mell +s278 +tetanus +casino-online +www.quote +dosen +melt +sevastopol +hunt +leonid +doladowania +huli +s285 +s287 +s176.as +mert +realgaming +jawa +gia +orlov +available +cisco7 +cisco6 +consider +s288 +auster +s911 +vanadium +2b +jani +grant +s305 +disease +s311 +testbench +sw6 +www.animale +dialup-63 +dialup-62 +dialup-61 +dialup-59 +dialup-58 +dialup-57 +dialup-56 +dialup-55 +dialup-54 +dialup-53 +dialup-52 +dialup-51 +dialup-50 +dialup-48 +dialup-46 +dialup-45 +dialup-44 +dialup-43 +dialup-42 +dialup-41 +dialup-40 +dialup-38 +dialup-37 +dialup-36 +dialup-35 +dialup-34 +dialup-33 +dialup-32 +dialup-47 +hoor +dialup-60 +dialup-49 +dialup-39 +hold +shade +s312 +safire +golds +haos +s313 +s314 +s85.as +hactar +s316 +viktoria +s317 +fallenangel +sankar +snd +blacklight +s318 +s324 +s325 +www.chemistry +nami +www.viajes +charter +elen +s326 +sastra +silvanus +harsh +ditweb +sepia +s329 +www.champions +my-life +mrbean +mp3music +tendo +hansy +droopy +s332 +yawaragi +tex +austral +s335 +familie +s336 +netdrive +bethesda +hamed +kazuya +s337 +newway +www.marco +webgate +datarecovery +s338 +www.ski +s86.as +daneel +goku +miyu +deng +habib +natali +s340 +anytime +s341 +domtest +feb2 +www.thekings +geel +s422 +giel +s427 +atma +gnys +dittest +rahman +s440 +freed +artemida +www.rachel +starway +prosperity +positive +naya +sanangel +pc03 +sftp2 +okra +s178.as +integrate +chandru +medialive +iccs +renat +samira +muz +stef +helga +s87.as +rse +tinyurl +awake +speakers +spectral +joe11 +www.counterstrike +isec +frn +srv04 +cltest +s179.as +moin +extdev +csadmin +bpt +clxy +web2005 +tsd +rvr +www.pixel +tatyana +kartik +ascent +moko +swarm +supercars +olk +mpacc +mog +www.cdn3 +s88.as +ramesh +galan +valeo +yxxt +begemot +fires +mont +s181.as +www.andres +sleepless +ix +hau +dagobah +hp5500 +gav +dwd +arbiter +rcb +wsapi +fofo +narcissus +www.safety +s89.as +raisa +phpmyadm +avr +geld +opac.lib +newns2 +nrs +ishika +www.vts +jinan +rosi +hilary +pollen +jelle +qingyuan +courant +gamesworld +www.gazette +s182.as +mybusiness +prairie +ivona +mehran +annemie +poczta2 +noop +loka +goodjob +gringo +paka +brightstar +nanobio +acd +www.musik +noto +mg2 +private2 +yulin +mastercard +powerweb +s91.as +pp11 +ecat +sp01 +yourway +numb +kameleon +delmar +fbook +web500 +cd2 +web89 +supportteam +teodora +s183.as +davies +boson +hamedan +pproject +amina +safir +dtd +celebration +tiku +rns3 +mail.spb +s92.as +sometimes +mydreams +xnova +aleks +cf1 +servi1 +pill +master007 +wec +pino +syjx +whitaker +devdashboard +s184.as +faraz +plot +pole +autoconfig.demos +xxzx +newscs +autodiscover.demos +geovax +dosa +raso +s5.svr.tdzs +dedi24 +dedi12 +reve +emedia +dedi10 +s109.as +talkfusion +starbook +ribi +www.warren +prophecy +kailas +focus2 +marquez +www.rebelion +paule +saza +engagement +s186.as +www.sell +nodo21 +nodo12 +seng +fs29 +dbtest2 +wuss +fs31 +sherwood +s95.as +cogito +rdgateway +fs30 +devel1 +fian +sec2 +gate01 +shed +webshield +fs28 +goodbuy +fs27 +ino +fs24 +fs18 +m.cafe +celebratelife +ttu265662 +m.gas +tour2 +sien +komatsu +workers1 +fs17 +popopo +chery +mail.nl +franquias +preisvergleich +phaeton +rainbird +bluray +slow +cycle +dtm +www.talkfusion +mnp +sooo +s188.as +www.maroc +that +www.macro +mdx +anesthesiology +s1.as +cyb3r +merengue +ztc +amaryllis +srv00 +overnight +ssra +duff +lacc +webdesigning +bil +reporte +elem +yogurt +www.sion +splendor +ucp +fmsadmin +nightclub +ville +wonderboy +diony +dictionar +unis +tsr +dipak +tobolsk +hahahaha +fashionhouse +box5 +contrast +www.test22 +www.pilot +s201.as +upup +box9 +wada +wang +variety +rm2 +diogo +s99.as +webp +weed +hypatia +box12 +srv50 +box25 +www.fund +vvip +www.elearn +yadi +cafedawha +s202.as +forester +radha +yawn +miu +www.bbb +www.amt +diezz +ives +www.big +grupa +diani +ceca +trabzon +heather +distant +spamd3 +worm +jewoo +www.sap +www.newforum +s203.as +www.craciun +origin-community.qa +origin-community.qat4 +origin-community.qat3 +origin-community.qat2 +s130.as +craciun +insem +s5.as +www.midnight +testvideo +origin-community.devstage7 +origin-community.devstage5 +origin-community.devstage4 +origin-community.devstage3 +origin-community.devstage2 +origin-community.psqa +sungyeon +yupi +www.cae +ricerca +kaktus +rpt +drak +lucho +egor +delux +bengali +www.gf +areon +imagesrv +x-nova +dasm +ext02 +doni +deka +www.autodiscover +mekuri +dody +hns1 +cocoro +cubo +dixi +demo03 +crop +crib +neuroshima +yocto +cam01 +deni +dene +mainframe +hinet +hns2 +irawan +www.kelly +buda +www.shadowcompany +nds1 +booster +www.kat +www.dgm +tuttifrutti +spencer +buza +xxxxxx +pld +jeffrey +non +damn +kuroneko +omniping +vz8 +collin +www.csc +seemann +www.edr +bestofthebest +byebye +elcamino +nscache2 +nscache1 +theleo +s6.tdzs +mediatech +www.presta +rafiki +damir +ctmail +mochi +hap +ayam +dalet +googleapps +shina +inbloom +www.almaty +abril +www.karen +www.newworld +nikon +kvik +www.gem +www.ghe +aaaaaaaaa +s29.as +www.gmi +unbreakable +esperanza +chorale +and1 +www.gsm +s208.as +thegirl +lydia +shibboleth2 +whitewolf +789 +aristo +www.ing +beautysalon +www.codex +tariq +growingup +relief +fdl +matsumoto +angry +bobs +cristiano +awan +mediaadmin +azizi +natale +hakata +moneta +jonny +shonan +avar +chema +thongke +bower +www.itm +hdtv +sfzx +futbolka +somewhere +barca +smtp-in2 +novamed +chatx +s150.as +coffeebreak +dok +blak +mirza +cyberman +vba +caps +geol +email01 +www.mer +alcantara +vist +fara +sity +webmail02 +xtremex +www.ba +kindergarten +asli +www.jerry +venue +wildlife +sazan +s40.as +zxcvbn +fukushi +magus +mrtg4 +s1.jzwc +pistache +na4 +lastresort +wanfang +jgm +web179 +na3 +herbalife1 +www.we +dimple +um1 +uploading +www.mvm +geranium +s002 +csstrike +netmeeting +apic +de9 +fineart +anuj +pftp +ansi +jp2 +mail.fr +softtech +csadm +isel +vinnitsa +give +shuzai.canoekayak +reason +commissions +redred +www.horse +procurement +spamtitan +infinit +partnership +boomerang +carik +anbu +wenxue +miriam +shenji +godaddy +guy +asmar +webkinz +bioinf +jwxt2 +www.psp +alii +alif +glenwood +hoffman +bbs8 +roel +holmes +kobato +ruda +test6398 +www.roy +nanako +webdirectory +srch +cameleon +www.rsm +bala +beter +steps +sams +hac +familypet +aira +sammisound +renuka +daeryuk +totaleclipse +new-world +baha +fakebook +myworks +extragames +ar1 +queenbee +hotdog +router11v04.zdv +gabriella +babu +sabur +malvern +recados +aidi +ashish123 +adri +mpg +www.gucci +aden +www.proba +www.grand +acen +orleans +strategic +smtpi +franquicias +pomme +fever +anisa +flower12 +alyssa +emailsecurity +megane +former +softpro +vmail1 +erotica +cassandre +myconnect +sagent +fss +grape +manutd +sairam +ananas +ptu +scan2 +cloudtest +nutella +www.hello +voyageur +salimi +serc +aerospace +arnaud +switzerland +preps +zo0om +ammar +www.testowa +girtab +www.wws +happyday +bellydance +sqlbackup +aliza +lululu +decision +waseda +ecolife +izar +londoneye +www.xc +vps10 +jibong +tc01 +sascha +kawanishi +recover +cezanne +sartaj +asaka +web-proxy +autodiscovery +electronic +dcvpn +xspace +tester2 +satish +clermont +ryugaku +croma +toutatis +www.dedicated +www.encuesta +arjun +apc6 +autoconfig.xml +autodiscover.xml +webdisk.xml +all4u +staging0 +asdas +mure +bardo +rewat +osvaldo +insect +espoir +savixx +www.conferences +badar +ahsan +mediastream +wwwh +funkymonkey +downloadzone +www.install +idesign +www.sanfrancisco +kum +teng +netsystem +rodman +sys2 +opale +comunicacao +cybele +mahmoud +vak +hogan +woodward +sabina +mercado +addme +kapo +abuja +scs2 +bemine +bestfriends +percy +acc1 +teahouse +blister +hopeless +test-site +www.homework +fullmovies +searches +www.proteccioncivil +addict +sendto +sepehr +sex169 +onlinebanking +whitenight +webmeet +artsandcrafts +atlantica +mx23 +peyote +monday +index1 +138 +nightwish +morgoth +romana +tribune +wvw +goodstyle +cardmaster +testabc +cfp +bab +ozgur +blackout +poochie +proteccioncivil +hkshop +apollo3 +blackhorse +blackwolf +francais +springtime +shark2 +catalogues +www.smiles +theunknown +kerman +hunt3r +web139 +epr +one12 +www.energia +sbc1 +roxana +airmax +chocho88 +service6 +moveon +excite +j4 +fanatic +lostmind +da5 +berich +webdisk.weather +cantabria +morrison +samsungindustry +www.surat +innocent +newsdev +viruz +icarus6 +encoder1 +hooka +amra +janko +blacksmith +darkarrow +ajmail +afrique +bandit +sidali +anythinggoes +theway +pacco +gideon +gamerevolution +www.websites +hedgehog +mrcool +tribunal +execute +horseman +ksiegowosc +r9 +mms1 +r6 +basecamp +r5 +mytool +vendetta +punchline +gameonline +rhn +shurik +mundotkm +alex99 +simmer +simon1 +simran +alexam +mechanic +alexan +servicio +infoservice +baxter +webdisk.cms +wales +roshan +addax +weiss +admin159 +baceco +fuyu +thebrain +spicy +aliman +webworks +mysample +fivestars +hardservice +onlygirl +sol1 +over40 +outlaws +hwachang +moodle-dev +jacket +mohajer +webdisk.dashboard +mohamad +seesaw +inet-gw +web149 +lazlo +billybob +wpp +architekten +ipcheck +lessing +bluegrass +oussama +informes +www.ecards +tede +phwt +alians +propaganda +madewithlove +webdisk.advertise +albireo +bourahla +dbs3 +dbs2 +web2012 +www.excel +ts05 +zav +raider +vbox2 +shadowland +ntk +bayan +alike +allin +msss +uap +hewitt +anthem +lyncpool +enrollment +moregames +www.helix +vconference +ansan +lsweb-ext +institute +exchange2010 +andrej +projectx +andris +jamaica +www.mtg +ftp.in +wlse +surplus +www.medical +asia2 +arche +howies +ashes +host002 +blanc +jhoncena +ohm +skating +appsrv +www.timeline +nikobellic +radiant +mcu2 +yusuke +atrix +benito +dominos +milkshake +healthylife +musicpro +www.uae +sonicboom +sumatra +bereal +webint +webmail7 +smartbox +magdalena +www-3 +technosoft +hyphen +chell +rookie +chime +chino +anurag +www.mastermind +ucm +hammadi +prueva +webseed +chong +personal2 +redskins +serv40 +mikimiki +backtrack +www.ipod +nicko +ender +vanguardia +dc02 +www.gamer +lmm +hessen +athlon +shared2 +sas1 +www.handmade +catalog2 +javelin +myhousing +wftest +tr1 +fang +letsplay +backfire +lestat +dalek +tala +www.webservices +roscoe +admin2012 +www.drink +bridal +www.drama +s1019 +bearmail +banquetes +honors +bblearn +matematik +www.poll +newyorkcity +starteam +soft32 +s1017 +s1016 +www.era +manhthang +officescan +tuma +grill +vdo +s1015 +sohail +s1014 +www.earth +outofcontrol +oviedo +s1010 +happyfriday +darklife +s1008 +notificaciones +filemon +badabing +perception +testnew +spacer +www.cross +exodo +sbp +host52 +host58 +bfc +persada +photograph +ezequiel +deepa +s187 +trf +pscn +s1009 +blogweb +stoplight +drupal6 +www.koala +chiro +netstar +at2 +santana +cools +www.cgc +howl +www.chr +www.craft +testlogin +www.coa +www.referat +citrix01 +pippin +serpent +teste123 +www.coral +tested +somali +filin +idm1 +meli +corea +agentur +mitch +steamgames +webanalytics +www.devil +konsole +azeroth +tomec +vietnamese +habanero +www.dig +fzghc +mail.voronezh +zaqwsx +vm52 +granja +pericles +outsider +nokian +cocopop +nch +gosolar +teplo +days +sorrel +morocco +www.color +oxo +www.soso +www.demo7 +redbike +s182 +biogas +minet +pri1 +ht001 +www.ym +livestock +www.ipo +dozer +www.dx +idkort +lyncrp +www.plotki +opc +www.dop +mwp +stargolf +maelstrom +arshad +organizer +st22 +shibu +www.kcc +st12 +www.infocenter +st28 +www.lb +st26 +jrsystem +init +st29 +st23 +st21 +st20 +thecodi +e-services +st15 +st14 +st11 +st03 +caution +econom +hotspring +serv10 +brano +iletisim +www.civil +dipesh +dvs +zup +blagoveshensk +www.chris +archive2 +dl45 +dl12 +nizhnekamsk +pietro +h216 +bambam +female +partnernet +woorifood +radmin +tomatoelec +authority +corps2 +phuc +jinjin +blackhawk +www.brett +morad +czone +www.bravo +www.iti +vanillasky +bigdeal +www.tala +www.zarzadzanie +bigsavings +dotop +kimberly +gudang +webber +pt2 +wth +siesta +jimmie +webct2 +monthly +referendum +doc4 +newuser +olea +girish +filez +cary +music4you +cave +weblogin +faint +www.t1 +highspeed +conestoga +yorkshire +www.issues +s179 +im5 +gamespace +charlie1 +ordinary +www.todofutbol +etforum +afghanistan +boni +natan +speakup +janedoe +stefan +mailserve +archery +d01 +pongo +base12 +s1121 +stigma +horseracing +authen +goodboy +suchet +itclub +pase +s174 +gns +cici +zerogravity +styleicon +www.tst +grover +h323 +benefit +ds11 +patrik +555 +loic +s172 +aea +giveme +ds8 +aqa +omt +tongji +geneva +vek +wab +baltazar +arabsex +malek +newcore +jjjjjj +facelook +clm +kannel +www.cargo +nazim +ips1 +cordoba +s164 +ptb +reputer +req +s162 +s161 +www.hardcore +mkmaster +www.melody +grafitti +bharat +sg123 +korrg +goodfood +landers +zloty +aceofspades +darkhunter +birdie +rid +www.mumbai +shockwave +gachi +belgrado +testbb +s189 +thelast +www.pune +fixer +hotmailmsn +webmailadmin +www.tad +www.thi +tabasco +thekings +marafon +viagra +gameronline +s1039 +www.step +esxi3 +s1038 +s1037 +mm2 +gaa +wolfram +bugz +ks1 +gabba +sing +s1023 +pbc +rihanna +eplant +jm2 +flexi +s1024 +s1022 +thi +igoogle +svt +thepit +zapisy +hangover +nst +advent +games2play +edan +eduline +msh +blob +banan +bors +office5 +bonzai +lovehate +garf +nead +viso +ggyy +t34 +gemma +wns +worldpaper +www99 +esample +construccion +satyr +sweb +ivanov +nine9 +walt +explorer1 +grb +nowayout +kendo +wvvw +www.images.a +www.outlook +estilo +zoli +cat4 +diversion +front6 +www.owner +iphonetest +sprint1 +beckham +imed +www.sif +alo +lantana +notenote +guzhou +www.testforum +music4u +spartans +kendra +epicure +voyager1 +pilates +haley +hammy +pt5 +iconnect +bebop +megapig +sabo +bernard +amod +www.kulinar +uppic +fudge +universitario +lguplus +lovingyou +themaine +happyhouse +sovet +onemoon +e-office +crazynet +ajay +sicherheit +mathe +sancasia +glock +grabber +digimarket +hannes +concerto +kashmir +hom +webclub +vitalis +gamerz +serkan +blog9 +slavik +www.poems +geocode +for-you +vincestatic +alexandrestatic +homesweethome +lorangerstatic +acropolis +loranger +webst +cyrillestatic +reply +playhouse +shopdemo +devtok +folkmusic +hanaliving +itce +andong +ouvidoria +4u +explo +htl +sugarray +chaotix +cdt +caiman +rzd +taster +kish +i2i +marca +aes +flyff +www.apex +116 +gorky +jiajia +wow-europe +lococo +lab3 +fv +xchat +enertec +sorteo +amulet +dut +liviu +mysql50 +classmates +fifi +with +360grad +bpd +brahim +gtl +supergames +uri +weboa +www.asdf +www.axis +administracion +yahoo-mail +attila +chirag +loveme +deejay +sxx +wiki-test +test011 +ocb +przemysl +andyb +whitefox +route +x-ray +ruth +kara +nellie +dorothy +www.personal +www.dash +lucille +ecop +chl +sienna +www.pila +murasaki +www.express +elemental +spw +www.gniezno +hrss +devis +s1002 +nissan1 +s1001 +iena +russ +melba +mcsupport +yak +bri +gniezno +przeworsk +customercare +www.vita +forgetmenot +www.przemysl +www.nisko +ninjaworld +24h +xiaoshuo +s2008 +gsoft +siscom +alcoholism +eternita +greentools +www.cook +elder +photoblog +capi +animatrix +elblag +www.diet +linux10 +www.dino +www.grid +www.elec +si1 +gospel +www.img3 +www.brodnica +haxx0r +rozan +xtime +webcare +n3 +minigolf +toxic +boinc +amma +departure +herbarium +whirlwind +www.mikolajki +badboys +daylight +discordia +gomi +katherine +howdy +orly +chai +eggplant +sanandreas +www.blacksun +www.przeworsk +addon +aba +www.krakow +dix +cnb +ns-master +wowwow +cosway +cpr +nisko +confucius +haze +abn +starcraft +szukaj +enable +adn +hana5 +zizhu +lists.mail +skysky +thesky +brodnica +gander +dr2 +mpf +gomail +www.kmm +wer +hpt +ecrm +director1 +tiburon +s152 +s151 +elites +imail2 +s147 +s146 +www.planeta +s145 +planetlove +xvideo +orione +ivo +dbu +dvds +acs2 +hata +www.images1 +qrcode +magazyn +www.led +anp +kluge +omnibus +duarte +yq +teamcenter +retiree +opti +server05 +avemaria +tectec +newhorizon +clab +anyang +dyndns +www.mitsubishi +comingsoon +agate +tcserver +betamail +vot +jihad +irony +kalam +microtech +coucou +hrportal +web11290 +web00 +websvr +softphone +testbbs +lowry +outdoors +johannes +timeserver +topweb +wormhole +chucho +silviu +webb +www.ebay +torres +letterbox +hendro +webdoc +xiaojie +web193 +fed1 +accenture +castrol +bluecard +shortcut +homeshop +cyberia +broadband +ardi +peterson +fantomas +nikola +www.geek +mimizu +polygon +ads3 +lao +jukebox +ave +father +blo +bluedragon +www.sugarcrm +www.axiom +pxetest +anatoli +formulaire +netadmin +p0 +carrefour +ccnet +mchs +teardrop +www.greece +bne +vacancies +www.full +aua +res12 +res11 +bmx +jobshop +dftp +cer +bigdaddy +web194 +jones +www.jake +nolan +grayhat +brunei +icts +vmm +web195 +gryps +mail.mailer +wapsite +rashed +autoconfig.sitebuilder +tweeter +arindam +subaru +autodiscover.sitebuilder +www.brown +www.hvac +cig2 +gonzaga +habbocredits +cng +wl2 +telefony +danish +atif +www.wii +carr +vanle +cata +legato +funfun +rakuten +www.http +cachorro +smd +e-pay +bazinga +mhamad +www.kolo +tvi +www.papa +alzahra +autodiscover.noticias +voldemort +cmis +access3 +gmaill +cst +www.v4 +cvresearch +www.vds +haddock +ctech +keene +www.calendario +jiro +freevideo +spiders +cvt +v19 +spidey +karbala +amxbans +cwp +gmaile +www.brotherhood +dpp +mothersday +ocean1 +autoconfig.noticias +wwwj +kimura +www.toy +ml2 +roxio +www.loto +orpheus +koe +www.luis +adad +powernet +e250 +gfc +agamemnon +erotika +winters +yoshikawa +cletus +www.snd +integracao +pre.www +arges +downtime +gatti +freefall +hrishikesh +jackie +mail.hk +fy +lovetale +emy +krish +ns132 +q17 +www.neon +recep1 +q16 +q15 +epn +angelwing +worship +mk4 +www.sas +www.pack +yahoomail +ns138 +pnt +ns143 +tp4 +ns144 +photo6 +etl +watchfreetv +partage +q100 +aditya +autoconfig.vip +www.aspect +killua +ns147 +spalla +mikids +mascara +www.pepo +juann +nxy +allkind +ns153 +ns156 +truong +web1021 +ns158 +shabab +daekyung +hashem +meduse +web1111 +web1116 +basem +web1127 +web1131 +www.bannersbroker +house2 +www.earnonline +www.msg +web2006 +web2007 +computertalk +s-107 +minhngoc +weiwei +www.opel +almuslim +rene +www.zh +gfp +www.pooh +sellit +gfw +anduin +divino +pc14 +maxgame +pc15 +www.als +www.arg +icg +click2call +www.mes +htm +www.ffl +kamensk-uralskiy +mahir +www.gib +www.htd +tadmin +supernet +pc6 +pc7 +www.sws +tejas +www.upd +cooltimes +mhl +cucm +www.ppp +ferien +upfile +marocsat +kdb +mgw1 +ipass +yado +98 +furuhon +abm +stirling +jom +pwk +ceshi +francoise +www.lak +ichiro +mdr +skp +yszx +www.domination +cronaldo +sm13 +ahbab +clickbank +vinci +asdasd1 +broom +sm9 +ehx +deviant +jeroen +elamal +partages +cppro +uksas +rstools +thenews +as7ap +sons +peewee +fxy +mzmz +ftp.cloud +decode +worldofgame +ns159 +technicalsupport +pao +jjs +minhquan +pfm +tmf +jayne +jaws +wireframe +xplode +hebergement +cogs +nye +www.parkour +webdisk.account +jszx +abdelrahman +www.designs +startimes55 +ntg +fsed +shadowcompany +direccion +eurotour +fp2 +contraloria +autosurf +vpns +thumbs2 +knoz +www.anis +www.asma +www.jokes +friki +tpx +www.general +www.cjc +e3lanat +forums1 +tc1 +sv19 +www.hq +www.ats +ftp.www2 +ng2 +ftp.cs +tareas +sv17 +sv20 +ymir +tmx +myonline +swat +riddler +habbox +www.professional +guitarman +ttd +kfz +hivemind +sql2008 +sart +chiltern +sofi +isas +sumo +smg1 +www.iraq +www.anc +www.tempest +dwh +alma3rifa +deluxe +www.nail +maor +uva +www.delphi +www.raid +vs5 +pna +jubilee +www.romantic +clickonce +bannersbroker +web190 +wwwwww +decima +web191 +web192 +voiptest +so3 +so2 +ultras +sampark +services.irc +ago +haryana +www.paranormal +starcity +heroo +www.studyabroad +gamersparadise +uweb +wwwprod +whitney +vacations +zon +hmsat +corus +marocstar +topshop +kaba +cogent +babylon5 +barclays +rozrywka +makarenko +www.ksiazki +soni +sjs +electronik +www.rozrywka +adminpanel +starlink +acte +montcalm +smit +ukdev +eefi +mke +test46 +bhushan +www.experience +rassegna +www.wis +erm +autoconfig.track +autodiscover.track +kiturami +autoconfig.c +autodiscover.c +upper +webdisk.c +shsh +ishikawa +www.anders +fouad +aomori +cervantes +pdr +amerika +toyama +www.sr +exchange2003 +web197 +habbouniverse +lookbook +nucleo +ragtime +kanagawa +ecologia +youngl +pic6 +atilla +catv +gw-vpn +ctf +loven +archaeology +s58b +s58a +s53b +s53a +s51a +j5 +xs1 +masashi +polarbear +vagabond +ftp.live +www.miracle +www.mybaby +medall +tour1 +testing3 +www.administrator +www.tour1 +istudy +im8 +troubleshoot +mail.live +blognew +mx-3 +itlife +backup55 +www.vivaldi +mssql2008 +www.webinars +ganpati +udon +actie +standrews +s-34 +wso +backup65 +oklahomacity +landingpage +asteroid +www.archiwum +backup70 +www.nowy +hofmann +oraculo +www.arquivos +yumi2 +boeing +ktp +otomotif +fhs +xh +modernstyle +s-95 +poss +karir +swd +paidtoclick +www.encuestas +k8 +k7 +problems +b2b-test +k6 +beta.www +istar +treasurehunt +kvm8 +webquest +esoteric +canli +ids05 +www.gestion +chus +www.touch +daemyong +highperformance +vweb +embroidery +gefest +freshwater +lib-db +commencement +study2 +grifone +zproxy +csportal +namdo +nintendowii +hamleys +seahorse +residence +toollove +sms3 +portal-test +ksa +chivas +aegir +lovemusica +chats +accreditation +momentum +agd +utv +supporters +www.admanager +ladylove +favicon +animalrights +cariart +fiera +iix +taki +artpop +decoline +corefit +cars2 +b2c +lvs01 +saku +omedia +220 +www.schools +trains +homerecording +momshug +xwing +green2 +gepir +bluebrain +landrover +reptiles +mai +bentley +camps +superforum +qos +freetimes +thinkup +emdev +mail-dr +safi +daycare +southbend +quotations +oldwebsite +webdisk.photo +grinder +concur +shtech +pekanbaru +giftsadmin +sogokju +tiendaonline +sumai +prueba2 +ver3 +pk1 +ns06 +pdamail +dg01 +clouddevapps +wlc02 +funds +licensing1 +licensing2 +tvco +hitchcock +hbtest +websurvey +mi2 +cybergames +aetos +www.mississippi +cloudapps +nnf +xenapp02 +xenapp01 +homevideo +astro2 +sl4 +hp4200 +shuzai.soapoperadigest +suen +ssmi +ibg +chinadev +guest1 +itg +tft +forumz +heartman +fda +sua +pixies +chrysalis +globedesign +ccn +kasanokarbu +bkd +architektur +ttserver +slgp +pbl +oyabin +centrix +groucho +notitia +seojapan +evoque +finaid2 +au10152771 +rongtail +alecto +rkis +webridge01 +webdisk.properties +www.tuning +gamewiki +serv9 +gulliver +colle +vamos +file6 +cgi2 +apple2 +bbgolf +vag +ecl +chattest +ocsrp +md2 +autoconfig.domains +jinsun +webdisk.domains +siphon +cio +laue +webdisk.affiliates +amer +infobell +autodiscover.domains +terminals +noema +kangoshi +wlc01 +harpo +constantin +weare +bura +wns4 +wns3 +base01 +base02 +attitude +base03 +remotehelp +ozon +mobilesentry +fandango +jls +gslb2 +dda +gslb1 +mail-a +rw5 +dde +eport +sto +musicmaster +www.curtis +impress +cerise +uol +iris12 +marinm +bk06 +partner.dev +overseer +greg2 +www.poligon +cnt +follow +pipo +scissors +dgm +hsms +laxmi +autoconfig.newsite +autodiscover.newsite +calltracking +kids1 +magicbox +dib +editest +locations +cw2 +wholesaler +engdev +ebaystore +s88 +s89 +jabapos +s90 +speed1 +kubrick +ntp02 +ucs2 +guerrilla +alquran +hepa +ww11 +webcast02 +webcast01 +sanjuan +bugati +dra +gp1maindns2 +dzb +vmt +rockfish +jalisco +dev97 +dev77 +noni +dev29 +dev23 +teambox +web1070 +web1080 +ut3 +tufi +web144 +asa3 +web1109 +jungang +web184 +web185 +emt +web187 +web188 +et6000 +web218 +webdisk.lms +autoconfig.lms +autodiscover.lms +bwing +172 +cnm +srf +web408 +buzz2 +www.graduate +ecare +www.epaper +irsa +www.networking +iptel +novidades +cosmoinc +ghe +vivalavida +takara +vanille +prost +freestyler +r-timc +mayflower +ginkgo +saphir +mixi +dcustom +edu5 +dobby +ulm +prunus +virtualoffice +chemnitz +aachen +laurier +www.oferta +pattaya +kama +majuro +hpm +dbmart +prg +www.amar +vmhost2 +ds09 +ipcop +nile +websupport +web4test +backma +beta-admin +gokmul +ssl14 +lumen +www.karma +ssl10 +www.re +www.np +ipm +newdomain +ssl9 +masrawy +www.fo +www.kw +hscl +ssl5 +www.ielts +moriyama +www.einstein +isac +programacion +katayama +tanimoto +aset +jos +fujigaoka +wcf +starwar +www.instant +mcch +albania +ishida +veyron +onodera +kelso +kawakami +tanpopo +ney +miwa +mizuho +kodiak +talento +chapters +cremona +www.moe +koh +copenhagen +lia +rostov-na-donu +ambiente +ogr +apuntes +chemwatch +slovakia +www.cheboksary +www.bryansk +circulo +publicidade +www.sankt-peterburg +iptv2 +gerencia +danju +rweb +bubbel +nhacchuong +lws +planeacion +farmers +media10 +media5 +beta.m +api.beta +winit +download.im +zc2 +www.transfer +nik +vps006 +vps010 +vps012 +vps013 +mysite.sharepoint +bfn2 +bfn1 +isidro +vps016 +vps017 +vps019 +vps021 +globalnet +vps023 +vps026 +vps027 +vps028 +wwwtest2 +vps030 +lists2 +whitetail +vps031 +vps032 +myo +vps033 +mpk +mail.chem +manet +redalert +spear-login.rcc +spear-login.hpc +ymfood +mge +vps124 +autodiscover.www +nwd +autoconfig.www +xterm +vps126 +vps129 +vps009 +report2 +adia +revenge +www.fms +monsters +lamasbella +miks +wade +viscon +decobox +bobmarley +prosoft +vps125 +wwwl +meer +geo1 +www.desarrollo +www.lookatme +damusi +napoli +lomis +lugansk +arkhangelsk +petersburg +cherepovets +cenha +latvia +www.rivne +bmsys +eng1 +randomhouse +thrawn +www.moldova +dvclub +amk +ecatalogue +lidl +fun4kids +www.czech +smena +www.xat +www.sibir +vid2 +www.belarus +tvadmin +rostovnadonu +carros +hgc +odesa +new-york +mshop +www.kuban +www.sts +la1 +uliss +derbent +dnepropetrovsk +michele +kipper +tallis +serv17 +neworld +wefactory +host16 +anm +knightmare +sqs +atoum +vds14 +addr +cms02 +sth +strelka +eseries +tech4 +ssrpm +voa +oxi +www.tmm +compta +ambre +tks +vcc +a25 +bsec +ip7 +nocps +intersport +cns2 +cns1 +casimir +jabri +themaster +mailarchive +ap02 +canter +clicks2 +bowmore +camilo +mailbck +fbe +eec +ura +www.typo3 +emoticons +bulut +lido +tyt +pmwiki +mpp +aljoker +www.mambo +www.mcc +newland +neopolis +dva +vpo +category +propiedades +keygen +www.digimon +theforum +www.firefly +mariner +lum +kili +acmilan +webdisk.tracking +digipath +seattle4 +autoconfig.tracking +autodiscover.tracking +webdisk.job +bib2 +pre-www +cpns +fac +x31 +x19 +estyle +region2 +dos2 +host33 +deathproof +isit +admin-test +webdisk.oldsite +host38 +npd +anjing +sabe +sdcserver +m07 +jyw +ebys +dominican +curs +ahlamontada +www.hit +jame +loginlivecom +autodiscover.katalog +autoconfig.katalog +posttest +temara +whereareyou +ipadadmin +antivirus1 +www.thehub +www.royalty +statm +www.yourgames +steampowerd +www.devblog +tol +mysterious +junction +power7 +www.hooligans +kiku +fileup +market2 +kaede +vmb +centerpoint +www.stories +www.ww5 +core-rn +www.besiktas +greenbeans +sphynx +asteria +coffeetalk +testingtesting +back01 +gerrit +webdisk.mag +elja +kyocera +test.admin +parenting +www.stamps +www.nintendowii +www-all +manohar +funforum +karan +endo +prewww +manjula +rhc +www.agency +bahrami +bcn +labnet +webdisk.host2 +fdo +shahrukh +dide +a123 +d217 +dias +diar +valiasr +www.risk +diba +webdisk.ssl +www.thailand +ect +www.regional +desenv +hsr +ele +f123 +link2 +vai +pecs +acis +jhony +instalator +ns.in +smalltits +crossdressers +cfnm +pregnant +gyno +nudesport +acne +pov +groupsex +menstruation +kic +swtest +web-ns +budi +nonnude +bisexual +ctu +shitting +bote +livesex +baum +dit +ye +strapon +folio +goly +nightshift +prpr +manya +push3 +natter +pangea +rde +sdl +nubiles +wsz +bilbao +ws82 +eagent +1221 +www.delivery.swid +atop +devwiki +pfizer +kak +drafts +ferhat +meng +zine +fanli +www.web-hosting +f0 +webmail03 +amed +eventhorizon +powerlink +masuda +link1 +graystone +iuno +www.apply +cheshirecat +bday +amsa +f5-2 +arge +carz +hoax +voyages +ader +habibi +motorcycle +f5-1 +aben +estrategia +syracuse +games3 +mx30 +rhiannon +www.teen +255 +dacs +elinux +ako +nema +cms6 +supertramp +dasa +vltava +ficus +www.dl2 +byblos +claw +clik +dion +host28 +dish +qas +luthien +weihnachten +ecko +spellcheck +xcart +dony +symposium +www.testblog +worklife +acces +beluga +1001 +solicitud +web-test +host42 +host43 +host44 +subject +admision +autoupdate +brahma +emilie +ns1.l +autoconfig.cl +central2 +ns2.l +host39 +webdisk.cl +masterhack +jocker +vs9 +softwares +wando +daisy1 +autodiscover.co +webdisk.art +autodiscover.cl +repl +www.publicidad +trustee +cthulhu +polaris32 +0001 +slots +bloomingcard +drum +www.nec +www.steve +pingdom +hanmaum +cortes +arquitectura +cadence +protest +bs01 +soz123 +vmbackup +asl1 +ip196 +crawl3 +brotherhood +detali +hgw +autoconfig.cpanel +autodiscover.cpanel +tmr +pastime +webdisk.cpanel +producer +webdisk.journal +iroiro +fed2 +antispam1 +chris123 +myportfolio +foreign +zcs +chroma +daesungco +batam +maxworld +videotutorial +smtp25 +favour +aniac +crm4 +pinguin +crawl2 +ip193 +www.primus +mfo +shadi +abdallah +goldencity +studyroom +iasi +websrv01 +umma +gtc +burmese +ip253 +icelandic +infrared +newscenter +sitenovo +devesh +ip245 +autoconfig.partners +salesdemo1 +barone +salesdemo2 +autodiscover.partners +bastman +webdisk.phpmyadmin +mobilux +democms +angelsofdeath +pandu +cs11 +diets +rgu +ip217 +factory4 +roe +cs12 +ugo +ip210 +pension1 +ip205 +ip204 +ip203 +ip188 +ip164 +ip162 +acris +ip147 +no1cafe +ip119 +datafeed2 +angelus +ip106 +authtest +youhei +from +d1-4 +nikesb +dns40 +keele +rcworld +fpo +sendblaster +yekwangco +choice1 +ssada +korack +subsidy +r1back +ingolf +tikal +bigmusic +uc2 +essex +sonax +server-0090 +orangemusic +psworld +ticket1 +www.old2 +songjin +beaunix +timecoach +fannan +emprendimiento +www.dic +kolang +iklangratis +testversie +web717 +1004 +cofe +pls +1012 +hwajin +reise +b133 +b124 +mansoor +alico +theoden +sinix +heliopolis +regret +taean +www.elsalvador +vpnserver +4free +kame +spare-240 +spare-248 +www.quimica +rodan +vm110 +moist +okdspack +linux02 +server-0087 +aurora2 +www.port +fon +parsley +nettest1 +nettest2 +ret +abs2 +nettest3 +corridor +nettest4 +waitingroom +insidepro +preview.cmf.staging +dte +fotoklub +www.twilight +joka +tns3 +tns2 +tumble +peyman +hanics +nightwatch +myapi +rei +tweets +elizabet +www201 +novosib +www.pasca +especiales +heffalump +ctk +sutech +jwdesign +feeds2 +kobalt +soulteam +sargon +megazone +eprint +topsoft +player7 +mext +explorers +bigsave +niels +flowersky +leipzig +selli +www.opensource +bosphorus +littlethings +samwon +modi +d142 +d141 +t7 +younghwa +d140 +d138 +d137 +d135 +d134 +d133 +d132 +d131 +d129 +t6 +d128 +d127 +d126 +d125 +d124 +d123 +d122 +d121 +d119 +d118 +d117 +d116 +d115 +d114 +d113 +sungju +d111 +sungil +sungho +faq2 +nghenhac +www.rover +salonb +www.franchise +edomain +win2008 +www.eko +pasiphae +securetransfer +mosk +goedel +sycompany +suzukishop +picdev +d139 +amalthea +image99 +d130 +jensen +d120 +boanerges +d110 +d108 +d106 +d105 +d103 +norn +www.countdown +test-vip +caen +save-big +bestone +host06 +host07 +tokai +gingerbread +adminweb +oliveland +switchvox +moonstone +cheops +ironbox +babypark +gasgiveaway +d109 +autoconfig.prueba +cmf.staging +d107 +windows3 +d104 +webdisk.prueba +autodiscover.prueba +foreclosure +abbot +opposite +avtech +sql2005 +www.oh +siberian +vargas +meru +v001 +preview.cmf +cacti2 +www.bookstore +blue-sky +www.cristian +qadb1 +esxi1 +stat5 +topup +invaders +pita +www45 +www46 +osos +www.404 +net7 +dc1002 +xray2 +dgw +tenshoku +sysadm +mywebpage +180 +pers +plexus +160 +153 +www.mailboxes +libreria +syscon +spare-44 +spare-96 +casi +mobileshop +worldpc +spascal +linksys +orangeave +geomusic +pilote +dongin +motorhead +rocinante +supporto +cvsweb +frame1 +schumann +timestore +ssv +bain +soho1004 +img32 +dev.support +consultants +ganz +signals +e001 +saib +besthouse +photoss +onclick +midiland +edubot +maleki +myra +mechanics +polomix +digiweb +unicoh +ifree +serveri +slam +luckymart +cornea +www.8 +pdns3 +www.42 +www.37 +www.36 +www.33 +tnns +vica +www.30 +www.27 +www.26 +dpec +manatee +nanotech +mjstyle +trax +shkorea +projecta +whw +artshop +admin9 +omerta +sunline +balkan +www.ns3 +laforge +nayely +wwwneu +backend1 +tintagel +mg1 +vegeta +vworld +racing +teak +wooster +n4 +thecube +netdisco +cosmas +ling +pectus +file3 +dimitri +animale +projekte +devdocs +dory +unlock +lago +eeyore +overlord +caesium +wechat +cs6 +jiang +gaspode +nawras +www.romance +preview-www +happyfamily +compra +brava +devportal +udb +pasta +lexicon +rzeszow +gao +www.dolls +windowslive +san1 +pns2 +viceroy +www.mylife +tuts +weight-loss +cys +mapserver +stary +sporting +mta7 +mobilewap +ischool +blackblood +labyrinth +mi6 +fws +pims +victorhugo +rax +zeus.cc +clp +stagingcms +mdf +ns1.cs +net5 +ruralvia +pwtest +vmhost3 +thefamily +astute +vikram +traveltips +db04 +automate +w15 +autokb +ive +lettuce +bennett +www.invaders +admin123 +cabbage +aluminum +cullen +nkh +healthyhabits +pier999 +cp09 +creativa +timm +buffett +cp22 +wangyi +steampowered +3arab +vcops +mapping +abtech +wta +saltlake +www.valhalla +smtphk +waters +pbx1 +fileserver2 +free-sms +goldeneye +maarouf +hosting5 +hayate +dbprosearch01perf +presd07 +dbapp01-6120 +web11690 +dbsearch01dbnet +ts16b +web11689 +web11009 +u1204s +web12789 +dbhps01dbnet +web10242 +web10239 +dbbuild01dev-6120 +web11679 +web10809 +web11678 +dbapp01qa-6120 +web10235 +web11677 +web10808 +web10229 +web11672 +web10228 +web12199 +web11669 +ws292 +ws291 +web11668 +cmdev +ws282 +ws281 +dbadmin02 +dbadmin01 +ws272 +ws271 +web10929 +ws262 +ws261 +ts05b +ws252 +ws251 +ws242 +ws241 +web12349 +ws232 +ws231 +web10918 +ws222 +ws221 +web10247 +rtpmaster03ete +routernet30subnet2oemail +web11982 +web11665 +web10248 +trade9950-test +web10249 +web12889 +loggingky +web13096 +web10222 +web10765 +recimmaster00 +recimmaster01 +recimmaster02 +db03perfext +recimmaster03 +web11696 +web12211 +web10219 +web10796 +web13124 +web13121 +web13120 +web13118 +web13117 +web13116 +web13114 +web13113 +web13112 +web13111 +web13110 +web10996 +dbsearch01collectorky +web13104 +web13103 +web13101 +web13100 +web13088 +web13087 +web13086 +web13083 +web13082 +fpftp01qa +web13080 +web13077 +web13076 +web13075 +web13074 +web13073 +web11659 +web13070 +web13068 +web13067 +web13066 +web13064 +web13060 +web13058 +web13057 +web13056 +web13055 +web13054 +web13053 +web13051 +web13050 +web13048 +web13047 +web13046 +web13044 +web13043 +web13042 +web13041 +web13036 +web13035 +web13034 +web13033 +win2ktestpc +web13030 +web13028 +web13027 +web13026 +web13024 +web13023 +web13022 +web13021 +web13020 +web13017 +web13016 +web13015 +web12305 +web13013 +web13011 +web12910 +web12908 +web12907 +web12904 +web12903 +web12902 +web12901 +web12900 +web12887 +web12886 +web12884 +web12883 +web12881 +web12880 +web12878 +web12877 +web12876 +web12874 +web12873 +web12872 +web12871 +web12870 +web12867 +web12866 +web12865 +web10795 +web12861 +web12860 +web12858 +web12857 +web12856 +web12854 +web12853 +web12851 +web12847 +web12845 +web12844 +web12843 +web12841 +web12840 +web12838 +web12837 +web12836 +web12834 +web12833 +dbbiddata01-6120 +web12830 +web12827 +web12826 +web11339 +web12824 +web12823 +web12821 +web12818 +web12817 +web12816 +web12813 +web12812 +web12811 +web12810 +web12806 +web12805 +web12804 +web12803 +web12801 +web12800 +web12788 +web12786 +web12784 +web12783 +web12782 +web12781 +web12780 +web12776 +web12775 +web12774 +web12773 +web12771 +web12770 +web12768 +web12767 +web12766 +web12764 +web12763 +web12762 +web12761 +web12760 +web12757 +web12756 +web12755 +web12754 +web12753 +web12750 +web12748 +web12747 +web12746 +web12744 +web12743 +web12742 +web12741 +web12737 +web12736 +web12735 +net27sub04 +web12731 +net27sub01 +web12728 +web12727 +web12726 +web12723 +web10209 +web12721 +web12720 +web11652 +web12716 +web12715 +web12714 +web12713 +web12711 +cp07dev +web11709 +cw09 +cw07 +cw06 +cw05 +web10208 +cw03 +cw01 +cw00 +web12346 +web12650 +web12647 +web12646 +web10207 +web11650 +web12640 +web12638 +web12637 +web12636 +web12634 +web12633 +web12631 +web12630 +web12627 +web12626 +web12625 +web12624 +web12623 +web12620 +web12618 +web12617 +web12616 +web12614 +web12613 +web12612 +web10196 +web12607 +web12606 +web12605 +web12604 +web12603 +web12601 +web12587 +web12586 +web12584 +web12583 +web12582 +web12581 +web12580 +web12577 +web12576 +web12575 +web12574 +web12573 +iftp03 +web12570 +web12568 +web10195 +web12566 +web12564 +web12563 +web12562 +web12561 +web12560 +web12557 +web12556 +web12553 +web11700 +web12551 +web12550 +web12548 +web12547 +web12546 +web12544 +web12543 +web12542 +web12541 +web12540 +web12537 +web12536 +web12535 +web12531 +web12530 +web12528 +web12527 +web12526 +web12524 +web12521 +web12520 +web12516 +web12515 +web12514 +web12513 +web12511 +cp05dev +web12508 +web12507 +web12506 +web12504 +web12503 +web12502 +web10803 +web12500 +web12487 +web11645 +web12485 +thirdwriteback01ete +web12483 +web12481 +web12480 +dbhps01qa-6120 +web12473 +web12472 +web12471 +web12470 +web12467 +web12466 +web12465 +web12464 +web12463 +web12461 +web12460 +cmqa +web10192 +web12454 +web12452 +web12451 +web12450 +collector2 +web12446 +web12444 +web12443 +web12441 +web12440 +web12438 +web12437 +web12436 +web12433 +web12432 +web12431 +web12430 +web12427 +web12426 +web10191 +web12423 +web12421 +net30sub01uploads +web12417 +web12416 +web12414 +web12413 +web12411 +web12198 +web10190 +web11933 +webmaildev +web12350 +web12348 +web12347 +dcoh +web12344 +web11641 +web12342 +web12341 +web12340 +web12337 +web12336 +web12334 +web12331 +web12328 +web12327 +web12326 +web12324 +web12323 +web12322 +web12321 +web12320 +web12317 +web12316 +web12315 +web10187 +web12313 +web12311 +web11640 +web12308 +web12307 +web12303 +web12302 +web12301 +web12287 +web12286 +web12285 +web12283 +web12281 +web12280 +cs04 +web12274 +web12273 +web12272 +web10186 +web12270 +web12267 +web11638 +web12265 +web12264 +web12263 +web12261 +web12260 +web12258 +web12257 +web12253 +web12252 +web12251 +web12250 +web12245 +web12244 +web12243 +web12241 +web12238 +web10185 +web12236 +web12234 +web11637 +web12232 +web12231 +web12227 +web12226 +web12224 +web12223 +web12221 +web12220 +web12218 +web12217 +web12216 +web12213 +web12212 +web12210 +web12207 +web12206 +web12205 +dbsearch01dev-6120b +web12203 +web12201 +web12200 +web12188 +web12187 +web12186 +web12184 +web12183 +web12181 +web12180 +web12177 +web12176 +web12175 +web12174 +web12173 +web12171 +web12170 +web12168 +web12167 +web12166 +web12164 +web12163 +web12162 +web12161 +web12160 +web12157 +web12156 +web12155 +web12154 +web12153 +web12151 +web12150 +web12147 +web12146 +web12142 +web12141 +web12140 +web12137 +web12136 +web12135 +web12134 +web12133 +web12131 +web12130 +web12128 +web12126 +web12124 +web11634 +web12122 +web12121 +web12120 +web12117 +web12116 +web12115 +web12114 +web12111 +web11998 +web11997 +web11996 +web11994 +web11993 +web11992 +web11991 +web11990 +web11987 +web11986 +web11985 +web11983 +web11981 +web11980 +web11978 +web11977 +web11976 +web11974 +web11973 +web11972 +web11971 +web11966 +web11965 +web11964 +web11963 +web11960 +web11958 +web11957 +web11956 +web11952 +web12050 +web12047 +web11632 +web12045 +web12044 +web12043 +web12041 +web12040 +web12037 +web12036 +web12033 +web12032 +web12031 +web12030 +web12027 +web12026 +web12025 +web12024 +web12023 +web12021 +web12020 +web12018 +orionb +web12016 +web12014 +web12012 +web12011 +web12010 +cp24 +web12007 +web10493 +web12005 +cp20 +cp18 +collector1b +web12000 +db01perfext +web11886 +dbadmin01-6120 +trade9955-test +web11883 +web11714 +web11881 +web11838 +web10979 +web11876 +web11873 +web11871 +web11629 +web11868 +web11867 +web11866 +web11863 +web11861 +web11860 +web11857 +web11856 +web11855 +web11854 +web11853 +web11850 +web11848 +web11847 +web11846 +web11844 +web11842 +dbhps01dbtmp +web11840 +web11837 +web11836 +web10789 +web11834 +web11833 +web11831 +web11830 +web11827 +web11826 +web11824 +web11823 +web11822 +web11821 +web11820 +web11817 +web11816 +web11815 +web11814 +cp08dev20 +cp08dev17 +cp08dev16 +cp08dev15 +cp08dev14 +cp08dev13 +cp08dev11 +cp08dev10 +web11626 +web11750 +web11747 +web11746 +web11745 +web11744 +web11743 +web11741 +web11740 +web11738 +web11736 +web11734 +web11733 +web11732 +web11731 +web11730 +web11727 +web11625 +web11725 +web11724 +web11723 +web11721 +web11720 +web11718 +web11717 +web11716 +web11712 +web11711 +web11710 +web11707 +web11706 +web11705 +web11704 +web11703 +web11688 +web10172 +web11686 +web11684 +web11683 +web11682 +web11681 +web11680 +web11676 +web11675 +web11674 +web11673 +web11671 +web11670 +web11667 +web11666 +web11664 +web11663 +web11662 +web11661 +web11660 +web11657 +web11656 +web11655 +web11654 +web11653 +web11651 +web11623 +web11648 +web11647 +web11646 +web11644 +web11643 +web11642 +web11636 +web11635 +web11633 +web11631 +web11630 +web11628 +web11627 +web11392 +web11624 +web11622 +web11621 +web11620 +web11617 +web10976 +web11614 +web11613 +web11611 +web11610 +web11608 +web11607 +web11606 +web11604 +web11603 +web11602 +web11601 +web11600 +web11588 +web11586 +web11585 +web11584 +web11583 +web11581 +web11580 +web11578 +web11577 +web11576 +web11574 +web11573 +web11572 +web11571 +web11570 +web11567 +web11566 +web11565 +web11563 +web11562 +web11560 +web11558 +web11557 +web11556 +web11553 +web11552 +web11551 +web11550 +web11548 +web11547 +web11546 +web11545 +web11544 +web11543 +web11542 +web11541 +web11540 +web11538 +web11537 +web11536 +web11535 +web11534 +web11533 +web11532 +web11531 +web11530 +web11527 +web11526 +web11525 +web11524 +web11523 +web11522 +web11521 +web11518 +web11517 +web11516 +web11514 +web11513 +web11512 +web11511 +cp04dev +bobj +web11618 +bobd +webadmin03qa +ts06 +web10787 +web11450 +web11448 +web11447 +web11446 +web11445 +web11444 +web11443 +web11442 +web11441 +web11437 +web11436 +web11435 +web11434 +web11433 +web11432 +web11431 +web11427 +web11426 +web11425 +web11424 +ss01qa +web11422 +web11421 +web11420 +web11418 +web11417 +web11416 +web11415 +web11414 +web11413 +web11412 +web11411 +web11410 +web11408 +web11407 +web11406 +web11405 +web11404 +win95testpc +web11402 +web11401 +web11400 +web11388 +web11387 +web11386 +web11385 +web11384 +web11383 +web11382 +web11381 +web11380 +web11378 +web11377 +web11376 +web11375 +web11374 +web11372 +web11371 +web11370 +web11367 +web11366 +web11365 +web11363 +web11361 +web11360 +web11358 +web11357 +web11356 +web11355 +web11354 +web11353 +web11352 +web11351 +web11350 +web11348 +web11347 +web11346 +web11345 +cp03dev-1 +web11343 +web11342 +web11341 +web11338 +web11337 +web11336 +web11335 +web11333 +web11332 +web11331 +web11330 +web11326 +web11325 +web11324 +web11323 +web11322 +web11321 +web11320 +web11318 +imnode05qa +web11316 +web11315 +web11314 +web11313 +web11312 +web11311 +dbhps01qa +web11307 +web11306 +web11305 +web11304 +web11303 +web11302 +dbprosearch01perf-6120 +web11300 +web11288 +web11287 +web11286 +web11285 +web11284 +web11283 +web11282 +web11281 +web11280 +web11278 +web11277 +web11274 +web11273 +webadmin01qa +web11270 +web11268 +web11267 +web11266 +web11264 +web11262 +routernet22 +web11260 +web11258 +web11257 +web11256 +web11255 +web11254 +web10831 +web11250 +web11248 +routernet20 +web11246 +web11245 +web11244 +web11243 +web11241 +web11240 +web11237 +web11236 +web11235 +web11234 +web11233 +web11231 +web11228 +checkmate6 +web11226 +web11224 +web11223 +web11222 +web11221 +web11220 +web11217 +web11216 +web11214 +web11213 +web11598 +dbhps01db +ws01qa010 +ws01qa008 +web11150 +web11147 +web11146 +web11145 +web11144 +web11143 +web11141 +web11140 +web11138 +web11137 +web11136 +web11134 +web11133 +web11132 +web11130 +web11127 +web11126 +web11125 +web11124 +web11123 +web11118 +imnode03qa +web11114 +web11113 +web11112 +ws01perf +web11110 +web11107 +web11106 +web11104 +web11103 +web10815 +web11100 +web10785 +web11087 +web11086 +web11084 +web12259 +s-test1 +greenberg +web11083 +web11082 +web11081 +web11080 +web11077 +web11075 +web11074 +web11073 +web11071 +web11070 +web11068 +web11067 +web11066 +web11064 +web11063 +web11062 +web11061 +web11060 +web11057 +web11056 +web11055 +web11054 +web11053 +web11051 +web11050 +web11048 +web11047 +web11046 +web11044 +web11042 +web11041 +web11040 +web11037 +web11036 +web11035 +web11034 +web11033 +web11031 +web11030 +web11028 +web11027 +web11026 +web11024 +web11023 +web11022 +web11021 +web11020 +web11017 +web11015 +web11014 +web11013 +web11011 +web11008 +web11007 +web11006 +web11004 +web11003 +web11002 +web11000 +web11593 +cplogin01ete +web10149 +web10847 +web10846 +web10845 +web10844 +web10843 +web10841 +web10840 +web10838 +web10837 +web10836 +web10833 +web10832 +web10830 +web10826 +web10825 +web10823 +web10821 +web10820 +imnode01qa +web10816 +web10814 +web10813 +web10812 +web10811 +web10810 +web10807 +web10806 +web10805 +web10804 +web10801 +web10800 +web10788 +web10786 +web10784 +web10783 +web10782 +web10781 +web10780 +web10777 +web10776 +web10775 +web10774 +web10773 +web10771 +web10770 +web10768 +web10767 +web10766 +web10764 +web10763 +web10762 +web10761 +web10760 +web10757 +web10756 +web10755 +web10754 +web10753 +web10751 +web10750 +web10748 +web10747 +web10746 +web10744 +web10743 +web10742 +web10741 +web10740 +web10737 +web10736 +web10735 +web10734 +web10733 +web10731 +dbadmin01perf +industrymail +web10499 +web10550 +web10548 +web10547 +web10546 +web10544 +web10543 +web10542 +web10541 +web10540 +web10537 +web10536 +web10535 +web10534 +web10533 +web10531 +web10530 +web10527 +web10526 +web10524 +web10523 +web10522 +web10521 +web10520 +web10517 +web10516 +web10515 +web10514 +web10513 +web10511 +cp03dev +web10508 +web10507 +web10506 +web10504 +web10503 +web10502 +web10501 +web10500 +web10487 +web10486 +web10485 +web10484 +web10483 +web10481 +web10480 +web10478 +web10477 +web10476 +web10474 +web10473 +web10453 +web10452 +web10427 +web10425 +web10420 +web10418 +web10417 +web10416 +web10414 +web10413 +web10412 +web10411 +web10410 +web10407 +web10406 +web10405 +web10404 +web10401 +web10400 +web10388 +web10387 +web10386 +web10384 +web10383 +web10382 +bk05 +bk03 +web10376 +web10374 +web10373 +web10371 +web10370 +web10368 +web10367 +web10366 +web10364 +web10363 +web10361 +web10360 +web10357 +web10355 +web10354 +web10353 +web10351 +web10350 +web10348 +web10347 +web10346 +web10344 +web10343 +web10342 +web10341 +web10337 +web10336 +web10335 +web10334 +web10333 +web10331 +web10330 +web10328 +web10327 +web10326 +web10324 +web10323 +web10322 +web10321 +web10320 +web10317 +web10316 +web10315 +web10314 +web10313 +web10311 +web11575 +cp02backup +web10250 +web10246 +web10245 +web10244 +web10243 +web10241 +web10240 +web10238 +web10237 +web10236 +web10234 +web10233 +web10232 +web10231 +web10230 +web10227 +web10226 +web10225 +web10224 +web10223 +web10221 +web10220 +web10218 +web10217 +web10216 +web10214 +web10213 +web10212 +web10211 +web10210 +cp02int +web10206 +urbanhome +web12894 +web10203 +web10201 +web10200 +web10188 +web10184 +web10183 +web10182 +web10181 +web10180 +web10177 +web10176 +web10174 +web10173 +web10171 +web10170 +web10168 +web10167 +web10166 +web10164 +web10163 +web10162 +web10161 +web10160 +web10155 +web10154 +web10151 +web10150 +web10148 +web10147 +web10146 +web10144 +web10143 +web10142 +web10141 +web10140 +web10137 +web10136 +web10135 +web10134 +web10133 +web10131 +web10130 +web10128 +web10127 +web10126 +cmbuilder +web10124 +web12419 +web11728 +web11729 +web11719 +ubr01swd +web12895 +dbsearch03dev +web11596 +rtp01qa +web10123 +collectorky +oh-mysql-02 +web10818 +web11735 +web10122 +web10121 +web12896 +web10120 +cp08dev +web10117 +prointernal +web10116 +dotla768 +lconline +tabul +web10115 +web11989 +web10114 +web10113 +web10111 +web10110 +web10108 +web10107 +web10106 +web10104 +web10103 +web10102 +web10101 +web10100 +web10087 +web10085 +web10084 +web10083 +web10081 +web10080 +web10078 +web10077 +web10076 +web10074 +web10073 +sm100 +sea10 +web10072 +crafty01 +web10071 +web10070 +web10067 +web10066 +web10065 +web10064 +web10063 +web11739 +web10061 +dbsearch0pro02qa +web10060 +web10058 +web10057 +web10056 +web10054 +web11742 +web10053 +web10052 +web10051 +web11748 +autoscout24 +web11749 +highlander +vmhosting +web10822 +web12295 +web12289 +web12899 +grandmom +web11878 +web10824 +presentations +smoky +red2 +tsst +pgtest +ptech +fps.eu1 +fps.tc1 +fps.wg1 +web12822 +web12912 +newky +upload01qa +dbadmin01qa-6120 +promonet +web11247 +asoft54 +ftwright +web12262 +web12914 +upload03qa +dbadmin01db +legacy20022test +directory1 +itn +hps01 +kwtest +hps02 +web10919 +mchproxy02 +leukemia +net27sub02a +docstest +rj2707368 +net27sub02b +net27sub02c +net27sub02d +web10829 +web12797 +ns1dev +rgt +web11737 +ppcm +dbadmin01perfext +www.wiwi +ky-mysql-01-qa +web11832 +dbprosearch01 +web10050 +web10047 +web10046 +web10045 +dbprosearch01tmp +web10043 +cw01qa +web10041 +web10037 +web10036 +web10034 +web10033 +web10032 +web10031 +web10030 +web10027 +web10026 +web10025 +web10024 +web10020 +web10018 +web10017 +web10016 +web10014 +web10013 +web10012 +powerkyalt +web11564 +web11726 +web10778 +web11561 +web11559 +web11685 +net29sub06web +dbprosearch02 +webback07 +webback04 +webback03 +webback02 +webback01 +web13108 +gregz +web11555 +web13097 +cp02dev +web12864 +web11554 +web13106 +trade9957-test +web13095 +web10099 +web13094 +web10098 +web10097 +webdesignpc +web13102 +web12195 +web10095 +web13089 +web11955 +web10094 +web10380 +web11232 +thirdwriteback01int +web12589 +web11419 +web12299 +db02dev +cp01backup +web10089 +web13084 +web11954 +web11539 +web13081 +db03perf-6120 +web13079 +web13078 +intmci9 +ts25kycb +intmci3 +ts20kycb +intmci1 +routernet4ky +collector1-6120 +ts14kycb +web10079 +web11299 +net29sub05web +web10510 +web10772 +web11529 +web13072 +web10381 +cp01dev +upload03dev +checkmate9 +checkmate8 +checkmate7 +checkmate5 +checkmate2 +web12204 +web13063 +web10165 +web11520 +web12049 +web10769 +web13061 +dbhps02db +web13059 +db01dev +web11515 +ws03dev +web10062 +web12048 +icpmchat02dev +web12178 +web13052 +olddocs +web12948 +rcollector2 +net29sub04web +web12946 +web12944 +routernet0ky +web12941 +web13039 +web11902 +web11945 +web12938 +web12937 +demo1398 +demo1390 +web10042 +web11409 +web11946 +cs01qa +web12935 +icheckdocs +web10039 +web12934 +web12932 +web12046 +ts27kycb +web12931 +web11398 +dbsearch04 +dbsearch01 +web13029 +web12309 +web11943 +dbsearch01-6120 +u1204c +icpmchat01dev +imail03 +imail02 +dbhps01-6120 +mmoem01qa +demo1015 +demo1012 +demo1011 +demo1010 +demo1006 +web11938 +web12849 +demo01qa +upload01ete +web10028 +web12314 +web12920 +web12848 +web13018 +web10023 +web10752 +web12917 +dbsearch01devbknet +newdocs +web12916 +web11829 +web13014 +web11395 +cplogin04 +supportweb02 +cplogin02 +cplogin01 +imapp01qa +web13012 +web12039 +web10759 +web12911 +web12792 +web12909 +web12279 +web12846 +web12897 +web12906 +web12038 +web12294 +web10758 +web12905 +xbcast01qa +web11615 +web11449 +compatible3 +compatible2 +web12891 +web11937 +winnttestpc +reports6000 +dbhps02dbtmp +dbbiddata01qa-6120 +trade9956-test +dbadmin02-6120 +web12885 +web11722 +web11922 +web11396 +dbapp04db +cpprosearchoh +net29sub02web +cpprosearchky +web11439 +web12882 +web12879 +web13098 +web12035 +dbapp01qa +web10969 +web10390 +dbapp01db +trade9951-test +web11934 +web11430 +web11428 +cpprosearch06 +cpprohomeky +urgnet2 +powerlink8 +powerlink7 +powerlink6 +powerlink5 +powerlink4 +powerlink3 +powerlink2 +web11391 +web12189 +uploaddev +reports04 +web11843 +cpprohome04 +cpprohome03 +cpprohome02 +cpprohome01 +db01dev-6120 +web12925 +web12196 +web10392 +web12820 +web11364 +ky2 +carpt +immaster01tst +web11619 +db5ext +webadmin01ete +supportimail +web10194 +webadmin01dev +web11344 +loggingoh +web11825 +icpmnode02dev +web11373 +web11845 +ts07 +thirdwriteback01qa +bk05dev +web11917 +web12779 +idevdocs +web10999 +web12778 +web12777 +web11334 +web12278 +defendermx03bb +web10998 +net29sub03web +cal01dev +web11329 +web12192 +benchweb01 +cw09web030 +web11328 +defendermx02bb +prooh +cw09web021 +web12769 +filesender +cw09web020 +web10393 +proky +web11915 +web10916 +web12277 +dbadmin02db +cw09web010 +defendermx01bb +web11298 +web13123 +web11369 +web10396 +web12598 +web11849 +web10397 +web10189 +web12765 +web11851 +web12927 +web10398 +web11852 +web10399 +web12939 +dbadmin01qa +petros +webadmin01 +routernet5ky +defendermx00bb +bs01dev +dbprosearch02tmpdb +mmoem01dev +web11249 +studev02 +web12276 +loggingdb +web11390 +web11862 +gaj +web12930 +cp02prod +web10419 +zgh +web11811 +web11839 +whatasite +web11319 +web11368 +mzj +web11864 +salem1 +web11317 +gzw +web12758 +web11865 +web11913 +web10423 +web12929 +xbcast01demo +web10424 +perf-route-ds3 +web11951 +web11818 +web11870 +web11872 +khaled1 +web10429 +web10995 +cpprohome01qa +www.mongolia +icpmnode01dev +web11893 +dbapp04 +web11875 +cpprohome02qa +web11693 +checkdocs +briandev +web11877 +web11835 +pw01ete +abaco1 +xbcast01ete +web11658 +web11639 +web12949 +web11880 +cpanel4 +web11882 +pdns4 +taban +cal01qa +qatest5 +cannotorder +qatest4 +web11702 +web11884 +web11885 +mehr +skyy2011 +ws61 +qatest2 +routernet9ky +appcgi1 +web10732 +web11219 +web10509 +appcgi2 +ws71 +web11310 +web11887 +web11888 +net29sub13sysadmin +mm01 +web12751 +dbprosearch01-6120 +blogadmin +basman +dbsearch01db +dbsearch01qa-6120 +web11900 +anderson2 +cpbidproc01dev +web11297 +web12001 +routernet8ky +web12945 +web11296 +ws02qa +web13099 +web11931 +web11295 +jeremydev606 +web11959 +web11294 +routernet7ky +web11293 +web11599 +imnode01tst +routernet6ky +routernet28 +routernet26 +routernet23 +routernet21 +routernet19 +routernet18 +routernet17 +routernet16 +routernet10 +web11910 +web10992 +web12739 +recimnode01 +recimnode02 +recimnode03 +web11908 +digiline +upload01dev +nasim +recimnode04 +recimnode05 +recimnode06 +recimnode07 +web12003 +web12004 +didattica +web12266 +web11905 +videoconferenza +web10991 +dbsearch01qadbperf +web12006 +routernet3ky +rtp01 +web11907 +web11399 +mooc +web12008 +web12734 +web12733 +web10849 +web12129 +web12009 +rogerlaptopwin98 +web12815 +web11911 +web11894 +web10011 +cpprohome01prod +web11279 +web11912 +cronweb02 +cronweb01 +web12732 +routernet2ky +web12918 +web12306 +web11897 +web12729 +web10015 +web12269 +arddb +jaysen +web11089 +routernet1ky +web11275 +web10798 +web11362 +araupload +alpacas +web12013 +web10019 +web10021 +cpprohomeoh +web12926 +web12725 +web11906 +cp02perf +web11271 +web10022 +www.s0 +web11914 +web10029 +web12015 +web10035 +web10464 +web10038 +web10040 +web11269 +web12185 +web11930 +web12719 +web11895 +kentuckyserver +cplogin03 +web12717 +reports5000 +web10749 +webim2104 +webim2103 +webim2102 +webim2101 +webim2100 +web11359 +web11263 +webim04 +webim03 +webim02 +collector2-6120 +ts24kycb +web11904 +routernet28g +routernet28f +ts18kycb +web12420 +web11261 +ts19kycb +web11259 +ts13kycb +web11950 +fpofc +webconfig01qa +web11903 +extmci1 +web11549 +web10985 +dbprosearch01qa +dbsearch04db +web11253 +web12799 +cpprosearch02qa +dbprosearch02db +cp08dev5 +web11251 +bk01net +web13092 +web12002 +dbprosearch02dbnet +cpprosearch01qa +dbprosearch01db +dbsearch01qa +ws02dev +cw09web029 +cw09web028 +cw09web027 +cw09web026 +cw09web025 +cw09web024 +cw09web023 +cw09web022 +telnetserver +cw09web019 +cw09web018 +cw09web017 +cw09web016 +cw09web015 +cw09web014 +cw09web013 +cw09web012 +cw09web011 +cw09web009 +cw09web008 +cw09web007 +cw09web006 +cw09web005 +cw09web004 +cw09web003 +cw09web002 +cw09web001 +web12798 +web11901 +demo1014 +web11242 +web12807 +web12028 +web11239 +web11890 +demo1013 +web11238 +thirdwriteback01 +oxops +datafeedext1 +web12017 +web12796 +monitoring01dev +web12291 +datafeed1collector +web11276 +web11230 +web11227 +web10980 +web11379 +web11225 +web12794 +demo1009 +web11940 +web12835 +web10978 +web12793 +web11218 +rogerspcupstairs +demo1008 +web12296 +web11215 +rpt2000 +web11349 +rtpval01 +web12802 +nihil +web11927 +web11212 +demo1007 +web11211 +web11891 +web11076 +dbhps02temp +dbprosearch01perfext +web12648 +web12290 +web12255 +web12790 +web12643 +web11713 +web12254 +ns2dev +web12639 +web11819 +web12525 +cp08dev9 +cp08dev8 +cp08dev7 +neildev02 +cp08dev4 +cp08dev3 +cp08dev2 +cp08dev1 +defendermx00 +web12787 +webimdev +web12632 +web11879 +web11929 +web11616 +web11926 +demoim +dbapp01net +stepmom +dbbiddata01qa +web11595 +paulcdev +web12785 +web12622 +web12621 +dbprosearch01qa-6120 +web12249 +web10970 +web13122 +web11692 +web12615 +dbbiddata01 +trade9952-test +dbbuild02dev-6120 +web12248 +rtp03ete +immaster03qa +cpbidproc01qa +web12239 +web12610 +ws01qa +web12247 +web11340 +immaster01qa +defendermx01 +web11612 +web12594 +fastparts +demo-3 +web12592 +fpftpserv +web12591 +web12940 +web12590 +win98testpc +web11925 +web12588 +web10827 +chatroomroster01dev +web11594 +winxptestpc +db03perf +web11609 +defendermx02 +web12292 +dbprosearch02tmp-6120-6120 +rtim +defendermx03 +web10964 +jeremydev02 +net28sub12datafeed +web11135 +web11919 +web11394 +web11597 +vbsii +cpprohome03ete +cc01qa +web10199 +winmetestpc +wiki01 +web11698 +dbprosearch02qa-6120 +thirdwriteback01prod +web13115 +rweb01 +web12565 +web13109 +web13107 +web13105 +web13093 +web10044 +web13091 +web13090 +web11119 +web11605 +web11701 +web11924 +web12214 +web11010 +web11117 +web12240 +web13071 +web11116 +web10959 +web13069 +web13065 +web11115 +web11715 +web13062 +cpprohome02int +loggingohnet +web13049 +web12947 +web11272 +web13045 +web12943 +web12942 +web13040 +db01perf +web13038 +web13037 +web11099 +web11948 +web11969 +web12933 +web13032 +web11098 +web13031 +web12928 +web10997 +web13025 +web12924 +web11699 +web12923 +web12922 +web12921 +web11096 +web13019 +rtp01ete +web12915 +web11095 +web11949 +web12913 +web12499 +web10994 +web12898 +web12772 +web10993 +web12893 +web12237 +web12892 +web11592 +web12890 +web12888 +web11092 +cp3web +web11091 +dbhps02-6120 +web12831 +web11090 +web12875 +web10988 +web11942 +web10792 +web12869 +web11899 +web12868 +web13119 +web11591 +web12863 +web12862 +web10986 +web12859 +web10048 +badpentiumii +cpprohome02dev +web12855 +web11085 +web12852 +web12850 +web10984 +web10983 +web11327 +web12842 +web12839 +web10982 +client03perf +web11590 +web12832 +web12828 +web11079 +web12532 +web12825 +web11078 +web10793 +web10977 +rtnode01dev +web12529 +web12814 +cpprohome01int +paulc02dev +web12809 +web12808 +web12795 +web10975 +web10974 +web12495 +web10972 +web12194 +web11587 +web11069 +gatewayrouter +web11708 +web12759 +web12791 +cpprosearch04 +web12519 +web12752 +web10966 +web12749 +web12275 +cpprohome01ete +web12745 +web10965 +web12740 +cpprosearch01 +web13085 +web12738 +web10197 +web10963 +net27sub03 +web12829 +web12730 +web10962 +web12724 +web12722 +web12718 +web11649 +web10960 +web12712 +web10958 +cpprohome01dev +web10957 +web12509 +intmci5 +intmci4 +intmci2 +web10956 +web10949 +web12936 +adminback +web12505 +web10952 +web11889 +trade9919 +trade9918 +trade9917 +trade9916 +trade9915 +trade9914 +trade9913 +trade9912 +trade9911 +trade9910 +web12491 +web10950 +web12501 +web12649 +poolmaker2 +cpreportsbackup +hawkingdialinrouter +web12645 +web12489 +web12644 +cp01prod +web12284 +web10049 +web12019 +web10467 +bwg +web10055 +db02dev-6120 +web12642 +web12641 +web12635 +web10945 +web12629 +web12628 +web12486 +web11393 +web11921 +web10943 +cp05qa +qadb3 +web11582 +web10059 +dbbuild01devcoll2 +web12619 +web12484 +web12611 +fascache +web12022 +web10469 +dvlabs +web12608 +nnssa1 +rcollector1 +web12597 +web10940 +web12596 +web12482 +web12595 +web10068 +web12593 +dbsearch02devbknet +web12602 +web11038 +web12600 +backofgen +dbprosearch02-6120 +kentucky2 +fn01qa +web10937 +designer-stg +web12585 +cp03qa +hpsbackup02 +hpsbackup01 +web10936 +web12579 +web12478 +web12578 +web12477 +web11923 +web12572 +web10069 +web12571 +web12569 +web10471 +natalie +shootingstar +web10075 +super2 +web10082 +dbadmin01dbnet +web12476 +cp02qa +5201314 +skel +web10086 +web12567 +webconfig01 +web100005 +trade9900-control +web100003 +web12559 +web12558 +web10932 +web11579 +web12555 +web12554 +cp01qa +cp01bench +web12552 +web12549 +studev +web12545 +web10928 +web12539 +web12538 +web12534 +web12533 +web11928 +jwebconfig01 +web10925 +web12523 +web12522 +web10924 +web12518 +web12517 +web10923 +web12512 +dbhps02 +dbhps01 +web12510 +web12498 +web12497 +web12496 +web11589 +web12494 +web12493 +web12492 +web12490 +web12488 +web10920 +web12222 +web12475 +web12459 +web12474 +jeffmlaptop +web11016 +web12469 +web12468 +web12457 +web12462 +cp08dev6 +web10799 +web12458 +web12456 +rtpnode05ete +checkmate4 +web12453 +web12455 +web12449 +cp08dev18 +web10912 +computerinabox +web12442 +web12435 +cp01perf +web12219 +web10939 +web12448 +web12418 +icdev +web12447 +web12412 +web12445 +backuppc4 +backuppc3 +backuppc2 +backuppc1 +web10938 +ts23kycb +cp04qa +web12439 +web11920 +ts12kycb +cp02qa002 +cp02qa001 +snoopyoh +web10739 +web12479 +lupin +web10088 +dukakis +sethu +routernet30sub03 +web11309 +imgcollector1qa +web12345 +web12343 +web12339 +web12338 +web12434 +web12335 +web12282 +web12333 +web12332 +v23mig +web12330 +web12325 +web11308 +web12319 +web12318 +cpprosearch05 +cpprosearch03 +cttest +web12429 +cpprosearch02 +web12312 +productsdemo +web10090 +web12310 +web12428 +web10091 +db7netdev +web10092 +jobfair +web12298 +web11695 +cpbidproc02dev +narab462 +web12297 +web10093 +web11918 +web11403 +web12304 +ky-mysql-01-dev +web12293 +web12215 +web12300 +web12288 +web12425 +web12424 +web11569 +web10105 +web10389 +web12422 +web10096 +web10109 +upcheckmate03 +web11812 +ky-mysql-01 +web11568 +web10112 +web11841 +web11869 +web12029 +web11389 +fighters +dbhps01temp-6120 +meta1 +web10118 +web10119 +jawknee +web11932 +web10479 +megap +usd +mel01 +web10125 +web10129 +web10132 +muskoka +pinker +homer2 +web12034 +web10482 +jeremie +orkutthemes +napstar +rogerhome +pge +jaydeep +unipower +webmail.webmail +builder.webmail +web10138 +web10139 +web10145 +mtf +neildev +web10152 +web10153 +web10156 +web10157 +web10158 +web10159 +jbz +web12919 +pt1 +webinterchange +web11941 +george1 +rpt1000 +web12042 +web10489 +hpg +rahuljain +gpc +owlseye6 +sizzle +isrc +woodlawn +server001 +ipv4add6 +umbracotest +mspro +web10175 +patch4 +www.uruguay +avatar2 +unified +web10178 +sentral +web10179 +db1net20 +web10202 +web10193 +msv +csv +web10204 +parking-san-mc +web11229 +park-memcached +web10495 +freeproxy +web10215 +biuletyny +aimtestpc +ts15kycb +web10496 +luciana +web11935 +cpimnode01 +cpimnode02 +dkn +cpimnode03 +cpimnode04 +cpimnode05 +cpimnode06 +cpimnode07 +cpimnode08 +cmdev2 +web11697 +web12950 +dbsearch03devbknet +parking-tor-mc +cplogin03ete +cpprosearch01prod +web12139 +web12268 +web11961 +web11962 +ts26kycb +ws01qa001 +ws01qa002 +ws01qa003 +ws01qa004 +ws01qa005 +narrabri +ws01qa006 +ws01qa007 +ws01qa009 +web10312 +testim02 +cpbidproc01 +cpbidproc02 +vzxca +mailstore2 +web11896 +child1 +web10318 +web10319 +dvredit-crackdb +portcullis +test.cms +froth +web11967 +web10325 +web11968 +web10329 +web10332 +braddev +web11970 +dbsearch01dev-6120a +web10338 +kiarash +cfm +web10340 +web10345 +web10349 +galahad +elgar +web10352 +trade9954-test +web12415 +cplogin01int +ulysses +web10359 +web11975 +phadmin +rmi +rodina +web10362 +web10365 +scottbat +web10369 +mcleod +web10372 +emin +butch +nwvl +mahboob +ph4nt0m +dbadmin01collnet +web10375 +web10377 +drugon +bugsy +dayna +chss +ohdc +barn +utenti +vcb +web10378 +thunderbird +blunt +web11979 +webformcc.web.d-dtap +web10385 +checkmate10 +web10391 +svevo +sasika +cpreports +web10402 +elinks +cafe1 +bilby +blogmu +jalapeno +smiler +web10403 +web10394 +web11947 +web10395 +ppp4 +web10408 +webdav1 +dan2 +dps1 +web10409 +web10415 +paraisossecretos +regus +web11984 +web11429 +web10421 +web10422 +web10990 +web10426 +trade9959-test +web10428 +web10430 +web10545 +web11988 +web10454 +web10549 +web10455 +web10456 +web10457 +frontpage1 +web10458 +web10460 +web10461 +web10462 +autoconfig.bd +webdisk.bd +web10463 +web10465 +web10466 +icpmmaster01dev +web10468 +web10470 +datacenternetoh +web11995 +web10472 +web10475 +db5kyint +web12609 +web10488 +web10490 +web10491 +web10492 +autodiscover.bd +web11999 +web10494 +web10505 +web10497 +web10498 +web10512 +tin-tin +db5collectorky +wiki01qa +web12112 +wv +playtime +ky-brianweb-01-dev +alrahma +web10518 +fastnnet +web10519 +web12113 +web10525 +web10528 +web10529 +hps01qa +web10532 +dbsearch03dev-6120 +web10538 +web10539 +web12118 +web11289 +web12123 +web12125 +www.phuket +web12271 +dbprosearch02tmpdbnet +web12127 +www.mauritius +dbsearch04-6120 +demoimmaster01 +padfoot +web12132 +web11898 +hollander2 +z-v-tamngung-20130130-www.mobile +hollander3 +www.ketban +hollander4 +web12329 +cmsupport +z-v-tamngung-20130130-www.mobilegame +tsung0 +web12138 +dbapp01 +web12143 +web10339 +web11519 +web12144 +streamings +gammoudi5 +web12145 +trade9901-control +datafeed5 +routernet0 +routernet1 +web12148 +web12149 +dbsearch01net +xbcast03ete +web12152 +web10745 +web11423 +cpimmaster01 +cpimmaster03 +cpimmaster04 +web11813 +web11939 +molitva +web12158 +routernet10ky +web12159 +web11909 +rtmaster01dev +web10790 +web10791 +web10802 +web11953 +web11892 +web10797 +web12165 +ts11kycb +web11252 +ts16kycb +web10828 +mx2o2 +ts22kycb +devnetrouter +mx2o3 +web12169 +web10834 +datafeed3 +cs01dev +mxo2 +mxo3 +www.prosper +webadmin01perf +tinnhan +stylesgiles +mmoem03 +web11694 +jnb +www.tuvangioitinh +m.nhac +xbcast01 +ipswich +xbcast03 +web12179 +web10989 +web11001 +net29sub01web +web12182 +autoconfig.survey +autodiscover.survey +www.lamquen +ucow200018 +web11005 +ucow200118 +dtdd +upcheckmate01 +upcheckmate02 +www.nhac +web10911 +web11012 +autodiscover.um +web10913 +smsbongda +web10914 +didong +products.demo +ws04dev +web10915 +viec +web10917 +www.tinnhan +intelec +web11018 +www.tuvantamly +z-v-tamngung-20130130-mobile +web11019 +onlinehelp +datafeedtest +web10921 +web10922 +web11025 +z-v-tamngung-20130130-www.javagame +www.dtdd +web10926 +web10927 +web11029 +skn +web10931 +ucow200218 +web11032 +web10933 +login01qa +ucow00018 +web10934 +web10935 +ucow00118 +web11528 +web11039 +web10941 +dbsearch01dev +web10942 +ucow00218 +web11043 +web12190 +mgd +web10944 +web11045 +www.didong +z-v-tamngung-20130130-javagame +krd +web10946 +lemmiwinks +web10947 +uploaddev2 +web10948 +www.operacje +tuvangioitinh +web12191 +lesath +vampira +tuvantamly +www.ukr +www.ringtone +lympne +web11049 +web10951 +web11052 +web10953 +test.nhac +www2.nhac +web10954 +web10955 +web11058 +web11059 +mxserv2 +www.smsbongda +z-v-tamngung-20130130-mobilegame +web10961 +web12193 +lamquen +c-3640-v03-01.rz +ladon-1.rz +web11065 +mailrelay-eddev +mailrelay-edprod +oxford1 +c-asa5520-v03-01.rz +c-5508-n04-01.rz +c-5508-v03-02.rz +dh-ramirlt +www.shop1 +cw01qa001 +webdisk.resellers +ranch +hughie +web10967 +web10968 +rhdev +web10779 +kevindev02 +web10971 +supportcenter +web11072 +sandal +acsteam +web10973 +web11691 +perky +web10981 +ssotest2 +arundel +dmv +dbsearch01collnet +0745 +web12197 +wsftp +treasurer +web11936 +web10987 +web12208 +virtuality +evision-test +web11088 +foe +bs01qa +web11102 +tutoriales +web11093 +lcezone +web12209 +anacreon +1006 +web11094 +cw01qa002 +web11105 +web11097 +web11108 +web11109 +intpt01a +mchproxy01dev +web11120 +web11122 +dbhps02temp-6120 +net29sub07proweb +trpz +web11687 +collector1qa +diseno +web11128 +web11129 +web11131 +web12202 +web11139 +ns2.sdns +ns1.sdns +itanium +www.rotor +web11148 +web11149 +tcoh +web12225 +powerky +poweroh +web12228 +wlan-switch.dyn +web10459 +web12230 +host35 +web12599 +jwebconfig01qa +prod.contentlibrary +web12233 +web12819 +web12235 +rtp01demo +web11265 +cpimmaster02 +web12242 +rw7 +webconfig01train +ssqa +pw01qa +web11291 +autoconfig.t +web11292 +web12246 +autodiscover.t +allthegreenhomes +wlan-switch.inf +credix +rtpmaster01ete +cs8 +ts09 +rtpmaster01 +dadmin +cs22 +www.arquitectura +begin +autodiscover.tmp +webdisk.tmp +autoconfig.tmp +qa-version +server1010 +yamyam +anteprima +www.bkr +www.aldrin +anne1 +www.toplevel +www.mmone +mmone +mcommerce +craven +rtpmaster03 +vps-107.cp +web12256 +protek +boneyard +steveo +web11397 +malang +creepers +trpz.dyn +trpz.inf +suites +cpprosearch01ete +wms5 +wms4 +supportweb01 +intratest +www.derecho +supportweb03 +ims2 +fascm +compbio +l4d +wms6 +www.psicologia +mbl +testnode1 +appstest +cp01prod001 +kaitain +web11438 +dha +messalina +agrajag +lucilla +garkbit +tps1 +profesores +planetree +web11440 +lti +carthago +compost +myteam +fabia +skyx +tsung1 +publicaciones +mockturtle +sartre +herbster +webdisk.david +iulia +db3collectorky +kirsten1 +cpprosearch01int +icpmmaster02dev +hamzeh +realcity +reports01 +dedicatedserver +dinosaurs +reports02 +reports03 +reports05 +kronos01 +srv06 +kronos02 +ts04b +ts06b +stg02 +stg01 +jhw +ts07b +wwwbeta +ts08b +ts09b +ts11b +ts12b +webdisk.minecraft +neerc +ts13b +ts14b +ts15b +www.ciekawostki +ciekawostki +jkoecher +gmail-iweb +brightmail +presd01 +presd02 +presd03 +idelivery10.platform +presd04 +presd05 +presd09 +ts18b +monitoringoh +ts19b +cpprosearch02int +images.swid +qa-route-intmci8 +imgcollector01dev +client01dev +cpprosearch03ete +dbprosearch01net +idelivery11.platform +rtpnode01ete +ws31 +rblack +berkshire +ws32 +hgxy +xinh +hug0318 +pcv +snieg +mmtp +ws41 +ws42 +evo-master +www.bioinformatics +jay.ns +newmeleno +a.riten.hn +ws51 +b.riten.hn +meleno.in +c.riten.hn +d.riten.hn +lindon +e.riten.hn +www-h +puppymoon +daa2 +fire1 +aep +bankruptcy +kronos1 +sunny.hn +meleno +wwwmeleno +newmeleno.in +ivy.ns +a.sunny.hn +b.sunny.hn +riten.hn +c.sunny.hn +d.sunny.hn +e.sunny.hn +munin.riten.hn +newsat +westy +superstore +kiril +localhost.net +kns +rsi +dhcp04 +yamamoto +holm +env1 +www.prosfores +admin.env1 +market3 +ws.statm +mkg-admin +statm2 +cas01 +hidayat +wayfarer +istra +hta-prodhost0.sol +isk +vakant.kc +stigmata +lbi +matthijstest +marilot1-design +randr +eufrasia.bio +teco45.ae +router-sdi.teseo +d37pc2.mp +uniflow +pent-x450.cbm +b20pc2.bq +darker +a37pc2.mor +pc22.icp +sidirect2.sidi +fcpc19.far +ws52 +dbapp04-6120 +b20ppc3.bq +fourhorsemen +ws62 +cl02 +ws72 +ws81 +ws91 +annabelle +rubis +ws92 +tamtam +trade9953-test +cplogin01prod +cp08dev12 +sonyericsson +biff +cp8 +alize +r2000 +mportal +accueil +cp9 +urgnet3 +web11828 +cp7 +cp6 +sql2k3 +sql2k2 +efront +prelive-admin +ew54384r9bcgh3 +ew54384r9bcgca +ew54384r9cxl7w +web11916 +ew54384r95tahl +ew53680r9cxfhg +ew54384r9d5fkm +ew54384r99z0rh +ew54384r9abzgm +win101 +web10794 +ew54384r9cxkf8 +sql2k5 +sql2k4 +sql2k1 +ew54384r9d4wth +ew54384r99nhcl +ew54384r9d6hla +ew54384r9d6hlw +ew53680r9ah4kc0 +ew54384r9arf7f +ew53680r991hl4 +collectorback +ew54384r9ca8rn +cp08dev19 +ew53680r9bbvt4 +wiki01dev +mail.chat +ew54384r9cxkyg +ew54384r9ca99x +ew54391r96vvye +data6 +client02dev +ew54391r96vvzv +trade9958-test +ew54384r95taeb +web11859 +web11874 +web10817 +dbbuild01dev +ew54384r95taee +ew54391r98m0p5 +web10819 +web10379 +mmoem01 +ew54384r95taka +mmoem02 +ew54384r979gln0 +www.eburg +routernet11ky +jeremyclientdev +ew54384r99nht2 +web11944 +ew54284r9fd8cb1 +routernet28sub13sysadmin +routernet12ky +routernet13ky +qaweb3 +dbhps01temp +www.aga +web10835 +routernet14ky +client03dev +demoimnode01 +routernet15ky +web10839 +ew54384r98e15v +web10842 +web12119 +grodno +www.instalator +urbanlaptop +1337 +web10358 +web10930 +dbbuild02dev +web10848 +www.tournaments +dbbuild01net +okr +web10850 +rtpnode03ete +web12172 +rtpnode01 +rtpnode03 +rtpnode05 +meier +ts21kycb +web12229 +web10738 +zhitomir +mercurio2 +ew54384r99z1wf +sivaram +ew54384r9cxlz30 +ew53680r9amhxg +ew54391r96zpan +luk +meca +lodestar +www.rating +cbb +ew54384r9d5f9n +sanket +pdn11g-scan +ew53680r992l0x +ew54384r96rm4f +administrativo +crecon +tournaments +ew54384r96rm8b +ew54384r98kz000 +ew54284r9ehm2n0 +ew53680r9d5ctz +ew54384r99nham +ew54384r99nhdp +ew54384r99nhm6 +ew54384r9ca8h7 +ew54384r9atcfd +ew54384r9d4t3a +nlplanner +www.erevan +ew54384r96vtbm +ew54384r9d5fep +ew54384r99nhwh +ew53680r970llk +kherson +uzbekistan +ew53680r970lly +ew53680r970lmv +maild +ew54384r9aa18v +ew54384r9aa19v +ew54384r9aa19x +ew54384r9aa19y +ew53680r99nlh6 +ew54384r9aa1a7 +ew54384r9aa1b1 +feniks +baks +ew54384r9aa1b6 +www.mur +mogilev +ew54384r9aa1aw +ew54384r96vtge +ew54384r96vt63 +test-p +ew54384r96vt97 +ew54384r96vta6 +ew54384r96vta9 +telaviv +ew54384r96vte8 +ew54384r96vtkl +ew54384r96vtlm +ew54384r96vtv7 +ipv4with6 +ew54384r9ac08e +viz +ew54384r9ac0f1 +lepus +ew54384r9ac0da +ns22266 +ns24331 +ew54384r9ac0dt +ew54384r9ac0ev +autoconfig.central +autodiscover.central +ew54384r9ac0ew +ew54384r9ac0gt +ew54384r9ac0gv +ew54384r9ac0gy +ew54384r9ac0hz +ew54384r9ac0ll +ew54384r9ac0nl +vps130 +ew54384r9ac0pb +ew54384r9ac0pt +ew54384r9ac0rc +vps120 +ew54384r9ac0wv +websrv1 +mango2 +ew54384r98m384 +ew54384r98gdm6 +ew54384r98gdn7 +ew54384r98gdr0 +ew54384r98gdnp +ew54384r98m1k3 +ew54384r9ca8g3 +ew54384r9bl31t +ew54384r9d4wd3 +vps160 +ew54384r9bcgk7 +vps167 +vps187 +vps178 +tripplanner +vps159 +vps146 +ew53680r992x65 +ew54384r9d4wcr +ew54384r9ae2c4 +ew54384r98kyyd0 +ew54391r98rhmz +txdowtp +ew54384r9d4wnd +ew54384r99nknz +ew54384r9cd2mp +ew53680r99v27b +ew54384r9bl2ke +ew54384r9cl1lc0 +jawhara +ew54384r9bl3bh +moslem +ew54384r9arh37 +ew54384r993neg +ew54384r9abzme0 +ew54384r9c03p60 +ip8 +vps046 +vps043 +vps039 +ian1 +ew54384r98kz0a +ew53680r981fpb +ew54384r98kyca +ew54391r99ngb9 +ew54291r9rg5k1 +vps025 +ew54384r98ng5f +ew54391r99ngen +vps022 +sjp +ew54384r9abzwz +ew54384r98nep6 +vps018 +ew54384r98nena +ew54384r9c9hl8 +ew54243r9f714d +vps014 +ew54243r9f713x +ew54384r99v3en +ew54243r9e8mkh1 +www.mature +vps008 +vps007 +ew54384r99l6gb +proxmox1 +vps005 +vps004 +vps003 +vps173 +ew54384r99l6gk +time.services +ew51fkya60218 +ew51fkya60254 +ew54384r99pp4n +ew54384r98xy7h +ew54384r97ygm5 +sparkhost +mailman1 +p001 +ew54384r9ca8yx +ew53680r9a2177 +ew53680r9a217p +ew54384r99pr0c +ew54384r9bkm3d +media8 +ew54384r9ca9cp +media6 +ew54384r9cd2pt +ew54384r99v3md +ew54384r9cd5a9 +ew54384r9be4p6 +ew54384r9be4pr +ncs1 +xkb +wwf +ew54384r9be4td +ew54384r9be4vv +ew54384r98rha1 +ew54384r9bab0m +ew54384r99pt3z +ew53680r987ct1 +ew53680r9a34w8 +ew54384r9bkn52 +ew54384r9baaw2 +ew54384r9ac006 +ew54391r9bhymp +ew54384r99ppag +ew54384r9ac011 +ew54384r99vc66 +ew54384r99pnxl +ew53680r987mrt +ew54384r99ppp6 +www.khabarovsk +ew54384r979gll +ew53680r987pkc +ew53680r987pla +ew53680r987pky +ew53680r99px0r +ew54384r9cd4g7 +ew53680r989df1 +ew54384r99vc4g +ew54384r9bcf1x +www.ulan-ude +ew54384r99prk4 +ew54291r9e8ehw1 +www.surgut +ew54384r9bcf9m +ew54384r99prgb +ew53680r990he1 +ew53680r990hfc +ew53680r99py2e +ew54384r9bcfkh +ew53680r978fdb +ew54384r99prnt +ew53680r9a706g +www.dreamweaver +naberezhnye-chelny +ew53680r99pz0p +ew53680r99pz76 +ew53680r99pz3t +ew53680r991hlk +ew54384r979gtb +red1 +ew54384r99nh73 +ew54384r99nh82 +ew54384r99nh8f +www.phpbb +listas2 +ew54384r9ca95v +test2012 +ew53680r992l0t +ew54384r9b5x46 +hirano +simomura +ew54384r99nhb5 +ew54384r99nhrr +yamamura +ew54384r99nkh8 +ew53680r992krx +ew54384r99nkmx +irifune +aoba +fujimoto +ew54384r99nkpb +shimomura +yoda01 +ew53680r97ent3 +taira +hasuda9230 +hanazono +naitoclinic +ew54384r9bl341 +chuuou +ew54384r9aprw5 +yamadaganka +ew54384r9bl35z +inagaki +ew54384r9bl37l +miyata01 +ishimoto +ew54384r9aprw9 +ew54384r9arh24 +ew54384r99pvpx +kiyose +ew54384r9arh0e +s4336 +morinoki +ew54384r9bl2dx +ew54384r9aprzb +kirin +ew54384r9bl2pc +yoshimi +bandgplotter.printer +nanohana +kensei +ew54384r9cc7f6 +ew54384r99v3d5 +ew54384r99v3fa +ew54384r99v3mp +ew54384r99pp77 +shinobu +kishi +kashima +fukushima01 +suzuran +fbc +greenpark +kumagai +machino +ishibashi +mick +ew54384r9cd2bt +ew54384r99pr11 +smtp1.net1 +ew54384r9cd2ht +ew54384r99pr5h +ew54384r99pt16 +ew54384r99pnfn +www.mw +ew54384r99pnle +ew54384r99pnlf +s4335 +bambou +www.ge +ew54384r99pnkv +ew54384r99ppfz +ew54384r99prmh +www.nf +ew54384r9arf64 +ew54384r99pvkx +ew54384r99pvva +ew54384r99pvvk +webvip +ew54384r99pvwt +ew54384r99pvzl +ew54384r99rm39 +nbdb +ew54291r9rmzf4 +gruppo +rsp +ew54384r99rm56 +ew54384r99rm5y +cndev +ew54384r9arcze +ew54384r9arfna +ew54384r9argzy +ew54384r9arfv5 +berlioz +s4334 +ew54384r9argfx +ew54384r9arfpz +neocom +ew54384r99z08t +ew54384r9bkp7m +ew54384r99z48z +ew54384r99z0pd +irfan +ew53680r9a6xf7 +ew53680r9a6xg1 +ew54384r99ppv0 +ew53680r9a6xz5 +a02 +ew53680r9b0ach +ew53680r9b0ada +grenache +ew54384r9bkn8a +ew54384r99vb58 +ecrins +ew54384r9bkp8a +ew54384r99vc1t +ew54384r99vc8d +a13 +a14 +orphee +ew54384r9bknhc +ew54384r99vah2 +a18 +motte +ew54384r99vahe +a22 +smetana +ew54384r99vaz3 +delos +ew54384r99vawt +v182 +vila +ew54384r99vbrx +ew54384r99vbtt +rossini +ew54384r99vbvz +birman +ew54384r99vbxr +ew54384r9bmbn7 +ew53680r9b68ln +ew54384r9bmdy1 +nexus1 +ew54384r9bmdz4 +babette +rimbaud +ew54384r9bmdxx +a20 +ew53680r9b806x +ew54291r9rdfbr0 +ew54384r9bkn5e +daf +ber +ew53853mjeapn2 +ew54384r99vbeg +ew52325r9xz5w9 +ew54384r9arnh0 +s821 +crumble +ir1 +nyco +willem +marocco +r01 +r02 +ardeche +ew54384r9bnn0f +sagarmatha +ew54384r9bnn0x +ew54384r9arnn6 +ew54384r9bt7c6 +ew53680r9b86vk +ew54384r99z08f +s4330 +ew54384r99xhzt +adserver1 +ew54243r9nt1gd +ew54384r9dd73l +shopinvent +ew54384r9ca8w5 +ew54291r9e8eff +ew54384r9dd80m +ew53680r85pndk +ew54384r99prg0 +ew53680r93z807 +s601 +ew53680r93z80f +ew54384r9dd6zp +peo +ew54384r99vat6 +ew54384r9bl3ay +novell +erp1 +ew54384r9ca9lz +igw +lmail +ew54384r99yzpv +ew54384r98e60l +ew54384r9atcg0 +ew54384r9cxly50 +ew53680r99z1mp +ew52241r87235h +ew54391r99nghf +mssql03 +ew54384r9cxm340 +spm01 +sd04 +ew54384r9d4whn +ew54391r99pw150 +sia2 +ew54384r9c9k5h +biotec +ew54291r9h948g0 +s4325 +rss4 +s4120 +ew54384r99z1vz +web320 +ew54384r9bz7kw +ew54384r99vbdw +ew54384r99vbpt +ew54384r9ac0dd +web334 +web328 +web324 +s4324 +web323 +web322 +web321 +ew53680r9c68w2 +ew53680r9c68wd +ew54384r9bkm7m +backuper +ew54384r99vbrm +ew54384r99ppvg +ew53680r992kn3 +ew54384r9cd2l1 +ew54384r9ac0l3 +ew54384r9bkn8b +ew54384r98m39n +ew54384r9bkn9k +ew54384r9ac0fn +ew53680r992kla +ew52241r8mmpaz +ew54384r9bkp3a +ew54384r99vb59 +ew53680r8l979b +www.qs +ew54384r9bac3a +ew54384r99vb8p +s4321 +web114 +ew54384r99vbye +ew54384r9bzb61 +ew54384r9ac0r1 +ew54384r98gdml +1985 +s4260 +ew53680r9be0ay0 +qa.contentlibrary +s4319 +ew54384r9bzazv +ew54384r9bknaf +alsaher +keepyourprivacy +ew54384r81gtrl +hideip-sweden +fish2 +floppy +wtnmodel5 +ew54384r9cd2l5 +zaq1234 +hideip-ru +ew54384r9bknpl +hideip-australia +ew54384r9ca9e3 +ew54384r99vam8 +l2tp-tk +l2tp-ru +internet1 +l2tp-sg +l2tp-se +ew54384r9ac0y3 +ew54384r9b5r1p +ew54384r9b5w73 +l2tp-sp +www.teachers +l2tp-fr +ew54384r9848m9 +ew54384r99vbmh +ip-tk +l2tp-ch +ip-ru +preview-fsc +ip-sg +ip-se +hideip-spain +ew54384r99vawk +ew53680r9c6wdl +ew53680r9c6wex +ew54384r99vbz5 +ew53680r9be2v20 +ew53680r9avg2c +ew54384r82h6y8 +ip-ch +ip-au +ew53680r9ah595 +ew53680r9ah53n +l2tp-au +ew54384r9ca9dw +hideip-ch +ew54384r9cxmdg +ew53680r94xpw6 +ew54384r99nkxt +ew54384r9c9hmg +ew54384r9ccldz +hideip-russia +hideip-turkey +ex54391r99txdr +ew54384r9bmbrg +ew54384r9b5x73 +ew54384r82k55z +ew53680r9d1mbc +ew53680r98rfgy0 +www.opinie +lists.h1.nl +ew54384r9cxa24 +ew54384r82dtnd +bpos-eas +ew54384r9cxakz +ew54384r9be4tz0 +ew54384r9cxl43 +ew54384r9cxl1a +ew54384r9cxm43 +ew54384r9cxm1l +ew54384r9cxlcy +ew54384r9cxlka +ew54384r9cxlr0 +ew54384r9cxlkp +ew54384r9cxmda +ew53680r9d5c45 +ew54384r9cxpev +ew53680r99hlwg +ew54384r85fp530 +ew54384r9bl2w9 +madowtp +ew54384r9bl2wb +ew53680r97cz9p +orfeo +ew54384r99ptbe +ew54384r9bkp9e +ew54384r9arnv9 +ew54291r9h948m +ew54384r81ttfy +ew54384r99v3dl +ew54284r9g3w0t +nportal +ew54384r99v3n5 +ew54243r9hlp9g +ew54384r9bl3vw0 +ew54384r99v3nb +comed +support.test +ew54384r83p17g +ew54243r9hlp830 +ew54384r9b1xrh +ew54384r99vben +ew53680r87gddw +ew54384r9bmby7 +ew54384r9clezv0 +ew54384r9c03dx +vmc1 +balrog +ew53680r9aa9m5 +ew53680r9aa9mf +ew53680r9aa9xl +ew54384r9cc7fk +ew53680r98gc97 +ew54291l1bgc17 +ew54384r9bl3az +ew54384r97lmy2 +ew54384r97lmy7 +ew53680r98gcda +ew53680r98gcea +ew53680r98gcgx +ew54384r9aa19z +vmhost01 +vmhost02 +ew54384r9ac00n +ew54243r9fdrf5 +ew54243r9fdrec +ew54384r9bcgd8 +ew54243r9phccn0 +ew54384r9c04kz +ew54243r9gd498 +ew54384r9cd572 +ew54243r9gd4az +ew54384r9cd579 +ew53680r9aaadk +ew54384r99ptbd +ew53680r9axv6n +ew54384r9dd802 +ew53680r9ah38a +ew53680r9ah3c4 +ew53680r9c68th0 +ew53680r9ah5b9 +ew53680r9ah5fh +ew53680r9ah5n0 +ew53680r9ah4yh +ew53680r9ah4zk +ew53680r9ack0l +ew53680r9acgfx +ew54384r96vt7p +ew54384r97gwae0 +ew53680r98ltf6 +ew53680r98ltg5 +ew53680r98lth3 +s4311 +ew53680r98lth7 +ew53680r98ltkd +testcontent +ew53680r9aebr5 +ew53680r9axxb1 +ew54391r99ngdm +ew54384r9ccny4 +ew54384r99vcg0 +ew54384r96vt8b +ew54384r98nefd +ew54384r9bcf430 +ew54384r99vcf7 +ew54384r96vtbg +ew54384r96vtar +bsl +reloaded +ew54391r99ty13 +ew54384r96vtef +www.passport +ew54384r96vtgn +ew53680r9bd61b +s4310 +ew54384r96vtt6 +ew53680r9be09x +ew53680r9be0ad +ew53680r9be0g9 +ew54384r98nga1 +ew53680r9be2v5 +ew53680r9be2wr +ew53680r98rfh8 +ew53680r98rfhv +ew54384r98ngb6 +ew53680r9ahkz0 +ew53680r9ahkve +ew53680r9ahkvl +ew54384r99prbn +ew53680r9be7dp +ew53680r9be7ev +ew54384r9abzgd +ew54384r9abzew +ew53680r979t3t0 +ew52429r9vd40t +ew54384r99pt9z +ew54384r9arfdp +ew54384r9ac0f6 +ew54384r9ac0g4 +ew54384r9abzmh +ew54384r9ac0cp +ew54384r9ac0dn +ew54384r9ac0k0 +ew54384r9ac0l4 +ew54384r9ac0gn +ew54384r9ac0n9 +ew53680r9ar66r +ew54384r99ppcn +ew54384r9ac0lp +ew54384r9ac0nh +ew54384r9ac0t9 +ew53680r9ca23f +ew53680r87gdcn +ew54384r9argc0 +ew54384r99vcn5 +ew54391r99txlr0 +ew53680r9amhxa +ew53680r9amhww +ew53680r99nl2l +ew53680r99nl8z +ew54384r9abzy4 +ew54384r9b5x4d +ew54384r9b5x4e +ew54384r9abzr7 +ew53680r9bk62c +ew54384r9b5x5m +ew54384r9b5x6h +ew54384r9b5x6k +ew53680r99nlc2 +ew53680r99nldc +ew53680r99v228 +ew53680r99v238 +ew53680r99v253 +ew53680r99v255 +ew53680r99v23c +ew53680r99v21z +ucs1 +ew53680r99v28h +ew53680r99v28k +ew53680r99v28v +ew53680r99v28w +ew53680r99v386 +ew53680r9cc7d6 +usdigitalws3 +ew53680r98xyxc +ew53680r99py57 +ew53680r99py90 +ew53680r99py5g +ew53680r99pz2y +ew53680r99pz3r +ew53680r99pz8x +ew53680r99pyam +ew53680r99pyca +ew53680r99pyp6 +ew53680r99pzkp +grupo +ew53680r99pzlh +ew53680r99pyzf +ew54391r9c6tlc +ew54384r9cd4lh +ew54384r9cd4kz +ew54384r99vc10 +ew54384r86gryw +ew52429r9vnera1 +ew54384r9cclcn +ew54384r99prfx +ew54384r9cd4ra +ew53680r99pyen +ew53680r9avg24 +s4306 +ew54291r9fevrn +simon2 +ew53680r9avg2y +ew53680r80crra +ew52768r82y5e3 +ew53680r9avn81 +ew52768r82y4y9 +ew54391r99txe4 +ew53680r9btdmh +s4305 +ew54384r9cd4vp +iis2 +ew54284r9fd8fg +ew54384r9bl3d1 +ew54284r9ehm2n +ew53680r9bt94r +as.im +ew53680r9amhwd1 +ew54384r991mr90 +ew53680r992l0a0 +56bpos-eas +ew53680r9bv0eb +ssl30 +ew53680r9axv6r +phylab +ew54384r9bakvh +ew54384r9bz919 +ew53680r9bw2z2 +ew53680r979t3c +ew53680r99pzaf +ew54384r9ccnx7 +tndowtp +ew53680r9bteb3 +ew53680r9bteba +ew53680r9btecc +ew54291r9fmtt0 +ssl15 +ew54384r9cxa4k +ew54384r9ca8r90 +ew53680r992kl00 +ew53680r9b7yhf +ew54384r9apt1f +ew53680r9daym1 +ew53680r9daym6 +ew54384r99nhwf0 +ew53680r9daylv +ew54384r9clevh +ew54384r9cleyb +ew54384r99nkfb0 +ew54384r9apt4g +ew53680r9cmp90 +ew54384r9apt3z +ew53680r970lpb0 +ew54384r9bcggw +ew53680r99nlrx0 +ew53680r99pykx +ew54384r99pvhm +ew54384r9c3wyk +ew53680r98ltex +ew54384r9ard08 +ew54384r9ca97p +ew54243r9p0cvy +ew53680r99pylx +ew53680r9c68wk +ew54243r9p0ct90 +ew53680r9btyzn +ew54384r9848f8 +ew53680r82avlm +ew54384r9848l3 +ew53680r9dme04 +ew54384r9ca8pg +ew53680r9dmdym +ew53680r9dmdyy +s80 +ew54384r9c04kk0 +ew53680r99pzm4 +ew54384r87rxac +ew54384r9cl1lt0 +ew54384r87rwzv +ew53680r9ah52f +ew54384r9bl2cw0 +ew53680r9ah53f +ew53680r9ah56m +ew54384r9bl2ka0 +ew54384r9bcf30 +ew54384r9cd2nc +ew53680r9cxffr +ew53680r9cxfhx +ew54384r9b5wgn +ew53680r9czak4 +ew54384r9cxm95 +ew53680r9ah4l0 +ew54384r9arf3l +ew54291r9hlez8 +ew53680r9ah5ez +ew54384r9arf4l +ew54384r99rm5p +ew54384r979gbk +ew54384r9arf6k +ew54384r979gbx +ew54384r979gkv +ew54384r9c2meg +ew54384r98r75d +ew54384r98r75v +ew53680r82lalh +ew54384r9cd4l0 +ew54384r96vtd20 +ew53680r83bzpy +ew53680r9aebf60 +ew54384r9cd2eh1 +ew54384r9bch2a +ew53680r83fbrm +ew54391r99ng98 +ew53680r83rrk9 +ew54391r980c1h +ew54384r9cd4z2 +ew53680r99pzng +ew54384r9bzb48 +ew54384r9bzc29 +ew54384r9bzc37 +ew54384r99ppbv2 +ew53680r98ltk4 +ew53680r98ltl6 +ew54384r9c0497 +ew54384r9bzc5f +ew52429r9tt12p +ew54291r9rg5hm +eric8 +ew54384r9c4hkp +ew54384r9c04h6 +ew54384r88xyyh +ew54384r9c04cd +ew54384r9abzw6 +ew54384r99vcl1 +ew52429r9vd40l +ew52429r9vd40m +ew54384r9ac0gb0 +ew54384r98nem3 +ew53680r98gcb70 +ew54384r9bzbbz +ew54384r98nenl +ew54384r9ac0kt0 +ephoto +ew53680r99pz3c0 +ew54243r9ne5rv +ew54384r98nexb +ew52325r9v1va9 +ew52325r9v1val +www.p202 +p202 +ew53680r99px5w +ew54384r9ca9bd +ew54384r9be4r7 +ew54384r9848dx +ew54384r9848fg +ew54384r9848er +ew54384r980c1x +ew53680r99pza70 +ew54384r979gt6 +ew54384r9bcfgf +wbsld8c0fx2j +ew53680r99pyt1 +ew53680r93mf6f0 +ew54243r9ne5r9 +ew54384r9bzb81 +ew54384r89ywa8 +ew53680r99pz81 +ew54384r9bcfv6 +huizhou +ew54384r9a1766 +ew54384r9a174k +ew54384r9a174m +ew54384r9a174t +ew54384r9a174z +ew54384r9a175x +dstore1 +dstore2 +ew54384r9a2304 +ew54384r9a235p +share2 +ew54384r9a17n9 +ew54243r9fdrgt1 +ew54384r9a23ry +ew54384r9ca9de +ew54384r9ca8t2 +ew54384r9areh10 +ew54243r9nng6k0 +ew54291r9r246b +wbsldh5xkx4j +ew54384r9cf5dp1 +ew54384r987pyw +ew54384r9a566p +ew53680r9akk8t +ew54384r989ebw +ew54384r990gmf +ew54291r9ma0bm +ew54384r9c04dp +uatwww +ew54384r9arhmm +ew51fkya59899 +ew54384r96vt9p0 +ew54384r99vchf +ew54384r991mvd +ew54384r989wz3 +ew54384r9c04n2 +ew53680r9be08n +ew54384r9c9k5y0 +ew54384r9atcg3 +ew54384r9ca8t3 +ew54384r993ne6 +ew54384r993nlf +ew54384r9b08d6 +ew54384r9b08lt +ew54384r9b11k7 +ew54384r9b09z7 +webdrive +ew54384r9b09zp +nfs2 +ew54384r9b13gk +ew54384r9b13gr +ew54384r9a3yd4 +ew54384r9a3yk5 +ew54391r98rhnf0 +ew54243r9nng8c +ew54384r9arf3w +ew53680r9be2td +ew54384r9b0a45 +ew53680r99hlx3 +s835 +ew54384r9a8n2k +ew54384r9b5x67 +ew54384r9bknat +ew54384r9be4p7 +ew53680r98rfgw +ew54384r9be4mt +vz107 +ew54384r98rhg5 +ew54384r9c9k51 +ew54384r9b1y23 +ew54384r9cxlat +ew54384r9a9zyw +ew54384r99vchp +ribbondalda +oxo4433 +ew54384r9c9k9p +ew54384r9c0448 +ew54384r9c040m +ew54384r9c041h +ew54384r9c044y +ew54384r9b1xn6 +ew54384r9b1xmf +ew54384r9b1xv7 +ew54384r9b1xv9 +ew54384r9b1xtk +ew53680r99pxwd +ew54384r9c03db +ew54384r9c03gl +ew54384r9c03tl +ew54384r96vtat +scidata +ew54243r9nz79x +ew53680r987cw9 +ew54391r96zpbk +ew54243r9gd4bg2 +ew54384r99pphd +ew54384r9b5r03 +ew54384r9b5r09 +ew54384r9b5r37 +ew53680r87gdga +ew53680r87gdgb +ew53680r87gdfm +ew53680r87gdfy +ew54384r9b5w6v +ew54384r9b5x4l +ew54384r9b5x5l +ew54384r9b5x4v +ew54384r9b5x4z +ew54384r9b5x7a +ew54384r9b5x5y +ew54384r9b5rzp +archivemanager +ew54384r9b7cbd +ew54384r999k880 +ew54384r9arh2c +ew53680r99pyyc +ew53680r9arbhb +ew54384r99vb3z1 +ew54384r99vc0b1 +ew53680r87gzlh +ew54384r9848be +ew54384r9ac0h7 +ew54391r99z0w8 +ew54384r9848el +ew54384r99pt3f +ew54384r9arnp5 +ew54384r99pt78 +ew52429r9wwnt7 +ew54384r9c2m2t +ew54384r9c2m4d +ew54384r9c2m5y +ew54384r9c1rm2 +ew54384r9c2mdc +ew54384r9c2lx6 +ew54384r9c2mkp +ew53680r970ll8 +ew54384r99pt86 +ew53680r98rfgr0 +ew53680r970lgn +ew54384r99vbhm +ew53680r987pkh +ew53680r987pkm +ew52429r9ygkld +ew53680r987plk +lema +ew54384r9bkn31 +webdisk.forms +ew54384r9arnx5 +ew54384r9ca9m5 +ew54384r9c3wna +ew54384r99vavx0 +ew53680r970llv +ew54384r9cd576 +ew54384r9a5683 +ew53680r970lpe +ew53680r9avg2z0 +ew54384r9d6hld0 +ew54384r9c6ph4 +ew54384r99nk89 +ew54384r9cxala0 +ew53680r9b30gc +ew54384r989ebv +ew54384r98nemn +ew54384r9c9hk1 +ew54384r9arf96 +ew54384r9c9htf +ew54384r98nelz +ew54384r9c9hwh +ew54384r9c9hxc +ew54384r9c9hwy +ew54384r9c9hxx +ew53680r9ah57k0 +ew54384r9arnr8 +ew53680r9cxfkp +aud +birt +xtranet +doxa +sansan +costss +hirohiro +strelet +tokyomonkeys +jintoku +tousi +partnershop +shinfoo +execube +erens +roboinq +microfix +eco7813 +artifact +bersbar +klient2 +oodonya +dpu +gekisapo1307 +morgmolmalmo +yomoyama +ansonweb +ha1228 +bsuki702 +test004 +test006 +test010 +pluse01 +test012 +test016 +test017 +test018 +remediate +akirag3 +uetenri +gqrbm055 +sankaku +zg5 +kyokushin2 +zebu +wankoroid +bni +fotokuma +sect +genco +eiesei +suneng +netanew2 +downline2762 +builwing +datsumou +myamya +tuhan +sasatani +rakuchin01 +incense +no003 +odaatsushi +kalla +birdcage +nekote +2hanjp +grafica02 +lapisdiva +salinger +leathermall +crosswork +grafino +test013 +81q +1tax +ryoyoss +nihonkai +odicgo +alpharise +glowsurf +tsuyoshioka +saokichi +hirokuni1 +aojiru +dotchimni +jstock +paulfactory +tuncay +koba +tsubo1 +sin7021 +unseal +kanrikyoku +goodway +reshikku +pcnishiya +wpwp +xenapptest +livedata +smile38 +gradius2 +gradius3 +biei +rsu +blueearth +tokusanhin +inc02 +dalimitr +smile201303 +usefulinfo +romiromi +chimeraworks +mifaso +addressbook +ant69tr +carong +gaza21 +suninjang13 +dw012384 +dusangzzang2 +minimarket7 +nhseaftr5422 +wowgusdn +laorange1 +axian993 +zegobs2 +arcanej1 +gaza212 +kimzang13 +kimzang14 +moorootr +yaehu7 +naiadlove +barocamping +ohryuken2 +seb3309 +dressupcartr +ssnongwon +annzooco +nosmoking95 +tinyaptr6195 +cromy69 +hsh2124 +daebok3 +joliejong +phji1230 +medifun2 +auntbaby +araonktr6801 +n1hstar +kemr1436 +akari24141 +jooo761 +vna +clais +ninnin +vengence +lva +wcache +deprep +bethany +timetables +lawpre +dongsan501 +bsjbsj791 +abettetr7772 +worldnewspre +sports7 +calla6251 +tabletennispre +piroco2 +civilliberty +christianmusic +kidexchangepre +frenchfoodpre +catholicismpre +jobsearchcanada +catspre +retireplan +islampre +altmusicpre +wichitapre +soapspre +usmilitary +babyparentingpre +emailpre +tattoopre +homevideopre +hartfordpre +dying +macsupport +gojapanpre +christianitypre +paganwiccanpre +profootball +worldfilmpre +torontopre +desktoppubpre +spanishculture +rockclimbing +investingcanadapre +crosswordspre +racerelations +heartdiseasepre +dentistrypre +houstonnwpre +gosouthasia +ldspre +websearchpre +toycollecting +frenchcaculture +budgettravelpre +palmtopspre +tucsonpre +christianteens +germanculture +waterskipre +southparkpre +miniatures +bicycling +worldsoccerpre +intranetspre +alcoholismpre +history1900s +interiordecpre +collectstampspre +businesstravel +pennybaycom +parentingteenspre +atheismpre +augustagapre +microsoftsoftpre +chicagowestpre +unixpre +vancouverpre +modelrailroadpre +women3rdworld +jpkr4 +bobptr2967 +fortron +sjjiyun +gocycltr6047 +cross1 +woojung1151 +papas5 +allergiespre +imgarden365 +kudos5850 +coordiplus +puny10 +crossg +albanypre +avidleeda1 +hgijung +hairblow +tprime +kidswriting +s1devsunny +portlandmepre +mng7772 +kwons2tr5012 +razypooh +max8812 +s2pedu +manypanda1 +hongse891 +kwh79021 +kwh79022 +gi0sky +animationpre +sstarhong1 +sstarhong3 +man8334 +lglg02051 +nowkandol1 +mchanman1 +voguenewyork3 +jfriend59tr8223 +momomoguri +puntoo +hymnself +s807 +welpiatr7295 +worldpapertech +ecopromise +berry61231 +snobier +inaemedi +dodamsoktr +mallcorea +lbs276tr3039 +mj1choco +hifoods1 +kms0744 +djh165tr9046 +smarttr1853 +amazine +mijiwang +chenyou +joorok +shinsm2001 +snhk2001 +ana0202 +ad0212 +yjwone1 +csgood +tentoy +motorbank2 +saltaquariumpre +chromcell +directdnp +godo197019 +jeekeem +godo143722 +kizzz09 +ohmyggod +hitouchpen +rkrn1965 +decojetr2505 +highones1 +highones2 +cartooning +ideabook +ver4fix +frenchcaculturepre +pkartitr9472 +gdream2 +gaylifepre +artsandcraftspre +stepparenting +vps3utdell2 +witnessespre +nursingpre +environmentpre +volleyballpre +winepre +investmentclub +careerplanning +mostafa1 +privateschool +jobsearchtech +rosespre +teenexchangepre +botanypre +lasvegaspre +min233 +finefc1 +finefc3 +fortlauderdalepre +kssks0509 +seefuture +drpojang +tera1439 +chl8270 +kimdh234 +jjfamily2 +dudtn815 +personalcreditpre +iamleech +sw83293 +dj76dgb1 +outdoorlook +ilovemommytr +saltaquarium +dlgmlqocjswo1 +familymedicinepre +speeddog2 +wheeya88 +yoyokids1 +decorativearts +npaper1 +venus4197 +hairdays +gratomo2 +gratomo3 +heal2013 +s4freeintsunny +jaguster2 +enamoopackagefix +homesenc +dobero2 +marketingt +pangicare +farmforyou2 +farmforyou4 +changwon81 +mgk0416 +beatcool2 +htmlpre +jutoyjoy +fishingmetro +goeuropepre +humanrightspre +hhyy7773 +garfield1 +cabinlee +allin11 +beautyswan1 +ssyoon1 +kks19911 +altmedicine +charlestonpre +gocanadapre +compactiongamespre +birdspre +netboot +philosophypre +quilting +interactfiction +candleandsoap +prochoice +ravehousetechpre +buddhismpre +horrorfilmpre +menseroticapre +altmedia +7-12educators +worldfilm +javapre +lamaisontr +quincemore1 +maktub0070 +s2frelease +skanskan42 +powerboat +cgogol2 +ecard1tr +bomuljido2 +konimi1 +masami04 +xmlpre +dlehddls11222 +roleplaygames +decorativeartspre +eugenepre +goorlando +macsupportpre +gogreecepre +kidsnetgamespre +votechpre +investmentclubpre +sanantoniopre +dcpre +latinoculturepre +publishingpre +goireland +mdsuburbspre +mountainbikepre +amateurphotopre +womenserotica +heatwave +chineseculture +activetravel +japaneseculturepre +bluespre +altreligionpre +compreviews +asianamculture +crochetpre +startrekpre +knitting +weatherpre +indianculture +columbiascpre +altreligion +ophthalmologypre +flyfishingpre +panicdisorder +usgovinfopre +ussoccer +gosanfran +compnetworkingpre +duluthpre +backandneckpre +tallahasseepre +actionfigurespre +milwaukeepre +proicehockey +internetgamespre +neurosciencepre +yabookspre +movieboxoffice +s739 +memcached2 +napoleon +hollywoodmoviepre +internal1 +prochoicepre +lesbianerotica +ravehousetech +bk14 +palmtops +bbqpre +s736 +bk13 +broadcastnews +diabetespre +rw4 +bk12 +chattingpre +s734 +bk11 +s733 +goirelandpre +s731 +architecturepre +depressionpre +amateurerotica +s728 +s727 +s726 +usnewspaperspre +fantasyleagues +gogreece +s725 +guitarpre +authorspre +memcached1 +gogermany +legalindustrypre +webworst +scottishculturepre +tvcomedypre +gonycpre +s719 +s718 +proskatepre +ecotourism +taimurasghar +mena55 +thyroid +backandneck +kidsciencepre +s715 +quiltingpre +gotexaspre +7331 +s713 +s712 +personalwebpre +classictvpre +goodnews +allmychildrenpre +kyyong +kgyg2 +soundmtr5992 +skinhappy +base05 +base04 +windowspre +jewelrymakingpre +womenshealthpre +pascalpre +satellitepre +kurosawa +generalhospital +compositepre +eatingdisorderspre +spacepre +skitrips +geologypre +okx +puchillena +gotexas +wildflower +carinella +disabilities +hiro3 +simpsonspre +lesbianeroticapre +onelifetolive +icandy +asianamculturepre +countrymusicpre +pdpt +ve1 +airtravelpre +file02 +dancemusicpre +www.pdpt +www.ejournal +www.faperta +cartooningpre +mailing._domainkey.sunnynews +www.odp +preview.rcw +dev.rcw +qa.rcw +burlingtoniapre +dev.build +sunnynews +mailing._domainkey.info +palmspringspre +paintballpre +lesbianlife +techwritingpre +substanceabuse +celebrityerotic +macospre +kidscience +kidspenpals +desktopvideopre +stlouispre +denverpre +paintingpre +progressiverock +infertility +contestspre +globalbusinesspre +fb90 +fb120 +worldnewspaperspre +fb117 +watermark +aipre +burlingtonia +cdn8 +resourcespace +sarasotapre +fb99 +goaustraliapre +koonja9194 +baroma19 +baroma24 +skinleader +migosa +kim33003tr +jihomansan +goodtime243 +gorenaratr +jeincool +blueseaj +min2m2 +enjoymall +sammishin +iyyob +istmalltr +finelbs +coffeeallday +maofamily +okgood +marui8443 +s4intkthkira +casiobank +osesunkr2 +skyman2002 +ftsystar +oton22 +lds2007 +bankline +oneorzero9 +wonhyo81 +medicatr9575 +ahch37711 +rexsolbt +academy-010 +academy-011 +gorussia +fb92 +s634 +worldnewspapers +chineseculturepre +pregnancypre +collectpins +luga +balletdance +bakingpre +nashvillepre +almetevsk +novomoskovsk +pirateradiopre +otradnoe +huntingpre +womensbballpre +s627 +weaving +spanishpre +multiplespre +burlingtonvt +conspiraciespre +collectstamps +gorussiapre +herbsforhealthpre +energyindustry +puppylinux +tipster +s619 +businessmajorspre +wjddladn29871 +atheism +spares +fishingpre +xfilespre +gayerotica +detroitpre +homerecordingpre +pardus +westvillage +waterski +icdiijesus +yaehu71 +eh1025 +kms1992 +iloveimc +academy-020 +green119 +unjm6212 +puppia +victorssi1 +academy-027 +lovelyel2 +academy-029 +dpzhthf12 +prettyaha +cakefactory1 +jb9709 +gplus7400 +yufron +heellary +agrinagrine +rhrlgus1012 +this0718 +raffles1 +freshaquariumpre +gcsd33009ptn +blueprint0 +lovepipi +yongchil2 +abbinewyork +puppyp +heeyoung01262 +mr01000 +wonjin1 +ch11137 +gpal10141 +kimex +entratr7837 +zibig8115 +fs1190 +clubhada2 +bhinfo +baduncle +daon12tr5708 +koobart +qwehk7131 +kimchreom +nthmax +soonung11 +sonjh253 +hptc21 +myhappy10921 +godotechjsseo +candlehouse +nascarpre +soo111 +soo112 +xartcard +ds497910 +pushnpull1 +whtjdfo0216 +lkp1961 +unifittr9743 +clonezilla +goaustralia +s611 +netculture +hronline +travelwithkidspre +bicyclingpre +telecomindustrypre +panicdisorderpre +pittsburghpre +beekeepingpre +gohawaiipre +accountingpre +englishculture +homecookingpre +balletdancepre +parentingteens +homeelectronicpre +realestatepre +asthmapre +southernfood +economicspre +poetrypre +autoracingpre +ibm03.lsdf +libertarianism +nonfictionpre +ipe +politicalhumorpre +clevelandpre +marketingpre +fortmyerspre +nutritionpre +freebiespre +spaspre +weavingpre +folkmusicpre +www.pse +dallaspre +goeasteurope +autoconfig.donate +autodiscover.donate +webdisk.donate +burlingtonvtpre +compreviewspre +westernmapre +multiples +casinogamblingpre +gohawaii +worcesterpre +chicagonorth +gayeroticapre +www.archiv +lancasterpre +quitsmokingpre +classicalmusic +afroamculture +uspoliticspre +internetgames +localftp +managementpre +milf +roleplaygamespre +lupuspre +journalspre +babylon5pre +uspolitics +lowfatcooking +basketrypre +sickjokes +conspiracies +catloverspre +portlandor +dogspre +musicvideopre +netconferencepre +portlandme +romancefiction +buffalopre +govegaspre +www.travelinfo +travelinfo +womenshistorypre +s408 +beadworkpre +crosswords +investingcanada +bodybuildingpre +starfire +rcvehiclespre +s4361 +renotahoe +netculturepre +os2pre +etransport +protestantism +divorcesupportpre +chesspre +montgomerypre +mailmag +blog-test +s4359 +s4271 +candleandsoappre +englishlitpre +rowingpre +arcticculture +britishtv +deafnesspre +onlineshoppingpre +golapre +neworleanspre +musicianspre +coptv +sandiegopre +pediatricspre +1web +sailingpre +teenadvicepre +chronicfatigue +s260 +btc-dev +collegegradjobs +homeparentspre +rede +s4341 +www.ipad +protestantismpre +addpre +bigdata +horseracingpre +humorpre +s4331 +springfieldilpre +sewing +keyan +interiordec +s4329 +bowlingpre +beadwork +244 +houston3 +houston2 +goitalypre +232 +slave3 +slave5 +slave6 +centrum +bdsmpre +generalhospitalpre +vintagecarspre +admin.beta +talkshowspre +powerboatpre +yabooks +lvs02 +portlandorpre +mars1 +surfingpre +sbinformation +northbeach +gonewengland +4wheeldrive +s4262 +biologypre +s4314 +compsimgamespre +kidscollectingpre +energyindustrypre +homeschoolingpre +toycollectingpre +artistexchangepre +comicbookspre +s4309 +runningpre +gocaribbeanpre +inventors +businessmajors +chinesefoodpre +memcached +seniorliving +goswitzerland +collectdolls +windowsnt +s4304 +walkingpre +remotemail +www.epiphany +seniorhealthpre +www04 +newagepre +adoptionpre +couponingpre +phoenixpre +s4301 +northernnjpre +gradadmissions +childparentingpre +cruises +slike +wichita +israeliculturepre +visualbasicpre +collectdollspre +specialchildren +s4320 +santabarbarapre +telecomindustry +purchasingpre +gardeningpre +mst3kpre +sacramentopre +richard5 +coloradospringpre +spelletjes +midimusic +archaeologypre +agriculturepre +classicalmusicpre +racerelationspre +telecommuting +s4255 +germanpre +gosouthamericapre +s4269 +skapre +bofa +menserotica +wisuda +forestrypre +lore +womensgolfpre +brighton +heartdisease +goorlandopre +www.mma +goukpre +s4254 +gocanada +qa710proplus3 +kidsastronomypre +girlscouts +christianhumorpre +familyinternet +s4264 +healingpre +distancelearn +chicagowest +cocktailspre +teenexchange +usparkspre +tennispre +s4261 +catlovers +s4259 +childparenting +gradadmissionspre +lacrossepre +chinesefood +wyxy +gw02 +honolulupre +italianculture +ecommercepre +womenseroticapre +fitzgerald +canadanews +detroitsuburbspre +urbanlegendspre +port80 +themeparkspre +craftsforkidspre +cardgamespre +springfieldmopre +airtravel +collectminerals +s4251 +s4249 +arttechpre +businesstravelpre +kansascitypre +proicehockeypre +goitaly +tampapre +sewingpre +canadaonline +frugallivingpre +daycarepre +jazzpre +s4245 +hamptonroadspre +healthcarepre +www.ets +s4250 +geographypre +gogermanypre +prinz +gaylesissues +profootballpre +s4241 +detroitsuburbs +s4239 +historymedren +amateurphoto +s4238 +santacruzpre +couponing +sportscardspre +genealogypre +sportslegendspre +irvingpre +singleparents +springfieldil +writerexchange +s4234 +bowlinggreen +inventorspre +s4233 +qa.portal +trackandfieldpre +classicfilmpre +starfish +englishculturepre +colbasketballpre +springfieldmo +homeworkhelppre +s4231 +distancelearnpre +photoweb +s4229 +chemengineerpre +microsoftsoft +pharmacologypre +collegelifepre +landscapingpre +backpacking +weddingspre +graphicssoftpre +ireport +gosouthamerica +heavymetalpre +hype +peripherals +collectbookspre +datingpre +mev +usmilitarypre +internetradiopre +radiopre +tensyoku +comicbooks +spyware +historymedrenpre +mentalhealthpre +ns1b +gonyc +dc01 +marriagepre +homecooking +usnewspre +fantasytvpre +spanishculturepre +jewelrymaking +vgstrategies +ussoccerpre +substanceabusepre +ufospre +fantasypre +mcguide1 +mcguide2 +mcguide3 +ceoblog +trl +mcguide4 +seniorhealth +gosouthasiapre +womensgolf +personalweb +prm1 +martialartspre +internetradio +rodeopre +orangecountypre +austinpre +greenvillepre +humanresourcespre +kidswritingpre +miniaturespre +singleparentspre +seniorlivingpre +gocoloradopre +fotogaleri +englishlit +buddhism +columbusohpre +americanhistorypre +ftpacc1 +ftpacc2 +reenactment +s4244 +columbiasc +cricketpre +eastvillagepre +vpn-gw +needlepoint +backpackingpre +eastvillage +netsecuritypre +onelifetolivepre +gymnasticspre +classicfilm +telecommutingpre +germanculturepre +mexicanfoodpre +saltfishingpre +holocaustpre +writerexchangepre +craftsforkids +edmontonpre +roundup +s4209 +busycooks +mobilepre +kvm10 +kvm11 +kvm13 +kvm16 +civillibertypre +ancienthistory +personalcredit +sportsmedicinepre +prowrestlepre +k10 +fedo +s4204 +exoticpetspre +antiviruspre +netconference +s4203 +screenwriting +britishtheatre +barbiedolls +usnewspapers +chemengineer +kidscollecting +progressiverockpre +artforkidspre +produtos +govegas +prowrestle +stepparentingpre +womensbball +fatherhoodpre +japaneseculture +s4201 +kidsastronomy +midimusicpre +careerplanningpre +afroamculturepre +pirateradio +quotationspre +homerepair +usparks +graphicssoft +israeliculture +collegeappspre +fortlauderdale +somapre +s4187 +hollywoodpre +pfsense +bowlinggreenpre +frenchculturepre +altmusic +westvillagepre +collectpinspre +s4185 +campingpre +celebrityeroticpre +s4184 +ecologypre +s4240 +sv59 +cpluspre +harlempre +screenwritingpre +hollywoodmovie +anesthesiologypre +surgerypre +frenchculture +s4182 +kidsnetgames +socialworkpre +sv65 +sv64 +arcticculturepre +daysofourlives +prolifepre +s4181 +sv58 +sv57 +sv56 +longislandpre +s4179 +horsespre +canoepre +history1900spre +4wheeldrivepre +gofrance +machardware +paranormalpre +urbanlegends +exercisepre +webdesignpre +javascriptpre +judaismpre +sv28 +tvschedulespre +themeparks +augustaga +incestabusepre +divorcesupport +collegelife +sv21 +southernfoodpre +amateureroticapre +gosanfranpre +chicagonorthpre +beatlespre +catholicism +s4174 +cincinnatipre +talkshows +jobsearchtechpre +altmediapre +needlepointpre +costumejewels +s4171 +certificationpre +costumejewelspre +s4169 +mbk +p9 +barbiedollspre +jacksonvillepre +teenadvice +dataroom +cruisespre +center2 +spokanepre +www.newdesign +industrialmusic +tvschedules +ceramicspre +arthritispre +s4164 +rcvehicles +gojapan +coloradospring +toledopre +bismarckpre +freelancewrite +webworstpre +mathpre +vgstrategiespre +s4161 +boardgamespre +k-6educators +www.freedownload +sharewarepre +gomiami +bboy +vegetarianpre +cheesepre +houstonpre +baking +probasketballpre +japanesepre +gocaribbean +bandbpre +webdisk.affiliate +incestabuse +basketry +actionfigures +atlantapre +interactfictionpre +italianculturepre +rockclimbingpre +marktwainpre +birdingpre +treasurehuntpre +bostonsouth +automotivepre +gofrancepre +s4151 +s4149 +kidexchange +modelrailroad +birding +latinoculture +sexualitypre +hans2 +s4145 +www.senator +s4144 +tracking2 +tyche +feronia +www.chopin +05 +04 +s4141 +s4139 +raporty +appcenter +s1319 +goeasteuropepre +bronxpre +lowfatcookingpre +fictionpre +specialchildrenpre +animalrightspre +judaism +gaylife +knittingpre +christianteenspre +freelancewritepre +chronicfatiguepre +reenactmentpre +hamptonroads +artistexchange +s4131 +s4129 +politicalhumor +infertilitypre +s92 +weightlosspre +movieboxofficepre +gomiamipre +gocolorado +astrologypre +gaylesissuespre +budgettravel +women3rdworldpre +paganwiccan +woodworkingpre +gameshowspre +italianfoodpre +s4124 +louisvillepre +homeelectronic +desktoppub +busycookspre +gonewenglandpre +entrepreneurspre +sleepdisorderspre +southbendpre +collegeapps +annapolispre +babyparenting +geneticspre +industrialmusicpre +history1800spre +librarianspre +softballpre +sbinformationpre +sickjokespre +fantasytv +deafness +financeservicespre +webdisk.myspace +autodiscover.myspace +autoconfig.myspace +northbeachpre +bostonsouthpre +usgovinfo +s4111 +ancienthistorypre +krasnoyarsk6 +s4109 +worldmusicpre +privateschoolpre +amateurwrestlepre +optika +shareware +sportsgambling +golfpre +stockspre +80music +s1230 +classictv +dossier +labeltape +baisisi +ipia119 +cho01233 +rumebag +fourmis841 +sunilover1 +narabio +s4224 +magicpre +russianculture +allday +laflo +lecjohn +crow778 +abcbike +hades10 +cap1460 +priel0071 +legalindustry +hometime +synccitykr +isstore +hmsdb1 +dugni00 +bunnysugar +puredm +iris121 +iris122 +iris123 +mhrich +whitehotae1 +biznoble +cho01453 +cho01455 +rimelite2 +indankorea1 +denis1110 +parhae +enskorea +goswitzerlandpre +siruwon +bk5389 +lance1998 +miiino +coco515 +kensert2 +ogi0418 +invisual001ptn +kwp +iwebple +s4intextacy +nikstyle3 +nikstyle4 +fineway +jss33333 +smarket2 +cromy691 +alliums +doradoel +compsimgames +kkamu +darphin801 +katusa9507 +evan87 +spad11 +andynashley +ssing71 +lsd1982 +seomuho +cherrybox +magictonertr +undermalltr +tyta5000 +elle82531 +probasketball +yhm19991 +yhm19992 +allstory +spplus1 +tlsgur755 +wolfkickbox +logostaff +mcubei1 +mcubei2 +sr656310 +smcnftr +voyage71 +haustyletr +megasnc1 +zkdhtm65083 +velohouse +lee8dofnb1 +j201331 +lcs2 +barishoptr +frenchpre +woongnyu823 +jh209700 +duoback2 +ckh00224 +duru1004 +phji12301 +bipolarpre +diphoad +miinkr +dyingpre +delete1984 +araon6 +araon7 +ddays0404 +recycletown3tr3178 +newshinsa +jdc132003 +s3intb +kn1905 +s3intp +bournemouth7 +s3intw +welavkr +collectmineralspre +kindjay1 +samjungshoptr +m89718971 +jwcjyh1 +hwangss771 +sem06052 +bloody127 +bo2848 +assinaturas +algovital +honeymoonspre +surgery7 +surgery8 +automobilespre +jamesf9h1 +asylum781 +bisang3 +bisang4 +soji25 +rock4utr6807 +dream3821 +dream3822 +dream3824 +ipaybmhstar +myaqua1 +jinee4786 +infedo1 +ink8do2 +imshyeon3 +h93063 +wonjin91 +kwonstesets +mtmkorea +s2shop +icefeel +luxsketch1 +infeel4 +qort0107 +coqls1004 +aryunyewon +seokjoop +mikak1 +officeinside +mssi85801 +markman +lsh178 +z007007 +sulem12 +jw2389 +naranlm +kyungseo +lpcc2012 +djkim1 +induk11-001 +induk11-002 +induk11-003 +induk11-004 +induk11-005 +induk11-006 +induk11-007 +induk11-008 +induk11-009 +induk11-011 +induk11-012 +induk11-013 +induk11-014 +induk11-015 +induk11-016 +induk11-017 +induk11-018 +induk11-019 +induk11-021 +induk11-022 +britishtvpre +dongsajung +induk11-025 +induk11-026 +induk11-027 +induk11-028 +induk11-030 +induk11-031 +induk11-032 +induk11-033 +induk11-034 +induk11-035 +induk11-036 +induk11-037 +induk11-038 +induk11-039 +induk11-041 +induk11-042 +induk11-043 +induk11-044 +induk11-045 +tojongage2 +s2skin +mutualfundspre +sg2 +devgodobill +beadsborntr +stringpage +jiincnt +ohandee1 +nasungin3 +nasungin4 +eunsun0504 +oklee9687 +kis77jjj1 +badaone1 +soostore +eudirect2 +eudirect3 +eudirect5 +latti +lauricidin +crafthouse1 +medall12121 +pm21001 +hazel101 +wkqldus +s2patch +kbldmk1 +duddkek009 +sandol77 +hikaru1616 +bearbird1 +wsfeel +pbmarket +haeun95953 +billiardspool +hanjin500 +igosancokr3 +igosancokr4 +igosancokr5 +pusiul +korezontr +dawon6376 +cromyoung11 +cromyoung12 +nohmk741 +nohmk744 +s2fsrelease +appletrees +toytopdome +minsokmalltr +jy05071 +ksy4065 +iamjangme +eint5013 +naturenbio1 +sojium +gooddayskt +dhdsifl +meatpow +apples1 +yawarano1 +takeuns +haemosoo +skfkrhfem +snskin +morenvy009ptn +eun0107 +jhy6065 +d2k54677 +hyoreen1 +mir001 +phill012 +3680sj +slckorea +en2free81 +plusjean +mediheals +beerpre +hermin1004 +vldzmenddl +jobsearchpre +firstwood +nari522 +catletter +marioztr9728 +mmisuk1 +mmisuk2 +mmisuk3 +dlaaldo201 +ddingle3 +mikimh +design114 +wishpot88 +boxking +mjcafe +gonsen721 +innerweb2 +lassiette +delphipre +invisual003ptn +alicenart2 +alicenart3 +alicenart4 +alicenart5 +alicenart6 +alicenart7 +alicenart8 +alicenart9 +anytoker77 +azm3224 +dogsound +neukkim +ad4444 +qlxmftiq1 +sleepdisorders +coptvpre +jobsearchcanadapre +columbusoh +scottishculture +giraffe +jjh +retireplanpre +www.spc +7-12educatorspre +domaindnszones.spc.comp +samstag +skitripspre +history1700s +s4221 +forestdnszones.spc.comp +qlxmftiq2 +qlxmftiq3 +jjung1121 +mir276 +kmall +mjassa +collectbooks +madmoon1 +kman4045 +okkill +iroomceo +hwangtojung1 +hwangtojung2 +fissler2011 +bk9846061 +naneca1 +dhfandb +hphone3 +hphone4 +jopersie15 +hajunbb3291 +scubapre +webclipart +s4354 +allmychildren +novocherkassk +mp3403 +hikosen +jijigo123 +qnaqna +sjkfree +iceapple +mir438 +nokchawon +sd00281 +bluesis2 +csmaru +chlgks771 +chlgks772 +pcrainz +ynhkm84 +beautypre +ekdnjs2002133 +jirisanak +berry66001 +berry66002 +eumban +daejinmat2 +compense +thegull7 +hbcommtr6900 +oktopcoffee +mjceo2 +arrmani +diamanteun +a082010 +mai38317 +jijeong +jhsign +sooj8375271 +longevity1 +escrowtest +juellove +free2fly1 +free2fly2 +free2fly3 +okganji +eve1004 +divehq +elight10141 +caraudiodc +sh40261 +kimsony03192 +golfdctr +qvely8239 +inaba20021 +paulandj +cockkhn +yoonjooyul +l0uis81 +seasun1004 +plscompany1 +jp5-rm00000 +compactiongames +indianculturepre +familyinternetpre +dltpwls3621 +jvibe +all4batr2866 +xhprof +adsonlinepre +s4219 +crossstitchpre +cardgames +webdisk.a +vintagecars +votech +cgmedia2 +howon17671 +mmagpie2 +multials +nstory +ysj2930 +nuchi2 +sol8282 +linuxhosting51-51 +tubularr +ozkimjo +hdpn1 +s2pdevp +smartnuts +rkddlfo11 +rebois +horrorfilm +www.res +cyberarts1 +ddalgi5 +ddalgi6 +tony70 +jikyjeon28 +ddalgee +oitalia3 +activetravelpre +yahocamping +herostock +lkjk551 +sejin77071 +kimwoo76 +jejusambo +kmkm9 +nadaum +pluseksm +mimi76 +gotooutdoor +collegefootballpre +uckorea +nuenara2 +charmhtr6375 +zaltabike2 +hocorp +nabut2 +daidanv +subsubpark +nolboo1 +constructionpre +jutty +textmove +blackblanc +ulppang1 +pbmaul +tshot12 +jmoore +tttestt +vangquish +mobilekr1 +vbsoma8 +vbsoma9 +zeropack +rookie0907 +lionyoon +newbankda +lovetkt1 +urbook +0ms +ledok +enindi193 +carm1004 +siyeon1234 +luxbabara +momoiatr3079 +sarrah233 +yu04042 +escapolo +enindi195 +airzol +lexingtonpre +costcatr0911 +freeguyyck1 +ecotourismpre +michabella1 +dream6644 +adel751 +mind33 +s4devb +jomakorea1 +pgl10045 +s4devp +fgmall1 +s2pdevmcpark +dkrlehd +viazoe +sajubaksa9 +somani +woorimf881 +hairim77 +kimwood2 +max88121 +max88122 +enindi209 +sollae +cf5869 +enamoopackage +s086428 +hans9494 +looz781 +lhw01033 +bada66541 +mjpark872 +kohwasop +dsbkoreatr +danmoojy1 +gojack6062 +kwak73kb1 +autonomon1 +lcs15544 +lucas2005 +qwpp123 +kokopening1 +zzimkjh1 +clickoff1 +alekkim +cutesoli +dekung1 +k7251203 +jjakhs +jw5361 +gaongift +tea30402 +guk680404 +toyfun2 +jangan4934 +bmbob91 +disabilitiespre +koaid +woodrotr8451 +girlscoutspre +qwe912-009 +qwe912-011 +enindi219 +morenvy019ptn +istory1 +leed201 +hyuninter +hypermed +stishotr4379 +mahanpear +djmtb1 +trianni6 +trianni8 +rosa5042 +qwe912-020 +lhowook +say10111 +andrew71 +jwy53601 +zeropia3 +zeropia5 +kbncomputer-020 +sjaqua165 +dracula851 +dracula852 +labnshtr1375 +trensetter +hs301301 +marom10 +marom12 +marom13 +marom14 +marom15 +marom16 +marom17 +marom18 +marom20 +marom21 +marom22 +km78020 +smj9827 +marom29 +tbalance +damaflower +cbtk10041 +whxogml +dimpleskorea +knots100 +jungbrave +jjugly2 +s4freeintkhs +ooinjaoo +tnrud2006 +tnrud2008 +gaonfurn +arlsatang +lovetaiwan2 +firetornado +winiworks1 +winiworks2 +aljjaman23 +duatkdgns +kkddd791 +song41 +organza111 +toxshotr9837 +fashionpre +kentanos +inkcasting1 +mineta +love3cmtr +hongikav +sjkukuri1 +itspresent1 +designgj +aileen2006 +kogal +jbsim2000 +jja09girl +lepas +heo2000 +christianmusicpre +kheo77 +tscoffee +partycook1 +gmj09034 +creatitr4412 +canuslim +dom12346 +takuteru +classicrockpre +colbasketball +benettong +redmir +dhdusdk +ieciecieciec +lionyoon2 +outdoorsman +yhcompany +gadmin11 +jeju824513 +xc4284 +jeju824516 +oteem011 +bestgarden +syung2kko3 +kbs0006 +icd900tr6382 +cancerpre +graphicdesignpre +crimepre +onlinework +homerepairpre +adobe-serialsdb +canadaonlinepre +gameshows +chemistrypre +linuxpre +financeservices +anas123 +wilkesbarrepre +drawsketch +wargamespre +amateurwrestle +stare +broadcastnewspre +techwriting +specialedpre +nowe +ns1.m +travelwithkids +ns2.m +woodworking +www.deporte +aolpre +altmedicinepre +k-6educatorspre +rediffmail +philadelphiapre +freshaquarium +romancefictionpre +nonprofitpre +christianhumor +sportsgamblingpre +tvcomedy +baltimorepre +comunitate +seattlepre +aviationpre +80musicpre +webclipartpre +mms3 +insurancepre +herbsforhealth +windowsntpre +v29 +capecodpre +britishtheatrepre +starwarspre +smtpc +tedx +libertarianismpre +folkartpre +lesbianlifepre +desktopvideo +canadanewspre +homeparents +history1700spre +houstonnw +machardwarepre +history1800s +goeurope +perlpre +mdsuburbs +computerspre +artforkids +saltfishing +eslpre +scellius +fantasyleaguespre +iinet +s1219 +billiardspoolpre +drawsketchpre +dinkes +signet +bubo +daysofourlivespre +physicspre +access5 +access4 +s4214 +sportslegends +funclub +collegegradjobspre +bwnews +compnetworking +farmingpre +topsales +kidspenpalspre +russianculturepre +thyroidpre +mikan +renotahoepre +westernma +westpalmbeachpre +physio +micros2 +peripheralspre +gwsmtp09 +www.uganda +apai +221 +besnik +priority +farm1 +www.arthur +baileys +nazanin +taran +sb2 +dreamsoft +siatkowka +nakaf982 +pisa +mestre +slarti +dlw93-2 +dlw7-2 +dlw7-1 +dlw187-2 +dlw187-1 +dlw66-2 +dlw239-2 +dlw239-1 +dlw93-1 +vpn214 +dlw225-2 +vpn193 +dlw225-1 +emusic +dlw131-2 +dlw131-1 +dlw10-2 +pfs +dlw10-1 +s1216 +dlw85-2 +shanram +dlw20-2 +dlw85-1 +s4211 +dlw158-2 +goool +beatriz +dlw20-1 +dlw37-2 +dlw37-1 +dlw6-2 +dlw6-1 +dlw186-2 +dlw186-1 +dlw89-2 +optimum +emas +dlw65-2 +dlw65-1 +s4210 +westdale +dlw224-2 +dlw224-1 +dlw130-2 +dlw130-1 +dlw66-1 +dlw157-2 +dlw157-1 +idx +dlw36-2 +dlw36-1 +dlw202-2 +dlw5-2 +dlw5-1 +dlw202-1 +dlw87-1 +dlw185-2 +dlw185-1 +dlw63-1 +dlw64-2 +dlw64-1 +dlw223-2 +dlw223-1 +s.ext +dlw150-2 +dlw150-1 +dlw128-2 +dlw128-1 +dlw156-2 +dlw156-1 +dlw35-2 +dlw35-1 +dlw90-1 +dlw4-2 +dlw4-1 +dlw184-2 +dlw184-1 +dlw100-2 +dlw100-1 +dlw206-2 +dlw94-2 +dlw206-1 +dlw94-1 +dlw222-2 +bestbuy +dlw222-1 +dlw127-2 +dlw127-1 +partenaire +dlw249-2 +dlw249-1 +dlw230-2 +dlw229-1 +dlw155-2 +dlw155-1 +dlw34-2 +dlw34-1 +dlw3-2 +dlw3-1 +dlw183-2 +dlw183-1 +dlw75-2 +dlw75-1 +dlw62-2 +computing +dlw62-1 +dlw221-2 +dlw221-1 +dlw126-2 +cpi +dlw126-1 +dlw98-2 +dlw98-1 +dlw248-2 +dlw248-1 +dlw154-2 +dlw154-1 +dlw33-2 +dlw33-1 +cacos-m104-i55 +s4134 +dlw2-2 +dlw2-1 +dlw158-1 +dlw182-2 +dlw182-1 +dlw61-2 +dlw61-1 +dlw219-2 +dlw220-1 +dlw79-2 +dlw79-1 +s1210 +dlw125-2 +dlw125-1 +dlw247-2 +dlw247-1 +dlw153-2 +dlw153-1 +dlw139-2 +dlw199-2 +dlw140-1 +dlw32-2 +spamassassin +dlw32-1 +dlw1-2 +dlw1-1 +dlw181-2 +dlw181-1 +dlw59-2 +dlw59-1 +dlw199-1 +dlw218-2 +dlw218-1 +dlw80-2 +dlw124-2 +dlw124-1 +chapi +dlw83-1 +dlw246-2 +www.wcs +dlw246-1 +dlw84-2 +dlw80-1 +dlw84-1 +dlw152-2 +desknets +dlw152-1 +dlw31-2 +dlw31-1 +dlw180-2 +dlw180-1 +dlw220-2 +dlw219-1 +dlw58-2 +dlw58-1 +dlw217-2 +dlw217-1 +dlw123-2 +dlw123-1 +dlw245-2 +dlw245-1 +dlw151-2 +dlw151-1 +www.sharp +dlw30-2 +dlw30-1 +dlw201-2 +dlw88-2 +dlw201-1 +dlw88-1 +dlw178-2 +dlw178-1 +dlw57-2 +dlw57-1 +dlw90-2 +dlw216-2 +dlw216-1 +dlw122-2 +dlw122-1 +dlw244-2 +www.hyundai +dlw244-1 +dlw149-2 +dlw149-1 +dlw70-2 +dlw70-1 +dlw28-2 +dlw28-1 +dlw177-2 +dlw177-1 +dlw56-2 +dlw56-1 +dlw205-2 +dlw129-2 +dlw205-1 +dlw129-1 +dlw215-2 +dlw215-1 +dlw121-2 +dlw121-1 +dlw243-2 +dlw243-1 +dlw148-2 +dlw148-1 +dlw27-2 +dlw27-1 +dlw176-2 +dlw176-1 +dlw74-2 +dlw74-1 +dlw55-2 +dlw55-1 +dlw214-2 +dlw214-1 +dlw119-2 +dlw119-1 +dlw209-2 +dlw97-2 +dlw209-1 +s1204 +dlw97-1 +dlw242-2 +dlw242-1 +dlw147-2 +dlw147-1 +www.gdansk +dlw26-2 +dlw26-1 +dlw175-2 +dlw175-1 +www.radom +dlw54-2 +dlw54-1 +dlw213-2 +dlw213-1 +dlw78-2 +dlw78-1 +dlw118-2 +dlw118-1 +dlw241-2 +dlw241-1 +dlw146-2 +lodz +dlw146-1 +dlw25-2 +dlw25-1 +dlw174-2 +dlw174-1 +dlw53-2 +dlw53-1 +dlw60-2 +dlw212-2 +dlw212-1 +dlw117-2 +torgi +dlw117-1 +www.szczecinek +dlw240-2 +dlw240-1 +dlw120-2 +dlw120-1 +dlw145-2 +dlw145-1 +dlw24-2 +tychy +dlw24-1 +www.wisla +dlw173-2 +dlw173-1 +dlw52-2 +dlw52-1 +dlw211-2 +dlw211-1 +www.tychy +dlw116-2 +dlw116-1 +dlw238-2 +dlw238-1 +dlw144-2 +dlw144-1 +dlw23-2 +dlw23-1 +dlw190-2 +dlw87-2 +dlw189-1 +dlw172-2 +dlw172-1 +dlw51-2 +www.lublin +dlw51-1 +dlw210-2 +dlw210-1 +dlw45-2 +dlw115-2 +dlw115-1 +dlw89-1 +dlw237-2 +ronnie +dlw237-1 +dlw143-2 +dlw143-1 +dlw22-2 +s1470 +dlw22-1 +dlw171-2 +rybnik +dlw171-1 +dlw49-2 +dlw49-1 +dlw194-2 +dlw92-2 +dlw194-1 +dlw92-1 +dlw208-2 +dlw208-1 +dlw114-2 +dlw114-1 +dlw236-2 +dlw236-1 +dlw50-2 +gdynia +dlw142-2 +dlw142-1 +dlw21-2 +dlw21-1 +dlw169-2 +dlw169-1 +srem +dlw73-2 +dlw73-1 +dlw48-2 +www.srem +dlw48-1 +dlw60-1 +dlw197-2 +dlw197-1 +dlw113-2 +dlw113-1 +dlw198-2 +snom +dlw96-2 +dlw198-1 +dlw96-1 +dlw235-2 +dlw235-1 +dlw141-2 +sieradz +gdansk +dlw141-1 +dlw19-2 +dlw19-1 +deploy.dev +dlw168-2 +dlw168-1 +dlw83-2 +dlw47-2 +latte +dlw47-1 +dlw196-2 +dlw196-1 +dlw179-2 +dlw77-2 +dlw179-1 +dlw77-1 +dlw112-2 +dlw112-1 +dlw234-2 +dlw234-1 +dlw140-2 +dlw139-1 +dlw18-2 +dlw18-1 +dlw167-2 +dlw167-1 +dlw29-1 +dlw46-2 +dlw46-1 +dlw195-2 +dlw195-1 +dlw111-2 +dlw111-1 +dlw233-2 +dlw233-1 +dlw82-2 +dlw82-1 +dlw138-2 +dlw138-1 +dlw17-2 +dlw17-1 +dlw166-2 +dlw166-1 +s4180 +dlw40-2 +dlw40-1 +sulis +dlw45-1 +szczecinek +gchq +dlw204-2 +dlw204-1 +dlw110-2 +dlw110-1 +dlw63-2 +dlw232-2 +dlw232-1 +dlw137-2 +dlw137-1 +dlw16-2 +dlw16-1 +dlw86-2 +dlw86-1 +dlw165-2 +lublin +dlw165-1 +opole +dlw44-2 +dlw44-1 +dlw193-2 +dlw203-1 +dlw72-2 +dlw72-1 +dlw231-2 +dlw231-1 +dlw136-2 +dlw136-1 +dlw170-2 +dlw170-1 +www.gdynia +dlw15-2 +dlw15-1 +dlw164-2 +dlw164-1 +dlw43-2 +dlw43-1 +dlw203-2 +dlw91-2 +dlw193-1 +dlw91-1 +dlw192-2 +dlw192-1 +dlw71-2 +dlw71-1 +dlw229-2 +dlw230-1 +dlw135-2 +dlw135-1 +dlw14-2 +s4366 +dlw14-1 +dlw163-2 +dlw163-1 +dlw42-2 +dlw42-1 +dlw191-2 +dlw191-1 +dlw69-2 +winsp +dlw69-1 +dlw207-2 +dlw95-2 +dlw29-2 +dlw207-1 +dlw95-1 +dlw228-2 +dlw228-1 +dlw134-2 +dlw134-1 +dlw13-2 +dlw13-1 +dlw162-2 +ewinner +dlw162-1 +dlw50-1 +dlw41-2 +dlw41-1 +dlw9-2 +dlw9-1 +dlw200-2 +dlw189-2 +intranet-dev +dlw200-1 +static.base +dlw76-2 +dlw76-1 +dlw68-2 +dlw68-1 +dlw227-2 +dlw227-1 +so5 +s730 +15mof +dlw190-1 +www.mod +chiangmai +so0 +dlw133-2 +dlw133-1 +mazda3 +dlw99-2 +dlw99-1 +dlw12-2 +dlw12-1 +dlw161-2 +units +dlw161-1 +dlw39-2 +dlw39-1 +dlw160-2 +dlw8-2 +dlw8-1 +jboss +dlw160-1 +dlw188-2 +dlw188-1 +dlw67-2 +dlw67-1 +dlw226-2 +dlw226-1 +dlw81-2 +dlw81-1 +dlw132-2 +dlw132-1 +dlw11-2 +dlw11-1 +dlw159-2 +dlw159-1 +dlw38-2 +yukle +dlw38-1 +www.dialer +www.comsci +rack12u18 +space1 +joc +sss1 +www.noda +rack1u36 +gs01 +rack1u20 +rack1u18 +arkadas +rack1u13 +rack14u11 +rack11u36 +covenant +rack11u34 +yanshi +rack26u36 +ag2 +cmh +scrm +rack6u37 +rack6u32 +rack6u28 +rga +rack6u30 +x7 +www.xboxworld +vsc +www.borg +s4170 +www.mystery +datashare +sag +bassline +www.divinity +epsi +edata +www.mylinks +mux +s723 +apidemo +cfa2 +www.jeff +www.reese +vad +conlang +cappa +monitor5 +moshe +amnesty +v1p +inara +sheree +tomoko +emailb +www.brothersinarms +ganbat +www.fairtrade +trt +senni +cpaneltest +sik +www.afterdark +www.nsb +xboxworld +brothersinarms +equipe +spdev +hikvision +belvedere +sandrine +www.uwf +gamesonline +mickey1 +mickey3 +cvg +cyb +www.mmk +www.ikg +www.message +imihotel +piyush +a-math +nlt +falcone +s4114 +pradana +motors +sharepoint1 +event2 +favor +orderhost +masterword +weirdo +bestwestern +pringles +minigames +paczek +galilee +simpleman +temp04 +nyserver +www.ut +www.fourhorsemen +megatherion +parklands +hivi +www.portrait +thecoffeeshop +mywork +jordanian +serveur1 +filmer +phungbinh +myslam +www.vt +mitie +goodav +broad +speedsoft +www.logiciel +uwf +trg +tide +maa +tejendra +bradleys +ikg +idf +demigod +s4s +elin +afterdark +magik +bizzy +s717 +mikako +intersoft +stromboli +www.depot +www.moodle2 +esx10 +empregos +cnsrv1 +sauvegarde +lain +ocean3 +onsen +moderation +s716 +www.virus +askus +strg +s4101 +preview15 +hutchinson +tradewinds +scraps +precise +cicada +berita +mm9 +www.teszt1 +superfly +banweb +greentree +ikt +www.retail +night281 +oktatas +webapps-dev +cat-test +vision3630 +tabor +ipaynicekuma2 +ipaynicekuma3 +ipaynicekuma4 +sunwoogagu +kgobs3 +zoy4444 +dayluck1 +tong0430 +mandhome +teamlead +yeonribbon +spn +pitstop +s714 +sulphur +googlpis +bandofbrothers +springs +www.ipv4 +okaward3904 +mememy315 +dsjeong48 +s2freesetuptest +prepstest2 +prepstest +s4159 +mm7 +mm5 +mm3 +prepsprod +www.codered +shortener +res01 +res02 +hassane +hassana +res03 +projekti +res04 +antikvar +res13 +free10 +maktaba +chalk +aacc +kinetic +fe3 +s4364 +yejung5 +keohanpnf +waterfall +ad4 +sp2010 +metalgear +bizhongikuniv +mycej83 +dollarbill2 +pg4 +www.fight4fun +ishow +september1 +bo7317 +nezumi +ssorung2da +cmh5839 +xerox2 +meti +indis +www.phantom2 +lespo +falcon1 +minkmu +isuzu +sunrice85 +k34j98s +point8798 +yescm11112 +yescm11113 +yescm11114 +yescm11115 +wealthpop +gcsd33011ptn +irix503 +primectr6489 +everei +friendshair +iferratr6780 +areumi +ledhaus +gdero1 +apxmfh +kkh18743 +kkh18747 +iferrav4 +sonhak +budstory +sheecho65 +dyl070808 +kbs0426 +hubsmell +bookgreen +serverhosting254-39 +sonian +fins011 +rubicon +melody713 +whitehouse +chuldori +thefemme2 +kirin12123 +hairpltr7190 +junad2013 +asadal001ptn +piclove +refarm +itlife1 +itlife2 +itlife3 +itlife4 +itlife5 +itlife6 +minnot +reelas +buse +benjamin791 +ukkinjay2 +kibee +jinhs0217 +iferra +yangh1 +siruboon +sonjin +s1intsunny +capeasy7 +godo12099 +xzizi9841 +gibbmi88 +brood1000y3 +tnevivid +hellobee4 +hymnself1 +andria10 +andria12 +maumcompany +realusers +gofud892 +gofud893 +asadal034ptn +gofud896 +hyemin0602 +educut1 +yangju +cocobia +mandulgo2 +mandulgo3 +massagek1 +thtkdanss2 +thtkdanss4 +tong1210 +ifeva2 +vstatitr2400 +zayougrid +dptmfl1258 +kpgnh +chunsig75 +copyplus1 +soocia +qwqasa1 +qwqasa2 +eunpal01123 +cwtest +godo12294 +jakal203 +nanumatr5145 +ia2do742 +enkcorp +nineonetwo +kblue010 +cocodia +yanji4 +yanji5 +howsigntr1 +sb6700 +mp7161 +ibaekchun +jinne0205 +dosinongup +ieonet +jeoung252 +hw12341 +vier4d +hirondelle +smartself +bpcosmedi +qkrwhdals4 +ggomjilak9 +kfc0930 +wandobada1 +wandobada2 +kiki1443 +ipayno2345 +nyangi1 +goodnara1 +apccoree1 +wowmin723 +feelnatr6784 +jw7570 +eliecho +jabjll1 +oie +morefun011 +psy770706 +raraaqua1 +gooseyeo +knj07231 +parancorea +aki0000 +hy4512tr8230 +canari +sonpre +dindon +therich1234 +hubfarmtr +dbgnlwo1 +kkomakoala +gajafishing +highkickzny +sorantr4808 +runbio1 +nages3 +runbio3 +nages5 +whdgur23 +newcm2 +greenmoa +hogine +cho08181 +ajajbraj +charmhtr9651 +releases +gank +jongi2001 +dnjswjd52361 +queenseating +akbo241 +ywhdtjs +viprice +soonsoo6132 +lci0901 +vz5 +jjairan +zeroscho +esapyoung1 +henb1 +najjooni1 +whrnlska33 +sangpaemalltr +skynsnow +godo13211 +ykaa11021 +sora0311 +peter77 +bodorok +lee040804 +marrang +chm8004 +canavena +miyoun15 +sky2sun5 +greenpin +ipayfothkc1 +lhhgm +thaitantawan1 +ingang1 +lsm947 +ljy9296 +imypen +eprivacy +epreaching +matishop +greenpns +mkhouse +k0121017 +jks1914 +kismet2010 +hwa15381 +hwa15382 +cryout +regeni +bypo1234 +buylcdtr +cronus +yooriapa2 +il8540 +clxkclxk2 +yooriapa5 +clxkclxk4 +hellojungwoo +helloboy0 +georgelee1 +cocolux +greensam +syc +me2style +cgang129 +baberina1 +leean5103 +sebins5 +ariapp +ezer19312 +zmfhqk +jolieugly +dain130 +midiclick3 +aurore7moi +snhk20011 +snhk20012 +trophymall +smarttest +bigraon +ssjh7119 +hanbyul +k96389272 +k96389273 +cake1st +axigen +s4intnulbo +shoemania +gaggi113 +kali0083 +tdk3776 +ejobs +nowtuning +nun275 +s2pselfsetuptest +peterc1 +footztr1075 +csj627123 +delphi1 +circlepia +woojung115 +sullai1 +russelpark4 +sewinggirls +suyedang +manplus73 +summersun +northwind +traff +nava +imanweb +hot-live +pre-live-m +hot-edit +pre-live +edit-m +pre-edit-m +huecard +pre-edit +sonja +premium1 +boss0582 +namseung11 +english2 +bbserver +airwolf +diving +rys +bramka +hsn +www.basel +ap04 +obgyn +grilsexi +pal2 +harel +andhika +mar +trixie +courtney +vbrick +ssq +peekaboo +www.mody +offroad +laval +christina +si2 +tarjeta +ds5 +xiaoying +yacht +spiegel +sigem +ds12 +phantom2 +diplomacy +vespa +totti +s4160 +kosova +www.wptest +purekids +morse +norules +alwakil +atlas1 +jlab-tv +emailtest +mdmtest +eason +cristianoronaldo +sinan +jada +technews +gameclientapi +putty +sunil123 +zxcasd +ncf +mvh +mno +anya +modul +s4230 +kenna +www.teamlead +s4150 +btw +videoportal +yosri +dagong +mavis +liyan +vos +maili +lamxung +www.roundtable +kadin +tamburki +pegasus.cpd +ikram +qq123 +lessons +citi +b2kclan +www.sef +workbook +www.artemis +www.ios +gekko +local-www2 +upgrades +kalina +formular +pr0xy +www.outofcontrol +botox +relations +manuales +badcompany +novedades +altan +bigfishgames +www.torque +herbert +abc12 +huxley +schumi +www.cheers +readmore +ades +oldip +germania +happyfun +elpunto +znanie +fight4fun +codered +refinance +neckermann +www.testes +digitaltv +moneymaking +agent007 +renegate +futures +testhost +gretel +wert +gva +serials +tekk +sink +rips +okok +norm +kub +mdk +lenz +criminal +annika +exel +foam +fwtest +cino +selim +aral +bela +alt2 +www.b2kclan +rbw +mtw +theconstruct +phoenixx +intime +vien +4you +software4free +komputer +panthers +rao +www.sow +www.ege +www.dnd +ingenius +s4140 +wesley +nefertiti +morphy +pflege +www.theconstruct +matrix3 +toz +peka +goody +xyw +wwm +udt +ssw +titan2 +sef +pll +moh +s1476 +krb +koc +kmk +kfs +one60 +hsc +janna +nonstop +ede +bbu +rampage +java2 +grob +webdisk.mall +neal +autodiscover.mall +gung +autoconfig.mall +edutest +swa +chis +0verkill +autoconfig.php +s4136 +webdisk.php +autodiscover.php +ip23 +dorel +pgb +gbkh +kjy +ghweb +rdp2 +gokhan +fantasylife +webservices1 +samu +autoconfig.temp +webdisk.temp +autodiscover.temp +im7 +im6 +rfid +nsct +noon +fcss +kikuchi +arta +godel +vcma +zingosu6 +zingosu8 +faramir +eolo +lod +lince +okul +mailgw01 +www.bookmark +elattar +riptide +movie0 +wide2 +webdisk.responsive +kango +sooptr +sntrade1 +www.gorzow +munki1002 +gorzow +cocorex +mds0701 +kooji55 +janghang991 +jw8833 +papp +www.koszalin +koszalin +tmo +syn +lshdvs +sjanwhdk221 +parkhw771 +automotor +pinn +pdev +wangji9676 +eunhasul632 +elifepc +akarios +kitty816 +server252 +micoffee +sungyi4234 +finflix.videocdn +wi2 +bbs4 +eyes +alarm-r0150-0g-g10-visoralarm-01.security +www.gok +dianying +4d +mst-dc +holmium +alarm-r0150-0g-g10-visoralarm-02.security +econ01 +lanetli +gis5 +ns.cs +profkom +imx +sopro +fido +marenostrum +gis3 +gis4 +latin +ats2 +komputery +ekstranett +ocsav +s1469 +www.nc +dbo +gestio +cdnt +bizdirectory +pri7 +pri6 +pri5 +pri4 +pri3 +pri2 +s4220 +aquiles +ciberlynx-wsvn-web2 +emaila +newsproxy +engproxy +trial0330 +dl001 +mailmarketing +www.112 +resizer +www.mpr +s4130 +hainan +hole +omni-rcms1 +gel +beda +dns1outer +banner1 +kaixin +warsaw +mobistar +paloalto +diogenes +autoconfig.marketing +autodiscover.marketing +vns3 +meetingmaker +filesharing +jkt6 +cod2 +wvc +rachelle +subfinder +idm2 +garry +tapioca +sn1per +idm3 +jedi-en +veryold +bbdb-scan +sisdb-sc +ecmdb-sc +tsisdb-sc +tfmsdb-sc +oahu +fmsdb-sc +halfmoon +annu +eduphoria +webdisk.proyectos +host83 +host55 +out3 +out4 +out5 +out6 +iptest +ulisse +clamps +arrowhead +sith +icestorm +www.moi +wettbewerb +s4350 +technique +chemist +www.statistics +jobjob +jogja +plutone +authwireless +catest +s4154 +blackbaud +ipdb +autoconfig.wedding +webdisk.wedding +autodiscover.wedding +madcap +stei +alexandr +foxbat +www.constructii +thunderbolt +ip2001312196.ice +nmswas +easyjob +www.pesteri +horeca +cashcow +doverie +poni +new-life +mail.vita +joylife +pewdy +housejj +pescuit +overdose79 +perte +jimmyalice +www.horeca +qhtrjr0319 +wharkdgks +toggi +hans1502 +ksuk8787 +tjdgus2011 +thd0683 +zuhbhsd53 +brandgo +zzline +salezone +okmembers +ebebebeb +g6368 +yang0905 +pesteri +boyandro +futureyyh +lys42343 +conan101 +varam99 +aisarang +wjdxotjs +s4119 +okokjoa +susss +rkswl888 +ostory +nview +didhd1004 +kks240 +nochen +beius +sealeeyu +kamilshop +hosancom +nov3004 +monumente +okmyshop +omh11 +faline24 +jinejoa +mino8841 +uni486sk +sulan18kr +akflffls +missnyacc +northrsoft +tsports1 +heejin0339 +b.a2 +b.a1 +b.a0 +cjn2424 +nice10300 +janghuk2 +jssoft +jino3698 +ssipo1 +chanagini7 +rosia +elboy +grayjazz +morek0294 +rudgk08 +wnrmfodlg1 +hyojae1005 +neuestyle +no7rose +hem7229 +tqny +tdsbjs +ljs3133 +wildegle +dud02 +hellen0302 +mihye5575 +eydong486 +www.abram +msnrkcs +tnlvk1 +brg111 +clstyle21 +uh64 +bong333 +s4152 +runbeast +lsdbabo83 +mo1109 +geunho76 +sgr5641 +tryrex +www.stei +ydowne +langeriea +akb2000 +hoattakji +kkijnh6 +jangnan01 +tofurs +soul1221 +see630 +popoki +babaon +chhpig +projecttracker +oukzzang +tourisme +setsj2010 +han3608 +dadaworld +pianoon +kchol000 +newpinkboy +tjcss0 +tjcss8 +sncfelice +imgtj +imgtj2 +tjcss2 +tjcss3 +yulevip +tjcss6 +picup +image5 +tjcss +tjcss1 +tjcss4 +tjcss5 +tjcss7 +tjcss9 +fseason +schilling +laposte +webex +gorira07 +blaine +cytel +ehdans9426 +mypul +mbro271 +golligi +jjp2040 +rlawkdal +graceme +burney007 +s4110 +lh2dream +cncompany +hyunchol2 +harace55 +harace33 +wjddud523 +pipimo +sensgirl +puba +hw8714 +happyromi +ckffltiq +ymcm8585 +dalgwang +jkyo521 +mimi3799 +plusinside +guy2009 +s4368 +s4367 +ysgdvgs5 +s4365 +qowo83 +s4363 +s4362 +jingo8927 +yanhsgwg +s4358 +s4356 +yanhsgwa +boyandro001 +s4353 +s4352 +junpos +zzeng541 +s4348 +yogoyogo +s4346 +s4345 +clickjbl +s4343 +s4342 +kongyh +jakad11 +s4338 +s4337 +bebeheaven +dkdlfltm12 +loves11 +s4333 +s4332 +thdeockd +ohjung4 +s4328 +s4327 +s4326 +bsh5276 +ujhnsgdr13 +s4323 +s4322 +zziccoogo +vintageny +s4318 +s4317 +s4316 +hosted2 +s4315 +eyefun +rrnflrrnfl +s4313 +s4312 +alliebaby +harace2 +harace1 +s4308 +s4307 +gpwlswkd +mydv +ny90201812 +cui3545 +aion0501 +thrushine +b991228 +s4303 +s4302 +jss647 +zacava +wwcatw +clockzone +yj972 +noa114 +muse9 +eunsaem +zltkzltk +s4273 +s4272 +oyi502 +o2mall +hi4363 +s4268 +s4267 +s4266 +kamzi800 +s4265 +jhjh012486 +dbmart1 +guzezzang001 +sealeeyu001 +ppp-11 +ppp-10 +jjmobil +s4263 +allenhan +hanafood +nammaecom +s4258 +f14okppp +s4257 +s4256 +kiper0119 +polo21c +wnrmfodlg +s4253 +aeun1009 +s4252 +shoeseller +themestory +s4248 +s4247 +s4246 +rhan12 +juliana +algeriano +ruach +s4243 +fitgam +japet +avokato +ryu6058 +studyphp +shinyo123 +innerstyle +s4237 +s4236 +s4235 +kimjk1191 +marado11 +s4232 +jinan4749 +kney1018 +s4228 +s4227 +skp5969 +kingmade +s4226 +s4225 +sendy77 +s4223 +blair1 +s4222 +ttff1030 +banana2 +run2run7001 +s4218 +s4217 +sioor +kk9999 +gsmom100 +s4216 +s4215 +onlharu +s4213 +s4212 +coco4652 +akacom +paypal1 +lovely1st001 +s4208 +irusy08 +filmtour +s4207 +s4206 +s4205 +mcmin92 +jm2k +p0725 +s4202 +jodongam +cjiyeong +wjk0529 +ilovez +s4188 +jong617 +j007962 +s4186 +mpopov +samiros +dosa3377 +qjnybkesz +shadowgold +b2b2 +qjnybkesk +tlsstory +s4183 +soumya +phernand +kk7935 +coolkids +lys9153000 +duhokfrm +ljh0217 +beeho3654 +ocstest +s4178 +adamantium +yanamanhup +condom +giga220 +s4177 +s4176 +s4175 +kludge +monit2 +tomyself +s4173 +s4172 +pau +daheeya13 +wowow78 +olntydbsrw +olntydbsrg +s4168 +s4167 +s4166 +s4165 +kikibox +fuscata +plz +s4163 +www.your +www.anywhere +s4162 +rlaghwls +sorajm +kanghun789 +s4158 +s4157 +vatenna +s4156 +s4155 +keese4 +renard +s4153 +ggrjuh1 +knan405 +parisapple +s4148 +s4147 +c101 +sgt783 +wjdduqdl12 +mudeapo7 +s4146 +lg6014 +gm77 +magictimes +aubade003 +karakoram +aubade002 +f200 +kidsksmxg +rang99 +riyaz +sdavis +qookace +ccc333 +sechuna +s4143 +s4142 +kimmigogog +abdu +pcht2901 +aber +s4138 +s4137 +gosu81 +dlrlals3 +s4135 +annsnamu +s4133 +joypsp +mac3d +daelimfood +jks2661 +changttr5949 +suntechdnc1 +suntechdnc2 +kimjezara +alekkim1 +kidscltr9222 +dipopo1 +dipopo2 +zzang79121 +sung27113 +sung27114 +lms0913 +elinfit003ptn +qhfka767 +itfactory +total7004 +rudgml56541 +rookie11 +wevestyle +joytac +fleury00 +joyti1 +robo1142 +s541129 +thdworms02021 +medisale +heehee6375tr +pcm9x1 +cocowa2 +ice979001 +jh8006202 +mcc6931 +hsc80442 +tjplus2dnob1 +choccolato +sera4j2 +kmj19601 +doguebox +engdevadmin2 +d61573 +hwajin72423 +anpabak +okok1428571 +star918 +yeonsung-010 +clever338 +njoypp1 +goededag1 +k1j1k071 +shinjichoi +yeonsung-015 +designbar +zerotest +ichoco3tr +picone2 +picone4 +yeonsung-020 +picone6 +picone7 +picone8 +yeonsung-021 +yeonsung-023 +s4132 +bird12311468 +lse0918 +thevillage +imggirl +amotion3 +audiencekorea +cocoyaw +marseme +sj6305021 +sj6305022 +mukuge +adem +koream79 +s4128 +adie +fleader +s4127 +ggro903 +orkutadmin +s4126 +haha0503 +agha +ahca +s4125 +artemis.cpd +mmo20 +ahly +s4123 +s4122 +ahoo +iqmart +sytvfc53 +nicole-screensaver +tjtls11 +s4118 +s4117 +s4116 +s4115 +jsndkerx74 +s4113 +rd01 +wlsdud0739 +s4112 +dataebank +modelsuk +s4108 +s4107 +s4106 +pks1279 +ban1 +s4105 +wegoshop +ddolggoo +s4103 +s4102 +yuran07 +newweapon +asia0416 +yeonsung-040 +ceratec1 +ceratec4 +mykang77 +bahy +ukctr001 +pkd0911 +hjyco +balu +bbmotors35 +wkaehfl +mini312 +twinz2 +ifthen +wlmuhebsdf +s4351 +s4349 +roy815 +eurostar9 +ceratec5 +ansgmlwns3 +dkf89701 +designskin1 +designskin2 +woriro +designskin3 +youprint +uni2399 +khhodu +yeonsung-050 +ericflower1 +bukseorak +sotye0109 +yeonsung-054 +wemako1 +greendust2 +yeonsung-059 +enfree150 +soji4148 +barr +enfree151 +enfree152 +enfree153 +pointed +legiocasa00 +yeonsung-069 +albi +aldi +qnsghde +tlrkfh +alek +alen +theplace001 +bayu +bambish +hddvdent +naru52 +kimgoon002 +london2008 +yeonsung-071 +finedeal +yeonsung-072 +yeonsung-073 +kyj01235 +alli +jingaone +minissuki1 +hyeok111 +hitro +acasiaaca +isisshop +ddrgx541q +mtshoes +stylenam +webmachine +pp0725 +inha212 +mykidsmall +kakaroka +f14okpp +bodria1 +bodria2 +spo119 +linuxhosting229 +linuxhosting231 +yeonsung-080 +insungtr5197 +yeonsung-081 +goldcarttr +yeonsung-082 +only52461 +ecox3739 +only52465 +only52466 +only52467 +only52468 +lsy1227 +hananim415 +mgraphy1 +mgraphy2 +ammu +stupa +qkrehdgml +s1465 +anja +anik +creed0606 +clamkorea +people9 +kp1012 +s1464 +nuguri100 +a327751 +eyeshape +finemart +linuxhosting239 +designclan003ptn +linuxhosting241 +spiao1 +yeonsung-091 +ebingo +yeonsung-093 +like1539 +designgj1 +dodo66991 +wj22741 +dblglobal1 +aurorakorea1 +burnoaa1 +cloudcorp +boardpan4 +vincentmani +choi60232 +mjin89 +whejs88 +kensingtonkorea +kswieyjkk +bookdang +rornwkddl +fegerri +mong123 +allsize +euroco +uniquecamp +msc9870 +hiya888 +begoddess +fun64601 +adfasdf +misuk5282 +snikystyle4 +wjyou0818 +lsp123 +s2pintp +borncompany1 +eurom4 +ajfxlxhr +rwakeman +hsyoon75 +leeark4 +hok04162 +jynistyle5 +qpswl75 +queenslook +narinim +scole4874 +geagea +jkksports +pantsbear +madollkr +jmtkdtk +katechoi8 +icafetekno +venuskwon +paxvobis0 +dhforhd +negasl6721 +hieva +s1434 +wjdsladl11 +jgms38317 +designtr7238 +choah +sd07081 +sd07082 +antz +ktkang1 +h1n1 +chlwk +fbwocks +gagamelxd +metavoxtr +jin03130 +comictone +os1101 +ykm20051 +ziinjjb66 +babytoto09 +omin881 +darknulbo4 +omin884 +darknulbo6 +barbiein +johculture +birdmarine +designlak +hsblue1 +purpletopaz3 +eaw +wellage +whippingc1 +promaltr6451 +cuz +mrsinabro +sorexi +cstamp +kookis1 +bnbglobal +wellbag +shinkee +lovelydeco2 +godosoft-007 +mh402 +theshopsw +godosoft-010 +sunwoo11 +aqua79 +twoco4ever3 +mami2 +doichangfarm +ririringeu +qqhwk +sweetfox513 +dendy2002 +scmwoo2 +scarlet2193 +yardin +godosoft-020 +todvhfl +nownow801 +godo15782 +pro25443 +dralkaitis1 +dralkaitis2 +ongame951 +ongame952 +celeryang +aznymohc003ptn +murrin3 +in4mall8 +godosoft-030 +cjy8232 +actgagu4 +actgagu6 +actgagu8 +chipmunk1 +protootr9743 +bokgily +sansotank +godosoft-040 +ksu12 +interhard +herra1124 +freebilly1 +ktl33 +wj23651 +overmimo +thejamie +s4intb +godosoft-050 +godo16072 +s4intp +salirery +cwsports1 +hayfine +godosoft-054 +godo16133 +rangin2 +zino1513 +y4utr1891 +promisej +missjjtr1425 +musiccoach +drmuscle2 +envylook1 +cubeqa +cueplan21 +amavilis +ladycode +adl3910 +azz21362 +chulho975 +kimujuok82 +leeborn +miz011 +jamesjeon +lixxi +keepsafe001ptn +trioutlet +rusidnew +goodoong +minamine01 +ikonet +kimyune +cjw001 +aref +eptnbhqmwfgvp +s1c2k3 +wizhomme1 +arin +bichnada +muzzima +leeys1123 +rlaqhrtjs +biju +dmsghk419 +bimi +thiskim776 +happy8841 +pmillion +cdc1 +toyplay +jflove5 +dnss +intromall +asem86 +return0610 +eksmcokr +heeja +hottime123 +cafebogner +oknnko11 +keun0912 +w3w +webcg +jilaldance +dress79 +stting +huni0906 +phy1771 +mlstory +wewe +s1429 +tjsxo20 +ahs234 +godqhr7755 +an520610 +law321444 +hai486 +ron7856 +dudfks33 +kdk5428 +khn1212002 +khn1212001 +tjdgmlrla +withtns +passion020 +s1430 +rhrhkdvlf +s1428 +oozzoo +designsky +trg486 +ruru +yea0317 +allclock +kingchoon +ktfnh +gelios +fortest +hjkh23 +gb890387 +pczoom001 +lhseok +org333 +hamji +judasoli +bestaym +kt1523 +hong5075 +s1420 +findlight +tyty +wlmuhebst +nahyenmom +mintcream +megaeunjoo +toymall +myshirts +k5227497 +k5227494 +krnaite +planwiz +reclama +s1471 +glamgirls +bluesky556 +chlwkzizi +lss9775 +lhsij +lesson +dc1114 +soosyy +mizzleone +shns551 +ladybear +cultmania +robo0672 +solo1214 +ypop77 +fakesmile +yms7474 +miggu77 +zwolx8673 +hunlee77 +friedegg +nam479 +tnsaldl12 +knight7667 +seon12 +kimjuok82 +hjm705 +tjdtnr64 +stockplaza +kim831017 +reemax +dodo2011 +raya144 +qaz4745 +ijnjdf78 +syl9709 +s1410 +gusdk8318 +koko6 +redox1 +cocoru99 +doogy7 +ilove6155 +rudal65 +webdisk.honduras +kogun +toppingkr +s1405 +mintcream001 +bdk +nihao +s4k1na +lty5959 +alsxor84 +autoconfig.radios +fable123 +cutetiti +cheonjia +autoconfig.honduras +s1402 +whdbsgml +minisign +maple417 +autodiscover.radios +solarpower +webdisk.radios +s1401 +selt22 +ehdans512 +rhone +gajisam +jdsfndh62 +jlcorp +alsgh4860 +ninahiyoko +ryuyoung7 +tuningshop +obzor +amkdh +basri0310 +jang829 +departures +autodiscover.honduras +realgamjao +cocoii +kdlwlsl +wild33 +vuisnjhxy +boomdiby +guzezzang +s4347 +zbjkim +ngbluaknsb +lovemekso +kkrtg +elboys +jgcbwm +issc +ssss21 +gugigigi +umewede +tjrwls5 +mkgallery +ctphilos +bluesoul34 +nixspy75 +www.consulting +baehongbum +image7777 +web499 +naviyaa +bone +wjdtnsl08 +aze1 +towclock +gengioh +hvfire +pjy12 +azer +azha +wcode78 +khs4341 +autest +s1371 +ukctr +clicks.runews +mihuij +irani +cimille79 +kkitime +ffrock +bwabwatv +gpdjsdl1 +chor +s4121 +portalpms +web109 +yourajoa +onoffsale +godehak +dais +foka +dane +bsnl +harang09 +ps0429 +shlvmj6 +dks8504 +kidsksmx +kimjovi +daya +jayholic +lhh2121 +nilufar413 +web1b +wsxedc +artweb +yns8645 +patrol +deny +race4000 +seriat001 +sweetglam +cole +zixvpm02 +dewi +topkki +birth0531 +cmj0410 +web64 +bliss220 +wjdtldyd +tifac +jinihome +dipu +gksltkdtk +lnybksrbz +lnybksrbh +focuspc +toonis +sexytoday +min304 +alflspwjd +dawoud207 +coolbuddy007 +bodyfriend +mfcsg +sspama +shyh22 +wj6838 +joug200 +rlwmd78 +gilmall +chp +jeonjisun +jb2110010 +kkoobi +realgamja +dacha +autoconfig.hr +autodiscover.hr +autodiscover.foto +autoconfig.foto +sreverse +mailadmin9 +coe081 +jiny8282 +sdoduk +www.tvconectada +tvconectada +tba +gmulco +key9614 +api-qa +ticketsystem +api-prod +a.test +ids01 +evilium +tmpsolutions +ckmina80 +rkddball +hotzzang +ljhljhhh +ftpm +forcar77 +s4339 +rtdata +sigweb +mikee +ailos +sus01 +egy1 +serpens +week +devww +sicoa +metricapublicidad +virt4 +enterprise2 +uploadftp +thaonguyen +webdirect +gfa +ludmila +sektor +viktoriya +www.traduceri +www.imobiliare +drem +kron +soki +gintaras +joyful +goodday +peaceful +carti +takahara +smbhostverw +referate +vinch +www.concurs +adg +lisin +strela +www.felicitari +www.referate +mail.biz +lek +www.retete +lev +supershop +daneshnet +traduceri +bullseye +subtitrari +nhm +rum +qlife +ftp.med +relevant +solution1 +felicitari +publicitate +phytotherapy +nataliya +wecan +ftp.sport +masterpiece +xoops +smbhost +glorious +mail.sport +aas +web430 +izhevsk13 +ftp.love +matter +aniz +mirny +superstars +fana +sampo +ability +crum +bulan +mystar +yaroslav +freeshop +sergeeva +minhhai +caring +nubian +tsh +vitamin +vira +taim +shiseido +lora +gesund +luch +runet +neopro +nira +vipnet +bewell +yesman +saludybelleza +valentineflowers +kio +sharper +web94 +tinman +ftp.job +ftp.pro +richlife +allnatural +www.u3 +autoconfig.stream +transaction +autodiscover.stream +mail.look +millen +tr3 +sats +perspective +grande +vipclub +autodiscover.tech +autodiscover.tutorial +webdisk.tech +autoconfig.tutorial +autoconfig.tech +lad +rioweb +flyer +rhodium +inlife +ww20 +www.angajari +angajari +mail.web +splendid +ns4-2 +ns4-1 +worker1 +s1220 +it01 +dbmail +entry3 +entry2 +longlife +nakagawa +sunlife +box8 +box7 +webdisk.designer +autodiscover.designer +autoconfig.designer +box6 +s1334 +szg +nomad2 +seimgex +mail02.gr +health-beauty +readme +webmail01.gr +pop01.gr +seieumg +saiwmng.is +sefcmg +partnerships +smtp01.gr +www.cameras +2ch +nietzsche +blondi +videos2 +notar +table +emperors +cdb2 +pubsub.jabber +media02 +server-1 +datacenter1.cesantia +orcus +datacenter1.solicitarclave +datacenter1.contingencia +knet +tartarus +aandp +avtest +pool-node-tr +hei +kmt +vpn9 +215 +skm +webpower +213 +ecourses +eurynome +196 +rtest +187 +atlas2 +www.auth +157 +kch +kof +pothos +145 +143 +141 +tadpole +enfer +190 +movies3 +www.uslugi +luminis +xtra +elie +autoconfig.pt +autodiscover.pt +tutoring +zrenjanin +cornelius +sticker +addm +www.sklep2 +ues +huonglinh +interstate +ipcamera +assd +hma +nonlimit +raps +dashboards +onlineco +signon +campusparty +whee +farwest +settlement +scrum +us.mobile +www.kumquat +vicon +orange2 +vigor +dwar +emre +dwin +feli +kebo +mmb +teamxtreme +netapp2 +ffff +kolab +suburban +ukki +ambassador +www.ent +peta +caracal +dyaa +c203 +kenny1 +webdisk.italy +util3 +cua2 +gamma1 +autodiscover.italy +mondeo +autoconfig.italy +www.spain +socialize +socialite +sanskrit +btk +dominator +validmail +tritone +freeinfo +referent +pix-outside +vpn-gate +amalthee +testias +utilscpo +planningweb +announcement +webdisk.rent +pc23 +www.renewal +realkey +cloud7 +www.reb +pgmi +tarbiyah +syariah +ie7 +docomo +www.domain2 +www.tgp +thevoice +erez +swl +localhost.live +www.plaza +bataysk +odincovo +www.kultura +www.67 +www.lm +www.nv +www.ekb +www.paradise +www.smr +trsc +skytest +webdisk.coins +sedi +autoconfig.coins +m.blog +ppid +autodiscover.coins +province +ipphones +atp +www.avrora +www.kurs +www.bat +www.moskva +www.tx +name2 +hildegard +ldap00 +sps1 +minecraft2 +caprica +skylab +tuyenct4 +wsd1 +chimaera +reliant +aceit +www-temp +www.arl +newsflash +laulima +bnm +www.course +ikebana +disconnect +synergist +previous +searchtest +bunbury +manav +ir2 +dl101 +1970 +9706 +interest +buildserver +www.jupiter +www.laguna +springfield +sitim +www.na +www.mysports +hanover +www.newhaven +apigold +melville +sendsms +www.gh +www.metro +yuanwei +www.tsw +etis +ogrody +youngstown +www.richmond +ftworth +restoran +www.myname +anirban +www.wall +www.vist +ware +northampton +denden +orf +princessworld +westchester +www.anderson +www.ssdd +www.saif +www.safa +janz +waldorf +freegroup +salisbury +www.noor +kanoon +www.brandon +moviegalls5 +anhngoc +duman +gonzales +www.nurse +velo +elect +www.lex +minhnguyen +tacoma +brentwood +rockville +uhura +sepp +www.hehe +moviegalls4 +moviegalls3 +fypproject +moviegalls2 +venera +www.butler +onur +kramer +moviegalls1 +www.cube +oguz +shareit +xlsx +www.amin +yap +hikmet +viruswall +testmoodle +suzy +essa +bahman +split +volunteers +mneme +maindb +pingvin +eses +bct2 +caixapreta +davood +exciton +wienaz +www.taobao +tock +webpub +www.amoozesh +lundbeck +bore +www.nts +essi +www.hussein +artbank +www.mgt +en.service +thienphong +stump +calci +wwwv +www.danesh +maomao +pibid +www.darkstar +mymaster +hungarian +automoto +farina +manhhung +mysports +meadmin +server62 +agahi +vichy +m31t +server66 +ezp +nas5 +luminary +rbl1 +thiru +konrad +s1235 +rbl2 +hawaiian +asse +inscricao +www.zamani +www.noclegi +www.2009 +noclegi +sahin +www.1111 +s1231 +rdv +s1229 +ppd +www.sunset +www.newton +twisted +www.conquest +glados +fansite +www.vpp +ssdd +wouter +benoit +ermine +www.ssa +vsphere +nguyenvanquynh +instruction +tien +www.gbp +www.env +www.elc +magictrick +tage +www.fatih +jaipur +www.friendship +www.ehsan +geni +genclik +mersin +www.testwebsite +hoangan +www.esra +potc +gjxy +vampir +desigo +reservaciones +vbnet +energetik +gast020f +semih +www.soa +pranav +luana +computer1 +mytestpage +bingo2 +executive +www.anand +cormack +www.turan +wrapper +sorrow +www.million +intranet-new +sb3 +suzuka +ibo +sn1 +m1m2 +maserati +flu +dtv +egl +rtrb040 +csx +dns101 +aam +www.pdl +www.ahmad +www.serv +dns118 +mib +web2013 +pancha +www.host1 +159 +kitap +osmanli +newbox +webdisk.host1 +barracuda.test +mamiweb +zsw +rec.messagerie +dev2.messagerie +eid +pp.messagerie +messagerie9 +rec2.messagerie +www.ims +www.results +www.result +ftp.upload +xinwen +bast +s1215 +#smtp +checking +picture1 +wilco +gorgona +middleeast +s1214 +kurumi +wsd +tane +yss +misr +eudoxus +www.britneyspears +zircon +beegees +optusnet +mx.staging +chihaya +shira +www.pinkfloyd +nurse1 +www.celine +rosehip +asama +lennon +shigeru +www.elvis +gnr +fsfc +awstat +rollingstones +ironside +kczx +www.u2 +s1209 +www.who +publicdns +webscan +www.gunsnroses +pagseguro +vdm +azaan +dkr +smtp140 +s1206 +dark-net +ip-156 +ip-150 +gita +s1205 +warda +smtp148 +informacje +smtp142 +smtp132 +ip-143 +hany +autodiscover.demo2 +autoconfig.demo2 +webdisk.demo2 +ip-136 +ip-130 +wksta2 +tstb056c +tstb056b +tstb056a +ip-56 +ip-49 +tine +tstb008c +tstb008b +tstb008a +ip-43 +expertiza +ip-29 +libmail +ip-23 +e-mailing +www.robo +suzanne +ip-16 +ip-155 +ip-148 +grok +s1201 +ip-142 +ip-135 +ip-159 +ip-62 +ip-55 +ip-149 +pc09 +pc07 +web63 +ip-48 +pc04 +web61 +web60 +ip-42 +ip-35 +hasu +paket +ip-154 +ip-141 +ip-134 +ip-61 +luce +ip-54 +autodiscover.in +webdisk.in +autoconfig.in +ip-34 +leen +ip-27 +ip-21 +ip-160 +ip-153 +ip-146 +temptest +ip-59 +grus +vols +ip-53 +ip-39 +ip-33 +ioannis +gmac +bagdad +remotedesktop +ip-158 +lutz +ip-152 +ip-145 +ip-57 +ip-50 +ip-58 +ip-52 +ip-25 +ddavis +www.preprod +ip-18 +ip-12 +gcms +dialin10 +s4270 +ip-40 +ip-30 +ip-157 +ip-28 +m7md +ip-144 +ip-137 +ip-51 +beta.mobile +ip-44 +autoconfig.ws +ecommerce2 +autodiscover.ws +ipartner +ip-17 +ip-11 +stream8 +blub +clarice +plotter +colu +rohini +directi +elb +basi +autoconfig.cms +autodiscover.cms +frontend2 +creasyst +nayami +shunwa +s530 +the-portal +jmpc +cwimedia1 +west01 +www.ino +yui +nanoha +dialin12 +dialin11 +dialin09 +dialin08 +dialin07 +dialin06 +dialin05 +dialin04 +dialin03 +dialin02 +dialin01 +colorp +receiver +haendler +vindhya +dbg +ftpapps +ss5 +net.aa2 +thyme +czt +rwanda +chandrashekhar +mdata +tulip.crsc +allseason.net +chetana +spring.net +ct095.eng +season.net +ccweb +zygo +compact +nb2 +autumn.net +car13.sci +inter01.rector +car21.elec +csr21.eng +car21.eng +winter.net +summer.net +inter02.rector +csr41.rector +alexr +testvis +car31.eng +car10.net +inter03.rector +csr41.eng +elec1.elec +millenium.crsc +daa1 +joshkar-ola +sergiev-posad +www.blagoveshensk +vaidya +rekha +www.pskov +vtk +solidrock +j210 +niobium +mssql8 +mssql9 +gameservers +rupali +phosphorus +2u +kritika +nilgiri +partnersw.ftp +biomed +nvision +poezd +sound11 +sound1 +partners2 +kurort +www.seotools +r.mail +indium +realestate.dev +ftpau +sftp1 +vmi +smsadmin +kollwitz +stephani +lgc +files8 +files6 +webreports +sinclair +www.desire +dev05 +iklansemua +dev08 +sigmanu +dev11 +dev12 +dev14 +www.garant +oldintranet +test60 +dev15 +test44 +test37 +nouvelle +netsky +fyzg +test06 +mikul +labstats +ibda +mnet +icah +netgen +www.fai +enemy +w18 +manager1 +test-client +piacha +angebote +hondacity +node22 +devcontrol +guto +free4all +zwalm +winweb02 +mail.exchange +daniele +hoho +oblomov +wana +nation +mx.www +ibi +avconf +eveready +dookie +democrm +hopa +more2 +adimg1 +infuse +ey +pem +dialup-30 +informe +www.turizm +c-00 +dialup-68 +dialup-69 +dialup-79 +dialup-88 +dialup-89 +torg +trout +dialup-20 +dialup-21 +dialup-22 +dialup-23 +dialup-24 +dialup-25 +dialup-26 +dialup-27 +dialup-28 +dialup-29 +dialup-31 +selfserv +devsql +easyweb +dns.cs +altus +winweb03 +devlinux +vc8 +flv4 +playm +flavius +dev123 +made +spmexp-clu-01 +pingifes +s323 +s327 +s328 +s510 +s4360 +s1129 +s511 +s512 +s514 +s515 +s516 +s517 +s518 +dialup-64 +dialup-65 +dialup-66 +dialup-67 +dialup-70 +dialup-71 +dialup-72 +dialup-73 +dialup-74 +dialup-75 +dialup-76 +dialup-77 +dialup-78 +dialup-80 +dialup-81 +dialup-82 +dialup-83 +dialup-84 +dialup-85 +dialup-86 +dialup-87 +dialup-90 +dialup-91 +dialup-92 +dialup-93 +dialup-94 +dialup-95 +hqglc +s519 +s521 +s522 +anggrek +s523 +videonews +vue +s524 +s525 +s526 +s527 +nhx +s528 +edmond +www.maintenance +termine +s529 +s531 +s532 +s533 +s534 +mv2 +s535 +s536 +s537 +s1119 +filer3 +printers +printshop +multivac +webreg +zbx +autodiscover.site +thaumas +autoconfig.site +nwa +notesmail +rc.webmail +delta2 +cserver +downloadshop +hexagon +dyndns2 +dyndns1 +rhine +johngreen +tobago +balance1 +eit +www.bmb +bhima +fluege +testlive +usatoday +vmweb +s4355 +bsd2 +hunk +logintest +rm1 +boujdour +torigin1 +torigin2 +torigin3 +lovemode +daytona +showroom2 +showroom1 +coimbra +smtpcelular +mtas +mtp2 +bramble +jour +tiraspol +inam +iloveme +stevin +transform +wak +duracell +igallery +promo2 +dizi +mail.co +calvino +imon +parcel +jena +yoann +jose81 +izzat +bari +rotterdam +jeni +macao +inox +especial +madras +cherish +irfb +s1477 +lasik +lovelyyou +bakery +qwe123 +leedh +nbi +eunice +lovenote +assemblage +s1473 +subaru25 +s1472 +krys +greentea +kreis +remon +allgrow +botandesign +tnd +kanshou +s1467 +seikofesta +hatogamine +s1466 +webconf01 +headspin +netzone +f64 +noatoshina +technolinks +ikachi +s1462 +ach +patio +nesjapan +bb3 +kosodatemama +space5 +mg130s2000 +gclass +pkobo +multi64 +otoku +bb4 +bb5 +marusuko212 +ahn +uranai +bbi +stagewww +kasf +ameblo +bgm +lujian +ote-telhosting +jellicle +vhost5 +autoconfig.india +webdisk.india +autodiscover.india +ronronear +bnb +ccy +nyantaro +cei +webdisk.clasificados +joycue +webdisk.youtube +bou +onsenichigo +tev +nrw +paulette +dak +mygw21 +ell +pops +dxb +bioty +s1447 +ecraft +socialapps +moneybookers +gge +hd1 +mailwatch +s802 +s811 +s810 +s820 +s824 +s823 +ktw +icache +mokeke +s818 +s817 +s816 +jltffukuoka +ti1 +predict +webdisk.whmcs +unb +ycc +perpetual +1banshop +hea +pumps +itvn +www.radiomaster +castanhas +genabog +kean +elearning2 +hhh +s1431 +jyoutokuji +katori +awoni +s1427 +s1426 +ampersand +sandhya +gun +s1425 +michinoku +funnyface +helloman +sorairo +shizuku +progressive +sunray-servers +tomatoclub +bellamusica +kel +lsworld +s1424 +s1423 +s1422 +sanuking +hiyos2 +hiyos3 +jondon +khn +jsl +s1421 +rewind +freefit +r18 +s1419 +s1418 +jtm +miz +mr1 +yakiniku +crmweb +s1417 +akamai +bw2 +s1416 +s1415 +s1414 +route66 +phm +s1413 +ais2 +ebis +resical +jole +dpop +haato +parasolife +joni +pog +yeti +ehime311 +type +sc4 +iekai +qpr +riyoukomaki +mcftp +chibarevo +s1407 +s1406 +izer +kimiyoru +yulily100 +sdt +shm +s1404 +rue +snk +s1403 +hellas +ascii +ryu +1000noha +tjo +beplus +tna +usb +bellport +s729 +kimu +rindesu +stokes +sigex +wus +fugakudo +ymh +valkyrie +zxx +strasbourg +bum +www.tel +www.mts +www.itc +nightsky +hs232c +autodiscover.singapore +dkweb +gotomarket +mimitsubo +autoconfig.singapore +webdisk.id +autodiscover.au +videowave +ponto +webdisk.singapore +jaybee +pharmamarketing +wolo +idms +yasunaga +unoichika +chikujyo +aggrenox.edetail +quonschall +mm100 +eastpoint +kyani +oracle-colo +aaaaaaaa +messy +avwqr374 +mbs47 +www.icmtest +starman +jsn +thorium +jesuc +mobilegame +lunartears +lovee +rano +webdisk.photogallery +loveu +teenbang +kimaroki +eel +mills +mimie +silom +firejam +mimmi +mysupport +artbox +nayan +sample2 +sample3 +39software +hotfile +sdmail +amemiya +kaigyo +hotgirl +asmith +mmmax +loveplace +buddysp +kelautan +zeitung +mercurius +trouttimes +eigohikaku +sunnyside +wordpresstest +nadeem +pall +takeover +machikadokan +yoshikinet +ichihara +skeleton +clowncrown +highscore +greenleaf +mems +furyuin +parfait +tanatos +bluedesign +sleepwalker +zodiak +wmx +mrdoo +newmonitor +mythology +lifeis +waiting +krit +eastward +saqqu +jamiroquai +enust +threee +obama +astaro +viagens +vendedor +thrill +mbah +kissme +mar1 +www.showbiz +test2006 +marketer +smo +mang +kodeks +musso +fallwind +shiney +brandnew +doner +nnnnn +vul +tt1069 +onlyme +myacc +peter2 +agasadek +mail.av +myboo +juvefan +mbmb +matu +fordca +configsrv +stun.techops +configsrv.techops +apartments +kull +servicestest +oldsupport +doumi +cdesign +meda +one04 +one03 +one02 +one01 +meky +freshair +one07 +1203 +one24 +meat +knd +strobe +mezo +lory +privilegeclub +tetsuo +wenku +firstclass +onoff +gypsophila +s1290 +miho +exquisite +steeze +revenda +naae +pyrenees +calculon +www.yn +danijel +mish +www.wl +www.nx +www.gx +sweetpea +hitch +www.farm +www.88 +tisiphone.olymp +qx +ckworld +counseling +bailang +pipik +xinhua +test222 +luiz +letterhead +pte +maskan +pi24 +neil +www.avatars +lmi +s710 +wma +websvc +nesh +situ +malicious +mycloud +radioman +jumble +mssql11 +christelle +sns1 +mosa +konga +hotelparadise +lyle +zarabotai +some +n7610 +admin0 +pictureperfect +niaz +ultra-vpn +z1z1 +tpeb +tpex +vpnbe +vpnat +s1233 +nish +vpnea +s1232 +vpneb +morteza +www.una +livezilla +sun333 +3gp +s1228 +tomochan +doubledesign +s1227 +schatten +inpress +raina +informationcenter +suns +tomer +vxml-lb +rac1 +impression +s1222 +s1221 +alert2 +s1218 +alert1 +satworld +ramwi +ransa +s1217 +mycv +nggums +appleseed +noos +directorweb +www.directorweb +s1213 +sitelink +s1212 +www.sitelink +dunhill +s1211 +integreat +ws100 +cpma +s1208 +cavell +fdb +mentality +s1207 +kfir +pc4353a +mytv +pc115h +s1203 +s1202 +cr006 +exec01.uus +mri-dieter.mri +enterprise.uus +uus-soc-01-od.uus +new2.uus +hallam_dev +koa-as-well +hallam_ad +junior06 +pc3256a +space4 +pcn125a +pc3457b +yhman-gw +pc3458b +pi01 +pata +ps115 +pc1051 +pc1103 +ps4101 +pc1309 +pc1401 +pc1405 +pc1452 +pc1102b +pc162a +pc201m +pc201n +pcn351a +pc1256a +justdoit +l3-1 +pc1257c +pc4051 +orki +pc1403a +ilomail +pc1458a +s4344 +pc1458b +cdn.greedy +healthc1 +pattern +qwert +dmedia-eg +mail-gw2 +235.bint3 +vxml-lbn +regio +reina +renee +maru0216 +dror +michelle1 +pran +revin +nonutilizare +bengolan3 +ten26llc +myhouse +bonobo +s1134 +rafa +someday +foolish +sanma +autoconfig.ajuda +autodiscover.ajuda +rahu +raji +sanne +sanuk +www.lukasz +filehost +saram +analyze +ramy +surreal +raph +theology +www.cristi +www.cosmin +www.danco +staffnet +invincible +redi +bonnie +rein +impressions +sweety +fore +thinkpad +qnoy +mild +core5 +hub1 +shami +poonam +rovin +madmin +carts +test1101 +saad +rima +sonetapijk +wednesday +el7oup +far +saha +taste +mechanical +be-plus +iraytb +fukuhara +norinet +vanillabeanz +hnctphotocb +saju +minase +4sight +anelog +noguchien +utbiscuit +mamezy +studiovier +sata +n47 +goodgroove +sass +s4340 +genkotu-dan +nasutaworks +hgf +obamaharumi +point136 +kinjyou +minibird +usr +roby +miyakon +semo +komala +wwz +9gnote +roo7 +starholic +ronn +icn +tacacs1 +designmaster +shab +roro +rosh +rory +kgh +rosu +ricoh2 +roxi +jsc +frigg +jsa +kkb +kredo +webdisk.magazine +taco +vesal +ldw +knr +maska +nosmoke +mbo +rubi +testshare +lmf +zhu +magento2 +magentotest +comsup +mmn +teco +conv +odl +sock +www.romania +rxhl +0rkutcom +custard +donovan +ecoman +nop +ramonin +samman +test100 +masood +greengolf +d60 +pme +ovo +b130 +b110 +z3r0 +rbf +uandi +rna +h159 +thesteamcommunity +h150 +h140 +h130 +caixaeconomica +straw +d99 +strom +c60 +dream20 +autodiscover.deals +autoconfig.deals +allgreat +versatech +b80 +hypnose +tram +copanel +a40 +j117 +h215 +h214 +h213 +h158 +islamweb +umax +h157 +h156 +registry-serials +h155 +h154 +upit +upld +clientaccess +h152 +h149 +h148 +h147 +h146 +h145 +wali +h144 +weoligre +h143 +h142 +pasrvt1 +ringtones865 +www.bundesliga +volk +vpro +h141 +silverfox +h139 +h137 +powertech +pantai +h136 +wova +wowo +h134 +h133 +h132 +yael +h131 +kousin +vaibhav +honeybee +allhere +h123 +chocho +kumis +larozum +taher +wtt +wwwk +staging.m +pikapika +c211 +www.tehran +www.mirza +www.ekat +saihi +www.akademi +b202 +b186 +tester12 +frankie +dmb +sympathy +mapi +modoo +zied +photocontest +webshell +grampus +yuji +chuang +ziko +b141 +zink +moneybook +libido +b135 +yuko +b132 +b131 +champs +zond +b128 +b126 +b125 +b121 +b119 +b118 +b114 +benreghda +b113 +b112 +b109 +arunkumar +fukuda +b107 +b106 +b105 +b104 +b103 +tunel +sheriff +b101 +b100 +a101 +k22 +pratik +transactions +whitedove +g25 +g21 +g19 +g15 +g13 +e94 +e91 +e78 +e26 +windows-serials +edge02 +e25 +wildrose +cis0 +e21 +e19 +e18 +www.guides +e17 +e16 +e15 +nasza-klasa +e13 +protrack +e11 +d89 +pbo +greenwood +iproxy3 +iproxy2 +images.beta +delivery.stress +leith.sandbox +controlproxy6 +images.loadtest +images.stress +iidb +delivery.beta +dbbktwin +idelivery2 +idelivery1 +delivery.loadtest +idbbktwin +icontrolproxy6 +icontrolproxy2 +icontrolproxy1 +adsummit2012 +www.adsummit2012 +codename +d64 +webdisk.jv +aalam +nullpoint +wb3 +toybox +mylover +cherryblossom +moonwhite +pocoapoco +tous +facabook +sleepyhead +kelmetna +yourname +lilian +c95 +c93 +c92 +c67 +c66 +talis +c65 +c63 +c62 +internasional +rachid +raj007 +laboratorium +c61 +c57 +gifts4u +nagaland +c42 +c41 +webit +7amoody +caramelo +designstudio +untouchable +undertree +heotun +readymade +odagiri +happychan +malhotra +theman +monophony +bexo +atak +rockband +blee +hypersonic +bom2 +daca +b44 +wwww2 +b42 +dang +daze +b36 +lavidaesbella +b34 +cnnc +toko2 +k100 +ws115 +achil +daydream +dmar +edom +a34 +mail01.test +mail02.test +mail002 +l3 +mail03.test +l4 +dogo +dope +ip28 +draw +apc02 +eins +apc01 +fajr +duet +nserver +topping +dw2 +nds2 +gams +s98 +s99 +rakesh +poverty +nwww +studentweb +s246 +mangoo +appmanager +smsgratis +fram +ggrw +amnhac +macpro +www.lmarsden44.users +www.pauldemo +www.dariuskrtn.users +wreck +www.plugins +fugu +mstest +godi +webdisk.newsletters +license3 +nederland +manyou +typo3.sapin.users +goon +www.daz134.users +ibar +ggi +idle +guuu +hmmm +prediksi +feminine +www.joomla.demo2012.users +snot +mingming +nautica +maxa +smsdemo +huhu +xswyh +ccie +cliche +sunclub +ecargo +dhcp01 +jyxy +tmgc +newftp +infi +rcu +booklog +secu +hxhg +www.antoniom.users +www.sheldor.users +jjjj +bigip +www.reiki.users +irun +qunar +katz +www.paigilin.users +heyyou +nightshade +jota +www.jakarta +webcounter +kualalumpur +nanjing +joss +bbarchive +ppg +web-local +joya +pauldemo +team2 +www.tonyawad.users +www.lsg +www.ankita.users +brilliance +kito +lama +yz.pass +oldimap +rha +mmf +www.stl +marmar +kody +mainte1 +imycro.users +demo2012.users +www.frankstar.users +backup1-11 +ssl-1 +www.abdallah.users +router1-main-ex +sv30a +mbackup1-1 +iahead.users +firey.users +ssl-2 +www.im21.users +rayalgar.users +test25.chris25.users +www.sakkoulas.users +router2-backup-ex +mysql10a +psychoman +router1-backup +mysql6a +tombutlerbowdon.users +mysql7a +sv50a +abanob20.users +ffv1000.users +mysql8a +mysql9a +sv31a +sv32a +sv33a +sv34a +sv35a +router2-main +koki +www.rams.users +router2-backup +router2-main-ex +router1-backup-ex +router1-main +koku +rohitwa.users +atera.users +sa3eedahmed24.users +tanx +koob +game8 +www.mefistofelerion.users +www.test25.chris25.users +behappy +ksas +courts +revenue +spider2 +foghorn +ffr +rsan +extdb +amirdaly +linuxv +mapo +www.holway.users +kunu +blueline +extender +anghel-andrei.users +oice +lmarsden123.users +northern +lmarsden44.users +www.chris25.users +pressoffice +dariuskrtn.users +kobieta +volley +michelin +loin +www.kobieta +s0s0 +pnw +daz134.users +www.arh +www.srt +ss7 +www.nica.users +www.sa3eedahmed24.users +signupsyeah.users +ocreg +ocvalidate +swieta +www.irfan.users +www.eis2sip.users +yayan +nacc +www.uz +novipazar +autoconfig.bancuri +www.samm.users +www.academic +tlabrvt1 +autodiscover.bancuri +webdisk.bancuri +miro +rushabh1211 +namo +slotmachines +oracle-dev +www.anghel-andrei.users +www.uta +etk +moab +cloud10 +www.bindas.users +ocr +reso +karcher +kkp +ankita.users +hartmann +mnmn +nesi +newindian.users +www.punniamurthi.users +aidycool456.users +def +rohit.users +neru +resultados +mold +nativelife +eclinic +www.maroculous.users +mobile10 +fris +mong +past +proxy7 +moth +iine +skg +downloads3 +no11 +uks +nms1 +t2t2 +stgadmin +alsalam +sysdev +irsdev +aii +lilboo +testluke.users +musk +www.grimothy.users +jilin +hefei +comunicador +chengdu +xiamen +fbtest +www.mailinglist +train2 +wuxi +haerbin +changsha +kiddy +beavis +s2test +jian +www.phoenixguild +gargamel +nosy +ytube +www.amy +roswell +blog.yasirakel.users +tianjin +zibo +vs15 +chongqing +soprano +blueocean62.users +funworld +app11 +eric3 +pedo +fmd +fsu +liberation +goi +webdisk.service +autodiscover.service +kiabi +graylog +autodiscover.intranet +mv1 +autoconfig.intranet +dataexchange +dcx +www.joergd.users +loller +pitt +www.lmarsden123.users +webpoint +www.cosanostra +highaltitude.users +us06 +poda +bowwow +libssummers.users +poom +yey +localmail +www.elysium +popi +spell +somethingnew +chillisauce.users +bj01 +www.devon +raha +www.basement +www.signupsyeah.users +mail.toy +maybee +snipershot +inmail2 +ramo +www.trial-022e98.users +www.yasirakel.users +rash +gameinfo +se02 +vs20 +screens +nevercry1.users +images9 +apologize +soora +boobytrap +saem +mailt +antoniom.users +zaphod3 +hopper +adplacer +www.trillian +pinkads +hotblack +www.abimassey.users +www.zaphod3 +ogc +www.lessharma.users +www.hotblack +dedibox +savy +www.magento.demo2012.users +www.paulgwyther.users +www.wonko +www.jeltz +sdat +reiki.users +katharsis +meno +maud +aftershock +www.typo3.sapin.users +roy.wang.users +helene +seen +www.aidycool456.users +www.ffv1000.users +sele +yotsuba +mail.song99 +roco +song99 +nodo3 +nodo1 +roop +natur +tomandjerry +smoothie +fs21 +fs20 +aun +rrrr +adcbs.users +fs19 +mail.avgame +tippspiel +avgame +tatu +blu-ray +reseller1 +cx1 +www.donkey +collaudo +tazz +kanna +sawa +snsd +www.androidctc.mefistofelerion.users +voucher +tsumo +onkyo +bitbit +nn1 +mh3 +ssis +ssuu +homenet +suni +supe +grange +smtpdel +fishman +zappy +morethan +decade +tpemail +www.bayern +ktest +trice +www.sphere +tune +paigilin.users +ycbf1 +new-web +stream12 +synca +webi +secureapp +wien +hayley +crpm +guadalajara +monterrey +www.rohitwa.users +www.frame +www.dad +www.cws +wito +xboy +srv26 +staging.services +xoso +mino +zero0 +www.scm +srv42 +www.hod +zeron +yoli +www.algeria +bindas.users +rougugu.users +delfi +bamboobites.users +dbdb +zhen +ziya +camellia +zoop +yomyom +ns261 +jxzy +ns152 +livingtodie +svn-source +hoainam +tonyawad.users +warden +cuncon +magicweb +nat510 +vip900 +vip600 +www.blueocean62.users +efsaneforum +trial-022e98.users +beshoy55 +preityzinta +leggings +mido92 +kakarot +antitrust +filmovi +frendz +beachboy +cerisier +mindstorm +supernovice +amir2007 +mervyn +corvet +corbin +papermachines +tra2002 +dracomalfoy +sylfaen +blade03 +blade06 +ndsunix +zootycoon +dreamer30169 +djc +examene +freersgold +demo24 +portal4 +blade05 +blade07 +demo28 +abdallah.users +cdac +rapidshar +yeuem +xnews +www.faith +egystar +matfiz +oldshop +www.massivegreyhound.users +shanerhodes.users +yatie +shankey +www.new.newuser.users +minhdung +nas0 +topspot +e0 +rapier +www.ausbildung +www.rural +manojm +customer1 +www.b12 +esx02 +fortyouth +autoconfig.audio +webdisk.audio +rett7.users +androidctc.mefistofelerion.users +kasino +sd2cx1 +autodiscover.audio +mamali +securitys +sd2cx2 +vonline +www.libssummers.users +sd2cx3 +sd2ca1 +sd2ca2 +toplink +sd2cx4 +lagiator +se7 +magnat +hegemonia +shababy +prayer +cmtk1 +cmtk2 +cmtk3 +birk +ugyfelkapu +spiruharet +libyan +www.wpeasy.users +webdisk.black +pinga +checkit +vedat +lb-www +www.dijinkumar.users +nanoicom +darkshade +sdb3ko2 +www.chillisauce.users +sdb3ko4 +www.90 +toual +paulgwyther.users +suzie +realdreams +azizim +sdb3ko1 +ajans +sdb3ko3 +spic +sexystar +www.sipac +proxy.ccs +vat +staffing +young2 +www.sigaa +webdisk.meteo +missingyou +www.realbusiness +dilemma +alex18 +chiemi +mail83 +mokka +fwe +mail51 +mail48 +webmail0 +www153 +www202 +serv148 +newuser.users +gate6 +growth +growup +www212 +autoconfig.coupon +autodiscover.coupon +webdisk.coupon +jenkins3 +joomla.demo2012.users +www215 +monkey02 +filedrop +k1000 +greatescape +wardrobe +philos +lessharma.users +negro +fastukhosts.users +www200 +cretiveadmin.users +whitemoon +yourdream +accura +zxcvb +megalodon +konoka +cooler +gslbdns3 +gslbdns2 +gslbdns1 +pps2 +proman +mr2 +exchange2007 +iapple +netmaster +tele2 +pavlodar +tictoc +donskoy +elmi +shum +veille +ohrana +unions +qbusiness +bousai +sloan +www.osi-app-new +saray +linkshare +swicki +autoconfig.kino +www150 +testmy +db.test +webdisk.kino +autodiscover.kino +x.www +learnmore +ssologin +engine1 +starlike +toip-cluster +uzem +velona +www.atrium +realbusiness +cyber10 +kukku +webdisk.faq +www.firey.users +joergd.users +triki +ilahiyat +ahmet +nerima +warframe +hachioji +englishgrammar +gabor +uxasr01 +radtest +fef +includes +insanity +untech +ux-mailhost +www.krs +manama +olopa +busan +diklat +creamy +vpn113 +cutebaby +napas +loveit +agx +uxwr1 +uxnbacu101 +netlive +starstar +dema +www.iahead.users +unused +visitoradmin +smarttv +webctdb +wildcard +uxpbasa01-pnr +u1-00-b1-sw03 +yulia +beloved +ws185n185 +zerobase +prison +telemedmon +uxr2 +tll +slivon +thb +denzil +rayray +dcgaming +na2 +ilya +dilmun +riche +really +illusionist +pln +dream2013 +idream +planethippo.users +iot +xwb +caliope +efax +orchard +thang +jhome +goldstar +wcedge +devman +design007 +ashtray +awo +www.apc +dpm +www.ukraina +www.ramazan +stage-api +palmbeach +sharetest +pr3 +inertia +pr4 +pr6 +ebichu +t222 +hwarang +www.calls +droplet +pr10 +bbs9 +pr11 +pr12 +uattravel +nbg +www.showman +renplace +analyst +showman +xserve1 +district +www.tijger.users +nakaji +ftp42 +brookland +ftp40 +reizen +ftp36 +ftp35 +usanet +ftp34 +mail-new +www-php +ftp32 +bic +ftp31 +michael.users +theisland +www.ieee +hongda +wpeasy.users +hillcrest +ftp41 +kouprey.users +ftp38 +henderson +lonelyplanet +whs +ftp37 +ntest1 +iismtp +lightspeed +ftp39 +oracle2 +osceola +khs +switch4 +frankstar.users +wiki1 +www-stage +www.planethippo.users +win33 +win46 +tixiliski.users +definite +oracle3 +demo29 +www.qazwsx +paradiso +thorin +tropicana +detective +autoconfig.ad +jmorris +belgium +www.finland +autodiscover.ad +www.atera.users +sunny1 +pc122 +pc123 +www.austria +www.ireland +chicago3 +www.norway +www.zave10.users +autodiscover.bt +chicago4 +www.lithuania +chicago5 +arttime +freeonline +pc251 +solitude +solidarity +staycool +exhub01 +exhub02 +himalia +batist +www.bamboobites.users +shoppingmall +iae +macabre +host198 +host197 +infected +host195 +loveletter +eternel +host189 +host184 +newns1 +host182 +dokeos +host179 +host178 +intuit +killers +atoll +host169 +host168 +host167 +dladmin +host165 +host164 +host163 +host162 +host161 +host157 +host155 +host154 +host153 +host152 +host151 +loopback-net +host149 +host148 +host147 +eastside +host145 +host141 +host139 +host136 +zippo +dav.ox-sd +matra +host129 +host128 +jacker +terni +omeka +stacks +apptest1 +dododo +bermuda +seabass +lovelovelove +vps20 +qwer1 +khurram +oana +calligraphy +apotheke +mailengine +feed1 +joomla15 +www.erasmus +iwamoto +godfrey +fib +www.arte +webfree +www.note +vls +ldapmaster +www.images3 +domdom +testads +leporis +alhimmah +www50 +djclub +host200 +host180 +samwise +host159 +dsvr2 +group11 +ib1 +moneymoney +www72 +mail.us +www60 +www70 +sr6 +dcf +saguaro +www76 +www59 +www69 +rakon +host192 +sr5 +host187 +host186 +mojito +dev-support +host207 +host191 +host190 +real1 +host185 +host183 +real4 +www.demo4 +cef +scca +moodle4 +horairetele +host166 +host158 +www.task +tarantella +smtp23 +dispute +host156 +www.jacksonville +galaxyworld +netmax +host142 +host140 +host138 +eres +apache2 +eduserv +ifs +apache1 +pc08 +multigame +salvia +dextro +southeast +samsam +samsun +montes +mimmo +tealeaf +super12 +ellipso2 +n33 +dp1 +gart +mashhad +existence +sbc2 +pirouette +dreamy +j6 +up6 +www.p1 +autodiscover.calendar +form2 +autoconfig.calendar +form5 +hellyeah +jkoecher1 +elog5 +stjohn +host160 +lastone +eis5 +iag +basie +vil +artic +download7 +freefiles +gliwice +printec +ip48 +arukikata +storytelling +ocn +picturesque +individual +zucker +usa3 +immune +d1002225 +d1002101 +d1001440 +border2.nntp.priv +border3.nntp.priv +h89 +h64 +h61 +h58 +h54 +h53 +h52 +h47 +h46 +crafty1 +h45 +border1.nntp.priv +h44 +h42 +www.vidhyasagar.users +h39 +joon.lee.users +h33 +h32 +alpha99 +h31 +h65 +h63 +gcf1-rr.nntp.priv +alt-relays +h40 +agroweb2 +border4.nntp.priv +h60 +gcf2-rr.nntp.priv +fafnir +vanburen +h59 +node11 +astat +changepoint +ipos +tijger.users +vax +hyperv +yyc-border +bonds +www.frm +snorlax +ldapadmin +uasdb-scan +fishbowl +www.genealogie +redwolf +autoconfig.laptop +autodiscover.laptop +waterwater +webdisk.laptop +jejeje +www.dave1 +doghouse +isildur +ms4idrac +sol2a +array +statdb +postfix4 +bweb +postfix3 +humancapital +am2idrac +www.mdi.users +dosya +lkm +kars +ms3idrac +vmserver +uranus1 +ns232 +ns212 +am2a +internetru +ds437 +ms4a +am1a +activenote +invftp +adore +ikou +slack +am1idrac +medblog +seed01 +medianomika +jmw +kab +mailstore01 +jcb +sweetdream +smup +kafka +hydrangea +e-card +tiktik +wuli +fairfax +zhongkao +smh +anxiety +bigworld +carrent +aimer +sendai +nel +shisei +bagel +fb-apps +coldheart +ahuntsic +hgj +beams +mail.secure +finam +rambler-tier +nnn-i +stars7 +master1234 +rambler-test +f116 +www.con +nbt +dududu +n226 +n202 +might +cummings +e-club +drishti +pee +kulabyte2.lab2 +live1.blr1 +smtp-rr.srv +in2 +syslog-rr.srv +www.manga +www.nauka +www.rpg +live5.bitgravity.cpe +v2.core3.sfo1 +http-rr.srv +live2.blr1 +zxc123 +bgp-rr.srv +axfr-rr.srv +infocentre +www.trust +ntp-rr.srv +sberbank +snmp-rr.srv +mysql-rr.srv +tacacs-rr.srv +jatim +tftp-rr.srv +gestor +rdns-rr.srv +rav +www.astro +shazam +ldap-rr.srv +kallum +kdc-rr.srv +renwen +efriend +kickstart +arden +bodyguard +autodiscover.downloads +tcdn +fujitsu +autoconfig.downloads +hardik +kingsfield +saveonline +lololol +cbi +qa.tools +waitting +couture +testintranet +heroine +lovedream +moondance +jabberwocky +avant +nintendowifi +sgames +withme +www.mailman +promail +blush +ssr1 +setup2 +romero +webpage +netscreen +oceano +cench +ides +noroozi +elodie +timewarp +aniworld +autoconfig.cars +www-beta +webdisk.cars +www-5 +chata +autodiscover.cars +kumsaati +esporte +kyouei +muscat +boyet +cider +limelime +choya +equip +gb3 +maps2 +visio2 +visio1 +ggl +antibes +agadir +americas +gw-mx1 +vps02 +dadan +nl.test +luckystrike +ucms +digitalclub +cipot +terminal-uk +uk.test +daili +panmog1 +cdh7210 +hispace3 +bybarang1 +playonline +teaks2hyun +s1devsf +eko2 +ncre +gosibook +cemerald +rc21com +kshcow723 +unionptr2870 +sunbow6958 +llux7831 +demoself +s1devnj +s1devsky +s3freeintsf +gomooke1 +cutemate +one5303 +jr286tr +omega1 +s1devhn +angkotr9019 +isaac87 +retonar2 +retonar1 +s3freeintnj +ipaydaejimobile1 +withpace3 +jsy8656 +go7art8 +go7art7 +go7art6 +go7art4 +dero +bbolemoosky1 +allgreentng2 +purperi3 +s1devsdg +ipayhybridin +guqlrnt +bizcsijang +yeonsung-060 +kaewoong1 +amazontr4268 +venus2259 +gw-10 +sodosi +blackbean2 +mielmp +sanyaco2 +seeun003 +miga01 +skingifttr +lovemina +antikimchi1 +hslv06081 +rksehfrns4 +snnet7 +jejuhbtr +doobedtr6434 +snninc1 +pumpkh +sungeun21 +bizcenter11 +zerogolf +damha +jokag +ohs5301 +killian2 +gdmgnsun +autotest +haesung083 +ysuri5 +bctraders +dirtydelta +poongcha1 +manchu99 +sam5284600 +eyo14682 +newadmin +hacker0 +sey5624 +snnbyn +tpoo8350 +brush +foxdiy +babywish12101 +gametoday6 +sugolftr +gametoday1 +dajoajoa2 +dajoajoa1 +rnfanf823 +sony723 +jackie2372 +najjang9 +artwell10041 +hans1544 +ssuissui6 +ssuissui5 +mi07285 +roastery2 +mi07283 +mi07281 +jooh12 +p999123 +myjuyer +kyoun1230 +collw3 +bobbymom +spacenoah +jbs0609 +hampil771 +cardupdate +siriusfrog2 +ddengali3 +s3freeintsky +seizerdlek1tr +sejinkorea +s1devman +reagelab +godo3d-039 +kcompany1 +reytak +sgv +ibb +vvmmvv8881 +nabiggtr7399 +civis +hankrlee +godo3d-030 +opstree +s3freeintsdg +chaiwoon +wlsdl04181 +nutraone +baeksehoon3 +diolla +quiltvalley +godo3d-020 +davin +beautytr2068 +rendezvous +sensekw +ansholic1 +cjh05101 +meatnpeople +godo3d-010 +iamcontr9499 +djembe +lily7979 +inno1121 +inno1116 +neotest2 +moncrotr8732 +lili1206 +foxlovely6 +jayeonmiin +blucelee2000 +jiae71472 +wellnessia +jonga1 +yim04252 +interbrain +dbstksgh09 +jnlee +yh710404882 +yh710404881 +slko10041 +powernike +nteam3 +nara2nara +hjretail +cmchair2 +moleok20 +hadam85 +moleok13 +moleok12 +myanb17 +myanb16 +myanb15 +myanb13 +myanb12 +myanb10 +mrgravy +photo-story +mslalala +tkdmleh3 +tkdmleh2 +kitweb-010 +eg6040 +mlist +maycoop1 +lover830 +helenoh3 +tripleo +silverheaven +kliccmart +bioskinceo1 +s2mile +s3freeintman +cooks +garyong4 +planm70001 +hucheum +artmusic65 +bananakiwi +tymca1 +pulmoo +aromisua +dingcs +powernet1 +inspired +designplus +cartoon74 +creep +kds7606 +kizstar719 +modainmall +ustivoli +imdm +miz1041 +smarthand +s3devw +a01066662966 +hooyoya +s3devp +dist1 +partyplace +ds34 +mail.crm +benegen +s3devm +jusihyeon871 +s3devb +maxjojo31 +ipayspxlwms925 +ipayspxlwms922 +ipayspxlwms921 +kimchitouch +nskway +smarthan1 +cameostar +megapeak2 +pvckkk +eyesystem +sa4318 +simple9454 +sellstyle +espoir175 +dcp4300 +www.iceman +sparkles +enshriue2 +enshriue1 +pamcom +okcnc2 +moser +ds123 +michaela +speed1234 +djdkz2 +midpoint +ouruniverse +brody +donga +natural2 +vhost102 +vhost101 +apia +gmini +ahn6244 +timeless +phillip1 +s076121 +gac +lukes +yt +assa +blur +ducks +pannchat +smartfree +tears +mistica +win28 +shibata +bong +webclass +azel +realpro +joa89 +aprilseven1 +spornack +dusty +boyz +wrmb12 +brat +wonilcnp +kanade +kyunh0 +sport113 +keunpb +cmstory +archv +egold +www.999 +kamiya +blender +elastic +movzeetr6058 +darkcity +mp119 +redhatkill1 +gamebobs3 +daehanmusic +lsa412 +picupu10041 +dimito +gawaa2 +kfaa2014 +officeboy1 +rentalshop +huborn4 +huborn3 +huborn2 +huborn1 +younpark29 +chl4318 +js4u1 +maryam +doogie69 +eppie +karyban +hopy5983 +myoungdesign +ihee08142 +hellomagic +sijjsijj +sj50425 +namph50 +ami5407 +recipe1228 +foammake +soccus +midan2 +godoweb2 +x6 +up2011 +gaile +wgna61113 +gandu +shishido +koryo +fixed +hotsauce +moldscooterclub +besakura +triplek11 +replay57 +innocence +campingpoint +dios +mandala +tserver +misocorp1 +crmdemo +theyasmina +provo.tile +srvweb +godovs16 +www.za +jomart +tema76 +jsgonno1 +yali8922 +ds24 +hucenf +flyit7771 +hazen +younguijung +hkfishingtr +bumilion9 +bumilion8 +cfmallcash +x428ma +pleatsme +abaoaqu +devit +gurye100 +nannuni85 +kdgtl +dm2 +iocean20121 +bulezou1 +smpp6 +politicas +vsnl +pony0701 +sms07422 +bodynjoy +dona +retrofactory +hepimina +plusbeam +lavastone1 +fire2 +teafood2 +mahim +fourm +smart3 +drill +partyween4 +hawkeye9 +paleum +bilety +giare +wjdals5611 +darkprince +cat1 +calverton +thewestvillage +voyager2 +seie5687-089 +godoshop-040 +rumagirl +jovial +rmx +twelve +godoshop-037 +www.bsec +bambie +silas +hukiworld +godoshop-035 +alchemist +godoshop-034 +joodung +godoshop-032 +mongo1 +godoshop-031 +godoshop-030 +godoshop-028 +godoshop-027 +godoshop-026 +godoshop-025 +godoshop-024 +godoshop-023 +limjh63061 +hiex +godoshop-021 +godoshop-020 +kekkon +godoshop-018 +www.hiex +godoshop-017 +godoshop-016 +misba1001 +godoshop-014 +godoshop-013 +godoshop-012 +www.zel +godoshop-011 +godoshop-010 +godoshop-008 +godoshop-007 +godoshop-006 +godoshop-005 +godoshop-004 +froyo +godoshop-003 +www.thewestvillage +www.copper +www.xp +www.twelve +candyfactory +udo +autosystem +nsd1 +segreto +godoshop-002 +godoshop-001 +amts +lsw4378 +skymoon1 +kimjs28125 +kimjs28123 +igrp +daejinclub +enyo +seie5687-079 +omdesignkr +mudra +dfmaltr5155 +joinmedical +lnt +eltpark1 +cybermedic1 +mail.jp +vtf07451 +heros +malka +kcroad1 +kimp2 +namph40 +seie5687-073 +seie5687-072 +riken +owa2010 +tamura +braxton +cocco +dangerous +seie5687-071 +lhy1pys +seie5687-070 +kctyb11 +pawlitic +daniella +grain +seie5687-068 +kidstravel +hyung05021 +lsc4455 +pharmsave +wosung2 +swpaper1 +onlyyou3 +onlyyou2 +spoonz1 +ranju +netservice +j8 +lear +mguess +thetop +seie5687-060 +arttest +italy2 +italy1 +polorl28 +besttour +grimm +achim +remover +polorl12 +gonatural +gmc0072 +dmsql121 +miainkorea1 +lovehome +hatena +s9356s +chunamujuk1 +uzumaki +waterlily +testhosting +detoxkorea +induk1-040 +induk1-038 +reorder +induk1-036 +induk1-035 +induk1-034 +induk1-033 +induk1-032 +induk1-031 +induk1-030 +induk1-028 +induk1-027 +induk1-026 +induk1-025 +induk1-024 +induk1-023 +induk1-022 +greenb +induk1-021 +induk1-020 +induk1-018 +induk1-017 +logosmart +thefacebook +opportunities +findme +geophy +simbata +anonym +snowwhite +pfa +maplezone +induk1-015 +induk1-014 +induk1-013 +induk1-012 +newfoundland +induk1-011 +248 +fiddle +induk1-010 +induk1-008 +induk1-007 +riri +myzone +plutonium +vendorftp +happyman +pp2 +mailcontrol +ratio +induk1-006 +induk1-005 +induk1-004 +skyweb +ironwood +nara118 +induk1-002 +induk1-001 +jpboom +cellexc +onlinemedia +seie5687-040 +ecoearth +smilee +univers +esn +dole +creators +www.camelot +tanada +azi +joanne +jokong +kimbok10 +sisgirl +parkhawon +handostr8771 +peace96902 +izzy08018 +izzy08017 +peoplenbeauty +micasatucasa +dresdenpp1 +koalamart +withaylatr5289 +thecristal +iphonern3 +spec01 +seie5687-030 +sensmall +jomo +seie5687-028 +nonggigoo +joven091 +rnlqls06 +zenheist +novelty1 +good3931 +cahaya +colorparty002ptn +biocospharm +seie5687-023 +saku435692 +seie5687-021 +bdsblog3 +massacre +webdisk.chef +tenichi1049 +daisuke140 +seie5687-019 +cuwoocuwoo +hostingblog +bugtest +svadba +jazzy +superadmin +webdisk.fashion +swacom2012 +dotmail +parceiro +sktoolz +emsp12053 +seie5687-012 +www.wh +pimpin +moonstruck +tamworth +jetty +cgj +servertest +knchintr9652 +chocolat +totoro +winserver +kabel +isty +iwork +seie5687-011 +ahi +guard2 +tricounty +murapic +loadtesting +fs9 +nashua +facebooktest +vanessahur +pluse +marchen +serverhosting254-239 +browns +seie5687-009 +h128 +rp2 +keroo +blr +woodbury +popov +georgetown +knigi +ed126861 +a-dtap.www +dev.ident +revel +itsti +fwupdate +mspark3 +a-dtap.klm +giftbaskets +tenorio +sankei +estargolf1 +mspark1 +ci.dev +collie +lims7738 +wakalee +bartlett +nutter +chemics +sommer +ferienhaus +webdisk.adm +myvpn +cress +imstore4 +harada +prev +ml1 +choianne-010 +ahabeauty +ili7067 +seie5687-001 +pepedeluxe1 +moadesign002ptn +clark1112 +09jungle +ahn4817 +qtmagpie1 +kkimkki +touchptr2555 +yg2213 +innofoodi +fruitage2 +kkang75652 +aux +semele +gestionemail.pec +houchukyo +pos2 +s2fsdevsunny +peg +shiga +iyatoy +shows +pablopark +apolo25 +piao +x60 +slt +xfb +citylife +v13 +samaa +v7 +syokunin +czj +erebe +sfj +natukorea +oneweb +hushin2002 +mixuk71 +k2cine1 +yhahsw +gmgleeyz +neree +fzb +maruko +steropes +kplaza +jcj +court +jgj +omykeytr5098 +brontes +saika +namph19 +frontal1 +optima2 +lcy +arrows +optima1 +fujinokuni +packsun2 +xen01 +dedale +bujaok1 +eje +micaad +itree +susil +nesting +saher +seohae1 +cms-test +search7 +dajungmotors +levanthanh +john11 +gentoo +loyforever3 +mail.store +loyforever2 +loyforever1 +stock01 +logger2 +msg1 +autodiscover.domain +wedcoupon +awaji +besthost +wping515 +boxiz0013 +boxiz0012 +dealernet +ghktjdrldjq +pc8 +astech2337 +www.ies +ubytovanie +autoconfig.domain +mgw2 +spak +arin0822 +xyglc +vw2 +webdisk.map +urushi +itory +baoming +taihei +lijun +dahanoo +geryon +ruter +tkatka33 +pagodapan +goodhope2 +billboard +ysxy +iol +zbb +www.rajasthan +rajasthan +qu +smartfarm +lamda +durian +inger +iao +wega +tyxy +economic +sqlmonitor +votus12 +jr761 +dhrwngmll2 +dhrwngmll1 +wbw +cartier07222 +yjcjms2 +icbc +netcity +webdisk.ecommerce +su2230 +palikorea +airljs2 +reception +webcluster +always +airljs1 +itwiki +demoofix +gananhan +zhibo +soban5 +ruthie +fujin +soban1 +adsimg +tell +www.nour +kislovodsk +greatmunkoo +pushkino +murom +villainy +yushin +korolev +komsomolsk-na-amure +jengokk5 +comodoto +pervouralsk +seiko +ooollooo81 +zooone426 +filter1141 +bodapnp +h001 +beniya +jhsi10044 +essentuki +desdemona +www.ppma +gratinus +dreamlandco +rsmc +hlssci +www.scarf +evserver01 +scarf +ag-control +wjdxodid +tcvpn +www.eat +kbmtb +photo5 +pinhole +elimsori +itmro +ms0107 +ez2 +bockhan +md02 +hcglobal +md01 +vip254-9 +vip254-8 +tocarpianoadmin +xn980b51ng3co8ntr +weblogsadmin +bakingadmin +blocs +dreamdipot +gusqop +aorwn6971 +sjinsji +joungage +sn3 +ilove471 +soul3523 +forceout1 +jewelrybouquet +ythsun +gumigagu +iucon +midtiti +sjsearch +freebiesadmin +habosae +copyrental +oh72184 +oh72182 +oh72181 +haemin3425 +mhr +gaylifeadmin +powerkjin +usconservativesadmin +jjaturinamu1 +useven1 +online-booking +grandparentingadmin +overce1 +nob +mwb +anh-mobile +anh-t +ipodadmin +gi414admin +gamen +anh-ipad +a-tha-2410-hn +bornstory +gosoutheastadmin +hong1 +gi165admin +jjtech1 +traveltipsadmin +dsn +ndt +giaithuong1.diemthi +listadmin +goneworleansadmin +asumi +moneyfor20sadmin +distancelearnadmin +aichi +astrologiaadmin +lsp +hnth +hinhanh1 +urbanlsladmin +naracnc3d3 +mhjjyy +lsc3103 +ctc825 +hajung486 +polostar1 +kurage +govancouveradmin +waterqualityadmin +jayecas.users +genki +decoy +mefistofelerion.users +gi92admin +mcprepacc1 +webbuilderify.users +massivegreyhound.users +bitcoinbear.users +another.users +collegefootballadmin +gi282admin +kidsfashionadmin +mdsuburbsadmin +saikyo +sexoadmin +www.tixiliski.users +budgetstyleadmin +gpsadmin +www.ddfgfg.users +sjdns2 +pzh +sjdns1 +fuu +www.jacqui.users +cooperative +fx1 +bateriaadmin +asakusa +uktop40admin +www.tombutlerbowdon.users +bto +bagus +dancemusicadmin +www.fastukhosts.users +dineroadmin +boxall2 +viewtiflow1 +dsyo331 +joocorp +kato6 +gryphon +gfgf2001 +istn1 +family1 +mil3034 +edorostr3920 +oneorzero004ptn +dc79231 +impuestosadmin +leegangju +jnss80 +gi408admin +mirador +moadesign001ptn +midong262 +midong261 +kasma +itisp +annason +el19772 +yjo09061 +aa09030903 +kijinceo +ipayjsegn +hyflux6 +hyflux2 +duggy74 +hemplee1 +sbycs486 +eco1004 +smartedu3 +smartedu2 +smartedu1 +abp +oddpactr5315 +macsadmin +jbj19992 +gardenhada1 +grimothy.users +kca +gi160admin +helmet +ctzone +mdi.users +chistesadmin +maeda +www.highaltitude.users +autonet +www.webbuilderify.users +bbspecial +asahisouko +motivacionadmin +publishingadmin +www.rohit.users +coptvadmin +autoconfig.futbol +new.newuser.users +autoconfig.musica +autodiscover.futbol +polkadot +www.roy.wang.users +autodiscover.musica +tenjin +mentalhealthadmin +compnetworkingadmin +www.demo2012.users +dijinkumar.users +telephonyadmin +aao +gezi +couponingadmin +macallan +nat-eduroam +gi129admin +foad +webdisk.eshop +www.rayalgar.users +ldap-ro +im21.users +neelix +guinness +autoconfig.eshop +autodiscover.eshop +www-cache-out-all +www.testuser.users +bebidasadmin +www.abanob20.users +starwarsadmin +www.anamul.users +searchnode +gi86admin +sheldor.users +drupal.rohitwa.users +swc +rams.users +nydns2 +jjy +nydns1 +zave10.users +birdingadmin +chat8 +vidhyasagar.users +chat7 +escience +www.jayecas.users +www.bitcoinbear.users +chat6 +yasirakel.users +chat5 +dzp +gettingengagedadmin +www.tmedia.users +esstest +guidepolls +bizsecurityadmin +www.newindian.users +irfan.users +vserver11 +drawsketchadmin +testuser.users +vs36 +skidki +www.newuser.users +www.blog.yasirakel.users +chicagoadmin +www.drupal.rohitwa.users +immigrationadmin +cruisesadmin +videogamessladmin +chessadmin +marriageadmin +gayteensadmin +gi403admin +vgstrategiesadmin +nica.users +militaryhistoryadmin +gi154admin +britishfoodadmin +ddfgfg.users +trabajoadmin +hydrasearch +rockclimbingadmin +usatraveladmin +gi81admin +www.michael.users +gi271admin +www.kouprey.users +celiacdiseaseadmin +www.adcbs.users +cravens +meatandwildgameadmin +samm.users +bssp +gi490admin +abimassey.users +www.rougugu.users +teenadviceadmin +huntsvilleadmin +webmail28 +magento.demo2012.users +mmsoundadmin +gi387admin +classtest +www.cretiveadmin.users +gi148admin +gi450admin +knittingadmin +collegeappsadmin +holidaytraveladmin +vserver12 +www.shanerhodes.users +vserver10 +www.joon.lee.users +beta.calorieconnection +vserver3 +www.testluke.users +vserver2 +sitebuilder1 +hbh +www.imycro.users +probe3 +careerplanningadmin +baproductions.users +eastbayadmin +chris25.users +trial-54a4f1.users +multiculturalbeautyadmin +www.nevercry1.users +candleandsoapadmin +ennuevayorkadmin +www.rett7.users +voice1 +sw7 +www.another.users +gi265admin +www.widgets +rubyadmin +punniamurthi.users +london2012 +househomesladmin +eis2sip.users +personalwebadmin +goeasteuropeadmin +prmcorp-forum +realestatecaadmin +southjerseyadmin +anamul.users +gi382admin +xls +tmedia.users +www.trial-54a4f1.users +realitytvadmin +gi143admin +maroculous.users +goisraeladmin +mysterybooksadmin +boyscoutsadmin +www.baproductions.users +seniorhealthadmin +jacqui.users +hojasdecalculoadmin +sugarfreecookingadmin +budgettraveladmin +gi70admin +holway.users +gi498admin +tvstream +pny100038 +lovehope +gi260admin +ddrmabu2 +seokamzz +s1shop +mi07286 +lsjholsj +paekguy042 +godoedu48 +godoedu47 +godoedu46 +godoedu45 +godoedu44 +hnaksi +godoedu42 +godoedu41 +godoedu40 +godoedu38 +godoedu37 +godoedu36 +godoedu35 +godoedu34 +godoedu33 +godoedu32 +godoedu31 +godoedu29 +godoedu28 +godoedu27 +sakkoulas.users +proskateadmin +pestcontroladmin +horrorfilmadmin +alanadi +santarosaadmin +ekip +www.ekip +www.phpmailer +us3 +piligrim +bluesadmin +wallpaperadmin +gi376admin +corporatedesign +indianfoodadmin +videodev +gi137admin +mobiltest +friend4ever +gocentralamericaadmin +asianamcultureadmin +eddb.team +www.workfromhome +tatuajesadmin +godoedu26 +godoedu25 +godoedu24 +godoedu23 +godoedu22 +godoedu21 +godoedu20 +godoedu18 +j2ydiver +godoedu16 +godoedu15 +godoedu14 +godoedu13 +godoedu12 +godoedu11 +godoedu10 +elnino417 +jounga88 +miyjs13 +gehunhun +iks10091 +voguentr4621 +enovia +freshers +homepmart +allart5 +allart4 +arumsaegim2 +soapschool +jarrodlee +kthkira +lycos +bodylink +kkksi175 +kkksi173 +heasunggo +mbp +pkteafood +bonolang +mailmiso1 +widesign1 +hotpinky2 +bujacat +bbmy4861 +skymap11281 +gold8gold1 +dbcjf111 +snowz123 +jadarmbi +sym14701 +tsgim7015 +tsgim7014 +tsgim7013 +dragonjjw +tsgim7011 +healingsoo +hyang777kr14 +mare15001 +pjs8642 +justly2 +image168 +image167 +image166 +dil +image164 +www.magic +image162 +yudo93211 +emit1004 +ziyun1 +giniginian +ggo9ma1 +mhj104693 +gi9admin +image113 +image112 +image111 +image109 +image108 +image107 +image106 +selrana3 +image104 +image103 +image102 +image101 +image100 +sesdevsunny +powerjkl1 +csakks +wooritelceo2 +spacejkj1 +isljh +miclove1 +dentalland +zizity +cd-rom +wowgulbi3 +djbank +oeufkorea +jo94511 +gtmen72 +seoes02 +kwons137785 +winca +bayi +lavka +www.pressroom +launcher +rehber +mclist +useconomyadmin +gtalk +petr +weirdnewsadmin +heeland +ws-test +gi64admin +gi493admin +ukproxy +gi254admin +preemiesadmin +feminismoadmin +katalepsija +ma2 +ma1 +hg3 +rapper +fl2 +pa1 +prabhu +home4 +musicaelectronicaadmin +yazd +goitalyadmin +germanfoodadmin +www.bushehr +lifeminders +gi309admin +breastcanceradmin +ruchit +bombuzal +bushehr +www.devcrm +ottawaadmin +gi371admin +bankingadmin +www.cable +www.lap +gi132admin +amidala +goafricaadmin +homeelectronicadmin +nonprofitadmin +filter4 +ravehousetechadmin +presenter +www.projekt +speckidsladmin +paperless +creditadmin +secure.team +detroitadmin +myk +uhstree +30pr1k1 +s120.avatar +gi4admin +7h1ck71 +giftedkidsadmin +ftpuser +postad.hightech +gsg-forum +5vsjz91 +rhyolite +zimmer +gi58admin +dicom +wm_j_b__ruffin +gi487admin +holocaustadmin +diorite +gi248admin +machardwareadmin +webdisk.img1 +nytstage1 +groupbuy +lagos +gasprice +web-1 +computadorasmacadmin +jeansadmin +gi106admin +gi365admin +gi126admin +theoneman +cutegirl +annuitiesadmin +mobili +gi396admin +coco67 +build.pages +gi196admin +sigil +test124 +socialinvestingadmin +destructor +nativeamcultureadmin +intim +wirelessadmin +lojavirtual +justgo +gi53admin +media04 +saude +gi482admin +ipayjugmaru +heyarech1 +jasangbox3 +jasangbox2 +jasangbox1 +dcjae83 +enjoydog2 +onlymystyle +csh168 +hwchunma1 +allmarket2 +orang1011 +oneorzero003ptn +cosstore +whdlfgh90 +xspiders1 +smartdev3 +smartdev2 +guseod +kch34p1 +hoon3264 +sevenhanse7 +jnj3907 +sshnad1 +swtrading +sweet88aa +irshj +zldry77665 +yerangmall +insun0917 +chunghonline +mailspooler +limedeco1004 +kwons137553 +sysmax11 +annahra +lovegolf +carrierzone +garu12 +homeplaza +xrion20121 +gadeuk1 +sgcorp11 +gfriendgs2 +nokyawon +hyungyu4862 +winkcg5 +jkim0918 +hotaruru +esumalltr +pagwow +dhcrace +marbbal +godotalk +gskim351 +yogodesign +gadess6 +yoyojjim1 +www.garage +kds3547 +kmksound +rayull +pkjy1219 +godosiom +unisel98 +fjrmal +jasung3 +dazzlingday +gunwi4989 +ht1216 +momoyagi6 +yurimgolf +momoyagi3 +api01 +hanshairtr +xunmei14 +xunmei13 +amashin +xunmei11 +xunmei10 +mirrato +hmsolution1 +fizssy +thegeacock4 +iwanora2 +magicart1 +uncledum5 +uncledum4 +bizcilsan +mpdpp661 +oceanblue2 +suyi5316 +www.sahil +lions777 +dinplus +pighappy +hadmin.seventeen +oskkage1 +jys1994662 +jys1994661 +ssgulbi +jpl3061 +vanessa1 +madeangel +aorvhfl9988 +crikit +ross9006 +vision533 +vision532 +jessie1010 +bronzehousetr +imarketings3 +iambylee75 +ssk2231 +eversell +cafemaster6 +cafemaster2 +kmrloveu +kinglionjay1 +jooheej1 +rlatjdwk774 +rlatjdwk771 +sunspider7 +sunspider5 +fishing1231 +ds49798 +rkarkarka3 +spomate +rcn854 +destudy1 +kej8399 +riravava +seojiho3 +seojiho1 +gabimaru +s4freedevsunny +www.gurgaon +hoon2203 +jjbb1 +logthink +countryman2 +jjanjjandc +sogum92 +pkwmyth +jackie23721 +seizerdlek3 +airing2 +skliving +ld0308 +premiummyth +morningpond +football1141 +powerhong +pulip123 +powerhome +larc1729 +gi243admin +play0400 +sky27918311 +sohafancy +enjoyday7 +kws79381 +cheezsaurus +jin2v +teenketch1 +soul1015 +w3bmaster1 +rayndy +jphoenix03 +maychao +dio712 +goldflower771 +bkoutdoor +antepost +curious001ptn +bbchs123 +word86681 +jyk1516 +jinwoo792 +cocosin17 +every091 +ujako810 +nemo88883 +ryan4ever012 +lovekissye +mailnew +qkqh7z +tem2ya +poohluna +mimi5791 +cybergeni0512 +jisy0331 +mydccokr1 +yeps001ptn +cnhotelarv +wjl1005 +ssspsysss6 +jjr09251 +npblegift +firstkks1 +gopung7 +jhnam +gopro1 +gcf200 +pink1313 +s1devmimi +lpayton +ksd0913 +scmdemo +www.expert +mymeetr8173 +shoesptr2592 +park1555 +kwons135843 +yeng0827 +nanna519 +vellashoes3 +vellashoes2 +godoid-030 +bigneovega3 +snfood +rokmcajh1 +lannara51 +kerb75 +huccaci +anytoy +graceskms +s3intkthkira +kong078 +godoid-020 +harcayo1 +mikael30 +digitalhp +ellipia13 +oneorzero001ptn +digitalgo +wodms19472 +everygoods +haedolli3 +gayafntr7917 +tmbh811 +joeundm1 +digitalcp +s2fsqa +digitalck +woori54891 +dorositr5538 +www.protocol +yeil1101 +humanbear +anysky +sunoak20111 +centratr2549 +newframe +nicenury1 +potterjj2 +cafedavin +noblemobile +foxlike9220 +cjrosetr0389 +hbsfoodold +yokurt9330 +ermac +seoul20133 +ippum +bsm7801 +p4-all +narangbu1 +iktc5539 +prodrug1 +eventrain2 +gaigalu9 +gaigalu8 +gaigalu7 +gaigalu6 +gaigalu5 +gaigalu4 +gaigalu3 +gaigalu2 +gaigalu1 +kjh12143 +garam70702 +egland +soonmin2677 +semir06152 +semir06151 +lightvampire1 +tkshop-030 +tkshop-028 +tkshop-027 +tkshop-026 +tkshop-025 +tkshop-024 +tkshop-023 +tkshop-022 +tkshop-020 +tkshop-018 +raycop +tkshop-016 +tkshop-015 +tkshop-014 +tkshop-013 +tkshop-012 +tkshop-011 +tkshop-010 +tkshop-008 +tkshop-007 +tkshop-006 +tkshop-005 +tkshop-004 +tkshop-003 +tkshop-002 +tkshop-001 +kalee995 +canstudy +indasom231 +imarketing071ptn +snkc1594 +neeke5435 +oeufkorea5 +airpass7 +skyladder14 +brandfactory +kds1480 +miele6363 +hanjubnd +www.debate +skyinfini +naturekorea +schooltr7902 +hym1987 +horsecore +anynow +hanjumalltr +hklkjs1 +mn7654 +jiji05021 +gma21 +lagon2002 +digital4d +havensports +zerotest-005 +zerotest-004 +zerotest-003 +zerotest-002 +zerotest-001 +phobiasadmin +thegglim +ok00yeol3 +engdevweb +mentoree3 +smassa1 +mentoree1 +kjhzzang102 +kjhzzang101 +cocosheis +ashgirl +godopost +enamooselffs +invers132 +hada114 +woosungdt +mhm518 +ganainfo +badamokjang +pjch9472 +chocolab +wingpet +ellimtrade +leavemealone2 +khgd2743 +nekoidea +purplehands1 +chengik2 +chengik1 +mervert3 +lastchance1 +iconsu +swirlkorea1 +joypaitr9417 +kbng6852 +ivycos +qhdgkduf12 +smartttr7541 +prankencorea +khw0531 +jucy421 +t48821tr1906 +mid181 +hanyinjijon1 +limpass14 +okyk734 +autocaddy12 +www.model +gon08232 +eatbag12091 +csoulcompany +lastfactory8 +kwons134521 +h85550101 +glasslock1 +s4freeintextacy +fixisterhous +bee20246 +thsalswo1 +daewony +smilejudo1 +dollspia +bys6210 +kimilgon103 +shininghairtr +bronnum +badasatr7498 +karuselli +rainbow1004 +netant +gogotori1 +jesusfor +greensoccer +hijkl01 +servantjin +hym1198 +theshah +netstermnc +shoppingone +barungil +digdug +tsgim70 +imarketing069ptn +dreamktr4076 +serverhosting254-210 +jongyeol +mwseo86 +canada79 +bonnie2caret3 +ju7023 +yangjumal +jongsoo +xbtion99 +www.experts +cafemaruni +lauradavis6 +jin9805 +jini8013 +gogotony2 +peace0945 +opstree2 +opstree1 +gbk20731 +hijungutr +nggift15 +nggift14 +nggift13 +oliveppo111 +nggift10 +mokdori2000 +keunkim7 +pradaas +snowin759 +snowin758 +c4family +k050326k1 +celtashop +seungsunme +kdwood +god2691 +hyun29182 +hyun29181 +mrballoon +hegaon3 +ss2inctr0712 +dudtjrdl1243 +syncbird1 +pspadmin +irewithmall +pys06044 +rabombaram +coolmercy +hwabantr1679 +vkvnflzk7 +bird123 +doozy2013 +wolfnfox +victoriash +jmlotus3 +decondtr7919 +zazak0200 +lsb7138 +skinnytr4415 +pjungmee +smnews +kent90 +caviar11 +hloety +jan65681 +jakespace2 +mhotelarv +serverhosting130 +smilechan +dufmaql +kdk05131 +digiwear2013 +elccikorea +redwolf7401 +bestsupertr +k217171 +thesalt +ipayjes11052 +owlove79 +poopoo1004 +jongu72 +zeusmarket +s1devjonr +puma0310 +fitsladmin +ilviet6 +ilviet5 +ilviet4 +bizcjaegi +wnsdmlx1 +lureman +m258ss +smart11 +lobchou702 +mantis3171 +jinline2000 +ampettr0590 +kjh09221 +cpaparky1 +kpmobile +inptr +indralee +chatcentral +jtoh7151 +beefood2 +wiki1234 +s86017 +modesto6948 +iblind1 +pkj3924 +imys1 +carebank +serambank +ljc7403 +youngsun1602 +jongeuns772 +jongeuns771 +wngks1013 +ashelon +egoodnature +mazdesign +smash47 +parkhc005 +zz6kies +imarketing068ptn +rvs4me +hearing1 +rujsh13 +acerokim +starwarssi +hcg32 +ryoo711 +origin-mobile.devstage5 +miracledu +kpj7422 +kpj7421 +ssyannie1 +yuni2901 +king2112k1 +lovol5 +kwons133158 +skytears79 +sera2tr5034 +nyfriend +samlim62 +gostmmr1 +diekun +bnjey62361 +kim7866 +gmb2002 +baekjj24 +mchang934 +alcammtr0389 +iamjjoon +sofia409 +kwons132944 +sjae0111 +gogermanyadmin +wogus1302 +dwpattern +smurfet +seniorhousingadmin +bemes97 +wezenbag +blingme +onchang1 +blueboo +thepuln +chowonherb +daewang +harutr1420 +relationsladmin +waikikiboy5 +wwxkorea2 +inkoa +baby1433 +nystylist +jonghap +xsports3 +xsports1 +dksgytjd071 +kate21c +jfarm +wooritool +didrns +s3devmimi +rathers0609 +sbh1692 +kdh74331 +biniwni +repair2 +llsell1 +cheece1 +djembes +yena54250 +bestmr91 +ssomuch1 +o8naman16 +o8naman14 +jongfal +koreasansam +hkfmbooktr +sagazangg2btr +coana91 +mimartco +hks0610 +neosense +lifemma +dinamico +kyungmin-030 +kyungmin-028 +kyungmin-027 +kyungmin-026 +kyungmin-025 +kyungmin-024 +kyungmin-023 +kyungmin-022 +kyungmin-021 +kyungmin-020 +group1 +preview.equisearch +disabilityadmin +gi142admin +messe +naturelover +punzer +promadmin +quebecadmin +allstate +saltfishingadmin +zapatosadmin +gi360admin +purkat +gi121admin +guglzlo +manage2 +scotlandadmin +angelz +free8 +tatar +thyroidsladmin +gi398admin +loungearchive-forum +filmmakingadmin +rasol +kyungmin-018 +kyungmin-017 +kyungmin-016 +sexualityadmin +info123 +mhammede +sys4 +gi159admin +chillywilly +schizophreniaadmin +gi47admin +sanswitch +conliv +playsadmin +grapple +puypal +fatherhoodadmin +hp-test +sasserver +panicdisorderadmin +kyungmin-015 +kyungmin-014 +kyungmin-013 +kyungmin-012 +kyungmin-011 +kyungmin-010 +kyungmin-008 +kyungmin-007 +kyungmin-006 +kyungmin-005 +kyungmin-004 +nationwide +gi237admin +www.customers +samos +noc1 +kyungmin-003 +kyungmin-002 +kyungmin-001 +www.multistore +dimebag +montrealadmin +colours +infomed +aiman +multistore +womensladmin +prescott +costumejewelsadmin +eventi +concorsi +diehard +sociologyadmin +dogsadmin +anissa +kidsinternetadmin +healingadmin +golftraveladmin +abdulaziz +consoles +160by2 +consejosamoradmin +wjdals6626 +kim7309 +violleta1 +rerfan +vision11011 +serverhosting254-200 +kys901 +there80 +watervis1 +young1107 +p098792 +terracoms2 +yukkuri77 +vinegar2 +pacoel +lovemary4 +kanggoon72 +anegels9 +anegels8 +gojangi4 +gojangi2 +gojangi1 +shpark75071 +ilyfe +kdk03632 +ideant +serverhosting-monitor +ideanj +imarketing067ptn +edworld1 +eightday1 +kwons132275 +gandg7 +dnp3368 +newcrystal4 +enrental185 +enrental184 +enrental183 +enrental182 +gunahp +enrental180 +enrental178 +enrental177 +foodplat0897 +enrental175 +enrental174 +enrental173 +gagarin +www.go4it +middleeastadmin +gi354admin +esx16 +www.ps3 +medicalsuppliesadmin +energyadmin +gi205admin +midlandsadmin +culturecafrancaiseadmin +gi42admin +gi471admin +type1diabetesadmin +altreligionadmin +shuzai.americanhistory +allaboutbabyadmin +disciplineadmin +bestmusic +mensfashionadmin +inl +papps +cyberweb +mutualfundsadmin +amz +gi348admin +vif +alena +rugbyadmin +aaabbb +rezgui +stroyka +shizzle +enrental172 +loverz +enrental170 +enrental168 +enrental167 +technoworld +fanny +hawk2 +phuong +wear +ittest +enrental166 +enrental165 +enrental164 +enrental163 +it2gpc-039 +it2gpc-038 +enrental160 +it2gpc-036 +it2gpc-035 +it2gpc-034 +it2gpc-033 +it2gpc-032 +it2gpc-031 +it2gpc-030 +it2gpc-028 +it2gpc-027 +it2gpc-026 +it2gpc-025 +it2gpc-024 +it2gpc-023 +today09tr6057 +it2gpc-021 +it2gpc-020 +it2gpc-018 +it2gpc-017 +it2gpc-016 +it2gpc-015 +it2gpc-014 +it2gpc-013 +it2gpc-012 +rbmart +it2gpc-010 +it2gpc-008 +it2gpc-007 +it2gpc-006 +ezpoint2 +ipaygandalfwr1 +it2gpc-003 +it2gpc-002 +it2gpc-001 +zero21631 +dkmguess +xacxac1 +cheol1987 +lover5 +dgweb1 +hmdo79 +bipumntr4004 +cacaocoach +ddkcmbb +hjh0328 +mienki13 +sillabath +lovej2 +bdangam +foavm83 +pinkmania +hurun2002 +myhome6660 +jongtae1987 +jk48041 +regenskinmalltr +fortmyersadmin +gi110admin +office365 +epishon +exa +gi480admin +goasiaadmin +providenceadmin +bingbong +britishtvadmin +paramvir +commoditiesadmin +sportsrocket2 +anass +abbass +as7ab +guglzlos +chrisking +tawfek +greencleaningadmin +sezer +gabvirtual +s001 +colbasketballadmin +bintang +gi36admin +gi465admin +parasite +literatureintranslationadmin +podcastingadmin +gi226admin +photoservice +www.proyecto +ued +communityserver +wlp +womenshealthsladmin +internetgamesadmin +rickon +endangeredspeciesadmin +roca +gi330admin +gi130admin +md6 +imr +straic +lamaison +honoluluadmin +md4 +dumbinlove +kei7167 +rlatnswk241 +ided93 +rica +ilplustr8773 +lifeib1 +lonsomeyez7 +agnes0927 +hdbike +thegeam3 +arariyon +gumgee +samsung2528 +moadenim +venta21 +n1234u +midasclub +kwons131750 +lovei101 +hnkcoltd +lovebin5 +familyup3 +kkozzam2 +soda41671 +jebl4 +anjunara +jebl2 +pinklive1 +ladymatr6788 +worldline1 +imiz2 +biolink1 +iltam +julia701 +yeorimong2 +choicetech +popshoes +homedvd4 +mencheres2 +young5563321 +winds61 +ebestone +s2pintmimi +manu10251 +mysug66 +kyjzz1 +demofree +miniorange1 +miso0530 +jnslife1 +haenamtr9809 +greenayon1 +buyinktr6518 +yehdam2 +sprendid71 +welloskorea +likelove0808 +md3 +suntree8 +idea08 +miae07065 +wowgita1 +kangje141 +sweet3273 +wkaxld072 +wkaxld071 +cottiny +wkaxld067 +kwons131413 +wkaxld064 +wkaxld063 +wkaxld062 +wkaxld061 +wkaxld056 +wkaxld055 +wkaxld054 +eliphoneadmin +wkaxld053 +interfaith4 +interfaith3 +interfaith1 +medifoodtr +youngchang01 +joj159 +wkaxld042 +wkaxld041 +sjhjjang09241 +wkaxld035 +wkaxld034 +byeyourjune26 +wkaxld032 +byeyourjune22 +byeyourjune21 +wkaxld022 +gogumatr6368 +ccimart +wkaxld014 +wkaxld013 +emqodqod13 +cartools77 +wkaxld008 +kibum613 +wkaxld005 +soj8111 +wkaxld003 +wkaxld002 +winnerywc +swkcygbha +kdh72791 +dfriendd +dmsrud2131 +bigredkane +heypon3 +dnftks9du +skykeep1 +jjy6632 +msa3580 +europeans001 +s3devh +newevery +ewok +dict12 +brandvideo1 +enbmt77 +schaefer +milkhome +inwoo09 +gi343admin +imarketing011ptn +oohiro1 +suntrade +koreafarmnet +cbrr929 +madkorea +aauxxkorea +stonektr1082 +kyh43306 +kitweb-020 +starsign +theone5 +check00 +gi104admin +rlatldud331 +mhfishing1 +a1bike1 +pen201107 +chichoney +tomatored1 +jubzone +bejjang194 +bejjang193 +bejjang192 +bejjang191 +lsb4101 +shezbag +biotrap +hl3qyetr6194 +nanum79 +dbstksgh091 +jks710912 +neoscrap +yjcase1 +aonecare +alal8334 +rkahfn1 +icovertr3582 +jnlee6 +jnlee5 +jnlee4 +jnlee2 +jeime207 +jmmug +ehwabun +necjjang1 +okeedokee +comegie3 +hoontop +btpspic +imbag +citynlife5 +citynlife4 +beautyus +eno0915 +viridis5 +squareone001ptn +rcvehiclesadmin +jbseo +icleen +mindstore +www.selfservice +cnitech +sung8815 +bellhyo +evanjarin +kwons130569 +kwons129565 +w680727 +filterdm +pbhfaith +winnerspo +tower12 +tower11 +webdisk.testsite +kobu2009 +gi276admin +spanishfoodadmin +reon2k1 +s2bike +ysh2030 +troylee2 +melodicpia +beautyhs +yhoon0011 +kfccc0 +godomdb2 +boblbee +lng4132 +tinymart1 +s3devjonr +lsmint1023 +tealim +sjh20821 +nocturne12 +esll00 +mykingdom2 +powernike3 +powernike2 +luxury9746 +hmaum1 +shs51421 +geoocarina +jyj4599 +pagold74 +applimate +jhflower1 +pheonix +testprepadmin +nydsosweb6 +nydsosweb5 +nydsosweb4 +nydsosweb3 +saadmd +nydsosweb2 +nydsosweb1 +webmail.staff +gtw +router1v119.zdv +venus2 +thevampirediaries +internationaledadmin +gi31admin +gi460admin +elmohajir +rdr +searching +letsrock +ukhumouradmin +router15v20.zdv +surfingadmin +residentevil4 +gi221admin +lovepink +birdfluadmin +minkyou2 +bestkim7 +bestkim4 +rlawntls12 +facepencil.co.kr +bestkim1 +godomail +beyondschooladmin +mana09761 +imarketing065ptn +cl3 +lsa +cl2 +ecosister +cpswo3 +bellacottage3 +ucat +cl6 +canadactualiteadmin +mawahib +cl4 +ffa +ubid +infectiousdiseasesadmin +safira +atlantaadmin +hpb +sportsrocket +dztimes +afr +achref +cl7 +gi304admin +enmexicoadmin +christianityadmin +webcfg +shewt888888 +dspam +charting +hope2 +votechadmin +menshealth +gi337admin +marinelifeadmin +bbfamily +webdisk.join +infofinder +anjoman +folding +homerepairadmin +sakthi +tutm +qwerty321 +sameer +bibleadmin +e-v07eawm14601.it +mugshots +sk-joule-office-4250.its +antoine +baris +e-ssrgmrrq4.physics +e-v07faskadmin4.it +h-s-h001801.humanities +skincanceradmin +e-sskd9w583.eps +sanuki +seniortraveladmin +childrenswriting +e-swm011902.seaes +arfan +c-s-148-146.csist +www.amal +jett +h-p4-ax-m4555-146.its +as2test +mujo +h-a07huh005450.it +ftpnew +bellacottage2 +smeiwonn +stata +outsider7224 +firstenc1 +kim5058 +statictest +haeyum93 +hmarin +apaya9 +apaya8 +apaya7 +apaya6 +apaya5 +apaya4 +kim4858 +louischoi2 +jini3792 +mm4mom +ddacco1210 +huead +kwons130045 +moronokimi +sockspill +innoffice +biotis2 +jini3701 +mulkunamu +bestyj +id3812 +shmeditech +pabang +psshoe +focusin35 +dog1036 +giman018 +schooltr2576 +manimore1 +mammutyjtr6015 +bestnz +www.credit +ynjynj63 +hadam851 +yuseunghun1 +gdtest-052 +ckd2131 +coffeemal3 +mylove1053 +hoyang1999 +gdtest-047 +jnkcom +jbiz8 +jbiz5 +prfishtr0601 +messrs7 +wlfjddl46581 +jjm4555 +dayroom +gdtest-040 +yeoinmin414 +dingzzz +gdtest-037 +openmd64 +stockhome1 +ctw1013 +headcom1 +neoad1472 +huaco +temptutr +waiguo88 +gdtest-030 +newoni1 +l-v07xx7g6yc5j.it +c-tr-wm206.csist +m-a07hdb7jl35j.it +e-ssr065602.physics +mailsql +windows01 +chef-test +h-i07huh005110.it +rizwan +h-s-h004012.humanities +l-v07xx3192nkm.it +www.wz +parabolica +e-s07ska16e001.it +www-int +mail.sp +abac +quarto +webdisk.encuesta +australianfoodadmin +familymedicineadmin +healthyheartadmin +satpal +redundante00 +satria +redundante01 +fdc12 +fdc30 +mdurohtak +saumil +fdc29 +willi +fdc43 +fdc59 +drm2011 +fdc98 +dervish +detecka +pesquisaclima +www.pharmacy +selecao +portalax +e-p2-sk-m4555-076.its +saymon +l-s-a000324.it +participante +moviestvcanadaadmin +cheurfa +h-s-h001400.humanities +tsqa +tsrh +aplic +tswa +spanishcultureadmin +gi25admin +ecrmqa +vcalfa +gi490 +ftptemp +e-d07eawm0275b.it +e-v07cmcytempa.it +images01 +massinissa +redteam +fdc123 +paex +anhtam +agendamentosala +altecrm +h-p2-ax-cm6030-111.its +gi454admin +m-a07bbc7jl35j.it +aleksandrov +arab4ever +mcl +c-e-230-029.csist +cawra +lembaga +h-s-h003927.humanities +egowennasb2.it +dragutzu19 +infinito +iv +gi215admin +southamericanfoodadmin +arc3 +literaturainfantiladmin +gi339admin +tget +sbinfocanadaadmin +atsil +gi332admin +redzone +svb +www.w2p +que +micro8 +micro7 +tnetworks +goparisadmin +schultz +collegeadmin +women3rdworldadmin +kidscookingadmin +ict3 +oris +qwerqq +ict2 +motorcyclesadmin +monte +banvatoi +stayathomemomsadmin +regedit +olympicsadmin +naturalbeautyadmin +breastcancer +zerkalo +ibsadmin +micro9 +mobinet +seikei +sfadka +student7 +newcms +seldon +nguyenhung +draugiem +sde +form8 +gi480 +dif +clickmyheart +m21 +theartssladmin +m22 +m24 +m25 +nyfreelist1 +m26 +gi19admin +m30 +gi448admin +gi210admin +gov2 +gameboyadmin +lkpfdns2 +lkpfdns1 +civillibertyadmin +modamer +traduccionadmin +www.annunci +skincareadmin +guidetraining +hoai +polisciadmin +settec +romano +childparentingadmin +webdisk.cloud +classicgamesadmin +webdisk.advertising +stressadmin +ronggo +nyrelay4 +lb-dns +holidayinn +sexbeybe +vexim +testing101 +serverjava +nyrelay3 +nyrelay2 +kimokimo +bakersfieldadmin +kidsmusicadmin +perfecto +moderador +rose18 +antic +devzone +dkny +enrico +malluwap +ispconfig +craftsforkidsadmin +optin +copacel +runningadmin +grapevine.specials +shahan +shahin +gi326admin +shakir +bachho +spanishadmin +gochinaadmin +shamil +blogwide +ywfas30951 +antilo +jimi12341 +moonjins2 +wsryou212 +simazeri +naniwajapan2 +ohsfss +gdtest-020 +contraceptionadmin +songee151 +apple1772 +automatic1 +gdtest-016 +chair119 +tworldtr1859 +cynical11 +linuxand +white1tr8989 +a4dc12 +kungjundduk +gdtest-010 +peterpapa +greenbodtr +uminpop1 +punnyshock +goldsoccer1 +oiioii +erst30 +ddol50 +minuse1 +vonokotr1787 +autodiscover.lync +buzzz71 +ndwor265 +anpkorea +yoohj2891 +jake0929 +tree4smart +imarketing064ptn +metro71113 +s2pintjonr +s2prelease +s1intp +manager2015 +vincaserin +besnow +italianadmin +partyhong +newroinlt +dirl2000 +sobombee +hudullini1 +nemosuv1 +apollo6 +kkduck21 +ysh0505 +germanhorse +asadal008ptn +sorkjoo2 +fptcmfkr1 +hanseol21 +sheeker +real2009 +tsshin80 +nerrmoa +nemocase +jinutech +jdy36383 +kkj13574 +spodaqtr9175 +kgecho1 +ivy622 +dibaoi +csl0398 +bella26 +dc2347 +s3freeintmimi +gui5859 +jewel8351 +doctoralex1 +honjjang2 +honjjang1 +dadana001 +issuetracker +hskim +herbalwife +daerimi +yeinwine +renny41 +thence1 +hoonbro +sung7022 +ysun920 +smandsw +happykenny +beautifl +yearimdeco1 +hs1624 +kwtechwin2 +schoolbee1 +st0607 +hs1608 +jinah615 +ms1intsunny +abcmusictr +hades2 +kyoumetr9835 +ibsk22 +quad4813 +ansimi +themeparksadmin +vegetarianadmin +perrosadmin +internalmedadmin +simonandschusteradmin +gi443admin +jesus923 +mn1126 +kim333a +strental +osong789 +ms1devkhs +godoedu50 +kyw88371 +you03161 +pakmunsung1 +edailyedu2 +edailyedu1 +craft5 +beside +gaimod +usoutlets +gearlounge2 +kkomahouse +nyshair +anshim +skysuf +jnhsds +ttiik04211 +dwarflee1 +techdata +funnkids4 +funnkids3 +funnkids2 +pointdecal +dmsgk0728 +asadal029ptn +seohyange424 +amidami8828 +kimchealjoo1 +napsmalltr +h780405182 +itdanatr8676 +miss-chocolate +ub0222 +boysnice791 +kmyungran2 +scfactory +todream072 +gniland1 +fineseafood +bike20003 +togetit4066 +oio486 +divedicehd +dosox2n +indsystem2 +indsystem1 +hipdeux1 +hoolv33 +offician +h8100210 +gi194admin +wargamesadmin +jun-jean +voice0809 +okgotr3676 +yjs51616 +ipayneoart12 +nanrigo +vyplus +ninety89001ptn +viopapa1 +kd44573 +jenpaulrey +polishlove +konadream +smh4866 +solarzen1 +badman +shazzy +punkrockadmin +badrou +realestateadmin +loungebb +gi321admin +campusplacement +punkmusicadmin +newsblog +www.demo5 +banybany +mekhamata +securitylink +ggupdegi +ultima55101 +anrnf1 +dyingadmin +simontasker +bandbadmin +ngagutr +mik1171 +dmo +customs +yahoologinpage +presentationsoftadmin +won30051 +assa5733 +lee11362 +dmsgk0315 +koreanshop24 +daytrips1 +rvgolf +sbk2720b3 +twowax +officeoa +yhm00001 +ecoholic +cheek2cheek1 +cdcomco4 +prmart24 +kim2581 +wooripets +kbhstar +ysms8167 +livejin052 +pkh2002042 +ohmytrader5 +ohmytrader3 +serverhosting202-69 +lim65281 +s1setuptest +concursosadmin +raning2580 +peterkhsong +hive781 +golfpeoples +firstchic +jini01227 +kyo199 +dodkdnjs1 +slowj3 +nuntings2 +nuntings1 +ydltmf07101 +gbm33044 +thyme63 +rura98 +mytujana +cjstk6671 +solomon4u +pane001 +benehost +ssong2127 +park0207 +ab1315 +designpixel +jaguarlim1 +jin22yo +tymca11 +botzim2 +botzim1 +youngkey7 +suji573 +cotorro +space87674 +space87673 +onefamily +www.educatie +americangreetings +www.servicii +citate +vavagirl +tkcjsdhkdtk1 +minjishoptr +shoocream +mysopum +morningheim +noorymart +wis2st1 +proyare4 +polodona +triplelife +gagus3 +imarketing062ptn +redsky06242 +mosaics7 +mtholdings1 +ipayungiuma71 +nom03152 +banikong +tomogitr7757 +manyotr3217 +redbetar2 +redbetar1 +molylove001ptn +jini0902 +helios1201 +dumpout1 +jbkim25804 +gaguae +wjdals66261 +dlaehd12342 +looya1 +lemontr2 +oihj25 +smfrei +ipayidreamtown +ilpumctr7356 +indra2k1 +anibigtr5444 +mgarden +starhwang +mj941169 +dobicycle +ojs18071 +cbx9001 +byjay +sdeah094 +jinwoo7925 +gadimbs +wleofn10042 +wleofn10041 +okxerox +trustline1 +kos1191 +seichong +dingcs1 +songother7 +ljw0709 +bumilion7 +oiuokm3 +looup3 +uzi1003 +funnjoy11 +mungmung79 +gangnam7879 +gksdkfhd +theklee +skygg4 +skygg3 +skygg1 +dla8909 +tool2788 +khc74460 +shyun29 +baruncorp +skyduk +newbeing03 +ganzishop7 +twomax +www.citate +ldy980204 +csakorea +jreat6027 +hansan00331 +cheeseadev +jejutour +twosmedi +jjk29432 +stdevmap +ogambaby +newerakr +bkrheem +park0063 +kim1452 +mac0615 +enavisave +hanadool12 +sujipbtr6591 +poloftr6195 +mmcandletr8074 +vip51-159 +choi7901 +artone2000 +nicezip +cocokktr7385 +s3freeintjonr +sathelper +s3freerelease +gi190admin +have3031 +anonymou +dbsgmlrudz +yyh63061 +eulnyung +dcgood3 +s1m2s3 +baekbooo +guitarnet1 +tokyoshop +romancefood +hijung761 +konet762 +jini0207 +mac0420 +guess182 +guess181 +mk10042 +smessp +feeling0841 +yjh7611 +youl0411 +okcom02 +shelko +mfg +seterecords1 +ipattern +namts001 +happyellitr +godoshop-039 +imarketing061ptn +godoshop-038 +grandzone +godoshop-036 +godoshop-033 +lsh002486 +jeoldatr4599 +tyche862 +gucci21 +godoshop-029 +sejin9898 +toppatoppa +s1devextacy +hyejin1 +jjpfartr7334 +hairsootr +chi1019 +godoshop-022 +godoshop-019 +cozcoz +cmy22953 +godoshop-015 +micostr +hahajinwoo84 +servicii +hstar44 +dafm414 +dafm413 +nissistyle +rhkdsutr4690 +hkorea +kwon7717 +erosis +domaedtr1856 +leejaeheon +hwrkorea +najeeman1 +rich2girl +anandatr5825 +iraf1010 +famertable3 +famertable2 +famertable1 +songhiii +tjsgml66371 +kipa1234 +bhhanyang +smscenter +heba0905 +dydghksgl4 +godo98104 +sodamon +mmagpie-049 +s43200542 +migabetr2372 +clearwater1 +wmozart +sparrowbear1 +binudduk +enuri4989 +kdong7 +heejung +kay1239 +hkoon1 +tourlv1 +gomnfood +sheonlee +kwon7425 +artbrotber +arienail +yjs34601 +gnstore1 +iljinkorea2 +four321001ptn +gg7772 +jje1324 +majortn +simuri121 +cheyun5001ptn +iyoungmi1977 +newsz +easyworks1 +galaxy-dev +s4freedevnj +hknuz3 +horim +aloe0504 +godotechnari +tec486 +worldmtb +jin1232 +jin1231 +prmart38 +prmart37 +prmart36 +pyw3658 +multex +prmart33 +innerweb +adamgirl +prmart28 +prmart26 +bjb25404 +bjb25403 +bjb25402 +prmart18 +prmart17 +nerois1 +bdtkorea +laskastr7912 +she135790 +smarthand1 +wowmin-020 +korina21 +judy8098 +s3devsf +cometrue101 +dbwjd66602 +s3devw2 +polobox2 +cooldaegon1 +hdw0002 +womenshealthadmin +romeoja3 +s3devnj +niceman0081 +usu007 +metrokrtr +viewzone +ifood +kc93isc +blazeguy11 +mobicrtr7531 +imarketing060ptn +dspkorea +wowmin-015 +minzoro +tnr1214 +chaerripo1 +bby8047 +gpfud00 +pharos03067 +pharos03065 +pharos03064 +sonjjang77 +pharos03061 +joc911 +enbmt78d +enbmt78c +s3devh2 +enbmt78a +hyperi99 +najukim4213 +enbmt77d +enbmt77c +enbmt77b +enbmt77a +issac00 +dudunna +yup0233tr +barry.dev +psd24002ptn +armani0823 +zabes07 +isolmgtr0449 +kakaofuck18 +asiana69373 +sh7686790 +astrogate +wowmin-011 +bniwork +imarketing009ptn +yikyupok +wowmin-010 +life114 +gmbservice +monchouchou +nanoids +sheslee +eartprint +doheejjang1 +myphpsql71 +logthitr2949 +godofont +kimchitouch1 +bagpia3 +ipaylsh7921 +lks99273 +lks99271 +stylish247 +gootboy +ss10299365 +sd794615 +ss10299362 +ss10299361 +ieonet2013 +yjh6128 +namwoon +newsoul135 +ssshimmm2 +ssshimmm1 +clear2300 +egolfmall1 +casa2580 +mmagpie-019 +steaven6 +steaven5 +steaven4 +nii25846 +rangin +cc112a30 +cc112a28 +cc112a21 +lb1laxtest +ohyoungkr +cc112a11 +ikj05184 +micofus +wkdeo8605211 +ivylandtr +freeonlinegames +redmaster +web2.lax +goindiaadmin +web9.lax +startimes2008 +mc.lax +wind526 +iwell4 +oprahadmin +kcsport +ksm51362 +nicehs1 +bluetangboy +freekhju3 +aga7878 +junho42791 +metavox2 +metavox1 +naturalpromise +chammidia1 +asa4821943 +dmboshop +un50251 +hanjinho +icloset2 +bancet +bandel +web4.lax +lb2.lax +lb2laxtest +db1.lax +jobs1.lax +future125 +web6.lax +db3.lax +chorus400 +bdagape +gododownload +sung2711 +kwons123452 +sellstyle1 +jinwoncctv2 +sajbco1 +boss76772 +ibobos +jjsofa2 +hcchae33 +induk1-029 +yj5575 +mmagpie-009 +polofactory +bizkim1 +kreesys +ubridge +lstrade1 +chapter32 +ibobo7 +anskintr2156 +morefun01 +wandobada +induk1-019 +azzi425 +imarketing058ptn +induk1-016 +wejangtr3554 +gdpants +delarei +donghun72 +newav +thefemme +securityteam +web1.lax +web8.lax +lituretr5901 +yootzee +joomla30 +dpsearch +tamana +lowes +gi437admin +kansascityadmin +ltest.team +gi188admin +webclipartadmin +musicadelmundoadmin +juguetesadmin +pcworldadmin +ocdadmin +archaeologyadmin +sowyen5 +stay4321 +psnavy +induk1-003 +dance1004 +kittypaw2 +xofkd002 +nicedob +icedesign5 +icedesign4 +cutyjina2 +choi5487 +yain77377 +yain77376 +yain77375 +cristorper +yain77372 +bluecloset +nesege1 +frontier398 +metermtr9539 +kyh6501 +actto7536 +songfirm +vivipetfood +rooibosmarttr +qkdrjsgh6 +blessing1 +ekdrnqhf +turnkks2 +heyjed1 +e2n1one +lagnn081 +superdaddy +ysj312 +yhkim7594 +ewavetechtr +babynetwork +iena1 +ipaykimhega +tear32183 +tear32182 +tear32181 +whddnjs0483 +manul1009 +myusim +web3.lax +mfashion1 +almighty1 +longupzin +hjg1122 +bigcoftr9283 +gvten +ilikeshop +gosteam3 +gosteam2 +fishvalleytr +mixel77 +woom2012 +sfglobal3 +bluesee7102 +bluesee7101 +jihoon00 +s3intsunny +tec20206 +tec20205 +tec20204 +tec20203 +tec20202 +culturenet5 +culturenet3 +gi315admin +bentouif +web10.lax +lb1.lax +db4.lax +yourcloset +hcj1477 +vldals123 +thegioiso +web5.lax +princess4u3 +jobs2.lax +db2.lax +web7.lax +videojuegosadmin +forestryadmin +alzheimersadmin +healthcareersadmin +w800 +bartek +yule +w214 +kyungdo +godosg-030 +ycj0831 +ezenbike +lkfg776 +donglaep2 +hms9391 +lkfg774 +binuya4 +maxion1 +ssung2shoptr +richaroma2 +richaroma1 +godosg-020 +emodeuntr +nexus75102 +jigging +knsydmaster +thegray +farmforyou +ibikeboy2 +alinekim +sky70394 +findpc +jujuring2 +jujuring1 +ririkos +fineds +godosg-010 +bell012 +dpmax007 +soulmie +gaedle +trazenkat1 +starjung +voguenewyork +godosg-003 +gi320admin +conradkwon +doslaos +kpwell1 +misscrow1 +mininmini +swgagutr +italgagu +jihomamma +www.reporter +imarketing057ptn +gi235admin +tarjan381 +whisenplaza +ra7al +serverhosting20-253 +s1devp +bingsugirl73 +serverhosting20-245 +ingress-03.mx +cha033tr2546 +ipaykjwook7 +willgolf +joa891 +ohs530 +serverhosting20-197 +serverhosting20-195 +serverhosting20-192 +serverhosting20-188 +serverhosting20-170 +bmhholdings +choicelab1 +dupimall +mblog +serverhosting20-137 +twinz21 +winner734 +bioskinceo +anzing999 +kgol0011 +gafilld56 +icdij3 +namuro1 +gmu +jizone2 +hayantan4 +maxjojo3 +atree4u +suhosgi +skinzone +ijy8282 +zong +bestcody +yeeumtr +designfingers +bbrmom1 +qoehtjd +psr2x4 +ssitmal91 +oscal441 +zzubong1 +hommedi +chrome12 +kwandong +willn413 +gi432admin +hardworking1 +dongoodong +bobdesign +sport1131 +akila2013 +annbox +withealthtr +gi183admin +eryberry +aeroc17tr +joa494 +photogoodtr +s3intextacy +hanamu2011 +tj3651 +godoedu9 +godoedu8 +godoedu7 +hjparkkr +hojungga1 +enfshop +hoon392 +godoedu2 +godoedu1 +selrana +bl5253 +gyunwoo287 +jinandco +rodiajp +yewon09031 +pts06061 +dragoner +jejunetr1890 +creditoadmin +kiki95811 +knpiano1 +anmira +elfbada +zoomcamera +phonehouse5 +phonehouse4 +spacehue +emote12 +emote11 +lee24192 +limgaram2 +csa251400 +mdpromise +pola1206 +kbk14481 +kmc9556 +codegears +midashjs +ingress-02.mx +mity0312 +lkm5282 +slr365 +flower2580 +hasimoto27 +ebiztr4968 +cubeintnj +oipmaltr6333 +mk05051 +jun34784 +leemiddleton +h1a2n3 +www.vivasms +hklkjs +washvath +twins61 +jinaleebbo +hikiake3 +bbosomtr7474 +vivasms +fool21c1 +ksong83 +schoolfun +lucky8669 +hakpower +hpvalley +wowwoman1 +gyuho9898 +cmstore2 +cmstore1 +gardena1 +workusa +merveil +dimiwon +iblind +godotest-012 +imageshack +vidaverdeadmin +parejasadmin +godotest-011 +bluevitamin +godotest-010 +bimp1234 +aromari +italiansladmin +godotest-007 +cicokorea4 +cicokorea3 +royaltyadmin +gi253admin +ingress-01.mx +bicicletasadmin +shibuya +secportal +infohelp +neverforget +murad +a1webmail +cadadmin +a1mail +roppongi +zone100.cepi +mandarinadmin +jahanara +gi299admin +alfian +iden2 +marketing.team +mhh1110 +jukoline +gtlife +dosevent2 +sejin5774 +sejin5773 +keongdtr7204 +lntnet +biomam2 +biolink +shutup +gothailandadmin +lb.www +preventbreastcanceradmin +miamiadmin +autodiscover.g +autoconfig.g +www38 +classiclitadmin +gi426admin +brandenburg +dietasadmin +nymph +priyankachopra +chelyabinsk-suu-rr03.backbone +alfa-romeo +gi177admin +nicu +rr02 +math3 +teleworker-sw-campus +financialaidadmin +snapdragon +benevita2 +benevita1 +cplant4 +dooob2kh2 +dgmart +yipsaac1 +dgmax1 +daehanmusic1 +kjw5525 +bejjang19 +ok258025 +lsa4121 +uchoice +anna18 +alimotr1558 +atonbtr6079 +dakineshop2 +yohanis3 +dbsaytns +smh731 +iclay +gododemo +hostingmanage +ipayticgirl +swisswatches +woorisai +ohlady +ssomina2 +kimeraj1 +artdepeau +choin11251 +jludia +bluangelo +leehansl +mmmobile3 +yjoung101 +ohkids +jung3568 +tennisadmin +swju555 +schoolbee +formula1admin +unisum +mflady +ahj19752 +ahj19751 +lounge.team +jslee369 +tigersm07 +officeboy11 +mi100942 +banybany2 +jungubox +hjudew +whitehtr0803 +proposals +cubeinflux +elitebasic11 +copymine2 +gilin575 +sindo8710 +toyfleatr +enfantstar +gonewenglandadmin +paransys3 +ipaykonvision +kinoprida +jikyjeon50 +jikyjeon47 +jikyjeon46 +jikyjeon45 +jikyjeon44 +jikyjeon43 +jikyjeon42 +jikyjeon41 +jikyjeon40 +swingfire +jikyjeon37 +jikyjeon36 +jinimall001ptn +jikyjeon33 +jikyjeon32 +jikyjeon31 +jikyjeon30 +guava1 +jikyjeon27 +jikyjeon26 +jikyjeon25 +jikyjeon24 +jikyjeon21 +jikyjeon20 +sunhd20026 +jikyjeon17 +didwogh22 +jikyjeon15 +jikyjeon14 +jikyjeon10 +hlch81 +hkoon +simuri12 +fcdesign5 +mrviura +kokomi2012 +netlabs +timestore228 +imarketing055ptn +edge52 +tvdramasadmin +zaengyi3 +zaengyi2 +yy1516 +jejuolle +metavox +reactiv1 +icdvd +lseinlondon +hkmug +bbqtown +yain7737 +mountkorea2 +taol1000g +soritong +hjs98824 +hjs98822 +monaco0421 +plusdiettr +seebuytr3276 +raisis +thefeel +enfanbebe +ss2004 +remott1 +honor +travelceo +maniastore +yauoo05051 +gswb2 +cubeintw +ii22ee +gamecp +ihack +aromero +dbal1126 +cubeintp +wellpeople +cubeinth +ulfarmer +cubeintb +nowlovetime2 +songchoi +www.tsm +vantruongvu +bhaskar +phish +magicsladmin +saunders +neotech +mohammd +corinth +gohongkongadmin +modelrailroadadmin +nuclearpoweradmin +halstead +gcreports +shorty +nyakamai2 +nyakamai1 +acheron +rankpeople +gi294admin +sanfernandoadmin +buddhsladmin +adra +thecrims +osteoarthritisadmin +financecaadmin +tariko +tarkis +canterbury +mundojava +coeus +onlinebusinessadmin +weighttrainingadmin +gi421admin +corrupt +gi172admin +dek +foodbeverageadmin +islamsladmin +support-us +santoso +nse +victorian +start1 +pytha3477 +m1300m +stickers +mohmmad +vlasov +urbanlegendsadmin +businessmajorsadmin +mediabank +www.iep +sexsladmin +socialanxietydisorderadmin +anhdep +americanhistoryadmin +milimetr +lms-test +harlemadmin +gi98admin +audioconf +snies +wadas +envivo +whitehat +rygby +goooals +www.artem +rengeko +benyamin +tmi +comunicaciones +unknown1 +nyquist +gi288admin +japaneseadmin +medecine +humorsladmin +dgt +thedexter +lswebconf +abdelatif +nyrelaytest +afv +medschool +mailsecurity +loggers +danceadmin +nesetka +zaryab +gi405 +lingerieadmin +ouray +expertsearch +www.manuals +berilo +pegasus.cc +www.tamer +gi415admin +gi166admin +eteam +intranetsadmin +bgboss +tpi-pdb-scan +anupam +weavingadmin +anusha +taxe +cytrynko +cellphonesadmin +sarki +www.taxe +gi369admin +arthritisadmin +calisto +anyone +cgi1 +fairfieldcoadmin +jamadmin +fisc +lovegate +riverbend +gi400 +mifa +hendrick +www.fd +eric7 +sparda +duel +357951 +pharmaadmin +criminologycareersadmin +tibio +adminsite +machinetoolsadmin +giaitriso +gtools.team +myold +dheeeraj +netlove +chikago +giveaway +englandseadmin +palmtree +gi93admin +dahmane16 +albuquerqueadmin +cefa +firewall1 +romanticmoviesadmin +www.hpc +autism +gi283admin +netopto +animalcareersadmin +dallasadmin +gly +postresadmin +corpuschristiadmin +sohaib +herbgardensadmin +psportal +external2 +generalhospitaladmin +arabtv +sitesearch2 +travelwithkidsadmin +newsdesk +teqnia +schooltest +autoconfig.webstore +autodiscover.webstore +birdsadmin +webdisk.webstore +incestsladmin +lindsay +www.gutachterausschuss +squish +difference +punt1bdd +punt5web +bankruptcyadmin +arrel1 +hirawan +arrel2 +punt3web +mail.rebots +glt +dnsgrupelpunt +zdravko +punt1web +persuit +sofiane +punt2bdd +nicedogs +speedo +sonice +punt4web +acidrain +arrel +punt2web +punt3bdd +gi410admin +shuzai.militaryhistory +gi161admin +spasadmin +kommunikation +washingtondc +enchicagoadmin +informacion +legalindustryadmin +shosho +www.zapisy +jabba2 +familycraftsadmin +yogaadmin +biggs +hnptuyen +hwvaxwp614 +thanos +sososo +legalcareersadmin +lucky7301 +witcommerce +mysille +dbc2191 +canoeadmin +quigon +ccbyungkr +ya09uni +ohjima +kkimtony +mansuvv +jijiwoong2 +linesence +yegalim +padosory3 +hihangongjakso +kwtank +greatewoman004ptn +annasui071 +idgodo-040 +idgodo-038 +lkm3473 +idgodo-036 +idgodo-035 +idgodo-034 +idgodo-033 +idgodo-032 +idgodo-031 +idgodo-029 +idgodo-028 +paddlingadmin +proxy1d +classicpoetryadmin +gi87admin +palpatine +gi277admin +lostadmin +energyindustryadmin +kdhap +okcadmin +soulro +goswadmin +dooku +getenaks +kooora2 +contestsadmin +idgodo-027 +idgodo-026 +idgodo-025 +idgodo-024 +idgodo-023 +idgodo-022 +idgodo-021 +idgodo-020 +idgodo-018 +idgodo-017 +idgodo-016 +idgodo-015 +idgodo-014 +idgodo-013 +idgodo-012 +idgodo-011 +idgodo-009 +idgodo-008 +idgodo-007 +idgodo-006 +idgodo-005 +idgodo-004 +idgodo-003 +idgodo-002 +idgodo-001 +ruis4u +alacmola +star38407 +star38406 +star38405 +hugreenplus +star38403 +star38402 +star38401 +chamgaram +zigzeg +icas99 +vlc-aacs +vlc-bluray +tabletennisadmin +newgame +khs535-010 +hoodtee +ing37771 +bkml.m +ovariancanceradmin +khs535-006 +khs535-005 +khs535-004 +khs535-003 +khs535-002 +khs535-001 +kmoon70074 +geosang3 +whalehh +minside +junsic-021 +yrsong06291 +hinokilife +seesawi1 +cpb56014 +netzantr2633 +cpb56011 +junsic-019 +world09 +umchichi +ice68 +mallandmall +free55661 +foxyshop3 +eqtech +ms1devsunny +gi394admin +yym0214 +grym2 +grym1 +kurare3 +tandymalltr +kmh5007 +imarketing054ptn +ssnbackupsvr +andongsoju +godobill +hwaldo1 +motm2464 +mebaritr7105 +estella1 +djsteelkih1 +airwalkmall +nativeedge +ultrahiya1 +chsong0505 +itac2500 +coollake2 +coollake1 +rainykk +homenlife +sizer20131 +junsic-010 +dawoom +gabang +ckc1407 +ihwasports +tigerlk1 +huehouse +wooricat +woodcatr1429 +babysitr0459 +smilelifeje +living2u +arkhe307 +kcy720 +lois99 +leonrider1 +gayacctv +hana18753 +tdaehan +petereon1 +skrghk +nalarizone67 +cl0521 +reweb2 +vogue21c +easyguitartr +jerum2001 +sqoop113 +sqoop111 +dal1143 +dal1142 +printout2 +jimmychic +brbrjbr5 +brbrjbr4 +woorigolf +brbrjbr1 +zzzoo +icaffe +wildfire +pocwebcast +softnet +gatishna +ashoka +baselgold +yumehinoki +zetmin4 +foammake1 +zetmin2 +hlanuep003 +kaspersky-serials +ssc-contentinfo +girlspouch +hlanuep002 +ilovedream +hlanuep001 +ashwin +pocmail +mobilevpn +cwvvpn +edrfnep212 +ssc-www +edrfnep211 +hudtest +alexro +googlle +arabnet +envoy +rure10011 +pjjpjs1 +ljb2644 +ibinfo +corptt +aaronshin +dukeland2 +dukeland1 +again789 +geddy751 +kimsil7252 +running21c1 +www2saml +tekocokr1 +drfswitch +uneecase +mdworld1 +tammy8321 +qkqh16171 +jhhan7512 +zino15131tr9875 +godoweb11 +ghsenfk +hhhqnwp007 +jen06152 +jen06151 +ww11721 +gookyuny2 +guandki +blue05722 +jejunet1 +littlewitch +hwvanwd1054 +chj1013 +pass69084 +pass69082 +aoh007 +sdh6161 +kidsss1 +songane1 +vfaccom +raja88 +hudadak2 +chally0524 +funfromfun1 +bbsbaby +ckw0467 +givekorea +gbs7071 +lls2ll48603 +z4ever1 +eatthestyle +hipet +nanikr3 +kongirang +imarketing053ptn +manualprime +nalraribebe +hope61371 +goho19721 +ezrock2 +sjyshs +optomamalltr +chic1215 +bultaewoo5 +bultaewoo4 +bultaewoo2 +buyheart +cw1537 +cw1531 +jiran0513 +haninara +will02304 +will02303 +will02302 +psd24001ptn +tj0115 +qbike96162 +shcompany1 +faboosh +vid1 +antiqpia +wooyeong3 +wooyeong1 +prendero +imarketing008ptn +callan +crimeclub +runxrunmalltr +saebingagu1 +wjj1876 +cleanshop1 +tj0012 +dal0357 +daegasports +coexmart +jejumitr +fone51110 +iview1 +arabweb +sipxt +xpsh1104 +arabtimes2 +caz +superdotadosadmin +gi155admin +winnipeg +xpsh1102 +inetdream2 +coscar +remobil +s3devdarknulbo +miocell1 +skagns75 +hill8 +lyspsw1 +busanedu +metaprov +manstone +jejumiin +kst14022 +grr21 +kjkim68031 +cfmallcash3 +cfmallcash2 +anewface8 +withusmobile +xpsh1101 +emall24 +pleatsme1 +binibini1 +hanhee2119 +elannep511 +cashdesign001ptn +hhhqnrp042 +bumilion19 +bumilion18 +bumilion16 +bumilion12 +ayouki20131 +skw620 +investtr6501 +js9441 +evol213 +evol211 +yaksontr1850 +greatewoman002ptn +lwy0302 +hwiyun +blackpc-020 +blackpc-018 +blackpc-017 +blackpc-016 +blackpc-015 +blackpc-014 +blackpc-013 +blackpc-012 +blackpc-011 +blackpc-010 +blackpc-008 +blackpc-007 +blackpc-006 +blackpc-005 +blackpc-004 +cino007 +blackpc-002 +blackpc-001 +ipcsung +phonkebi +eheh49363 +yahya1233 +sc55862 +sc55861 +optionsadmin +gjithqka +publicrelationsadmin +gi310admin +comidaperuanaadmin +gofloridaadmin +upimg +lounge.contribute +businesstraveladmin +gi82admin +terrorismadmin +gi272admin +gi40admin +publication +admin2010 +dl360 +taxesadmin +dramaticmoviesadmin +gonewzealandadmin +babyshoesadmin +gi230admin +kauaiadmin +sandiegoadmin +bostonsouthadmin +homecookingadmin +diyfashionadmin +gi388admin +gi149admin +ftpuk +gi486admin +classicrockadmin +lowcaloriecookingadmin +powerize +quotationsadmin +detroitsuburbsadmin +compsimgamesadmin +mwsladmin +hqfailover-css2 +compactiongamesadmin +hpc1 +federalcontractadmin +onlineretailingadmin +gi76admin +gi75admin +gi266admin +sweetboy +todaysladmin +jpagent +babyadmin +barmssl +christianmusicadmin +ctan +tabiat +highbloodpressureadmin +heartburnadmin +hakers +hollywoodadmin +deano +gyncancersadmin +movingadmin +gi383admin +ftp.members +gi144admin +mxc1s +assistivetechnologyadmin +thisweeksladmin +adolescentesadmin +prowrestlingadmin +nytoolsmail4 +nytoolsmail3 +websql +basin +nytoolsmail2 +nytoolsmail1 +kddb +xtremesladmin +prolifeadmin +gi71admin +kariyer +zai +gi499admin +muhendislik +sjakamai2 +gi261admin +boxingadmin +mxc1 +englandswadmin +nlsl +ahead +gi329 +gi333 +netbeginsladmin +oldradius2 +hindusladmin +gi377admin +ultra3 +gi138admin +jobsearchcanadaadmin +courriel +gi330 +bibweb +lawadmin +ukfootballadmin +beginnersinvestadmin +ip-reserve-139-126 +northbeachadmin +baltimoreadmin +toysadmin +musicsladmin +drugsadmin +militarycareersadmin +gi208admin +ns2v35 +ns1v18 +gi65admin +curio +coolmasti +gi494admin +gosanfranadmin +pirateradioadmin +basketballadmin +x28 +x25 +gi325 +x18 +x14 +freethingstodoadmin +internationalinvestadmin +m08 +frenchcacultureadmin +m09 +bdsmadmin +nurma +m06 +m05 +eor +gi346admin +cm.equisearch +hqfailover-css1 +x64 +hwvaxwp072 +nthhqsmtp2 +chatwrite +tictac +entpsl +hlannwp010 +totoro5948 +selly19 +selly18 +haagen11 +acervo +hlannwp005 +hlannwp003 +web3dadmin +hlannwp002 +hlannwp001 +gi372admin +hlanunp003 +bizpro +hlanunp002 +gi133admin +hlanunp001 +anchor +iis-mapping3 +trunks +darklight +elannep311.exh.prod +devilboy +elannep312 +elannep311 +lugia +hudboxdemo +testswitch +hlanudp001 +aulas +cybki +astyle +homeschoolingadmin +devmobi +poetryadmin +sgglb +hudgatelm +btenroll +hudappsint +obgynadmin +hudappsr +lanswitch +hhhqnwp001 +nthhqsmtp3 +moonnet +hhhqunp003 +hhhqunp001 +resumes +gi317 +hudgater +hhhqnrp021 +hhhqnrp020 +dannyboy +poppy +mfpilot +drgr +hwvalwd3231 +sustainablelivingadmin +louisvilleadmin +taras +elannep313.exh.prod +auth.portatest +ilannatv001 +lhfailover-css2 +lhfailover-css1 +hlannwp004 +oshcgms +cracked +gi5admin +proxy53 +proxy31 +eir +bellona +afroamcultureadmin +mazu +hhhquft001 +pictest1 +sftptest +fedtraveler +stepan +archana +stephy +consultingadmin +mobileenroll +lansslvpn1 +homeworktipsadmin +hostadmin +hhhqnwd007 +elannep313 +pwctest1 +spandan +sfgis +hudstarsr +test786 +mangusta +euthd +lanvpn2 +peripheralsadmin +lanvpn1 +czqwa +gi60admin +gi488admin +proxylm +chemistryadmin +acko +gi250admin +internationaladoptionadmin +sgvpn +hwvauwp059 +ceejay +sudeep +fotografiaadmin +hamada +horluep903 +hlanuep902 +scifiadmin +hlanuep901 +nthhqsmtp +cwvglb +foiahud +gi309 +kothari +www.ezec +flirting +mx39 +bbnbackupsvr +americanfoodadmin +testest +hudmobiletest +hwvauwd491 +maynard +hudcomp +hudlist +moroccanfoodadmin +xpsh2104 +xpsh2102 +blogsespanoladmin +testrun +sufyan +videocast +faizi +xpsh2101 +bitdefender-crackdb +collegeboard +tesal +worldfilmadmin +duhokforum1 +gi308 +maroctimes +elannep312.exh.prod +uiv +hwvalrp1162 +hudstars +sulake +habbofans +some123 +fhadirect +alarab +stuffs +laksslvpn1 +quizadmin.historynet +fasda +portatest +hudgate +hhhqunt001 +greetingcards +xxeniks +btmdm +mywebhosting +horluep003 +classiccarsadmin +ftplm +lounge-forum +hehehe +gi366admin +hwvanwt415 +sg125 +lowcarbdietsadmin +kovardo +sg124 +sg122 +cessco +xerblog +sg121 +sg120 +swapna +asianhistoryadmin +sg112 +sg111 +webdisk.launch +dev3.www +sg107 +www.launch +ofelia +ayesha +booker +freshaquariumadmin +gi127admin +salafi +tenisadmin +pauta +videoserv +hairremovaladmin +pinnwand +gi399admin +wishop +suzane +kspack2 +cc1115 +anhy00 +potato76062 +zooicl20 +adc79 +moonsteam2 +tonicyhg +maeumsori +shenring3 +ddongbalsa +fjrzl4758 +hepashopping +loveholetr +kmartkorea +d2icide +ey0506 +byhom7 +manucare +eneskorea +thechae +cotm974 +junek001ptn +icaller +retrofactory1 +durifishing +rpmuno1 +petitvelo +biologyadmin +zziny1004 +naju52tr7662 +nikonpark +sskim328 +yjun23c +seuhong999 +homtoy3 +prestige3 +gate.iitr +jee.iitr +thebom3 +nakwonguitar +pgadm.iitr +spanishsladmin +gi302 +internetbanking +mioh25801 +niaplus +u1trading3 +u1trading2 +colakids +smstory +s2pdevsky +merrymac +smstore +artemoa2 +kang0107 +cheongdam-039 +warmgrey2 +warmgrey1 +mario1812 +s2pdevsdg +corbu2 +econatr4735 +cheongdam-029 +jmet.iitr +mba.iitr +angusn +joont9995 +blanden1 +anguse +sky19991 +ziope014 +ziope011 +cubeintsunny +touch182 +cheongdam-020 +puzzlebebetr +chan9485 +saicorp +worhkd4 +gustjrr223 +byeol0486 +allfocus1 +cheongdam-010 +steamptr6064 +sjtrdco2 +chakra +www.iitr +photoupload +mail.iitr +syncovh +daehan1 +woolee +helloeunji1 +bebiromie +tieworld +ex8888 +hmmedical1 +kamin711 +hdw01241 +min0gomin0 +judy0403 +lk11191 +karajo6 +scentedmen +karajo4 +yonghun2 +adminovh +gest +adobe-crackdb +virtualtour +hazellemomo +thecamt +ipayssnchbe +www.tsc +greatewoman001ptn +egoodesign +lohas1 +clubtsp1 +sseryun +thearte +ehdqkd1gh +buelin +moin21c +logjin +lawhuaaa1 +kyh0080 +zatool2 +queen6c36 +jjanga2010 +indycomics3 +tnrruddk1 +artemin3 +uturn051 +catholicismadmin +core71 +lesswire +astinpark6 +astinpark3 +odaesanfarm +yesoobin2031 +joyparty +grab3 +grab2 +fhxjtm12 +danidud1 +inucare2 +seunghwk +muni63 +indomart +moneydream +chan9067 +sky3371 +letier2 +apitrading +bell01 +www.warning +mark2164 +mark2162 +yhuj12341 +bigcoftr2817 +quiltscent +graypsycho +lrs0115 +bookinmylife +love56742 +s2pdevman +sweetool1 +program11 +orbit19795 +imarketing051ptn +s4intsun +seed9493 +seed9492 +birdmarine11333 +danggal21 +yooohco +sbarasee +natural8426 +kwonss +kjlee95a2 +crosscase +moifood +jjoonjang +hanapack +jooho0830 +ara8508 +kwons6 +jongromedi4 +wonu26 +wonu24 +wonu23 +wonu21 +xn3v4bl9ggh +yhkim0731 +didskatlr +glsbike2 +namphtr +livingnice1 +xwing911 +wonlyo +hoya8749 +amw610 +myomg1 +s4intsdg +schg20092 +kjc01kr +haanvit7 +zzi33 +fotoware +inomvala1 +zenithtr4611 +kang23277 +inashop +kingkong772 +hansung20105 +hansung20104 +hansung20101 +ugreen7001 +theallo +woobul +abcsports1 +yunm2581 +thing +thewellbaus +nrfworks +electom +facednd +qkrantjd77 +woodc2 +adoptionadmin +gi483admin +toboju +figureskatingadmin +wpadmin +dimelee +wawagift +yooriapa +rudxo1 +kkn0428 +dogbakery +getmind7 +cubeemom +good031004 +kdy8412-009 +girocall +mskye51 +masterblue +nasa01 +sisleeeun3 +sisleeeun2 +durihana2 +zyo21 +halfpangpang +aleatorik1 +aneunjuone +rladlstjq01 +baggno1 +paris11191 +yyhee3300 +hejan855 +hejan853 +kongstyle14 +hejan851 +enflqn1 +mceshop1 +a2golf +stardu12 +rkeltm0317 +jayminapp +happyb2012 +seoultree +www.clickme +unitrust1 +nasa02 +ekgmlqkr1 +ini53533 +ini53532 +jsu20002 +jsu20001 +otamin1 +hiyazz +s1intkthkira +elimtrade +yhseom1 +ipayidugi +jmgagu +busanbank2 +umnine +clubthea +hexy3 +beggob +kkamandol +domemart3 +elecpia +keh0527 +imarketing049ptn +tvcomediesadmin +cher90 +186 +gi244admin +kancha +history1900sadmin +forgotten +credits +cfd321 +kidscartoonsadmin +chetna +honeycom142 +kwonsyy-001 +hyunjoon94 +s4intkhs +mkyungro2 +performanceartadmin +gi300 +gi289admin +onelifetoliveadmin +couchcrunch +namph49 +namph48 +namph47 +namph44 +namph43 +namph42 +namph41 +namph39 +namph38 +namph37 +namph35 +namph34 +namph33 +namph32 +namph31 +namph29 +namph28 +namph27 +namph26 +namph25 +namph24 +namph23 +namph22 +binsbench1 +buyhalf3 +namph18 +namph17 +namph16 +namph15 +namph14 +namph13 +namph12 +namph11 +namph10 +kec860 +jungsiki8 +jungsiki7 +jungsiki6 +jungsiki5 +godobill2 +jhb1044 +hanaro3894 +tpdl1001 +smc105 +midas574 +bbosomtr1268 +canscan2 +wji8039 +micarewiz +yumji831 +nouveautes1 +myc81014 +myc81013 +spoonz11 +kyg7480 +shoutzzang5 +shoutzzang4 +idman77 +dongja700 +ipasscom +centrial1 +opatz001 +foxya331 +rosaflower +ipayjunimall01 +egoist139 +mommy10443 +tobi41141 +sella76 +ripsoul +treenwater2 +yoobooral +pirenze7 +zenhide1 +wondas +kik8704 +godo199370 +wonbox +mucompany +geosungnc5 +onemulti8 +onemulti7 +geosungnc2 +geosungnc1 +wtrading88 +ktk09931 +naver1968 +akstjr782 +akstjr781 +unigown +financecareersadmin +golf4 +demos2 +cry74stal3 +sjhahm2 +lovely9679 +sorkdmsdud +unisoft +sleeky +prmory +eggirl0034 +yulki83 +julenom2 +ssunjun1002 +ssunjun1001 +xalomyx4 +seoultile +erkekorea +printmate +farmers9 +glass76 +qinfo3 +qinfo1 +godo199087 +beesek +wooree01 +ccs10203 +nara2013 +namodiy +rkarl1127 +shdy815 +carbitna +pmj3808 +x-large +ddilbong2 +yabes4u +jebongzzang +tcctv2 +tcctv1 +ky741209 +pjinside7 +nfriends +s3devsunny +cool87 +jimipage +dharltr3162 +jomichael16 +jomichael15 +jomichael14 +jomichael13 +jomichael11 +bush2080 +whitelux0223 +moon36085 +gogl2 +imarketing048ptn +iamss2 +aromacandle1 +keugkim +corinlhw1 +totostand1 +gloria75 +gloria74 +gloria73 +sinyuntns3 +jeyoon0429 +okwatchtr +eoqkr12 +cpla2k6 +cpla2k1 +gloria37 +mylure +geeker2 +fifasp +yeonsung-030 +rosemelia1 +prmanager +shoesmong9 +shoesmong8 +marchespublics +shoesmong7 +shoesmong5 +shoesmong4 +jsh167551 +shoesmong2 +shoesmong1 +alwns7 +antoniuse1 +take2650 +footbox +namseung13 +kj5778 +radkay +sunrisejr +hamsukman1 +xn21tr8635 +kinosida2 +muai50313 +zhenlong98 +bodasadmin +cubedevw +lsmnice +cubedevp +edana021 +cubedevh +kweek10882 +cubedevb +plum33size +poon5404 +kidslaktr +stepparentingadmin +phone4tr3183 +cafeier +odaemanmul +gy01142 +gilenge1 +you81133 +qldrnrdl +bytherin +dasanmedi +fafa3fafa3 +costcotr8018 +kjb7594 +soarcom +smcommerce +jalgreen2 +timekorea +hongik91tr +jjjongs1 +file0309 +sjgift1 +godo198146 +mokpofood +youhansol5 +goodday0298 +imarketing050ptn +holyspi +ohs26251 +gog12 +cellexc1 +amga +gi287 +smartdragon4 +highsuccess +endlless15 +brandshine +mom0won1 +godoedu43 +mrpieinewha +jurmy842 +jurmy841 +golf1sttr +godoedu39 +kimsony8 +seiber33 +s1devwheeya88 +dsa08221 +hoon3922 +agnet76 +hoon3921 +skinsale +hongsi1 +godoedu30 +hongsan +vaticanhouse +howdisplay +psclub +tapmill1 +n5821 +hongsd1 +gmpkb +bagsaseyo +gtech1 +hanaro2187 +godoedu19 +packtory1 +godoedu17 +dekill1 +xeve12 +js253830 +totorez +unco77 +lavender2tr +imarketing047ptn +hong7904184 +yjkim1130 +bikerush +guitarand1 +cape +alicecoco +final13911 +ptg2020 +eukim2100 +ksuppcts +ggghwe1 +igamenettr +starcany +bluemaster6 +bluemaster5 +melon2 +melon1 +sswu20052 +jnktra66182 +jnktra66181 +mymee1 +wntmvk +gms9776 +elinfit002ptn +independentfilmadmin +frenchfoodadmin +godoedu-030 +aiia1124 +jiwoolove4 +jiwoolove1 +kjk133 +daynnitr9735 +godoedu-023 +devmap +cnutsm +lee07982 +ehoh2010 +ensheet +smith44211 +godoedu-020 +sang198512 +jungyeosa +gadogolf +bosuly1 +nandii2 +godoedu-010 +sem06053 +boasguitar1 +sem06051 +dlcnswk3 +monter +oraprod +yunchulwoo791 +qimo20011 +cooky73 +pcquinoa +p747715 +wang11111 +gf4946 +botamedi1 +runice79 +comism +banzzake +tksrhkruf +qwe1090 +mathlove +dm4leaf +dongbubio11 +xnells2 +saengi771 +shj337 +eternalblue1 +nffood +caga777 +raboom +dosanet +gmis3 +crmart1 +modurental01 +paintong +playhome1 +masaru202 +hongkal +rixkorea +jangpantr +jungs337 +outliftr4175 +kdsw28003 +debien001ptn +living365 +uncbag +bycons +melias +na84972 +dwcho3004545 +campingrover2 +campingrover1 +jinrose792 +y2k8711 +shdbsrud +hongo71 +dltmd0829 +tatacompany4 +enwj1234 +ongzi0118 +hedgren +bijoukorea +young76oo +bosun09 +tikikite +hyosocorea +klstory21 +dever2 +theeden1 +ezpyun1 +usenetadmin +imarketing046ptn +brunyeux1 +canagroup +hispace1 +dkdleldkdlel1 +giftzone +moongubox +maximum1 +manmin2 +sportsgamblingadmin +yptech +hangs0809 +nnbworld +mysanso +allder13 +non3001 +hwanz32 +funis +baehouse +dnfskfk +elinfit001ptn +yoojong +godo3d-040 +godo3d-038 +godo3d-037 +godo3d-036 +godo3d-035 +godo3d-034 +godo3d-033 +godo3d-032 +godo3d-031 +godo3d-029 +gi122admin +us.img +onlinebrokerageadmin +godo3d-028 +godo3d-027 +godo3d-026 +godo3d-025 +godo3d-024 +godo3d-023 +autoconfig.co +darkrider +maquillajeadmin +godo3d-022 +godo3d-021 +godo3d-019 +godo3d-018 +godo3d-017 +godo3d-016 +godo3d-015 +godo3d-014 +godo3d-013 +godo3d-012 +godo3d-011 +godo3d-009 +godo3d-008 +godo3d-007 +godo3d-006 +godo3d-005 +godo3d-004 +aiesec +webdisk.co +godo3d-003 +godo3d-002 +godo3d-001 +hubpage1 +wchyun06281 +jiwoomessi +mindtesting +kkjj09241 +rabine +sujipbanktr +eqlinc +nimirrr2 +iking783 +passion973 +nalarizone672 +nalarizone671 +drum4989 +cigarahn1 +faville +promise100 +woorajil +sagabang7 +sagabang6 +ipayhoneyshop1 +bat87442 +sagabang1 +peachoi2 +peachoi1 +s3intnulbo +rabick +onoffstore1 +minfabric +khskorea4 +nande07 +csgbboss +brbrbr86 +jeanmania1 +bsta2tr3009 +madamyoon +roi +blossomandco +haryoh2 +patamania +etrang73 +dux +shinilpack +musclebeef +daebo99 +hfa +doowool7 +doowool2 +hanbell5 +collage0071 +moulay2 +rif +fessu +imstore3 +xboxdesign +superpasha3 +nvisual +a2core +dyota2080 +dlwogus1 +vitaminstore +redlife821 +streaminglog +nhretail +earlychildhoodadmin +houstonadmin +ipaybarun0900 +vanessahur1 +mrlighttr +sorimaru +romantiquej +lovems2756 +nbgkorea +less751 +lygll2091 +bunyoung +godo191622 +phonebuy +say201231 +nospin +mira8724 +myjinan2 +herbncell +feel2025 +manmart +lemoncandy +kyung3043 +kyung3041 +bboglee76 +icandoit7a +rubi82 +mansu382 +tsgim7012 +nani427 +somsimaepsi +acnenomore2 +sjskr1 +byo37441 +emma1981 +devnavercheck +hbook +jyp00316 +hwh6366 +thvldkfhfp6 +godo195737 +sujip-dev +imarketing045ptn +linux4 +mejoos +kwe3838 +ti2214 +shmarket +danieljung81 +kimteddy +godps6223 +meeples1 +simac0603 +xmfkdnak +essyoon +enjoyetr1820 +jooajoa11 +biobkj1 +airwalkkorea +dmsghk1983 +hitguy +ahabeauty1 +gobraziladmin +s4freedevkthkira +japanesecultureadmin +bagstyle1 +skinnyco +wkan200b +polebeancci1 +wowavtr7884 +oksmoking +92food +aps3332 +omniremote-crackdb +motionpixel5 +kyh22272 +beaure +dooderjy +totalsds +sp4510041 +sungcho91 +hwangsj +cnr1004 +kassanobada +autocntr8491 +hi-ho +www.league +nagne159 +crysy2k4 +s0aman +yeunkim5 +bsretail1 +good1985 +eatbag12092 +gi48admin +gi477admin +toltec +pcf +anno +beatman1 +beatoy +image110 +nemodale +piaoyj7 +hcbig +image105 +harridan2 +bakeryzone +ohhyuk96 +assistking7 +triratna +foryoutr6147 +gmdrmslee +cjk1979 +amilove121 +ymmink1 +doremitr8685 +ara3080 +dugilb2b +showrang +jinyingyu800 +alsso9 +pkh0214 +hanshatr0859 +beasia +yabamtr +anart3 +ssunworld1 +je78kim9 +unicoh1 +njmal88 +hwajin0 +mjnamja +ebedding1 +09jungle2 +dhahrry +carapass +anastone +khkbest1021 +ckw04671 +skijun +gnookim3 +gnookim1 +tarkko +cntese +bikedream +hana09241 +pdk0518 +rivermee3 +smartsetting +slackware +eyeglassesadmin +srjy1234 +ubplus2 +ubplus1 +cokes4 +trustfactory1 +dmd +quitsmokesladmin +imarketing044ptn +skykeep5 +skykeep3 +skykeep2 +gi238admin +popololo1 +widephoto +lotto97961 +sommo7979 +odcmalltr +moontk88 +ipaynoteari1 +yahoofss +wnrfla +vngkt12 +dsbkortr2522 +jokkrye +dawwenter2 +dawwenter1 +hanoc +rain001 +gi8 +goldposition1 +gimme51912 +go2cool2 +pe1501 +godo204620 +minhwan90 +seunghyun97 +fishintr8255 +brightsk6761 +jmagic +etern4283 +selenia +mongmania +hthlsy9 +hthlsy8 +hthlsy6 +hthlsy5 +hong1sutr +oddy051 +youinn42 +edu35 +headstone012 +jihoon1004121 +carstudio +partyanimal4 +partyanimal3 +partyanimal2 +tgmedia +ppk +cooluktr +mccoywatch +kansinny +okbible +morffstyle1 +rockordie1 +itself +skinnwtr1082 +shdnjs1 +t202 +bebecare +s042833 +amberhouse3 +vbsoma-020 +vbsoma-018 +namjm96 +jojun26 +jojun25 +femshoetr4711 +jojun21 +vbsoma-015 +bear71 +edu30 +hjp710 +mikeoiya +kronos012 +twinssm72 +lissy11042 +lissy11041 +funchiptr +nodecore +godo194241 +godo194240 +beet8838 +lee11361 +cctvclub +suguntop +junghwaw +heedubu2 +techtalk-forum +duji1381 +medela00 +ksh85791 +wlsk003 +ivadak +cjj9937 +ip209 +dsignhoo5 +koreayb15 +yw9000 +kazugb +mercurium +seawoong2281 +byh000 +coke76 +injeju +jachin11 +ip198 +fshop +woodyctr4561 +kyneo7536 +ninefirst1 +emofood +gocaruso +csj0035 +airmedi4 +nanacom +forsellerrelay +ip197 +mantomanjr1 +gi6 +szngsilver +daingolf1 +inumber +yoyohi +godo193812 +sksdltmf +hushin20023 +smsmile +hankook72 +eyoungrla +medibank16132 +mangno2 +wrtoysports1 +junghwa741 +brother1 +imarketing043ptn +jlcorea1 +js100j +wk040304 +totalbus +sanegatr7906 +iblpkotr8975 +siwon1siwon2 +uc24891 +ipayhaircool +incobbtr1465 +us82go +kch34p2 +anbd11 +girlnshop +mtbzone1 +byeatopy +wlfjddl46582 +mehode +goodnfs +brandywine +streetdia +jungo871 +jumpgyu1 +oops01231 +anjinil +shy980204 +yhcmidas +pamikyung1 +raeo1004004ptn +jw33world1 +radstore +alzipmtr2005 +pok7204 +godosg-029 +godosg-028 +godosg-027 +godosg-026 +godosg-025 +godosg-024 +godosg-023 +godosg-022 +godosg-021 +godosg-019 +godosg-018 +godosg-017 +godosg-016 +godosg-015 +godosg-014 +godosg-013 +godosg-012 +godosg-011 +godosg-009 +godosg-008 +godosg-007 +godosg-006 +godosg-005 +godosg-004 +hommejk +godosg-002 +godosg-001 +memorydream +pro109 +paranshop3 +crs2 +azrael0907 +kidjoo9 +dhadepot +eteamart +myshiny1 +pstrain +today2421 +vinch701 +hansclupp +ssf80001 +cottsco2011 +ogamoktr1404 +gofla0tr9221 +micaad1 +tysopumtr +choh0211 +vivicar +ruddk23141 +dm1159 +ipayjehwan0202 +meareman1 +vinusman +ye01072 +kimstoon2 +kimstoon1 +overdose +runescapebetatest +ns29939152 +pearlkorea +cubedevsunny +hanguomj +gike1 +jeosystem +kuoo1914 +gateau12 +bk1199 +emedimalltr +lifeedu-019 +lovesole83 +ip195 +ip194 +hmsladmin +peter123 +gameshowsadmin +media-bpo +youtubers +hsb +sqr +azozrm +sudha +www.psy +ip191 +azshop +wowedu +yasedesa +ip190 +pharaoh +editors +ip183 +shkola +ip180 +ip169 +symbol +atlastest +ip163 +dafaka +profcom +ip140 +homebuyingadmin +natadm +www.dale +kms2 +retailindustryadmin +paladin +ip252 +ip251 +dahaya +syntax +italiancultureadmin +germansladmin +ip247 +ip246 +hotmailservice +autoconfig.linkedin +mentalhsladmin +dakati +autodiscover.linkedin +www.reklam +ip243 +o1.send +skylark +ip241 +southbayadmin +gi355admin +gi116admin +irishcultureadmin +guideway +jacksonvilleadmin +tpt +westchesteradmin +golfadmin +spj +foodreferenceadmin +ip229 +gomexicoadmin +stylespeak +data4 +danlod +ip224 +chaubathong +ip223 +mvd +ora2 +ip222 +ip221 +phonecards +hibuddy +alisha +ip220 +ip218 +exoticpetsadmin +ip216 +ip215 +gi43admin +ip212 +stream02 +ip211 +ontheway +gi472admin +ip208 +conecta +nadorino +ip207 +ip206 +alojamientos +gi69admin +gi233admin +correo1 +campingadmin +ip192 +ip201 +cocinalatinaadmin +ip187 +ip186 +hkmail +ns133 +ns145 +dbrown +ip185 +deadman +ecomerce +bilalof +ip184 +samysoft +testesite +ns148 +akulah +ip182 +ip181 +ip179 +ip178 +ip177 +ns154 +servicelogin +ns157 +ip176 +ts17b +hayati +ip174 +ip173 +ip172 +ip171 +ip170 +ip168 +ns167 +ns170 +ip167 +ip166 +ns199 +mailmkt +ip165 +raleighdurhamadmin +itblog +www.kst +ns221 +midimusicadmin +oldblog +ip161 +ch3 +armando +ip160 +vasant +videomost +sm14 +sm12 +ns220 +sm25 +ip148 +vinaconc +efecan +ns230 +ns190 +ns240 +butnow +ns250 +ns210 +ns204 +cheeseadmin +ns260 +ns205 +dafa +ns180 +ip146 +ip139 +ip138 +ip136 +sisters +ip133 +ip131 +ip130 +ip121 +mp3test +ip118 +ip113 +ip219 +ip199 +ip189 +delphiadmin +pureumlnt +rapadmin +www.azmoon +asdfghj +bbarts +bujamy56 +risses0520 +doreuri +lifeedu-012 +lifeedu-010 +relaxchair +xkqrhfvpstus1 +jb34387 +jb34386 +jb34384 +phonejtr7321 +arcmalltr +kestech +jyuhwan +amigyu +xn2ztr5941 +vbsoma-010 +saintvin1 +kgcbee1 +pchansol +anativ21 +camp60 +magicgonet1 +sandboy293 +intheliving5 +intheliving3 +bigboy930 +bluesunh2-040 +gateall2 +weppow2 +storenet +moohan21c2 +saejong0063 +homme4u +akak54543 +hcwb031 +maestro4 +maestro3 +khunderjapa +bluesunh2-030 +imarketing042ptn +yjy75574 +pool5519 +hdauto01 +storenettest +gi350admin +bonaebada1 +gi111admin +art9403 +masa3029 +reddesign +bluesunh2-020 +openmaeul +bluesunh2-018 +taeannet1 +seldesk +wookjoong +noa64101 +ksu0921 +audgml8586 +ks91554 +ezmro +bluesunh2-006 +ojas20128 +gien1 +yah0216 +bluesunh2-004 +ojas20124 +tj04016 +guandki5 +guandki2 +guandki1 +bchhome +eugenephil2 +eugenephil1 +sanfranciscoadmin +jjoong978 +duluthadmin +goodcna +noplan +crossector +dpakorea4 +dpakorea3 +dpakorea2 +dpakorea1 +hgh9111 +gf0103 +sang3570 +efinlandia +jad3343 +dominoland5 +wow9173 +dominoland3 +godo202351 +solsongju +motosadmin +fos0830 +zzuujjoo +jejucjh3 +nepoung +hipole +skywithsea +sangwoopool +lohasprime +storymt +signmul2 +entrada +divorcesupportadmin +gourmetfoodadmin +comicsadmin +video77 +godo192239 +gi37admin +gi466admin +ftlauderdaleadmin +gi227admin +franchisesadmin +aerospaceadmin +siliconvalleyadmin +kidsbooksadmin +rcmetr3143 +cmzip2 +dm0107 +cjdgo71 +gimtech212 +m9202 +quantez1 +dangmuji +n011n +wikids1 +sewon122 +simswfcc +besthimall1 +myfran +arin08222 +rhdrkdgml +cocoluxury1 +snow2tt1 +kinisia1 +ahk0729 +parangsae +doremii +parkmina0318 +joafabric +padipros1 +aftermidnigh +neubible +damoa2171 +ediya05051 +sogakjang1 +tontoy13784 +moms9112 +jonggkim01 +house2641 +kchair +kitchenmall +dustnsdl +godoedu-029 +godoedu-028 +godoedu-027 +godoedu-026 +godoedu-025 +godoedu-024 +ipaykhaksoon +godoedu-022 +lee07981 +godoedu-019 +godoedu-018 +godoedu-017 +godoedu-016 +godoedu-015 +godoedu-014 +godoedu-013 +godoedu-012 +godoedu-011 +godoedu-009 +godoedu-008 +godoedu-007 +godoedu-006 +godoedu-005 +godoedu-004 +godoedu-003 +godoedu-002 +godoedu-001 +dahanoo4 +dahanoo1 +ksa0403 +team65071 +ezgo3 +kk11569 +cctvpartnertr +tyjjang +sinba8tr8703 +dass65982 +pagodapan1 +ggomi +imarketing041ptn +sang3022 +sang3021 +shinyo1232 +kuc0121 +lovelysani5 +lauraashley1 +khskorea3 +khskorea2 +khskorea1 +hyunny7468 +raja883 +gi204admin +vmde +gi305admin +littlerockadmin +gonwadmin +printscanadmin +uspoliticsadmin +xfacfory +bbcompute +autodiscover.adserver +wsw +autoconfig.adserver +contributeadmin +bsosbos +allamlatakia +gi344admin +oreon +gi105admin +b365 +b364 +calle +b359 +smtpout5 +deivid +b350 +smtpout3 +b322 +onlineorder +ejercicioadmin +b316 +restaurantsadmin +biztaxlawadmin +kidsmathadmin +www.newjersey +www.connecticut +newlywedsadmin +northcarolina +gadadmin +dekrow +managementadmin +www.alabama +www.massachusetts +bindass +frugallivingadmin +newmexico +homebusinessadmin +latinmusicadmin +www.arkansas +gi32admin +www.delaware +demons +www.minnesota +dsample +newscool2 +highteen +doumikorea1 +sungkunc2 +koryms2 +e-malltr4552 +kjwoo32293 +neverdiesp3 +labote131 +gi461admin +jytrade +harugy2 +lee9501 +haesung20843 +outweltr0931 +bbridge0734 +sheungmo1 +story62 +skdiwndl1 +gogumagogo2 +csangsun75 +jhcho8420 +jhcho8418 +www.newhampshire +www.illinois +gi222admin +steelni1tr +foodduck +www.vermont +zzuzz2 +lee9393 +jncseller1 +foodfarm +clubchamp +southcarolina +siwori +young2536 +harueui +cnsgh334 +iconsupply +siwood +ksm32601 +no1boiler +nno12345 +spatherapy +sojin7475 +probastr1978 +rhodeisland +www.wisconsin +genetichong +mtsports1 +ipayhercules001 +this2157tr +arab-net +jinmax371 +bomto3434 +godo201251 +es4today +pdstudio +pjhwass +vision894 +hipet2 +woonsanhb2 +woonsanhb1 +hgyjsa1 +hds3406 +alaya2776 +zpii2 +aodkorea +winksjd1 +widcase1 +haoba8tr4026 +james75861 +gosisktr9155 +uss8888 +myeraf +cilon79 +luhzenblanc +bigca4u2 +glffldqnwjr +deccario +bsosabt +kimshow2 +mrsong21 +baboking +blcmath +noogle +dorcus2 +bhhanyang1 +wiclara1 +aimys67 +grandhil1 +hkp2560 +spycoffee1 +pick.rap +fhwmepdlf6 +minkj001 +fhwmepdlf2 +linenumma12 +kjhmisope +godogodo-049 +pon2mart +idealistar2 +khs9281 +soapschooltr +lee07101 +jsoo100 +mozart4426 +jinsori2 +byeonghg4 +plumbingadmin +boxeoadmin +godogodo-039 +dailymans +zigngn4 +zigngn2 +s3intsky +coomim77 +kkareu2nara +drcorp1 +godogodo-030 +jamongc +co980329 +fromnongbu +imarketing039ptn +carajsj1 +godogodo-022 +s3intsdg +sailingadmin +sts1 +iklanbaris +godogodo-019 +tae64802 +tae64801 +guess001001 +ljh82403 +khs8989 +iaandp1 +eyely +iaey57 +kkdyoon +lsmfish +godogodo-010 +gomputer1 +buybetter3 +wjh7975 +akxogh +exmtb +atco6565 +godogodo-005 +shesgotr9676 +shockyoo +sangsev +designnice1 +vaionote +horroradmin +bantdoduk +qnrgoe2224 +ub12121 +gi338admin +metafaux +ghs04022 +raeo1004001ptn +morenvysenior +gi100admin +ocsedge +centralnjadmin +haven-forum +plasticsadmin +waterskiadmin +toolsdevadmin +www.clone +gi358admin +dev29s +bonkorea +snowmatr4981 +hkjajae +cornerb +sound16 +cttc +tardis.ntp +nonno100 +swy9988 +goobbuy +densun +finishline1 +youto1 +itmyth +seland2 +printec09 +ektl1004v1 +js2proj +alpskorea +a18burn +kgh8860 +inooint3 +inooint2 +inooint1 +tjdejrah1 +cosmamtr6185 +ttt308091 +mamongs +ilgimae1 +hyj0616 +jwy8044 +vdmsv125 +sekkei +earangel +pinkiegirl +bikemac1 +meal1owner +godo190245 +chungzz +rocbi01 +alone0301 +hot95292 +honai77 +korbiketr +gerpm +kptool1 +jm40491 +jejucjtr9330 +jb1130 +aquaclean1142 +yewonnn1 +tory8787 +inhotel2k4 +youppy +inhotel2k3 +dadream7 +gerio +itechkorea1 +s3intman +greenstarlab +akyba1 +and136 +eyesore +hurbnvinu +racetech1tr +sardo763 +syr0247 +geniusynh +chunghs +aurora3333 +h9944021 +punkgirl141 +damin9496 +sisunagency +limjaddd +sj12240 +sang1172 +jungfarm +yasw1109 +milwaukeeadmin +uncledum2 +pjk8112122 +bboyan1 +surfingthemag +bbongc84 +eye31 +yesjubang1 +dbswjd12001ptn +gokimyong51 +vustore123 +mangbae +gi120admin +felttree +jungah2009 +noriko +newdept1 +allthat01 +chamosa +wlgus0606 +imarketing038ptn +sisamall-020 +bysooni2 +beliefstoretr +hinius +articandle +bridgestone +lion98988 +lion98987 +lion98986 +municipalcareersadmin +lion98984 +zippy0883 +zippy0882 +zippy0881 +busangirl +mangjang7 +icon1220 +about1103 +jyhong851 +sisamall-010 +dororo21 +ivoguetr3185 +dressmoon +mimartco4 +oxybion +gi26admin +iamamine002ptn +caliella +sarahmell +bockhan2 +bockhan1 +icamp4tr +atozsaib +gi455admin +ruchaga8 +ruchaga4 +ruchaga3 +sugar003 +girlsego +hmhee0130 +dberry001ptn +kimterry17 +moduru +belldand02 +cocoamilk29 +cocoamilk23 +gtc017 +sado102 +sunrise1 +yeecya2 +yeecya1 +thfactory +shilla041 +lsw31391 +badboys532 +takyp4 +dongkang-015 +dongkang-014 +takyp1 +dongkang-012 +dongkang-011 +dongkang-009 +dongkang-008 +dongkang-007 +dongkang-006 +dongkang-005 +dongkang-004 +dongkang-003 +dongkang-002 +dongkang-001 +rongee19901 +godobusan-025 +godobusan-024 +joy2htak +godobusan-022 +godobusan-021 +godobusan-020 +godobusan-018 +godobusan-017 +godobusan-015 +godobusan-014 +godobusan-013 +godobusan-012 +godobusan-011 +godobusan-010 +godobusan-008 +godobusan-007 +godobusan-006 +godobusan-005 +godobusan-004 +godobusan-003 +godobusan-002 +camerasadmin +godobusan-001 +seoulitle +woowing +tod01081 +lily0728 +cok2yj +tj00692 +eyetag2 +gagugood +win5010 +suuv1226 +cyan071011 +avibookstr +shoptr6928 +zzzooon4 +solgartr1956 +mad41303 +segyeuhak1 +lison982 +sksk0622 +tuinsports +dynfou +fpfp883 +babohtj1 +jaehunx1 +lifesaver9 +lifesaver1 +takuti +wfishingtr +godo188085 +dengol +kichpony +kblue0 +yewon0903 +dodan15258 +gi216admin +ckdghks5317 +armarkat3 +armarkat2 +itckorea215 +itckorea213 +itckorea212 +domain11 +hangreen +youngink411 +hansj1128 +debr1004 +abzapps +dadajubang +urizone3 +bbcountry2 +curtbein +rainsoul00 +cyj19742 +jikyjeon137124 +maxsavtr0357 +changwoo0120 +bswoo414001ptn +pungnew9 +gongze1 +j2story +touch1822 +jwpkg0696 +lifehanbok +imarketing037ptn +eggstar +sjmall +banilafruits +ldhstudio2 +missleeshoes2 +missleeshoes1 +niubung +chase69002 +chase69001 +hangravi +jikyjeon136869 +ych78772 +ych78771 +mega70 +iamamine001ptn +milisitr8615 +jean218 +nakim0103 +topia12342 +topia12341 +stylelight +kissmethe21 +coffeegsc5 +badwin7 +fmmol +badwin3 +badwin2 +trueguy211 +gumigagu1 +wunderkammer +myclic +dltkdgusz22 +flux9 +namdo71 +agafriend2 +hit000 +jikyjeon136672 +seolmisoo +laurenjoo2 +minneapolisadmin +frog0815 +amdkyt +gocamptr +ttouch85 +hhb9397 +knstamp3 +buelin1 +mandoo1 +lifestylist1 +skidlove3 +agprinses +aws26801 +go90861 +dudalj1 +usnewspapersadmin +munbook +staryuja68 +delsey +sujung0807 +webhostingadmin +dk83666 +treefrogco3 +treefrogco2 +treefrogco1 +dk83661 +chuksan +cueplan2 +elbtano +pygetec2 +koino11 +mymiru09184 +vusrmawhd +haemin34251 +kwang8481 +penjoby +duckbai +therapycareersadmin +yesmi10042 +1004sg +als112911291 +supplyctr1 +plan20133 +dbs001171 +work.americangreetings +rtj01034 +sisusu +yosong201 +weddingcar1 +talk11 +everland04 +songahry1 +kwj123 +goodgown2 +coscat3 +coscat2 +sjanwhdk22 +ptuebiz-050 +ptuebiz-048 +ptuebiz-047 +ptuebiz-046 +dressline +ptuebiz-044 +ptuebiz-043 +ptuebiz-042 +ptuebiz-041 +ptuebiz-040 +ptuebiz-038 +ptuebiz-037 +ptuebiz-036 +ptuebiz-035 +ptuebiz-034 +ptuebiz-033 +byminlee +ptuebiz-031 +ptuebiz-029 +ptuebiz-028 +ptuebiz-027 +ptuebiz-026 +ptuebiz-025 +ptuebiz-024 +ptuebiz-023 +ptuebiz-022 +ptuebiz-021 +ptuebiz-019 +yvette +ptuebiz-017 +ptuebiz-016 +ptuebiz-015 +ptuebiz-014 +ptuebiz-013 +ptuebiz-012 +ptuebiz-011 +ptuebiz-009 +ptuebiz-008 +ptuebiz-007 +ptuebiz-006 +damulkorea +ptuebiz-004 +ptuebiz-003 +ptuebiz-002 +ptuebiz-001 +ramses15 +kagamii2 +kagamii1 +baksakimchi1 +highend +bukku +s2pintsunny +inagi99 +gcore +ozled1 +hyun790320 +auteurkim1 +ilove3691 +imarketing036ptn +gomppi1 +nana312 +kiwamimall +ip2001771221.none +cafepremio +luxurycity9 +delt77 +daljae113 +s4freeintsf +wlachacha +donghan53 +biny1122 +kundservice +wow4482 +yasuike +jeju82457 +entertainingadmin +s4freeintnj +samiamseo +itspresent +sjkjh2 +mamsarang +coroner +jjoodol +s4freeinthn +ansanmooki6 +ansanmooki5 +ansanmooki2 +gogofishing1 +pcmaker +serverhosting245 +serverhosting244 +serverhosting243 +serverhosting242 +serverhosting241 +serverhosting239 +serverhosting238 +kidsmotors +serverhosting236 +serverhosting235 +serverhosting234 +serverhosting233 +serverhosting232 +serverhosting231 +serverhosting229 +serverhosting228 +serverhosting227 +serverhosting226 +serverhosting225 +serverhosting224 +serverhosting223 +serverhosting222 +spatial +fox9head1 +serverhosting199 +serverhosting198 +serverhosting197 +serverhosting196 +serverhosting195 +serverhosting194 +serverhosting193 +serverhosting192 +serverhosting191 +serverhosting200 +serverhosting188 +serverhosting187 +serverhosting186 +serverhosting185 +serverhosting184 +serverhosting183 +serverhosting182 +serverhosting181 +serverhosting179 +serverhosting178 +serverhosting177 +serverhosting176 +serverhosting175 +serverhosting174 +serverhosting173 +serverhosting172 +fieryguy +serverhosting170 +serverhosting168 +serverhosting167 +serverhosting166 +serverhosting165 +serverhosting162 +serverhosting161 +serverhosting160 +serverhosting157 +serverhosting156 +serverhosting155 +serverhosting154 +serverhosting153 +serverhosting152 +serverhosting151 +serverhosting149 +serverhosting148 +serverhosting147 +serverhosting146 +mk3477 +serverhosting144 +serverhosting143 +serverhosting142 +serverhosting141 +serverhosting140 +serverhosting138 +serverhosting137 +serverhosting136 +serverhosting135 +serverhosting134 +serverhosting133 +serverhosting132 +serverhosting131 +kdypiano +yuki2 +serverhosting125 +chlgks77 +serverhosting123 +serverhosting122 +serverhosting121 +serverhosting120 +serverhosting118 +serverhosting117 +serverhosting116 +serverhosting115 +serverhosting114 +serverhosting113 +serverhosting112 +serverhosting111 +serverhosting110 +serverhosting108 +serverhosting107 +serverhosting106 +buildup66 +serverhosting104 +serverhosting103 +serverhosting102 +serverhosting101 +serverhosting100 +yepia2 +binco41 +eosyun +cromyoung1 +enjoyholictr +baeoun2013 +kbldmk +pfcb2btr9416 +hanqtour2 +kangaloo681 +smallvtr1168 +kc0505 +bbulai +teacera1 +namja5979 +boromaru +eblshop +itjump +ok907212 +mcubei +meeandjoo +yun6208 +ukss12 +wansung2 +wansung1 +gocamera +foxdata +tyfnb07011 +flagoutr7506 +soonung1 +woghksbs12 +yeskr872 +kptool2 +julee2722 +tkvkdldj12 +itscamtr7819 +hot2shtr8324 +uicivfer +anticalf +stupid13 +ghj11243 +beatcool +wolimmungu12 +gonysoda8 +gonysoda7 +gonysoda6 +gonysoda5 +iamyulmo2 +dm1159tr7350 +composer2020 +jijangsoo +sunny8711 +broadbandadmin +highkickz +theholyseed +footstreet +allfun +homemeat1 +emall244 +khuart +imarketing035ptn +greenfamilyadmin +minki65451 +fjqmapwlr +bau +ainmall +lee4651 +shcompa +leejy12292 +btoall9 +miraeatr2005 +gabenori +ftforest1 +city06971 +rhkdwls723 +ktk4051 +jujutiti +new.www +na73732 +yh71040488 +myanb9 +airing3 +sminco041 +myanb3 +sltlwjf2 +soban9999 +ipayecolv1 +oldprime +inul003 +kumsansane +totor19181 +viatc011 +pbuild5 +ohayo +jinhui11202 +minormajor1 +konkuk-064 +pcs1218 +allect +syjmom3 +syjmom1 +heejung2 +jobsearchadmin +wpraitr3844 +mygdgr3491 +anima69 +heejung1 +bdsblog +ksmkoo3 +dkt1234 +merci21 +ip2001311721.nrc.ice +naru49494 +roverto +greiding3 +timmy92 +tong043012 +daun79 +tong043010 +garnetstory +kojj073 +kojj072 +scribe +urbantake3 +leegun19804 +leegun19803 +protuve4 +cartoman +miscel2 +dadajch +zeezer +hhsol7 +dreamtime2 +jaboshop +junetek2 +junetek1 +ribonbebe3 +hyang777kr7 +salimsali +allapp +artisan85 +williamws44 +jungang3 +jungang2 +manager84 +kch34p +hangoeul +saeyangint1 +somang01532 +ypp002 +ypp001 +automotr5324 +allbab +yeoback8 +myahn7 +pdazone +tbldesign01 +cjihun792 +cjihun791 +gi333admin +trynulbo +starexon1 +stoneis +klove76 +garak +pks82191 +chairbo +hiki88 +royalaqua +snoopy0712 +indasom23 +micromtr8776 +vollzzang1 +dap +gksrudfla1 +kdiden1 +himoon43141 +cmw1287 +cinemathe +koreantea +nemestar1 +luvmung6 +luvmung3 +badpoet9 +badpoet8 +badpoet7 +badpoet5 +sooguncafe +bmpshop +vpn2gftp +imarketing034ptn +lmss042441 +rkdxogh011 +lee3642 +gamzi +swdps0811 +snkc001 +lobchou70 +choisseung +chonggakpapa +pusary74 +babylish1 +coalsk +king2112k +chunbak1 +kimsj418 +bigtown7 +hesaidsmart +bobdodook1 +doleyetr3930 +newface22 +newface21 +xpshx777 +dcbook +asctaix +calla11190 +gurilla +sytkfkdgo31 +sachajuan +rlatnswk24 +dnckorea +lwy03022 +lwy03021 +upmotors +erumn +isolmg1 +inhyun44tr +hyflux11 +cnb57091 +kkaebong +insunui +therefore1 +isroad +ulife1 +duggy741 +cnn5326 +domaejoa +eosida +ahwld53 +foodztr +asdq001 +jjsk26 +auddnjs2 +puppyworksmall +djbiart +coolmans +yejin3 +mcsh97 +dlfrnjs9102 +dlalswns14 +zzus70 +dkkj0518 +jwjuliashin +ping3059 +perevod +lee3122 +neosans +moung4839 +yuna04791 +allatpay +foodzen +todream07 +raimtree +bank1 +ohtrade1 +yc0098 +abstrait1 +gagus +herezzim +pink129-030 +fruitsoban +fix05 +xenodori2 +pet2day +dpency +mrherb +kokory932 +cjyhm +sunnyhouse +nicolekimbs +pink129-020 +dpseller +gahee +hdwegutr3316 +graphicn +pink129-012 +antiqland +kkangtae852 +pink129-009 +gg777 +a72481 +gspungsun +ilove0702 +ilove0701 +na462791 +bluesanitary +tenorlky2 +dbwjd6660 +animationadmin +moning620 +hqmon +imarketing033ptn +gaeun +acbccc3c +cc112a8 +dasom7735 +cc112a5 +cc112a4 +cc112a3 +corelee6 +cc112a1 +uniquebutton +jamomalltr8882 +asanever +prcup1 +morenvy028ptn +iamgantr3550 +baejina +prori61811 +hjg112 +lightingadmin +jayhome5 +mediasoul +bada264 +w90226 +lucylove +swissmall +jizone +rsho04 +s33h3001 +suniya10041 +mjs1051 +chhj1017 +jspark3661 +mk0505 +webdevsladmin +outdooraz +cstyle2 +yogoyotr6804 +dsr62083 +jewbling +mmmobile +bbosasi51 +hgk5361 +wooriv4 +queenslook3 +amadas +woorioh +coopnutr9554 +livinjs +gibrocom2 +hyunju0510 +coscostr0282 +innohouse2 +innohouse1 +morn1020 +costfetr6892 +cyhealth4 +cyhealth2 +tfarmstr5694 +ipaycbk326 +searchlist.pmy +jhmeditec +coollake +tjdtnrnen +gyuho771 +wooro22 +hobongtr0513 +jsdesign00 +qkqh1617 +tamina +zeyo12 +outdooreuro1 +dialogic2 +lesha12 +robean2 +sasari729 +mttech +parandul10042 +kst1402 +bbosasi +isb12151 +as1115 +samohago1 +foodome +green1004kr1 +eduts23213 +clubdica +sadbluerose +hangju123 +kapsik +silicon63 +qkqh1403 +eorect +ftts12272 +bj1003 +godointerpark +superdhk1 +metdolkimchi +gaiasun9 +jkd21004 +anaclicom +ho9318 +redtough78 +manijoa36 +manijoa34 +manijoa31 +infos912 +eueverpure2 +kwakjh53 +weert77 +geddong +imarketing032ptn +pppman +orzr2me2 +khs2145 +darkenen +ktk0993 +akstjr78 +domemart11 +hnaksi2 +gi404admin +sinzza +netcr61-029 +netcr61-028 +netcr61-027 +netcr61-026 +netcr61-025 +netcr61-024 +netcr61-023 +netcr61-022 +netcr61-021 +netcr61-020 +netcr61-018 +netcr61-017 +netcr61-016 +netcr61-015 +netcr61-014 +netcr61-013 +netcr61-012 +netcr61-011 +netcr61-009 +netcr61-008 +netcr61-007 +netcr61-006 +netcr61-005 +netcr61-004 +netcr61-003 +netcr61-002 +dns254-4 +dns254-3 +morenvy027ptn +pinknatr9125 +tinda78 +ks75b72 +eticket24 +brion4311 +seo11011 +joinxstudio +mom0won +ahnjb281 +silverasun2 +seawari1tr +headintr3582 +stylehtr0153 +as0601 +jschang9 +taeky96 +shoptr1282 +jellyfish1 +taeyangkim +sugarjy +chan501 +kny1213 +ichina98 +durifishingtr +kkjj0924 +nohant +hascos2 +wjsquddnr +enurictr4861 +ppori2 +salmon6948 +marys-igloo.powerize +parabol +wheeya882 +ynskorea +hue3087 +wineworld +backshtr8387 +painfred +phototile +anshhans1 +unjung17 +bsbosan +daegunet +dysky +moon18451 +lee1136 +ilsanrc +yeye159 +seawoong228 +moolzil1 +infomax4 +infomax1 +hyoseob90 +coretnt +yeon0408 +jit00400 +haegung1 +gursung +march322 +yoyo8 +isplan +dmswn63352 +khan3815 +jun10031 +cucu811 +asianain +yoon01 +ufirst11 +highandlow +taeuki +eugenephil +jssj0515 +greum07 +cantaville4 +kjmy119 +gkdlfndgl +mudetppo1 +flaming +lee0798 +inpiniti1 +sponia95 +pkgagu80 +narintr7342 +biyosekkai1 +eugenephi1 +gjrjsrkd +ouangkn1 +dbstmdwn +hgyjsa +mool203331 +koreamall +spycoffee +hujung87 +fghj40 +zpdl161 +imarketing031ptn +mhke486 +erumn0701 +wjddudejr2 +eventplanningadmin +yyao +auddlfdu +isoral +yewonnn +bi80002 +bboyan +jphoenix037 +jphoenix036 +jphoenix035 +jphoenix034 +jphoenix032 +gagustory +zexcom +kthkira3 +kthkira2 +kthkira1 +tabacstation +ey05061 +sweetpack1 +sdphottr4640 +swimnara2 +shj3449 +tnchtr4676 +gdtest-049 +cmdesign16 +cmdesign15 +cmdesign14 +cmdesign13 +cmdesign12 +cmdesign11 +cmdesign10 +buymi1 +jhngyu11151 +takyp2 +sinsa2 +dongkang-013 +ych7877 +seniorlivingadmin +dongkang-010 +jsa0821 +vivianan5 +multinaratr +nice7174 +gurdl1207 +yewonb1 +peppercenttr +planetm81 +bangang5 +coreok2 +ellisvtr3425 +ppoip5 +ppoip4 +dudskawnd7 +dudskawnd3 +foodbay +dldydtmd +serserser +vip254-16 +godobusan-023 +enstyletr +godobusan-019 +vip254-10 +herbkorea +godobusan-016 +yubu +cnt32321 +godobusan-009 +gkdms92541 +simwon +tobewing2 +styleformen +cham292 +thechae2 +rudy1248 +badpoet +na7282 +haenim20001 +soybonita2 +ollehdo +altenergyadmin +bc20092 +xmanjee1 +vitacatr6990 +luxurytr3289 +minilever +hmsolutr6091 +leejihyec1 +gatwo1141 +chagal4 +hjspomedi +amarzon2 +kchul9111 +kimdaehyuni +gt-camp +bradpato +gundamhousetr +ftts1227 +cham100 +gnrecycle +hello21c1 +bomnalecom +woong12062 +yoon1 +yellyky2 +yellyky1 +koorie69 +yang2625 +jangmoer +lovejlovej1 +vpn2crc +giftme71 +baegma2 +rkqjsj1 +agrnco01 +y48199811 +gi21admin +tazale1 +jiyaaa +kimhyohyoun3 +kimhyohyoun1 +wdbyeon2 +imarketing030ptn +gatwo114 +sgmania +hansongcnc +dyfkrl +gi449admin +leeah3573 +leeah3572 +leeah3571 +bboori1 +snp2009 +moon17005 +rjsgml56941 +a025085 +a025083 +hojungga2 +expertlounge-forum +petitange +morenvy025ptn +iluminox +gi211admin +nobujang +abalico5 +hansgallary +daaec112 +gdtest-023 +pinkicon4 +pinkicon3 +aramusic +tdrp774 +amaretto4 +carstera +foodallergiesadmin +ynj6 +akongs +pswzag +protool +betahome +tmx0907 +joypolo3 +alike123 +editors.team +happy23593 +noduel +happy23581 +any49527 +passimo57 +hahaback21 +kowakorea +jiwon1 +buybiz +mir83577 +mir83576 +mir83575 +mir83573 +parkhahang +js90203 +js90202 +foteckorea +michs372 +bizjapan +sally7tr9258 +okpack97 +artmania7 +alexno1 +sjk752 +pjo4422 +gagastudy +miiusnc001ptn +philosophybag +ensso +dycal +atechmall +hue1104 +rusi1001 +barbiedollsadmin +kiztopia +minilca1 +magajean +trionsun +kksh7028 +johyomi +todayonly1 +zlwhs1231 +bujacat1 +jeonginzone +rcpowetr8500 +fiveray1 +styx11211 +body70772 +no1cctv1 +jgarden +drimi28 +drimi25 +drimi23 +venosan1 +raehyun1 +lastcamping1 +wndhrl +ksfishing +cpt091117 +dptnfrh21 +beeradmin +yepia7 +a0250811 +healingstay +granvill +k8w3s +fm012 +yogitea10 +wonwoo2 +heatertr4486 +hongmessi +koaf4949 +bumilion2005ptn +konkuk-069 +konkuk-068 +konkuk-067 +konkuk-066 +konkuk-065 +somgulem1 +konkuk-063 +konkuk-062 +konkuk-061 +konkuk-060 +konkuk-058 +konkuk-057 +konkuk-056 +konkuk-055 +konkuk-054 +konkuk-053 +konkuk-052 +konkuk-051 +konkuk-050 +konkuk-048 +konkuk-047 +konkuk-046 +konkuk-045 +konkuk-044 +konkuk-043 +konkuk-042 +konkuk-041 +konkuk-040 +konkuk-038 +konkuk-037 +konkuk-036 +konkuk-035 +konkuk-034 +konkuk-033 +konkuk-032 +konkuk-031 +konkuk-030 +konkuk-028 +konkuk-027 +konkuk-026 +konkuk-025 +konkuk-024 +konkuk-023 +konkuk-022 +konkuk-021 +konkuk-020 +konkuk-018 +konkuk-017 +konkuk-016 +konkuk-015 +konkuk-014 +konkuk-013 +konkuk-012 +konkuk-011 +konkuk-010 +konkuk-008 +konkuk-007 +konkuk-006 +konkuk-005 +konkuk-004 +konkuk-003 +konkuk-002 +konkuk-001 +jgh09171 +flykys13093 +imarketing028ptn +gorillakt1 +flyforest +dms33281 +berry0007 +mrteddy +heewoon0 +falltvadmin +selfcrab1 +jungjs3142 +feelmedia +tkfkdgo01 +vuuv01 +qhejrqh +anykeyezon +d431214 +dinnovation +morenvy024ptn +cristianosadmin +btmobile +carpediem01 +rinshua +ydsm +giftall +yejj +thfwl1911 +kkium1484 +yedo +tendori1 +gi298admin +lubu1061 +gunsa1231 +james5272 +likesam1 +catcafe3 +makeoftr4787 +kamnol +keyang40049 +keyang40048 +keyang40047 +keyang40046 +ljm19671 +keyang40043 +aszx11201 +isofum2 +isofum1 +englandnwadmin +jcym1537 +jcym1535 +colorshopping +mdeco6 +seyong10 +asung291 +labelladea +hoidap +fastjun +dufkddl1191 +mj5358 +akdlfjtr8602 +gi173admin +homehealth1 +carmemorabiliaadmin +na5284 +ddmris +azazaz3331 +ogapylove +pozl0865 +yain +itk418 +green8 +yje4875 +smtpapps +mecafitr3291 +lightmodel +ae-admin +wmcom1577 +godoshop-009 +somimom12342 +chemical91 +neoprize2 +tmc8683 +dancompany +deehes +duzon +jkyoonc +jjnara +gi327admin +heewon85 +benefitkorea +joinusb +wansocar +stayawake77 +moon15193 +darius211 +kunstler2 +kunstler1 +ontiptoe +bumilion2004ptn +tnhawaii1 +kamit2 +ismine +ipayjola0559 +rudtn119711 +eun1590 +wizbook4 +wizbook3 +penblan +wptndtr5678 +annandy1 +hanxiaojie1 +globalfood1 +minimaltaste +hyunyx2 +vividtt1 +imarketing027ptn +tonerpiatr +enest +asepsis4 +wjswn73 +mtb7679 +enfid +starfavorite +bockshot1 +garsia7 +summerfunadmin +ho5154 +ddos134 +ddos133 +morenvy023ptn +gary812 +gary811 +jeongrh1 +heeflowertr +urizone1 +k123625 +things1 +yang0346 +goeuropeadmin +skinevent3 +s2pdevsunny +feel4 +skarndalsdn2 +cjtrophymall +protool1 +yonginmis +iyuneun +evenmore +xwave +sky2000aa +kibon13 +mario18121 +aoupersona2 +aoupersona1 +oneself01 +kjnet76 +eomji7 +ycleeforl +cjcylee7 +ericgolf +cndizn +tkshop-029 +earlywire4 +ya09 +bc16271 +kgswon2 +kgswon1 +ohdaejun +kukujj +soccerbu +minetatr9974 +tkshop-021 +nalee14 +ojiland +godotechmijung +lamodem +leech12208 +tkshop-017 +liveplane2 +white5now1 +einein3 +bas03134 +bas03133 +rockwall +tkshop-009 +smartdev1 +bobbarabob1 +seirart +alecas +yooo782 +wwwmall +gi439admin +monicahair +koo6002 +hicodi +baitas +greendoughnuts +livedo3 +shimsb75 +momipotr6212 +crazyshaun +jangle65 +tig233 +gi210 +tig232 +chadago +epdevb +gi208 +thacker62 +physicaltherapyadmin +gi191admin +pungnew1 +bread35tr +kami44 +bastoni1 +gb098 +lemon8250 +dbwjd1004 +mkphw214 +mkphw213 +simsimq9 +simsimq7 +hm50061 +simsimq5 +coratex +dalcomkids +daontech1 +toktokki +charlieyeo +happyyj86 +godo177009 +kakti2 +kalito +sooa5548 +altaicho2 +kkk6099 +hjw8500 +ssh83311 +shellac1 +bumilion2003ptn +esecretr8940 +koo5586 +ipayetlaw +hyunqok +rhim1119 +litta1999 +ldy03021 +zespa6 +zespa2 +godo176770 +srynn1 +soulful12 +enjoybike +shchdud +cc.m +event114 +naknrak8 +imarketing026ptn +hk28914 +knight76671 +vnsy +sjk8402 +good365food +ss2inctr2004 +ahn1222 +mudeuntr7725 +ey12191 +voff +doosikl +spacenowave +rice9661 +sportsday +morenvy022ptn +dvdva +haendel +chaawoo +soccer11 +iskra1 +ocmart4 +shug00 +heeddong +pringlesk2 +w861104 +bikeshowtr +cacmall +tnfusdl81 +weisure00 +silvercat +eballet1 +daolnet0072 +daolnet0071 +redhwang993 +redhwang992 +sjkw0414 +ddolyka9 +ddolyka8 +shinhwatex5 +siiyou +saltlakecityadmin +emcpb +p0won01081 +saicorp3 +saicorp2 +saicorp1 +hhkim5112 +yjscac1 +nicekido3 +pajama6 +pnlenter4 +jeongeun +sinjin77 +kyungmin-009 +remnant1 +godanbtr8389 +saehan5340 +autoconfig.whois +coolgun83 +venygood +c1s1o1 +cana12122 +wkdrns09 +gi15admin +kemuri82 +cmd79564 +nearndear1 +herbjuicy +yoonwata +commando1 +gi444admin +skinspecial2 +ajume1 +stdevos1 +timeseo +ks75251 +tjytr5406 +npaper212 +kwakcom4 +tinyeltr2933 +autoplaza1 +elju6 +elju5 +elju3 +elju1 +ggamsnet2 +ggamsnet1 +asiabridge1 +starfatr7406 +totoking3 +eastvillageadmin +phermia +vium +toolemart +qwe912-016 +cadboy2 +dicovery +autodiscover.whois +bwchon +gustjrr2231 +sykim9403 +glandblue21 +shaians1 +ylg2670 +elime +despresso +vasuburbsadmin +condance1 +hanzone4 +zeroxl +plantgallery +gi195admin +cleanok +mj23kr +sunflower92 +wassadacable +sin51151 +urer +sa1004a +bmw320d +mahleria +kotak04411 +fourthb2 +nycdowntownadmin +jinnwon +dlwngml672 +missung +ynsgbm +wjddmlwjd678 +alexno11 +rydesign3 +rydesign2 +inasound +aidpower +iskins +duwls2651 +uucloud +master375 +kfinco1 +michiget +shoptr6378 +yongilpak +imarketing025ptn +dbnaksi +pmh0624 +mbsool +unui +eprincess1 +tyg3 +loveganome +yeahyayo11 +gd3363 +skorea20101 +cosmogoni +gray73 +hyungje +ljs3943 +hibini +morenvy021ptn +ver5 +ver4 +comprarautosadmin +ink2150 +parkts3242 +jbhg1231 +canopix3 +cnccom +potape +wecomarket3 +wecomarket2 +jj4ncts +cncbuy +only114 +jinana004ptn +parksee8 +batekorea +izzang65 +quiltquilt1 +wjglobal +erunner2 +nhdcmart +corecube +key4989 +jaemin205 +jaemin204 +smartpremium +refurbishadmin +missred +magicyi1 +pdy07911 +yun890804 +nightcoffee +cookingequipmentadmin +ssinsunwood +joypartners +whtpwls3 +ukino3 +littletommy7 +happygrim7 +yojan5 +whats1004 +ppippo +omi0927 +solmi213 +tryp +tryb +dfactory005ptn +choijy8767 +happyzone1 +bassoj +libbon3 +ho59ho1 +nodazi90902 +monblank +moonares +pszoth8 +dbsthf12 +kuiry0 +hyunant +enters +envious1 +silrupin2 +prominwoo2 +mdbaby +butgod +skiingadmin +dandyryu +cdma +soccerestore +konkuk-049 +safeuni +knou0505 +inteck2 +rugga24 +taeyonintl1 +doori91 +www.3g +akabelle +hjkhjk1410 +nailone1 +kodomo +enteen +motor6292 +ecopyzone2 +yonghun23 +soho10045 +dspnf +bumilion2001ptn +vlclgmd +thehogeon2 +thehogeon1 +ekhan +ai7412tr6555 +redmangchi +artherot8 +artherot7 +artherot6 +artherot5 +artherot4 +postshtr8475 +brandmall +godo174774 +chimique +dbsrud10131 +cutygenie +dsone +imarketing024ptn +heavening2 +leehyejin1 +cgolfood1 +ymusic13831 +hantmdwls +ryusoyoung +ipaysmartinside4 +ariel2023 +kdemall +h42310031 +morenvy020ptn +greenyou494 +kake28 +beibet1 +chul0830 +cha03305 +jinana003ptn +yedam21 +sehwadang +dsmnf +zalea +arrum486 +combacom2 +janusre +bngintl +yasira1 +nemostory +photonart +alani1 +conadeli +ucpb +dreiburg +gurrms84 +jjrepublic2 +ekcis +mamapai +zizibe5 +sseryun1 +suip +moamax4710 +merrygrin +saypc15 +youngstr6157 +heavenjade +kitlej1 +ziziba7 +jcd5144 +t9 +outriger7 +roberto17 +thepose1 +rook1261 +nfmbrisbane +the30dtr1835 +butaha +ch2365 +gi201 +basecamp65 +mungkle27 +omh8915 +mungkle25 +hongmans7 +jlove7k +boram30031 +gcsd33019ptn +naturalmomo2 +ssok +daejonilbo +edmport +kuhsre +minifix8 +insun09171 +yii +ohydragon +netserv3 +wwwalt +encoree +yim9885 +lottoherb +uxc3007 +hotelsadmin +mj1205 +jjungyk2 +pgupnpgdn +sonicbio12 +incross +pc0905 +welltuned +buffalotr +sindoha +led21tr +www.verbraucherschutz +sjwang211 +passionsubit1 +intcln1 +hyr882001 +rjmhouse +csb +vivianan +ecoritr5876 +alqorzmf +halu08152 +mpizone3 +hanxiaojie +gi200 +atheismadmin +gi322admin +assignments +na1010 +asia63661 +soho100410 +wkzid2 +wkzid1 +smdv5000 +dingdong1 +ezpro1234 +zeroboard +kiwi045 +tjwls80 +dev.intranet +geuxer2 +godo58510 +accmania +davicom +audio-bay +spo4 +ssupltr6200 +nice0472 +llim98 +redgrape1 +tlfdj22 +koizora6 +koizora5 +imarketing023ptn +chul0075 +kanku76 +powervk +robertpacino1 +spai +ipayamatchday +sogm +mirimkim833 +mirimkim832 +joo0575 +gomdodi +bethelav1 +ipaynomad62 +sala70-020 +sala70-018 +sala70-017 +sala70-016 +morenvy018ptn +sala70-014 +sala70-013 +sala70-012 +sala70-011 +sala70-010 +sala70-008 +sala70-007 +sala70-006 +sala70-005 +sala70-004 +sala70-003 +sala70-002 +sala70-001 +lavazztr9156 +hanacome4 +yeppy671 +muyoungs +slevin4 +neomin21 +ppod102 +jinana002ptn +imarketing070ptn +junss77 +core741 +ddalki0111 +jjoojjo +okabkorea +ojas201212 +ojas201211 +ojas201210 +limhj925 +backshtr0601 +clsrndid +photomate +kss1762 +lmj52501 +kbs60160 +mbj1752 +pygetec1 +kmg21216 +ndk0719 +giftlg365 +kaienb +hak0312 +soo9236s +bogosago6 +bascon +jikyjeon +orangesptr3 +orangesptr2 +orangesptr1 +khsdad +joejylimtr +dscm1 +cartechadmin +hojung1 +tomatoi5 +yeonsung-090 +vizworks2 +vizworks1 +showy1 +dla60253 +skjp +taylortr6432 +yeonsung-088 +mingihong +nakiha1 +seven2012 +gcsd33018ptn +lovegomon +jlsi1459si +poporu +enctotal +hiroeriko +godo279 +auctionimg +godoedu-021 +kopasi +eijih +lolipoli +tsj01080 +godo275 +truelinks +uneec68035 +drim365 +sincomo +oznara11 +hangingchair +krap635 +krap634 +shplus +ohhora777 +missc15 +bigpig043 +behip87 +honeystyle +finalcooo1 +santa114 +remixcartr +trykhs +carsmith +dubero1 +soojlee71 +uhmting2 +lucedeco +jangfood +bnckbr22 +heartbroke +junsic1 +sgtl +migafotr9290 +goyuil +host51 +ko32288 +ko32287 +ko32286 +aramong1 +ko32284 +wlstjs07192 +wlstjs07191 +forpuptr5490 +mihcelob +enepatr5166 +cstool3 +isungnam +memoforyou +buret6 +host46 +shinyk06 +nananaksh +imarketing022ptn +test18520 +designzibe +logient +seum +petshopsadmin +gointsunny +zzang09061 +kbsvitamin +flak882 +suyonga7 +luxfertr8958 +bkc86111 +strat791 +koo1411 +morenvy017ptn +metro71114 +lampjeil +shinyi61 +turiya66 +punchto003 +punchto002 +punchto001 +gollmoo +jinana001ptn +designmaker +yeonsung-070 +baxo782 +healingstay1 +inhee1008 +shoptr +flyfishingadmin +demoselffix +mirmirtr0954 +personallog1 +ymkm841 +okham621 +kwc0620 +jjgolf +cozyroom1 +ho0010 +popino +diychoco1 +jerryim +capotr0570 +euna0910 +ssdbr10 +buj8131 +euroshtr5237 +k8w3s2 +rnjs91315 +menstime3 +menstime1 +kemandwb +tcentetr2067 +clients.pmy +suken12282 +popo77 +jeonboo2 +jeonboo1 +sportsline-ver4 +spbq1234 +dalcom85 +fishingtv9 +myshop-014 +fishingtv7 +ykn0628 +xartcatr6602 +ssclan2 +pophit +min35841 +yhjj3 +simbaddacase2 +hd55131 +yangcheon2 +y989212712 +universoadmin +haieland1 +syj32562 +lwb62741 +ldm1217 +beb +gcsd33017ptn +jjhdha +dfactory002ptn +sa05311 +wiseket1 +mvpp10 +hairintr4904 +myshop-010 +shda100 +kpshop2 +kpshop1 +kankan1 +egtai +akiapc +ssodesign +gi10admin +houseplus3 +houseplus2 +s2pdevmimi +shinwoul +eibe1 +puhaha275 +kkk1311 +gi438admin +dodocupid +bbibbi +colorline1 +sincez3 +jojia692 +sincez1 +safecomtr +mineralallga +lat1255 +asde717 +sksskdi601 +moorkoreatr +xcolletr0603 +malgum1 +saehan1006 +ingemail +saehan1003 +ptuebiz-049 +spsports +fastpooo +ptuebiz-045 +ghkd1603 +omanmul +mint67825 +ilrang1 +xpop4 +ptuebiz-039 +goldcrtr3577 +vvstore2 +misowa4 +misowa3 +misowa2 +misowa1 +krownbtr9246 +cma7539 +digitalsori +sortie4 +designlink1 +ar0012 +ptuebiz-032 +phinix2850 +uditlife +ksj83351 +ptuebiz-030 +chorong2 +shaudwo +designline5 +gerpfile +minicong +ptuebiz-020 +chamalook1 +newsissuesadmin +hby12201 +imarketing021ptn +gobigstr +arsenic83 +gports +yeok78 +ptuebiz-010 +koreadaco +czeon +charlottetownadmin +ptuebiz-005 +egreengeo6 +ppymangs +jjggoo +morenvy016ptn +doyoulove +kf1234 +eastline1 +safewater1 +friends0447 +karl2plus +leech12209 +ina2011tr +jiphan +cyt51 +shawsank1 +johnj213 +jjungbal +clove1 +mvisking +mysql04 +wnddkddusgml2 +edugreen8 +sansamm1 +simon3979 +bomi05261 +webdisk.perm +jeanfarmtr +roseflo +selandshoptr +cabosan +studiostyle1 +gpdbs09 +clothe +reve19851 +pup7 +mi7437 +bluesea9311 +taraswati +bara38 +jlosak1 +hangaram +maxlim +apumpkin +foxlike929 +rcdh +foxlike922 +foxlike921 +vieltabagyj +bkcat05251 +ehara +o1045295435 +hangilman +untitle0 +ybibo80 +elance2 +elance1 +mayi17 +gcsd33016ptn +maummatr7381 +dfactory001ptn +byhemee +jichul5 +gomasil +bigstons3 +bigstons2 +mayamk +gskim3015 +jinvas +france88281 +gi200admin +enterlink +hbtgt0828 +maxion +poogaa +gucciman7 +ssncc2010 +somee5230 +s3freeintextacy +mail.39 +y12600831 +c653219 +hapsung +efree +siena1 +ver3-ext +jinuc1 +jeffkim75 +harcayo +misomo1 +sinhwa69 +design0jang +dhejrgtr7517 +rsk2577 +morenvy3 +morenvy1 +go4364 +woodworkingadmin +tomi1242 +tomi1241 +signtotr9219 +sinta271 +click2buy +zendys +samho9352 +visualpun +chan88191 +metroden +able882 +dainemall +dongiltr3463 +amazondvdtr +s3freedevsf +jjangkkw +eunamall2 +sunplaza +pung07084 +pung07082 +littlefarmer7 +swkim0831 +littlefarmer3 +littlefarmer1 +koreacake +xpege +philadelphiaadmin +ryuseong +yeson1 +priest65691 +s3freedevnj +racerelationsadmin +sansam31 +moderntc1 +nt002543 +s3test2 +s3test1 +bionprime +jjm005 +aron4097 +hk22827 +hue11044 +firstaidadmin +galbimyoung +serverhosting +morenvy015ptn +hoon27271 +pode +flyforest1 +historymedrenadmin +chsjin1003 +tibultina1 +chemicalguy +steven612 +s2pintnj +zenco2 +tyvldahf123 +jm19851 +luxurycity4 +cervicalcanceradmin +skhong321 +qltkorea6 +egcom +miznow +suny0286 +djdental +sik830 +srs1275 +swy99881 +teukwootr +jgh0735 +jinu34 +mkplaza112 +qazz +kwonsss +spdkorea +jja0521 +misoful +injujung3 +burlingtonvtadmin +jayeontr4710 +aries29011 +click71t3 +svn-fashion +oys12471 +bikon4u +tendgolf +edenkorea +snghyunkim +ossw +kwonss2 +kwonskw +westside18395 +othe +kleenup1 +goldonsmog7 +huppiness +classykr +amin77271 +rcfieldadmin +gibackin +yesb2 +yesb1 +samisound +hmedical1 +modernlux +wlikorea +woom012 +s2pinthn +oeeja4 +godo169165 +hnjiho132 +redstone96 +namu0369001ptn +itk9099 +hntile4 +nggift11 +infobank +doobagi2 +kleentek +soonetws +jirobotics +jjoonjang1 +yepia +mky19911 +banamoon7 +photohow1 +jangcong +trust4 +kswigo +uniquedonut2 +test15695 +sidmar +byoungil81 +rizhao8889 +republic391 +te8ayo +ggplaza1 +coupplan +qhejrqh2 +qhejrqh1 +jinix1 +rnskorea3 +rnskorea2 +dawun0121 +kimzzbcom1 +jdb63813 +rnddevsj +ysleeb1 +s2pdevjonr +saekcci +houstonnwadmin +semicolon6 +goksgo1 +yeain00 +highallatr +bau833 +nyhc +aldhr82121 +efolium +doomall +nwwp +imarketing018ptn +pcnara213 +koko3817 +mylovecx2 +jinhit +chojinbal +aneuntc3 +fix20244 +jjangair +ybhwin1 +morenvy014ptn +willbeok3 +bank9688 +christianhumoradmin +kjn18243 +kjn18242 +documentariesadmin +bigcoffee2 +alfoodtr4354 +answjddbs82 +aloe05041 +bums2251 +bau639 +fashiondesignersadmin +junkg0001 +motor1004 +gi316admin +dodoa +arcos001 +kwons71 +mentodream5 +mentodream4 +mentodream3 +kwons55 +kwons54 +kwons50 +kwons43 +quinka0707 +kwons39 +kwons38 +ggebi17 +kwons29 +kwons27 +kwons26 +kwons23 +so4879 +wantuphone1 +kwons15 +kwons13 +kwons12 +mbawool +gjwjd5190 +s3freedevmimi +pk-2 +tstyle1 +inntzone +cck5846 +hidochtr6423 +kunduun +join001 +metrustyor +yejin +skarch +kjs7494 +serverhosting240 +baro8949 +serverhosting237 +garagesadmin +bumhokim3 +giftall3 +bumhokim1 +giftall1 +kimsunjang +foruricky +usbrand0301 +nsta +idea1007 +serverhosting230 +kbk8246 +sekamotr4627 +tx.js.get +r23 +jejy1029 +s1intsky +nsoa +freshd1 +whitefeel122 +raeslor +jang850314 +cwith +draw10033 +godo168211 +reviewtr3247 +lohasfarm +jindam +dj6058 +olive10 +ohyo +ogolkei +r17 +r20 +sejinkid +s1intsdg +ukbul1 +pckbook +ipayatop0001 +sohojob2 +aspiringirl +bluesunh7 +jinbt4 +nsan +kaisership3 +bluesunh2 +serverhosting190 +needfor3 +treesandshrubsadmin +xkkangi01 +shmj73 +oitalia +santintr2102 +ekfashion +dypowetr8234 +moani001ptn +serverhosting180 +jinana +id41004 +gowj21 +polishes +ginachoko1 +hair1009 +adamspark1 +jangboja +bangup +capecodadmin +ohhi +dontlee +serverhosting171 +oxenbed2 +serverhosting169 +hdq1121 +ynmc89 +wangpanda3 +wangpanda1 +bstjoeun-020 +bstjoeun-018 +bstjoeun-017 +bstjoeun-016 +bstjoeun-015 +bstjoeun-014 +ipaywakemedical1 +toystotr3977 +bstjoeun-011 +bstjoeun-009 +bstjoeun-008 +bstjoeun-007 +bstjoeun-006 +donia +bstjoeun-004 +bstjoeun-003 +bstjoeun-002 +compnet99 +seok5582 +rotifl2 +latinocultureadmin +singimalltr +sorento800 +worldsocceradmin +halu03011 +imarketing017ptn +rokmc7483 +serverhosting150 +serverhosting145 +ddedon +kwak09791 +ecwox +pic2942 +yedo1 +edu49 +edu48 +edu47 +edu46 +joo7242 +edu44 +edu43 +edu42 +edu41 +edu40 +edu38 +edu37 +edu36 +morenvy013ptn +edu34 +edu33 +edu32 +edu31 +funnychild +edu28 +edu27 +braun +edu25 +edu24 +edu23 +edu22 +gi433admin +edu20 +edu18 +edu17 +edu16 +edu15 +edu14 +edu13 +edu12 +p13n +stat10 +pern0303 +kidmusics +mvpp +tti777 +cwpjn81 +urinong +wicked0827 +serverhosting124 +minsstory +choice00211 +needlepointadmin +amdesign6 +serverhosting119 +amdesign4 +05lead +domeketr5519 +maluwilz1 +han10711 +seahauto1 +levisman +mvie +monkie16541 +serverhosting109 +iconpower +mo05199 +mo05198 +mo05197 +mo05196 +mo05195 +mo05194 +mo05193 +parksunjea1 +wishcompany +serverhosting105 +rafjeon +s1intman +mattya +kkang17011 +anthem025 +dudshdtk4 +xfilesadmin +zecjojo +icd9004 +icd9003 +divxwant5 +shinjiwon +zzari7 +hansang4862 +zzari3 +soundforgetr +xnsytr6592 +primetest +hosan111 +muel +n2comm +lovehanna +spacectr1616 +zoodesign +kangbk2 +maidea +enoliter +tnzone2 +wico9160 +nixy +condo +gcsd33013ptn +kk0040008 +bung25 +mattia +homenhouse +mhtoilet +tnqls2023 +mamacloset1 +taiguk01 +nikekids +sejinbiz +realpicky1 +reedmall +jesusani +cookers2 +aboobar +neocla7 +kji135tr4770 +share88 +njco +www.hbh +toodury701 +sadream +yjcompany81 +roast5286 +jerome1 +zzamti +jujudeco +parkjoye +lyun +kiboonup +literaturaadmin +zzange +rice0905 +golfzen +truan1 +tweety0501 +qwertyu0303 +joinshome +yoanna +gutaguta2 +gutaguta1 +dlakswjd81 +packingclub1 +ssayer1 +siason +hellofilly +bbodongtr +ujkim7 +xixi21135 +xixi21134 +xixi21133 +xixi21132 +xixi21131 +ba2sports +skmarket247 +skmarket246 +skmarket245 +hostingtest253-170 +skmarket242 +mutu14 +mutu13 +mutu11 +gi184admin +mot2 +silinmaum2 +apc9 +queenscloset +ybilion +mntk +tetsu1999 +mioggi2011 +drbodytr +mjstyle1 +majuro1 +montonson1 +helpmatr3787 +www.pki +ajs707 +bumk22 +ecoco +designtory +morenvy012ptn +blisscamping +ncs4011 +meprette2 +poorbotr9474 +bedboy782 +formtabc +keyang400423 +keyang400422 +esusanmul +upperlady4 +keyang400411 +keyang400410 +hapoom333 +hapoom318 +cumni +gaonnara2 +anytube2 +kslee41 +swshop13 +swshop12 +swshop11 +kookjaets +jjuni225 +hanjh04011 +oa4u +mmix +mnbc +kslee01 +shee118 +elaine1 +kennyrtr3273 +nasagirl2 +nasagirl1 +balanceline +jmmart00 +neobob5 +neobob4 +ipaymaximagolf +sunwoowd1 +neobob1 +eversell2 +eversell1 +jey6766 +hkfishtr2422 +clmart +t-na +tdggolf2 +cpftl33 +gotoy7 +masull +epari0208 +eyaaeyaa1 +aqua1151 +hansollife +aderskr +sbprobio +s4st +megafish +woongnyu82 +netzang69 +innerweb5 +wksjsn +junsic-023 +junsic-022 +s4qa +j1224h1 +junsic-020 +junsic-018 +ravie2012 +linux11 +derkuss070611 +any49521 +sds45002 +wpdmstkfkd82 +carrots1 +masss1 +junsic-009 +s3freedevjonr +junsic-006 +s3qa +gaylezsladmin +tndorskfk +junsic-002 +jikku1 +gi500 +poorbotr8843 +oldshot +yhsatti13 +bluebetar +haihui +herbalifemall +datanlogic1 +scholarj4 +plasticsurgeryadmin +gwellkorea1 +saludreproductivaadmin +mgs7 +a01118a +hairplustr +ikin0704-010 +halimmalltr1 +kittysh +kendoo1004 +pascal75 +imarketing015ptn +dlarlxo +uriiyatr3460 +www.controlpanel +onedesign003ptn +bkfashionmal +sehooni +ardorwin5 +doubleyoubay +jhengsungil +shuzai.europeanhistory +adrianmole +accountingsoftwareadmin +koreasound2 +inseo75 +tohwantr6957 +morenvy011ptn +nettictr2174 +backsee +jesus307442 +granjete +jjangbaegee1 +ttmedi1 +ddmshop +hardess802 +summonworks1 +ninefruits +lietime99 +mdml +yoonei123 +yooriapa8 +yooriapa7 +yooriapa6 +ipaymiha124 +yooriapa4 +yooriapa3 +saegida +eoasis +lnbi +jmhn1015 +shop.flyfishing +martmoa2 +eutti204 +jeonlatr4684 +yasoo +medi3651 +cuclo +chwnm20123 +voltonenm2 +shantih +lymphomaadmin +erchjinho +victorssi +mbri +dldbsdhr +jakujaku1 +capdialog1 +nanumcafe +boardktr6805 +joy0120418 +ttldrmt1 +ujini1 +ebizs +gungantr6670 +maummind +leenayoon +neoncho1 +hspcgreen +souskin1 +k3238 +scalix +waterptr7232 +leedaeri +silra +spielwiese +gi477 +woodmoritr +mediabus2 +161 +thebaram +missmore +marske +techlene2 +sosl205 +gi476 +naadia77 +gi474 +s2pqa +mute74 +foursbiz05 +kukumada +leenawon1 +gminter +heyshotr4633 +ice97900 +mysql0 +point1 +gi470 +diyya +eveni331 +eduhope1 +pisirusi1 +liet +ai74123 +sfsdgdsfsdf +troikatr1776 +adioslee +xchres +roomsearch1 +smile08142 +smile08141 +babycenter +echohouse +studioakka1 +golfgs1 +marre1 +electee1 +chrezotr8204 +cookcore +billyksp +kblue06 +tbaksa +designshop +ipaypurmir81 +gortez +joo4348 +imarketing014ptn +mingoon6 +mingoon5 +mingoon3 +mingoon2 +mingoon1 +badasky +dbsrnwl2 +otw01 +jyav +onedesign002ptn +aya04265 +ddanga +zigprid701 +jhshin +flspent +jwny +ujin70 +inkcasting +maroo1 +celebritynewsadmin +morenvy010ptn +marom3 +marom2 +eanju +happysangja +fa9390tr4632 +warship +mi1640 +dbckdgns1981 +4989119 +doumzoosio +welskitr4589 +godoid-009 +yu0404 +mart3d +star2015z +pazziya +mobilekr +juun +yasan66 +asasjjj +jjung214 +salomon2 +exitmusic12 +jjin871 +goready2 +thenew452 +cacaka7 +irion1 +bonanza24 +mirz021 +silkworm1 +gi311admin +sunwooland2 +backhee +gusxo02236 +leej1001 +gusxo02234 +mvp21c +dive1 +gi290 +koyh01301 +fops0045 +kkk73324 +kkk73323 +heoshey +chohwanwoo +s2pselfrelease +laum +lcbp +spascal1 +cdb1719 +shim09 +florytr0668 +park868011 +isak12 +baracoffee +cookiepet2 +logthink1 +musai1 +gi460 +gcsd33010ptn +healthpia1 +cskcs +academy-030 +academy-028 +raypark +academy-026 +academy-025 +academy-024 +academy-023 +academy-022 +academy-021 +academy-019 +academy-018 +academy-017 +academy-016 +academy-015 +academy-014 +academy-013 +academy-012 +metrigen +academy-009 +academy-008 +academy-007 +academy-006 +academy-005 +academy-004 +academy-003 +academy-002 +academy-001 +saramsai42 +briquetrib1 +operationstechadmin +goldsea +dh9696 +serverhosting254-241 +kcs39014 +thsdudtls +oopsi1156 +cdmacs11 +gi450 +serverhosting254-240 +yaesodam +mobiledevicesadmin +jeje93kdy +ruffa76 +ecare10 +ksy103 +pkwmyth4 +pkwmyth3 +freecphone1 +totorozzang +jiworld3 +enjoykon +moonjw7001 +okwhitelily +hm35846 +chorc +newtroll +librosadmin +youstar2 +redmin70 +shcandle3 +good06084 +good06083 +bakingtr6528 +kohanee +ukstyle1 +goldone +dvdlife1 +ho55551 +soccerdom4 +imarketing013ptn +chou7 +bigpaprika +wj22745 +maryhotr0525 +onedesign001ptn +happyliya +moon01021 +lth1260 +rlice1234 +mi0728 +gosaib +taylormade2 +serverhosting254-229 +mblbumtr8147 +crok1 +img22 +mika7073 +edugodo-009 +morenvy008ptn +gong1225 +ipaykies3341 +ebizcom +jio841 +bizcbusan +dingm +sinic291 +zespatr3559 +eunsil0613 +doshkorea3 +doshkorea1 +tomalgim +tsgolf +leviolla +ksqms2 +godevsunny +mylove4u3 +mylove4u2 +mylove4u1 +gunp75 +ityn +harimeng +kddk +img20 +ifishlove +newgolf +kcis +win4eva +img18 +jjww +wjdwogns628 +istn +moonxoxo2 +k320sh1 +rain20722 +isoo +gcsd33008ptn +img17 +mari00 +dhss5 +kthkha +unibasic1 +shippingadmin +enjoydog +childrensbooksadmin +wrice +goodplusman +jeju57888 +wlsdud6222 +jinny38182 +larc17291 +img08 +darkvgirl +entereins +yoshikisuny1 +cafemaster +tnckorea2 +jjang2011 +img07 +ddmsal4759 +ujako89 +ujako86 +lakki2630 +ujako84 +alatda77 +firstled +sewingclub +imarketing3 +play04001 +friendlykids1 +ddomddom +honeymoonsadmin +serverhosting254-209 +vipjuitr4157 +ssunshower +medi1193 +medi1192 +gi442 +christ101125 +firstju1 +happy06063 +enjoyday71 +yamakko831 +pnk01180809 +img19 +bbmmart2 +pnksol13 +y2bcom1 +plcretail +shinpei2 +halladonma1 +tradappy1 +llkmll +cheezsaurus4 +cheezsaurus3 +cheezsaurus1 +market2013 +imarketing012ptn +oceanblue1 +sugarlong1 +gi440 +sltlwjf1 +rusidnew1 +madetrue +brandbaby +kkma1117 +dearderm +hanna5291 +wane1277 +felicidad6 +imys +sametour +trioutlet2 +lawenforcementadmin +a1.pwr.a02.f4s01.logo +morenvy007ptn +ucomedia1 +s4intmimi +deokjune +ds9324 +serverhosting254-201 +ksowlv +o8naman2 +ssspsysss7 +paldorok +ssspsysss5 +great8403 +ssspsysss3 +lixxi2 +lixxi1 +serverhosting254-189 +saedin3 +sadmin2 +gnoneint1 +pnppc001 +daytur +eightday +ansdudwn12 +midofood1 +tnxod0430 +jebl +jworld2 +dguri +jdis +clifwear +ahzoa5 +wingworld +wkaxld07 +wkaxld06 +wkaxld05 +wkaxld04 +wkaxld03 +wkaxld02 +wkaxld01 +capra782 +nonstop731 +heypon +a1.pwr.a03.f2s02.logo +shumade +healsdak1 +kokozenytr7919 +kskim36 +kskim35 +sina119 +kmyhsid +hvco +furnioffice1 +simdae1 +autodiscover.entertainment +hsyo +goho69 +dafishing4 +newyanus76 +boy50402 +knpiantr9201 +ruhitr7902 +headcom +iop2621 +jssh0802 +hspm +uhan2009 +supeolle2 +ipaywelesclub +gcsd33007ptn +space8943 +dj0123 +gi427admin +serverhosting254-179 +scotland +lepio00 +als112tr8265 +goldbal +moyhada +erogizer +s2pselfdevwheeya88 +admj +dwarflee +tobaccon +eedentr +jijon09891 +heylux +autoconfig.photography +farmsea +dfactory004ptn +hsdc +catstree +aiqing +y5001242 +suho91371 +h140498 +heykyu +cubeqam +cubeqah +bzjb1 +ugibang +mallmallmall +gemspell +mjstudio +farmri1 +jeon2001001ptn +xbogx1 +dcgolf +hawk21c1 +xgodo +smile5457 +sdcran +jeongtel1 +jwork71 +tahang64 +italia2u +dlenfl86 +enbmt78 +jch102310 +gxms +bagpia +lgk327583 +berymilk1 +mapget +jjoggumi002ptn +sosppor1 +pionext +juliet0691 +an19400 +anygear1 +smartpuppytr +doctorphoto +morenvy006ptn +phoebe56651 +yesdaehyun +tnhawaii02 +tnhawaii01 +hmss +crc11 +angela02173 +angela02172 +styleftr0769 +pionet3 +pionet2 +artalltr +dlawk1234 +dlawk1233 +dlawk1232 +dlawk1231 +iampartners +kyoung0915 +khjin31 +idix +milleiber +knpiano +gbicom1 +zeejun +ywdeco21 +wonhh74 +autodiscover.antiques +chinasample +hjpark4 +hjpark3 +hjpark1 +nebalrokorea +aeroc171 +primenext +incubus07231 +pekoe44 +chicgirl84 +mentopark +insaero +vyard +a2232682314 +mbkangtr0339 +topseed +enggul +smoothguy1 +needlesladmin +bravolej +osm5353 +designnut1 +cjy05131 +grym +webdisk.beauty +ekfrl1007 +gcsd33006ptn +power8029 +counselling +webdisk.promo +tonyhaustr +procnc1 +sohokorea30 +min8938 +autodiscover.financial +oderi2 +tong043014 +byjen +jhome1 +cw153 +soundmedia1 +sohokorea19 +scalemart +serverhosting254-149 +fdoor1 +dpency7 +zeen77 +dragonhwan6 +ipayicewindow1 +dragonhwan2 +jinwoo7922 +jinwoo7921 +eofldjf2 +eofldjf1 +menpico +gold7771 +jungpum +kspack +ilsimdongchetr +berry6600 +snrn0111 +godowebhard +lbj0202 +higb +dameetr5725 +bamboobebe +cubefnp +mansa9 +wookh +oxengi +insaart +frandutr8883 +angel2468 +vnfma215 +rental1 +wssin6w5 +hayfine2 +specialists +traceroute +glsbike +s4intsf +kji59821 +ssusdii +myfitting +woodc +helpboy +spike0330 +bnjmalltr +s4intnj +kjbaek1 +hop2yun +eneuropaadmin +sciencesladmin +lsinstr +gi178admin +squadsb +sk92791 +gyoungdug +imarketing010ptn +cooky +panchokmul2 +soripes +s4inthn +jjoggumi001ptn +enfmedix +safeboard +up5907 +mjinst3 +mjinst1 +gi430 +sportsmart +sullsull +cplusadmin +morenvy005ptn +henb +cindy8121 +khs535-009 +hyunjoon941 +spromotion +kdypsn5 +muns45 +khs535-008 +sweetforest1 +lyusia +julaikorea1 +biketek +popoiland +norang10075 +iamsoyoung +qcidea1 +jungs79 +psalms151 +sinchotr5575 +como2 +bongsem1004 +jsjang693 +jsjang692 +ijoaau2 +nemodol +orangeave1 +hj1000y1 +dogmanse +mver12 +goperuadmin +dons823 +teatigs1 +jwdleho1 +safecompsladmin +redbagstory +aserving +amapspace01 +skinmecca +bioflex1 +stonehous +emmarttr7025 +s4release +unto1 +kika0505 +s3devsdg +bikerak +gi420 +balgolla +craftcream +enfid3 +enfid2 +chrisjlee +sehee50871 +autoconfig.entertainment +junhair +gcsd33005ptn +sheet2 +webdisk.photography +jcphone +wlsdud3243 +soundstreamtrans +shinhung1 +garam4292 +sj8410022 +goodqt +meepobtr2122 +muyoungs2 +umjui741 +parkssgood +bizcdaegu +jungo87 +thestotr5627 +won55 +nstortr8909 +fox4864862 +today242 +cutqueen +gike +gjam +manjdk +spio2tr +munib1 +alrzldirkwk2 +parkk018 +jjungeun1981 +backup90 +gien +snh4u2012 +chindo214 +dnwls21 +sangpetr75636 +jeonga12031 +hakmaeul +ssh9751 +orichair +gomuin +rogellean +jangjunu4 +jm10301 +kuc012 +ghey +feelux +cafricool1 +zetmin3 +cello2017 +misojinal +coffeeteaadmin +backup69 +gftp +mangno +gi414 +autodiscover.photography +kidscollectingadmin +gi410 +lovelyand7 +annaflora1 +gi398 +ypp000 +sinicare +backup57 +krukovo +kimlleo +saskatoonadmin +sbgs22 +kjr7846 +sasari7217 +sasari7216 +sasari7215 +feelie +anewface7 +nndesign2 +anewface5 +gerp +gomsin +guitarbugtr +jhl02001 +nasagirl +lion9898 +rikyjeon +bancrest +mooyemart +hanaro38941 +morenvy004ptn +redmanso1 +jumoney +h2fishtr7956 +footmart +ener18 +ener17 +tod0108 +mikilove2 +boanmart3 +boanmart2 +tscorp +hm5989 +gongzi +shaina1 +toyfuntr0890 +jjtamna1 +smc1052 +smc1051 +jhmoon +serverhosting99 +serverhosting98 +serverhosting97 +serverhosting96 +serverhosting95 +serverhosting94 +serverhosting93 +serverhosting92 +gongte +sevenbiketr +gi397 +ligtime1 +bware +kcc75731 +bikeing +geniuseh +reikaz2 +godo158416 +revive22 +hama1245 +joinshop +gi406 +baekwh +gi2j +koil09091 +unitrust2 +leejy1229 +danyangok1 +lamanh +bwlee +wskang7 +autoconfig.antiques +wjtsyg +gasinaeya +ksmkoo +zecipe +baekop +ljm00552 +autoconfig.financial +webdisk.financial +dastard1 +k2worltr2721 +hohyung +webdisk.antiques +smiledtr3201 +hkm0803 +gi395 +gany +gcsd33004ptn +utub1 +kyungmin-029 +esmt +sesdd9546 +inbeom20023 +vlvl933 +gi404 +kyungmin-019 +baekby +strabbit +mtvice +gi403 +nocsunfood +gi392 +boazfood +kkk67082 +topjjoo +lightree +roulette +gi295admin +nmj851 +espclothing +emmom1 +leeje1125 +soyou1221 +bigstarkid +s2pdevsunny2 +ks56906 +aone4945 +ahzlomall1 +bikefac +fila0116 +fk5577 +dndauto +mbk001 +ejrdl21391 +ahn12221 +gi401 +silgange +dasung1 +sunny386 +caravel73 +wcanopy +agimanse +chhubcas2 +lovegate7 +sgirl33 +oatopitr7394 +partydress +imarketing007ptn +yonginmis1 +sudaebak1 +kssunmin1 +gi389 +dptnfrh2 +uamysoul +vispelar7 +peopletr3877 +morenvy003ptn +badu78 +ynskorea1 +toolsdev +kginicis +yurigudu81 +sonmk1122 +enha +duuub2 +coolgen +jsoutlettr +southparkadmin +ranger12881 +autosusadosadmin +zona671 +teamo1114 +jangkn1 +daontech +sm5w80 +skykeeper5 +kyc5398 +hcompany +trione2 +trione1 +eloi +wellbest +kki6564 +greenmart21 +mocuni4 +poweryongin +jeay0924 +bandykorea +aincom +silhihi +aqua982 +aqua981 +xotns771 +abrahamsheen1 +thesuptr +sleepspa +skylink +steamltr5295 +pluto0628 +viewzoneintl +happyfoot +kibon131 +w415chdl +keelisk1 +hohohomimi9 +hohohomimi8 +hohohomimi7 +hohohomimi6 +hohohomimi4 +hohohomimi3 +hohohomimi2 +hohohomimi1 +gcsd33003ptn +ejnj +cooldk2 +badkid +debak +aqua792 +aqua791 +clary777 +qosse01 +baru31411 +bluesunh2-039 +bluesunh2-038 +bluesunh2-037 +bluesunh2-036 +bluesunh2-035 +bluesunh2-034 +bluesunh2-033 +chuckman +bluesunh2-031 +bluesunh2-029 +bluesunh2-028 +bluesunh2-027 +bluesunh2-026 +bluesunh2-025 +bluesunh2-024 +bluesunh2-023 +bluesunh2-022 +bluesunh2-021 +bluesunh2-019 +dauri9 +bluesunh2-017 +bluesunh2-016 +bluesunh2-015 +bluesunh2-014 +bluesunh2-013 +bluesunh2-012 +bluesunh2-011 +bluesunh2-010 +bluesunh2-008 +bluesunh2-007 +ojas20129 +bluesunh2-005 +icmkoreatr +bluesunh2-003 +bluesunh2-002 +bluesunh2-001 +duaeod78 +jsh1143 +hgh9112 +limeplus +dorangmal +nanulee21 +egoist1391 +whitehometr +ssb04091 +do93099 +do93098 +do93097 +do93096 +do93095 +do93093 +ksd09132 +ksd09131 +ipayokfoto +gomcnc +buj813 +pnksintl +wishhouse +wonders +quarterbag +gi353admin +gi380 +chamalook +www.40 +babyrb +studiostyle +koreacm2 +iamymj1 +gomast +crom4404 +vodasipi +es4self +kjnet761 +osgagu1 +dlqnftiq1 +splaybill3 +mukmul +kmk5719 +macdesign1-010 +poke1007 +dhshop17510 +dlwlsgh03 +kyoulri +imarketing006ptn +ssk5589 +shinkee1 +jwko21c1 +chaos19952 +dandjik2 +dandjik1 +hvi21153 +hvi21152 +ripsoul1 +www.49 +min21321 +ver3-biznet +kth4989 +luviewtr1058 +cookie2 +cutesarah3 +goksgo +gbk2073 +miffy30412 +revital1 +dongyang152 +go1394tr +dcclub +trizen +www.47 +morenvy002ptn +rossi3 +eduo +ecocanvas1 +chha9 +pirenze71 +www.46 +ash4287 +chocogtr6562 +changstyle4 +kopasi21 +enrental181 +enrental179 +edu9 +edu8 +edu7 +edu6 +www.45 +www.43 +gi374 +www.41 +santorini +outstage +enrental176 +iri750 +sonang79 +enrental171 +edkg +enrental169 +cookers +mychoi01 +bbcountry1 +whataplay2 +whataplay1 +www.39 +golfya +ecoi +enrental162 +domostyle001ptn +gold5tr +enrental161 +www.38 +gi370 +nc5manager +cmcr3 +pongdang1 +golfup +drjungle +depaola1 +enrental156 +cpm2621 +enrental155 +ggosijoa +safecare8 +safecare7 +houseplantsadmin +safecare5 +safecare4 +it2gpc-029 +wnsdmlx +jbk7143 +hotsauce1985 +ipaymiha12 +golhs1004 +primese3 +primese2 +cozyrang1 +it2gpc-022 +sw7114 +www.35 +it2gpc-019 +www.34 +tecumseh +smile0814 +cakelatte +st06072 +induk +golfgs +goaustraliaadmin +gcsd33002ptn +it2gpc-011 +it2gpc-009 +aimbio +malarb +jaks2233 +errordb +www.31 +gi366 +it2gpc-005 +smpys715g +it2gpc-004 +pocq1004 +eumby7 +bongver4 +wldb5568 +kmk5116 +jang62510 +daezanggan1 +julujuly +ns3000 +dimf +s2freedevwheeya88 +zigwatch +sun99251 +morenvy030ptn +dreamfield1 +do91 +www.28 +ohdaejun3 +leespocket +kukujj9 +aka082 +sbk +nonmission +afroamlitadmin +samdo02252 +www.sbk +pognibiz +maxfeel1 +primehtml +soccerboy +kku19781 +gi422admin +mistyle1 +swenlee +junee11 +soccerbu1 +dbout +mimineaqua1 +polomixs2 +golatv +studiostory +selyoun2003 +guitartr6386 +bsfactory +gi360 +thgus99311 +yyh1204 +byha +onlyalice +corefit12 +cindy812 +endiettr7344 +nmckorea +devu +imarketing005ptn +jereint +ottchilstore1 +aznymohc010ptn +polomixdb +dbold +gi145 +cindy741 +gi470admin +walesadmin +autodiscover.imagegallery +autoconfig.imagegallery +bizforge +jh56441 +hessed +morenvy001ptn +webdisk.imagegallery +love05tr +ilrgglho +korva52445 +maket1 +smile0225 +wooricoop3 +dasung +ty823096 +banghanbok +s2pdevwheeya88 +darknulbo17 +mx100 +papameal1 +leech122011 +sleepy2 +podseowon +yyu35355 +hgh911 +makedd +ct04 +boorusu +enaroo +grandplan1 +ocktool1 +dbmk2 +brandtown2 +nanzzangna5 +ddcr +gi350 +primeins +lhy699915 +inurface +cwj0933 +cwj0932 +hyteid +ksjs12 +alsals71791 +qwzxmm +jaeil13701 +baberina001ptn +catsone +dbmk +hcseafood +oroanreb2 +bongtooi +geosunglife +wkrkfcl001 +jerusalem +dh88 +healthplanadmin +dudwls3498 +zucca1 +gcsd33001ptn +wizstyle +maxbest1 +bestsellersadmin +cotton3 +happycyc1 +love1002 +herbnyoung +dkhousing2 +solee1120 +bsm6 +ys9914 +jjmk123 +chyc +hanxs71 +maureenjewel2 +burundi +trikke +booska1 +dongwon5 +dave2 +skblossom1 +rkdeoaks +prochoiceadmin +eclipstr +collectmineralsadmin +kingcome +kyky1 +jmelody +icenerve +enamoo +wilywily2 +mh2012 +gfeshoptr +iveloce +cgod +golaadmin +royaldtr3914 +leehoon79 +uro4122 +sk8mania +lwt2013 +bbbusiness +dasom1 +ipaytamarama3 +godomall-060 +www.29 +alleyhouse6 +gi339 +queensadmin +rayfoto +gi336 +gi99admin +footidea +yog05192 +gkssk020 +kyhj1022 +gold2523 +imarketing004ptn +polarsoul +arkas22 +gi140 +automotec1 +godomall-050 +tanzania +enaksi +sawoo45 +ty1029 +gjim0515 +loan2345 +ardormin2 +ardormin1 +lookihyun +koreagolfshop +december12 +hl3qye +godomall-039 +boem +gradschooladmin +twinborn1 +beisboladmin +bromang +gugu59203 +dizzy015 +gugu59202 +gugu59201 +godomall-030 +novelasadmin +herse2 +herse1 +vrtra +damom +aritani +yurim0607 +a-land +sriver7410 +laborsafetyadmin +ecojejuwork +godomall-020 +limetree1 +diycars +qingeun +aspoon14 +aspoon11 +maini1 +bangsanga +cjs68 +juni416 +jjs4422 +jjs4421 +godomall-010 +nicehong1 +concertsh1 +shl19551 +gs96604 +gs96603 +godomall-005 +cwcho77 +herotj +sossay1022 +nyfactory +wise69763 +gi290admin +djshiva +koangjin7242 +healthinsuranceadmin +pfpenstr7881 +gngnt1008 +cayl +youngnam3042 +bobby8088 +bobby8087 +homeimart1 +filmnara +bobby8081 +dbbs1 +eng1tr +junggomaeul +sens1984 +uiiudesign +myanais00 +paegilju +zenith2734 +jarin625 +patme1st +manjang10000 +steng19 +arthistoryadmin +audrnrdl1233 +audrnrdl1232 +eumjiwon12 +power2000 +bsshon +jandie1 +godo55552 +bonibell +arre +tgyh1004 +mh1225 +suyonga8 +rdl +psy23251 +gloomypig +webtrans +herebaba +aqus +designhug1 +futuretak1 +jtapparel2 +neomagicshoptr +ssh4862 +ssh4861 +piohan1 +godoa3-029 +cjifm +kyungse3573 +tkshop-019 +oco823 +eurohnj +banggitong +beez102 +beez101 +majisun +xorudtkdtk +godoa3-020 +aqr5 +incobb +gi320 +michaelkkors1 +egreengeo4 +egreengeo2 +te0404 +trackandfieldadmin +haoting11 +mameden1 +mmarket +exhobbytr +durnfk +mirimi9 +godoa3-010 +coppa +freemo1 +biotis +givesoul9 +godoa3-005 +salesops.team +s2pintsf +luxlucci +rithfale25 +happyclay +wingk +asadal020ptn +hothighest +choice511 +tjdghgud +omero1 +ssanc01 +imarketing003ptn +oriontek1 +dongwontc +apci +chadago5 +id1230 +chadago2 +chadago1 +headami1004 +forsythia88 +ats1212 +martaza4 +martaza3 +winad +kumin07112 +kumin07111 +flseok3 +flseok2 +jung94811 +catsneo1 +chaoskym0 +ufosys1 +kidsclubsadmin +jcl2008 +daeju +dongin2 +dongin1 +chorong8293 +caspian +tucsonadmin +westvillageadmin +marketingadmin +organicstory +historiausaadmin +dvradmin +gi310 +gi298 +jackal +militarykor1 +rora9326 +yoddanger +syyoon +leevelys +lanoviagd +gi307 +devdf +shortcake +sohojob9 +sohojob8 +sohojob7 +sohojob6 +sohojob5 +sohojob4 +sohojob3 +bluesunh8 +sohojob1 +bluesunh4 +cjscn7 +bks1016 +dizzi50 +cjdgo +knowglobal +leeaprk2 +claraj2 +msc98703 +msc98702 +msc98701 +iocean2012 +ssambo1 +sohokorea003ptn +topwatch-trans +allc +soyariel +jg130412 +cafenoli +kobomo1 +thdnjs8112 +jewelrysoo +nbsaleshop +pdae101 +ssaljin +baechu0910 +shadirs +kbcs9771 +xxizee2 +xxizee1 +catsnara +gi150admin +dbakintr5549 +leejihamcos +e1it32 +o2music +cms8673 +tenatena +tstkim4 +tstkim2 +tstkim1 +jahunbangtr +gi306 +wonilchoitr +bbac +godochina229 +cellgreenon +littlelys061 +godochina214 +designida1 +rxbiketr +elastica91 +phillife1 +gi305 +fiestasadmin +jsjsjs2 +bayyard1 +emerzency +didcksdh331 +hgh5076 +kumhoan1 +pkw6862 +ijennifer +leesujae17 +ronin9499 +chogood772 +chogood771 +justaromatr +ncr0331 +s4freest +bokmintoy +aidl +eggtoktr0979 +mynezz2 +dongwon91 +gi304 +herbsj +s4freeqa +webroin +gogun3 +herbok +roots3 +sstudio +comaharim +pius11151 +seemille +bota1004 +kimpanjo +herbjo +salarymanbox +bydharl1 +yhstop-025 +freedai +dongang +dextoon12117 +daedoosm +yhstop-022 +innakmtr8388 +shamu +kwonbing3 +imarketing002ptn +yhstop-020 +hanjun06112 +yhstop-017 +gaintelecom +rpangkjh3 +gi303 +rebirthsr +enamutr +gi301 +krdoctorstr +hapeach +cokelabo +yhstop-010 +capdocokr +camwear1 +sadeabu +gaigalu17 +gaigalu16 +gaigalu15 +gaigalu14 +gaigalu13 +gaigalu12 +gaigalu11 +gaigalu10 +innermarket +loveyu722 +gi167admin +ormedic1 +qkrwjdtn113 +qkrwjdtn112 +roori2 +roori1 +yuyounho +byesang1 +cguru +hm0008 +decore1 +noobs69131 +shingh29 +juju122786 +kidswritingadmin +silveresk3 +jump420 +sjblind +guswls0630 +wkha72 +tourmania +et0124 +sohokorea002ptn +nmagic2 +nmagic1 +webtong2 +jeillatr9571 +sohokorea29 +sohokorea28 +sohokorea27 +sohokorea26 +sohokorea25 +sohokorea24 +sohokorea23 +sohokorea22 +sohokorea20 +sohokorea18 +sohokorea17 +sohokorea16 +sohokorea15 +sohokorea14 +sohokorea13 +sohokorea12 +sohokorea11 +sohokorea10 +catsmart +jung302 +tabacconaratr +todajung +ysu7100 +acuzzang +spolandtr +foundstore +aegkorea +ahri28 +koolfella03 +terahtz2 +abec3579 +dh894k +sospica3 +sospica2 +gooutkorea +sammarket +xogud0415 +jj72721 +junwhatr +acecnc011 +tys102714 +awsfreedom +wannabtr0916 +brent +slabest11 +ssri48 +malltest +gyubin2 +gi416admin +fingerist7 +glamstarlab +hjw85001 +uyork +finewolf3 +nala +ssangchu +gon444 +ryoo95551 +filmbank01 +adinplan +miran96681 +chho1 +kirisatr +gbmarket +aldo211 +enamoo1 +hyoroo +yunojapan +herbhouse +yooyk1 +tribalgear2 +gana2000 +dove2 +macdesign1-009 +macdesign1-008 +macdesign1-007 +macdesign1-006 +macdesign1-005 +macdesign1-004 +macdesign1-003 +macdesign1-002 +macdesign1-001 +ldhoony2 +dasomco +increase3 +miffy30411 +ifagain4 +manlejjong +gi280 +j9020721 +imarketing001ptn +uniflame2013 +projectb033 +theavenue +kksshh99 +sanyangsam1 +ptpcomm +duometis +ecoplanet6 +hsnambu +hhhsolution +s51022 +wkaxld052 +bigmantr0007 +tjsgml809 +wkaxld051 +shsaa8101 +asahi01 +kisszone +jung0711kr +snjfurtr7672 +wkaxld043 +qnibus1 +publicnt +bpms2 +spechrom0506 +aulkorea1 +helloko +s3devpekoe +sm7002 +sm7001 +shelko281 +jaeyon27 +leehoon792 +leehoon791 +banhallawoo +thezillo +godo-11781 +naigie5 +naigie3 +sohokorea001ptn +godo-11768 +englishmug +ipaymico7com +lcd80202 +rwakeman6 +rwakeman5 +rwakeman3 +rwakeman2 +novocos +eju2013 +maha09 +wkaxld012 +nobody2 +buycare1 +apexstudio +bignbigtex +dpxndkf +pqpq2250 +wkaxld004 +sportsline4u-ver3 +lucegolftr +chukactr6385 +akfoajfo +kyyong1 +gnomya +s3freedevsunny +colorman +rivusdesign +elnutr +cfdevel +persona871 +truegaon +cfdevel-anzeigen +richgold3 +kimkiki +officevendor1 +cfdevel-immo +littlean2 +bestbuyusa2 +ydptong +wedps25742 +iapplian +m9611053s +classices1 +dventure1 +qortodn +godo151041 +cubeintextacy +skautous +ain100 +bori4 +serverhosting189 +donnadeco +imarketing066ptn +cfdevel-partner +cfdevel-stellen +xjunior1 +cdmnte2 +enjoybike1 +evenly229 +maternityadmin +paranormsladmin +aile357 +gi276 +eko4849 +boova +icecast +wordprocessingadmin +mathlessonsadmin +phplive +gi270 +wiggle2 +roseflower1 +gi266 +iamss21 +sohokorea +waassa1 +turdef +alren819 +alren818 +fspatch +shabell +hanstar +alren816 +iya04052 +mypopcase +sury2848 +gyilove1 +fabholictr +runnersworld3 +runnersworld2 +raybond +woodongeya2 +henshe +silvercat-v4 +joyav1191 +kho8939 +danuri +youngr3 +reslinux247-254 +reslinux247-253 +shoeshouse +tgifd776 +coa6071 +wawapcb +tgifd772 +memong1 +dbfls7 +somino58861 +hcjung1117 +ke6753 +yumpie963 +yumpie962 +pradise3 +muell1 +rookiehjy +in4sea4 +sjk84021 +imi13801 +lean06012 +keydalee +soracom +shoeshosue +asadal039ptn +corelee5 +good365food1 +odc251 +minkuy06221 +linkz4 +ramhandmade +treedn +cuberelease +apolloeos +junginsam7 +primedemo +nam92952 +jj70771 +iluly1842 +iapplian2 +fieldro +shoesmongdb +nyguy111 +abecast01 +free01022 +prmjung +hantek2 +agyang +qorthd2 +qorthd1 +bumystar3 +alsl981206 +guideadmin.metrics +bbanyong1 +ndealtr6026 +asonejkh1 +designer17 +yoolose1 +yesjubang +kkm77777 +loan23451 +pinkscy +hens77 +qwe78221 +solanin201 +ymj810 +akwjr1111 +cos80825 +urbanetr7159 +jwandme +dc894 +cpla2k24 +cpla2k11 +nzshop24tr +timelesstime +bonheur1 +tone20102 +heode2 +heode1 +tailorsuit +mswest +moira1231 +bestgsm18085 +passtwo1 +ljs49541 +dapanda114 +hymsl1 +akwjr1000 +hymtb1 +dc821 +pravs1003 +fly2820 +dmsdk6029 +mn76541 +gapkids +jayhome6 +janghana7 +dodolfarm1 +vdlove8 +dvdvcd +hanforyu2 +hanforyu1 +gomoojtr7532 +ncpsys1 +bodyx +romiok +jejuilhak +sneakersadmin +kangbo822 +anzelto +finedeal1 +jplusinfo +fishingadmin +agigatr +uzooin1 +gi262 +eth19012 +dlqmsvhddl +jinnwon3 +linemk +europa88 +unnatas7 +pluter +busanftr5995 +gi94admin +potowoong +naturalalice +jinirose0709 +eksvndsk1 +happysaem4u3 +rosa2007 +sunyaro1 +tai1228 +jumflow +jhm08272 +ccimartr7337 +kkm7724a +shoesmong12 +shoesmong11 +shoesmong10 +onlinefair +healthcafe +topic74 +sky10888 +siena59582 +siena59581 +fddsfd +fashion24 +y4219371 +em2daytr7411 +simple71 +mnkwear +yes991113 +flowerstate +yeowocom +gi259 +donbook +oceand +legiocasa001 +ddung6641 +chungangtr +chameleonwoman +opop997911 +hiwin77 +shfreetr1964 +agyangfarm2 +agyangfarm1 +hgh1302 +sm4707 +parentingteensadmin +ando1e1a +flower72 +edenhill11 +baby-farm +dc251 +touchplus1 +jdepot1 +chacer23 +clickit001ptn +purity194 +gododb +take26502 +youngface +ssonso +bobai +topia07 +sinbiclub3 +boseong341 +seven3 +senspot205 +ecofairtradetr +rubatocare2 +bomdigital +bonusp2 +bonusp1 +designeda1 +woonsatr1136 +vasooyu +headsetkoreatr +jamie32 +designcube +jmk7808 +eicompany +gmwear +pecglobal +jinboms +hunmintr2873 +hyunny76 +mjy10752 +wego4 +wego3 +nelly74 +icreative1 +funnydeco +radkay1 +kokoe2 +goldhase2 +boomin5 +boomin4 +mineedon6 +doriskintr +ssun587 +eliyuri1 +doufeelme +gi284admin +kosobank +goodgn2010 +matsutake1 +hsj1373 +roy0719 +nzlifetr9453 +cjwatch2 +brooknw1 +line46 +jhkwon85 +sdphoto +riravatr8889 +babekhj +shh920428 +dandy7 +rosemart +marsch0625 +dandy2 +kddong1 +fohwchoi +aquaritr5907 +rosemari +hanbyul1 +googi813 +o2musitr2123 +esladmin +2meplus +premier72 +peter9961 +kimhong001ptn +godqhrgks +k7j6k7 +maybe2012 +icw0073 +madmax200 +yjo0906 +jeon20016 +jeon20014 +mirean2 +bsrabbtr5724 +seostephen3 +finger822 +agil282 +cho110044 +wkdb99 +madeit +baey03191 +majorbook +hongilyua +hjlepotr7846 +luxurysp2 +samsungitv +singme81 +min12111 +scale114 +madee1 +prettydari +aidlv4 +tpdud521 +gocost +cjb33335 +edun1126 +bebewise +smedia01 +limcha +kitels7 +kitels4 +kitels3 +miyoung07141 +mastertool8 +mastertool6 +sweetpotato +song5844 +imypen2 +limbo8 +limbo5 +shuzai.history +antiquesadmin +nari5221 +shyun8861 +gagumtr5758 +yjb4023 +recyclingadmin +redlineon +esosi79 +esosi78 +esosi77 +esosi76 +esosi75 +esosi74 +esosi73 +esosi71 +kesjjjang +motohouse +dtuomo +lilix1 +sesweb +cubedevnj +aroma1 +orange89 +beanmarket +pscsaws +gszkimjy1 +haebaragi +withtv +tryextacy +79house +noriter00x +seubyy +qingeun1 +jycustom1 +sweetyj +tnghxmfhvl +iamchudo +tlawndud1004 +adonia5 +allmedicus +yets032 +yeawon231 +goldbat733 +cdicn +jas7725 +comtive +applefactory2 +zinepages +dolcevocal +ismlove486 +bf4949 +ntperson +kkungku +wkdud14784 +pooh72583 +safecatr8353 +krlibe +godo146856 +yyjkingman1 +yju4uu2 +comsta9 +bookpot +ambleside62 +gi249 +bachstyle +es3self +truebowl +iprohmc +innergongju +ksw87091 +ccomz +kes5850 +wksckakxm +cirt +wbweb +hoo7878 +howhow3332 +txdns2 +txdns1 +blackboard9 +squared +gi246 +bbdev +camcordersadmin +gi245 +libraries +cakedecoratingadmin +goukadmin +gopuertoricoadmin +humanresourcesadmin +gunyis2 +gi239 +file023 +miscarriageadmin +cpapshtr7934 +bedroomadmin +gi230 +farbuytr9058 +doozycom2 +ilgun77 +bsmedi +kkh5789 +khy26833 +hanworld +chatyjjang +bezclub +wemake4u +ahnc87 +kitchensense +twintreekore +sgaqua +shoppingtong +treebd +tncmotor1 +soyamall +jgy6727 +sinjukushop5 +cdeco +compositeadmin +anytarot +bjtcoltd1 +mitme841 +oto04152 +oto04151 +baby-club +impmedia6 +impmedia5 +impmedia4 +ins98054 +ins98053 +hichang +cambibi1 +sweeple +pooh0220 +ckj315 +entro76 +popscoaster +akasia20004 +johnjacobs5 +johnjacobs1 +themin76 +damano +ttlc207 +ttlc204 +gi411admin +ttlc202 +sujiyayo3 +sujiyayo2 +daebagg4tr +gi162admin +asuratime +hanwool3 +sesoft +nekoidea8 +nekoidea4 +nekoidea3 +nekoidea2 +hanwooda +kumanbo1 +hot95291 +asas5377141 +tkd02241 +yaoming77 +emdevtest +hmfood1 +bookkey +rental1004 +indiplus +wlsdnr777 +jason65 +speedstackstr +tngmlolbbl +yjwone +sossay10222 +bingsugirl7316583 +vannersky +onnahana +vanessa6 +vanessa5 +rubydog +lsy831114 +heamil1020 +mlisttr +foxy1000 +prinseum1 +psj9362 +zese40132 +satunljs +kakaku75 +randynoh +dakorx +iplant +nat58164 +teentea +bueno-shop +hellofriday +bpksg1 +boynine +duka123 +waahaha-019 +waahaha-018 +waahaha-017 +waahaha-016 +waahaha-015 +waahaha-014 +waahaha-013 +waahaha-012 +indianapolisadmin +holidayentertainmentadmin +gi452admin +waahaha-011 +waahaha-009 +insectsadmin +waahaha-008 +gi219 +gai +waahaha-007 +dartsadmin +plan9 +waahaha-006 +secureserve2 +gi199 +waahaha-005 +api.membership +waahaha-004 +waahaha-003 +waahaha-002 +acneadmin +waahaha-001 +ky2900 +eom19828 +sbdesign002ptn +gi207 +es3rent +tintvillage +juna771 +anycasetr +qec +environmentadmin +smhyun741 +shafali4 +donabi4 +autowill +rora2500 +sandbox6 +mrotec7 +maimai1 +mariweb-029 +mariweb-028 +mariweb-027 +mariweb-026 +sandbox5 +mariweb-025 +astrologyadmin +gi196 +mariweb-024 +breastfeedingadmin +mariweb-023 +gi195 +webdisk.test2 +mariweb-022 +mariweb-021 +mariweb-019 +mariweb-018 +jobsearchtechadmin +mariweb-017 +mariweb-016 +mariweb-015 +mariweb-014 +gi194 +shuzai.historymedren +benrokorea +gi193 +w20 +ocspool +db06 +mariweb-012 +hbsfootr8762 +gi202 +mariweb-009 +gi189 +kugong90 +mariweb-007 +mariweb-006 +mariweb-005 +mariweb-004 +cheerleadingadmin +mariweb-003 +gi88admin +mariweb-002 +mariweb-001 +w14 +samdoic1 +moterora1 +wildwolf +tentingkr +naiasis +emcars +tobe7009 +voltrun +twotwobebe +apt201r001ptn +dev2-self +bond20011 +shinwonwood5 +shinwonwood3 +maano1 +momo3624 +diso98380 +filmtvcareersadmin +f8018011 +culinarytraveladmin +godo145509 +headin031 +webmail.login +sky07012 +bodhi +gi278admin +sky07011 +ambmembership +gi185 +s1patch +gi183 +sikgaek +wt23456 +savanna +devmini +soostyle08 +angelesymilagrosadmin +q5850117 +psdgogo +digilog +bigeagle42 +dh3311 +mymimin +claires +mstamp +ykm2005 +chammarket +projecta3 +projecta1 +leechandoo3 +focusing1 +munia +danbi06532 +fathersdayadmin +sleepdisordersadmin +m4498m44983 +audwls7117 +ipaydurihana7 +partyparana +foodlina8 +foodlina7 +wuriwa1 +adcenter +gi180 +syrmhj +willsadmin +shjk1013 +leghorn +dpwl5312 +lawschooladmin +mountaineer +jyw3727 +bpmem +autodiscover.painel +autoconfig.painel +webdisk.painel +gns2 +www.ehs +citio5 +uppereastsideadmin +gns1 +hydravg +gi170 +help2 +citio4 +jpboomv4 +valueitem +petrel +gi168 +docshare +akabelle02 +jmedia2 +kagu +kursy +rubberstampingadmin +aboutdssadmin +daytradingadmin +jmedia1 +sector2000 +aznymohc +babi570 +motelb2b +muckping +peoplelook +www.hobart +junco +www.stafford +cmcrtr7858 +kwons129457 +notepeople +gi159 +gi395admin +stlouisadmin +gi156admin +architectureadmin +gi150 +lesbianasadmin +samheung1 +smcommerce1 +brainegg2 +electionadmin +bebeadmin +chronicfatigueadmin +animalrightsadmin +healthylivingadmin +madisonadmin +gi130 +gi83admin +gi273admin +estatement +cncinc1 +souladmin +funnyhoney +bebetoy1 +www.tos +gi122 +netforbeginnersadmin +www.kita +jugendschutz +gi120 +cincinnatiadmin +echanges +gi393admin +candyadmin +gi110 +haedolli +simple30488 +pizzaadmin +gi101 +vinpaper +gcny +aldccc1 +africanhistoryadmin +airjoon78 +bandwidthadmin +ksg700518 +gi389admin +gi99 +gi98 +gi97 +mail.it +landlord +gi96 +gi95 +gi94 +gi93 +gi92 +gi91 +gi89 +defterim +gi88 +www.ad1 +s1.lzzh +gi87 +gi86 +gi85 +gi84 +gi83 +thanhvinh +cuxiao +gi82 +gi81 +gi79 +gi78 +gi77 +gi76 +nx2 +gi75 +gi74 +gi73 +gi72 +gi71 +premiere +gi69 +gi68 +gi67 +gi66 +gi65 +gi64 +gi63 +gi62 +gi61 +gi60 +gi58 +gi57 +greatoffer +gi56 +gi55 +gi54 +2for1gift +global3 +antalya +gi53 +educator +hatay +politicalhumoradmin +gi51 +gi49 +gi48 +www.chery +www.audi +gi47 +gi46 +gi45 +gi44 +gi43 +blogs2 +gi42 +gi39 +ntu +gi38 +portsmouth +3m +voluntary +lmt +gi37 +gi36 +gi35 +gkh +gi34 +salford +gi33 +glu +gi32 +gi31 +gi27 +gi26 +gi25 +gi23 +gi22 +breadbakingadmin +bpp +gi19 +img.narodna +gi18 +img.blogs +gi17 +img.tabloid +karamba +www.hud +gi16 +postgrad +gi15 +gi14 +gi13 +gi12 +bangor +wool +gi11 +gi10 +flowersadmin +datingadmin +gi77admin +tweenparentingadmin +superbowladmin +gi267admin +deafnessadmin +burlingtoniaadmin +eqtx +shuzai.gardening +huntingadmin +ufosadmin +footballadmin +gi145admin +personalcreditadmin +catsadmin +80musicadmin +shuzai.britishhistory +poesiaadmin +familyinternetadmin +gi329admin +oscarsadmin +geologyadmin +kmx +humoradmin +collectstampsadmin +gi72admin +www.vids +gran +cableadmin +ccm1 +gla +accessoriesadmin +gi262admin +gi259admin +rc3 +nytools4 +mathadmin +southern +nikko +arima +homefurnishingsadmin +industrialmusicadmin +loungeadmin +ebayadmin +momrecommendsadmin +cancersladmin +beadworkadmin +addadmin +vancouveradmin +slp +origamiadmin +financialplancaadmin +gi378admin +content.team +gi139admin +pcosadmin +wwwftp +stayingactiveadmin +affiliates.metrics +buscadoresadmin +autowax3 +manchesternhadmin +allforfamily +zzzcool35 +hugosoft +bluesky2969 +serverhosting254-254 +majordomo +mail-out2 +mail-out1 +windows2000admin +immagini +serverhosting254-253 +bagheri +guitarraadmin +strefa +226 +exoticcarsadmin +serverhosting254-252 +rodeoadmin +serverhosting254-251 +rza +weddinginvitationsadmin +gi66admin +serverhosting254-249 +serverhosting254-248 +ww1.co.za +www.els +serverhosting254-247 +gi495admin +feminin +nashvilleadmin +gi256admin +elcanceradmin +petsladmin +bif +serverhosting254-246 +serverhosting254-245 +forextradingadmin +mustangsadmin +serverhosting254-244 +unesco +hifi +aarmssl +serverhosting254-243 +webdesignadmin +collectsladmin +bangormeadmin +serverhosting254-242 +prettyaha2 +jarin6252 +hto +gi373admin +ip17 +jarin6251 +palmtopsadmin +apartmentsadmin +greekfoodadmin +cartooningadmin +serverhosting254-237 +fmr +serverhosting254-236 +info.lounge +gi6admin +glusterfs +walkingadmin +origin-download +advogados +automotiveadmin +serverhosting254-235 +serverhosting254-234 +serverhosting254-233 +gi61admin +gi489admin +serverhosting254-232 +serverhosting254-231 +applehearts9 +mxs2 +babyproductsadmin +gi251admin +712educatorsadmin +enchileadmin +pilatesadmin +serverhosting254-228 +serverhosting254-227 +gi179admin +serverhosting254-226 +serverhosting254-225 +labweb +serverhosting254-224 +blogues +serverhosting254-223 +serverhosting254-222 +webext +publicrelations +draft2 +webinterface +mothersdayadmin +busycooksadmin +imam +serverhosting254-221 +aceh +serverhosting254-219 +alikhan +orthopedicsadmin +gi367admin +semarang +gi128admin +canadateachersadmin +strokeadmin +medan +serverhosting254-218 +arttrans +homeofficeadmin +gi1admin +gi207admin +mountainbikeadmin +furnitureadmin +zipi +jem +nuevaeraadmin +serverhosting254-217 +serverhosting254-216 +perun +serverhosting254-215 +serverhosting254-214 +gi55admin +gi484admin +gi245admin +serverhosting254-213 +gi464admin +serverhosting254-212 +serverhosting254-211 +askweb +go1 +serverhosting254-199 +webtrader +mta004 +mta003 +serverhosting254-198 +mta002 +housekeepingadmin +shuzai.floridasporstman +verdetest +dnssec2 +css.m +www-admin +windowsadmin +serverhosting254-197 +serverhosting254-196 +serverhosting254-195 +serverhosting254-194 +enhanced +serverhosting254-193 +serverhosting254-192 +somilee2 +serverhosting254-190 +serverhosting254-188 +serverhosting254-187 +serverhosting254-186 +serverhosting254-185 +serverhosting254-184 +serverhosting254-183 +serverhosting254-182 +serverhosting254-181 +jailzotr7394 +serverhosting254-178 +serverhosting254-177 +serverhosting254-176 +khs640109 +pediatricsadmin +serverhosting254-174 +serverhosting254-173 +serverhosting254-172 +serverhosting254-171 +serverhosting254-170 +northernontarioadmin +serverhosting254-168 +serverhosting254-167 +serverhosting254-166 +serverhosting254-165 +serverhosting254-164 +serverhosting254-163 +serverhosting254-162 +serverhosting254-161 +serverhosting254-160 +serverhosting254-158 +ch779 +serverhosting254-156 +serverhosting254-155 +serverhosting254-154 +serverhosting254-153 +serverhosting254-152 +serverhosting254-151 +prettyaeng +serverhosting254-148 +serverhosting254-147 +serverhosting254-146 +serverhosting254-145 +serverhosting254-144 +serverhosting254-143 +serverhosting254-142 +serverhosting254-141 +serverhosting254-140 +multisite +serverhosting254-138 +serverhosting254-137 +serverhosting254-136 +serverhosting254-135 +designaide +serverhosting254-133 +serverhosting254-132 +serverhosting254-131 +serverhosting254-130 +like02 +versha +weightlossadmin +shoe9111 +igosantr +jinsi07123 +jinsi07122 +jinsi07121 +dugotech2 +jxmusictr +unizone1 +chlrhkdwhd +handdud1 +taijoon9 +taijoon8 +sero82 +tpfakxm3 +fstyle3 +tpfakxm1 +ilpumcrab +atfc1 +sm1026 +sm1025 +lkg2821 +autoaction5 +promusicstory +ucat-er +muttagi1222 +artxx +zlem24tr2876 +music104 +aiang1 +cholibdong +zywall +dicovery2 +bsun20031 +danggal2 +parkek3399 +snikorea1 +jin020526 +gi362admin +daitda +curlyseo77 +megacoffee +gcsd339 +dakbal +gcsd337 +gcsd336 +gcsd335 +gcsd334 +gcsd333 +gcsd332 +asr12 +arto9 +joeun01 +jnttravel1 +queenas +a7896bv2 +evintage +wooliad1 +dlatprhkd1 +ckh135 +rohoco +toshinchotr +hooskin1 +spreadsheetsadmin +morning1010 +ndaily1 +designaco1 +boojang +tjdfhr76 +pchw8300 +koomin211 +cisil2 +yhcompany1 +kkh3311 +bony213 +foodliatr +ajh2565 +bebecloset2 +aengrani +shine777 +wings911 +roby1977 +redmist8420 +bediant1 +qadw123 +kkarigirl-019 +kkarigirl-018 +kkarigirl-017 +kkarigirl-016 +kkarigirl-015 +kkarigirl-014 +kkarigirl-013 +gi123admin +kkarigirl-012 +kkarigirl-011 +kkarigirl-009 +infos911 +kkarigirl-007 +kkarigirl-006 +kkarigirl-005 +gabang36 +kkarigirl-003 +kkarigirl-002 +kkarigirl-001 +fixpage +bebetete +love2vent +mh12253 +korea25691 +chjm53842 +enargentinaadmin +vector16 +vector15 +vector13 +chicagonorthadmin +hancommunity +kperpect +yedawoomtr +iconbay +zayu18 +djdxjfl07 +okyk7310 +webkey1004 +adventuretraveladmin +signstar2 +signstar1 +kissryou +goinfit +coinsadmin +diso9838 +osk7777 +bobolang +gloomypig1 +suyonga10 +valentinesdayadmin +bobolala +newsinda +pinktailtr +hick409 +lakanto +cafeoutlet +all100tr7242 +nisimshop +iamlsm3 +elpaperie +ksh15142003 +pridegolf +inhakjis3 +inhakjis1 +luxurytr4814 +mecca3622tr +wlska48 +jkwatch +drimi +aqus2 +solian011 +purnnuri2 +purnnuri1 +hnaksi1 +jyyuni +roomnhometr +vartist +gunvest +signpost +mooncho51 +vip89 +abehouse2 +bshsky +justbiococo +epi6901 +kbase +fuzone +netcr61-030 +bumyul2000 +metalsadmin +kukjea1 +brandshine2 +lily95053 +lily95051 +hww3633 +hww3632 +motiblue2 +chunjang1 +you1smile1 +netcr61-019 +dralkaitis +chlaytlf +ipaybaobab2011 +wjcwoojeong +airqualityadmin +idiomart +zenastar +ganglia +medisale3 +kk446688 +netcr61-010 +02creative +ksp1488 +room4500811 +bike4 +myoyeun +sandlntr8264 +kyb1093 +mygodman2 +robert01 +olivemuz +netcr61-001 +poohkny90 +nintendoadmin +fight156091 +shimfood +gi50admin +amysred1 +h121519 +fixisterhous4 +noblemetro1 +zzang7912 +hyunism1 +w1nu10041 +ysj78716 +ysj78715 +millard +ysj78714 +anyhost +kangbo821 +wantedher1 +playclay1 +phanminhchanh +gi478admin +owieoe1 +lkj43562 +andrewjkang +janaworld +plumgarden +varioum1 +must05782 +yjleejun +gi239admin +comprarcasaadmin +memphisadmin +altrelsladmin +iovesoon +artbtr3686 +tattva1 +hometheateradmin +daewony1 +sweetcloset +takeitnow1 +ssanta3651 +plovew +granbury +freightadmin +modo10042 +espritshop +autoting +duicnc +sss29991 +blossomj1 +kay98631 +bigj2 +bigj1 +shpor1214 +ljp1100 +dgprint1 +winiworks +winxii +mariashop +tlatlao +nectar78 +sjakamai1 +only4711 +symy2009 +madeby42 +inchalbase +gangzzang00 +toyhan +cm.seventeen +artteck2 +qpit261 +kimsony11 +kimsony10 +autosply +cmkorea +lunasy14 +joongsan1 +realway +chakhankong1 +kmrush27783 +angeltr9617 +ibk4141 +animalsadmin +majisun1 +autonbtr5897 +thdgustn27 +shslion +ecovelo2 +fishfriend +suajoang +hong790418 +shaneman +90srockadmin +andyeven1 +qlxmftiq +hidejeeman1 +kkongtol +funkyjoo481 +zzinge3 +xxlae000 +rich20081 +nan2han +lhmarket4 +smartmov +godo142326 +danawagolf +pkujoon2 +ks080108 +banasun6 +banasun5 +banasun4 +exweb7tr4591 +banasun2 +byengpung +aliceddm +jjoyful5 +jjoyful4 +maming31563 +maming31562 +detoxktr6608 +kitlabtr +leejiyeproject +joonggophone1 +digistar10291 +agri00 +scienctr8825 +missultr +indankorea +zxc764 +swys2101 +apron +mygking1 +s3freeqa +aaid2142 +lhote0 +chunscompany +ppman1111 +congaru +www.ww6 +godo141977 +bho73 +zzang6886 +thdxoehd1 +qorwngkd +hoondosa1 +ham74241 +posncom +jm8248 +ywkimera1 +hans352 +enrental158 +ipaykingzoon +jungpotr7702 +kidsactivitiesadmin +lee590271 +yh78310 +avidleeda +seojun +gces1033 +conprost +akari2414 +randy381 +withpastel +sensti +yellowpisces +coney12 +coney11 +smarthud +wolseong +cleanat3 +cleanat1 +hongsd73 +hongsd72 +wakyakya2 +s21004v +dnlemehrm +lo0olz +ginza006 +hcbig1 +joeunphoto +www.wwwww +toughsky +hongsd12 +hongsd11 +magicmotors +qwop45 +duwls26511 +icnthok +hwangjiniw +jayeonmee +smarthan +bsfund +ptocoi3 +ptocoi2 +s07612 +hjlee2915 +lstrading1 +jcdldlto +ggambu4 +odsnote +picupu1004 +zms202 +sootdol +topaz8625 +hyfood +sun970815 +devrelease +ipayunseus +sonjimall +oj07042 +autosladmin +ksjjjks1 +lagunayang +adkang992 +survivaladmin +ssabari +viridis4 +inyeok +soole821 +dongkis4 +hachikuro +dlwjdfid +doowool +hoianfl +seohg1 +agrina89 +thewang121 +edeco1142 +americano1 +ballpenmoa1 +scienctr8040 +darksode3 +darksode2 +midong26 +smartedu +conely1 +hangsang881 +tsgim707 +oapack +almaher +tsgim703 +tsgim702 +tsgim701 +cnst1616 +s2pintsky +khc03433 +vdhouse3 +vdhouse1 +brid076 +godo141203 +dntwk83 +smartcs5 +minjin9999 +smartcs3 +smartcs2 +smartcs1 +almajed +olzwell +geosystem +mir0014 +unzip +bigin8642 +nomiri01252 +mijumart +fgwj532 +shcho2000 +jys199466 +s2pintsdg +s2pdevkthkira +godotechyby +winink +toluene2504 +jyh65212 +cavabien +main393 +asolution +jclayshop +ibrother +mothertr4216 +sunmoontex +the2102 +gmpcom +zerobag +youngg09the +ziba1 +elfnix +kishimo +oriongolf +iamkei6 +iamkei5 +reglasespanoladmin +propro69 +thehockey1 +the7shop2 +greenmade99 +grain52392 +suamplastic +danawaba +smartacc +eatbag1209 +edenhouse +godo140762 +hanawelfare +mastuoka1 +americahot +lyh67991 +chadvissel +monnani0735 +therokoh +ogs5795 +bonghang +btbgift1 +iamamine1 +joeunwoor +kdw134679 +ds22sonia004ptn +anygear +micoffee1 +manomano1 +nawaf +gi356admin +englishlitadmin +jongsori +japanrecord1 +gojangi +hersh80 +kchairtr3504 +ipaykauring21 +sildmax3 +sildmax2 +yyoo22 +jebeef +untoc +godotechsky +kitty8162 +dlsektk2 +webtootr4979 +jdreamer +pengolf1 +wbhouse +mary0534 +dongwontc1 +gi117admin +s2pdevoneorzero +klasf1755 +japanesefoodadmin +salescareersadmin +inwoo2 +herbolle +bulhandang +note4youtr +cipi22 +midomall +chdrkrtbwm32 +kjp8556 +jong69994 +canoeandkayakadmin +bullyingadmin +handsltr5719 +s2pintman +realmack1 +sjrjj +taoismadmin +parvez +recetasninosadmin +pensieroinc +gmail2 +mana1071 +dojagiyatr6557 +livonasia +hyangcountry2 +naraenet8 +yamanashi +truejisoo1 +bestd +godotechptj +inkeshop +bebepink +jaemin3961 +bestflasher +midimitr4285 +mac330 +www.for +www.eye +polladmin.seventeen +bbqadmin +camberlin +1stdol +enstory +yishugtr9470 +s4freeintkthkira +xigoldkr212 +altccatr9318 +gks7531 +hanjt67 +youmean08 +illumegate13 +s4freesetuptest +itswattr3663 +pjk4256 +mainsqtr8842 +pjk4254 +pjk4251 +asia54321 +golfmatr6324 +yks901 +designctrl +chowho1 +godotechone +mpumwedm +yahanbametr +injeongwon1 +ksc82171 +naraentr2791 +naraentr2786 +wing09 +naraentr2781 +chonbatr6758 +jhp0215 +peace09451 +naraentr2775 +godo139053 +ssadagtr5866 +newseoul +twomo +opstree23 +opstree22 +opstree21 +www.aoi +wjdgus +actonis1 +peter2277 +nan6446 +hyejin +bluesunh2-032 +beautyland1 +m4627225 +clubrenai +morenvy029ptn +famertable +hyorisun1 +gfph94461 +heejuz +belladdle1 +bokding21 +xorrb0604 +synack +kooji551 +prmart3 +prmart2 +prmart1 +wimhh2 +wine21 +natureaquatr8672 +rubas8412 +yajoongsa +godotechloy +pdbsabrina +qorwldrb +investingeuropeadmin +starbeauty +ksw80041 +radiojack +godotechlhr +corexmall +senal2 +ds22sonia003ptn +kyy33821 +yanoritr +rok142 +api.caloriecount +guitarplanttr +annis +bigecoltd +kpwell +hwangkeunho1 +sks0967 +youngmtbtr +shhoustr5979 +bl525 +lsg22841 +brheavy +desirable +lse03081 +hyran031 +godotechhym +sinura +gi44admin +bluezeta +fi6096 +fivesix1 +suhon006ptn +alicedeco3 +alicedeco2 +ohsungad +lee0932 +ocolortr3028 +sdb1604 +godotechgye +designfactoryfile +anma1 +anointingm +cncprint +lalaone4 +gi473admin +joosawng1 +tirestore +dhcurvtr1241 +naraentr2119 +hyunetre +gi234admin +crevate +syyoontr2302 +coupontr3371 +binuyatr1512 +bekei +gorani77 +wondongtns1 +progressiverockadmin +coupleboarder +assist05132 +assist05131 +captin832 +sweetyjeju +daein2 +wimall +edugodo-059 +edugodo-058 +lydiastore +edugodo-056 +edugodo-055 +edugodo-054 +edugodo-053 +edugodo-052 +edugodo-051 +edugodo-050 +edugodo-048 +edugodo-047 +edugodo-046 +edugodo-045 +edugodo-044 +edugodo-043 +edugodo-042 +edugodo-041 +edugodo-040 +edugodo-038 +edugodo-037 +edugodo-036 +edugodo-035 +edugodo-034 +edugodo-033 +edugodo-032 +edugodo-031 +edugodo-030 +edugodo-028 +edugodo-027 +edugodo-026 +edugodo-025 +edugodo-024 +edugodo-023 +edugodo-022 +mysohotr0910 +gaytraveladmin +edugodo-019 +edugodo-018 +edugodo-017 +edugodo-016 +edugodo-015 +edugodo-014 +edugodo-013 +edugodo-012 +edugodo-011 +edugodo-010 +edugodo-008 +edugodo-007 +edugodo-006 +edugodo-005 +edugodo-004 +edugodo-003 +edugodo-002 +edugodo-001 +bluesunh2-009 +babyatr8713 +bsdoye +mmagpie-039 +wowmin-019 +hb0201j +wowmin-018 +sonjjam08 +wowmin-017 +heecol +akfoajfo4 +daehi1 +akfoajfo3 +gnf1230 +wowmin-014 +wowmin-013 +wowmin-012 +heebum +mmagpie-029 +wowmin-009 +swancnt +gi306admin +airwalkbag +m491900 +wowmin-006 +hero870 +induk1-039 +wowmin-005 +ssalmaul +wowmin-004 +wowmin-003 +wowmin-002 +jun58012love +click7tr2661 +fillgoon +aleatorik +induk1-037 +techdatr4078 +mmagpie-012 +jaeyoungbiz +djpuer3 +godo137771 +cretoy1 +zinoent +pkdd9111 +ele2779 +walmartr9308 +godotechdfk +prismsystem +ye7605212 +lhs751 +nongsagun +skanskan41 +chsun7931 +sewingtr8878 +godotechchy +acacia36367 +gloria7 +gsdaectr0704 +dbtn1517 +ds22sonia002ptn +withfootball +s4freedevextacy +ramarama2 +ramarama1 +daedoktr5956 +southeastasianfoodadmin +liberty16 +dns32 +jym12344 +will34 +livinglife5 +sshp3385 +kenny68 +manguitar6 +viewone +nurseryadmin +sem0605 +spysman4 +hydm74 +wjglobal1 +swprotech +bellutr0046 +hongik911 +noa10001 +kvanes2 +kokacoffee1 +totokt +my7cm4 +oufo1 +treeofhill2 +sjh0177 +bricktechnic +lonsomeyez11 +daebok +dkdleldkdlel +oamart +stickerbank +jinok523 +corecube3 +whalemtr4127 +corecube1 +jsjfeel +epsetuptest +redlife82 +ugmc33 +suupgil +suhon005ptn +ph565tr5757 +dongateco +kbncomputer-029 +kbncomputer-028 +babyutopia +kbncomputer-026 +kbncomputer-025 +kbncomputer-024 +kbncomputer-023 +kbncomputer-022 +kbncomputer-021 +sannoul1 +kbncomputer-018 +kbncomputer-017 +kbncomputer-016 +kbncomputer-015 +kbncomputer-014 +kbncomputer-013 +bebob +kbncomputer-011 +kbncomputer-009 +kbncomputer-008 +kbncomputer-007 +kbncomputer-006 +kbncomputer-005 +kbncomputer-004 +kbncomputer-003 +kbncomputer-002 +kbncomputer-001 +freeimt2 +beaum +zpsla52 +zpsla51 +devjuso +alto1 +meatpojang +bluesunh16 +naturescent7 +bluesunh12 +bluesunh11 +bluesunh10 +campnetr3452 +ljha1017 +foxrain7 +urmine212 +wieluxe +ecosaver1 +atelierf +backup74-2nd +hdmedi +misnmari +soundnrecording2 +jisoo81 +darkogt +cb114 +freejch2 +ccamdio +hjs8603 +kx4123 +jhigh11 +asmamatr6103 +kbsharp2 +dlqmssjn002ptn +redtiger1843 +dami3224 +cressc1 +dlwnsgh04 +vcomm +artshop4 +rexdectr7541 +dns29 +yunchulwoo79 +yunchulwoo77 +choiwy0309 +fromysgd +chocobo85 +raneeman +ceocharles4 +nunbit69 +luxuryfriendly +s2fdevsunny +as2as +almightygear1 +godo136812 +rnlbio +ti2posrv +dpflsskfk051 +oosnuyh862 +myoungshim991 +induk1-009 +ps1203ps2 +godo86022 +victoriash1 +joyuneed1 +dns28 +comlab3 +peter0115 +peter0114 +kptool +samwonsm +szarbo +haemosoo1 +inonsan +yunth84 +ds22sonia001ptn +comeon19 +dns27 +anybuy1 +dns26 +iocean20122 +s1intw +dns25 +budgetingadmin +ipetbrand +wjddudejr1 +robo74 +irecc5041 +bigpink1 +trueguy21 +hwang9805 +danhbaweb +dbswndudv34 +hwang9801 +eraitman2 +teatroadmin +ru3030 +kangjoung282 +ata3dphoto +supplyctr +babara0307 +toody +yosong20 +bluework +aweadmin +kangageu2 +kangageu1 +armario1 +coolman6761 +s4freeintp +vincentia +s4freeintb +otthtr +rosebibi +dg4321 +azukis +hkcorea +rainbebop1 +catboy2 +suhon004ptn +ozmisozo +indihero +gigadotr3592 +digout13 +metaljin3 +metaljin2 +ultraracing1 +whgdmsdls1 +allat +saeroevent +jujutel +enst0821 +bj21c +choice1588 +soyariel5 +soyariel4 +soyariel1 +entiger +pinah85 +orb59 +goldman1969 +moteevtr7993 +funfungirl +orb54 +do9115434 +johnny421 +elcha1 +v6ralph +wizday3 +gsgtel-019 +ptsdadmin +jyhong85 +p65jun +morenvy026ptn +smarket11 +granadacnt1 +jgreenfarm +tortex +dlqmssjn001ptn +ipayobeauti +strangecat +versacehome +tkmulkorea +rurico712 +yourday2 +cmdesign9 +cmdesign8 +cmdesign7 +cmdesign6 +cmdesign5 +cmdesign4 +cmdesign3 +cmdesign2 +cmdesign1 +smarttest3 +smarttest2 +smarttest1 +rkdwogks +hwang9184 +naamzoon2 +jmj5588 +gi351admin +esajang78 +sensretr5948 +gerpmsg +kimhyohyoun +ddr222c +lohaspia +enhotelarv +leeah357 +visualmelody +bychoi8249 +sweetpack3 +uknew +inthou +sayin007 +gnomya1 +mtau01 +vndck +bungae801 +squaress82 +gsgtel-009 +hongdaesalon1 +ys0831-019 +tonnie1 +ys0831-017 +ys0831-016 +ys0831-015 +ys0831-014 +ys0831-013 +bawoo +decains +gmdrmalltr +ys0831-008 +ys0831-007 +ys0831-006 +ys0831-005 +ys0831-004 +ys0831-003 +ys0831-002 +beerline +sjsmssorjt12 +cellboomcos +brotherworkstr +dosxlrwhgdk +baechu09101 +word00521 +odjfnrtm +rt4403tr4716 +rockxury +chae64652 +chae64651 +ipaycozplaza4 +cyj0921 +urizone +gi112admin +styleeyetr +artshare +beansmade +jailzonetr +twowintr7461 +wowpower2000 +kescorp1 +janghs0620 +hyanni +wjseodns12 +dearosa +suhon003ptn +vsjangho +dlsl0124 +skorea2010 +syffb73 +staygold1st1 +bisniscepat +daesinmeat +jsonline +greenpr10041 +zmfhqk1 +jkut123 +ipanes +s2pintextacy +openfishing +samsungdica1 +gbeovhs +studiosato2 +nesladmin +fiberopticsadmin +quickandhealthyadmin +miningadmin +www.keys +gi38admin +arice82 +multinara2 +dnfs74791 +seojiho0722 +gi467admin +nimsikorea +aland +tonature2 +ipaysoulkiss00 +onionmarket +danbiftr4724 +trysf +salesdemo +saerohtr5219 +inganryu +kkssyyy +trynj +gi228admin +dimasqi +mindoro1 +ybk2002 +dbkn8700 +soonine +dubero +afikim58 +ukhan +ipaymtwonhyo +hs1862 +dicaframe +daara1 +pp49125 +hyunchun +hyconere +kbsmart +godoshop000-015 +godoshop000-014 +godoshop000-013 +godoshop000-012 +godoshop000-011 +godoshop000-009 +godoshop000-008 +godoshop000-007 +godoshop000-006 +godoshop000-005 +godoshop000-004 +godoshop000-003 +godoshop000-002 +godoshop000-001 +bioworks +dcomskin1 +ghddotnr11 +joena68 +pieen7911 +misspapa +freegine +xellos1225 +candyluv213 +cbkbass +acdcgoo +debijou +chorock +airtraveladmin +aktionen +cyj0213 +monkeystreet +caoliotr5340 +mobiple +torida +ggstory2 +ggstory1 +mky1991 +banamoon +ajoo5 +sjy07051 +dawun012 +pcnara21 +sun99424 +ubigeotr4072 +soaphouse +innercircle +orangepink1 +ukbul +godo134606 +kilim2004 +jgg1kr +choulhak1 +jeross +hegys0207 +babygirl2 +cotjdtn092 +cotjdtn091 +gackt500 +herbpeople +heret41 +rache01 +fishlove +foll0603 +yunkiri486 +roselian +z1m0b65 +z1m0b64 +woori0101 +leoirae4 +beebeez +artherot001ptn +juli451 +melitta +nemo-box +semicolon87 +rachel789 +arise +cocolulu +sesladmin +ssodesign001ptn +georgelee11 +derkuss07064 +choianne-020 +derkuss07062 +derkuss07061 +bizcsuwon +freejuick1 +bestbatr0142 +choianne-011 +chosale +shopisready +sooncho +caffemuseo1 +uokdc0079 +kracie +hpauthdream +madamepapill +clxkclxk24 +clxkclxk23 +thine20111 +ddr0715 +maya4000 +tustown +kcrjjk031 +aison +chungpung1 +ghj1123 +headachesadmin +khn3552 +cdmacs9 +cdmacs8 +cdmacs6 +cdmacs2 +hyaj13 +iojazz +uglypuppy +landasco +tkddn511 +namcheonwood +monblank2 +monblank1 +hjnetwtr1099 +sosobaby +by2004 +moon4284001ptn +yonacat1 +tradappy +sugarlong +epdevsunny +sda34431 +bigthing3 +bigthing2 +bigthing1 +clokemed +midofood +hummel1 +jinny10041 +kristingale +asadal049ptn +hjs5553 +insamq +edrf12342 +edrf12341 +nauridle1 +dbsthf121 +rcd73251 +ilovekichen +godo133852 +pfire +neoist77 +sinhanlight3 +mksssang57 +samjung2662 +hdiled +cjy0513 +sbmaster-015 +sbmaster-014 +sbmaster-013 +sbmaster-012 +sbmaster-011 +sbmaster-009 +sbmaster-008 +sbmaster-007 +sbmaster-006 +sbmaster-005 +sbmaster-004 +ggaemuk +fontmatr6627 +sbmaster-001 +silkriverfd +usehacker +wjdvnatiq7 +hopeelpis1214 +godo133761 +qcidea +ddres15881 +makingstyletr +jabiznet +bbosasiseller +kem08121 +sehee5087 +drdori1 +sjh7even +dreamtime +ndg1111 +jm1030 +pat50071 +kozola +rlatkdrjf +xnittr9142 +s2psetuptest +gambbong +grunamu +young87171 +badu4 +minzy28 +kisskey1 +taeyohan +gs9957 +visualhip4 +suomi3tr8442 +sk8923 +blinkisbling +solerebkorea +durgatm +redglass2 +sojungs24 +gs9881 +misslyn1 +andjean +kozmic +bodacompany +badas +comhdtr +artcook +viganhu +banana64 +miyoun913 +hj9795 +kws1324 +ottchilstore +healthynote +coff22man +afsco2 +prada801 +lastchaos +artcraftsladmin +mdbaby1 +infertilityadmin +hsjun1112 +bubicattr0219 +texmate +ambergage +harumemory +me9273 +xteenman +egreengeo +jsturtle2 +greenplum +raontec1 +comfun4 +mallboom +judokiss6 +ufosys +kpmobile1 +bluesunh +www.hi5 +ipayescooo +hosbinz4 +cihri1 +quizadmin.seventeen +vadesign +farm30 +gimp +tbjung3 +lenahc2 +jaeyoung1 +zirh +hummingblue1 +moongift +frombin2012 +goodreview +dkpingpong +godosoft-059 +godosoft-058 +godosoft-057 +godosoft-056 +godosoft-055 +cbk73768 +godosoft-053 +godosoft-052 +godosoft-051 +godosoft-049 +godosoft-048 +godosoft-047 +godosoft-046 +godosoft-045 +godosoft-044 +nairi +godosoft-043 +godosoft-042 +poolandpatioadmin +godosoft-041 +godosoft-039 +godosoft-038 +godosoft-037 +godosoft-036 +adfines +godosoft-035 +godosoft-034 +godosoft-033 +godosoft-032 +godosoft-031 +godosoft-029 +godosoft-028 +godosoft-027 +godosoft-026 +godosoft-025 +godosoft-024 +godosoft-023 +godosoft-022 +godosoft-021 +godosoft-019 +godosoft-018 +godosoft-017 +godosoft-016 +cambridgemaadmin +godosoft-015 +godosoft-014 +godosoft-013 +godosoft-012 +godosoft-011 +godosoft-009 +godosoft-008 +gamakjae +godosoft-006 +godosoft-005 +godosoft-004 +godosoft-003 +godosoft-002 +godosoft-001 +ms1intkhs +yooncitr7021 +kozo01 +tiumtech +ilabguide +imysen +littlebe +traxacun +obbs +herbhong +poloislands +aaacoffee +kisssilver +dametr7277 +somino5886 +daesun771 +ddingminji +babilove +trees +ecopyzonetr +treen +dlrlghks811 +ellinchung +kim8310k +ua9499 +faqdesign +mtes +yong1830 +inparo1 +prori6181 +therich1 +wiki12344 +hyung747 +edenhill3 +sfglobtr5110 +threesisters1 +mineedon +agiga2 +agiga1 +pig200301 +actiongirl +bileepia2 +ecofairtrade1 +doriskin6 +photickertr +canavena1 +jameslee922 +showtime +jwclub748 +jwclub742 +noyou55001ptn +aidl2 +mansoura +securedmail +mallaqus +justinny1 +ydyo +kimya +slasys1 +moviesadmin +diypapa +mmotorpart +godo132406 +sneaker1 +ochairtr7416 +smurp777 +jjh19824 +jjh19823 +shoulder991 +innsoo1 +kidzmetr4592 +dumbo8311 +fantfant1 +blueteng +icedemon +gi345admin +bbliving +gabanna +eunsungbae +ttutt2582 +godo132316 +sangt1004 +sangt1003 +spirrastore +peter731 +kimdsjtr0169 +travelmart +dlsl851 +tarih +jinsi0712 +whynot612 +ihmeditr4510 +iblind11 +tpfakxm +afrch0 +svmans1 +dreemee +oidb +jnttravel +ubispo1 +neofootlockertr +besyo +hialice +sora03111 +yong1360 +s2pdevextacy +hanasyj1 +pjhgreen +afox773 +baadshah +afox772 +soulmatebed +jongkuk2 +niland120 +s2fsselfdevsunny +carebank2 +carebank1 +sungardkorea +waterfall21 +ks29973 +valueon1 +hee530 +ksc74182 +rlaxoqhr7 +comesta +topart +dancershop +mecca36222 +skynsnow3 +skynsnow2 +freeman9634 +cocokids +sookin1 +funkyjoo48 +yesgo24 +azpeper1 +king99230422 +goulyeon +ticketman1 +soole82 +carolin1 +porntube +iki2626 +coolkiss1015 +lhmarket7 +lhmarket6 +lhmarket5 +anibig11 +odvelotr3621 +chowon +lh10922 +eightcollect +r42tr7690 +onesukyu +japanrecord +viva12042 +kajinsp +hwphot +lowfatcookingadmin +uhfc2 +flovetr +wlsrbdus1 +hallawine +en2free88 +en2free87 +en2free86 +en2free85 +en2free84 +en2free83 +en2free82 +dongasys +hongo713 +hongo712 +hongo711 +eropanda1 +kaiser2 +gi359admin +tra78 +ddorai0702 +knmidal +mshb13 +morgandev +key94120233 +assist0513 +key94120231 +hj8136 +mitisyun +hcorp5 +hcorp4 +yoon0 +kbyggkbygg1 +smartnara365 +lonsomeyez8 +roots32 +lonsomeyez6 +lonsomeyez5 +babyforce +bacaboca +goeric79 +tonong +enwj12341 +enteam2 +talatula +nipc6016 +skaudcjf +simswf +sstudio1 +seorabul1609 +zencostr9567 +uni1122 +wondongts1 +chouh1 +redmangchi1 +blueya9 +rhwntjs1 +chool995 +polar885 +ecinderella +bizfinanceadmin +polar884 +dh96962 +dh96961 +dkfqlsh3 +jeonil +jeonid +s2freeqa +jennysoap +yesfkil +renoirart +inpeed +funnkids1 +candyluv21 +sensenara +syeng1 +egowrapping +ggstory +wltjd999 +noru19631 +hollyhock +mashile8 +maya0824 +googondo +chungpung +fishing7 +jjpack +zaggusa9 +zaggusa7 +gi287admin +hanaweb +film09tr +bss699 +bycode2 +wanturoom +rodemgagu +hyssop03291 +cctv365 +klstory211 +newhavenadmin +carenet0123 +net00041 +wildkam1 +solgreen1 +envymall +freebud1 +gi33admin +gi462admin +a8544012 +demofreefix +hanatoy +joboksam4 +imarketing063ptn +bugatti2 +bugatti1 +millioneyes +godo131003 +tifhffld +eshuma +daewoonara +quddusrla2 +quddusrla1 +zangi2 +totalstr5555 +chefmeal +choone +kbyggkbygg +hyang2e4 +pikimini1 +godotech05 +godotech04 +godotech03 +godotech02 +godotech01 +tnwjde +kmh8766 +autoka1 +iw90952 +nytech18 +sivakumar +tnwo88 +dadaicksun +zion6333 +admin.horses +paran7730 +imarketing029ptn +yisukrye1 +yjb85582 +gs7228 +carpedium922 +designdemocracy +styleinseoul +sony724 +testwebserver +hanasbt +sonrojj +dmswn12031 +jsung9743 +ipayhongdetr +joyhil1 +acerokim2 +acerokim1 +moongubox3 +polysuka +cctx +workplan +mybrandla +gi223admin +sigoljang1 +sunmi211 +vigossjean +kyuso6079 +viki9209 +dreny7171 +jongkim882 +mini17495 +laiic88 +tv365 +mirhkinc +chinesesladmin +senior21 +dragon15251 +biz9742 +s4freedevp +jhsuh88 +s4freedevb +baekfood +happypsk1004 +ttt911 +samdaein +goodcaremalltr +mk82324 +neckknead +ccs.m +classythe1 +herbbox +gymnasticsadmin +biblejohn +gounface +nandamo1 +happystr7224 +e-yeha +a025088 +khakis822 +yb07301 +edumost +pkh35001 +todaymalldev +mindlerae +lowepro +autocos +agamo +baseballadmin +sinwoo84 +rainbotr6420 +ingodoman +heo38021 +ibtkfkdgo +dwelling00 +autocnc +mimpikang1 +cartier07223 +tongup2 +cartier07221 +greenmoa3 +greenmoa2 +ccam2314 +sea11sun2 +tookhanwoo2 +minuya82 +smreo007 +zhdfyddl +hyec9778 +dodoteen +petitmamang +kdh743tr7167 +emsp12052 +organitr7178 +inoble +pny100044 +godo130082 +pny100042 +pny100041 +pny100040 +bigbangt5 +pny100037 +mcmon +lkm721231 +secupc +pny100019 +seungsam534 +bluekhi +ssum331 +honggift +simhu +tp00781 +kyong12k3 +mistama1 +freebits +thd0950 +mhj104692 +todakorea +ksgolf1 +ccny +nanacotr4958 +sbs2212 +allmarket1 +hanalcd +5min +july6268 +missbotr1340 +design29202 +mir33091 +greennara +indibank +assetad1 +hyderen2 +hyderen1 +saegilfood +financeservicesadmin +www.umbrella +simpleplan +atokoreatr +koreahobbytr +hipolee5 +jmhyon841 +gkgk1212 +lounge-test +dgyoon77 +youarespecial +kwh8391 +sptcmrh1 +excelwis +manjis10041 +smash12121 +bluegun +credere2009 +seoul266 +globe3 +shero20001 +myjuyer1 +godo99762 +foodlitr2047 +ssukland +hvdica3 +zootrade +pknara +cmmgxml +zambus +tokyo9 +gs6078 +magic1tr2987 +livingartmall +awesom711 +totorujjun +waahaha-020 +hdcytr +pak4382 +yooncibang +ez1213 +biottatr9337 +sms9645 +jeeyae2 +mcitelecom +computadorasadmin +zambi3 +ipaywonderlisa +knetdev1 +zandie1 +cindy11142 +teappong +serverhosting107-156 +barbieholic +jpoomtr +godo128311 +freebeau +yayadldl1 +cmos01 +kmh7242 +gjs2ya2 +gjs2ya1 +magooganshop +anniy3493 +oneupfood1 +bigsmusic1 +yankeetr9528 +parksb220 +tong18171 +enpranihome +kimschain +oroyoeo1 +suyue +eod13923 +decorico +mikako1505 +brownmusic +marie70541 +ica0702 +innovid +certificado +circadies1 +tomceo +wgtrop +topwatchtrans +wnffldhp84 +factor41 +geriotr +ranye81s1 +mono85862 +mahasiswa +mono85861 +snusptr6247 +behomme +rnddev +mhjar232 +roadwise +najuoh +kocom23 +fat644 +eoq2592 +fat641 +leeruli1 +nova121 +tmpdb +shy0579 +ptuebiz-018 +hj71161 +serverhosting33-249 +boysnice792 +soosan21 +lhj930 +ych20102 +ych20101 +trooper +funchi +ubiplus +irises22 +citysound +mipds33 +maryon2 +jellyshop +orgio8848 +mscogo +sweko +ksuhyeon +spicyyeon +tomoro1 +hinomura1 +arecelcard +yedam114 +adoll +lenscare +tjfflfkd +okokna1 +hanbitart +swinpro2 +swinpro1 +greenmade +imurak +commattr8236 +mcinfra +moca771 +tlawo +shinan12072 +jukbox0826 +paragoncys +zama79 +marktwainadmin +juneogi4 +device1141 +voltakorea1 +dopamines +naturesvie +pass3097232 +seika +fsmatetr0262 +www.websoft +ha1jek2 +nicekido +babyhoo2879 +okparty +dlarkddnr1 +daedongsa +heybuilding +suun000 +sk3897 +rms004 +lohas500 +parkzicj1 +rabkorea +soqlcwns +white5582 +godo127430 +zion3914 +veilsuit +grayleopard +mirhkinc1 +lys64647 +soung4016201 +tnwjd483 +gps123-v4 +backchj61m +jellyp +super9373 +terryreside2 +qusxo6 +arenas63 +fsmaster4 +fsmaster2 +acryl +jejuhalla +insurancecompanyreviewsadmin +tnsehd33 +dreamfc +gi340admin +cjhlime011 +winwin20112 +luvhyun812 +rltnr2 +suri3 +suri2 +purelock +herbflora +bumho741 +sure1 +smlove1 +ong2012 +backup73-2nd +edu5011 +housemade1 +chs915 +fifkorea3 +sosom3 +www.umc +sbsports2 +jys142 +aliassu1 +hisynim +ableinc +ohyesysj +hjleports +akld9919a +ejrdka12 +kappa100 +praiselord87 +msbook +s4freedevsf +namdo0523 +zzarupet1 +tolbae +godo98089 +snj8188 +minkmink74 +passion9731 +blackpc +my-web +brch3927 +thinkfree772 +blackt1 +jyjyok +blackjn +ipaygspkr3 +vhdldpak12 +www.guitarhero +s4freedevhn +hyewon962 +puremax1 +kwkydoor1 +sweetshadow +mgmmdl4 +pkma00 +castroledge7 +jminhyug +ddos-linux160 +koss0965 +busan2good +fine1224 +thetanpopo +decopan741 +shineshoe +denimctr4582 +scuba2 +xlove10 +yjgogagu +pyo0325 +izu19811 +purelily +skycomm3 +skycomm2 +qutin7 +gurrms841 +uniquedonut6 +www.summer +guswls06301 +bass792 +gi101admin +jibong9981 +am4582 +ciclo2 +act21 +zion2977 +mogaebi1 +aromapack +sigongeshop +plases +sshhjjoo +ckhkill +fanta70 +faline +microsoftsoftadmin +contact2yeon +s3setuptest +qudwns36 +rentalpc +danacross1 +na52442 +jw75701 +maxkorea1 +misshera +omin0531 +happyag0211 +godobilldev2 +godobilldev1 +hsr12352 +na52441 +gloryinkn +juokled1 +leuny256 +huhu0821 +godo126275 +westyle +joyel01 +jejusc +myucstory2 +myucstory1 +teasle228 +mgraphtr6510 +njangttr2530 +tpana4 +tpana3 +tpana2 +qfriend +urijeju721 +godoacademy +jj060707 +heoja331 +jejuif +verylovely +slashbak +mansnonno +ekayak +gi405admin +seoinjae1 +fall76 +innobox +nystylist1 +kosres +bjm7x167 +prostatecanceradmin +utzentr2899 +zion2476 +egujjoa +ipayishop01 +minsuke +endan4513 +lohanlife +celebrityeroticadmin +realboutique +suga1 +wind23472 +cocoritt111 +kont +gungang +koryms +sonkang092 +ojm72722 +dlsdo513 +sonnori +csgbboss1 +supersoft +judystory +iamzookeepe +emgreen +print1004 +sgfood2 +cometbicycle1 +mystgirl +lovej1111 +saenong7 +promade215 +saenong4 +saenong2 +vanessa3 +babo7749 +fulam1 +childrenstvadmin +newcalion +babo7727 +purelady +m7bike +actorsadmin +gram400 +sorrjcyxl1 +jinyoo103684 +westso9 +westso7 +saynew +westso4 +westso1 +chnsky4 +rodemnamunet +khyunt4 +khyunt1 +scshin +klover001ptn +madamyoon2 +helpboy1 +godo96584 +bodmodutr +nfmbrisbane1 +ziani222 +kifood77 +littlejune +sssmi +getmind4 +getmind1 +suh10211 +fbi100 +subag +yiyinha248 +mcoamall +goodberg3 +goodberg2 +investingglobaladmin +food1232 +kkomi301 +cdsaesae2 +cdsaesae1 +pr2011 +osj0404 +babaco0306 +ninewishes +jran72711 +zang9180 +dhfl4444 +jhp02151161982 +dameetr +yss0941 +westore +gi27admin +minworam +yht81101 +mfccafe +kimura52da +eusebio1 +yesfarm3 +feroxkr +eunhyehan +i2r025 +gi456admin +hongmans75 +s6822143 +kkhy08076 +kkhy08075 +strongbaby1 +brapra +iq25781 +seongyu22 +ginsengnara +cooldange +godo125063 +seacom +bynu4225 +andbike +mykika1 +ssono +space87678 +vmsjune2 +a0250810 +myr1111 +siyeonb1 +volaweb +cocolsports +ucc93 +gergerh +s3devextacy +malfa0529 +aegkorea1 +korrg1 +k6807221 +fm014 +prayk10041 +redwingshoes +gadangel +eq18aa +michinkimchi2 +michinkimchi1 +top092 +yangleehair +junecrtr3729 +greenmoatr +namdointeri +kangsky2402 +steek +citynetphone +nagakig +nt99993 +nt99991 +tiuum +campsaver1 +otime5 +ssyu7852 +otime3 +otime2 +shallom30 +gi217admin +imshin +lab-ware +naseeyou1 +kkomange +kumhoad7400 +lovetournet16 +lunacy71 +haudxjjh3 +qvp081 +agapa1 +dzkorea +facstore +risingsu +ta9933 +rpm6400 +hitdeco +stbiz +loenstore +zuzu7967 +jihoon1004122 +ll2tr4543 +jsbae3408 +korkys +mer2371 +vbsoma-019 +jielkumsok +vbsoma-017 +vbsoma-016 +leesim31 +vbsoma-014 +vbsoma-013 +minso02 +vbsoma-011 +vbsoma-009 +vbsoma-008 +vbsoma-007 +vbsoma-006 +vbsoma-005 +vbsoma-004 +vbsoma-003 +vbsoma-002 +vbsoma-001 +winenara +halom65 +espacios +minicarnara +violetreetr +konkuk-070 +waahaha-010 +waffletr7761 +enple +usliberalsadmin +gogocar +sssk005 +cafeluwak +monomini1 +go123 +konkuk-059 +storymall +neo8977 +jdy482 +hyunte0615 +pureleeyun1 +lky2345 +chinaguide +jinne02051 +ltecompany +icalys +konkuk-039 +boy6girl9 +boy6girl8 +jadepost1 +bhc2013 +glinda +david1982 +asphodel1 +greengrimm16001 +konkuk-029 +ptlabo06 +bigstons +hscommunity +choin33 +badpark +simmong4 +simmong3 +simmong2 +konkuk-019 +neverdiesp8 +neverdiesp4 +ssgj1 +neverdiesp1 +italianfoodadmin +greenmoa88 +manomano777 +kpigup +ssey1 +shalombada +that2000 +konkuk-009 +kienforever +yanji12 +juette18 +sseon843 +sseon842 +nacaoo88 +rabbityjk3 +bomin78 +mj12052 +godogodo-050 +godogodo-048 +godogodo-047 +godogodo-046 +godogodo-045 +godogodo-044 +toqha0719 +godogodo-042 +godogodo-041 +godogodo-040 +godogodo-038 +godogodo-037 +godogodo-036 +godogodo-035 +godogodo-034 +godogodo-033 +godogodo-032 +godogodo-031 +godogodo-029 +godogodo-028 +godogodo-027 +godogodo-026 +godogodo-025 +godogodo-024 +godogodo-023 +srobe +godogodo-021 +godogodo-020 +godogodo-018 +godogodo-017 +godogodo-016 +godogodo-015 +godogodo-014 +godogodo-013 +godogodo-012 +godogodo-011 +godogodo-009 +godogodo-008 +godogodo-007 +godogodo-006 +hwa4385 +godogodo-004 +godogodo-003 +godogodo-002 +godogodo-001 +warrior1 +wyfe +dshuni +corvettesadmin +mun0305 +tacocoke3 +rarestone22 +shoestore +y0017035 +workingmomsadmin +inobrid +godo94768 +sisamall-025 +sisamall-024 +sisamall-023 +sisamall-022 +sisamall-021 +sisamall-019 +sisamall-018 +sisamall-017 +sisamall-016 +sisamall-015 +sisamall-014 +sisamall-013 +sisamall-012 +sisamall-011 +sisamall-009 +sisamall-008 +sisamall-007 +sisamall-006 +sisamall-005 +sisamall-004 +sisamall-003 +sisamall-002 +sisamall-001 +jinnam01 +gumho7033 +gumho7032 +nj00901 +pearlsma5 +pearlsma4 +eztechtool +ckyudong +natural365tr +kshan33 +rlvndqa +magicpuppy +thsui +lhl1982 +s2pselfdevsunny +qwe879 +gameshowsure +wlgus0411 +prime05562 +prime05561 +jejufood2 +magicadmin +mariweb-030 +sintoboolitr +www.sol +ykssk209 +dbflfksp +smreo0071 +mariweb-020 +jinying7771 +meatstore +cmpsnd11 +attybook +m.development +mariweb-013 +parkyuri01 +edell2214 +mariweb-011 +designhappy +mariweb-010 +hhjk090 +mariweb-008 +kailas1 +ohhifeel1 +babyonekorea +quitsmokingadmin +pulmoo1 +www.smi +bsmedi1 +j0103s +wskang71 +hanakwon71 +coolsangchun +musicsesang +feha1004 +soguitar +violinbank +white1331 +inicis +ams1237 +blackhairadmin +me0200 +seinntech +sgadidas +babokachi1 +frandus +greenfund +coverqueen +gi334admin +ucnownet3 +ucnownet2 +dalgudayo +daumplus5 +daumplus2 +isshe841 +msdoye1 +dlaehd12341 +layetttr6850 +caribul +nospin1 +rolex112 +jimi1234 +nam0428 +blueland +ywoojoo +www.smc +pink129-029 +pink129-028 +pink129-027 +pink129-026 +pink129-025 +pink129-024 +pink129-023 +pink129-022 +spiratek +pink129-021 +pink129-019 +pink129-018 +pink129-017 +pink129-016 +pink129-015 +pink129-014 +pink129-013 +herherbada +pink129-011 +pink129-010 +pink129-008 +pink129-007 +pink129-006 +pink129-005 +pink129-004 +pink129-003 +pink129-002 +pink129-001 +acasia4 +acasia1 +patiloma3 +flood37 +contentsweb001ptn +choib81 +syoe1992 +theprayg +cjrail1503 +hoeun55 +lsyzizibe +ansdydwk77 +ocm8245 +moon2nn1 +ryusc +jygolf +vitamineya3 +qkznsocm19 +qkznsocm17 +qkznsocm16 +qkznsocm15 +mqnix00272 +woohaha1212 +woohaha1211 +dental08011 +raonix17 +ilovepcbang +rina4634 +daewoong0525 +aceoutdoor +smilebean4 +fexchange +jacekorea1 +lemoncandy1 +saiquick +hana2ju +youmi4262 +buy7942 +yoojimin +stmediakorea +sujunggun +enamoofs +ebookstore +chon355 +chon354 +bunkers +chon351 +hera463 +sambsamb4 +sambsamb3 +sambsamb2 +vivianan8 +www.sit +vivianan1 +woohyang +hadouken +poplarathome +rhj66072 +barreme +yibok1002 +bunnywtr3877 +cbin777 +misomommy2 +impnic +fan951 +danahblind +sj7727 +crowhell +jsminicam +alejantr0857 +dalnara +zaxshop +pyhee74 +maytwo3516 +dcstore12 +dksckdejr081 +kidsclub1 +leesum0101 +kukujj007ptn +oliveanne1 +liongroup +hospitaladmin +psmcreep6 +psmcreep5 +psmcreep4 +jjoo12341 +lalahaha7 +freebits1 +tong12104 +motorex +ckzks331 +godo50687 +jeeyeon486 +nickyeh2 +cara4422 +hanxiaojie3 +hanxiaojie2 +sudamnet +gogoko123 +ithecompany +mate1987 +blackzang +cha8055 +rainbow05 +lcooljl +juallygirl1 +moagift +windsor7 +ssismcss2 +windsor1 +godo92959 +tofino +leeunsoo2 +goldwinwin +sayyonara3 +rosebud0314 +do6803041 +xmflgha1316 +soraebada +curemed +factory13 +winiamando1 +zbatsugar +mlove2013 +sbj32941 +hileesee1 +a482 +giftcard24 +inmytime +striftr2773 +islamnet +ninetwo +itconsultingadmin +kmh0662 +dragoniron +sbs22121 +pacoel7 +pineintr2455 +pacoel3 +kbg1616 +iintimatetr +lexon3 +miu8209 +scpack +shairsys2 +fridaytr7648 +hamchom +emma19813 +emma19812 +ssadoo2013 +dadam92002ptn +zamting00 +godo121570 +mhworld +sianfb123 +sianfb122 +fittz0514 +beagastr2931 +lifell01 +c29042 +tni2005 +urygusl +boxtv1 +eyaaeyaa +ouadms1004 +wdne20102 +kindperson1 +jeeyae +cjscience +klp2man +s2fsselfdevwheeya88 +deepsy11 +artmonos1 +anegels13 +anegels12 +anegels11 +anegels10 +safeland +nookyshoptr +shfksgorl2 +greennare1 +americalatinaadmin +ire1532 +ire1531 +ideantb +onesberrytr +omokdae2 +godcadmin +yjkkkyj2 +tyj0711 +indibank1 +sg13735 +wnrkq2322 +sklg3377 +basstuba1 +ongame95 +boxsquare +large777 +jiwon1601 +jisabal +ryu3362 +easy71 +leenalee77 +rainboweshoptr +charmfnd1 +gocanadaadmin +minou73 +minou71 +liitto +y46581 +i2free +locobozo14 +simsimcocall +dsfood +choeran +gibbmi881 +valansis +wnsghk2 +wnsghk1 +nytech011 +uro09828 +uro09827 +uro09826 +egujjoa1 +uro09822 +daedohead +cha7142 +teaping1 +wooilyo1 +bellabella +jujoonet +jayblood +tressa +gamessladmin +rubana3 +sabakki2 +hwa1538 +yoyo07164 +clef4404 +clickpsh +rjstkah +woalguswls +pdoohan +higro814 +koohip +xn9ktr9341 +greenfarm +clickpos +koreacc1321 +kitchensense2 +dandyhong +sooda +able881 +newhelloabc +shindusik +dnp33686 +dnp33685 +dnp33682 +fatfatbaby2 +phonebank +chworld +www.sat +heoshey1212 +imstyle-v4 +lovekssh20 +antivirusadmin +iamstore +dilly9898 +florencearoma +glfood3 +iferra2 +thegroup +sensetime19412 +sib2b777 +ds3ebr3 +ds3ebr2 +ipayibogenalse +gionara1 +de7521 +swan20084 +www.equilibrium +kool77 +kool74 +shjung80 +manlejjong1 +gani793 +bodacard1 +ideaguy +hanakang +lovetnb +dadam92001ptn +somac +ohbeeho +longdown +river0205 +jfdesign +jiahyoon +imom24 +mediffice1 +sddre1 +thiscotr8602 +hjm306164 +cloverisyou +jinubooin +jcommerce +widepicture +juicybam +naraeb2b5 +naraeb2b4 +naraeb2b2 +naraeb2b1 +hyoreen +cntoto762 +yinang +no6248 +unluv29 +lmgkorea +hscosmetic +boyami +levelize +myifthtr3838 +ohandee +kmg8290 +ssy0918 +yjean1 +hi6732 +sm72152 +jiniee2001 +ttaeyeops11 +p16442868 +glennn +godo119237 +dbcrktyd +samchotr7819 +godo119190 +son3s +parfumtr7383 +lwinkl5 +lwinkl1 +truejisoo +rolenjoe +godo119110 +ziucore +doongsun76 +konimi +coldr11 +cgogol +slaimhj +lovenz1 +excellent1 +winwintr8114 +wjdgml04022 +ifm1209 +whatever1 +quarter +misso79421 +zsunho +ksw60251 +seoul3000 +konico +rhew3373 +rhew3372 +ribbonnco +shinilmusic +jujoocom +iworld21 +puremskim +hamjimin +illideg +cpainsladmin +innodesign +hyunchtr4151 +twithutr5899 +jeehui +adonis928 +inface +ipitta +ielttr8266 +barny22 +jpory64323 +duometis3 +safefirst1 +hello2friend +seul5868 +tjddk007 +ysjune1 +creed26 +sodo3 +nient011 +hiais777 +bethebiz +nudull1 +lgallery +doshkorea +soday +fafarm +jeon9897 +sodam +artistry2001 +shinykon1 +seongnew2 +juxilove +hadongm +totalsun1 +qusdydgo +major10121 +h82y1548433 +june00531 +secrettr3719 +pommevert +interweb +mjl5923 +xmfkdnak1 +gogimat +gjalfk1062 +xenonplus4 +xenonplus2 +wssin6w9 +wssin6w8 +gomddange +crystal28 +cottony0 +powerkim +yami9999 +keiron48002ptn +hanacome18 +hanacome17 +bolt365 +yesljunu1 +bau6393 +hjkim99 +plelece1 +rain8192 +buyshock +kyweb +daha4136 +muyoungs3 +smsd2 +muyoungs1 +popsnow +cacaocoach1 +tkyng555 +gyc9393 +www.musiczone +foodritr2117 +boardkorea +ecoskill +minione301 +xunmei9 +xunmei4 +lollishop +vision76 +chin5445 +qkrdnjswls131 +smpre +aliceyul +eversee +jajaja +dltnstls047 +dltnstls042 +artnartr1382 +historyquizzes +scmdev +kgy09064 +mmmgooo1 +designnaudio +miz013 +analoggo +beck981 +duoback1004 +ghjp559 +moon5822 +s2fsselfsetuptest +dabidang +hi5425 +quarium +cltrs5011 +intercom +haim1004 +tong043013 +happybaby1 +dgblind +diypronet +dnsap001 +lauradavis +tong043011 +cw70672 +volgali1 +lyunmi +jjellymo +radiodj +uisookpark +minimotors4 +scmall +minimotors2 +minimotors1 +gownmart +pch93474 +desingarts3 +hoyoa +dlscjs27 +heechany761 +publicnt1 +dang3000 +lovej21 +dmstory591 +dksgytjd07 +cheece +powerfe6 +powerfe5 +powerfe4 +powerfe3 +powerfe2 +powerfe1 +1001mall +ehdchdltm +greenchoi +yhj9475 +tae9290 +zinoljm4 +synthpark1 +zarabiaj +yjs4187 +moacom3 +moacom2 +moacom1 +vintage302 +artrack1 +lovei10 +gsme1 +rcarena4 +rcarena2 +eunhee461 +exweb8 +fsuri2 +kb4741 +bbanyong +babyparentingadmin +godo117484 +yaho0627 +lightnara1 +familyon +iceworld994 +ionhaitr1044 +europeans00 +serverhosting254-250 +imi8061 +incom1 +rokaf8217 +kopd13093 +uworld1111 +interkorea +planthej5 +planthej3 +prettyaha1 +serverhosting254-238 +zach32 +kona21 +hersvill +wangsstr1532 +beauty1 +pkhong147 +miracltr9261 +keiron48001ptn +serverhosting254-230 +jdmedi +mysohome2 +myhome66601 +ghdrbekd1 +ruril +mageel2 +serverhosting254-220 +jjanghyuk1231 +deadhorse.powerize +kukujj003ptn +solletr3301 +ttiik0421 +ljw57441 +osr777 +beautec +gi22admin +lioncra +serverhosting254-208 +serverhosting254-207 +gi451admin +serverhosting254-206 +myspacelayouts +blbox119 +serverhosting254-205 +runtoptr +serverhosting254-204 +theshitr7447 +backup72-2nd +ohmytrader +serverhosting254-203 +bagsa1119 +serverhosting254-202 +capulus +lwimall +serverhosting254-191 +botzim +caw22 +dnridnri123 +pinkdangkn1 +woosukgagu +joen1120 +guglielmo2 +gdbird1 +psh1310 +jusin3333 +gi212admin +dkfdkqhsl2 +wharl7 +bandyoun +godo117022 +serverhosting254-175 +ourking121 +kss17621 +ovis79 +aajenny2 +ldsmalltr +gks410 +serverhosting254-169 +seoulflower +tjsgml6637 +reshkorea2 +foreverkdy1215 +foreverkdy1214 +foreverkdy1213 +dnjsemr02081 +kjokjo6869 +yjs3460 +tinytottr4759 +packfna +lion27192 +beaure1 +serverhosting254-159 +choisakra +jrin6981 +gi30admin +serverhosting254-157 +www.wwenews +dm-bike +withtng114 +tigger1 +westoretr +serverhosting254-150 +ssshimmm +wellooker +s4devmimi +shilladsmalltr +dbgma11 +phill19772 +billiardsadmin +ddangwee +lagon20021 +serverhosting254-139 +totalsds7 +totalsds6 +onyaganda1 +turnkks +lagnn09 +lagnn08 +totalsds1 +greenbike +serverhosting254-134 +sarah2660 +toyzon +gjreform +mkflower +rumi1 +yo3una +drsousu1 +hayantan +leejinwon87 +foothealthadmin +kyeong3919 +hojungga +zabcho +eunicorn +happyday0413 +hasooni2 +solyeep +sinbalmall +no3same3 +enamooselffix +giftlg3651 +lbebecom +hotkrtr4482 +googi811 +dkfhddl1286 +choi24k +lesvie +cjb33333 +ladytable +jikyjeon9 +jikyjeon8 +jikyjeon7 +qkr9477 +hwaya3 +jikyjeon1 +jkpark9000 +hkh8026 +testedu-003 +testedu-002 +testedu-001 +allmychildrenadmin +nagne1591 +kamangoo2 +jaeho0404 +bikeon +mostive +countrymusicadmin +vedika +dnstory1 +agnes09271 +mrcafe +jihyuntt +cool6270 +ss102 +ss101 +gisadmin +dukeland +marientr3765 +marientr3763 +jaeho0310 +kukujj002ptn +vortec +danbi6510 +modafamososadmin +gm00008 +kysgreat1 +jk3384 +jihyunin +revernco +raykkorea +tkfkd5353 +jjellise +jennyholic92 +aa114 +tazal +hwasss +whistlemotor +whitelee85 +legomaniaxtr +ellisvilltr +nosturbo +f4040 +tobang +finfratr2080 +ftshoptr0819 +jiny14452 +ilsub1 +haudi3 +chchd5 +chchd4 +godo87112 +fourth +hwasin +dang1191 +www.eldorado +padg771 +modencase +ckhanda +hanil8807 +uamarket +dbskk2tr5271 +trinta302 +sj2290 +fstr07 +purecare +ssadamoll2 +ddmsaip82 +rococotr0634 +tcctv +nam58501 +hayunine +shoesmong +namseung1 +dafm415 +inarai +netlinks +realhockey1 +jeon7075 +minhair +www.por +dodogirl +ssmario1 +uhakinside +qlwn482 +cararis +leesan79 +ajedrezadmin +nixxxtr0906 +mrmarket +inaoro +sbshop +speeno5 +dapark94 +saengi77 +jcsoot +envy55712 +heat4860 +wnrjstn +hispace +hcbig3 +padadac +pen1003 +blooming1 +minhyounga +ssaneon11 +myjinan +p216212 +nostume1 +trendi1 +polyhan2 +anjunara1 +theziatr6193 +pishon +kaidlee +teappong2 +teappong1 +musiczone +oldro72 +misoen0422 +chomi0628 +sbseul +estoneme2 +ssunworld +bemakakao +cfmall3 +kwak3709 +nsj1224 +photomade1 +trymimi +lotto9796 +goldposition +navercheck +purebess +iamsoul2 +ljh06874 +ljh06873 +ljh06871 +fineartadmin +snlovely +www.graphics +sseu1234 +shin01181 +kukujj001ptn +danzzac +skgun +child5572 +chongmu1024 +rcchamp +ttong044 +ostingirl2 +ksm3431 +cottontr0542 +popo66zz +mmagpie-050 +mmagpie-048 +mmagpie-047 +mmagpie-046 +mmagpie-045 +mmagpie-044 +mmagpie-043 +mmagpie-042 +mmagpie-041 +mmagpie-040 +mmagpie-038 +mmagpie-037 +mmagpie-036 +mmagpie-035 +mmagpie-034 +mmagpie-033 +mmagpie-032 +mmagpie-031 +mmagpie-030 +mmagpie-028 +mmagpie-027 +mmagpie-026 +mmagpie-025 +onlylove +mmagpie-024 +mmagpie-023 +mmagpie-022 +basilio +coloncanceradmin +mmagpie-021 +mmagpie-020 +mmagpie-018 +mmagpie-017 +mmagpie-016 +mmagpie-015 +mmagpie-014 +mmagpie-013 +swancnf +mmagpie-011 +mmagpie-010 +mmagpie-008 +mmagpie-007 +gi328admin +togames +mmagpie-006 +mmagpie-005 +chinesecultureadmin +mmagpie-004 +mmagpie-003 +mmagpie-002 +1100 +mmagpie-001 +gi476admin +oilfreetr +kdh4715 +justice1233 +seven20121 +drkein +hwamong1 +capsmal +sjph1 +freshgarden +jolrida +dlwhddnr4400 +violet3x +financialplanadmin +bosomi +kimejj1414 +kys17x3 +kys17x2 +s2freerelease +pjh9019 +qkr7903 +feria74 +lucksr78104 +hanil7772 +zizyzix1 +nicecd3862 +shezhome +cha1850 +carajsj +hyokyum +golfchaetr +sch7095 +victree +btsmono2 +sj1234 +kwonmihang +bc2765 +harry2 +ciellight +namju815 +ipaybbwood +mdpkang2 +pcsimmani +moohanfa +minitar +cultfilmadmin +koo20033 +toolsjoa3 +ekbooktran +limh6151 +newageadmin +minishe +momejon2 +momejon1 +missleeshoes +jujh10081 +underthewind +kcw30100 +eusenstr2920 +minjine +edmontonadmin +ohfashion +liveinsoccer4 +e2com +coffeehearth +bigmouse +rtary +backfactory +nybigmama2 +decofarm +ksm2766 +sunahouse +gi192admin +popshoes3 +popshoes2 +megacross +hansung501 +kissme1719 +momsoutlet1 +asadal050ptn +speedbi +ansunyoung11 +tong04309 +tong04308 +tong04307 +tong04306 +tong04305 +tong04304 +tong04303 +tong04301 +carrusun6 +naltene1 +chaiz1 +skineva +moaba34 +nemestar +homendream1 +youngfly882 +youngfly881 +hstrading1 +kojavi +sonhyeran +dodosense +tsj010803 +tsj010802 +tsj010801 +jid4382 +webiketr5947 +ipayelechorn +procycle1 +plcorea +ryung132 +wicked3 +showrang4 +showrang3 +ubicom2 +agnes07074 +in2diet +swibin +kdh3755 +familia +hdk6246 +gagudawoo +kays0310 +tokyoshoes1 +chacha8606 +luxclub +ssawoona +porori1121 +tjd4804 +oilback3 +specialedadmin +darknulbo5 +leey0333 +omin883 +emileok1 +blueday1 +huritz +betty020 +htata2013 +ksmtmdal +sj0401 +gwangpiler +sjm03 +showdr3 +sopimiran12 +amebaworks2 +asrada1 +gcsd338 +gocoloradoadmin +bomnalco +beasia4 +beasia3 +beasia2 +chaeks +storehouse2 +storehouse1 +fiveray +kimnh62561 +drimi3 +drimi2 +ep1421 +kumsanew +anypiawp +gcross +www.oks +gi16admin +chadol +gi445admin +stickoa +rtjean1 +doosol1 +serverhosting107-245 +silverss +healthy3 +bluemingky +09jungletr +serverhosting107-225 +ltc1221 +jungin36122 +codecode +next10304 +serverhosting107-201 +dkxmvlf115 +tnhawaii +silkn +serverhosting107-175 +dpan081 +gyounet +serverhosting107-157 +bae12sh +serverhosting107-154 +serverhosting107-138 +bc1627 +caribul18 +caribul10 +feel11012 +mi9792hyon +qkrfodbf2 +cktiger +hansolvan +richman602 +miniii3 +miniii2 +miniii1 +bakoonpro3 +ace2525kr +inplacebo +animefans +iljinkorea3 +kiwi121215 +ntreeux1 +ljsystem +chowonherb2 +monosara +hujun94 +boriya +asadal048ptn +sonddam +bsw02271 +mapia12031 +rssports2 +rssports1 +cad1042 +hjkhjk146 +haha5502 +uzzbebe +wnffldhp841 +ameliejsohn +gi206admin +jap4045 +iapplian1 +minifix +ezinext +asia6366 +pjmh1234 +hehishim +redgrape +gaintkys +yeppy67 +oaky10041 +fsleeco +gdayadtr +hanacokr +hitec91 +theshitr3463 +chujasong +weasd4312 +mhchosj +motif73 +the154225 +moon0925 +www.nsc +authorsadmin +egon07881 +es4free +rv114tr3544 +leomaltr2861 +minidog +chicagowestadmin +cozyroom +kk7375 +memsearch +maymong12 +a328jank +pipoca +ulookmalecom +shuba +ilrang +waitingsky +rommystory3 +moonyoung1 +acc114tr +forhome1 +heon1119 +modernxx +namutrading +nanang00 +shnpg +safecom5 +nl10052 +safecom3 +sifff +manguluv2 +manguluv1 +silver75 +crossstitchadmin +holystar +jliten2 +jliten1 +br00315 +health-i +codus8474 +s2pintw +ebaypop9 +ebaypop6 +oshea2 +haramj +cbc26161 +isicmaster84 +heritzen7 +heritzen4 +rhrhtlsgp +wjdgml2 +kjn1824 +dojagiholic +goevent +snikystyle3 +psywigtr +jkglobal21 +gydmsl91 +ajflrl831 +ksm1048 +avlabtr7583 +study000 +soutiger2 +soutiger1 +kwons222 +tathlon1 +ganhedinheiro +ghdejr +drice1 +gi255admin +gotodigital +sblindtr4686 +cling721 +eunmmmi +carapass1 +yeppne2 +yeppne1 +htable1 +lypkmr +shkim +theredclub1 +rooiboskorea +nysearch +iamshinq2 +vasilius1 +sonido +recetasfiestasadmin +guitartr1175 +prmart34 +goodstore +prmart31 +prmart29 +donnadeco1 +asadal047ptn +prmart25 +yummibtr6393 +mubjstr6205 +tigi228 +leeheni1 +woo37721 +jelson5 +mplandtr7697 +bluebelt +cghjlc +godo83445 +shimc +modesale +eve282mj +mingoon +prettysfc +desong1 +prmart12 +mhj0035 +wpspw12 +cowalking +leoug4 +chukachuka5 +aart1232 +flyhouse +ahrdus93 +rain2151 +furunbory +dujin1004 +playtitle +tolive1 +multisam1 +kky1121 +suniltoolz +seongnew4 +seongnew3 +taemin8101 +seongnew1 +kky1101 +theminime +eastgagu2 +althing +takuteru1 +zacboard +healsdak +prinart +power3g5 +jwkim21 +godehdal +shinyehwi +basematr0983 +castbrain +skijun2 +skijun1 +godo111866 +mikimjh1 +sema20001 +juny8075 +www.fmipa +sohokorea2 +okyumion +mogu05015 +kkarigirl-020 +hansoletc +e2gee +fancyk4 +fancyk2 +fancyk1 +rookiegirl2 +kkarigirl-010 +kkarigirl-008 +laminating1 +welcomebbtr4436 +mikilove +kkarigirl-004 +lsk8233 +hwani4u1 +codyand1 +s4freeintmimi +tjesther +kimdongeok2 +mykim02062 +lkmnature1 +1001gagu +isesangkids +purplecart +yca8004 +kwen8567 +ppjar861 +happystory +fantazzi2 +jjwp6929 +eoqkr7976 +breakers3 +turkishfoodadmin +lucasmall +leontailor +evenly2210 +casinogamblingadmin +exerciseadmin +safecare +primese +comsun777 +aone322 +redmay6 +redmay3 +redmay1 +jbizweb002ptn +cafemano3 +moonsu803 +sawori +gi323admin +asadal046ptn +green4kids1 +muzaki73 +lemonteeflower +londontr9096 +honamfood1 +haso10246 +haso10245 +haso10242 +loveandwe +sbmug491 +primeex +mh12252 +mh12251 +alleyhouse +m9611053s1 +goldrightgr +sambakza +haha751000 +aspoon7 +ruchagtr3159 +zzooni400 +jsm9394 +imeux2 +imfs76 +haieland +fancy4u +juslisen8182 +juslisen8181 +khwon10261 +jja09girl6 +chogootr5558 +danwooc +dngkfka3608 +power120 +mscogo1 +hanvok +e1it3 +www.bluesky +southnine +bodypeople +byuka4454 +illkwon831 +she-k +tanksolution +eveyatr4937 +godo111084 +bbosihae +deakug13 +dfriendd1 +bumpertr0601 +mhbooks +illak79 +sohokorea5 +sohokorea4 +sohokorea3 +signstars +oldnew2 +oldnew1 +fictionadmin +alspo3o +jj7272 +gonycadmin +jmh0707 +s3devhn +booora +sfoo3 +sm700 +daedongfood +geuxer1 +godo81935 +actpos6 +owoo4343 +jhtech1002 +labonsella +hansum +gkgi36 +boyscouting +rain0735 +enindi223 +enindi222 +enindi221 +enindi220 +enindi218 +enindi217 +enindi216 +luxpalace +enindi214 +enindi213 +enindi212 +enindi211 +enindi210 +enindi208 +enindi207 +enindi206 +womensissuesadmin +enindi205 +enindi194 +oxo7910041 +enindi202 +enindi201 +enindi200 +mp3admin +imi1380 +girl2783 +bigredkane1 +limjd1 +fundoo +osekun +boomss +tyvld011 +hitalk7 +obchungs +enindi215 +minking10022 +pharos03068 +sangginara +enindi199 +enindi198 +enindi197 +ipayksh41451 +agyangfarm +enindi196 +jdepot +hptoptour +enindi204 +alpsgom2 +enindi203 +geosung +mkad13 +junkno772 +boomin +enindi192 +dresso +sury111 +enindi191 +enindi190 +core0413 +sjy1980 +shygirlj2 +kukuy7551 +khk8863 +donnafugata +lorde +volvmr +godo109511 +aseva +seoroin3 +seoroin2 +anna7332 +lkw7607 +mst3kadmin +booknm +enbmt78b +creep5862 +ins9805 +cm36513 +cm36511 +mimoo25 +tphanwoo2 +jbizweb001ptn +godo81529 +brown77071 +mjsw2001 +heinzman5 +iatoz841 +pickupnuri +major001tr +prettyang +donxiote +asadal045ptn +j7001021 +popkorn +endlesrain87 +allenjung +godo15013 +brocante1 +toolserve +imfeel +iceworld99 +sytkorea +anvgagu +ssuper111 +omikorea1 +koreaittimes +opencloset +duipsatr9452 +ykchoyoungho1 +sbic1101 +hsmarttr2079 +pak110044 +www.scarlet +finedeco +perfume2u +peunyang1004 +seri2 +login.flyfishing +thulebox +a19911114 +jcodi1 +catnortr6101 +jdh9688 +victorynana +boogun +ddprince +godo81258 +zhik112 +s3freeintnulbo +gi11admin +tointomalltr +hannam +blackstar07 +anatma991 +seorm +uyeah1 +kmrush2778 +elfinmk1 +vonglass +shosemd +jw58361 +godo110037 +ritz1224 +funnyfancy +popklml +sheet123 +xezza04 +goyonara +coshouse +rustichouse +woodener1 +sophiekim881 +multipic1 +maroo007 +ksj392221 +kange1082 +jjdstore3 +bizcarshop +henshe3 +henshe2 +in0fishing +dldmswjd682 +misomobile +info2008 +verashoe +maker11233 +soundwho +illumegate8 +pjk425 +gnointl +kakaokong +selus +gksqlrldjq +mbc7095 +gi440admin +hishuh1 +iovesoon3 +kids0310 +hailua +daonbnb +roning5 +magnifik +deospot +gi201admin +ashleydsk +book09 +gpddl223 +nineseedtr +macouttr5779 +hamse7 +bomuls +brotherkorea +sr11051 +spdental +muum1004 +pregnancysladmin +senq21tr2021 +inwoo091 +roast +tpwlsrkrn +sck001 +thismom2 +thismom1 +robho +boutigirl5 +paragoncys1 +mizu1206 +hinduismadmin +pupple92921 +pucca82 +mptextile +cmdesign +jjugly21 +dydwn8199 +performingartsadmin +asadal044ptn +chae6465 +uniwith4 +laflore1 +osama36 +jindogift +gi301admin +s4freerelease +legendblue +minehg4 +hamong +beans00 +teamzeus +jerrysog +sjy0705 +anyone13 +nanana30008 +inposition +jpmpshop1 +collegesavingsadmin +devtr6545 +sindanmo +lovealdo7 +byallie +hdwestore002ptn +neoway001ptn +stylezoa5 +innorec1 +superex +knmira +lenahc +internavy +yh870430 +homerecordingadmin +cholesteroladmin +dojavil +hotelmann2 +binutgage2 +givmii +tess6824 +june5379 +anjumaru73 +fantfant +www.mortalkombat +svmans +lalamaison +verychu +mallufriends +sonamu1317 +withyjs4 +lee5555kh +sonamu1314 +bluworld +lhmarket +dopamines4 +dopamines3 +dopamines2 +dopamines1 +halujk +pcm3319k +tldus11021 +yegaboard +zetbit +nohsora1 +jellytup +nora00141 +y42193713 +y42193712 +y42193711 +bonge5 +dragon1525 +dogpartr2198 +kli2519 +www.maplestory +blak1004 +anmiwonjae +jya802 +ghdtjsdud99 +lead0419 +actol21 +mpinc2009 +nicekido2 +gillsung002 +allmarket +miraclefish2 +miraclefish1 +anes5020 +goyongho +onestar +ixoustr1734 +kwankyou1 +hthlsy11 +hthlsy10 +bradshaw +kps6235 +satta4 +wumuw12 +wumuw11 +wumuw10 +ilogan +tophomme +sedar +edutest-003 +edutest-002 +edutest-001 +adnet4 +adnet3 +designtr6405 +goofee74 +info0704 +dop3030 +kmungu +happymind3651 +mottny +siatkids +uriwa +eleddong20111 +ehaesungtr +designpilottr +innopole +dtrend003ptn +woo5218 +asadal043ptn +serverhosting254-180 +pageenter2 +cdsaesae +kwc06203 +kwc06202 +valuer21 +dltkdrb2202 +starsign1 +auri22tr6901 +cntcctv +mexicancultureadmin +mincoon +tad8878 +lovetournet3 +trivistr3679 +breakset +tiresadmin +happyroomstr +ckwls0707 +adnbiz +golfmotion +shoeptr5574 +diychoco87 +popo0724 +gnjsclfalska3 +smj98271 +drcorp +smartsunny +km780201 +hdwestore001ptn +newdian012 +kkozzatr1147 +backup71-2nd +bettysl +cubicpan1 +donongwol +harmonydeco +ha61114 +admit4 +baseballkid +wfr123 +winzgirl2 +gosuccess +suuh75 +marokiki44 +khk6362 +suun0001 +secho70 +tlssmc +qorwldrb1 +tree4smart1 +liosnaif001ptn +jennykim +salam7777 +idikorea +euphorie71 +rih21005 +kazetr7485 +ykustic +charmfnd +mirae09 +ssdbr103 +ssdbr101 +okebary2 +irowoon +mini0762 +akira34991 +haechowon +saywhat +mirae02 +npaper213 +younnam0 +yongyong2k +chinaweb +pen2011075 +pen2011074 +pen2011073 +euniett +jwminbak2 +atzoootr1292 +teamj0317 +buyblackberrytr3134 +h82y1536641 +lejybe +wanghh330 +shopjtr +bomebi +ysisky2 +limux21 +tommy20023 +tommy20021 +anyparts +juheun1105 +hh4088 +hansori7 +emotionno1 +costarmarket +tulip7787 +bambara +improvn2 +smartwax +httpej +cc112a31 +dreamwk7 +sevenled +koyw0225 +say79kr +kimjobo5 +kimjobo4 +trustkor +kimjobo2 +hahacoba7 +gomnimtr5839 +alphawill +gi317admin +yuooooo +bos300 +dtrend002ptn +asadal042ptn +sj192 +cfmall +pck1977 +urbanoid +biotree7 +osarao +yuni06251 +y4141 +rhkdsus21 +stayathomedadsadmin +moto3651 +rsko9210 +rdh74351 +redglass +khwanik5 +fdonetr7285 +pjh1296 +eunhea82 +spbq12342 +spbq12341 +servlet12 +rssports +dreamsji +deazon +moto11 +cslighting1 +rin8531 +fishingtv16 +s0428331 +fishingtv11 +woo3772 +koi111 +odc2512 +odc2511 +yhp778 +leemaking1 +hellosports +apolo25v4 +abc16164 +jdleports +pkr9776 +da2sso1 +cm3743 +ehleem +dapanda +k1textr4114 +khwon1026 +windev18 +bluesunh-001 +lovebicycle +gydms851 +luluchemical +bizcincheon +hukhuk +juny6340 +neo2885 +styleyang +cm3651 +spokhatr0080 +jdh990 +jwarehouse +k2juni +exe03112 +satang +daejunbank +gps123-v4r +absj +maxtoto +adkoko +tasd121 +iaan1004 +diottica +enprani +hobbyandtoy +ink82tr4547 +signup.mail +har107 +iope79422 +swam82 +osmosetr7286 +mini103 +s383638 +an194001 +photos24 +windowsespanoladmin +tzmcom1 +cheongon5 +clzlseowkd +threeh03011 +dbgma111 +acts96 +wlfmddl6 +wlfmddl4 +wlfmddl2 +wlfmddl1 +fd10813 +www.kss +dtrend001ptn +asadal041ptn +acts29 +mauntain2001 +un50252 +kyr3089 +ucnehandwork +ymfoodtr3901 +cheongnam +mjcsong +nhbai2 +ecorian2 +csco3040 +swacom +booksell3 +booksell2 +mrcompany +wesang +kmerkatz +limsurk +kidsartscraftsadmin +jcreative8 +jcreative7 +thyroidadmin +jcreative4 +rex19951 +jcreative1 +mrman16 +mrman15 +mrman14 +soonsou755 +mrman12 +mrman11 +apolloeos2 +ropatree +manualfree +saemichan1 +vision97001 +canonhousetr4170 +sukpopo1 +euromatr3933 +youmsangwoo +kicheolpark +edubooks +kkh84233 +sdmysong6 +sdmysong4 +bladerpk +egg0419 +zillion3 +meetree +zillion1 +nani94401 +saqa1 +qusxo61 +okeedokee1 +fantary6 +chojisoon1 +arenas632 +hspark01 +etodayshop +wonwoo612 +ybrenttr5235 +yuseonk1 +mystertr8093 +moroo1 +crqdaiwa +koaid3 +svn-season2 +svn-season1 +moru82 +trydeng +elnoya1 +ntree906 +cph1113 +cctvclub1 +nabiggum +moses5 +jjhdha2 +jjhdha1 +the336 +kobj0706 +woodpeer1 +tank10081 +vldzmvostl +wonderstr +zaizzaiz +kpc101 +x5dr5 +stylesay +chdlminji1 +pbs9425 +plateros +girlscoutsadmin +goodsin3 +thdud8848 +voip3 +joagoltr4470 +mirepaok +designw +whtjdms0392 +soleil1024 +tgs52471 +imagok +lsed +winnerswon +minajo2 +thecentaur +bjw1990 +backpackingadmin +gi434admin +debbnmor +urzzang4 +urzzang1 +familykorea +ustyler1 +twohaptr2258 +fcss0700 +joe08181 +ysboard153 +sdfsdf +digity81 +www.srs +euneunv +kog515 +ewoojoo +joy740423 +eduedu-002 +eduedu-001 +ljhookart +keeka1 +risma +asadal040ptn +bumilion2002ptn +cyshop +j2s0408 +urbanworks2 +suren2 +jbseo3 +soya04071 +wayomedia +moriz2 +shongun +damano1 +jsealing +fishingshow +skyanbg3 +nicegam +karu1220 +ipayatoben1 +gufarm +yhlayuen +wcysports +hosikgi123 +oltramania +bonpeople1 +minam54 +ts9492 +mamijjangtr +hajemt +werbew +dd1999 +prmjung3 +msr97973 +brandplanet +jchull +worldbath3 +thirtday7 +dcjae831 +florist4 +florist3 +florist2 +konom777 +acts1270 +cellsladmin +hghong09141 +awy03034 +goho1972 +petruspark1 +kimjs9374 +ghyawr01 +gi185admin +paolo202 +playgon +medicalofficeadmin +www.ldc +multikids +jazz75 +bigboycustom +mmiijjoo2 +m5acnc +arktour012 +wesang2 +linkbel +hansu5802 +lhj2425 +sz900831747 +yoyoyo6 +youoh252 +zfishitr6882 +skdiwndl +godo18tr8760 +kal32000 +pluscheese +orij79 +edding1 +bojeon +joungyoon3 +orange6716 +godo75002 +medusa1599 +dmscjf892 +ymoonchan1 +jjicoffee1 +seoilfood +ballista2 +aux9971 +gi336admin +wizz +abzzab +jkkorea2 +surlira +hoontech +loshfinna2 +loshfinna1 +leegonelee3 +tobangfood +idc06002 +amateureroticaadmin +alsl9812061 +kby3388 +tlswjdgns12 +tlswjdgns11 +ceresjane +cinthea2 +perpetual77 +wearnet +wellnlife +busexpress +teamevo +dashuhouse +bicyclecrew +statisticsadmin +yuildent2 +onepice +lyhyun2 +gifteabox +es4rent +dk04272002 +asadal038ptn +artnwine +gojack60621 +anna1203 +jchnew +cleanmobile +sungkunc +ipayy1684220 +babycrew1 +daoncp1 +eveelf5 +chs9152 +chs9151 +eoqkr0773 +coloryarn3 +ssong49921 +solmart +nwwnulbo +hnmobile +yeonsung-100 +viridian2 +won92ko +minibyuri +hahobj +danpopo +psdstudio +www.tik +anna0927 +uber7328 +nggri2 +sj162863 +nyrelay +ecoperm +orgpbh +trio1195 +trio1193 +stars231 +kyy3382 +lafirst +xotjd0523 +leeseo2322 +xiao611 +wanjin1 +joatel4 +yesoya2 +www.villa +alfo3093 +dkehd71 +goelrai +ccy0222 +cocdesign +psychologyadmin +hukaura +zizimaa +ahnsj00 +jsspace6 +junsic-030 +jnsglobal +kjc7890 +goodboybsy3 +goodboybsy1 +homekimchi +skgulbi +sac31 +dodream1 +youra961 +hens771 +ultimateroms +miae6941 +dudghktl +ashley715 +styleup2u +cooltrack +friendog +vapalux +sunyub +hisynim3 +fgns5119 +pojangmd +bbo1029 +ch79191 +adhomi +gbk12031 +namhunzz +asphp72 +totalbtr3290 +mr70042 +sterniqeq +fromdaniel1 +ipayhsomang +medicmedia +pojangdr +luxzero +u102hyun3 +u102hyun2 +csj00354 +csj00353 +csj00352 +dlfrnstk +skimxbeen +hjleports5 +pjaeoh7 +pjaeoh2 +kongsooni +sanyum +mstowel1 +lhsgkrtn +xoghk03 +www.aaaaaaaaaa +koemtr1622 +dgtong9 +dgtong8 +dgtong7 +dgtong2 +aram7074 +lhj1025 +papadaughter1 +sevachrist +nextgo +adicok +luxury97461 +asadal037ptn +pro83132 +han777 +yshwsdj +designartsmart +park7270 +shkim5439 +petitmore2 +okidoki9 +gmskin10 +bbabboo183 +bluecarpet +parkjohns3 +godo73378 +dannykr +shonnlee +adnet7tr9530 +findgoods +forestfood1 +seleibe2 +seleibe1 +kmrush27786 +sps49051 +pasteryn +kjytoyou +durifitr2595 +shoesbang +rjckdahf +koh0811 +pjw306142 +motelriabed +bitdrugstore +makeupcar +hun337 +eduweb34 +greenhands1 +babyve71 +baiclef1 +koshmarket +s4freedevsdg +jollykidz +oleiros +gill263100 +sanup1 +phonia +yakplay +s379103 +otimetr1039 +p2sung22 +pangpang2 +myshop-029 +myshop-028 +myshop-027 +myshop-026 +myshop-025 +myshop-024 +brotherjj +myshop-022 +myshop-021 +myshop-019 +myshop-018 +myshop-017 +myshop-016 +myshop-015 +ipaykhtr9885 +myshop-013 +myshop-012 +myshop-011 +myshop-009 +myshop-008 +myshop-007 +myshop-006 +myshop-005 +myshop-004 +myshop-003 +myshop-002 +myshop-001 +allthegate +lohas0011 +agijagime +bogok1 +soccerkoone +englishcultureadmin +gyuri010 +www.tic +sim3419 +popdkdl +westernunion +maxport +sante2 +sante1 +mounggoong1 +godo270 +moonan +csnet00 +nurinail1 +bestkim99 +barack4j4 +lesportsackr +dnjsgnsgml741 +sinnara1 +moitie70 +hocsong +omniherb3 +omniherb2 +jaekyumlee +acticon +kokundo4 +jlp1357 +npschool +kate37501 +i68425 +stwood +pluto134340 +nancho911 +anagod1 +boombox811 +jun9453 +douglasmac +mn22ang +atfox02 +go04124 +hantara2 +somang2 +somang1 +wkdwjdwk11 +binine00 +newsz3 +newsz2 +gi312admin +rhas2 +imarketing052ptn +stseller +drumman +prides2 +park6322 +park6321 +asadal036ptn +bear0235 +popcorp +tonyaqtr1378 +newsjw +minami21 +needforspeed +t1inter +boggisland1 +beans87 +duruduru +zillfin2 +ddos253-133 +ddos253-132 +ddos253-131 +seoulem +switch001 +icars +blackmat +hanmibook +ahorroadmin +thenow23 +kookmitr2323 +mapline3 +es3today +haitnim +blackkpg +okkuyng +sspp800 +jes1550 +yeonsil +fromto +ansholic +snoberry2 +gkkoreana5 +dmsdk60291 +jyjyok3 +jyjyok2 +cheongyewon1 +gaegul211 +s4devtj +partiniitr +s4devsf +brother12 +lilymag +dntjr001 +k7k6w35 +s4devnj +bea60482 +filternara +romiys1 +kan1017 +www.webdemo +soljin2 +koreacarcare1 +i2373720 +s4devhn +cosmosqaqa3 +k9301251 +tvschedulesadmin +jason9808 +newoni +logic875 +limoti4 +enter3853 +enter3851 +jsm0045 +s3freedevnulbo +mindhomme +housentr9794 +wbdw777 +gkstoalek1 +zeropark6 +sankdy +orilove +evan1052 +pcsupportadmin +sori50783 +usgovinfoadmin +dsptools +ntree9061 +ji0ij1 +www.crazychat +papavov8 +enfgksk1 +lhwfree +ejlove1109 +news71 +orientationbb +myoungs9152 +sunhae +www.freeworld +goscandinaviaadmin +youngmusic +tpijhkim +eunjin11181 +pazzihouse +sangvi +partners1 +newksc +kdg0309 +starquad4 +godo100517 +haemiltr2502 +ojh96792 +ojh96791 +tabu +hahoetal70 +dowser +lyhpjo5050 +qoqo4496 +rkeheo +jykorea +marok +gucci27kr +kstorch1 +dreamgive +limta1351 +monkie +choi760301 +skfl730 +entadmin +leicanaracom +haeorm +asadal035ptn +bicicokr +kmmukg +hyundai-040 +hyundai-038 +hyundai-037 +hyundai-036 +www.fcbarcelona +hyundai-035 +hyundai-034 +hyundai-033 +pjb916 +hyundai-031 +hyundai-030 +hyundai-028 +hyundai-027 +hyundai-026 +hyundai-025 +hyundai-024 +hyundai-023 +hyundai-022 +hyundai-021 +hyundai-020 +kyung07201 +hyundai-017 +hyundai-016 +hyundai-015 +hyundai-014 +hyundai-013 +hyundai-012 +hyundai-011 +hyundai-010 +hyundai-008 +hyundai-007 +pricedn +hyundai-005 +hyundai-004 +hyundai-003 +hyundai-002 +hyundai-001 +gi115admin +kan0245 +kmobis +wormwood89 +eslatti +leeju7 +2bbu +winwin11 +blackbs1 +hyhan2010 +bodyya +tokitoki88 +sogangtnt +patbingsu3 +gaon16103 +teradesign1 +sujakssam1 +gautiermall +mono12 +kinoson +park5058 +ebccenter +ecoment +haenam +monic0 +czeon4 +wpspw1tr8125 +yeonhoj +xofkd003 +soccer2002 +nzblueyang2 +rl755 +rl753 +safecotr7794 +zzipzzuck1 +limboskin +yeijak +edumentor1 +norinori82261 +wowcouples +pinkstory +helloko2 +coffeecha +squashon1 +babarara041 +linuxhosting245 +linuxhosting243 +stickermalltr1180 +linuxhosting240 +linuxhosting238 +linuxhosting237 +linuxhosting236 +linuxhosting235 +gi428admin +linuxhosting234 +linuxhosting233 +linuxhosting232 +phlux1 +linuxhosting230 +tmp0301 +hellojju +microbridge +romiok1 +kim7323737 +mulkibel10 +allthatstory +ancrystal +stylertr7079 +cnrqrh13 +gi180admin +park4673 +kkw70041 +dltmato1 +wjdtmdgns77 +graphicssoftadmin +carm10041 +jmoons007 +stuhamm2 +ljkeang73 +ljkeang72 +tkdstory +misoshop +coodgns2 +newcrc +picto2 +rladidgus94 +jang05051 +santamonicaadmin +sebins7 +linex01 +sebins3 +dinnerfactorytr +hisekina +strobeau +homecorea +imtoyv4 +colle723 +anymusic +apaya10 +shineshoe1 +dvduck1 +hbckorea1 +vmeet +jhg90wkd +green61611 +hkschul3 +hkschul2 +dosion213 +ssongyi17 +slrrent +bodysktr7751 +leecos +a0221642700 +ju27921 +pej0620 +gensiro +nalgai20001 +jjh2161 +classicmotorcyclesadmin +leechi +sameun +adelie +shop001 +designmz +natedanji +gcsd33015ptn +danjifood +secada1 +s3freeintsunny +harry7000 +rainboworld +daewang01 +fasionbiz +mvpclub +aidsadmin +godo69353 +xn3htr9530 +spxkoreatr +designfx +aurorakorea +soleil10244 +soleil10242 +soleil10241 +molylove005ptn +juwon0622 +jhtrend2 +luvpratik +ladycat +lch66511 +designbs +zeropack2 +zeropack1 +ucs337 +kool2741 +sullai +zzang6881 +winnerswon2 +ttl7812 +baberina +quddn4 +kkw7004 +upartners +neo17304 +neo17303 +neo17302 +neo17301 +urban3822 +zaoln +www.soto +semonemo +vangquish4 +vangquish3 +vangquish2 +geojerc +hallolupin1 +yyasz10041 +redhairanne +redfroger +vnfdlv03031 +koreachild +esedog +bomcmall1 +l3035757 +choimin20042 +jinirose07091 +www.sora +love22431 +kodi0725 +silstar4 +plusme5 +toc +njtrading2 +njtrading1 +mazanta3 +alicatr4694 +samack +loitraitim +adel75 +chounghyun2 +pandor00 +ulppang +monitor161 +dowh67 +choikimine3 +jzid1284 +btylife +pluskey +hulux0920 +nzmall +stevehtr8770 +hyenminee +go01171 +design2h +sonamu1315 +leehongsuh +sonamu1313 +ledek4 +rebiz +yonghun22 +amare2241 +happyshook +yonghun21 +uziuzi4 +yoojin19994 +fairtrade7091 +postictr +genfa091 +asadal033ptn +www.puskom +jooni12341 +sulem7 +suyunpuls +hamo0003 +applyresults +hacong +enteam +duoback +dream347 +papastoy +gksmfls1 +boltplaza3 +www.sips +sayujung1 +dumelife +rlaeorua12 +twoseven +molylove004ptn +artherot2 +claraestee +dltksemf +www.sica +sensafeel1 +www.rose +apnakarachi +syspharm1 +rainstone +alwls10243 +d2gmedia +netpod +meiko +addinb +milgarujsk3 +minhee25051 +openorkr +azmazh8 +azmazh6 +azmazh4 +mulkunamu1 +puny9 +puny4 +puny3 +puny2 +estokorea +stylistr1696 +namin2z3 +citrus10251 +jagci326 +rcom2 +rcom1 +daynight3 +ironpin +edugameschooltr +vmflwms +fishingmega +lybon2 +san3datr9921 +sali12 +jkw23144 +allgreentng +hollabagtr +meganetserve +saxoflute +wandoph +juheesh +www.safe +ledcorp01 +dajoajoa +ruruyang +falinux +biartmtr9184 +aegiyeosi001ptn +golfthtr0292 +roadbella +ifoodnet +cunti72 +cunti71 +jwbong +thewonderful +mis4244 +kekeker +uckorea4 +uckorea3 +maycoop +ssonssu +sokoon3 +thenmark +ipaydawoo2com +joungsw +lcsvvv +enshriue +vnak3000 +pabang1 +systech3 +style911 +pinknana +cabosan8 +huborn +aclock +nzkiwi +cabosan4 +cabosan2 +godoweb +hanraynor +heejin09 +ehgud7642 +ssonso1 +iseya79 +aceeuro1 +tendenciaswebadmin +ed12686 +estargolf +k2cine +jmadang1 +kmlee7 +abwreq2 +dnpqzh1 +imarketing020ptn +ldwhs82 +rainmakers +k1like +filter114 +nadayos +uomya1325 +goorlandoadmin +image98 +image97 +image96 +asadal032ptn +hache3 +hache2 +nikkip1 +changdae3 +changdae2 +richwater +babywish1210 +godosup +bara581 +cantong +mikum76 +collectiblesadmin +ssun9804 +godosoft-060 +jasangbox +celini +justla +ritdye +godosms +sungyou1 +www.pink +godotax +gemnara1 +rcguy +joy7404233 +joy7404232 +joy7404231 +youngjo123 +totalspeaker +oneinno +jj4944 +wekami +woon2013 +dcr +kinglionjay +olivekong +icikorea +asadosadmin +dajunghan +goodsflow +fsblue +usall0321 +rojukiss +nmj1185 +ubskin +kg75932 +kg75931 +godo67173 +asiasound1 +hbsfoodv4 +qmzp1818 +gideon3012 +gideon3011 +oceand2 +oceand1 +zazu +aileda +tkawjdtlrvna3 +choihyunsoo1 +oyerdan +latinamericanhistoryadmin +maxlim3 +wlgus33451 +bestnz1 +bosaeng +gogotori +luvcrystal +alttut11 +godopjt +lsw133090 +dlawlsrkd00 +balletstar +xn9itr6850 +jan6568 +hstp07 +hckorea +sun40013 +wawo90201 +roseleaf71 +shoon5071 +hungsicjang +oem1982 +shoesyo +kmkeun +luxhk242 +kmjpce +pse9023 +bestmnb +mytime12 +duduworld +mntk10 +marylucia +ks1630651 +babypear +roaorlrnjsdu +foxlike9225 +foxlike9224 +foxlike9223 +foxlike9222 +foxlike9221 +foxlike9219 +foxlike9218 +svspower3 +omega3egg +host09 +limlhk2 +limlhk1 +mohico86 +just79 +byeyourjune2 +byeyourjune1 +cantico +harawoo4 +hsr123521 +assayes2 +khj4817 +godokys +eyedabom +k2bioz +won3306 +klyhbm8284 +klyhbm8282 +kmiway +rkahfn +fuzzy0071 +host99 +gi296admin +godomdb +lds5876 +correradmin +dkdhtl1 +koolmobile +contemporaryartadmin +i8e1793 +goodjoin +asadal031ptn +plusmro2 +yoohj333 +godolee +haanul +dtegsecurity +tantoos1 +defaultmedia +doctoralex +swordsbear +itsbetter +edailyedu +molylove002ptn +dreamco01 +gi390admin +iamyulmo1 +ekzktl791 +cncinccart +bestgul +neople1111 +suji57 +wjd66551 +suhui2 +zillion2 +adart4 +pbs0708 +www.noel +a369002 +guitarnet +miljs93 +jetaimtr6840 +capamax +www.niko +mlineshot2 +matenmall4 +matenmall2 +matenmall1 +hjyoo10011 +jejusy1 +jbiz10 +seventr +battery1st1 +jcake5 +jcake3 +se000000 +jcake1 +dktkrhkswnd +diy-shop +cantaur +volky2001ptn +grayzone +sty121 +obmilk1 +sskcr8000 +m2skin +wosung +yok9900 +audioandvideo +jichun37 +jjimin8718 +lsj2307 +ubrich +nesege +bass5214 +idgodo-039 +byhemee2 +byhemee1 +icoco6801 +hawaiiview +mayzzzang +cysticfibrosisadmin +renewals +sevenam +jejusc2 +jejusc1 +kimjs1004 +hanme10 +jajaemadang2 +hitopic8 +hitopic5 +hitopic4 +ettyk86 +hsy819 +nts0311 +godoedu +cumashoptr +ghshop +selebean +alsanis +zzukbbang +yjoung1015 +kwongroup1 +robottap2 +teddylee20102 +shoesbucks +rjp011 +pointbox3 +hoonklee +wowmin-016 +hoony81111 +cavatina114 +gododev +joonggobaksa +danbistyle +apple18961 +woqjf073 +www.moka +monoama3 +younsunhwa +bnp04171 +icd +jjm4352 +asadal030ptn +www.moha +bestbed +jtouch1 +dummerce1 +minam322 +idgodo-030 +dass8027 +yrsong0629 +revoice +rahsy +yakroad2 +hainok1 +brandntr6827 +img43 +jinatman1 +iplus20 +sungwonr +financialservicesadmin +s1intextacy +ukdaycareadmin +cult03tr1408 +yy99008 +bultaewoo +yy99004 +itmyhope +jane9006 +karam2491 +impressbuy +park0115 +baramggi +kimssy1 +www.nana +kimtkid +waycosmall +mb1210 +captain761 +werbew1 +iluxbotr3041 +mmm365110 +ssysts +homeworkhelpadmin +minne1029 +stylencom +s2pdevsf +lk1119 +guweb120 +morin77 +dd19991 +kook7676 +s2pdevnj +sunme7071 +goxneul +maditis +ray7055 +babymoov +onesports +ibbnyani +s2pdevhn +pgreen +kwonskwons +hap6327 +fenixtr5898 +gd2011 +mceshop +anycompany3 +bestd24 +viewtiflow +cobaltray +doubleyou1 +jejukdc +choseoni3 +littdrg +hgy78872 +asadal010ptn +sinanto1 +nnwwssgg +saibi1 +stcok15tr7777 +seanews +all100flower +cheongdam1 +adam59 +betinfo +tubularr1 +serverhosting202-114 +balggorock2 +balggorock1 +djkorea7441 +loveottogi +godoedu6 +w4cky +godoedu5 +guitarand +bassertr5514 +godoedu4 +godoedu3 +howlattr5838 +ccw9812 +jiun1115 +js2538301 +saintyum +ds54527461 +seodoori1 +parautr7082 +shulintr8114 +ssyoon +sun767 +dnzsky +ysj29301 +gcsd33020ptn +zpzg94 +verylovely1 +bgarlic +simplemind +happy4049 +oikos10041 +getsworld6 +ljk1766 +aojoa20087 +babylucy +bbiero1224 +peopleutd1 +dunesu4 +tsgim704 +rose76651 +hillstate +blue95063 +enkai09721 +topglass +bagstyle +healingsu +asadal028ptn +yellowbus +dndns12065 +dndns12064 +dndns12061 +ambassa8 +godotest-009 +godotest-008 +mols1441 +godotest-006 +godotest-005 +godotest-004 +godotest-003 +godotest-002 +godotest-001 +midistation +pris8 +pris6 +pris5 +trustfactory +paintballadmin +presentbox +kimna0403 +jinstar +yellowcap +ygftr3693 +kimsiyeon +dldyd11521 +vivitar +konom7772 +coffeeschool +innocnf4 +goldberry +dobuddtr7765 +atsumare +nachoi1 +jin51774 +saintvin +hoya104 +recsladmin +jejucs2 +abriny +ii1121ii3 +ii1121ii2 +wowmin-008 +leo08212 +jejucjh +oncarmalltr +wavlady365 +ektelecom +ecofoam +ardorwin58768 +suhank +dass6598 +wlfjddl4658 +artnworks +rainbownature +psd24 +bst8575 +zzz12994 +andishe +banzz33 +nightsky23 +s4intsunny +rock0813 +godlove +dhfmrhf125 +piezoprinter +inlater +jinsori +gi151admin +recon905 +goho19722 +dressoo2 +nelly741 +wowmin-007 +osteoin +amigyu3 +amigyu2 +amigyu1 +dahong0704 +ontarioswadmin +joons791 +stylemana +www.loco +fall766 +newchina21 +fall763 +happy-art +philos11 +enrental159 +sdkcool +a4dc121 +design8883 +stylemam1 +dorazl +saintsei +fourwolf +isme0220 +muhanbit +cheongdam-040 +cheongdam-038 +cheongdam-037 +cheongdam-036 +cheongdam-035 +cheongdam-034 +cheongdam-033 +cheongdam-032 +cheongdam-031 +cheongdam-030 +cheongdam-028 +cheongdam-027 +cheongdam-026 +cheongdam-025 +cheongdam-024 +cheongdam-023 +cheongdam-022 +cheongdam-021 +cheongdam-019 +cheongdam-018 +cheongdam-017 +cheongdam-016 +cheongdam-015 +cheongdam-014 +cheongdam-013 +cheongdam-012 +cheongdam-011 +cheongdam-009 +cheongdam-008 +cheongdam-007 +cheongdam-006 +cheongdam-005 +cheongdam-004 +cheongdam-003 +cheongdam-002 +cheongdam-001 +jwpresident7 +jwpresident6 +jwpresident5 +jwpresident3 +lixuanyu +mega-phone +goodgown +eframe +otcgreen +sweetool2 +kagamii +alus1209 +www.mass +tophana2 +cameramart +won0321 +allmirae +lovetemtr +sgdbswl +wdong3 +teacera +jaesoox3 +kdsjhr1 +nemoshtr8535 +pkdd91112 +ego108 +viatc01 +jsk12191 +asadal027ptn +skygksmf22 +kosrhee +ssun5871 +ntm21com +turboap1 +foot7010 +hhan8258 +bobdodook +recon28r +em8888 +jinhak4733 +kdh710105 +seamart +eukwang +stoymall +innocctv +wom9035 +chcnc11812 +sojubar +w8883 +babylife +myfashionny +gaon08087 +gaon08085 +gaon08084 +gaon08083 +gaon08082 +api0011 +manijoa3 +eueverpure +seamam1 +sandboy2932 +sandboy2931 +vivianco +heaton +y63075411 +edennu1 +dodls531 +ppori +fashionflying +campingmall +gabidream +kkc1206 +oznation +jun1003 +uniphoto2 +02ne +exelone +a88356337 +ming4881 +yskaou4 +handock1052 +gi423admin +tourplaza +innocasa +suhkj1103 +kkc1117 +blastmedia1 +amishop3 +amishop2 +gi174admin +nanoex532631 +doo83891 +fanypink2 +fanypink1 +eagle216 +makingsoom2 +fashionwing1 +belle625 +yaksunfood +ksmanmoon +andwhite +themotor +blackcat587 +tayajuka +sagao5 +nzgaza +likerockers +mymarootr +smile81 +rollei2 +rollei1 +meena +lacida +le40971 +papero1201 +gptnr1972 +akkidirect +bodysktr1050 +um.exchange +ipayosshop79 +godo62724 +eunhye7521 +wowmin-001 +smile06 +shuzai.ancienthistory +besound +choiyh64 +dnels2003 +altaicho +adosindong +wyelec +cdplex +bread355 +jaja6644 +samtalmo +ilchulphoto +seodongik +s3freedevsky +ipet2000 +inyourtr8448 +neokey +asadal026ptn +woozlim2 +hannara352 +newdate +debali76 +seulbi1 +mogait +silver6022 +gi90 +dbsrud1013 +s3freedevsdg +peliculasadmin +nikesb1 +healscompany +nexen211 +combacom +lifeedu-020 +lifeedu-018 +lifeedu-017 +lifeedu-016 +lifeedu-015 +lifeedu-014 +lifeedu-013 +hantech21 +lifeedu-011 +lifeedu-009 +lifeedu-008 +lifeedu-007 +lifeedu-006 +lifeedu-005 +lifeedu-004 +lifeedu-003 +lifeedu-002 +lifeedu-001 +soho10049 +soho10048 +soho10047 +soho10046 +linkbee1 +soho10044 +soho10043 +soho10042 +soho10041 +hbglobal1 +themoons +sara1929 +godo62323 +woogen91 +dogndog +gardencity2 +godo12129 +pinbantr9179 +newyorkstory +altcgotr5936 +yeonsung-029 +canoncw +wlstjs0719 +bmw017 +ck7914 +yoyoyo80 +pic711 +chrissouth +patmos1 +jijh3246001ptn +heedihee10314 +jhk1233 +dagle91141 +wls2gml2 +lyw66851 +sungkunc3 +chengjiyin +sungkunc1 +isero11 +kisoo202 +gsgtel1 +ksj8335 +kototo527 +latexravie +singleparentsadmin +feelcom32 +bluebirdie +sparras +jsl0118 +dlwotjddms1 +pool55192 +pool55191 +morenvy +eugene08042 +kal320001 +whykiki10042 +momshanger +ub12122 +damocos +gi80 +jnhyun7 +gigaro +thglobiz2 +thglobiz1 +www.josh +vivid45553 +babekhj3 +michaelcue +dmdwbsp +neodsn +koreabipum +sm42591 +king0530 +gi291admin +orij792 +migogallery +xpeedy +phdsys +doolho +jki0727 +yjinlab +ipaypop365 +wldus0106 +khalomsky1 +s1intsf +sandman01261 +wangpanda +kim39981 +rokmc756 +www.inti +romanplus +s1intnj +door00 +s3freedevman +markantr2453 +sulgaul1 +sjw1978 +sewon6473 +s1inthn +packingclub +xixi2113 +canon4u +antidotekr +jsk8634 +www.jeje +heeinging +neobob +a77chosh1 +bam12181 +bivouac4 +acegolfball +humanpivot1 +diosoft6 +diosoft4 +dkwelltr +manager20151 +lbwmk1 +bod21c +jihee121 +asadal025ptn +capdialog +campingjoa1 +ipayraonpets +hprental +neobay +ipaygiggolaid1 +dasom9205302 +s817067 +onecomm +comaider +flexakorea1 +ensceo +autorepairadmin +s3freesetuptest +lowblow3 +lowblow1 +emall246 +emall245 +casebotr9054 +emall243 +emall242 +babyjjan +kayoyon +intimo85454 +mc8837dj +ijoayo +sctaix1 +doogi3 +nuripltr5732 +thekitchen81 +junipi +sala70-019 +player11 +bye365tr1992 +bora2007 +stdadmin +lunchbell +sala70-015 +jjm0054 +kim39439 +baesam03 +madeun1 +winex1 +typesafe +sala70-009 +sadmi2 +dool22 +hskim67672 +jdesigncore +dlawk123 +goodfarm +bananashake +fremd4 +fremd1 +helena23201 +kangajtr2483 +samsin34 +samsin32 +lololokiki +sonnpark +cbu1116 +ddakjol1 +nemogg +weldman1 +jubilee79 +www.jane +saeang +thenelim +maybe20124 +maybe20123 +maybe20122 +yijisuk +sasari725 +sasari722 +tonicyhg1 +eom3338 +hedgrenmall +gi70 +perf +ecobubs +ogbizcom +skyaa10042 +spacecyk +die09070002ptn +guirin2 +guirin1 +ghdtjddus3 +ghdtjddus1 +kimjin71672 +thereturn +egfarm +blacklabelr +soodragon +wlgmlwjd123 +jangjs +unbeaten +mdowlsm +godo60683 +f1corp3 +depotstar +dandjik +cutesarah +canadapcs1 +janest +icw00732 +icw00731 +s4devkthkira +madmax2004 +jh5679 +madmax2002 +madmax2001 +tongbooks +dyd2978 +inbusfnb +mariamagic3 +eduts2321 +jhj8540 +asadal024ptn +yah02161 +europeanhistoryadmin +noah0202 +mdg37601 +oroanreb +diana11 +jeon200140 +jeon200137 +snjkorea +gigameta1 +kimos79 +modish +parkinsonsadmin +chsjin10032 +xwidox1 +art92654 +hellosanso +miffy3333 +sik9890 +donho1 +drinkbeer +sik9874 +youjin0927 +jams77 +gi59 +freeng +freemo +patientsadmin +jandl3 +jandl2 +mipi2013 +ck5995 +www.hawk +kangsmwin1 +cwkimchi2 +lilo0607 +mihwanamdae +nylong +vampn2n +kprosen1 +july2k +jung75 +jinoria +haja1133 +tlckr1 +sejinwtc1 +ja131007 +kittykitty87 +knifeya +jayinlee1 +mentor4733 +unicare0128 +sadbye +dakeda72 +kaircotr8614 +serverhosting51-131 +ygbori +hanbest51 +insummer1 +babyhanbok +hl49897 +steven6121 +noxnemo8 +cbday3651 +wara6133 +cdnews +sejungkr +sslegy23111 +andpetr4271 +dcu0048 +s4freeintsdg +dventure +en3r145 +en3r144 +en3r143 +en3r142 +www.speak +en3r141 +stlink +johnkasih +machambre1 +rochester1 +forestko +pop12061 +dammee1 +www.girl +canadanewsadmin +pdalsoo1 +june26 +ogilvy1004 +sunhill7 +sunhill6 +sunhill1 +s3freedevextacy +eth1901 +golf1217 +acdong +kwonsiljang2 +hkscietr5240 +roboholic1 +htk008 +htk007 +bobp7234 +coffeekaffa +die09070001ptn +powerntr8846 +acem11 +sportsmedicineadmin +jewelsmk1 +jinasong3 +opengodo +ipaydotr7734 +jphotelarv +gi52 +wellooo1 +hillantr6673 +dolsol +gi50 +jhj17493 +jhj17491 +tjsdla9910 +knight38x1 +thlove7 +stkong +kimhokr8 +kimhokr7 +jhbyeol +happyskintr5755 +tokkippin +dejigom1 +eshoptr4336 +henaa55 +yohann8711 +jalogi +techwarm +travelgetsladmin +dolsan +dolsam +koreagoyang +kkumee +danchoo +asadal023ptn +neozen21 +imagemaker001ptn +sls98tg +villi0001 +bluepuffin1 +sik9012 +wocns131 +bogoinfo3 +kkamangddi +godo286 +godo285 +godo284 +coolio +godo283 +godo282 +godo281 +godo280 +godo278 +godo277 +godo276 +citybean +godo274 +godo273 +godo272 +godo271 +godo269 +godo268 +godo267 +witheuro +gisu0101 +kumkangsys +jean4utr3831 +jinutech1 +mntadmin +limecup +khd59251 +xv3080 +weast2 +workscm +tinypotr0821 +naegadeok +gi417admin +jumall +wellnlife1 +kimjiman751 +daesaree +dk440011 +you1smile +daltone +farmparktr1557 +domeup +playclay +pgm2392 +sensorline +gksqjrn +busexpress3 +yesoya +ham20302 +godo58189 +beaverty22 +hoondosa +mijung2 +mijung1 +episode3tr63230 +bicyclecrew1 +unicorn931 +denty2804 +julies +dametalk +ouoii +chuliz001ptn +olivedeco +flowera1051 +ecobebe +qhdtjr992 +pmeng +bunnyttr1826 +happytool +sunsun03303 +chan05173 +chan05172 +saprada +tmpdns253-240 +realmack +hsfood2 +hsfood1 +bbss31882 +okyoulove2 +sosjj777 +onebase +kss1590 +mscctv1983 +laniboutique +dfsdfsfds +cctvsi +rurunrun +kswlove +pm-korea +cookie1 +sean0jr +ssspot +bebeclara +skitripsadmin +www.dusk +artskin5 +ccumim +yellow0o1 +ecocandle +dingorio2 +actionman +sangsang5142 +sangsang5141 +ssang10055 +ssang10054 +domall +yoyang1 +gi168admin +choqueen60 +minjoo5779 +rlagkstn0513 +baram222 +ipayoneulthe1 +opengodo27 +doljip +bluemong7 +bluemong2 +rainskiss +coolman676 +csl03984 +pnj1205 +korviet +chenhyesam1 +yeskin +tt808022 +yesjoy +tt808016 +cyj092 +storyweb +asadal022ptn +storywax +skinmell6 +ghgo007 +skinmell2 +skinmell1 +sports1004 +sean394 +gogomay2 +ace5395 +em4013 +www.fast +www.dota +ftsound +stkcom +dnjsen2011 +dome24 +babybombom22 +drimpeople3 +drimpeople2 +vhcjsglftm1 +www.dodo +jukkee +hobolee +www.edge +ojoagency +spdkorea2 +getmind19 +ondino1 +glamfit1 +bowlingadmin +heybread +irenecompany2 +shopready +threesisters +zzuujjoo4 +marinesea15 +zzuujjoo2 +zzuujjoo1 +mori009 +c11c +cyawny +godo57164 +get2get +shin1687 +haze10042 +fitchnlux +nowparis6 +peoplepet1 +pjshpp1 +cozzyup +seoys09032 +aidlman +otoo6 +otoo2 +otoo1 +doldol +qud5482 +sappira +allga0051 +wooddanjo +sssils +hooncom1 +ecopiety +dbswls2087 +sanjunghosu +enamoossl3 +enamoossl2 +hskime +danbee1 +beantrtr9889 +maya009 +minji5 +lumenled +prime1233 +endxldnjs +x2tank +kwg24241 +mcoamall2 +mcoamall1 +neulchan +unionpettr +hiendcable +kmansu +scrooji +bikon4u2 +osung +iemmedia +dltkdgns6 +noodleroad +damisoo +pooh11291 +juju24 +skywow7 +zaikan2 +plusditr5407 +wheemory1 +kdjmhh002ptn +judas5802 +w3344 +withdoll +extrimer0303 +anna12211 +huhu082 +hnarutr6952 +keviinimi +cruiizy +bouncing7 +dnphoto +limcha1 +otjoa +cocoritt11 +kaymix7 +fishingart6 +hikorean +fishingart1 +goodbest +qkraudwl +valuti3 +polypix +asadal021ptn +breezecoffee2 +smardi +dalsik1 +gudwls6572 +kindcom +k680722 +yooeukim +beanshightr +gi41 +ossco +otime +kmall2 +mailuzza +jinikkoo +luxurygroup3 +luxurygroup2 +away55 +haaa8113 +hnfarm2007 +garryong1 +kitels13 +bonnietr0255 +adilike +lgnuritr0797 +agatory +digitalsalad +monickim3 +doffltm +monickim1 +dypower +rockenespanoladmin +osj04041 +danagga +jinying777 +zzana9991 +jinilamp +jucibel22 +bt2924 +fisharp +blnk5959 +koreabeko1 +ysun9201 +tastec1022 +golugolu2 +hskart +rjacob +ysun9149 +austech1 +magicmode4 +sooldoga1 +nenno0701 +wk0916 +xtrongolf2 +www.crew +tlc20122 +tlc20121 +zelkova04 +kwc1130 +egbecs +able88 +wnx11282 +gsfantasy +dawoosf1 +skywin3 +lwinkl +dhfl44442 +dhfl44441 +s2pintkthkira +st06071 +roadstter +hshs0925 +janggabang +pizzer4 +itingroup +naviro3 +naviro2 +naviro1 +soulani3 +gi40 +daejinkorea +jikyjeon49 +zetmin005ptn +sharpkwon3 +ya7897 +jikyjeon38 +moacom +jikyjeon35 +choish82 +jaeheeya2 +woodytop +koran7201 +oosame +jikyjeon29 +dkfdkqhsl +ubigeo +russellkwon1 +kdy8412-039 +foreverkdy123 +foreverkdy122 +foreverkdy121 +kdjmhh001ptn +ideakeyword +cgnflower +shsports +djdj49182 +yonex2013 +skyteam7 +skyteam4 +jikyjeon19 +kdy8412-029 +jikyjeon18 +chamzoun +www.creo +dmarktr3569 +mnijsj +steng18 +gameswtr0311 +dang119 +kdy8412-020 +rusyowner1 +givesoul8 +givesoul7 +givesoul1 +godo55503 +asadal019ptn +enamooself +cho39272 +cho39271 +movenations2 +djs0210 +top4556 +dex0562 +ikin0704-009 +ikin0704-008 +ikin0704-007 +ikin0704-006 +ikin0704-005 +ikin0704-004 +ikin0704-003 +ikin0704-002 +ikin0704-001 +nicecd386 +tonydani +trauma1 +misubaok +oiio192 +michael941 +minworam1 +dkco113 +dal +navicnctr +dhc +jjektg1 +ddilbong21 +yhstop-027 +yhstop-026 +k7176k +yhstop-024 +yhstop-023 +faholo771 +yhstop-021 +yhstop-019 +yhstop-018 +ksbtech +yhstop-016 +yhstop-015 +yhstop-014 +yhstop-013 +yhstop-012 +yhstop-011 +yhstop-009 +yhstop-008 +yhstop-007 +yhstop-006 +yhstop-005 +yhstop-004 +yhstop-003 +yhstop-002 +yhstop-001 +ikai99 +vs63001 +godogodo-043 +pippi +www908 +hooni003 +mikimsh +manguluv +donamona +oshea +kimjua1 +nabimom +afak +guinea1 +juslisen818 +rety58582 +prebebe1 +esosi712 +esosi711 +esosi710 +sentikim76 +mother2328 +taesanceo +mikimjh +sema2000 +dietcoffee1 +wishpot881 +whitepsm01 +ths4750 +happyotr4976 +iisaka +tnsckd7 +prety0717 +do20002 +prettypop1111 +mujuwine +blindview +mlk1225 +mk81758 +mchat +ys0831-018 +hiplus1 +artemis2009 +lucy172 +j700102 +wolf33403 +dakyung +sweetie87 +ahmi +vvoo1231 +eunijung3 +ribbonandtie +dflex001ptn +neuroscienceadmin +oliveadam +miiinotr0932 +dldmswjd68 +godo54840 +kim90223 +bmkc01 +geunsill2 +geunsill1 +eyestar2012 +debak729 +go1224 +epopdesign2 +epopdesign1 +sz9008 +ben-bat.co.kr +quad48131 +zetmin004ptn +soung0305 +goddnr1 +knpc2 +s4devsunny +artrecipe2 +militarycontractadmin +sunrisejr1 +telecomindustryadmin +is04211 +mmisuk18 +mmisuk16 +mmisuk15 +usmania +yoyumheni2 +salomon4 +doriskin-v4 +s4setuptest +nks0081 +ys0831-010 +silverbest1 +go0921 +daesintr2934 +bluecabin2 +jaehong664 +jaehong663 +kirinkyg7 +kirinkyg2 +pddental +sunrisein2 +aljjaman25 +kmsjgr77 +appu +iidooltr1903 +chiropracticadmin +toilet1 +jinklim +pollutionadmin +sangsang2013 +www.host2 +jetsky82 +vivajenny +i2r0251 +elibrary1969 +asadal018ptn +asluxe7814 +doozycom1 +booksell +canh +haramdnd +mbc70951 +true0420 +kimiart1 +bok32621 +sugunbank +qaact +s68221431 +wkzid +goodjob857 +neo152 +junopark1 +anigraph1 +byun1747 +ys0831-001 +jaesheen +citypet +sanshoenco +babycrew +kosint1 +godo54263 +daon7179 +gainstory +doggebe +mook1030 +sunhye03224 +a2core1 +wara1231 +luxian1 +imsli72 +banchado4 +sejinsign +mingallerytr2885 +winnerface898 +winnerface897 +winnerface896 +koreanfoodadmin +winnerface895 +cete +winnerface894 +winnerface893 +go0412 +wgroupe2 +hyuna2003 +yl7732 +byte011 +hyunminco +babycong +tubepink +gi29 +duchi77021 +plusjean1 +o3ozz6 +kym58062 +dew8100 +zhenyi1 +realvtr +hide7674 +girllovers5 +girllovers2 +mulkibel4 +hd967234 +roadstar1 +proonan29 +mdc0815 +ytotodau +romiaril7 +ennoble +laverhan +wbkorea5 +wbkorea3 +ggonara +hsystem +yowonil +lafirst3 +siyeong25 +siyeong23 +siyeong22 +xotjd05237 +xotjd05236 +xotjd05235 +xotjd05231 +sngriver +xeroxclub1 +heartbeats1 +last1020 +genfa09 +luxhera +sungoltr7233 +nose1727 +hyuna01241 +holybride +dudeo222 +bngdss79 +leeseo23221 +skytool +kim33003 +edailyedu12 +maizon1144 +pantocrator +craft42 +hjn26242 +enenfjsl +daedongsound +kenj5522 +woodchang +maniacsh3 +maniacsh1 +ddong5626 +wisekids +japson08 +mir0017 +yejin0707 +mir0015 +auri22 +dfellas6 +flow0479 +www.cias +vuelistr9567 +asadal017ptn +ssono2 +ssono1 +campingclub +haseo52121 +kchair2 +kchair1 +withanew +deeb +hjyoo1001 +uchanee1 +yeoily +xgear0072 +eunyicha +near10042 +jtouch +mahatma3 +mahatma2 +mahatma1 +yhbloveshy2 +yhbloveshy1 +kimji86 +kgoodtime1 +sbdesign001ptn +dfsl +naviclean +great8401 +jkw1915 +bsm07094 +philippapai1 +themsel3315 +kjk1331 +dldms0520 +kikibibib +luxhanu +sweetple +cjangcho +rcskytr +gshawon +doheup +myr11111 +dullymt +nkkomom +sweetool +prince3022 +prince3021 +s4devextacy +leesedesign1 +ideaaudition +applephd +resinok1 +fantastix1 +hemeelhome1 +marom19 +on32201186 +cabbage23 +jihyukbae13 +seconddress +sharpay1 +sunfung1 +diybatr2427 +sbgs221 +avgood +ljhon00 +kudos +realnut +juese11 +leebrkorea1 +sszang00 +dslrstore +maintopv4 +black17071 +dkwl486 +ys27254 +campingchon +mmoody +sddw1234 +bivouac +uspalmtr0409 +zeus9941 +flexakorea +openhub +glorygagu +lovelyhangs +wizard082 +archi5u +helena2320 +smdesign1 +orb52 +warmoviesadmin +h4ck3r +jagang3 +bytimerobe +syg2013 +isunghun +rnddevsnu +e10014 +lsh8232 +zetmin002ptn +hueplus1 +luxmgz +thunderants +kwave817 +ghghss +qlss481 +withskin1 +pculture +gi28 +aceshotr1593 +cnc7051 +soltolove +onmysky022 +onmysky021 +ccotti +foxred +pride6733 +boriflower +dairyfreecookingadmin +hukaura1 +fabulousfall +midorock1 +dhdsifl2 +kapuccino2 +choianne-025 +choianne-024 +mediacareersadmin +choianne-023 +choianne-022 +choianne-021 +choianne-019 +choianne-018 +choianne-017 +choianne-016 +choianne-015 +choianne-014 +choianne-013 +choianne-012 +applejoy +choianne-009 +choianne-008 +choianne-007 +choianne-006 +choianne-005 +choianne-004 +choianne-003 +choianne-002 +choianne-001 +ganghun1 +wowzip6 +soccerbridge +jsspace63 +actioni2 +jsspace61 +asadal016ptn +d-station +wlfmddl8 +treestory +liverpoolkp +donakim1 +sunbeltkorea2 +lyncxmpp +tlc2012 +eunsajang +tyjjang4 +aquagarden +ccopain1 +galaxy101 +sgi3162 +itspoptr1978 +ijeshop +platinumid1 +jeong2012 +alsp0124 +cbk73762 +nakwontr9317 +ever51685 +ever51683 +yesthink +siyangx +www.atom +ggomse +kimiart +naturenbio13 +junopark +hsgagu1 +euro7961 +hyunju79486 +vavagirl2 +bachstyle1 +modavintageadmin +gadangel1 +lsj94102 +hitenbike +gi24 +hyuna0124 +trianni5 +ssmug1 +gogofree +peoples12 +soundplusltdtr +forencos +dmonline +money8tr5198 +openme +inonos007ptn +gi21 +ggos38 +efolium10 +pondomtr2582 +gn7420 +ez14174 +ez14173 +efolium9 +efolium8 +efolium7 +efolium6 +efolium5 +efolium4 +efolium3 +efolium2 +efolium1 +ilmare1130 +vanac76 +flexpower1 +neepac +neopicnic +idabank +finefactory3 +kosfun1 +sweeteel +ucc106 +hanaro8555 +carsin101044 +kkohh1 +robomarket +yainsim +kjh700s1 +ipayprimrose0021 +misoarttr +tkfka0072 +sususu90 +skateboardadmin +ryuit76 +tobeemom8 +lime111 +ezerop +gasaraki +zetmin001ptn +lighting10 +goldenbridge2 +zzuujjoo3 +tgajet4 +realdie +alltoskin +prmydream +mypett2 +sensegood4 +oemparts2 +extremezone +mintcard +joyplus0503 +yupkimania +metropia +pgapi +sy4989 +gi20 +lulu1012 +onsmi +mokjang114 +asadal015ptn +mhk24912 +ndesign739 +haneul0066 +tny0566 +ndesign731 +goargentinaadmin +pyj3037 +ethiopiabet1 +spxkorea +mine4sw3 +samjin5468 +vmdlee +rosecold +damibears1 +ryuiji1 +diamond4c +flowernate3 +ditng21 +bittyboy1 +www.anna +ss2inc3 +ssa1092 +open24 +sbas +bokbunja80 +idgodo-037 +nagakig2 +nagakig1 +myoung5383 +jam0900 +kekeker1 +msgr4404 +dbgudwns79 +jhj0315 +tjsl901119 +xyclx0124 +ksing007 +keptto001ptn +supguy2 +partycs +idgodo-019 +kimsil725 +songchang +jswjam +kdjmhh8 +yhs5011 +atma2012 +ssyu78522 +abm111 +idgodo-010 +luxeinc +inonos006ptn +ww1172 +xebec751 +jnlbath1 +yuhwa82 +o3obbb +petitchou828 +divedicehd1 +www.bell +hyun8055 +dhflrndl777 +edugodo-060 +dosox2n1 +edugodo-057 +vintagecity +moblue3 +onemind08 +umakemefeel +foxart +godo50899 +namikim0105 +star38404 +edugodo-049 +sinji2006 +dam2 +foxbed +ezziroo +photojoony2 +sincez2 +pjy3588 +mjsmile1 +edugodo-039 +datastop +jojia691 +wshuztr +idc0600 +fstyletr3005 +jewelleaf +ebeds4957 +seven0770 +avenca +khk881206 +edugodo-029 +cozyshtr2117 +shinbi921 +luxem23 +morowind +khs535-007 +baraba123 +godo50707 +godo50706 +chbh44 +kimgh29711 +lime1111 +ucnehandwork1 +edugodo-021 +gi95admin +onijuka771 +edugodo-020 +qowognl1 +gaphoto1 +dkfqlsh9 +www.test1234 +dkfqlsh8 +dkfqlsh7 +eunsun272 +eunsun271 +dkfqlsh2 +envylook3 +tkcomm +gonde2002 +finival +mdhsl898 +leesr82 +cafeyteadmin +juapage +bstation +gi285admin +le8015002ptn +onkid +hyuantr0976 +namhunzz1 +southernfoodadmin +ccocca +pondaiso +sweetdeco +onurie +partsda +sweetclub +asadal014ptn +hic24851 +esom85301 +join2020 +kbsok7788 +uschool1 +heyman +cnc4721 +s2freedevsunny +clinique706 +clinique702 +davin1322 +bumdagu3 +luckysman2 +luckysman1 +jdy8591 +shoppingtong2 +lsangi1 +smcis82822 +smcis82821 +lovejini123 +lore794 +mkmk514 +sportsabctr +uossifesoom1 +yemac1 +ezdog1 +petiteadmin +grc1000 +gvtgswa +poll75821 +bosongyi +ipaymymoongchi +bau6392 +jaehwy1tr +castro77 +seberuse1 +sterniqeq1 +mt4877 +cpb56013 +woodtory +cns20101 +sskssg +acampitr9136 +judikr +kangluck +designbook5 +sevenmarket +kks3ho +exweb6 +inonos005ptn +hikaru16161 +godo50141 +ssmall +lavier +dodu11 +goldwell +sporting1 +beemer76 +hebron212 +hebron211 +seie5687-090 +peeps +fish153 +sojusocool +seie5687-088 +ssline +nwwkira +mmkww5 +soyoyou +lapis2 +o2vill +seie5687-087 +myhottopses12 +any77any771 +camnara +youuyouu2 +s3devkthkira +sinjukushop10 +gordie +bonafarm +famenity +sticketr3548 +impmediatr +chrisroh15 +jacpum +33store +zorim922 +edutps +www.acer +s99320671 +naturei +www.abcd +patchnaratr +luj4926 +helmet114tr +hangilsemi1 +kast +aange1 +ponbada +hobbang0083 +cbwcom +godo48694 +dongin991 +peco2 +nutra +sapin01 +hyun6651 +godo48659 +gkrthd20 +realcore +vbsoma-012 +hbshop2 +risingsu1 +soapsadmin +leeztyle1 +launel +pyj1210 +thdgml8652 +anthropologyadmin +dlfgns316 +kgt18851 +polishlove1 +kobacco4 +kobacco3 +unstudied +terra63 +naturalcat +kerz +jwckong3 +ssknit +damchon +asadal013ptn +zoominmall +hy-mtb +pgbrl1 +www.landscape +fabrica7202 +dodo34 +wxya2 +naturalall +fishworld +dodo12 +patapum +starphone +downunderugg +myeston +nonsul5 +edupod +atomicsnow +edutige1 +kaymix002ptn +psh77701 +xpxltm95 +ningstar84 +businessinsureadmin +petpig +lcyregina2 +lcyregina1 +kwons219 +rkdrn209 +colorbaduk +rick83 +any4952 +boardya1 +musicforum +pjaeoh10 +kwons169 +gaegoory +qlsxlwl1208 +ipayplaye +rui61781tr +trinity001 +gameindustryadmin +marley81 +godo48234 +playstationadmin +changmo20se4 +fujiara +ohowow2 +una72331 +kwons111 +kklikk +sktworld +inonos004ptn +rksrnajf2 +yogobara1 +ps4youkr +starus +forestpeople +riccio +grandparentsadmin +lux07111 +kimdy21 +feeltex2 +welskin1 +feelingyou +imbak1 +mac20022 +mklee11291 +sunyata +hongsamfirst +ozflower +candytoy +buynz0019 +chunsoul +starie +zeropark +jkcho0405 +jimmye4 +jimmye3 +yykaze +hytelecom2 +apmglobal +isaiah43211 +pcne2 +hsj001 +hsyimjit4 +jade86 +nextnm7 +rose3237 +jgbros12 +killwyj +momcook +duckbill001ptn +s2fssetuptest +mmlee2 +ammanara +nstar22 +zyoonliving +phoneplaza3 +phoneplaza2 +ironhytr3789 +joyplus05031 +mungbean1 +kubi +gi232admin +imjajatr0379 +minetree002ptn +cih3385 +idsoon2 +han8851 +mjss8077 +enjgroup +y4dot2 +eprelease +sssch3111 +os21c +seie5687-050 +eruzaming23 +roori +olitt +fophidden1 +superface03 +jmrho777 +asadal012ptn +mypumpkin +eduhoc +godo47457 +oksem +bebecare2 +bebecare1 +chunrun1 +yupkimania2 +assets.team +jielkumsok3 +jielkumsok2 +jielkumsok1 +profootballadmin +jeinobi +dode81 +ikdesign +s2fsselfrelease +hoodia +boardpan +olies +docglo +noeyedeer2 +yejin2 +yejin1 +mp100 +kaymix001ptn +neoeyen +yoyomaha +ula5959 +bigdookim2 +nab3 +lastem +hssohn74 +embarazoypartoadmin +engdevadmin +rianfn +milc +boardmtr +ligkorea +bookookm1 +thiscore1 +peoplefood +vogcody +ezauto +okpkr +capzzang +lollypop1 +peterc +thaitantawan +ydy810 +dnjswjd5236 +inonos003ptn +ironstory +cdbox1 +hsy9988234 +youdong50081 +bluecomfort +hoony2718 +younga1727 +gi412admin +pearpeach1 +onsnap +choijmjy +vistyle +jadegreen1 +freshia1 +hseok95 +laceplaza +genetichong2 +genetichong1 +cbs09581 +cj3651 +gangdoo1 +499645059 +ddalgibebe +scaniakr01 +hrdiary3 +rkrxm87 +dbcity3 +pje0802 +heraenglish +qkqhrlwls1 +digisys1 +ngatetr7925 +www.todogratis +preppy1 +hohomimi +pgl1004 +ziyegatr1791 +qkfka910 +pje0708 +tojongdac +ezbike +cssh1903 +sandletr2160 +foric7905 +bbo06262 +plscompany +sungyeon17 +free2fly +minetree001ptn +iiiyep +doath1 +jiin20111 +webnaeil +mokjang1141 +pomie84 +oojjdd +criss2879 +heynamu1 +qhrdmsgk +topnotch1 +safetytr4987 +nstal +kjkorea11 +asadal011ptn +luxsketch +dobidu +sang24601 +celeborn2 +jamesf9h +hyun4441 +jmg21402 +crash0507 +dojangtr3313 +ssimmi +shnjs08111 +yhm1999 +ssing7 +hhk7092 +realbean +maisonparis1 +jsskjh +ds49799 +lovelyweb3 +ds49797 +lch280 +ds49794 +ds49793 +cyd0609 +caselogic1 +sodasoo021 +kimhaozhe2 +kimhaozhe1 +dbwjdgkfaja1 +mufc +argirael +psports +happyngo8282 +tattooing2 +tattooing1 +leftory +godo46260 +foxeye2 +ckrheetr6079 +damam13 +sportstr4798 +eratoint5 +eratoint4 +suhan253 +bogsili +cvsoft +yajambo +officegem +pch0691 +gain251 +inonos002ptn +luxbaby +enamoofreefix +natto001 +anjn3030 +iikala +sesoft1 +jijh3246002ptn +pyj30373 +pyj30372 +pyj30371 +suhan116 +imfact0508 +refresh11 +lonsomeyez10 +hairi1 +retonar +sustainabilityadmin +en3f121 +insami331 +ehgud7641 +sjkyong2 +sslweb4 +haesung08 +poongcha +mineralco +vvmmvv888 +gagyo21 +palus +goldenage +mahamalltr +modenjeju1 +insamcall +venceremos +www.mirrors +gi59admin +londonmob +braceinfo2 +www.excellence +ramin +oemattr3699 +woodstory1 +buddhai6 +hnnature +gsme6 +anxhfptr7954 +themaker +gsphonak +spnskorea +saladin006 +xniea4 +xniea2 +pafc +jw23891 +thefaith +oopsmimi1 +s3803972 +heechany762 +ljcompany2 +jmadang +lpmusic +uittum4 +uittum3 +bmhouse2 +majiyabe2 +dongjinds001ptn +ro1003 +rlatjdwn77 +nydmarket1 +jssdr1 +jsy521 +kafa421 +pcko +kimck77 +officebay +huisoonn +guweb119 +guweb118 +guweb117 +iknow214 +mcyama1 +godomantis +smh45641 +indiplus2 +asadal009ptn +lookyweb +serverhosting202-104 +pm1023 +sshms2 +www.sid +ggo9ma +taesongf +l2zone1 +cbsint +sshnad +jinimage101 +godo144957 +meetain +choihw21 +jindam1 +collaman1 +icamp4tr7977 +kjkgis014 +kbncomputer-030 +daisymom +kbncomputer-027 +donadona +cjlim11 +mcloud8642 +canarywharf3 +canarywharf2 +lilalim +kbncomputer-019 +gi163admin +hymtbold +lay123 +narangbu +newman60 +semir0615 +leejiyea54181 +bbliving2 +rlawls01 +kbncomputer-012 +choo9646 +inonos001ptn +eshoptr4777 +kbncomputer-010 +kenzpeople +sohojob10 +egw8191 +hyun2981 +sophi77 +adamas29 +twinkleahn +bsretail +suomi37 +nggift9 +nggift8 +nggift7 +nggift6 +nggift5 +nggift4 +nggift3 +nggift2 +oliveppo11 +hyun2918 +syncbird +godo44923 +shmedia +koongstr3115 +sso119 +sk05843 +sk05842 +bychance486 +sidecom +isecetr +anglicanismadmin +art1gagu +babybabar +jusihyeon87 +vpn2gadmin +onchang +godoid-026 +blingi +ansadon +babi03191 +depressionadmin +inglesina +hitodachi2 +www.ebusiness +shpark7507 +edworld +skyanbg2 +hsj80261 +po77701 +chungilfarm2 +wellbeing251 +coqls10041 +eshoptr4437 +toonlee3 +yehdam +plannetr9495 +poloo79 +apple365 +jam09002 +han5878 +jini467 +bikinistore3 +kimshosa1 +matziptr3159 +hyowon1229 +kangil88 +peniel1004 +kygsan1 +ozq8 +hyojoong2 +ababyo +hun4032 +yonexjapan5 +yumidavid +jdy3716 +wembstore +kimbj89 +f16t1253 +esecretgardentr +kobuworld +risingbike +fox739 +salesctr0964 +coordians +sb6007 +nadaje +mijumarket2 +p10499 +marineland +essvalve +selebetr5470 +geographyadmin +girlingirl +funnkids +bros90071 +indsystem +winstory +jhj03151 +desig11051 +browse012 +smallfarmadmin +kimh313 +koh08111 +jinseok120 +listen007 +herbtrees +xmore1 +mainpark206 +lhj06203 +dswoodlac +dobidop3 +jjk2943 +bydo82412 +kdy8412-040 +kdy8412-038 +kdy8412-037 +kdy8412-036 +kdy8412-035 +kdy8412-034 +kdy8412-033 +kdy8412-032 +kdy8412-031 +kdy8412-030 +kdy8412-028 +kdy8412-027 +kdy8412-026 +kdy8412-025 +kdy8412-024 +kdy8412-023 +x1x1 +kdy8412-022 +kdy8412-021 +kdy8412-019 +kdy8412-018 +kdy8412-017 +kdy8412-016 +kdy8412-015 +kdy8412-014 +kdy8412-013 +kdy8412-012 +kdy8412-011 +kdy8412-010 +kdy8412-008 +kdy8412-007 +postad.sewing +kdy8412-006 +kdy8412-005 +kdy8412-004 +kdy8412-003 +kdy8412-002 +gi249admin +kdy8412-001 +dydghksgl +rma2 +hipgirtr3630 +law924 +golden1295 +peplus +kaybes1 +dsspotr2418 +polobox +eraecorp +idsky11 +lkdc3535 +halifaxadmin +ihkim20004 +ihkim20003 +heenam71 +ihkim20001 +mascara1 +soso0808 +ohseungkon2 +fmricetr3811 +wkdeo860521 +hiona08 +force1 +chammidia +atomyctr7292 +bookssladmin +jinee47861 +alpo801 +aljjaman +woolungnar +sems +tear3218 +jinho781 +akswkehf1 +momoihome +ohstylishe1 +dhtown54373 +acecounter +kkhgh1 +ibikeboy +cotorro1 +jeilad2 +ipuhaha1 +designarts3 +wiso811 +kjmoon973 +sunsim09062 +rooz +jwellday1 +chamsallee1 +ghkdvy11 +atree3 +miiragi +space876712 +kkang732 +gi90admin +saokkum +spacehs +oohjuwon1 +slow2go21 +kjhmisope2 +kjhmisope1 +hellomaniatr +ndmshop +gi279admin +hustler2011 +casadela +guide25 +allergiessladmin +wowzip2 +alicekids124150 +kkhk11 +arawon10 +zeus1592 +id410041 +me10921 +reactiv +msleesh663 +parpado +skinhappygeo +yeetj1 +asadal007ptn +tlstmdxor772 +pon2mart1 +polishes1 +free5566 +nudienara +ziopack +koy0829 +www.musicman +troikakoreatr +qlqushs +smallej2 +artzero +k1984321 +hotstk5 +hotstk2 +th485001 +m1544tr1530 +nprn1 +toggi021 +datamove +soh21832 +kjnd1218 +cherryheel +hotsso6 +calla7tr9299 +krr0516 +selandshop +yj55755 +nanumfood +woodmarkers +powermtr4204 +gm8579 +kjw190 +automobilesadmin +byul9651 +scandinavianfoodadmin +cjstkek24 +silvers +hdmarket +paganwiccanadmin +goblin1 +bangsuk +greenmoa884 +greenmoa883 +newsnetr8213 +jasmine925 +inmigracionadmin +plus67021 +controlman +grace60232 +grace60231 +jwckong2 +petsland +hyun0987 +yuyichoi +ohero +cnsgh338 +cnsgh337 +cnsgh336 +cnsgh335 +hyun100p +cnsgh333 +youngsuhp +hheawon +tuning1 +surgery13 +surgery12 +surgery11 +surgery10 +mojomall1 +ms7675 +yiminhee +tworld2 +onemulti +omycom +thesuptr7962 +acemodel2 +phonia3 +phonia2 +lowercholesteroladmin +tpghk05311 +nsnyc39792 +vino62001 +m1m3y3 +enamoofree +marylennox +bomul90009 +tworing +naver062 +lovesjeong +spdhalsxm +epqa +dudcosp2 +zeus0705 +luv4tion1 +m897189712 +bicyclingadmin +neoeurtr9209 +fndkorea +persent991 +mvadmin +castleb1 +godo42593 +compnet991 +poongwoontr +hhhjjjkkk +berenice07 +tesas77 +hipark7 +xuxgirl1 +laciel1 +luckylady +leezipp +malzahar +jayunmart +funfromfun2 +gi406admin +comtachi +mirsystem +nacaoo882 +nacaoo881 +t-pani +asadal006ptn +yuhwa821 +taekyupark +www.tam +pinkjjunga1 +komsunni +skykeep +godo15tr8668 +moonoogi1 +godo42440 +game19653 +sarlira2 +ksh8579 +mikibonbon1 +kimsuk3181 +naebrotr0181 +bank88521 +ghayour +bubbleangel1 +woodnice +aaa9470 +bianzai +angeloarte +ezmrotr0665 +dsamples2 +godo42315 +efreeworld +taemiwon44 +firstblush +chamvium +supaek1 +ahjun7111 +hadesway +alphain +dlqnsl183 +quffl0613 +granty +moguchonlove +hanih70 +gomsoman +sanubis +godo42184 +lifeyotr9845 +longdown1 +avenue5 +gulbiwon +ljh8354 +kiansha1 +jhcho845 +jhcho844 +costcomarket +krepis +richkor1 +penmoa +jsmysh +www.dcs +tileart3 +ljh8240 +s3intsf +nonno21 +aboutshoe +zzi33tr +nonno10 +hsd123 +s3intnj +heliosji +kiss9035 +dayoun01 +jinyunung +www.cmd +imarketing019ptn +rany0111 +gofla0101 +jhh9866 +kjunggu1 +yeecya +seezytank +jw20122 +mydiy +itemssada +mydiw +eyetag +imarketing059ptn +kck33371 +fpfp88 +dsfashion +ceycey801 +kn19051 +hh119tr8019 +annesattic +wjdwldnjs +godoshare2 +gi157admin +han2963 +cypark113 +cypark111 +www.sdi +thekoitr +lemonttt2 +bluelink5 +wkahd25 +dshuni5 +godobackup +dshuni3 +lannen +ojy5220 +ingpp7488 +korezon +rexbattr5147 +yecstr +conceptsmith +chummy1004 +kjh6312581 +ms6336 +jpspace3 +manna2641 +godoedu49 +vitrosports2 +ysm5208 +somangmalltr +rheeys +kdw8881 +asadal005ptn +ninja781 +dltjdwn682 +isensemom11 +hiv2000 +wshoesj +sojabon +divoff82 +cnb5709 +mailike1 +bngcenter +godo41390 +onlyu2 +onlyu1 +flag119tr6840 +www.susan +ogemma3 +ogemma2 +sohometr1208 +sunheealsk +kadian2 +kn35403041 +jubangtr5297 +cubesetuptest +benrokorea1 +bible4ne1 +emliving +eyelux +needss1 +ohjoojoo +hsc345 +foodkk +lenmonglass +wowbizbiz +lucea64462 +cbj6503 +s3inthn +dgdg00251 +thepottr4429 +eugenephi +lshyun0202 +vpop +bathroomsadmin +woorihanwoo +izzle365 +applebarista1 +joy8334 +moongkl +eugeneph2 +eugeneph1 +rongee1990 +alicatr0439 +ipaychaesowa1 +toycorea +young23391 +dddog91 +miraicej2 +tinaea +sundaymarket +sewingadmin +wildwolf1 +mmeeok +zona67 +leeneahn +minyoung3 +gundamhouse5 +gundamhouse2 +iktc55391 +audwls +joo72421 +evendoztr +frankkcl +chai37461 +godo39838 +skplastic1 +lubu106 +dajutns +markgolf +hohohaha +tawn05252 +eoakeka13 +eoakeka12 +eoakeka11 +csinfotel +dna4300 +help62ne +usshotr1632 +chanoj98 +jhdigitech1 +ipaycarpr0112 +bluelife5 +bluelife3 +vsinv55 +youn22ya2 +freshblue2 +dlatmdxo +lemontr21 +naiasis2 +eballet +loisemall +mwj4780 +noi20133 +noi20132 +noart +moondal +mwkim +ds09-trans +kotak0441 +sb12341 +gkdl1111 +makyung414 +shs1127 +kyuri231 +lprecord +neoblume3 +jeonjinok-003 +jeonjinok-002 +jygolftr7526 +dlsrnjs1358 +sunge03145 +sunge03144 +asadal004ptn +sunge03141 +hjw02273 +food75 +momtobee +yedam2 +kevin9001 +han1661 +soocol83 +jstephanie +leoncafe1 +artrxtr7959 +tpy8297 +www.gamerevolution +camel76 +pcstop1 +halu0815 +trysunny +mideastfoodadmin +geuxer +firstwave1 +iium242001ptn +itools.team +dl3094whgdk +enjoycoffee1 +tobe70091 +collahaha +villi000 +hkc9711 +dpplaza +finalcooo +photome1 +misotrees +cheung62231 +jh2097001 +www.runescape +bantdoduk1 +rio20003 +coffeeseed +mwfss +happytgr +mvpp9 +mvpp8 +mvpp7 +mvpp6 +classicalmusicadmin +mvpp5 +mvpp4 +mvpp3 +mvpp2 +mvpp1 +odysseygolf +arums84 +xn2otr1926 +danparkb +dposter +thinkplus +idio0121 +amban3339 +lune12 +einein1 +foodplan +smframe +puryhouse +preist1 +satelliteadmin +godo38991 +qkqh80801 +jaesheen1 +idmoontr7517 +kjmo23 +hmk50403 +sooyeoun2 +gobigs3 +richqueen9 +handerson +healthtr1831 +diva4789 +lovelypink +ibbeoneh2 +dajung1 +psyche3171 +vision12001ptn +moitie701 +minsstory2 +minsstory1 +photohow +abator83 +dsand261 +danchooya +aldhr8212 +parklon +deblanche +detoxpw1 +golfbank2 +leexcom +reddj752 +jbd04131 +maxdm3 +ginachoko +kleeu12 +hwhv981 +cubedevextacy +halu0301 +dshuni4 +a622dday2 +cohanamalltr +han1071 +unicityro +narae3943 +exmiki +time24 +jsj77402 +gochicagoadmin +mklee74 +mell99381 +hsyhan1 +mkqhouse +jejutrust +han1014 +mutu9 +ssayer +nukorea100 +mutu2 +www.rv +s1intmimi +aramjo2 +s2fsdevwheeya88 +bonkorea2 +bonkorea1 +everhome1 +fuhrer1 +santaatr9816 +shin202 +jopersie13 +lawncareadmin +akdlxl3 +gsgtel-020 +gsgtel-018 +gsgtel-017 +gsgtel-016 +gsgtel-015 +gsgtel-014 +gsgtel-013 +gsgtel-012 +gsgtel-011 +lee84352 +gsgtel-008 +gsgtel-007 +gsgtel-006 +gsgtel-005 +gsgtel-004 +gsgtel-003 +gsgtel-002 +gsgtel-001 +ufo112381 +asadal003ptn +espanolsladmin +soo8407 +misan1234 +rmsdud9909 +diyadmin +aldhrwhgdk +artgroup21 +aaa11 +sugar8080 +redstyle76 +halloweenadmin +tanic79 +skyho50461 +elsabyelsa +sbyung4422 +paanmego +lovelyone1 +odin3 +meet202 +nick218kim +ilovetoyz +autochamp +happymax +luvmary +wastec +jehomme +gomdontr6981 +samyuko +studygoon1 +hueyounsun7 +bubblestore +hueyounsun3 +jay4114 +foby004 +witcommerce1 +plannetr4111 +jini07064 +jini07063 +jini07062 +sdgvictory +coffeetr4517 +dasstr0493 +ghdiaka1 +detoxjoa +cider4567 +chungchowon +smm8277 +tlsgur7551 +na995444 +jidomatr5165 +naragu92 +peakswoods +stdevw5 +stdevw4 +stdevw3 +stdevw2 +stdevw1 +taerin333 +ns707 +fineyes1 +printmtr8091 +samoondoh +sqube4 +cottoyamyam +helinara +kji5982 +wowman21 +zigprid70 +dkfqlsh10 +lhsmkbs +fishingart +quezon11 +rurisnaby1 +artlife6 +artlife1 +bnw3835 +odmaru1 +choibs76 +time3040 +sorra777 +mega701 +koreavi +wt234561 +happyho2 +truebeans +mn22ang1 +parkj21 +behaptr0410 +djawdj55 +djawdj53 +gi84admin +bandiac +babystown +starexon +kodtsite +happyhan +john316tr +wowksk88 +eyesrue1 +cocobunny +naturalline +cnb3078 +oo0103 +han0011 +bpktoolpia1 +indi-web141 +indi-web140 +indi-web138 +indi-web137 +indi-web136 +indi-web135 +indi-web134 +indi-web133 +indi-web132 +indi-web131 +gi274admin +mettlertoledo +wnaks316 +mukie +altmedicineadmin +ssanot +ms6204 +evendotr7447 +dmfoodtr0677 +oobike +ssanmk +kaulbach5 +wowhouse2 +seomuho1 +oxygenmall +mega325 +asadal002ptn +twomomo +filcotr7304 +qkrtmddo +karisub3 +karisub2 +karisub1 +themadtr3641 +www.worldofwarcraft +yeosinmall1 +poplittle1 +catsin +getpda +free0530 +lsd19821 +tundra3 +worldmusicadmin +rbghks1 +eclips1 +ssh486 +fourleafs +monkeystreet1 +ochw1 +kkassi +leejin120 +kanegi85381 +designangle +sohojob +ssambo +hsk7005 +edgestory +kdw5701 +oje1990 +edinburghadmin +jayeon4 +agas00705 +mungmung795 +mungmung794 +mungmung793 +mungmung792 +mungmung791 +stdev24 +stdev22 +bandi83 +blackpc-019 +sunyoung +fahsai2 +blackmoo3 +ksana11 +team3point0 +sooya300 +najs8412 +sohohub +ksumahu +jhko21c1 +blackpc-009 +water20201 +le8015001ptn +shjcy1348 +gogoga121 +march03111 +redbb10107 +redbb10106 +redbb10105 +armyinsa1 +yeg777 +blackpc-003 +lxh05121 +ndaoom +sinatrano1 +susan123 +susan120 +classices +yeonsung-099 +yeonsung-098 +yeonsung-097 +yeonsung-096 +yeonsung-095 +yeonsung-094 +polomin17551 +yeonsung-092 +bigsun38051 +yeonsung-089 +johansoo +yeonsung-087 +yeonsung-086 +yeonsung-085 +yeonsung-084 +yeonsung-083 +nms2223 +stcok19 +yeonsung-079 +yeonsung-078 +yeonsung-077 +yeonsung-076 +yeonsung-075 +yeonsung-074 +stcok12 +pondaiso1 +stcok10 +joyav119 +yeonsung-068 +yeonsung-067 +yeonsung-066 +yeonsung-065 +yeonsung-064 +yeonsung-063 +yeonsung-062 +yeonsung-061 +rgbtable +yeonsung-058 +yeonsung-057 +yeonsung-056 +yeonsung-055 +diva2763 +yeonsung-053 +yeonsung-052 +yeonsung-051 +yeonsung-049 +yeonsung-048 +yeonsung-047 +yeonsung-046 +journalismadmin +yeonsung-045 +yeonsung-044 +yeonsung-043 +yeonsung-042 +yeonsung-041 +yeonsung-039 +yeonsung-038 +yeonsung-037 +yeonsung-036 +yeonsung-035 +yeonsung-034 +yeonsung-033 +yeonsung-032 +yeonsung-031 +waityo3 +yeonsung-028 +yeonsung-027 +yeonsung-026 +yeonsung-025 +yeonsung-024 +hospitalityadmin +yeonsung-022 +xross01 +yeonsung-019 +yeonsung-018 +yeonsung-017 +yeonsung-016 +cc112a +yeonsung-014 +yeonsung-013 +yeonsung-012 +yeonsung-011 +yeonsung-009 +yeonsung-008 +yeonsung-007 +yeonsung-006 +yeonsung-005 +yeonsung-004 +yeonsung-003 +yeonsung-002 +yeonsung-001 +s4freedevmimi +unomito +sanorm1 +siena5958 +aid09082 +fire881 +dybox7711 +mattox3 +paris05 +n2comm6 +n2comm5 +n2comm3 +hinokid +eliyuri +venusbt +fish6033 +johnny422 +childrens8 +wholesee +luvite7116 +bonnie1988 +tas78335 +mamangtr2075 +buxtest +kygwings +urbanx +saemartd2 +leejieuna +dukgun2 +karismay +skinustr +ybmidas +uiyi007 +naraetek +jcfl9275 +evenfalltr +skyreins +sakeimalltr +yourlim +warmer +serverhosting254-77 +projecth +luxurytraveladmin +serverhosting254-64 +khc744601 +serverhosting254-52 +limsh03045 +serverhosting254-43 +welpia +serverhosting254-40 +todaymall +serverhosting254-36 +agstore +xlsh23 +unioutlet +seatline +rira10291 +ssadoo +dugotech +sangsangcat +omrpro +gaiazone1 +als24681 +lastlove72 +hyou.co.kr +econian6 +econian5 +japansladmin +thsqndud1 +dmi9797 +s1intjonr +masus990 +dmmpowtr9582 +s1release +shyun293 +youhansol +qwe912-019 +qwe912-018 +qwe912-017 +in4mal +qwe912-015 +qwe912-014 +qwe912-013 +qwe912-012 +lsg2646 +qwe912-010 +qwe912-008 +qwe912-007 +qwe912-006 +qwe912-005 +qwe912-004 +qwe912-003 +qwe912-002 +qwe912-001 +ds5evj +shinkangco +looz784 +looz783 +bodybuildingadmin +brandsil +qpit26 +aks35351 +moohyun +khmedical +oilotaku +salenjoy +hhoow8585 +parkyuri011 +chaosrever +browsersadmin +bbsports +hikingadmin +veffka +gi401admin +vipjuice +saerom123 +sketch1993 +hdw112006 +columbiascadmin +stsunwoo +popcone1 +banner7963 +godo36074 +designclan002ptn +hjlee215 +jaednr2 +newromi +csy11223 +seomsky2 +cs51311 +xigoldkr21 +green4all +aroundtable +startac1011 +belajar +young18284 +shjk10131 +churi4861 +gagooya +tammy69 +gnfcorea +ciellove83 +freeover1 +skanskan4 +hksh012 +feelidea +mplay20131 +sannoul +nongbufarm +esceramic +astden +homenhouse2 +esfreak3 +bjs1979 +xmidas +gi152admin +gmdrmatr3060 +keiangel1 +wansophonetr +malddotr0520 +joyuneed +mkscho +kjjcyh +kangageu +tommyboard +tradech +wook0308 +manjijak004 +manjijak003 +jinhaney +luji54 +johnny42 +mindsports +shuzai.history1900s +broadcastnewsadmin +photoworld +nativeamericanhistoryadmin +canadahistoryadmin +adnet8 +yhl1239 +fish1tr2605 +istel0701 +m9927254 +tonature +classicfilmadmin +wldms0105 +colorparty003ptn +dasoon222 +pink29001ptn +s3intmimi +caster07 +shin971111 +gobekjy +gomsinne +flyingtr6350 +luna4781 +sjjwe1212 +dnstars84 +godo102867 +eunjungddal +mmmjbw6 +green9629 +rcd7325 +revimotr0488 +jikukak +jhwa211 +kdonggin +khalili +odedesign001ptn +raontec +floraquilt +kq1219 +purebounty2 +kdykorea2 +ms0921 +homesuda4 +miiino1 +ozzguitar +bohwa1124 +loosfly +yafil72701 +onitstyle +hataesoo2 +lovejini1231 +lskwoan3 +lh1092 +iknew06254 +leatherworks +asitaka7221 +shaeizzang +i1127724 +rmfpdlq11 +printmtr5136 +hydrus86 +coolchoice +gojapanadmin +rhkr51451 +wh9022 +bizydp +carone +yongsanoa +caros4 +visualbasicadmin +designclan001ptn +seob60139 +dyparttr8149 +sunmi21 +blog131 +blog130 +aboobar1 +purplelove1 +wholeart +channelpc +europankorea +ldm523007 +damwoori +vdvctr2705 +casejo +vdoffice +inicis1 +koj24572 +ansholic2 +timetreehue1 +ksh2081 +nbreed +artrxtr3451 +come3840 +edev +asksal2 +pensive0042 +cuberental140 +kitweb-019 +kitweb-018 +kitweb-017 +kitweb-016 +kitweb-015 +kitweb-014 +kitweb-013 +kitweb-012 +kitweb-011 +kitweb-009 +kitweb-008 +kitweb-007 +kitweb-006 +kitweb-005 +kitweb-004 +kitweb-003 +kitweb-002 +kitweb-001 +soung401620 +sengju1937 +biscuit65 +luview2 +ssdiarytr +youplus +rui61781 +navydew2 +navydew1 +pyungyi +yesmountaintr +cppower +skycomm +defenseadmin +usplus +daebbang010 +diva0427 +hotemeil1 +armssl +crsharp +seie5687-099 +seie5687-098 +seie5687-097 +seie5687-096 +seie5687-095 +seie5687-094 +seie5687-093 +seie5687-092 +seie5687-091 +seie5687-100 +k3d2c33 +k3d2c32 +seie5687-086 +seie5687-085 +seie5687-084 +seie5687-083 +seie5687-082 +seie5687-081 +seie5687-080 +www.terri +seie5687-078 +seie5687-077 +seie5687-076 +seie5687-075 +seie5687-074 +hannongcc +alsdlf789 +jaeinfarm +seie5687-069 +hongsamajc +seie5687-067 +seie5687-066 +seie5687-065 +seie5687-064 +seie5687-063 +seie5687-062 +seie5687-061 +seie5687-059 +seie5687-058 +seie5687-057 +seie5687-056 +seie5687-055 +seie5687-054 +seie5687-053 +seie5687-052 +seie5687-051 +seie5687-049 +seie5687-048 +seie5687-047 +seie5687-046 +seie5687-045 +seie5687-044 +seie5687-043 +seie5687-042 +seie5687-041 +seie5687-039 +seie5687-038 +seie5687-037 +seie5687-036 +seie5687-035 +seie5687-034 +seie5687-033 +seie5687-032 +seie5687-031 +seie5687-029 +www.victor +godo34474 +seie5687-027 +seie5687-026 +seie5687-025 +seie5687-024 +saku435693 +seie5687-022 +saku435691 +seie5687-020 +seie5687-018 +seie5687-017 +seie5687-016 +seie5687-015 +seie5687-014 +seie5687-013 +cmb200tr5801 +chunglim +seie5687-010 +seie5687-008 +seie5687-007 +seie5687-006 +seie5687-005 +seie5687-004 +seie5687-003 +seie5687-002 +koran21 +kim171802141 +and1364 +bosongyi1 +paran219 +srhj95 +neverdiesp +emberhm +tachhotr1929 +oceanfamily +cosmosseed +nj0090 +lkc1120 +newrack +wepix003ptn +kiras3 +ys0831-020 +ehdgl9622 +hanakwon7 +lohason2 +lubicon +hoyup2 +hoyup1 +snuspo52347 +s4devsdg +fullart5 +fullart4 +fullart2 +ds2pcw +cmj8547 +lovelyjudy +gi78admin +gagu331 +ys0831-012 +esher24 +ys0831-011 +ys0831-009 +whn1482 +upgrade8kwb +mjceo +crackman +dychemi2 +roast52863 +roast52861 +calicoz +chanwido8 +chanwido7 +chanwido6 +chanwido5 +chanwido4 +cupyeon1 +cmplus12 +khyse2 +eunsun27 +bluelover55 +shinhyoun +mob0117 +rextop +calibow +jcw75651 +bat1207 +minukorea +yooho0802 +dl68136 +tofto99 +yuhaenam +howsign1 +damin94961 +gi268admin +ehdgoanf10 +sangkoma +psworld2 +ezziroo1 +carein +parkjoye1 +creamstr5719 +zzangzo +kwh83911 +www.terror +nika20101 +godoid-029 +gamerspot +godoid-028 +godoid-027 +puretime +godoid-025 +godoid-024 +godoid-023 +godoid-022 +godoid-021 +godoid-019 +godoid-018 +godoid-017 +godoid-016 +godoid-015 +godoid-014 +godoid-013 +godoid-012 +godoid-011 +godoid-010 +godoid-008 +godoid-007 +godoid-006 +godoid-005 +godoid-004 +godoid-003 +godoid-002 +godoid-001 +goldmommy03 +dodopiggirl +coconenne +sound8224 +ivory60 +kiboonup3 +ok00yeol5 +ok00yeol4 +petcentral4 +petcentral2 +maneryun +dollkooo1 +nasoyo1 +mandu10202 +gomiamiadmin +arteadmin +je79hs +jingu721 +godo33568 +green7804 +zzangfa +funnysuper +dodoham +warefile2 +lsg0000 +pointbar +usbhouse1 +btbgift +greenpet114 +zzange2 +pys06045 +shoedealertr +powertr1217 +colorparty001ptn +djjjahwal +airjoon783 +airjoon782 +airjoon781 +dt0043 +s4devkhs +toolsjoa +gka64711 +ljh0625 +ivitacost1 +hoparkc2 +karimi +ydgbb1 +lahatz +hpstar20011 +cure75 +kikis13 +kkt1227 +minhee4205 +3000ton +asrada +carace +lilylee1 +yovery1 +fishcatch +caribul9 +aspris +caribul6 +caribul5 +kwons41 +kji1351 +onggij +wepix002ptn +kidsastronomyadmin +edawool +jjile799 +bizcdaejeon +sonsubook +inowater +ttbehan1 +giftspoon +namikkoquilttr +s3intjonr +sevenmarket3 +snd3282 +nanacom2 +s3release +inoi3357 +comsaja1 +duck66815 +fazel +codyand +7-12educatorsadmin +ds1lza +audi88131 +himomoko +mrc22 +bbshine2 +gi109admin +vidan2002 +yeseee1004 +gyorim +youl04111 +fromap1 +graceraiment +it2gpc-040 +dasincn5 +dasincn4 +amahime1 +bizcws +gobawoo +comebine1 +exfron +it2gpc-037 +gdtest-055 +gdtest-054 +gdtest-053 +coffeemal5 +gdtest-051 +gdtest-050 +gdtest-048 +coffeemal1 +gdtest-046 +gdtest-045 +gdtest-044 +gdtest-043 +gdtest-042 +gdtest-041 +gdtest-039 +gdtest-038 +jolibabytr +gdtest-036 +gdtest-035 +gdtest-034 +gdtest-033 +gdtest-032 +gdtest-031 +gdtest-029 +gdtest-028 +gdtest-027 +gdtest-026 +gdtest-025 +gdtest-024 +jimi12342 +gdtest-022 +gdtest-021 +gdtest-019 +gdtest-018 +gdtest-017 +cody4man +gdtest-015 +gdtest-014 +gdtest-013 +gdtest-012 +gdtest-011 +gdtest-009 +gdtest-008 +gdtest-007 +gdtest-006 +gdtest-005 +gdtest-004 +gdtest-003 +gdtest-002 +gdtest-001 +car7979 +miracle1201 +kkw29142121495 +abcbike3 +metro71112 +rose44781 +yuginara +voglenza7 +applehearts10 +damoainc2 +myloveday76 +cara06 +outsider2 +uamake2 +recipeformen +autofactory +jk91792 +jk91791 +hautechocotr3818 +motahari +nasungin2 +artpia1 +monitoro4 +p098791 +gabangusa +monitoro1 +guciogucci +monokio +goprinting +hellosra2 +polyflower +bokdory1004 +orangehold4 +jangmanho1 +narabio1 +necomas +whoislover +teraled +mayfresh +interesia8 +interesia6 +interesia4 +interesia2 +babyprism1 +yoanna1 +mosac +hoah441 +gi385admin +porfavor +mejiro11191 +divineworks5 +divineworks3 +mrherb1 +wepix001ptn +yong4535 +ilovesneaker-trans +kisstreet +blackhoon +wanjin +jhcho8tr5661 +toto0609 +babegiraffe1 +kajawine +caphjm +mr7004 +need232 +joyaudtr1183 +bumhokim2 +terahtz +gmskin7 +gmskin5 +gmskin4 +actionfiguresadmin +godo32207 +sunilv4 +deartdesign1 +paratopia +gi146admin +iherbalife +tex2105 +rumebag7 +rumebag6 +rumebag1 +jhchae71 +kswl0626 +naye01 +monoful +cariart1 +park632 +tmdrlfs +jeonjinok-001 +hyj01636 +youngs2 +moon3 +luveret +lgslgs +lbs8788 +seongbuk +smileparty +onevskorea +ysj1215 +hosogkim +induk11-040 +uniqueme +sun04041 +uppermost0622 +alimoradi +wake777 +ks0801081 +montage2013 +annaj20121 +induk11-029 +sigane42 +bizcbucheon +c-olymp +cmailreceive +wangga +sponiatr3499 +bada88222 +eggbbang +moncl +induk11-024 +induk11-023 +geojin +parkjung962 +designaide3 +designaide2 +designaide1 +induk11-020 +rovltr3141 +haessac +choimin2004 +ksg7939 +monitor16 +induk11-010 +hamdp3tr +syspharm +changcom +upside1 +unclemulti +cosmosmall2 +cosmosmall1 +ifxeye +gcsd33014ptn +sesalo2 +autobiltr +sesame2 +sesame1 +ylife39 +blueking3 +bblocal +trueness78 +uslux1 +lemonmall +hwjhaisr +exwin20101 +maurizio +realestateprosadmin +byeyourjune +mioggi20111 +dgplus1 +falconshoptr +rlaqhdus12 +valenciano +thechakhan +domainparking +mtsearch +easyfile +passecompose +swon06161 +09land +jyn7771 +coolsohot2 +ckh5853 +kimsy3 +haeorums +en74421 +kjh8347 +nanotometer +sabatapark +hskim7201 +clientjh001ptn +daoud +hapoom10041 +www.graveyard +ii1121ii +sulem10 +fashionweekadmin +superftr9577 +lcs111985 +ryuyangrod +www.vendor +thedark +textbooks +chatserver +emjstyle3 +zatool4 +gadgetgiftsadmin +mexicanfoodadmin +windowsntadmin +www.fernando +partypoker +heavymetaladmin +gi73admin +gi263admin +ldssladmin +tatsladmin +www.teknik +gi384admin +asthmaadmin +emergingmarketsadmin +prono +www.escorts +opensourceadmin +embroideryadmin +womenshairadmin +twohcnc +zatool3 +lwj6166 +cano33332 +monocruz2 +petarian +kingze +feelcos6 +kalpataruhan4 +kalpataruhan3 +www.rocks +k6educatorsadmin +miguelangel +lesha121 +s3designskin +ruu70781 +todayfood +yabooksadmin +yjh8505182 +wanasa +habb0 +celebritystyleadmin +leejiyea5418 +simon02711 +luciashop +feelcom3 +myssoltr5863 +humanrightsadmin +findwlsfl +claudia1004 +bengillee +studenttraveladmin +ecojoon76 +saesoltr1810 +twoweek000 +cozcoz1 +abdev1b +jhun731 +mendoza +ziodeco +cuticase +narapuppy +bumk222 +mnt21 +sakurasweety7 +glutton +sakurasweety6 +sakurasweety5 +iandsoop +dandy8613 +serimmf +mobyj +reusea +kimnno +bluepuffin +wocns13 +jacpum4 +jacpum3 +jacpum2 +monixcop +thisa25 +ssbk10942 +ssbk10941 +houze0 +hanumatr4803 +codica +shr1217 +kindpc +ilovenamu +kiddykorea +willvi +phone1001 +walltv +lemadang +pettong +ondino +xhfl098 +kjk517 +coordicoordi2 +ubi9134 +okits211 +enamoossl +ugly7707 +sss0083 +hsj9191 +formtabc1 +ilikeshop003ptn +zyoon +cho5253 +jisungju +tkdalswnsdl +jinokey01 +playplay +heh525 +wldus33841 +pawpawkr +kang8017vs9 +michael91 +ryumin +godo30384 +pedia1 +girlsego1 +lhwfree1 +nextone +cricketadmin +gi379admin +sparedb +organicgardeningadmin +gi141admin +thehero +ramansaran +pirtnews +www.ricky +tabletasadmin +dentistryadmin +icecreamadmin +cigarsadmin +giopt +jayp +www.bussines +laundryadmin +www.pcgames +showslow +gi67admin +gi496admin +gi257admin +gi429admin +financesadmin +goseadmin +googlpiz +godasky +inventorsadmin +crazzy +www.motahari +publictransportadmin +orangecountyadmin +themillionaire +gi374admin +testgb +gi135admin +biomedicineadmin +heartdiseaseadmin +intljobsadmin +ukjobsearchadmin +gi7admin +lovestar +gi391admin +chattingadmin +retireplanadmin +www.funnystuff +4wheeldriveadmin +gi62admin +webdisk.pruebas +www.theempire +search.jabber +www.amigos +timor +gi491admin +gi252admin +www.brothers +armftp +gogreeceadmin +55555 +awesomesauce +bogy +upperwestsideadmin +rgarcia +www.roger +enusaadmin +vhenzo +mariyasexi +shuzai.afroamhistory +csforum +evolutionadmin +ksiegarnia +personalorganizingadmin +izh +tuto +www.inmuebles +puzzlesadmin +gi220admin +carolina +facebooka +nybfreelist1 +gi368admin +facebooks +ngraphics +ahl +blackbirds +ashvini +gi54admin +ashwini +lenga +oportunidades +javaadmin +elpasoadmin +taesang1 +coomheedo +edaun00 +hplus7 +dodomint4 +oncore +lavert3 +lavert1 +jounnal77 +twinya +liesangbong1 +luxyi +www.calculus +donnland1 +dmdoll +kilroy +mdwootr4250 +lomis-v3 +yhbloveshy +voc +turki +canceradmin +gdlist +stampsadmin +gmailwebmaster +startweb +registros +specter +abdalla +postad.primediaautomotive +preschoolerparentingadmin +www.capa +www.archangel +www.smiley +sportscardsadmin +minorleagueballadmin +thienha +www.skynet +srvjumirim +nea +saudi1 +esx11 +arabcafe +stratus +srvlpta +gi2admin +alex2alex +vineet +szxy +weatheradmin +netsecurityadmin +english4all +prmpix +www.liberty +www.thereturn +sosnyt +vipboy +hhh01 +gi56admin +gi485admin +farmacia +www.muonline +vinicius +gustave +sv71 +billabong +teennewsgossipadmin +mifamilia +gi246admin +peloadmin +reparacionesadmin +famososadmin +pixelworld +www.terranova +seafight +www.samp +contratos +spread +nguyenvanha +melani444 +cupido +zyx +www.tablet +abeille +ftpdata +theateradmin +arabtube +retailadmin +postad.equisearch +proicehockeyadmin +www.terminal +johnpaul +www.capacitacion +3bnat +gi89admin +abdulla +mueblesadmin +gi363admin +latinfoodadmin +ga3datimes +bluestone +gi124admin +teachingadmin +maira +www.pablo +www.tecno +timmytimmy +arabstar +tnl +radioindustryadmin +www.sergio +qcc +metal13 +fdn +gi14admin +peaceandlove +childcareadmin +longevityadmin +entrepreneursadmin +www.toto +gi280admin +farpoint +sexygirl +www.integra +abubakar +orlandoadmin +bussines +enfermedadescorazonadmin +mynameis +climbingadmin +gi51admin +gi479admin +webdisk.management +luckypoem +privateschooladmin +mycache +immigsladmin +acuario +gi241admin +goirelandadmin +investingcanadaadmin +www.freezone +dreamsat +addictive +rvtraveladmin +gi297admin +www.zen +gonorthwestadmin +assistedlivingadmin +dixon +guddu +cvyrko +group01 +topgamer +nysshgateway1 +culinaryartsadmin +slisar +dosti +fotoalbum +bsosnyt +ba-reggane +writerexchangeadmin +gi357admin +dosug +boardgamesadmin +cyber1 +www.ssh +gi118admin +freesladmin +www.ruda +diginto +freeupload +musicaadmin +www.ankieta +tcmadmin +outsourcingadmin +webone +13579 +flagstaffadmin +www.msm +webtec +www.neo +mpendulo +www.mis +hotgirls +lawoffice +vidasanaadmin +www.min +lossimpsons +folkmusicadmin +teenhealthadmin +saveenergyadmin +redirecting +gi45admin +www.ito +gi474admin +collegegradjobsadmin +workathomemomsadmin +loinersa +weihua +vipxinh +ladiabetesadmin +friendshipadmin +ogw +mysqlread +www.imc +aminelove +abigail +weller +www.ima +www.jay +hip-step-stop +ynote +yokkaichi-kougai +genasite +withcom +manualidadesadmin +experimentosadmin +hontomo +irvingadmin +coolcrewpar +ndkrouso +www.hey +gi352admin +windows-remote +steam-community +www.hbt +gi113admin +www.gfp +algerstar +enclave +kapok +financialsoftadmin +www.gcm +zinfo +yumaadmin +www.emo +coldfluadmin +www.fas +gi361admin +www.dnt +www.dks +www.freetime +cuppycake +manhattanadmin +collegelifeadmin +reus +mrmehdi +dresci +hanabera +cleanmypc-serials +www.cmt +ranjoy +lenceriaadmin +shiro +usnewsadmin +usparksadmin +commando +www.daa +www.chm +apnetwork-forum +raj6 +free-money +gamekid +linsday +rosmawati +neworkut +www.cal +sanjiv +thedying +housewaresadmin +actividadesfamiliaadmin +progres +islamadmin +lediscret2006 +fadcav +fba +fantom +www.bet +gi39admin +gi468admin +www.bca +www.bbf +gi229admin +www.air +www.aic +studio2 +www.age +fungame +fahmed +frederik +www.kedr +besthotel +nininho +teachworld +www.tanya +logitech +83181928 +black11 +physicsadmin +optionsfutures +neurologyadmin +sweetlove +www.night +radionet +sosbos +farahdesign +aviationadmin +jacki +estateplanningadmin +grammaradmin +armagedon +gi140admin +imadmin +starter +frenchadmin +protestantismadmin +www.enrique +idtheftadmin +gi107admin +brunner +alternativefuelsadmin +freedomx +sosabt +lafinca +backandneckadmin +easteuropeanfoodadmin +fooddrinksladmin +maritimeadmin +gi9 +www.ingenieria +catalin +uff +jalsa +miraesto +stararabe +lasaguilas +gi34admin +www.mas +elecon +umi +gi7 +freejobs +gi463admin +gi4 +gi3 +www.reload +group13 +rikkoyt +gi2 +shadow77 +gi1 +diabetesandyouadmin +sabnamtusiba +islamna +gi224admin +syr +duhokz +afifinho +pavlosss +3dadmin +beatlesadmin +dinosaursadmin +chinni +bhavesh +aristoteles +hijosadmin +weddingtraditionsadmin +fbtips +marines +teenfashionadmin +tcw987654321 +dimitris +ie4search +alzahraa +sikhismadmin +databasesadmin +secure.horses +gi341admin +cs.m +unforgiven +gi102admin +palestine +test.game +gi442admin +clu +webdeveloper +alcoholsladmin +aboutdss +tef +babyclothesadmin +dupree +manmohan +dionys +isaksakl +tdi +gi193admin +enlosangelesadmin +husna +www.punk +gi409admin +goggle +coloradospringadmin +sjftp1 +incon +gi209admin +fannansat +gi28admin +tekken +www.metin +mala3eb +xaryte +gi457admin +www.raptor +waf7225 +contactos +artstyle +gi218admin +simpsonsadmin +javascriptadmin +gi459admin +enmiamiadmin +najm-arab +arquitecturaadmin +womeninbusinessadmin +madeira +phongthan +newhope +learningdisabilitiesadmin +www.pretty +ibscrohnsadmin +musicedadmin +puppiesadmin +pipelin +interiordecadmin +bluealgea +gi335admin +usedcarsadmin +bbtravel +homevideoadmin +marveluniverse +summertime +electronsladmin +music4life +kamlesh +selfhelpbooksadmin +crochetadmin +www.tests +hispanosadmin +lasvegasadmin +iclickadmin +www.lay +specialchildrenadmin +cvitky +gaylatinoadmin +pintura +bazi +gi23admin +daysofourlivesadmin +gi213admin +tahar +phx +d1000116 +cookingfortwoadmin +d1000138 +darkcode +souleater +welshcultureadmin +apes +yoursite +decoracionadmin +detodounpoco +nwr +enelcaribeadmin +nishant +www.androidtablet +pbr +asis +horsesadmin +ergonomicsadmin +www.paco +freegame +freefile +winzip-serialsdb +freedown +d1000150 +searchrank.guide +vista-crackdb +macrobioticadmin +freecash +evaluacion +www.blogspace +gorussiaadmin +mooncake +foodservice +boby +www.novi +pythonadmin +www.nota +computerzone +d1000182 +bowo +bigabout-ext +leyendasadmin +christiansladmin +longislandadmin +makeupadmin +incognita +i4u2 +mafioso +pianoadmin +gi17admin +gi446admin +telewest +www.angeles +gi197admin +jazzadmin +gi292admin +seattleadmin +gi324admin +fishcookingadmin +electricaladmin +darkgame +ecologyadmin +misadmin +ahmed2010 +gsreddy +homedepot +fashiontrendsadmin +juegosadmin +nahdd123 +hebrewadmin +www.rebel +swimmingadmin +specsportssladmin +blackheart +masearch +kidstvmoviesadmin +www.gamingzone +womensgolfadmin +starforum +shuzai.demo +vampireknight +www.phenom +jewelrymakingadmin +celularesadmin +kabir +nutritionadmin +kidspartiesadmin +gi12admin +gi441admin +gi202admin +gocaliforniaadmin +www.paraguay +quebeccityadmin +blast01 +dost +lim +roofingadmin +www.extrem +boiseadmin +lolipop +www.lia +martialartsadmin +bkorcan +fary +manisha +programsladmin +www.httpwww +default-search1 +ssbb +xango +accesoriosadmin +kashyap007 +paypallogin +secureyahoo +onlinegames123 +homestagingadmin +mafiahack +hotmailserver +shuzai.horses +www.anarchy +www.zim +gi318admin +www.emerald +desktoppubadmin +victoriaadmin +sahiwal +estudiantes +www.ecrc +www.callofduty +directv +angelic +directo +wholesalersadmin +webdisk.manage +autoconfig.manage +inteligencia +auth-smtp.vmail +backtoschooladmin +rafaeloliveira +gi435admin +ums-auth +curbas +jmd +smtp.vmail +gi186admin +nascaradmin +lucifer666 +vinayak +newcurbas +auth-smtp +rsiadmin +abhisek +www.manitoba +glutenfreecookingadmin +frenchsladmin +vatex +www.vanessa +harm +testsecure +hydroponicsadmin +www.robert +gmbm +gi134admin +automax +ncstest +gi313admin +perladmin +fmso +cwlounge +chronist +korsan +artesanos +www.navarro +techwritingadmin +www.petrozavodsk +kafa +2609_n_www +anh-m +vetmedicineadmin +androidtablet +0907_n_hn.m +bk.sukien +netcultureadmin +cron02 +vipersky +cron01 +mohsin +bk.mst +eshwar +ibro +code.m +kalp +gi430admin +sukien2 +sukien3 +guys +blackstage +mgj +0507_n_hn +www.sukien +wpi +playfreegames +modeltrainsadmin +blingee +gi181admin +gamesforall +orientation-forum +govcareersadmin +technologies +vm104 +vegetalesadmin +sakblog +lomejor +bronxadmin +dbtest-scan +rosesadmin +joule +englandneadmin +55545082 +dinamic +gendbtest-scan +gi400admin +sakeena +erpdbprod-scan +terabyte +vaughn +askjeeves +eship +gi189admin +shuzai.collectdolls +worldnewsadmin +crusher +jhon +www.sanantonio +rugsandcarpetsadmin +jiko +gatika +gi307admin +alcoholismadmin +pregnancyadmin +dwdbtest-scan +oficinavirtual +www.paintball +dbprod-scan +ebrahem +gendbprod-scan +fta +cinemania +americanpie +christianteensadmin +londonadmin +aryan123456 +diabetessladmin +backtoschool +environmental +erpdbtest-scan +loggingadmin +gi424admin +gi175admin +gar +websearchadmin +losangelesadmin +fgs +gosouthasiaadmin +sbatimes +vacationhomesadmin +primesearch +gamblesladmin +www.element +torontoadmin +gi302admin +ravi1234 +voipadmin +www.independent +xinxin +saltaquariumadmin +dwdbprod-scan +napalm +emf +renotahoeadmin +marinos +santabarbaraadmin +yahoo9 +donations +hollywoodmovieadmin +redessocialesadmin +gi418admin +meenakshi +gi169admin +gi500admin +markyie +carinsuranceadmin +dirk +dheeraj +ap9 +civilengineeradmin +whatismyip +bengalicultureadmin +gi96admin +sanyi007 +www.mig +www.switch +mady +www.strike +gi286admin +theroseanneshow +ibdcrohnsadmin +atheismsladmin +lizzard +dll +na20 +torchwood +portuguesefoodadmin +shakespeareadmin +triton.dis +vipadmin +golosangelesadmin +conspiraciesadmin +suvsadmin +yanuar +windowssladmin +purelife +cpv +gi413admin +gi164admin +www.wallpaper +gossipadmin +turbo2 +yardim +martina +cla +matisse +remediosnaturalesadmin +tejeradmin +lifemadeeasyadmin +birminghamaladmin +yaseen +aprenderinternetadmin +pcbiblio +aleman +cuisine +slingshot +newjil +yasmin +kapre +cmm +yassin +crearte +eatingdisordersadmin +gi91admin +kriminal +starcom +starfes +swimwearadmin +www.descargar +gi281admin +starlik +dermatologyadmin +divorcesladmin +comidamexicanaadmin +sj.js.get +www.gomel +starsat +www.cristianoronaldo +admin.flyfishing +personalinsureadmin +bbhealth2 +gi409 +jewelryadmin +logisticsadmin +lrss.team +cukerko +clcs +cyberdemon +familybusinessadmin +sultan +gohawaiiadmin +stringer +menshairadmin +dvredit-serials +gi407admin +gi158admin +foodpreservationadmin +www.paul +are +deltaforce +thaifoodadmin +www.thefamily +mudy +mobilegamesadmin +www.nutrition +www.neobux +gocaribbeanadmin +mangaadmin +allexperts +sanfour +nehemiah +canadamusicadmin +suncewap +moooon +sanjeet +sanjeev +gi85admin +gi275admin +paintingadmin +iadmin.pmy +flashmania +ceramica +randomstuff +scottishcultureadmin +akis +softweb +gi349admin +guidepolladmin +horrorbooksadmin +encolombiaadmin +theend +greenlivingadmin +cukorki +textil +insideprimedia-forum +gi402admin +petsuppliesadmin +mandawe +compreviewsadmin +www.bullying +gi153admin +osly +mubaraq +futboladmin +ludia8 +news906 +resinok +global99 +chelseaprany +oldiesadmin +3dbangla +metin2forum +junje +smusic +garibaldi +ropaninosadmin +pesca +gotexasadmin +volume +theempire +cigarsladmin +pittsburghadmin +koronful +baileadmin +lobbyadmin +paranormaladmin +trucksadmin +gi80admin +www.bandits +uniqroom +marinesby +computersladmin +www.lovers +fororo +gi270admin +sunnysk69 +babylon5admin +shopmanual +scw1025 +freshchan +bkk730 +jolifemme +zen88282 +zen88281 +depresionadmin +karacoco5 +sqs123 +mrcha321 +nataraza +ymslhs1 +mamalatinaadmin +gun0216 +origingeoje +86236 +miinstory11 +houseplus +designtoken +ngelpc1 +candy9 +crown9022 +topcook5 +topcook2 +kym5470 +backtoschoolfashionadmin +todayfeel +socee2011 +janghyuk18 +sungiu1 +sunghwa +hsj8441 +top40admin +dcheroes +poembaseball +edutige +velofltr0443 +jhonatan +ihcorp +cypaper +godoshop000-010 +zeusbsj2 +lanos4153 +navazo +audwk991 +www.midgard +youinn41 +jaworld +mklee0982 +hytelecom +fkdlagkfmxm +dorikorea +colemangear +hpixtr6886 +leadersway +junggotr7791 +winshade +yj3300 +aramistr5696 +www.tnp +gamistyle1 +reve12 +bstjoeun-019 +ilikeshop002ptn +caselogic +mmix6 +shoenettr +bstjoeun-013 +heyjune1 +bstjoeun-012 +boulderadmin +yangposs +woodstory +bstjoeun-010 +lsszzi +highsora +ihdeco +lux4u +bstjoeun-005 +akwlswn +byplekorea +daisyv4 +jinyuk001 +sksk10011 +bstjoeun-001 +drjungletr +bth3804 +sugarcare1 +npshoptr9027 +smj6242 +sevenwell +kseongbuk +cho3927 +ihkim2000 +jolrida1 +ms4747 +mcfoods1 +balltop +jwellday +www.tkd +kobaccotr +fbiscout +queenz67 +daibokorea +th48500 +toggi02 +ribbonshopv4 +bbobbodi1 +suhojjang1 +multiplesadmin +lhy5363 +bombom2124 +myshop-030 +exclu1 +eclock +nzonbf +myshop-023 +inwoocomm +dlqnrnr4 +myshop-020 +wendt30 +creator1141 +nator1 +mukuk9tr6244 +enamoofix +ceoyaa +kjunggu +kgsmook4 +kgsmook3 +kgsmook1 +lee73772 +realtitr9751 +kohjeondo2 +hunetdong +locohouse1 +park6742 +onblog +artvus +eugeneph +yhdiva56 +seuumcom1 +knovita1 +sinhongagu +artwiz +banyflat1 +marinpet +mesosuk1 +fireird16 +fireird11 +jasaengdang1 +ruawhk119 +sice328 +music1042 +bj10031 +megaphone2 +moses129 +pyj12101 +ebule1 +thsdkgus +enfi2389 +girlngirls +wowman2 +foruzone1 +green2902 +barolife1 +ahemskiss1 +horseracingadmin +polomonster +coffeegsc4 +coffeegsc3 +coffeegsc1 +ptypty1 +ksk77762 +www.jackass +rfc53403 +car3921 +kjh5640 +www.rpm +www.saa +bungalow +collectpinsadmin +remy +mathavang +yahata +augustagaadmin +jukstory +daisseo +gi386admin +yetiman1 +calldo +www.pmm +balmers +muse8119 +rlatkdtr1002 +mlcast +dpoint3 +s1devkthkira +newlight46 +artryx +hkent12273 +son7446 +ggstory4 +ellistar +pauly842 +pauly841 +cho3234 +soyea0529 +gion716 +dalmados +cashpricedn +gi147admin +us3acid +zzambbang +dakbam33 +parkyh +xkrrod +dkssud588 +colicehockeyadmin +cho3146 +nousa971 +tiamo1 +rmfpdlq1 +sangt01 +x86x +resolutionsadmin +www.pcs +www.nsn +www.nox +takamura +vlounge-forum +sano +www.ngw +vip90 +alrong11 +jehyeub85 +www.nba +sih7811 +hejgirl +ksgo7263 +ksgo7262 +olspecta1 +hosuko +edu50 +dryad8221 +edu45 +aspera75 +jjboaba +hbnow1001 +kjbird +aruih2 +chorokseum +cas012 +kpham0503 +edu39 +winnipegadmin +hpstar2001 +ttbehan +jrjrjr +pomipomi +edu29 +anyweb002ptn +ilovejoo2 +yagooshoptr +golilaking1 +tameus2 +kikifs +wonu27 +noble0730 +edu19 +rhdrptns +artjugg +airsense1 +jb97091 +biglocust1 +chunbe87 +lovestarlit +mkjohn +icysniper5 +pil1001 +pom50231 +dalky123 +www.low +minpower +encittr +carrymtr2154 +daol0778 +ilmare79 +january11662 +leemj71 +oh19552 +sanfran +m63200 +petitptr0838 +viscon4 +viscon2 +megafish1 +blindplus1 +tkk017 +rirosystem +donakaran77 +nblt2 +youinn4 +threeboard4 +threeboard2 +threeboard1 +artkyu +kkum77777 +dmarket +tekken986 +toolmt09 +calbin6 +calbin5 +calbin2 +sogangtr0438 +giogiaa +hosoon +jazziscool +jhngyu11152 +tgdometr2527 +swood33 +leemh77 +thinkerk +luci2 +luci1 +namublind +bankmatch +leonaeyo2 +wainat +memorykitv4 +bisto01 +mooas09 +arai1103 +anonima123 +airsense +jardinadmin +heavearth2 +luvbean1 +paulrhim +artkit +jhngyu1115 +chinadesign +gcsd3314 +gcsd3311 +gcsd3310 +rlatkdrjf119 +crabman2 +gomnfood1 +pacu4ng1 +t2rtr7289 +majunil3 +xowls1 +gkstoawltoa +kimys861 +adream4u +youilemv +leearm0217 +fkgt19801 +heret43 +heret42 +copdadmin +kitlabtr7995 +bbhobbies +bcuwkorealtd +takwon2 +ncbank1 +asr122 +arirang01732 +yoofan4 +marketingengine +pure081 +rosejang110 +peanutsco +crowhell5 +crowhell3 +canon7813 +crowhell1 +uyacco692 +cho2024 +bnutopia1 +nabiritr9900 +comicztr9477 +decoline1 +edpchair +omion1 +wkdtn3007 +pjb9162 +pjb9161 +uhhng01 +chung131 +join09151 +woodkid +spsh79 +anyweb001ptn +jirisnnm +lesepy34 +sejin577004ptn +loakekorea1 +tig234 +newd +chonm9122 +biopioneer4 +qkrrudcjf +huencos +azh1207001ptn +gnnew5425 +naroit +hotelarv +nambookmusic +jjutwo1 +fashionsmctr +lavenders +narodo +day12312 +lovemomo82 +wonmee +cacaoharu2 +ercedutr +zone19741 +saru +www.mat +otinane +gi74admin +pricedn2 +njy1281 +gulbia1 +ropdacom +pyhee741 +adream4u2 +adream4u1 +kmbabara +smartknife +honey3139 +redhotman +ipayrosthill +realdeep1 +www.mao +piosbike +yuow7531 +tarifa +gotoday +desecret +ladymama +cakee1 +mrtelecom +gi198admin +tsgim7tr7930 +dorothymalltr +gi264admin +kns10302 +cz0138tr9701 +youguy2 +youguy1 +demobb +duke356 +donghwasys +mkart +artbom +ihammer +derkuss07063 +kwk2381 +decolight +operassi1 +free1261262 +miss2 +luxury50491 +bumhee147 +to0622 +baliya2 +designstory +nsd0290 +miz01 +artand +wmbaldy +miraeppa +supremacy1 +myeston8 +nattskin11 +thing95 +bootintr8750 +hellojungwoo2 +leesum01011 +hellojungwoo1 +leh01091 +hy45122 +walidos +steiner4869 +shine10262 +osooso3 +kimsontr9280 +mggarden +nahri +woman4u3 +goodkim20042 +goodkim20041 +dodo6699 +sanga89 +jts3200 +bbworksmaste4 +hoscnn +johnonline +hongjamong +sewingateliertr +bulrogeon +shoptr1430 +booriboori +dokyngo2 +dokyngo1 +gwon10082 +e-weddingcar +bbocksil +sky486ym1 +nsd130529 +kikass1 +blogshop1 +wsyoun +morning6 +nfarmer +repuni +atomicsnow4 +ckh0630 +atomicsnow2 +atomicsnow1 +posiinc00 +nzlandshop +raraaqua +jemmaroh91 +pokeuni +sejin577003ptn +asiooy +2bbu2 +very0421 +scubaom1 +gsgtel-010 +decorativeartsadmin +tuntunkids1 +mimyu +mandulgo +bom1004 +leeks10072 +artima6 +mint3 +artima3 +mei12131 +enamoodemo +kornesia +jikr771 +mhvsg +night28 +burstbany +col0101 +goldonsmog6 +oneorzero17 +oneorzero12 +oneorzero11 +oneorzero10 +wjdgml21 +s4edu +kt200505 +lseed +hotwjddus +mini0 +mulangsa +lhy1984 +ej1378 +sw8901 +ribbonvalley +michabella +horie1 +lsz1022 +nabut +shesplus +big1301 +hana5249 +bodyya3 +rkdrn2091 +kama1122 +k198432 +dgamdong001ptn +youngadultsadmin +toilettr4563 +ch29952 +gi381admin +euorganic +bbbseul +ipaymall +say24112 +anttelecom +hoopcitr6821 +campingfirst2 +campingfirst1 +modernagecut +didmontr2712 +natalri4 +natalri3 +lemonfish +acebless2 +mijkr +geo821 +artgyp2 +ebonghwa +futuremediatr +roby19772 +roby19771 +pyw36582 +bodyup6 +bodyup2 +toysun2 +hwan5855 +newjjang +junsic-029 +junsic-028 +junsic-027 +junsic-026 +junsic-025 +junsic-024 +innerweb4 +innerweb3 +www.balance +finefamily +innerweb1 +any49526 +junsic-017 +junsic-016 +junsic-015 +junsic-014 +junsic-013 +junsic-012 +junsic-011 +kims18418 +junsic-008 +junsic-007 +cooljoon2 +junsic-005 +junsic-004 +junsic-003 +biggolf1 +junsic-001 +denis119 +denis118 +denis117 +denis116 +www.jas +denis115 +denis114 +denis111 +onky5346 +imex771004 +cho0123 +smzluv +gog5202 +sj112911 +mapline4 +jy0222 +stylesock +cok8370 +gaegoory1 +jabi8874 +kongstyle15 +kimssang7775 +kongstyle13 +kblue08 +ch1005 +kblue05 +kblue01 +thddl8666 +lovelyone7 +es3free +highones +view21001 +dlient +nordkap8 +iloveherb +how4u4 +how4u1 +april20 +sejin577002ptn +tmddus5411 +je0224 +hwan5487 +popcorntreetr +spoutltr6391 +icon21phil +nacodory +blueskyym1 +s3freeintw +demping +s3freeintp +s3freeintb +nakis2 +ciplatform1 +cross56221 +thfdbxhd1 +denveradmin +touchdog3 +ipayjwmall3 +miega +iorizia +s3edu +cho2001s +pensarangtr +lifelink6 +htmladmin +lifelink4 +lifelink3 +lifelink1 +kama0242 +saddog74 +onagana +wigdesigner3 +s4freedevkhs +romeojang +ch0559 +monic01 +flyte2 +mya000001ptn +jackmen +gi20admin +lovehouse3651 +dasan247001ptn +binilatr1277 +kim9hs +daenong1 +leeji2k +mangonamu +ppunia2 +finelbs5 +sismedia +finelbs3 +finelbs2 +okgolf1 +swpaper +sandfox +madeinreal +semicon21 +pelletcamp +gosouthamericaadmin +nicepick +best12295 +best12294 +sakeimtr9141 +cocosribon +ipayeve58153 +taewoo32022 +dhrwngmll +sketch +luna7658 +stwood1 +miapc +abbishop +a01086700679 +s3devsky +softlon +ryoocs +kjbaek2 +soekaldi5 +soekaldi1 +fleamarketadmin +rhotcool +zktmxkem122 +herman77 +jonejmama1 +kghjl79 +skymap1128 +ilovemusic3 +demomt2 +fairy001 +enjoymall4 +enjoymall3 +backup89 +backup88 +backup84 +backup83 +backup82 +nesteggz +backup68 +backup67 +backup66 +www.ess +backup64 +backup63 +backup62 +backup61 +backup59 +backup58 +lovelyand4 +backup56 +www.fer +backup54 +backup52 +backup51 +intorock11 +hmsolution +another0 +redstars4004 +www.fdc +baksa77 +sktworld2 +sktworld1 +pdfox4 +jates2121 +allip600 +qutjin60 +tree9613 +buja49483 +minovia1 +spolex +aaasss84 +demor44 +wanggung +sl1238tr2669 +sh3123015 +deplant1 +swch4040 +rlawndo613 +kyw01 +godqhrtoa +s2fqa +icarus89 +natur331 +sejin577001ptn +godo23125 +superhoya1 +rentop +gracex82 +hairtoo3 +newyorker9 +ribbonarts +daein69414 +kingdisplay +soyaco +twoace1 +chgoods +wyh1015 +directmall +yangok331 +rkdrudtjrs +emlifetr +theo06182 +theo06181 +aznymohc009ptn +www.fca +mycom84 +ginseng2000 +downie3 +yukinongup +ksjbank2 +offman21 +offman20 +offman18 +ryu858 +www.cup +thepnk +sensrect2 +www.diy +flashram +sulry20 +s3devman +ololaa +drmartens +jsh0727 +kyjzz +adagioepiano +varam089 +interfaith +godomall-059 +godomall-058 +godomall-057 +godomall-056 +godomall-055 +godomall-054 +godomall-053 +godomall-052 +godomall-051 +godomall-049 +godomall-048 +godomall-047 +godomall-046 +godomall-045 +godomall-044 +godomall-043 +godomall-042 +godomall-041 +godomall-040 +godomall-038 +godomall-037 +godomall-036 +godomall-035 +godomall-034 +godomall-033 +godomall-032 +godomall-031 +godomall-029 +godomall-028 +godomall-027 +godomall-026 +godomall-025 +godomall-024 +godomall-023 +godomall-022 +godomall-021 +godomall-019 +godomall-018 +godomall-017 +godomall-016 +godomall-015 +godomall-014 +godomall-013 +godomall-012 +godomall-011 +godomall-009 +godomall-008 +godomall-007 +godomall-006 +nanum2 +godomall-004 +godomall-003 +godomall-002 +godomall-001 +s3devkhs +nuny78 +parisfrance +gongze11 +reon2k +usemix +aramseosan +kangs2445 +can337 +godoa3-030 +godoa3-028 +godoa3-027 +godoa3-026 +etiquetteadmin +www.eaa +firstchoice +e-store +www214 +ldschristmasadmin +gi199admin +rola +epilepsyadmin +deportesadmin +shashwat +shuzai.sewing +www.cee +wertex +gi68admin +gi497admin +ldsadmin +godoa3-025 +godoa3-024 +godoa3-023 +godoa3-022 +godoa3-021 +godoa3-019 +godoa3-018 +godoa3-017 +godoa3-016 +godoa3-015 +godoa3-014 +godoa3-013 +godoa3-012 +godoa3-011 +godoa3-009 +godoa3-008 +godoa3-007 +godoa3-006 +ryu18077 +godoa3-004 +godoa3-003 +godoa3-002 +godoa3-001 +jdoutlet +wanggolf +artform +euncho2 +tofino1 +comfs1004 +mixjs1 +bornstreet1 +yujane21 +qkralwjd94 +heykeung +godohomez +momv230 +hsj1993 +ybmtb1 +pequalno1 +arablionz +lilybebe +sigmini1 +arimaltr6888 +skypjhek +kperpect2 +y0603791 +cdcomco +dr7799 +qq14121 +toysale +andynaudrey +papassun3 +sw6385 +goodbutton +bestshop221 +dlaehd1234 +gi258admin +sbmaster-010 +jsh0258 +enostyle +vldals1231 +syprime +zinsol1 +ezcominc +scrapbookingadmin +abdou474 +everydaybeautyadmin +familyfitnessadmin +palmspringsadmin +www.amd +radiovip +farmingadmin +sitech +canadaonlineadmin +sani336 +frends +cumurki +www.infamous +sbinformationadmin +bcom +miniaturesadmin +giggle +them +racquetadmin +gi375admin +tibs +koraa +certificationadmin +gi136admin +angelsadmin +inlineskatingadmin +blablabla +loveforever +internetradioadmin +subas +totalsport +adithya +homebasicsadmin +moulay +sportscareersadmin +www.estilo +sune +designsladmin +toldmeher +petroleumadmin +gi8admin +gi63admin +gi492admin +sura +www.llamas +frames +militaryfamilyadmin +habibo +kidmoneyadmin +hack15 +hack33 +musiciansadmin +infamouz +teensadmin +djims333 +scifimoviesadmin +stocksadmin +bpdadmin +enbrasiladmin +funpower +megabyte +gi370admin +sasanka +soapssladmin +european +gi131admin +www.michael +hotmailcom +www.facebok +collectdollsadmin +starlines2 +akermoune +holidaysadmin +allergiesadmin +charlotteadmin +saiko +landscapingadmin +mbx +gi3admin +adamadmin +cycles +economicsadmin +goatlantaadmin +wara +ravinder +restaurantes +registered +cstrikes +freelancewriteadmin +gi57admin +hammas +vintageclothingadmin +gi247admin +webo +hamood +silverboy +swsladmin +hanlin +hannan +k-6educatorsadmin +www.magnet +internshipsadmin +parquesdediversionadmin +sarthak +frederictonadmin +ny.js.get +snowrides +www.pizza +portlandoradmin +www.stephanie +tampaadmin +easylife +15minutefashionadmin +charlottesvill +www.consultant +mazika0 +tvcomedyadmin +www.papillon +triathlonadmin +www.lasvegas +nom +raviteja +teenlifesladmin +ivalice +marketresearchadmin +www.faceboook +gosouthwestadmin +www.tbt +zerarda2008 +womenshistoryadmin +mcspecial1 +carrerasadmin +gi364admin +gi125admin +hassen +bullying +saludinfantiladmin +carsadmin +m4trix +www.metin2 +hdvideo +elyogaadmin +usnan +nailsadmin +chandrasekhar +sanantonioadmin +softwaredevadmin +www.vg +comediansadmin +ximo +mita +www.rk +snowboardingadmin +admin99 +healthsladmin +weaponsadmin +mauro +flooringadmin +www.oa +marouane +startimes333 +ablistadmin +kikopolo +gi52admin +mayas +viprasys +gi481admin +moneyover55admin +quiltingadmin +gi242admin +saobang +greennet +fonari +husam +kudanil +ablist +coolbuddy +animeadmin +crimeadmin +bulldogs +gi119admin +www.blueteam +haven.team +dutchfoodadmin +pharmacyadmin +zalizo +androidapp +organicadmin +brooklynadmin +gi170admin +homerenovationsadmin +hilaryduff +volleyballadmin +gi46admin +gi475admin +gi236admin +ladygaga +seasianfoodadmin +shortstoriesadmin +kutta +magicshop +gi380admin +history1800sadmin +nhan +zoey +cybermafia +meera +masterhost +www.programas +xstone +drjohn +gi499 +yourphotos +gi498 +gi497 +gi496 +tutut +clarence +kollywood +gi495 +gi494 +www.streetart +taj +flashart +gi493 +www.dulce +vijaykumar +gi492 +demonic +gi491 +retal +lumberjack +www.clima +gi489 +gi488 +gi487 +godwin +muslims +gi486 +gogeta +gi485 +fworld +gi484 +sani335 +hwan3592 +sbmaster-003 +rebels +pgc +sbmaster-002 +guess18 +sociales +iconbay1 +ppakuns +nariswater +gi483 +gi482 +gi481 +gi479 +subhadeep +mav +www.jordan +gi478 +tcenter1 +enter3854 +aa1 +sksdhkdtn +dslgstr8532 +volky2006ptn +pinksuger1 +infsch2 +infsch1 +sesintsunny +eggstar1 +wellbeingtowel +dksro2454 +qodtns +hamdang1 +dasom77352 +regalos +fengshuiadmin +gi475 +mule +hackeriraq +gonzalez +golane +www.boris +gi473 +gi472 +gi471 +afn +salma +facebuk +googel +gi469 +gi468 +www.hotline +comptech +chatworld +gi467 +gi466 +gi465 +animeonline +gi464 +virtualcity +gi463 +gi462 +www.fox +jack-aceh +www.pokemon +gossip +mp109 +contenido +gi461 +ccf +clemente +gi459 +www.congress +www.tokiohotel +gi458 +gi457 +gi456 +images.b +mobiclub +gi455 +www.theotherside +gi454 +delivery.o +gi453 +gi452 +gi451 +dalia +gi449 +yogesh +marukima +badboy123 +gi448 +gbt +gi447 +doheejjang +a27974844 +kbm77005 +kbm77002 +kifid2 +mygaras2 +blue1192 +www.kankan +myepicase +sjlock11 +pascal752 +pascal751 +flyant +imarketing016ptn +biomta +zzukppang +boss7628 +gosteam +long4 +sunyaro4 +four321 +namuwa +seechans +d0tb1t +zzubong +sodom1982 +dns51-4 +dns51-3 +hbmart07041 +baesilri2 +baesilri1 +godo21667 +hj2000kk +green12671 +kwil7191 +biomam +brozdist +delete01263 +hsdacam +arome1 +reflexkorea +ideaz021 +dns50-4 +gi446 +seralee +han92501 +gi445 +academyshop +aroma4 +aroma3 +skyonemoon8 +aseva1 +www.tobi +gi444 +zaengyi +momodd1 +samjogo +hwan3049 +nari230410 +dportal +lovesuho89 +namph30 +doshirac +sehooni1 +vit424 +namph21 +merot +red4sky +namph20 +smartdoc +allthatkid1 +thednd +nuno12 +eastiger75 +ninetyg2 +welpia2 +welpia1 +indishoptr +gsv +plam +gi443 +anhquan +kidsss +dailymi +byonce5 +pen2011071tr +loveintr0102 +godo21354 +cakent1 +fone5117 +diso98381 +mm045 +mantralight +artenis +wkaxld00002ptn +halla4529 +okfishtr6461 +enicostr1 +gpsauto1 +quickbattery +lohft +pcmpcn +sogjg86 +tree7584 +ssizoo1 +issac001 +juni10981 +godo21215 +manyo331 +queen6c3 +oofbird4 +oofbird3 +oofbird2 +whitemoon951 +gi441 +plusgajun +gi439 +khy8166 +hejan85 +ssiznet +livelocks3 +renbow +amore1111 +yesyakim71 +thevassi1 +namph9 +namph8 +namph5 +namph4 +namph3 +namph2 +namph1 +wonphu2013 +wkseoul +jwj15414 +tranquan +jwj15412 +chyra521 +elsm5101 +eurodirect6 +gi438 +giftodaymart +supersim +yvespotr3947 +zabes072 +cocovenni +naxpungtr4570 +varun089 +upperlady3 +upperlady2 +rmfjadpeh12 +gi437 +samiri1 +supersg3 +starceo +gi436 +jego114 +gi435 +coqueterra +mnbnm52 +lodee +cafertr +gagsital11 +saeromedu +yooa47753 +sirbanny +yms39401 +ckwlgh122 +fristar1 +caselogicshoptr +lovestory2 +onstore +ledstyle +kbs8303 +hwangtosum +jeanmania +cjymsms2 +haepal79 +ohohoh55 +wjdghks6 +hyde0228 +gymboreei +i16322 +jaypark4 +jaypark1 +poohaha21c1 +hoangdeptrai +www.rock +jm3 +aroma0063 +hanna2012 +hwan2013 +synergykorea +artegio +ebedding +hana0924 +sungje +jungjm49891 +runa0401 +summary +pursevalley1 +morffstyle +cleansafe +gi434 +hautegallery +spy007m +sunjinpet +adev167 +khmkjt +youtubee +clubmobile +www.korean +gi433 +plus12193 +naa +cuongth2009 +www.rage +jakob +gi432 +www.antonio +gi431 +gi429 +jrn +yasmina +gi428 +gi427 +cabalph +red-dragon +www.telefon +gi426 +www.searchengineoptimization +gi425 +www.amira +gi424 +gi423 +gi422 +gi421 +www.jersey +jlcorea +honeycom143 +hemohealth +pamikyung +rcrace +wabtel +fishingmetro1 +ipaysupplyurs10 +junarian1 +livingsens1 +wkaxld00001ptn +kjtop41 +gi419 +hoshino +saygolf +bonaebada +sanai81 +yangil23 +ouranus +sanai57 +dominoland +gi418 +gi417 +harvardmarine +www.nick +gi416 +salmankhan +ms146 +gi415 +llbejll +vempee3 +waltz00204 +chj84291 +www.mona +dsharp1 +wellhouse +moms911 +psd10022 +psd10021 +bbcareers +purefarm20111 +parsley1 +wndus2422 +imagejan4 +sang230 +theiluvi +newinggo +jagex +dasanbooks +worlddigital +wsw10254 +wsw10252 +splink +gurm0001 +ajjvsl7 +raise +gi413 +exlife +cjhwa86 +neobob3 +mcommuni +neobob2 +sang115 +linkhouse +newdept +happygrim73 +happygrim72 +gi412 +www.impact +gi411 +bluebook +gi399 +himura +gi408 +blink182 +albin +pinpin +www.levi +gi407 +happygrim71 +unionflower +allegrouz +eunbeo3o +jmeatman +gsharpmall +mikanginc +ssjoun1 +cjws2000 +opusone +honbeelsh +client792 +client791 +gi396 +geolay1 +adoresun1 +khumalltr +ctnara +sweeteeleng +yung3651 +flux91 +bmdcorp +everydaygreen +eoasise +gi394 +gi393 +alameda +reflections +group2 +tana +gi402 +p1r1 +cybertricks +marquee +hisham +gotoday2 +dbenamoo +jks7292 +gi391 +erlandsen +yooriapa20 +yooriapa16 +yooriapa15 +yooriapa13 +gi390 +ahmedasem +moriarty +gi388 +gi387 +allamerican +nutting +xyz123 +gi386 +chewy +gi385 +gi384 +clothes +optica +stararab +asadullah +starcasa +gi383 +samirbba +tfm +gi382 +harshita +starcom2 +schulte +2rbine +stardoll +tja +yooriapa11 +jjh7457 +phenom +gi381 +gi379 +rhorse58 +song2000991 +hohs6870 +gi378 +gi377 +remates +cibertec +stargirl +cinkabene +gi376 +fasebook +stylentr0015 +gi375 +gi373 +gi372 +mau +gi371 +woaini +gi369 +gi368 +gi367 +starkora +gi114admin +gi365 +eartprint1 +zerowox1 +esmailzadeh +infopia +horoskope +blackie +gi364 +starnet2 +3arabforest +gi363 +gi362 +gi361 +gi359 +zzinga7777 +hit0043 +testgodo-003 +testgodo-002 +testgodo-001 +motorplus +mk211 +cadkdy +homeic +nobelkorea1 +oksysy +kangkosy1 +theseatr5545 +digilog1 +runescape3-beta +geosungnc4 +geosungnc3 +jcy80801 +knan4053 +moltz13 +witharbina +jentcosm1 +mnbmato +soundforum1 +innohouse +danmist1 +cjmarttr +bumbi1 +samohago +gi358 +geddoi +marusol +hikang93 +whb +jshak1012 +tkfdkdltsp +weddingnbaby1 +bomool10141 +petsbtr5164 +furnipeople +baleda2 +rkatkatjd3 +khs106 +lloom +bymommaster +promaltr8853 +khlife +thatbe +dh13571 +matjoeun +snowcathome +bysummer +nalgae +olleh1 +atmanhouse +zi9 +uiseok4 +godo17925 +medcos2 +shira81 +yatene +yespump2 +yijungah1 +oizang3 +nanogolf +kissthehydra +rauschtr1027 +sshousing +bigbangt4 +t2002kr1 +gi357 +mjkt84 +aznymohc005ptn +golfmax1 +yonggary311 +meditotr1586 +billyb +vou +hyundai-039 +kimjinryeol +gi356 +bnutopia +jeeyae1 +taebancosmetic +ltlkorea +sofoom1 +koolz18 +koolz15 +unixmart1 +www.iq +hyundai-032 +staristr8183 +hyundai-029 +vmulti2 +sw2803 +ieonet20131 +qqqqq7600 +tourkorea +f1tr7745 +eternal0424 +infoic2 +gcsd33012ptn +hyundai-019 +sosomm +hyundai-018 +jungmi803 +intopkorea +aidiishop +monavan +sonaten231 +hyundai-009 +sosom4 +marketing11 +totaltrade +www.jd +hyundai-006 +marumoa +yein5151 +gi355 +snailsp +bimax1 +marumir +hka7898 +kbvintage +motor629 +mcumart +dodam16 +hori5000 +jjungyk +dlwlstn12343 +hosikstyle +emtmaster1 +artbom5 +artbom3 +yonsuart97 +dktak +nalabi +gi354 +ddalki011 +wikitetr0769 +gi353 +jazz2you1 +gi352 +carsm5252 +mbri1 +ggamsiya +mis0142tr +viiv6153 +gi351 +eros10921 +godo17272 +mydaisy +bori25603 +ggamtan3 +eum9960321 +tjdwns092 +gi349 +min3584 +so17702 +seolleim +motibluetr +page2940 +delskin +mkara1 +yjh61281 +wooridream2 +gi348 +seong9557 +samhang +vitamitr3086 +akddong11 +outdoorlook5 +shinwha +misomo +narsha67 +infocad +s3freedevw +stylesaysmart +s3freedevp +roradress +s3freedevb +gi347 +unistarlp +maxi9 +venisarmy1 +extra44 +wintop253 +wintop252 +wintop251 +toytoylego +pka +fluaos +mondoudou1 +misoap +gi346 +rjmhouse1 +gi345 +cuteysoo +aznymohc004ptn +attic831 +dream12451 +gi344 +sgsgcbs +youal12 +msraion +taddyseo +newdctour +artbike +livingquilt +miss64 +arti112 +ecoliebe +nzlandtr4975 +shinter +mds3515 +vitamitr2648 +gi343 +gi342 +ajkzz4292 +ajkzz4291 +pnbhfood +bel13941 +ipaysupplyurs9 +ipaysupplyurs8 +smartcs4 +gaonnara +parkinn +a2amanager3 +gi341 +r4tt1 +naturaltown6 +wkdrnthd12 +cubist +upflykorea3 +upflykorea2 +datanlogic +flue87 +www.4u +ujini11 +pakch042 +pakch041 +darkrookie78 +designsol +zing212 +mirae021 +shiny02 +marue +polaris321 +sam840711 +designsmc +yoursea +mirtel +www.3e +pazzu1 +gi340 +www.partypoker +designtag +sd08051 +yurian +ksroh +ebizs1 +zerozin +mirz02 +sunwooland +michellekor +chaeeunabba2 +anirudh +aquan1 +maqua +misit5 +gi338 +misit4 +arabicsoft +zioips2 +dwkorea3 +okidoki +h10516156 +gi337 +gi335 +thaddy +jjang98 +miso99 +uriiya +tera14391 +k320sh +amecano +ramosu3 +maummind1 +tjdgml8004 +gi334 +mouad +gi397admin +gi332 +informate +gi331 +register1 +gi328 +foxhound +gi327 +gi326 +lupusadmin +aquatech +startime +gi324 +reflect +gi323 +startoon +gi322 +gunjan +gi321 +www.aras +gi319 +gi318 +lakshman +www.dta +addictionsadmin +emotions +gi316 +gi315 +gi314 +www.rebels +jengkoil +canadian +december +gi313 +gi312 +gi311 +freechips +powerfull +aloevera +gi299 +famillypower +gi297 +my123 +conquerors +firewolf +tigertiger +profesor +freeleech +zlatko +hostweb +www.restaurant +www.douglas +gi296 +one101 +officefile +gi295 +idrees +gi294 +freewallpapers +studies +gi293 +suleman +gi292 +gi291 +gi289 +bodexdas +qa.myportal +gi288 +www.rcm +gi286 +gi285 +yulong +www.royal +gi284 +gi283 +gi282 +gi281 +kakalot +shareblog +mudit +gi279 +gi278 +musicfa +coolpages +gi277 +gi275 +gi274 +gi273 +cavake +www.iceland +gi272 +loveofmylife +www.hicham +gi271 +startunisia +test123456 +arivolker +rhapsody +profile123 +chillout +gi269 +gi268 +musics +m.video +gi267 +gi265 +gi264 +farouk +medinfo +gi263 +samoloty +bebobebo +cyberworld +mahjong +gi261 +myfamily +gi260 +www.lemonade +doddysal +hostme +gi258 +orkutnet +gi257 +gi256 +gi255 +gi254 +houser +gi253 +gi252 +gi251 +gi250 +manish786 +cukinate +gi248 +dprakash +redatimes +gi247 +www.helpme +posters +mega007 +gi244 +takuya +al3mlaq +www.emilia +stallion +gi243 +ezekiel +gi242 +acompany +gi241 +khan786 +gi240 +photolab +gi238 +chimung +azmail002 +vinay +gi237 +funnythings +gi236 +gi235 +gi234 +soundsystem +gi233 +gi232 +gi231 +richar +gi229 +freesupport +lipe +gi228 +gi227 +www.des +danubio +gi226 +legende +hrishi +netweb +mail111 +oldworld +gi225 +coley +gi224 +a7lam +gi223 +gi222 +gi221 +gi220 +gi218 +gi217 +a7zan +www.cnc +gi216 +cruel123 +gi215 +mmusic +zouzou +gi214 +jagoda +www.friend +gi213 +sipe +robinhood +gi212 +www.freetv +ghatipati +pcsafe +mikerichardson +friendsworld +www.hospitality +aluminium +gi211 +gi209 +arabsoft +gi198 +gi197 +sitecore +gi206 +integrity +gi205 +gi204 +cementar +gi203 +gi192 +gi191 +gi190 +gi188 +tube8 +viswateja +cirugia +gi187 +robinsons +gi186 +netwalker +realserver +gi41admin +troya +huatak +mixter +certika +samspade +searchengineoptimization +gi184 +gi469admin +barcalona +antijboura +gi182 +mistic +gi181 +gi179 +pisby +gi178 +dreamhack +gi177 +phpmysql +gi176 +gi175 +gi174 +gi173 +occo +wandi +sfsf +jayson +www.abraham +www.eureka +www.flores +fasling +gi172 +vishnuvardhan +gi171 +achratech +gi169 +torre +laminate +www.evelyn +gi231admin +macipoli +www.crema +impulso +promotor +gi167 +amec +hamoud +todojuegos +sanandres +www.backtoschool +gi166 +gi165 +gi164 +gi163 +dostavko +miauto +www.bit +gi162 +gi161 +gi160 +gi158 +www.ibrahim +gi157 +www.aws +promusic +mycomputer +hussam +www.gamerz +gi156 +gi155 +gi154 +guitarhero +gi153 +gi152 +zuzki +ingame +skyking +gi151 +www.escort +gi149 +imotok +gi148 +davidoff +www.wiiworld +gi147 +www.ccr +gi146 +desktopvideoadmin +gi144 +hortensia +gi143 +abadi +abaja +gi142 +wiking +gi141 +abdoo +gi139 +puremusic +melanie +gi138 +gi137 +gi136 +usman +gi135 +achat +imstar +gi134 +www.iloveyou +myphone +mohseni +www.newspaper +addie +gi133 +adept +adjie +aditi +videoclip +gi132 +gi131 +gi129 +www.xx +www.tz +www.tu +gi128 +bayarbat +www.to +gi127 +www.rq +www.rg +aftab +gi126 +netboy +afzal +ahlam +gi125 +www.or +gi124 +gi123 +gi121 +ahrar +gi119 +gi118 +gi117 +badin +gi116 +bahaa +gi115 +gi114 +ajith +gi113 +gi112 +gi111 +gautamkumar +gi109 +balli +apnafun +gi108 +aizaz +aizen +barde +gi107 +gi106 +gi105 +batar +gi104 +gi103 +www.kt +missou15 +gi102 +alex4 +northernirelandadmin +gi100 +insuranceadmin +algno +alibi +usmilitaryadmin +aliii +zanzibar +www.iz +www.jm +amany +constructionadmin +allam +graphicdesignadmin +webtrendsadmin +allie +artforkidsadmin +plussizeadmin +ameet +gi347admin +clevelandadmin +www.ew +gi108admin +jevans +www.er +amjed +laborissuesadmin +sweetpoison +ucat-sl +anami +youngadultbooksadmin +www.dt +employeebenefitsadmin +frenchcultureadmin +bedda +gomontrealadmin +desmoinesadmin +www.dg +alwrd +angad +angga +inglesadmin +bipolaradmin +gi35admin +askjpartners +betans2 +betans1 +fictionwritingadmin +ucat-gl +gi225admin +pmsadmin +familyfunadmin +lungcanceradmin +chicagosouthadmin +berta +preview.seventeen +antar +homesecurityadmin +gospainadmin +stage.americangreetings +menopauseadmin +comicbooksadmin +eathealthsladmin +shuzai.womenshistory +llamas +arab1 +yaser +aqila +arbab +memo242 +www.vintage +gi342admin +gi49admin +bidet +ardhi +powerboatadmin +gi103admin +sweetums +rockstar1 +beaguide.team +usforeignpolicyadmin +cabba +hernandez +arkay +biker +startrekadmin +gi240admin +filosofiaadmin +lesbianlifeadmin +gi29admin +cadsf +simonsky +gi458admin +gi219admin +dyingsladmin +asker +musicacristianaadmin +canadapoliticsadmin +faccbook +asmaa +gi30 +gofranceadmin +preschoolersadmin +arwef +asoft +purples +worldsport +stereosadmin +austinadmin +coffeehouse +activetraveladmin +usana +politicaadmin +www.original +carob +www.elites +westernmaadmin +pepa +cave1 +gi24admin +gamebattles +blero +kratos +www.photoworld +john123 +gi453admin +chinesefoodadmin +aures +aurum +gi214admin +inmuebles +gi303admin +adultedadmin +leandro +womensbballadmin +contemporarylitadmin +cezar +boots +gi331admin +awardsadmin +officeadmin +kosherfoodadmin +collegehockeyadmin +ussocceradmin +psoriasisadmin +francia +altmusicadmin +tesis +amblogin +gi18admin +gi447admin +azert +kvartira +piotr +rufus +diversions +braga +talkshowsadmin +mychemicalromance +tails +cidar +diabetesadmin +shots +choci +yaziland +choki +catloversadmin +azooz +enperuadmin +stardays2 +drumsadmin +columbusohadmin +dacad +azumi +gi325admin +cindi +foodpolicyadmin +goamsterdamadmin +kmusic +globalizationadmin +azzam +homesite +daima +tuankiet +downsyndromeadmin +movieboxofficeadmin +dalac +djk +ancienthistoryadmin +subsabsladmin +gi13admin +damin +fisheryadmin +classifieds.history +gi203admin +reich +philosophyadmin +enespanaadmin +gatosadmin +bbissues +theaymane +www.phantom +www.ia +darsh +beautyadmin +twitteradmin +goseasiaadmin +www.sac +minhaconta +simpozia +nanotechadmin +kalemat +www.gl +dcebe +doctorpc +davor +dayan +govegasadmin +menshealthadmin +gi319admin +taxtimeadmin +webcams +genealogyadmin +seniorsladmin +gi392admin +bux08 +componentsadmin +debaj +oaklandadmin +judaismadmin +deepu +digesto +mobileofficeadmin +afroamhistoryadmin +banserver +juarez +pcfix +medisys +libertarianismadmin +cokoo +eastangliaadmin +salas +hwpoll +dewki +containergardeningadmin +potteryadmin +servant +gi436admin +gi187admin +corpi +www.terra +lamoon +budgetdecoratingadmin +musix +museo +weddingsadmin +keralam +gi420admin +encrucerosadmin +dilan +importexportadmin +crkut +collaborationadmin +motos +gi314admin +gi300admin +etfadmin +africanculturesadmin +tech.team +shuzai.in-fisherman +bbcultures +localfoodsadmin +kabaka +feeds.beta.nytmy +nachi +menus +paltalk +kidclubsadmin +mentors +kacang +gi431admin +dmail +kpax +gi182admin +photographyadmin +michaele +musicamexicanaadmin +cyganka +biotechadmin +socialworkadmin +oulfa +www.bs +redvampir +portablesadmin +gosanfranciscoadmin +hepatitisadmin +dodaj +calgaryadmin +tavera +www.geography +sacramentoadmin +latex +advertisingadmin +germanadmin +lapaz +dolar +homeparentsadmin +izone +donat +el7ob +richinnyd +www.divine +doyen +joyas +elemailadmin +johny +jogos +ehome +login12 +gi308admin +search.beta.nytmy +fafaz +owais +www.cyprus +fahid +bbhealth +judaismsladmin +bondsadmin +kathy +bestofadmin +txsearch +herbsforhealthadmin +buddhismadmin +falak +irshad +fanna +bejoy +bismarckadmin +fara7 +faqih +farad +iskra +goswitzerlandadmin +cocktailsadmin +heemo +fbitb +oncologyadmin +fatik +www.tecnicos +geneticsadmin +fauzi +jithin +gi425admin +animatedtvadmin +fayez +fazal +ekrem +duhok +beautysupplyadmin +wasp +jeans +gi176admin +computersadmin +elmir +humer +nightcrawler +mobilecouponsadmin +elove +scubaadmin +javis +golondonadmin +kaname +artsandcraftsadmin +translations +jacks +jabar +clks +bahaiadmin +www.create +kamran +feras +igirl +tourismadmin +barto +fetka +electricpoweradmin +rahma +alesa +herbsspicesadmin +surgeryadmin +macsladmin +erfan +gi79admin +himanshu +karami +ictus +superheroes +fikri +gi293admin +sexcerbobi +icast +masterman +firas +icaro +spaceadmin +abhilash +ertin +amarilloadmin +www.add +guitaradmin +www.wowinfo +graff +galib +painadmin +neworleansadmin +fitzy +shuzai.africanhistory +divingadmin +gardeningadmin +www.neptune +gi269admin +chemengineeradmin +gi419admin +realptc +rarediseasesadmin +gi171admin +habbi +somaadmin +txsshgateway +gazal +childrenshealthadmin +familysladmin +randbadmin +bmcmail3 +mazinkhalil +landlordsadmin +katrok +www.animezone +gi97admin +yahoo360 +fname +negociosadmin +crosswordsadmin +estec +fixit +chandni +gamex +alfars +gamet +galax +feathers +www.coches +habboisland +www.server3 +drunk +gmcj +www.thevoid +static.hdw +toss +moodleold +www.promotions +curly +rammstein +djmax +www.expresso +freds +www.selenagomez +alessandro +susa +frisk +www.dinamic +porki +danes +hacki +citec +hacko +haray +hacky +bross +www.theclub +damascus +bonne +haise +hakar +bline +cassa +asnet +hakki +bijou +www.oldschool +anunt +alinea +hamdi +palomino +vdp2 +wm5 +bigmike +z20 +kenmore +ambar +www.webmasters +alain +phpmy +parallel +bandy +lop12a6 +bball +nwp +sportclub +germain +boualem +www.videochat +gjwap +www.chrome +claremont +gmale +www.tga +pelco +www.brasil +sekhmet +tns9 +tns8 +gnews +tns7 +terraria +tns6 +gohan +tns5 +gogol +tns4 +tns10 +nero10 +magyar +www.encyclopedia +tecnicos +googl +www.maestro +cerebro +rocklee +frenz +www.valentina +pokemonworld +proweb +blogspace +transportation +www.water +ely +www.cancer +www.miniclip +www.turbo +cyhwyyx +www.tsweb +www.thesimpsons +coldplay +megajuegos +wolfer +sexygirls +ibda3 +suhani +www.arcade +www.smile +ichal +mindcontrol +www.simon +nhatlinh +dso +celeron +findfriend +translators +www.remix +idris +tne +metepol +gamesource +www.raman +web4you +www.rahul +sendibadtv +kolumbus +rafay +www.radyo +www.animes +www.animax +overkill +www.plati +www.autoresponder +obsolete +www.oscar +nidalstyle +www.piano +wiiworld +meloode +zone-ghost +www.opera +bestptc +raytech +www.peter +jadid +frederic +senegal +mland +alnoor +alhamaty +www.ocean +santuario +rockshop +www.niche +www.albert +nacho +series +janne +marketingonline +tetsu +itworks +www.society +muro +sonia007 +chebdal +www.mazda +www.godofwar +jawad +prv +www.laura +www.laila +www.videoclub +kristoff +www.joker +lagrange +stacey +hunny +www.https +jockey +runescapeforum +autismo +gamemaker +www.lincoln +checkme +upton +www.igame +www.violetta +www.ictus +doubler +gigantes +www.gtaiv +mito +jenna +1direction +nad +jerin +intan +revenant +www.glory +www.hakim +www.hades +medalofhonor +ratki +www.habbo +www.fresh +iraq8 +www.active +www.ourspace +modiran +www.etech +stem +jimmi +badgirls +knightonline +www.concept +popolo +franki +sese +mysms +www.lighthouse +maja +mago +www.curso +www.eddie +redz +mosafer +kanak +extasy +www.crane +kamon +keshav +irwin +www.cobra +www.class +www.clans +www.chile +www.ayoub +www.chevy +www.cheat +katie +olis +www.blink +www.askme +lawa +kethek +laera +clf +kikki +descargar +www.alexa +mirc +lino +www.toyota +kedar +javaworld +iranian +ledo +descarga +kern +tmtest01 +kaya +www.bazar +gandhi +jlca +www.dentist +empireearth +ns85824 +dznet +webmail.deepsron.com +feta +www.sebastian +ns1.vps2 +darkmoon +ecco +khoso +ns2.vps2 +kiman +killa +kingm +darkhero +kingz +cora +www.medalofhonor +cica +kanika +bows +capo +arca +holo +comunicate +enduro +www.yuri +jtech +judgement +baccarat +carlosr +fallen +latef +jvc01 +joomla3 +traveling +careful +www.mario +caramba +capitan +dorcas +cyber7 +bdp +klotz +cemolo +cancion +knihy +www.wtf +harpreet +www.wms +luckylife +webgame +www.why +dineshkumar +emtoi +www.uli +www.tomodachi +kolas +www.svm +www.sao +www.rgp +cuervo +soufiane +deathnote +loikili007 +wwenews +www.mrs +forumnet +www.met +liana +www.jessie +www.mac +www.needforspeed +www.kms +fedaa +jmed +www.kia +xerumide +www.jos +starpage +www.whatever +madhu +duniamaya +hairstyle +creare +sciencetech +mal3b +www.gts +cratos +mahsa +lirik +www.cookie +www.tmd +majed +www.ggg +www.dys +mysteryman +mortalkombat +www.bulldog +www.eli +mall3 +freez +dextra +compro +detodo +www.blk +desing +www.asd +mandi +www.afb +mandy +www.reptiles +protektor +magictricks +emedical +euros +manoj +mannu +fcall +manos +mansa +mansi +marah +faruk +mark2 +drxox +bigfish +ven +uli +goodwork +tga +tcr +sil +ros +sdn +rct +tamlym3ak +ghazali +bodega +pkr +videoclub +mazin +oli +ksd +customer-service +jus +cybercity +kursi +jmt +video15 +ixa +soliman +www.illuminati +matban +memet +www.nicolas +www.rentacar +timbo +lokee +loker +hbb +incidencias +abomosa +forum2009 +luckyweb +camara +eht +salvatore +cnv +mgl +vampires +cfn +cff +cfc +bmp +www.daniela +www.agricultura +diastery +adt +egeli +kzone +www.easymoney +www.carpediem +belzebuth +mp110 +www.computerfix +www.hollywood +www.mad +nadal +andrex +beaute +naeem +crisis +burma +amauta +minou +ventura +alexei +najem +www.hackers +knyexchg +knysaprout1 +bugsbunny +misto +barcha +barber +nanou +musicweb +www.pin +mkhan +somos +bander +solteros +isem +musicislife +webdisk.md +cheat +abdelghani +habbovip +www.melissa +wendell +aguila +vacaciones +www.on +www.pspgame +thelegends +mouhcine +mediaplayer +askme +lutfi +webdisk.cdn2 +artec +redhill +actual +www.allstars +www.veda +hankook +anthrax +columbus1 +unitel +redrum +nesta +vendo +guillermo +www.brainstorm +monem +mastergames +infocom +lovemusic +pleyades +yoyogi +pspgame +www.habbomusic +www.tips +www.michelle +com1 +lukoil +www.conquerors +todogratis +streetart +www.gamemania +pacifica +kakamilan +justchill +njoel +pctech +thunderstorm +cocacola +spektrum +haseeb +www.toledo +yale +mulya +www.myhome +www.dragonball +vals +snet +tato +federal +siac +shai +laluna +sahmed +ropa +seni +lamrfe +agricultura +patr +mycar +nola +lugo +yousha +salsabil +kiya +kits +karn +noor1 +iori +javi +jams +nosil +gyym +mypic +idee +goma +sam4u +glob +darkempire +ersa +saied +gabo +admix +dhruv +pacio +padam +eddi +cros +cres +dien +palms +dex1 +bunk +crazy1985 +sama7 +djromeo +brea +ceto +creat +carp +camu +asch +didac +arbo +juanes +aime +adas +www.noname +okkut +www.profit +guild +computerfix +ahmed12 +mygames +okrut +sania +greentech +www.blacklist +wmm +falloutboy +vsm +websales +tbt +rox +educadores +pfc +dreamspace +nnc +humbert +ricardogarcia +klz +httpwww +phani +knc +klk +lcd +jpc +jfs +runescapeforums +him +wwjd +fpi +evl +fez +orkot +orkul +dvitre +end +fae +edr +dng +ebg +piwky +dekza +qasta +djg +aliali +bso +8638521 +bmf +ra3ed +apr +apg +bdo +amm +junaid +aby +owned +godigital +publico +thecrew +timeweb +ratchet +junjun +managua +www.radios +trad +circassian +warhammer +vermillion +www.webchat +qf +posti +vivek123321 +alraqqa +www.freelancer +www.you +trucker +narutox +ozono +dragana +yut +mismail +tph +jurgen +shor +profu +tio +sah +rhp +rey +rfc +psr +rpc3 +pk2 +ntr +ola +ramzi +nsn +loveorhate +rasha +ratti +kkxiaozi +mvr +msr +mro +razer +sabr +kmz +jol +jml +inu +hoc +gsi +hec +pandata +gmh +fsr +shahad +putri +eti +ppc4 +ept +reema +dnf +dhp +btr +clg +bsp +myideas +dao +renda +bsb +azr +cif +dalex +aliahmad +alm +akp +davywavy +agentx +bag +aia +afb +d66 +sabah +papiro +naranjo +www.vvv +ju +sadiq +safar +safin +saida +calton +www.futurama +4g +rishi +4c +sakil +www.e-commerce +clipping +samad +hyla +reademail +slcam +fortimail +oferty +ipad1 +www.garden +utt +riyad +inf2 +saveearth +pressrelease +rizki +j9 +agra +scada +linuxmint +www.chennai +sawaw +noida +sso1 +scoot +gotr +toletol +lektro +www.planning +www.acc1 +www.housing +eia +sweett +openview +seo11 +www.startup +seksi +co2 +edir +simplet +www.pstc +km1 +sqlweb +setyo +romio +romka +intermedia +www.road +www.disa +www.eso +tws +www.doe +shaan +shafi +shaft +love177 +www.csh +shanu +shari +tcdms +www.fbm +fath +bizinfo +dorm +shery +ines +kuchnia +httpd +frango +wwwd +shone +accessedge +tablo +www.mrtg +shared1 +node0 +textads +bobby +shank +saludmental +tamas +sharj +www.grd +tabaco +bugg +tapan +danc +tarak +tareq +wproxy +onlinegate +jw1 +cans +libserver +fraser +clippers +www.camping +championsleague +kolchi +sportsbetting +nrl +nbl +listy +domeniu +www.xr +www.listy +xr +www.omni +labtest +catarina +l212 +pazar +www.standard +shida +video123 +konici +soft1 +dopuna +l209 +adar +abid +logistica +shoponline +begood +some1 +orice +cti +ct2 +spidy +fraktal +twww +mailsend +npm +fpt +s434 +starnet4 +s430 +kopral +thoma +s420 +s414 +bbk +s442 +s441 +s439 +tinku +playgame +s438 +www.versuri +versuri +s437 +s436 +s435 +s433 +superpixel +s432 +s431 +tlee2 +rfaxa +s429 +luminoso +s428 +s426 +sumer +s424 +s421 +formations +videosex +wxinlin +s419 +saber123 +s418 +s417 +s416 +s415 +s413 +s412 +s411 +s410 +s403 +s402 +s401 +s339 +biohazard4 +kpreet +s406 +rendy +s342 +talal +s334 +s333 +libana +s331 +thechosenone +askar +s315 +s309 +s308 +kenza +s307 +s306 +s304 +s303 +s302 +ttttt +s301 +noujoum +sigmini09 +serdar +mongol +aldous +cadou +wb407 +arnet +s320 +star50 +www.mmc +mailbk +star10 +www.aed +paras +www.consumer +mla +sanerdex +fragile +aed +vhera +lightz +www.cpe +vibhu +www.mrb +usama +alpo +komak +backup-1 +neza +amiga +kaker +tins +copia +roko +varsity +navdeep +slr +rsv +irp +viswa +sombrero +mobiplanet +capacita +wapbd +wwwi +goldendragon +aser +www.aser +vlado +foton +hojo +foren +cubic +codon +madani +www.wind +convict +duesseldorf +webku +recht +riverdeep +johann +www.tony +ericl +www.emploi +intercambio +bevan +aei +xxq +www.grad +bepro +shabeer +973 +prolog +www.classified +autoconfig.shopping +autodiscover.advertising +osodaleslam +autoconfig.assets +autodiscover.assets +autoconfig.advertising +loire +autodiscover.shopping +accountmanager +maicol +mbe +www.company +www.idea +mahome +db13 +shadowx +www.riv +lisboa +friday +vm53 +vm18 +vm17 +staralarabe +vm16 +triskelion +vm15 +makeit +vm19 +xawer +shafqat +livejournal +vipdosug-ac1 +forte94 +mall50 +mall52 +mall59 +mall61 +mall62 +mall63 +smtp.be +malice +hsmtp +math4 +cheeta +divis +sfecm2 +littos +sfecm1 +ups2 +manishjain +mp3find +krusty +clientstorage +svc2 +off6 +tfe1 +maniek +manpda +proxy.whois +r4v37t +wqdxa +balan +cam6 +sfe5 +ginko +toubib +sfe1 +netdot +tokyo-hot +babui +q23 +q14 +q13 +crozet +q12 +q11 +audiodrom +ghost-zone +o14 +marlin +chosta +o13 +o12 +o11 +da20 +wtaty +sfe2 +shop.new +mp3list +vipdosug-ac6 +sarina +vipdosug-ac5 +vipdosug-ac4 +vipdosug-ac3 +vipdosug-ac2 +yasso +aayushi +masoom +forimage +yazan +sigmini +mail.server +pinterest +ac3 +ivrstat +test2.shop +off5 +off4 +off2 +off1 +bill1 +cp-epay +load.support +off3 +fr.staging +es.staging +www.inside +barencevo +orkutcommunity +ttg +mickael +extranet-test +travelfree +tcn +facturation +sql6-replicat +sql-dell-i30 +torkica +subby +afonso +eh4-i +ntp-int +parsa +ftphosting +eurorack +varaujo +thaer +tibialogin +baise +zahid +sql2-replicat +gameloft +laurenttest +ludo3 +zargo +flashtrack +tunisian-hacker +zm1 +newton.phys +localhost.hist +localhost.geol +kooora +ymail +localhost.chem +newbux +localhost.biol +kumar1 +localhost.soc +hackersun +localhost.psy +localhost.med +localhost.cns +localhost.maths +newstaff +ghoghnoos +localhost.ciel +zeroo +arbuckle +cherkessk +jm1 +ghostbd +blueeyes +www.kis +quiosque +xxx4u +autodiscover.users +autoconfig.users +webdisk.users +linuxserver +karlos +bah +test1000 +mobsite +abodi +webopac +vpn100 +assets0 +www.mapy +internode +games-online +fizyka +politika +radman +www.mage +publicsite +agc +onlineserver +appservices +stratusbeta-pns +stratusstage-pns +rajendra +akrimnet +myproxy +profilesyahoo +ppccore +tot3 +jm15222 +smers +supertimes +hawkeye +panida +thabet +zohar +reham +vpsisgred +okazii +timur +renovat-e3 +mohanagy +apc03 +vikrant +hnode-iberonet04 +maguire2 +palace +pakida +royalking +stech +trouble +musick +clifton +startimes5 +startimes3 +netsupport +ministryofsound +rogermase +mronline +rajsite +loginn +wqdfa +symbian +myshare +games4you +karrox +runescape3 +valentino +metal2 +langzii +lwfchn +mfkggi +rincewind +mysite1 +saurav +guitars +tahichi +downloadvideo +winjie0618 +lololo +williams +dawson +suomi +myteril +rapidsharee +losans +smoka +mytimes +zabarom +shantou +desiworld +mystuff +elking +larachesat +4islam +eleicao +megaonline +vactin +satheesh +rickyroma +toxicity +jesske1990 +deepanshu +michel +wolfstar +smilies +luiscarlos +ridah +hoangtan +habbomania +bazooka +gemstones +star-forum +mylogs +nounou +mylist +meriberas +vivekanand +fatakata +yasirweb +4algeria +kadra +awfda +blackfoot +rifai +bhavinpatel +habbobeta +myhack +mall60 +forumstar +nejasno +loginpage +bisnisonline +mall51 +codebase +frncisa +server100 +server123 +xxenik +slimshady +mirela +shahbaz +noldor +praneeth +rahul123 +proxyvn +nbaztec +mithun +neural111 +elattaf +gandhiji +elghedir +gandistq +anydvd-serials +sindbad +thebestforum +harbour +smartconnect +metka +rajkumar +freegifts +teguh +gmaillogin +nassim +myupload +supratim +nader +darkwing +nazari +passreset +hayder +jiangfan +giglio +facebook22 +rafaell +bandhan +irancell +omany +umang +t-online +testforum1 +mrali +madworld +kissmp3 +versasex +startimes22 +startimes07 +certified +shubham +whitenoise +serial88 +digitech +killer1 +nod32-crackdb +lvivka +mistery +safezone +batumki +montadana +trebor +batuhan +uniqe +xiao77 +aviemore +tounsi +sestrada +drsonia +topreplica +venky +jopasaran +sajal +travel1 +johanna +detyatko +biohazard +teddybear +goldeneyes +neobux +tupola +norton-serials +accident +shobhit +prasanna +dz-down +nitesh +orkutlogin +danghuy +rajatarora +chlorine +easyjobs +kalpana +jakitan +b8000 +outreach +hunterxhunter +faksoma +nimesh +cimislia +naveen +darbuka +ngetes +caqer +blacksea +hacktrack +albaraa +arab-4ever +loginorkut +wasim +webtimes +asuna +pritish +viyeu +southtown +abdulla-raid +thespider +carnaval +wap4u +princes +vipin +usher +forum4sobe +madina +saleswwo +feel800628 +softech +ppls-y2lab-202.ppls +srv105.csg +hca-netprint-bw8.shca +hss-hca-0015.shca +csg-pps-0011.csg +mvm-ri-l107136.roslin +nimbus2 +ppls-y2lab-207.ppls +hss-hca-0009.shca +ngocha +hss-hca-0021.shca +ppls-y2lab-213.ppls +psy-adlab07.ppls +www-test.epeople-fin.humanresources +csg-hr-0021.csg +sci055.scieng +csg-srs-0003.csg +wapas +hss-hca-0026.shca +ppls-igel-2-01.ppls +function +sas-cas-0083.sasg +prithvi +nhatky +csg-corp-0002.csg +csg-as-0000391.csg +nightstalker +pps-xer3.csg +clickone +mvm-ri-d097063.roslin +mvm-ri-l615158.roslin +csg-fin-0091.csg +dsb-g-1-mfp-reader.ppls +mvm-ri-d087171.roslin +sas-cas-0077.sasg +madhav +billard +hotmaillogin +csg-est-0197.csg +hss-hca-0032.shca +passwords +hotmaill +parashar +csg-fin-0036.csg +frisco +ris-valx02.roslin +ppls-pc31.ppls +hss-hca-0037.shca +hss-health-l27.health +sas-cas-0072.sasg +hca-lab3-mac30.shca +phhh-g-lab-mfp-col.csg +kinka +csg-fin-0086.csg +www-test.vle +mopeda +hss-hca-0103.shca +sas-reg-0163.sasg +danniel +aap071.sasg +newell +ml-3-openplan-mfp-col.sasg +mvm-ccbs-060164.ccbs +ccnsm076.ccns +alberic +sas-cas-0066.sasg +glowwebcast.trg +ppls-sem-0001.ppls +redhacker +csg-as-0000781.csg +mvm-ri-d086168.roslin +hss-hca-0043.shca +csg-fin-0146.csg +radioplus +msprevak-mac.ppls +hca-lab3-mac24.shca +csg-est-0025.csg +associations +csg-as-0000841.csg +sas-alumni-0017.sasg +sci162.scieng +megavideo +useless +hss-hca-0048.shca +sas-cas-0064.sasg +ppls-igel-4-01.ppls +travianbest +kingpin +sce-coll-0051.scieng +csg-est-0075.csg +harakiri +jack007 +9hps-2-206.csg +stayfree +hca-lab3-mac18.shca +sec06.roslin +blahblah +mspace +ltsmeet.lts +kishore +www-beta.events +vertu +hss-hca-0054.shca +cerc-d002.roslin +sce-coll-0015.scieng +sci126.scieng +sas-cas-0055.sasg +unpef +vcs-lap-167201.roslin +mvm-ri-i055152.roslin +hss-hca-0059.shca +mvm-ri-d106023.roslin +twitt +csg-est-0185.csg +hss-ppls-0005.ppls +bems-011.csg +mvm-ri-l076063.roslin +youcandoit +hss-hca-0065.shca +hss-ppls-0079.ppls +www-tmp.vle +hca-lab3-mac07.shca +resolution +mvm-ri-l105188.roslin +health-omq-012.health +rii-105168.roslin +barada +oc-3-corridor-mfp-col-2.csg +dallas1 +mvm-ri-i075019.roslin +scico +eeg.ppls +csg-est-0245.csg +csg-est-0304.csg +mvm-ri-d116235.roslin +moneymaker +sas-cas-0038.sasg +health-omq-037.health +telus +serviceit +tralala +int-jet14.sasg +iknowyou +hss-ppls-0011.ppls +hss-hca-0071.shca +hca-lab3-mac27.shca +www-dev.eit.finance +cuonline +hss-ppls-0096.ppls +yu-gi-oh +csg-est-0305.csg +csg-est-0029.csg +claret.ppls +fuckyeah +ms-dw6-3-pg-mfp-col.health +haggis.cache +csg-est-0244.csg +alton.ppls +sas-cas-0033.sasg +dropdead +sci101.scieng +mvm-ri-d134249.roslin +mvm-ri-d117194.roslin +csg-est-0184.csg +themastermind +mvm-ri-l115187.roslin +ppls-y2lab-131.ppls +23wpc-g-siteoffice.mfp-bw.csg +claudiu +freddie +mvm-ri-l117089.roslin +dadada +investigator +hss-ppls-0016.ppls +hss-health-0120.health +mercadopago +munish +ml-3-reception-mfp-bw.sasg +roslin-dc2 +mvm-ccbs-060207.ccbs +jaskaran +inspiron +hss-hca-0076.shca +prodent +sas-cas-0047.sasg +ppls-pgpc37.ppls +oc-1-r210-mfp-col-1.sasg +reconnect +csg-est-0134.csg +ppls-y2lab-125.ppls +gemilang +aap119.sasg +helping +hss-ppls-0022.ppls +sas-cas-0022.sasg +mvm-ri-m107111.roslin +csg-est-0074.csg +hss-hca-0082.shca +ppls-y2lab-119.ppls +csg-fin-0205.csg +lel-power1.ppls +ppls-psy-002.ppls +aligator +sas-cas-0016.sasg +csg-as-0000840.csg +g2cpx1.ccbs +bambina +orgasm +struga +scoda +hss-ppls-0027.ppls +mvm-ri-i065092.roslin +reflector +csg-est-0024.csg +ppls-y2lab-114.ppls +csg-sss-0007.csg +vcs-127149a.roslin +mvm-ri-l134242.roslin +csg-fin-0145.csg +sas-cas-0011.sasg +nasa10 +csg-as-0000780.csg +iahe-2k3cesrv01.roslin +feitian021 +ppls-y2lab-108.ppls +mvm-ri-v105232.roslin +vcs-126039a.roslin +sas-cas-0005.sasg +phstl-2-managers-mfp-bw.csg +ppls-y2lab-103.ppls +hss-health-l63.health +dragonfire +csg-fin-0035.csg +alessio +hss-hca-0087.shca +demopc01.ccbs +mvm-ri-d107205.roslin +hss-issh-l1.health +mvm-ri-d115231.roslin +rii-055147.roslin +amilcar +ris-lx13.roslin +armas +maroc4ever +ris-esxi05.roslin +saada +gfl105247.roslin +csg-corp-0001.csg +vc-g-shop-mfp-bw.sasg +hca-jpglab-015.shca +mvm-ri-d107215.roslin +lect-health-004.health +hss-hca-0093.shca +ppls-mcguire.ppls +mvm-ri-d125042.roslin +sas-chap-0003.sasg +unblock +mvm-ri-i055209.roslin +ris-lx03.roslin +mishra +sourcecode +mvm-ri-i065031.roslin +manoo +mishka +zuikong +mvm-ri-l085047.roslin +najeeb +fusilli.ppls +anamuslim +superfoto +ris-lx12.roslin +csg-pps-0009.csg +fblikes +npatel +vis014.sasg +sas-cam-0026.sasg +hss-health-l53.health +surendra +ebri043198.roslin +sci019.scieng +riv-amxmodero.roslin +my-world +vis008.sasg +trang +praveen +mall17 +mvm-ri-l125245.roslin +hss-hca-0098.shca +nadina +sas-cam-0021.sasg +mygame +beshoy2050 +csg-hr-0050.csg +myfree +wheeler +vis003.sasg +newhaven.scieng +iad-pc19.iad +mvm-ri-d127086.roslin +csg-cse-0058.csg +hss-iad-0032.iad +mikado +hss-ppls-0044.ppls +navin +jackiechan +crystalx +sas-cam-0015.sasg +mvm-ri-d097174.roslin +laptop-gmiller.health +mvm-ri-l087102.roslin +whitesnake +ppls-monmac.ppls +mymate +textiles +hss-hca-0114.shca +as-lock-001.csg +hackman +ris-backup.roslin +dns1.inf +cripple +newsecure +hca-jpglab-010.shca +loginfb +www.estores.finance +mvm-ri-sx01.roslin +ris-vlxweb11.roslin +hss-ppls-0049.ppls +doga +csg-cse-0056.csg +sce-coll-0025.scieng +mohandsen +mangal +mvm-ri-d075153.roslin +www-dev.admin.eves.myed +rajiv123 +csg-cse-0006.csg +sas-cam-0004.sasg +csg-hr-0052.csg +airline +thebridge +textbook +ris-ifs3.roslin +lalolanda +mvm-ri-d136101.roslin +cyanide +hss-hca-0119.shca +sas-sra-0028.sasg +csh-2-2.14-mfp-col-1.csg +mydesign +www-trn.ess.euclid +pps-xer4.csg +mialee +holyrood +hss-ppls-0055.ppls +ppls-printer11.ppls +hss-hca-0125.shca +sas-cas-0075.sasg +sujay +wolfi +citrixweb +hss-hca-0101.shca +rim-076017.roslin +sonyvaio +woolf +rip-brfm5.roslin +ebrd075073.roslin +lokesh +csg-est-0106.csg +metals +computerworld +oldbryght +mvm-ri-d067183.roslin +hca-mfd-003.shca +background +hellfire +oc-3-corridor-mfp-col-1.csg +scarface +fishies +ms-dw6-g-1-mfp-col.health +chris.temple-lib-web7 +jasonwu +templeton-dev +temple-lib-web.temple +groovyonline +ebri073195.roslin +login7 +hss-health-vaio.health +live.dosomething +sci065.scieng +still.temple +csg-est-0207.csg +motorsport +ipswap +jarvis +mvm-ri-l115162.roslin +moleman +ccintranet +csg-est-0243.csg +mvm-ri-d125087.roslin +menber +ppls-y2lab-015.ppls +oldlinode +int-usbmac11.sasg +delphine.temple-lib-web7 +greatist-legacy +civil-rights.temple +zumba +meknes +mvm-ri-d096140.roslin +stampy +facebookalbum +melani +javachat +csg-est-0183.csg +panela +sas-leaps-mac1.sasg +ppls-y2lab-009.ppls +www.admin.drps +ebri083204.roslin +sas-reg-0129.sasg +zstar +lindo +mvm-ri-d136045.roslin +csg-pps-0012.csg +ppls-printer16.ppls +5forrhill-c-c20-mfp-col.sasg +ssl38 +zonex +phcc-g-trades-mfp-bw.csg +lect-hca-009.shca +zonda +ip-40.138 +babait +ppls-y2lab-004.ppls +ppls-igel-3-01.ppls +csg-est-0073.csg +www.tqintra.dev +facebookvideos +flipbook +hss-health-0109.health +class1 +riv-hd2.roslin +lect-hca-004.shca +csg-fin-0194.csg +mvm-ri-m126083.roslin +ris-vlxftp01.roslin +csg-est-0023.csg +www-dev.epeople-fin.humanresources +oldies +hca-escreen-05.shca +redhot +medhat +csg-sss-0006.csg +bigfm +csg-fin-0144.csg +csg-as-0000778.csg +hss-health-l37.health +csg-hr-0047.csg +ris-vwlx03.roslin +mvm-ri-d115195.roslin +csg-fin-0084.csg +hss-health-0023.health +mvm-ri-d096210.roslin +13infst-2-openplan-mfp-col.csg +mvm-ri-d126046.roslin +discounts +chan-1-gu438-mfp-bw.ccbs +srv016.csg +angrybird +mvm-ri-d096068.roslin +tortoise +dev.services.learn +sce-coll-0061.scieng +csg-saf-0021.csg +amprint.lts +uhs002.sasg +mvm-ri-l096142.roslin +hss-hca-0100.shca +mvm-ri-d127009.roslin +hss-health-0107.health +psy-pc024.ppls +test.scieng +health-omq-022.health +csg-pps-0008.csg +ed1st.csg +mvm-ccbs-060423.ccbs +pps-xer1.csg +forensic +mermoz +srv045.csg +zsmtp +www-dev.myed +www.esp.myed +qmail1 +sas-cas-0020.sasg +stewartmacair.lts +csg-hr-0048.csg +mvm-ri-m096007.roslin +mvm-ri-l117232.roslin +ris-pvnb01.roslin +hss-iad-0031.iad +sas-bu-0042.sasg +hss-health-0129.health +dcmobile +dns0.inf +montezuma +online-games +notes2 +reg-jet48.sasg +csg-cse-0055.csg +kurama +www-dev.suppliers-admin.finance +www.psc +ebrsqlsrv2.roslin +sas-bu-0036.sasg +csg-cse-0005.csg +sas-bu-0031.sasg +xzone +gamespot +handc-pc66.shca +csg-fin-0202.csg +sas-bu-0025.sasg +vpn118 +vpn119 +vpn122 +vpn123 +vpn124 +hca-tlab-023.shca +sas-bu-0020.sasg +hca-tlab-017.shca +hss-ppls-0077.ppls +vpn101 +vpn102 +vpn103 +vpn104 +vpn105 +vpn106 +vpn107 +vpn108 +vpn110 +vpn111 +vpn112 +vpn114 +vpn115 +vpn116 +vpn117 +vpn120 +vpn121 +vpn125 +vpn126 +hss-health-l73.health +vpn109 +cas-mlb3-013.sasg +mvm-qmri-0113.roslin +hca-tlab-012.shca +auction2 +kusa +sas-bu-0008.sasg +mvm-ri-d096114.roslin +pcntterm1.ppls +cas-mlb3-007.sasg +mvm-ri-d107225.roslin +ema5.ppls +hca-tlab-006.shca +ksh +health-lap67.health +sas-bu-0003.sasg +csg-est-0242.csg +cas-mlb3-002.sasg +per-jet11.iad +nexen +ppa011.ppls +hca-tlab-001.shca +csg-est-0182.csg +mvm-ri-d125240.roslin +sce-coll-0009.scieng +mvm-ri-l096177.roslin +www-test.courses.myed +www.epeople-fin.humanresources +getlink +ziani +amaterasu +mvm-ri-d086044.roslin +mvm-gf-l115204.roslin +ns-uk +ns-za +googleorkut +sce-eccc-0002.scieng +mvm-ri-d127045.roslin +csg-fin-0203.csg +adminstaging +tetis +hss-health-l12.health +cale +jano +csg-fin-0125.csg +csg-est-0022.csg +hss-hca-0105.shca +sas-cas-0008.sasg +febe +hinatabokko +csg-corp-0003.csg +csg-sss-0005.csg +csg-fin-0143.csg +mvm-ri-d086222.roslin +sgw1 +backup-2 +ednet-fv +sgw2 +ltsp +mvm-ri-l067160.roslin +hss-health-0140.health +handc-mhist24.shca +csg-fin-0083.csg +ppls-y2lab-210.ppls +mvm-ri-m125244.roslin +sce-coll-0035.scieng +sas-reg-0107.sasg +csg-scecc-0001.scieng +csg-fin-0033.csg +photosite +sas-reg-0179.sasg +s-1 +iad-pclaptop02.iad +mvm-ri-d135000.roslin +kfc +mvm-ri-d096221.roslin +handc-mhist13.shca +sciengmscs.scieng +mvm-ri-d067025.roslin +mvm-ri-i125096.roslin +rid-vuoe.roslin +www-dev.wpmservice.finance +ris-vlxbio01.roslin +sas-reg-0191.sasg +phstl-b-postroom-mfp-bw.csg +hss-hca-0090.shca +csg-pps-0007.csg +greymatters +localhost.admin +forhill-1-trades-mfp-col.csg +sas-reg-0185.sasg +srv042.csg +mvm-ri-l115172.roslin +hss-health-0104.health +ebrmclsrv1.roslin +sas-reg-0180.sasg +hss-ppls-0030.ppls +mvm-ri-d105038.roslin +sas-reg-0092.sasg +mvm-ri-d096150.roslin +hss-iad-0030.iad +sas-reg-0174.sasg +mvm-ri-l137046.roslin +www.timetab +ril-115152.roslin +rid-056189.roslin +csg-cse-0054.csg +mvm-ri-d107161.roslin +csg-fin-0032.csg +sas-reg-0168.sasg +bbl-dev.vle +pc02.chem +ppls-labds-020.ppls +csg-cse-0004.csg +saf-laptop6.csg +pc202.chem +ris-vlxweb05.roslin +mvm-ri-d065137.roslin +mvm-ri-m115037.roslin +pc203.chem +sas-reg-0060.sasg +wrk-laptop13-2.csg +sas-reg-0157.sasg +hss-ppls-0209.ppls +mvm-ri-v086079.roslin +pc93.chem +hss-health-l47.health +sas-reg-0152.sasg +health-omq-002.health +hss-health-0033.health +mnc +www.psr +hss-ppls-ccace-lbc-01.ppls +sas-reg-0146.sasg +sas-leaps-0008.sasg +phoenix3 +mvm-ri-l097085.roslin +mvm-ri-d107200.roslin +mvm-ri-v115215.roslin +csg-est-0301.csg +sas-reg-0141.sasg +sas-leaps-0003.sasg +ppls-cns-srv1.ppls +csg-est-0241.csg +sas-reg-0135.sasg +mvm-ri-l096152.roslin +g2ctmdev1.ccbs +csh-2-2.csg +darkking +rim-097065.roslin +csg-est-0181.csg +sas-reg-0130.sasg +lib1.lib +hardcore1 +g2cdb2.ccbs +www-test.admin.alumni.dev +pc176.chem +sas-dis-0019.sasg +csg-est-0131.csg +yokai +sas-reg-0124.sasg +csg-est-0140.csg +www-test.api.payments +health-omq-032.health +eri268.roslin +pha002.sasg +csg-est-0071.csg +sas-reg-0118.sasg +csg-fin-0192.csg +csg-as-0000836.csg +ppls-win8-srv1.ppls +csg-est-0021.csg +sas-reg-0113.sasg +sce-coll-0010.scieng +csg-sss-0004.csg +csg-fin-0142.csg +csg-as-0000776.csg +hss-ppls-0083.ppls +dsb-g-1-mfp-col.ppls +hss-ppls-0199.ppls +hss-health-0139.health +www-dev.fpm.finance +sas-reg-0097.sasg +sec01.roslin +ftpadm +csg-fin-0082.csg +raspi1.ccns +hca-mac044.shca +lmn +sas-reg-0102.sasg +mvm-ri-d077237.roslin +hca-mac038.shca +sas-reg-0086.sasg +meter-82-186.csg +hca-mac033.shca +sas-reg-0081.sasg +mvm-ri-d116184.roslin +tapas +wrk-kbrc-g-office.csg +hss-hca-0073.shca +mvm-ri-l127022.roslin +hca-mac027.shca +sas-reg-0075.sasg +www-test.readrae.planning +hca-mac022.shca +mvm-ri-l115146.roslin +pc179.chem +csg-pps-0006.csg +www-dev.suppliers.finance +pc83.pol +csg-fin-0037.csg +u22 +sas-sra-0010.sasg +mvm-ri-d126183.roslin +hss-health-0094.health +sas-intl-0004.sasg +pc90.pol +mvm-ri-l106019.roslin +hca-mac016.shca +kumako +sas-reg-0064.sasg +ymcmb +pc92.pol +ppls-onelan.ppls +mvm-ri-l134240.roslin +pc93.pol +evet2-pc.ppls +hca-mac011.shca +hss-health-0110.health +sas-reg-0058.sasg +hss-ppls-0088.ppls +csg-hr-0046.csg +mvm-sbms-130280.ccns +csg-est-0265.csg +newhaven-webcam.scieng +hss-iad-0028.iad +morcom01b.ppls +rim-115126.roslin +mvm-ri-d125250.roslin +teketeke +gregg +sas-reg-0053.sasg +csg-cse-0053.csg +coolface +sas-reg-0047.sasg +www-dev.miniportfolio.euclid +gfd065253.roslin +cassiopee +csg-fin-0087.csg +csg-cse-0003.csg +fpweb +prodweb +sas-scs-0014.sasg +yoda2 +www-dev.dpts.drps +sas-reg-0042.sasg +dsb-4-7-mfp-col.ppls +iron1 +sas-cas-0081.sasg +hss-health-l22.health +hss-health-0007.health +sas-reg-0036.sasg +sas-cam-0030.sasg +ignace +zacky +jeanne +lr9569 +health-mac002.health +hss-ppls-0208.ppls +sas-reg-0069.sasg +vhosting +sas-intl-0009.sasg +csg-as-0000116.csg +sas-reg-0031.sasg +sci156.scieng +ns1.noc +sce-coll-0045.scieng +vcs-126011a.roslin +sas-reg-0025.sasg +ppls-g26-009.ppls +adrien +csg-est-0300.csg +antonin +mvm-ri-l096073.roslin +sas-reg-0020.sasg +candidates +fujino +csg-est-0240.csg +sas-reg-0014.sasg +ebri053199.roslin +yifan +csg-as-0000782.csg +network3 +pplsms12trial.ppls +mvm-ri-l105183.roslin +extraction +hss-ppls-0094.ppls +sce-coll-0027.scieng +health-omq-006.health +media-cd +csg-est-0179.csg +sas-reg-0008.sasg +francine +ppls-mac028.ppls +csg-est-0129.csg +sas-reg-0003.sasg +csg-fin-0147.csg +csg-est-0069.csg +eric2 +jupiler +csg-fin-0201.csg +csg-as-0000835.csg +boulet +lsystem +kampus +hss-health-0114.health +uberlolz +jean-charles +vip9 +ril-v107107.roslin +mvm-ri-d095048.roslin +sas-sra-0021.sasg +laure +mvm-ri-l106055.roslin +media-z +csg-sss-0003.csg +csg-fin-0141.csg +csg-as-0000775.csg +csg-hr-0080.csg +silvio +newhay +bofh +mvm-ri-d095226.roslin +csg-fin-0031.csg +ppls-igel-5-01.ppls +mvm-ri-d085135.roslin +sas-scs-0020.sasg +cerc1.roslin +popmedia +mvm-ri-l087096.roslin +csg-as-0000615.csg +hostingtest +baisemoi +mvm-ri-d097032.roslin +www-test.scs.euclid +techbase +sas-cam-0018.sasg +hss-health-l57.health +weir-g-17-sfp-bw.scieng +it.dev +csg-est-0026.csg +csg-as-unitots2.ppls +sas-intl-0015.sasg +mvm-ri-d115225.roslin +ris-lx07.roslin +ppls-g26-015.ppls +health-laptop24.health +mvm-ri-l097105.roslin +mvm-ri-d107209.roslin +nmx1 +csg-as-0000842.csg +fresher +hss-ppls-0110.ppls +health-mac009.health +bill3 +csg-fin-0207.csg +sas-sra-0026.sasg +fanaticos +mvm-ri-d137050.roslin +csg-est-0076.csg +wwwfacebookcom +csg-est-0095.csg +dsb-4-7-mfp-bw.ppls +q20 +csg-hr-0030.csg +termo +ac7 +q30 +webdisk.h +leela.tardis +csg-hr-0045.csg +q46 +q31 +q48 +q50 +hss-iad-0027.iad +alumni1 +xlife +www.pubsadmin.recordsmanagement +mvm-ri-d127029.roslin +mvm-ri-d107159.roslin +csg-hr-0013.csg +sas-reg-0010.sasg +www.courses.myed +q80 +ppls-g26-021.ppls +yasha +www.announce.myed +sas-sra-0032.sasg +marsal +q90 +yanti +q98 +csg-cse-0052.csg +www-test.course-bookings.lifelong +o4 +o5 +health-omq-042.health +11infst-1-corridor-mfp-bw.csg +mvm-ri-d115154.roslin +csg-fin-0210.csg +q5 +q6 +q7 +csh-g-reception-mfp-bw.csg +q9 +yaniv +saf-laptop4.csg +csg-est-0136.csg +sas-intl-0026.sasg +hss-health-0003.health +hss-health-l17.health +ics-001.ppls +mvm-ri-l097034.roslin +sce-coll-0019.scieng +hmx +hss-ppls-0121.ppls +egypt25 +markie +marisa +csg-est-0186.csg +sas-intl-0032.sasg +q18 +q19 +sce-coll-0060.scieng +q22 +yahaa +q24 +q25 +q26 +q27 +q28 +q29 +q32 +q33 +q34 +mvm-ccns-0097.ccns +q36 +q37 +q38 +q39 +q41 +q42 +q43 +q44 +q45 +q47 +q49 +q51 +q52 +q54 +q55 +q56 +tempe +q57 +q58 +q59 +q61 +q62 +q63 +q64 +q65 +q66 +q67 +q68 +q69 +q71 +q72 +q73 +q74 +q75 +q76 +ml-3-31-sfp-bw.sasg +q78 +q79 +q81 +q83 +q84 +q85 +q86 +csg-est-0190.csg +q87 +q88 +q89 +q91 +q92 +q93 +q94 +q95 +q96 +q97 +q99 +porto.ppls +mn2 +ris-vlx08.roslin +hss-ppls-0126.ppls +research-innovation +ac4 +phhh-g-microlab-mfp-bw.csg +sas-alumni-0023.sasg +smtp-6 +psy-alz-nas1.ppls +q108 +q110 +scm2 +csg-est-0246.csg +csh-2-2.15-mfp-bw.csg +q120 +mvm-ri-i067176.roslin +sas-alumni-0055.sasg +wrath +sasg-oldcoll-3-r299.is.ed.ac.uk.sasg +g2cpxdev1.ccbs +cam8 +cons2 +chp004.sasg +q130 +mrlonely +ds443 +ds454 +ds460 +steve620.ccns +q40 +oc-reg-g116.sasg +sas-alumni-0050.sasg +ppls-m-vico.ppls +mx-10 +mx-11 +mx-12 +mx-13 +mx-16 +mx-18 +mx-19 +q53 +konami +q160 +g2csrv2.ccbs +hss-ppls-0132.ppls +svc3 +svc4 +ppls-igel-1-01.ppls +ppls-psylib-01.ppls +ris-vtlx01.roslin +cecelia +q70 +cd1 +sas-alumni-0044.sasg +sas-intl-0043.sasg +q82 +q101 +q102 +q103 +q104 +q105 +q106 +q107 +q109 +q112 +q113 +q114 +q115 +q116 +q117 +q118 +q119 +q121 +q122 +q124 +q125 +q126 +q127 +q128 +q132 +q133 +q135 +q139 +q141 +q142 +q143 +q144 +q145 +q146 +q147 +q148 +q150 +q151 +q152 +q153 +q154 +q155 +q156 +q157 +q158 +q161 +q162 +q129 +q131 +q134 +q136 +q137 +q138 +q140 +csg-est-0238.csg +q149 +q159 +q163 +octopus1 +rim-076012.roslin +csg-fin-0149.csg +sas-alumni-0038.sasg +q111 +mvm-ri-d067177.roslin +csg-est-0178.csg +ds376 +mvm-ccbs-060439.ccbs +smtp-3 +smtp-4 +smtp-5 +sunny123 +ris-vlx09.roslin +bizet +csg-cse-0019.csg +beaufort +billold +sci060.scieng +mvm-ri-i045241.roslin +ris-vwindev.roslin +mvm-ri-l115156.roslin +mvm-ri-d064175.roslin +www-tmptest.star.euclid +pop.be +mvm-ccns-0092.ccns +sas-alumni-0027.sasg +csg-est-0068.csg +mfe2 +mvm-ri-d136024.roslin +csg-fin-0189.csg +malcom +csg-as-0000834.csg +mddb +ds243 +mvm-ccns-0106.ccns +sas-alumni-0022.sasg +ds303 +ds317 +ds326 +ds346 +ds360 +mx-17 +ds392 +ds394 +mx-20 +ds411 +ds445 +ds450 +ds457 +ds458 +ds459 +ds461 +ds462 +ds463 +ds464 +ds465 +ds466 +ambro +ds500 +ds501 +hca-netprint-col1.shca +babylab3.ppls +db17 +hss-ppls-0137.ppls +mx-5 +mx-6 +mx-7 +mx-8 +mx-9 +csg-sss-0002.csg +webmail.server +webmail.corp +csg-fin-0140.csg +csg-as-0000774.csg +mvm-ccns-0101.ccns +sas-alumni-0016.sasg +hss-hca-0040.shca +mvm-ri-l106197.roslin +ris-dxi01.roslin +csg-fin-0080.csg +rim-096009.roslin +mvm-ccns-0085.ccns +www.fantasyfootball +sas-alumni-0011.sasg +csg-est-0091.csg +csg-eusu-0009.csg +mvm-ri-m116133.roslin +csg-fin-0030.csg +mvm-ccns-0079.ccns +vm153 +vm154 +csg-fin-0090.csg +sas-alumni-0005.sasg +www-dev.pod.drps +hss-health-l32.health +cam-mac008.sasg +rid-046157.roslin +hss-health-0017.health +www-dev.ess.euclid +mvm-ri-m115200.roslin +cam-mac003.sasg +mvm-ccns-0068.ccns +sce-coll-0055.scieng +hybridb.scifun +sas-ssp-0007.sasg +riv-vc03.roslin +www-test.pubs.recordsmanagement +hss-ppls-0149.ppls +mvm-ri-d117173.roslin +mvm-ri-d087114.roslin +mvm-ccns-0052.ccns +mvm-ri-i085236.roslin +vds25 +csg-hr-0044.csg +dcn040225.ccbs +health-omq-016.health +rii-105173.roslin +hss-iad-0026.iad +mvm-ri-d116239.roslin +sm790.ccns +oc-1-221-mfp-col.sasg +csg-cse-0051.csg +psy-g7-pr.ppls +mvm-ri-d127172.roslin +csg-cse-0001.csg +mvm-ri-d134254.roslin +hss-ppls-0099.ppls +hca-jpglab-035.shca +mvm-ri-d064165.roslin +mvm-ri-d096053.roslin +www.1000 +hss-health-0124.health +ppls-igel-04.ppls +hcanda-laptop12.shc +mvm-ccns-0029.ccns +hca-jpglab-030.shca +mvm-ri-l115164.roslin +hca-jpglab-024.shca +g2ctm1.ccbs +ebr-bcd1.roslin +mainst +int-usbmac4.sasg +vm21 +csg-est-0237.csg +g2cweb2.ccbs +maimai +musicmusic +hss-health-0086.health +hca-rc-016.shca +mvm-ri-v115246.roslin +ppls-printer1.ppls +db15 +db16 +mailbe9r.staffmail +freekey +ppls-igel-6-01.ppls +hca-rc-011.shca +hca-spglab-046.shca +hca-jpglab-013.shca +mvm-ri-d126000.roslin +idb1 +hss-ppls-0143.ppls +vm157 +cam-mac009.sasg +hca-rc-005.shca +hca-spglab-041.shca +laser20.roslin +qiwi +mvm-ccbs-060437.ccbs +ris-esx02.roslin +hss-health-l67.health +alerte +mvm-ri-d096119.roslin +www.mbe +correio1 +mvm-ri-l115131.roslin +freefun +rip-e02m4.roslin +ebrl065182.roslin +pps078.csg +mvm-ccbs-060432.ccbs +csg-est-0177.csg +www.ww2 +maheen +mahaba +hss-health-l29.health +appleroam.csg +dailydeals +aswebcast.csg +ris-esxi10.roslin +jds +skb +chan086169a.roslin +hca-spglab-030.shca +mvm-ri-d107219.roslin +ris-vlxftp.roslin +mvm-ccbs-060421.ccbs +csg-est-0067.csg +mvm-ccbs-040220.ccbs +testfms +p-20 +mvm-ri-d125234.roslin +p-52 +csg-fin-0188.csg +csg-as-0000833.csg +honeywell +hss-health-l54.health +vongola +precision.lts +www.bq +mvm-ccbs-060415.ccbs +maffia +www.icc +www.goto +snejana +csg-as-0000773.csg +sra-57gsq-g-203.isg +babylab.ppls +hss-ppls-0148.ppls +weir-g-corridor-mfp-bw.sasg +gxy +xuan +mvm-ri-d067081.roslin +hss-ppls-0218.ppls +membrane +ris-vwinprn.roslin +www.bioprocess +bye +csg-fin-0078.csg +cam-mac004.sasg +hca-spglab-007.shca +proxy.lib +wfxy +hss-health-l06.health +round +sas-dis-0028.sasg +mvm-ccbs-060404.ccbs +csg-eusu-0008.csg +hss-ppls-0213.ppls +csg-fin-0028.csg +hss-ppls-adl27.ppls +csg-as-0000663.csg +ppls-igel-1-17.ppls +mvm-ri-d086216.roslin +bahnhof +doc39 +hca-spglab-002.shca +sas-dis-0023.sasg +mvm-ccns-0070.ccns +hss-health-l78.health +hss-ppls-0197.ppls +hss-ppls-adl22.ppls +mikhail +health-lap01.health +www.alterego +data-nas1.ppls +sce-coll-0029.scieng +sas-intl-0059.sasg +sas-dis-0017.sasg +ebrl033183.roslin +hss-ppls-0202.ppls +horoskop +tcd +mvm-ri-i115102.roslin +khatri +hrs045.csg +sas-dis-0012.sasg +www.nrg +vanhelsing +hss-ppls-0186.ppls +maddox +mvm-ri-l107222.roslin +hss-ppls-0013.ppls +sas-dis-0006.sasg +hss-ppls-0181.ppls +ppls-mac032.ppls +macmac +hss-ppls-adl05.ppls +www.dpts.drps +svg +ppls-mac005.ppls +sas-dis-0001.sasg +hss-ppls-0175.ppls +hss-ppls-0154.ppls +ppls-mac026.ppls +www.gbs +ebri042174.roslin +sas-reg-0019.sasg +ustar +win98router1.ccns +mvm-ri-v095247.roslin +kristi +mvm-ri-d085034.roslin +amitkumar +arzt +csg-cse-0009.csg +dean-jvcs.scieng +hss-ppls-0170.ppls +csg-hr-0043.csg +relay8 +gfl065240.roslin +mvm-ri-d086121.roslin +ppls-mac021.ppls +alog +hss-iad-0025.iad +mvm-ri-d097077.roslin +www.works +sas-intl-0038.sasg +ebri073210.roslin +sci070.scieng +ppls-mac015.ppls +giac +csg-cse-0049.csg +cmsr +bolero +lbc-backup.ppls +mvm-ri-l115166.roslin +rii-115146.roslin +ppls-mac011.ppls +hss-health-0088.health +rip-brfm9.roslin +hss-ppls-0158.ppls +faeebook +scifunlaptop6.scifun +hamer +ppls-mac009.ppls +auditoria +intek +slv12 +sci115.scieng +hcanda-print13.shca +mvm-ri-d096144.roslin +mvm-ri-d136034.roslin +hss-ppls-0153.ppls +asterix2 +caretta +ppls-mac004.ppls +hss-ppls-0159.ppls +studio4 +csg-est-0239.csg +mvm-ri-i134156.roslin +wahed +ris-vwinlic.roslin +dongchang +csg-cse-0059.csg +ris-fas1a-bmc.roslin +taxa +tech-center +bondi +hss-ppls-0165.ppls +mysql2a +sas-intl-0058.sasg +vcs-126214a.roslin +gornik +labirint +ppls-igel-1-16.ppls +hss-ppls-0147.ppls +mvm-ri-i125030.roslin +ppls-pc151.ppls +int-usbmac8.sasg +kula +ucuprinter.ucu +webmail.exseed +vpsadmin +brune +mail-5 +mvm-ri-d136212.roslin +berk +fitnes +miha +hss-ppls-0142.ppls +zeman +int-usbmac3.sasg +hss-ppls-0136.ppls +babylab2.ppls +valeur +www.tqtelethon.dev +sas-intl-0033.sasg +hss-health-l42.health +mvm-ccns-srv1.ccns +hss-ppls-0131.ppls +hss-health-0027.health +csg-est-0206.csg +sas-intl-0036.sasg +csg-est-0236.csg +mvm-ccbs-060316.ccbs +mvm-ri-l127148.roslin +hss-ppls-0125.ppls +mvm-ri-v115199.roslin +idobsonvb.csg +mx1.email +sas-intl-0031.sasg +fcms +urist +csg-est-0176.csg +sas-sra-0036.sasg +hss-ppls-0119.ppls +csh-g-g3-col.csg +mvm-ri-l115182.roslin +elec1 +www.employment +csg-est-0180.csg +ebri064179.roslin +www.pds +sas-intl-0025.sasg +csg-est-0126.csg +www.wrd +www.dse +elec2 +www.elections +mvm-ri-i086180.roslin +arisa +sas-sra-0031.sasg +upanh +ppls-mac022.ppls +csg-hr-0053.csg +mvm-ri-l096146.roslin +ppls-g26-019.ppls +sas-intl-0019.sasg +csg-est-0066.csg +mvm-gf-l115163.roslin +sas-sra-0025.sasg +csg-fin-0187.csg +hss-ppls-0108.ppls +csg-as-0000832.csg +sas-intl-0014.sasg +ris-onelan03.roslin +csh-3-3.4b-sfp-bw.sasg +sas-scs-0018.sasg +sas-sra-0020.sasg +sce-coll-0031.scieng +health-omq-026.health +hss-ppls-0093.ppls +csg-as-0000772.csg +omaha +hss-ppls-0171.ppls +ppls-g26-008.ppls +chplap-bg.sasg +sas-intl-0008.sasg +sas-scs-0013.sasg +mvm-ri-d086181.roslin +csg-fin-0077.csg +mvm-ri-m115138.roslin +ppls-g26-003.ppls +hcanda-pc71.shca +s248 +sas-intl-0003.sasg +csg-eusu-0007.csg +mvm-ri-d077054.roslin +sas-scs-0007.sasg +sas-sra-0008.sasg +csg-fin-0027.csg +hss-ppls-0082.ppls +ebri003227.roslin +sce-coll-0004.scieng +www-test.estores.finance +hss-health-0134.health +sas-sra-0003.sasg +cse-46pleas-1-12.csg +phoenix.ppls +pc5155 +tuner +mvm-ri-l107187.roslin +sas-intl-0039.sasg +wwms235igel.shca +win98router2.ccns +ppls-kuhn.ppls +spgamers +tuhin +hss-ppls-adl01.ppls +mvm-ri-d126250.roslin +hss-ppls-0071.ppls +ppls-alistair.ppls +ppls-printer21.ppls +hss-ppls-0065.ppls +shahrukhkhan +ppls-igel-7-01.ppls +46pleas-g-fasic-mfp-bw.csg +ppls-printer15.ppls +charlotte.scieng +hss-ppls-0059.ppls +bigtree.ppls +ppls-mac027.ppls +hss-hca-0124.shca +ris-trac01t.roslin +lect-hca-007.shca +tsoft +kpage +ppls-printer10.ppls +hss-ppls-0054.ppls +ri-dpcs2.roslin +csg-est-wmds.csg +hss-health-l77.health +csg-hr-0042.csg +hss-ppls-0176.ppls +sas-dis-0002.sasg +www-test.research +sergik +hss-health-0135.health +dancersoul +sextube +hss-ppls-adl06.ppls +ppls-mac033.ppls +csg-pps-0013.csg +sas-reg-0070.sasg +toxik +fireblack +myuser +sce-coll-0005.scieng +srv097.csg +hss-ppls-0182.ppls +hss-ppls-0048.ppls +sas-dis-0007.sasg +mvm-ccbs-040161.ccns +saf081.csg +www-test-old.jobs +hss-ppls-0187.ppls +testing1234 +mvm-ri-d086182.roslin +hcanda-laptop14.shca +todoparatupc +stara +csg-est-0309.csg +hss-ppls-0193.ppls +health-omq-027.health +telia +frankel +adult-sex +sas-dis-0018.sasg +data-nas2.ppls +starnet1 +hss-iad-0024.iad +jira2 +mvm-ri-d125066.roslin +hss-ppls-adl23.ppls +discourse +hss-iad-0019.iad +csg-corp-0004.csg +mvm-ri-m115197.roslin +hss-ppls-0198.ppls +hss-hca-0113.shca +hss-ppls-0043.ppls +mvm-ri-d076050.roslin +spoof +csg-cse-0048.csg +misdev +csg-est-0070.csg +mvm-ri-d137119.roslin +mgp +hss-hca-0097.shca +hss-ppls-0037.ppls +spiel +apps.sps +hss-hca-0092.shca +cam-backup.sasg +sony1 +maildev +www-dev.admin.alumni.dev +hss-ppls-0032.ppls +csg-fin-0191.csg +mvm-ri-d136176.roslin +wrw-1z-19-mfp-bw.shca +hss-hca-0086.shca +hss-ppls-0026.ppls +ppls-atl01test.ppls +srv-dht-ground-servitors.csg +gu206hotdesk.lts +hss-hca-0081.shca +mvm-ri-v086048.roslin +www.overseas +hss-ppls-0021.ppls +hss-health-l16.health +hss-health-0002.health +gwy +2004 +hss-hca-0075.shca +srv-lap6.isg +autonation +hss-ppls-0015.ppls +www.logistics +www.imode +sas-princ-0004.sasg +rip-a00m4.roslin +calvintemp.ppls +hss-hca-0070.shca +mvm-ri-d137048.roslin +mvm-ccbs-060434.ccbs +lovesex +hss-ppls-0010.ppls +sce-coll-0040.scieng +console1 +j8017e.ccbs +hss-ppls-0095.ppls +recarga +csg-cse-0027.csg +hss-hca-0064.shca +hss-ppls-0004.ppls +iad-mac003.iad +www-dev.rssjobs.careers +mvm-ri-d096225.roslin +sofia2 +vcs-127052a.roslin +mvm-ri-d117157.roslin +csg-est-0125.csg +evn +hss-hca-0053.shca +drumstanex-g-g4-mfp-col.csg +srb +health-omq-001.health +aaao +abc3 +rid-086208.roslin +sas-dis-0024.sasg +kzn +acci +csg-fin-0186.csg +hss-hca-0047.shca +csg-as-0000831.csg +mvm-gf-l115164.roslin +ebrgeldoc.roslin +hss-ppls-adl28.ppls +mvm-ri-l127052.roslin +vsys +wlf +hss-hca-0042.shca +hss-ppls-0214.ppls +giaitri +bellagio +csg-as-0000771.csg +mvm-ccns-0078-vm2.ccns +aap070.sasg +hss-hca-0036.shca +cbweb +berserker +fry +l114 +prodaja +sas-dis-0029.sasg +plavi +l203 +l206 +l207 +mvm-ri-l125095.roslin +csg-srs-0005.csg +hss-health-0108.health +11infst-g-genoffice-mfp-col.csg +csg-fin-0097.csg +zajecar +aaa2 +ris-igel1.roslin +hss-ppls-0100.ppls +csg-fin-0026.csg +hss-hca-0031.shca +csg-as-0000661.csg +tatties.cache +hns5 +hns6 +aap058.sasg +www.apps.disability-office +csg-fin-0088.csg +www.audyt +www.artykuly +9hpsq-printer1.csg +hss-ppls-0219.ppls +truffaut +hss-hca-0025.shca +mvm-ccbs-060411.ccbs +linkbuilding +zenek +lib-pc-1713.isg +maile +www.linkbuilding +audyt +ppls-y2lab-212.ppls +www.omniping +sce-coll-0066.scieng +hss-hca-0020.shca +beverly-pc.ppls +ppls-y2lab-206.ppls +ram-marg-122.ccbs +hss-hca-0014.shca +bundesliga +mvm-ccbs-060135.ccbs +hca-netprint-bw7.shca +nickolas +predators +sci177.scieng +int-scanner-01.sasg +ppls-y2lab-201.ppls +csg-as-0000783.csg +raiders +www.testa +vis009.sasg +vcs-126149a.roslin +sas-cas-0087.sasg +hss-ppls-0190.ppls +bslt +tawan +caba +hss-health-l13.health +ppls-et1.ppls +hss-hca-0008.shca +ruchi +sci018.scieng +ccbb +hss-health-l52.health +hca-netprint-bw2.shca +wlxt +sas-scs-0009.sasg +ris-lx02.roslin +hss-hca-0003.shca +mvm-ri-l107090.roslin +xbserver +mvm-ri-d107204.roslin +jwxt1 +cfs1 +cgeng.ppls +hss-iad-0023.iad +sas-cas-0071.sasg +www.vacancies +csg-cse-0047.csg +sec07.roslin +nptel +ccnsm075.ccns +csg-fin-0148.csg +sas-cas-0065.sasg +incubation +bret +dns.ee +hca-lab3-mac23.shca +ms-dw6-1m-8-mfp-bw.health +mvm-ri-d116082.roslin +homesecurity +www-test.tqmobile.dev +mvm-ri-m135039.roslin +sas-cas-0060.sasg +hca-lab3-mac17.shca +csg-est-0027.csg +municipios +siwar +tamia +mvm-ri-l127149.roslin +csg-fin-0209.csg +sas-cas-0054.sasg +mvm-ccbs-060416.ccbs +www.clips +mvm-ri-d115148.roslin +46pleas-g-communications-mfp-col.csg +www.textads +www.feed +hca-lab3-mac12.shca +csg-saf-0034.csg +csg-est-0315.csg +romuald +mvm-ri-d137023.roslin +csg-as-0000843.csg +www.scs.euclid +shared3 +simmi +liz +sci125.scieng +sce-coll-0014.scieng +cerc-d001.roslin +mvm-ri-l115222.roslin +www-test.admin.careers +csg-fin-0198.csg +www.ppmd.euclid +csg-est-0284.csg +mvm-ri-v107133.roslin +pie1.lts +www.kuchnia +sidra +centrala +maret +mvm-ri-d076059.roslin +sec05.roslin +shiba +mvm-ri-d125147.roslin +shera +courseware +www.jt +cwm1 +sas-cas-0037.sasg +dog1 +int-jet13.sasg +med-000847a.roslin +mvm-ri-l107196.roslin +csg-est-0234.csg +csg-fin-0206.csg +hss-health-0028.health +www-dev.transparencyadmin.fec +rifranking.roslin +hss-health-0089.health +www.advertise +csg-est-0174.csg +ppls-y2lab-129.ppls +sas-cas-0026.sasg +ris-valx01.roslin +ppls-y2lab-124.ppls +vbs-194252a.roslin +pwb +sas-cas-0021.sasg +sheen +doc22 +mvm-ri-d115211.roslin +csh-g-g21-mfp-col.csg +csg-est-0064.csg +www.dba +csg-est-0077.csg +ppls-y2lab-118.ppls +mvm-ri-d097062.roslin +csg-fin-0185.csg +sas-cas-0015.sasg +www.edunet +drumstanex-g-office-mfp-bw.csg +csg-as-0000829.csg +fxnavi +sci160.scieng +www.edm +hss-health-l43.health +www.dof +www.dial +sci054.scieng +hss-ppls-0139.ppls +www.eoc +www.tupc +ppls-y2lab-113.ppls +happykids +www.goodtime +geo-cc-003.scieng +sas-cas-0010.sasg +csg-est-0137.csg +lenova +ronja +esig +www.hac +csg-as-0000770.csg +summerfun +www.urbandesign +ppl001.sasg +docweb +www.look +wwms00m05igel.shca +mail.bote +www.antivirus +win2000 +mvm-ri-d126187.roslin +mvm-ri-d096128.roslin +romel +happyhome +hca-spglab-031.shca +setan +www.lda +hca-jpglab-003.shca +ris-vlxweb01.roslin +ppls-y2lab-107.ppls +csg-fin-0075.csg +www.dome +sas-cas-0004.sasg +flea +csg-eusu-0005.csg +www.course-bookings.lifelong +www.pbs +ppls-y2lab-102.ppls +ftp.bote +gem1 +csg-as-0000659.csg +mvm-ri-d064184.roslin +ccnsm003.ccns +1brsq-1-staffarea-mfp-col.sasg +hca-spglab-036.shca +sweetp +csg-fin-0190.csg +www.mso +www.hoping +mvm-ri-d086058.roslin +sam-test.roslin +wrk-jet32.csg +sas-chap-0002.sasg +www.tct +mvm-ccbs-060020.ccbs +hss-health-l26.health +www.zone +csh-g-g6-mfp-col.csg +www.sec +hss-health-0012.health +isw +www.landp +www.tac +sas-cam-0013.sasg +vis013.sasg +rid-057084.roslin +www.tec +www.land +viera +www.xinyi +www.msdn +health-mac006.health +sas-reg-0140.sasg +sas-cam-0025.sasg +sci161.scieng +www.hw +vis007.sasg +hca-rc-001.shca +ncm +tmu-g-office-mfp-bw.csg +fatimah +www.retire +sccm13 +sas-cam-0019.sasg +csg-hr-0039.csg +rnd01 +leland +www.kolkata +ms-dw6-1-genoffice-mfp-col.health +sccm14 +sccm16 +csg-est-0247.csg +vis002.sasg +hss-iad-0022.iad +sas-cam-0014.sasg +www.hyderabad +hca-spglab-042.shca +cas101.sasg +sce-coll-0050.scieng +offprinter1.scifun +www.delhi +csg-cse-0046.csg +sas-cam-0008.sasg +48pleas-1-union-mfp-col.csg +sardi +ris-boxi.roslin +hca-rc-006.shca +rii-105167.roslin +myadm +mvm-ri-i134157.roslin +sas-cam-0003.sasg +scifundock.scifun +g2cwebdev1.ccbs +ppls-igel-b-21a.ppls +rstest +ris-ilx02.roslin +hca-jpglab-014.shca +jtinspiron.ccns +mvm-ri-d136035.roslin +ipad3 +mvm-ri-d086165.roslin +hca-spglab-047.shca +sccm15 +mvm-ri-d107210.roslin +erecording.lts +mvm-ri-d077038.roslin +sci100.scieng +hca-rc-012.shca +j23 +hula +oin +ois +ntn +sek +mvm-ri-d134248.roslin +mvm-ri-l115186.roslin +hss-health-0118.health +hca-mfd-007.shca +gaus +mvm-ri-d076095.roslin +elvs01sq01.roslin +hca-mfd-002.shca +dsb-lptp-4.ppls +csg-est-0283.csg +eniac +samia +scifunlaptop7.scifun +www-dev.org.planning +mvm-ri-d125093.roslin +ppls-pc7.ppls +www.alumni.dev +csg-est-0233.csg +ghosting01.roslin +ppls-y2lab-014.ppls +csg-est-0187.csg +csg-est-0173.csg +ppls-y2lab-008.ppls +csg-est-0123.csg +uranio +hca-jpglab-019.shca +lect-hca-008.shca +ppls-y2lab-003.ppls +hss-health-0090.health +1a +hss-health-l62.health +7gs-g-26-mfp-bw.ppls +saini +csg-est-0171.csg +hca-rc-017.shca +ppls-carver-01.ppls +csg-est-0063.csg +lect-hca-003.shca +ris-esxi04.roslin +csg-fin-0184.csg +csg-as-0000828.csg +health-omq-010.health +mvm-ri-d096103.roslin +mvm-ri-l127168.roslin +mvm-ri-l097109.roslin +hca-jpglab-025.shca +lax1 +lect-health-003.health +lccu +hca-escreen-04.shca +csg-fin-0134.csg +csg-hr-0078.csg +csg-fin-0074.csg +www.adminrae.planning +mvm-ri-l097130.roslin +csg-eusu-0004.csg +tmp-health-004.health +csg-fin-0024.csg +mvm-ccns-0025.ccns +riley +sevendays +csg-as-0000658.csg +mvm-ri-m135049.roslin +gfl035237.roslin +www-test.pure +www.oma +ppls-zak.ppls +uhs001.sasg +hss-health-l01.health +rii-115142.roslin +products1 +ris-vbiolx01.roslin +phi-aristotle2.ppls +psy-pc023.ppls +sce-coll-0024.scieng +sas-bu-0052.sasg +csg-fin-0169.csg +mvm-ri-d106209.roslin +fastservice +ris-ifs2.roslin +ppls-printer7.ppls +mvm-ri-d136089.roslin +c85 +c88 +rid-086179.roslin +mailbe11r.staffmail +abb +csg-hr-0038.csg +abi +mvm-ri-m126200.roslin +hss-iad-0021.iad +sas-bu-0041.sasg +acv +csg-as-0000859.csg +csg-cse-0045.csg +hss-ppls-0145.ppls +sas-bu-0035.sasg +hss-hca-0030.shca +vcs-126157a.roslin +www-test.pubsadmin.recordsmanagement +agp +agr +rodas +mvm-ri-d116198.roslin +mvm-ri-d067071.roslin +sweetgirls +mvm-ri-d126030.roslin +csg-fin-0208.csg +kazanova +rip-brfm4.roslin +csh-b-b1.5-mfp-bw.csg +sas-bu-0040.sasg +csg-est-0061.csg +sas-bu-0029.sasg +www.eit.finance +sci064.scieng +bff +sas-bu-0024.sasg +mvm-ri-l115161.roslin +aom +aoo +ppls-deploy-01.ppls +rii-115141.roslin +lect-hca-005.shca +mvm-ri-d125086.roslin +mvm-ri-l126093.roslin +ml-3-42-mfp-col.sasg +hca-tlab-022.shca +sas-bu-0018.sasg +mvm-ri-d096138.roslin +mvm-ri-l107145.roslin +riv-tpiaud.roslin +mvm-ri-i127149.roslin +rachael.ppls +bkn +atu +hca-tlab-016.shca +sas-bu-0013.sasg +avi +cas-mlb3-012.sasg +mvm-ri-l125159.roslin +axe +hca-tlab-011.shca +gfl065241.roslin +sas-bu-0007.sasg +hss-hca-0109.shca +mvm-ri-l115185.roslin +bpr +hca-jpglab-031.shca +nordine +mvm-ccns-0031.ccns +cas-mlb3-006.sasg +riv-hd1.roslin +googlemoney +hss-health-0030.health +pwn3d +hca-jpglab-036.shca +vweb.ppls +ema4.ppls +hca-tlab-005.shca +sas-bu-0002.sasg +csg-est-0232.csg +hss-ppls-0089.ppls +strozzapreti.ppls +cas-mlb3-001.sasg +beograd +iad-1208.iad +hss-ppls-0040.ppls +csg-est-0172.csg +hss-health-l36.health +mailbomber +ris-vwlx02.roslin +hss-health-0022.health +stpeter +rid-076052.roslin +mvm-ri-d125025.roslin +csg-est-0122.csg +csg-est-0062.csg +mvm-ri-d105052.roslin +sce-coll-0059.scieng +csg-cse-0002.csg +dus +enc +csg-fin-0183.csg +csg-as-0000827.csg +www-test.employerdatabase.careers +ebri063215.roslin +ebrptdmr.roslin +www-dev.drps +mvm-ccns-0036.ccns +gab +tehnologija +ers +csg-fin-0133.csg +mvm-ri-d096245.roslin +zerberus +hss-hca-0104.shca +cse-kiosk2.csg +sas-cam-0009.sasg +csg-fin-0073.csg +mvm-ri-v105134.roslin +csg-srs-0002.csg +csg-eusu-0003.csg +handc-mhist17.shca +eyi +csg-fin-0023.csg +ghh +fse +health-omq-021.health +redif +ris-trac01.roslin +ftr +mvm-ri-d094162.roslin +punit +gmx +gon +gpl +sas-reg-0106.sasg +handc-mhist12.shca +mvm-ri-d086175.roslin +www-test.wiki +csg-cse-0011.csg +ifl +ebri053171.roslin +hss-health-0128.health +handc-mhist01.shca +sas-reg-0190.sasg +jaz +mvm-ri-d067021.roslin +hss-ppls-adl10.ppls +ebrsqlsrv1.roslin +raven.ppls +sas-reg-0184.sasg +itl +hss-hca-0088.shca +data-nas1b.ppls +sas-reg-0178.sasg +sce-coll-0020.scieng +speedgroup +jmr +csg-hr-0037.csg +jmx +vbs-194198a.roslin +sas-cas-0082.sasg +hss-iad-0020.iad +kme +kml +sas-reg-0173.sasg +sas-intl-0056.sasg +koi +reg-jet1.csg +syndicate +hss-ppls-0028.ppls +mvm-ri-d126004.roslin +sas-reg-0091.sasg +mvm-ccns-0047.ccns +csg-cse-0044.csg +mvm-ri-i064167.roslin +mvm-ri-d095105.roslin +sas-reg-0167.sasg +sci038.scieng +hss-health-l72.health +sas-reg-0162.sasg +mvm-ri-d096113.roslin +kindzadza +mvm-ri-d107224.roslin +wwms227igel.shca +sas-reg-0156.sasg +csg-as-0000684.csg +dragonz +health-lap66.health +dragoon +cor-jet3.csg +dragon2 +csg-est-0255.csg +thunderbolt-display2.scieng +www-uat.star.euclid +sas-reg-0151.sasg +mvm-ri-d064168.roslin +mvm-ri-l125134.roslin +prc +haymarket +rca +csg-hr-0054.csg +mvm-ri-i115103.roslin +mvm-ri-d084163.roslin +raged +mvm-ri-d125238.roslin +hss-hca-0083.shca +rog +rii-087001.roslin +sets.sps +wwms236igel.shca +tfc +sas-reg-0145.sasg +sas-leaps-0007.sasg +csg-hr-0070.csg +csg-fin-0050.csg +mvm-ccns-0053.ccns +mvm-ri-d087154.roslin +swt +sce-eccc-0001.scieng +eplab1.ppls +sas-reg-0139.sasg +sas-leaps-0002.sasg +tash +hillier-mac.sasg +sas-reg-0085.sasg +health-pc94.health +hss-health-l11.health +csg-est-0231.csg +mvm-ri-d115168.roslin +www-dev.estores.finance +tornado.ee +sas-reg-0128.sasg +xio +g2cdb1.ccbs +capturedroslin01.roslin +sce-coll-0034.scieng +csg-est-0121.csg +qihaa +sas-reg-0123.sasg +www-test.announce.myed +sowa +mvm-ri-l127135.roslin +mvm-ri-d095098.roslin +autodiscover.exseed +sas-reg-0117.sasg +dishnetwork +cheapshopping +csg-fin-0182.csg +emanuel +ebri053213.roslin +csg-as-0000826.csg +www-test.alumni.dev +percheron +atlantia +mvm-ri-l107226.roslin +www.moregames +hss-ppls-0203.ppls +pspinfo +castillo +adcity +sas-reg-0112.sasg +prash +llano +mvm-ri-d067024.roslin +hss-health-0103.health +psy-adam-moore.ppls +en-macmini.ppls +mvm-ri-d117137.roslin +sas-reg-0096.sasg +cse-kiosk1.csg +csg-fin-0072.csg +sas-reg-0080.sasg +www-dev.ppmd.euclid +csg-pps-0014.csg +www.artdesign +csg-srs-0001.csg +ris-vwinwja.roslin +srv098.csg +hca-mac043.shca +yourfuture +sas-reg-0101.sasg +ponco +mvm-ri-d085106.roslin +vangogh +www.interior +tran +brawler +www.thehacker +www.oblivion +milagro +csg-fin-0022.csg +geac-annex +www.patty +hca-mac037.shca +www.ambiente +csg-saf-0008.csg +www.classics +mvm-ccns-0064.ccns +sci074.scieng +rii-105220.roslin +mvm-ccns-0069.ccns +sce-coll-0052.scieng +ppls-mac-jk.ppls +travelguide +lib-pc-1708.isg +sas-alumni-0049.sasg +hss-health-0093.health +sas-alumni-0001.sasg +mvm-ri-m125235.roslin +cam-mac010.sasg +arcl-hpgen7.shca +hca-mac032.shca +csg-corp-0005.csg +sas-reg-0079.sasg +aec +psy-kiosk01.ppls +am5 +ags +uren +ajv +fuckers +sas-dis-0013.sasg +bel +kogepan +gfl075243.roslin +hca-mac026.shca +sas-reg-0074.sasg +csg-saf-0026.csg +estates.csg +ril-115151.roslin +hca-mac021.shca +sas-reg-0068.sasg +hca-tlab-009.shca +scimac.scieng +hca-mac015.shca +sas-reg-0063.sasg +vhost01.scieng +rip-b01m4.roslin +hca-mac009.shca +sas-reg-0057.sasg +csg-hr-0036.csg +ebri023184.roslin +geo-cc-006.scieng +hss-iad-0018.iad +fun-zone +anarosa +mvm-ri-v086078.roslin +www.crb +cnd +hss-health-l46.health +coi +dgc +hca-mac004.shca +sas-reg-0052.sasg +www-test.org.planning +hss-health-0032.health +csg-cse-0043.csg +leven +sas-alumni-0006.sasg +mvm-ri-m115214.roslin +osoft +lisa.ppls +csw +www-test.dlhe.careers +sas-reg-0046.sasg +chocolates +mvm-ccns-0081.ccns +mvm-ri-d077055.roslin +rip-c01m3.roslin +csg-pps-0030.csg +health-lap63.health +www-dev.transparency.fec +sas-reg-0041.sasg +ebri043221.roslin +sas-cas-0080.sasg +fcl +mvm-ri-l125108.roslin +green5 +pinki +fdp +sas-reg-0035.sasg +llc-staff-pc2.shca +sas-alumni-0012.sasg +mvm-ri-l137152.roslin +ycfc +mvm-ri-l086031.roslin +sas-reg-0030.sasg +mvm-ri-d127018.roslin +sas-reg-0024.sasg +health-omq-031.health +lafamilia +csg-fin-0109.csg +mvm-ccns-0086.ccns +wsprueba +mvm-ri-d107221.roslin +gic +eri267.roslin +wrk014.csg +csg-est-0280.csg +sas-reg-0018.sasg +hdc +gnp +gox +csg-est-0205.csg +theparty +csg-est-0229.csg +sas-reg-0013.sasg +sce-coll-0008.scieng +hrh +philo +jac +jad +rii-045132.roslin +mvm-ri-l115216.roslin +salavirtual +htt +csh-3-3.7-sfp-bw.sasg +ppls-polar2.ppls +sas-reg-0007.sasg +ucsisa2.csg +php10 +csg-hr-0049.csg +jhs +hss-ppls-0039-dsb.ppls +sas-reg-0002.sasg +lect-hca-001.shca +jmc +shilo +jmj +mvm-ri-d076041.roslin +csg-est-0059.csg +csg-fin-0181.csg +csg-as-0000825.csg +aberdour.ee +nylon +ccslaptop.scifun +kmh +satin +laxman +csg-fin-0100.csg +ldu +ris-vlxweb04.roslin +csg-est-0009.csg +csg-fin-0131.csg +mvm-ri-l127120.roslin +mlh +csg-as-0000765.csg +mvm-ri-d096109.roslin +vbs-194232a.roslin +mvm-ri-l117190.roslin +www-test.rae.planning +mui +most-wanted +bettyboop +ris-115173.roslin +ppls-printer6.ppls +health-omq-011.health +csh-g-g12-sfp-bw.csg +csg-fin-0071.csg +mvm-ri-d107056.roslin +7brsq-g-1.204-mfp-col.iad +www.hesa.star.euclid +ebri073178.roslin +pcl +omf +ris-esxi11.roslin +www.electronica +www.electronics +precor-demo.csg +mvm-ri-l127049.roslin +sas-alumni-0033.sasg +sty001.csg +csg-saf-0057.csg +raz +www-test.etime.finance +rdx +csg-fin-0021.csg +mvm-ccns-0102.ccns +sas-reg-0039.sasg +rix +www-test.reward.humanresources +sel +health-omq-013.health +mvm-ri-l097129.roslin +www-dev.course-bookings.lifelong +uncharted +mvm-ri-d057106.roslin +at-g-office-mfp-bw.csg +ebri013160.roslin +www-dev.pubsadmin.recordsmanagement +mvm-ri-d064178.roslin +www.blackfire +tce +amusement +csg-as-0000784.csg +csg-as-unitots1.ppls +mvm-ri-l096186.roslin +hca-mac010.shca +ssn +mvm-ri-d076232.roslin +mvm-ri-m135070.roslin +uea +hss-health-0119.health +mvm-ri-d117233.roslin +mvm-ri-v125248.roslin +www-test.student-experience +hss-health-0100.health +hss-health-l21.health +dsb-et2.ppls +vll +csg-est-0085.csg +csg-fin-0150.csg +hss-health-l68.health +mvm-ri-d115178.roslin +hss-health-0006.health +wse +www.hugoboss +emasonmac.ppls +www.boxing +pcline +csg-hr-0019.csg +maquinaria +csg-hr-0035.csg +jayaprakash +saf074.csg +smartcom +andrade +hss-iad-0017.iad +mvm-ri-l107058.roslin +www-test.eves.myed +elshady +scieng2.scieng +health-hopepark-print1.health +ris-esx03.roslin +csg-cse-0042.csg +hss-hca-0110.shca +pauls +sweetdreams +cubeworld +sce-coll-0044.scieng +sprinkler +hss-hca-0049.shca +mvm-ri-l096125.roslin +www.explore +gfl035236.roslin +acac +mbplvdev2.ccns +xeroxbd.ccbs +mvm-ri-l077167.roslin +csg-fin-0200.csg +www.ess.euclid +csg-fin-0171.sasg +www.wonderful +vcs-167225a.roslin +sas-alumni-0059.sasg +health-omq-005.health +mvm-ccns-0096.ccns +aila +juanjo +yingyang +sas-alumni-0054.sasg +mvm-ri-d077041.roslin +sas-alumni-0048.sasg +vbs-19475a.roslin +amix +alvi +csg-est-0278.csg +g2csrv1.ccbs +mvm-ri-l115181.roslin +mvm-ccns-0107.ccns +hss-health-0113.health +csg-as-0000844.csg +b4-et-host.ppls +hotwear +bing +paseo +caph +saltoftheearth +www-dev.star.euclid +avel +ceci +hss-ppls-0160.ppls +ppls-colwyn.ppls +csg-fin-0199.csg +sas-alumni-0037.sasg +mvm-ri-l107165.roslin +parin +ceus +axon +mvm-ri-d127112.roslin +csg-est-0168.csg +waverley-p4 +csg-sec-0032.csg +swanston-jvcs.scieng +cire +ppls-osxsrv-7gs.ppls +cita +csg-fin-0110.csg +dass +health-lap-029.health +sas-alumni-0032.sasg +tommy-lap.trg +mindgames +deck +khalifa +cpam +csg-est-0118.csg +dm4u +sas-alumni-0028.sasg +affirm.sps +csg-cse-0010.csg +webcam.scieng +risen +rim-086213.roslin +digitalnet +sas-alumni-0026.sasg +cssc +csg-est-0058.csg +zap178a.roslin +edam +csg-fin-0180.csg +starlive +csg-as-0000824.csg +cyds +mvm-ccns-0105.ccns +mvm-ccns-0091.ccns +sas-alumni-0021.sasg +csg-est-0008.csg +mvm-ri-d097031.roslin +faty +rbci083206.roslin +csg-fin-0130.csg +emailpro +mvm-ccns-0090.ccns +emis +epsa +fide +gabe +sysinfo +sas-alumni-0015.sasg +hss-health-l56.health +sahid +ccace-media-pc.ppls +esme +mvm-ri-l125020.roslin +garo +csg-fin-0069.csg +csg-fin-0096.csg +csg-est-0267.csg +www-dev.jams.finance +even +sas-alumni-0009.sasg +mvm-ri-d126156.roslin +gets +fora +habo +rim-117144.roslin +hass +csg-saf-0056.csg +betaversion +mvm-ri-l097104.roslin +csg-fin-0020.csg +csg-as-0000777.csg +cam-mac013.sasg +grin +mypix +mvm-ccns-0078.ccns +homa +csg-est-0138.csg +mvm-ri-l134245.roslin +jaco +noris +ikka +atl2-1 +jaza +sas-alumni-0004.sasg +cam-mac007.sasg +inso +csg-fin-0079.csg +iona +atl2-2 +cam-mac002.sasg +ipop +mvm-ri-l137162.roslin +irce +mvm-ccns-0067.ccns +kafu +csg-as-0000243.csg +waverley-p5 +myers +ghosting02.roslin +waverley-p3 +waverley-p1 +mvm-ri-m116086.roslin +mvm-ri-d127028.roslin +kosmetyki +saga3 +lalo +atl2-3 +atl2-4 +sas-intl-0060.sasg +beans.cache +sas-alumni-0034.sasg +koru +ebri093151.roslin +m.cod +sas-ssp-0006.sasg +mvm-ri-i065202.roslin +noman +m.mcp +health-omq-041.health +ris-hpcx01.roslin +eplab4.ppls +nerv +nexa +sas-cas-0076.sasg +mvm-ri-d086205.roslin +franking.sasg +www.wpc +m.vip +musi +mvm-ccns-0093.ccns +padi +oldcoll-2-corridor-mfp-col-1.csg +srv028.csg +blackdiamond +nagios2.ppls +www.coaching +mvm-ri-l127092.roslin +smartshop +www.star.euclid +gfl-117117.roslin +mvm-ri-m106026.roslin +rii-115122.roslin +opps +csg-hr-0034.csg +www.gameonline +sce-coll-0018.scieng +lindy.ppls +mvm-ri-i075201.roslin +hss-iad-0016.iad +pcscifun7.scifun +psic +mvm-ccns-0045.ccns +mvm-ri-l085157.roslin +resa +www.creativity +sec10.roslin +sape +csg-fin-0089.csg +csg-nad-003.csg +csg-cse-0041.csg +csg-est-0188.csg +rody +sert +mvm-ri-d096194.roslin +timeshare +rope +penelope.sie +shey +mutaz +statusuri +sige +sigi +csg-fin-0029.csg +sas-alumni-0040.sasg +is-apps-0094.isg +temp-wipe.ppls +mvm-ri-l137067.roslin +mvm-ccns-0034.ccns +herpderp +suka +gfl-117141.roslin +www.habbohotel +csg-saf-0049.csg +audiovisual +med-000407.isg +wars +www.forsale +uuuu +iroquois +munir +lordanime +pacwest +csg-cse-0029.csg +yari +xmac +ed1stcatering.csg +mvm-ccns-0080.ccns +hca-jpglab-028.shca +csg-est-0248.csg +zape +yiyo +zhan +newhaven-gss.scieng +jwhite +cocacolo +sas-alumni-0045.sasg +workforce +geos-d-0036.scieng +sisco +nitin +animaco +mvm-ri-l116088.roslin +tuki +topmovies +mvm-ri-d116203.roslin +sasg-oldcoll-g-foy-2.sasg +ppls-printer5.ppls +mvm-ccns-0023.ccns +mvm-ccbs-060453.ccbs +mundodigital +ppls-psylib-02.ppls +hca-jpglab-023.shca +g2cweb1.ccbs +hca-rc-015.shca +www.megaupload +hca-jpglab-017.shca +www.portugal +delarosa +dextroyer +www.steven +soporteinformatico +mvm-ri-l115155.roslin +www.atlantis +g2csrv3.ccbs +hca-rc-010.shca +shinyshop +nikky +jewelery +emporio +hca-spglab-045.shca +ris-vblx04.roslin +mvm-ccbs-060442.ccbs +postales +hca-jpglab-012.shca +mvm-ri-d076064.roslin +www.salfor.finance +timetoshine +hca-rc-004.shca +dontask +mrgud +esolution +www.muzica9 +hca-spglab-039.shca +www.chistes +bendice +vis-hp1.sasg +megamanx +moxie +hss-health-0125.health +mvm-ri-i135018.roslin +hca-jpglab-006.shca +sas-reg-0029.sasg +csg-est-0308.csg +help.sps +sas-alumni-0051.sasg +mvm-ri-d134255.roslin +monop +hca-spglab-034.shca +mvm-ri-d115189.roslin +cateringopsprint.csg +hca-jpglab-001.shca +ppls-util.ppls +ebrm073200.roslin +bahia +socialnet +mvm-ri-d115021.roslin +balde +wrw-g-8-mfp-bw.shca +webmail.beta +dsb-lptp2.ppls +www.habboretro +monal +artecultura +www.totalwar +csg-est-0117.csg +ipv4.beta +devreports +ns.beta +star4ever +ppls-g26-test.ppls +csg-est-0057.csg +mvm-ccbs-040218.ccbs +www.readrae.planning +abelnf +csg-fin-0178.csg +www.literature +contractors +www.smartnet +csg-as-0000823.csg +mvm-ri-i135017.roslin +hca-spglab-017.shca +lync2010 +hss-health-0016.health +mvm-ccbs-060414.ccbs +snafu +csg-fin-0128.csg +edgesight +ris-fasdev.roslin +darkhacker +reg-oc-ho.sasg +elitesports +www.cristina +pfp +health-mac011.health +hca-jpglab-008.shca +hca-spglab-012.shca +mdi +kinera +rock-2 +antonyo +sas-dis-0033.sasg +vbs-lap1948a.roslin +blackbear +hss-ppls-0217.ppls +actecs +csg-fin-0068.csg +mocco +sas-alumni-0056.sasg +sce-coll-0054.scieng +hybrida.scifun +www.shakira +sas-dis-0027.sasg +mvm-ccbs-060403.ccbs +pat0.roslin +edunet +hss-ppls-0212.ppls +eltigre +csg-saf-0055.csg +csg-fin-0176.csg +csg-fin-0018.csg +calis +campi +hss-ppls-adl26.ppls +lukka +artstudio +mvm-ri-i066099.roslin +csh-2-2.15-bw.csg +www.protektor +luken +thelion +vbs-laptop01a.roslin +www.playgames +hss-ppls-0206.ppls +importaciones +www.alexgames +wrk-laptop2.csg +hss-ppls-adl21.ppls +www-test.transparency.fec +sas-dis-0016.sasg +deg +hss-health-0096.health +happyfeet +hss-ppls-0201.ppls +ppls-trans.ppls +refused +20gilmerton-g-office-mfp-col.csg +nazia +rayitodeluz +habboweb +www.parking +health-omq-015.health +sas-dis-0011.sasg +www.fotoalbum +www.shemale +borax +hss-ppls-0185.ppls +bienestar +zyngachips +health-omq-017.health +brokendreams +mvm-ri-d077115.roslin +looking +nasir +hss-ppls-adl09.ppls +m.rai +ris-vlxrt.roslin +sas-dis-0005.sasg +hss-ppls-0180.ppls +puzzles +bandas +www-tmpdev.star.euclid +darkblood +ms-dw6-2-15-mfp-col.health +mvm-ccbs-040169.ccbs +ppls-mac031.ppls +pib +mk13 +csg-est-0279.csg +habbonet +mk2 +nandi +hss-ppls-adl04.ppls +mvm-ri-d077043.roslin +mvm-ri-d134253.roslin +baster +opensocial +ppls-pc159.ppls +miron +alerta +hss-ppls-0174.ppls +csg-cse-0012.csg +skyonline +myshopping +www.practicas +alivio +tokunaga +mvm-ri-d096242.roslin +mvm-ri-l115191.roslin +ppls-mac025.ppls +flamenco +hss-health-0123.health +ppls-pc154.ppls +www.transformice +minna +amigas +hss-ppls-0168.ppls +csg-hr-0033.csg +teenchat +relojes +mvm-ri-d095131.roslin +nadav +numismatica +kampala +ppls-mac019.ppls +www-test.miniportfolio.euclid +casagrande +buenavista +mvm-ri-l107175.roslin +hss-iad-0015.iad +mvm-ri-d137169.roslin +kaffee +ppls-mac014.ppls +ebri063164.roslin +csg-cse-0040.csg +webmastertools +sas-intl-0037.sasg +antrax +ecci-jh.scieng +rim-115171.roslin +mvm-ccns-0063.ccns +hss-ppls-0157.ppls +csg-fin-0219.csg +seguros +ppls-mac008.ppls +oldcoll-2-corridor-mfp-col.csg +www.lacoste +sbmacbook.ppls +appsgt +www.cityweb +sas-intl-0063.sasg +www.universe +sce-coll-0056.scieng +hss-ppls-0152.ppls +arceus +ppls-mac003.ppls +hss-iad-0037.iad +www.employerdatabase.careers +sas-intl-0057.sasg +bda +minaret.ppls +webmsn +mvm-ri-l094187.roslin +space3 +ris-esx01.roslin +hss-ppls-0146.ppls +sas-mac001.sasg +jzj +sci033.scieng +www.rae.planning +eulib +old.search +csg-est-0230.csg +hss-health-l66.health +int-usbmac7.sasg +loyal +mailwfe5.staffmail +mailwfe7.staffmail +mvm-ri-i005076.roslin +hss-ppls-0141.ppls +int-usbmac2.sasg +mvm-ri-d096107.roslin +www-dev.secure.vle +mvm-ri-d127244.roslin +cen +reza1 +csg-hr-0055.csg +sas-intl-0046.sasg +mvm-ri-d107218.roslin +chd +lect-health-007.health +clb +www.downloads.euclid +hss-ppls-0135.ppls +rid-056021.roslin +dbz +services.learn +hss-ppls-0129.ppls +mvm-ri-d125022.roslin +cylon +mvm-ri-d125233.roslin +hss-health-0018.health +hca-lab3-mac29.shca +tortilla +asesor +hss-health-0138.health +mvm-ri-l126240.roslin +mvm-ri-d115201.roslin +cvc +sas-intl-0035.sasg +mvm-ri-l096171.roslin +ecu +www-test.ess.euclid +hss-ppls-0124.ppls +roslin-dc +sas-intl-0030.sasg +www.jams.finance +csg-est-0166.csg +csg-sec-0029.csg +dti +mvm-ri-d067079.roslin +sas-sra-0035.sasg +mvm-ccbs-060310.ccbs +hss-ppls-0118.ppls +ppls-g26-024.ppls +eoc +sas-intl-0024.sasg +monkeys +csg-est-0116.csg +hss-hca-0062.shca +digger +rmsbigsave +hss-health-l05.health +arturo +virtualgames +sas-sra-0030.sasg +mvm-ccbs-060294.ccbs +www.kira +flg +ppls-7gs-011.ppls +winocular +atenas +learnspanish +ppls-g26-018.ppls +csg-est-0169.csg +capcap +doc-e-fil +sas-reg-0183.sasg +metod +m.people +csg-est-0056.csg +csg-sec-0033.csg +www.usana +pantherplace +carols +calentamientoglobal +sas-scs-0023.sasg +sas-sra-0024.sasg +csg-fin-0177.csg +hss-ppls-0097.ppls +www.blackandwhite +csg-as-0000822.csg +ibn +www.vega +hlm +bus-routes +ppls-g26-013.ppls +catweb +mvm-ri-d137037.roslin +fahrenheit +jck +atomix +sas-intl-0013.sasg +icue +ipf +ebri053211.roslin +www.borderweb +itt +fivestar1 +gtmbigsave +estadistica +searchme +habbinfo +gmsbackpack +www.vanguardia +sce-coll-0028.scieng +cyberchat +11infst-1-fsu-mfp-col.csg +kicker +sas-sra-0018.sasg +iparent +lec +pinkribbon +lio +kui +csg-fin-0127.csg +hss-ppls-0092.ppls +ppls-g26-007.ppls +nursietoo.ppls +sas-intl-0007.sasg +boombang +ecopro +33bp-g-reception-mfp-bw.sasg +lsu +sas-scs-0012.sasg +sas-sra-0013.sasg +csg-fin-0067.csg +hss-ppls-0086.ppls +mvm-ri-svcen.roslin +freemovies +ppls-g26-002.ppls +mvm-ri-d096214.roslin +mvm-ri-d136094.roslin +sas-intl-0002.sasg +csg-saf-0054.csg +www.coffeebreak +sas-scs-0006.sasg +sas-sra-0007.sasg +csg-fin-0017.csg +nrc +hss-ppls-0081.ppls +csg-as-0000652.csg +psy-elaine-niven.ppls +csg-hr-0061.csg +hss-health-l33.health +sas-sra-0002.sasg +wrw-3-06-mfp-bw.shca +mvm-ri-d136220.roslin +www.watson +verizon +wwms313igel.shca +flawless +ril-026062.roslin +csg-pps-0015.csg +cybersoft +srv109.csg +ova +www.vital +cyberstop +ppls-atlab-001.ppls +matty +themusic +csg-hr-0040.csg +mvm-ri-l117220.roslin +www.philosophy +hss-ppls-0069.ppls +rip-brfm8.roslin +www.freelance +riv-amxwap.roslin +rjc +mvm-ri-d097076.roslin +ppls-printer20.ppls +ebri073208.roslin +hss-ppls-0064.ppls +tmu-g-trades-mfp-bw.csg +sci068.scieng +mvm-ri-l115165.roslin +www.earnmoney +marwa +www-test.ermis.planning +shk +hss-health-0087.health +rim-107122.roslin +masla +dulce +ppls-printer14.ppls +bonami +csg-as-unitots3.ppls +lewis-mac.ppls +mvm-ri-d125091.roslin +scifunlaptop5.scifun +mvm-ri-d095032.roslin +csg-est-0093.csg +pcs4000-5001.roslin +mvm-ri-l115159.roslin +mvm-ri-d076074.roslin +hss-hca-0123.shca +srv026.csg +ppls-polar1.ppls +stn +mvm-ri-d077185.roslin +mvm-ri-m136033.roslin +vmlgen-pc.ppls +tml +sas-intl-0021.sasg +hss-hca-0117.shca +findlove +csg-hr-0032.csg +peffer-g-office-mfp-bw.csg +hss-ppls-0047.ppls +chacal +chacho +ttm +sas-cam-0020.sasg +ulp +hss-iad-0014.iad +nrg5.ppls +csg-corp-0006.csg +csg-hr-0011.csg +hss-hca-0112.shca +chavez +mvm-ri-l136096.roslin +csg-nad-001.csg +wdb +hss-ppls-0042.ppls +boxing +ppls-y2lab-001.ppls +csg-saf-0027.csg +boxnet +csg-cse-0038.csg +scs-onelan.scieng +motorcu +www.warlords +madeleine +d194135a.roslin +hss-iad-0010.iad +www.landing +enric +hss-hca-0096.shca +ppls-tilllptp.ppls +hss-ppls-0036.ppls +csg-fin-0197.csg +www.pod.drps +csg-est-0060.csg +hss-ppls-0109.ppls +xam +sce-coll-0041.scieng +michaeljordan +hss-health-l41.health +www.elegantmodel +macad.ppls +evilempire +smartdesign +mvm-ri-d115208.roslin +dagmar +valles +hss-ppls-0031.ppls +ppls-hegel.ppls +vabel +marea +mvm-ri-d107246.roslin +hss-health-0026.health +arquitecto +bestchoice +hss-hca-0085.shca +comentarios +yoel +hss-ppls-0025.ppls +mvm-ri-v115198.roslin +www-test.ppmd.euclid +sci175.scieng +fatos +www.fina +sce-coll-0064.scieng +clanak +goldenstar +hss-hca-0079.shca +ribbon +cech +www.dinamico +hss-ppls-0019.ppls +neurosys +meandyou +www.laptops +djzone +www.videogames +wrk059.csg +www.taekwondo +bungie +printer25.ppls +prismatic +hss-hca-0074.shca +www.demoshop +hss-health-l50.health +mac-skype.csg +jantar +ris-vblx06.roslin +mvm-ri-l115157.roslin +www.nintendo +cifs +mvm-ri-l096145.roslin +www-test.timetab +csg-as-0000735.csg +est-forhill-g-trades2.csg +gfl075344.roslin +www.deltaforce +sce-coll-0049.scieng +sas-princ-0003.sasg +sci061.scieng +cocina +codigo +csg-as-sacconference.csg +artesanias +mvm-ri-d116071.roslin +www.skating +pacifico +csg-cse-0034.csg +www.ape +hss-ppls-0008.ppls +manar +sas-scs-0019.sasg +csg-est-0225.csg +ris-onelan02.roslin +www.cbc +hss-hca-0063.shca +health-omq-025.health +manan +ebri073201.roslin +hss-ppls-0003.ppls +csg-est-0165.csg +hss-ppls-0104.ppls +est051.csg +csg-sec-0028.csg +www.dam +www.dcc +mvm-ri-d067178.roslin +devsys +www.cpt +www.cri +www.xl +hca-lab3-mac25.shca +corpuschristi +mall5 +mall4 +www.edy +www.eet +hss-hca-0057.shca +contactanos +cortex +ppls-g26-010.ppls +mall2 +mvm-ri-m115137.roslin +www.ems +lissa +csg-as-0000785.csg +rip-brfm1.roslin +csg-est-0115.csg +int-jet5c.sasg +signs +hss-hca-0052.shca +sce-coll-0003.scieng +mainy +www.fra +csg-fin-0151.csg +cristovive +csg-est-0055.csg +plazma +holyrood-le1 +majda +holyrood-le0 +www.reward.humanresources +csg-est-0030.csg +controlcenter +www-test.events +hss-health-0133.health +rim-076013.roslin +lion1 +creato +espf.ppls +hss-hca-0046.shca +www.hun +csg-as-0000821.csg +www-test.admin.eves.myed +ppls-kiosk-01.ppls +axa-hrm +csg-est-0005.csg +rip-c02m4.roslin +www.boombang +sas-intl-0010.sasg +macka +ctx2 +csg-fin-0126.csg +djdark +hss-hca-0041.shca +mvm-ccns-0078-vm1.ccns +metsa-ctx +est026.csg +csg-fin-0066.csg +www.nostalgia +dinero +inditex +hss-hca-0035.shca +szukajpracownika +vbs-194162a.roslin +escuela +o1.email.praca +empikbeta +bdb-1-finance-sfp-bw.ccbs +ris-vlxgw01.roslin +aap063.sasg +scanner-b62959b.csg +csg-saf-0053.csg +sas-sra-0015.sasg +boygirl +extremedream +www.corina +www.nak +www.ned +csg-fin-0016.csg +evaluaciones +hss-hca-0029.shca +csg-as-0000651.csg +mvm-ri-d086118.roslin +camaieu +sas-reg-0161.sasg +hca-lab3-mac20.shca +djneto +bluesoft +www.oli +rivoli +hrkim.ad +hss-hca-0024.shca +www.pim +www.osi +mvm-ri-d107202.roslin +xpr-touchscreen.shca +www.ganoexcel +bitbyte +mvm-ccbs-060145.ccbs +www.pti +hrm2 +www.raw +ofertypracy +holaholahola +kv.ad +ri-dpcs1.roslin +ppls-y2lab-211.ppls +kokos +pp4sspc6.scifun +www.nelson +hss-health-l76.health +www.sav +www.mifamilia +www.sk8 +www.rac +www.sig +hss-hca-0018.shca +www.onedirection +mailbe12r.staffmail +tokio +www.stc +ecosol +fin210.csg +multikino +ppls-mbook01.ppls +ppls-y2lab-205.ppls +do.atman-isp +hss-hca-0013.shca +jbr +csg-as-0000845.csg +transglobal +hca-netprint-bw6.shca +ppls-y2lab-200.ppls +canizares +sas-cas-0086.sasg +vesuvius +www.vid +hss-hca-0007.shca +erptemp.ppls +ultimategames +seminario +mvm-ccns-0030.ccns +hca-netprint-bw1.shca +biggboss +orca.ppls +csg-fin-0211.csg +lemot +firenet +mvm-ri-i115172.roslin +www.wot +mvm-ri-d064173.roslin +mvm-ri-d067070.roslin +hca-jpglab-029.shca +ris-vtlx02.roslin +dmusic +csg-hr-0081.csg +mvm-ri-d125243.roslin +www.xyz +jully +www.tutorials +csg-est-0079.csg +4life +rid-056211.roslin +hss-hca-0002.shca +kincaids-cctv4.csg +kb-canon2.csg +msantos +hss-health-0101.health +alex24 +sas-scs-0008.sasg +csg-hr-0031.csg +www.vampires +satisfaccion +mvm-ri-d096170.roslin +autotech +galaxia +sas-sra-0009.sasg +mileycyrus +nikolas +mvm-ri-d086047.roslin +lib-mac-009.trg +hss-iad-0013.iad +www.worldsport +csg-eusu-0001.csg +mvm-ri-d104172.roslin +redox +sas-cas-0069.sasg +csg-cse-0037.csg +starprogging.ppls +www.animemanga +gameday +games11 +gamenew +gamertv +welding +hss-health-l15.health +ccnsm074.ccns +eljoker +www.futbolmundial +kallisti +www.soporteinformatico +nextgeneration +sas-reg-0004.sasg +www.reading +hss-health-0001.health +www.robotic +radiostyle +guidance +www.pirate +newwave +casanet +psychlaptop.ppls +www-test.adminrae.planning +mvm-ri-l127110.roslin +fanime +rankings +hca-lab3-mac22.shca +edadfed +isitech +rastaman +ms-dw6-2m-0-mfp-col.health +mvm-ri-l067163.roslin +sas-cas-0058.sasg +gfd075245.roslin +elblog +www.numbers +phstl-g-reception-mfp-bw.csg +www.informatika +sce-coll-0038.scieng +catcher +mvm-ri-i005193.roslin +csg-scecc-0004.scieng +handbags +lib-mac-011.isg +stellamaris +et-dsb-b04.ppls +sas-cas-0042.sasg +elixir +sas-sra-0004.sasg +thunderbolt-display.scieng +hca-laptop-006.shca +rogerio +ris-redi01.roslin +wwms226igel.shca +sas-cas-0036.sasg +sas-reg-0150.sasg +insplap.scifun +csg-est-0139.csg +csg-est-0224.csg +mvm-ri-l086049.roslin +lamis +teu009.csg +lamer +www.playlist +sas-cas-0031.sasg +csg-est-0164.csg +csg-sec-0027.csg +lambo +ppls-y2lab-128.ppls +ccnsm035.ccns +sas-cas-0070.sasg +ebri053150.roslin +mvm-ri-l137093.roslin +epicfail +onlinesv +bbox +sas-cas-0025.sasg +mvm-ri-i075149.roslin +andr +mvm-ri-l115175.roslin +hss-health-0097.health +csg-est-0114.csg +vijay123 +netc +junior12 +asan +arsa +ris-vnlx01.roslin +www.webzone +ppls-y2lab-123.ppls +sas-reg-0009.sasg +sas-cas-0019.sasg +lib-pc-1707.isg +ccgh +worldclub +business-school +www.nextel +boda +csg-est-0054.csg +ppls-y2lab-117.ppls +mvm-ri-d096206.roslin +envole +godknows +stargames +csg-fin-0175.csg +happyhappy +hca-jpglab-018.shca +sas-cas-0014.sasg +csg-as-0000820.csg +creditos +csg-est-0200.csg +sas-alumni-0058.csg +darkhell +ris-vlx10.roslin +csg-est-0004.csg +ppls-y2lab-112.ppls +jozef +edel +mvm-ri-i085148.roslin +csg-as-0000759.csg +wwms01m20igel.shca +egac +forums5 +josua +gatoman +ppls-y2lab-106.ppls +fede +moonster +csg-est-0135.csg +khanh +motoshop +csg-fin-0065.csg +cyberstore +miweb +sas-cas-0003.sasg +neeps.cache +mypag +esma +galt +estudio +csg-saf-0052.csg +geco +foci +folk +ppls-y2lab-101.ppls +darkzone +gavilan +escool +csg-fin-0015.csg +ebri023188.roslin +www.zoom +hss-health-l51.health +webdisk.student +www.smartdesign +autoconfig.student +www.pubs.recordsmanagement +jordy +mvm-ri-l085045.roslin +www.legends +ris-lx01.roslin +gamess +lauren-lptop.ppls +obelisk +sas-reg-0015.sasg +jags +ebri013159.roslin +backup-kbserv2 +mvm-ri-d107203.roslin +sas-chap-0001.sasg +www-dev.admin.drps +mvm-ri-l134239.roslin +lelman20.ppls +ketty +www.hero +jesu +ipsa +www-devupg.myed +vcs-126125a.roslin +isam +sas-cam-0029.sasg +ris-buildlx02.roslin +sbvm2012.ppls +irys +kass +mvm-ri-l115228.roslin +www.animeworld +vis012.sasg +malaika +jomar +avaluos +jmmp +sas-cam-0024.sasg +homosexuales +mvm-ri-l137156.roslin +ebrilx093139.roslin +rim-097068.roslin +tsuki +csg-hr-0079.csg +www.goddess +vis006.sasg +csg-est-0249.csg +kincaids-cctv3.csg +hss-health-0099.health +lau03.roslin +csg-hr-0029.csg +www.abcde +sce-coll-0021.scieng +health-omq-035.health +lgbt +kosh +proxycom +vis001.sasg +samadhi +hss-iad-0012.iad +mvm-ri-d107140.roslin +winsrv2.ppls +evelin +health-lap-seminar.health +sas-cam-0007.sasg +floral +ris-ptesxi.roslin +sce-coll-0013.scieng +csg-as-0000209.csg +sas-cam-0002.sasg +mvm-ri-l115221.roslin +ppls-imac-01.ppls +www.bandy +www.banks +www.barra +musiconline +mvm-ri-i136255.roslin +murali +csg-fin-0019.csg +sec04.roslin +hca-jpglab-007.shca +www.alone +sas-reg-0021.sasg +mvm-ri-d096188.roslin +mvm-ri-l097195.roslin +sci034.scieng +mvm-ri-d117131.roslin +www.blackboard +lib-mac-010.isg +www.annie +jlink +mvm-ri-l127231.roslin +castelli +sanchez +mrcs +www.language +www.bible +www.tata +mvm-ri-m135069.roslin +www.arias +www.aries +hss-ppls-0061.ppls +hca-mfd-006.shca +sheridan +shishi +mvm-ri-d127198.roslin +csg-est-0333.csg +holyrood-kbserv3 +holyrood-kbserv2 +www.blast +hca-mfd-001.shca +sampler +excess +csg-est-0310.csg +satsuki.ppls +mvm-ri-d087240.roslin +genial +paes +hca-spglab-035.shca +ppls-y2lab-018.ppls +csg-est-0204.csg +mvm-ri-d086191.roslin +mvm-ri-d086207.roslin +www.bosch +ris-vwinprn3.roslin +katar +www-test.disability-office +www.parsian +sas-reg-0026.sasg +weir-g-105a-mfp-bw.scieng +ppls-y2lab-013.ppls +www.ciber +rii-115129.roslin +csg-est-0163.csg +csg-sec-0026.csg +ppls-y2lab-007.ppls +hca-jpglab-002.shca +vbs-lap-194222a.roslin +petz +marks.sps +www.cisco +csg-est-0113.csg +health-omq-043.health +gfd-117143.roslin +www-test.office365 +www.destiny +ppls-y2lab-002.ppls +www.decor +sas-reg-0032.sasg +wonka +ris-hpc01.roslin +www.abcdef +sec09.roslin +csg-est-0053.csg +new2008 +rii47245.roslin +lect-hca-002.shca +csh-3-3-7-mfp-bw.sasg +www.echelon +csg-fin-0174.csg +www.diana +kandy +csg-est-0003.csg +mvm-ri-m135074.roslin +www.happiness +hca-escreen-03.shca +qixi +csg-fin-0124.csg +csg-as-0000758.csg +mvm-ri-m116126.roslin +arres +csh-b-b1.9-mfp-col.csg +trg-sr2.trg +esi-tosh.ccbs +mvm-ri-d067072.roslin +www.spl +halocombat +ismet +www.contactanos +csg-as-0000708.csg +hss-health-l25.health +csg-est-0144.csg +mvm-ri-i075053.roslin +mvm-ri-v105156.roslin +www.wisard.registry +jivan +sas-reg-0037.sasg +hss-health-0011.health +csg-saf-0051.csg +jithu +rid-057083.roslin +health-mac005.health +csg-fin-0010.csg +lib-lap-1653.isg +sci159.scieng +jiten +sce-coll-0048.scieng +ril-047127.roslin +kaiba +hss-hca-0120.shca +www.drive +www.droid +shay +franck +sas-reg-0043.sasg +frases +metroid +ris-vlx01.roslin +www.mhm +csh-3-3.8-col.sasg +www.era.finance +www.aslan +sec08.roslin +www.elisa +www.emaus +mvm-ri-l096129.roslin +mvm-ri-d096234.roslin +tavo +www.emily +www.adagio +newhaven-jvcs.scieng +freepc +jinji +frenzy +teru +www.enric +www.enter +csg-pps-0038.csg +www.confort +mvm-ri-l120711.roslin +psy-pc022.ppls +mvm-ri-i135223.roslin +hss-ppls-0050.ppls +psy-pc016.ppls +habbbo +habbix +habbuk +mvm-ri-d104179.roslin +csg-cse-0013.csg +health-omq-009.health +www.gapps +sawayaka +rii-105166.roslin +muebles +www.lastminute +isaak +mesh +srv023.csg +sas-reg-0048.sasg +kincaids-cctv2.csg +ris-ilx01.roslin +csg-est-0084.csg +sas-bu-0045.sasg +mvm-ri-p125078.roslin +lib-mac-007.trg +mvm-ri-d094190.roslin +hca-mac001.shca +hss-iad-0011.iad +mvm-ri-d107211.roslin +themasters +ionut +www.survivor +sas-bu-0039.sasg +csg-cse-0063.csg +health-mac001.health +mvm-ri-d134247.roslin +csg-cse-0035.csg +www.cooking +hss-health-0117.health +sas-bu-0034.sasg +www.funny +insta +ris-trac01d.roslin +www.heart +csg-as-0000849.csg +health-omq-020.health +www.gogle +mvm-ri-d126158.roslin +sas-bu-0028.sasg +pef +haoyun +mvm-ri-d137164.roslin +lap-temp.csg +seniors +majoo +eventus +sas-bu-0023.sasg +myworlds +yasui +uniad +wrk116.csg +manes +hca-tlab-021.shca +sas-bu-0017.sasg +www-dev.employerdatabase.careers +cerc4.roslin +www.santamonica +sas-reg-0054.sasg +hca-mac006.shca +mvm-ri-l087099.roslin +ris-esxi01.roslin +www.messaging +webmix +www.arteycultura +indah +momo11 +hca-tlab-015.shca +checker +morcom01.ppls +gamemania +www.restaurante +lupo +csg-est-0034.csg +aston-martin +sas-bu-0012.sasg +mvm-ri-d097035.roslin +imgup +www.human +velas +mvm-ri-l104182.roslin +nsu-union-0001.unison +cas-mlb3-011.sasg +hca-tlab-010.shca +www.intro +blueway +hss-health-l61.health +sas-bu-0006.sasg +csg-est-0272.csg +www.kakao +ris-lx11.roslin +cas-mlb3-005.sasg +ris-lx08.roslin +hca-spglab-029.shca +infinitygroup +ris-esxi03.roslin +ris-devlx.roslin +ftp.srna-mammal.roslin +hca-tlab-004.shca +www.kenzo +www.jogos +ramos +mvm-ri-d126161.roslin +www.joshi +hss-iad-0038.iad +www.joyas +ksltop.ppls +orizont +www.label +sas-bu-0001.sasg +hss-health-l58.health +csg-est-0222.csg +www.laser +www.judas +randi +hss-hca-0108.shca +csg-as-0000248.csg +mvm-ri-l097108.roslin +mvm-ri-d107213.roslin +csg-hr-0056.csg +hss-hca-00104.shca +henrry +csg-est-0162.csg +nakayama +javed +roraima +csg-eusu-0002.csg +csh-g-g22-mfp-bw.csg +ahazlett-pc.ppls +hss-ppls-0038.ppls +presenter.csg +hernan +csg-est-0112.csg +tmp-health-003.health +www-dev.ccts.careers +golazo +alejandra +csg-est-0052.csg +www-test.reporting.euclid +sas-reg-0059.sasg +mvm-ri-m116091.roslin +hca-mac012.shca +tele3 +jarno +csg-fin-0173.csg +csg-est-0002.csg +estebanoc +5forrhill-c-printroom-mfp-col.sasg +warzone +hsoft +micke +spacegames +csg-fin-0123.csg +www.milan +ppls-shared.ppls +www.miweb +www.levelup +laser11.roslin +csg-as-0000757.csg +mvm-ri-l005145.roslin +ris-vwinrep.roslin +taalman01.ppls +knak +shalomshalom +ppls-y2lab-109.ppls +handc-mac3.shca +mvm-ri-i117139.roslin +csg-fin-0063.csg +csg-fin-0105.csg +www.neuro +dsb-lptp-ng.ppls +www.nexus +www.underworld +csg-saf-0050.csg +www.motos +sce-coll-0023.scieng +www-test.learn +nextlevel +genka +childcare +srv051.csg +www.alerta +jamil +ris-pvnb01a.roslin +hss-ppls-0033.ppls +sas-reg-0065.sasg +hca-mac017.shca +cerc2.roslin +mvm-ri-i093219.roslin +hca-netprint-bw12.shca +jorgemiguel +vcs-127104a.roslin +mvm-ri-i137100.roslin +www.noobs +www.myjob +sas-reg-0105.sasg +csg-fin-card6.csg +ril-035183.roslin +soiree +csg-pps-0037.csg +lib-pc-1579.isg +www.pavel +meyer +sas-reg-0188.sasg +www.amigas +mvm-ri-d056248.roslin +rip-brfm3.roslin +csg-hr-0077.csg +tatan +mvm-ri-sx06.roslin +csg-pps-0016.csg +sas-reg-0071.sasg +kincaids-cctv1.csg +www.worldwide +rid-056254.roslin +csh-g-g20-mfp-bw.csg +sci063.scieng +sas-reg-0177.sasg +mvm-ri-l115160.roslin +www.osaka +viks +hss-ppls-0014.ppls +csg-hr-0027.csg +mvm-ri-l085091.roslin +udm +lib-mac-006.trg +www.planb +www.plane +sas-reg-0076.sasg +saf066.csg +hca-mac028.shca +mvm-ri-d065136.roslin +psy-eblic.ppls +sas-reg-0082.sasg +overload +hss-iad-0009.iad +sas-reg-0172.sasg +mvm-ri-d126196.roslin +www.ozone +hoots +mauricio +rim-086014.roslin +mvm-ri-d096137.roslin +www.secure.vle +hca-mac034.shca +gamestore +hss-ppls-0189.ppls +www.bertha +www.reich +fecebook +guerra +www.admin.alumni.dev +supercar +mvm-ri-m125085.roslin +csg-as-0000257.csg +ris-sb01.roslin +sas-reg-0166.sasg +www.scary +www.apocalypse +sas-reg-0100.sasg +iclick +josemaria +www.mycareer +www.seals +cgltop.ppls +ideias +hss-health-0115.health +www.shara +www.rouse +dbritmac.ppls +vcs-126190a.roslin +irweb +www.shoes +csg-corp-0007.csg +www.socialnetwork +www.tarot +pp4sslaptop1.scifun +sas-reg-0155.sasg +www.skype +mvm-ri-d086067.roslin +alaan +csg-saf-0028.csg +www.teach +www.snake +www.smoke +gstar +rip-b02c3.roslin +5kc-g-siteoffice-mfp-bw.csg +www-test.tqintra.dev +sas-reg-0149.sasg +hca-mac039.shca +13-2rc-g-siteoffice-mfp-bw.csg +xjcmblta +sci086.scieng +ppls-labds-001.ppls +www.stamp +mvm-ri-l097180.roslin +hss-health-l35.health +ris-vwlx01.roslin +thrasher +mvm-ri-d115193.roslin +csg-est-0331.csg +www.buyandsell +sas-reg-0144.sasg +sas-leaps-0006.sasg +mvm-ri-d125024.roslin +metal25 +metal14 +mvm-ri-d126135.roslin +hss-ppls-0207.ppls +www.thewarriors +mvm-ri-d096066.roslin +sas-reg-0138.sasg +sas-leaps-0001.sasg +metal10 +www.minegocio +edneuro-imac.ccns +health-lap30.health +www-dust.star.euclid +sce-coll-0058.scieng +afterlife +mvm-ri-d097094.roslin +csg-est-0221.csg +sas-reg-0133.sasg +munoz +hca-copier-xpr.shca +www.camila +himar +csg-est-0161.csg +sas-reg-0127.sasg +sas-reg-0093.sasg +sellout +www.chatbook +csg-sec-0024.csg +mvm-ri-d137245.roslin +ris-vwintslm.roslin +www.naa +www.carlos +mvm-ri-d127007.roslin +sas-reg-0122.sasg +mvm-ri-d094189.roslin +www.blanco +sgrant2013.ccbs +programme +sce-coll-0030.scieng +health-omq-019.health +csg-est-0051.csg +lamode +www.celulares +macc +hca-mac045.shca +sas-reg-0116.sasg +rid-057020.roslin +csg-fin-0172.csg +happydog +www.forall +sas-reg-0111.sasg +csg-fin-0122.csg +utt.ppls +www.valentine +csg-as-0000756.csg +hss-ppls-0192.ppls +hca-mac047.shca +www.grafika +elecom +sas-reg-0095.sasg +rockman +vmet-test.ppls +gonza +jadore +rii-pda2.roslin +mvm-ri-d086162.roslin +csg-fin-0092.csg +csg-fin-0062.csg +csg-as-0000706.csg +generic +hss-health-0127.health +hcanda-laptop-dkaufman.shca +mvm-ri-l065067.roslin +lovetolove +hca-mac042.shca +sas-reg-0090.sasg +csg-saf-0048.csg +mvm-ri-d096173.roslin +narcisse +sas-reg-0078.sasg +mvm-ri-l107179.roslin +ris-vwlx04.roslin +hca-mac036.shca +sas-reg-0084.sasg +hca-mac031.shca +www.myed +csg-fin-card5.csg +sas-princ-0005.sasg +www.gordon +rofl +extm +sublimate +gameshow +mvm-ri-d085051.roslin +www.rosi +jazmin +mobotix +vscan2 +vscan1 +olife +siba +crashbandicoot +goldy +vsproxy +www.mmt +sas-reg-0098.sasg +herry +hacer +mvm-ri-l136185.roslin +ris-ifs.roslin +help3 +moneytoday +www.boxnet +alfadigital +health-omq-007.health +galb +slap +ffacebookk +csg-fin-0152.csg +www.hsl +sas-reg-0114.sasg +www.ilk +ekstra +punky +www.magma +www.bsl +ministranci +acies +adama +www.plb +ilk +csg-est-0031.csg +ppls-y2lab-206a.ppls +mvm-ri-d067036.roslin +plb +www.slk +www.abc1 +hagar +kenichi +csg-as-0000846.csg +kreativ +mvm-ri-v125180.roslin +www.ministranci +snte +bacho +baile +csg-fin-0212.csg +akane +www.galb +hss-ppls-0020.ppls +baner +www.ekstra +hugocastro +sas-reg-0119.sasg +ap108 +libertine +teen-sex +ext01 +intima +csg-est-0081.csg +rid-067080.roslin +mvm-ri-l096127.roslin +www.dealers +posts +gw-adsl +jetset +anais +pp4sspc3.scifun +hss268.ppls +darkassassins +csg-sec-0004.csg +montclair +beloo +flipflop +berny +sas-reg-0125.sasg +csg-est-0141.csg +getaway +kirikou +dwn +dowhome1 +hss-ppls-0195.ppls +www.danger +mastertrack +dowiepplus +dasdmail +asada +korokoro +cacsa +bitfm +caleb +virtualx +hss-hca-0080.shca +cantu +carga +ppls-mac007.ppls +gismo +cbtis +schoolbus +stoneware +auris +dowesp01 +cedro +persefone +lmswx +boden +lyncext2 +dasd-ttc +natwest +dasd2 +dasd3 +bolix +dasd4 +girly +hafis +dasd5 +chick +chipi +hadis +cristiana +squad +cinna +hcanda-ckolotur.shc +fujimura +gille +sce-coll-0046.scieng +danis +gfd +danko +danty +nakayoshi +datos +riv-amxnetlinx.roslin +dasd6 +www.clicks +dasd8 +sas-reg-0131.sasg +cochi +dasd9 +dmswx +nateast +cores +dasdweb +tamatama +www.hip-hop +csg-est-0201.csg +fredy +dasdvideo +mvm-ri-d106054.roslin +dimex +health-mac003.health +dasd-sharepoint +facedook +dasdwise +lesbianas +rip-brfm10.roslin +natdasd +sas-reg-0136.sasg +www.zeta +tupperware +dwodm +digitalplus +franc +qpteach +csg-est-0251.csg +donar +hss-health-0008.health +dongo +www.pakistan +hss-health-l23.health +ssca1 +rbigc01 +sas-leaps-0004.sasg +mati +securetibia +forty +imageup +fanni +avacs +bssd +rickysfr +earls.staging +fstraining +emaus +movpublic.stg +sas-reg-0142.sasg +avengers +maracaibo +www.dejavu +winmac +ucmall +expro +www.cronica +colecciones +christin +adventcom +joeys +ucmallnew +ergon +expel +ugifit.temp +gam3r +karlim +gle +csg-est-0311.csg +fpcss +13infst-g-transport-mfp-col.csg +webdisk.green +sas-leaps-0009.sasg +esraa +autodiscover.green +every +autoconfig.green +geeko +geekz +geims +gfd065254.roslin +csh-g-g14-sfp-bw.csg +mvm-ri-m135072.roslin +ponce +rim-106001.roslin +www.chess +forja +forti +ppls-labds-004.ppls +nagaraju +hca-mac040.shca +gigas +sas-bu-0019.sasg +sas-reg-0153.sasg +sas-reg-0158.sasg +mvm-ri-d117057.roslin +mvm-ri-c124035.roslin +gimbo +ezweb +windows8test.ppls +mvm-ri-l097132.roslin +mvm-ri-d076056.roslin +essam +furia +sas-reg-0164.sasg +heber +csg-cse-0014.csg +kashif +ppls-labds-021.ppls +vest +rocking +sas-reg-0169.sasg +chaimaa +itinfo +sci051.scieng +sas-reg-0175.sasg +galah +histo +evaluate +scifun1.scifun +csg-hr-0057.csg +finny +sas-reg-0181.sasg +lab-copier-xpr23.shca +icool +cox.ee +israil +fidel +kaotic +handc-m-titan.shca +hoola +dyana +ferro +ferdi +gypsy +fendy +law1 +health-pc10.health +www.dario +litho +jabes +jacal +emule +jafra +sas-reg-0103.sasg +crazygamers +hubbo +emmet +csg-srs-0004.csg +sas-reg-0186.sasg +kamael +www-beta.estores.finance +goldmember +ebri063182.roslin +csg-pps-0017.csg +aaaaaaa.csg +mvm-ri-d077238.roslin +upr +sas-reg-0192.sasg +kenmon +mvm-ri-l107193.roslin +jetta +mrlab +moon-light +hss-hca-0091.shca +sec02.roslin +highnoon +fares +www.teahouse +www.free-software +mvm-ri-d076060.roslin +handc-mhist08.shca +fanta +kanon +faker +www2.hcrc +eitai +itachi +csg-corp-0008.csg +mediaweb +csg-saf-0030.csg +itpro +battousai +csg-fin-0102.csg +iwant +parishilton +sas-alumni-0030.sasg +csg-fin-0218.csg +drako +health-omq-033.health +csg-fin-0103.csg +cyrex +natmark +handc-mhist25.shca +www.ital +www.tequiero +lacom +ladob +dolby +lau01.roslin +sas-reg-0108.sasg +taalman04.ppls +blackhearts +facehack +www.viktoria +health-print7.health +kodai +csg-est-0032.csg +csg-fin-0213.csg +sas-cas-0009.sasg +mexicocity +www.lima +hss-hca-0106.shca +du110 +csg-est-0082.csg +fisicamoderna +krieg +csh-1-corridor-mfp-col.csg +edent +faceface +ppls-tms-01.ppls +mauri +csg-sec-0005.csg +misi +ecell +lostsoul +mendo +djsky +meson +csonline +djsam +securehost +mvm-ri-d107191.roslin +lucio +www.i2i +csg-est-0142.csg +dirty +www.edison +ppls-psy-test.ppls +sasg-oldcoll-g-foy.sasg +chidori +neuma +teu006.csg +ultimasnoticias +monto +mvm-ri-l116223.roslin +www.supernatural +dinno +ilovepets +msdos +csg-est-0192.csg +hss-health-0034.health +ris-lxpoc01.roslin +nevermind +hca-tlab-002.shca +handc-pc40.shca +adana +aydin +hss-health-l48.health +friendsforlife +conceptos +jhonny +noize +ukulele +thecompany +sasukeuchiha +muhaha +conny +freedoom +cas-mlb3-003.sasg +zaqxswcde +sci015.scieng +csg-est-0252.csg +desai +j0k3r +sas-bu-0004.sasg +vbs-194176a.roslin +cinemark +ollin +sanji +blue04 +behemoth +cnbbs +mvm-ri-l124238.roslin +hca-tlab-007.shca +materiales +www.eas +elegantmodel +ddddd +microweb +agustin +ema6.ppls +csg-cse-0026.csg +www-test.jams.finance +sas-bu-0010.sasg +plata +fastbook +rim-096018.roslin +hca-tlab-013.shca +www.justice +daved +bloopers +ris-vlxweb06.roslin +junkie +polka +iad-1-bw.iad +darth +impacto +cas-mlb3-014.sasg +proma +claim +friendsforever +rally +darky +sas-bu-0015.sasg +seeds +lifelonglearning +hca-tlab-018.shca +bsoft +ril-115153.roslin +danil +damon +motocross +www.epicfail +natacion +yeya +sas-bu-0021.sasg +sagem +damas +salvo +www.makemoneyonline +jmccpres.csg +hca-tlab-024.shca +dalal +dala3 +mvm-ri-l077088.roslin +muerto +lemans +seals +mvm-ri-d126219.roslin +sas-bu-0026.sasg +hss-health-0095.health +www-test.esp.myed +azura +sas-bu-0032.sasg +musicone +www.fabian +www.wpmservice.finance +ebri073216.roslin +rouge +ayush +www.bettyboop +taboo +csg-cse-0015.csg +mvm-ri-d107207.roslin +colpitts +chits +rid-057010.roslin +rouse +ris-vlxbio02.roslin +www.facelook +mvm-ri-d115109.roslin +www-test.rssjobs.careers +sas-reg-0120.sasg +www.avengers +sas-bu-0043.sasg +lae +hss-iad-0041.iad +blog002 +littleboy +csg-hr-0058.csg +mvm-ri-d067026.roslin +csg-as-0000398.csg +mvm-ri-l107228.roslin +chama +www.cstrike +azadi +suini +mvm-ri-l096117.roslin +mash.cache +veracruz +bonga +todos +bolek +top40 +www.demon +csg-pps-0018.csg +celeb +avoid +slevin +riwebserv2k3.roslin +sce-coll-0036.scieng +aus22 +ebri053218.roslin +d112211.sasg +psy-pc031.ppls +mayer +reclutamiento +mvm-ri-l116230.roslin +aubbs +assam +taalman01-host.ppls +ppls-laptop2.ppls +csg-corp-0010.csg +wally +mvm-ri-d125179.roslin +ebri043220.roslin +hss-ppls-0053.ppls +maddog +websd +callo +fotoclub +www-test.services.adminrae.planning +www.recetasdecocina +documentacion +magana +radiomax +camap +box2.ee +csg-fin-0094.csg +hca-escreen-01.shca +www.crystal +csg-sec-0021.csg +mvm-ri-d125241.roslin +binay +www.heritage +mvm-ri-i135238.roslin +christo +abraxas +csg-est-0033.csg +billm +malawi +ppls-y2lab-130.ppls +www.jasmin +wilfredo +asako +www.enlace +h2so4 +csg-as-0000848.csg +contacto +wespace +csg-as-print235.csg +abcdefghij +csg-fin-0214.csg +csg-est-0083.csg +ppls-igel-3-02.ppls +www.bis +arabi +sinewave +iad-mac002.iad +marcello +app17 +vbs-195120a.roslin +qwertyu +gamersworld +matteo +ppls-y2lab-005.ppls +antik +csg-sec-0006.csg +csg-est-0143.csg +ahmedabdo +hss-health-l74.health +ebri003153.roslin +med-000658a.roslin +ppls-y2lab-011.ppls +laser26.roslin +teu007.csg +mvm-ri-l096174.roslin +csg-est-0203.csg +csg-est-0191.csg +int-usbmac12.sasg +ppls-y2lab-016.ppls +csg-est-0253.csg +mvm-ri-l106249.roslin +mvm-ri-m124191.roslin +csg-est-0313.csg +sas-cas-0032.sasg +hca-mfd-004.shca +csg-est-0217.csg +altec +rii-115145.roslin +www.dlhe.careers +ppls-igel-01.ppls +hss-health-0131.health +csg-as-0000129.csg +alpen +ebri053173.roslin +psy-macbook.ppls +www.gp +ameen +mvm-ri-m096008.roslin +mvm-ri-d077051.roslin +www.gz +www.jh +old-www.ppmd.euclid +sas-cam-0005.sasg +alist +csg-cse-0016.csg +health-omq-023.health +www.jw +sas-cam-0011.sasg +ppls-cns-01.ppls +webcity +alang +ebri043174.roslin +ppls-kiosk02.ppls +ris-lxnbmedia01.roslin +barit +csg-hr-0010.csg +sas-cam-0016.sasg +akess +psy-f23print.ppls +hss-iad-0042.iad +balam +rip-d02c4.roslin +rip-a02m4.roslin +airam +aims1 +vis004.sasg +csg-cse-0050.csg +csg-hr-0059.csg +mvm-ri-d106247.roslin +mvm-ccbs-060441.ccbs +ssllogin +mvm-ri-l096143.roslin +colour22.roslin +sas-cam-0022.sasg +mvm-ri-d125205.roslin +ris-vlx03.roslin +vis010.sasg +petrolheads +www.etime.finance +sas-cam-0027.sasg +sce-coll-0062.scieng +www.vs +vbs-19595a.roslin +vis015.sasg +www.zz +csg-pps-0019.csg +micha-lap-01.ppls +hss-hca-0118.shca +mvm-ri-d096070.roslin +www-dev.salfor.finance +mvm-ri-m115196.roslin +www.freedownloads +mvm-ri-d125027.roslin +psgames +xtina +giggles +rid-026105.roslin +abrar +hss-health-0024.health +sas-chap-0004.sasg +webdesigns +extremex +axlrose +hss-health-l38.health +pcscifun10.scifun +www.lafamilia +psicodelico +www.cbr +abced +hss-ppls-0070.ppls +ucu1.ucu +enlinea +fingerprint +www.stalker +dsb-pgman-01.ppls +ris-vlxftp02.roslin +meimei +csg-corp-0011.csg +mvm-ccns-srv1b.ccns +secure0 +brescia +jbg +swastik +csg-est-0327.csg +www.openbook +www.blackfriday +sector7 +ppls-y2lab-104.ppls +mvm-ri-d057005.roslin +hardrock +ad123 +www.timeweb +onlinetv +mall7 +swapnil +www.ctb +ebri083205.roslin +hellrider +sas-cas-0006.sasg +neoworld +www.eclipse +mvm-ri-d087014.roslin +pixelhotel +hurray +csg-fin-0095.csg +zr +ppls-y2lab-110.ppls +ris-condor01.roslin +mvm-ri-d077064.roslin +mvm-ri-d096141.roslin +sas-cas-0012.sasg +csg-fin-0155.csg +ppls-y2lab-115.ppls +scifunlaptop3.scifun +mvm-ri-d125088.roslin +intermax +g2cpx2.ccbs +rii-115143.roslin +mvm-ri-l115163.roslin +www.lapagina +crazyboy +sushant +www.musicacristiana +swagger +csg-as-0000850.csg +sas-cas-0017.sasg +wanna +animania +csg-fin-0215.csg +www.facebook-com +ppls-y2lab-121.ppls +mikasa +sas-leaps-0010.sasg +booklist +sci066.scieng +blackmetal +mehrdad +34bp-4-4z3-mfp-bw.sasg +cardona +pennyauctions +dsb-2-19-mfp-col.ppls +www.wc +sas-reg-0147.sasg +radiochat +sas-cas-0023.sasg +ppls-y2lab-126.ppls +nakata +www.edesign +acapulco +banjarmasin +garantias +htmail +rip-brfm6.roslin +www.economy +vitor +machoman +csg-sec-0007.csg +www-test.calum-maclean.celtscot +facebooklet +ppls-y2lab-132.ppls +valor +srikant +mudanzas +facebookapi +lomas +virtualassistant +csg-est-0194.csg +hss-health-l70.health +sicario +csg-fin-0193.csg +arc-printer.ppls +facebookconfirmation +sas-cas-0034.sasg +exelent +teddyweb +www-test.intra.finance +csg-est-0254.csg +operaciones +ppls-lap-011.ppls +www.api.payments +vox.ppls +www.whynot +sas-cas-0039.sasg +facebook123 +techworld +jaleel +ris-ifs4.roslin +mywap +hobbahotel +www.freepc +ebrptweb.roslin +negros +musicmax +mvm-ri-d086201.roslin +www-test.wpmservice.finance +wapftp +mvm-ccns-0089.ccns +ucd +aap003.sasg +fucoidan +www.habbux +jaffar +ebri033140.roslin +sce-coll-0026.scieng +mvm-ri-d127214.roslin +puritan +radiocool +gfl045238.roslin +jaeger +happysun +csg-as-0000111.csg +netbox +mysite123 +sas-cas-0056.sasg +hca-lab3-mac19.shca +hss-health-l03.health +sas-cas-0062.sasg +ilovemusic +csg-cse-0017.csg +www.dmg +sohil +sas-cas-0067.sasg +x919 +kaafox +m.porno +hca-lab3-mac31.shca +mvm-ri-l115179.roslin +campeones +health-omq-036.health +bareback +sas-cas-0073.sasg +innovations +et-temp.ppls +tequiero +www.socrates +adlabtemp.ppls +hss-iad-0043.iad +sunflare +masbelleza +jeanpier +sas-cas-0078.sasg +www.metalmilitia +lect-health-005.health +cursosgratis +mvm-ri-d107216.roslin +candy123 +mrmoon +hss-hca-0005.shca +juanproductions +handbag +sas-cas-0084.sasg +www.dhl +mvm-ri-d126164.roslin +www.boletines +hca-netprint-bw4.shca +ris-esxi06.roslin +blackmagic +www.webcontrol +mvm-ri-l116238.roslin +ris-lx14.roslin +www.goblin +www.helios +hss-hca-0011.shca +m.sport +www.dim +csg-pps-0021.csg +pcserver1 +farzad +youtubes +www.herbal +ppls-y2lab-203.ppls +jornadas +clancsw +thewalkingdead +hca-netprint-bw9.shca +mvm-ri-d125026.roslin +www.detodoparatodos +hss-hca-0016.shca +vikas +ppls-y2lab-208.ppls +djlatino +magama +hss-hca-0022.shca +sas-cas-0059.sasg +www.clubdescargas +aaaaaaaaaaa +aap055.sasg +sas-alumni-0035.sasg +anonymoushacker +hss-hca-0027.shca +murder +csg-corp-0012.csg +csg-saf-0033.csg +sospc +saras +hss-hca-0033.shca +espada +www.grafik +kittykat +www.granma +paei +csg-fin-0046.csg +dodatki +www.mame +pangolin +barrio +mvm-ri-d076097.roslin +bacardi +musicacristiana +aap066.sasg +www.mundomagico +med-001024.sasg +www.hikaru +csg-as-0000741.csg +hss-hca-0038.shca +pkforfun +csg-fin-0106.csg +makina +emailupdate +www.ground +zidane +yunior +techzone +willian +ris-vlxdb01.roslin +mercadeo +hss-health-0121.health +www.detodounpoco +ppls-sem-0002.ppls +cabernet +xlab-0 +mvm-ri-d134251.roslin +studyin +mixes +celulares +akropolis +hokey +hss-hca-0044.shca +www.pruebasweb +sas-intl-temp1.sasg +studio6 +paty +cartel +techspot +www.multiverse +hss-ppls-0102.ppls +www.atlantida +csg-est-0035.csg +atrium-onelan.scieng +habbomoney +csg-as-0000851.csg +mapics +kod +hackforums +digitalsolutions +mabel.ppls +sandeman +csg-fin-0216.csg +pagina +mvm-ri-d086167.roslin +tantan +www.hellokitty +marilynmonroe +tolstovki +clinica +mvm-ri-d116236.roslin +ccbs-mvm-060142.ccbs +www.ted +hss-hca-0055.shca +videomax +solucion +salida +rii-105169.roslin +www.sharon +faninc +juanita +mrose +nai +csg-as-0000237.csg +www.alan +nayma +www.dofus +www.simo +www.alto +latinos +www.amos +csg-sec-0008.csg +csg-est-0145.csg +mathiasl +gunner +hss-hca-0061.shca +teu010.csg +www.army +www.surgery +aquarios +www.dsm +dadadada +sbvmref.ppls +csg-est-0195.csg +habboradio +hca-jpglab-020.shca +www.cied +mvm-ri-d096237.roslin +cataclysm +mvm-ri-l126192.roslin +julia123 +sas-princ-0001.sasg +starsale +hindustan +hss-ppls-0012.ppls +mvm-ccbs-060193.ccbs +ppls-card-01.ppls +iijima +hss-hca-0072.shca +sas-princ-0006.sasg +sci163.scieng +hss-ppls-0017.ppls +ppls-psy-unitots.ppls +pinguino +www.shadows +hss-hca-0077.shca +mvm-ri-d127239.roslin +hca-lab3-mac28.shca +sas-reg-0170.sasg +omusic +health-mac008.health +hss-ppls-0107.ppls +csg-est-0039.csg +hss-ppls-0023.ppls +hss-health-0014.health +ykh +wrath.ph +hss-health-l28.health +ppls-psy-003.ppls +mvm-ri-v116130.roslin +mvm-sbms-120224.ccns +mvm-ri-d067113.roslin +khoctham +csg-est-0109.csg +paradoks +wgw +jaejoong +www.lk +csg-fin-0204.csg +hss-ppls-0034.ppls +www.rh +mvm-ri-d086061.roslin +hss-hca-0094.shca +rim-096006.roslin +vahid +falkon +sas-intl-0018.sasg +www.wy +prevention +nesto +achin +userservices +sas-sra-0029.sasg +quadra +pineview +csg-cse-0018.csg +firephoenix +rbm +hss-ppls-0039.ppls +amorg +www.gore +mustafa1 +stratos +cooldownloads +hss-hca-0099.shca +newproject +bombsquad +jackman +www.idee +ebri083185.roslin +starboys +hss-ppls-0045.ppls +www.indy +mvm-ccbs-060236.ccbs +humphrey +appsfacebook +starback +paginaprueba +www.jazmin +smw +mvm-ri-d007155.roslin +www.iris +floresta +xyz111 +www.children +www.itec +www.isra +csg-hr-0012.csg +www.roman +hss-hca-0115.shca +www.visualbasic +mvm-ri-l137027.roslin +goodmusic +affect +www.micasa +mvm-ri-l134241.roslin +hss-iad-0044.iad +mvm-ri-d106131.roslin +loa +pur +hss-ppls-0051.ppls +www.renata +thekillers +csg-hr-0062.csg +hss-hca-0121.shca +futureworld +periodico +gameshell +brethren +motociclismo +reg-oldcoll-g-rfoyer-dhl-1.csg +csg-est-0089.csg +hss-ppls-0056.ppls +deathrun +www.maze +concepcion +hss-health-l20.health +www.prevencion +sci056.scieng +hss-hca-0126.shca +www.bicentenario +nvc +mvm-sbms-0054.ccns +hss-iad-0040.iad +hotnews +cromarty +ebrptsql.roslin +highlights +www.nina +freeyourmind +marumon +hayashida +csg-pps-0022.csg +www.esc +www.muse +ppls-printer17.ppls +autohits +www.xd +edogawa +mailbe10.staffmail +www.omar +loh +csg-hr-0041.csg +mvm-ri-l115194.roslin +ppls-printer23.ppls +alwayson +www.mercury +lect-hca-010.shca +ppls-ccace-01.ppls +testgame +kgk +tattoos +www.yorkshire +mvm-ri-l127158.roslin +hss-ppls-0073.ppls +decibel +iadl3.iad +commander +www.shock +ppls-mac016.ppls +temptop +semillas +www.nasa +narutouzumaki +www.riot +smartcoder +www.sal +hss-ppls-0078.ppls +lataberna +sas-sra-0005.sasg +csg-corp-0013.csg +mvm-ri-d106202.roslin +l2top +mvm-ri-l106087.roslin +www.sims +dsb-mon1.ppls +sas-dis-0022.sasg +ifd +www.sony +nbf +www.fairyland +mvm-ri-d106228.roslin +sanal +www.suri +ris-vlx05.roslin +www.leisure +www.darkorbit +www-test.star.euclid +backwoods +everywhere +fsn +ltn +nax +mileva +easyrider +veronika +fussion +rii-085135.roslin +www.casas +hss-ppls-0084.ppls +ltv +zentai +haddohotel +csg-fin-0047.csg +sas-sra-0011.sasg +sas-scs-0010.sasg +sas-intl-0005.sasg +sce-coll-0016.scieng +www.cbtis +mvm-ri-d096149.roslin +www.pegasus +stewbot.lts +hss-ppls-0090.ppls +zelene +ccw +csg-fin-0107.csg +imnotafraid +sas-sra-0016.sasg +cobain +provac +sas-scs-0015.sasg +oblivionguild +easyliving +gurdeep +www.cynthia +est057.csg +sas-intl-0011.sasg +bg4 +henrique +touchme +bg3 +bg1 +scotsman-kbserv1 +newhack +scotsman-kbserv3 +ppls-g26-011.ppls +ppls-y2lab-010.ppls +hessel +mamita +penguinshow +publik +snake1 +hss-ppls-0105.ppls +sas-sra-0022.sasg +love8 +sector +mvm-ri-i136081.roslin +csg-est-0119.csg +csg-est-0036.csg +pag +ocha +sas-intl-0016.sasg +delfind +health-omq-038.health +euc024.sasg +tramites +aspirantes +ppls-g26-016.ppls +ciclismo +statement +www.nowayout +hss-ppls-0111.ppls +mendez +rsg +cityville +csg-fin-0217.csg +sweet-dreams +x-zone +delawder +poli +www.environmental +sas-sra-0027.sasg +csg-est-0086.csg +sas-intl-0029.sasg +eletronica +sas-intl-0022.sasg +kuldeep +ppls-g26-022.ppls +obb +hss-ppls-0116.ppls +informatic +fairview +mvm-ri-d086025.roslin +cybernet +sas-sra-0033.sasg +mvm-ri-d116084.roslin +csg-sec-0010.csg +csg-est-0146.csg +shelley +sas-intl-0027.sasg +eurostar +www.delux +ms-dw6-1-genoffice-mfp-bw.health +hss-ppls-0122.ppls +www.bluemoon +roadkill +audiobook +tsukahara +teu011.csg +www.f5 +tsadmin +sas-sra-0038.sasg +csg-est-0196.csg +greenvillage +mvm-ri-i115150.roslin +hss-ppls-0127.ppls +ppls-ma-old.ppls +speedupmypc +blueray +health-lap48.health +ris-esx04-nic2.roslin +mvm-ri-d107206.roslin +rgomez +csg-est-0199.csg +zcom +hss-ppls-0133.ppls +gehealthcare2.ccbs +www-dev.etime.finance +ril-v097033.roslin +csg-est-0316.csg +muhamad +www.je +blubber +onlygames +ris-lx04.roslin +mvm-ri-m125233.roslin +sas-cas-0040.sasg +hss-ppls-0138.ppls +www.services.adminrae.planning +g2ctm2.ccbs +heartless +sas-intl-0050.sasg +int-usbmac5.sasg +kubin +hss-ppls-0144.ppls +oc-1-r210-mfp-bw.sasg +otherside +www.uc +sas-intl-0055.sasg +mvm-ri-vbarry.roslin +mvm-ri-sx02.roslin +www.julian +ppls-mac001.ppls +bouanane +wfm +minera +clubdescargas +mvm-ri-l136120.roslin +hss-ppls-0150.ppls +sabino +mouse-db.bioservices.aaps +infernal +srv112.csg +pcsoftware +sas-intl-0061.sasg +rid-vrepos.roslin +ppls-mac006.ppls +hss-ppls-0155.ppls +mvm-ri-d087030.roslin +samael +thanatos.activedir +info9 +csg-cse-0020.csg +ppls-mac012.ppls +www.softzone +ppls-7gs-058.ppls +www.facebook2 +www.facebookk +offprinter2.scifun +hss-ppls-0130.ppls +desiree +scaner +pcserver1-2 +www.onlineshop +breakout +ppls-mac017.ppls +sichem +test2010 +maike +harden +happy1 +hss-health-0111.health +sarvesh +hss-iad-0045.iad +bradford.lts +www.faceebook +www.mara +ppls-mac023.ppls +www.maggie +mahen +csg-hr-0063.csg +hss-ppls-0172.ppls +gfd095246.roslin +hamzah +hss-ppls-adl02.ppls +thunders +hamza1 +lorenz +ebri003177.roslin +mvm-ri-d116226.roslin +lilis +hss-ppls-0177.ppls +vmed +www.salsa +hanabi +pab +mvm-ri-d127201.roslin +dimensionx +lunatik +sas-dis-0003.sasg +confort +health-omq-003.health +ppls-mac034.ppls +www.madrid +tyre +csg-pps-0023.csg +hss-ppls-0183.ppls +sas-dis-0008.sasg +lacasa +ebr-i500.roslin +servit +twar +tuku +multiverse +giorgi +hss-ppls-0188.ppls +mvm-ccbs-060380.ccbs +sas-dis-0014.sasg +www.cadillac +mvm-ri-d106227.roslin +hss-ppls-0194.ppls +csg-fin-card3.csg +www.little +sexual +ppls-pc179.ppls +sas-dis-0020.sasg +syma +data-nas3.ppls +zaadu +hack3d +mailfe11.staffmail +www.marcos +hss-ppls-adl24.ppls +swag +www.marina +hss-hca-0019.shca +www.comunicate +mvm-ri-d127142.roslin +www.matrix +www.theghost +www.dico +csg-corp-0014.csg +faecbook +csg-saf-0035.csg +pasarela +www.darkempire +sce-coll-0042.scieng +hss-ppls-0210.ppls +mvm-ccbs-060401.ccbs +sas-dis-0025.sasg +scieng0.scieng +mvm-ri-l067166.roslin +hss-ppls-0215.ppls +sas-dis-0031.sasg +mastergamers +hss-health-0004.health +mvm-ri-d115176.roslin +csg-fin-0108.csg +www.tcr +meb +www.matematik +www.sociales +hss-health-l18.health +22-2sciennes-g-siteoffice-mfp-bw.csg +ktnlaser01.roslin +soniya +cordova +csg-est-0037.csg +sone +mvm-ccbs-060417.ccbs +freeup +www.abs +www.ade +honeybone-ltop.ppls +www.afs +sispro +www.aki +stadtplan +mediaone +ppls-skype.ppls +www.ani +gartner +mvms +lmao +hca-spglab-021.shca +www.ark +csg-as-0000853.csg +ppls-laby2-002.ppls +newhaven-touch.scieng +sjqy +tamo +www.ccp +csg-est-0087.csg +mvm-ri-d107199.roslin +www.cdp +coolpage +sfss +www.cle +hca-spglab-026.shca +csg-sec-0011.csg +stelios +www.crc +csg-est-0147.csg +mvm-ccbs-060428.ccbs +ebr-bcd2.roslin +www.cst +www.cvc +hca-jpglab-004.shca +mvm-ri-l127186.roslin +ris-vlxweb02.roslin +sas-alumni-0039.sasg +hca-spglab-037.shca +www.gci +hca-rc-002.shca +www.gif +www.fsm +mvm-ri-d125068.roslin +hca-jpglab-009.shca +mvm-ccbs-060440.ccbs +www.gpa +hca-spglab-043.shca +hca-rc-007.shca +ebri003158.roslin +sctc +ris-vlxnbmaster.roslin +ppls-igel-b-21b.ppls +csg-est-0317.csg +www.jjm +mvm-ri-d127123.roslin +www.memory +hca-spglab-048.shca +hca-rc-013.shca +www.las +mvm-ri-d086122.roslin +www.micronet +csg-saf-0029.csg +hca-jpglab-021.shca +homebanking +mvm-ccbs-060451.ccbs +www.mcr +mvm-ccns-0021.ccns +mvm-ri-d105248.roslin +mvm-ri-l095144.roslin +hca-jpglab-026.shca +mvm-ccns-0026.ccns +toscana +ppls-printer8.ppls +www.mna +googgle +scat +mvm-ri-v125139.roslin +www.mrp +hca-jpglab-032.shca +www.mrx +www.a-team +rite +www.pal +mvm-ri-l137078.roslin +mvm-ri-l107188.roslin +terserah +mvm-ri-d136072.roslin +slayer +hca-jpglab-037.shca +www.pit +www.pkm +www-dev.wisard.registry +www.rad +www-trn.star.euclid +estadoavatar +hss-health-0136.health +photogroup +www.rma +www.sdx +www.rok +www.sfs +csg-cse-0021.csg +www.sgp +phhh-mfp-reader.csg +sce-coll-0006.scieng +www.shp +www.smt +www.sok +www.sos +weir-g-14-mfp-bw.scieng +nounours +g2cdbdev1.ccbs +www-test.eauthorisations.finance +www.mga +www.tsh +backup-atm +umesh +csg-hr-0014.csg +www.marvin +wolfteam +www.nora +csg-eusu-0006.csg +hss-iad-0046.iad +www.precios +www.slipknot +darkanime +mvm-ri-d116252.roslin +health-omq-039.health +mundoanime +www.paranoia +rj45 +csg-hr-0064.csg +webfrontend-lb.staffmail +zones +cmacbookdsb.ppls +habboz +habbux +weir-g-11-mfp-bw.scieng +radiator +rads +health-omq-028.health +www.feri +mvm-ccns-0054.ccns +www.judgement +www.victorhugo +eplab2.ppls +mvm-ri-m086015.roslin +www.fakebook +ccbs-mvm-060455.ccbs +csg-fin-0059.csg +mvm-ri-l137150.roslin +xfactor +csg-pps-0024.csg +ibrahem +mvm-ri-l096148.roslin +mvm-ccns-0065.ccns +www.today +epis1.scieng +mateus +mvm-ri-i115139.roslin +www.newstyle +solano +csg-est-0149.csg +www.kosmetyki +sas-ssp-0015.sasg +adult-dating +sce-coll-0067.scieng +virtualtuning +escalante +cam-mac005.sasg +www.chihuahua +thedie +ooo0 +hss-ppls-0067.ppls +asdfasdf +nce +psy-tmp-phd-01.ppls +sas-alumni-0002.sasg +bioinformatica +eso-laptop1.csg +blackdead +www.familia +www.pcdoctor +www.santander +blackfire +pjuegos +www.playstation +khamim +jhyun +mvm-ri-l127151.roslin +olaf +www.infinity +mvm-ri-d096075.roslin +cam-mac011.sasg +tdt +mailfe12.staffmail +mechatronics +bax +csg-corp-0015.csg +stavros +chanty +csg-saf-0036.csg +www.modelo +www.exseed +hss-health-0029.health +baracuda +masster +sigmar +sas-alumni-0007.sasg +www.pars +csg-fin-0093.csg +much +mytestsite +liss +mvm-ccns-0082.ccns +www.jalisco +cyberdevil +www.tango +syncmaster +metalmilitia +hss-health-l44.health +ppls-stlaptop.ppls +cdd +guevara +www.mgr +antique +nimi +csg-fin-0049.csg +geo-cc-004.scieng +sombras +sas-alumni-0013.sasg +mvm-ccns-0087.ccns +csg-fin-0099.csg +crayola +luba +sas-reg-0044.sasg +www.guatemala +lect-cassem-001.sasg +sas-alumni-0018.sasg +csg-fin-0120.csg +mvm-ccns-0103.ccns +flicker +hakan +www.sou +mane +wrw-01m-30-mfp-bw.shca +csh-g-g20-mfp-col.csg +flashpoint +10dc-g-siteoffice-mfp-bw.csg +icerose +megapromo +sherman +ml-3-3-31-sfp-bw.sasg +losperros +ladii +ddg +hss-health-0020.health +m.english +csg-est-0038.csg +millions +creaweb +nase +www.paramore +mediosdecomunicacion +www.admin.careers +musicblog +margherita +www.nikita +sportscience +gnss +msanchez +acs1 +nabd +sas-alumni-0024.sasg +mvm-ccns-0098.ccns +cpo +jesuschrist +baru +netbanking +airs +rituraj +csg-as-0000854.csg +iptv1 +ebd +myusi +csgo +www.infotech +csg-fin-0220.csg +msasa +www.enfermeria +mvm-ri-i134158.roslin +ral +maza +canoa +pinv +sinfronteras +revo +eed +csg-est-0088.csg +baloon +mailbe8.staffmail +edy +shangrila +mvm-ri-d137147.roslin +efm +clayton +www.batman +sas-alumni-0029.sasg +basant +scieng-ps1.scieng +pcassist +nicolass +csh-b-b1.6-mfp-bw.csg +leod +www.banquetes +mvm-ri-l107153.roslin +mvm-ri-d136036.roslin +csg-sec-0012.csg +hickory +csg-est-0148.csg +scifunlaptop8.scifun +atticus +mare +mvm-ri-d125094.roslin +deadlock +hss-health-0091.health +saf030.csg +alfadesign +xingyu +sas-alumni-0019.sasg +lightyear +kewl +csg-est-0198.csg +ourfriends +joes +jodi +ixan +www.players +spinoza +sci072.scieng +sas-alumni-0041.sasg +utility1 +sorteos +www.anorexia +antioquia +kitesurf +vasilis +mvm-ccns-0094.ccns +varios +ggc +ipsectest +echizen +minihacker +tsm2 +mccc +mark121 +kathleen +ebri073212.roslin +ebrboxisrv1.roslin +fsk +jims +sebas +xibalba +csg-est-0258.csg +bestsoft +gfl065242.roslin +sas-alumni-0046.sasg +gmk +www.mylove +pizzaking +netmgr +wright +ppls-psylib-03.ppls +tutube +tutweb +www.francais +www.mystic +csg-cse-0039.csg +www.lala +ris-plx02.roslin +thecrow +griffin +csg-est-0318.csg +dinasty +negociodigital +sas-reg-0050.sasg +endgame +ivas +sas-alumni-0052.sasg +www.dominios +janu +zcm +victorbravo +sas-cam-0010.sasg +korisnik +ihab +gopher +clanforum +tryit +anticrisis +pruebaweb +globes +wrk043.csg +bestgame +studioadmin +odontologia +www.parati +alumno +csg-nad-002.csg +oc-2-copyroom-mfp-bw.sasg +spaceweb +www120 +funn +wspa +www121 +urbana +hasa +elitehacker +views +jjm +hillary +kerk +sas-alumni-0057.sasg +jkl +bias +www.reporting.euclid +doreen +torabora +cows +csg-bems-0002.csg +gewinnspiele +relocation +mvm-ri-d096217.roslin +www.docs.sasg +sce-coll-0032.scieng +rii-115124.roslin +csg-est-0065.csg +karna +nazareth +sbattemp.ppls +csg-cse-0022.csg +visita +www.santalucia +shca-laptop-dei.shca +mvm-ri-d126108.roslin +mvm-ri-d086218.roslin +rng +lightyagami +www.lfs +phi-pythag.ppls +solex +alexgames +unkoman +hss-health-l08.health +nosomosnada +csg-hr-0015.csg +hss-iad-0047.iad +animex +sas-mac002.sasg +sk8 +www.miri +soltec +fastmoney +csg-hr-0065.csg +rameshkumar +ppls-mac020.ppls +verkaufen +www.navi +rim-107087.roslin +csg-sec-0030.csg +www.alumnos +bestflowers +dsb-et1a.ppls +don1 +indexhtml +www.raja +mvm-ri-d116110.roslin +www.serial +huma +mvm-ri-l076115.roslin +svnproto.ppls +www.mam +mvm-ri-d125236.roslin +csg-est-0120.csg +echa +csg-pps-0025.csg +kostenlos +www.nena +www.myproject +www-dev.events +health-lap64.health +webfox +ril-104171.roslin +psy-hcn-nas3.ppls +sci-jet20.iad +felicidad +mateolaptop.ppls +www.orbita +hss-health-l69.health +sci036.scieng +www.primaria +preview03 +haka +ris-esx04.roslin +ftp.mobile +www-test.exseed +csg-fin-0001.csg +csg-corp-0016.csg +maverick1 +pcweb +recetasdecocina +csg-saf-0037.csg +donmez +peritus +nde +societe +dfdf +disconnected +appfacebook +carlitos +hyves +farideh +mvm-ri-l127008.roslin +hss-health-l02.health +www.rama +allsolutions +csg-fin-0051.csg +mailbe8r.staffmail +onelan.sasg.001.sasg +mvm-ri-m124186.roslin +rosarito +csg-est-0170.csg +csg-fin-0111.csg +csg-est-0158.csg +dsb-4-05-mfp-col.ppls +m.tr +jaga +mut +phonecard +ris-esx03-nic2.roslin +mvm-ri-m126241.roslin +stmedia +csg-est-0040.csg +lacosta +mvm-ri-d096172.roslin +aboutme +csg-as-0000855.csg +tios +csg-fin-0221.csg +rip-colour0102.roslin +softwaredownload +mvm-ri-d125130.roslin +csg-est-0090.csg +hss-health-0126.health +csg-est-0175.csg +csg-fin-0196.csg +bertrand +platinium +nta +sas-reg-0005.sasg +sci097.scieng +csg-sec-0013.csg +www.page +csg-est-0150.csg +fahmi +pca +mymovies +www.drps +hardstyle +www.oviedo +33bp-basement-b1-mfp-bw.sasg +acaiberry +sas-reg-0011.sasg +whocares +csg-est-0210.csg +freeporn +vallarta +www.buscador +mc-1-siteoffice-mfp-bw.csg +csg-est-0202.csg +spek +nbn +vijesti +mvm-ri-v127006.roslin +sas-reg-0016.sasg +amad +www.arquitecto +health-omq-018.health +playback +pmk +abogado +intex +www.mastergamers +superanimes +mvm-ri-l105205.roslin +csg-est-0259.csg +rid-115174.roslin +www.pipe +www-test.salfor.finance +sas-reg-0022.sasg +www.eves.myed +mvm-ri-d087116.roslin +www.madan +kanchan +7777 +csg-est-0320.csg +maligno +hss-ppls-0179.ppls +katyperry +pug +theanswer +sas-reg-0027.sasg +mvm-ri-d096243.roslin +dbz-episodes +freehabbocredits +csg-pps-0010.csg +www.rasta +herramientas +karthick +www.maple +www.dreamteam +www.sun +phhh-g-lab-mfp-bw.csg +wrk104.csg +uben +csg-bems-0003.csg +www.prensa +sas-reg-0033.sasg +sce-coll-0057.scieng +www.prints +csg-est-0235.csg +mvm-ri-m126134.roslin +sas-reg-0038.sasg +optik +www.rafael +ebrpttse.roslin +www.enlaces +www.pronet +playhard +wini +www.mercado +www.anormal +mvm-ri-d096065.roslin +mvm-ri-m115202.roslin +workpc +memberservice +mvm-ri-d125023.roslin +sblel1.ppls +mvm-sbms-130293.ccns +businesscenter +www.merry +jehad +hss-health-0019.health +hss-health-l34.health +soptec +jacaranda +pl-b-catering-mfp-bw.csg +sitemusic +csg-cse-0023.csg +sce-coll-0039.scieng +technetium.ucs +hss-ppls-0009.ppls +elsaka +keller +www.real +mvm-sbms-130271.ccns +blazers +sas-reg-0049.sasg +www.psycho +hca-mac002.shca +csg-hr-0016.csg +ecci-3-307-mfp-col.scieng +api.money +www.cristovive +sas-reg-0055.sasg +hca-mac007.shca +faceeboook +realcom +kicha +mvm-ri-m095203.roslin +mvm-ri-l136090.roslin +bestteam +qwertyui +hss-iad-0048.iad +csh-1-1.3-mfp-col.csg +schooldemo +csg-hr-0066.csg +sas-reg-0061.sasg +newslist +anorexia +marimba +ml-3-photocopy-mfp-col.sasg +sas-reg-0066.sasg +hca-mac018.shca +www-test.adminermis.planning +www.obb +wisata +mvm-ri-d077235.roslin +softwaretest +maquina +elmatador +www.regina +ugm +mvm-ccns-srv1lom.ccns +mvm-ri-d096136.roslin +www.paginaprueba +www.juguetes +serviciosweb +pluto2 +csg-pps-0026.csg +gothic +www-test.eit.finance +system1 +alarabia +mvm-sbms-130303.ccns +sas-reg-0072.sasg +martial +xiomara +www-test.forums +nightowl +sfp +mvm-ri-l115158.roslin +tvc +privaters +www.mov +browsergames +santra +sas-reg-0077.sasg +hca-mac029.shca +king123 +freegold +sci062.scieng +ebri073202.roslin +www.sexo +mvm-ri-d077039.roslin +mvm-ri-d127138.roslin +hca-mac030.shca +sas-bu-0009.sasg +hca-mac035.shca +mvm-ri-d126027.roslin +labib +www.epis +mvm-ri-l116092.roslin +fbgame +www.roxy +oscom +adim +www.shoe +www.petcare +thekiller +csg-fin-0002.csg +csg-saf-0038.csg +sas-reg-0088.sasg +jaypee +silentkiller +www.freechat +networksolutions +specialforces +hca-mac041.shca +elmas-mac.ppls +animeland +fanfan +www.ricardo +www.slap +susy +www.cvresearch +petcare +www.regalos +vri +melu +openbook +phlaptop.ppls +unipol +audiomaster +solver +suspend +hackersworld +blacker +gangxta +sbimgtest.ppls +wibawa +hss-ppls-0191.ppls +image170 +mahavir +csg-fin-0052.csg +sas-reg-0094.sasg +hca-mac046.shca +hotmarket +mvm-ri-d116028.roslin +csg-fin-0112.csg +www.alf +www.ami +worldwid +shailesh +mydatabase +facbok +www.are +sas-reg-0109.sasg +www.cad +mac24arg.ppls +www.asg +www.ata +fatalerror +tomomitsu +www.avm +tik-tak +www.boo +goodstuff +gamehub +itran +www.cim +login111 +www.transparencyadmin.fec +ris-vlx11.roslin +vone +www.ddr +aquaservice +www.cpc +tmw +mcfly +spooks +www.dmb +www.dnc +www.formula +www.cvt +nimbuzz +dron +www.dps +www.dsb +dispenda +sas-reg-0115.sasg +www.fdm +logiclab +www.fds +anshuman +www.epo +www.fic +www.flg +www.fmc +www.flp +iwc +www.gep +f4rr3ll +monokawa +www.gio +mvm-ri-l115230.roslin +banlist +rt-test +helpinghands +paypals +freecredits +www.hrh +www.jac +sce-coll-0022.scieng +www.jaz +softworld +csg-as-0000856.csg +molto +double +www.detox +www.isa +www.isi +csg-fin-0222.csg +biz3 +facetoface +abhinav +evilboy +www.jon +sas-reg-0121.sasg +madhead +csg-est-0101.csg +www.lou +gamersclub +alienware +www.mit +www.mps +dondon +www.mus +www.astra +www.uspeh +www.ott +alarm +www.psa +www.krzyz +krzyz +www.ren +kalai +mvm-ri-l067146.roslin +www.ses +sas-reg-0083.sasg +csg-est-0189.csg +www.rps +www.rrr +sas-reg-0126.sasg +csg-est-0151.csg +www.rvr +www.sot +www.rejestracja +cyber2 +digimap +itbbs +ris-biolx01.roslin +est-forhill-g-keys.csg +facebooook +www.engine +www.vms +www.vsm +dante.ppls +sas-reg-0132.sasg +facebook32 +facebook20 +canggih +www.zzz +www.singer +prevencion +kalinga +trotamundos +ryuichi +nightrider +csg-est-0211.csg +florin +linweb +gfl035235.roslin +fotografias +autodiscover.cse +bibliotek +jayaram +webdisk.cse +autoconfig.cse +www.mileycyrus +musicrecords +blackspider +sas-reg-0137.sasg +www.alcatraz +sumaho +mitiendita +www.bestofthebest +zac +onlinevideo +www.pc-gamers +securemail1 +psse +buchen +www.fanfiction +mvm-ri-l105053.roslin +sas-leaps-0005.sasg +www.marathon +sas-reg-0143.sasg +csg-est-0321.csg +www.pintura +truefriends +sas-leaps-0011.sasg +sas-reg-0148.sasg +consultas +phstl-b-financeoffice-mfp-bw.csg +optec +syko +www.supra +www.radiostyle +merk +vinhxuan +www.rick +infosystems +www.ton +netcenter +lect-health-001.health +toner +base4 +base3 +base6 +mvm-ri-d107212.roslin +dineroextra +tvt +base8 +traspaso +all4all +ufd +base7 +www.serialkiller +base5 +sas-reg-0154.sasg +www.movistar +petrovic +ris-esxi02.roslin +vis1.scieng +ris-lx09.roslin +sorrylove +www.fotografias +modt +coolgames +owc +smarteye +www.madagascar +www.testtesttest +justinbieber +guadalupe +www.mercadolibre +mvm-ri-d115227.roslin +communaute +ppls-chltop.ppls +www.pad +specialist +rodolfo +www.motocross +phi-plato.ppls +facebo0k +omp +heller +loca +csh-g-g21-mfp-bw.csg +testarea +globalchat +wlkt +hss-health-l59.health +sas-reg-0159.sasg +wakwak +sci026.scieng +rajakumar +mvm-ri-l094181.roslin +sas-reg-0165.sasg +www.webs +injection +wakaka +mvm-ri-l087098.roslin +www.entrepreneur +giftshop +www.pepsi +www.target +ric67255.roslin +disable +www.xboxlive +www.uriel +camila +cerc3.roslin +csg-cse-0024.csg +cuartoa +www.yamato +www.monavie +sv74 +www.student-experience +sas-reg-0171.sasg +fanfics +riv-vc02.roslin +csg-hr-0017.csg +fastline +sollid +hss-iad-0049.iad +fenerium +animelatino +libe +csg-fin-0101.csg +memes +suicide +www.maristas +esx12 +sudhakar +csg-cse-0057.csg +metatron +spamgw-fb +mvm-ri-m126231.roslin +www.gigabyte +www.soluciones +finplan +csg-hr-0067.csg +sas-reg-0182.sasg +www.tra +bashayer +portalcliente +tumoda +www.gamecenter +overseas +dartagnan +www.newyear +www.intra.vacancies +www-dev.admin.careers +www-beta.myed +www.socios +hss-health-0116.health +totalwar +www.robin +rid-096215.roslin +www.radiomax +www.salvador +sas-reg-0187.sasg +ashwani +www-dev.pubs.recordsmanagement +blackwolves +television +facebookk +facebooki +ytrewq +besiktas +magistral +www.apuntes +forum.beta +faceboock +revshare +sprzedaz +wep +bimde +csg-est-0209.csg +pemilu +dipika +www.thewalkingdead +clickhere +saral +www.recovery +youporn +piccolo +mvm-ri-d134246.roslin +www.fight +csclub +www.ruben +csg-pps-0027.csg +sas-reg-0104.sasg +artedigital +www.calentamientoglobal +wrk-jet10.csg +coolradio +www.bigboss +ppsxer2.csg +sas-reg-0189.sasg +www.prime +mvm-ri-d086163.roslin +aerosol +opteron +hca-netprint-bw11.shca +firedragon +www.stella +ncis +thestig +thespot +detodoparatodos +nightmares +chatcam +nasiri +www.tqmobile.dev +health-omq-008.health +sinlimites +webdesing +teenangels +hss-hca-0102.shca +csg-fin-0085.csg +tupagina +pspstore +tdcom +internetwork +www.styles +hss-health-0105.health +www.trinidad +solochat +handc-mhist15.shca +promocja +optimusprime +www.mechanics +csg-saf-0040.csg +www.show +mvm-ri-m106233.roslin +amirul +payesh +fortesting +ppls-macbook2.ppls +csg-fin-0053.csg +health-omq-029.health +renova +csg-fin-0113.csg +hca-tlab-020.shca +patrimonio +www.summit +dika +sce-coll-0047.scieng +vigo +thehive +www.armenia +anabella +summoner +zuniga +mian +kconspiracy.ppls +www.mp3music +sb2012eval.ppls +sas-reg-0099.sasg +radiofm +thelover +www.salem +propools +csg-est-0042.csg +poop +www.trauma +www.itsmylife +clancsi +grancanaria +gustavomartinez +health-mac004.health +rid-057082.roslin +www.raf +hss-health-0009.health +csg-est-0102.csg +vgs +hss-health-l24.health +enternet +papaz +www.liverpool +pcscifun11.scifun +ebri023162.roslin +www.gabriel +distancia +prokom +www.cfd +cyberlink +premiergolf +socute +eitin +www.mystore +uidev +mecal +c600c +c600b +apibeta +ecal +uidemo +uibeta +ichikawa +www.tukasa +psico +c600a +eitin-email +hombres +ceitin +test.reports +ptp2 +www.netcom +www.arsenal +apiprod +antoniosantos +uiprod +ptademo +og +sonicteam +jangueo +cmeitin +oghma +csg-as-0000238.csg +hss-hca-0107.shca +psoft +www.projectx +www.oconnor +temptation +csg-est-0152.csg +mvm-ri-d117236.roslin +enriqueiglesias +sasg-oldcoll-r203.sasg +www.dineroextra +imprenta +www.teamo +www.radiomix +mvm-ri-m135073.roslin +mx99 +contratacion +www.newweb +www.sampler +csg-est-0212.csg +www.refused +mvm-ri-l106190.roslin +www.tekno +ppls-bert2.ppls +ril-106002.roslin +cas-mlb3-004.sasg +www.sofia +csg-est-0262.csg +dreamroad +sas-bu-0005.sasg +www.casino-online +www.jennifer +www.contactus +www.freegames +horacio +difusion +hca-tlab-008.shca +rcmodels +cas-mlb3-010.sasg +headshot +todalamusica +bestportal +angielski +grosik +www.rssjobs.careers +wrk053-2.csg +elvs01ts02.roslin +csg-est-0322.csg +oudev +www.megajuegos +scsn2 +pinna +foxsports +sas-bu-0011.sasg +www.viktor +www.alm +gisap-ov +exch7-ov +fwwlan +cfmh +juancarlos +primera +selectron +www.promocja +mvm-ri-d076057.roslin +hca-tlab-014.shca +thebeat +csg-est-0220.csg +cas-mlb3-015.sasg +puebla +www-test.downloads.euclid +phpcoder +hss-health-l31.health +sas-bu-0016.sasg +www.wagner +facebook-login +sas-bu-0037.sasg +hca-tlab-019.shca +sapling +incentive +mvm-ri-l115229.roslin +wrk106.csg +www-dev.scs.euclid +sas-bu-0022.sasg +www.sensor +mvm-ri-d107059.roslin +watchmen +scifun2.scifun +skd +ohlala +www.mountainbike +csg-as-0000839.csg +ppls-barbmac.ppls +redflag +videoadmin +rid-077044.roslin +politec +xmix +sas-bu-0027.sasg +csg-fin-0195.csg +www.akropolis +sas-bu-0033.sasg +elcloset +aguilas +canarias +ppls-y2lab-120.ppls +www.todojuegos +csg-cse-0025.csg +saf064.csg +sas-bu-0038.sasg +hss-iad-0001.iad +offprinter3.scifun +sas-bu-0044.sasg +innovacion +nouri +hss-iad-0051.iad +graficos +sumt +sec03.roslin +rii-115189.roslin +technomarket +csg-hr-0068.csg +mvm-ri-l115220.roslin +csg-as-0000830.csg +bombay +sce-coll-0012.scieng +nt6 +recargas +mvm-ri-m106020.roslin +shua +vrc +mikel +nailart +shaun.ee +www.cul +wwms01m27igel.shca +www.rcmodels +rip-e01m4.roslin +sask +sar7 +www.blackwolves +psy-pc021.ppls +mvm-ri-l127095.roslin +avecamour +mysterio +csg-pps-0028.csg +crieff +www.industrial +stelizabethannseton +mobydick +mvm-ri-i075016.roslin +www.sgs +www.nicole +health-omq-034.health +pepi +fredo +www.spectrum +chabab +hss-health-l64.health +ordu +ucu4.ucu +remas +yoo +student11 +myit +tvh +gourou +www.gas +www.nouri +www.topgames +pitagoras +www.evm +lau02.roslin +lsbb +www.velas +mexy +lois +www.wizard +mailbe11.staffmail +progreso +teka +mvm-ri-d116080.roslin +prettygirl +granados +loft +mvm-ri-l096154.roslin +www.sombras +klnm +imedios +csg-fin-0004.csg +www.lamoon +gamearea +r222 +kedu +a-team +www.imperium +jmjm +yudha +irus +www.acme +jari +riv-idr8aud.roslin +www.agus +ris-buildlx01.roslin +www.bara +csg-saf-0041.csg +www.alba +www.bart +www.alef +www.alma +iedu +www.amix +xzerox +voltron +hati +ebri033186.roslin +csg-fin-0054.csg +csg-sec-0014.csg +www.argo +isg-lj500.ccns +demo.unidesk +qa.gateway +esko +gaad +www.slm +cope +www.cctv +www.arboretum +purr +www.bomb +mvm-ri-d107192.roslin +www.chao +emlak +sinema +csg-fin-0114.csg +www.chic +hca-escreen-02.shca +maxam +botn +www.cisa +cerl +www.dart +7gs-g-5-mfp-col.ppls +www.cmcc +msj +www.vcm +www.cody +www.desk +jalali +yaghoobi +gamehack +www.dice +dlf +hss-health-0035.health +plone3.ppls +sepehri +dpl +hss-health-l49.health +www.earn +www.eden +www.eddy +stasi +crazyjane +balmoral +www.down +dmccarth-macb.ppls +astrosoc +www.eman +fairlight +www.alessandro +skap +www.fifa +csg-est-0043.csg +www.find +www.gaia +7gs-g-10-mfp-bw.ppls +www.ersa +www.gala +www.even +www.flog +www.flor +crazylab +fire-net +www.eauthorisations.finance +www.foxy +disturbed +csg-est-0167.csg +www-dev.eves.myed +www.hair +abcdefghijklmnopqrstuvwxyz +www.hana +csg-as-0000858.csg +buyung +lect-hca-006.shca +csg-est-0103.csg +mvm-ri-d116151.roslin +haida +ppls-y2lab-006.ppls +ktncolour01.roslin +csg-sec-0016.csg +csg-est-0153.csg +www.jazz +frontdesk +www.inco +www.jeep +eccc001.scieng +ppls-y2lab-012.ppls +www.kala +ppls-igel-f-30.ppls +portoalegre +www.jojo +health-lap-023.health +www.juan +www.judo +www.lead +csg-est-0213.csg +psy-haloscopic.ppls +www-test.wisard.registry +jharris +www.lina +www.mane +gimel +ppls-y2lab-017.ppls +cookingclass +32bp-g-corridor-mfp-col.sasg +www.mccc +mvm-ri-d134250.roslin +eldoctor +www.mesh +www.loki +hss-ppls-0060.ppls +www.long +hss-health-0106.health +www.mian +csg-est-0323.csg +gameplay +img50 +img51 +pixelstudios +ebri003189.roslin +www.lulu +minegocio +info21 +volia +mvm-ri-m126043.roslin +www.mono +mvm-ri-d077040.roslin +www.more +www.wanda +ultrapurewater +wrk107.csg +weir-g-29corr-mfp-col.scieng +sausages.cache +mvm-ri-d086153.roslin +ris-vlxbio03.roslin +speles +www.pera +rii-pda1.roslin +sas-cam-0001.sasg +denny.ppls +siltop.ppls +hca-spglab-040.shca +sas-cam-0006.sasg +www.posh +saf008.csg +phil-mcltop.ppls +wwms126igel.shca +www.ropa +winsrv1.ppls +www.ross +royalarmy +srv057.csg +sas-cam-0012.sasg +csg-scecc-0003.scieng +hss-iad-0002.iad +mvm-ri-i055151.roslin +www.snow +www.snte +www.song +csg-hr-0020.csg +sce-coll-0037.scieng +www.iptv +mvm-ccbs-060001.ccbs +www.ssss +sas-cam-0017.sasg +www.tuki +www.unik +www.vlad +hss-iad-0052.iad +vis005.sasg +csg-hr-0069.csg +pritchard-lptop.ppls +mvm-ri-d086224.roslin +fabiana +www.doctorwho +libertad +twk +sas-cam-0023.sasg +geo-cc-005.scieng +vis011.sasg +serafin +cas115.sasg +avpn +athletic +www.jjj +hss-health-l14.health +sas-cam-0028.sasg +ebri103169.roslin +www.iss +pitter +vis016.sasg +linko +csg-pps-0029.csg +mvm-ri-d127047.roslin +retrospect1.ccns +mvm-gf-l115206.roslin +mytvonline +www.astral +mvm-ri-l137181.roslin +mvm-ccbs-060023.ccbs +sas-chap-0005.sasg +www.jol +mvm-ri-d125242.roslin +newhaven-netlinx.scieng +www.khb +hca-rc-009.shca +globalsolutions +usenet +fanlisting +www.peternakan +ppls-m-sonnet.ppls +www.think +ppls-y2lab-100.ppls +csg-saf-0042.csg +ppls-mac029.ppls +geinternalip1.ccbs +www.toys +maill +melvin +mvm-ri-d096116.roslin +csg-fin-0055.csg +ppls-y2lab-105.ppls +www.blossom +rid-096160.roslin +7gs-g-5-mfp-bw.ppls +hss-health-l75.health +www.tcm +sas-cas-0007.sasg +sci042.scieng +csg-fin-0115.csg +habbocash +ppls-y2lab-111.ppls +sas-cas-0013.sasg +csg-fin-0165.csg +www.ksm +ppls-y2lab-116.ppls +www.mcb +www.aviator +www.crearte +csg-est-0044.csg +mvm-ri-d115065.roslin +www.mcm +ebri993167.roslin +habboking +habbolife +csg-as-0000860.csg +electronica +www.sto +sas-cas-0018.sasg +csg-fin-0225.csg +ediciondigital +cas160.sasg +ppls-y2lab-122.ppls +csg-est-0094.csg +habboside +sas-cas-0024.sasg +immobiliare +www.transparency.fec +ppls-y2lab-127.ppls +www.mim +oldhome +csg-sec-0017.csg +csg-est-0154.csg +mvm-ri-l137074.roslin +publimajes +sas-cas-0030.sasg +hss-ppls-0076.ppls +mavi +www.everest +afterschool +mvm-ri-d126246.roslin +csg-est-0214.csg +venu +massimi-ltop.ppls +zapateria +producciones +sonder +actimel +ris-netbackup.roslin +hss-health-0132.health +tchoukball +www.playstation3 +int-jet11.sasg +wormwood +www.florian +sas-cas-0035.sasg +sce-coll-0002.scieng +jonathang +sas-cas-0041.sasg +il-mac03.sasg +mvm-ri-d106010.roslin +www.digitalmedia +www.msp +csg-est-0324.csg +laser13-wp.roslin +mvm-ri-d086178.roslin +rid-046104.roslin +pruebasweb +greendragon +csg-corp-0009.csg +waterlife +mlearning +antibullying +siat +wifiman +redbeard.ppls +health-omq-024.health +csg-hr-0071.csg +ris-onelan01.roslin +mvm-ri-m086011.roslin +ppls-pc11.ppls +www-beta.pure +need +blackhat +degrassi +mvm-ri-d116069.roslin +earn-money +villalobos +feb +sas-cas-0057.sasg +hca-lab3-mac21.shca +sas-cas-0063.sasg +www.steel +ccnsm073.ccns +www.per +fridge +www.warzone +connor +www.bite +ebri093168.roslin +hca-lab3-mac26.shca +www.tibor +www-dev.hesa.star.euclid +blessed +sce-coll-0063.scieng +sci174.scieng +sas-cas-0068.sasg +ghost3 +0707 +ris-pttse.roslin +modelos +sch-admin.ppls +fbgames +bestcollection +sas-reg-0110.sasg +www.anonymous +onlinejobs +hss-iad-0003.iad +ecotours +mvm-ri-m115207.roslin +rip-colour0001.roslin +webmail.student +www.ram +www.ottoman +sas-cas-0074.sasg +mvm-ri-d125028.roslin +omega-zcdr.ccns +hss-health-0025.health +ris-lx10.roslin +hss-iad-0053.iad +th14 +ris-vwlx05.roslin +hss-hca-0001.shca +www.rex +hss-health-l40.health +sas-cas-0079.sasg +ris-vwinftp.roslin +hss-hca-0006.shca +giacmo +sas-cas-0085.sasg +hca-netprint-bw5.shca +designhome +mvm-ri-d136199.roslin +www.zafer +lionold.ppls +hss-hca-0012.shca +mainoffprinter.scifun +videos1 +hss-health-l60.health +rafaela +mailbe10r.staffmail +beerworld +ecron +ppls-y2lab-204.ppls +ppls-pc50.ppls +mvm-ccbs-060138.ccbs +www.wolfteam +hss-hca-0017.shca +princesa +sas-reg-0160.sasg +mvm-ri-i134154.roslin +mvm-ri-m136032.roslin +ppls-y2lab-209.ppls +mvm-ri-d077184.roslin +www.airsoft +mbplvdev.ccns +hss-hca-0023.shca +sas-sra-0014.sasg +www-test.transparencyadmin.fec +phoenixcorp +melchett52.ppls +scifunlaptop4.scifun +www.younes +a12345678 +ebri073156.roslin +hss-hca-0028.shca +rii-115144.roslin +csg-saf-0043.csg +hca-jpglab-034.shca +aap062.sasg +csg-fin-0104.csg +mvm-ri-i124155.roslin +secom +www.helen +hss-hca-0034.shca +csg-fin-0056.csg +mvm-ri-d097075.roslin +aegis +ppls-cm.ppls +rip-brfm7.roslin +www-dev.downloads.euclid +hss-hca-0039.shca +csg-fin-0116.csg +glamur +uids +mvm-ri-l136166.roslin +www.sunflowers +mvm-ri-l105165.roslin +jorgeblanco +mvm-ccbs-060166.ccbs +hss-hca-0045.shca +www.yugioh +csg-fin-0166.csg +csg-est-0045.csg +hss-health-0092.health +mvm-ri-d067017.roslin +ekspert +www.thc +hss-iad-0029.iad +csg-as-0000861.csg +spritz +hss-hca-0051.shca +csg-fin-0226.csg +csg-cse-0007.csg +www.portalweb +hss-ppls-0103.ppls +velasco +www.vic +www.vik +csg-est-0105.csg +hss-hca-0056.shca +www.tab +sas-sra-0019.sasg +csg-est-0155.csg +aeros +scripps +mvm-ri-i060001.roslin +www.miniportfolio.euclid +hss-ppls-0002.ppls +mailbe9.staffmail +www.miyazaki +health-mac007.health +hca-jpglab-039.shca +csg-est-0215.csg +hss-ppls-0007.ppls +odoriko2 +test-cake +okurin +kenpou +odoriko +testshutv +hss-hca-0067.shca +licensing.research-innovation +piedrahita +standalone +sas-princ-0002.sasg +bmt +vcs-157058a.roslin +mvm-ri-d115162.roslin +biz2 +mvm-ccbs-060204.ccbs +hss-health-l04.health +printer24.ppls +wintest +csg-est-0325.csg +hss-ppls-0018.ppls +mvm-ccbs-060209.ccbs +www.hastane +ailehekimligi +pps-laptop-csh.csg +mlive +hss-hca-0078.shca +zawiercie +temp-dcarmel.ppls +edneuro-mbp.ccns +tofu +www.zawiercie +siedlce +hss-ppls-0024.ppls +starwap +flykit +ppls-g26-014.ppls +ril-047134.roslin +hss-hca-0084.shca +parking1 +hss-health-0013.health +csg-est-0050.csg +lync.corp +ppls-psy-004.ppls +ris-vlx07.roslin +mobilebookwiresvc +mybip +bisnes +demo.doe +hss-ppls-0029.ppls +hss-hca-0089.shca +hss-ppls-0035.ppls +lect-health-006.health +mvm-ri-d107217.roslin +hss-hca-0095.shca +dns23 +dns24 +mvm-ri-d096106.roslin +csg-cse-0028.csg +hss-ppls-0041.ppls +cam-laptop2.csg +hss-hca-0111.shca +www.soc +mvm-ri-i005075.roslin +sas-intl-0020.sasg +hss-health-l65.health +kiba +hss-iad-0004.iad +hss-ppls-0046.ppls +csg-hr-0022.csg +joliette +hss-hca-0116.shca +wham +hss-iad-0054.iad +hss-ppls-0052.ppls +s2000 +health-pc20-1.health +readbook +mishmash +www.nanako +csg-hr-0072.csg +hss-hca-0122.shca +www.calum-maclean.celtscot +randt +heavens +www.heavens +iwa +ghazi +lcn +hss-iad-0039.iad +santhoshkumar +www.homeandgarden +kuwa +exist +mvm-ri-l005043.roslin +hss-ppls-0057.ppls +www.assist +m.www +mvm-ri-l105129.roslin +ppls-g26-020.ppls +magadmin +jcmb-pc-1 +www.hmoob +jcmb-pc-2 +jcmb-pc-3 +wwwq +jcmb-pc-4 +ccdemo +www.0 +buscar +hmoob +kht +esx24 +mobilia +esx23 +esx22 +esx21 +www.version1 +mvm-sbms-0055.ccns +sas-reg-0176.sasg +local5 +rl2 +news4 +www.domain-registration +wlm +korma +hss-ppls-0114.ppls +garlic +csg-est-0099.csg +direct123 +hue +snlaptop.ppls +ipv6test +hss-ppls-0063.ppls +mvm-ri-i134180.roslin +ppls-printer18.ppls +hss-iad-0050.iad +mvm-ri-d076098.roslin +palapa +www.ccts.careers +hss-ppls-0068.ppls +corpdev +stage.admin +palmer +mailout01 +mvm-ri-l126132.roslin +sgadmin +csg-fin-card1.csg +bestworld +ppls-printer24.ppls +csg-hr-0051.csg +mediaserver2 +huduma +ospace +hadoop1 +hadoop2 +newreports +licenses +hss-health-0122.health +seishu +hss-ppls-0074.ppls +algoma +refworks +books2 +dataverse +sas-sra-0001.sasg +spkt +mvm-ri-d134252.roslin +pgl +mvm-ri-d077042.roslin +scotsman-1 +scotsman-2 +purpose +mvm-ri-d127170.roslin +hss-ppls-0080.ppls +bogor +www.ateam +csg-fin-0007.csg +pinfo +ijoh +bengkulu +sas-sra-0006.sasg +endah +jambi +sas-scs-0005.sasg +csg-saf-0044.csg +152 +srv012.csg +esist +ftp.server +weblin +commonground +karafarini +glasgow +sas-intl-0001.sasg +darman +ppls-g26-001.ppls +webmktg4 +mhrc +exchangeserver +hss-ppls-0085.ppls +ip9 +keshavarz +csg-fin-0057.csg +origin-attach +www.lhs +template2 +farabi +520 +tucana +aen +pserver +ihc +sweeper +connect1 +connect4 +hotdeal +joomlatest +hotdeals +sas-sra-0012.sasg +rssdev +sas-scs-0011.sasg +health-omq-014.health +sas-intl-0006.sasg +rid-115170.roslin +mnt +sarmad +ppls-g26-006.ppls +myac +gku +tort +huffman +www.db2 +phppgadmin +schulweb +abri +wel +walentynki +hss-ppls-0101.ppls +sas-sra-0017.sasg +gmis +www.ile +www.cct +www.schulen +sas-scs-0016.sasg +sas-intl-0012.sasg +autoconfig.club +autodiscover.club +eccc-mac001.scieng +hss-ppls-0120.ppls +csg-est-0160.csg +ppls-g26-012.ppls +hss-ppls-0106.ppls +e-prihlaska +csg-fin-0167.csg +sas-sra-0023.sasg +mvm-ri-d086195.roslin +searchdemo +sas-scs-0022.sasg +csg-est-0046.csg +sas-intl-0017.sasg +autoconfig.card +mvm-ri-l126193.roslin +autodiscover.card +webdisk.web-hosting +11infst-1-drawingoffice-mfp-col.csg +ppls-g26-017.ppls +hss-ppls-0112.ppls +aaaaaaa +snihon +riv-vc01.roslin +shizuka +mvm-ri-d084185.roslin +csg-est-0096.csg +sas-intl-0023.sasg +sce-coll-0065.scieng +sce-coll-0053.scieng +sci176.scieng +mukai +ppls-g26-023.ppls +numbertwo +hss-ppls-0117.ppls +jpns +suivi +sas-sra-0034.sasg +hiv +autoconfig.transport +comm7 +www.curs +managers +webdisk.transport +ettest.ppls +autodiscover.transport +mailguard +csg-sec-0020.csg +csg-est-0156.csg +nursie.ppls +sas-intl-0028.sasg +health-mac010.health +hss-health-0021.shca +daisy.ppls +ris-vdlx01.roslin +hss-health-0015.health +csg-est-0216.csg +hca-netprint-bw3.shca +hss-health-l30.health +csg-est-0193.csg +hss-ppls-0128.ppls +sas-intl-0040.sasg +chp006.sasg +hss-ppls-0134.ppls +csg-est-0326.csg +ridley +sas-intl-0045.sasg +ris-winnbevault.roslin +hss-hca-0010.shca +hss-ppls-0140.ppls +sas-intl-0051.sasg +int-usbmac6.sasg +canser +brookes +stir +1gs-g-corridor-mfp-bw.ccns +dundee +napier +www.cla +ngage +psy-lap-06.ppls +oc-g-reception-mfp-bw.sasg +sms-studies.ppls +wgh-worksprinter.csg +demo.m +mailfe10.staffmail +mvm-ri-d136022.roslin +data.hanscom +wachusett-rhs +fps-web +ns1.sps +ppls-mac002.ppls +vybory +bcu +aru +csh-3-3.2-mfp-col.sasg +hss-ppls-0151.ppls +ris-vblx03.roslin +mvm-ri-d125079.roslin +brd +sas-intl-0062.sasg +mvm-ri-l115154.roslin +tellurium.ucs +hss-ppls-0156.ppls +monah +sci057.scieng +redondombp.ccns +csg-cse-0030.csg +gcu +mvm-ri-l615161.roslin +buckingham +glm +ppls-mac013.ppls +hss-ppls-0162.ppls +mvm-ri-d067069.roslin +hss-iad-0005.iad +hwu +ppls-mac018.ppls +csg-hr-0023.csg +hss-ppls-0167.ppls +sas-reg-0193.sasg +mmu +csg-est-0269.csg +mvm-ri-d115209.roslin +ucw +as12 +www.archer +nwi +dmu +presenze +edinburgh +ppls-mac024.ppls +atel +csg-hr-0073.csg +hss-ppls-0173.ppls +hss-ppls-adl03.ppls +ppls-mac030.ppls +hss-ppls-0178.ppls +hss-ppls-adl08.ppls +new-smtp +ris-vlx06.roslin +a500-repo +uatcms +mtrade +rize +burs +ppls-mac035.ppls +malatya +rii-085136.roslin +hss-ppls-0184.ppls +sas-dis-0010.sasg +corum +wgb +sas-intl-0042.sasg +pipeline01.roslin +michaels +sce-coll-0017.scieng +csg-fin-card2.csg +eknowledge +hss-ppls-0200.ppls +ish +sas-dis-0015.sasg +hss-ppls-adl20.ppls +mvm-ri-d136155.roslin +mailwfe6.staffmail +mvm-ri-d086204.roslin +ucu3.ucu +hss-ppls-0205.ppls +sas-dis-0021.sasg +data-nas4.ppls +hss-ppls-adl25.ppls +relayd1 +health-omq-040.health +csg-saf-0045.csg +hss-ppls-0211.ppls +sas-dis-0026.sasg +rss1 +ebri093197.roslin +mvm-ri-m116085.roslin +rip-brfc1.roslin +csg-fin-0058.csg +hss-ppls-0216.ppls +sas-dis-0032.sasg +tmp-psy-mac.ppls +csg-fin-0118.csg +sra-mac.sasg +mvm-ccbs-060413.ccbs +provatest +rip-brfm2.roslin +www.admin.eves.myed +www.groupware +serv03 +rid-v076077.roslin +csg-fin-0168.csg +nara.ppls +spam.cn +csg-est-0047.csg +mvm-ri-l134244.roslin +hca-spglab-022.shca +mvm-ri-d107197.roslin +gfl105249.roslin +csg-est-0107.csg +ppls-pc58.ppls +oversea +dsb-lptp1.ppls +weihu +temphiss.health +cmfs +hca-spglab-027.shca +dsb-pc2.ppls +v10 +ris-lx05.roslin +mvm-ri-d115223.roslin +riv-nlinxaud.roslin +vpn.cn +hca-spglab-033.shca +www.nils +hca-jpglab-005.shca +hsu +mvm-ccbs-060435.ccbs +work2 +www.elma +www.arbeitsschutz +www.datenschutz +reklamlar +hca-spglab-038.shca +arbeitsschutz +hca-rc-003.shca +ademo +yonet +prion +cadburybeta +synthos +snipe +tfbasic +redpoll +pell +pracodawca +mcbeta +ris-fas1a.roslin +wedelbeta +udtbeta +rako +fileserver3 +vpn.au +drongo +hca-jpglab-011.shca +hca-spglab-044.shca +ehs +hca-rc-008.shca +skua +inflight +hca-jpglab-016.shca +tern +mvm-ccns-0016.ccns +www.venue +landadmin +dictionnaire +cormorant +mvm-ri-i134169.roslin +hca-spglab-049.shca +hca-rc-014.shca +wrw-01m-26-mfp-bw.shca +vmo +hca-jpglab-022.shca +dcds +gull +mvm-ccns-0022.ccns +kcms +ppls-printer4.ppls +wrk112.csg +rodc +phi-m-mattch.ppls +ip12 +hss-health-0112.health +fileserver4 +csg-saf-0039.csg +hca-jpglab-027.shca +mvm-ccbs-060457.ccbs +mvm-ccns-0027.ccns +mvm-ri-l115180.roslin +pod1 +pod2 +w04 +ppls-printer9.ppls +bex +test.services.learn +macmini-royw.ppls +www.bex +mandr +w16 +w17 +www.abbey +webaccess2 +hca-jpglab-033.shca +mvm-ccns-0033.ccns +eflow +w02 +mvm-ri-d107208.roslin +flatland +w05 +w03 +www.videoblog +mvm-ri-i125029.roslin +spam1.cn +rupert +mupd4.staffmail +zachary +hca-jpglab-038.shca +csg-cse-0031.csg +pickup +vpn-nyc +health-omq-004.health +axis2 +itis +mvm-ri-l125249.roslin +mvm-ri-d104174.roslin +hss-iad-0006.iad +voir +heaf +wrw-02m-25-mfp-bw.shca +dayang +busdev +mybackup +csg-hr-0024.csg +www-dev.eauthorisations.finance +mvm-ccns-0049.ccns +eudb +interscan +tsmith +bsmtp +fa2 +fa1 +sas-alumni-0010.sasg +layton +ianmac-mac.sasg +csg-hr-0074.csg +box21 +csg-est-0319.csg +avupdate +jewish +lion.ppls +mvm-ccns-0055.ccns +eplab3.ppls +sce-coll-0043.scieng +lrc +health-lap14.health +scieng1.scieng +darw-8-810.csg +mvm-ri-d096051.roslin +csg-fin-0070.csg +kronos2 +mvm-ccns-0066.ccns +mvm-ri-d086229.roslin +cam-mac001.sasg +ehud +hss-health-0005.health +mvm-ri-l136253.roslin +mvm-ri-d115177.roslin +5forhill-4-attic-mfp-col.sasg +hss-health-l19.health +mvm-ccns-0072.ccns +sjohnson +mvm-ri-d074176.roslin +sas-ssp-0016.sasg +rohde-laptop.ppls +cam-mac006.sasg +mvm-ri-d115224.roslin +bs1 +ktnlaser02.roslin +sas-alumni-0003.sasg +cam-mac012.sasg +uat-dig +uat-ftp +csg-fin-0009.csg +uat-online +cqm +b08printer.ppls +uat-start +corp-relay +nonasp-nfusion +o1.pulse +uat-connect +eccc-0002.scieng +bert-ltop.ppls +sas-alumni-0008.sasg +csg-fin-0060.csg +sas-alumni-0014.sasg +mvm-ccns-0100.ccns +mvm-ccns-0088.ccns +guard1 +csg-as-0000754.csg +csg-fin-0119.csg +mvm-ri-l097128.roslin +lect-cassem-002.sasg +sas-alumni-0020.sasg +mvm-ccns-0104.ccns +sas-pharm-0001.sasg +csg-fin-0170.csg +csg-est-0048.csg +sas-alumni-0025.sasg +csg-fin-0129.csg +csh-2-2.15-mfp-bw-1.csg +mvm-ccns-0099.ccns +dsb-2-19-mfp-bw.ppls +switch10 +ppls-mac010.ppls +cmacbook.ppls +switch9 +csg-est-0108.csg +sas-alumni-0031.sasg +linuxpc.roslin +grizzly +flounder +mvm-ri-d127124.roslin +csg-as-0000239.csg +csg-sec-0022.csg +mvm-ri-d086123.roslin +sneezy +switch11 +winstats +mvm-ri-d136251.roslin +csg-est-0330.csg +csg-est-0218.csg +mailback +sas-alumni-0042.sasg +csg-est-0268.csg +sas-alumni-0047.sasg +mvm-ccns-0095.ccns +mvm-ri-l107189.roslin +tcms +csg-est-0328.csg +sas-alumni-0053.sasg +dcs2 +wrk053.csg +cisco01 +www-test.tqtelethon.dev +hss-health-0137.health +www-test.ccts.careers +www-test.secure.vle +sce-coll-0007.scieng +dsb-1-19-mfp-bw.ppls +csg-fin-0179.csg +handc-mesh02.shca +health-omq-030.health +hss-iad-0008.iad +www.omerta +mvm-ri-m086016.roslin +csg-cse-0032.csg +ms-dw6-2-7-mfp-bw.health +maldives +www.py +rinkon04.roslin +csg-hr-0009.csg +kyrgyzstan +cityd +gambia +luxembourg +southafrica +mvm-ccbs-000002.ccbs +haiti +suriname +hss-iad-0007.iad +anguilla +hss-health-0010.health +mvm-ri-d125212.roslin +ftp03 +vbv +ecampus2 +derrik +www.dreamgirl +ris-vlx04.roslin +mvm-ri-l125107.roslin +epis2.scieng +ppls-attest.ppls +ppls-jesper.ppls +mvm-ri-v096076.roslin +csg-hr-0075.csg +ppls-m-tzu.ppls +hss-hca-0050.shca +mvm-ri-d107223.roslin +www.org.planning +leilao +majalah +gfl095239.roslin +mvm-ri-m115213.roslin +15bp-4-attic-mfp-col-1.sasg +hss-health-0031.health +csg-as-0000475.csg +icl +wrk105.csg +hss-health-l45.health +webdisk.www1 +sci012.scieng +o3.email +hss-ppls-0169.ppls +cas-lap2-kb.sasg +csg-fin-card4.csg +ris-vlxweb03.roslin +csg-hr-0060.csg +7-1enp-g-siteoffice-mfp-bw.csg +ebri053187.roslin +arrnc-is.ccbs +csg-fin-0011.csg +crdp2 +mailbe12.staffmail +csg-saf-0047.csg +mvm-ri-i134159.roslin +autoconfig.test1 +rip-d01c4.roslin +webdisk.insurance +csg-fin-0061.csg +sra-jet11.sasg +mvm-ri-d096147.roslin +onelan.sasg.002.sasg +autodiscover.arts +autoconfig.resellers +autodiscover.resellers +autodiscover.local +nbr +autoconfig.insurance +mvm-ri-d095036.roslin +autoconfig.local +mvm-ltsel21.lts +csg-fin-0121.csg +webdisk.local +hss-health-0102.health +mvm-ri-l115169.roslin +josephs +webdisk.pets +www.construction +mvm-ri-i124161.roslin +flows +csg-est-0049.csg +hss-hca-0060.shca +cm1 +webdisk.arts +sas-reg-0001.sasg +npi777253.ccbs +leyou +csg-est-0110.csg +sas-reg-0006.sasg +api-old +autoconfig.arts +mvm-ri-d104164.roslin +autodiscover.insurance +www.suppliers +www.marine +ub3 +mydomains +csg-sec-0023.csg +img41 +testint +csg-est-0159.csg +ris-lxbio01.roslin +sas-reg-0012.sasg +csg-est-0219.csg +csh-2-2.7-mfp-col.csg +www-test.hesa.star.euclid +sas-reg-0017.sasg +mvm-ri-d095097.roslin +csg-est-0270.csg +envy.ph +256 +shca-laptop-mac1.shca +stat8 +149 +sas-reg-0023.sasg +infini +sce-coll-0033.scieng +csg-est-0329.csg +hca-tlab-003.shca +lp3e +142 +sas-reg-0028.sasg +www.eval +www.impress +mvm-ri-d115167.roslin +adecco +sas-reg-0034.sasg +rygel +hss-health-l10.health +mvm-ri-d115190.roslin +sauvignon +vent +csg-pps-0020.csg +227 +sas-reg-0040.sasg +mvm-ri-d086042.roslin +mvm-ri-d116111.roslin +242 +mail70 +mail100 +manganese +sas-reg-0045.sasg +www.gpr +sukusuku +mvm-ri-m125237.roslin +mvm-ri-l126244.roslin +csg-cse-0033.csg +mvm-sbms-130272.ccns +sas-reg-0051.sasg +aagc +gbpackbell.scieng +ppls-igel-s-38.ppls +mailbb +mycpanel +iemail +saf065.csg +health-lap65.health +sas-reg-0056.sasg +hca-mac008.shca +mvm-ri-l107118.roslin +r21 +mvm-ri-d096112.roslin +med-000616a.roslin +webdisk.ufa +csg-hr-0076.csg +sas-reg-0062.sasg +hss-health-l71.health +webdisk.samara +webdisk.kazan +mvm-ri-l134243.roslin +mvm-sbms-130288.ccns +sas-reg-0067.sasg +hca-mac020.shca +netstore +mvm-ri-d126003.roslin +csg-pps-0036.csg +cas-mlb3-009.sasg +profusion +mail.35 +sas-reg-0073.sasg +hca-mac025.shca +psy-actv1.ppls +autoconfig.subscribe +autodiscover.subscribe +webdisk.subscribe +wave3 +vergabe +www.vergabe +livedemo +seoservices +phwifiprint +demo00 +ereserve +hhwifiprint +host73 +host87 +ccwifiprint +hv1 +t14 +www.onlinekatalog +traci +bork +pabx +plataforma +svr2 +mbone +beard +ktai +nali +tauro +brighton-hove.foi +nuernberg +tilma +barrowbc.petitions +augsburg +parlvid +c.tilma +b.tilma +guardian.services +a.tilma +guildford.petitions +nottinghamshire.petitions +citizenconnect.staging +citizenconnect +stedmundsbury.petitions +wellingborough.petitions +westminster.petitions +planning.barnet +surrey.petitions +citizenconnect-uat.staging +gaze +asp3 +sbdc1.petitions +suffolkcoastal.petitions +bassetlaw.petitions +runnymede.petitions +disko +stevenage.petitions +bins.barnet +fpa.staging +congbao +brighton-hove.foi-register.staging +thanhtra +forest-heath.petitions +passengerfocus.staging +barrow.petitions +waveney.petitions +rbwm.petitions +barnet.petitions +melton.petitions +eastcambs.petitions +tilma-osm +mansfield.petitions +islington.petitions +sbdc.petitions +hounslow.petitions +newforest.petitions +east-northamptonshire.petitions +rushcliffe.petitions +jed.whatdotheyknow.dev +salford.petitions +blackburn.petitions +ipswich.petitions +lichfield.petitions +sholland.petitions +air-test +www.d1 +cv1 +spacesoft +recman +bbn +jugend +www.statistik +crowley +www.coroner +vpn15 +shinho +blueice +vpn12 +evaluator +vpn7 +konsultant +vard +vcse1 +fatest-mbp.cit +faitspare_mbp.cit +www.libraries +bluefire +agava +ncdm +greening +ggcc +test.mail +hpss +passwd +redhawk +www.vis +vpn14 +www.university +www.summerschool +www.tlm +vpn13 +badmin +linkup +rocketseed +zoning +www.fax +betasite +productinfo +smds-gw +bala.cit +www.dnn +eweb2 +fajphillip-mp.cit +baller +comdev +newdelhi +sexylove +inspections +coders +vmd01 +smtpout4 +pingtest +copland +herkules +l14 +www.editor +eliminator +ibss +heartnet +cctest +uke +www.nowa +ilikepie +sexylady +www.sleep +www95 +medprof +osv-support +motelgw +syglc +sfe +blogfaro +clickpb1 +clickpb2 +blogdovictor +verychic +dercio +swvx +ssml +osv-message +kvmde +debackup +osv-exchange +shamtech +www.collocation +macu +de-1 +de-2 +www.hypernet +flickr.com +dev2ns +ns10.hyperhosting.gr +www.grdomains +www.flickr.com +dv1 +afrodite +collocation +c5p4m4 +sw31 +xen-de +debackup-old +tc22 +tc21 +ns9.hyperhosting.gr +grdomains +host5b +hypernet +video165 +video164 +video163 +define +reactor +tc3 +iroda +be2 +p78 +theothers +tstyle +pressclub +jaikumar +newsrv +justin-bieber +ip124 +ip125 +ip126 +ip127 +ip128 +greenmile +ip134 +ip135 +ip137 +www.arthouse +spartak +ip141 +ip142 +ip143 +ip144 +ip145 +scriptstest +ws8 +sm8 +ip149 +ip152 +ch1 +www.pavlodar +ns222 +piko +ns165 +abc1234 +ns164 +skater +sistec +manpreet +aliceinwonderland +freethings +waheed +ns149 +fighter +starsteam +ns146 +mail.eu +vagent +mimoza +stream01 +dentista +pornstar +bino +docentia +medicalgroup +maya2 +iraqsong +reserve2 +gamescenter +shailendra +solucionesdigitales +darren +mmtest +whatson +gaudi +danial +carpark +agat +journalist +danang +dalnet +tto +data13 +zhongwen +minerals +sibbs +vim +erick +gplus +autodiscover.twitter +webdisk.linkedin +autoconfig.twitter +nelapsi-servers +beatmix +textart +orthanc +mc2pro +daewoo +blog.lib +kerdoiv +innov +ip150 +prishtina +chuszz +graphix +fos +aztech +aps1 +charliebrown +warrants +th-diary +ts02 +qps +pravo +city3 +city1 +www.sonda +www.gryf +hsmx +webedge +gryf +sourcing +virtualhost +x200 +priroda +referee +xhamster +linux7 +mindhack +autodiscover.mx +l19 +brainz +mysql58 +webdisk.mx +autoconfig.mx +mysql17 +mysql54 +webdisk.ru +cfd125 +morini +fotbal +stab +magic2 +shoppers +fume +exer +cennik +gastronomia +liuxue +www.trader +ccu +saeideros +mtlive +alip +daiko +officevpn +examenes +cfdme01 +clients2 +prokat +cheezy +funnel +ebox +cheche +kamilo +copier +www.alliance +front21 +ali1 +charli +exchas +www.goldenkey +asd1234 +goldenkey +chachu +app-1 +app-2 +mailsec +tragamonedas +bags +2fast4you +pili +books1 +loginid +sg101 +thepearl +criminals +surfer +nikunj +tetsuya +webimages +boonboon +cybertech +cedar2 +constantine +isadora +adli +mx29 +thenexus +kowalski +basher +jharkhand +morefun +testing12345 +subodh +bob123 +succes +homicides +ritu +cyberking +virat +stereo +bloggy +dzalgeria +steady +maryann +ogma +stbkat +aveiro +embla +stars2 +priscilla +star25 +tinky +nunki +digits +cinedb +devmy +moviehouse +govideo +music4fun +medoo +alphatest +newsbox +venster +teranet +caoliu +x55 +spunky +x77 +howcom +x87 +x89 +kpas +piaf +eurofins +oldptu +tonekunst +klinikholmberg +thaiscanhomes +purekidsforside +prodenmark +x30 +min-baad +bygroth-uk +x12 +x13 +udviklingskompagniet +x15 +x16 +x17 +designedwithpleasure +x20 +x21 +fuldkorn +x23 +x24 +amroptest +x26 +x27 +model47 +x29 +ilearntypo3 +x33 +x34 +x35 +x36 +x37 +x38 +x41 +albertslundweb +origin-secure +kddi +optimair +koelnmesse +kognitivpraksis +skraldetrumf +livingage +breakoutimage +skysupport +nannaleschly +urtekram-fi +nyhedsbrevholm +hkiintranet +user010 +sundhedsrevolutionen +sundstafetten +gespage2 +portail-wifi +deltakoncept +gespage +eric-photo +urtekram-de +model4 +model3 +model2 +thysen-nielsen +connecting-fields +pintxos-tapas +advocatel +tpoemobil +isfo +curait +nester +eilersen +costakalundborgkaffe +bornholms +veins +dvin +urtekram +nageshop +tand-klinikken +lhengros +familieopstilling +solagergaarden +scopti +whatcanido +sbf +tys +istatistik +hjernebarnet +cttm +skovdyrkerne +shopteam +lisereitz +tryknet +2900larocca.dk +stiki +zen-garden +mploy +dosyalar +solander +topmotion +denfriedanskepresse +holm-old +implantatcenter +hs3 +alt1 +swiftdeposit +gucuhb +securemobile +nadine +multivaco +ck-travel +fsfp +flytteforretning +ebmaalmand +hrapp +bookingblotter +mobilsite +fwpeak10 +logistics2 +birm +releaselog +codecompliance +amrop +vendorapp +activeinmatestwenty +bsocrimescene +municipalordinance +firecop +pawnshop +nutana +webeocbk +icereport +vendorregistration +bookingregister2 +hrapp2 +bsovpn +combilent +raaschou +kunstnergaarden +bookingregister +sundhedsrev +miketest +vm-1 +pallium +loftlys +larocca +karrierecafeen +pharmaforce +fsgh +noramobil +kajvhansen +urtekramblog +jeton +minbaad-shop +sadra +isy +spbridge +tug2 +sancy +kurfood +geolab +bodyschool +enallia +hydrogennet +el-light +tartufo +vzweb1 +onlinefragt +kryten +ivanmadsen +bioactive +innerpower +umahro +enalia +humanvision +frieser +horoscopes +nageold +nielsahansen +vinstedet +motivaco +signuption +nivaagaard +tolbod +dacapo-aps +rrjny +wiik +designdev +karlshoej +bovesse +wwi +shopinvent-ny +komandskab +www2007 +urtekram-uk +kernesund +sitescreen +slipslikket +emediate +maintest +danskfamilieopstiling +shopinvest +certify +tpoe +urtekram-se +circulodosmilionarios +www.circulodosmilionarios +amalienborg +en-stillads +thesociety +accellion +beckett +teikei +photostore +fs111 +candra +nabchelny +exec73 +calido +kanri1 +calama +simson +healthadmin +invicta +therealworld +airports +cmsplc +heathcote-ivory +ryodan +jarjar +nicenice +dartington +mikimotoitalian +bakerross +howiespro +staging.bakerross +mothma +veers +mikimotomobile +fbcomp +powervamp +staging.yellowmoon +luckyangel +pukkaholland +mikimotofrench +howies-de +powervampracing +mikimoto +dev.yellowmoon +gymworld +electionresults +mikimotogerman +dynamix +marajade +simonsays +network-services +bulkpowders +needa +endocott +medisin +bvwindows +gauntlet +iphone4 +bhutan +chukshelp +staging.bbq +ozzel +ee-1-12-0-2 +bismillah +teebo2 +tvgstudios +tarkin2 +anakin2 +staging.calor +padme2 +chewbacca2 +dnsin2.in +sportfish +cmsplcold +farlows +dooku2 +mobile.yellowmoon +nps1 +dnsin.in +asddsa +mikimoto-old +mikimotospanish +tarkin +greedo +enterprisedemo +dev.calor +scrubspro +arman2 +mobile.bakerross +dev.pukkaherbs +wicket +scrubsuk +aquino +bulkhelp +staging.pukkaherbs +dev.bbq +mikimotorussian +howies-test +effekta +admiralblank +stephenarnold +mikimotoamerica +hum +piett +sidious2 +elderberry +bathroomvillage +bikram +dziekanat +dev.bakerross +lyco +brainfood.howies +mobilefun4kids +lightingdirect +labhut +howiesmobile +lygo +jango2 +maxxis +ackbar2 +nserver1 +m.calor +images12 +buy-online +asalam +stadtbibliothek +psbank +esh +sparsh +www.westend +spania +wunschkennzeichen +thenorthface +lovenight +nettech +spam11 +adv1 +coen +buran +arafat +theodore +flood +lovepage +netserv +soft14 +netserv2 +casdev +tensai +hadrian +wireless-dhcp238231.dod +webdisk.anunturi +mail.cs +replics +gilles +dnsa +ezproxy.lib +pic-upload +lovelife +www.sdc +tristate +acidburn +vlan311.c1.dsl +demonoid +bbdb2 +livraison +vinco +peano.math +guildwars2 +dns-backup +matchdaymail +oldforums +darkdivinity +drzwi +www.minfin +creed +ironmail +zc1 +icy +parlament +documente +albena +grandchase +tse02 +statistica +imap5 +tse04 +www.aac +www.surabaya +www.bandung +atis +arcims +skyway +kickboxing +secftp +waterman +worldweb +maybach +snowboarding +socialgame +www.armani +tesla1 +autodiscover.students +nosik +matricula +rockheart +mecanica +hmp +rgl +djblack +artemisa +www.artis +huyhoang +athene +www.wellington +berater +www.andromeda +asistencia +asia1 +coolfriends +daniel88 +texasholdem +yousuf +oab +webcaster +www.syt +www.asia1 +linares +upcode +mackenzie +reis +setec +magadan +photos1 +packers +slamet +tatata +wi-fi +gammi +www.uniquedesign +silverknight +alhambra +www.daflow +rmtweb +www.faccebook +daflow +demohost +raytheon +unicall +dungeon +vashdom +www.mebel +avis +enef +rosreestr +smallworld +likeaboss +proekt +annonce +remington +faceb0ok +x-games +essay +pultehomes +sodapop +glink +amparo +autoconfig.dashboard +dosantos +autodiscover.dashboard +mezcal +flats +crafty +tessera +garagedoors +lab10 +marcial +finearts +nat14 +seabreeze +oliva +bharath +economie +vip-web +layla +voeux +www.regions +friendsnetwork +agis10g +hagen +kivanet10g +kassel +magdeburg +potsdam +dortmund +tajimi +int-prezza +ftp.drupal +darty +ftp.magento +mclub +duisburg +darmstadt +minden +empretecno +www.z2 +mononoke +afi +conquer +hick +takeda +shukri +futureman +aoyama +madame +shruti +4b +turniej +zafer +www.4b +basura +www.turniej +ditu +8000 +kundenservice +qz +origen-movil +www.progress +andrew1 +giordano +bugfree +w107 +finance1 +kdc1 +kdc2 +basera +devilsadvocate +barium +008 +shogun +kinzig +www.of +isearch +kalbach +phpbb2 +newnews +ffm.members +rio-de-la-plata +wwwserv +steinbach +of.members +www.hessen +westerbach +jangtsekiang +nidda +agws +sonam +bakili +www.logic +vb3 +kdg +bbs0 +searchapi +bmg +testns1 +testns2 +khachhang +shihan +somebody +net2ftp +vanhoa +orionweb +chiva +portal22 +www.economic +coolcat +truelife +rajeev +hesoyam +badges +shariq +sharin +poseidon02 +asjp +waa +prace +aa2 +shanel +sinister +deven +capes +gabana +com-vatk1 +com-nytk4 +com-nytk6 +pando-dns1 +pando-dns2 +pando-dns3 +pando-rss-vip +aikman +c1026-services +pando-dummytk +fcuk +accessdenied +pando-bliptk1 +com-cache-vip +bugs-vip +glustercl1 +com-cks-vip +glustervm1 +glustervm2 +glustervm3 +pokaz +glustervm4 +pando-tk1 +office-dr1 +office-dr3 +com-dd +pando-tk2 +office-dr2 +pando-protk1 +pando-tk4 +shaddy +pando-dws1 +ft-ecomm +shaadi +com-publisher-vip +pando-dd +x1-ws1 +publisher-vip +testssi2 +pando-oob +com-eutk1 +wanem-20 +com-nytk1 +com-nytk2 +com-nytk3 +com-nytk5 +com-nytk7 +com-nytk8 +pando-cache-vip +gf-test +preddy +pando-dgr-vip +pando-plustk1 +pando-tk3 +x1-sp1 +pando-oob-mail +pando-sb1 +x1-tk1 +tahari +pando-ptk1 +pando-ptk2 +dsta +sex-dating +pando-cks-vip +pando-ws-vip +fla +ib3 +ayhan +netforce +esso +rommel +www3stage +accm +encuentro +wmtest +pond9 +cgate +mail-b +nmrserver +mx24 +softy +mx22 +setting +seci +curp +serhan +goszakaz +sneakcode +hermsen +abonnement +sam123456 +intranettest +meis +www.sispro +concour +labinf +jacques +mobile7 +awais +www.renew +sapa +nicola +myimages +venedig +vitec +freemont +dima7 +axistv +ghetto +ndbs +www.freemont +svp +ferrets +traxx +rpb +easyserv +arc2 +andreys +ta3lim +post1 +svk +al3ilm +img.cams +mail.cams +amazinginfo +assaf +techman +easymail +binladen +itemshop +onezero +artix +www.whocares +amrica +saydon +ucl +www.motd +www.servlet +finans +pdrives +wastelands +vedic +blogfarm +www.pj +quadro +labrat +countingtweets +twittrd +nyt +mrpeople +as24 +adinda +bellaluna +sanwar +autodiscover.postgrad +webdisk.invoice +autodiscover.invoice +cmsstage +autoconfig.invoice +salinas +thanxx +sandip +methods +sanaka +222222 +webdisk.reports +serverbackup +shohada +autoconfig.training +autodiscover.training +webdisk.training +awesomeness +openaccess +dresses +oar +saiyan +g-mail +iet +eastwest +renewaltest +cybertest +akr +safiya +ambika +sam007 +ggb +logz +riet +cnbc +glide +aboali +leonhart +eastman +gridmon +spare1 +snsgwy.zdv +wm01 +medizin +saanvi +domainrenewal +best2 +betac +rws +qqqwww +planer +anuncios2 +dianne +helpinghand +democrazy +ayeha +autodiscover.affiliates +autoconfig.affiliates +azmoodeh +autodiscover.testsite +autoconfig.testsite +tlg +anmol +finanzen +pitagora +gopi +hemanth +ardis +catdog +s4.sq +fsecure +rcis +csps +www.sof +ftech +ns.s +bramj +1234567890 +dxpt +mercury2 +pushkar +xjgl +mainstream +jp1 +jp3 +gabvirtual2 +andie +sistemas2 +s005 +s003 +circlek +skillz +s004 +runes +file7 +fia +mail-02 +akuma +mail-04 +newcolors +www.soup +rodrigues +assist5.sq +tunisia-sat +kamini +afg +afu +akb +veer +qwerty12 +grundbuch +matras +anons +hfd +sport1 +jasmina +automania +kmu +ksb +fileexchange +tiko +cyberfox +b245 +www.gadgets +callmanager2 +globalservices +www.nuts +micheal +videoconference +cornetto +enligne +guest2 +informatix +visualkei +socialgo +www.admin2 +vmanager +ristoranti +blocker +bahar +mickeymouse +beholder +viaggi +racedrivergrid +siv +webtek +giti +nhp +foofighters +www.studenten +almanara +myguitar +inquery +severus +okadakisho +tibiia +vz7 +woodpecker +sys5 +sys6 +celebs +vz24 +localhost.dev +dds100 +dds101 +feisbuk +daas +gt4 +mgate +marsh +punkrock +s6.sq +fergie +mail-vip +rolls-royce +fairbanks +dvc +vipmail +riverdale +hera2 +ni2 +lib4.lib +statecollege +b.ns.chmail +design4 +d.ns.chmail +mas1 +origin-signup.mobile.devstage7 +memberstest +a.ns.chmail +f.ns.chmail +nasu +c.ns.chmail +e.ns.chmail +www.abiturient +www.pass +www.faces +www.alumniforum +imi +colleges +www.colleges +www.distant +alumniforum +ceas +pump +xboxpoints +locales +nazar +bdb1 +kemi +webmail-test +www.poem +risc +thewizard +razvoj +t-shirts +s1.ds +varejo +sa-loadbalancer1 +testlog +sa-db1 +sa-workers1 +sa-archive +tarun +jxpt +sa-www1 +admin-2 +nfuse +m.buy +seatwave +angler +changeworks +blackwater +meredith +www.poisk +balashiha +seker +peca +gvll00989 +img.m +staging.mobile +seiya +www.kiosk +daiichi +htc76 +zdalna +newip +goldbar +www.smax +www.publicidade +ussd +fields +lineservice +ad-test +k-1 +export2 +test.stats +test.exchange +www.scotland +test.register +invoicecentral +apogee +iphone3gs +pdesign +123asd +psteam +shr +krasotki +rashmi +rashid +ipphone2 +mc3 +mc4 +rapido +mg3 +ioan +maquette +cryptshare +casa2 +webdisk.katalog +eblast +rtmail +assist4.sq +youthclub +camchat +videostage +nte +hart +securemail2 +mxs1 +nl2 +enhance +lb-origin +mail.ci +watervdi +coawts +coabenefits +www.test8 +myatlis +atlis +coavdi +supportbridge +courtview +coaspam +atlisdoc +qls +sslc +catstest +sjh +jypx +sjz +yousendit +catsprod +pwops +webmailatl +atlopen +atlisdoc2 +citycouncil +xrd +coaworkaway +zjy +xn +myatlis2 +catcrp +atl26 +elmsencoder01 +atlis2 +xmail2 +assist6.sq +rt2 +nyoman +skripsi +simpel +fnet +d170 +hjp +s5.sq +probe1 +webmail8 +webmail12 +webmail14 +jnf +webmail22 +mgh +mjg +webmail30 +webmail24 +webmail18 +assist8.sq +d150 +changzhou +d156 +jiankang +d159 +d144 +d145 +dedicated2 +d146 +athkar +www.mybb +www.topics +d147 +d149 +d151 +d152 +d153 +d154 +d155 +d157 +d158 +d160 +d161 +d162 +d163 +d164 +d166 +d167 +www.katalog-stron +katalog-stron +d168 +d169 +srvmail +d171 +d172 +geoff +probe2 +shoppay +innu +mlib +1010 +libs +gridui +tky +zzc +sjjx +www.yf +wwt +10010 +doxygen +gec +ezri +webdisk.sub +chris2 +duvel +jcd +slideshow +airforce +gqt +www.pobeda +kansei +ikd +plat +tpower +ftp.blogs +dionis +sgu +navic +artnet +xon +suslik +harukaze +ksuzuki3 +openspace +iseeds +acb +postcards +suteki +caa +kasaneno +jenni +chk +orga +e-district +jsac +regd +tomonokai +eapada +sameti +hellosensei +cobanks +eaapada +hgh +daa.aguime +matsuya +naoto +diaries +jah +saitama +hrw +mottainai +eizo +glim +clear1 +jpl +kagawa +rewrite +donut +ehime +comco +heb +niigata +mqm +sofuto +iwate +minhdang +dangquang +game7 +anhduc +hanami +hoangnhan +game10 +nhi +maigia +vanity +game9 +richy +gialong +isdn +www.bratsk +koiru +rankurusu +perushian +dooburu +every-pokemon-ever +matsuura +floors +systemy +markham +2play +techfaq +hyperlink +tmlite +jivko +test.mobile +ez1 +mr01 +ec-test +rdn +www.lines +mail.img +lines +abis +cbw +pen +wbsnhes +apps01 +psystems +otb +ecorp +vopforms +granicus +webdesktop +etrakit +oth +univega +kuvat +services.int +saj +mahad +rux +ryb +sop +koreii +priest +dpf +trd +kukai +companyweb +taigame +tsvb +trangchu +clang +ww01 +thumbs4 +ote +ms03 +validacion +static-3 +fwsm +trr +fp1 +web-mail +webdisk.tracker +henan +wai +zyzx +rbj +kazak +yoshiya +webforms +vw4 +alfactory +www.stmarys +s8.sq +aerc +xueke +jfx +gce +crear +arch2 +zulutrade +depth +moebius +worksite +webdisk.r +extra3 +allsouls +yoshimitsu +nst1 +sample101 +ogura +pcshop +tsearch +pc9 +ucupdates +mapp +mindscope +buyer +autoconfig.banner +idgroup +akind +youtuber +secure-dev +esthe +er3 +autodiscover.banner +webdisk.banner +coral2 +peyvand +admain +tshop +muumi +pressa +pc30 +trinusduo +pine.wan +takku +dawin +pc19 +pt-br +saifu +spare2 +eurydice +lyj +tact +assist7.sq +nakai +llb +smartsolar +res8.sq +sample11 +lgb +linos +telemaque +olympe +sample12 +sftest +melma +infopoint +ater +isoft +concorso +gdl +saito +neron +dnl +fatou +moloch +ploiesti +crios +bbdd +pandore +mathgraph +ecole +sjj +rrb01 +slj +rakuchin +fujiya +logstash +youtrack +www.cro +personalmail +www.789 +milehigh +backes +kikuken +ksuzuki4 +mothers +ttac +univa +templ +pt.dev +homestead +loreto +fayette +muta +taiba +john1 +vpn.office +autodiscover.ask +bruna +autoconfig.ask +harrington +link2012 +coastline +w52 +birchwood +res6.sq +iwantyou +downeast +pokerface +brevard +dioni +fats +solid1 +tsn +lise +lovelove +haxball +socool +mlsti +arabesque +diimo +tenryu +appetite +mixture +hillcountry +jjoo +spellbound +odell +inche123 +katze +ballin +medicare +rivervalley +jixie +clarks +duonet +api6 +kamas +license4 +www.svadba +odonto +cccs +feijao +usi +jeen +takagi +streamserver +battlefield3 +estafeta +l2l +signage +inset +joejoe +streetball +imyme +iwai +layouts +serv207 +broccoli +amoremio +asterism +benxi +shiv +ftp.uat +petworld +doubled +ultraman +wowow +betula +res5.sq +www.otto +horseshoe +viceversa +deadant +00 +jmsa +onlineapps +3x +aabbcc +bcy +www.albion +boulansserie +www.ntp +zps +apathy +s7.sq +arakawa +wpress +333 +llv +mmx +lxr +ibps +lanker +horie +luntan +tvshop +letters +sxt +lessismore +xgs +diimonet +persistence +225 +puchar +www.puchar +goteam +mail1a +mail2a +bb1.mtq +gentle +coool +mof +majdi +sisi +webdisk.server1 +autodiscover.server1 +wangfeng +quoka +springer +machine +autoconfig.server1 +lag +kikimimi +marverick +mucchin +hate +250 +kondo +hawthorne +seasons +mysql-dev +appetizer +girafe +habi +slhost +gazelle +grove +cnn +kikugawa +ours +benzy +smtown +requin +jpress +thewave +linki +bessel +mpi2 +mcds +doomed +yangzi +ponta +delusion +ksworks +edger +webdisk.html +midas777 +tarhely +minori +websl +m.secure +novato +ispy +www.platform +www.orbit +mitsumoto +hirogaku +apothiki +jptest +serv48 +serv46 +submarine +www.bus +serv44 +eraser +www.uppic +dvor +alum +tbrown +radic +fiberarts +moonstar +serv43 +www.radic +hanji +www.martian +serv42 +www.fornax +www.butters +serv41 +vitamin9 +hanae +vodokanal +ftp.ns +marieke +www.qmail +anakku +villain +mammatus +ftp.ns3 +named4 +market9 +radar1 +bbcs +www.pz +talisphere +www.fiberarts +leming +www.jjmusic +sbs01 +www.rs3 +sweetcelebrations +jjmusic +supermoon +www.parked +www.mmedia +www85 +verynice +www.jsd +woodruffs +ftp.lists +telephonie +www.radianthealth +haccp +habit +radianthealth +www.bounce +gijoe +www.jamestest +weyl +venerdi +autodiscover.notes +www.webmarket +webdisk.notes +ulva +www.shortcuts +autoconfig.notes +free4 +res2.sq +www.sweetcelebrations +smartdata +www.squid +losm +angelique +mconnect +mcv +autoconfig.exchange +ezbiz +mcdermott +webdisk.exchange +sads +ao.www +cordial +getup +adtech +lacolmena +cayenne +samenwerken +aboutus +doha +www.paper +dobe +mydear +ardbeg +elbruto +flush +navtest +humanitarian +testuk +chambre +mha +ridgeback +fourseason +origin-www3 +sv13 +arys +gazel +hodges +leica +gblog +digo +ashida +kofax +scan01 +redsys +morita +moco +tsuchiura +ashiya +galle +days7 +ritmo +coli +thallium +further +ksuzuki5 +ok123 +clue +grunwald +8community +rokas +ecook +www.monalisa +dain +kangin +ds20 +darma +miura +www.mywedding +www.farmer +adrenalin +sgl +sandc +bestlink +res1.sq +kompas +demodemo +darkages +jordan3 +rizon +jordans +fault +xproject +freesia +mistest +duong +moonbeam +disillusion +swoboda +peachblossom +sincity +limno +qwerty123456789 +maldi +ap2012 +doran +marc2 +vhost103 +vhost104 +monochrome +dokdo +autodiscover.correo +olivaw +dodam +dnine +amit +after12 +webdisk.correo +autoconfig.correo +highquality +happy77 +graviton +ballet +inthesky +vgk +echo2 +modtech +super4 +www.profi +botticelli +cstar +shaho +contec +akasha +goodmorning +mejis +ds54 +munzer +spareparts +wolfs +fifaonline +dimon +acetech +dhome +emic +vps201 +autodiscover.fun +crave +acro +dvorak +autoconfig.fun +nbs1 +const +cook2 +conco +comma +newalliance +chise +is3 +triplea +feel20 +ddos1 +galleryb +debut +kellycodetectors +347878-web1 +347879-web2 +sb_0601388345bc6cd8 +sb_0601388345bc450b +close +gearlink +asumil +vpngate2 +webcrm +fryingpan +sile +opac2 +nbs +intuition +affection +sqldev +quirk +testsql +radagast +nctest +voffice +ea2 +dpms +asd123456 +res3.sq +kp2 +xwgk +jwjc +www.taiwan +pl.test +es.test +webtrain +www.bn +elysian +ns281 +tabloid +creativex +ccftp +musicnet +wosp +autodiscover.sport +czaplinek +www.wosp +frodon +barrier +conges +gb4 +minerve +scmail +assist1.sq +hschool +detailing +josselin +ldapx +autodiscover.article +autoconfig.article +space12 +www.recreation +www-6 +www-7 +liberator +proxydev +testsp +gws +cetis +bohum +raze +jarek +majesty +mailpro +znane +ns-slave +galleries2 +hr2 +mofa +cus +smok +mikro +www.giochi +mpay +maravillas +autor +tunnel1 +inference +ftp.test2 +pro02 +miniminilion +fatura +attic +demosrv +www.lis +tht +csg2 +moat +wisely +yongo +subway +e119 +db001 +host001 +login-test +qa02-ca.qa +tamsa +qa02-va.qa +prod03-fl.prod.new +prod03-ca.prod.tools +qa01-fl.qa.tools +prod03-ca.prod.new +prod03-fl.prod +tm-glb-dns-validate +salestools +qa02-va.qa.legacy +prod03-ca.prod +prod03-va.prod +2271 +qa02-ca.qa.legacy +qa01-fl.qa +qa01-fl.qa.legacy +prod03-va.prod.tools +s1.sq +dev.tools +prod03-fl.prod.tools +prod03-va.prod.new +dev.legacy +gopoiskmedia +sanko +wmn +vmart +jito +mizan +webmail.support +monsieur +compile +gangnamstyle +apure +smgw +rmanager +arare +memcache +rt-dev +sneaker +arang +freetest +gisdev +spdc +cas5 +yjy +fazheng +waiyu +ojd +ftp.eu +onceuponatime +smtp-02 +rsync.us +produk +msbd +oldvpn +ircache +www.arhiv +russianwomen +ms22 +ntp1.srv +dedicate +ms9 +ntp2.srv +egi +www.horror +redakcja +plikownia +www.muzyka +in3 +securedev +admin22 +think3 +www.exams +in11 +rest1 +www.yuva +csat +n234 +www.kvm +assist3.sq +webdisk.global +anony +u4 +v0 +www.proc +n50 +da-test +n66 +sitex +devel.int +being +pillowtalk +n67 +n68 +hp3 +idh +informer2 +n71 +estore.local +ipad.local +neon1 +selina +andro +ofcgw.012 +beans +uxguidelines +dstyle +downloadtubevideos +beads +along +ipad-app +bdragon +wakawaka +amboy +sg-staging +sandstorm +genyou +aimin +oki +smtpenterprise2 +jfk +madara +ap200 +kogetsu +rule +bonheur +reggae +ctlfrlay +ctlfrcm +ctlfrspt +ctxnorth +examiner +rika +ffx +kejian +lishi +seemeee +www.ecard +www.value +heine +www.munin +landesk +schiller +opie +jager +daj +aesop +gxs +lights +worldclass +youthcenter +mailin01 +backupbackup +soba +jyj +kv2 +s2.sq +www.livechat +sensational +clicktocall +timebomb +beta.search +vsb +newonline +storage01 +audio1 +interia +buss +desq +packaging +ebp +ereport +mti +ices +firefighters +kweb +peony +homepages +s3.sq +www-uk +webdisk.halloween +fishbone +inq +sysdb +autoconfig.halloween +aerius +autodiscover.halloween +digitel +leti +refrigerator +fanpage +exotika +cpweb01 +fundamentals +nasdaq +wnc +vpn-mtl +aaaa1 +tropical +sidewinder +obis +mgiri +gandaki +dosyagonder +www.kopia +seehotel +emp3 +urbangear +koval +soporte2 +h02 +mjnet +mail.media +dialin26 +pg1 +kenshikai +www.push +heimdal +pipes +dialin29 +testweb01 +test212 +lester +awaken +plotters +wtk +mailing3 +mailing4 +img.shop +311 +motorshow +ftp100 +minimum +archive.mail +dialin33 +starmoon +www.kabin +suntar +nine999 +neogroup +redmoon +estatico +easygame +login-dev +www.webpro +sime +faa +www.faa +www.p2 +adminold +saveme +quickr +n61 +matest +db-old +n44 +dragonknights +n17 +mthorpe +dsa123 +admintool +dialin39 +tabriz +ofd +sasasa +xxxxxxx +pcprof +noaccess +battlezone +towel +spacewar +n69 +voctest +onemore +redbone +vmware3 +verify1 +samsan +hmt +arara +soft2 +toyoshima +madteaparty +sanford +miyasaka +ludo1 +inane +calgon +lacie2 +www.musteri +musteri +transcripts +avin +lyapunov +bluejay +macaron +smtp21 +prest +quiniela +sr4 +danielh +ripper +wdesign +origo +fifteen +hello123 +saehan +webmailus +p10 +sip.it +sr8 +autodiscover.corp +www78 +metaverse +sipi +dsvr +assist2.sq +seventy +op1 +roms +verto +weasel +rpgcentral +nortia +stats.ads +www.rpgcentral +webdisk.up +www.bloodlust +www.sen +autoconfig.up +www.solace +autodiscover.up +www.parse +foodsafety +wc2006 +news7 +mcash +news6 +sys3 +leaders +wido +prolab +blanket +itrack +utel +kbc +lhotse +ekaterina +bpj +res4.sq +regista +lovelygirls +libmedia +grouper +unitas +doyouknow +puente +view-security +sml-104-2931 +gemini4 +www.foo +ledger +openldap +dobong +www.rcc +pva +mckenzie +wamp +host146 +marvellous +swebmail +remart +animaster +araki +nisystem +gk2 +keid +web-stats +moodle-test +mafs +artshow +vps21 +vps22 +vps32 +res7.sq +sumika +gtk +tv123 +johni +jinyi +dhcp5 +mcast +ascend +nangman +webdisk.avia +pc232 +pc231 +tradmin +agena +project4 +pc224 +pc222 +pc221 +pc188 +pc187 +yourday +pc171 +www.switzerland +tokimemo +pc151 +www.sweden +pc103 +pc210 +pc99 +netherlands +pc160 +neverending +sip4 +old-forum +les2 +atkins +potatochip +onandon +rhs +pc203 +pc202 +indians +hampton +ecolab +dewitt +pc201 +kjk +brady +ipsentry +adver +youknow +edutech +lakewood +balloon +native +de15 +asin +wilson1 +saratoga +humming +another +siker +mayfly +pro04 +asder +pro03 +vasoula +ufs +hypercube +killzone +sdesign +topliste +www.kultur +www.iks +monimoni +autoconfig.speedtest +webdisk.speedtest +ingenue +stinger +autoconfig.kb +heights +autodiscover.speedtest +autodiscover.kb +roam +www.nbg +hackett +melaleuca +benibana +prices +pr5 +pr9 +miracl +pr8 +s244 +pr7 +gibraltar +biosphere +cloud01 +www.02 +pakupaku +vidocq +alphaomega +hacksite +lx2 +redeye +latenight +blackblack +entreprise +shiningstar +www.association +edoas +moremi +faj +springday +coer +webpac +jeffery +urbane +gateway3 +adminka +summerhouse +sybase +bgt +netque +dayou +arial +bacc +webasp +directors +www.ntc +supremacy +dqxy +vtw +durov +garret +smtps.pec +zerohour +bluecat +media15 +teensworld +smarthome +evanescence +weapon +zaygr +sokolov +highlight +maus +ipr +jit +watchcat +powerdown +justone +ales +redfox +slb +sova +yin +rayman +accom +frankfort +downland +wei +denpasar +soybean +orange1 +smee +fresnel +leek +kuznetsov +loveni +annex2 +alexk +scrat +lovein +godin +webhome +pusdiklat +manowar +elysion +ahimsa +www.palermo +plab +2gis +emailarchive +trisland +host254 +waggawagga +thenine +veracity +dnsweb +turnover +koikoi +alamode +kyobashi +motel +analyser +imaps.pec +paran +www.kobe +ueno +phpmyadmin9 +umeda +www.shanghai +sakae +phpmyadmin7 +tsukuba +zinnia +www.nara +akiba +phpmyadmin10 +thucnghiem +napster +vbox1 +uprising +pop3s.pec +webdisk.codex +webdisk.control +zimbra01 +lonely +stockage +servera +atwork +autodiscover.free +www.exp +revues +bootcamp +autoconfig.free +destinations +portaleducacional +vast +norther +important +www360 +nginx2 +musicspace +reaction +static7 +rtmp4wowza +nsd3 +costco +rtmp5red5 +rhsa +rtmp1red5 +boroda +rtmp5wowza +rtmp5host +dunkan +rtmp1host +whitesoul +cdn3.cache +twiggy +autoconfig.br +cdn4.cache +autodiscover.br +rtmp4red5 +rtmp3wowza +core10 +webdisk.br +cdn5.cache +pinkchoco +cdn2.cache +mr7 +soocool +rtmp4host +websms +www.success +rtmp2host +semicolon +aftp +fortpoint +btl +rtmp3red5 +wikimoodleadmin +rtmp2wowza +twenty +prserver +vimeo +chatme +yolo +www.fmsadmin +cdn1.cache +www.wikimoodleadmin +rtmp2red5 +rtmp3host +rtmp1wowza +tanuki +home7 +mukda +mathsci +devile +agnieszka +squiz +optimist +handshake +detail +hiroko +ndroo +www270 +monkey01 +cisweb +asca +carrollton-dvr2 +carrollton-dvr1 +gpsinfo +atw +statistiken +dientu +mail52 +gozer +xxxxxxxxxx +mail88 +angeltest +grotto +armonia +mail89 +michiel +routine +idrissi +www.funds +iproject +capybara +csm1 +mydarling +www.jobcenter +jobcenter +marielle +autodiscover.liriklagu +bsn +monsieur64-com +spoool-cojp +hilucon-com +xn--nckxa3g7cq2b5304djmxc-biz +sundesu-com +kpc-entertainment-com +gary01-com +angesalon-com +soundremix-com +nikibin-biz +kusai-biz +ad-japan-com +sishuu-xsrvjp +en-yukari-com +tanpopo-eduhk +lgts-biz +kurohige-biz +cosmicray-cojp +autoconfig.liriklagu +xsample125-xsrvjp +meditationscan-info +yukihitotrend-com +kaitaikainou-com +consultant-labo-com +grit-xsrvjp +most-h-com +mizu-shori-com +naniwaku-jp +officewill-xsrvjp +kurofune37-xsrvjp +reconnection-lightyourfire-jp +iinkaigyo-navi-net +kyotango-grjp +sneaktip-tokyo-com +sunforest-kinoe-cojp +catjp-info +emigocoro-com +planclair-com +wccf-kaitori-com +sairiyou-cojp +angels-swing-com +gakujutsu-com +aroedance-xsrvjp +dental-mg-com +unagi-matsukawa-cojp +s-seeing-cojp +hyakushojuku-com +nimurasekizai-com +two-roses22-xsrvjp +ikedahikaru-com +sqlcore-net +bibiten-com +ikejin01-xsrvjp +1yes-me +rootxx-com +italiacity-com +hinacyan-xsrvjp +athome-hiei-com +urayasuconpa-com +miyabi-est-com +test-xsample35-xserver-com +aibofund-net +craftbeer-tokyo-info +appruns-xsrvjp +shingen0905-com +tubo-test-xsrvjp +xsample208-xsrvjp +suomikyoukai-xsrvjp +toyama-maguro-cojp +clinocompass-com +xinfo530-xsrvjp +eathalal-jp +asa-kudamono-com +zero-house-net +xinfo764-xsrvjp +kamijin-fanta-info +luxury-wedding-jp +hibiyacon-com +o-bje-net +nap-net-jp +kenritsu-edu-com +ivis-xsrvjp +e-laguna-net +kitagawa-planning-cojp +nagochare-org +xsample55-xsrvjp +nijiironokoe-com +doorico-net +xn--cckcdp5nyc8g2837ahhi954c-jp +viteras-jp +gscsrv-xsrvjp +kaedefa-com +battlestar-cojp +eigonokai-jp +jj-office-net +zqbfcx-com +xinfo524-xsrvjp +jinbrave-com +ajdm-biz +xsample156-xsrvjp +pison-us +t-kawai-net +matubarabara-xsrvjp +sudou-h-info +b-mode02-xsrvjp +onishilaw-com +tcds-biz +ximera-jp +r-mhoot-com +luifle-com +hirayama-k-com +tirell-cojp +pointfort-biz +tukasak-xsrvjp +moneyymmt-com +koubesannomiyaconh-com +s-plat-jp +xsample302-xsrvjp +yumepocky-com +xn--28j4bvdyc334s6knv0o-net +trend-toybox-com +matsuya-bento-com +fandp-biz +ngtselect-com +fd-k-com +snowstyle-tv +aa11-me +ginga-card-xsrvjp +yamaukamaboko-com +pc7-jp +retorushop-xsrvjp +horaiya-cojp +consulting-firm-jp +stepmailmagazine-net +010system-com +crystal-dolphin-jp +takanoshinkyu-com +naniwaku-com +pmafamily-com +test-xinfo757-xserver-com +wanokurashi-jp +compia-info +test-xinfo747-xserver-com +kuma8088-com +sarasarahair-net +gundoujo-net +a-jingumae-com +xn--ruqs6f40az48fx3pk4y-com +ms-kun-com +x007-biz +xsample86-xsrvjp +s-rimo-com +kogaoseitai-com +howssupport-jp +e-katana-biz +test-xinfo727-xserver-com +overflow-xsrvjp +ecokoro-jp +so-na-ta-net +flashpool-jp +aspire-co-jp +waraok1-xsrvjp +test-xinfo717-xserver-com +hrc-mmc-com +xinfo555-xsrvjp +yasuragi-seitai-com +kanahebi-com +nagasakishiroari-com +cleaning-every-jp +test-xinfo707-xserver-com +antiaging-jc-com +pokerface-cojp +drepla-kyoto-com +gomi-cleaners-com +menuiun-com +f-magic-xsrvjp +fufu-design-jp +tsuruya-info +jidousuisen-com +tokkushouzai-com +denkisogo-jp +yama-net-jp +idb-aaa-cojp +nariagari63-com +machidadecon-com +pe-co-com +zenkaikyou-orjp +page-nabe-xsrvjp +smile2u-info +gakugeidaicon-com +hair-baizu-com +yumeya-eps-net +tech-angle-net +xinfo701-xsrvjp +kasugaurara-xsrvjp +bushuya-xsrvjp +oecmikage-jp +aozoranote-com +xhamx-com +0all-net +japan-af-com +urakata-biz +veggie-kouso-info +topparty-jp +webdisk.liriklagu +mamekichi-xsrvjp +twinow-jp +gan-gan-xsrvjp +chayamachi-yasuhei-com +osaka-footballcon-com +crerea-info +xsample312te-xsrvjp +ordering +iruma-shaken-com +daibutu-com +pikarin01-com +y-ecotech-jp +himeji-shaken-com +topic-path-com +mailaffiliate-info +massage-bed-net +rekishiya-com +suma-pula-com +berkarte-com +gekidan-ise-com +project-zero-biz +e-bibi-com +futabakikaku-xsrvjp +taiyo-shokuhin-com +somecco-cojp +it-walker-com +onna-hitoritabi-com +suemasa-cojp +rimtam-com +niji-web-net +xn--7ck2d4a8083aybt3yv-com +value-stone-com +japan-smilist-org +logo-cookie-com +watarai-xsrvjp +kyusyu-koiki-com +mitsutaso-com +test-xinfo557-xserver-com +ykdgroup-com +pachislot777-jpncom +xsample228-xsrvjp +fukuya-gh-jp +donguri-nihongo-com +test-xinfo547-xserver-com +detailflower-com +fromhimuka-com +wsugugiya-xsrvjp +beatmania-clearlamp-com +negishi-nbm-com +kanankaga-com +test-xinfo537-xserver-com +daicyu-jp +imayuu-net +houkikougei-com +haqbi-com +aurens3-xsrvjp +jibai-biz +test-xinfo527-xserver-com +hiro-emaga-net +hiraknet-cojp +ngc-office-net +ecogunma-jp +woodynouen-com +nakanocon-com +test-xinfo517-xserver-com +p-w-name +repro-nikibi-info +cgi-library-com +dent-miracle-com +xinfo732-xsrvjp +xinfo346-xsrvjp +bonn-jp +tazaki-info +muro-gnomise-com +test-xinfo507-xserver-com +tetsufuku-com +ht-backyard-com +nishisawa-com +apricotpark-xsrvjp +manamazu-net +lcfp-jp +xn--n8jwkwb3d155rfvd1osyt9a-com +jpcg-cojp +yamazumi-info +kankyotou-jp +genkibitorelay-com +osaka-con-com +mutsumi-dc-com +nagoya-cci-xsrvjp +friendsnet-biz +npo-yulife-com +sakata5-xsrvjp +t-plus-p-com +ayakakinoshita-fc-com +sanuki-hoken-net +tetsxserverdomain701-com +ap-g-net +honetsugi-kenshin-com +saeki-ce-xsrvjp +j-sp-net +shunan-rh-jp +xsample124-xsrvjp +xn--u9jy52g80cpwok9qjzosrpsxue7ghkv-com +misawasi-com +creva-xsrvjp +tvkansou-info +mages-et-cie-com +lifeworld-cojp +igarashi-asia +wkmarch-jp +meiyo1-xsrvjp +juen-info +takahide73-com +kajikazuaki-com +hachioji-s-o-com +cityonline +robin-s-com +gcapps-jp +minobu-sakura-com +usbmemory-info +ohssebs-cojp +universemove-cojp +xsvx1015036-xsrvjp +zxdxdl-com +gafu-biz +hyugadsgroup-com +sjeng +altervistas-com +get-wave-com +deco-shine-com +topix +gasnukiya-com +halla-jp +giropponcon-com +belpon-com +sannomiya-yasuhei-com +kichijoji-de-con-com +philknot-com +aritajin-com +terry-f-com +plan-do-japan-com +greenbird-net-com +macco-xsrvjp +test-xsample222-xserver-com +xn--u9jxfma8gra4a5989bhzh976brkn72bo46f-com +arths-net-xsrvjp +sunnytrend-info +test-xinfo357-xserver-com +seikoshokai-cojp +fujitaippu-com +xinfo763-xsrvjp +testsp1208274-com +ecobaza-com +dreamcreate-jp +singlemalt-club-com +test-xinfo347-xserver-com +xn--ecki4eoz9849l-biz +www.daleel +macshoppe-com +gz-burst-xsrvjp +testsp1208279-com +goku-raku-info +yonsei-tounyu-com +xsample54-xsrvjp +hanabicon-com +lucky555-xsrvjp +xn--y8jvc027l5cav97szrms90clsb-com +sennokyaku-com +biznsr-xsrvjp +nikken-n-com +saidaiji-yasu-com +mtymxykhk-com +gaka-serizawa-sachiko-com +toyota-shigikai-jp +220088-net +kouhata-net +fly-tabitomo-net +tsunechan-net +xinfo523-xsrvjp +ekinozusaadet-com +testsp1208301-com +ando-furniture-com +mix-colors-com +kamejikan-com +xsample155-xsrvjp +weegeni-com +musashino-rokuto-com +meitoku-xsrvjp +compa-yado-net +salliance-org +arrangebit-com +nagasawakazami-com +adoo-mi-cojp +tokunori-net +win-technos-com +b-nishijin-cojp +test-bloom-blooming-com +joint-elements-com +hatenumura-com +cawacon-com +alliumu-com +n-plaza-xsrvjp +seoul-inn-com +budoya-jp +lauderdale-cojp +e-worldshop-net +b-and-c-jp +caggio-com +kakeizu-s-jp +xsample301-xsrvjp +ms-uni-xsrvjp +office-kisook-com +insideworld-biz +himawari-day-xsrvjp +office-shiratori-com +oita-kaibutu-xsrvjp +jr7yrc-net +glow-united-xsrvjp +tamalll-com +diet-trend-net +manat0-com +mirahalo-xsrvjp +communicationss7-info +newfrontier-biz +pcfureai-com +r102-jp +okazaki-shaken-com +orekuma-net +shonensanso-com +mikurencia-com +ndc-office-com +xinfo350-xsrvjp +xn--lckxc7c-com +xsample69-xsrvjp +nikko-shaken-com +xinfo342-xsrvjp +teis-jp +denpun-com +enjoyrose-info +dreamspaces-org +primegate-t-jp +mobadis-xsrvjp +web240 +hm-lab-net +nakamori-shinji-net +yoshida-ya-org +ex-profit-biz +cosmicray-xsrvjp +linkstyle33-com +softwaregaming-net +xn--t8j3b111p8cgqtb3v9a8tm35k-jp +xsample85-xsrvjp +morikumado-com +import-lecture-com +sru-xsrvjp +bakery-cork-com +honyakukonnyaku-com +zipaddr2-com +urawacon-com +trill +j-trader-net +arahama-org +crapre-net +atnnta-net +glamoroush-com +houkaiji-net +xsample92-xsrvjp +xinfo554-xsrvjp +ueno-sr-com +tai-gee-com +revive-kawagoe-net +temple3930-com +nlp-jp-com +godiving-jp +nandemoya-com +weightlossgains-com +jikkoujitsugen-com +keiba-twitter-com +xn--zck3adi4kpbxc0dh-net +aajd-biz +azoo-xsrvjp +melkare-com +findsports-jp +shonansmile-com +anela-kilika-com +b-official-jp +sukuhiko-xsrvjp +lacherie-jp +netdesoho-com +ikeikegolf-com +washokuan-sara-com +e-iwatate-jp +webboo-xsrvjp +shitakke610-xsrvjp +nihongi-web-com +yamazaki-js-jp +jubancon-com +finanza-asset-com +socket-cojp +fastingmana-net +worldoasislife-net +xsample332-xsrvjp +liba21-com +moromoro-info +win5pro-xsrvjp +nikkai-info +infinity-create-jp +jichangwook-jp +xsample220-xsrvjp +yoisaito-net +mituba-xsrvjp +tender-kaigo-com +nuc-xsrvjp +keb-inc-xsrvjp +sorano-biz +little-newton-jp +ai-serv-com +bolly-jp +contents-marketing-jp +ductblade-com +ikebukurocon-com +kumonoyasuragi-net +naruhodoshop-com +udono-com +yubiz123-com +willowtree-jp +herbest-biz +hitachinaka-shaken-com +shatikushota-info +ashiga-body-xsrvjp +hachimoku-com +nigarimai-jp +dreamconcept-gfx-com +fwdsclub-net +test10-xsample30-xserver-com +ultim-jp +frenchstylejouy-com +hkky09hiro-xsrvjp +emix-cojp +human-kyu-com +xinfo561-xsrvjp +omote-xsrvjp +www.ibf +xinfo585-xsrvjp +shiki-magokoro-jp +r-sun24-xsrvjp +terradonorte-xsrvjp +sakifuji-com +xn--new-h93bucszlkray7gqe-jp +xsample227-xsrvjp +pay-off-bills-net +xn--2-uc7a56k9z0ag5f2zfgq0d-jp +ono-lc-jp +maihonya-com +atout-jp +tef2-net +r-onward-com +test-xsample220-xserver-com +tanopasopcs-com +inu-recipi-com +northfacle-com +prede-com +cfk-xsrvjp +nanosolution-cojp +rj-works-com +nkginfortec-net +nuutori777-com +juri-jpnet +jibundesumaho-com +zippersoft-jp +xn--pckam4ohe2b9aya2mf9574hyfduy9g-com +vstlink-net +ishikawa-nu-acjp +maeam-net +xinfo731-xsrvjp +ripple-k-com +publafabbrica-com +sk66-xsrvjp +jun-okawa-com +xn--68j4bva0f0871b88tc-com +zimbabwecheapflights-com +okayama-shaken-senmon-com +acperience-biz +rakurinza-com +danceahero-jp +apparel-logistics-master-com +beachrocket-xsrvjp +avante-act-com +karadafactory-com +iqb-cojp +hr67ekh4-xsrvjp +athleticgolf-xsrvjp +tbc-direct-net +full-throttles-com +willmatch-xsrvjp +open777-xsrvjp +kigyoujin-info +lets-import-com +pentan-xsrvjp +xsample123-xsrvjp +bk-1-com +prime97-xsrvjp +royalsalon-vip-com +sundubu-xsrvjp +keage-jp +motto-tokyojp +xn--w8jm3fycxc-com +kampo-yamato-xsrvjp +mizumotoryu-shinto-com +ff-spa-com +teiyobi-net +kurashi-kyoiku-com +m-mindset-xsrvjp +fp-service-no1-com +ideal-partner-org +intibali-biz +mr-webinar-com +xsample120-xsrvjp +fxbookmaker-info +kisarazu-shaken-com +egcg-info +ad-innosence-xsrvjp +for-others-com +denshibato-net +npo-jambo-jp +milliona-sd-com +aibrain-orjp +softrance-com +yamau-xsrvjp +siramitsubushi-net +smartphonesite-info +hanshinkaen-green-com +yosimitu17-com +otokuna-life-com +akirasblog-com +healing-spot-com +xn--mck0a9jm25le99ae3b91q-com +inmoss-com +ryo331-com +alfastart-net +suncompany-cojp +tukasa-saba-jp +kouchouen-com +alfactory-xsrvjp +specbrothers-com +1-web-jp +xn--68jza6c5o5cqhlgz994b-jp +k-balance-com +fly-system-biz +morebeauty-jp +xinfo762-xsrvjp +jbiz-cojp +11desune-com +fullcollection-jp +garden-kinokawa-jp +kancraft-com +sugiuravet-com +gan-bare-jp +kokko-net-xsrvjp +r-m3-jp +nightworkwe3-com +toshiki-cc +makiko2nd-obog-com +xn--zck3adi4kpbxc7d2131c5g2au9css5o-jp +xn--u9j5h1btf1e9236atkap9eil-jp +xsample53-xsrvjp +buranara-com +yotsuba-insatsu-com +mailbess-com +web-business-freeman-review-com +hataya-ah-com +raku2-kenko-com +hageno-ikumou-info +tri-works-cojp +yasukuteiiie-jpncom +two-d-net +yoshikawateien-jp +yukki-lanakila-creations-com +iruma-mobi +xinfo522-xsrvjp +a-koike-grjp +xn--7-hju882iudfymevpi891bvkcm7t-com +dateformc-com +zinen-xsrvjp +xsample154-xsrvjp +xsample309-xsrvjp +oonishikoumuten-jp +merumeru5252-biz +lions-forum-org +hotel-yagi-cojp +smilehonpo-com +sunflower-kashiwaya-com +burapan-xsrvjp +floramor-net +shachu-haku-com +rbs-xsrvjp +jubancon-net +n-yomiuri-com +battlemusic-net +jbn-xsrvjp +xinfo706-xsrvjp +asbmain-net +zasso-taisaku-com +bonbon-chouchoux-com +sv3rd-com +asayake-jp +xn--u9j5h1btf1en15qnfb9z6hxg3a-jp +genkishop-jp +aajd-net +calmcalm-net +xsample300-xsrvjp +kikimethod-com +arigatou13-xsrvjp +artepo-com +midjapan-jp +yokota-camera-com +nksmmc-xsrvjp +rakuzanet-jp +lappi-jp +minatokobe-hanabicon-com +acnweb-xsrvjp +xjs-xsrvjp +yamatsu-jpncom +heart-oasis-xsrvjp +happyrich8-com +e-gakki-com +kumamoto-roumu-com +ja-kumamotoshi-jp +yamadajimusyo-com +subarudayo-com +tekka-merumaga-com +xinfo51-xsrvjp +mckokoro-com +www.kwp +xn--54qq0q0en86ikgxilmjza-biz +papaben-club-com +hkazuf53-xsrvjp +crazyken-com +torisyou-jp +lhotel-du-lac-com +suigekka-jp +active60-jp-com +daitreck-com +shutoku-info +xsample84-xsrvjp +takechi-info +andseeyou-jp +bjmz-jp +ts1981-xsrvjp +meiko-plus-academy-com +interior-option-com +10english-net +satelliteworks-asia +youvision-jp +nanosui-com +pison-net +abiesmikuriyacake-com +web-business-freeman-com +xinfo553-xsrvjp +sf2727-xsrvjp +gokugero-com +j-crew-sc-info +yakuzaishi-fc-com +kurema-cojp +tansan-beauty-com +hiroshi-project-jp +sisei-orjp +baikyaku-kyoto-life-jp +xn--1cr778h-com +biz4apple-com +manten-ff-com +donnatokimo-info +next-color-xsrvjp +gouhime-com +yamada-dc-info +kokorozashi7-net +kenmana-xsrvjp +ii-yado-net +italian-otto-com +kusugula-com +ryuka338-xsrvjp +heian-suzuki-cojp +shihou-jpnet +xsample331-xsrvjp +lamphouse-jp +original1930-com +aiwish-xsrvjp +kimayu-com +grow-up77-com +chukeikyo-xsrvjp +diesel-watchshop-com +sachis-pocket-com +masiki-denchi-cojp +nice-pc-xsrvjp +mamma-cojp +yrnetmind-xsrvjp +fbms444-xsrvjp +studio-hinemos-com +rakurita-com +mamegen-com +japan-cmc-jp +sure-oil-com +funeralville13-com +xsample218-xsrvjp +en-job-com +asia-create-xsrvjp +perfect-fucoidan-net +ecoms-store-xsrvjp +visage-group-net +kaientai-to +doronko-rokko-net +hana-aprico-com +kagami-town-xsrvjp +jpcrossroads-asp-com +yasuhiro-tanaka-com +temp5 +ashiya-grace-com +outlawdesigner-org +diamondlight-cojp +dropsite-xsrvjp +vagstp01-jp +setdeem-com +fraise15-com +meiyo-is-com +organic-nana-net +hairmake-polish-com +is-style-mode-com +grit-inc-com +hospitalities-cojp +silvers-site-org +webrent3-xsrvjp +sawamemo-com +xn--live-995g-com +gene-potential-com +ryukou-butsudan-com +genko-nyuko-com +powerfarm-info +anemptyspace-org +izumos-info +englishosaka-com +xsample226-xsrvjp +ten10-xsrvjp +moneychild-com +campus-web-jp +ashitakaasatte-com +meteor7-xsrvjp +junjimu-jp +hold-chance-party-com +xn--t8j9b3du706b3ud-net +kandasr-com +kobo-cuoluce-com +nanba-yasuhei-com +wsp-jp +evina-biz +bis-project-com +colors-leaf-jp +etoca-net +seibouan-com +daikuru-com +reopardi-cojp +gargantia-jp +onsen1126-net +or-nitta-com +suuudesign-xsrvjp +naritareal-com +xinfo730-xsrvjp +xinfo344-xsrvjp +wealthhappy-info +throughsky-xsrvjp +yukitake-jp +rebcreation-com +tsuruoka-jc-info +tamate-jp +aoteapacific-xsrvjp +testdomainx324-com +takemitsu33-com +yoshihiko01-com +inaka-pipe-xsrvjp +freedom-benefit-com +kuroiso-bagel-com +inging-cojp +testdomainx329-com +kukankaihatsukobo-com +hara-ringo-net +inugumi-xsrvjp +yota8000-com +jtp-corporation-com +bigone-cojp +testdomainx335-com +la-chouette-fuji-com +xsample122-xsrvjp +fujimurabl-com +brandbay2012-xsrvjp +kantan-hp-net +littleany-com +painter +blogyanwei-com +happystylelife-com +hand7-jp +fretta-jp +test2-xinfo744-xserver-com +kojima-mf-xsrvjp +originalbook-net +nitii-info +camuro-grjp +hp-tuneup-com +threey-net +6notes-jp +xn--facebook-9s4goej6w4khkpe-com +yonago-giken-cojp +xn--eck3a2bze7g088r1tnu2vn7l46v-com +2oku-jp +hunabashi-yabusaki-syaken-com +endingnote-music-com +a7works-com +baisersvoles-net +style-lab-biz +united7-xsrvjp +craft-mai-jp +tomioka-suga-shaken-com +sagi-yattukeyou-com +nanomi-xsrvjp +youvisiondenotest-com +loran1990-net +maru777-xsrvjp +can-cara-com +duzele-com +notokids-net +yuyu-rlx-xsrvjp +styliv-com +openerp-asia-net +visual-exchange-com +gokugero-xsrvjp +jp789-xsrvjp +soola-jp +cloud-www1-xsrvjp +elocalgov-xsrvjp +denkikai-com +i-m3-jp +yosibo54-xsrvjp +plussoft-cojp +kokona922-com +blackyuushi-com +its-ec-com +xsample52-xsrvjp +xn--qckr4fj9ii2a7e-jp +api-master-com +chatoran-com +joy-circle-com +murakoh-jp +jiyubito7-xsrvjp +kassist-xsrvjp +yui-aragaki-net +eapuranntu-xsrvjp +xinfo521-xsrvjp +dreamlife-invitation-biz +y-guard-com +yorozuya-system-com +xsample153-xsrvjp +delmot-tea-com +bizzone-xsrvjp +nanikowa-com +como-nejp +pla-neta-cojp +asp-kawahara0202-biz +sezaki-cho-com +sakuzei-com +teccsearch-xsrvjp +kyoto-shaken-com +mebel-antique-com +rubowa-jp +sidebrains-com +karuizawa-silk-com +pazzot-net +collabox-xsrvjp +keisei-cs-com +houdinisportswear-jp +acmeloo-net +admi-biz +celebrity-rich-biz +starhill-cojp +odakesyokuhin-cojp +jun25-biz +heartful-travel-com +kk-to-bu-cojp +tailor-yoshidaya-jp +yamazakiminoru-biz +hoken-consul-jp +itousanfujinka-com +carve-jp +nusoft-jp +uenodecon-com +kawagoecc-com +sasakitchen-biz +furubayashi-eye-com +kamo-jinjya-orjp +h5910lv8000-com +yurai +tanagokoro-kyoto-com +crerea-net +otonworks-com +studio-carrot-com +us-vocal-info +manzashop-net +slightfeverboy-com +kou-mei-ok-com +yukimanta0808-com +otonohachan-com +tnc-ep-cojp +kenou-info +mellow-touch-com +apa-inc-cojp +syouhachi-com +ieda-dc-jp +xsample83-xsrvjp +yamagatagift-info +currencytradingexpert-org +testxdomain305-com +787navi-com +rpmoutsourcing-com +watchrepair110-com +habu-kouji-com +infotrek-xsrvjp +katsuyama-g-jp +adropmeet-com +rep-power-jp +testxdomain311-com +haridekenkou-com +xinfo552-xsrvjp +murakami-kagaku-jp +nafooe-com +gotouchi-japan-com +aiwa-f-nejp +neco-store-com +n-insurance-cojp +sanom-net +fpp-jpnet +testxdomain316-com +tsubomi203-xsrvjp +global-smi-com +ogasa-biz +ism-blue-com +usadrumshop-com +mahoo-net +grocen-shop-com +nomiiko-com +nakayama-kids-com +wildcard-jp-com +uruoi-water-com +muex-net +ht-backyard-xsrvjp +yumikp-xsrvjp +w-life-jp +test-xinfo745-xserver-com +xn--kckky1j2cwa8f1cb-net +kotomine-event-info +e2-square-cojp +truck-asahi-com +juggler-jpncom +musashinogroup-com +tototoclub-com +test-xinfo735-xserver-com +xsample329-xsrvjp +ro-haircare-jp +xn--u9j5h1btf1eo45u111ac9hf95c-com +oo0n-net +acr-net-xsrvjp +test-xinfo725-xserver-com +yuyasawada-xsrvjp +clubcafe-gr2-jp +jream-jp +test-xinfo715-xserver-com +maaaks +kami-to-nuno-com +food07-com +normanet-cojp +olive5-xsrvjp +odaiba-con-com +test-xinfo705-xserver-com +xsample151-xsrvjp +art-craft33-com +volleyball-coach-info +liv-design-net +himeji-jv-com +log-research-com +shop-pro-jpnet +hkwspace-xsrvjp +hikaru114-com +shonanbank-xsrvjp +sanda-kokuzo-com +salmon007-com +roumu-sodan-com +820-co +matsuda-siko-xsrvjp +horie-yasuhei-com +christianpeau-com +clarity-life-jp +inter-act-jp +tsujiguchi-jp +tmk-xsrvjp +yujinetwork-info +testkimura34php4-com +siestarea-com +rakanka-com +nipponhomeopathy-com +tokaibun-com +hunabashi-coating-com +vellsheena-jp +teitoukentouki-com +localfoundation-jp +xinfo583-xsrvjp +imagebank-nejp +xsample225-xsrvjp +daisycreate-com +ristoranteilcanto-com +hitachi-shaken-com +add02tv-xsrvjp +makinos-biz +bs-mebius-xsrvjp +ietateru-com +stakao-net +xn--t8j0cgq9xucf2ooiyhodz566d8m0a-com +neko-free-com +gofun-xsrvjp +xinfo121a-xsrvjp +takuya-yoshimura-jp +check-the-rhyme-com +megamatu-net +libgarden-com +homare001-com +kjmail-biz +sell-car-info +xn--3kqvs447ab16b-net +gaobu-xsrvjp +cipiu-com +test-xinfo585-xserver-com +wapos +kitashinchicon-com +xinfo728-xsrvjp +xinfo343-xsrvjp +style-lab-net +fbms1-xsrvjp +higotokou-com +nouen-chokuhan-com +n-t-lab-com +fatal-encount-com +hanacupid-plus-net +myt-p-com +myanmar-partners-com +med-takaoka-xsrvjp +takayuki-1973-com +offshoring-digest-com +e-kagayaki-jp +ii-kao-m-com +ishikaitoriya-com +kzyhsgw-com +test-xinfo555-xserver-com +jinbochou-com +hands-on-it-com +eyomo-xsrvjp +trashcan-xsrvjp +design-omakase-com +test-xinfo545-xserver-com +iwamoto-clinic-jp +xinfo737-xsrvjp +xinfo352-xsrvjp +new.new +junwa-jp +aiaokayama-orjp +g-orion-com +kuwabara-dental-com +wawer +spinell-biz +test-xinfo535-xserver-com +picocraft-xsrvjp +wakuwakuart-com +xsample121-xsrvjp +chofu-daikokuya-com +hypr-info +story-kobori-com +test-xinfo525-xserver-com +ctrans-org +test-xinfo515-xserver-com +test-xinfo505-xserver-com +fishing-shopping-com +style-lab-org +y-jig-com +m-shinkyuin-com +sipweb-xsrvjp +esdirect-shop-com +wood-roots-com +h-yoshikawa-com +sowa-com-com +hotel-tenjinplace-com +enjoylifeafi-com +bgm21-com +kazuart-cojp +test-xsample330-xserver-com +web-main-biz +jeffnipplesworld-com +kakinoshizuku-com +enduromasa-com +mitumorisien-com +rightcure-net +evis-xsrvjp +gatturikun-com +wealthhappy13-info +homus-cojp +gontakun-xsrvjp +momo00-com +connect-k-com +inexpenshop-xsrvjp +xn--eckm3b6d2a9b3gua9f2d2431c1m6a-com +everythingdoeswell-com +test-xsample309-xserver-com +kanewa-m-com +mc-ken-cojp +nail-frosty-net +xsample333-xsrvjp +k-go-biz +chuo-f-com +test-xsample300-xserver-com +eaupure-org +calibexpress-com +rsapo-com +sxscontrol-com +vd23-xsrvjp +takasaki-nagai-shaken-com +gdchoice-jp +ows-npo-org +la-maison-courtine-com +dr-fukuoka-net +pharmacistnavi-net +flets-jp-com +shdanavi-com +kurofunemarketing-com +slashowy-com +xn--fdkc8h2az097bv1wbh4e-jp +brens-jp +nichiei-cojp +test-aflat-com +rlize-org +jouhousyouzaimassatu-com +yukabon-xsrvjp +trendwadai-com +binarygift-biz +siraisi-cojp +seodelink-com +smart-housing-biz +xinfo519-xsrvjp +sansak-jp +genzankai-xsrvjp +homepage-ya-info +theyarehogtied-com +futurepirate-asia +taimai77777-com +woody-life-cojp +rokko-dog-net +tajimagyu-sushi-com +test-xinfo355-xserver-com +syakosyoumei-hiroshima-com +avantijapan-com +samuraiclick-japan-com +saeki-jibika-com +felicejapan-net +test-xinfo345-xserver-com +tomenokome-com +e-pro5-com +ebook-jpnet +skjioudhcuy656dius-com +jey-string-jp +kazami-com +global-trendmkt-com +business-strategy-meeting-com +kobayashi-og-net +design-the-way-com +l-com-xsrvjp +rjbb-xsrvjp +yahabus-com +e-maku-com +xn--yckvb0d4245c-jp +frigus-jp +kireinadiet-com +xn--i0tq7meooqf-com +testxdomain307-com +ginga-card-com +izu-xsrvjp +form-com +4c-session-com +haisyanokunitora-com +kome-shibahara-com +kyorindo-com +ktscope-com +inthegarage-jp +milumoda-xsrvjp +kumomakurax8-info +dowkakoh-cojp +yamatonadeshiko-biz +xn--39ja4cb4nqb6d4fu546bkkucpl7d-jp +oita-cando-com +daiki-suisan-com +unfixedsystem-com +xsample82-xsrvjp +kataoka-kaikei-orjp +hok-me +airakirishima-com +panopano-jp +jorjia-comtw +cheerful-xsrvjp +taka02dive-xsrvjp +goe001up-jp +testxserverdomain12030159-com +m-benz-cojp +taxiway-jp +xinfo551-xsrvjp +kikusui-sushi-com +break-through-net-com +low-cost-jp +suemasa-xsrvjp +silverrush-jp +excy-biz +xn--u9j0goar6iyfrb7809ddyvakw0e2vh-biz +miyake-office-com +freeken01-com +dance-studio-itsuki-net +tetuzuki-dairi-com +unpatta-com +2chomecon-com +a-cnet-cojp +hirosaki-redapple-com +kokai-jp +xsample129-xsrvjp +sendai-proxy-service-com +morii-tatami-jp +jewelrydisk-com +akabanedecon-com +kurashikihanpu-com +parchive-xsrvjp +hello358charlie-info +sm-solutions-biz +anac-jp +xsample328-xsrvjp +osakapeer2010-com +surg2-twmu-jp +cubik-com-net +xn--nckuad2au4azb6dvd8fna2594hb0sc-biz +karadano-com +heart-full-com +pasosuku-com +aiuto-jp +okazakitokki-cojp +honmahajiku-com +fphokensoudan-com +avatarhp-com +oki-xsrvjp +u-amon-com +yamatocool-com +babyumiwake-com +san-ei-info +asupro-jp +mavericks09-com +rakuyasu-e-net +iizo-info +tamalink-biz +stylegate-jp +ecollab-cojp +amjinternational-net +akazukin-xsrvjp +fan-gate-info +slot-1game-xsrvjp +ogino-dental-com +zeal-xsrvjp +nailstudioasa-com +umedaconh-com +prime-more-com +t-tcn-jp +pojiiki-com +ryukyu-goten-com +yukatajapan-com +e-janai-com +aicle-bu-com +isiwatari-com +v-fort-com +tui-cojp +ikejiridecon-com +toeishinyaku-com +tanabereform-xsrvjp +autoconfig.booking +morinonaka-net +enkara-net +watanabe-gate-biz +lesthetic-net +comm-w-com +yurumean-com +yumyin-com +trendmaturi24-net +sync15-xsrvjp +liberal-woman-com +niji-nejp +satou666-com +tcplus-cojp +matsu-nomi-com +naganocon-com +shoeip-cojp +minatoku-kaigoren-com +wakamatu-biz +sunbeltpartners-cojp +neo-create-com +xinfo582-xsrvjp +webdisk.booking +niiza-syaken-com +xsample224-xsrvjp +hiramatsukenzai-com +haradajun-info +1100club-jp +airial0525-xsrvjp +ayumikuro-xsrvjp +junkbuyer-mac-com +kyuudann-com +webstyle-cojp +kaitonaka-xsrvjp +sin-kaisha-jp +sgsg-jp +invest2001-com +meichashop-com +kintenshi-com +humanlink-xsrvjp +vvuvvu-info +sikumi-jiyuu-com +smount-com +bahamas-freeport-info +xinfo727-xsrvjp +cote-to-com +thesoundofthousands-com +posy-xsrvjp +is-token-com +tatuo-xsrvjp +yumizclub-com +marungai-info +worldrings-hana-net +ddbistro-com +kijima-lab-com +king-soukutu-com +ohmiya-com +g3company-com +kitesky-net +shimizu-motorcycle-com +kumafukucen-com +japantrading-cc +wlb-xsrvjp +bambisystem-xsrvjp +vaomusic-com +bailojapan-com +igam-info +motsuya-olc-com +catseye-jpncom +xn--eckm3b6d2a9b3gua9f2d6658ehctafoz-jp +iine-kawanishi-com +shonai-ya-com +kozantyaya-com +hr67ekh1-xsrvjp +dreamcreate8-xsrvjp +autodiscover.booking +kaigyoushien-com +roppongi-con-com +acqwords-com +hoshino-me +xsample119-xsrvjp +townace-pro-com +arrowcom-net +avantijapan-net +trend7shin-com +yoshiihoikuen-com +yasooo-net +heart-furufuru-com +goodreform-navi-com +animesoku-com +tc-sanwa-cojp +xinfo148a-xsrvjp +hakodate-shi-com +hss-web-jp +seijo-dosokai-info +bass-gatsun-com +pyonchi-info +iloveharajuku-com +wajoy-xsrvjp +avon-shop-jp +jyuzen-dental-com +grand-plan-com +sakabeclinic-com +xn--line-yn4ckbymxil606bodya-jp +fujihomes-com +ehhen-com +shoda-pack-cojp +mo4c-com +anvar +ticketlife-jp +mizuha-jp +nakahara-agency-com +yarukiswitch-biz +surge-hair-com +paint-kumamoto-com +bi-frecce-com +chuuka-com +xinfo124a-xsrvjp +enpelancer-com +kindai-mansion-com +jwvaughan-com +l-guldy-xsrvjp +meiyo-biz +maod-jp +ilt2-biz +xinfo586-xsrvjp +yaseru-asia +asa-eshop-com +ryugakuseiwork-com +xinfo758-xsrvjp +la-confy-com +cross-share-com +imagiq-jp +osaka-denkikouji-com +sugar-lake-com +yuri-pharma-xsrvjp +jiyugaokacon-com +mikihifuka-jp +tme-i-jp +ieys2-xsrvjp +entanna-com +hirotakanet-com +do-inaka-com +karada-koubou-com +turuta-jp +xsample50-xsrvjp +hayakawa1456-com +kazu-hirono-xsrvjp +tsunageru-net +almetyevsk +chuen-navi-xsrvjp +xinfo549-xsrvjp +sabo-kanon-jp +rikuafijyutu-com +xn--pride-ym4dqj0d4g-com +xinfo518-xsrvjp +test-xinfo716-xserver-com +ilt2-com +taku222-com +honatsugi-con-com +xn--u9jxhkb1qu36p2lc-com +ijinjin-xsrvjp +pethelper-tumugi-com +turigu-ten-com +g-zipangu-jp +8010-cojp +minamisenba-yasu-com +theisao-xsrvjp +machikado-xsrvjp +magropan-xsrvjp +xn--eck7ake2fza4b-jpnet +shimatomo-com +kokkoya-net +mikasasports-asia +non0804-xsrvjp +rirema-xsrvjp +amc-seiwakai-jp +hs-dhr-com +4415-jp +namikawa-gear-jp +accessall-xsrvjp +test-xinfo541-xserver-com +luxury-music-jp +membering-jp +akeru-an-com +parco-jiyugaoka-cojp +yellowstone-jp +xn--zck9awe6d5565ccnra-biz +sapporo-recycleichiba-com +eikoudo-com +kondodenki-jp +zest-camera-02-com +xn--cckueqa6319czn9a-com +iris-eyelash-com +uttigogogo-com +testament-xsrvjp +kobadesigns-xsrvjp +sawata-cojp +cyber-i01-xsrvjp +terasaka-xsrvjp +gh-yanagi-com +knowhowrecipe-com +kota-papa-com +fkuykoukdoa-xsrvjp +miryu-ya-com +venus-space-com +gyoseishoshi-shiose-com +hutako-com +piano-renshu-com +e-okinet-com +9pt-jp +xn--u8j7b0f9doa5d4g3281ak25ctkc-com +mystery-room-org +test-xinfo534-xserver-com +fbms33-xsrvjp +jenrobgroup-com +netter1-xsrvjp +sen-ju-com +office-sam-com +sho01-com +saiseikai-gotsu-jp +okakaikei-cojp +nankankyo-net +mae-ca-com +evahs-eternal-com +eigoforth-biz +7gates-net +shiraiwamitsugu-com +kazunobu7878-com +sylphide-m-xsrvjp +tuapse +lagunadental-jp +milky-holmes-ofc-com +train55-com +xsample81-xsrvjp +tenryosui-com +fxchips-com +aroma-na-net +dimitrovgrad +tom-sawyer-net +rihabirikan-net +earth-t-jp +estax-cojp +tanaka-group-cojp +nobukisaito-com +nabetabebe-com +hk-teahouse-com +111e-jp +homestyle-bz +officetom-xsrvjp +xinfo550-xsrvjp +yasumochi-law-net +mitsutomoltd-com +xn--zck9awe6d692p972a905d-jp +eccome-jp +saitsu-com +dmd-nejp +sweet-beauty-jp +yao-ec-cojp +tiffany-ogoto-com +isiscoltd-xsrvjp +xn--tor55ycb159b1ndz7ifa3356d-biz +giulietta1886-com +sawadayuya-com +broslink-cojp +kayaxiv-net +foxhdy-xsrvjp +tenogeka-com +w-clutch-cojp +nail-aries-com +amochi-jp +kpro93-com +xn--jdka9gb-net +dorotonyusekken-info +fx-crosses-com +drbarkus-com +h-bee-com +saika-sports-com +heart-cushion-com +sale-xsrvjp +gucci0122-com +tokyo-roujin-jp +umaoh-com +dearmine-net +catservant-xsrvjp +tourgorilla-biz +hikari-flets-com +aliatokyo-com +icreative-jp +kotonoha32-com +kobeee-com +metaltech-8-cojp +yoshikk-com +hokuken-xsrvjp +website83-com +tvaom-com +uta1-xsrvjp +t-intl-cojp +paintory-com +hoken-sukkiri-com +macross-fanclub-com +xn--y8j2eb0209aooq-biz +popcul-net +mirei-uranai-info +tukuba-con-com +soujukai-net +i-leaf-hanahata-jp +kkinjo-com +eco-up2012-com +waocon-com +handcreation-net +toyosudecon-com +ecconnexion-xsrvjp +publishable-biz +kibino-com +xinfo581-xsrvjp +verylen-com +kyodokun-net +ogataengei-xsrvjp +rsw01-com +xsample223-xsrvjp +pleasuregene-xsrvjp +yagurazushi-com +c21-ogswr-com +la-matie-com +noa-moving-com +denden0375-xsrvjp +delsole-bios-com +beauty-art-ryo-com +haisyanonishikawa-com +kagayashio-xsrvjp +webkentop-com +shihatsudo-xsrvjp +ki2-xsrvjp +3dsatsuei-com +terry-f-p-com +pman-bros-com +k-ravi-com +bgtv-xsrvjp +tatamidani-com +xsample327-xsrvjp +manabu53-com +bloomspace-kannai-jp +balakovo +center53-jp +uchiwa-ooyabu-com +kareisyuu-biz +you-tak-com +estage-biz +ogawa-shika-orjp +xinfo726-xsrvjp +xinfo341-xsrvjp +kagawacoop-com +dokuzimesen-com +bo-doya-com +u-archi-cojp +tabi-con-jp +worldholdings-jp +takayuki-ll-xsrvjp +devil8-com +risokakaku-com +mayumbb-com +akane-e-com +sefuri-shaken-com +fucker +sihousyosi-houjyou-jp +syougatuapp-com +nogami-wedding-jp +syosendo-xsrvjp +kotobayo-tv +qjin55-com +sapuri-kenkou-com +kaigahanbai-com +xsample118-xsrvjp +riddlepuzzle-xsrvjp +mie-sports-orjp +kanto-clinic-com +ziggy007-net +goko-h-com +hidamari-b-jp +king13-xsrvjp +s-kensha-com +cloverhomes-cojp +xn--zck3adi4kpbxc7d-biz +subete1-com +wwwxyz-jp +gaiaokane-xsrvjp +takaragi-iin-xsrvjp +wing7878-com +homard-festa-info +ohakanomadoguchi-com +xn--duz45k-com +archm-com +shibano222-biz +volzhsky +yokohama21-com +nakasuhaken-com +dddmmm-info +relcc-com +ile-jpncom +paparazzi-tokudane-com +wubarosiermaschine-com +stencilhair-com +shihiro-info +g-kscom-xsrvjp +zare-goto-com +kashiwadecon-com +evisevis-info +0906daiki-com +e-mediasystem-biz +pier-stone-com +yusukeaoki-biz +xn--u9j5h1btf1e330r917aok7b5id-com +shvideo-biz +boxystyle-com +minasayo3734-xsrvjp +clic-clac-jp +jushin-biz +kelbox-com +cer-n-net +xn--ujqp84atlah52f-com +koto-note-com +denritsu-cojp +cookiec-com +bike-ridestar-com +eym-xsrvjp +filingcabinetscheap-org +yual-jp +aquastudy-jp +o-bs-cojp +hotelgranbois-com +gong-yoo-jp +aoi-nakamura-net +xn--news-4c4cuuha3z9b3580f16c-jp +round-dev-org +xinfo757-xsrvjp +test-xsample318-xserver-com +happy3-org +sansak2-xsrvjp +tenryosui-net +pallu-jp +ebisuyasan-jp +next100-mobi +headway-xsrvjp +all381-com +humanlink-r-com +erika-jpnet +ehimeokayama-com +bastiani +al-medic-com +k-plus-nail-net +egaonojikan-com +test-xsample315-xserver-com +sv0-xsrvjp +fd02-info +palmgate-xsrvjp +yamashow-reform-com +findanect-com +technopolice-jp +kabu-michishirube-com +yamato-ac-com +nobuyukieto-com +kagi-qq-jp +adsl3 +xinfo517-xsrvjp +mid-nation-com +carole +sunnyshmail-net +shigacon-net +xsample149-xsrvjp +yuyasawada1-com +al5586-xsrvjp +kotosangenkyousitu-com +porphyria-jp +enter-word-com +tokyo-sundubu-net +facebook-page-jpncom +xn--zck3adi4kpbxc7d3858d8zc-com +jstaf-jp +cosa-xsrvjp +himito-com +seirios-net +wih-viewer-com +teryori-jp +jen-xsrvjp +bid-xsrvjp +dream-east-info +tachikawacon-com +htn-cc +crazykenband-com +bltc-cojp +light01-xsrvjp +go33l-com +hagicyosu-dou-com +10pipstrader-com +maristhand-com +psychotherapy-jp +saykei-com +svcdeaf-org +cressence-salon-com +nt-mc-com +doredoko-xsrvjp +xn--k9j703lrer-com +yuhiglass-com +nakayamaderavet-com +xinfo127a-xsrvjp +cl-hearts-com +yasooo-xsrvjp +onishia-xsrvjp +leadea-xsrvjp +brixc-com +test-xsample31-xserver-com +pit-design-com +yellow1003-com +nets-han-com +fit-leading-cojp +abccraft-xsrvjp +maltholic-com +makotonohsan-com +premier-ballet-com +1streform-jp +xn--gdkxar8d4dc-com +xn--n9j8gnb1bza2am-jp +ishigaki-wedding-jp +toryburchoutletv-com +cepavinis-com +woo-yan-net +hpservice-jp +js2013-net +koara-kids-com +piececlub-jp +risshun-info +mahalo-web-com +xsample80-xsrvjp +japinglish-com +bellbell-info +hana-salon-jp +xinfo103a-xsrvjp +suisokan-org +sansyu-ya-cojp +testdomainx322-com +studio-angel-net +ba-chi01-com +xn--zck4ba9kwb1956ag23ad2za-com +recolon46-biz +nishiazabu-con-com +tokyofanhaus-com +wattam01-com +blobiz-xsrvjp +xinfo548-xsrvjp +pclub-xsrvjp +intention-xsrvjp +taikyoku-en-com +beastpets-com +element-bz +okane-antena-com +tbo38-com +daikichido-net +astone-info +ryugu007-xsrvjp +sun-face-jp +musou-gakuen-com +photopierre-com +src123-xsrvjp +le-kind-com +s-motoclub-com +fis-xsrvjp +studiomym-com +s-treat-com +guiters-biz +pluspeace-net +dip-dev-net +jhigh-net +kawaiolaokona-jp +le-bretagne-com +angeproduction-net +fieldofpine-com +kurumecb-com +ecollab-biz +basskiti-com +xsample326-xsrvjp +nihonweed-cojp +iphone-iresh-com +3maison-cojp +hanamine-com +otanjoubi-cake-com +tij-babysitter-com +44ok-biz +xn--9ckkn5226aut1aee3bgbf6ma-jp +brandoffkaitori-com +hro777-com +hiyoko-f-xsrvjp +honmahajiku-xsrvjp +mikamiyui-com +eikaeika-xsrvjp +thinkingbirds-jp +incenx-com +toriii-xsrvjp +kyouritu-f-com +mms12-jp +iwakuni-ymca-xsrvjp +test-xinfo763-xserver-com +hthththt-com +yoshino-koumuten-com +ngsk-dha-org +kumamoto-hp-com +iscle-com +5rion-jp +sinsaibashi-com +easypachi-com +kampo-yamato-jp +test-xinfo743-xserver-com +imokoi-com +0257762813-com +entrust-xsrvjp +amu-web-xsrvjp +www.spirit +petone1-net +asmik-xsrvjp +test-xinfo733-xserver-com +atsuko-coubo-com +kul-site-com +test-xinfo723-xserver-com +testsp1208270-com +hot100fm-com +xinfo204-xsrvjp +rehman2039-com +test-xinfo713-xserver-com +l-create-com +xsample222-xsrvjp +boribonoeuf-net +testsp1208275-com +test-xinfo703-xserver-com +nixon-watchshop-com +ebisucon-com +xn--ecki4eoz2903cuuwhdt-biz +mocjp-com +esthe-core-com +gori3355-com +hamakazetarou-com +aigi-net-com +itp-xsrvjp +yuminn-xsrvjp +yuki-inoue-com +soylatte-jp +korudeo777-com +tereza-cojp +takken-mobi +md123-net +shinkikakutoku-com +meiyo-jp +xinfo725-xsrvjp +noni-happy-xsrvjp +canal-interior-com +skeppshult-jp +nano0321-com +yuzumo-com +s-a-jinzai-net +assist21-orjp +meichu-cojp +taaf-shinjuku-org +alphatau-net +xn--88j6ea1a4250bdrdi9am84bkx5cbp2b8xe-asia +luciole-classe-com +gigeinc-com +keeno-asia +uranai2012-biz +gijiroku-center-cojp +green-2-com +shimomatsu-com +homus1-xsrvjp +saiki-nejp +dik-xsrvjp +vd23-com +kaguyahime-f-com +richquest-org +footballshop-legends-com +sweet-loveletter-com +shinjuku-conpa-com +toramaru02-xsrvjp +all-cosme-jp +monodukurijapan-com +xn--cckwaf4jng-com +er-nerima-com +fit-hip-com +u-bike-com +ipnetfarm-com +projectexceller-com +memory-travel-com +one-mode-jp +terrapin21-com +xsample117-xsrvjp +kigyo-kokuchi-com +baketu-cojp +junko21-com +7-cs-blog-net +vakcom-info +badgeblackbox-com +test-xinfo593-xserver-com +daaue-com +eeyorechan-info +template-party-com +auc-aifa-com +100seo-jp +sentei-kumamoto-com +yukuru-honten-com +neoapx2121-xsrvjp +yukyu-h-com +tanizawa01-xsrvjp +test-xinfo563-xserver-com +nakatsurutomoko-com +nakatomo-step-com +eyehorn-net +novelsounds-jp +test-xinfo553-xserver-com +puru2-org +studio-th-com +infinity88-jp +shoga3-jp +seoky-xsrvjp +test-xinfo543-xserver-com +karate-jp-com +club-s2-com +eden-japan-jp +xn--u9jz52g24i4sa42enx9aeir8k1b-net +m0523t-xsrvjp +uud-info +test-xinfo533-xserver-com +koreaichiba-xsrvjp +diginfo-xsrvjp +facebookapp-xsrvjp +ipaa-nagoya-com +e-enak-com +xsample140-xsrvjp +theory-ken-xsrvjp +epa-c-com +test-xinfo523-xserver-com +parama-xsrvjp +houjo-cojp +xinfo756-xsrvjp +simple-smile-net +test-xinfo513-xserver-com +mc-sss-com +slip-case-com +fl432-com +takeuchi-hoken-com +hiro-guitars-com +b0 +odairashoukai-jp +test-xinfo503-xserver-com +c-sharing-xsrvjp +81smile-com +yod-on-xsrvjp +xn--pcka3d5a7ly86z14i-biz +welcometo-nejp +nagase-syaken-hikawa-com +motionworks-jp +st-tajima-biz +takasakidecon-com +nightfactory-info +dental-no1-com +kaoku-biz +xsvx1011020-xsrvjp +kickboxing-kashiwa-com +test-xsample327-xserver-com +harimitsu-cojp +atelier-mamemaki-com +herbteapresents-com +most-adult-xsrvjp +onara-kusai-com +pachiloca-net +xinfo516-xsrvjp +test-xsample317-xserver-com +ampchan-com +5-es-com +sg-transmgr-xsrvjp +try-and-buy-net +xsample148-xsrvjp +re-sound-jp +test-xsample307-xserver-com +kousikai-net +esa-sawamura-com +seikoukai-zushi-orjp +super-compa-net +akashi-syaken-com +psclub-jp +fishmind-jp +bantyou-org +g-s-c-cojp +designbatake-jp +1chou-jp +teppanteppan-com +air-victory-jp +sogacha-com +ace13-info +matusitakoumutenn-com +hatachi-xsrvjp +eishin-ac +naluto-net +intercross-info +mediaserver1 +yumemarche-com +1919okinawa-com +f-maruka-com +isshisha-com +yamatoroman-biz +smtown-passport-com +nkk2-xsrvjp +hukugyou-shoukai-com +045310-com +yoppy009-xsrvjp +implant +jbja-jp +rouxdistaff-cojp +hoaloha-ohanaband-com +pcsidejob-com +xn--ddkf1h-com +koikurozu-com +blanchett-hair-com +hondaisuki-com +west-m-jp +6pmn-com +koiz-me +noh-oshima-com +test-xinfo344-xserver-com +xn--u9j5h1btfxee0254c9vzb-com +test-xinfo353-xserver-com +murakon-net +four-friend-com +xn--ecki4eoz6990n-net +doutonboricon-com +app48-jp-com +test-xinfo343-xserver-com +dynamiteking-org +xn--u9j1b755lhwlmhm0pjeia182v-com +pointmaster-biz +bekotei-jp +b-trust-systems-com +test-xinfo341-xserver-com +markc1 +xsample78-xsrvjp +kagami-tm-jp +nanbacon-com +la3-beam-com +fitsdiet-com +augolfjp-llp-com +sm-link-xsrvjp +alpalp-xsrvjp +royal-body-jp +webone-cc +athome-mj-cojp +zone-portal-com +otopost-net +xinfo547-xsrvjp +jp-hyperweb-com +fr3 +chefclub-com +ginzaconpa-com +kappou-kikuya-com +cpk-hh-com +europort-ironprint-com +xn--n8jwa6c-com +azvogel-com +admini-s-com +gaia0369-com +newbabygiftbaskets-net +kamig-jp +tag-dake-com +barikan-blog-net +sakamoto-engei-jp +gocebu-jp +minato-auto-jp +tarabya-boschservisi-com +seach-c-com +msinter-pv-com +ventitre-xsrvjp +wk-apple-asia +xn--u8je2227b-com +re-frames-com +sabureview-com +tm-ad-cojp +xsample325-xsrvjp +dr-suisosui-com +webject-biz +tenjinhanabicon-com +nara-conh-com +ya-maki-com +toyofumikaneko-com +kazuno-meishi-com +santoku-net-xsrvjp +kengyofx-biz +keep-life-com +office-sr-net +webooman-com +kyonara10-xsrvjp +onlinecasinodekanemoti-com +primavera1997-com +truenature-jp +vtwins-net +celebstyle-japan-com +owlview-jp +s-ponii-info +muko-shaken-com +dance-sports-net +nemlino-jp +car-seibi-com +xn--tdkg5cc9fc7935nm0f-biz +hito-chiiki-org +thaisrs-com +tokyo-ic-xsrvjp +cradle-to-grave-net +uji-chara-net +soundspice-jp +takakurashinji-com +jbiz-xsrvjp +tsukamoto-dental-net +alpine-apps-xsrvjp +ascorbic-box-com +fc01-xsrvjp +selfish-cm-xsrvjp +worldcycle-info +katsuragigarden-com +xinfo106a-xsrvjp +secondstage-car-com +sanjyuushi-xsrvjp +ein-xsrvjp +jubjub-jp +transfer2 +kandai-mansion-com +asunaro-grp-jp +r-pep-com +sat-mental-net +wec-future-com +ootani-net +xinfo560-xsrvjp +datsu-colle-com +boschsecurity-jp-net +hirosaki-ringo-com +tkmyume-xsrvjp +koutazeroism-xsrvjp +haisyahatap-com +xinfo578-xsrvjp +npo-jfta-org +hispano +mistgrass-com +ioris-xsrvjp +xsample221-xsrvjp +netter1-com +kumage-rk-jp +testsp120423424-com +k3-style-com +hand-yume-com +minohharmony-com +6777-jp +theoutdoor-jp +farfaro-com +penplus-jp +korakudo-com +makkoy-xsrvjp +takashimabio-com +freelifelike-com +12sunhome-com +steak-ichiban-com +lcs-jikken-com +spawn-dev-com +sakura-cosme-com +w-workhome-com +teikokusoken-cojp +makibi-com +hachioji-con-com +0507sun-field-jp +kankouniiza-xsrvjp +xinfo724-xsrvjp +xinfo338-xsrvjp +stra-ws-com +adonary-com +bangnacoupon-com +idarts-cojp +amalpha12-com +magic-gu-com +freedom-corp-com +gz-burst-com +kawasaki-sougi-net +rekishi1-xsrvjp +coloplfan-com +focal-p-xsrvjp +jwebcreation-com +bloomsbury-photo-com +seitai-kumeda-com +jobpla-com +joy-strike-com +znf-cc +ayuda-biz +gori2012-xsrvjp +atsumaru-xsrvjp +sishuu-com +makoto2008-net +em-agency-xsrvjp +test-xsample319-xserver-com +liflow-net +tint-jp +chubuoudan-com +nonhoi-farm-jp +teiken-xsrvjp +kishimotosyouten-com +style-lab-xsrvjp +fisphoto-com +xsample116-xsrvjp +ooidekan-com +p-pr-info +xn--pcka3d5a7ly86z14i-net +with-planning-jp +cyber-cubic-com +hibinotatami-xsrvjp +kokumotsu-saikan-jp +fukumachi-cojp +ikawa-xsample50-xserver-com +artificial-photosynthesis-net +testxdomain320-com +nagoshi01-com +arujanaika-com +xn--ruqr59dpuht2t50er1m-jp +azureblue-xsrvjp +r-mizuno-xsrvjp +gakushif-com +influentialmen-net +samaa-cojp +silc-jp +ee-english-com +senbokumomoyamadai-doin-jp +cecodel-org +ssid-xsrvjp +seibunsha-info +web-matrix-jp +webarx-cojp +shikitu-net +tokusuru-print-com +mydays-off-com +japanadvancedmall-cojp +musicmmm-xsrvjp +sanoyas-eng-cojp +gpc-kyushu-com +irezumi-ya-sl +hiro-odecon-com +kouninkaikeisitoyo-com +mamasnote-com +aquarela-jp +niveusjp-com +lsstatsuta-com +atlantisfalling-com +ahti-jp +siw-jp +fantanet-jp +harbarlandcon-com +ntc-tool-com +zero-sys-cojp +bijincoupon-com +1day-implant-net +couleur2013-com +minami-yasu-com +jgt-tour-com +kizan-kouyou-com +kaiungoods-jp +counselornavi-net +green-dental-me +trendoffice55-com +cobee-jpncom +xn--hckqzd2f3dwc2d3a-com +kyoubashi-cul-com +xinfo515-xsrvjp +facebook-app-biz +runchan-jp +keiko12-com +heimat-ltd-com +xsample147-xsrvjp +xn--zck9awe6d418qo4jbw1c-jp +okumura-sekkei-net +m-tree-info +takatec-info +lebouquet-bz +maiko-hotel-cojp +arklab-biz +aslynx-xsrvjp +es-lash-jp +arcras-com +akaike-ss-com +image-nail-net +misao09110704-xsrvjp +tsi-xsrvjp +shinga-cojp +proxy03 +geppappixi-xsrvjp +karaj +forest-kk-com +reitakukai-jp +oki-navi-net +mikuriya-xsrvjp +eco-imagine-com +n-rock-xsrvjp +rakujisha-net +akion-cojp +tem-pus-com +tomtak-com +fb-jisenkai-com +powerstone-rin-com +healing-spot-xsrvjp +sousha-net +ripd-info +kawasaki-syaken-com +happybank-cojp +br-tokuyama-hcp-com +arakazz-info +1-1st-com +whitoa-com +roupeiro-com +12suh-jp +takeshi97-com +econowa-org +wee-xsrvjp +jrps-org +rjen-asia +bilbao-jp +mohemohe-net +121btl-com +rimowa-suitcaseshop-com +ijci-info +miyoki-cojp +omotenashi-job-jp +onsen1126-xsrvjp +rinotter-k-xsrvjp +tsuruda-ganka-com +tempur-com +ito-consul-com +kaga-fes-com +i-lightly-com +matsudaiyaku-cojp +graphicbeat-xsrvjp +lbks-jp +winningfield-net +lib-job-com +t-lostbarrel-xsrvjp +kyoubashi-yasu-com +web-blend-com +xn--cckc3m9cq08p0u3ai3w-biz +ainsel-info +xsample77-xsrvjp +shoppeta1104-xsrvjp +en-pc-jp +web-arte-biz +uesportal-com +launch-pad-jp +zigzag-label-com +g-ks-com +mistgrass-net +hacophoto-xsrvjp +pug1-net +chargercam-com +cyberdesign-xsrvjp +cp-av-com +higashishinsaibashi-yasu-com +ideaman-nu +kameyamashachu-nejp +varuna-xsrvjp +mental-h-xsrvjp +nori1-xsrvjp +moba-net-info +onejustice-biz +haniwa02-xsrvjp +dreamplus-biz +tukasayou-xsrvjp +kanayama-cl-com +golgol-xsrvjp +yokadoichi-com +djr69-com +okashinosakai-cojp +e-hm-life-com +xn--cckc3m9cq08p0u3ai3w-com +expand-muchiuchi-com +balletstudio-reverance-com +testdomainx325-com +soylatte-xsrvjp +hai-saga-jp +jubjub-asia +hk715-com +kawano-tm-com +social-game-biz +bishukan-com +umihair-com +mtshastajapanhealingfoundation-com +h-h-lab-com +testdomainx331-com +ris2r-com +k-i-jp +kimuragosei-cojp +nissei-cc +life3-xsrvjp +shiraibutsuri-com +hyper-kenchiku-com +xsample324-xsrvjp +mental-cojp +jyokoshoken-cojp +poco-a-biz +www.pardis +testdomainx336-com +mk-financial-xsrvjp +ebook-jp-net +premium-life-info +e-takino-com +xn--kckwar5itb7grc-com +testxserverdomain585-com +ashiba-cojp +shirakabako-biz +cihs-courage-org +taa-xsrvjp +touch--me-com +qtopia-jp +rto +lapps-jp +big8686-com +satellite-seo-com +mame-no-sato-com +kaigojoho-hokkaido-jp +d-linnet-com +sym-q-xsrvjp +kagushop-biz +osakakita-hanabicon-com +affili8-marketing-com +lukema3-xsrvjp +slaves-jp +joowon-net +whiteline-bicycle-com +ayuzak-info +kuroiso-net +enecost-cojp +otakuism-net +kazupk66-com +rakuikumama-jp +xn--00-bh4a8cuhme-jp +suidousetubi-xsrvjp +denoukeiba-com +olivenoniwa-ookubo-com +montejapan-jp +www204 +sus-wako-cojp +collegehula-com +marrrtaru-com +bistro-vignoble-info +bonia-jp +xn--n9jxild6c580r9ygq36b1ocor6evm4b39d-com +japanflower-net +prosemi-com +shimamura-reform-com +peekaboo-xsrvjp +xinfo577-xsrvjp +oumigawa-com +xsample219-xsrvjp +scryypy-com +solare-kenchomae-com +reito2274-com +cobaten-com +sprouts-xsrvjp +sideway-jp +you-our-com +jes-co-jp +blisslife-jp +xinfo158a-xsrvjp +aipet-cojp +yousai-jiten-com +osnews-org +arcadia-systems-net +tintlab-com +xinfo723-xsrvjp +xinfo337-xsrvjp +tipmarketer-com +hatomove-com +yamaurakensetsu-com +kieishouji-com +sr-onetop-xsrvjp +xn--p8j5cxcyjlcygn342e-com +dounano-net +xn--u9j9eud6c3b3bzb3015d38xbyhc-biz +kagurazaka-con-com +f-estate-cojp +taiyakitei-com +plusdesign-info +soltilo-com +yuutasiro-com +sakadesign-xsrvjp +afortuneteller-biz +affiliate-negoro-biz +tokimekihonpo-com +lib-gate-cojp +l-deza-com +kiyoshi1180-com +tokorozawacon-com +clincash-com +test-html-com +kimono01-com +sumahotuuhan-com +rakutarojp-com +hapicom-jp +kita0770-xsrvjp +xsample115-xsrvjp +toolbox-repair-com +jumonji-u-net +mkcore-com +aspect-dp-jp +v-softball-info +aomori-me +fukuokabiz-com +scale-xsrvjp +raku-nakano-com +eco-imagine-net +xinfo109a-xsrvjp +kakuyasujoho-com +belle-cheveu-com +g-wks-com +loisir-hakodate-com +kumamoto-investment-com +ys-square-jp +zakkanowa-com +facebook-app-net +a-business-mail-com +smo-labs-net +densyoku-net +orb-pro-jp +test-xinfo726-xserver-com +japanesedesigncollectibles-com +sanjo-b-com +fullplusca-com +oogai-com +westfieldhousebnb-com +umekantei-com +greendaikou-com +fineartstudio-info +samuz-net +testxdomain301-com +note-123-com +iine1-xsrvjp +modernforest-xsrvjp +ladykaga-me +newentertainer-com +kamata-con-com +hokkaido-traveling-com +igial-com +design-sample-com +kurumedecon-com +boy3-net +testxdomain306-com +kouhoukai-jp +tsukasen-com +kakazujimai-com +shinjukuconpa-com +kurokawaonsen-xsrvjp +smartphone-club-info +e-iwatate-xsrvjp +kazu-bz +rakugofan-xsrvjp +xn--cckagl3fc6czknac2gn3hscza2hzgf0225npksd-com +39antenna-com +testxdomain312-com +koshigaya-dc-com +partyglide-com +addplaza-net +g-excellent-com +momosign-com +miki-soft-com +bellegraph-com +n-insurance-xsrvjp +adman-cojp +ecomott-cojp +testxdomain317-com +lomy-thai-com +anesakiapart-com +f1-gate-com +moicom-xsrvjp +keishin-net-net +santomiya-jp +yes-2784-com +yogurtia-net +kyotoujigawa-hanabicon-com +goyoutashi-jp +keyringpics-com +eco-works-jp +jellnail-dvd-com +xinfo514-xsrvjp +o2ocafe-net +ishiwata-rashi-jp +shougai-navi-com +allmato-me +xn--cckc3m9cq08p0u3ai3w-net +ankul-com +lovesquare-info +atozwindows-com +yoshino-i-cojp +gyakutensaiban-info +nichiei-sv-xsrvjp +tomidvd-com +sakura-marche-info +independent4-xsrvjp +apptongs-com +jens-e-jp +sfa-chitose-com +franco-jp +mermaid-xsrvjp +shinkodo-jpncom +ittaku-xsrvjp +mtcr-jp +xn--yckc3dwa2295ckj8ah82a-com +xsample35domain-com +jlr-kobe-com +rinotter01-com +dream-stone-jp +tomohirokinoshita-com +xinfo340-xsrvjp +adworks24-cojp +meo-auto-biz +tom16-com +liveinasia-info +about-doors-xsrvjp +yanbaru-jp +soulofgold-net +tekken2-biz +chisuibrass-com +xn--ick9dc3e7aa8i-biz +pinkorokakumei-com +goodreform-biz +probbax-jp-jp +media-producer-net +myaru-com +estenad-bigan-info +xn--zck9awe6d4687a257a-jp +griter-biz +l-w-xsrvjp +lisec-orjp +622334-com +testup-net +rokugo-asia +kiyoshi17-biz +densisyoseki-reader-com +saizou01-com +camonoe-com +aris-kg-com +monozukuri-nippon-com +easycom-cojp +newglasgownovascotia-com +dayori-net +asahi-agency-xsrvjp +nogizakacon-com +ebrietas-org +takahata-auto-jp +mashuk-jp +info-netbiz-com +quick-eigo-com +mallorca-western-festival-com +matusima-xsrvjp +vanah-nakatsu-com +onsenken-jp +seina-xsrvjp +fifty7tk0188-xsrvjp +zakinosuke-com +kinkmonster-com +atelier-nobara-com +fcsakuragaoka-com +kk-sc-net +xsample76-xsrvjp +ktaiseed-com +horie-t-jp +gavaiyoka-com +optronics-auction-jp +xn--t8jo4884a-jp +jsfs-info +yoikode-xsrvjp +quesera-yuki-com +arailaws-com +xinfo545-xsrvjp +mori-photo-com +bongyaku-com +testsp1205552242-com +katuos-com +autm-hamamatsu-jp +media-producer-org +celebrity-pro-am-classics-com +aurens-info +under-the-tree-com +haco-photo-com +yuugajapan-com +1515-tv +happyabundance-info +km-land-xsrvjp +packb2b-net +assorezo-com +demo30 +guchikiki-chamomile-com +suninlet-com +shopafroaudio-com +yuri-medical-jp +j-forest-com +asumia-jp +shinsuke2315-xsrvjp +jakujoen-com +info-movie-com +happyrich-biz +smtn-jp +fusenya-biz +isogube-xsrvjp +wishcraft-cojp +uvd6-xsrvjp +pilkasiatkowa-com +xn--cck0a2a1dug6be8l-com +hellogoodbye-xsrvjp +fujinomiya-dry-cojp +irisartjapan-xsrvjp +xsample323-xsrvjp +ogasa-xsrvjp +km-land-net +bebit-com +xinfo379-xsrvjp +kokoronoisan-com +karenaiki-com +wp-sample-info +ryouridougarecipe-com +thecomputersolution-net +xn--39jube-com +xn--cckwaj0kmd4e9bd-com +xn--nbkzdraq9b9dtevlv08xj5b1vh-com +hikarinotane-com +tre-ca-xsrvjp +xsample150-xsrvjp +otowa-wedding-jp +superb-cojp +tomosdeputyservice-com +orugae-com +ihavetogo-asia +macha13-com +sakaisouzoku-com +function-five-com +hashimoto-schule-com +successrecipe-biz +keihan-exterior-plan-com +getphonetic-com +xn--kckb1c4m-jp +matuya-knit-com +aquapress-xsrvjp +bujutsukarate-com +gestaltschwarze-info +ekenzai-com +hsk-archi-xsrvjp +bestplanning-xsrvjp +tuuhan1-com +ts-ado-com +chuo-dc-net +c-how-biz +gangnambeauty-jp +1okuen-com +garam-dinnings-com +cpsjp-com +bit-com-jp +himawari-shinkyuin-com +kokorono-resort-com +7go-jp +zaxtuta-com +kellch-com +plan-kimono-com +furanotourism-com +xn--n8jvdsa6soa5jz01vtb9bg6lk11hdbi-jp +specialforce2-net +xinfo576-xsrvjp +marathon-festival-medical-net +soooooooooon-com +design-cha-com +roots-eng-com +yurakucho-con-com +pset-xsrvjp +aquafarm-cojp +kenaf780-net +sp-vision-xsrvjp +gallerybooth-com +mcls-xsrvjp +kanagawa-roujin-jp +wind3000-com +daiwakogyo-net +soralink-com +celemarry-com +merumeru5252-xsrvjp +net-businesses-biz +laneige-info +earlyservce-com +golden-item-xsrvjp +yururi-web-net +xinfo722-xsrvjp +test-xinfo741-xserver-com +yutakajutaku-com +i-seikotsuin-jp +komon-se-com +test-xinfo731-xserver-com +fufuzukan-com +meimonconsul-com +niku-kitayoshi-net +mhplan-net +coniconi-card-com +test-xinfo721-xserver-com +granpia-jp +yo1-xsrvjp +yumemagic-net +bloom-blooming-com +test-xinfo711-xserver-com +cvsiy-info +moe-jk-com +blue-drop-xsrvjp +gyakusenkyo-com +gyusujicurry-com +test-xinfo701-xserver-com +samegai-me +sendai21-com +office-akashiya-com +xn--cckcn4a7gwa5p-com +xsample114-xsrvjp +kotsuban-belt-jp +fudemojihonpo-com +samansa-eco-com +dailymnews-com +miurazoen-com +sleepnightsheep19-com +facebookmultilingual-com +astalive666-com +bidtest-info +jbn-cc +jishamap-com +hana--home-com +test-xinfo107a-xserver-com +hired-cojp +club-hanasakura-com +nichihaku-com +e-fuzzy-jp +juice-no1-info +dl-sys-xsrvjp +1985games-com +cosmo-mizu-info +mic-fishing-com +triplearrows-jp +forum6 +do-ticket-air-com +bscreate-xsrvjp +coco39-com +horaiya-net +vizdev-xsrvjp +xx00123-com +asa-pro-net +happybirthdaypresent-net +slim-free-jp +mimatsu-wd-jp +fly-journey-net +smaho-media-net +atelier-niji-com +hack-you-org +test-xsample312te-xserver-com +scachan-com +dogwoodgreensboro-org +machipri-com +hikaku-jouhou-com +ebisu-go-jp +test-xinfo591-xserver-com +addtrust-cojp +hodou-jp +art-law-jp +hakoniwa-toybox-com +test-xinfo581-xserver-com +quafolium-com +tk0324-xsrvjp +taguchi-xsrvjp +ms-cl-com +sannomiya-yasu-com +kunikotakahashi-com +waganse-jp +nakamegurocon-com +bp-labo-com +r-mansion-net +eco-power-jp +test-xinfo561-xserver-com +uttishop-com +test-xinfo551-xserver-com +kazukasegu-com +horiba-gafu-com +jiyugaoka-orjp +it-serv-jp +xn--jprz31c82x93etka-com +xinfo513-xsrvjp +sousha-jp +aijanren-com +landjp-com +dream-g-info +aglo-shop-jp +utayuki-com +roks-xsrvjp +xsample145-xsrvjp +gakuho-net +test-xinfo531-xserver-com +blessvery-com +gunma-kansenjohokyoyu-net +ishiyy-com +test-xinfo521-xserver-com +yotume-com +bousai-bread-com +test-xinfo511-xserver-com +tsuyamakoyou-xsrvjp +zz5-biz +machiori-xsrvjp +aginfo00-com +schoolie-jp +arcadia-ex-cojp +mizunasu-org +hsfdf119-com +test-xinfo501-xserver-com +xn--n8j4mybtf1e2217b-jp +shophidamari-com +ntcsd-xsrvjp +la-neigh-net +amashiro-asia +keirin-campaign-com +www103 +matchkk-com +chiezou-xsrvjp +nihongo-daisuki-com +uovo-kyoto-com +webstyleair-xsrvjp +maruka-nouen-com +pcjp-com +phine-jp +kanto3-xsrvjp +funabook-com +goalkaramiru-com +olc-xsrvjp +cafeterrace-syu-jp +arteq-jp +kaminishi-xsrvjp +test-xsample305-xserver-com +twinv-info +pagejp-com +keicyousou-net +kashiwada-xsrvjp +satotti33-com +ogikubo-magazine-com +221b-net +sakematsuri-com +isseico-xsrvjp +mrmk3-com +anotherselect-com +soma-bikeseat-com +at-attain-com +admans-xsrvjp +egaodaisuki-jp +xsample75-xsrvjp +cowabunga-app-com +thedcdimension-com +rengoku-circus-info +matusima4494-com +imonobito-net +heart-cocktail-net +yutaget100-com +aotate-com +sakurado-xsrvjp +cranky-hb-xsrvjp +sogo-hone-com +rokushu-com +kagayashio-cojp +tsujishikaiin-com +yossy01-com +poplifejapan-org +swish-xsrvjp +ji-beer-com +xinfo544-xsrvjp +travelingbirder-com +malzel-com +casablanca-jp +5on-biz +atplus-cojp +zanimoenfolie-com +sailingstyle-cojp +lovpap-info +god-cleaner-com +omoti-biz +yoshu-shoji-com +nishitaka-jp +funai-consul-xsrvjp +pees-cc +xsv-xsrvjp +ggoodnews-com +test-xinfo351-xserver-com +ankhresearchinstitute-com +yullyanna-com +techno-pia-com +tomasuke-com +jc-project-xsrvjp +malaz +delight-workshop-com +xsample322-xsrvjp +kumakko-jp +xinfo592te-xsrvjp +xn--qiq69x-saitamajp +testdomain1072123-com +0da-biz +tabi-yoyaku-com +kyugoro-xsrvjp +bvbox-net +mangoflag-com +kumashinren-com +lc-takatori-jp +masa-don-com +androjapan-xsrvjp +yoihanko-jp +dekomechan01-com +two-roses-com +cloverhomes-jp +xinfo540-xsrvjp +tobira5963-xsrvjp +fs-xuxu-jp +freedom555-biz +kurumacom-xsrvjp +ogoutatamiten-jp +suppli-kaiin-com +1031produce-com +kote-site-com +to-ko-ne-com +igamblingreview-com +senkyo-navi-me +ispice-jp +beauty-salon-la-alegria-ubecity-com +cocolable-xsrvjp +ozawaayumu-com +pasoall-com +pinachee-com +uxf-cojp +garlic-onion-com +attosystem-cojp +kumitori711-xsrvjp +nagoya-con-com +pre-style-com +meimonedu-cojp +sakata5-com +tea-studio-y2-cojp +shiroyandesu-xsrvjp +xinfo575-xsrvjp +okw-snowmobile-com +richness-xsrvjp +kujukushima-visitorcenter-jp +xsample217-xsrvjp +enjoy5key-com +ks-corporation-jp +action24-jp +yumekanau-k-com +ohnoseikotsu-com +mizuhokai-jp +lopple-xsrvjp +besty-ichigomilk-info +w-bukkyo-jp +xn--z9j3g6bzc7bxc8c4074d4nud-biz +abundance13-info +ds-shikinoie-cojp +vendium-net +netapp1 +emoto-dental-net +announcement-xsrvjp +kurose-info +dreamaffilistyl-com +hs-group-cojp +linx-corporation-cojp +toyota-kansenkyo-com +tsuruhashicon-com +tanakashika-com +fkohuman-com +114tx-net +skeidai-3l-com +fudousan-nagano-com +paperhousesha-cojp +rikon-bengoshi110ban-com +thinkplanet-xsrvjp +xinfo721-xsrvjp +server-ptsd-xsrvjp +north-giken-net +kataigi-xsrvjp +xinfo566-xsrvjp +turning-point-biz +battlespirits-kaitori-com +benriya-net +minken-net-com +shidou-seitai-com +mz-corp-jp +gotanda-mien-org +valueup-info +syousuke-jp +miu111-xsrvjp +touho-yamawa-cojp +youvision-info +otokukasegu-xsrvjp +xn--6oq618aoxf4r6al3h-biz +gakuseievent-xsrvjp +e-douguya-info +aiheart-jp +xsample79-xsrvjp +libertyandmoney-com +umehara-biz +xsample60-xsrvjp +tryangle-sys-com +xsample113-xsrvjp +test-xinfo109a-xserver-com +extreamposition-com +miki-mobi +testxserverdomain120301356-com +fukensan-jp +wonderparty-net +test-xinfo108a-xserver-com +tribuddha-xsrvjp +misato-kome-net +7and6-net +holiday-homestay-com +gojo-aijien-net +nagasaki-sam-com +m-davide-net +aroma-shikaku-com +lyl-jp +portalshake-com +re-pair-biz +test-xinfo106a-xserver-com +idumi-g-cojp +xn--28jzf747o512b-jp +test-xinfo105a-xserver-com +pacoten-xsrvjp +yamada-tatami-jp +ignis-nejp +sharedsoft-xsrvjp +dhcp1a +xn--pcka4lj0a1as2jf5847fr93b-net +bbboso-jp +shinsaibashi-con-com +mikado-bekkan-jp +testsp120821test-com +piisko-xsrvjp +mitumame-jp +test-xinfo103a-xserver-com +aiware-distribution-com +babybaby2012-net +web-shin-xsrvjp +londongeisha-com +test-xinfo102a-xserver-com +nioistop-net +murakami-b-com +goodchance-xsrvjp +amazon9948-xsrvjp +nds3 +testxdomain315-com +test-xinfo101a-xserver-com +gofun-p-net +6230ongaku-com +officels3-xsrvjp +basekobe-xsrvjp +thegate-key-com +asa-hh-com +come-x2-com +hakkou2-xsrvjp +bfesca-com +uni-space-com +sanwakougei-com +unhwa-cojp +myt-p-info +infinite-as-com +globalimporter-jp +33abundance-com +xn--a-5fu2f3a-com +zepto-jp +machiako-san-xsrvjp +bethpage-jp +4ssb-com +fusacorp-xsrvjp +lai-inc-jp +xn--k-pfuybek0nvb2cue-com +yoshixx-com +kekoberry-com +enomad-jp +xinfo512-xsrvjp +afl-design-com +wakouki-com +xsample144-xsrvjp +mogabrook-jp +starvictory-com +datusara-net +jin-1999-xsrvjp +o9obank2-xsrvjp +rakuzanet-xsrvjp +mekakushi-fence-com +fukuchiyama-shaken-com +freelife100-com +independent2-xsrvjp +chibadecon-com +nfo-bridge-xsrvjp +carelearning-jp +ys-office-xsrvjp +meguminosono-net +promotion-writer-biz +lifecore-cts-com +advanbill-com +takamiyaki-jp +eaglebowl-jp +sie-ric-com +tabi-17-info +deceuninck-thyssenpolymer-com +test-xinfo539-xserver-com +metaltex-jp +double-in-com +testbsoy-info +t-marunouchicon-com +zaidan-zenyokukyo-com +kyoka-biz +m-nakamura-biz +testsp1203334243-com +luggage-mania-com +whynotme-xsrvjp +oh-bento-com +lipostore-xsrvjp +becasse-jp +1create-es-com +clsc-biz +kaguray-com +testdomainx327-com +guitarland-hagoromo-com +josei-kigyou-info +bwest-net +cyrano-studio-com +xn--pcksd1bza2ae0c0qsen902bcxvc-net +cne +ocatcon-com +honeycomb-tani-com +browniedesign-xsrvjp +apps-f-net +maruxen-jp +takarakujimeteor-com +go-youtatsu-com +lc-takatori-com +afiri-navi-com +kateru-jp +hzm-jp +netpost-biz +a-itc-xsrvjp +tetsuro-matsumoto-com +kenpo-cojp +nijino65-info +kandadecon-com +powerstones-jpncom +homecare-net-it-com +sanom-xsrvjp +minami-skh-com +near-sendai-com +xsample74-xsrvjp +xn--zck9awe6dr30vedfmxiwrkn2c-jp +ikecon-xsrvjp +liberators-jp +tomochiku-com +si-man-com +shihou-jp-com +yumemorita614-com +cuoluce-com +bunbunshop-net +xinfo543-xsrvjp +boatrace-tokoname-com +japanflower-jp +cyber-intelligence-jp +kawaoto-jp +atrapas-net +noizumi-org +tomihata-dc-com +sunkujira-pj-com +kouei-wellcab-net +hokuroku-com +club-zex-jp +osakanaichiba-net +stylemart-jp +clarity-xsrvjp +leadea-org +fukuoka-shaken-com +atomic-synergy-cojp +unc-xsrvjp +acnekill-jp +arugeki-net +esanyasou-com +iphoen-net +e-kurozu-com +antidote +adcee-jp +evis-jp +tonbi1-xsrvjp +outmatch-jp +nasdebarraca-com +magokoroichi-com +hosokawa-k-net +xsample321-xsrvjp +aisai-home-jp +elitedance-jp +ttcom-jp +echo1456ms-xsrvjp +hoscoco-com +shouhinhikaku-com +shiseikan-biz +server08-xsrvjp +taste-technology-com +morito-xsrvjp +runjapan-net +sdj7-com +iki-shokusai-com +kumacafe-com +irohamai-com +ogreservice-com +theisaoharikyuu-jp +sokuhoyo-xsrvjp +kanankaga-xsrvjp +zappa-st +jdg-toyohashi-com +yeast-cojp +miebbf-com +xn--z8j2b6je2iphpbxa6it546f-com +waterbottlepeople-com +nmas-xsrvjp +multipharma-cojp +denritsu-solar-com +oyaizu-xsrvjp +mantanya-com +pandwitch-ishikiri-jp +clavor-xsrvjp +amihata-com +tonkatsutei-com +studio-kawamura-com +yumepi-com +soft999-xsrvjp +k2s-xsrvjp +feru-g-com +fit-labo-jp +qhm-lab-info +testdomainx328-com +trusty-co-jp +brandingbox-net +testmail-xsrvjp +calintjp-xsrvjp +adachisekiyu-com +okabe-medical-com +moneychild-xsrvjp +office-sugiyama-com +xinfo574-xsrvjp +sozokobo-xsrvjp +h-osaka-jp +kansai55-com +steer-wimax-jp +testxdomain319-com +jbg-ongakuin-com +xn--u9jtfobzbycc5c2d5a7kxky383a900c-biz +fd01-info +jsn-hokkaido-com +itukinosato-com +ikoh-jp +gchaoo-com +schonbu-com +handsome-xsrvjp +jobcrew-jp +2waraji-com +e-hanjyo-com +mayser-jp +y-jig-xsrvjp +mm2850-xsrvjp +diamond-dust-jp +aoi-cosmetic-net +hatsuneya-net +cds-xsrvjp +lc-g-jp +ballanconsulting-net +tanabe12-xsrvjp +kamei-acjp +shinwa-sprt-jp +roots-eshop-com +pendet-com +xinfo719-xsrvjp +ca-room-com +teppouya-com +koba-labo-info +creli-com +hibino-tatami-com +lushlife237-com +ddhouse-xsrvjp +gen-mu +4976-jp +fujibaba-xsrvjp +karadalab-xsrvjp +xn--zck9awe6d872rezhp3y9g1f-jp +abinvestag-com +listforless-sc-com +cgi-kudoshoten-com +bla-cojp +micrya-com +heartful-trust-jp +seibudai-com +rootx-xsrvjp +xsample112-xsrvjp +horitaclub-com +fujii-nouen-cojp +p-ch-info +reframe-jp-com +e053-com +magnolia-coffee-com +mienaidaigaku-com +tosuseitai-com +sakaikunmeidou-com +purple-stitch-net +jap-xsrvjp +crossabi-xsrvjp +osakasouzoku-net +takkensyuninsya-info +sinminato-com +ikujiikumen-com +hayatombo-com +teisuiyu-xsrvjp +la-cle-jp +xsvx1023248-xsrvjp +xn--fdkc8h2a1876bp0k-net +hatomaga-com +fdo3-com +alpa-sys-cojp +reibee-com +syakarikiya-com +kvit-xsrvjp +tomomo-jp +baytradingclothing-com +doris-spanish-com +caleb098-com +golden-angel111-jp +dsplus002-xsrvjp +liucompany-xsrvjp +xn--w8j6ctc930wo9za2qf-com +y-matanity-com +nakayama-shouji-jp +e-nichido-net +seconddreamstage-biz +withyou7-com +planju-cojp +a-itc-info +meimon-edu-jp +xn--88j6ea1a3393bcta3o5g868o374cpxo-biz +asia-create-jp +techsupport-jp +oirastar-com +satoyama-land-com +ddwh-xsrvjp +love-phantom-xsrvjp +iteya-office-com +mollymaidjapan-cojp +fujiyoshiya-xsrvjp +about-a-stitch-com +casa-bebel-com +acehive-cojp +afilife-com +xinfo759-xsrvjp +aijinkai-xsrvjp +shimizu-ya-jp +xsample160-xsrvjp +kanemitsu-gaku-com +ikkyu-me +junk-buyer-com +goldcard-web-com +tkms3256-xsrvjp +ozkuni-com +xinfo511-xsrvjp +netjapan-xsrvjp +seven-to +xsample143-xsrvjp +ankhcloud-com +kisoku2784-com +jyuushou-com +itabashi-shaken-com +jiw-fc-jp +suzuran21-com +ad-income-com +sosakujo-xsrvjp +hypnoroom-cielbleu-com +lineishikawa-com +dailyrootsfinder-com +nposalvage-com +shinjukucon-com +jmb-orjp +gaogofing-info +uesaka +o-yoga-jp +kashuestyle-xsrvjp +nikorinoie-com +mugendou-xsrvjp +dr-support-jp +happy-cre-com +tech-web-info +kabu-sokuhou-com +xm-net-com +komachi-akita-com +linadream-net +kabuatoz-xsrvjp +bit-blog-jp +dodorufin-xsrvjp +honki-mode-com +us-vocal-biz +neo-hair-com +emurashika-jp +alive-marketing-com +hubcafe-jp +dbm1 +royz-fc-com +rom4-xsrvjp +kikanshi-web-xsrvjp +kishinjyuku-com +maido-ari-xsrvjp +ariakesangyo-xsrvjp +einstein-net-cojp +xn--cckcdp5nyc8g2745a3y4a-biz +rainbowwin-net +test-xinfo759-xserver-com +katakome-com +at-breeder-net +coursfrancaisparinternet-com +guchikiki-biz +2han10-net +you-yu-com +bono-table-cojp +xn--dckix0be3bww9s3erh-net +eyecen-xsrvjp +ch01 +testsp1208271-com +hm99qq-xsrvjp +gambarou-com +e-mikan-xsrvjp +us-vocal-com +bows1989-xsrvjp +noto-p-com +honeytokyo-azukaritai-com +xsample73-xsrvjp +quick-worker-xsrvjp +testsp1208276-com +hatalab-org +yoshiki201-com +kototama-himehiko-com +kumano-jinjya-com +nakamedecon-com +yoridoko-net +breeze-xsrvjp +xinfo542-xsrvjp +zentokyo-orjp +fukuta-shoji-com +kyouzon-xsrvjp +m-ins-cojp +robotkiyosaki-com +storage-jp-com +kanoya-in +miserve-xsrvjp +freeillustclub-net +free-style24-com +core-japan-net +yamamoto-gyosei-com +kvit-cojp +otsu-shaken-com +sun-apricot-com +photofixx-com +equal-825-com +shiomishika-jp +dips-a-jp +asanet-xsrvjp +hiraknet-com +axis-training-jp +kankyo-mirai-com +vietnamfes-jp +xinfo710-xsrvjp +xn--t8j0a6ivbyo0d2h2g2785a340f-jp +happychance-xsrvjp +imo-syaken-com +futo18-com +wakuwakudayori-com +gakki-kaitorihonpo-com +rainbow-br-com +mypre01-xsrvjp +jiten8-net +jdatabank-com +fukutomi-yutaka-com +sho-ichinose-info +thadogpound-net +o2cloud-cojp +kobe-arima-jp +movie-monroe-jp +xinfo168a-xsrvjp +belpon-jp +yabagawa-com +okayamacon-com +orion-dispatch-com +luvisto-net +s-e-r-jp +color-shikaku-com +tokai2x4-com +sikakustyle-net +yoruan-xsrvjp +roidshop-biz +tanshinbox-com +palace-iwaya-jp +atn38-net +puku89-com +unique-sunaba-com +koutsujiko110ban-com +2012parallelworldexpress-com +kanbanmitumori-navi-com +zala-tribune-com +h-colors-com +yeti-net-com +centlaw-xsrvjp +test-xinfo729-xserver-com +guchi-talk-com +fpnomori-com +body-shaving-com +kazuok66-xsrvjp +navit-j-net +ki-ketsu-sui-com +xn--eckd5a1es53u4s4bnvb-com +ku-kan-jp +6sasi-com +testdomainx339-com +test-xinfo739-xserver-com +iikamo-me +r09-jp +nanela-com +syu0128-com +mayuzo-com +c-throb-cojp +w-angelica-com +toeic-technic-info +xinfo573-xsrvjp +webkohbo20-net +mobile-pc-jp +grandeborsa-com +nrj-acjp +cent-law-com +ahb-rs-jp +fussa-net +akb48ob-com +kenkouni-xsrvjp +murphy-xsrvjp +ilt2-xsrvjp +shoukichi-staging-org +greenerfaqs-com +whitestar7-com +sumabu-com +event-kyuden-xsrvjp +skao-info +citabria-xsrvjp +xinfo120a-xsrvjp +smakl-net +toshiyuki-biz +oshie-k-com +test-xinfo720-xserver-com +bcsv1-xsrvjp +haklak-com +shiga-kaigotaiken-jp +xinfo718-xsrvjp +supertaikyu-com +tokuringi-com +crystallize-jp +minami-cl-com +itiman-net +yamatoyo-cojp +elimsmile-jp +kansya888-com +pier-s-com +flaviahair-com +originalbook-xsrvjp +kolocle-com +donald1 +mogmog-xsrvjp +ktskk-com +xn--vcsu3i28mez0b-biz +deliver-xsrvjp +alpep-com +netbus-jp +parts-depot-jp +kotoba-gift-com +ikeconnight-com +rameatlantique-com +niigata-tmc-com +test-xinfo710-xserver-com +server9-xsrvjp +xsample111-xsrvjp +cyberdesign-cojp +xsvx1009156-xsrvjp +inaba-koji-com +sweets-orjp +aircon-clean-com +nccard-xsrvjp +mythoswork-biz +hana-mido-com +wedding-maria-com +satohya-com +willdrive-net +soudan1-com +mizu-fiore-xsrvjp +x-side-net +sumi-orthod-com +test-xinfo738-xserver-com +araimotors-com +jars-jp +minani2468-xsrvjp +test-xinfo728-xserver-com +gioncon-com +a-site-me +rom10-xsrvjp +test-xinfo718-xserver-com +saiyou55-com +mignonette-jp +whitecrow +a-lash-com +ahsma3662-xsrvjp +sal-nejp +kashiwa-vocal-info +test-xinfo708-xserver-com +monifihi-xsrvjp +baseballshop-legends-com +okazaky-xsrvjp +titti-orjp +dreambuild2011-com +hayspec-com +hyse-biz +alta-marea-jp +hotelkensaku-info +netshop-studio-com +hikage-xsrvjp +soulofgold-xsrvjp +adnacom-jp +jinyublog-net +mizukakifu-com +idolrevolution-jp +decome2012-biz +sunace-biz +sunburst-n-cojp +xn--zck9awe6d5989b6fc-jp +kotox100-com +hello-mystyle-com +eyecen-com +a-sapo-jp +sabotenya-com +sakihi-xsrvjp +sugenosato-com +pc8137 +m1182-com +kirara-shinyuri-com +kagari-jewelry-com +skjmd-com +ariege-patrick-immo-com +mokuteki-net +nextone-srv-xsrvjp +h-tb-biz +xinfo510-xsrvjp +13abundance-com +t-aliveestate-cojp +onesmileoffice-com +xsample142-xsrvjp +funteamhs-cojp +shepard-navi-com +tochi-bus-com +supamankazu-com +tv-valve-cojp +kikuimo-sato-com +img-jp +yamaiku-com +matsuda-siko-com +kitpas-com +chirashi-print-net +svnl-info +xn--hckp3ac2l-jp +stylejam-xsrvjp +solvic-net +adomi-xsrvjp +uchihamono-xsrvjp +meichu-biz +tomson1-xsrvjp +ikebukuro-con-com +testxserverspdomain121030-com +lovelyjazzchan-com +nijinotane-xsrvjp +dogs-fvh-net +tennoujicon-com +guruguru-gourmet-com +test-xinfo558-xserver-com +buschool-xsrvjp +ochaukeya-com +e-plant-me +ncare-m-ch-jp +baistone-jp +kumamiya-com +kukumu-g-com +test-xinfo548-xserver-com +luna-shine-net +oirasekiyoraka-com +simple-work-net +test-xinfo538-xserver-com +thedodo-jp +misato-mariage-com +fruit-garlic-com +test-xinfo528-xserver-com +dancestudio123-com +yunomi-us +kazumis-biz +xinfo339-xsrvjp +honzo-mall-aichijp +mk0088 +dai-anshin-com +awesomediet-net +xn--asp-ei4btb8qwj6169acyva-com +free-way-xsrvjp +aandgweb-cojp +exformation-jp +shimada-clinic-jp +test-xinfo508-xserver-com +yagishika-jp +flm13-jp +5oku-com +nanela-net +nlight-xsrvjp +xsample72-xsrvjp +karadacure-com +ipi-radio-info +more-com-xsrvjp +project99-xsrvjp +slowliving-conz +hanryu-eikoh-com +obubutea-com +test-xsample333-xserver-com +inatani-shika-com +nittokyo-xsrvjp +kondo-shoten-xsrvjp +xsample90-xsrvjp +xinfo541-xsrvjp +benefix-cojp +trendtrend-info +wonder-poems-com +rinrin3-jp +takumi-qol-com +st0p-net +nakai-iin-net +a-cue-com +gardensite-xsrvjp +kenu-xsrvjp +beautycosmer-com +sakaedecon-com +masa-keiba-com +kitasenjudecon-com +biztask-net +test-xsample303-xserver-com +r-creatives-com +4nen-com +lensman-jp +rumi-kg-com +hokenbooks-com +xn--7-kgu4es24uf5q-jp +pentalab-cojp +fleurshair-com +zero-free-xsrvjp +omotesandou-net +rooky-jp +f-cre-com +smirnoff77-xsrvjp +tamai-chuo-com +yumeflower-com +bon-marriage-com +regalsense-com +studio-ibis-com +carhonpo-com +dental-com +xn--u9j360h32opa140d-com +hiroshimade-com +sedai50-net +s-publish-com +sprachschule-xsrvjp +human-respect-cojp +bookend-xsrvjp +lbm-xsrvjp +gacchiri-jp +rakunadiet-com +genkigoo-com +centrair-bluechip-com +chayamachicon-com +cosmicengine-biz +zenta-tv +kazart-jp +xinfo595-xsrvjp +2102-jp +you-yu-xsrvjp +ube-shaken-com +sasaki2228-xsrvjp +test-xinfo358-xserver-com +celtislab-net +daigo555-info +xn--t8j0a6ivbyo0d2h2g-jp +herbkenkyujo-spur-jp +mikami-masa-jp +kusakaya-jp +test-xinfo348-xserver-com +autoflex2000-com +twds-jp +tamabayashi-cojp +gokujyouwagyu-com +digital-sensation-jp +iijimakazunao-com +wano-tv +taka-afiri-xsrvjp +hanyuxuexiban-com +familyhall-kounandai-com +ichikawa-shaken-com +toriikengo-com +inpres-jp +homepage-desite-info +xinfo572-xsrvjp +longtail-seo-jp +happystyle555-com +sorasys-com +tennouji-yasu-com +asobigokoro-info +stemflag-com +haruri-jp +tyube-yokkaichi-syaken-com +xinfo761te-xsrvjp +4-home-teeth-whitening-com +yamazakishika-jp +teen-affair-com +saiminjuku-com +isc-kansai-com +hotel-yagi-xsrvjp +kameoka-trust-shaken-com +yuanchuang-hk-net +aginc50-com +saitama-tokiwa-shaken-com +fujieda-info +1plus1-cojp +ec-aichitriennale-info +krk6868-com +someiyoshino-com +xinfo717-xsrvjp +xinfo332-xsrvjp +mb-mori-com +hanaya-3a-com +rasc-xsrvjp +muj-orjp +tc-legal-net +you-creative-com +ontic-to +spa-thai-com +olimpiade +ticketsoko-xsrvjp +carecreates-com +kazu-alohi-apopo-info +ayatocom-net +toyoda-eri-com +senmon-web-com +m-shien-com +kaitori-kan-com +sancha-de-con-com +ktimz-xsrvjp +custom-fk-xsrvjp +macco-cojp +kijpn-com +inotaka-xsrvjp +matthew-mcconaughey-org +zy-x-com +magic-sense-com +lader-xsrvjp +pointkeyword-com +giocondos-com +omise-org +xsample109-xsrvjp +akazukin-cojp +bobble-asia +takasumi-com +evolution365-net +kobe-syaken-com +multi-pure-info +shinagawadecon-com +theliksun-com +bistorot-le-reve-com +soba-kochi-com +reibee-xsrvjp +cosmo-s-net +sekiryu-xsrvjp +umenomochi-com +sentaku-llc-cojp +tetsuzan-xsrvjp +neo-ah-com +d-stage-xsrvjp +cd-ok-com +xn--vck5d6ae0cyc7801bnpyb-jp +med-takaoka-jp +yamako-f-com +yamato-h-com +allsedori-com +xinfo123a-xsrvjp +niwaya-info +ragress-com +tokushima-syaken-com +gs-yumekoubou-com +hiyoshi-net-com +pacg-jp +musashinogolf-com +taguchi-tax-jp +lifeup-nejp +jnome-jp +onamae-taiso-com +xsample310-xsrvjp +furumoripopopiano-com +yanaibrands-xsrvjp +chintai-jpn-com +otakaland-com +b-life-id-com +planclair-xsrvjp +ehon-app-com +testdomainx321-com +i-loceo-com +seo-tail-com +fellows-japan-com +monodzukurikidsfund-org +xn--u9j5h1btf1e613xpbuzkc252m-jp +tanaka-iin-jp +palais-riviere-com +himawari-day-com +testdomainx326-com +eport1984-xsrvjp +xinfo508-xsrvjp +artbird-jp +canalsalon-com +mahounoikuji-jp +trno-biz +karatsujuku-com +facebook-connection-com +hibimarche-net +xsample141-xsrvjp +ymsthrs-com +super-raku-net +testdomainx332-com +zipaddr-com +deaikei-max-net +oita-golf-com +msr-pro-com +tetta-jp +la-lu-ce-com +testdomainx337-com +sunadesignlab-net +retoru-com +luxury-photo-jp +yoichiro01-net +kyokuto-h-com +yourmenucreations-com +ph-sister-com +tokyo-sk-com +isitaka-mokko-com +xn--ppc-773bzqgah5a30akezjj654f-com +hub-create-com +n-chemitech-com +titania-cojp +ambient-nejp +thisistrend-com +sayaka-dp-com +b-map-jpncom +cappee-net +mobile-japan-ok-com +kyoto-alice-com +ito-coffee-com +rjen-cojp +tms-first-xsrvjp +waocon-test-com +hiver-shop-net +showtimes-xsrvjp +refresh-kaatsu-com +penplustown-com +jyutaku-k-cojp +rgh-jp +web-kaisha-com +510office-jp +kousyu-fukuoka-com +adtec-xsrvjp +levpiece-jp +fuwairu-com +news9plus2-com +1-3-cojp +direct321-xsrvjp +tsuge-seitaiin-net +heroicstory-biz +ninbai-nejp +upat-jp +idessey-com +testsp1208445524-com +sasazukadecon-com +kenko-ya-xsrvjp +753st-net +aspix-archives-com +xsample71-xsrvjp +xsv08-xsrvjp +kawagoecon-com +kan-ki-jp +makitt-biz +shusaku-sasada-com +naisou-mitumori-com +ivy616-com +actginza-cojp +kazelu-jp +clementia-inc-com +thedykeenies-com +sports1230-com +ncef-manel-com +onomichi-bbs-com +xinfo539-xsrvjp +esukei-xsrvjp +tidalism-com +brandbank-cojp +tomsonking-com +test-xinfo560-xserver-com +kanamonokouhou-com +lamb-cloud-com +autospeed-jp +bisamobi-net +uruoi-gel-com +oita-mbbl-jp +fd-works-com +hifu-mi-com +shinryo-info +1writing-com +mishimashika-com +syoknin-com +azzip-azzip-com +naturalcurlist-org +test-xsample304-xserver-com +dreamwarp-jp +overagain1059-com +mail-pal-com +tensai21-com +tomakomai-shaken-com +tsubamesanjo-jc-orjp +musashi-soil-com +test-xinfo595-xserver-com +kanto3-com +tottoko-net +akirazx-xsrvjp +xsample317-xsrvjp +k-flat-net +incenx-xsrvjp +sabu-official-com +rena-nounen-net +misuno-net +taiten-hoikuen-com +paint-fukuoka-com +aimistone-com +osamukubota-net +test-xinfo550-xserver-com +vient-net-xsrvjp +beta-beauty-xsrvjp +ag-ent-net +vvfm-net +e-conomyhotels-jp +hiroshimakoigokoro-jp +values-break-com +chiangmailanna-spa-com +hamamatsucon-com +xsample216-xsrvjp +okinawanoni-juice-jp +fukuoka-ot-com +gkconsul-xsrvjp +pachinkoslot-biz +f-kimono-com +kanjyou-com +design-cube-jp +capelli-di-arte-com +heart-oasis-com +iidabashi-kagura-com +polepoleti-me +a-rec-com +fbapple-info +the-greatful-life-com +cafebar-cross-com +egao-c-a-com +xn--elq250e1mhg47a-jp +gengochoukakushiken-com +netbusiness-jpnet +info-camp-xsrvjp +handsome-web-net +photo-prime-com +aishin-housing-com +ontheroad-jp +forcearound-com +kazuno-com +daimyou-net +m-gene-com +epatrol-info +testxdomain302-com +yumimega-com +pc11112 +pc11111 +grooverider-net +af-binary-biz +wg995-com +aoba-fudousan-net +geek-boys-com +info-utu-aid-com +xsample213-xsrvjp +fresh-terrace-com +yuyu104-com +aoivvu-info +testxdomain313-com +milumoda-net +careebyte-com +fk-kenchiku-com +testxdomain318-com +pcgiken-xsrvjp +desite-jp +xsvx1022093-xsrvjp +ouensha-oita-net +manmaru0701-xsrvjp +xinfo716-xsrvjp +tanimotoyoshiaki-jp +test-xinfo530-xserver-com +kitchenfactory-ac-com +ulvac-es-cojp +ecocker-jp +mpgd-net +kcs-fc-info +aichi-roujin-jp +burningrain-net +aprs-jp +musics-xsrvjp +doyu-ichihara-jp +golfbar-star-jp +sbproject-xsrvjp +gokoushokuhin-com +pc103-com +mhplan1-biz +cyber-ec-xsrvjp +xn--gmqq3i52e2nhgrdevx-biz +piropi55-xsrvjp +highest999air-info +isuppo-xsrvjp +niigata-shaken-com +sylvieann-com +osakabeshinkyuin-com +actors-u-com +lli-insurance-com +xn--ickhj5b7d6fua4f-com +forty-one-biz +shinshu-comprehensive-jp +lototrial-jp +daikitkgs-com +sinzan-cojp +jiko-pr-jp +nature-v-com +otogr-net +kmasato-com +columbus-in-phil-org +sswd-jp +a-pu-pu-com +xsample108-xsrvjp +lifeart-nyan-com +akasakadecon-com +satomisou-net +test-xinfo519-xserver-com +gloseq5-info +fairy-kiss-jp +simon01-com +career-searcher-info +ogataengei-com +tugunari55-xsrvjp +rickun0401-com +asentia-cojp +botaniquelife-com +axceed-tax-com +seigyobako-com +yuya00yuya-com +sekatu-com +sakaizyukuanimationclubmemberonly-com +vaomusic-xsrvjp +kuranomachikado-com +anshin-jutaku-com +mdex-xsrvjp +wings-shop-com +syadan-net +officels-xsrvjp +united-smiles-com +costech-xsrvjp +housewand7stone-com +inter56-com +biyoujyuku-info +ces-ent-com +fukushikikai-com +burse +favori-tokyo-com +hizanaoshikata-com +test-xinfo509-xserver-com +mothercloud-biz +ryou1212-com +a3s3f23rsadfasf-com +kiwanda-jp +kando-honda-jp +pbear-jp +xinfo720-xsrvjp +higuma-xsrvjp +riquan-cojp +xn--88j6ea1a3393bcta1uk721ac9l-asia +valueyume-com +gimix-tv +sunao-corp-com +n-hoikukai-jp +suzuki-jun-xsrvjp +no1eigo-biz +tazima-net +out-sourcing-cojp +bookbooth-jp +cssnite-sapporo-jp +blockfun-net +cia-j-com +cloveregg-com +mt-japan2-xsrvjp +koishi0105-com +awaji-mandai-jp +neko22-com +ah-tokyo-com +garden-one-net +michimoto-cl-com +xinfo507-xsrvjp +pcnet-nejp +smsfacil-net +minshuku-toshiya-com +brainphantasm-com +katteni-kanko-com +satoshi001-com +xsample139-xsrvjp +fukamayu-com +m-hico-com +sukegawa-office-com +tenjinsita-com +interwindow-cojp +aplan-cojp +hikakuichiran-com +xn--av-693a1dpa20aaa2gsa2gd1bd4a8bzooolevh5230egdpb-com +nsbz-net +kawai-orjp +pref-shizuokajp +noble-trust-com +nfa-g-com +help-cashing-com +united-studio-com +nogiya-com +shijyou-karasumacon-com +peeeeeee7-com +katokoyodo-com +bkk-bz +kikaku-keiei-com +tu-han-shop-com +gasenenews-citygas-com +ubijin-com +jin-cycle-xsrvjp +xinfo126a-xsrvjp +zenkaikyou-xsrvjp +satoshi001-xsrvjp +goenno-wa-com +takatori38-xsrvjp +ganbanchip-com +fujiishinji-com +gicland-cojp +xsample51-xsrvjp +fbms22-xsrvjp +webya-3-com +4mr-method-com +kogure21-cojp +kushida-koumuten-jp +shibuyacon-com +web-design-office-net +odawaracon-com +leffervescence-jp +camp-map-com +johosyozai-biz +zone-portal-info +testdomainx340-com +xsample57-xsrvjp +xsample70-xsrvjp +equipment-com +yushoya-com +layer-s-com +xinfo102a-xsrvjp +asuka-fujiwara-jp +tsujikawa-net +c3d-xsrvjp +okonomi-sugino-com +beniiche-jp +kokushi-musou-com +twchain-com +japinglish-xsrvjp +nyankox2-xsrvjp +super-sozai-com +xinfo538-xsrvjp +egao-kondo-com +project-mm-com +illust-bag-com +mothers-inc-com +gluonz-com +kindaikobo-com +akiraclub-xsrvjp +music-ikehara-net +sanwahd-net +game1mart-com +tyube-nisshin-syaken-com +enyuu-ji-com +dougamarketing-com +alcarentacarny-com +test2-xinfo745-xserver-com +next-commu-xsrvjp +inuzakura-xsrvjp +saksrv7-com +kouichi541-com +balboa-trading-com +butugu-net-com +national-st-com +trendprichan-info +com-alpha-com +v-glame-jp +okawari-japan-com +pcmania +tomiget-com +spica-bs-jp +inkyo-nyuudo-xsrvjp +xsample316-xsrvjp +onshinan-xsrvjp +shizenkeitai-tamura-com +ndai-xsrvjp +kyoubashicon-com +social-tour-com +xn--eckm3b6d2a9b3gua9f2dz124ebp0a-jp +shinko-denki-xsrvjp +test-xsample314-xserver-com +ii-anbai-xsrvjp +fischer-golf-com +gion-yasu-com +english-box-com +edocon-jp +xn--vekz09jiqk-com +ictsg-net +kaiwajyutu-net +mirumirukogao-com +stdesign-jp +friends-ah-net +higashiumeda-yasu-com +dolphin50-com +ktmn-biz +tkdore-xsrvjp +be-flat-xsrvjp +team-cellacise-com +jnapcdc-com +i-photokg-com +zen-clean-com +xn--4gq539c5gsb3a-com +zzz00zzz-com +ukiuki-shopping-biz +rokkosan-net +s-click-net +vachao-com +reo825-net +nobletrustfb-xsrvjp +obentoutei-xsrvjp +jeo-stylist-com +38hawaii-com +tanimoto-ironworks-com +xn--n8j9cqo2a0nk59oghe-com +poco-a-poco-tokyo-com +giang +jeb-bz +1010teisuiyu-net +pluss295-xsrvjp +freepiero-xsrvjp +joemaguiredesign-com +kaoru-nioi-com +japanflower-xsrvjp +kkkanri-xsrvjp +appliyakun-xsrvjp +hunabashi-rentacar-com +xsample212-xsrvjp +cambodia-today-com +owl-office-com +18teen-jp +e-fugu-com +kitakyucon-jp +denkiya-me +maemukikotoba-net +matsudatetakayuki-com +test-xsample34-xserver-com +srv-350-net +daisyo-biz +hosoda-nousan-cojp +adusamdodo-info +kodomokai-jp +mito-con-com +bamboo-i-cojp +kagamix-xsrvjp +yumiz-jp +aspmanblog-info +webdesign-jp-net +rwive-com +allthezeal-com +xinfo715-xsrvjp +20pips-com +setagaya-jpnet +ianextproject-net +katazome-style-com +ohanashi-rosecafe-com +barbie-c-com +universal-joy-net +tcp-makeup-jp +sada-office-jp +mizutama-tv +alohabeststyle-com +ame-ha-biz +cbhomefield-com +l-communication-net +asaicrop-com +1lunch-marketing-com +angie-xsrvjp +majcalmami-com +dekogang-xsrvjp +toho-premium2012-jp +xinfo735-xsrvjp +deko-gang-com +heart-cs-com +xinfo349-xsrvjp +tsuiteru-cojp +umedacon-com +012-vc +pets-e-com +palca-xsrvjp +mafu29-com +airsplan-xsrvjp +xsample107-xsrvjp +clover-realestate-com +sonic-labo-com +lesson5-com +micreate-jp +chokubai-com +moritagakuen-edjp +f-kizuna-com +xn--88j6ea1a0780bctddtas67ckx5cbp2b8xe-asia +sendai-con-com +solarnavi-net +ura-keizai-com +vivloom-com +hama-shou-cojp +hanacel-com +ezomac-com +portal5 +muura50-com +shinbashidecon-com +ai523-com +yuta77-com +ynny-xsrvjp +rinohome-com +hale29014129-xsrvjp +xn--pckwb0cua2ei-jp +joyplaza-cojp +karadano-xsrvjp +designroom-xsrvjp +tukasayou-com +kasahara-shika-com +nail-shikaku-com +cont-p-com +japanphoenix-xsrvjp +ohnojyousousai-cojp +csss-jp +gallery-dan-com +sho-info-com +test-xinfo766-xserver-com +locreo-jp +waisu-net +iyashi-no-ma-com +sym-sym-net +tynsystem-xsrvjp +test-xinfo756-xserver-com +shukutoku-yoyaku-com +kizuna0615-xsrvjp +sato-club-com +romandeal-com +acai-tripleberry-com +chain12step-xsrvjp +isilon2 +delico +kashiwa-kaburaki-shaken-com +test-xinfo746-xserver-com +xn--b9j2a1gzmkb4n-com +cosmo-group-info +yumetrain-jp +neko-nikuq-net +mitsuandjigens-com +nh-purelyshop-com +mamorigami-com +test-xinfo736-xserver-com +isilon1 +shintaroubiz-com +ibako28-xsrvjp +saka-design-com +miu-flower-com +tamagomura-com +shioyama-info +drsmart-biz +kitaya-dental-clinic-com +blooming-dear-com +b-life-mail-com +sokuyokudou-com +inoue-orjp +mmsharon-xsrvjp +kouchanservice-jp +leapair-net +hamamatsu-con-com +sunnan-cojp +business88-biz +test-xinfo706-xserver-com +xinfo506-xsrvjp +humans-y-com +xsample138-xsrvjp +runimagine-com +level3-xsrvjp +ohtsubo-clinic-jp +eskobe-com +umeda-yasu-com +xn--dckiy8ad8fl0jub0bzhub-com +k-tominaga-net +asp-rsv-jp +platinacon-com +fmharo-cojp +gomi-calendar-info +lapps-xsrvjp +sportscosme-com +thaicom-cojp +herbnoaruseikatsu-com +tti-i-cojp +estem-group-com +4x4-cojp +app-producer-net +clearstone-jp +ogatanouen-xsrvjp +maca-xsrvjp +tectron-jp +nishii-com +web-bz-com +web2tokyo-net +aseanfes-jp +axiomaticmagazine-com +snd-xsrvjp +sfy-cojp +net-oyaji-com +hiro3237-xsrvjp +ep-coat-com +acnalumni-com +ganriki-jp-net +keitai-custom-com +auction-daiko-net +hamamatsu-bankin-com +ag-tax-orjp +yumegoal-com +test-xinfo586-xserver-com +matsukun0-com +heavenshemp-com +usuge-chiryou-info +s-suppli-cojp +sym-q-com +dekitate-site-com +milkywayproject-com +shimizu-esperanza-biz +tk-ikebukuro-xsrvjp +seo358-com +mariko-cook-com +xsample68-xsrvjp +test-xinfo566-xserver-com +doll-kaitai-com +suisolife-com +tekunaka-com +oride-jp +xn--eckp2gt04l48ehp0a8v3ams3b-com +kashimako-com +test-xinfo556-xserver-com +yoshikawa-wedding-com +facebook01-xsrvjp +learning-playce-com +test-xinfo546-xserver-com +xinfo537-xsrvjp +samec-ct-com +cultivate-xsrvjp +miraira-affiliate-com +infopremier-jp +re-seul-com +xsample169-xsrvjp +seo-ex-com +gamegekiyasu-com +eitai-org +test-xinfo536-xserver-com +hi-as-eco-jp +super-propolis-com +admac-jp +xn--edktc2a4827cket59b-com +strider-jp +mokkinsedori-com +xn--p8judqlpc9fsf-jp +test-xinfo526-xserver-com +test-xsample227-xserver-com +peicolor-jp +naisyhoku-biz +kihodo-jp +sg119 +organic-tshirts-net +sakaicon-com +the-hoken-com +test-xinfo516-xserver-com +love-project-net +uedakaihatsu-com +yutaka-style-com +xn--tdkl0c-com +okusurifujin-com +yamanisi-info +infodesign-jpn-net +test-xinfo506-xserver-com +usami001-xsrvjp +tsuchiura-info +nattoku-naitei-jp +tabiji-org +hiratacy-com +anblend-jp +xsample315-xsrvjp +uchiyama-gg-cojp +js-ps-orjp +test-xsample221-xserver-com +e-biss-jp +rep81-com +media-producer-jp +syoueibms-com +football-fukuyama-com +charites-nail-com +pronto-xsrvjp +blossom-hotel-com +xinfo129a-xsrvjp +xn--pckwbo6k815nvjfp43bt81d-jp +test-xinfo360-xserver-com +unamu-ch-xsrvjp +yamatoya-cleaning-com +smile-com-net +fishtone-com +kscompany-xsrvjp +takanorik66-xsrvjp +ashula-king-com +fenikkusu-com +old-domain-sale-com +test-xsample301-xserver-com +inovation-xsrvjp +test-xinfo356-xserver-com +penmaru3cg-com +e-takara-com +nlight-info +blend-blog-com +horaiya-jp +matkaa-com +aki-takahashi-net +morisige-hotel-jp +zest-camera-info +kyu-be-info +c-road-jp +xsample99-xsrvjp +dreamrice-jp +xinfo105a-xsrvjp +unicco-kyotojp +pisces678-com +sg118 +fast-lifestyle-info +koyasan-xsrvjp +gallery-mura-com +jscoach-com +yositaro-xsrvjp +omphalos-xsrvjp +test-xinfo350-xserver-com +freedomshigoto-com +builderyoshi-xsrvjp +hunabashi-shaken-com +dilshad +masahome-cojp +xsample211-xsrvjp +web2sendai-com +my-esu-com +kasyu-jp +designex-jp +catacrico-jp +1tokkun-com +s-ogikubo-net +fudekoubou-com +cocomo-interior-com +meirin-seni-cojp +cadio-biz +toraikatz-xsrvjp +sunlight-cleaning-jp +sekkotsu-in +japan-italia-com +toitoitoi-net +raggachina-com +d-strage-jp +toresenkeiba-com +kyoumihonten-cojp +singi-biz +nakagawa39-com +bizm-ag +hachiouji-shaken-com +test-xinfo346-xserver-com +citabria-cojp +get1000man-com +xinfo714-xsrvjp +copasystem-xsrvjp +optimization-service-com +kichijoji-con-com +ryom-design-com +moto888-net +smaphosticker-com +shop-degree-jp +matsmile-jp +bellness-com +ana-pr-jp +nakano-suginami-syaken-com +eikaiwakoushi-com +tycoon-com-com +studiog-xsrvjp +backsnet-com +xsample106-xsrvjp +ami-amie-jp +maruka-uchiyama-com +kindlenotukaikata-com +wine-ec +asmec-cojp +abikobar-com +mclamego-com +uni-axis-com +unking +hiroisatoru-com +kekkonsite-biz +tomybikepark-com +aomori-wats-com +sp-c-org +xn--n8j4mybtf1e613xwn2bc64b-jp +horie-yasu-com +tenjinplace-com +h-s-xsrvjp +lyrical-works-com +edogawa-town-com +lifejam-jp +car-uni-com +infozapper-biz +neo-hair-jp +hakkou-sushi-jp +kagoshima-syaken-com +niwadani-cojp +dash-man-jp +sinn9-com +minoru7227-xsrvjp +kudoken4-xsrvjp +shop-orb-com +money-sense-net +developer-xsrvjp +takariha02dive-com +degu-factory-com +haima-tonosato-com +7082abc-com +cool-rock-com +storageroom-jp +xn--tckybd3guczb1829b7ghx6trhlupd-jp +otera-net +stampp2-xsrvjp +homepark-xsrvjp +ipl-soft-xsrvjp +u66-info +rom-test-net +sea-design-cojp +zin-blue025z-com +yume-affiliate-com +info-z-net +twitterjp-net +xinfo359-xsrvjp +net-sidejob-com +xn--ccktea4bylb8496czbtysx-com +xn--veky30o2gq-com +hotel-kiyosato-com +tontonton-jp +fumisedori-com +mr-pages-com +haggis-on-whey-com +momokuri-xsrvjp +gobousei-info +7memo-com +xn--eckyb5bg3k-com +four-d-org +chunichi-kodomojuku-com +happyplan-net-com +alicestone-xsrvjp +changethemindandworld-com +silky-closet-com +butsujuji-xsrvjp +rb-apps-com +atozrentacar-com +fp-partners-com +fukuzawa-xsrvjp +webpro-xsrvjp +test-xinfo-xserver342-com +immm21-com +gourmet-circus-jp +kumiyama-shaken-com +qqpm5cb89-xsrvjp +narashino-jp +shibajimusho-orjp +xinfo505-xsrvjp +super-arts-com +moebiarc-com +ace-pro-air-jp +iwaken-studio-com +tuchi-con-com +xsample137-xsrvjp +yokoyan201-com +homepark-cojp +plus-a-me +kounotori-honpo-jp +steelkogyo-com +matsudocon-com +ijinjin-com +tskym-jp +kyouikukoyou-org +grapparetto-xsrvjp +sylph-biz +moriokw-com +doroawa-sekken-info +uenode-con-com +cocoro-esthetics-com +apple-pie2-com +pclibs-com +asahi-pack-com +zumi-xsrvjp +aquacube-xsrvjp +augking-lab-info +clsc-jp +yuri-pharma-com +otoku-tsuhan-com +aplaninc-xsrvjp +aaronbrowne-jp +hishi-ki-cojp +xn--pck2bza7489c4ld-com +fujibus-cojp +affiliate-school-net +denden0375-com +painting-mouse-com +lc-takatori-xsrvjp +tajima-takamiya-com +musubinoayumi-com +lahal-net +daim-global-com +e-sakaki-com +ultraseo-net +mfun-jp +unhwa-mobi +yuu-shi-kai-com +celebstyle-xsrvjp +toyoda-wedding-com +pages-xsrvjp +musashino-hp-jp +machiko-biz +h-water-net +e-gokai-jp +xsample67-xsrvjp +hkwj-cojp +med-plan-jp +furano-areaguide-com +sweets-sakai-com +zombie-star-com +toretate-net +reveassocie-com +e-bene-com +yod-on-com +morito-k-cojp +merveille-ushimado-com +vanah-kawagoe-com +meichu-jp +body-tc-info +iaso-supple-com +otaniah-com +kokusan-takegami-com +xinfo536-xsrvjp +cdr-jp +xn--vck8crcy307btiva-jpnet +socpartners-xsrvjp +seo-clare-com +test-xinfo767-xserver-com +xsample168-xsrvjp +vakcom-xsrvjp +arata0613-com +okashinosakai-xsrvjp +utamaro-denki-com +taikokk-com +xdock-net +dbmagic-biz +advance-chirouka-com +onishi-amimono-com +p-c-rescue-com +daikanyama-con-com +riddlepuzzle-com +news-share-xsrvjp +apt-japan-com +kitashinchi-yasu-com +kankoku-keitai-com +xn--gps-pg2j70g-net +nihoneco-org +e-interiorshop-com +otoxxxoto-net +alohi-apopo-biz +xinfo766-xsrvjp +tanba-shaken-com +i-mao-net +jf-aji-net +iphoneer-jp +graphium66-xsrvjp +onejam-biz +maki-nameart-com +toriikengo-xsrvjp +xsample314-xsrvjp +t340-com +message-japan-com +sonicwave-nejp +existence-inc-com +grandeporte-net +kk-union-biz +kobeco-net +gyousename-com +shinseikai-dental-com +reito2274-xsrvjp +doi-chiro-com +aristo-net-cojp +yoshi-affili-com +saisinstar-com +contrax-cojp +webeer-info +logorin-com +christmas-giftcards-com +little-ribbon-com +granmacoltd-com +goemon-the-web-com +taguchikaikei-com +xinfo729-xsrvjp +soleado-t-xsrvjp +suzuki-kobo-com +musee-biz +kagaloli-jp +the10-biz +cui-cojp +you-photo-com +take-c2009-com +oz-ucar-jp +ariakesangyo-cojp +destiny01-xsrvjp +xsample98-xsrvjp +kaisekislot-com +makusora-jp +takanawa-clinic-com +becoca-xsrvjp +stkmwdzm7-com +junkoh-jp +sub-click-xsrvjp +test-willgate-com +ncyell-com +petanque-asia +kudo114-com +test-xinfo592te-xserver-com +yldc-org +0nq-net +advance-ec-jp +kartepost-com +machiori-jp +ion-ginza-com +faxdm-org +xsample210-xsrvjp +xcely-ht-com +drawiz-jp +masu-kazu-com +tekkyo-biz +versa-jp +geihoku-minsyuku-kamioka-com +monifihi-com +aha-online-shop-com +j-sweat-com +afirieitoshu-xsrvjp +social-sendai-jp +xinfo157a-xsrvjp +yamada-ot-xsrvjp +xn--88j2foda3h0b8ny00x2i5adx6d-jp +kensaku-xsrvjp +guccionliner-com +okusamaya-com +kompetis-com +minds-farm-com +led-kumamoto-com +ishi-kura-jp +xn--n8jtcugwh9cqhlg845v6k6d-com +molamo-labs-com +xinfo713-xsrvjp +around-jpn-com +stock-capital-com +trust-rb-com +2580-org +infonich-cojp +tomoyan11-info +xn--gps-rm0e442jhgp-com +black007-biz +k-thp-xsrvjp +tagukaikei-xsrvjp +ondrecords-com +otakeiki-com +tanmo-net +bonne-chance-co +musashikoyamacon-com +xn--kckk7aw5tpb8c-com +opus77-xsrvjp +ootone-reien-com +sakai-manekin-com +kaigyojunbi-com +universalcitycon-com +osaka-roujin-jp +enjoystreet-jp +test-xinfo591te-xserver-com +xn--m-u8ts56nvoza-biz +xn--3dsll-z53dvhlb9bwe97aphtgm016cftxa1m0b-com +cleangreen-nagoya-com +solare-muromi-com +harmonicsdesign-cojp +xsample105-xsrvjp +shop-authentic-com +xn--b9j6am4izjxd2h2gq122d-jp +team-6eco-com +sg117 +xn--qckq9mc4ac-com +maebashicon-net +uni-p-net +esaki-onlineshop-com +webaxel-jp +meigetudou-com +tomkatsy-xsrvjp +k2rinc-jp +xinfo108a-xsrvjp +woof-jp +shirahamafuminori-com +central-medical-cojp +daikaen-mishima-com +xn--vckg5a9g8fj6937cw1bjtsha205u-com +2chinfo-com +europort-cutting-com +kobeya-me +ars-town-com +hold-chance-xsrvjp +bcc-xsrvjp +xn--u9j0c604kneons8a-com +wakaba-f-net +slimtaikei-com +glow-united-com +testdomainx330-com +kanngosi-kyuzinn-com +oyaizu-cojp +yousworld-com +genkuu-jp +cs-delight-cojp +xn--u9j1hsdzb9d9bv308dff9c-net +silk-jubai-com +pcfureaiforest-com +xn--eck6e6b987uy7i-jp +testsp1208272-com +nishinocho-com +siriasu-info +latour-jp +sps-mg-xsrvjp +autm-hama-xsrvjp +kouenjin-xsrvjp +igakubu-guide-com +bouquet-de-bianca-jp +event-kyuden-jp +testsp1208277-com +xsvx1019774-xsrvjp +xinfo744-xsrvjp +xinfo358-xsrvjp +furiahau-com +one-kyoto-jp +opt-01-com +patec-xsrvjp +digital-global-agency-com +s-eiken-com +tosa-kanran-xsrvjp +5june-com +gantarou-com +jiin-xsrvjp +starfield00-biz +pet-hatakeshimeji-com +ayu-aclass-com +marushige-chicken-com +fluentgarden-com +xsample35-xsrvjp +exercise-and-diet-net +storedx-net +ym-advice-com +sg116 +matsugenn-com +iineclub-com +freeman-affiliatekouza-com +okusurishop-com +yumekanaeyo-com +xinfo504-xsrvjp +kimuracooking-net +mcprogram-com +avante-act-cojp +xsample136-xsrvjp +wakuwaku-tsuhan-com +xn--gckg0b0b8evmbbb4044fll9bk5iqk9i-com +tassmania-biz +kana-xsrvjp +tongdee-com +mma-xsrvjp +pha08069-xsrvjp +sanfuroa-com +hiro042928-com +linxstone-com +dokokanocafe-com +t-bs-net +arcadiafp-net +1000goku-net +tpz-xsrvjp +iine-p6-info +taxiyoyaku-com +chuchu3-com +tsubasa114-com +pongsitgolden-com +motomachi-yasu-com +d-rentacar-com +touhokumiyage-cojp +fudousan-neta-com +tnknbyk0103-xsrvjp +reclaimthestreets-net +next11-xsrvjp +eishin-re-com +mikasasports-cojp +iloveyou-fc-com +nextstage-produce-com +gklineconsulting-com +houki-ganka-com +c-bz-net +kitchen-kyoto-com +dlappli-com +chiken-navi-jp +lingua-franca-jp +stella-sr-net +webkohbo10-net +papa3-com +nin-fan-net +alohi-apopo-net +it-osaka-jp +takayukikawase-com +heat-tech-biz +m28-xsrvjp +optsa-cojp +relaxstart-net +midenaru-biz +youtubejp-xsrvjp +tsuhan-faq-com +toyoriken-cojp +9nine-fan-net +urayasusunclinic-jp +shimabara-shaken-com +x-system-biz +kyougakukan-jp +naka2220-xsrvjp +hiro-design-jp +naturezo-jp +ryokounokensakudayo-biz +wakeshoten-cojp +note-sp-com +xsample66-xsrvjp +tenalux-jp +kokkororen-com +light-hous-com +alabrava-com +carcenter-khoki-com +yama1pm-com +xn--nbk1d7buav9cududsezd4619b-com +2cv-club-com +xinfo360-xsrvjp +xinfo535-xsrvjp +rmtop-jp +xsample167-xsrvjp +futekigou-xsrvjp +izumi-k1-jp +hokkaido-ra-jp +news-trend-jp +sv2nd-com +ancreate-jp +yumepi-net +room-worker-com +nedlize-us +kishiwada-syaken-com +labellart-com +junpa-com +tsuruya015-xsrvjp +8project-jp +mendokorosato-com +maccarina-cojp +miyanur-com +onshinan-com +sato-bankin-jp +komazawa-ttc-com +amritara-com +xn--pckuae6a2167c95i-biz +gaiji-movie-jp +onlinestoreexchange-com +ipasso-jp +xsample313-xsrvjp +s340-com +wassalon-jp +lotopj-xsrvjp +tyokkan-com +runchan-net +tomtrade-xsrvjp +1fineday-biz +at-iroha-xsrvjp +suntruss-cojp +naret-jp +isotope25jun-xsrvjp +wakuwakudouga-com +chouju-orjp +webtest-client-com +tennenzinen-com +morita-shika-net +shirafuji-xsrvjp +futekigo-com +ukuuku-com +harta +miyama-satoko-com +mikuyaproject-com +touch-japan-net +eastshining-com +gen-en-monitor-com +ejan-biz +spock-xsrvjp +jptravel-asia +kurohigehonpo-com +tsbizs-com +seiwakoumuten-com +cw-shonan-info +kurofune37-com +yumerita7769-com +mietakun-com +adamforcongress-com +chibakogao-com +8one-jp +easyfreemind-com +xsample97-xsrvjp +mooncom-jp +technaceres-com +e-sanoshopping-com +morioka-ind-cojp +sinsihuku-club-com +mk-bikelove-com +isochugoku-cojp +sato-bankin-xsrvjp +fureai-navi-com +yo111-net +pasoigusa-com +friendship-jr-com +rs12-xsrvjp +iphoneworldmap-com +shiranecycle-com +deltazulu-xsrvjp +sohjusha-cojp +election-xsrvjp +steels-jp +storeworks-jp +kasegu888-com +yao-en-com +profit-gym-jp +fujishina-com +skw777-com +shabering-com +kaiyoutei-com +wakasa-obama-jp +gatherlink-net +no1-marketingcoach-com +atagosan-xsrvjp +club-rize-com +miteyan-com +hokkaido-shikinoaji-com +hitokoto1221-com +zen-platform-jp +xinfo712-xsrvjp +si-a-net +jey-string-net +jyosyu-udon-jp +raisuhatakefuji-com +cocoro-rhythm-com +mm-style-net +learve-jp +team-sns-jp +contrax-xsrvjp +didier +nobuaki-xsrvjp +imaizumi-dc-com +samurai-ticket-com +minamoto-jitsugyo-com +xn--mdko7702aecw-com +souzirou-com +webworkroom-com +kanagawaku-net +souzoku-houki-org +humainus-info +musashikosugicon-com +gsxsetp1-com +fujir-net +mcprogram-net +marketing-mindset-com +ta-93-com +xxxkawaii-info +danceconnection-jp +jyuku-me +pridejapan-net +mahalo-love-com +tokyo-skytree-navi-com +st-angelina-com +novartis-app-xsrvjp +snok-com +saikinokai-com +sosyaken-jp +geinouch-com +xn--48j7bzfzeohwa4c1c7a5ah7pd1297hupq830awta860ojid-net +vegaworld-xsrvjp +jinpu-kai-jp +imaccer-com +chuyo-denon-cojp +marufuku-nouen-com +senbeido-kyoto-com +kenko-dna-com +youbook-xsrvjp +minobusan-trail-com +office-koh-com +sekizawa-biz +tohoku-chuo-com +exercisediet-xsrvjp +kamikamihobby-net +looping-jp +fussa-shaken-com +williamhill-japan-info +oisii-takoyaki-com +hangahokkaido-com +yokamon-biz +momokane-com +racersnavi-com +light-kan-com +piaaplus-com +yarujan-xsrvjp +ohisamahouse-xsrvjp +atomvetme-com +duelmasters-kaitori-com +oo0n-xsrvjp +3d-printers-jp +tkt-center-xsrvjp +sg115 +hikari-ntt-com +partsya-com +best-fresh-net +lastier-com +nikken-ltd-cojp +pkcreek-com +m-takato-com +open-chirashi-com +chamrocca-com +atamidecon-com +yama-toku-com +sancoa-hbs-com +sunrise-inc-com +goodchance-biz +seminar-jp-info +yu-i-net +xinfo743-xsrvjp +xinfo357-xsrvjp +salon-lyn-com +minoritougei-com +creclamitaka-com +sakicorp-com +horyukai-com +kaztas-com +fukuyama-mokukei-com +shiseibi-jp +stupidproxy-com +xsample34-xsrvjp +wings-consulting-jp +celtislab-xsrvjp +thefukugyou-com +jamselection-com +hyuma2010-com +onotoukisokuryou-com +kk-hinode-cojp +graceflowerart-com +chip-pe +sokabe-biz +strelicarski-savez-srbije-org +iboji-net +washizucleaning-com +dysczs-com +yanheejapan-info +surgery-iwate-med-jp +xinfo503-xsrvjp +tokyowill-lionsclub-org +kagula-xsrvjp +daimon-cl-com +xsample135-xsrvjp +nlp-island-jp +lfamille-com +test-xinfo764-xserver-com +refresh-kaatsu-jp +hondainsatsu-com +office-ogawa-biz +so-na-507-com +sakura-0322-xsrvjp +bizsp-net +xn--n9jtb0cui4i1f2488azjtak97d-net +nishikawa-camera-com +fujimura-shika-com +yokohamaconpa-com +ise-lotasclub-shaken-com +swagger-co-com +kyokawaseikeigeka-com +kudamonooyasai-com +ecatch-mhss-net +nanba-dance-com +choishimichi-com +spoiler +denritsu-lighting-com +test-xinfo734-xserver-com +sg114 +uecyan-net +thebluesky-xsrvjp +maruyo-xxx1-com +takahashi-jun-com +test-xinfo724-xserver-com +r-sun24-com +hamamatsu-coating-com +pc-katekyo-com +g-laser-net +jas-pet-com +test-xinfo714-xserver-com +webkikaku-com +rarewater-biz +deecrea-com +dearmine-jp +miecon-net +test-xinfo704-xserver-com +inter-plan-jp +infowinwin-net +shinafu-jp +tetsutabi-xsrvjp +miraikentiku-com +tobie0508-com +eajpn-com +test5150-asia +ks-holdings-com +hazama-design-com +iworkin-asia +mao-mao-cojp +mietakun-net +hijirinone-com +d-cruies-com +a-gaienmae-com +xn--kckj3dudb-biz +iscn-xsrvjp +golfcraftjapan-com +sun-i-org +kawasho-hl-jp +e-youkan-com +infinityinfo-xsrvjp +murisoku1-biz +itukinosato-xsrvjp +xsample65-xsrvjp +studio-phiz-com +theunbookables-com +i-arc-com +only1fashion-com +itajiki-com +990933-com +michiga-com +asdageorgeclothingrange-com +w-catalog-net +sg113 +shoprakuten-com +xinfo534-xsrvjp +xn--p8j0c259m22li4s-net +xsample166-xsrvjp +eggconsul-com +soranoao-xsrvjp +ageo-shaken-com +isujkn-com +pricewave-net +xn--yckc2auxd4b1246f4y1b-jp +xinfo591te-xsrvjp +chiffoncolor-com +designote-jp +asebyebye-info +test-xinfo594-xserver-com +xn--nckg8jh0ek1dbb7f7459eehdhr8gg18a977c-net +osho-fragrance-com +13office-com +sarobetu-info +pitchshifter-net +oc1-xsrvjp +rakudoku-akashi-com +xsample320-xsrvjp +yukendou-com +admaterial-cojp +komazawacon-com +nanba-yasu-com +nanmoku-net +japanese-movie-info +orihu-net +lunion-biz +santoku-net-cojp +kashinotakanori-com +dokuritujison-com +mirakuruza-com +pmc-cr-jp +taikai-jp +janzzysbar-com +jats-cojp +daily-speech-com +trade-king-biz +test-xinfo554-xserver-com +thaiivf-com +jp-alna-com +make-sms-com +test-xinfo544-xserver-com +rehabilitation-jp +xn--49s538bm8ux8c-net +i-utsuwa-com +smaphoappli-factory-info +green-cycle-biz +umai-yo-com +intrepid-project-org +joshin-xsrvjp +xn--line-tk4c0cf2ooiyhod-jp +alpinawater-info +otogr-shizuoka-net +shougaihoken-info +munakata-cl-jp +test-xinfo524-xserver-com +ve-g-com +freeinfoapp-com +xn--pckhnj8ayp6atu7e2djb-com +meiyu-ip-jp +kenkou-bi-biz +hakurin-com +test-xinfo514-xserver-com +palulu-jp +barpolaris-com +adic-orjp +trust-solution-jp +test-xinfo504-xserver-com +xsample96-xsrvjp +tosakanran-com +hyuga-daiichihotel-com +konwakai-jp +xn--fa-og4aod4a8v-com +longsmart-mobi +m-design-xsrvjp +xn--5ckhs7czfb6c0dd-com +atstyle-xsrvjp +tese0903-com +oda-estate-com +csw-jyuken-com +dreammanager-info +sg109 +osaka-shaken-senmon-com +jardin-favori-com +atelier7-jp +yatsutakamikoshi-com +aimistone-xsrvjp +rs11-xsrvjp +anomaly-cojp +tmp-inc-com +xsample207-xsrvjp +handsfreedigitalcamera-com +kaorigikoubou-cojp +tokeishop-jp +crp-sapporo-com +europort-cameo-com +nihonbashiconpa-com +suna-lab-com +test-xsample308-xserver-com +trimworks-jp +kawaramachi-yasu-com +browser-check-jp +merrittmurals-com +sakuyafb-xsrvjp +cardboard-art-com +mnmj-asia +kyushugodo-jp +tokudax-com +altechjp-com +testsp1208319-com +camonoe-jp +yumesiokaze-com +xn--lurea-mm4dysia-jp +inaka-nakoudo-com +xinfo711-xsrvjp +tcmic-net +w-shinkyu-com +mssrv-org +chugeikanko-com +xn--3kqu3oh0b77g34dt2lxzn4mre5ohlvlx1c-com +frou-frou-org +h2works-jp +xn--akb-fu0e63gwsk9wi4dt38bp4bk6ivrnww8e2uwcils-com +houkon50-com +green-dental-info +chayamachi-yasu-com +testdomainx333-com +infinitewisdom8-com +rucksackspace-com +eigo-joutatsu-net +sg106 +europa-artist-com +rocohouse-jp +kingrocker7-com +afwd081028-xsrvjp +testdomainx338-com +yushonokai-com +xn--ihqw3zba21d-biz +test-xinfo761te-xserver-com +tomimido28-com +irinamihira-net +izunousagi-jp +nomikaisiyouze-com +xsample103-xsrvjp +ion-ceramic-com +central-bldg-clean-com +test-xinfo354-xserver-com +golfshoshinsya-com +pochitama-jp +htz8513-xsrvjp +koura-takeshi-com +kangoshi-service-com +themanwhomarriedhimself-com +tenshoko-com +hamamatsuchocon-com +fukudashika-jp +ringo-no-ki-com +densai-s-com +kaisen-fan-com +heartwing-info +e-joho-com +civil-design-net +life21inc-com +tani-you-com +villa3-jp +choanshin-com +syugaa0415-com +crapre-kawasaki-net +wakayama-yasu-com +to-ritsu-cojp +tomonphoto-com +akashisyuhan-com +twproducts-jp +xinfo156a-xsrvjp +funeralville-xsrvjp +sakaearumi-cojp +makibi-xsrvjp +sweep-aside-com +jiin-net +kouenjin-com +fushigiplate-com +roc-cojp +kansaibridal-com +f-magic-com +4tune-nejp +mikagesushi-net +ryouhei0206-com +photoria-jp +ydental-com +akihonda-com +sasanobu2228-com +manwatching2010-com +xinfo742-xsrvjp +xinfo356-xsrvjp +ateam-cojp +smilinghpj-org +smartphone-affili-com +am-shika-com +sni-tobitakyu-orjp +ofc-osaka-com +realize-iboc-com +advance-soleil-com +sannou-r-jp +kotobus-com +bbq-con-com +hopewill-net +asbestos-jp +xn--1sq130aw9j5qh-com +dunan123-xsrvjp +kirikui-com +nakasendo-cycle-com +xinfo739-xsrvjp +yachimata-ds-com +atsushitagawa-com +guitarstylist-com +xinfo502-xsrvjp +xsample134-xsrvjp +parallel-xsrvjp +double-moon-info +mankintan-net +invside-jp +snag-golf-net +faith-hair-jp +bs-saori-com +li-ta-jp +dalmatian-jp +actionscript4flash-com +5con-jp +ust-tsu-jp +studio-zeal-com +office-nis-com +ohta-cl-com +p-con-net +sekisondb-xsrvjp +mcrownroyal-xsrvjp +mentalcafe-net +yumesake-com +tokubetu-orjp +hobbymall-xsrvjp +oita-syaken-com +inuno-cage-com +jones5672-com +psychicno9-com +marei-me +inc88-xsrvjp +mocolife-xsrvjp +abundance3313-com +masudaya-net +news24s-asia +katesippey-com +pha10074-xsrvjp +tsi-p-com +raysfactory-jp +xsample335-xsrvjp +moki +tenmabashicon-com +orionxserver-xsrvjp +nmtjapan-com +futami-xsrvjp +radia-xsrvjp +testxdomain303-com +guggenheim-m-com +nikibitosayonara-com +kis-s-com +lush-xsrvjp +atelier-stellar-com +a-cherry-blossom-com +ishimorikusa-com +testxdomain308-com +mei-san-cojp +kawaramachi-yasuhei-com +itb-cojp +yoshi0308-com +xsample64-xsrvjp +uchiyama-kikou-jp +naown-jp +k-fukuda-dental-clinic-com +rhrinks-com +hotshot358-net +nextplan-info +testxdomain314-com +kyasshinngu-info +lockonshop-xsrvjp +nh-pma-com +xinfo533-xsrvjp +trd2-xsrvjp +tre-ca-com +39city-net +xsample165-xsrvjp +shopuu-sedori-com +sensatsu-com +network-jp-com +tabekuru-net +stayconnecticut-com +all-cosme-xsrvjp +okuei-com +0250587150-com +rokusetsu-com +onlyrealinfo-com +gakuseievent-com +itanaka0722-com +makaino-com +notch502-xsrvjp +opus77-net +the-secret3-com +glittering-stars-com +rumi-ne2-xsrvjp +lien365-com +tora2011-xsrvjp +hanashite-sukkiri-com +gamajapan-com +sapporo2jyou-net +msg-philos-jp +fresh-yamamoto-jp +shinseikai-d-xsrvjp +plus-q-net +moriya-cooking-jp +catsway-net +dreamer-xsrvjp +trendstars-biz +lead-next-com +xsample311-xsrvjp +shinshu-u-acjp +twitter-xsrvjp +awamori-cojp +trophyqueen-jp +kenko-ya-jp +crystalfallsmotel-com +u-tan-jp +nakajima-reiji-com +fujix-corp-com +i4wave-com +paikaji1-cojp +4mix-cocktail-com +fukuikaikei-com +fun-music-school-biz +xsample215-xsrvjp +dancealive-tv +keihan-green-com +kintaroueco-com +jtreasures-com +mensfaltusyon-info +venus-times-xsrvjp +tresrey-d-com +china-phs-com +nmaj-xsrvjp +senryukensetsu-com +flicks-cojp +wp-affiliate-info +nas-recovery-jp +kibune1923-com +project-ex-net +rotier-xsrvjp +pazpaz7-com +whity-whity-net +toufu-yamato-com +sisei-jp +xsample95-xsrvjp +bitclay-com +t8-itakura-xsrvjp +subaru-juku-jp +cbc-canada-com +richesse-hij-com +tokei-akashiya-com +ty-plan-net +test62-xsrvjp +marupuri-jp +gmunion-xsrvjp +detopush-com +holidayclothesforwomen-com +ontamashop-com +mr-clean-net +wano-xsrvjp +super-r-xsrvjp +philoballet-com +nichibei-xsrvjp +xsample206-xsrvjp +babyraids-net +aoyama-nail-com +eiken-home-com +galaxy-universe-com +sg103 +start-trust-jp +dog-yamamoto-net +iwamun-xsrvjp +takada4976-com +stech-pro-cojp +osaka-transport-cojp +blacksanta-cojp +mvhits-com +harajukudecon-com +kingburak-net +bnca-jp +yamatoya21-jp +le-reve-nail-com +mtplace-biz +crecer-client-com +webessentials-biz +webbingstudio-com +sps-mg-com +create-o-com +rusty2-com +xinfo709-xsrvjp +mfimp-com +konkatsu28-com +sect-xsrvjp +propodentalex-net +bio-s-net +xn--y8jua4a3aa5irgf4841fknvi-asia +kasegujoho-com +nordic-showcase-com +power-rips-com +satoshi002-com +zero-family-com +famicom-market-jp +arths-net-cojp +momose-orjp +testsp1201231231-com +golftool-net +minamihorie-yasu-com +yanehoken-com +plaisir-beauty-com +galenhall-jp +karesansui-biz +restaurant-rumi-com +hosomi-kogyo-cojp +mugendou-osaka-com +sg102 +koubou-imaya-com +satotti-xsrvjp +0507landbrain-com +xsample102-xsrvjp +ryomolive-net +iwakuni-ymca-jp +ba373-xsrvjp +optronics-ebook-com +trend7777-com +renkon777-com +enflor-net +koshibasaki-com +diemilch-com +freelifec-com +handworkcafe-jp +3dphoto-ar-com +morisitaya-com +xinfo369-xsrvjp +wtte-xsrvjp +sea730-com +nkohichi-com +premiersoundfactory-com +ta-me-shi-te-net +testxdomain300-com +tohan-co-com +vin-nerd-com +pcsubnet-com +create01-xsrvjp +crimp-jp +yrnetmind-net +vivify-xsrvjp +c-how-jp +omotesandou-h-net +affiliate-matutake-com +concaragan-com +globalshopper4u-com +cuisine-xsrvjp +sept-couleur-com +contech-jp +kouboukujyaku-com +xinfo741-xsrvjp +xinfo355-xsrvjp +xn--u8jua8gqbf5b9c-com +colmn-cojp +hoc-jp-com +kumachan-info +xn--ehqvz02f3w2b4ha256p-com +mic-fishing-xsrvjp +megumibaby-com +xinfo107a-xsrvjp +nazegroup-jp +big8787-net +sapporoshi-shaken-com +elle-jt-net +strongroove-com +sss-mizuno-cojp +miyasaka-ss-com +mc-academy-info +francheeno-com +allmymate-com +nichido-monthly-net +egbaism-com +xinfo501-xsrvjp +toko-08campaign-com +lakalomi-com +pokertips4beginners-com +asaichuzo-xsrvjp +k-tanigawa-com +rokusetsu-net +xsample133-xsrvjp +fujigaoka-service-com +primo-st-com +my-nemuri-jp +walker-id-com +npo-panda-jp +wakasho-xsrvjp +thykm-net +mmaru-biz +jtta2013-org +goodkyoto-com +cesiumkafun-com +takitoh-com +ebmtrading-com +kodokai-net +kon-gene-com +kk-morita-ss-cojp +corocorooon-com +sskgroup-info +iwaki-shaken-com +midpalm-com +netcross-usr-xsrvjp +trader7-net +ikealife-net +vege-tore-com +nagoya-cci-com +poca-ket-com +sweet-emotion-net +ikefuku-xsrvjp +ult-japan-com +enes-cojp +longwin-cojp +netdegungun-com +money369-net +fs-lifeworks-com +martinique-barreau-com +kishimoto-hideo-jp +kamoike-com +seruraito-info +sym-sym-xsrvjp +stillkid-net +851-jp +sandaekimae-com +kyoutani358-jp +diamond13-info +centurion-club-com +comleading-qrs-com +lifecyclopedia-jp +gateau-shirahama-com +soryusha-com +libary-tv +9demo-info +rosn-info +ys-grp-com +hqt-jp +iharats-xsrvjp +marry-port-com +i-friends-biz +xsample63-xsrvjp +akadon-biz +amilove-net +daisei-loginsystem-net +uotake-jp +wealth13-info +se7en +m-tresor-info +century21cosmoland-net +kasaiportal-com +bluewings-xsrvjp +mechanic-recruit-tsu-com +82905236-com +xinfo532-xsrvjp +c-ty-jp +xsample164-xsrvjp +komaya-info +fauvizme-xsrvjp +niiza-net +subaru-chuhan-jp +learningplay-xsrvjp +kijuna-net +kamig-cojp +2d6y-jp +net-newstyle-jp +yoiko-sakuragumi-com +geo-plan-cojp +newday-r-xsrvjp +phahp-info +hii-peple-com +xn--a-geuzc8b9bxq-com +akibeach666-com +xn--ockc3d5hu632b-com +toco2dog-com +happy-777-biz +real-aide-com +avenier6288-com +aki2844-com +xenoland-net +itkids-jp +headandhand-xsrvjp +miyazu-net +fugetsu-sapporo-cojp +dragon-cross-xsrvjp +owlinone-xsrvjp +tm-ms-com +sci-jpnet +okusuripet-com +sk-design-cojp +yo-affiliate-info +excel-ins-jp +phoenipro2-xsrvjp +zgmfx20a-com +allaboutplumbing-inc-com +aul-jp +suuu-design-com +garage-candle-com +oniku-xsrvjp +test-xinfo768-xserver-com +lamb-cloud-xsrvjp +office-eql-com +koufu-con-com +npo-jcia-com +netapp2b +uks-cc +miyako-aquaticadventure-com +lakalomi-jp +zaitakujosi-com +xdock-xsrvjp +estenadsonic-mobi +insatu-hikaku-com +test-xinfo765-xserver-com +wabisabi-ya-com +xsample94-xsrvjp +md-trunk-box-com +vital-life-cojp +design-memo-net +kusatsujuku-jp +testsiefafasdf121219-com +sw201212-com +legend-mj-com +asaichuzo-com +fleugel-xsrvjp +xn--u9j282gwio42bb83h-com +xinfo563-xsrvjp +room-coating-com +r-union-org +nayamikokuhuku-com +celebstyle-jp +livetp-com +mrise-xsrvjp +oyayubi-cojp +test-xinfo758-xserver-com +gasenenews-xsrvjp +born-in-the-darkforest-com +credi-hikkoshi-com +108-octo-com +nayaminai-net +hfog-info +arafor-com +ulife-reform-com +kyoryu-shougi-com +muramatsunews-info +a2unit-com +shikishiki-com +baseballgear-jp +consult-semi-com +sesame123-net +kurahashi-hifuka-com +sasahaya8-xsrvjp +ootani-xsrvjp +golden-item-cojp +yutaking-com +xinfo708-xsrvjp +xn--z8j2bwkxag2fvhmi9cc9847r-com +rafjp-org +mholi-com +suzutech-com +rimosuke-net +erena-ono-net +iowanazkids-org +dn-net-cojp +kyabaraku-jp +keihin-technical-com +josho-kiryu-com +you242-com +aruz-shop-com +saisokunews-com +mitsukawa-net +hairsupple-jp +rt-planning-com +hoken-partner-com +arbel-xsrvjp +b-garden-com +ryu-tan1945-com +test2-xinfo701-xserver-com +wellnext-info +xn--ols92risjhpv-asia +ksl-auction-com +hanaoka-dc-net +rom9-xsrvjp +xsample101-xsrvjp +xxsunflowerxx-com +noto-xsrvjp +tcplus-xsrvjp +xn--u9j5h1btf1es15qifb9z6hcj5d-jp +tsunagaru-ktq-com +myhome-uehara-com +akihisakondo-fc-net +bmarket-inc-com +bestplanning-nejp +kessetsu-net +fly-sky-asia +meiyobiz-xsrvjp +tjvm01-com +lapilos-pure-com +yoyakuweb-net +immersion-xsrvjp +aval-jp +jpzekken-com +kichijoji-unmei-com +fuku-sui-net +hamacon-cojp +bestlife-ytf-cojp +xinfo594-xsrvjp +mutsumi-rental-com +xn--eck7alg1e2b-biz +youngmate-jp +throb-xsrvjp +anpeiji-net +xn--u9j5h1btf1e613xwr2drrjbqs-com +miyagikankou-xsrvjp +hys-inc-com +nananasalon-com +crerea-d-com +m-shop-net +dr-hiro-com +excelpon-com +delta-group-xsrvjp +ezcite-net +test-xsample30-xserver-com +neo708-xsrvjp +all-dietary-supplements-net +x1gg-com +keb-xsrvjp +xn--u9j0c2f2crdwc2cwdv219fiuc-biz +xsample330-xsrvjp +satyrise-xsrvjp +rt-planning-xsrvjp +cutout-jag-com +xinfo740-xsrvjp +xinfo354-xsrvjp +shigeno-motors-com +oisii-wan-info +keepcool-biz +yuukyuu-com +memokore-com +kenko-coffee-com +good113-com +realgrow-cojp +xn--68je3c7dsev110a6cu7y0e6xk71t-jp +worldwidejob-info +xsample31-xsrvjp +micro-powder-cojp +t-moving-com +designbnk-com +ryouguchi-salon-com +japanese-goods-biz +nexenta1 +fbmarketing-lecture-com +haripanda-com +fnettest-com +nabeshika-com +flysky-xsrvjp +heartfultrust-xsrvjp +xsample132-xsrvjp +liveplus-xsrvjp +ashibatobi-orjp +hp-omakase-com +augace81-com +srsrsrno-1-com +talkwith-jp +xn--u9jb5p4ctkpbzdu307a19ai49kda-jp +easy-pace-com +sky-aff-com +6348-cojp +m-styles-jp +kazoorock-com +ja-suzuka-orjp +mtb-production-info +trd-xsrvjp +wwvision-xsrvjp +ontaya-com +xn--vck0et49h-com +kenkou-dajya-com +bunkagakuin-net +sutekini-net +blackma-n-com +omiyacon-com +engtests-cojp +mayub0628-com +link-face-com +hoken-opinion-com +gessyu50man-com +uduki65-com +kusuki-biz +macbook-fan-com +willowtree-xsrvjp +ckb-xsrvjp +ciel-c-jp +uedafumio-xsrvjp +takaranoyado-com +xsample209-xsrvjp +test-xinfo762-xserver-com +tarinukarte-com +shirasaki-hifuka-com +yushoya-net +mekikijuku-jp +ve-gate-com +yoneharu-net +koube-shaken-com +dolphin-watch-net +panarl-cojp +sweet-w-com +test-xinfo742-xserver-com +muw-do-com +sedorinomirai-com +xn--zckqft4pu73w8go-jp +ravenrileyisahottie-com +biyakusalon-com +test-xinfo732-xserver-com +xsample62-xsrvjp +xn--30r99m89hxoam8ze2n05l-biz +netviewer +kite-misawa-com +ajajajajajaw-xsrvjp +teruhito-thank-com +yuasisu-net +test-xinfo722-xserver-com +test-xinfo702-xserver-com +k-kotani-com +ikebukuro-tk-com +tem-baby-com +test-xinfo712-xserver-com +xinfo531-xsrvjp +cpa-museum-com +xn--icko4ae3d6o-jp +xsample163-xsrvjp +eishinjuku-xsrvjp +originaltshirt-jp +vision-industries-cojp +candy-0210-xsrvjp +sonicjob-com +bijinkan1988-com +ykd-cojp +appliyakun-net +imperial-bms-com +jiko-jitugen-info +tprint-info +matsushitasatomi-com +remedier-net +rakupa-com +wp-customize-net +tatsuokw-com +codes-a-com +s-unit-xsrvjp +dreamtoma-com +haatm-net +sky-afiri-com +sisan-unnyou-asia +hawaiiwater-tohoku-com +goods1-com +alex-ah-com +youvision-biz +isa-grjp +xsample308-xsrvjp +fumitan-net +kizimun-net +machinaka-link-net +karadalab-jp +sptm-i-xsrvjp +653655-com +d-salescopy-com +amano38-com +a-cue-info +kawaramachi-ponto-com +fujiwara-ahp-com +garden2495-xsrvjp +sky-afiri-xsrvjp +modernchild-jp-com +y-studio-jp +kimanmakoubou-kikori-com +monnickendam-dia-com +firewing-xsrvjp +plan-menkyo-com +assam89-net +test-xinfo592-xserver-com +support-surunara-com +nobunobu981-xsrvjp +hyugads-xsrvjp +join-with-jp +osusume-net-com +tkitano-com +kaigyo-kekkon-com +yokosuka-shaken-com +keituiherunia-com +atrapas1-xsrvjp +xsample93-xsrvjp +teru-dental-com +kyo-kure-com +real-trade-cojp +credoseitai-xsrvjp +new-figyua-com +test-xinfo562-xserver-com +seiko-kaiun-com +googoojapan-xsrvjp +jorro-design-com +xn--v8jvcby2l2b4hqftnh804cpktafq8h-net +land-create-cojp +guitar-shop-cojp +frosty-school-com +test-xinfo552-xserver-com +yunifurerm-com +xinfo562-xsrvjp +meishi-plus-com +clipmusic-cojp +sap-inc-cojp +xn--line-ym4cqkvg9752bcyva-jp +senior-care-cojp +crazywedding-jp +dreamstage-weekly-net +test-xinfo542-xserver-com +toruscloud-com +satoo-biz +okeya2525-xsrvjp +brand-shop-xsrvjp +yuzuruhassamu-biz +ikkyu-seikotu-com +pet-academy-com +test-xinfo532-xserver-com +nadia-bz +portal-website-biz +heart-flow-com +masu-okazu-com +hidamarinet-com +test-xinfo522-xserver-com +alienfunnypot-com +network-hikari-com +skystream-info +kessai-ikkatsuhikaku-com +takepon7-com +test-xinfo512-xserver-com +l-bamboo-com +pdevelop-xsrvjp +kenshoukan-com +sugaoreiko-com +officesakura-com +tech-angle-xsrvjp +xinfo707-xsrvjp +test-xinfo502-xserver-com +fusa-cojp +modelcase-net +g-factory-xsrvjp +xn--vsqv9lppf53f-com +xhtml5-jp +keihan-ophelia-com +2week-info +brand-repair-com +mmm-cx +unicolabo-jp +yokkaichi-mj-com +my-powerspot-com +btest-xsrvjp +koba-i-xsrvjp +minacom-xsrvjp +t-eigo-com +ndu-ec-com +organicvegereview-com +plusone-ps-com +yuki0509-com +test-xsample326-xserver-com +bamstest-com +peace-shop-com +hikaku-creditcard-net +nano69-jp +chokki-com +ohimesama-info +djtomo-com +popcorn-papa-com +life-go-info +sofuken-com +test-xsample306-xserver-com +yokoi-site-studio-cojp +tmc-labo-com +rom8-xsrvjp +tmi-st-com +branduce-xsrvjp +sunnysh-xsrvjp +kurt120-xsrvjp +tsuyahime-org +anti-aging-club-net +propeller-pigs-com +shuihubook-com +one-sky-net +mkksh-jp +technosound-cojp +yumaishodo-xsrvjp +b33-org +sayuu-jp +kiyotaki3-com +tontonlife-com +netokaru-com +tenpoo-xsrvjp +xsvx1019633-xsrvjp +akitacon-com +xinfo593-xsrvjp +xsvx1013269-xsrvjp +selection-up-com +kuma4864-com +jandc-xsrvjp +daieltuto-info +hanjyo-info +garasunosato-com +directoryfound-com +my-days-off-com +storey-s-com +land-21-com +xinfo122a-xsrvjp +xn--t8j3b4ef5oa1c8e1srau1b8r-jp +kyara-jpncom +europort-stika-com +test-xinfo352-xserver-com +yt-kaikei-com +snaptokyo-jp +collect-xsrvjp +xinfo353-xsrvjp +hiserve-cojp +tokyodegibe-xsrvjp +rockeys-biz +ii-kao-com +ayabeshi-jp +toint-net +ainsel-xsrvjp +fumika-shimizu-net +mhousing-jp +takatora7-com +horiecon-com +machikado-tokyojp +the-kobetsu-com +mdt-cms-net +sakiyama-bc-com +infolma-xsrvjp +innervision-xsrvjp +appri-ya-com +crimage-jp +springwater-h-com +englishfamiliar-com +s-d-h-com +woorom-com +akasaka-eyes-com +xinfo546-xsrvjp +chibadiet-m-com +mizu-shori-xsrvjp +xn--3-4eu4ewb4f-jp +xsample131-xsrvjp +matusima9656-com +arga1039-xsrvjp +artoria-xsrvjp +bigtrout80up-xsrvjp +xn--navi-ul4c1e8bg9i0h4h-jp +jtta2012-org +koneko-navi-com +stella-si-com +kyoto-ennosato-com +nijiironotane-com +studiobibi-cojp +shahotaiou-com +ryokanichinoi-com +e-crom-com +xinfo509-xsrvjp +baat-memory-com +dumpvars-com +s-atoz-com +cbs-datsumou-com +packuntyo-xsrvjp +seifuku-labo-com +cozy-cafe-grace-com +yasui-press-com +herbest-college-com +shi-gyo-com +bijin007-com +haraganka-orjp +high-top-jp +bluemurder-biz +rea-lizar-com +tahatsu-net +pitat-nabari-xsrvjp +beautybeast-cafe-com +yoruslim-info +kayama-sakaki-cojp +sagawa-construction-com +homebs-net +sumizoku-com +alive-corp-cojp +saikounosumai-com +tiger-dragon-org +xn--0ck0d1a2et21sbko82s-com +tcigp-net +testxdomain310-com +hochzeit-profis-com +bonn0815-info +medical-chain-orjp +koizumi-orjp +xn--t8jg7fsgvi0d2h2g-jp +agir-osaka-com +adman-jp +jsotop-com +seiko-kaiun-net +xsample61-xsrvjp +muryo-offer-com +qdcop-com +lsecretservice-com +toushirou-web-com +fukushi9000-com +ht-produce-xsrvjp +takeshi-dream-biz +xn--eck2csav0byit522b1t9a6fk2u5d-biz +xn--cgi-qs9d423tgelegd-net +abundance33-info +semco-okuda-com +xinfo529-xsrvjp +ktt-school-jp +p-fine-biz +xsample162-xsrvjp +successmindset-info +yu-momo-com +ichinoi-jp +maru8maru8-com +akizukiminami-com +utahutah-com +onokan-jp +g-freak-com +newbiz-task-com +testsaba-hiroo-prime-com +weilaigongben-com +wirelesspencamera-net +cycle-force-com +neltutokasegu-biz +rlifesupport-com +cattly-com +rose-royale-com +raytenor-com +erb-xsrvjp +ukiukishop-xsrvjp +logue-jp +effort-corp-jp +line2-tv +shinku-ya-jp +padm-jp +reject2-net +j-alliance-com +moe-jk-xsrvjp +peace-shop-xsrvjp +akb48akb84-jp +ecomado-net +works-jobs-com +firstitpro-com +keiba-info-net +kjyu-art-com +xsample307-xsrvjp +pro-13-info +earthpolish-com +kyoto-kyoto-net +adworks-design-com +boribon-net +nakasuhaken-xsrvjp +asayoko-net +pokkarigumo-com +getti-info +machida-shaken-com +yukaisoukai-com +clinic-webseminar-com +localstock-jpnet +internet-agent-net +popran-net +sato-iin-info +vanguard-kaitori-com +french-code-com +decormaison-jp +jagdd-net +inuyamachuohospital-orjp +tess211-xsrvjp +sugita-photo-jp +inexpenshop-com +koshigaya-con-com +shin-ei-kan-com +officek-s-com +xn--nckgh0pyb4cb0662e3ze8mpt2h2w6bmjzaoh9a-net +webzou-info +barakamon-com +def-hair-com +studio-arai-com +nnn00001-com +seiryuunouen-com +one-piececollection-com +takada-babadecon-com +rikoh-s-com +kawagoe-com +relaxrich-com +yands-jp +doutonbori-yasu-com +testsp1208273-com +m-credo-cojp +touch-express-net +sympret-com +kyoeihomes-com +rio3 +1024-cojp +shantishanti-info +i-exec-jp +dct-japan-cojp +kyotanba-dog-com +suzuna-web-com +fullsato-jp +testsp1208278-com +xsample203-xsrvjp +wedding-pipi-com +asukamura-jp +info-abaku-com +805-ch +rengenosato-cojp +regal3-bg-com +excelsuke-com +fukushimadance-higashimatsuyama-jp +66sk-org +s-pro4-com +sekainomadopower-com +kagoshima-shaken-com +you-rec-cojp +overload-xsrvjp +fujir-biz +yusyutu-business-info +girlslovin-com +beppy-xsrvjp +slabri-com +fuusui-kantei-com +xn--y8jl1nr86je03c-net +takagi-jds-com +seizen-zouyo-net +p-fucoidan-xsrvjp +siota0913-com +i-country-cojp +parts1-amagasaki-com +xn--u9j5h1btf1e9236ag6b1v8idc0a-jp +yuyasawada-com +toxictwostep-com +youclub-jp +kootec-jp +sakashita-s-jp +k2style-jp +chintaisoudan-com +banyan-therapystyle-com +moemore-jp +novus-dairiten-jp +miduho-seikotsuin-jp +iroha-affi-com +e-goyoukiki-com +xn--pcktab2b3dta2oze-jp +h-kazama-net +snowflakes-xsrvjp +meadow +ske-xsrvjp +kumanomai-com +kotohogi2672-com +oumi-tankai-shaken-com +touchkun-com +abe-iin-org +michinoeki-totsukawago-com +masturbatiomenu-com +afoi23j4ofadf-com +rise-p-info +mhcolors-com +sawhde-com +unitedstyle-cojp +akahori-print-cojp +netbizch-com +tetta-xsrvjp +officetecchan-com +xn--54q764c9gar1l-biz +kotog-jp +amb21-com +calender55-com +clover-factoring-jp +move-s-jp +minamo-ichiba-com +php-factory-net +suiso-water-com +ateam-xsrvjp +225nikkei-biz +ecoaichi-com +grandia-cojp +yukinanjo-com +seitouen-net +wisdomdesign-jp +hxgjp-com +xinfo592-xsrvjp +homepage-sokkurisan-com +xn--t8j3b4ef5mpcvq0dvb-jp +banbankasegu-com +terada-jpnet +takenakawasai-com +osaka-gentei-com +seasea-jpnet +ccrcjapan-com +otoriyosecurry-com +safety-finance-com +search-c-com +kizuna-cafe-jp +officesam-xsrvjp +ncare-a-ch-jp +ongakujan-com +furano-kankou-com +xsample204-xsrvjp +it-success-net +melissa-acp-com +jeunesse-espoir-com +post-announcement-com +hahacom-jp +ryuka338-com +xn--eckle2a3a6k5eucvec7hu028b33tg-net +osanpo-shopping-com +cross-farm-com +joy-space-xsrvjp +hsk-archi-cojp +roks-dev-com +nikemercurialvapors-com +imaizumi-gardens-com +posao +takahirofree-com +r93yu1130-xsrvjp +pr-jp-com +nakayoshi-hoikuen-com +newral-info +rocket-english-com +at-cynthia-com +hokodate-jp +hokutokeibi-xsrvjp +bicimp-xsrvjp +tennoujiconh-com +fgroup-jp +green-ceremony-com +chip-clip-com +arigatougozaimasu33-com +xsample130-xsrvjp +sample1 +i-exo-com +maruni-seiki-com +skincare-style-info +it-force-info +nekretnine +lgtv-cmp-xsrvjp +tashiro-ent-jp +neostage-info +vixell-net +handsfreevideorecorder-com +sp-shoppro-com +sanwa-de-com +ozak-cojp +slimfan-xsrvjp +remaria-com +towatowa-cojp +facebook-lab-biz +growniche1-xsrvjp +mahae-cojp +atopy-stop-jp +reisyu-xsrvjp +firstitpro-net +affirieman-biz +burlesque-style-com +vegaworld-biz +monochro-org +akashiyanet-xsrvjp +yasutom-com +com10mo-com +ms-garlands-com +soc-p-cojp +transpace-jp +namba-ten-jp +sanneisya-com +risktec-jp +j-premier-com +ikel-cojp +kazukuniyuri-xsrvjp +how-to-wordpress-biz +ngc205-biz +miracle-fun-com +hinoshin-com +dq-sei-com +measurement-labo-cojp +bluetiida-xsrvjp +xinfo125a-xsrvjp +rocksocks-jp +camp-sej-com +takamatsucon-net +shin-ei-kan-net +ecoa3-com +happylifeysh-net +yukakun-com +shinkyuin-com +kansaibridal-xsrvjp +xn--98j8ah3e9333bwksbg2d-net +sumideny-xsrvjp +xinfo768-xsrvjp +muraki-ltd-cojp +alohatherapy2002-com +tryday-xsrvjp +patine-jp +pearl-house-com +test-xinfo518-xserver-com +kawaoto-xsrvjp +sprout-grjp +best-future-net +vontesi-com +aidparty-xsrvjp +testxserverdomain363-com +lineaworks-net +navi49-xsrvjp +xsample319-xsrvjp +m-davide-xsrvjp +bose-xsrvjp +uranai-uranau-com +asa-eirakusou-com +11code-net +xsample59-xsrvjp +4get1self-com +autogalaxy-jp +xinfo101a-xsrvjp +heavendays-net +best-relation-com +shinhwa-fc-jp +tsukurie-jp +ujiharablog-com +tradeli-com +aries8-com +infor-mations-com +hunabashi-bankin-com +oekaki-factory-com +nature-jpnet +inugumi-net +trust-cars-com +lancers-high-info +kitanihon-xsrvjp +xn--t8j4aa4nt10m093dusc-com +xinfo528-xsrvjp +gift-campaign-net +mint-rua-com +magni-hyogo-com +xsample161-xsrvjp +sptm-so-au-com +ys-office-cojp +highkicktattoo-com +tohotv-jp +cocoro-kiku-com +hanakobo-juran-net +heavy-rotation-jp +yoi-hanarabi-jp +perfume-mens-info +yscube-com +mydays-off-xsrvjp +gk-asiapremier-com +oita-eikosha-cojp +kenkoumai-com +freeofferfreelife-com +lscart-xsrvjp +xsvx1024554-xsrvjp +kyushubiz-com +office-tsh-net +dreamv1-com +kite-image-cojp +forvisionaries-com +webbing-hp-com +kigyouka7-xsrvjp +pictonico-com +xsample306-xsrvjp +ringo33-xsrvjp +social-marketing-orjp +testkimuraphp5-com +datumo-asia +ebook-fj-com +min-han-net +j-c-y-com +mitamachicon-com +station-fc-com +net-kigyou-info +office-koi-com +hasegawa-r-com +1stcreate-com +libero-3star-cojp +mokotarou-com +haitai-cojp +mashike-winery-jp +matsuiisamu-com +futaba-dd-jp +rleia-net +artplayer-jp +katadukeichiban-com +foodening-jp +dez-cojp +rio1 +tmdu-mo-com +dabetabe-xsrvjp +xn--x8jc3d5hp94mb34d3m4a-jp +sekainomado100-com +xn--eckg4cd6wc6i-com +1tomodati-com +kessetsu-xsrvjp +thegoldenratio-net +1jsma-com +p-answer-com +tmc-labo4-xsrvjp +ireba-pikako-jp +xinfo559-xsrvjp +organiccolors-jp +pulse-group-biz +xsample202-xsrvjp +direcsion-com +sunny-gem-jp +naps-web-jp +sitifukujin-xsrvjp +saiko-tei-com +inafami-com +chikyukazoku2020-com +webris-net +linesystem-jp +mydoykadoya-com +worthliving-cojp +yossi01-com +ridestar-xsrvjp +light01-com +esprituals-com +phantom7-xsrvjp +xn--t8jxd7cyb-jp +xinfo705-xsrvjp +interior-mk-com +effectorweb-com +xn--24-zb4aym5cqhlgl55v9p2b-jp +welcomeroom-net +afdiscovery-com +3soeurs-com +djc-xtension-xsrvjp +bejilife-info +kondo-shoten-cojp +yushonokai-xsrvjp +nakain-com +29mailmaga-com +stocktonspringsme-com +j-online-jp +dream-mt-xsrvjp +knc-xsrvjp +xn--cckl9b3gza2011c8f9e-biz +olao-jp +kazoorock-xsrvjp +xn--p8jn4h6d6kxd2h2g-jp +yakuzenn-com +jporg-net +biyouch-com +xn--ddk0a0ev93mf5jpqm7g9a01bjvzkyih1est2f-com +primetime-kozaru-com +532up-jp +ryouhei-rea10naru-com +test-xsample320-xserver-com +sukedai-net +thefirststep-info +xn--hck9an9sbc3455c1ye8x1mr56a-net +dogtraining-f-com +nomadaffiri-com +rokkomokko66-xsrvjp +test-xsample316-xserver-com +seo-don-tatsumi-com +dear-wig-com +mailsien-net +ichiko-oki-jp +zipanguhunter-com +testsp12042343332-com +xinfo591-xsrvjp +kencyomae-matsuya-com +kamig-xsrvjp +kaorumorita-info +ariga10noie-com +whiteblackdesign-com +xn--gckvas0t2a-biz +test-xsample313-xserver-com +te-cross-com +machicom-tokusyu-com +satukou-com +dr-monroe-jp +australie +sronetgw-com +pgengo-com +uggstovlarforsaljning-com +machi-link-info +xn--yckc2auxd4b6564dogvcf7g-jp +miyazuke-com +energize-cojp +maedaphoto-net +xn--58j5bk8cwnpc8czq6095c-jp +xn--pckj3hf8gj-biz +koubou-imaya-xsrvjp +test-xsample310-xserver-com +yoloport-com +debcheebo-com +thinkingtest-xsrvjp +kurihara0999-biz +adsky-jp +nakazawa-sekkei-com +xinfo736-xsrvjp +xinfo351-xsrvjp +uwakaishop-com +kawahara0202-com +xn--bcke3b8a3d7d8jlc4234hersb-com +harajiri-com +ebrietas-xsrvjp +s-a-biz +fujiyoshiya-online-com +waocon-xsrvjp +hibio-orjp +hmmr0403-xsrvjp +cnvsx-com +youtube-club-com +med-infom-com +moko-lp-net +logo-r-jp +xn--u9ju31pa341jhjg-com +nxi-xsrvjp +art-repair-com +ineed-jp +nakazono-kensetsu-cojp +jpinfo-you-com +colorwirecraft-com +ogata-h-com +seoplus-jp +apbot-info +nanomi-me +rideout-biz +felice2004-net +shihatsudo-orjp +s-kouenji-net +sunbridal-jp +xsample128-xsrvjp +ujilc50th-net +diginfostation-jp +amigo-latinshop-com +go-nagomi-com +midorimushi-kenko-com +kijima-xsrvjp +shiobara-arai-shaken-com +goodlistener-biz +teradox-jp +jinvtm-com +mahou-awa-com +neco-inc-com +anet-inc-jp +puttyo-com +kitaurasenkou-com +xinfo760-xsrvjp +xn--18j3fvcefx9ttdwi0233e-com +suginami-town-net +lilacgray-com +yaeyama-yacht-club-com +rucion-com +5489-in +tyube-ichinomiya-syaken-com +acda-jp +fisland-info +npo-nsa-jp +ggoodnews-xsrvjp +ryokufu-sya-com +kbr1971-com +taiyo3333-com +cuibap +honmakale-xsrvjp +wvroadbuilders-com +sbjel-info +followmatic-info +eco-easy-jp +k-handc-com +k-decoration-com +slackline-xsrvjp +syufudada-com +acr-net-com +pasokonlife-com +union-bz +satisaffili-com +gshuhou-com +plan-lasik-com +hollywood-air-jp +marutakeya-com +anc58749-xsrvjp +mikanno-com +xinfo767-xsrvjp +yss-school-jp +mitsuo-tosou-com +yamato-pub-jp +kumasyasui-com +patec-tech-jp +madangler-jp +hawk01-com +shinbangumi-net +yamamototatami-com +speedarea-biz +gebo-affili-info +tenposhuukyaku-jp +bicycle-stage-com +crane-xsrvjp +wellsrich-xsrvjp +xn--n8jub7qsb3inewa4c7463e-biz +yata-garasu-info +sporture-tv +clarenet-biz +xsample58-xsrvjp +junkbuyer-form-com +vois-net-jp +howto-manual-net +synchronote-net +tsukeobi-com +npo-polano-orjp +hs-eternalstudio-com +arihiro-gallery-com +weboons-com +grandhill-net +hair-climb-info +kouritu-info +xn--48j1ar8krh1b6dyk4649eygh-com +hp1980-com +xinfo527-xsrvjp +pepentel-com +testdomainx323-com +hokkorisan-net +goken-g-cojp +xsample159-xsrvjp +gomutimes-cojp +junction-xsrvjp +finefeatherheads-jp +tanaka-stn-cojp +nightresort-com +lavous-com +namgite-com +test-xinfo760-xserver-com +hananotakumi-net +xn--1rw4k17v0yq-biz +tikabou-biz +fc-kyoto-info +mms-xsrvjp +gashintei-com +azure-style-com +devayoko-com +xsample318-xsrvjp +yokosakata-com +testdomainx334-com +maki-nao-com +megurocon-com +test-xinfo740-xserver-com +jyokoshoken-xsrvjp +zero-school-com +kyoualice-com +eraberu-hoken-com +pino-books-info +sawada-cooking-net +freedomken02-com +test-xinfo730-xserver-com +best-book-biz +smatre-news-com +rapislazuli0678-com +xsample305-xsrvjp +n-hakko-com +tora-corp-net +tokupri-xsrvjp +dadada000-xsrvjp +test-xinfo719-xserver-com +xn--38j9do54hodfw8a26fyr7e-com +dreamsguide-net +gojune-xsrvjp +transurl-xsrvjp +titan4 +xn--h9j8c2b370ru87b3rya-com +ajiwaiya-gen-com +test-xinfo709-xserver-com +xinfo128a-xsrvjp +horsedealeronline-com +sowa-com-xsrvjp +yumegift-net +specs-jp +beau-magasin-com +code-r-xsrvjp +wakakusa-call-com +nagomi-gh-jp +ad-car-jp +retoto-com +dash2012-xsrvjp +ysvs-xsrvjp +beachbreaktx-com +wa-ipi-com +itasui-xsrvjp +chaos-grjp +akebono-seitai-com +tatsukawa-dental-net +turu503-com +onestead-com +within24-biz +pontajapan-com +wecop-net +voiceapp-xsrvjp +sachinoka-com +aplanning-info +porce-in +f01fuji01-xsrvjp +xn--4gr53rqoa84h99wywlvrxf7n-net +machi-shirube-net +devrm-net +line-t-com +hyphening-com +san-ten-net +tokushima-shaken-com +bath-paint-com +tachikichi6-com +worldrings-net +risounopapa-com +classtream-jp +xinfo558-xsrvjp +sakurafubuki-xsrvjp +morisige2356-com +kami01-com +datsu-genpatsu-info +aaa-icons-com +tanimuratakahiko-com +rougo-asia +susweb-cojp +satoshop-com +best-3-biz +naritatomisato-hp-jp +r313-net +i-himawari-cojp +alice-pg-com +tatsuyafukuda-com +xn--swqq1zt9i4xa94dl3f-net +ouenryoku-net +kinkipesodan-xsrvjp +imagebank-cojp +cycle-esaka-com +mysticalbullmastiffs-com +marukin-ad-jp +itoshima-in +tamai-tsuriclub-com +vinhlong +xinfo520-xsrvjp +e-oguni-com +hamuchiri-xsrvjp +xinfo704-xsrvjp +techno-factory-com +isamuhazama-info +europort-craftrobo-com +kumosukedango-jp +minoru-asia +air-studio-jp +pluton-jp +rigakunishi-com +test-xinfo559-xserver-com +aruz-saifukaban-net +process-1-cojp +smtown-passport-jp +to-1-info +test-xinfo549-xserver-com +herbery-jp +fbms55-xsrvjp +j-officeweb-jp +giulietta-xsrvjp +fstory-jp +test-xinfo540-xserver-com +kawaisakusen-com +takatori38-com +mikelab-xsrvjp +test-xinfo529-xserver-com +xsvx1016120-xsrvjp +iinet-n-com +facts-xsrvjp +rom5-xsrvjp +blend-shop-com +ymtenjin-jp +matsuda-ph-com +test-xinfo520-xserver-com +test-xsample226-xserver-com +ktscopex-xsrvjp +sabre-jp +naritetu-xsrvjp +a-pf-com +minaoshi-1-com +shinsaibashi-yasu-com +test-xinfo510-xserver-com +mint-rua-xsrvjp +enecost-com +ropponginohaha-com +amaeco-com +platform-xsrvjp +ji-ji-affiliate-com +seminar-kasui-com +lsecret-info +ssr-orangetantei-com +frantz-fanon-com +gentei-kumanavi-com +re-use-biz +aononet-xsrvjp +risshun-net +tknd-info +handshake-orjp +tula-xsrvjp +nagoya-adm-com +piers2011-com +smzh-xsrvjp +sokuhoukan-info +furano-melon-jp +osakabijin-com +yumemaga-com +hunabashi-taiya-com +jita-premium-com +gogobusiness-info +testxdomain304-com +cosplaytravel-net +lockone-service-com +class-shonan-com +career-staff-com +dk-daiko-com +greaterpittsburghciogroup-org +ikebanaohara-com +sv1st-com +testxdomain309-com +net-cross-com +tassmania-xsrvjp +digital-crest-tv +zenyokukyo-xsrvjp +spsjapan-com +enchan0408-com +bbphp-net +500yen-net +shigesg-com +eastadventure-jp +xn--y8jp0mua-jp +tanio-hoken-cojp +mamanurse-com +wscape-xsrvjp +tsumekusa-net +xn--uckg3gj1hd8c0399cdf1bux7bxfd5ub-com +bisamobi-xsrvjp +dcsblog-xsrvjp +iyasinoamore-com +tm-ad-xsrvjp +naotjewelry-com +chapter-xsrvjp +nosyoko-jp +worldwalker-jpnet +saiseisuru-info +tobiken-divetofree-net +xn--88j6ea1a3393bhfatv1xi104a0ln8if-biz +xn--cckj1c2j2bwf6044bomgf76b-net +uoshige-biz +kyoto-rentacar-com +nki-print-com +hotta-ganka-com +cancionvivaradio-com +bavi-plh-com +hmhits-com +tkt-center-info +waisu-xsrvjp +cs-delight-xsrvjp +fairyparadise-com +k2k2-jp +prenew-xsrvjp +gifu-notiku-com +sol-tec-cojp +sorahime-com +db08 +gates-xsrvjp +xinfo738-xsrvjp +soooooooooon-xsrvjp +marukyo-net-cojp +kimono-united-com +denebola-jp +4-lips-com +e-melon-net +cocowa-net +yasumuro-plan-net +apurituru-com +xsample127-xsrvjp +napoleon-hill-jp +atelier-iki-com +fushimiyoujien-jp +bikkurimeisi-com +test-xinfo359-xserver-com +fine-one-jp +nishiumeda-yasu-com +chaigo-info +smile-switch-net +sidebrains-xsrvjp +test-xinfo349-xserver-com +shinnihon-koukoku-tokyojp +whyling-jp +kinshichodecon-com +takagi-biken-net +test-xinfo342-xserver-com +yamamu-net +bayshin-craft-com +ke-tai2-com +air-i-xsrvjp +golfwalker-jp +xn--zckwa8eyf040sre5d-jp +tanu3355-xsrvjp +smedic-xsrvjp +roi-pro-net +eurotexjapan-com +fukuokare-com +shiminuki-takedaya-com +aplan-house-com +seisenryo-jp +nmf-acjp +magokoro-ticket-com +td-honeywife-com +broslink-net +fans-xsrvjp +wintechnos-xsrvjp +junjimu-net +uggeinkaufenboots-com +mt-east-com +koubesannomiyacon-com +grk55-com +ktstv-cojp +lookphotography-net +hanagasa-net +kouta-zero-ism-com +xn--n8jub3du45qx2ykjyeow-com +kamineko-info +maru01-com +houmu-jpnet +hatakeyama-dc-com +lockey-group-com +ohariko-onaoshi-com +windows-nt40-com +kishokai-orjp +golfer-apps-com +enshunavi-xsrvjp +d-k-o-info +flowertriangle-com +infonity-jp-com +ariake-oak-jp +u-fphoken-com +tryforce2000-com +hiyoko-f-jp +kikoijapan-jp +xn--dcklt3fn2gyc8fzfz425ac76g-com +easelhome-jp +sakuragaokacon-com +picoslab-com +jack-o-lantern-in +ryumeikan-tokyo-jp +pongsityume-com +kotoni-copint-com +futamura-orjp +gamification-marketing-com +self-shop-com +pitat-nabari-com +xinfo526-xsrvjp +xn--i8s707c3pk-com +xn--cckyb9em8gz324b8q4a-com +tax-adviser-info +xsample158-xsrvjp +jouhoumax-net +nccard-nejp +gyousei-jp +kizunakeikaku-com +b-partners-xsrvjp +saintegenevieve-org +yamashiro-onsen-com +xn--eckwb2en5f611vnxq34uzyntm0b3z9b-biz +fukugo-jp +xn--n8j0ao7f9a7304bz1zayk3ai7h-com +ogikubo-i-com +hino-comu-com +surutan01-xsrvjp +xn--u9jz52gfmk3iag51dizovw7adwx-net +eigode5-com +satonoria-com +favori-tokyo-xsrvjp +elieze-com +basketballshop-legends-com +onishi-housing-cojp +xn--yckc0gk6h-com +taiyok-cojp +hiro-ok-org +cpa-library-com +xsample304-xsrvjp +motherlip-net +kosodate-saitamajp +kawata-s-com +yonehouse-jp +syuluxnxn-xsrvjp +movie-sign-com +issinmaru-com +after3-in +tsukumo-biz +kk-uchikoshi-jp +ohimachi-net +solare-hirao-com +bookend-cojp +uxf-xsrvjp +j-acp-com +xsample214-xsrvjp +nikibichiryou-info +art1922-xsrvjp +spainbar-gracia-com +yoppi-asmec-com +menuetto-net +gooddealing-com +xsvx1020066-xsrvjp +toukai-shaken-com +hp-mail-org +aliciaadamsalpaca-cojp +neko-z-com +mm-design-jp +takuryu-jp +getmoney-2swordstyle-com +bellrin-com +kbit-cojp +kawaehonpo-jp +xsample88-xsrvjp +webstyle-xsrvjp +konosoranohana-jp +itamiakira-jp +mushinashi-kaiteki-com +nisinihon-info +densiteikan-com +sophora-xsrvjp +lcc-sky-com +inaka-pipe-net +sodsugar-com +ysai-xsrvjp +tsuyakuguide-org +ohashi-eye-jp +xinfo557-xsrvjp +yiizhu-com +nichido-monthly-tokyo-net +hotbook-biz +studiokakita-com +ecomohonyaku-net +kuroda-studio-com +xn--cckwcxetd-jpnet +morise-kaigo-com +tdg-okayama-xsrvjp +gucciz03-com +kobeco-xsrvjp +livuhey-com +auctionsite55-com +kakogawa-matsuricon-com +1102k-com +standup07-com +eco-h-cojp +bluefreeuk-com +w-server-jp +id-cmp-com +takajin0524-xsrvjp +kakushin-group-com +dragoon75-com +gasho-an-com +designam-com +yokoya-xsrvjp +xinfo703-xsrvjp +e-buy-cojp +koukokai-jp +matsumura-parts-com +skytown-jp +builderyoshi-com +tenkokuart-com +la-beaute-info +gion-kyoto-net +future-c-net-jp +matsuiseijyo-com +sonejapan-com +snopiek-com +aemk-orjp +la-mariage-cojp +louis777-com +keih87-com +y-motomachi-com +innosence-xsrvjp +bizcube-jp +shumiplus-net +hibinoiro-net +design-symph-xsrvjp +accettazione +kawakamiya-com +htwing-com +450k-net +palca-jp +ii-anbai-net +violinjp-com +dougaldrich-com +llpbookend-cojp +livre-jp +lifeinnovation-jp +kishiike-com +xn--v6qz1w6gr96i6dfrk9a-com +mentor0511-com +junkbuyer-ipad-com +xn--2-jeum8gra4a4456m88i-biz +hirayama0925-xsrvjp +hirokouren-kango-net +taikou-kensetsu-com +hatakawa-aijien-com +curtain-semi-com +tomomist-net +kawaimagokoro-cl-net +nippon-wine-com +ishis-piecemontee-com +kenbungaku-com +kitano-sumai-jp +notch-cojp +guramu-net +tensodo-xsrvjp +air-upt-e-com +excel-xsrvjp +conferlist-jp +xsvx1011070-xsrvjp +ncp-acjp +tmre-jp +xsvx1014136-xsrvjp +sinmiura-jp +purple-dahlia-scene-com +xsample231-xsrvjp +unveil-xsrvjp +29014-info +iwate-megabank-org +friendear-net +xn--u9jz52go0jr6al94bizovw7ajzq-net +nksj-car-com +tanuchan-in +ihara-web-net +moekoosawa-com +japanpage-jp +yutakakk-xsrvjp +vegelife-kouso-info +takatora-xsrvjp +ia-project-mobi +o-bikers-jp +fly-solotravel-net +hokkaido-partners-cojp +takenogakkou-xsrvjp +shirobei-com +kumagayacon-com +ishida-s-net +xinfo734-xsrvjp +xinfo348-xsrvjp +ebizou-info +xn--u9j8frbzkk62uxef9lo-com +torijiman-cock-com +xn--t8j4aa4nwj5byg4ih4e4eb6496q264d-com +xn--czr78lt57a-jp +atnworks-xsrvjp +labayj-com +japandental-cojp +youvision-xsrvjp +kokorozashi-jpncom +health-support-japan-com +yuto-nakagawa-com +xn--w8j3k0bua1eyf0cz786bewa-com +cinselhaz-com +nara-con-com +xn--fx-mg4avc2gyk-biz +green-pocket-toshima-com +as-job-com +chapt-info +market1-xsrvjp +nats-planning-com +kandkcollection-com +chayamachiconh-com +fa-style-com +premiermember-jp +sg110 +dreamstage-info +noto180-com +superdice-net +xsample126-xsrvjp +tula-cojp +bur-juman-com +ip-agent-biz +creative-h-cojp +pw-wedding-com +ryuuseinogotoku-trend-com +shoukichi-org +nishikawa-shika-jp +rom-jsys-com +skampermusic-com +otopost-xsrvjp +cpe-s-com +wincul-cojp +gluck-jp-net +ikeikehiroshi-com +clinicalart-atelier-nae-com +japandental-xsrvjp +popteen-jp +xn--ecki1b5br0ae8iyd3due-jpnet +kango-fair-com +membersite-xsrvjp +first8-xsrvjp +cpa-toyohara-com +dosakan-net +ebisuconpa-com +kiyox-com +onoue-tatami-net +a-zu-net +shimokita-con-com +izunet-jp +ac6162-com +plustest-xsrvjp +wcd-xsrvjp +design-pack-net +m2k-xsrvjp +hiroshima-cu-net +wadai01-com +4ta9-com +maido-navi-com +hando-law-com +mugenprtest-com +naomicious-com +otanjoubi-xsrvjp +xinfo765-xsrvjp +xinfo380-xsrvjp +health-beauty-no1-com +pt-algarve-com +ypj-jp +suzukisanfujinka-com +with-music-net +cnet105-xsrvjp +bear007xp-xsrvjp +easternstudiesdatabase-com +kizna-club-com +colkdesign-jp +yscube-xsrvjp +biz-abroad-net +yuri-akitajp +nyudani-net +m884-com +himono-hashidate-com +xsample56-xsrvjp +novum-jp +ayumi-k-xsrvjp +funabashicon-biz +r93yu1130-com +okiattend-com +umeda-yasuhei-com +xsample230-xsrvjp +mailinfo1-xsrvjp +49764mominoki-net +koreaichiba-jp +affiliatecenter-net +dogrun-navi-com +xinfo525-xsrvjp +clubking-xsrvjp +unveil-cojp +hirano-unyu-cojp +u-801-com +validate-js-com +xsample157-xsrvjp +nm-archiseeds-cojp +totoka-jpncom +d-fuctory-com +g25krishna-info +jimoez-com +b-town-jp +mocomocolife-com +xn--n8jubz39q56dpy2a01gj60a-com +tenpoukaku-jp +si-himeji-cojp +minamoya-info +obatec-jp +sharedsoft-com +drcaco-jp +bni-kinshachi-com +cocoon-wave-com +tenmabashi-yasu-com +prenup-hiroshima-com +elm-gakuen-com +sonejapan-net +xsample303-xsrvjp +takezo-jack-com +bihadadou-xsrvjp +bonic-info +mensato-xsrvjp +dr-monroe-cojp +tk-housing-jp +kumamotoshi-shaken-com +charis-arts-com +xn--qcklm0bzd1dvkk82tdv9au53b-com +gekiumawin-com +kt-manage-xsrvjp +olao-org +xsample146-xsrvjp +jp-harg-jp +tujitaiga-info +leo-house-com +slackline-jp +learning-with-me +afi-hisyo-com +valuation-cojp +an-aim-com +misyukudecon-com +tmatoba-xsrvjp +rouet-jp +chinzao-com +okitokuski-com +kosuge-shimazono-com +a-mue-cojp +jqa +sanpachi-cojp +tachikawa-town-net +mudage-asia +bennpi-asia +xsample87-xsrvjp +niigata-tatami-jp +fujihomes-xsrvjp +tkrd-biz +yamatsu-kk-cojp +xsample110-xsrvjp +syowasangyo-jp +webelement-jp +saori-mizuno-xsrvjp +takayamaseika-cojp +ctwpromotion-xsrvjp +firstitpro-jp +over-flow-net +xinfo556-xsrvjp +lopple-jp +senkyo-goods-com +codenight-com +fours-cc +jmp7-com +torunkmr-xsrvjp +e-hda-jp +inforeport-jp +saito-takao-com +interplan-xsrvjp +ryokan-kaikou-net +mayumi-fukasawa-biz +shimane-u-xsrvjp +iris-scarlet-info +sallp-xsrvjp +xn--qckyd1cv50v-jp +music-spark-com +ishibashiblog-com +synx-in +netbisiness-saikou-biz +sstechno-cojp +damedamefx-com +afs-net-com +red-con-xsrvjp +raimu-nagasaki-com +toda-shaken-com +koike1265-com +shikitsu-net +dai1cred-com +esute-tv +xinfo702-xsrvjp +takaranoyume-xsrvjp +kuushitsu99-com +kogakusatei-otasuketai-net +xsample334-xsrvjp +kontown-jp +easy-style-jp +officebeans-net +hiroakix10-xsrvjp +bd-gs +ssijp-net +sevennananana-info +tokuyama-rh-jp +funabashi-xsrvjp +pccqq-com +iyashi-kotsubu-com +granheart-jp +pfolios-com +tecjapan-biz +tukkysedori-com +clear-healing-info +wakeyumeup-biz +wtte-info +saimuseiri-hotline-info +jolijoli-jp +nanbaconh-com +kamikawa-xsrvjp +dr-monroe-biz +330hm-niigata-cojp +credoseitai-com +doublesteal-xsrvjp +smbanana-jp +sakamotokogyo-com +tyrant +iris-accounting-com +saitama-souma-shaken-com +hyugaginou-com +tenderlove-pcb-biz +japantn-xsrvjp +xsample229-xsrvjp +miyukinoko-com +shahzad +sakai-yasu-com +kaminaritei-cojp +ikegaku-org +yu-touch-com +syoujiki-com +eikeis-com +kakeru-net +2ch-kakunou-com +yodoyabashift-com +tomochige-com +xinfo159a-xsrvjp +shoppingphone2-info +enkadestar-xsrvjp +overload-nejp +gyouza-cojp +viewland-jp +the-xlive-com +ishizo-com +doghug-jp +tamuranouen-jp +xinfo733-xsrvjp +xinfo347-xsrvjp +clayblock-info +techno-energy-jp +xn--n8jl84alc9fsf5446c-com +corepan-com +freedrapery-com +galpachi-tv +mariage-space-com +cocowa-store-com +santam-jp +touenji-jp +pharmaco +hellohello +home5 +ns301 +z3 +ns242 +kakeibo +curling +webdisk.ls +yubi +ftp46 +mwtest +ssl0 +zerr +test-mobile +mutsumi +kibinago +absinthe +deeper +brenna +www.bellevue +zelos +space8 +yaho +xero +ein +sunshineboy +chartreux +srv51 +bluewhale +tida +masai +genso +turnos +kind +rembrandt +smilehome +whim +lifehacker +residential +weby +www.repo +msn04 +wlsy +intradoc +dds112 +www.peach +ycbf8 +himitsu +vitoria +suap +results3 +very +twix +detsad +unit +gubernator +pereselenie +tete +ctmx +kands +gakushi +takeru +mahoroba +www.funtime +www.preschool +preschool +www.others +www.preteen +preteen +highwind +nambu +gulf +tott +udin +teaparty +mh4 +tkog +maruzen +kuruma +tion +neustadt +devz +nss1 +www.okami +snaps +www.personals +hijiri +img.new +soda +globalwarming +slit +www.greencard +suk +tbox +jtc +kcj +macca +kyoka +sinsa +siso +www.izumi +stepup +siho +justhere +vilya +katsuno +roya +board2 +rowo +yamatai +shaa +fs32 +pdesk +tokidoki +kawabata +nodo2 +nmk +nodo6 +nodo7 +nodo8 +nodo9 +nodo10 +nodo11 +takanashi +nodo13 +nodo14 +hks +flt +appmobile +seah +versailles +angelle +azurite +bkc +gudrun +teamfortress2 +aol2 +craftcrazy +kamal12 +www.craftcrazy +sant +berthe +sbbs +sylvie +softwareinfo +www.softwareinfo +buratino +san3 +zork +sna +www.69 +print2 +www.62 +webfax +rian +www.58 +www.57 +www.54 +www.03 +www.01 +qnet +anita1 +zakshop +courierjournal +gilgamesh +upgrading +irregular +freeshare +www.autocad +vs11 +berenson +mazika +soss +thenewage +darkrealm +pray +gamgam +pote +webdisk.learn +us02 +us03 +www.desaparecidos +guizza +muscle +ppc3 +ppc2 +prince93 +accademia +avco +spxw +jxzyk +pipi +juso +jyxx +valcea +desaparecidos +hawks +animeclub +piel +orai +oraa +maw +idi +www1.n +redis1.n +db1.n +oooo +workers1.n +dairen +peoo +bikers +cfa +rv1 +phpinfo +www.comune +www.hosted +princeofpersia +icont +www.was +paky +pala +rcl +chaoyang +www.wolfpack +www.nwa +www.igs +wardo +testwap +bello +zhongshan +huabei +parus +minzdrav +yuncheng +lanzhou +ust +waves +xtreme2 +minsport +hangzhou +nong +kinomax +minobr +autenticador +spamwall +siping +guilin +autodiscover.property +fpga +shijiazhuang +murf +newpro +pfj +mumu +ryanm +mubi +niza +abtest +obey +ycopy +pusatsukan +www.publications +bendahari +ezra +electrical +sitedev +mond +node02 +uweb1 +pusatislam +neti +cil +pusatkesihatan +miqbal +dwa +fbdev +productie +cere +keselamatan +bids +sibnet +bumbum +yoitsme +mob1 +shapley +ioncube +lure +zim1 +autoconfig.espanol +mmm3 +autodiscover.espanol +webdisk.espanol +www.deutsch +luny +pendaftar +luco +redac +mjmj +mso +miru +intsys +autoconfig.stiri +www.question +phpmyadmin.test +autodiscover.stiri +mimp +miky +www.philos +webdisk.stiri +mx2-out +autoconfig.s1 +autodiscover.s1 +nabi +miga +abramo +lona +kawaly +www.2b +dowcipy +www.ping +hiltonhead +katalog2 +imprezy +pcn +denim +mela +autodiscover.team +downloadcenter +autoconfig.team +dzzw +prenota +olc +ktkr +telnet2 +lfe +conference2 +topspin +mamu +malo +magi +krol +crussell +maed +foods +quill +marron +forexreviews +www.imgs +pitch +steamboat +kyoshin +d02 +aventail +mbanking +ssl21 +webmail6 +seattle2 +ssbprod +juna +gdg +gway +autodiscover.edu +laon +lfa +myservices +buildbot +www.usosweb +www.mbp +izzy +kidz +phpfox +www.tokyo +milionar +miliardar +uus +joon +jong +ethiopia +killroy +kep2 +yarrow +mail.c +publikacje +uniba-gw +tellmemore +karb +wapgame +testowy +hjxy +ebook1 +sjy +irim +flynet +immi +masterminds +icar +husi +vms02 +manawa +mystere +vms03 +p03 +monsite +thorax +huge +mailwall +jaun +assalam +automatic +lavigne +ip69 +info-tech +iax +hoot +pstutorials +sanu +travaux +uyu +iden +testdir +testmap +autoconfig.dvd +gumi +autodiscover.dvd +guma +ibon +hiya +orderdesk +dcg +grip +woori +manten +gpis +abstest +nsmail +italie +duitsland +manner +authorize +manman +gkgk +wpdev +data01 +www.front +neighbor +inmemoriam +otic +hibiki +expt +fois +mandoo +mi5 +xp1 +s250 +ovis +was3 +aweb +cassettetape +fairytales +mmedia10 +apd +envi +elle +l226 +infocomp +aquario +betavmadmin +fhweb +l007 +l004 +dorr +images1e +eerr +advantage2 +c119 +c129 +c249 +ip11 +a23 +ny-site2 +a35 +a36 +a37 +a38 +a39 +a41 +a42 +a43 +a44 +a88 +webdisk.2013 +rahuls +eazy +mama11 +gamesd +www.wbg +noscript +byun +cord +como +coil +derf +direktori +pemilu2009 +cobi +toolserver +quad2 +bubi +dask +quad1 +rbh +proxie +kph +azzi +hideout +betavmadmin1 +ciel +axyz +chie +chee +b54 +chap +idl +b56 +auna +b58 +bot1 +bois +b62 +europe1 +b64 +beta-inventory1 +tera01 +awin +b68 +atto +aszx +ccem +betavminventory +b78 +sp2digital +lioness +ch20 +swh +ch13 +b87 +b88 +bion +art2 +aple +b96 +anen +amam +rmm1a +ajun +aeve +www.arhiva +rmm1e +aeco +acre +acle +tsubaki +wonderboys +aang +abad +mbs01 +nml +jdesign +decoder +rmmglx0 +sasamoto +aube +saturday +rmmglx1 +d211 +careerfit +proba1 +rmmglx2 +onboard +webcm +pahan +adminapi +dpus +rmm2u +inventory2 +upload2u +looloo +kesehatan +hoja +c77 +demolink +orson +www-tst +hellos +c001 +betavmad +xenserver3 +myvalentine +mol +distribute +wildwild +xenserver4 +toytoy +marrakech +mahogany +evil666 +urmom +d38 +d52 +mi-lvs1 +d57 +d58 +d59 +ny-site1 +d62 +d63 +sliders +d65 +images1u +d68 +d69 +d71 +d72 +d73 +d74 +d75 +d76 +d77 +d78 +d79 +d81 +d82 +d83 +d84 +d85 +d86 +d87 +d88 +dpreports +d94 +d98 +right-click +moonwalker +tumbleweed +e14 +cci4admin +mbase +pref +cis2 +creditosgratis +pjocuri +manchesterunited +verus +skydrive +adam20 +i18n +images2u +delhtca06 +invetalcom +e99 +eurasia +f15 +delhtca05 +emobile +g18 +delhtca03 +ministry +delhtca02 +cm8testdigital +h92 +h93 +h94 +h95 +h96 +h97 +h98 +h99 +j24 +rmm1u +k23 +l11 +upload1e +a102 +a103 +a104 +a105 +a106 +a111 +northridge +w28 +ny-lvs1 +m73 +m70 +pramod +ny-lvs2 +pradee +zonedirector +longtime +www.mikail +tvbox +betavmad1 +cci4ad +upload1u +b116 +pa-lvs1 +pa-lvs2 +gamesplanet +btest1 +www.offtopic +zolb +mms.noscript +yuli +yugi +thequeen +www.evdenevenakliyat +longtail +mail.ucakbileti +www.chivalry +xyzx +healer +wrangler +we2 +papercup +newsmail +b150 +operahouse +www.turkforum +jyoung +www.west +toon +www.haxball +highcolor +b160 +b162 +b163 +zary +b190 +www.highschoolmusical +b240 +c110 +c111 +c112 +c113 +c114 +c115 +c116 +zis +www.the-best +c118 +c120 +c121 +c122 +singo +c124 +c125 +c126 +c127 +c130 +c157 +mail.android +c212 +c213 +c216 +c241 +c242 +c243 +c244 +tokki +c248 +xtc +pentagon +wartime +allfree +f105 +f106 +f109 +f112 +paramount +f168 +yese +wweb +h102 +h103 +h104 +h105 +h106 +h107 +h108 +h110 +h112 +h114 +h115 +honeydew +h117 +h118 +h119 +h121 +h122 +yeah +takashi +h125 +yala +macvpn +maninder +chaterbox +www.ilahi +h135 +vyas +uzu +xela +h138 +fuentes +parole +www.eyebook +partho +wapi +pathak +onlinejob +vish +poweruser +rs01 +h151 +wadi +h153 +deadline +doktor +tung +vara +h211 +h212 +vanu +vani +szyx +h217 +h218 +h219 +h222 +f250 +i123 +i124 +i125 +i131 +thecracker +suffolk +k139 +testprojects +a59 +immo +megumi +syw +b60 +b70 +servicemaster +b90 +tomm +e104 +greenish +ambroise +c90 +luminous +payday +d70 +d80 +suju +sukh +turbo1 +tb2 +h109 +meitan +h124 +stim +www.badcompany +tiwi +paxton +title +starx +pchelp +reminiscence +h220 +a205 +ssad +paypol +www.hifi +sensations +b159 +ryuk +spsp +painel2 +anchovy +cometa +chicha +sory +pazuzu +spfm +anonymous2 +soga +smps +ns.img +test.my +omaker +rongcheng +chirashi +creeper +source2 +xgeneration +onurair +ruma +atyourservice +adldap +biznet +fireheart +thy +jstreet +skol +lmy +lnk +ws05 +ecredit +sunexpress +anadolujet +borajet +taky +autoconfig.bm +macherie +webftp2 +autodiscover.bm +shut +kkt +webdisk.bm +www.renaissance +s1025 +s1026 +s1027 +sidi +kickass +sid1 +theagency +rots +s1028 +s1011a +s1011b +teeth +ini +jee +fq +ext00 +mydb +jby +coretest +team5 +romy +s1032 +shindou +s1033 +s1034 +sens +s1035 +selo +s1036 +s1012a +roan +gse +s1012b +s737ipmi +s1013a +s1013b +s4348ipmi +s1014a +skyofking +hatred +s1014b +s101a +s101b +gks +riya +slime +s4354ipmi +s1015a +sakr +hag +s102a +evv +sajt +carlyle +s1031 +polyglot +wfl +s1016a +saim +s103a +rbg +uniq10 +flowerpower +s103b +s1017a +itsmine +yoghurt +tasak +partyanimal +beta2013 +wmin +stride +mobile-device +mortgageapp-pa +documatix-slc +propointslave +documatix-den +mail5.mail +mail4.mail +mail3.mail +click1.mail +mail2.mail +mail7.mail +myarticles +mail1.mail +mail6.mail +s1017b +k55 +geovany +s1102 +ironport01 +dhk +s104a +webdisk.123 +boring +scandal +shock +inton +noproblem +chachacha +s1105 +shiri +skyblack +12345678 +s1106 +jworld +rida +gdz +s1107 +mandark +ceylon +s1108 +websitehosting +s1018a +rere +s1018b +oneandonly +sevgi +s1112 +webdisk.me +rena +melancholy +soapbox +feed2 +s1113 +seory +s1114 +pinata +ws190 +trickster +ws203 +ws195 +ws199 +ws200 +ws194 +sekai +s1115 +ws120 +interlude +ws179 +momofuku +ws183 +ws184 +ws185 +ws186 +ws187 +ws188 +ws189 +s1116 +ws193 +ws196 +ws197 +ws198 +rye +ws180 +clamav +s1117 +snl +mytest1 +s1118 +s1019a +hamham +s1019b +scoop +scene +s1122 +s1123 +s106a +pop.test +s106c +vide +s1127 +s1128 +sugar03 +s1021a +bmfresh +s1021b +pros +rafi +www.back +furukawa +s1132 +mysql21 +goodshop +s1133 +boiler +radh +s1135 +s1136 +storyteller +hmaster +youngjin +manni +s1138 +choopa +s1022a +8.ab1 +pras +freeid +prag +michelle2 +fwp +connectra +s1022b +moran3 +ltm +hollyweb +aviram +s207ipmi +s108a +poma +continent +s1023a +1.ab1 +1.ab4 +pogo +iomega +hitachino +2.ab4 +ouma +s1023b +s1334ipmi +osun +pixe +goodmail +3.ab1 +videoz +qais +bidon +pips +webdisk.pro +associates +micro6 +micro5 +micro4 +micro3 +shirka +s1024a +w00w +w00t +s1024b +opic +s111a +s902a +pero +s1025a +s1025b +s112a +s112b +s1026a +s1026b +s213ipmi +s113a +s113b +paps +s1027a +flex3 +flex2 +okay +s1027b +upw +paki +rab +scamper +lazare +s114a +s114b +s114c +tusa +trinity2 +lea1 +bkmain +masquerade +netwise +doron +31337 +mss2 +parkiet +s1028a +twyla +cotopaxi +pc158 +puchi +anir +sonny2 +nomen +mylab +rzadmin +s115a +nosa +s115b +s-gcclientadmin +eaglegatefa +mmconline +s-gcglobaladmin +americansentinel +globalview +s-gcwebserver +neci +globalcampus +reportservices +s-gcwebservice +pophost +s115c +cosc +nomi +stripe +bbsm +nnnn +s1029a +s1223 +s116a +s116b +survival +hazmat +www.hrd +builder.login +sunmail +fabrica +ranga +withlove +www.hazmat +navigare +mums +stefanie +s116c +www.livezilla +epbx +muni +dobbie +rabin +muko +bkp3 +muki +store8 +muka +fcs2 +s1031a +emptiness +painel3 +bdirect +acri +portaledu5 +s1031b +dnsnowy +msky +planete +s117a +s117b +s1236 +s1237 +s1238 +s1240 +s1032b +datenbank +s1242 +www.belka +ps137 +ps136 +www.grc +suffix +goaway +s1243 +popol +goodgame +s118a +s118b +moti +mosh +mssql10 +bloomberg +s1033a +free24 +s1033b +campusnet +s120a +atlas1b +netz +atlas2b +lqfb +tase +out33 +s119b +blumen +passive +tw1 +moju +seasonsgreetings2012 +secureportal +s4111ipmi +mohd +ostar +treasure +beautifulgirls +s1034a +s1034b +hortus +vendorportal +blooming +s121a +s121b +neek +maringa +s1035a +mail1-uk +s1035b +s122a +lsn +kfq +boz +s122b +withyou +s1323ipmi +layback +kvm2-1 +kvm2-2 +kvm2-3 +liya +mfp +napo +nang +perfection +jnj +sk01 +applepie +s123a +s1test +www.010 +s123b +www.kf +synchron +test12345678 +www.pv +www.sw +www.sz +t168 +pennylane +www.xz +s1351ipmi +optimal +www.yx +www.yy +www.zx +webdisk.jd +sunglass +appellini +hanjie +databassano +s1037b +s1302 +sitetest +www.gk +lavish +s1303 +hjys +s1304 +s1305 +weld +loth +loni +s1306 +opus2 +opus1 +www.66 +0701 +tissue +0912 +s1307 +www.xj +s1308 +www.shy +www.zang +zang +s1038a +ciaociao +animecity +ollie +embrace +wmsaiglive +keypartner +s1311 +meko +s1312 +s1313 +kusu +s1314 +redribbon +trickstar +soundwave +s1315 +director2 +sdev +font +med1 +perido +s1316 +twdvd +mail.vul +blackswan +mail.3d-sex-and-zen +mail.pornhub +6k +pornhub +xvdieo +router01 +4399 +s1317 +s1318 +kudy +mail.4399 +mail.xh +mail.bbs-tw +mail.ben10 +instagram +mail.twdvd +mawi +mail.xveedeo +noway +s1039a +mail.fungo +s1039b +disscuss4u +666pic +mail.666pic +3d-sex-and-zen +contabil +mail.01 +poki +999av +s1322 +636 +s1323 +mail.ff +s126a +mail.qk +s1325 +mail.thisav +mail.tube +mail.xvideos +edulis +toyworld +discuss4u +foodie +lanice +santateresa +mail.xvdieo +wagdy +graylady +mail.6k +mail.u6 +thisav +kkclub +mail.kkclub +bbs-tw +blackpink +mail.ut +mail.zgame +mail.999av +xveedeo +mail.tt1069 +greengreen +mail.disscuss4u +andantino +mail.sex169 +mail.discuss4u +s1326 +mail.redtube +s1327 +hdcctv +arecont +iqeyeedge1 +opssecurity +iqeye +vivotek +zseries +americandynamics +stardot +codero +sserieslite +s-series-demo +evapi +axisedge4 +axisedge3 +axisedge2 +axisedge1 +evwebtest +exacqsecurity +adtrac +fourboard +sanyo +iqeyeedge2 +kermit30 +cesars +laurac +s1328 +s1329 +hilltop +test77 +s1331 +montblanc +s1332 +betaforum +newskin +breaker +gso +s1333 +nismo +mahi +s127a +s1335 +clayworks +queserasera +bluejam +gna +teardrops +jurist +s1336 +pillars +nicki +wimax +s1337 +pallet +s1338 +koon +mailbak +s1339 +geopia +touhoku +dazzling +golfclub +newsa +rbldns +wsdy +selection +fukfuk +zeropage +nella +blackhand +s1341 +abaris +s1342 +s128a +simak +kaimin +gender +s128b +s1346 +minero +lusty +michelangelo +s1347 +aconite +creativeweb +s1348 +phone7 +disabled +tent +s1350 +leng +s1351 +s1352 +naver +nagara +trams +fogbugz +mixed +namoo +slxsync +qa.congressional +isbnws +test.pubeasy +testbed1.congressional +mpe-web +nebula-dc4 +imageweb +beta.bows +s1353 +marimari +testbed2.congressional +beta.isbnws +mydiary +pubtrack +syndetics +securenfuse +olbers +prediction +leka +seventeen +s129a +takenaka +sunsoft +mailorder +amedia +serverreg +s129b +olink +neogene +ldapprod +dr-vpn +learntest +admin111 +student-dev +worldpeace +s1356 +auth-dev +shine99 +cloudcity +s1357 +s1358 +lotte +aixin +itedu +ebl +s1359 +bberry +s1361 +s1362 +infoblox +mbs2 +starlet +neuf +thefly +monello +phantasma +moji +s1363 +shimoda +www.rw +s1364 +s1365 +s1366 +s1367 +s1368 +yacy +pdu-a1-1 +apmail +pdu-a1-2 +thebox +jportal +muttley +s1372 +backroom +autoconfig.au +mutant +blauer +s1373 +s1374 +duckduck +maid +s1375 +breed +cvsup +s132c +meetu +haitham +s1362ipmi +bgw +yorokobi +lumi +s133a +www.mil +ecohouse +kwang +lame +webdisk.origin +div +bridgit +martinique +norisuke +mislab +ysl +behavior +mpx +webhost4 +vagues +s133b +yks +yhk +asdfas +hal9000 +thinkagain +nari +parkman +wmi +xax +carmel +store3 +store4 +store5 +wad +hone +tyc +basestation +uhc +resource1 +um2 +tob +hoco +tmh +sfd +hkit +spz +solitaire +zed +tgw +s133f +wins02 +s1391 +attis +s1392 +neuronet +googoo +s1395 +cas6 +dill +hypnotherapy +lisp +s1408 +directmail +costume +rook +s1399 +jord +jsbach +s1411 +mbook +maybe +joyfull +w0w +pok +sudhi +s1412 +own +otf +paraiba +www.norilsk +dmitry +pjm +dinc +pel +allergy +calec +noo +myb +juiced +ds2012 +atama +chun +wakako +vanquish +bucuresti +nmm +bw1 +webdreams +odi +ode +nkc +no9 +psnext +keno +topaze +wiser +nn2 +mnj +nee +luv +mmd +myfreedom +dlinks +mjh +frogger +naruho +kus +maruo +smtp03-out +liv +kow +ansy +farmhouse +merami +joce +keka +aff1 +ker +joh +kdw +nozawa +hisashi +nagios01 +hyp +krolik +hyj +funakoshi +ine +hsj +pinkpearl +jai +auth02 +auth03 +marvin2 +iie +pitcrew +iez +sunray3 +manara +gwmobile +iaa +webreporter +kronus +moose +s1367ipmi +s1432 +dc1.ad +s137b +miv +ms0 +her +hdw +hek +hdp +gnf +linebreak +hdk +happytrading +tasuke +huckle +cisco8 +autodiscover.whmcs +s1437 +hci +autoconfig.status +n26 +s1438 +n21 +hai +autodiscover.maps +bakchos +s1439 +autodiscover.status +autoconfig.whmcs +manja +autoconfig.maps +webdisk.maps +ynb +duvar +cvport +s1441 +rjh +icache2 +misite +www.sevastopol +s1442 +s1443 +s138a +bebeto +dbserv +s138b +astore +kshop +open2 +24x7 +exe +eur +bdw +s138c +project5 +bf8 +epl +s1448 +s1449 +nextworld +fam +renovatio +csh +mrgreen +newgen +s1451 +kbot +soest +ayo +kaza +angelstar +autodiscover.youtube +autoconfig.youtube +domen +transhelp +s1452 +s1453 +www.uv +s140a +specialevents +www.correio +lls +pocas +s139b +vhost6 +s1456 +blg +s1457 +twcprod +tdgdev +dragun +h2s +keriomail +gridjoyweb1 +sshstaging +ohsuf +sshprod +promoaid +ablogs +aot +seoplink +karm +webact +s1458 +s1459 +aee +s1461 +spaceship +s1463 +worldgames +s141a +oaweb +s141b +d90 +fineday +rabbitman +s1468 +bo2 +pdu-a2-1 +seong +samhan +gingko +gayline +pdu-a2-2 +lethe +benzaiten +kano +azmoodehcomplex +lefty +s1107ipmi +leeco +narciso +jitu +lovesick +snowflowers +test00 +kako +webtop +bmaster +s142b +kahn +judai +isha +s1101 +www.worldgames +arashi +kadi +s1478 +s1479 +magrathea +s1481 +jial +s1482 +s1483 +suva +tta3 +cadiz +onc +sibiu +www.amsterdam +jshop +zmta +stanford +wlan-gw +s143a +icecube +s143b +hampshire +td01 +www.reservations +tondu +s1480b +s806b +s144a +tarragona +kimek +topanga +jbh +ducati +nsj +webhouse +iphone4s +maputo +s144b +s1104 +izmir +leefamily +s527ipmi +cranberry +cworks +larisa +services1 +webdisk.sklep +sakura39 +tenten +tenshi +khaki +s746 +wr1 +wl3 +aitken +showroom3 +switch7 +dallas2 +midge +homburg +happymondays +albarr +associate +patz +scotte +stas +mattk +aar +s427ipmi +pro1 +s1113ipmi +psv +mailvip +lyncpool01 +s4315b +kiki.ads +ns-test +external1 +dh10 +pubtest +webfarm +s4144ipmi +s1110 +autodiscover.mob +s630a +autoconfig.mob +frauen +s1111 +www.frauen +www.52 +www.63 +mersey +bfl +ilja +jays +s533ipmi +pleione +beta-www +s642ipmi +galinka +s1328ipmi +s1118ipmi +moneysaver +s1474 +vsphere1 +webct1 +mailer6 +steen +clinical +cimarron +touchnet +ikki +worth +grenada +rayleigh +malthus +s4149ipmi +s622ipmi +s153a +casweb +s814ipmi +vpn-inside +osteo +dts2 +cwserver +chambers +s4316b +misentry +s1029 +hildebrand +kcn +belloc +s1390ipmi +s1124ipmi +s156a +s156b +www.kokomo +shopping2 +www.megami +xpert +redm +adportal +s1013ipmi +illuminate +s4142a +consulthts +vmview +pakwest +pakwestx +s1124 +consulthtsx +wlzx +blog-en +s819ipmi +s1471ipmi +iheb +gjzx +s1126 +wserv +s913 +s4317a +mailhub3 +mailhub4 +s1405ipmi +s50a +s1129ipmi +s729a +s4161ipmi +s1131 +s825ipmi +s163a +s163b +s195 +dev.media +secure12 +secure02 +passport1 +icrm +strato +immo1 +mysecure +cloudsupport +rust +tease +eliana +host-2 +recruiters +arquitetura +s4330a +s1209a +pop-gw +ocsinventory-ng +babi +s1209b +s1411ipmi +ircd +suckhoe +s4270ipmi +s1135ipmi +internalcrm +s4130b +vps2.serverel.com +sanit +s4166ipmi +s831ipmi +webrep +s4318b +aow +lyncadmin +s4333ipmi +s1140 +swu +dev200 +s1416ipmi +iees +calamari +s4230ipmi +s4220a +s708ipmi +s4172ipmi +sold +webweb +s834ipmi +s734a +eftp +barbi +johnjohn +hellgate +server-8 +bittern +server-6 +avocet +server-5 +server-4 +miyakawa +webforum +easytrack +adnet2 +kgn +s836ipmi +tfh +pma3 +s285ipmi +faleconosco +weegee +tdp +mailgw02 +baltimoredc +s934ipmi +shukatsu +camil +ftp.p +hscm +www.outdoors +s173a +s173b +autodiscover.invest +webdisk.invest +autoconfig.invest +rangoon +oai +danielw +s1422ipmi +s4320b +www.comps +maigret +ras06 +s320a +wpmu +node21 +vshield +netapp01 +ras05 +iradio +citrix02 +ras01 +ras02 +hastur +synology +s801 +siw +s4179ipmi +s4177ipmi +lindsey +tabs +www.dog +s175a +www.igri +lasvegas.dev +pauling +agent1 +s175b +giskard +s842ipmi +s176a +advs +yingjie +img.blog +s176b +sage3 +thor2 +san01 +s301ipmi +footprints01 +grot +s1101a +hind +s1101b +jbbs +s1369 +s320b +gepetto +thalamus +athens.dev +phoenix.dev +rivka +test43 +s1102a +test45 +s167ipmi +test50 +s4132b +yttrium +k02 +quaoar +cyclades +education2 +s1401a +s1103a +saucer +s4183ipmi +dev07 +cross-reference +dcdctest +usso +mehqnotes4 +s1391b +ussotest +test-psearch +glomis +protector2 +s1104a +mehqnotes1 +mehqnotes2 +glomis-rep +imsrv1 +smartbomtest +mobile2.dev +hqmailsrv1 +cross-reference2 +muratavoip +smartbom +quotetest +ocenter +mesjnotes1 +dcdc +s1105a +campanhas +s1105b +s920 +galaxy1 +s4365ipmi +sm153 +thestudio +man01 +s1106a +osirix +s1106b +ivanhoe +s1107a +snarf +osmium +okane +s4188ipmi +kazunori +www.americanstudies +cybercrime +sound2 +sound5 +s1018ipmi +sound6 +sound7 +sound8 +soundx +s1108a +s185a +sound10 +soury +s1402a +testtest1 +cidep +farfalla +piata +margus +www.tfb +tfb +donbot +boxy +tinnytim +crushinator +coilette +mail-p +source1 +source3 +motis +vdt +backup04 +www.loadtest +iexternaldb +korund +soscenter +oweb +monkey12 +services.dev +s1110a +nei +s312ipmi +snip +cisco4-2 +njn +s186a +daa3 +s186b +s186c +carolan +s1111a +midias +lcp4 +lcp3 +s303ipmi +lcp2 +s1438ipmi +nancy1 +grapes2 +s1112a +portmaster +vally +pdu9 +jtb +drteeth +s822 +intmed +duckie +ket +s4204ipmi +infra11 +s1113a +portier +synaps +boadicea +infra10 +s1470ipmi +lodging +s190a +s190b +hoian +s1114a +s317ipmi +vinno +s1115a +s620a +mailadm +hospitalists +bhsftp +physicianupdates +s1444ipmi +extmail1 +physicianupdate +a380 +cenp +epicproxy +extmail2 +bpa2 +quarantine51 +mdportal +towerpacs +ellen2 +ssstage +residency +veena +webdisk.devel +g-star +do1 +maccer +mstock +motech +read2 +flyspray +nicolash +mackie +cjp +s202a +lscs +s1393a +s1116a +blazar +medgen2 +notepc +yorozu +santoku +topkapi +dialin13 +cc5 +dialin14 +negima +kasturi +dialin15 +dialin16 +dialin17 +dialin18 +casiopea +dialin19 +dialin21 +dialin22 +dialin23 +dialin24 +dialin25 +dialin27 +dialin28 +dialin32 +dialin34 +dialin37 +s1393b +nugo +vmwww +cappella +auva +lcp1 +nabiki +shadok +s4209ipmi +ecommerce1 +s830 +s1117a +s204a +parc +idmap +s1429ipmi +partner-test +s1118a +bulk2 +s323ipmi +s205a +debs1 +s1120a +s1120b +mobileqa1 +dialin20 +dialin30 +dialin31 +420 +z8 +ciscolab +s1449ipmi +dialin35 +dialin36 +dialin38 +s206b +dialin40 +helo +dialin41 +dialin42 +fogo +s1121a +ftir +bedford +purifier +s1121b +s207a +s207b +flights +euclides +old-blog +conf2013 +s1122a +fiber +diffy +s1122b +dreamline +s208a +pushpin +s1394a +eames +s1123a +krab +s1123b +s1394b +mwanza +luno +feline +s209a +paytest +s210c +s842b +prod01 +s1124a +s1124b +s4110a +s211a +pc05 +pc06 +eeepc +retoure +www.kunde +s1455ipmi +www.intern +s1125a +lectures +s1125b +gutscheine +s842 +s1126a +s1126b +purl +dom11 +sbin +svn5 +newbeta +s213b +smtp153 +rood +206.182.105.184.mtx.apn-outbound +malindi +s1127a +s1127b +smtp180 +184.182.105.184.mtx.pascal +tira +ns-svwh +smtp160 +189.182.105.184.mtx.pascal +bartel +s609ipmi +geochron +eu-ns +outscan-200 +outscan-202 +mailspike +outscan-201 +outscan +201.182.105.184.mtx.outscan +outscan-231 +outscan-232 +outscan-233 +outscan-230 +outscan-235 +outscan-237 +outscan-238 +outscan-239 +hostkarma-eu +204.182.105.184.mtx.outscan +s214a +smtp174 +s334ipmi +outscan-real +205.182.105.184.mtx.apn-outbound +smtp177 +spamstore +appletree-outbound +smtp129 +smtp131 +s1340ipmi +smtp133 +smtp134 +smtp135 +smtp136 +smtp137 +retry2 +smtp139 +smtp141 +s1128a +smtp143 +smtp144 +smtp145 +smtp146 +smtp147 +zamowienia +smtp149 +209.182.105.184.mtx.apn-outbound +smtp152 +smtp154 +smtp155 +smtp156 +smtp157 +smtp158 +smtp159 +smtp161 +smtp162 +s1128b +smtp163 +smtp164 +smtp165 +smtp166 +smtp167 +smtp168 +smtp169 +smtp171 +smtp172 +smtp173 +s215a +smtp175 +smtp176 +jef-admin +smtp178 +smtp179 +smtp181 +smtp182 +digiweb-outbound +smtp184 +smtp185 +smtp186 +s1405a +generals +smtp189 +smtp188 +smtp190 +ob-230 +ob-232 +ob-233 +ob-234 +ob-235 +s1461ipmi +ob-236 +ob-237 +ob-238 +apn-outbound +ns-prgmr +hostingsource +185.182.105.184.mtx.pascal +mailspike2 +182.182.105.184.mtx.pascal +smtp183 +ns-t3n +3a +s1130b +bpel +200.182.105.184.mtx.outscan +outscan-236 +203.182.105.184.mtx.outscan +s1395b +208.182.105.184.mtx.apn-outbound +s216a +ob-231 +180.182.105.184.mtx.pascal +apn-outbound-205 +apn-outbound-206 +apn-outbound-207 +apn-outbound-208 +apn-outbound-209 +183.182.105.184.mtx.pascal +mailview +ob-239 +segan +188.182.105.184.mtx.pascal +smtp187 +nick1 +nick3 +187.182.105.184.mtx.pascal +neonova-outbound +t3n +outscan-backup +smtp130 +spamd0 +s4226ipmi +smtp151 +207.182.105.184.mtx.apn-outbound +rbl0 +rbl3 +rbl4 +rbl5 +rbl6 +rbl7 +rbl8 +rbl9 +181.182.105.184.mtx.pascal +s1131a +ubuntulinux +outscan-234 +202.182.105.184.mtx.outscan +smtp138 +186.182.105.184.mtx.pascal +sugar01 +outscan-203 +outscan-204 +sugar02 +bobdylan +kyy +hostingsource-195 +hostingsource-196 +hostingsource-197 +hostingsource-198 +duran +s1131b +dummy3 +dummy4 +www.robbie +smtp191 +masscheck +s217a +smtp150 +makeweb +s217b +www.ozzy +www.biblioteka +ddns3 +www.beatles +stones +gino +www.abba +gdns1 +gdns2 +cgi01 +belgia +s1132b +www.ledzeppelin +s615ipmi +www.rem +s218a +s218b +s339ipmi +celinedion +dialup3 +newsold +gigs +www.s12 +thedoors +www.bonjovi +www.britney +presley +ayame +azuma +s1133a +s1133b +roselle +s1133c +aerosmith +databank +selab +ledzeppelin +ac-dc +s219a +ap10 +www.tori +s1466ipmi +shamash +www.celinedion +s1134a +stpexch +hadoop01 +hadoop02 +hadoop03 +s1134b +koji +trui +s1134c +s221a +spirits +gw-test +wyvern +www.middleeast +s221b +www.mum +wayf +s221d +s4232ipmi +incarose +www.yss +jobstest +safehaven +s1135a +subscription +www.dobre +cmssandbox +dobre +tdh +s1135b +#pop3 +video10 +video11 +xchange1 +ftp.wap +s222a +base10 +base14 +base13 +base11 +base09 +base08 +freshman +base07 +arthistory +base06 +gwh +itcnet +fotobuch +alid +webdisk.host3 +www.cruise +csharp +4a +ftpwork +relaunch2 +single +furien +transporter +s1406a +ifi +pilkada +hoaipk +nabeel +hadish +www.mos +apunts +latihan +www.paypal +www.host3 +gitit +www.christine +s4127ipmi +kunduz +dns178 +renderer +s1136a +alexpc +carioca +dns114 +dns112 +dns111 +colo2 +seotool +subasta +www.train +kaolin +dns105 +dns104 +s1136b +www.pisa +abt +dns103 +dns102 +erikm +gtr +repeater +zys +ega +cyh +s1396b +t1ns +karine +cbh +kennethv +s223a +licserv1 +f50 +ntop +s223b +roberta +flits +iwm +ksn +communicationsftp +kzr +www.arif +www.basic +sites2 +s224a +nur +venpur +s4331ipmi +ever4 +lachesis +gjacll +eddspermits +thietke +wba +cjis-dr +cjis-webdev +s1472ipmi +iskender +tulipa +s1030a +genix +ejury +s1138b +veliyayla +www.journey +geezer +musab +www.walker +pdinfo-new +metal123 +stmichel +www.she +s1206ipmi +www.chatchat +www.bugtrack +labelle +postdoctor +shorouk +s225a +pecora +www.surajit +sanjesh +eddsplanreviewdev +qlvb +minhhoang +www.badminton +dwrarcgis +ourlove +www.seminar +s225b +jcats +www.cihan +www.rental +s1140a +s1140b +www.asptest +phimviet +xfire +market24 +essits +www.kolaypara +debs2 +gwinnettftp +sanitationftp +saeid +eddspermitsdev +sadeh +sacom +charlot +zag +www.programci +employeeaccess +s226d +pdinfo +operator1 +uperform +www.cosmo +ticaret +gcecc +merkez +s902ipmi +abbasi +s748ipmi +onshop +biodata +www.giaitriviet +www.lazaro +website1 +cjis +indigentdefense +wif +s626ipmi +computerfreaks +www.webapp +www.pharma +s1224 +s1225 +rith +s1407a +www.filmclub +shel +meloen +silver2 +bhavik +s931 +s1209ipmi +eddsplanreview +sadegh +s1407b +fruits +boudoir +www.bon +www.cso +biofuel +artemis2 +zatopek +provid +daingean +framboise +cortez +gabbro +www.babacan +kayseri +monkey11 +mohpc +dev-m +twitch +s1212ipmi +filler +www.tpc +www.salimi +www.ttt +www.addons +hooba +eponline +testowe +s230a +kevindm +portobello +www.sample +s230b +wowza1 +s4243ipmi +promedia +patrickb +empower +trunghieu +veolia +webpanel +salaam +eskimo +www.lojavirtual +snappy +moller +quiksilver +cengiz +www.yusuf +nhacpro +thanhvn +palmon +www.mymoney +www.tiny +www.artgallery +przepisy +www.haber +s907ipmi +averhuls +tempdemo +thesis1 +www.1234 +s510a +charisma +pcounter +www.yellowpages +s233a +s1219a +www.hasan +www.automoto +s1483ipmi +www.election +goldmane +cografya +mark109 +s1217ipmi +lede +fractal +boreas +wrzuta +s234b +hulshout +simay +ieper +brecht +hutech +s720ipmi +go4 +www.teksty +www.egysoft +salessupport +rims +stinky +s4248ipmi +micron +haylaz +www.dostlarfm +www.healthy +s235a +thaian +thong +pot +s235b +tale +mdbs +diederik +prevail +nas6 +hdgh +server79 +www.experimental +server68 +tvg +waw +www.furkan +ikbox +www.info1 +net02 +server65 +star6 +s510b +xss +s913ipmi +vdh +gays +sufi +toofan +s1440a +hieumu +server54 +s237a +hqfw +amigos4ever +furkan +www.jimbo +www.restoran +server36 +akhil +www.astrology +www.pdfs +pishro +s237b +pond1 +nas7 +s1032a +www.earthquake +justnews +www.gara +websolution +s238a +utf8 +www.citrix +beos +s1223ipmi +ingilizce +www.daemon +s1241 +s1120ipmi +bijay +homewiki +motif +s4254ipmi +maciek +bluey +gmusic +s239a +scenic +s240b +thunghiem +www.iranit +www.daugia +s934 +tantale +www.800 +s241a +s241b +winterfell +seyhan +fluor +rimsky +amid +topol +berg +aosa +ara3 +sk2 +boro +teszt3 +www.malik +azin +iranit +buty +companies +www.marta +s241c +lunca +ns2.cloud +dimi +wiera +www.gomobile +dev.my +ns1.cloud +vario +s1345ipmi +s242a +s1410a +quickplace +spaniel +wikidev +eisk +fox5 +quasimodo +tatara +japo +infohub +s1410b +s243a +thanhtrung +mahesh +fizi +www.fathi +s1228ipmi +www.sendsms +marcio +extranet-dev +nissim +miramar +ebdaa +yehia +s4330b +s4259ipmi +s901 +www.okyanus +www.asso +neel +www.azad +itcyber +creativesoft +bestanden +www.buty +ophrys +s630ipmi +www.bahonar +berkeley +s924ipmi +mad-dog +www.egov +www.clearwater +slon +ower +www.enes +cent +www.hh +www.qd +www.etis +amozesh +packard +auburn +saud +s206a +s4222a +sgfs +s1222a +sika +mitra +huseyin +wpg +www.japo +etab +picsgall1 +poldi +southside +s119a +s1234ipmi +www.joko +s120b +www.nab +s4265ipmi +coolfish +www.myhost +gadi +horiba +www.knightonlineworld +wscr +www.mira +www.spartan +galls777 +www.myapp +dzup +gargantua +www.omid +s1029ipmi +s908 +www.workspace +notos +www.oyun +s810a +s910 +www.pages +shalin +sevdagul +forestpark +maihama +www.soap +www.brisbane +justtest +brin +sitest +anhtung +component +postadmin +s407a +chakwal +carson +s1240ipmi +s253a +s253b +leith +www.aksehir +s4271ipmi +s528ipmi +www.hme +fblogin +s4143a +s935ipmi +www.intermed +www.apollo +s255a +cisco5-1 +www.seattle +s118ipmi +www.plesk +supplement +medford +www.frederick +melike +www.tampa +mahmoudi +carp1 +testme +kolaypara +www.peterborough +seguimiento +conroe +truongpro +www.tpl +filo +estat +s331b +evanston +stgeorge +realmadrid4ever +coventry +giaitriviet +nokiazone +www.prism +www.pibid +www.yas +telemed +hoangdinh +leading +brampton +s1412b +www.sanjesh +brookvale +bentre +alisveris +canton +s256a +s256b +habesha +sarasota +braintree +s919 +www.newcity +vandai +s921 +clovis +www.easymetin2 +s258a +studentinfo +www.centurion +www.rohit +chantilly +s258b +s941ipmi +facebbook +richmondhill +isams +www.shree +www.noavaran +dover +www.site2 +softline +www.tales +bestwishes +chatham +marietta +mehmetali +www.sohbet +s260a +chemgio +prakash +avanish +egitimci +s260b +s823z +1964 +s4178b +cf3 +santarosa +compsci +s240ipmi +s420ipmi +s224ipmi +arlington +s262a +s262b +bayside +serv6 +s941 +linuxsrv +hillview +stpetersburg +s930 +humanities +wifi-portal +s1450b +kimbo +s264a +www.clinton +cxy +s1391ipmi +hermitage +artarmon +zikao +s405ipmi +autodiscover.ru +s1036a +gold.chem +stowarzyszenie +wwwssl +kumu +s130ipmi +smtp00 +framework +s265a +www.epscor +uhsp +s265b +s1036b +s265d +teax +s942 +www.shelby +pftest +s4260a +edytor +astro11 +frameworks +debata +s266a +meadows +logistyka +cms-dev +futuresite +bak184 +s1420a +ex4200-1 +ex4200-2 +futuretech +ihs +vika +simferopol +s610 +petruk +s4240ipmi +sudak +modtools +vcs2 +stargate2 +vcs1 +www.prashant +buriram +s134a +phichit +s411ipmi +proxy29 +s409ipmi +bima +s270a +webdisk.pagerank +s270b +s940 +s312b +s1102ipmi +sdb1 +www.fortuna +www.astrakhan +merchandise +autodiscover.pagerank +autodiscover.offer +autoconfig.offer +www.hebrew +olimpic +s271a +autoconfig.pagerank +s271b +s1037a +seomedia +origin-image +testcss +platinum1 +www.podolsk +blogstaging +www.rybinsk +s4303ipmi +mysql.web +test.intranet +solman +s1305b +s272a +oskol +www.nova +mail.mailtest +webmx +s272b +s1301 +tripoli +navy +kerokero +host98 +s501b +mtx +s134b +s1040a +misfits +s416ipmi +s141ipmi +designpark +ie6 +siakad +gozinesh +s1226b +www.aig +hed +cepe +cloud8 +eci +btt +partner.test +s124a +scorpion2 +s102393ipmi +scorpion1 +s1002ipmi +mirada +s4308ipmi +s276a +s276b +medialib +s4116ipmi +stangel +www.maharashtra +karnataka +s1201a +inpac +quant +maharashtra +s1201b +s4112a +entrepreneurship +virage +autoconfig.money +webdisk.money +gc2006 +s603ipmi +autodiscover.money +s1202a +malayalam +webdisk.songs +autoconfig.songs +autodiscover.songs +tinymce +s1202b +s278a +autodiscover.egypt +nzz +s278b +wdm083 +webdisk.japan +s1038b +s1203a +smalls +autoconfig.japan +s1203b +dhcp-1 +autodiscover.japan +cr2 +jette +webdisk.egypt +hbi +s1007ipmi +autoconfig.egypt +s1204a +wwwdr +gwtest +nim +mail.alumni +s1204b +sw14-1 +direct1 +gwmta2 +testzone +physlab +amms +mail.student +pascasarjana +forumsdev +s4314ipmi +mini3 +s1205a +s1205b +immanuel +inkubator +huey +ttu262114 +s703ipmi +brk +service7 +mmt +s1206a +cnap +starfleet +s1206b +s1416b +stations +mcp2 +ou +flix +s1207a +m.account +s1207b +s1208a +tctest +webg +emir +zblog +latam +hdb +s1208b +otd +antena3 +wapuk +wapus +hendri +s285a +megafon2 +ttu275560 +s285b +s1321 +frequency +s1210a +radius7 +s1210b +sepa +antero +tj3 +logindev +rapor +tmp2 +s4360ipmi +s1228a +www.ja +autoconfig.nl +lowcost +lov +deca +s1211a +restorani +s1211b +wingspan +s433ipmi +webdisk.nl +autodiscover.nl +s1409 +luna2 +proaction +s287b +www.freeads +s1324 +s1212a +skatt +eproxy +s1212b +www.atendimento +zerg +cheqa +jaci +musicvideos +projectserver +infierno +s288a +206 +s288b +s1213a +kpg +gelendjik +autoconfig.ca +autodiscover.ca +webdisk.ca +147 +s1213b +s731ipmi +sinau +p21 +158 +s4325ipmi +174 +179 +181 +garagestore +183 +184 +s1214a +box29 +nd209 +aither +pythia +194 +ts-cat4900-gw +vm004 +s1214b +reservoir +s301a +s1340a +s1215a +web-forward +ksj +zwierzaki +narty +www.narty +http7-00 +beta.preprod +wdb7 +test-equinix +nocache.promo +hb03-preprodwwwphp01 +http6 +http7 +http8 +http9 +http3-00 +rueduco20 +wdb5 +devphp1 +http1-00 +hb06-smtp01 +cdn.preprod +http10 +http11 +http12 +http13 +wdb7-00 +wdb7-10 +nocache.ressources +http13-00 +ssl.preprod +hb05-smtp01 +test-iliad +hb01-smtp01 +hb01-smtp02 +wdb5-00 +wdb5-10 +http9-00 +http11-00 +http11-01 +eptica.preprod +khk +www.0001 +provide +s1215b +appointments +kef +skh +brandcentre +s1406ipmi +ucv +lins +s438ipmi +relaydump +relaybackup +s163ipmi +harveys +partsweb +www.reminder +www.contador +metcalfes +nt3 +s1216a +s1216b +nt2 +s303a +devgit +s303b +s1229a +tula1 +star-sat +nana123 +s1290ipmi +daru +s1217a +sinitsa +s1217b +s1024ipmi +antarctica +websys +rmsdemo +tmpftp +clickheat +mobiel +devtools +smarthealth +zerno +afina +mail.strela +mail.med +vehicle +s1229b +diadema +agent-test +wapadmin +analyse +s304b +www.szg +xia +cssource +s218ipmi +s1340b +summercamp +webmail.s +s1218a +mysql28 +ftp.s +s1218b +s4149b +inoue +s305a +s305b +teacher1 +vr1 +timisoara +constanta +wg11 +sikora +s1220a +ns4-3 +s1220b +hp3000 +eserver +s719ipmi +wg3 +wg5 +ssbdev +wg6 +unta +wg7 +spyro2 +manage-vps +mailrescue +shopftp +s306a +relay100 +box5vm2 +serval +druid +veranda +mailhosting +paid +mg80 +fakel +wssl +skip +box5vm4 +enews2 +s1356ipmi +arama +this +mail.love +whserver +evgenia +wwwp +vilena +box10 +jastreb +worldvision +s516ipmi +snms +shox +s1221a +nd187 +ftp.black +mandela +biashara +nd200 +sail +notary +betterlife +healthyliving +auhans2 +dxbans2 +auhans1 +dxbans1 +nd191 +puls +www1a +www2a +www3a +ftp.god +nd192 +tatjana +klv +gcms.qatools +cms.qatools +wwapp.qatools2 +gcmsadmin.qatools +preview.qatools +ww30.psqa +wwapp.qatools +cms.qatools2 +nd203 +web1702 +s1221b +web912 +web914 +web915 +web916 +web917 +web918 +web920 +web921 +web922 +web923 +web924 +web925 +web926 +nd196 +web930 +web931 +nd208 +web932 +web933 +web934 +web935 +web936 +web937 +web938 +web940 +web941 +web942 +web943 +web945 +web946 +web947 +web948 +web950 +web951 +web952 +web1239 +web954 +web955 +web957 +web958 +web960 +web961 +web962 +web963 +web964 +web333 +web967 +web968 +web970 +web971 +web972 +web973 +web974 +web975 +web976 +web977 +test6359 +web980 +web981 +web982 +web983 +web984 +web985 +web986 +web987 +web988 +web991 +web992 +web993 +web994 +web995 +web996 +nd210 +web997 +vm003 +web1249 +web1253 +web913 +nuni +web1259 +chas +dobro +web919 +pani +nd212 +web1266 +web338 +web1269 +web1699 +factor +web927 +web1273 +web929 +web340 +web1280 +web1286 +web1288 +web944 +web1290 +web1291 +web1293 +web1294 +web949 +web1295 +web1296 +web1297 +web953 +web1298 +web1299 +nd214 +web956 +web959 +web346 +web965 +web966 +web1323 +nd215 +web969 +nd216 +web1330 +web979 +cbia +web1339 +gud +web990 +web1349 +web353 +web998 +web999 +alga +web1359 +web1370 +nd190 +web1379 +web360 +web1389 +web1391 +web1393 +web1394 +web1405 +web1406 +web1397 +web1398 +web1410 +web1428 +nd204 +web1430 +nado +test6394 +mija +s307a +web1440 +s1305ipmi +web1449 +s1222b +mail.black +test6399 +mail.happy +waterford +krim +naira +behealth +lagu +fortunate +feelgood +web1530 +nake +s1030ipmi +s308a +web386 +web1539 +kedr +s4336ipmi +dudi +duch +web1549 +lavinia +businesshouse +web390 +web1560 +web391 +web1565 +s139ipmi +sv374 +ifly +rados +web1569 +web1572 +svetlana +web394 +web1578 +web1579 +ourlife +millioner +web395 +web549 +web1585 +s1223a +web406 +web1589 +web1591 +web1592 +web1603 +galo +web1594 +web397 +s1223b +web1595 +web1596 +web1597 +web1608 +gain +web1599 +web398 +gago +mail.total +web410 +web1620 +woz +greenpower +s309a +web1628 +web1630 +s310b +s932ipmi +web1639 +s1224a +wmp2 +web416 +web1650 +www.eep +eep +narek +web1660 +s1224b +web419 +web1669 +brzeg +mail.piter +web1679 +s725ipmi +web423 +s311a +s311b +web1688 +web1689 +web1701 +web539 +s1344 +web1693 +web1694 +web1705 +web1696 +web1697 +alya +web1698 +web1709 +wellnesslife +s1225a +bady +web1719 +s1225b +s1345 +dorian +web1730 +sapfir +s312a +web1740 +stroke +mail.ural +makarova +web1749 +s1419a +position +merri +web436 +fbcc +allworld +welly +serp +ws03dev000 +s620 +web439 +kostanay +lvis +muslima +web1819 +s1226a +web219 +game24 +wita +web1849 +mail.vip +ftp.freedom +s1311ipmi +zas +s1420b +web456 +s1410ipmi +kulik +web1828 +curs-valutar +use +web1829 +s1035ipmi +web459 +masini +rvp +test6489 +s313b +laif +web2023 +web1924 +web2025 +web1926 +web1927 +powerstation +web1928 +web2030 +qaz +plg +web1931 +web2032 +web1933 +mail.god +web1934 +web1935 +s4342ipmi +web1936 +web1937 +web2038 +ltd +web2039 +web1941 +web1839 +web1943 +web1944 +s1227a +web2045 +s1227b +web470 +web1946 +web1947 +web1948 +s4122ipmi +www.feminin +s314a +margo +web1958 +test6498 +web1959 +s314b +wellcom +web473 +web1964 +web1965 +giv +suplementy +s601ipmi +web1969 +s1349 +web475 +akt +web1978 +web476 +web1979 +vipstar +bao +s1228b +gradina +vilma +web1990 +s315a +s1230a +lidya +web479 +greenway +web2616 +www.poze +web2115 +s1230b +ftp.sakura +ftp.deal +web483 +fady +ejay +soli +web2128 +www.curs-valutar +web2129 +best1 +rodnik +s316a +web2139 +www.dictionar +taishin +vella +viktorija +web1858 +aniya +web2149 +web488 +zdorovje +s316b +ter +web2160 +web491 +www.i1 +kakimoto +beautylife +s4347ipmi +web492 +s1231a +virt1 +web2169 +web493 +healthlibrary +katrin +s1231b +web494 +s1316ipmi +web495 +ehms +web1866 +shoppingcart +web2188 +web506 +web2190 +s317a +web2191 +web2619 +web2193 +web2194 +milena +web497 +elizaveta +web2195 +error2 +web2196 +zs1 +web2197 +smjy +web2198 +www.sbe +web2209 +web498 +s317b +syl +artikel +aliga +s1354 +s1232a +web509 +s1232b +s1355 +plain +jabber2 +testcp +lavie +web1873 +s318a +web2229 +gavan +ree +naso +web2239 +s1421a +test6292 +healthandbeauty +narayana +s1233a +sasc +eger +web2249 +dood +receitas +web2251 +egel +web2258 +web2259 +web2265 +web520 +beleza +holger +web2269 +s1233b +s319a +web2278 +web2279 +web2285 +va1-middev01 +web2288 +web2300 +web2291 +web2293 +web525 +web2294 +web2295 +web1886 +web2296 +web2297 +web2308 +web526 +web2309 +bcrtfl1-inqa01 +stngva1-dc01 +web1888 +web1900 +stngva1-an01 +web529 +web1901 +web2328 +web2329 +web1902 +dev03-web +web1903 +web2339 +web533 +web1904 +qa02-web +idsfeed +web2348 +web2349 +web1905 +remedy1-21 +ar-dev +web1906 +bcrtfl1-arsbx02 +bcrtfl1-arsbx01 +web1907 +abtools +stngva1-ar07 +stngva1-ar06 +web1908 +stngva1-ar05 +stngva1-ar04 +stngva1-ar03 +stngva1-ar02 +web540 +bcrtfl1-wdev02 +web1911 +bcrtfl1-wdev01 +web1912 +bcrtfl1-rqa01 +web1913 +web223 +ar-qa +web1914 +web378 +web1915 +web2890 +bcrtfl1-arqa02 +web2016 +bcrtfl1-arqa01 +web546 +nccqa1 +ids09 +web1917 +api_portal_dev +web1918 +web1889 +devrvip +web2430 +bcrtfl1-indev01 +web2019 +webp1 +web550 +stngva1-web02 +web2437 +web1921 +stngva1-web01 +web2439 +ar-prod +web1922 +web2445 +fisheyedev +stngva1-dc03 +web1923 +web2449 +bcrtfl1-ardev02 +web2458 +web1925 +bcrtfl1-ardev01 +api_web_dev +web2469 +comvault +api-web-dev +web2479 +web2029 +web2483 +stngva1-rkm02 +virtual-vr +web2500 +web2491 +web2492 +web2493 +web1932 +web2494 +web2495 +web2496 +web2497 +web2498 +web2509 +web1899 +web2528 +web2529 +stngva1-wqa01 +vbi1 +web1938 +web2539 +web1940 +stngva1-rkm01 +stngva1-in01 +web2549 +vmop +web1942 +web380 +web2560 +web2566 +qa04-web +web2569 +web1945 +web2573 +remedyreply +dev-greenhopper +turkiye +web2580 +stngva1-wdev01 +tmpids04 +arsdocs +web2586 +web2588 +web2590 +stngva1-dhb02 +web2049 +web2593 +web2594 +web2595 +stngva1-dhb01 +web2597 +web2598 +web2609 +fl1-middev01 +web1952 +greenhopper +webp2 +remedy1-18 +bcrtfl1-wqa01 +vdsm-devmail +qa05-web +web2630 +web1950 +portal-net-sb01 +web2639 +tmpids05 +fl1-midqa01 +b.test +web2649 +stngva1-ar08 +va1-midqa01 +rkm +api_webi_dev +rr2 +stngva1-ar01 +rkm-prod +web610 +web2719 +web1972 +artfac +web2729 +web1892 +web2749 +dev-fisheye +web393 +ids04 +ids03 +web2760 +ids02 +web2764 +web2769 +bot.search +web2779 +qawebp2 +web2790 +web2791 +web2792 +web2793 +web2794 +c.test +web2795 +web2796 +web2797 +web2798 +web2809 +remedy-qa +dev-jira +web2819 +web1989 +web629 +qawebp1 +web2830 +widgetmanager +web2915 +web2839 +web2849 +widget00 +s319b +client00.chat +web2916 +web329 +web119 +web1893 +web2917 +ircip1 +web1310 +ircbot.search +web2909 +web1909 +web2923 +web2924 +web2925 +parteneri +web2926 +web2927 +www.parteneri +web2928 +web3029 +clientsearch +web2931 +web2918 +web2119 +web2933 +web2934 +web2935 +web2936 +web650 +web2937 +web2938 +web3039 +web2941 +web2942 +web2943 +my.chat +web2944 +web2945 +web2946 +web2947 +web2948 +web3049 +wbf-mdm +mx-10-brighthouse +web3060 +web2920 +evserver1 +web3068 +web3069 +web656 +remote-filter +web-dmz01 +web489 +co-op +web3079 +web660 +web2921 +web3088 +web3090 +web3091 +web3092 +web3093 +web3094 +wbf-mobilesurf +web3095 +web3096 +web3097 +web3098 +web3109 +ircip4 +web663 +sha2 +ircip2 +web3120 +web2922 +web3139 +web668 +web1894 +ircip3 +web3149 +web359 +web3158 +web3159 +netgraphs +web3169 +webdisk.foto +web675 +web609 +web3179 +web676 +web3200 +web3191 +web3192 +web3193 +web3194 +web3195 +web3196 +web679 +web3197 +web3198 +web3209 +s1234a +web3219 +web683 +web3228 +web3229 +cherries +s1234b +s185ipmi +web3239 +s321a +www.dacha +web339 +web3249 +web2159 +web690 +s1360 +s1235b +web693 +graves +kundenbereich +rs4 +web706 +don4 +web1895 +web2919 +s507a +s1322ipmi +s322a +dodi +web699 +s322b +test98 +edi2 +pdu10-1 +pdu10-2 +web519 +s323a +k219 +s323b +s1237a +cric +s1237b +web57 +web58 +s324a +web66 +web67 +web68 +s324b +web71 +web72 +web73 +web74 +web84 +diaa +coob +devb +web2950 +s511ipmi +s1238a +desh +s1238b +litecommerce +s325a +junkyard +dela +web3b +s1422a +mail.ufa +s1239a +s1240b +webdisk.library +cisco6-2 +web2298 +web2299 +s1327ipmi +s235ipmi +s1241a +clad +dara +s1241b +dami +daff +dado +web2315 +web2319 +web2322 +s4124ipmi +rbg-web2 +web3059 +chileanplants +s0-0-0.gw1 +s0-0-0.gw2 +devcelebratelife +lo0.gw2 +lo0.gw1 +gw-internal +congotrees +fa0-0.gw2 +fa0-0.gw1 +devgreenpages +greenpages +web649 +briz +chon +ayub +s327a +s327b +s1370 +sellers +s1242b +reyco +s328a +portonovo +zamin +paddock +vmhost +s328b +marija +www.vak +bozo +test.service +s747ipmi +s1243a +s1243b +s206ipmi +azam +s331a +s1333ipmi +s132a +s63a +s4241ipmi +web928 +www.fastcash +s4364ipmi +s332a +befragung +s1376 +pdu11-2 +pdu11-3 +s333a +s334a +consent +apc5-bad +web2939 +offerta +s335a +vgm +s1338ipmi +s1020 +web403 +cisco1-1test +s336b +s1235a +s646ipmi +bling +s337a +s1219ipmi +web2419 +s1424b +s339a +s339b +becker +s339c +s1344ipmi +s604ipmi +s341a +s4109ipmi +s1390 +s511a +s342a +pdu12-1 +vsetovary +pdu12-2 +s1236a +s1393 +s1010a +s1236b +s1394 +s1350ipmi +s705 +s1396 +s1006ipmi +s1397 +s504ipmi +s710a +dl53 +dltotal +dlfiber2 +s228ipmi +wertyuiop +s435f +s4313ipmi +dlpaintcopy +dlfiber +poc01 +dltefal +dlplato +dlpaint2copy +dltefal2 +dltank +dl42 +dltank3 +dl43 +dlpress2 +dlmatrix2 +dl44 +dlpacman2 +dlplato3 +dltotal2 +dlpaint5 +dlpaint +dltank2 +dlplato2 +dlmatrix +dlpaint2 +dlpaint3 +dlpaint4 +s1355ipmi +video19 +s927 +dl52 +router2a +router2b +s4121ipmi +s135a +away +s135b +fsb +s509ipmi +pdu13-1 +pdu13-2 +s1426b +s1361ipmi +sp10 +s241ipmi +s742ipmi +s4126ipmi +online-shop +bizon +s435ipmi +s617a +s4220b +s515ipmi +bak11 +s136a +catz +s1366ipmi +s1101ipmi +s4223ipmi +s702ipmi +s4132ipmi +s4347a +vpnhq +s521ipmi +s514a +pdu14-1 +pdu14-2 +pdu14-3 +onestep +s1372ipmi +s1106ipmi +aspx +999999 +s1240a +arza +s1433 +ciara +s1239b +s137a +billiard +stasik +s802ipmi +reinout +web2429 +www.reinout +s1436 +s4356b +s326a +s1112ipmi +s1440 +s4143ipmi +s4133ipmi +s807ipmi +s532ipmi +test6249 +test6251 +test6252 +test6256 +test6258 +s256ipmi +asid +test6263 +s1444 +s4358ipmi +test6265 +test6267 +web389 +test6269 +s1445 +test6272 +www.demo01 +arne +s901a +test6279 +s1430a +test6284 +test6285 +test6290 +test6302 +asar +test6294 +test6306 +test6297 +test6308 +test6309 +test6319 +web2929 +test6327 +web1006 +test6331 +test6332 +s1117ipmi +web978 +web392 +test6335 +s1446 +s1430b +s4350a +test6349 +test6356 +test6360 +test6361 +test6366 +test6368 +test6369 +test6372 +bigg +s4148ipmi +test6379 +ctw +web2459 +web989 +test6390 +s4350b +test6391 +test6392 +test6393 +test6404 +s813ipmi +web1019 +s1450 +test6409 +s537ipmi +test6413 +web939 +sg0 +test6419 +s262ipmi +s1290a +s1290b +test6429 +s1242a +test6438 +s1123ipmi +test6439 +s1301b +tmp4 +s139a +test6449 +s4154ipmi +s1302a +appy +s1302b +s140b +web396 +s640 +anup +s1303a +test6459 +s1303b +test6461 +ansh +s4259a +beno +cisco1-2bad +s1304a +s1304b +s1394ipmi +s1305a +test6469 +s1128ipmi +s4159ipmi +test6472 +s1306a +s1306b +s1435 +s4259b +s1307a +s1307b +tmp6 +s1308a +s1308b +test6479 +amro +beep +cisco7-1 +anda +test6490 +s1399ipmi +test6491 +s1310a +test6492 +test6494 +s1310b +test6495 +test6506 +test6497 +test6508 +test6499 +anam +s1134ipmi +s329a +s707ipmi +s1311a +hayes +s1311b +almi +s743ipmi +s1312a +s1312b +s829ipmi +s1373ipmi +s278ipmi +s1313a +s1313b +s1415ipmi +web409 +s1314b +web2490 +akil +s1140ipmi +s401a +s142a +s1315a +s1315b +s1475 +s402a +bant +s1316a +s1316b +ajit +s835ipmi +s403a +s1317a +carotte +s1009a +forum126 +s404a +kiwi2 +s1318a +s1421ipmi +modem1 +s405a +s1320a +s1320b +geode +s406a +aiai +web2179 +afcc +poc02 +adit +adik +s1321a +abus +s1321b +s1010b +s1484 +abii +s1322a +s1322b +abdi +abbs +s408a +aa11 +pconline +telenor +cisco2-1bad +jira.dev +gm2 +it0 +s1323a +s1323b +s1426ipmi +s409a +s1324a +s1324b +eau +www.ri +s411a +carl3 +hl01 +s1325a +s1325b +s412a +handicap +s1326a +nat144 +nat146 +s1326b +s305ipmi +ip204-110 +thehunter +s413a +ageforum +s413b +pdu11-1 +cmpunk +s1327a +s1327b +bastia +paypall +taranis +s1432ipmi +s414a +thesee +miage +s1328a +test-support +kazino +s1328b +s415a +auch +s1329a +sapir +s1329b +raisin +s1201ipmi +smartkey +eminent +s1331a +s1331b +web2499 +s311ipmi +s417a +humboldt +s1109 +s1332a +s1332b +cassis +web2932 +s1437ipmi +s418b +ws01qanet009 +s1333a +s1333b +visioconf +s419a +s4203ipmi +rion +elsayed +s1334a +s1334b +s4150a +s421a +ppp-12 +web2519 +ppp-13 +ppp-14 +ppp-15 +suze +s1335a +s1335b +perceval +s316ipmi +etoiles +s1336a +s1336b +pdu4-1 +s423a +s1443ipmi +web366 +s1337a +s1337b +s424a +s4150b +www.parser +s4208ipmi +botiga +mediaworld +web670 +web1859 +s1338a +s1338b +testededns +s425a +s1339a +s1339b +s426a +s322ipmi +s1341a +s1341b +s427a +s427b +s1448ipmi +s1342a +s1342b +s428a +s4214ipmi +s1343a +s1343b +s429a +s430b +ws02qa005 +ws02qa006 +s1344a +s1344b +s431a +s327ipmi +s1345a +s1345b +s432a +s1454ipmi +s1346a +web199 +s1346b +uaz +s433a +web1100 +s4219ipmi +s1347a +web1093 +s1347b +s434a +s434b +s4170a +s1348a +cp05qa001 +cp05qa002 +s1348b +cp05qa003 +cp05qa004 +s4170b +s435a +cp05qa006 +cp05qa007 +s435b +web1020 +web1099 +web2189 +s333ipmi +s4338a +ip204-115 +s1349a +s1349b +community-test +s929ipmi +s336a +web349 +web496 +s1459ipmi +ip204-116 +s1351a +s1351b +s437a +s101393ipmi +web2559 +s1352a +agent2 +ds922 +ds0210 +s1352b +sacem +s1352c +web2192 +s4104ipmi +s525b +s1353a +s1353b +s439a +s439b +s338ipmi +s1354a +web1919 +s1354b +s441a +s441b +web379 +web3189 +web1130 +s4329ipmi +s1355a +s1355b +s808ipmi +s442a +cisco1-1old +s1356a +s1356b +s1439b +s4360a +s1357a +testbn +s1357b +web1220 +web2579 +s924a +s619ipmi +s-1001001 +s-1001002 +s-1001003 +ip204-118 +mac3 +eurolines +webagent +s-1001004 +webengine +euroline +s1359a +s1360b +web689 +s1205ipmi +s1361a +s1361b +s4236ipmi +web2589 +cisco1-1 +cisco1-2 +s901ipmi +s625ipmi +s1363a +web2591 +s1363b +s338a +web2592 +s4305b +s1364a +s1364b +web399 +s1476ipmi +s1211ipmi +s217ipmi +s1120 +s1365a +web2596 +s1365b +s718ipmi +s4242ipmi +web429 +web2599 +web2199 +s4105ipmi +s1366a +s1366b +s906ipmi +s631ipmi +s1367a +s1367b +s917a +s1368a +s1368b +s1482ipmi +cisco8-1 +s1216ipmi +s1369a +s1370b +asak +s1442b +s340a +s430a +s1371a +s1371b +s912ipmi +cisco2-1 +cisco2-2 +s339d +ip204-119 +s1373a +s1373b +s1455 +s1222ipmi +s1374a +s1374b +s4253ipmi +ip204-121 +s4150ipmi +s1375a +s1375b +s906a +s1376a +ip204-122 +s1376b +s906b +s318b +s911a +s1227ipmi +s530a +s4258ipmi +s745ipmi +s529b +s4110ipmi +s923ipmi +s1317ipmi +cisco3-1 +ip204-124 +cisco3-2 +s4335ipmi +s4340a +s1233ipmi +s907b +s4264ipmi +s1480ipmi +s928ipmi +s531a +s112ipmi +s4340b +s404ipmi +s102396b +holod +s720a +s1238ipmi +s1460 +s4269ipmi +s1390a +s1390b +s1440ipmi +s908b +s1391a +s117ipmi +cisco4-1 +s1392b +s223ipmi +s724ipmi +s1403a +s1403b +s1404a +s1404b +s940ipmi +vip-club +s1395a +valid +valia +s1405b +s123ipmi +myvision +s910a +zdorovie +smilelife +vipgroup +s1396a +mail.world +pdu1-1 +pdu1-2 +kamchatka +tovar +bonappetit +viet +s909b +s173ipmi +eticaret +lees +s1397a +dbprod +candidate +s1397b +s1456b +flashlight +s1130 +s1408a +basis +s1408b +aceware +s1399a +s1399b +s128ipmi +s910ipmi +careless +ce1 +dwtest +s1411a +cline +starfighter +web159 +dbtest +s1411b +web2623 +slpda2 +s1412a +flogger +cisco5-2 +s4155ipmi +oasis2 +websp +digitallife +s634ipmi +web696 +web2629 +www.static2 +s1413a +cszx +21st +ccip +jsjxy +devl +www.devl +webdisk.devl +autoconfig.devl +autodiscover.devl +s1413b +chenkuo +abdulrehman +s911b +kk888 +telsis +tuthanika +ds110 +topspeed +rathna +stpatrick +starfoot +s4263a +rucker +devecser +dev.linksoflindon +www.fundraising +s1414a +linksoflondon +s1414b +mailrelay4 +yucca +s134ipmi +mailrelay3 +frans +fsi +rubidium +strange +helion +brinda +starker +s1310ipmi +vpn-6 +s1415a +tushar +s1415b +potassium +leopold +isaias +senecio +s502a +fatman +merritt +micael +gedeon +huashan +s502b +pablito +out9 +karnaugh +s1416a +pdu2-1 +pdu2-2 +host60 +ip204-130 +s4302ipmi +matias +host84 +host54 +host47 +host48 +host49 +s503a +host56 +host57 +host59 +host61 +host62 +host63 +out7 +s503b +out10 +s4115ipmi +out8 +www.myjobs +myjobs +moises +jateng +lyncfe +znc +ldn +s1417a +s1417b +haiyu +ftpserv +s504a +testserv +s504b +s4180a +s1418a +www.rsonline +fms3 +s1418b +s415ipmi +sis1 +s4180b +s505a +s140ipmi +s102392ipmi +s1419b +fluke +s4341ipmi +s4352a +s506a +s506b +qutaiba +akpinar +s820ipmi +jianyi +s1001ipmi +www.muir +shopp +s1421b +ikea +mail-m +s4307ipmi +cisco6-1 +www.marcin +lmu1 +dcomalumni +test-gateway +blenc03sl04 +blenc01sl08 +s1422b +lmufedserv +dsolmediasite +changemypassword +mediasiteiis +avstmobile +mediasitewms +teststudent +s508a +s1423a +webdisk.mailing +s1423b +s421ipmi +s509a +pointer +s509b +s1424a +gjjl +yiliao +www.web2 +pm04-1 +pm03-1 +mop +s102397ipmi +pkpk +cisco-res1 +cisco-res3 +daweb +skazka +anhkhoa +nzs +s511b +yag +ai100 +s4301ipmi +s1425a +s1425b +s512a +s1450a +mbox2 +s1426a +pdu3-1 +pdu3-2 +galias +mail.29 +smsh +esite +www.obmen +gpweb +s513a +www.demo6 +s1427a +sanchar +s1427b +s426ipmi +s330a +s1428a +s1428b +s515a +rd02 +s1429a +s1429b +s1012ipmi +s516a +s730ipmi +s4318ipmi +srv-1 +s1431a +ss13 +s1431b +s517a +sr12 +xxbs +www.zy +s517b +www.ln +www.jc +www.material +s1432a +mdm2 +cisco7-2 +cisco7-3 +s1395ipmi +dajie +webmail.film +www.komputery +ns.film +s518a +mail.film +germany2 +s432ipmi +s156ipmi +econ03 +s1433a +econ02 +mail.ap +klemens +zmx +usk +calendrier +shadow1 +thuvien +macewindu +zenworks +special3 +special2 +s1433b +s519a +ip204-114 +douzi +s520b +s1339ipmi +acf +s1434a +s1434b +s1017ipmi +s521a +sitebrand +xenpig +www.miner +s521b +s4324ipmi +s1435a +s1435b +vaccine +jpmalltest +inqshare +www.delo +s522a +s522b +s1436a +sa12 +oldcms +s1436b +tkt +pdu4-2 +bbs5 +vpos +igrs +mobilecity +uwallet +s713ipmi +ppsm +s523a +directadmin +s523b +s437ipmi +lnd1 +olna +sng1 +s1030 +s1437a +s1437b +s524a +s524b +s1438a +s1438b +s1023ipmi +wolontariat +s1452a +s102392a +s1439a +testcore +komfort +s1440b +teacherstore +scg-pub +tnsmtp +www.nd +gp1 +fuzoku +s1452b +horizont +s526a +balaji +autoconfig.bookmark +s526b +jimmy01 +autodiscover.bookmark +liew +webdisk.bookmark +libo +s1441a +cgiirc +s1441b +sv533 +darkover +s527a +s527b +s1442a +azazel +cisco8-2 +esus +s528a +s528b +sipproxy +vz02 +wildman +s1443a +s1443b +jsyl +s1304ipmi +s529a +kiri +s1028ipmi +jab +backupmail2 +scanmail +s1444a +smtp-gw2 +s1444b +s1039ipmi +s436e +s531b +s1445a +s1445b +s532a +submission +ff14-new +snappring +eclipsofeden +onepiece-musou2 +seimado +3dsgirlsmode +chaos-heroes +divinesoul +suparoboux +ys-celceta +granado-espada +parutena +hekiku-grace +s532b +pokemon-magunagate +onepiece-romancedawn +puzfanwiki-3246 +dungeonhero +guiltgear-plusr +arche-age +class9 +castlevania-makyou +biohazard6 +mapleland +fightersclub +dragon-quest7 +extroopers +rustyhearts +xn--3ds-z63b4f2a1b0dw667e +mamesangoku +dragonpoker +koisurukyabajogp +s1446a +toukiden +luigi-mansion +livetalk +s1446b +pdu5-2 +s533a +faqs +educloud +isen +s533b +s1447a +yxb +jwc2 +s1447b +verve +testss +bian +s1309ipmi +chenyi +bizz +d03 +hung +s534a +larochelle +dcgou +s534b +s1034ipmi +s1448a +s1448b +s535a +arzamas +photonet +vidau +vidas +egloo +s535b +s1449a +s1449b +s536a +s536b +balajitech +s729ipmi +s4264b +hitc +smartlab +s1451a +shahty +chelny +www.state +osmp +fest +nav2 +titan3 +timkiem +amss +korek +hiba +mryellow +amazigh +stabilo +speedgame +flippy +1st +s1451b +www.knights +abaddon +bbbb +manojk +s537a +bbe +s537b +bwd +crv +kraz +cisco9-1 +cisco9-2 +s4306ipmi +gue +s1315ipmi +itu +s1040ipmi +s1453a +s1453b +s4346ipmi +s1454a +u-s +fregat +s1454b +plp +rdi +redbaron +s4120a +rwh +smz +s510ipmi +s1455a +s1455b +ttv +boas +wiw +strannik +s1456a +pdu6-1 +gleysmo +pdu6-2 +nulled +olorin +sanctus +trafford +wuhuan +ggmm +smirnoff +www.genetics +s1321ipmi +s234ipmi +s1457a +lazio +adis77 +wiseguy +www.secured +s1457b +s735ipmi +s4352ipmi +linh +sals +s1458a +good4u +kernelpanic +patievn +hacker1991 +pdu13-3 +acrobat +sofd +sparks +s741ipmi +s1459a +s1459b +www.ihb +grav +www.kcb +s190ipmi +s1461a +dyxx +www.wee +mobitest +s1461b +s1326ipmi +www.fishi +xphone +narcotic +51av +revealer +ufuk +designor +vaka +s1462a +hailin +s1462b +s4357ipmi +s1463a +alexs +chaseman +adw +burglar +cge +granplus +esxi2 +xdns +xdot +fun-forum +manal +fiz +audioman +s1463b +lingdu +s930a +jia +free-hosting +kmw +s4342b +a1234567890 +yhwyxl +rak +s621ipmi +gmaiil +sailormoon +triangulum +syk +falc +uob +yojoy +xcn +www.fortune +www.robson +www.testdomain +momk +fellini +yszm +bigmama +thegladiator +www.fontane +cray +dadafarid +shahram +express1 +paddington +s1464a +www.nassau +n0n4m3 +ap28 +www.lessons +s1464b +scenery +s915ipmi +anse +arad +s1480 +cato +nofuture +allyouneed +s1465a +s1465b +s1332ipmi +www.marek +cmon +buzu +cotl +j2me +cvrd +dobo +czom +fars +dxsj +epix +gant +angelos +s1466a +exco +pdu7-1 +pdu7-2 +pdu7-3 +gobi +alhayah +mygallery +poesie +o51k +hlzx +hmsn +hpjs +htpc +kodi +s920b +me10 +linx +menz +naba +cory +ensi +mmss +nemi +s4363ipmi +s1467a +kuk +s1467b +s1468a +daher +orcamentos +rew2 +minhthanh +s1468b +saja +sex5 +kbo +s730a +smis +s1469a +cloe +thom +ssmm +s1469b +s1337ipmi +toya +ugur +vima +vlin +darc +possum +s1471a +wfjt +wika +gsb123 +wong +mianfei +www.tardis +xuhu +s1471b +zhao +qidian +efserver +ytuu +s4368ipmi +dsquared +s287a +asperger +hhzwly +s1472a +peko +acess +roflcopter +kompendium +www.pussy +guildwars +starlite +www.remax +s1472b +ws01qanet001 +ws01qanet002 +ws01qanet003 +ws01qanet004 +www.hotspot +ws01qanet006 +ws01qanet007 +ws01qanet008 +ws01qanet010 +s921a +thisandthat +rayden +s1473a +team-elite +lixinghua +www.openerp +backupftp +peak +s1473b +s216ipmi +s1474a +menfan +s1474b +deluxer +s330ipmi +techstore +bux4 +s1343ipmi +s1475b +s1476a +mybus +otherland +jwforum +skys215 +pdu8-1 +cerbero +outlawz +laotse +benni +huang1qi2 +claro +sabeur +suprema +pdu8-2 +huaiji +jsrong +alik +sex8 +abydos +pdu8-3 +jishui +shia +y1y2 +www.webserver +s1477a +s1477b +parcerias +danilo1051 +lovecards +yyhns +cassy +s1478a +www.vitalis +s1478b +s922a +s1479a +hotsprings +s1348ipmi +airen +basty +alexd +s1458b +hycssq +gbren +sensiz +djaligator +confixx +s1481a +s1481b +web2219 +cally +s4114ipmi +legendkiller +s1482a +htonline +chary +s1482b +shaded +tman +burak +zied212 +bagger +madeline +hackbase +one23 +stor11 +galaxytool +maggio +s503ipmi +tastypear +merrigan +frostmourne +signed +ra7el +dudul +s1483a +dzbbs +time2kill +homeboy +etang +ganja +s1483b +flagi +s1484a +s1484b +gifty +s1354ipmi +www.kan +hk008 +andpey +s239ipmi +s734b +hekui +gohsy +s742b +miles +s4119ipmi +s1460a +prenew +pdu9-1 +grunt +crossline +puyizhen +filehosting +pdu9-2 +mediator +janan +smilie +mtgw +aolzol +partnerzy +shabbir +mementomori +s508ipmi +mediaportal +isola +www.ontheroad +xbpjzx +s233ipmi +kewell +dilys +mikj +kicks +klbbs +terrox +rockitman +www.whitehouse +51xx +bir +www.gekko +s1360ipmi +bml +s4125ipmi +manno +s735a +manus +matis +zkb +s514ipmi +mbyte +s238ipmi +pre2 +lucky110 +animeforum +s640ipmi +steffi +losty +keyshop +dwf +s1365ipmi +lujun +t-rex +s4131ipmi +caowei +capone +ninad +mrtom +sdv +cassie +hkt +mutou +haikal +nnsky +nosek +raptimes +mysky +dominika +fishi +ut1 +paoli +madhur +blackwood +preferences +judson +mhk +s519ipmi +plany +rsleech +muk +s1371ipmi +s1105ipmi +fager +s4136ipmi +xinxi +www.garrison +proxi +qq163 +uoit +x2828 +blf +boondock +s4319ipmi +s440ipmi +inplay +fantasy88 +rls +sahan +dreambox +home-business +dreamfly +s410ipmi +schat +scorp +leminh +bonker +brt +shima +admin.t +partner.t +s801ipmi +s525ipmi +zdjecia +s1376ipmi +qazqaz +slava +s1111ipmi +uta +slobo +securitas +lsav +s-1003002a +sneki +s4310a +zithromax +snoox +sonos +yangjian +webcomp +sorin +gwen +wls +turkforum +freewebhost +webdisk.wptest +siggy +hoss +s4142ipmi +s737a +mylaner +tolar +testboard +autoconfig.wptest +fafa1688 +bkvpn +rbd +vanem +vassa +referenzen +autodiscover.wptest +webspell +city80 +sccf +danime +dapengtx +magier +whity +s601a +web169 +s601b +web1229 +www.suspended +s806ipmi +s-1003004a +davidb +web3129 +cego +wiingaard +mits +wx123 +marcop +s707 +xiong +s531ipmi +web2889 +marten +siem +www.feng +00001 +s602a +www.goya +uploadserver +theville +s602b +xssmw +s255ipmi +theoc +ds9 +monge +s926a +etudiant +fennec +aeronaut +s603a +p4p +caillou +web2759 +www.sign +chimere +cocoty +s4310b +noora +apresentacao +web719 +acamatzu +klemmster +web2489 +sq4 +gungnir +web1260 +thescream +test6389 +s1116ipmi +contao +theband +s1460b +merlyn +s604a +s4147ipmi +ppr +asw +arf +pal4 +s-1003007a +4seasons +web2940 +web659 +game-cheats +s605a +habboch +msn1 +s-1003008a +s536ipmi +tpvpn +xiaocharm +bjunioren +web1860 +ovid +ringerfotos +ovh1 +s607a +patos +web2739 +s607b +51free +s4337ipmi +web1289 +starmedia +cursed +ashuohu +bdd +web1302 +raddex +s1122ipmi +web1303 +pa2.lync +motorhome +web1600 +espaceclient +imail1 +s608a +pa1.lync +hcw +mimir +s746ipmi +s4153ipmi +pb1.lync +lsmods +testcitrix +s609a +www.traff +s4266a +onlineuk +shahar +nassau +calico +michael87 +s610e +pb2.lync +downunder +web1309 +weltwind +multiplayer +s610f +perfectworld +7seven +tianxin +s817ipmi +orar +elite-hacker +ndlm46 +s611a +erol +torn-ams5 +free4you +streamline +web1316 +kalinin +s266ipmi +s611f +s733ipmi +kairu +web1319 +vautour +s612a +s612e +s1403ipmi +web669 +xiaoxi +s1127ipmi +s613a +zenzen +phproxy +s4227ipmi +s613e +s613f +s4158ipmi +s614a +lostcity +s614b +miyamoto +seyuhai +momo90 +cp05qa009 +eagleone +s743b +web1329 +s823ipmi +candykey +esr +kintaro +dragao +pdu14-4 +carparking +technix +headstrong +s272ipmi +xiaoyao +web2289 +visao +baihua +ipost +pittbull +www.minigolf +smtps3 +smtps2 +me1 +s616a +s616b +zhiliao +ads5 +mymoodle +tie-up +farmgw +gmg +sohu +permanent +web1869 +huasende +mydriver +blacklotus +test-select +www.referenzen +my-dev +ad5 +img-test +kami001 +gb1 +test-column +makarov +s1408ipmi +s1130ipmi +mulder +s1133ipmi +jiangfei +ingredients +csserver +sasquatch +guckstdu +etienne +web2789 +test6296 +bluestorm +mursel +wireshark +s617b +litocaa +eastree +www.cupid +s4122a +cccp +missyou +test6247 +test6248 +test6250 +alexius +test6253 +test6254 +test6255 +s4164ipmi +test6257 +test6259 +test6261 +test6262 +s618a +test6264 +test6266 +res09 +test6270 +test6271 +test6273 +test6274 +test6275 +test6276 +test6277 +test6280 +test6281 +test6282 +test6283 +res07 +test6287 +test6288 +test6300 +test6301 +test6293 +test6304 +test6305 +res06 +test6307 +cisco4-1-bad +test6310 +test6311 +test6312 +test6313 +test6314 +test6315 +test6320 +test6322 +test6323 +test6324 +test6325 +res05 +test6328 +test6330 +s619a +test6333 +test6334 +test6336 +test6337 +test6338 +test6340 +test6341 +web524 +s619b +test6350 +test6351 +test6352 +test6353 +test6354 +test6355 +test6357 +s932a +s621a +test6358 +s741b +s709a +test6362 +test6363 +test6364 +test6365 +res10 +test6370 +test6371 +dude123 +test6373 +test6374 +test6375 +test6376 +test6377 +test6378 +test6380 +test6381 +test6382 +test6383 +test6384 +test6385 +liuxing +test6388 +test6400 +test6401 +test6402 +test6403 +nobelman +test6405 +test6406 +test6397 +test6408 +s1414ipmi +test6410 +test6411 +test6412 +web2799 +test6414 +test6415 +test6417 +test6418 +test6420 +test6421 +test6422 +test6423 +test6424 +test6425 +test6427 +test6428 +test6430 +test6431 +test6432 +test6433 +test6434 +test6435 +s622a +test6440 +test6441 +test6442 +test6443 +test6444 +test6445 +test6446 +test6447 +test6448 +test6450 +test6451 +test6452 +test6453 +test6454 +test6455 +test6456 +test6457 +test6458 +test6460 +test6462 +test6463 +test6464 +test6465 +test6466 +test6467 +test6468 +test6470 +test6471 +thl +test6474 +test6475 +test6476 +test6477 +test6478 +test6480 +test6481 +test6482 +test6486 +test6487 +test6488 +test6500 +test6501 +test6502 +test6493 +test6504 +test6505 +s1138ipmi +test6507 +s1466b +s4169ipmi +s4137ipmi +vh01 +faul +beatbreak +eoin +distrib +blackriver +mynick +erdzan +thesilence +demopage +equity +finanz +vh02 +wqdman +blackspace +balint +s624a +cello +s624b +traderportal +basalt +likenoother +zzm270782357 +daxiongmao +pedroferreira +s920a +daylite +jatek +sandstone +pams +s4246ipmi +web196 +s625a +s1419ipmi +wiki-dev +fms5 +s626a +esm5 +web469 +esm6 +esm7 +web2829 +esm8 +esm9 +esm10 +s626b +webdisk.sitebuilder +ftp.gallery +s4175ipmi +st17 +st24 +st25 +st27 +st31 +st9 +st19 +st30 +agility +s627b +ykc +ikuei +koutoku +toyokawa +web198 +gbcp +panopto +thing2 +uphinh +edari +zirconia +testfiles +wmiller +suchart +alexi +akmal +s4208a +staging.dev +babyx +hammoud +lazerpoint +hy1 +goo +adhoc +pru +s628a +ekatalog +s840ipmi +epbx2 +synxis +longhorn +sisu +s931a +web2302 +web1390 +giangnt +s4353ipmi +jffnms +s288ipmi +s600 +web534 +web1392 +web1880 +wwwwwww +multidevice +bluesun +fringe +web1929 +npp +kinen +s630b +web1399 +s4108ipmi +valverde +sm69 +s1425ipmi +salak +s632a +docworks +sakusaku +webhelp +hisho +s4181ipmi +s4311a +newapi +s633a +web2859 +mins +somphong +chekui +anicet +www.collections +s633b +mtest2 +web1420 +s526ipmi +envoi +mailsv1 +ocean11 +web59 +s743a +ns1.dec +www.futures +izu +s610ipmi +s304ipmi +fmat +proofs +patientclinic +myminicity +www.thenews +www.gamersparadise +s4101a +www.sparky +heartrhythmclinic +www.theda +maytinh +malin +tamam +web2869 +talib +ladyp +hashim +regulators +www.starz +www.noorsat +phillyskaters +s4101b +ansarysa +www.abood +blackwatch +ricksplace +www.adnan +legio +www.bandofbrothers +affguild +cardiosurgery +web69 +s202ipmi +s635a +6rb +numerology +pavlovic +horseheaven +www.godlike +zhangbo +www.newengland +myvoice +siteweb +www.regulators +lve +rohclan +terrafirma +www.alternativeenergy +icemage +www.testingplace +web2879 +www.thedoghouse +r131gzhbxci3cnaq2to9878u6mx7asr7vepltmpq +baladna +www.kidney +www.trick +scionsoffate +s2s +egr +s1431ipmi +blazed +etw +s4102b +igt +applicants +confused +web79 +www.sithacademy +s636a +s4103a +s4103b +t3m +noh +owe +psq +rcr +rfm +s637a +www.blackwatch +row +www.theway +www.ricksplace +theburrow +vd3 +wthfwcluster +wal +s4104a +wgr +www.playtime +www.numerology +tl1 +hpclub +web2900 +justforkicks +web2891 +web220 +wthfwmgmt +hamami +web2892 +www.sacredrealm +web2893 +wthadfs +web2894 +www.xerxes +web1890 +www.amipest +amipest +web2895 +s4104b +thebus +www.thetardis +web2896 +s1427ipmi +animefreak +diablerie +hooligans +web2897 +ihotblack +thesithacademy +shadesofink +www.magnacarta +web2898 +saihu +s726ipmi +web2899 +doglovers +web1891 +biteme +www.cappa +web2911 +www.dbadmintrillian +cesar1 +web2912 +selva +www.roughnecks +saicomputers +higherground +web2913 +s638a +thenewfrontier +360clan +www.jediknights +www.week4 +web2914 +www.renegades +web1000 +web1001 +web1002 +web1003 +web1004 +web1005 +web1007 +web1008 +web1010 +web1011 +web1012 +web1014 +web1015 +web1016 +s309ipmi +web1017 +web1018 +www.animefreak +web1022 +web1023 +web1024 +web1025 +web1026 +web1027 +web1028 +web1030 +web1031 +web1032 +web1033 +web1034 +web1035 +web1036 +web1037 +web1038 +web1040 +web1041 +web1042 +web1043 +web1044 +web1045 +web1047 +web1048 +web1049 +web1051 +web1052 +web1053 +web1054 +web1055 +web1056 +web1057 +web1058 +web1060 +web1061 +web1062 +web1063 +web1064 +web1065 +web1066 +web1067 +web1068 +web1069 +web1071 +web1072 +web1074 +web1075 +web1076 +web1077 +web1078 +web1079 +web1081 +web1082 +web1083 +web1084 +web1085 +web1087 +www.crhs +web1090 +web1091 +web1092 +web1094 +web1095 +web1106 +smartphones +web1097 +web1098 +web1110 +splitinfinity +web1112 +web1113 +web1114 +web1115 +www.comworld +web1117 +web1118 +web1120 +web1121 +web1122 +web1124 +s4105b +web1125 +web1126 +www.wesam +web1128 +videoplanet +web1132 +web1133 +web1134 +web1135 +web1136 +web1137 +web1138 +web1140 +web1141 +web1142 +web1143 +web1144 +web1145 +web1146 +web1147 +web1148 +web1150 +www.blazed +web3019 +www.nintendowifi +pwm +www.bones +web1211 +web1212 +web1213 +web1214 +web1215 +web1216 +web1217 +web1218 +web1221 +web1222 +web1223 +web1224 +web1225 +web1227 +web1228 +web1230 +web1231 +web1232 +web1233 +web1234 +web1235 +web1236 +web1237 +web1238 +web1240 +web1241 +web1242 +web1243 +web1244 +web1245 +web1246 +web1247 +web1248 +web1250 +web1251 +web1252 +web1254 +web1255 +web1256 +web1257 +web1258 +web1261 +web1262 +web1263 +web1264 +web1265 +web1267 +web1268 +web1270 +web1271 +web1272 +web1274 +web1275 +web1276 +web1277 +web1278 +web1279 +web1281 +web1282 +web1283 +web1284 +web1285 +web1287 +leosplace +web1300 +web1301 +web1292 +web1304 +web1305 +web1306 +web1307 +web1308 +web1311 +web1312 +web1313 +web1314 +web1315 +web1317 +web1318 +web1320 +web1321 +web1322 +web1324 +web1325 +web1326 +web1327 +web1328 +web1331 +web1332 +web1333 +web1334 +web1335 +web1336 +web1337 +web1338 +web1340 +web1341 +web1342 +web1343 +web1344 +web1345 +web1346 +web1347 +web1348 +web1350 +web1351 +web1352 +web1353 +web1354 +web1355 +web1356 +web1357 +web1358 +web1360 +web1361 +web1362 +web1363 +web1364 +web1365 +s639a +web1366 +web1367 +web1368 +web1369 +web1371 +web1372 +web1373 +web1374 +web1375 +web1376 +web1377 +web1378 +web1380 +web1381 +web1382 +web1383 +web1384 +web1385 +web1386 +web1387 +web1388 +web1400 +web1401 +web1402 +web1403 +web1404 +web1395 +web1396 +web1407 +web1408 +web1409 +web1411 +web1412 +web1413 +web1414 +web1415 +web1416 +web1417 +web1418 +web1419 +web1421 +web1422 +web1423 +web1424 +web1425 +web1426 +web1427 +www.gamedev +web1429 +web1431 +web1432 +web1433 +web1434 +web1435 +web1436 +web1437 +web1438 +web1439 +web1441 +web1442 +web1443 +web1444 +web1445 +web1446 +web1447 +web1448 +web1450 +hostile +web363 +www.desouza +retouching +web2930 +web1511 +web1512 +web1513 +web1514 +web1515 +web1516 +web1517 +web1518 +web1520 +web1521 +web1522 +s4106a +web1523 +web1524 +web1525 +web1526 +web1527 +web1528 +web1531 +web1532 +web1533 +web1534 +web1535 +web1536 +web1537 +web1538 +web1540 +web1541 +web1542 +web1543 +web1544 +web1545 +web1546 +web1547 +web1548 +web1550 +web1551 +web1552 +web1553 +web1554 +web1555 +web1556 +web1557 +web1558 +web1559 +web1561 +web1562 +web1563 +web1564 +thorns +web1566 +web1567 +web1568 +web1570 +web1571 +theprence +web1573 +web1574 +web1575 +web1576 +web1577 +manna +web1580 +web1581 +web1582 +web1583 +web1584 +www.dbzuniverse +web1586 +web1587 +web1588 +web1601 +web1602 +web1593 +web1604 +web1605 +web1606 +web1607 +web1598 +web1609 +web1611 +web1612 +web1613 +web1614 +www.chums +web1616 +web1617 +web1618 +web1619 +web1621 +web1622 +web1623 +web1624 +web1625 +web1626 +web1627 +realms +web1629 +web1631 +web1632 +web1633 +web1634 +web1635 +web1636 +web1637 +web1638 +web1640 +web1641 +web1642 +web1643 +web1644 +web1645 +web1646 +web1647 +web1648 +s1436ipmi +web1649 +web1651 +web1652 +web1653 +web1654 +web1655 +web1656 +web1657 +web1658 +web1659 +web1661 +web1662 +web1663 +web1664 +web1665 +web1666 +web1667 +web1668 +web1670 +web1671 +s740a +web1672 +web1673 +web1674 +web1675 +web1676 +web1677 +web1678 +web1680 +web1681 +web1682 +web1683 +web1684 +web1685 +web1686 +web1687 +www.zamalek +web1690 +web1691 +web1692 +web1703 +web1704 +web1695 +web1706 +web1707 +web1708 +web1710 +web1711 +web1712 +web1713 +web1714 +web1715 +web1716 +web1717 +web1718 +web1720 +web1721 +web1722 +web1723 +web1724 +web1725 +web1726 +web1727 +web1728 +web1729 +web1731 +web1732 +web1733 +web1734 +web1735 +web1736 +web1737 +web1738 +web1739 +web1741 +web1742 +web1743 +web1744 +web1745 +web1746 +web1747 +web1748 +web1750 +nightriders +phonebill +web1910 +web1896 +emarati +web490 +web1811 +web1812 +web1813 +web1814 +web1815 +web1816 +web1817 +web1818 +web1820 +web1821 +web1822 +web1823 +web1824 +web1825 +web1826 +web1827 +www.epay +web1830 +web1831 +web1832 +web1833 +web1834 +web1835 +web1836 +web1837 +web1838 +web1840 +web1841 +web1842 +web1843 +web1844 +s744a +web1845 +web1846 +web1847 +web1848 +web1850 +web1851 +web1852 +web1853 +web1854 +web1855 +web1856 +web1857 +macgyver +web1861 +web1862 +web1863 +web1864 +web1865 +web1867 +web1868 +web1870 +web1871 +web1872 +web1874 +web1875 +web1876 +web1877 +web1878 +web1881 +web1882 +web1883 +web1884 +web1885 +web1887 +thetardis +web2000 +web2001 +web2002 +web2003 +web2004 +s641a +6688 +yourweb +web2014 +web2015 +web2017 +web2018 +www.insanesanity +web2021 +web2022 +web2024 +molham +web2026 +web2027 +web2028 +web2031 +chicanorap +web2033 +web2034 +web2035 +web2036 +web2037 +www.mysample +web2040 +web2041 +web2042 +web2043 +web2044 +thedoghouse +web2046 +web2047 +web2048 +web2050 +web1951 +ws01qanet005 +web1953 +web1954 +web1955 +web1956 +web1957 +romane +web1960 +web1961 +web1962 +web1963 +web1897 +animeuniverse +web1966 +web1967 +web1968 +web1970 +web1971 +www.thedeathsquad +web1973 +web1974 +web1975 +web1976 +web1977 +toby +web1980 +web1981 +web1982 +web1983 +web1984 +web1985 +web1986 +web1987 +web1988 +web1991 +web1992 +web1993 +web1994 +web1995 +web1996 +web1997 +web1998 +web1999 +web2111 +web2112 +web2113 +web2114 +web2116 +web2117 +web2118 +web2120 +web2121 +web2122 +web2123 +web2124 +web2125 +web2126 +web2127 +milkman +web2130 +web2131 +web2132 +web2133 +web2134 +web2135 +web2136 +web2137 +web2138 +web2140 +web2141 +web2142 +web2143 +web2144 +web2145 +web2146 +web2147 +web2148 +web2150 +web2151 +web2152 +web2153 +web2154 +web2155 +web2156 +web2157 +web2158 +web2161 +web2162 +web2163 +web2164 +web2165 +web2166 +web2167 +web2168 +web2170 +web2171 +web2172 +web2173 +web2174 +web2175 +web2176 +web2177 +web2178 +web2180 +web2181 +web2182 +web2183 +web2184 +web2185 +web2186 +web2187 +www.puddles +web2200 +web2201 +web2202 +web2203 +web2204 +web2205 +web2206 +web2207 +web2208 +web2210 +web2211 +web2212 +web2213 +web2214 +web2215 +web2216 +web2217 +web2218 +web2220 +web2221 +web2222 +web2223 +web2224 +web2225 +web2226 +web2227 +web2228 +web2230 +web2231 +web2232 +web2233 +web2234 +web2235 +web2236 +web2237 +web2238 +web2240 +web2241 +web2242 +web2243 +web2244 +web2245 +web2246 +web2247 +web2248 +web2250 +web1898 +web2252 +web2253 +web2254 +web2255 +web2256 +web2257 +freebieworld +web2260 +web2261 +web2262 +web2263 +web2264 +s4107a +web2266 +web2267 +web2268 +web2270 +web2271 +www.kurdistan +web2273 +web2274 +web2275 +web2276 +web2277 +whitecliffs +web2280 +web2281 +web2282 +web2283 +web2284 +s4107b +web2286 +web2287 +mercenaries +web2301 +www.weareafamily +web2303 +web2304 +web2305 +web2306 +web2307 +temp09 +web2310 +web2311 +web2312 +web2313 +web2314 +urbanchaos +web2316 +web2317 +web2318 +web2320 +web2321 +sunt +web2323 +web2324 +web2325 +web2326 +web2327 +www.gatelords +web2330 +web2331 +web2332 +web2333 +web2334 +web2335 +web2336 +web2337 +web2338 +web2340 +web2341 +web2342 +web2343 +web2344 +web2345 +web2346 +web2347 +www.newhorizon +web2350 +www.crimsonrose +www.darkbrotherhood +s4202ipmi +web2272 +s642a +web2411 +web2412 +web2413 +web2414 +web2415 +web2416 +web2417 +web2418 +web2420 +web2421 +web2422 +web2423 +web2424 +web2425 +web2426 +web2427 +web2428 +web2431 +web2432 +web2433 +web2434 +web2435 +web2436 +madhatters +web2438 +web2440 +web2441 +web2442 +web2443 +web2444 +web2446 +web2447 +web2448 +web2450 +web2451 +web2452 +web2453 +web2454 +s1470a +web2455 +web2456 +web2457 +chickens +web2460 +web2461 +web2462 +web2463 +web2464 +web2465 +web2466 +web2467 +web2468 +web2470 +web2471 +web2472 +web2473 +web2474 +web2475 +web2476 +web2477 +web2478 +web2480 +web2481 +web2482 +web2484 +web2485 +web2486 +web2487 +web2488 +web2501 +web2502 +web2503 +web2504 +web2505 +web2506 +web2507 +web2508 +web2510 +web2511 +web2512 +web2513 +web2514 +web2515 +web2516 +web2517 +web2518 +web2520 +web2521 +web2522 +web2523 +web2524 +web2525 +web2526 +web2527 +www.payback +web2530 +web2531 +web2532 +web2533 +web2534 +web2535 +web2536 +web2537 +web2538 +web2540 +web2541 +web2542 +web2543 +web2544 +web2545 +web2546 +web2547 +web2548 +web2550 +web2551 +web2552 +web2553 +web2554 +web2555 +web2556 +web2557 +web2558 +web2561 +web2562 +web2563 +web2564 +web2565 +web2567 +web2568 +web2570 +web2571 +web2572 +web2574 +web2575 +web2576 +web2577 +web2578 +web2581 +web2582 +web2583 +web2584 +web2585 +web2587 +daniel2 +web2600 +web2601 +web2602 +web2603 +web2604 +web2605 +web2606 +web2607 +web2608 +web2610 +web2611 +web2612 +web2613 +web2614 +web2615 +web2617 +web2618 +web2620 +web2621 +web2622 +web2624 +web2625 +web2626 +web2627 +web2628 +web2631 +web2632 +web2633 +web2634 +web2635 +web2636 +web2637 +web2638 +web2640 +web2641 +web2642 +web2643 +web2644 +web2645 +web2646 +web2647 +web2648 +web2650 +web1519 +s4108b +web2711 +web2712 +web2713 +web2714 +web2715 +web2716 +web2717 +web2718 +web2720 +web2721 +web2722 +web2723 +web2724 +web2725 +web2726 +web2727 +web2728 +web2730 +web2731 +web2732 +web2733 +web2734 +web2735 +web2736 +web2737 +web2738 +web2740 +web2741 +web2742 +web2743 +web2744 +web2745 +web2746 +web2747 +web2748 +web2750 +web2751 +web2752 +web2753 +web2754 +web2755 +web2756 +web2757 +web2758 +web2761 +web2762 +web2763 +web2765 +web2766 +web2767 +web2768 +web2770 +web2771 +web2772 +web2773 +web2774 +web2775 +web2776 +web2777 +web2778 +web2780 +web2781 +web2782 +web2783 +web2784 +web2785 +web2786 +web2787 +web2788 +web2800 +web2801 +web2802 +web2803 +web2804 +web2805 +web2806 +web2807 +web2808 +web2810 +web2811 +web2812 +web2813 +web2814 +web2815 +web2816 +web2817 +web2818 +web2820 +web2821 +web2822 +web2823 +web2824 +web2825 +web2826 +web2827 +s933a +web2828 +web2831 +web2832 +web2833 +web2834 +web2835 +s642e +web2836 +web2837 +web2838 +web2840 +web2841 +web2842 +web2843 +web2844 +web2845 +web2846 +web2847 +web2848 +web2850 +web2851 +web2852 +web2853 +web2854 +web2855 +web2856 +web2857 +web2858 +web2860 +web2861 +web2862 +web2863 +web2864 +web2865 +web2866 +web2867 +web2868 +web2870 +web2871 +web2872 +web2873 +web2874 +web2875 +web2876 +web2877 +web2878 +web2880 +web2881 +web2882 +web2883 +web2884 +web2885 +web2886 +web2887 +web2888 +web2901 +web2902 +web2903 +web2904 +web2905 +web2906 +web2907 +web2908 +web2910 +web3011 +web3012 +web3013 +web3014 +web3015 +web3016 +web3017 +web3018 +web3020 +web3021 +web3022 +web3023 +web3024 +web3025 +web3026 +web3027 +web3028 +web3030 +web3031 +s642f +web3032 +web3033 +web3034 +web3035 +web3036 +web3037 +web3038 +web3040 +web3041 +web3042 +web3043 +web3044 +web3045 +web3046 +web3047 +web3048 +web3050 +web3051 +web3052 +web3053 +web3054 +web3055 +web3056 +web3057 +web3058 +web3061 +web3062 +web3063 +web3064 +web3065 +web3066 +web3067 +triggerhappy +web3070 +web3071 +web3072 +web3073 +web3074 +web3075 +web3076 +web3077 +web3078 +web3080 +web3081 +web3082 +web3083 +web3084 +web3085 +web3086 +web3087 +faisal1 +web3100 +web3101 +web3102 +web3103 +web3104 +web3105 +web3106 +web3107 +web3108 +web3110 +web3111 +web3112 +web3113 +web3114 +web3115 +web3116 +web3117 +web3118 +web1529 +web3121 +web3122 +web3123 +web3124 +web3125 +web3126 +web3127 +web3128 +web3131 +web3132 +web3133 +web3134 +web3135 +web3136 +web3137 +web3138 +web3140 +web3141 +web3142 +web3143 +web3144 +web3145 +web3146 +web3147 +web3148 +web3150 +web3151 +web3152 +web3153 +web3154 +web3155 +web3156 +web3157 +mesfichiers +web3160 +web3161 +web3162 +web3163 +web3164 +web3165 +web3166 +web3167 +web3168 +web3170 +web3171 +web3172 +web3173 +web3174 +web3175 +web3176 +web3177 +web3178 +web3180 +web3181 +web3182 +web3183 +web3184 +web3185 +web3186 +web3187 +web3188 +web3201 +web3202 +web3203 +web3204 +web3205 +web3206 +web3207 +web3208 +web3210 +web3211 +web3212 +web3213 +web3214 +web3215 +web3216 +web3217 +web3218 +web3220 +web3221 +web3222 +web3223 +web3224 +web3225 +web3226 +web3227 +s1470b +web3230 +web3231 +web3232 +web3233 +web3234 +web3235 +web3236 +web3237 +web3238 +web3240 +web3241 +web3242 +web3243 +web3244 +web3245 +web3246 +web3247 +web3248 +web3250 +s4109a +www.chicanorap +www.coffeetalk +elmagic +realdeal +www.daedalus +2222 +www.erepublik +www.freeforall +fsgroup +izaphod +fabulations +amonline +glr +1212 +www.rst +agoraphobia +fragglerock +www.acrossthepond +www.urbanchaos +s315ipmi +www.bluray +pps01 +childrenofthenight +www.oddfellow +www.handson +www.madhatters +s4111a +www.theelites +dbadminmarvin +www.hangman +thechosenfew +www.zappa +www.chooseme +www.elsaedy +noorsat +poema +nadim +s4111b +skyf +m38 +theswarmwar +www.elves +www.amnesty +www.wargods +www.sailor +patiyut +wutang +www.abf +www.theimperiallegion +tm2 +cics +s1442ipmi +www.bcr +www.btw +mobilepalace +www.crg +www.dla +archimede +www.dkt +conservatoire +www.dow +www.egr +www.kamensk-uralskiy +www.eoa +web2292 +www.etw +www.postyourstuff +www.yerbamate +www.hos +elitewarriors +www.jak +s712ipmi +vremeto +www.justus +serenityguild +www.maa +www.lod +sondos +www.salama +mailgw3 +s-1003007 +www.mog +www.noh +www.ork +www.owe +www.ppt +www.psq +teremok +www.sc2 +www.rov +www.row +www.islarti +s-1003008 +www.trg +cricri +www.uhs +www.vba +www.trt +kupon +www.funit +s4112b +www.wgr +www.zmm +numerique +www.moneymaking +www.ahlamontada +voodoocrew +s4207ipmi +www.soundsource +ffforum +sher +s721 +www.chatroom +www.footballforum +www.scionsoffate +www.superc +justus +blendedlearning +www.thesithacademy +funkytown +www.contacts +hamada2010 +pavlik +www.warpigs +relic +s646a +www.htmltest +sanane +wolfclan +www.morrow +www.chatterbox +www.justforkicks +dch +moviemagic +sportlife +www.hastings +s4113a +brokenstraw +awf +www.voodoocrew +superc +www.satsat +thaifood +www.spiders +www.deviance +www.1001 +beardeddragons +fighterace +revue +www.totaleclipse +s4113b +www.sweets +www.phillyskaters +4jesus +www.haste +ceridian +yerbamate +www.havoc +s4114a +www.highschool +wwwtm +s4114b +norris +stopsmoking +www.moviemagic +eworkshop +s321ipmi +samra +s744b +www.higherground +www.rednecks +testo +adulthood +montest +www.primrose +www.thebus +www.fighterace +www.hivemind +footyfans +www.ladyp +desouza +s4115a +www.guardians +www.adulthood +www.4all +www.azerty +dkt +chatalot +zik +footballfrenzy +www.catalyst +www.back2back +therefuge +www.rainclan +zamalek +formas +flyingcircus +teamworkshop +myislam +luntai +therebellion +palstine +liverpoolfans +mohamedzaki +www.thenewfrontier +ldgaming +comworld +www.sharpie +www.diablerie +nobilis +konoki +www.ihotblack +myles +www.iford +www.stickwar +www.thepit +theda +creativecorner +www.doglovers +insaneasylum +theimperiallegion +s4115b +www.horseheaven +www.printing +cavaliers +www.noob +katoteros +www.simulation +www.fathers +camelia +www.lunaris +tcf +kodclan +www.shadesofink +tt9 +uhs +www.numberone +urd +bat2 +gangstaparadise +alfnan +web1916 +www.deathinc +www.leosplace +deathscythe +p005 +madcows +s934a +www.splitinfinity +s1447ipmi +www.xsquad +exiledro +s101390a +youdecide +www.programmersparadise +www.hybrid +s4116a +www.elders +narutorpg +www.anrforum +s4116b +www.sellit +trick +alkamar +alaseer +www.parklands +www.dynamite +bader +darkhearts +www.beardeddragons +xsquad +www.elhamd +haflinger +www.townline +cisco12-2 +sori +sejours +almot +elsaedy +www.nightriders +elders +mystik2 +www.thechosenfew +www.nwoclan +sportsnetwork +www.animeuniverse +theartofwar +divers +s4213ipmi +www.starcity +www.revelation +wargods +futurefiction +roughnecks +web3089 +exohax +turkteam +hot1 +www.chaotic +elhamd +www.darkhearts +www.dungeons +devgames +fables +www.alibaba +userbars +islarti +theasylum +back2back +anhlam +www.footballfrenzy +slaughter +www.selfish +kimvan +www.wolfclan +s4117a +www.kiran +s4117b +chooseme +warpigs +numberone +noursat +www.legionofdarkness +www.chatter +legionnaire +stmichaels +s4118a +sktao +s4118b +www.sportsbetting +s326ipmi +week4 +programmersparadise +s4119a +sandanski +www.prisonbreak +www.hesham +foxscape +woodbine +legionofdarkness +jedimasters +eclips +www.creativecorner +www.fabulations +battleroyale +adsa +bd16 +www.agoraphobia +chums +parinya +bedo +sriram +atef +cctb +s4119b +www.dbadminmarvin +insanesanity +microsoftwindows +steg +ws123 +clanex +www.emoney +zms +azhr +www.liviu +mehrshad +www.discussion +www.6rb +s1453ipmi +s4121a +milla +wr3an +baljeet +habilar +mapdev +www.thegods +orangebox +knuckles +s4121b +fade +duha +s3eed +www.tribalwars +vpn-us-west +th1 +www.roadtrips +homeschool +weareafamily +www.marwan +www.theswarmwar +s4218ipmi +www.stigmata +www.rstools +www.nutter +www.serenityguild +www.theunderworld +hoda +www.guesswho +htfc +hupt +thedon +www.blendedlearning +s4213b +gali +s4103ipmi +s4122b +www.group4 +lexo +www.abdo +www.karnage +kannan +www.mitie +www.funkytown +shimmy +www.alaa +www.bedo +vpn-au +www.lovehome +masrya +thegreatescape +www.aucc +www.srsm +alsace +www.equinox +newp +www.habilar +s4123a +www.chen +flanders +www.cogs +s4123b +rednecks +s332ipmi +www.dmo3 +tsj +vendome +s4124a +toh +www.therebellion +sharpie +www.thesims +s4124b +s306ipmi +www.entity +qne +www.liverpoolfans +www.dust +www.b3 +oui +akhbar +pshl +s1458ipmi +www.gaza +s419ipmi +pwnd +footballforum +testxml +s4125a +bjbjbj +simplehelp +s4125b +www.hala +gdw +thesims +sithacademy +s101392ipmi +www.software4free +federer +rsps +s4224ipmi +www.hood +srsm +www.htfc +suba +www.insaneasylum +ca2 +rainclan +www.iman +euroleague +entity +real4ever +sacredrealm +www.faithless +patterson +smarttech +s4126a +s4126b +www.dystopia +xintuo +xaoc +www.aliman +soundsource +s4127a +www.chitchat +elves +tollfree +swapitshop +www.mizo +s4127b +www.nate +www.mofo +www.affguild +www.nita +s613ipmi +s337ipmi +s4128a +stickwar +web2949 +www.hiking +www.katoteros +s4128b +myfirstforum +s101392a +s4129a +minime +www.none +www.yourhealth +allsaints +www.talent +www.euroleague +www.denial +class2d +thoughtcrime +sumana +www.sart +coton +www.serc +www.sportsnetwork +mancave +s4129b +site4u +hiking +jediknights +www.swapitshop +leonjackson +www.alsadr +deathinc +www.tigerclan +tigerclan +www.void +dbadmintrillian +www.mohamedzaki +s4229ipmi +forgottencoast +testingplace +www.abdelrahman +dbzuniverse +web3099 +www.rohclan +www.childrenofthenight +www.slarti +anrforum +www.baladna +www.cavaliers +www.ultras +s4131a +tapion +king1 +alsadr +www.thehill +web186 +s4131b +www.demigod +s4132a +aliance +theunderworld +townline +s1433ipmi +s618ipmi +s4133a +s4133b +s1469ipmi +s4134a +s1204ipmi +s717ipmi +s4235ipmi +s4135a +s4135b +s937a +s4136a +s4136b +s624ipmi +router1ipmi +s4137a +s4137b +s1475ipmi +s4138a +s4138b +s1210ipmi +s4139a +s4139b +www.lotto +s4141a +s4141b +www.forward +s905ipmi +s629ipmi +s1475a +s4142b +s938a +s1481ipmi +s4143b +s1215ipmi +s4144a +s4144b +s4145a +s4145b +s911ipmi +tpsmtp +fcmi +tmsg2 +webhr +irmtrade +apitwca +tpsmtp2 +goldtwca +s635ipmi +tmsg +webrtqt03 +webrtqt01 +webrtqt +bctrade +webrtqt05 +webrtqt04 +s4146a +elearnap +webrtqt02 +rmtrade +nbclient1 +s4146b +s1303ipmi +s4147a +s4147b +orders2 +netops +gwanak +www.nsw +s1221ipmi +s745b +wandoo +tmp1 +s185b +s4148a +s4148b +s4252ipmi +tmp8 +lubin +tmp9 +www.jaroslaw +s1476b +s4149a +s229ipmi +www.ostroda +s916ipmi +www.znin +s607ipmi +www.gliwice +s641ipmi +znin +s4151a +s4151b +www.wolsztyn +s1109a +s4152a +s4152b +slupsk +s1226ipmi +s4153a +s4153b +s4257ipmi +s4154a +ostroda +marki +rt3 +www.debica +www.osiek +s4154b +s922ipmi +s640a +www.brzozow +hel +thw +s4155a +s4155b +s4156a +mieszkowice +milakowo +s4156b +chorzele +piaseczno +avaya +s1232ipmi +brzozow +s1309 +s701a +s4157a +s4157b +s4360b +s4263ipmi +s702a +s4158a +s4158b +s927ipmi +s703a +s1130a +osiek +www.gizycko +s4159b +kolbuszowa +s111ipmi +nightline +s704a +s4161a +www.rzeszow +s4161b +orneta +www.rozan +s1237ipmi +s705a +s222ipmi +www.bialystok +s4162b +gizycko +s4268ipmi +www.lubaczow +s824ipmi +s706a +s4163a +www.turek +www.gostyn +s4163b +s933ipmi +s707a +s4164a +s4164b +www.milakowo +www.chorzele +s116ipmi +s708a +s4165a +czestochowa +s4165b +s1243ipmi +nintendoland +s4166a +s4166b +www.mielec +s711a +s4167a +lesko +s4167b +s4322ipmi +s712a +s1480a +s4168b +s1479b +s122ipmi +s4169a +strzyzow +ropczyce +turek +s4169b +s908ipmi +www.lesko +s714a +s4171a +s4171b +s715a +s4172a +s4172b +s716a +s4173a +s4173b +www.jaslo +www.slupsk +www.ilawa +www.tarnobrzeg +s640b +s403ipmi +s717a +s4174a +s4174b +s4366ipmi +s718a +s4175a +s4175b +lubaczow +s740 +s719a +s4176a +s4176b +s721a +s4177a +s4177b +s408ipmi +jaroslaw +s722a +s4178a +s133ipmi +tarnobrzeg +s635b +jaslo +s4340ipmi +s723a +ilawa +www.scp +s4179a +s4179b +wolsztyn +s818ipmi +s724a +s4181a +s4181b +s746b +s725a +www.strzyzow +www.ropczyce +s4182a +oldsmtp +s4182b +s414ipmi +s4183a +s4183b +gostyn +s102391ipmi +www.orneta +s727a +s4184a +s4184b +s728a +s4185a +pd1 +s4185b +s918ipmi +s409 +www.kolbuszowa +s4186a +mielec +s4186b +s731a +s4187a +s4187b +s144ipmi +s713a +s102396ipmi +s732a +s4188a +s4188b +s227ipmi +s1005ipmi +www.livescores +ftp.old +ftp.game +s733a +oftp2 +localnet +periwinkle +s4312ipmi +s1020a +mailin1 +valerian +s4201a +nmr +mailin2 +rsl +s4201b +s701ipmi +s1404ipmi +s4202a +s4202b +s736a +gridview +s4203a +s4203b +s505ipmi +s1011ipmi +rushmore +shasta +s4204a +s4204b +s4317ipmi +d2l +s738a +s4205a +s4205b +s706ipmi +s4206a +s4206b +s431ipmi +s741a +petition +s4207a +s4207b +s742a +s1016ipmi +anc1 +s4208b +s4323ipmi +s4160ipmi +s4314a +relais2 +s4209a +s4209b +www.ultimate +global1 +s4320a +talento2 +crossroad +s4211a +s4211b +autoconfig.library +autodiscover.library +s436ipmi +s4226b +s745a +chapo +s4212a +www.harley +s4212b +s746a +webdisk.cp +s4213a +coimbatore +s1022ipmi +www.coimbatore +s4314b +parent +s4328ipmi +zacharias +sportmaster +s747a +s4120ipmi +geab +s4214b +s620b +strand +mazars +harlequin +benq +xlnt +s748a +e-butik +fns +oneoff +alfa3 +s4215a +s4215b +s442ipmi +samhall +s525a +questionmark +s4216a +webgui +procom +xcri +s4216b +www.fencing +s902b +cisco3-1bad +s4159a +s4217b +s1027ipmi +s4334ipmi +infoserv +s4218a +s4218b +s4219a +s4219b +s723ipmi +s4221a +s4221b +s4160b +s1119a +www.bestdeals +bestdeals +s1308ipmi +s4222b +ewr +s1033ipmi +s4339ipmi +s4223a +s4223b +s4224a +vss2 +webdisk.tm +autoconfig.tm +autodiscover.tm +s1450ipmi +s728ipmi +s747b +s4225a +s4226a +s1314ipmi +s1038ipmi +vpn-us +s4345ipmi +s4227a +s4228a +s4228b +s734ipmi +s1409ipmi +s4229a +s4230b +s1309a +images-nc +images-na +images-ns +images-ni +images-no +images-ng +s930ipmi +s1309b +s4231a +s4230a +s1319ipmi +s4215ipmi +s4232a +s4232b +refill +compliance +kob11 +s4168a +s739ipmi +s1359ipmi +s919ipmi +s4235a +cathaylife +chinatrust +s1325ipmi +s735 +s4236a +s4356ipmi +s4165ipmi +s720 +s4237a +s4237b +www.f1 +vpn201 +vpn205 +www.country +vpn200 +vpn202 +vpn203 +vpn204 +s337b +s4238a +s4238b +s1320ipmi +fialka +selfish +www.konvict +www.limelight +www.diplomacy +royston +funit +lodi +redtoblack +dtw +s4239a +dli +fma +www.pnt +s4239b +lim1 +s1331ipmi +www.youdecide +gnome +www.mrm +thedeathsquad +www.skp +s422ipmi +dungeons +angelhaven +www.deathscythe +www.nomore +crimsonrose +s4241a +stela +www.ldgaming +www.battleroyale +jodie +www.6arab +www.redtoblack +s4241b +gatelords +sliver +s4362ipmi +www.factory +www.rawan +telethon +prodotti +www.360clan +polska +thegods +s4242a +www.recon +ics2 +muzammil +s4242b +rateme +darkbrotherhood +www.angelhaven +www.ravenous +sias +exp3 +www.futurefiction +iford +s4243a +s4243b +s803ipmi +s4244a +s210a +mail-int +s1336ipmi +iod +s4245a +s4245b +s4367ipmi +s4102ipmi +s4246a +s4246b +access6 +s4247a +s4247b +inspektorat +s215ipmi +s4248a +uci +s4248b +s1230ipmi +s1342ipmi +web1920 +s4249a +topman +outfit +s4249b +wallis +ptk +s4107ipmi +s4251a +s4233a +s4252a +r15 +s4252b +nexus2 +s320ipmi +v14 +s221ipmi +s4253a +v15 +v11 +v20 +v24 +s4253b +s1347ipmi +linode1 +linode2 +linode3 +linode4 +linode5 +linode6 +ipaddress2web +www.winxp +orochi +s4254a +s4254b +s740ipmi +orkutt +s4113ipmi +s4255a +s4255b +s823y +s4256a +nomina +s4256b +s1314a +s502ipmi +s226ipmi +s801a +s4257a +ldap5 +rootdir +s4234a +s1432b +www.projetos +6dkj +s748b +s1353ipmi +s4258a +s4258b +s4221ipmi +s803a +s4118ipmi +s4250ipmi +s804a +s4261a +s4261b +s507ipmi +s232ipmi +s4262a +graphics3 +ext1 +s4262b +s806a +sslgw +s1358ipmi +s4263b +s4171ipmi +autoconfig.a +s4264a +integracja +autodiscover.a +s213a +s4320ipmi +s808a +magnolie +s4265a +s4265b +s833ipmi +s513ipmi +s809a +campari +s237ipmi +s4266b +s811a +s4267a +s1364ipmi +pinklady +www.hum +s812a +s4268a +s4268b +s4129ipmi +s813a +s4269a +s4269b +s1226 +s518ipmi +s814a +s4271a +s4271b +s812ipmi +s815a +s4272a +s4272b +s1369ipmi +s813x +s1104ipmi +s813z +s816a +s4273a +s4273b +s4135ipmi +s1010ipmi +web3119 +architect +s817a +s524ipmi +s819a +s820b +s1375ipmi +s1110ipmi +s1317b +s4141ipmi +s822a +ppmail +s822b +adminer +s520ipmi +s2b +vps.serverel.com +s529ipmi +s904a +s824a +ipmi14-2 +s1129a +s1115ipmi +sbl +s825a +rscomp +baykal +s825b +s1129b +s-108 +s-110 +s826a +potok +s826b +s904b +s811ipmi +www.citforum +s827b +s4162a +s260ipmi +s828a +s828b +s1121ipmi +s829b +s4152ipmi +s831a +s831b +s816ipmi +s832b +s638b +s265ipmi +s833a +s833b +s1319a +s1402ipmi +s1319b +s1126ipmi +s4240a +s4176ipmi +s-201 +s835a +s4302a +s4302b +s926b +s4303a +s4303b +s271ipmi +s837a +s4304a +s4304b +s1132a +s1397ipmi +s838a +s4305a +s1132ipmi +s732 +s4163ipmi +s4306a +s4306b +s4307a +s4307b +s276ipmi +s842a +s4308a +s4308b +s340ipmi +s1413ipmi +s4309a +s4309b +s4240b +s4168ipmi +s4311b +s4312a +s4312b +greenhotel +s4313a +ftp.survey +s4313b +06 +07 +www.trac +segway +08 +vbg +s1418ipmi +s220a +www.greenhotel +www.segway +s340b +ellada +s4315a +s4174ipmi +s4316a +s219d +s838ipmi +cisco3-2bad +eximstats +s287ipmi +s4317b +s4318a +s1424ipmi +test.static +dwg +s4319a +s4319b +matos +s4321a +s4321b +darkdragon +9999 +stj +s4322a +s-34a +s410a +s4323a +s4323b +s1430ipmi +s4324a +s4324b +autodiscover.accounts +s711ipmi +s4185ipmi +autoconfig.account +s1239 +webdisk.accounts +s4344ipmi +s4325a +s4325b +s4326a +s4326b +s308ipmi +s602 +autodiscover.account +s905b +s4327a +s4327b +s1435ipmi +s4244b +s4328a +www.knowledgebase +s4328b +s4182ipmi +s4201ipmi +s4329a +s4329b +s4331a +autoconfig.accounts +s4331b +s314ipmi +s4332a +s4332b +s1441ipmi +s4333a +s4333b +s4206ipmi +s119ipmi +s4334a +s4334b +sv60 +s4335a +s4335b +s319ipmi +s4336a +prawo +s4336b +s1446ipmi +s4337a +xds +sv18 +s4337b +sv22 +sv23 +sv24 +sv25 +sv26 +sv27 +sv29 +s4130a +web3130 +s4212ipmi +ipmi-a1-1 +ipmi-a1-2 +sv36 +sv37 +sv38 +s1040b +s4338b +s4339a +s4339b +s325ipmi +sv61 +sv62 +sv63 +s4341a +s4341b +s1452ipmi +mail2b +s4342a +p11 +p12 +rajneesh +s328ipmi +s4217ipmi +s4343a +s4343b +nimda +s4344a +s4344b +s606ipmi +s1138a +s1401b +s331ipmi +s4345a +rachael +cache4 +autodiscover.top +autoconfig.thumbs +webdisk.click +s4345b +s1457ipmi +webdisk.thumbs +s4346a +autodiscover.click +s4346b +www.konto +s101391ipmi +autoconfig.ny +webdisk.ny +autoconfig.top +autodiscover.ny +webdisk.top +kvm25 +autodiscover.thumbs +autoconfig.click +s4347b +ipmi-a2-1 +www.trojans +amesmtp +localhost.shop +ipmi-a2-2 +kvm24 +s4348a +s4348b +s612ipmi +bonitasprings +s336ipmi +www.worms +k13 +s4349a +s4349b +s1463ipmi +s4351a +s4351b +s4237ipmi +s716ipmi +kvm18 +s4228ipmi +astest +s4352b +s4353a +s4353b +kvm17 +s617ipmi +s342ipmi +s4354a +s226a +s4359ipmi +s226b +kvm22 +s1468ipmi +s4355b +geodaten +s1203ipmi +s4304ipmi +shaiya +interbase +s4187ipmi +dl104 +dl103 +dl102 +s4356a +s4234ipmi +s4357a +lutsk +s4357b +s102390a +s102390b +s623ipmi +ipmi-a3-1 +ipmi-a3-2 +s4358a +s4358b +s102391a +s102391b +s903a +s4359a +coldwellbanker +s1474ipmi +s1330a +s1208ipmi +s102392b +s1330b +s4361a +s4250a +s4239ipmi +s102393a +s102393b +s4250b +s905a +monitor01 +work1 +s4362a +s4362b +mob01 +s102394a +webdisk.test123 +autoconfig.test123 +autodiscover.test123 +s102394b +s628ipmi +s416a +trojans +filesend +s4363a +s4363b +s530b +s102395a +s102395b +s907a +s4364a +s4364b +s1479ipmi +stats.test +s102396a +s1214ipmi +s908a +auto2 +s4365a +s4365b +s4245ipmi +s102397a +s102397b +s1454 +s909a +s4366a +s4366b +s138ipmi +s909ipmi +s4367a +s4367b +s520a +s912a +s4368a +s4368b +ack +s1220ipmi +s913b +s4251ipmi +s914a +s914b +stjoseph +s1477ipmi +s606a +s915a +s639ipmi +s916a +s916b +s1225ipmi +s917b +dpanel +s636ipmi +s1392a +s918a +s918b +s921ipmi +s418a +s919b +s830a +s804 +s921b +s1402b +s1231ipmi +s922b +s-91a +s722ipmi +s4262ipmi +s923a +s923b +s924b +s925a +s925b +s1040 +s420a +s1236ipmi +s-95a +s420b +s4267ipmi +usability +telemarketing +s927a +s927b +s928a +gw04 +s928b +s929a +s115ipmi +bauk +s931b +fik +s1242ipmi +s4273ipmi +s932b +s632ipmi +smartcampus +s933b +s937ipmi +s1464ipmi +airi +oia +snmptn +s934b +s121ipmi +s610a +bapsi +www.bars +divas +s935a +s935b +s808 +psw +s936a +s936b +local2 +s4210b +autoservicio +s937b +eclectic +s4210ipmi +s402ipmi +s938b +s422a +s941a +s941b +s942a +s942b +vdev +vweb2 +mqa +www.aj +s642b +s407ipmi +s234a +s611e +s827ipmi +s423b +s938ipmi +s413ipmi +s137ipmi +s102390ipmi +s4305ipmi +s4257b +webadv +s1393ipmi +s425ipmi +s418ipmi +s143ipmi +s102395ipmi +s424b +s1004ipmi +s424c +s4311ipmi +s613b +s4354b +digitalpub +feedfetcher +s424ipmi +s802a +s412ipmi +s4157ipmi +s1009ipmi +s4316ipmi +gravitron +s637ipmi +media-ext +s4210a +edi1 +s705ipmi +s429ipmi +liquidweb +s1015ipmi +wwwi1 +regie +proline +wwwi2 +clipper +s426b +wwwi3 +s920ipmi +s1021ipmi +rails2 +238 +s805b +web1590 +s4327ipmi +holytrinity +ssf +s441ipmi +s1302ipmi +skillsoft +s1026ipmi +s1307ipmi +s805a +s1032ipmi +s4338ipmi +s240a +s727ipmi +slashcode +tkc +s176ipmi +s4160a +s1313ipmi +s1037ipmi +community-resources +s319 +raritan1 +raritan3 +web373 +s230ipmi +s1318ipmi +s4349ipmi +s738ipmi +lucru +clubforum +www.ucom +s807a +www.hdd +www.musicclub +arcserv1 +s1324ipmi +s823b +s4355ipmi +s242b +s404 +s405 +s242c +s407 +ns2.barrie +localnnf +wms7 +s423 +s1329ipmi +s741 +smtp-temp +clouddevnnf +devnnf +cloudnnf +s208ipmi +s828ipmi +s827 +s4310ipmi +s1335ipmi +s4322b +vigilantes +www.webnews +s4180ipmi +bdh +mi1 +s4101ipmi +simpleviewftp +s501 +saptest +homo +dildo +xtive +s502 +s503 +newdevapps +promo3 +s504 +s505 +s506 +s507 +s508 +s214ipmi +s1341ipmi +s4260ipmi +www.speakup +s4106ipmi +s47a +s47b +afk +aph +blt +range +s219ipmi +urm +ifg +s49a +s50b +s49c +s50d +s1346ipmi +s4220ipmi +s4112ipmi +s811b +suita +s53c +s501ipmi +onlinetutor +s4267b +imd +s4140a +s225ipmi +s603 +s604 +enquiry +s605 +s606 +inbtest +s607 +s608 +s609 +moblin +atl-sql-serv.dc02 +pcos +s612 +s1352ipmi +s614 +imageupload +origin-staging +s615 +s616 +s617 +s618 +xandros +s621 +slax +knoppix +s4170ipmi +s623 +s4117ipmi +s625 +s626 +cheatcode +aznakaevo +testv3 +habboville +s623a +s629 +s631 +kirishi +s632 +s633 +chns +rtd +s635 +atd +cdn7 +cdn6 +cdn0 +s636 +s637 +s638 +fb0 +s506ipmi +sechs +academi +livraria +s641 +remote01.co +remote02.co +wolfman.co +cocatnt06.co +cocatnt18.co +library.co +s642 +idd +s231ipmi +s646 +s58d +s1357ipmi +s4134b +s522ipmi +s4123ipmi +www.bpm +fti +s4130ipmi +astoc +s608ipmi +s512ipmi +s236ipmi +s63b +s63c +s701 +pueraria +s702 +s703 +ns7.x +s704 +s1363ipmi +s706 +report.dev +s435e +s708 +internal230.dev +fundir +s709 +s711 +s1350a +ns4.x +ns3.l +s4128ipmi +nugget +pdu5-1 +ftp201 +rw3 +s1350b +s722 +s4270a +6789 +internal201 +s724 +6666 +internalfc.dev +web1610 +web2290 +rebates +web431 +webupload201 +masseffect +microchip +s4270b +admin201 +s517ipmi +s242ipmi +internal.dev +ftp202 +webupload202 +internalru.dev +s737 +ns2.x +s738 +esc123 +s436a +s742 +s743 +s744 +dhcp-37-29 +s745 +host-209-149-115-165 +host-209-149-115-60 +host-209-149-115-93 +host-209-149-115-96 +host-209-149-115-209 +host-209-149-115-160 +host-209-149-115-158 +host-209-149-114-251 +host-209-149-115-90 +host-209-149-114-249 +host-209-149-115-89 +host-209-149-114-247 +host-209-149-115-152 +host-209-149-115-77 +host-209-149-115-143 +host-209-149-116-7 +host-209-149-115-228 +host-209-149-115-150 +host-209-149-114-99 +host-209-149-114-98 +host-209-149-114-97 +host-209-149-114-96 +host-209-149-114-95 +host-209-149-114-94 +host-209-149-114-93 +host-209-149-114-9 +host-209-149-114-91 +host-209-149-114-89 +host-209-149-114-88 +host-209-149-114-87 +host-209-149-114-86 +host-209-149-114-8 +host-209-149-114-84 +host-209-149-114-83 +host-209-149-114-82 +tisiphone +host-209-149-114-81 +host-209-149-114-79 +host-209-149-114-240 +bu01 +host-209-149-114-77 +host-209-149-114-38 +host-209-149-114-75 +host-209-149-114-74 +host-209-149-114-73 +host-209-149-114-6 +host-209-149-114-71 +bio313 +s1368ipmi +s747 +host-209-149-114-69 +host-209-149-114-68 +host-209-149-114-67 +host-209-149-114-209 +host-209-149-114-5 +host-209-149-114-64 +host-209-149-114-63 +host-209-149-114-62 +host-209-149-114-61 +host-209-149-114-59 +host-209-149-114-4 +charpac +host-209-149-114-57 +host-209-149-114-56 +host-209-149-114-55 +host-209-149-114-54 +host-209-149-114-53 +host-209-149-114-3 +host-209-149-114-51 +s748 +s1103ipmi +s4134ipmi +s1460ipmi +hrlaser +s523ipmi +s436f +s4140b +host-209-149-114-49 +s814b +s1374ipmi +host-209-149-114-48 +host-209-149-114-47 +host-209-149-114-46 +host-209-149-114-234 +host-209-149-114-44 +web1615 +host-209-149-114-43 +host-209-149-114-42 +host-209-149-114-41 +host-209-149-114-39 +host-209-149-114-233 +host-209-149-114-37 +host-209-149-114-36 +host-209-149-114-35 +host-209-149-114-34 +host-209-149-114-33 +s1108ipmi +s803 +suzuki2 +s744ipmi +s805 +s806 +s4139ipmi +s809 +s812 +s813 +s814 +host-209-149-114-232 +host-209-149-114-31 +host-209-149-114-29 +host-209-149-112-255 +host-209-149-112-254 +host-209-149-112-253 +host-209-149-112-252 +host-209-149-112-251 +host-209-149-112-250 +host-209-149-112-248 +host-209-149-112-247 +host-209-149-112-246 +host-209-149-112-245 +host-209-149-112-244 +host-209-149-112-243 +host-209-149-112-242 +host-209-149-112-241 +host-209-149-112-239 +host-209-149-112-238 +host-209-149-112-237 +host-209-149-112-236 +host-209-149-112-235 +host-209-149-112-234 +host-209-149-112-233 +host-209-149-112-232 +host-209-149-112-231 +host-209-149-112-229 +host-209-149-112-228 +host-209-149-112-227 +host-209-149-112-226 +host-209-149-112-225 +host-209-149-112-224 +host-209-149-112-223 +host-209-149-112-222 +host-209-149-112-221 +host-209-149-112-219 +host-209-149-112-218 +host-209-149-112-217 +host-209-149-112-216 +hiraki +host-209-149-112-215 +host-209-149-112-214 +host-209-149-112-213 +host-209-149-112-212 +host-209-149-112-211 +host-209-149-112-210 +host-209-149-112-198 +host-209-149-112-197 +host-209-149-112-196 +host-209-149-112-205 +host-209-149-112-194 +host-209-149-112-193 +host-209-149-112-192 +host-209-149-112-191 +host-209-149-112-189 +host-209-149-112-188 +host-209-149-112-187 +host-209-149-112-186 +host-209-149-112-185 +host-209-149-112-184 +host-209-149-112-183 +host-209-149-112-182 +s815 +host-209-149-112-181 +host-209-149-112-180 +host-209-149-112-178 +host-209-149-112-177 +host-209-149-112-176 +host-209-149-112-175 +s819 +host-209-149-112-174 +host-209-149-112-173 +dscp1 +host-209-149-112-172 +cva +host-209-149-112-171 +pdb1 +host-209-149-112-170 +www.parents +host-209-149-112-168 +yourgames +acrossthepond +host-209-149-112-167 +host-209-149-112-166 +host-209-149-112-165 +host-209-149-112-164 +s804ipmi +host-209-149-112-163 +host-209-149-112-162 +host-209-149-112-161 +host-209-149-112-160 +host-209-149-112-158 +host-209-149-112-157 +host-209-149-112-156 +host-209-149-112-155 +host-209-149-112-154 +host-209-149-112-153 +host-209-149-112-152 +host-209-149-112-151 +host-209-149-112-150 +host-209-149-112-148 +host-209-149-112-147 +host-209-149-112-146 +host-209-149-112-145 +sonorous +host-209-149-112-144 +host-209-149-112-143 +host-209-149-112-142 +zorin +host-209-149-112-141 +ksys +host-209-149-112-140 +host-209-149-112-138 +kimono +pontvisio +pstage +afaf +host-209-149-112-137 +host-209-149-112-136 +host-209-149-112-135 +host-209-149-112-134 +ikkome +akamal +host-209-149-112-133 +host-209-149-112-132 +host-209-149-112-131 +host-209-149-112-130 +host-209-149-112-128 +host-209-149-112-127 +host-209-149-112-126 +host-209-149-112-125 +host-209-149-112-124 +vere +host-209-149-112-123 +host-209-149-112-122 +host-209-149-112-121 +host-209-149-112-120 +host-209-149-112-118 +mprobst +ucats +host-209-149-112-117 +host-209-149-112-116 +host-209-149-112-115 +mega10 +host-209-149-112-114 +host-209-149-112-113 +fujiwara +jest +host-209-149-112-112 +apex2 +host-209-149-112-111 +host-209-149-112-110 +host-209-149-112-108 +host-209-149-112-107 +host-209-149-112-106 +host-209-149-112-105 +host-209-149-112-104 +host-209-149-112-103 +ems01 +host-209-149-112-102 +host-209-149-112-101 +host-209-149-112-100 +mancini +biomet +host-209-149-114-206 +hibo +host-209-149-115-112 +host-209-149-114-205 +host-209-149-114-204 +web1930 +comit +conceptmart +host-209-149-115-110 +host-209-149-112-209 +ghsaedge +host-209-149-114-202 +host-209-149-114-201 +ghswebedge +host-209-149-114-190 +host-209-149-115-207 +host-209-149-115-200 +host-209-149-115-104 +host-209-149-115-45 +vocalise +host-209-149-115-87 +host-209-149-115-86 +host-209-149-115-85 +host-209-149-115-84 +locals +domingo +host-209-149-115-83 +host-209-149-114-182 +host-209-149-116-29 +host-209-149-115-81 +host-209-149-114-180 +host-209-149-116-27 +host-209-149-115-78 +host-209-149-116-26 +host-209-149-115-136 +bc03 +pharmacist +host-209-149-114-177 +host-209-149-115-29 +host-209-149-115-76 +host-209-149-115-75 +host-209-149-116-23 +host-209-149-115-62 +host-209-149-114-58 +host-209-149-116-30 +host-209-149-115-73 +host-209-149-115-222 +host-209-149-115-72 +host-209-149-115-202 +host-209-149-116-20 +host-209-149-115-71 +brainbus +host-209-149-114-170 +host-209-149-115-59 +host-209-149-115-68 +host-209-149-115-130 +host-209-149-116-16 +leciel +host-209-149-115-215 +host-209-149-115-208 +epforum +host-209-149-115-66 +host-209-149-116-14 +caruso +sniegs +host-209-149-115-65 +host-209-149-114-164 +gamay +estaticos +host-209-149-115-50 +diario +web513 +web516 +host-209-149-115-63 +host-209-149-115-198 +fw0 +www.games2 +host-209-149-115-206 +host-209-149-116-10 +host-209-149-115-61 +host-209-149-114-230 +itorapp +host-209-149-114-160 +host-209-149-115-58 +host-209-149-115-57 +host-209-149-115-213 +host-209-149-113-250 +host-209-149-114-155 +host-209-149-114-154 +host-209-149-115-53 +host-209-149-115-37 +host-209-149-115-74 +host-209-149-115-52 +host-209-149-115-212 +cfusion +host-209-149-114-150 +host-209-149-115-48 +host-209-149-115-47 +host-209-149-115-46 +host-209-149-113-240 +host-209-149-114-7 +host-209-149-115-221 +host-209-149-115-44 +host-209-149-115-43 +host-209-149-114-142 +host-209-149-114-141 +host-209-149-114-92 +host-209-149-114-139 +host-209-149-114-140 +host-209-149-114-138 +host-209-149-114-225 +host-209-149-114-90 +gmmcgrnappisc01.gmmc +host-209-149-114-137 +host-209-149-114-136 +host-209-149-115-92 +host-209-149-113-230 +host-209-149-114-20 +host-209-149-114-135 +host-209-149-114-134 +host-209-149-114-85 +host-209-149-114-133 +host-209-149-114-132 +onlinem +host-209-149-113-225 +host-209-149-115-31 +uscsomgapp +ntpc +host-209-149-114-130 +host-209-149-114-50 +host-209-149-115-28 +host-209-149-115-197 +host-209-149-114-80 +host-209-149-115-27 +host-209-149-114-78 +host-209-149-115-26 +host-209-149-113-220 +host-209-149-115-79 +host-209-149-115-25 +host-209-149-114-76 +host-209-149-115-24 +host-209-149-115-23 +host-209-149-115-196 +securelink +host-209-149-115-22 +host-209-149-115-21 +host-209-149-114-72 +host-209-149-114-120 +host-209-149-115-18 +host-209-149-115-120 +host-209-149-114-70 +host-209-149-114-117 +spectron +host-209-149-115-205 +host-209-149-115-220 +host-209-149-115-16 +host-209-149-113-209 +radius-1 +host-209-149-115-15 +host-209-149-114-66 +host-209-149-113-208 +host-209-149-115-14 +host-209-149-114-65 +chem3 +host-209-149-113-207 +host-209-149-115-13 +host-209-149-113-98 +host-209-149-113-206 +host-209-149-116-28 +host-209-149-115-12 +host-209-149-115-204 +host-209-149-116-25 +host-209-149-116-24 +host-209-149-113-205 +host-209-149-116-22 +host-209-149-115-11 +host-209-149-114-220 +host-209-149-116-18 +host-209-149-116-17 +host-209-149-113-204 +s1420ipmi +s1349ipmi +s825 +host-209-149-116-15 +s826 +s253ipmi +s828 +s829 +s831 +s832 +s833 +s834 +s836 +s837 +host-209-149-114-110 +host-209-149-116-13 +s838 +s840 +s1114ipmi +s4225ipmi +s4145ipmi +s815b +host-209-149-116-12 +s809ipmi +s534ipmi +s1370ipmi +s258ipmi +s438a +s1119ipmi +s813y +s304a +s627a +render2 +s4151ipmi +s902 +host-209-149-116-11 +host-209-149-113-203 +host-209-149-114-60 +s903 +s904 +s905 +host-209-149-113-202 +host-209-149-114-45 +s906 +s907 +host-209-149-113-201 +host-209-149-115-203 +s4251b +s909 +s912 +host-209-149-113-190 +host-209-149-115-42 +host-209-149-115-67 +host-209-149-115-192 +host-209-149-114-52 +host-209-149-113-183 +host-209-149-115-88 +host-209-149-113-99 +s815ipmi +host-209-149-113-97 +host-209-149-113-96 +rutherford +host-209-149-113-95 +s914 +host-209-149-113-94 +s915 +host-209-149-113-93 +host-209-149-113-92 +host-209-149-113-91 +host-209-149-113-89 +webgroup +host-209-149-113-88 +s916 +host-209-149-113-87 +s917 +host-209-149-113-86 +s918 +host-209-149-113-85 +s264ipmi +appraisal +kaur +host-209-149-113-180 +s1330ipmi +host-209-149-113-83 +fesztivity +host-209-149-113-82 +openbravo +manage1 +host-209-149-113-81 +mlp.fantasy +host-209-149-113-79 +host-209-149-113-78 +portalantiguo +consultpermcarga +viajeroseguro +mapale +siginvias +host-209-149-113-77 +host-209-149-113-76 +host-209-149-113-75 +host-209-149-113-74 +s922 +host-209-149-113-73 +host-209-149-113-72 +host-209-149-113-71 +s923 +host-209-149-113-69 +test6289 +s924 +s926 +host-209-149-113-68 +host-209-149-113-67 +s928 +s929 +host-209-149-113-66 +imagestorage +s1401ipmi +s932 +host-209-149-113-65 +s933 +s1125ipmi +host-209-149-113-64 +host-209-149-113-63 +s935 +s936 +host-209-149-113-62 +s937 +partnertest +s938 +grade +s4355a +ref1 +s440a +host-209-149-113-61 +s4156ipmi +s501a +s614ipmi +s821ipmi +s270ipmi +host-209-149-113-59 +host-209-149-113-58 +host-209-149-113-57 +www-24 +www-23 +host-209-149-113-56 +www-21 +host-209-149-113-55 +s4361ipmi +host-209-149-113-54 +host-209-149-113-53 +host-209-149-113-52 +host-209-149-113-51 +host-209-149-113-49 +host-209-149-113-48 +host-209-149-113-47 +host-209-149-113-46 +host-209-149-113-45 +host-209-149-113-44 +host-209-149-113-43 +host-209-149-113-42 +host-209-149-113-41 +host-209-149-113-39 +host-209-149-113-38 +host-209-149-113-37 +s1396ipmi +s1131ipmi +host-209-149-113-36 +host-209-149-113-35 +host-209-149-113-34 +www-10 +host-209-149-113-33 +host-209-149-113-32 +s4162ipmi +www-12 +www-20 +host-209-149-113-31 +host-209-149-113-29 +host-209-149-113-28 +host-209-149-113-27 +web1879 +host-209-149-113-26 +host-209-149-113-25 +ew53680r99pzgc +s830ipmi +host-209-149-113-170 +www-11 +host-209-149-113-23 +docdir +dlink1 +host-209-149-113-21 +eas2 +dlink2 +dlink3 +host-209-149-113-19 +host-209-149-113-18 +host-209-149-113-17 +host-209-149-113-16 +host-209-149-113-15 +host-209-149-113-14 +host-209-149-113-13 +host-209-149-113-12 +host-209-149-113-11 +host-209-149-113-10 +dlink4 +host-209-149-114-32 +host-209-149-114-40 +s826ipmi +s1239ipmi +s1412ipmi +s1136ipmi +s629a +s1406b +s4167ipmi +s1465ipmi +s818a +s834b +s1417ipmi +s4173ipmi +s4120b +s837ipmi +s631a +s1423ipmi +s820a +s4231ipmi +s710ipmi +s4178ipmi +s313a +s302ipmi +s612f +s1428ipmi +s1310 +s1109ipmi +s4184ipmi +hyman +host-209-149-114-30 +s821a +hb2 +s912b +host-209-149-114-28 +host-209-149-113-80 +host-209-149-114-27 +s307ipmi +s255b +host-209-149-113-160 +s4138ipmi +host-209-149-114-26 +s1434ipmi +s620ipmi +s904ipmi +prodigy +s4217a +s313ipmi +s1358a +s1439ipmi +host-209-149-114-25 +host-209-149-115-254 +host-209-149-115-253 +s1358b +s4205ipmi +biomedic +s1019ipmi +host-209-149-115-252 +s318ipmi +host-209-149-115-251 +s919a +host-209-149-115-249 +host-209-149-114-24 +s634a +s1445ipmi +s805ipmi +s634b +host-209-149-115-247 +cisco10-1 +host-209-149-115-246 +cisco10-2 +host-209-149-115-245 +host-209-149-115-244 +host-209-149-115-243 +s4211ipmi +s1360a +host-209-149-112-249 +host-209-149-115-241 +host-209-149-115-239 +host-209-149-115-238 +host-209-149-115-237 +host-209-149-115-236 +test6298 +host-209-149-114-22 +s823a +test6299 +web619 +host-209-149-115-234 +web623 +web626 +web630 +host-209-149-115-233 +web3190 +web636 +host-209-149-115-232 +web639 +host-209-149-115-231 +web643 +host-209-149-115-229 +noether +host-209-149-114-21 +host-209-149-115-227 +host-209-149-115-226 +host-209-149-115-225 +host-209-149-115-224 +host-209-149-115-223 +host-209-149-114-19 +host-209-149-113-90 +host-209-149-115-219 +host-209-149-115-218 +host-209-149-115-217 +host-209-149-115-216 +host-209-149-114-18 +host-209-149-115-214 +host-209-149-113-70 +aslonline +host-209-149-115-132 +host-209-149-115-211 +host-209-149-115-199 +host-209-149-114-17 +gmmcgrnappisc01 +host-209-149-113-150 +host-209-149-115-195 +enctech +host-209-149-115-194 +host-209-149-115-193 +web1009 +host-209-149-114-16 +host-209-149-115-191 +host-209-149-115-189 +host-209-149-115-188 +host-209-149-115-187 +host-209-149-115-186 +host-209-149-114-15 +host-209-149-115-184 +host-209-149-115-183 +host-209-149-115-182 +kamery +host-209-149-115-181 +host-209-149-115-179 +host-209-149-114-14 +host-209-149-115-177 +web1013 +host-209-149-115-176 +host-209-149-115-175 +host-209-149-115-174 +host-209-149-115-173 +lending +host-209-149-114-13 +host-209-149-115-171 +mail.dev2 +host-209-149-115-169 +s4224b +s1359b +host-209-149-114-203 +s530ipmi +s324ipmi +s739a +host-209-149-115-167 +host-209-149-115-166 +s1451ipmi +s4216ipmi +s4102a +s605ipmi +host-209-149-114-12 +host-209-149-115-164 +host-209-149-115-163 +host-209-149-115-162 +host-209-149-115-161 +host-209-149-115-159 +host-209-149-114-11 +videocms +host-209-149-115-157 +host-209-149-115-156 +s329ipmi +host-209-149-115-155 +host-209-149-115-154 +host-209-149-115-153 +host-209-149-114-10 +host-209-149-115-151 +host-209-149-115-149 +host-209-149-115-148 +host-209-149-115-147 +host-209-149-115-146 +host-209-149-115-145 +host-209-149-115-144 +host-209-149-113-60 +host-209-149-115-142 +host-209-149-115-141 +s1456ipmi +host-209-149-115-10 +host-209-149-115-138 +host-209-149-115-137 +host-209-149-113-140 +s509 +host-209-149-115-135 +host-209-149-115-134 +host-209-149-115-133 +host-209-149-114-208 +host-209-149-115-131 +s101390ipmi +s715ipmi +host-209-149-115-129 +host-209-149-115-128 +hidaka +host-209-149-115-127 +vmhost04 +login.beta +host-209-149-115-126 +host-209-149-115-125 +ironport04 +ironport03 +host-209-149-115-124 +host-209-149-115-123 +host-209-149-115-122 +host-209-149-115-121 +host-209-149-115-119 +host-209-149-115-118 +host-209-149-115-117 +login.test +s4222ipmi +vmax +host-209-149-115-116 +web1029 +host-209-149-115-115 +web1949 +host-209-149-115-114 +host-209-149-115-113 +host-209-149-112-230 +host-209-149-115-111 +host-209-149-115-109 +hs01 +host-209-149-115-108 +cisco11-1 +uter +host-209-149-115-107 +host-209-149-115-106 +host-209-149-115-105 +securitybackup +host-209-149-115-103 +host-209-149-115-102 +coppola +host-209-149-115-101 +host-209-149-115-100 +cisco11-2 +host-209-149-114-207 +host-209-149-115-39 +host-209-149-113-50 +s611ipmi +host-209-149-114-210 +s335ipmi +s1462ipmi +torn-ams2 +hacienda +host-209-149-113-129 +cp05qa005 +s4260b +host-209-149-112-3 +s4186ipmi +host-209-149-115-56 +host-209-149-112-1 +staging.intranet +host-209-149-113-126 +s1362a +s1362b +host-209-149-112-220 +host-209-149-115-69 +host-209-149-113-84 +host-209-149-115-55 +s616ipmi +host-209-149-113-40 +host-209-149-113-120 +host-209-149-112-90 +host-209-149-115-54 +s341ipmi +host-209-149-112-199 +s714ipmi +web3199 +s1467ipmi +web1039 +www.dvds +s913a +s1202ipmi +s4330ipmi +s823x +accounts1 +s4233ipmi +s4146ipmi +s1320 +s-109 +s637b +cisco12-1 +s101390b +cisco12-3 +s101391a +s101391b +s1473ipmi +s1207ipmi +s101392b +host-209-149-112-208 +web700 +host-209-149-112-207 +s4238ipmi +s101393a +s101393b +web691 +web1046 +s1219b +s903ipmi +web692 +s627ipmi +s310ipmi +host-209-149-112-206 +s1478ipmi +s1213ipmi +web703 +sunet +s827a +fotos1 +web694 +s4244ipmi +s535ipmi +s633ipmi +cisco13-1 +host-209-149-112-195 +host-209-149-113-30 +web1050 +web695 +cisco13-2 +cisco13-3 +cisco13-4 +s810ipmi +s1484ipmi +s1218ipmi +s4249ipmi +s639b +s914ipmi +s4106b +3333 +s638ipmi +s926ipmi +cp05qa008 +web697 +s1224ipmi +cobacoba +s220ipmi +web698 +web709 +cp05qa010 +web713 +web1059 +mariachi +s4255ipmi +web720 +ns1.barrie +web721 +s103ipmi +web726 +web1073 +web729 +mathlab3 +web1086 +s641b +web1088 +web1089 +web1101 +sva +web1102 +web1103 +s50c +s1229ipmi +s4359b +web1104 +web124 +web1105 +web1096 +web1107 +bfa +jamsession +s829a +web1108 +s4261ipmi +s830b +web164 +s264b +web174 +s4140ipmi +s925ipmi +s1235ipmi +s4266ipmi +s129ipmi +web200 +s4108a +s910b +s4256ipmi +host-209-149-112-204 +web221 +web222 +web1119 +web228 +web1123 +web1939 +host-209-149-113-110 +host-209-149-112-79 +s931ipmi +s114ipmi +host-209-149-112-203 +web1129 +test6339 +s1241ipmi +s4272ipmi +s1001b +s936ipmi +web325 +web327 +host-209-149-116-9 +web331 +web332 +host-209-149-116-8 +web335 +web336 +web337 +web341 +web342 +web343 +web344 +web345 +web347 +web348 +munin2 +web350 +web351 +web352 +web1139 +web354 +web355 +web357 +web358 +web361 +web362 +web364 +web365 +web367 +web368 +web370 +web371 +web372 +web374 +web375 +web376 +web377 +web381 +web382 +web383 +web384 +web385 +web387 +web388 +web400 +web401 +web402 +web404 +web405 +web407 +s803b +web411 +web412 +web413 +web414 +web415 +web417 +web418 +web420 +web421 +web422 +web1149 +swta +web424 +web425 +web426 +web427 +web428 +web432 +web433 +web434 +web435 +s1330 +web437 +web438 +web452 +web453 +web454 +web455 +web457 +web458 +web460 +web461 +web462 +web463 +web464 +web465 +web466 +web467 +web468 +host-209-149-115-82 +web471 +web472 +web474 +s120ipmi +web477 +web478 +web480 +web481 +web482 +web484 +web485 +web486 +web487 +web501 +web502 +web503 +web504 +web505 +web507 +web508 +web510 +web511 +web512 +grace2 +web514 +web515 +web517 +web518 +web521 +web522 +web523 +s4110b +s832a +web527 +web528 +web530 +web531 +web532 +web535 +web536 +omfg +web537 +web538 +web541 +web542 +web543 +s4350ipmi +web544 +web545 +web547 +s266b +web548 +host-209-149-112-202 +web607 +web608 +web611 +web612 +host-209-149-116-5 +web613 +web614 +web615 +web616 +web617 +web618 +web620 +web621 +architekt +new-test +web622 +web624 +host-209-149-116-4 +web625 +web627 +web628 +web631 +web632 +web633 +web634 +s942ipmi +web635 +web637 +web638 +web640 +web641 +web642 +web644 +web645 +web646 +web647 +web648 +web651 +web652 +web653 +web654 +web655 +web657 +web658 +web661 +web662 +host-209-149-116-2 +web664 +web665 +web666 +web667 +web671 +web672 +web673 +web674 +web677 +web678 +web680 +web681 +web682 +pweb +web684 +web685 +cloudy +web686 +web687 +web688 +web701 +web702 +web704 +web705 +web707 +web708 +web710 +web711 +web712 +web714 +host-209-149-115-201 +web715 +web716 +web718 +s1002a +web722 +web723 +web724 +web725 +web727 +web728 +web730 +web326 +s401ipmi +web1219 +web1700 +s204d +web330 +web1226 +s4351ipmi +cyjy +zzxx +s736ipmi +host-209-149-112-201 +host-209-149-112-200 +host-209-149-113-24 +host-209-149-115-38 +sdesk +sko +ldi +myhp +host-209-149-113-22 +ccaa +vidistar +s1370a +ghsavedge +s1369b +s406ipmi +galactic +host-209-149-113-20 +s131ipmi +s4247ipmi +s1003b +s1392ipmi +s834a +host-209-149-112-70 +naumen +s4301a +s1409a +s4301b +s726a +s-201a +s602ipmi +www.japanese +s417ipmi +www.french +s613 +host-209-149-115-70 +giresun +pinot +pelvoux +host-209-149-115-49 +vercors +faucon +host-209-149-112-179 +host-209-149-114-200 +host-209-149-115-9 +r03 +s142ipmi +s186ipmi +host-209-149-115-8 +s102394ipmi +paquerette +s1409b +s1003ipmi +fred1 +panthere +s646b +ensigate +s4309ipmi +s238b +host-209-149-115-7 +malte +s832ipmi +s1372a +vanoise +s4361b +sambuy +godot +s1372b +s423ipmi +hakka +cocktail +hanuman +s4105a +s622 +host-209-149-115-6 +s1008ipmi +host-209-149-115-5 +host-209-149-115-4 +host-209-149-115-3 +host-209-149-115-2 +moring +topnotch +host-209-149-115-1 +s4315ipmi +s624 +class8 +groseille +host-209-149-115-0 +cola +s704ipmi +beaumont +r-viallet1 +v125 +nefer +s428ipmi +s430ipmi +s836a +s628 +s822ipmi +a19 +a17 +s1014ipmi +a15 +jarl +s630 +s4321ipmi +host-209-149-112-59 +host-209-149-115-98 +a07 +sphynx1 +a06 +balboa +gazon +a05 +mics +a04 +a03 +s709ipmi +host-209-149-112-169 +traverse +darhan +fangio +host-209-149-115-140 +brenner +host-209-149-112-55 +neb +host-209-149-115-91 +mjollnir +host-209-149-116-6 +rude +host-209-149-114-255 +mtp1 +amorgos +host-209-149-114-254 +host-209-149-114-253 +andaman +host-209-149-114-252 +albator +host-209-149-114-23 +s434ipmi +kayak +gmmcnfuseisc01.gmmc +patator +host-209-149-114-248 +host-209-149-112-49 +host-209-149-114-246 +host-209-149-114-245 +host-209-149-114-244 +www.stable +afex +getitnow +www.getitnow +host-209-149-114-243 +www.afex +host-209-149-114-242 +host-209-149-114-241 +host-209-149-114-239 +host-209-149-114-238 +s1340 +host-209-149-114-237 +macs +s1020ipmi +s4326ipmi +xtender +s915b +host-209-149-114-236 +host-209-149-114-235 +host-209-149-114-2 +s837b +host-209-149-114-1 +host-209-149-114-0 +host-209-149-114-231 +host-209-149-114-229 +www.cx +host-209-149-114-228 +host-209-149-114-227 +host-209-149-114-226 +www.kp +host-209-149-112-159 +host-209-149-114-224 +host-209-149-114-223 +www.tn +host-209-149-114-222 +www.li +s639 +www.oscommerce +host-209-149-114-221 +mail.trash +host-209-149-114-219 +asano +host-209-149-114-218 +host-209-149-114-217 +midorikodomo +host-209-149-114-216 +host-209-149-114-215 +host-209-149-114-214 +host-209-149-114-213 +s439ipmi +host-209-149-114-212 +host-209-149-114-211 +host-209-149-114-199 +s1301ipmi +shinoda +s1025ipmi +funayama +host-209-149-114-198 +host-209-149-114-197 +okuda +host-209-149-114-196 +host-209-149-114-195 +baldur +host-209-149-114-194 +s732ipmi +host-209-149-114-193 +host-209-149-114-192 +host-209-149-114-191 +host-209-149-114-189 +www.podpora +shirasaki +doujin +host-209-149-114-188 +host-209-149-114-187 +s4332ipmi +kufa +ganka +host-209-149-114-186 +sagawa +host-209-149-114-185 +host-209-149-114-184 +host-209-149-114-183 +gmmccsgisc01.gmmc +host-209-149-114-181 +host-209-149-114-179 +host-209-149-114-178 +miyoshi +sasakinaika +sousei +s721ipmi +s1407ipmi +host-209-149-112-39 +motomura +maruyama +libstats +hananoki +host-209-149-114-176 +host-209-149-114-175 +takizawa +s101390 +kawada +s101391 +s101392 +shibasaki +host-209-149-114-174 +s101393 +tsubakigaoka +shigeta +host-209-149-114-173 +host-209-149-114-172 +mysql3.sgmanaged.com +turukawa +s1306ipmi +host-209-149-114-171 +s1001a +nishikawa +juku +host-209-149-114-169 +host-209-149-114-168 +suzumura +1206 +host-209-149-114-167 +mizonokuchi +s1031ipmi +s4214a +host-209-149-114-166 +s1002b +host-209-149-114-165 +host-209-149-114-163 +s1343 +host-209-149-114-162 +host-209-149-114-161 +s1003a +s1301a +host-209-149-114-159 +s175ipmi +s1004a +jpo1 +host-209-149-114-158 +host-209-149-114-157 +host-209-149-114-156 +host-209-149-112-149 +s1004b +host-209-149-113-210 +host-209-149-114-153 +host-209-149-114-152 +s917ipmi +logon2 +host-209-149-114-151 +host-209-149-114-149 +host-209-149-114-148 +s1312ipmi +www.dominio +host-209-149-114-147 +marcon +s1005a +host-209-149-114-146 +www.tolyatti +s1005b +nizhniy-tagil +host-209-149-114-145 +s1036ipmi +www.photoshop +s310a +s4343ipmi +host-209-149-114-144 +host-209-149-114-143 +host-209-149-113-9 +s840a +host-209-149-113-8 +host-209-149-113-7 +1346 +s1006a +s505b +s1007a +s1003 +s1005 +host-209-149-113-6 +host-209-149-113-5 +s1006 +host-209-149-113-4 +host-209-149-113-3 +s1007 +inns +s1008a +sipeg +host-209-149-113-2 +www.rostov-na-donu +host-209-149-113-1 +hd2 +host-209-149-113-0 +cyw +s1009b +host-209-149-114-131 +host-209-149-114-129 +host-209-149-114-128 +ncs2 +host-209-149-114-127 +host-209-149-114-126 +s1021 +screensavers +ob2 +host-209-149-114-125 +host-209-149-114-124 +host-209-149-114-123 +monkeybutt +media9 +cvm1 +host-209-149-114-122 +host-209-149-114-121 +host-209-149-114-119 +host-209-149-114-118 +host-209-149-112-30 +host-209-149-114-116 +host-209-149-114-115 +host-209-149-114-114 +hotornot +host-209-149-114-113 +rss8 +autoconfig.oldsite +host-209-149-114-112 +host-209-149-114-111 +host-209-149-114-109 +host-209-149-114-108 +host-209-149-114-107 +host-209-149-114-106 +host-209-149-114-105 +dashboard2 +autodiscover.oldsite +host-209-149-114-104 +host-209-149-114-103 +host-209-149-114-102 +host-209-149-114-101 +host-209-149-114-100 +rappelz +host-209-149-112-139 +gatewayproxy +host-209-149-115-41 +lyncextweb +cmaster +ghslink +host-209-149-112-20 +newspapers +host-209-149-112-9 +host-209-149-112-8 +circles +toe +image03 +image02 +host-209-149-112-7 +host-209-149-112-6 +vps035 +host-209-149-112-5 +vps037 +host-209-149-112-4 +vps041 +host-209-149-112-99 +host-209-149-112-98 +host-209-149-112-97 +host-209-149-112-96 +host-209-149-112-95 +host-209-149-112-94 +ranjeet +host-209-149-112-93 +ip25 +ip24 +ip22 +ip21 +myespace +ip16 +feed3 +host-209-149-112-92 +fif +host-209-149-112-91 +routerbackup +host-209-149-112-89 +host-209-149-112-88 +www.tutos +vps121 +vps123 +gseweryn +host-209-149-112-87 +host-209-149-112-86 +caphus +host-209-149-112-85 +host-209-149-112-84 +countyline +host-209-149-112-83 +host-209-149-112-82 +filter01 +host-209-149-112-81 +host-209-149-112-80 +sb01 +wap01 +host-209-149-112-78 +host-209-149-112-77 +host-209-149-112-76 +host-209-149-112-75 +vps040 +host-209-149-112-74 +host-209-149-112-73 +host-209-149-112-72 +host-209-149-112-71 +host-209-149-112-69 +host-209-149-112-68 +routerwifi +host-209-149-112-67 +fns1 +vps122 +lecerta +host-209-149-112-66 +host-209-149-112-65 +host-209-149-112-64 +from-atm +freitag +cauta +host-209-149-112-63 +host-209-149-112-62 +pictor +spex +host-209-149-112-61 +northeast +wmb +from-gts +hp3050 +previews +host-209-149-112-60 +host-209-149-112-58 +slserver +host-209-149-112-57 +host-209-149-112-56 +host-209-149-112-54 +wc4 +host-209-149-112-53 +www.siberia +corrus +fns2 +www.arkhangelsk +www.entekhabat +prestasklep +hp1522 +koenig +burg +host-209-149-112-52 +tjumen +entekhabat +host-209-149-112-51 +centralka +www.odesa +kolpino +tweglinska +www.stuttgart +rejestracja.scs +sagita +www.finlandia +emon +www.estonia +atm-ksk +host-209-149-112-50 +berdyansk +dulo +dgma +economicas +www.phystech +sklep_test +sinxron +teresardp +host-209-149-112-48 +www.latvia +nlpro +kishinev +www.nightclub +wirelesscontroller +host-209-149-112-47 +kharkiv +host-209-149-112-46 +ternopol +rdp3 +ak-to-ksk +www.hse +www.dnepr +leopoldina +www.reflex +host-209-149-112-45 +vyatka +host-209-149-112-44 +ksk-to-ak +finlandia +marica +teresardp2 +host-209-149-112-43 +www.msu +booth +www.viborg +www.kirovograd +campos +albus +viborg +www.derbent +quested +www.volga +omega7 +autoconfig.stamps +www.ehr +saintpetersburg +host-209-149-112-42 +aviv +webdisk.stamps +mendes +vinnica +cev +host-209-149-112-41 +www.uzbekistan +huodong +cej +www.tuts +host-209-149-112-40 +phystech +autodiscover.stamps +autoconfig.host2 +autodiscover.host2 +www.prague +mgu +www.ukraine +www.new-york +dro +www.umnik +host-209-149-112-38 +host-209-149-112-37 +host-209-149-112-36 +hermod +host-209-149-112-35 +www.vyborg +www.petersburg +host-209-149-112-34 +umnik +coi1 +rovno +host-209-149-112-33 +123a +host-209-149-112-32 +host-209-149-112-31 +host-209-149-112-29 +www.luk +host-209-149-112-28 +enformatik +h3c +www.albus +host-209-149-112-27 +host-209-149-112-26 +host30 +host-209-149-112-25 +host-209-149-112-24 +valentines +social1 +cryptonector +data7 +host-209-149-112-23 +host-209-149-112-22 +unql +darcy +host-209-149-112-21 +serv15 +michelson +host-209-149-112-19 +host-209-149-112-18 +host-209-149-112-17 +host-209-149-112-16 +sb02 +host-209-149-112-15 +host-209-149-112-14 +host-209-149-112-13 +host-209-149-112-12 +host-209-149-112-11 +axia +host-209-149-112-10 +host-209-149-115-51 +host-209-149-115-36 +solusvm1 +ssl-test +64studio +host-209-149-115-250 +host-209-149-115-248 +host-209-149-115-80 +cl04 +cl03 +customers2 +host-209-149-112-109 +host-209-149-115-64 +host-209-149-115-35 +ghscontent +host-209-149-115-242 +officetracker +host-209-149-115-240 +host-209-149-115-30 +host-209-149-115-34 +therejects +www.theartofwar +dfserver +host-209-149-116-1 +symfony +host-209-149-113-200 +host-209-149-112-129 +dspc +playready +host-209-149-115-235 +oldserver +ghsedge +host-209-149-115-33 +resolver3 +unicampus +host-209-149-115-19 +host-209-149-115-230 +homeaccess +patrons +host-209-149-115-32 +host-209-149-112-2 +www.ravens +lostsouls +corail +diable +www.almuslim +host-209-149-115-210 +host-209-149-115-95 +pcencryption +bego +cp10 +www.angelsofdeath +arago +www.sadia +host-209-149-113-255 +www.sadik +nbd +host-209-149-113-254 +host-209-149-113-253 +www.salah +oddfellow +host-209-149-113-252 +amgm +host-209-149-113-251 +host-209-149-113-249 +host-209-149-113-248 +host-209-149-113-247 +host-209-149-113-246 +www.iwonko +host-209-149-113-245 +host-209-149-113-244 +host-209-149-113-243 +host-209-149-113-242 +host-209-149-113-241 +host-209-149-113-239 +jazan +www.sandanski +theelites +safeguard +www.ayman +ait3 +dolph +testserver5 +www.realdeal +housekeeper +ravens +www.forgottencoast +businesstechnology +www.theforum +host-209-149-113-238 +offerte +host-209-149-113-237 +host-209-149-113-236 +presentatie +host-209-149-113-235 +www.therejects +host-209-149-113-234 +www.tur +vcsmtp +www.naughty +host-209-149-113-233 +postyourstuff +www.attorneybrisbane +iwonko +hashembaddad +www.salvation +host-209-149-113-232 +xbox36o +www.maarouf +www.jedimasters +host-209-149-113-231 +konferens +nsider +attorneybrisbane +www.lostandfound +www.toontown +www.faisal1 +host-209-149-113-229 +host-209-149-113-228 +www.lucky13 +ivpn +www.elmagic +nexusforums +ebrahim +www.christianity +abc4 +www.izaphod +braca +wowguild +poinx +cas02 +host-209-149-113-227 +host-209-149-113-226 +utility5 +gmmcctxwebisc05.gmmc +host-209-149-113-224 +host-209-149-113-223 +jxxy +bg2 +host-209-149-113-222 +fetch +host-209-149-113-221 +www.inc +mypanel +host-209-149-113-219 +host-209-149-113-218 +occa +host-209-149-113-217 +nortech +host-209-149-113-216 +enviro +kna +lietuva +tools2 +tinar +obits +www.ligamistrzow +ligamistrzow +t001 +vn1 +bossa +neotelecom +completel +host-209-149-113-215 +drywall +webdisk.casino +radis +r10 +host-209-149-113-214 +geisha +host-209-149-113-213 +radius02 +dhcp05 +host-209-149-113-212 +autoconfig.resources +autodiscover.resources +host-209-149-113-211 +webdisk.resources +tfl +bluefish +host-209-149-113-199 +host-209-149-113-198 +ultima2 +hanley +sasha0 +wheelers +host-209-149-113-197 +venik +localhost.nano +izida +host-209-149-113-196 +host-209-149-113-195 +polimer +autoconfig.thailand +autodiscover.thailand +host-209-149-113-194 +elbrus +www.commerce +host-209-149-113-193 +host-209-149-113-192 +host-209-149-113-191 +host-209-149-113-189 +host-209-149-113-188 +host-209-149-113-187 +host-209-149-113-186 +host-209-149-113-185 +host-209-149-113-184 +webdisk.islam +autoconfig.islam +gmmcctxcsgisc01.gmmc +host-209-149-113-182 +host-209-149-113-181 +autodiscover.islam +baze +host-209-149-113-179 +mailgva +autodiscover.board +wende +autoconfig.board +wishes +darkblue +whistleblower +host-209-149-113-178 +dui +xcache +host-209-149-113-177 +host-209-149-113-176 +host-209-149-113-175 +host-209-149-113-174 +www.acp +host-209-149-113-173 +host-209-149-113-172 +host-209-149-113-171 +host-209-149-113-169 +host-209-149-113-168 +host-209-149-113-167 +host-209-149-113-166 +host-209-149-113-165 +host-209-149-113-164 +host-209-149-113-163 +host-209-149-113-162 +host-209-149-113-161 +host-209-149-113-159 +soha +host-209-149-113-158 +host-209-149-113-157 +host-209-149-113-156 +host-209-149-113-155 +mysqldumper +valuation +clipart +www.webhost +dudley +host-209-149-113-154 +tristar +host-209-149-113-153 +r1softbackup1 +wgy +flv3 +pth +rpsp +xle +lingqcentral-de +monitorizacion +owatest +host-209-149-113-152 +lingqcentral-cs +lingqcentral-ru +lingqcentral-en +host-209-149-113-151 +lingqcentral-th +www.delivery.platform +lingqcentral-es +host-209-149-113-149 +ked +lingqcentral-sv +blogsetup +lingqcentral-tr +google-translate +host-209-149-113-148 +lingqcentral-fr +host-209-149-113-147 +mx15 +mx16 +lingqcentral-zh-tw +lingqcentral-ja +host-209-149-113-146 +host-209-149-113-145 +lingqcentral-hu +staging.community +lingqcentral-zh-cn +chogori +lingqcentral-it +host-209-149-113-144 +lingqcentral-ko +lingqcentral-lt +host-209-149-113-143 +lingqcentral-lv +lingqcentral-nl +lingqcentral-beta +lingqcentral-ar +lingqcentral-pl +host-209-149-113-142 +www.apitest +tennant +shopdev +donald6 +donald5 +host-209-149-113-141 +sql03 +lingqcentral-pt +formazione +narzedzia +host-209-149-113-139 +aberfoyle +differenttan +midy1dev +chatterbirds-qa +fullrefund +eltelby +seriouscallersonly +gaghalfrunt +offler +ofetta +cendrawasih +host-209-149-113-138 +fenchurch +hotblackdesiato +polyhymnia +bobjobdev +ithoughthewaswithyou +host-209-149-113-137 +goyourownway +kidzglobal +asnaf +doji +ethicsgradient +howandaland +karluvmost +blartversenwald +ipl +kirsten-host +host-209-149-113-136 +perrycox +shahrdari +basicspi +somuchforsubtlety +prazskyhrad +notwantedonvoyage +cargocult +arthurdent +vstore +gallagher +vps.unilux +host-209-149-113-135 +kirsten2 +uninvitedguest +jaundicedoutlook +redalien +elliott +oidong +giediprime +foreigner +callioper +mendeddrum +iuliar +deathsdomain +stemcell +djelibeybi +eccentricagallumbits +tradesurplus +wuziprack +dedicatedservers +brentcrude +sb-us1 +dolcefarniente +iqgeek +spacemonster +bigsexybeast +host-209-149-113-134 +dpcs01 +hummakavula +sb-at1 +sb-at2 +helenar +backups.unilux +dresseduptoparty +david2 +chatterbirds-dev +limitingfactor +medialibrary +nervousenergyr +bacula-s1 +babelfish +galaxyfx-vps +hildesheim +host-209-149-113-133 +domitia +ngs01 +staremesto +epunk +porcia +mountlofty +lofty +serviceforhosting +justpassingthrough +tiamatr +dns-br +www.acm +miriel +www.redes +magnumderby +worralorrasurfa +edgeofseventeen +lth-ns2 +prostetnicvogonjeltz +host-209-149-113-132 +younaughtymonsters +host-209-149-113-131 +host-209-149-113-130 +palyang +lucillar +sb-us2 +nineveh +arresteddevelopment +salusasecundus +w31fmt2 +socalpai +elliotreid +freeserver +www.naturaleza +ciat +lth-ns1 +dishoftheday +wunderwuzis +humerdev +bobkelso +onlyslightlybent +livefeed +melian +triadian +midy2dev +veetvoojagig +cant +karluvmostr +dormouse +cyrener +nervousenergy +melqart +twoflower +clearairturbulence +fateamenabletochange +theshaymen +carthage +yoodenvranx +angelface +halletcove +host-209-149-113-128 +feanor +ignacio +unfortunateconflictofevidence +nofixedabode +enviroinfo +pompeia +www2.epunk +networkclean +coventgarden +cg-gw-prg1 +orienta +securecommercesite +msx001 +snapshot1 +zarniwoopvanharl +host-209-149-113-127 +verylittlegravitasindeed +youllthankmelater +coffeenostra +fortytwo +ghsms +ovz-s1 +host-209-149-113-125 +nevertalktostrangers +mdh +congenitaloptimist +welliwasintheneighbourhood +porkbellies +finetillyoucamealong +cosifantutte +aphroditer +carthagor +justreadtheinstructions +wunziprack +llamedos +zaphodbeeblebrox +host-209-149-113-124 +bidev +lobsangludd +hanovere +irregularapocalypse +malastrana +host-209-149-113-123 +chatterbirds +bidpunk-db +nomoremrniceguy +lovina +subtleshiftinemphasis +krtek +waterstones +abrissbirne +host-209-149-113-122 +tacticalgrace +spinningtop +attitudeadjuster +www.ciat +hurlingfrootmig +emupoint +pico1 +host-209-149-113-121 +highurtenflurst +biddingbedangdev +cg-gw-fmt2 +ankhmorpork +secureserver +growthr +prostheticconscience +donttrythisathome +blandford +problemchild +v-cust-vps168 +partialphoticboundary +ldexw3h +trintagula +nowwetryitmyway +foocity +wowbagger +frankexchangeofviews +lallafa +rhiannonr +prazskejaro +cygnusx1 +strangerheremyself +armchairtraveller +theshades +ashipwithaview +ns1-praha +kashnkari +host-209-149-113-119 +ofcourseistillloveyou +bennybunny +virtualprivateserver +hanoverer +reospeedwagon +host-209-149-113-118 +utro +www.policy +cultural +divisions +helios2 +host-209-149-113-117 +fabro +oastest +host-209-149-113-116 +dhcp0 +host-209-149-113-115 +sslvpn1 +webdisk.malaysia +sqtest +host-209-149-113-114 +host-209-149-113-113 +autoconfig.malaysia +ams1 +ams3 +clamp +autodiscover.malaysia +pol1 +www.inst +host-209-149-113-112 +host-209-149-113-111 +www.tutoriales +host-209-149-113-109 +host-209-149-113-108 +hopkins +www.real-estate +citech +webdisk.scratchcard +webdisk.flash +autoconfig.journal +www.hopkins +financial-planning +hedwig +autodiscover.journal +www.periodismo +mailmems +absensi +host-209-149-113-107 +www.trials +host-209-149-113-106 +awr +host-209-149-113-105 +host-209-149-113-104 +host-209-149-113-103 +demoblog +cui +host-209-149-113-102 +host-209-149-113-101 +panel4 +rbk +host-209-149-113-100 +host-209-149-115-190 +onu +bolsatrabajo +host-209-149-115-94 +presnya +host-209-149-115-185 +cloudm +host-209-149-115-40 +host-209-149-115-180 +rodica +host-209-149-115-178 +yna +ysd +saladeprensa +host-209-149-115-99 +opinasantafe +coreo +coordsantafe +jalalpa +territorialsanangel +tolteca +planparcialsantafe +img.php +host-209-149-115-139 +host-209-149-112-119 +host-209-149-115-20 +host-209-149-115-172 +somgftp +koran +host-209-149-115-170 +host-209-149-115-168 +ip220-116 +ip220-193 +cs10 +ip220-194 +ip204-9 +ip204-7 +cs14 +cs21 +host-209-149-115-97 +cs29 +host-209-149-115-17 +myvps +cs9 +ip204-5 +ip204-4 +inscripciones +intercambios +smtp.zimbra +autoconfig.toko +d1-7 +d1-6 +ip204-99 +poaui +hipernet +webdisk.imobiliaria +imobiliaria +webdisk.projetos +autodiscover.toko +internal202 +voyance +naturaleza +ip204-98 +interspire +ip204-97 +ip204-96 +ip204-95 +ip204-94 +sendit +www.dent +ip204-93 +ip204-92 +commune +ip204-89 +ip204-88 +kerrigan +ip204-87 +ip204-86 +ip204-85 +ip204-84 +ip204-83 +ip204-82 +ip204-81 +ip204-79 +ip204-78 +ip204-77 +ip204-76 +ip204-75 +ip204-74 +ip204-73 +tw.member +tw.portal +tw.class +ip204-72 +ip204-71 +host36 +www.nutricion +lekarstva +ip204-68 +tulum +ns1.x +ip204-67 +ip204-66 +ip204-65 +stuweb +ip204-64 +card1 +kxx +ip204-63 +ip204-62 +josie +ns3.x +ip204-61 +ip204-59 +parkes +gebuilding.hol +upsmon +leach +ns5.x +ns6.x +ip204-58 +alex7 +ip204-57 +autoconfig.art +khp +ip204-56 +cuthbert +ip204-55 +decs +autodiscover.art +ip204-54 +ip204-53 +ip204-52 +printserver2 +ip204-51 +ip204-49 +linux03 +ip204-48 +venturi +ip204-47 +ip204-46 +giftcards +erecord +deianira +ip204-45 +ip204-44 +ssotest3 +ip204-41 +ip204-39 +ip204-38 +cornell +helicon +keylogger +ip204-37 +ip204-35 +www.newsroom +www.premier +vdesk +www.intercambios +ip204-34 +ip204-33 +ip204-32 +ip204-31 +alderaan +www.frontpage +ip204-27 +ip204-26 +www.slots +taw +tdd +kurzy +fotokonyv +kpe +qubit +ip204-25 +kroeber +ip204-24 +www88 +www109 +cdu +jueves +www243 +ip204-22 +bigfarm +www89 +ip204-21 +ip204-19 +ip204-18 +ip204-17 +ip204-15 +www.bigfarm +development1 +ip204-14 +streamtest +yudian +www.sqladmin +activecollab +beta15 +bladerunner +webdisk.s1 +smtpext +brel +trium +www.cgi +autodiscover.stats +sirius2 +autoconfig.stats +navneet +fangorn +local1 +sawneeexc10b +sawneeexc +sawneearcsrv +krc +sawneecas2 +sawneecas1 +sawneearcgis +sawneeexc07 +sawneesftp +sawneeexcfrt +www.cazari +cazari +sawneemail +sawneeecx10a +sawneelync +ray1122 +eao +dhcp253 +dhcp252 +dhcp251 +dhcp250 +dhcp248 +dhcp247 +dhcp246 +dhcp245 +dhcp244 +dhcp243 +dhcp242 +dhcp241 +dhcp239 +dhcp238 +dhcp237 +dhcp236 +dhcp235 +dhcp234 +dhcp233 +dhcp232 +dhcp231 +dhcp229 +dhcp228 +dhcp227 +dhcp226 +dhcp225 +dhcp224 +dhcp223 +dhcp222 +dhcp221 +dhcp219 +dhcp218 +dhcp217 +dhcp216 +dhcp215 +dhcp214 +dhcp213 +dhcp212 +dhcp199 +dhcp198 +dhcp197 +dhcp196 +dhcp195 +dhcp204 +dhcp203 +dmn +dhcp192 +dhcp200 +meteoweb +burwood +dhcp249 +dhcp240 +dhcp230 +dhcp220 +dhcp211 +dhcp210 +dhcp208 +dhcp207 +dhcp206 +dhcp205 +dhcp194 +dhcp193 +dhcp202 +dhcp201 +aristote +yeu +dhcp209 +maslo +snb +hi-fi +website-promotion +www.website-promotion +www.web-designing +web-designing +carlisle +aboshop +qwerty1234 +game4 +400 +suddenattack +fb-app +ip204-13 +chuangye +alberich +ip204-12 +corwin +eridanus +mihir +asiansensation +sexmovies +steak +netx +ccadmin +www.mpl +mailserver4 +hardcoremovie +autodiscover.oh +webdisk.oh +autoconfig.oh +smtp-bigip +jezebel +tomkat +bluetooth +88say +discuzx +www.vpc +ip204-240 +ip220-198 +ip204-6 +sp5 +timr +testapp1 +ip204-70 +ip204-43 +ip204-30 +ip204-28 +ip204-23 +ip204-10 +secureforms +hardcorehentai +ip220-166 +ip204-204 +ip204-203 +tinhdonphuong +share1 +ip204-190 +smut +ip204-91 +ip204-90 +ip204-80 +ip204-69 +ip204-60 +ip220-156 +ip204-50 +ip204-42 +ip204-40 +ip204-36 +ip204-29 +ip204-16 +ip220-188 +ip220-187 +ip220-184 +ip220-182 +ip220-181 +ip220-179 +ip220-177 +ip220-172 +ip220-169 +ip220-168 +ip220-167 +ip220-165 +ip220-164 +ip220-161 +ip220-160 +ip220-158 +ip220-155 +ip220-153 +ip220-113 +megsons +ip220-191 +wge +wct +ip220-190 +ip220-140 +ip220-171 +ip220-152 +www.wi +ip220-149 +ip220-144 +ip220-139 +ip220-136 +seocat +qisserver +ip220-133 +ip220-131 +ip220-128 +falstaff +ip220-126 +ip220-122 +elnath +support-dev +ip220-117 +support-ext +navidad +ip220-109 +ip220-107 +ip220-124 +bild +ofi +ip220-106 +nol +1208 +baldrick +ip220-100 +payslip +ip204-210 +lockss +spare-16 +spare-17 +spare-18 +ip204-150 +ip220-119 +ip204-192 +xantia +collaboratori +ip204-182 +scuba1 +ip204-163 +ip204-158 +ditracker +betaonline +mobilenow +ip220-115 +ip204-254 +ip204-253 +ip204-252 +ip204-251 +ip204-248 +ip204-247 +ip204-245 +yds2 +ip204-243 +ip204-242 +ip204-241 +ip204-239 +ip204-237 +ip204-235 +ip204-234 +ip204-232 +ip204-231 +ip204-229 +ip204-228 +ip204-225 +ip204-224 +ip204-223 +ip204-219 +ip204-218 +ip204-217 +ip204-215 +ip204-214 +ip204-213 +ip204-179 +ip204-177 +ip204-175 +ip204-174 +ip204-173 +ip204-172 +bdn +bunya +command +ip204-171 +sociologia +malone +ip204-167 +hosting7 +oldsearch +ip204-165 +ip204-162 +ip204-159 +ip204-157 +portsnap +ip204-155 +ip204-152 +ikon +ip204-151 +ip204-147 +ip204-143 +bsl110 +sipus +ip204-141 +ip204-139 +ip204-137 +ace01 +ip204-133 +ip204-132 +ip204-131 +ip204-129 +ip204-128 +ip204-127 +ip204-126 +www-cache +v800 +ip204-123 +banyan +poodle +rhun +sapdb +ip204-113 +ip204-112 +ip204-111 +origin-extras +ip204-108 +contributestage +ip204-107 +ip204-106 +ip204-104 +ip204-103 +isl +ip204-102 +gcp +ip204-101 +hartree +gamemaster +ip204-100 +ip220-110 +v002 +ip204-250 +susanita +samba1 +ip220-129 +ip204-246 +ssl48 +chaucer +ssl27 +ssl25 +ssl23 +ssl19 +ssl17 +stubbs +ip204-238 +ip220-189 +ip220-186 +ip220-185 +ip220-180 +ip220-176 +ip220-175 +ramsey +ip220-173 +scrabble +ip220-163 +ip220-148 +phalanx +ip220-147 +manasseh +krym +perfmon +ip220-145 +ip220-141 +ip220-138 +ip220-134 +ip220-127 +ip220-125 +ip220-123 +ip220-121 +ip220-118 +ip220-114 +ip220-111 +ip220-105 +ip220-104 +ip220-102 +ip220-101 +ip204-212 +ip204-209 +www.cbt +ip204-208 +ip204-207 +ip204-206 +ip204-205 +ip204-249 +ip204-201 +ip204-244 +ip204-236 +ip204-233 +ip204-230 +adminv3 +ip204-227 +ip204-226 +ip204-222 +ip204-221 +alu +tylerc +d136 +distr-2-out +fw2-ext +cms-old +ip204-220 +park3 +pns1 +ip204-216 +fw1-ext +georgian +minichat +sung +nmo +habboonline +anitha +ip204-211 +clones +amazinggrace +alperen +ip204-199 +training3 +www.espana +urbanp +ardent +ip204-198 +fpro +davidlee +vulcan2 +ssl50 +texas2 +matche +ip204-197 +onlive +visp +server101 +melc +ip204-196 +yangzhou +lone +cc7 +blx +lite3 +ip204-195 +ip204-194 +ip204-193 +flooding +d143 +peipei +ip204-202 +reklamy +nmt +byrd +nvg +ip204-191 +os8 +pga +testestest +ip204-189 +mispel +ip204-188 +rsn +neustar +sww +ip204-187 +wh1 +imap.mail +crazy123 +visacard +autoloan +ip204-186 +ip204-185 +ip204-184 +vauxhall +nowandzen +onfire +ip204-183 +mike1 +foreclosures +spec1 +arthur2 +testbox +kikaku +dcmail2 +ip220-196 +ip204-181 +ip204-180 +hkb +bullets +detektiv +poker2 +netad +xmedia +patt +informant +hm1 +hosted.by +teszt2 +e002 +ops1 +training5 +ip204-178 +soroosh +market13 +ip204-169 +munro +ip204-168 +ip204-166 +sabamail +nezarat +ip204-164 +ip204-176 +sepid +mcb-erp +bluemaple +ssl49 +redpandaplus +ip204-161 +hszh +ip204-160 +mcdonald +ip204-156 +titan5 +newzone +cs0 +peek2 +list2 +ip220-120 +ip220-197 +loretta +ip204-154 +aurion +www.siva +ip204-153 +ecom2 +ip204-146 +test987 +ccmail +ip204-145 +coldfusion +ip204-144 +ip204-142 +encom +users1 +it033605 +server108 +janus1 +janus2 +ip204-140 +storm8 +ip204-138 +ip204-136 +mtest1 +navigation +ip204-200 +ip204-8 +www.domainnames +dottie +system68 +domainnames +www.showyourcolours +sfp1 +ip204-135 +super5 +hudsons +ip204-134 +showyourcolours +rogerfederer +haider +gamegame +teamon2 +www.apollon +cessna +remoteapps +msx002 +ip204-170 +ip204-125 +acadmin +bolla +paulsen +tipweb +goodwill +apro +jc1 +sp12 +ip204-120 +ip204-109 +sugiyama1 +ip204-105 +nichop +ip204-117 +ip204-149 +hazelnut +access11 +ns2.cs +hitech1 +ns2.cc +ip204-148 +email5 +www.forumtest +koko10 +ingatlan +tbsoc +take-survey diff --git a/docker-push.sh b/docker-push.sh new file mode 100755 index 00000000..531601eb --- /dev/null +++ b/docker-push.sh @@ -0,0 +1,230 @@ +#!/bin/bash +# ============================================ +# Docker Hub 镜像推送脚本 +# 用途:构建并推送所有服务镜像到 Docker Hub + +# 多架构构建:./docker-push.sh -p linux/amd64,linux/arm64 worker +# ============================================ + +set -e + +# 启用 BuildKit(支持高级缓存功能) +export DOCKER_BUILDKIT=1 + +# ==================== 配置 ==================== +# Docker Hub 用户名(修改为你的用户名) +DOCKER_USER="${DOCKER_USER:-yyhuni}" +# 镜像版本标签 +VERSION="${VERSION:-latest}" +# 是否推送(默认 yes,设为 no 则只构建不推送) +PUSH="${PUSH:-yes}" +# 构建平台(默认当前架构,可设为 linux/amd64,linux/arm64 进行多架构构建) +PLATFORM="${PLATFORM:-}" + +# 镜像列表 +IMAGES=( + "xingrin-server:docker/server/Dockerfile" + "xingrin-frontend:docker/frontend/Dockerfile" + "xingrin-nginx:docker/nginx/Dockerfile" + "xingrin-worker:docker/worker/Dockerfile" + "xingrin-agent:docker/agent/Dockerfile" +) + +# 颜色 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[OK]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# ==================== 帮助信息 ==================== +show_help() { + cat << EOF +用法: $0 [选项] [镜像名...] + +选项: + -u, --user USER Docker Hub 用户名 (默认: $DOCKER_USER) + -v, --version VER 镜像版本标签 (默认: latest) + -p, --platform PLAT 构建平台 (如: linux/amd64,linux/arm64) + --no-push 只构建不推送 + -h, --help 显示帮助 + +镜像名 (可选,不指定则构建全部): + server 后端服务 + frontend 前端服务 + nginx Nginx 反向代理 + worker 扫描 Worker + agent 心跳上报 Agent(轻量) + +示例: + $0 # 构建并推送所有镜像 + $0 server frontend # 只构建 server 和 frontend + $0 -v 1.0.0 # 使用指定版本标签 + $0 --no-push # 只构建不推送 + $0 -p linux/amd64,linux/arm64 # 多架构构建 + +环境变量: + DOCKER_USER Docker Hub 用户名 + VERSION 镜像版本标签 + PUSH 是否推送 (yes/no) + PLATFORM 构建平台 +EOF + exit 0 +} + +# ==================== 解析参数 ==================== +SELECTED_IMAGES=() + +while [[ $# -gt 0 ]]; do + case $1 in + -u|--user) + DOCKER_USER="$2" + shift 2 + ;; + -v|--version) + VERSION="$2" + shift 2 + ;; + -p|--platform) + PLATFORM="$2" + shift 2 + ;; + --no-push) + PUSH="no" + shift + ;; + -h|--help) + show_help + ;; + server|frontend|nginx|worker|agent) + SELECTED_IMAGES+=("$1") + shift + ;; + *) + log_error "未知参数: $1" + show_help + ;; + esac +done + +# ==================== 检查 Docker 登录 ==================== +check_docker_login() { + if [ "$PUSH" = "yes" ]; then + log_info "检查 Docker Hub 登录状态..." + if ! docker info 2>/dev/null | grep -q "Username"; then + log_warn "未登录 Docker Hub,请先执行: docker login" + read -p "是否现在登录?(y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + docker login + else + log_error "需要登录才能推送镜像" + exit 1 + fi + fi + log_success "Docker Hub 已登录" + fi +} + +# ==================== 构建镜像 ==================== +build_image() { + local name=$1 + local dockerfile=$2 + local full_name="${DOCKER_USER}/${name}:${VERSION}" + + log_info "构建镜像: $full_name" + log_info " Dockerfile: $dockerfile" + + # 构建命令 + local build_cmd="docker build" + + # 多架构构建使用 buildx + if [ -n "$PLATFORM" ]; then + build_cmd="docker buildx build --platform $PLATFORM" + if [ "$PUSH" = "yes" ]; then + build_cmd="$build_cmd --push" + fi + fi + + # 执行构建 + $build_cmd \ + -t "$full_name" \ + -t "${DOCKER_USER}/${name}:latest" \ + -f "$dockerfile" \ + . + + if [ $? -eq 0 ]; then + log_success "构建成功: $full_name" + else + log_error "构建失败: $full_name" + exit 1 + fi + + # 推送(非 buildx 模式) + if [ "$PUSH" = "yes" ] && [ -z "$PLATFORM" ]; then + log_info "推送镜像: $full_name" + docker push "$full_name" + docker push "${DOCKER_USER}/${name}:latest" + log_success "推送成功: $full_name" + fi +} + +# ==================== 主流程 ==================== +main() { + echo "" + echo "==========================================" + echo " Docker Hub 镜像构建与推送" + echo "==========================================" + echo "" + log_info "用户: $DOCKER_USER" + log_info "版本: $VERSION" + log_info "推送: $PUSH" + [ -n "$PLATFORM" ] && log_info "平台: $PLATFORM" + echo "" + + check_docker_login + + # 切换到项目根目录 + cd "$(dirname "$0")" + + # 如果指定了特定镜像,只构建那些 + if [ ${#SELECTED_IMAGES[@]} -gt 0 ]; then + for sel in "${SELECTED_IMAGES[@]}"; do + for item in "${IMAGES[@]}"; do + name="${item%%:*}" + dockerfile="${item##*:}" + if [[ "$name" == "xingrin-$sel" ]]; then + build_image "$name" "$dockerfile" + fi + done + done + else + # 构建所有镜像 + for item in "${IMAGES[@]}"; do + name="${item%%:*}" + dockerfile="${item##*:}" + build_image "$name" "$dockerfile" + done + fi + + echo "" + echo "==========================================" + log_success " 完成!" + echo "==========================================" + echo "" + + if [ "$PUSH" = "yes" ]; then + log_info "镜像已推送到 Docker Hub:" + for item in "${IMAGES[@]}"; do + name="${item%%:*}" + echo " - docker pull ${DOCKER_USER}/${name}:${VERSION}" + done + fi +} + +main diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 00000000..cbc08455 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,52 @@ +# ==================== 数据库配置(PostgreSQL) ==================== +# DB_HOST 决定使用本地容器还是远程数据库: +# - postgres / localhost / 127.0.0.1 → 启动本地 PostgreSQL 容器 +# - 其他地址(如 192.168.1.100) → 使用远程数据库,不启动本地容器 +DB_HOST=postgres +DB_PORT=5432 +DB_NAME=xingrin +DB_USER=postgres +DB_PASSWORD=123.com + +# ==================== Redis 配置 ==================== +# 在 Docker 网络中,Redis 服务名称为 redis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_DB=0 + +# ==================== 服务端口配置 ==================== +# SERVER_PORT 为 Django / uvicorn 对外端口 +SERVER_PORT=8888 + +# ==================== 远程 Worker 配置 ==================== +# 供远程 Worker 访问主服务器的地址: +# - 仅本地部署:server(Docker 内部服务名) +# - 有远程 Worker:改为主服务器外网 IP(如 192.168.1.100) +# 注意:远程 Worker 访问数据库/Redis 也会使用此地址(除非配置了远程 PostgreSQL) +PUBLIC_HOST=server + +# ==================== Django 核心配置 ==================== +# 生产环境务必更换为随机强密钥 +DJANGO_SECRET_KEY=django-insecure-change-me-in-production +# 是否开启调试模式(生产环境请保持 False) +DEBUG=False +# 允许的前端来源地址(用于 CORS) +CORS_ALLOWED_ORIGINS=http://localhost:3000 + +# ==================== 路径配置(容器内路径) ==================== +# 扫描结果保存目录 +SCAN_RESULTS_DIR=/app/backend/results +# Django 日志目录 +# 注意:如果留空或删除此变量,日志将只输出到 Docker 控制台(标准输出),不写入文件 +LOG_DIR=/app/backend/logs + +# ==================== 日志级别配置 ==================== +# 应用日志级别:DEBUG / INFO / WARNING / ERROR +LOG_LEVEL=INFO +# 是否记录命令执行日志(大量扫描时会增加磁盘占用) +ENABLE_COMMAND_LOGGING=true + +# ==================== Docker Hub 配置(生产模式) ==================== +# 生产模式下从 Docker Hub 拉取镜像时使用 +DOCKER_USER=yyhuni +IMAGE_TAG=latest diff --git a/docker/agent/Dockerfile b/docker/agent/Dockerfile new file mode 100644 index 00000000..724e4f53 --- /dev/null +++ b/docker/agent/Dockerfile @@ -0,0 +1,24 @@ +# ============================================ +# XingRin Agent - 轻量心跳上报镜像 +# 用途:心跳上报 + 负载监控 +# 基础镜像:Alpine Linux (~5MB) +# 最终大小:~10MB +# ============================================ + +FROM alpine:3.19 + +# 安装必要工具 +RUN apk add --no-cache \ + bash \ + curl \ + procps + +# 复制 agent 脚本 +COPY backend/scripts/worker-deploy/agent.sh /app/agent.sh +RUN chmod +x /app/agent.sh + +# 工作目录 +WORKDIR /app + +# 默认命令 +CMD ["bash", "/app/agent.sh"] diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml new file mode 100644 index 00000000..b74eb7db --- /dev/null +++ b/docker/docker-compose.dev.yml @@ -0,0 +1,109 @@ +services: + # PostgreSQL(可选,使用远程数据库时不启动) + # 本地模式: docker compose --profile local-db up -d + # 远程模式: docker compose up -d(需配置 DB_HOST 为远程地址) + postgres: + profiles: ["local-db"] + image: postgres:15 + restart: always + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./postgres/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh + ports: + - "${DB_PORT}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + restart: always + ports: + - "${REDIS_PORT}:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + server: + build: + context: .. + dockerfile: docker/server/Dockerfile + restart: always + ports: + - "${SERVER_PORT}:8888" + env_file: + - .env + depends_on: + redis: + condition: service_healthy + volumes: + # 统一使用固定路径(开发环境:ln -s ~/project/backend/results /opt/xingrin/results) + - /opt/xingrin/results:/app/backend/results + - /opt/xingrin/logs:/app/backend/logs + - /var/run/docker.sock:/var/run/docker.sock + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8888/api/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + # Agent:心跳上报 + 负载监控 + agent: + build: + context: .. + dockerfile: docker/worker/Dockerfile + restart: always + env_file: + - .env + environment: + - SERVER_URL=http://server:8888 + - WORKER_NAME=本地节点 + - IS_LOCAL=true + command: bash /app/backend/scripts/worker-deploy/agent.sh + depends_on: + server: + condition: service_healthy + volumes: + - /proc:/host/proc:ro + + frontend: + build: + context: .. + dockerfile: docker/frontend/Dockerfile + restart: always + depends_on: + server: + condition: service_healthy + + nginx: + build: + context: .. + dockerfile: docker/nginx/Dockerfile + restart: always + depends_on: + server: + condition: service_healthy + frontend: + condition: service_started + ports: + - "80:80" + - "443:443" + volumes: + # SSL 证书挂载(方便更新) + - ./nginx/ssl:/etc/nginx/ssl:ro + +volumes: + postgres_data: + +networks: + default: + name: xingrin_network # 固定网络名,不随目录名变化 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..c719940e --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,109 @@ +# ============================================ +# 生产环境配置 - 使用 Docker Hub 预构建镜像 +# ============================================ +# 用法: docker compose up -d +# +# 开发环境请使用: docker compose -f docker-compose.dev.yml up -d +# ============================================ + +services: + # PostgreSQL(可选,使用远程数据库时不启动) + postgres: + profiles: ["local-db"] + image: postgres:15 + restart: always + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./postgres/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh + ports: + - "${DB_PORT}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + restart: always + ports: + - "${REDIS_PORT}:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + server: + image: ${DOCKER_USER:-yyhuni}/xingrin-server:${IMAGE_TAG:-latest} + restart: always + ports: + - "${SERVER_PORT}:8888" + env_file: + - .env + depends_on: + redis: + condition: service_healthy + volumes: + # 统一使用固定路径(部署时需创建:mkdir -p /opt/xingrin/{results,logs}) + - /opt/xingrin/results:/app/backend/results + - /opt/xingrin/logs:/app/backend/logs + # Docker Socket 挂载:允许 Django 服务器执行本地 docker 命令(用于本地 Worker 任务分发) + - /var/run/docker.sock:/var/run/docker.sock + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8888/api/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + # ============================================ + # Agent:轻量心跳上报 + 负载监控(~10MB) + # 扫描任务通过 task_distributor 分发到动态容器 + # ============================================ + + agent: + image: ${DOCKER_USER:-yyhuni}/xingrin-agent:${IMAGE_TAG:-latest} + container_name: xingrin-agent + restart: always + environment: + - SERVER_URL=http://server:8888 + - WORKER_NAME=本地节点 + - IS_LOCAL=true + depends_on: + server: + condition: service_healthy + volumes: + - /proc:/host/proc:ro + + frontend: + image: ${DOCKER_USER:-yyhuni}/xingrin-frontend:${IMAGE_TAG:-latest} + restart: always + depends_on: + server: + condition: service_healthy + + nginx: + image: ${DOCKER_USER:-yyhuni}/xingrin-nginx:${IMAGE_TAG:-latest} + restart: always + depends_on: + server: + condition: service_healthy + frontend: + condition: service_started + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/ssl:/etc/nginx/ssl:ro + +volumes: + postgres_data: + +networks: + default: + name: xingrin_network # 固定网络名,不随目录名变化 diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile new file mode 100644 index 00000000..37d6c53a --- /dev/null +++ b/docker/frontend/Dockerfile @@ -0,0 +1,60 @@ +# 前端 Next.js Dockerfile +# 使用多阶段构建 + BuildKit 缓存优化 + +# ==================== 依赖安装阶段 ==================== +FROM node:20-alpine AS deps +WORKDIR /app + +# 安装 pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# 复制依赖文件 +COPY frontend/package.json frontend/pnpm-lock.yaml ./ + +# 安装依赖(使用 BuildKit 缓存加速) +RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile + +# ==================== 构建阶段 ==================== +FROM node:20-alpine AS builder +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@latest --activate + +# 复制依赖 +COPY --from=deps /app/node_modules ./node_modules +COPY frontend/ ./ + +# 设置环境变量(构建时使用) +ARG NEXT_PUBLIC_API_URL +ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} +# Docker 内部网络使用服务名 server 作为后端地址 +ENV API_HOST=server + +# 构建(使用 BuildKit 缓存加速) +RUN --mount=type=cache,target=/app/.next/cache \ + pnpm build + +# ==================== 运行阶段 ==================== +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production + +# 创建非 root 用户 +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# 复制构建产物 +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile new file mode 100644 index 00000000..fbfd958f --- /dev/null +++ b/docker/nginx/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx:1.27-alpine + +# 复制 nginx 配置和证书 +COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf +COPY docker/nginx/ssl /etc/nginx/ssl diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 00000000..d2a8b13e --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,67 @@ +worker_processes auto; +events { worker_connections 1024; } + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + + # 上游服务 + upstream backend { + server server:8888; + } + + upstream frontend { + server frontend:3000; + } + + # HTTP 跳转到 HTTPS + server { + listen 80; + server_name _; + + return 301 https://$host$request_uri; + } + + # HTTPS 反代(将证书放在 /docker/nginx/ssl 下映射到 /etc/nginx/ssl) + server { + listen 443 ssl http2; + server_name _; + + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + client_max_body_size 50m; + + location /api/ { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://backend; + } + + # WebSocket 反代 + location /ws/ { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 86400; # 24小时,防止 WebSocket 超时 + } + + # 前端反代 + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://frontend; + } + } +} diff --git a/docker/postgres/init-user-db.sh b/docker/postgres/init-user-db.sh new file mode 100755 index 00000000..5d0a6f9f --- /dev/null +++ b/docker/postgres/init-user-db.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +# 创建应用数据库(生产 + 开发) +# 使用条件创建避免与 POSTGRES_DB 自动创建的数据库冲突 +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "postgres" <<-EOSQL + SELECT 'CREATE DATABASE xingrin' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'xingrin')\gexec + SELECT 'CREATE DATABASE xingrin_dev' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'xingrin_dev')\gexec + GRANT ALL PRIVILEGES ON DATABASE xingrin TO "$POSTGRES_USER"; + GRANT ALL PRIVILEGES ON DATABASE xingrin_dev TO "$POSTGRES_USER"; +EOSQL diff --git a/docker/restart.sh b/docker/restart.sh new file mode 100755 index 00000000..5cf46f32 --- /dev/null +++ b/docker/restart.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")" +source "./scripts/common.sh" +init_docker_env_with_env_check + +# 颜色 +CYAN='\033[0;36m' +GREEN='\033[0;32m' +NC='\033[0m' + +echo -e "${CYAN}[RESTART]${NC} 重启服务..." + +# 尝试重启两种模式的容器 +if [ -f "docker-compose.yml" ]; then + ${COMPOSE_CMD} -f docker-compose.yml restart 2>/dev/null || true +fi +if [ -f "docker-compose.dev.yml" ]; then + ${COMPOSE_CMD} -f docker-compose.dev.yml restart 2>/dev/null || true +fi + +echo -e "${GREEN}[OK]${NC} 服务已重启" diff --git a/docker/scripts/common.sh b/docker/scripts/common.sh new file mode 100644 index 00000000..d56ea6ef --- /dev/null +++ b/docker/scripts/common.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# 公共函数库 - 被其他脚本 source 引用 + +# ==================== 颜色定义 ==================== +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# ==================== 日志函数 ==================== +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# ==================== Docker 环境检查 ==================== +check_docker() { + if ! command -v docker >/dev/null 2>&1; then + log_error "未检测到 docker 命令,请先安装 Docker。" + exit 1 + fi + + if ! docker info >/dev/null 2>&1; then + log_error "Docker 守护进程未运行,请先启动 Docker。" + exit 1 + fi +} + +# ==================== Docker Compose 命令检测 ==================== +detect_compose_cmd() { + if command -v docker-compose >/dev/null 2>&1; then + COMPOSE_CMD="docker-compose" + elif docker compose version >/dev/null 2>&1; then + COMPOSE_CMD="docker compose" + else + log_error "未检测到 docker-compose 或 docker compose。" + exit 1 + fi + export COMPOSE_CMD +} + +# ==================== 环境变量文件检查 ==================== +check_env_file() { + if [ ! -f .env ]; then + log_error "未找到 .env 配置文件。" + echo " 请先根据 .env.example 创建 .env 文件。" + exit 1 + fi +} + +# ==================== 数据库配置检测 ==================== +detect_db_profile() { + DB_HOST=$(grep -E "^DB_HOST=" .env | cut -d'=' -f2 | tr -d ' "'"'" || echo "postgres") + + if [[ "$DB_HOST" == "postgres" || "$DB_HOST" == "localhost" || "$DB_HOST" == "127.0.0.1" ]]; then + echo "[DB] 使用本地 PostgreSQL 容器" + PROFILE_ARG="--profile local-db" + else + echo "[DB] 使用远程 PostgreSQL: $DB_HOST" + PROFILE_ARG="" + fi + export PROFILE_ARG +} + + +# ==================== 获取 docker 目录路径 ==================== +get_docker_dir() { + # common.sh 位于 docker/scripts/,所以 docker 目录是上一级 + local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + echo "$(dirname "$script_dir")" +} + +# ==================== 初始化检查(一次性调用) ==================== +init_docker_env() { + DOCKER_DIR="$(get_docker_dir)" + cd "$DOCKER_DIR" + check_docker + detect_compose_cmd + export DOCKER_DIR +} + +init_docker_env_with_env_check() { + init_docker_env + check_env_file +} diff --git a/docker/scripts/init-data.sh b/docker/scripts/init-data.sh new file mode 100755 index 00000000..2e712a4b --- /dev/null +++ b/docker/scripts/init-data.sh @@ -0,0 +1,171 @@ +#!/bin/bash +# +# 数据初始化脚本(公共模块) +# +# 包含: +# - 数据库迁移 +# - 初始化默认引擎配置 +# - 初始化字典 +# - 初始化 Nuclei 模板仓库 +# +# 被以下脚本调用: +# - install.sh(安装时) +# - start.sh(启动时) +# - update.sh(更新时) +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# 颜色输出 +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +log_info() { echo -e " ${GREEN}OK${NC} $1"; } +log_warn() { echo -e " ${YELLOW}!${NC} $1"; } +log_step() { echo -e " ${CYAN}>>${NC} $1"; } + +# 检查服务是否运行 +check_server() { + if ! docker compose ps --status running 2>/dev/null | grep -q "server"; then + echo "Server 容器未运行,跳过数据初始化" + return 1 + fi + return 0 +} + +# 等待服务就绪 +wait_for_server() { + log_info "等待 Server 服务就绪..." + local max_attempts=30 + local attempt=0 + + while [ $attempt -lt $max_attempts ]; do + if docker compose exec -T server python backend/manage.py check &>/dev/null; then + log_info "Server 服务已就绪" + return 0 + fi + attempt=$((attempt + 1)) + sleep 2 + done + + log_warn "等待 Server 服务超时" + return 1 +} + +# 数据库迁移 +run_migrations() { + log_step "执行数据库迁移..." + + # 开发环境:先 makemigrations + if [ "$DEV_MODE" = "true" ]; then + docker compose exec -T server python backend/manage.py makemigrations --noinput 2>/dev/null || true + fi + + docker compose exec -T server python backend/manage.py migrate --noinput + log_info "数据库迁移完成" +} + +# 初始化引擎配置 +init_engine_config() { + log_step "初始化引擎配置..." + docker compose exec -T server python backend/manage.py shell -c " +from apps.engine.models import ScanEngine +from pathlib import Path + +yaml_path = Path('/app/backend/apps/scan/configs/engine_config_example.yaml') +if not yaml_path.exists(): + print('未找到配置文件,跳过') + exit(0) + +# 检查是否已有 full scan 引擎 +engine = ScanEngine.objects.filter(name='full scan').first() +if engine: + if not engine.configuration or not engine.configuration.strip(): + engine.configuration = yaml_path.read_text() + engine.save(update_fields=['configuration']) + print(f'已初始化引擎配置: {engine.name}') + else: + print(f'引擎已有配置,跳过') +else: + # 创建引擎 + engine = ScanEngine.objects.create( + name='full scan', + configuration=yaml_path.read_text(), + ) + print(f'已创建引擎: {engine.name}') +" + log_info "引擎配置初始化完成" +} + +# 初始化字典 +init_wordlists() { + log_step "初始化字典..." + docker compose exec -T server python backend/manage.py init_wordlists + log_info "字典初始化完成" +} + +# 初始化 Nuclei 模板仓库 +init_nuclei_templates() { + log_step "初始化 Nuclei 模板仓库..." + docker compose exec -T server python backend/manage.py init_nuclei_templates --sync + log_info "Nuclei 模板仓库初始化完成" +} + +# 初始化 admin 用户 +init_admin_user() { + log_step "初始化 admin 用户..." + docker compose exec -T server python backend/manage.py init_admin + log_info "admin 用户初始化完成" +} + +# 主函数 +main() { + # 解析参数 + DEV_MODE=false + SKIP_MIGRATION=false + + while [[ $# -gt 0 ]]; do + case $1 in + --dev) DEV_MODE=true; shift ;; + --skip-migration) SKIP_MIGRATION=true; shift ;; + *) shift ;; + esac + done + + echo "" + echo -e "${BOLD}${BLUE}────────────────────────────────────────${NC}" + echo -e "${BOLD}${BLUE} 数据初始化${NC}" + echo -e "${BOLD}${BLUE}────────────────────────────────────────${NC}" + echo "" + + if ! check_server; then + return 1 + fi + + wait_for_server || return 1 + + if [ "$SKIP_MIGRATION" = "false" ]; then + run_migrations + fi + + init_engine_config + init_wordlists + init_nuclei_templates + init_admin_user + + echo "" + echo -e " ${GREEN}数据初始化完成${NC}" + echo "" +} + +# 如果直接执行此脚本 +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/docker/scripts/setup-system-monitor.sh b/docker/scripts/setup-system-monitor.sh new file mode 100755 index 00000000..1a97c42d --- /dev/null +++ b/docker/scripts/setup-system-monitor.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# 系统监控初始化脚本:8G Swap + Netdata + OOM 保护 +# 需要 root 权限运行 + +set -e + +SWAP_SIZE="8G" +SWAP_FILE="/swapfile" + +echo "========== 系统监控初始化 ==========" + +# 1. 设置 Swap +echo "[1/3] 配置 ${SWAP_SIZE} Swap..." +if swapon --show | grep -q "${SWAP_FILE}"; then + echo " Swap 已存在,跳过" +else + fallocate -l ${SWAP_SIZE} ${SWAP_FILE} + chmod 600 ${SWAP_FILE} + mkswap ${SWAP_FILE} + swapon ${SWAP_FILE} + + # 添加到 fstab(如果不存在) + if ! grep -q "${SWAP_FILE}" /etc/fstab; then + echo "${SWAP_FILE} none swap sw 0 0" >> /etc/fstab + fi + echo " Swap 配置完成" +fi + +# 2. 安装 Netdata +echo "[2/3] 安装 Netdata..." +if command -v netdata &> /dev/null; then + echo " Netdata 已安装,跳过" +else + curl -fsSL https://get.netdata.cloud/kickstart.sh -o /tmp/netdata-kickstart.sh + bash /tmp/netdata-kickstart.sh --non-interactive --stable-channel + rm -f /tmp/netdata-kickstart.sh + echo " Netdata 安装完成" +fi + +# 3. 设置 Netdata OOM 保护 +echo "[3/3] 配置 OOM 保护..." +OOM_CONF_DIR="/etc/systemd/system/netdata.service.d" +OOM_CONF_FILE="${OOM_CONF_DIR}/oom.conf" + +if [ -f "${OOM_CONF_FILE}" ]; then + echo " OOM 保护已配置,跳过" +else + mkdir -p ${OOM_CONF_DIR} + cat > ${OOM_CONF_FILE} << 'EOF' +[Service] +OOMScoreAdjust=-1000 +EOF + systemctl daemon-reload + systemctl restart netdata + echo " OOM 保护配置完成" +fi + +echo "" +echo "========== 配置完成 ==========" +echo "Swap: $(swapon --show --bytes | awk 'NR==2{print $3/1024/1024/1024 " GB"}')" +echo "Netdata: http://$(hostname -I | awk '{print $1}'):19999" +echo "" diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile new file mode 100644 index 00000000..66b8beac --- /dev/null +++ b/docker/server/Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.10-slim + +WORKDIR /app + +# 安装系统依赖 (用于编译某些 Python 包) +RUN apt-get update && apt-get install -y \ + gcc \ + libpq-dev \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +# 安装 Docker CLI(用于本地 Worker 任务分发) +RUN curl -fsSL https://get.docker.com | sh + +# 安装 uv(超快的 Python 包管理器) +RUN pip install uv + +# 安装 Python 依赖(使用 uv 并行下载,速度快 10-100 倍) +COPY backend/requirements.txt . +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --system -r requirements.txt + +# 复制后端代码 +COPY backend /app/backend +ENV PYTHONPATH=/app/backend + +# 暴露端口 +# 8888: Django/uvicorn +EXPOSE 8888 + +# 复制启动脚本 +COPY docker/server/start.sh /app/start.sh +RUN chmod +x /app/start.sh + +CMD ["/app/start.sh"] diff --git a/docker/server/start.sh b/docker/server/start.sh new file mode 100644 index 00000000..acefd681 --- /dev/null +++ b/docker/server/start.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +echo "[START] 启动 XingRin Server..." + +# 1. 生成和迁移数据库 +echo " [1/3] 生成数据库迁移文件..." +cd /app/backend +python manage.py makemigrations +echo " ✓ 迁移文件生成完成" + +echo " [1.1/3] 执行数据库迁移..." +python manage.py migrate --noinput +echo " ✓ 数据库迁移完成" + +echo " [1.2/3] 初始化默认扫描引擎..." +python manage.py init_default_engine +echo " ✓ 默认扫描引擎已就绪" + +echo " [1.3/3] 初始化默认目录字典..." +python manage.py init_wordlists +echo " ✓ 默认目录字典已就绪" + +# 2. 启动 Django uvicorn 服务 (ASGI) +# 定时任务由内置 APScheduler 处理,在 Django 启动时自动启动 +echo " [2/3] 启动 Django uvicorn (ASGI)..." +uvicorn config.asgi:application --host 0.0.0.0 --port 8888 diff --git a/docker/start.sh b/docker/start.sh new file mode 100755 index 00000000..4943ef3a --- /dev/null +++ b/docker/start.sh @@ -0,0 +1,156 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# 解析参数 +WITH_FRONTEND=true +DEV_MODE=false +for arg in "$@"; do + case $arg in + --no-frontend) WITH_FRONTEND=false ;; + --dev) DEV_MODE=true ;; + esac +done + +# 选择 compose 文件 +if [ "$DEV_MODE" = true ]; then + COMPOSE_FILE="docker-compose.dev.yml" + echo -e "${YELLOW}[MODE]${NC} 开发模式 - 本地构建镜像" +else + COMPOSE_FILE="docker-compose.yml" + echo -e "${GREEN}[MODE]${NC} 生产模式 - 使用 Docker Hub 镜像" +fi + +# 检查 Docker 环境 +if ! command -v docker >/dev/null 2>&1; then + echo -e "${RED}[ERROR]${NC} 未检测到 docker 命令,请先安装 Docker" + exit 1 +fi + +if ! docker info >/dev/null 2>&1; then + echo -e "${RED}[ERROR]${NC} Docker 守护进程未运行,请先启动 Docker" + exit 1 +fi + +if command -v docker-compose >/dev/null 2>&1; then + COMPOSE_CMD="docker-compose" +elif docker compose version >/dev/null 2>&1; then + COMPOSE_CMD="docker compose" +else + echo -e "${RED}[ERROR]${NC} 未检测到 docker compose,请先安装" + exit 1 +fi + +# 检查配置文件 +if [ ! -f .env ]; then + echo -e "${RED}[ERROR]${NC} 未找到 .env 配置文件" + echo " 请先复制 .env.example 为 .env 并配置" + exit 1 +fi + +# 确保数据目录存在 +DATA_DIR="/opt/xingrin" +if [ ! -d "$DATA_DIR/results" ] || [ ! -d "$DATA_DIR/logs" ]; then + echo -e "${CYAN}[INIT]${NC} 创建数据目录: $DATA_DIR" + sudo mkdir -p "$DATA_DIR/results" "$DATA_DIR/logs" + sudo chmod -R 755 "$DATA_DIR" +fi + +# 读取数据库配置 +DB_HOST=$(grep -E "^DB_HOST=" .env | cut -d'=' -f2 | tr -d ' "'"'" || echo "postgres") + +if [[ "$DB_HOST" == "postgres" || "$DB_HOST" == "localhost" || "$DB_HOST" == "127.0.0.1" ]]; then + echo -e "${CYAN}[DB]${NC} 使用本地 PostgreSQL 容器" + PROFILE_ARG="--profile local-db" +else + echo -e "${CYAN}[DB]${NC} 使用远程 PostgreSQL: $DB_HOST" + PROFILE_ARG="" +fi + +# 启动服务(启用 BuildKit 缓存 + 并行构建加速) +export DOCKER_BUILDKIT=1 +export COMPOSE_DOCKER_CLI_BUILD=1 +export BUILDKIT_INLINE_CACHE=1 + +# 使用指定的 compose 文件 +COMPOSE_ARGS="-f ${COMPOSE_FILE} ${PROFILE_ARG}" + +echo "" +if [ "$DEV_MODE" = true ]; then + # 开发模式:本地构建 + if [ "$WITH_FRONTEND" = true ]; then + echo -e "${CYAN}[BUILD]${NC} 并行构建镜像..." + ${COMPOSE_CMD} ${COMPOSE_ARGS} build --parallel + echo -e "${CYAN}[START]${NC} 启动全部服务..." + ${COMPOSE_CMD} ${COMPOSE_ARGS} up -d + else + echo -e "${CYAN}[BUILD]${NC} 并行构建后端镜像..." + ${COMPOSE_CMD} ${COMPOSE_ARGS} build --parallel server scan-worker maintenance-worker + echo -e "${CYAN}[START]${NC} 启动后端服务..." + ${COMPOSE_CMD} ${COMPOSE_ARGS} up -d redis server scan-worker maintenance-worker + if [ -n "$PROFILE_ARG" ]; then + ${COMPOSE_CMD} ${COMPOSE_ARGS} up -d postgres + fi + fi +else + # 生产模式:拉取 Docker Hub 镜像 + if [ "$WITH_FRONTEND" = true ]; then + echo -e "${CYAN}[PULL]${NC} 拉取最新镜像..." + ${COMPOSE_CMD} ${COMPOSE_ARGS} pull + echo -e "${CYAN}[START]${NC} 启动全部服务..." + ${COMPOSE_CMD} ${COMPOSE_ARGS} up -d + else + echo -e "${CYAN}[PULL]${NC} 拉取后端镜像..." + ${COMPOSE_CMD} ${COMPOSE_ARGS} pull redis server scan-worker maintenance-worker + echo -e "${CYAN}[START]${NC} 启动后端服务..." + ${COMPOSE_CMD} ${COMPOSE_ARGS} up -d redis server scan-worker maintenance-worker + if [ -n "$PROFILE_ARG" ]; then + ${COMPOSE_CMD} ${COMPOSE_ARGS} up -d postgres + fi + fi +fi +echo -e "${GREEN}[OK]${NC} 服务已启动" + +# 数据初始化 +./scripts/init-data.sh + +# 获取访问地址 +PUBLIC_HOST=$(grep "^PUBLIC_HOST=" .env 2>/dev/null | cut -d= -f2) +if [ -n "$PUBLIC_HOST" ] && [ "$PUBLIC_HOST" != "server" ]; then + ACCESS_HOST="$PUBLIC_HOST" +else + ACCESS_HOST="localhost" +fi + +# 显示结果 +echo "" +echo -e "${BOLD}${GREEN}════════════════════════════════════════${NC}" +echo -e "${BOLD}${GREEN} 服务启动成功!${NC}" +echo -e "${BOLD}${GREEN}════════════════════════════════════════${NC}" +echo "" +echo -e "${BOLD}访问地址${NC}" +if [ "$WITH_FRONTEND" = true ]; then + echo -e " XingRin: ${CYAN}https://${ACCESS_HOST}/${NC}" + echo -e " ${YELLOW}(HTTP 会自动跳转到 HTTPS)${NC}" +else + echo -e " API: ${CYAN}http://${ACCESS_HOST}:8888${NC}" + echo "" + echo -e "${YELLOW}[TIP]${NC} 前端未启动,请手动运行:" + echo " cd frontend && pnpm dev" +fi +echo "" +echo -e "${BOLD}默认账号${NC}" +echo " 用户名: admin" +echo " 密码: admin" +echo -e " ${YELLOW}[!] 请首次登录后修改密码${NC}" +echo "" diff --git a/docker/stop.sh b/docker/stop.sh new file mode 100755 index 00000000..56111981 --- /dev/null +++ b/docker/stop.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")" +source "./scripts/common.sh" +init_docker_env + +# 颜色 +CYAN='\033[0;36m' +GREEN='\033[0;32m' +NC='\033[0m' + +echo -e "${CYAN}[STOP]${NC} 停止服务..." + +# 尝试停止两种模式的容器(生产模式和开发模式) +if [ -f "docker-compose.yml" ]; then + ${COMPOSE_CMD} -f docker-compose.yml down 2>/dev/null || true +fi +if [ -f "docker-compose.dev.yml" ]; then + ${COMPOSE_CMD} -f docker-compose.dev.yml down 2>/dev/null || true +fi + +echo -e "${GREEN}[OK]${NC} 服务已停止" diff --git a/docker/worker/Dockerfile b/docker/worker/Dockerfile new file mode 100644 index 00000000..9a55bde1 --- /dev/null +++ b/docker/worker/Dockerfile @@ -0,0 +1,104 @@ +# 第一阶段:使用 Go 官方镜像编译工具 +FROM golang:1.24 AS go-builder + +ENV GOPROXY=https://goproxy.cn,direct +# Naabu 需要 CGO 和 libpcap +ENV CGO_ENABLED=1 + +# 安装编译依赖(libpcap-dev 用于 naabu,git/build-essential 用于编译 massdns) +RUN apt-get update && apt-get install -y \ + libpcap-dev \ + git \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# 安装 massdns(puredns 依赖) +RUN git clone https://github.com/blechschmidt/massdns.git /tmp/massdns && \ + cd /tmp/massdns && \ + make && \ + cp bin/massdns /usr/local/bin/massdns + +# 安装 ProjectDiscovery 等 Go 工具(需要 CGO 的工具如 naabu) +RUN go install -v github.com/projectdiscovery/httpx/cmd/httpx@latest && \ + go install -v github.com/projectdiscovery/naabu/v2/cmd/naabu@latest && \ + go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest && \ + go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest && \ + go install -v github.com/projectdiscovery/katana/cmd/katana@latest && \ + go install -v github.com/tomnomnom/assetfinder@latest && \ + go install -v github.com/ffuf/ffuf/v2@latest && \ + go install -v github.com/d3mondev/puredns/v2@latest + +# 安装 Amass v5(禁用 CGO 以跳过 libpostal 依赖) +RUN CGO_ENABLED=0 go install -v github.com/owasp-amass/amass/v5/cmd/amass@main + +# 安装漏洞扫描器 +RUN go install github.com/hahwul/dalfox/v2@latest + +# 第二阶段:运行时镜像 +FROM ubuntu:24.04 + +# 避免交互式提示 +ENV DEBIAN_FRONTEND=noninteractive + +# 设置工作目录 +WORKDIR /app + +# 1. 安装基础工具和 Python +RUN apt-get update && apt-get install -y \ + python3 \ + python3-pip \ + python3-venv \ + pipx \ + git \ + curl \ + wget \ + unzip \ + jq \ + tmux \ + nmap \ + masscan \ + libpcap-dev \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# 建立 python 软链接 +RUN ln -s /usr/bin/python3 /usr/bin/python + +# 2. 使用 pipx 安装 Python 扫描工具 +ENV PATH="/root/.local/bin:$PATH" +RUN pipx install uro && \ + pipx install waymore && \ + pipx install dnsgen + +# 3. 安装 Sublist3r(统一放在 /opt/xingrin/tools 下) +RUN git clone https://github.com/aboul3la/Sublist3r.git /opt/xingrin/tools/Sublist3r && \ + pip3 install --no-cache-dir -r /opt/xingrin/tools/Sublist3r/requirements.txt --break-system-packages + +# 4. 从 go-builder 阶段复制 Go 环境和编译好的工具 +ENV GOPATH=/root/go +ENV PATH=/usr/local/go/bin:$PATH:$GOPATH/bin +ENV GOPROXY=https://goproxy.cn,direct + +# 5. 安装 uv(超快的 Python 包管理器) +RUN pip install uv --break-system-packages + +# 安装 Python 依赖(使用 uv 并行下载,速度快 10-100 倍) +COPY backend/requirements.txt . +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --system -r requirements.txt --break-system-packages && \ + rm -f /usr/local/lib/python3.*/dist-packages/argparse.py && \ + rm -rf /usr/local/lib/python3.*/dist-packages/__pycache__/argparse* + +COPY --from=go-builder /usr/local/go /usr/local/go +COPY --from=go-builder /go/bin/* /usr/local/bin/ +COPY --from=go-builder /usr/local/bin/massdns /usr/local/bin/massdns + +# 6. 复制后端代码 +COPY backend /app/backend +ENV PYTHONPATH=/app/backend + +# 工作目录设置为 backend,方便运行 python -m 命令 +WORKDIR /app/backend + +# 默认命令(实际由 TaskDistributor 指定具体脚本) +CMD ["python", "--version"] diff --git a/docs/wechat-qrcode.png b/docs/wechat-qrcode.png new file mode 100644 index 00000000..32b0c021 Binary files /dev/null and b/docs/wechat-qrcode.png differ diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..1ec5eb93 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,43 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +certificates \ No newline at end of file diff --git a/frontend/app/dashboard/data.json b/frontend/app/dashboard/data.json new file mode 100644 index 00000000..ec087364 --- /dev/null +++ b/frontend/app/dashboard/data.json @@ -0,0 +1,614 @@ +[ + { + "id": 1, + "header": "Cover page", + "type": "Cover page", + "status": "In Process", + "target": "18", + "limit": "5", + "reviewer": "Eddie Lake" + }, + { + "id": 2, + "header": "Table of contents", + "type": "Table of contents", + "status": "Done", + "target": "29", + "limit": "24", + "reviewer": "Eddie Lake" + }, + { + "id": 3, + "header": "Executive summary", + "type": "Narrative", + "status": "Done", + "target": "10", + "limit": "13", + "reviewer": "Eddie Lake" + }, + { + "id": 4, + "header": "Technical approach", + "type": "Narrative", + "status": "Done", + "target": "27", + "limit": "23", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 5, + "header": "Design", + "type": "Narrative", + "status": "In Process", + "target": "2", + "limit": "16", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 6, + "header": "Capabilities", + "type": "Narrative", + "status": "In Process", + "target": "20", + "limit": "8", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 7, + "header": "Integration with existing systems", + "type": "Narrative", + "status": "In Process", + "target": "19", + "limit": "21", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 8, + "header": "Innovation and Advantages", + "type": "Narrative", + "status": "Done", + "target": "25", + "limit": "26", + "reviewer": "Assign reviewer" + }, + { + "id": 9, + "header": "Overview of EMR's Innovative Solutions", + "type": "Technical content", + "status": "Done", + "target": "7", + "limit": "23", + "reviewer": "Assign reviewer" + }, + { + "id": 10, + "header": "Advanced Algorithms and Machine Learning", + "type": "Narrative", + "status": "Done", + "target": "30", + "limit": "28", + "reviewer": "Assign reviewer" + }, + { + "id": 11, + "header": "Adaptive Communication Protocols", + "type": "Narrative", + "status": "Done", + "target": "9", + "limit": "31", + "reviewer": "Assign reviewer" + }, + { + "id": 12, + "header": "Advantages Over Current Technologies", + "type": "Narrative", + "status": "Done", + "target": "12", + "limit": "0", + "reviewer": "Assign reviewer" + }, + { + "id": 13, + "header": "Past Performance", + "type": "Narrative", + "status": "Done", + "target": "22", + "limit": "33", + "reviewer": "Assign reviewer" + }, + { + "id": 14, + "header": "Customer Feedback and Satisfaction Levels", + "type": "Narrative", + "status": "Done", + "target": "15", + "limit": "34", + "reviewer": "Assign reviewer" + }, + { + "id": 15, + "header": "Implementation Challenges and Solutions", + "type": "Narrative", + "status": "Done", + "target": "3", + "limit": "35", + "reviewer": "Assign reviewer" + }, + { + "id": 16, + "header": "Security Measures and Data Protection Policies", + "type": "Narrative", + "status": "In Process", + "target": "6", + "limit": "36", + "reviewer": "Assign reviewer" + }, + { + "id": 17, + "header": "Scalability and Future Proofing", + "type": "Narrative", + "status": "Done", + "target": "4", + "limit": "37", + "reviewer": "Assign reviewer" + }, + { + "id": 18, + "header": "Cost-Benefit Analysis", + "type": "Plain language", + "status": "Done", + "target": "14", + "limit": "38", + "reviewer": "Assign reviewer" + }, + { + "id": 19, + "header": "User Training and Onboarding Experience", + "type": "Narrative", + "status": "Done", + "target": "17", + "limit": "39", + "reviewer": "Assign reviewer" + }, + { + "id": 20, + "header": "Future Development Roadmap", + "type": "Narrative", + "status": "Done", + "target": "11", + "limit": "40", + "reviewer": "Assign reviewer" + }, + { + "id": 21, + "header": "System Architecture Overview", + "type": "Technical content", + "status": "In Process", + "target": "24", + "limit": "18", + "reviewer": "Maya Johnson" + }, + { + "id": 22, + "header": "Risk Management Plan", + "type": "Narrative", + "status": "Done", + "target": "15", + "limit": "22", + "reviewer": "Carlos Rodriguez" + }, + { + "id": 23, + "header": "Compliance Documentation", + "type": "Legal", + "status": "In Process", + "target": "31", + "limit": "27", + "reviewer": "Sarah Chen" + }, + { + "id": 24, + "header": "API Documentation", + "type": "Technical content", + "status": "Done", + "target": "8", + "limit": "12", + "reviewer": "Raj Patel" + }, + { + "id": 25, + "header": "User Interface Mockups", + "type": "Visual", + "status": "In Process", + "target": "19", + "limit": "25", + "reviewer": "Leila Ahmadi" + }, + { + "id": 26, + "header": "Database Schema", + "type": "Technical content", + "status": "Done", + "target": "22", + "limit": "20", + "reviewer": "Thomas Wilson" + }, + { + "id": 27, + "header": "Testing Methodology", + "type": "Technical content", + "status": "In Process", + "target": "17", + "limit": "14", + "reviewer": "Assign reviewer" + }, + { + "id": 28, + "header": "Deployment Strategy", + "type": "Narrative", + "status": "Done", + "target": "26", + "limit": "30", + "reviewer": "Eddie Lake" + }, + { + "id": 29, + "header": "Budget Breakdown", + "type": "Financial", + "status": "In Process", + "target": "13", + "limit": "16", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 30, + "header": "Market Analysis", + "type": "Research", + "status": "Done", + "target": "29", + "limit": "32", + "reviewer": "Sophia Martinez" + }, + { + "id": 31, + "header": "Competitor Comparison", + "type": "Research", + "status": "In Process", + "target": "21", + "limit": "19", + "reviewer": "Assign reviewer" + }, + { + "id": 32, + "header": "Maintenance Plan", + "type": "Technical content", + "status": "Done", + "target": "16", + "limit": "23", + "reviewer": "Alex Thompson" + }, + { + "id": 33, + "header": "User Personas", + "type": "Research", + "status": "In Process", + "target": "27", + "limit": "24", + "reviewer": "Nina Patel" + }, + { + "id": 34, + "header": "Accessibility Compliance", + "type": "Legal", + "status": "Done", + "target": "18", + "limit": "21", + "reviewer": "Assign reviewer" + }, + { + "id": 35, + "header": "Performance Metrics", + "type": "Technical content", + "status": "In Process", + "target": "23", + "limit": "26", + "reviewer": "David Kim" + }, + { + "id": 36, + "header": "Disaster Recovery Plan", + "type": "Technical content", + "status": "Done", + "target": "14", + "limit": "17", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 37, + "header": "Third-party Integrations", + "type": "Technical content", + "status": "In Process", + "target": "25", + "limit": "28", + "reviewer": "Eddie Lake" + }, + { + "id": 38, + "header": "User Feedback Summary", + "type": "Research", + "status": "Done", + "target": "20", + "limit": "15", + "reviewer": "Assign reviewer" + }, + { + "id": 39, + "header": "Localization Strategy", + "type": "Narrative", + "status": "In Process", + "target": "12", + "limit": "19", + "reviewer": "Maria Garcia" + }, + { + "id": 40, + "header": "Mobile Compatibility", + "type": "Technical content", + "status": "Done", + "target": "28", + "limit": "31", + "reviewer": "James Wilson" + }, + { + "id": 41, + "header": "Data Migration Plan", + "type": "Technical content", + "status": "In Process", + "target": "19", + "limit": "22", + "reviewer": "Assign reviewer" + }, + { + "id": 42, + "header": "Quality Assurance Protocols", + "type": "Technical content", + "status": "Done", + "target": "30", + "limit": "33", + "reviewer": "Priya Singh" + }, + { + "id": 43, + "header": "Stakeholder Analysis", + "type": "Research", + "status": "In Process", + "target": "11", + "limit": "14", + "reviewer": "Eddie Lake" + }, + { + "id": 44, + "header": "Environmental Impact Assessment", + "type": "Research", + "status": "Done", + "target": "24", + "limit": "27", + "reviewer": "Assign reviewer" + }, + { + "id": 45, + "header": "Intellectual Property Rights", + "type": "Legal", + "status": "In Process", + "target": "17", + "limit": "20", + "reviewer": "Sarah Johnson" + }, + { + "id": 46, + "header": "Customer Support Framework", + "type": "Narrative", + "status": "Done", + "target": "22", + "limit": "25", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 47, + "header": "Version Control Strategy", + "type": "Technical content", + "status": "In Process", + "target": "15", + "limit": "18", + "reviewer": "Assign reviewer" + }, + { + "id": 48, + "header": "Continuous Integration Pipeline", + "type": "Technical content", + "status": "Done", + "target": "26", + "limit": "29", + "reviewer": "Michael Chen" + }, + { + "id": 49, + "header": "Regulatory Compliance", + "type": "Legal", + "status": "In Process", + "target": "13", + "limit": "16", + "reviewer": "Assign reviewer" + }, + { + "id": 50, + "header": "User Authentication System", + "type": "Technical content", + "status": "Done", + "target": "28", + "limit": "31", + "reviewer": "Eddie Lake" + }, + { + "id": 51, + "header": "Data Analytics Framework", + "type": "Technical content", + "status": "In Process", + "target": "21", + "limit": "24", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 52, + "header": "Cloud Infrastructure", + "type": "Technical content", + "status": "Done", + "target": "16", + "limit": "19", + "reviewer": "Assign reviewer" + }, + { + "id": 53, + "header": "Network Security Measures", + "type": "Technical content", + "status": "In Process", + "target": "29", + "limit": "32", + "reviewer": "Lisa Wong" + }, + { + "id": 54, + "header": "Project Timeline", + "type": "Planning", + "status": "Done", + "target": "14", + "limit": "17", + "reviewer": "Eddie Lake" + }, + { + "id": 55, + "header": "Resource Allocation", + "type": "Planning", + "status": "In Process", + "target": "27", + "limit": "30", + "reviewer": "Assign reviewer" + }, + { + "id": 56, + "header": "Team Structure and Roles", + "type": "Planning", + "status": "Done", + "target": "20", + "limit": "23", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 57, + "header": "Communication Protocols", + "type": "Planning", + "status": "In Process", + "target": "15", + "limit": "18", + "reviewer": "Assign reviewer" + }, + { + "id": 58, + "header": "Success Metrics", + "type": "Planning", + "status": "Done", + "target": "30", + "limit": "33", + "reviewer": "Eddie Lake" + }, + { + "id": 59, + "header": "Internationalization Support", + "type": "Technical content", + "status": "In Process", + "target": "23", + "limit": "26", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 60, + "header": "Backup and Recovery Procedures", + "type": "Technical content", + "status": "Done", + "target": "18", + "limit": "21", + "reviewer": "Assign reviewer" + }, + { + "id": 61, + "header": "Monitoring and Alerting System", + "type": "Technical content", + "status": "In Process", + "target": "25", + "limit": "28", + "reviewer": "Daniel Park" + }, + { + "id": 62, + "header": "Code Review Guidelines", + "type": "Technical content", + "status": "Done", + "target": "12", + "limit": "15", + "reviewer": "Eddie Lake" + }, + { + "id": 63, + "header": "Documentation Standards", + "type": "Technical content", + "status": "In Process", + "target": "27", + "limit": "30", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 64, + "header": "Release Management Process", + "type": "Planning", + "status": "Done", + "target": "22", + "limit": "25", + "reviewer": "Assign reviewer" + }, + { + "id": 65, + "header": "Feature Prioritization Matrix", + "type": "Planning", + "status": "In Process", + "target": "19", + "limit": "22", + "reviewer": "Emma Davis" + }, + { + "id": 66, + "header": "Technical Debt Assessment", + "type": "Technical content", + "status": "Done", + "target": "24", + "limit": "27", + "reviewer": "Eddie Lake" + }, + { + "id": 67, + "header": "Capacity Planning", + "type": "Planning", + "status": "In Process", + "target": "21", + "limit": "24", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 68, + "header": "Service Level Agreements", + "type": "Legal", + "status": "Done", + "target": "26", + "limit": "29", + "reviewer": "Assign reviewer" + } +] diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx new file mode 100644 index 00000000..5147f346 --- /dev/null +++ b/frontend/app/dashboard/page.tsx @@ -0,0 +1,33 @@ +import { DashboardStatCards } from "@/components/dashboard/dashboard-stat-cards" +import { AssetTrendChart } from "@/components/dashboard/asset-trend-chart" +import { VulnSeverityChart } from "@/components/dashboard/vuln-severity-chart" +import { DashboardDataTable } from "@/components/dashboard/dashboard-data-table" + +/** + * 仪表板页面组件 + * 这是应用的主要仪表板页面,包含卡片、图表和数据表格 + * 布局结构已移至根布局组件中 + */ +export default function Page() { + return ( + // 内容区域,包含卡片、图表和数据表格 + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + {/* 顶部统计卡片 */} + <DashboardStatCards /> + + {/* 图表区域 - 趋势图 + 漏洞分布 */} + <div className="grid gap-4 px-4 lg:px-6 @xl/main:grid-cols-2"> + {/* 资产趋势折线图 */} + <AssetTrendChart /> + + {/* 漏洞严重程度分布 */} + <VulnSeverityChart /> + </div> + + {/* 漏洞 / 扫描历史 Tab */} + <div className="px-4 lg:px-6"> + <DashboardDataTable /> + </div> + </div> + ) +} diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/frontend/app/favicon.ico differ diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 00000000..fdfb503b --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,292 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "@xterm/xterm/css/xterm.css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --font-sans: Plus Jakarta Sans, sans-serif; + --font-mono: IBM Plex Mono, monospace; + --font-serif: Lora, serif; + --radius: 0.625rem; + --tracking-tighter: calc(var(--tracking-normal) - 0.05em); + --tracking-tight: calc(var(--tracking-normal) - 0.025em); + --tracking-wide: calc(var(--tracking-normal) + 0.025em); + --tracking-wider: calc(var(--tracking-normal) + 0.05em); + --tracking-widest: calc(var(--tracking-normal) + 0.1em); + --tracking-normal: var(--tracking-normal); + --shadow-2xl: var(--shadow-2xl); + --shadow-xl: var(--shadow-xl); + --shadow-lg: var(--shadow-lg); + --shadow-md: var(--shadow-md); + --shadow: var(--shadow); + --shadow-sm: var(--shadow-sm); + --shadow-xs: var(--shadow-xs); + --shadow-2xs: var(--shadow-2xs); + --spacing: var(--spacing); + --letter-spacing: var(--letter-spacing); + --shadow-offset-y: var(--shadow-offset-y); + --shadow-offset-x: var(--shadow-offset-x); + --shadow-spread: var(--shadow-spread); + --shadow-blur: var(--shadow-blur); + --shadow-opacity: var(--shadow-opacity); + --color-shadow-color: var(--shadow-color); + --color-destructive-foreground: var(--destructive-foreground); +} + +/* 基础主题 - Vercel 风格 (黑白灰) */ +:root { + --radius: 0.5rem; + --background: oklch(0.9900 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --popover: oklch(0.9900 0 0); + --popover-foreground: oklch(0 0 0); + --primary: oklch(0 0 0); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.9400 0 0); + --secondary-foreground: oklch(0 0 0); + --muted: oklch(0.9700 0 0); + --muted-foreground: oklch(0.4400 0 0); + --accent: oklch(0.9400 0 0); + --accent-foreground: oklch(0 0 0); + --destructive: oklch(0.6300 0.1900 23.0300); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.9200 0 0); + --input: oklch(0.9400 0 0); + --ring: oklch(0 0 0); + --chart-1: oklch(0.8100 0.1700 75.3500); + --chart-2: oklch(0.5500 0.2200 264.5300); + --chart-3: oklch(0.7200 0 0); + --chart-4: oklch(0.9200 0 0); + --chart-5: oklch(0.5600 0 0); + --sidebar: oklch(0.9900 0 0); + --sidebar-foreground: oklch(0 0 0); + --sidebar-primary: oklch(0 0 0); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.9400 0 0); + --sidebar-accent-foreground: oklch(0 0 0); + --sidebar-border: oklch(0.9400 0 0); + --sidebar-ring: oklch(0 0 0); + --font-sans: Geist, sans-serif; + --font-serif: Georgia, serif; + --font-mono: Geist Mono, monospace; + --shadow-color: hsl(0 0% 0%); + --shadow-opacity: 0.18; + --shadow-blur: 2px; + --shadow-spread: 0px; + --shadow-offset-x: 0px; + --shadow-offset-y: 1px; + --letter-spacing: 0em; + --spacing: 0.25rem; + --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18); + --shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18); + --shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18); + --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45); + --tracking-normal: 0em; +} + +/* 基础主题 - 暗色模式 (Vercel 风格) */ +.dark { + --background: oklch(0 0 0); + --foreground: oklch(1 0 0); + --card: oklch(0.1400 0 0); + --card-foreground: oklch(1 0 0); + --popover: oklch(0.1800 0 0); + --popover-foreground: oklch(1 0 0); + --primary: oklch(1 0 0); + --primary-foreground: oklch(0 0 0); + --secondary: oklch(0.2500 0 0); + --secondary-foreground: oklch(1 0 0); + --muted: oklch(0.2300 0 0); + --muted-foreground: oklch(0.7200 0 0); + --accent: oklch(0.3200 0 0); + --accent-foreground: oklch(1 0 0); + --destructive: oklch(0.6900 0.2000 23.9100); + --destructive-foreground: oklch(0 0 0); + --border: oklch(0.2600 0 0); + --input: oklch(0.3200 0 0); + --ring: oklch(0.7200 0 0); + --chart-1: oklch(0.8100 0.1700 75.3500); + --chart-2: oklch(0.5800 0.2100 260.8400); + --chart-3: oklch(0.5600 0 0); + --chart-4: oklch(0.4400 0 0); + --chart-5: oklch(0.9200 0 0); + --sidebar: oklch(0.1800 0 0); + --sidebar-foreground: oklch(1 0 0); + --sidebar-primary: oklch(1 0 0); + --sidebar-primary-foreground: oklch(0 0 0); + --sidebar-accent: oklch(0.3200 0 0); + --sidebar-accent-foreground: oklch(1 0 0); + --sidebar-border: oklch(0.3200 0 0); + --sidebar-ring: oklch(0.7200 0 0); +} + +@layer base { + + html, + body { + height: 100%; + } + + * { + @apply border-border outline-ring/50; + } + + body { + @apply bg-background text-foreground; + /* 禁止页面级滚动,滚动交给主内容容器 */ + overflow: hidden; + letter-spacing: var(--tracking-normal); + } + + /* 全局滚动条样式 - Webkit 浏览器 (Chrome, Safari, Edge) */ + ::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + ::-webkit-scrollbar-track { + background: transparent; + } + + ::-webkit-scrollbar-thumb { + background: oklch(0.708 0 0 / 0.3); + border-radius: 5px; + border: 2px solid transparent; + background-clip: padding-box; + } + + ::-webkit-scrollbar-thumb:hover { + background: oklch(0.708 0 0 / 0.5); + border-radius: 5px; + border: 2px solid transparent; + background-clip: padding-box; + } + + /* 暗色主题下的滚动条 */ + .dark ::-webkit-scrollbar-thumb { + background: oklch(0.556 0 0 / 0.4); + border-radius: 5px; + border: 2px solid transparent; + background-clip: padding-box; + } + + .dark ::-webkit-scrollbar-thumb:hover { + background: oklch(0.556 0 0 / 0.6); + border-radius: 5px; + border: 2px solid transparent; + background-clip: padding-box; + } + + /* Firefox 滚动条样式 */ + * { + scrollbar-width: thin; + scrollbar-color: oklch(0.708 0 0 / 0.3) transparent; + } + + .dark * { + scrollbar-color: oklch(0.556 0 0 / 0.4) transparent; + } + + /* 隐藏滚动条但保持可滚动 */ + .scrollbar-hide { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ + } + + .scrollbar-hide::-webkit-scrollbar { + display: none; + /* Chrome, Safari and Opera */ + } + +} + +/* 通知铃铛摇晃动画 */ +@keyframes wiggle { + 0%, 100% { + transform: rotate(0deg); + } + 15% { + transform: rotate(15deg); + } + 30% { + transform: rotate(-12deg); + } + 45% { + transform: rotate(8deg); + } + 60% { + transform: rotate(-5deg); + } + 75% { + transform: rotate(2deg); + } +} + +/* 进度条条纹动画 */ +@keyframes progress-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } +} + +.progress-striped { + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-size: 1rem 1rem; + animation: progress-stripes 1s linear infinite; +} \ No newline at end of file diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 00000000..98607374 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,86 @@ +import type React from "react" +// 导入 Next.js 的元数据类型定义 +import type { Metadata } from "next" + +// 导入全局样式文件 +import "./globals.css" +// 导入颜色主题 +import "@/styles/themes/bubblegum.css" +import "@/styles/themes/quantum-rose.css" +import "@/styles/themes/clean-slate.css" +import "@/styles/themes/cosmic-night.css" +import "@/styles/themes/vercel.css" +import "@/styles/themes/candyland.css" +import "@/styles/themes/violet-bloom.css" +import { Suspense } from "react" +import Script from "next/script" +import { QueryProvider } from "@/components/providers/query-provider" +import { ThemeProvider } from "@/components/providers/theme-provider" +// Google Fonts 在中国大陆无法访问,直接使用 fallback 字体 + +// 导入公共布局组件 +import { RoutePrefetch } from "@/components/route-prefetch" +import { RouteProgress } from "@/components/route-progress" +import { AuthLayout } from "@/components/auth/auth-layout" + +// 定义页面的元数据信息,用于 SEO 优化 +export const metadata: Metadata = { + title: "XingRin - 星环", // 页面标题 + description: "XingRin - 星环", // 页面描述 + generator: "XingRin", // 生成器标识 +} + +// 使用原有的 fallback 字体栈,不依赖 Google Fonts +const fontConfig = { + className: "font-sans", + style: { + fontFamily: "system-ui, -apple-system, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif" + } +} + +/** + * 根布局组件 + * 这是整个应用的最外层布局,所有页面都会被包裹在这个组件中 + * @param children - 子组件内容,即各个页面的实际内容 + */ +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + // 设置 HTML 根元素,语言为中文 + // suppressHydrationWarning 避免主题切换时的 hydration 警告 + <html lang="zh-CN" suppressHydrationWarning> + <body className={fontConfig.className} style={fontConfig.style}> + {/* 加载外部脚本 - 使用 beforeInteractive 策略确保在页面交互前加载 */} + <Script + src="https://tweakcn.com/live-preview.min.js" + strategy="beforeInteractive" + crossOrigin="anonymous" + /> + {/* 路由加载进度条 - 放在最外层 */} + <Suspense fallback={null}> + <RouteProgress /> + </Suspense> + {/* ThemeProvider 提供主题切换功能,跟随系统自动切换亮暗色 */} + <ThemeProvider + attribute="class" + defaultTheme="system" + enableSystem + disableTransitionOnChange + > + {/* 使用 QueryProvider 提供 React Query 功能 */} + <QueryProvider> + {/* 路由预加载:在后台预加载常用页面的 JS/CSS 资源 */} + <RoutePrefetch /> + {/* AuthLayout 处理认证和侧边栏显示 */} + <AuthLayout> + {children} + </AuthLayout> + </QueryProvider> + </ThemeProvider> + </body> + </html> + ) +} diff --git a/frontend/app/login/layout.tsx b/frontend/app/login/layout.tsx new file mode 100644 index 00000000..50a55ad4 --- /dev/null +++ b/frontend/app/login/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: "登录 - XingRin - 星环", + description: "登录到 XingRin - 星环", +} + +/** + * 登录页面布局 + * 不包含侧边栏和头部 + */ +export default function LoginLayout({ + children, +}: { + children: React.ReactNode +}) { + return children +} diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx new file mode 100644 index 00000000..b3e6a327 --- /dev/null +++ b/frontend/app/login/page.tsx @@ -0,0 +1,131 @@ +"use client" + +import React from "react" +import { useRouter } from "next/navigation" +import Lottie from "lottie-react" +import securityAnimation from "@/public/animations/Security000-Purple.json" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Card, CardContent } from "@/components/ui/card" +import { + Field, + FieldGroup, + FieldLabel, +} from "@/components/ui/field" +import { Spinner } from "@/components/ui/spinner" +import { useLogin, useAuth } from "@/hooks/use-auth" +import { useRoutePrefetch } from "@/hooks/use-route-prefetch" + +export default function LoginPage() { + // 在登录页面预加载所有页面组件 + useRoutePrefetch() + const router = useRouter() + const { data: auth, isLoading: authLoading } = useAuth() + const { mutate: login, isPending } = useLogin() + + const [username, setUsername] = React.useState("") + const [password, setPassword] = React.useState("") + + // 如果已登录,跳转到 dashboard + React.useEffect(() => { + if (auth?.authenticated) { + router.push("/dashboard/") + } + }, [auth, router]) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + login({ username, password }) + } + + // 加载中显示 spinner + if (authLoading) { + return ( + <div + className="flex min-h-svh w-full flex-col items-center justify-center gap-4" + style={{ + backgroundColor: '#DFDBE5', + backgroundImage: `url("data:image/svg+xml,%3Csvg width='180' height='180' viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M82.42 180h-1.415L0 98.995v-2.827L6.167 90 0 83.833V81.004L81.005 0h2.827L90 6.167 96.167 0H98.996L180 81.005v2.827L173.833 90 180 96.167V98.996L98.995 180h-2.827L90 173.833 83.833 180H82.42zm0-1.414L1.413 97.58 8.994 90l-7.58-7.58L82.42 1.413 90 8.994l7.58-7.58 81.006 81.005-7.58 7.58 7.58 7.58-81.005 81.006-7.58-7.58-7.58 7.58zM175.196 0h-25.832c1.033 2.924 2.616 5.59 4.625 7.868C152.145 9.682 151 12.208 151 15c0 5.523 4.477 10 10 10 1.657 0 3 1.343 3 3v4h16V0h-4.803c.51.883.803 1.907.803 3 0 3.314-2.686 6-6 6s-6-2.686-6-6c0-1.093.292-2.117.803-3h10.394-13.685C161.18.938 161 1.948 161 3v4c-4.418 0-8 3.582-8 8s3.582 8 8 8c2.76 0 5 2.24 5 5v2h4v-4h2v4h4v-4h2v4h2V0h-4.803zm-15.783 0c-.27.954-.414 1.96-.414 3v2.2c-1.25.254-2.414.74-3.447 1.412-1.716-1.93-3.098-4.164-4.054-6.612h7.914zM180 17h-3l2.143-10H180v10zm-30.635 163c-.884-2.502-1.365-5.195-1.365-8 0-13.255 10.748-24 23.99-24H180v32h-30.635zm12.147 0c.5-1.416 1.345-2.67 2.434-3.66l-1.345-1.48c-1.498 1.364-2.62 3.136-3.186 5.14H151.5c-.97-2.48-1.5-5.177-1.5-8 0-12.15 9.84-22 22-22h8v30h-18.488zm13.685 0c-1.037-1.793-2.976-3-5.197-3-2.22 0-4.16 1.207-5.197 3h10.394zM0 148h8.01C21.26 148 32 158.742 32 172c0 2.805-.48 5.498-1.366 8H0v-32zm0 2h8c12.15 0 22 9.847 22 22 0 2.822-.53 5.52-1.5 8h-7.914c-.567-2.004-1.688-3.776-3.187-5.14l-1.346 1.48c1.09.99 1.933 2.244 2.434 3.66H0v-30zm15.197 30c-1.037-1.793-2.976-3-5.197-3-2.22 0-4.16 1.207-5.197 3h10.394zM0 32h16v-4c0-1.657 1.343-3 3-3 5.523 0 10-4.477 10-10 0-2.794-1.145-5.32-2.992-7.134C28.018 5.586 29.6 2.924 30.634 0H0v32zm0-2h2v-4h2v4h4v-4h2v4h4v-2c0-2.76 2.24-5 5-5 4.418 0 8-3.582 8-8s-3.582-8-8-8V3c0-1.052-.18-2.062-.512-3H0v30zM28.5 0c-.954 2.448-2.335 4.683-4.05 6.613-1.035-.672-2.2-1.16-3.45-1.413V3c0-1.04-.144-2.046-.414-3H28.5zM0 17h3L.857 7H0v10zM15.197 0c.51.883.803 1.907.803 3 0 3.314-2.686 6-6 6S4 6.314 4 3c0-1.093.292-2.117.803-3h10.394zM109 115c-1.657 0-3 1.343-3 3v4H74v-4c0-1.657-1.343-3-3-3-5.523 0-10-4.477-10-10 0-2.793 1.145-5.318 2.99-7.132C60.262 93.638 58 88.084 58 82c0-13.255 10.748-24 23.99-24h16.02C111.26 58 122 68.742 122 82c0 6.082-2.263 11.636-5.992 15.866C117.855 99.68 119 102.206 119 105c0 5.523-4.477 10-10 10zm0-2c-2.76 0-5 2.24-5 5v2h-4v-4h-2v4h-4v-4h-2v4h-4v-4h-2v4h-4v-4h-2v4h-4v-2c0-2.76-2.24-5-5-5-4.418 0-8-3.582-8-8s3.582-8 8-8v-4c0-2.64 1.136-5.013 2.946-6.66L72.6 84.86C70.39 86.874 69 89.775 69 93v2.2c-1.25.254-2.414.74-3.447 1.412C62.098 92.727 60 87.61 60 82c0-12.15 9.84-22 22-22h16c12.15 0 22 9.847 22 22 0 5.61-2.097 10.728-5.55 14.613-1.035-.672-2.2-1.16-3.45-1.413V93c0-3.226-1.39-6.127-3.6-8.14l-1.346 1.48C107.864 87.987 109 90.36 109 93v4c4.418 0 8 3.582 8 8s-3.582 8-8 8zM90.857 97L93 107h-6l2.143-10h1.714zM80 99c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zm20 0c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6z' fill='%239C92AC' fill-opacity='0.28' fill-rule='evenodd'/%3E%3C/svg%3E")` + }} + > + <Spinner className="size-8 text-primary" /> + <p className="text-muted-foreground text-sm" suppressHydrationWarning>loading...</p> + </div> + ) + } + + // 已登录不显示登录页 + if (auth?.authenticated) { + return null + } + + return ( + <div + className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10" + style={{ + backgroundColor: '#DFDBE5', + backgroundImage: `url("data:image/svg+xml,%3Csvg width='180' height='180' viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M82.42 180h-1.415L0 98.995v-2.827L6.167 90 0 83.833V81.004L81.005 0h2.827L90 6.167 96.167 0H98.996L180 81.005v2.827L173.833 90 180 96.167V98.996L98.995 180h-2.827L90 173.833 83.833 180H82.42zm0-1.414L1.413 97.58 8.994 90l-7.58-7.58L82.42 1.413 90 8.994l7.58-7.58 81.006 81.005-7.58 7.58 7.58 7.58-81.005 81.006-7.58-7.58-7.58 7.58zM175.196 0h-25.832c1.033 2.924 2.616 5.59 4.625 7.868C152.145 9.682 151 12.208 151 15c0 5.523 4.477 10 10 10 1.657 0 3 1.343 3 3v4h16V0h-4.803c.51.883.803 1.907.803 3 0 3.314-2.686 6-6 6s-6-2.686-6-6c0-1.093.292-2.117.803-3h10.394-13.685C161.18.938 161 1.948 161 3v4c-4.418 0-8 3.582-8 8s3.582 8 8 8c2.76 0 5 2.24 5 5v2h4v-4h2v4h4v-4h2v4h2V0h-4.803zm-15.783 0c-.27.954-.414 1.96-.414 3v2.2c-1.25.254-2.414.74-3.447 1.412-1.716-1.93-3.098-4.164-4.054-6.612h7.914zM180 17h-3l2.143-10H180v10zm-30.635 163c-.884-2.502-1.365-5.195-1.365-8 0-13.255 10.748-24 23.99-24H180v32h-30.635zm12.147 0c.5-1.416 1.345-2.67 2.434-3.66l-1.345-1.48c-1.498 1.364-2.62 3.136-3.186 5.14H151.5c-.97-2.48-1.5-5.177-1.5-8 0-12.15 9.84-22 22-22h8v30h-18.488zm13.685 0c-1.037-1.793-2.976-3-5.197-3-2.22 0-4.16 1.207-5.197 3h10.394zM0 148h8.01C21.26 148 32 158.742 32 172c0 2.805-.48 5.498-1.366 8H0v-32zm0 2h8c12.15 0 22 9.847 22 22 0 2.822-.53 5.52-1.5 8h-7.914c-.567-2.004-1.688-3.776-3.187-5.14l-1.346 1.48c1.09.99 1.933 2.244 2.434 3.66H0v-30zm15.197 30c-1.037-1.793-2.976-3-5.197-3-2.22 0-4.16 1.207-5.197 3h10.394zM0 32h16v-4c0-1.657 1.343-3 3-3 5.523 0 10-4.477 10-10 0-2.794-1.145-5.32-2.992-7.134C28.018 5.586 29.6 2.924 30.634 0H0v32zm0-2h2v-4h2v4h4v-4h2v4h4v-2c0-2.76 2.24-5 5-5 4.418 0 8-3.582 8-8s-3.582-8-8-8V3c0-1.052-.18-2.062-.512-3H0v30zM28.5 0c-.954 2.448-2.335 4.683-4.05 6.613-1.035-.672-2.2-1.16-3.45-1.413V3c0-1.04-.144-2.046-.414-3H28.5zM0 17h3L.857 7H0v10zM15.197 0c.51.883.803 1.907.803 3 0 3.314-2.686 6-6 6S4 6.314 4 3c0-1.093.292-2.117.803-3h10.394zM109 115c-1.657 0-3 1.343-3 3v4H74v-4c0-1.657-1.343-3-3-3-5.523 0-10-4.477-10-10 0-2.793 1.145-5.318 2.99-7.132C60.262 93.638 58 88.084 58 82c0-13.255 10.748-24 23.99-24h16.02C111.26 58 122 68.742 122 82c0 6.082-2.263 11.636-5.992 15.866C117.855 99.68 119 102.206 119 105c0 5.523-4.477 10-10 10zm0-2c-2.76 0-5 2.24-5 5v2h-4v-4h-2v4h-4v-4h-2v4h-4v-4h-2v4h-4v-4h-2v4h-4v-2c0-2.76-2.24-5-5-5-4.418 0-8-3.582-8-8s3.582-8 8-8v-4c0-2.64 1.136-5.013 2.946-6.66L72.6 84.86C70.39 86.874 69 89.775 69 93v2.2c-1.25.254-2.414.74-3.447 1.412C62.098 92.727 60 87.61 60 82c0-12.15 9.84-22 22-22h16c12.15 0 22 9.847 22 22 0 5.61-2.097 10.728-5.55 14.613-1.035-.672-2.2-1.16-3.45-1.413V93c0-3.226-1.39-6.127-3.6-8.14l-1.346 1.48C107.864 87.987 109 90.36 109 93v4c4.418 0 8 3.582 8 8s-3.582 8-8 8zM90.857 97L93 107h-6l2.143-10h1.714zM80 99c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zm20 0c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6z' fill='%239C92AC' fill-opacity='0.28' fill-rule='evenodd'/%3E%3C/svg%3E")` + }} + > + <div className="w-full max-w-sm md:max-w-4xl"> + <div className="flex flex-col gap-6"> + <Card className="overflow-hidden p-0"> + <CardContent className="grid p-0 md:grid-cols-2"> + <form className="p-6 md:p-8" onSubmit={handleSubmit}> + <FieldGroup> + <div className="flex flex-col items-center gap-2 text-center"> + <h1 className="text-2xl font-bold">XingRin - 星环</h1> + <p className="text-sm text-muted-foreground mt-1"> + 一站式安全扫描平台 + </p> + </div> + <Field> + <FieldLabel htmlFor="username">用户名</FieldLabel> + <Input + id="username" + type="text" + placeholder="请输入账户名" + value={username} + onChange={(e) => setUsername(e.target.value)} + required + autoFocus + /> + </Field> + <Field> + <FieldLabel htmlFor="password">密码</FieldLabel> + <Input + id="password" + type="password" + placeholder="请输入密码" + value={password} + onChange={(e) => setPassword(e.target.value)} + required + /> + </Field> + <Field> + <Button type="submit" className="w-full" disabled={isPending}> + {isPending ? "登录中..." : "登录"} + </Button> + </Field> + </FieldGroup> + </form> + <div className="bg-primary/5 relative hidden md:flex md:items-center md:justify-center"> + <div className="text-center p-4"> + <Lottie + animationData={securityAnimation} + loop={true} + className="w-96 h-96 mx-auto" + /> + {/* <h2 className="text-xl font-semibold text-primary/60 mt-2">安全扫描平台</h2> + <p className="text-sm text-muted-foreground mt-1"> + Web 应用漏洞扫描与资产管理 + </p> */} + </div> + </div> + </CardContent> + </Card> + </div> + </div> + </div> + ) +} diff --git a/frontend/app/organization/[id]/page.tsx b/frontend/app/organization/[id]/page.tsx new file mode 100644 index 00000000..3c9d50fd --- /dev/null +++ b/frontend/app/organization/[id]/page.tsx @@ -0,0 +1,22 @@ +"use client" + +import React from "react" +import { OrganizationDetailView } from "@/components/organization/organization-detail-view" + +/** + * 组织详情页面 + * 显示组织的统计信息和资产列表 + */ +export default function OrganizationDetailPage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const resolvedParams = React.use(params) + + return ( + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + <OrganizationDetailView organizationId={resolvedParams.id} /> + </div> + ) +} diff --git a/frontend/app/organization/page.tsx b/frontend/app/organization/page.tsx new file mode 100644 index 00000000..5c8e44b6 --- /dev/null +++ b/frontend/app/organization/page.tsx @@ -0,0 +1,33 @@ +// 导入组织管理组件 +import { OrganizationList } from "@/components/organization/organization-list" +// 导入图标 +import { Building2 } from "lucide-react" + +/** + * 组织管理页面 + * 资产管理下的组织管理子页面,显示组织列表和相关操作 + */ +export default function OrganizationPage() { + return ( + // 内容区域,包含组织管理功能 + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + {/* 页面头部 */} + <div className="flex items-center justify-between px-4 lg:px-6"> + <div> + <h2 className="text-2xl font-bold tracking-tight flex items-center gap-2"> + <Building2 /> + 组织 + </h2> + <p className="text-muted-foreground"> + 管理和查看系统中的所有组织信息 + </p> + </div> + </div> + + {/* 组织列表组件 */} + <div className="px-4 lg:px-6"> + <OrganizationList /> + </div> + </div> + ) +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 00000000..37dbe60e --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from 'next/navigation'; + +export default function Home() { + // 直接重定向到仪表板页面 + redirect('/dashboard'); +} diff --git a/frontend/app/scan/engine/page.tsx b/frontend/app/scan/engine/page.tsx new file mode 100644 index 00000000..a02e3be7 --- /dev/null +++ b/frontend/app/scan/engine/page.tsx @@ -0,0 +1,379 @@ +"use client" + +import React, { useState, useMemo } from "react" +import { Settings, Search, Pencil, Trash2, Check, X, Plus } from "lucide-react" +import * as yaml from "js-yaml" +import Editor from "@monaco-editor/react" +import { useTheme } from "next-themes" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { EngineEditDialog, EngineCreateDialog } from "@/components/scan/engine" +import { useEngines, useCreateEngine, useUpdateEngine, useDeleteEngine } from "@/hooks/use-engines" +import { cn } from "@/lib/utils" +import type { ScanEngine } from "@/types/engine.types" +import { MasterDetailSkeleton } from "@/components/ui/master-detail-skeleton" + +/** 功能配置项定义 - 与 YAML 配置结构对应 */ +const FEATURE_LIST = [ + { key: "subdomain_discovery", label: "子域名发现" }, + { key: "port_scan", label: "端口扫描" }, + { key: "site_scan", label: "站点扫描" }, + { key: "directory_scan", label: "目录扫描" }, + { key: "url_fetch", label: "URL 抓取" }, + { key: "vuln_scan", label: "漏洞扫描" }, +] as const + +type FeatureKey = typeof FEATURE_LIST[number]["key"] + +/** 解析引擎配置获取启用的功能 */ +function parseEngineFeatures(engine: ScanEngine): Record<FeatureKey, boolean> { + const defaultFeatures: Record<FeatureKey, boolean> = { + subdomain_discovery: false, + port_scan: false, + site_scan: false, + directory_scan: false, + url_fetch: false, + vuln_scan: false, + } + + if (!engine.configuration) return defaultFeatures + + try { + const config = yaml.load(engine.configuration) as Record<string, unknown> + if (!config) return defaultFeatures + + return { + subdomain_discovery: !!config.subdomain_discovery, + port_scan: !!config.port_scan, + site_scan: !!config.site_scan, + directory_scan: !!config.directory_scan, + url_fetch: !!config.url_fetch, + vuln_scan: !!config.vuln_scan, + } + } catch { + return defaultFeatures + } +} + +/** 计算启用的功能数量 */ +function countEnabledFeatures(engine: ScanEngine) { + const features = parseEngineFeatures(engine) + return Object.values(features).filter(Boolean).length +} + +/** + * 扫描引擎页面 + */ +export default function ScanEnginePage() { + const [selectedId, setSelectedId] = useState<number | null>(null) + const [searchQuery, setSearchQuery] = useState("") + const [editingEngine, setEditingEngine] = useState<ScanEngine | null>(null) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [engineToDelete, setEngineToDelete] = useState<ScanEngine | null>(null) + + const { theme } = useTheme() + + // API Hooks + const { data: engines = [], isLoading } = useEngines() + const createEngineMutation = useCreateEngine() + const updateEngineMutation = useUpdateEngine() + const deleteEngineMutation = useDeleteEngine() + + // 过滤引擎列表 + const filteredEngines = useMemo(() => { + if (!searchQuery.trim()) return engines + const query = searchQuery.toLowerCase() + return engines.filter((e) => e.name.toLowerCase().includes(query)) + }, [engines, searchQuery]) + + // 选中的引擎 + const selectedEngine = useMemo(() => { + if (!selectedId) return null + return engines.find((e) => e.id === selectedId) || null + }, [selectedId, engines]) + + // 选中引擎的功能状态 + const selectedFeatures = useMemo(() => { + if (!selectedEngine) return null + return parseEngineFeatures(selectedEngine) + }, [selectedEngine]) + + const handleEdit = (engine: ScanEngine) => { + setEditingEngine(engine) + setIsEditDialogOpen(true) + } + + const handleSaveYaml = async (engineId: number, yamlContent: string) => { + await updateEngineMutation.mutateAsync({ + id: engineId, + data: { configuration: yamlContent }, + }) + } + + const handleDelete = (engine: ScanEngine) => { + setEngineToDelete(engine) + setDeleteDialogOpen(true) + } + + const confirmDelete = () => { + if (!engineToDelete) return + deleteEngineMutation.mutate(engineToDelete.id, { + onSuccess: () => { + if (selectedId === engineToDelete.id) { + setSelectedId(null) + } + setDeleteDialogOpen(false) + setEngineToDelete(null) + }, + }) + } + + const handleCreateEngine = async (name: string, yamlContent: string) => { + await createEngineMutation.mutateAsync({ + name, + configuration: yamlContent, + }) + } + + // 加载状态 + if (isLoading) { + return <MasterDetailSkeleton title="扫描引擎" listItemCount={4} /> + } + + return ( + <div className="flex flex-col h-full"> + {/* 顶部:标题 + 搜索 + 新建按钮 */} + <div className="flex items-center justify-between gap-4 px-4 py-4 lg:px-6"> + <h1 className="text-2xl font-bold shrink-0">扫描引擎</h1> + <div className="flex items-center gap-2 flex-1 max-w-md"> + <div className="relative flex-1"> + <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> + <Input + placeholder="搜索引擎..." + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + className="pl-8" + /> + </div> + </div> + <Button onClick={() => setIsCreateDialogOpen(true)}> + <Plus className="h-4 w-4 mr-1" /> + 新建引擎 + </Button> + </div> + + <Separator /> + + {/* 主体:左侧列表 + 右侧详情 */} + <div className="flex flex-1 min-h-0"> + {/* 左侧:引擎列表 */} + <div className="w-72 lg:w-80 border-r flex flex-col"> + <div className="px-4 py-3 border-b"> + <h2 className="text-sm font-medium text-muted-foreground"> + 引擎列表 ({filteredEngines.length}) + </h2> + </div> + <ScrollArea className="flex-1"> + {isLoading ? ( + <div className="p-4 text-sm text-muted-foreground">加载中...</div> + ) : filteredEngines.length === 0 ? ( + <div className="p-4 text-sm text-muted-foreground"> + {searchQuery ? "未找到匹配的引擎" : "暂无引擎,请先新建"} + </div> + ) : ( + <div className="p-2"> + {filteredEngines.map((engine) => ( + <button + key={engine.id} + onClick={() => setSelectedId(engine.id)} + className={cn( + "w-full text-left rounded-lg px-3 py-2.5 transition-colors", + selectedId === engine.id + ? "bg-primary/10 text-primary" + : "hover:bg-muted" + )} + > + <div className="font-medium text-sm truncate"> + {engine.name} + </div> + <div className="text-xs text-muted-foreground mt-0.5"> + {countEnabledFeatures(engine)} 个功能已启用 + </div> + </button> + ))} + </div> + )} + </ScrollArea> + </div> + + {/* 右侧:引擎详情 */} + <div className="flex-1 flex flex-col min-w-0"> + {selectedEngine && selectedFeatures ? ( + <> + {/* 详情头部 */} + <div className="px-6 py-4 border-b"> + <div className="flex items-start gap-3"> + <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 shrink-0"> + <Settings className="h-5 w-5 text-primary" /> + </div> + <div className="min-w-0 flex-1"> + <h2 className="text-lg font-semibold truncate"> + {selectedEngine.name} + </h2> + <p className="text-sm text-muted-foreground mt-0.5"> + 更新于 {new Date(selectedEngine.updatedAt).toLocaleString("zh-CN")} + </p> + </div> + <Badge variant="outline"> + {countEnabledFeatures(selectedEngine)} 个功能 + </Badge> + </div> + </div> + + {/* 详情内容 */} + <div className="flex-1 flex flex-col min-h-0 p-6 gap-6"> + {/* 功能状态 */} + <div className="shrink-0"> + <h3 className="text-sm font-medium mb-3">已启用功能</h3> + <div className="rounded-lg border"> + <div className="grid grid-cols-3 gap-px bg-muted"> + {FEATURE_LIST.map((feature) => { + const enabled = selectedFeatures[feature.key as keyof typeof selectedFeatures] + return ( + <div + key={feature.key} + className={cn( + "flex items-center gap-2 px-3 py-2.5 bg-background", + enabled ? "text-foreground" : "text-muted-foreground" + )} + > + {enabled ? ( + <Check className="h-4 w-4 text-green-600 shrink-0" /> + ) : ( + <X className="h-4 w-4 text-muted-foreground/50 shrink-0" /> + )} + <span className="text-sm truncate">{feature.label}</span> + </div> + ) + })} + </div> + </div> + </div> + + {/* 配置预览 */} + {selectedEngine.configuration && ( + <div className="flex-1 flex flex-col min-h-0"> + <h3 className="text-sm font-medium mb-3 shrink-0">配置预览</h3> + <div className="flex-1 rounded-lg border overflow-hidden min-h-0"> + <Editor + height="100%" + defaultLanguage="yaml" + value={selectedEngine.configuration} + options={{ + readOnly: true, + minimap: { enabled: false }, + fontSize: 12, + lineNumbers: "off", + scrollBeyondLastLine: false, + automaticLayout: true, + folding: true, + wordWrap: "on", + padding: { top: 12, bottom: 12 }, + }} + theme={theme === "dark" ? "vs-dark" : "light"} + /> + </div> + </div> + )} + </div> + + {/* 操作按钮 */} + <div className="px-6 py-4 border-t flex items-center gap-2"> + <Button + variant="outline" + size="sm" + onClick={() => handleEdit(selectedEngine)} + > + <Pencil className="h-4 w-4 mr-1.5" /> + 编辑配置 + </Button> + <div className="flex-1" /> + <Button + variant="outline" + size="sm" + className="text-destructive hover:text-destructive" + onClick={() => handleDelete(selectedEngine)} + disabled={deleteEngineMutation.isPending} + > + <Trash2 className="h-4 w-4 mr-1.5" /> + 删除 + </Button> + </div> + </> + ) : ( + // 未选中状态 + <div className="flex-1 flex items-center justify-center"> + <div className="text-center text-muted-foreground"> + <Settings className="h-12 w-12 mx-auto mb-3 opacity-50" /> + <p className="text-sm">选择左侧引擎查看详情</p> + </div> + </div> + )} + </div> + </div> + + {/* 编辑引擎弹窗 */} + <EngineEditDialog + engine={editingEngine} + open={isEditDialogOpen} + onOpenChange={setIsEditDialogOpen} + onSave={handleSaveYaml} + /> + + {/* 新建引擎弹窗 */} + <EngineCreateDialog + open={isCreateDialogOpen} + onOpenChange={setIsCreateDialogOpen} + onSave={handleCreateEngine} + /> + + {/* 删除确认弹窗 */} + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除</AlertDialogTitle> + <AlertDialogDescription> + 确定要删除引擎「{engineToDelete?.name}」吗?此操作无法撤销。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + disabled={deleteEngineMutation.isPending} + > + {deleteEngineMutation.isPending ? "删除中..." : "删除"} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </div> + ) +} + diff --git a/frontend/app/scan/history/[id]/directories/page.tsx b/frontend/app/scan/history/[id]/directories/page.tsx new file mode 100644 index 00000000..f573f403 --- /dev/null +++ b/frontend/app/scan/history/[id]/directories/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import { useParams } from "next/navigation" +import { DirectoriesView } from "@/components/directories/directories-view" + +export default function ScanDirectoriesPage() { + const { id } = useParams<{ id: string }>() + const scanId = Number(id) + + return ( + <div className="px-4 lg:px-6"> + <DirectoriesView scanId={scanId} /> + </div> + ) +} diff --git a/frontend/app/scan/history/[id]/endpoints/page.tsx b/frontend/app/scan/history/[id]/endpoints/page.tsx new file mode 100644 index 00000000..3d191e29 --- /dev/null +++ b/frontend/app/scan/history/[id]/endpoints/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import React from "react" +import { useParams } from "next/navigation" +import { EndpointsDetailView } from "@/components/endpoints" + +export default function ScanHistoryEndpointsPage() { + const { id } = useParams<{ id: string }>() + + return ( + <div className="px-4 lg:px-6"> + <EndpointsDetailView scanId={parseInt(id)} /> + </div> + ) +} diff --git a/frontend/app/scan/history/[id]/ip-addresses/page.tsx b/frontend/app/scan/history/[id]/ip-addresses/page.tsx new file mode 100644 index 00000000..21af44f4 --- /dev/null +++ b/frontend/app/scan/history/[id]/ip-addresses/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import React from "react" +import { useParams } from "next/navigation" +import { IPAddressesView } from "@/components/ip-addresses" + +export default function ScanHistoryIPsPage() { + const { id } = useParams<{ id: string }>() + + return ( + <div className="px-4 lg:px-6"> + <IPAddressesView scanId={Number(id)} /> + </div> + ) +} diff --git a/frontend/app/scan/history/[id]/layout.tsx b/frontend/app/scan/history/[id]/layout.tsx new file mode 100644 index 00000000..3b951c65 --- /dev/null +++ b/frontend/app/scan/history/[id]/layout.tsx @@ -0,0 +1,120 @@ +"use client" + +import React from "react" +import { usePathname, useParams } from "next/navigation" +import Link from "next/link" +import { Target } from "lucide-react" +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Badge } from "@/components/ui/badge" +import { useScan } from "@/hooks/use-scans" + +export default function ScanHistoryLayout({ + children, +}: { + children: React.ReactNode +}) { + const { id } = useParams<{ id: string }>() + const pathname = usePathname() + const { data: scanData, isLoading } = useScan(parseInt(id)) + + const getActiveTab = () => { + if (pathname.includes("/subdomain")) return "subdomain" + if (pathname.includes("/endpoints")) return "endpoints" + if (pathname.includes("/websites")) return "websites" + if (pathname.includes("/directories")) return "directories" + if (pathname.includes("/vulnerabilities")) return "vulnerabilities" + if (pathname.includes("/ip-addresses")) return "ip-addresses" + return "" + } + + const basePath = `/scan/history/${id}` + const tabPaths = { + subdomain: `${basePath}/subdomain/`, + endpoints: `${basePath}/endpoints/`, + websites: `${basePath}/websites/`, + directories: `${basePath}/directories/`, + vulnerabilities: `${basePath}/vulnerabilities/`, + "ip-addresses": `${basePath}/ip-addresses/`, + } + + // 从扫描数据中获取各个tab的数量 + const counts = { + subdomain: scanData?.summary?.subdomains || 0, + endpoints: scanData?.summary?.endpoints || 0, + websites: scanData?.summary?.websites || 0, + directories: scanData?.summary?.directories || 0, + vulnerabilities: scanData?.summary?.vulnerabilities?.total || 0, + "ip-addresses": scanData?.summary?.ips || 0, + } + + return ( + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + <div className="flex items-center justify-between px-4 lg:px-6"> + <div> + <h2 className="text-2xl font-bold tracking-tight flex items-center gap-2"> + <Target /> + Scan Results + </h2> + <p className="text-muted-foreground">扫描任务 ID:{id}</p> + </div> + </div> + + <div className="flex items-center justify-between px-4 lg:px-6"> + <Tabs value={getActiveTab()} className="w-full"> + <TabsList> + <TabsTrigger value="subdomain" asChild> + <Link href={tabPaths.subdomain} className="flex items-center gap-0.5"> + Subdomains + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts.subdomain} + </Badge> + </Link> + </TabsTrigger> + <TabsTrigger value="endpoints" asChild> + <Link href={tabPaths.endpoints} className="flex items-center gap-0.5"> + URLs + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts.endpoints} + </Badge> + </Link> + </TabsTrigger> + <TabsTrigger value="websites" asChild> + <Link href={tabPaths.websites} className="flex items-center gap-0.5"> + Websites + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts.websites} + </Badge> + </Link> + </TabsTrigger> + <TabsTrigger value="directories" asChild> + <Link href={tabPaths.directories} className="flex items-center gap-0.5"> + Directories + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts.directories} + </Badge> + </Link> + </TabsTrigger> + <TabsTrigger value="vulnerabilities" asChild> + <Link href={tabPaths.vulnerabilities} className="flex items-center gap-0.5"> + Vulnerabilities + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts.vulnerabilities} + </Badge> + </Link> + </TabsTrigger> + <TabsTrigger value="ip-addresses" asChild> + <Link href={tabPaths["ip-addresses"]} className="flex items-center gap-0.5"> + IP Addresses + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts["ip-addresses"]} + </Badge> + </Link> + </TabsTrigger> + </TabsList> + </Tabs> + </div> + + {children} + </div> + ) +} diff --git a/frontend/app/scan/history/[id]/page.tsx b/frontend/app/scan/history/[id]/page.tsx new file mode 100644 index 00000000..65e13875 --- /dev/null +++ b/frontend/app/scan/history/[id]/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import { useParams, useRouter } from "next/navigation" +import { useEffect } from "react" + +export default function ScanHistoryDetailPage() { + const { id } = useParams<{ id: string }>() + const router = useRouter() + + useEffect(() => { + router.replace(`/scan/history/${id}/subdomain/`) + }, [id, router]) + + return null +} diff --git a/frontend/app/scan/history/[id]/subdomain/page.tsx b/frontend/app/scan/history/[id]/subdomain/page.tsx new file mode 100644 index 00000000..9a76fcd5 --- /dev/null +++ b/frontend/app/scan/history/[id]/subdomain/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import React from "react" +import { useParams } from "next/navigation" +import { SubdomainsDetailView } from "@/components/subdomains" + +export default function ScanHistorySubdomainPage() { + const { id } = useParams<{ id: string }>() + + return ( + <div className="px-4 lg:px-6"> + <SubdomainsDetailView scanId={parseInt(id)} /> + </div> + ) +} diff --git a/frontend/app/scan/history/[id]/vulnerabilities/page.tsx b/frontend/app/scan/history/[id]/vulnerabilities/page.tsx new file mode 100644 index 00000000..e2ef249b --- /dev/null +++ b/frontend/app/scan/history/[id]/vulnerabilities/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import React from "react" +import { useParams } from "next/navigation" +import { VulnerabilitiesDetailView } from "@/components/vulnerabilities" + +export default function ScanHistoryVulnerabilitiesPage() { + const { id } = useParams<{ id: string }>() + + return ( + <div className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"> + <VulnerabilitiesDetailView scanId={Number(id)} /> + </div> + ) +} diff --git a/frontend/app/scan/history/[id]/websites/page.tsx b/frontend/app/scan/history/[id]/websites/page.tsx new file mode 100644 index 00000000..5cb34645 --- /dev/null +++ b/frontend/app/scan/history/[id]/websites/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import { useParams } from "next/navigation" +import { WebSitesView } from "@/components/websites/websites-view" + +export default function ScanWebSitesPage() { + const { id } = useParams<{ id: string }>() + const scanId = Number(id) + + return ( + <div className="px-4 lg:px-6"> + <WebSitesView scanId={scanId} /> + </div> + ) +} diff --git a/frontend/app/scan/history/page.tsx b/frontend/app/scan/history/page.tsx new file mode 100644 index 00000000..811e461c --- /dev/null +++ b/frontend/app/scan/history/page.tsx @@ -0,0 +1,34 @@ +"use client" + +import { IconRadar } from "@tabler/icons-react" +import { ScanHistoryList } from "@/components/scan/history/scan-history-list" +import { ScanHistoryStatCards } from "@/components/scan/history/scan-history-stat-cards" + +/** + * 扫描历史页面 + * 显示所有扫描任务的历史记录 + */ +export default function ScanHistoryPage() { + return ( + <div className="@container/main flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + {/* 页面标题 */} + <div className="flex items-center gap-3 px-4 lg:px-6"> + <IconRadar className="size-8 text-primary" /> + <div> + <h1 className="text-3xl font-bold">扫描历史</h1> + <p className="text-muted-foreground">查看和管理所有扫描任务记录</p> + </div> + </div> + + {/* 统计卡片 */} + <div className="px-4 lg:px-6"> + <ScanHistoryStatCards /> + </div> + + {/* 扫描历史列表 */} + <div className="px-4 lg:px-6"> + <ScanHistoryList /> + </div> + </div> + ) +} diff --git a/frontend/app/scan/scheduled/page.tsx b/frontend/app/scan/scheduled/page.tsx new file mode 100644 index 00000000..4ddda864 --- /dev/null +++ b/frontend/app/scan/scheduled/page.tsx @@ -0,0 +1,222 @@ +"use client" + +import React from "react" +import { ScheduledScanDataTable } from "@/components/scan/scheduled/scheduled-scan-data-table" +import { createScheduledScanColumns } from "@/components/scan/scheduled/scheduled-scan-columns" +import { CreateScheduledScanDialog } from "@/components/scan/scheduled/create-scheduled-scan-dialog" +import { EditScheduledScanDialog } from "@/components/scan/scheduled/edit-scheduled-scan-dialog" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { + useScheduledScans, + useDeleteScheduledScan, + useToggleScheduledScan +} from "@/hooks/use-scheduled-scans" +import type { ScheduledScan } from "@/types/scheduled-scan.types" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" + +/** + * 定时扫描页面 + * 管理定时扫描任务配置 + */ +export default function ScheduledScanPage() { + const [createDialogOpen, setCreateDialogOpen] = React.useState(false) + const [editDialogOpen, setEditDialogOpen] = React.useState(false) + const [editingScheduledScan, setEditingScheduledScan] = React.useState<ScheduledScan | null>(null) + const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) + const [deletingScheduledScan, setDeletingScheduledScan] = React.useState<ScheduledScan | null>(null) + + // 分页状态 + const [page, setPage] = React.useState(1) + const [pageSize, setPageSize] = React.useState(10) + + // 搜索状态 + const [searchQuery, setSearchQuery] = React.useState("") + const [isSearching, setIsSearching] = React.useState(false) + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPage(1) + } + + // 使用实际 API + const { data, isLoading, isFetching, refetch } = useScheduledScans({ page, pageSize, search: searchQuery || undefined }) + + // 当请求完成时重置搜索状态 + React.useEffect(() => { + if (!isFetching && isSearching) { + setIsSearching(false) + } + }, [isFetching, isSearching]) + const { mutate: deleteScheduledScan } = useDeleteScheduledScan() + const { mutate: toggleScheduledScan } = useToggleScheduledScan() + + const scheduledScans = data?.results || [] + const total = data?.total || 0 + const totalPages = data?.totalPages || 1 + + // 格式化日期 + const formatDate = React.useCallback((dateString: string) => { + const date = new Date(dateString) + return date.toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }) + }, []) + + // 查看任务详情 + const handleView = React.useCallback((scan: ScheduledScan) => { + // TODO: 导航到详情页 + }, []) + + // 编辑任务 + const handleEdit = React.useCallback((scan: ScheduledScan) => { + setEditingScheduledScan(scan) + setEditDialogOpen(true) + }, []) + + // 删除任务(打开确认弹窗) + const handleDelete = React.useCallback((scan: ScheduledScan) => { + setDeletingScheduledScan(scan) + setDeleteDialogOpen(true) + }, []) + + // 确认删除任务 + const confirmDelete = React.useCallback(() => { + if (deletingScheduledScan) { + deleteScheduledScan(deletingScheduledScan.id) + setDeleteDialogOpen(false) + setDeletingScheduledScan(null) + } + }, [deletingScheduledScan, deleteScheduledScan]) + + // 切换任务启用状态 + const handleToggleStatus = React.useCallback((scan: ScheduledScan, enabled: boolean) => { + toggleScheduledScan({ id: scan.id, isEnabled: enabled }) + }, [toggleScheduledScan]) + + // 页码变化处理 + const handlePageChange = React.useCallback((newPage: number) => { + setPage(newPage) + }, []) + + // 每页数量变化处理 + const handlePageSizeChange = React.useCallback((newPageSize: number) => { + setPageSize(newPageSize) + setPage(1) // 重置到第一页 + }, []) + + // 添加新任务 + const handleAddNew = React.useCallback(() => { + setCreateDialogOpen(true) + }, []) + + // 创建列定义 + const columns = React.useMemo( + () => + createScheduledScanColumns({ + formatDate, + handleView, + handleEdit, + handleDelete, + handleToggleStatus, + }), + [formatDate, handleView, handleEdit, handleDelete, handleToggleStatus] + ) + + if (isLoading) { + return ( + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + <div className="flex items-center justify-between px-4 lg:px-6"> + <div> + <h1 className="text-3xl font-bold">定时扫描</h1> + <p className="text-muted-foreground mt-1">配置和管理定时扫描任务</p> + </div> + </div> + <DataTableSkeleton + toolbarButtonCount={2} + rows={5} + columns={6} + /> + </div> + ) + } + + return ( + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + {/* 页面标题 */} + <div className="px-4 lg:px-6"> + <div> + <h1 className="text-3xl font-bold">定时扫描</h1> + <p className="text-muted-foreground mt-1">配置和管理定时扫描任务</p> + </div> + </div> + + {/* 数据表格 */} + <div className="px-4 lg:px-6"> + <ScheduledScanDataTable + data={scheduledScans} + columns={columns} + onAddNew={handleAddNew} + searchPlaceholder="搜索任务名称..." + searchColumn="name" + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + addButtonText="新建定时扫描" + page={page} + pageSize={pageSize} + total={total} + totalPages={totalPages} + onPageChange={handlePageChange} + onPageSizeChange={handlePageSizeChange} + /> + </div> + + {/* 新建定时扫描对话框 */} + <CreateScheduledScanDialog + open={createDialogOpen} + onOpenChange={setCreateDialogOpen} + onSuccess={() => refetch()} + /> + + {/* 编辑定时扫描对话框 */} + <EditScheduledScanDialog + open={editDialogOpen} + onOpenChange={setEditDialogOpen} + scheduledScan={editingScheduledScan} + onSuccess={() => refetch()} + /> + + {/* 删除确认弹窗 */} + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除</AlertDialogTitle> + <AlertDialogDescription> + 确定要删除定时扫描任务 "{deletingScheduledScan?.name}" 吗?此操作无法撤销。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction onClick={confirmDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90"> + 删除 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </div> + ) +} diff --git a/frontend/app/settings/notifications/page.tsx b/frontend/app/settings/notifications/page.tsx new file mode 100644 index 00000000..f67472f5 --- /dev/null +++ b/frontend/app/settings/notifications/page.tsx @@ -0,0 +1,263 @@ +"use client" + +import React from 'react' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import * as z from 'zod' +import { IconBrandDiscord, IconMail, IconBrandSlack, IconScan, IconShieldCheck, IconWorld, IconSettings } from '@tabler/icons-react' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Switch } from '@/components/ui/switch' +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Separator } from '@/components/ui/separator' +import { Badge } from '@/components/ui/badge' +import { useNotificationSettings, useUpdateNotificationSettings } from '@/hooks/use-notification-settings' + +const schema = z + .object({ + discord: z.object({ + enabled: z.boolean(), + webhookUrl: z.string().url('请输入有效的 Discord Webhook URL').or(z.literal('')), + }), + categories: z.object({ + scan: z.boolean(), // 扫描任务 + vulnerability: z.boolean(), // 漏洞发现 + asset: z.boolean(), // 资产发现 + system: z.boolean(), // 系统消息 + }), + }) + .superRefine((val, ctx) => { + if (val.discord.enabled) { + if (!val.discord.webhookUrl || val.discord.webhookUrl.trim() === '') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '启用 Discord 时必须填写 Webhook URL', + path: ['discord', 'webhookUrl'], + }) + } + } + }) + +const NOTIFICATION_CATEGORIES = [ + { + key: 'scan' as const, + label: '扫描任务', + description: '扫描启动、进度、完成、失败等通知', + icon: IconScan, + }, + { + key: 'vulnerability' as const, + label: '漏洞发现', + description: '发现安全漏洞时通知', + icon: IconShieldCheck, + }, + { + key: 'asset' as const, + label: '资产发现', + description: '发现新子域名、IP、端口等资产', + icon: IconWorld, + }, + { + key: 'system' as const, + label: '系统消息', + description: '系统级通知和公告', + icon: IconSettings, + }, +] + +export default function NotificationSettingsPage() { + const { data, isLoading } = useNotificationSettings() + const updateMutation = useUpdateNotificationSettings() + + const form = useForm<z.infer<typeof schema>>({ + resolver: zodResolver(schema), + values: data ?? { + discord: { enabled: false, webhookUrl: '' }, + categories: { + scan: true, + vulnerability: true, + asset: true, + system: false, + }, + }, + }) + + const onSubmit = (values: z.infer<typeof schema>) => { + updateMutation.mutate(values) + } + + const discordEnabled = form.watch('discord.enabled') + + return ( + <div className="p-4 md:p-6 space-y-6"> + <div> + <h1 className="text-2xl font-semibold">通知设置</h1> + <p className="text-muted-foreground mt-1">配置系统通知的推送渠道和接收偏好</p> + </div> + + <Tabs defaultValue="channels" className="w-full"> + <TabsList> + <TabsTrigger value="channels">推送渠道</TabsTrigger> + <TabsTrigger value="preferences">通知偏好</TabsTrigger> + </TabsList> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + {/* 推送渠道 Tab */} + <TabsContent value="channels" className="space-y-4 mt-4"> + {/* Discord 卡片 */} + <Card> + <CardHeader className="pb-4"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-3"> + <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-[#5865F2]/10"> + <IconBrandDiscord className="h-5 w-5 text-[#5865F2]" /> + </div> + <div> + <CardTitle className="text-base">Discord</CardTitle> + <CardDescription>将通知推送到你的 Discord 频道</CardDescription> + </div> + </div> + <FormField + control={form.control} + name="discord.enabled" + render={({ field }) => ( + <FormControl> + <Switch + checked={field.value} + onCheckedChange={field.onChange} + disabled={isLoading || updateMutation.isPending} + /> + </FormControl> + )} + /> + </div> + </CardHeader> + {discordEnabled && ( + <CardContent className="pt-0"> + <Separator className="mb-4" /> + <FormField + control={form.control} + name="discord.webhookUrl" + render={({ field }) => ( + <FormItem> + <FormLabel>Webhook URL</FormLabel> + <FormControl> + <Input + placeholder="https://discord.com/api/webhooks/..." + {...field} + disabled={isLoading || updateMutation.isPending} + /> + </FormControl> + <FormDescription> + 在 Discord 频道设置中创建 Webhook 并粘贴地址 + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + </CardContent> + )} + </Card> + + {/* 邮件 - 即将支持 */} + <Card className="opacity-60"> + <CardHeader className="pb-4"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-3"> + <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted"> + <IconMail className="h-5 w-5 text-muted-foreground" /> + </div> + <div> + <div className="flex items-center gap-2"> + <CardTitle className="text-base">邮件</CardTitle> + <Badge variant="secondary" className="text-xs">即将支持</Badge> + </div> + <CardDescription>通过邮件接收通知</CardDescription> + </div> + </div> + <Switch disabled /> + </div> + </CardHeader> + </Card> + + {/* 飞书/钉钉/企微 - 即将支持 */} + <Card className="opacity-60"> + <CardHeader className="pb-4"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-3"> + <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted"> + <IconBrandSlack className="h-5 w-5 text-muted-foreground" /> + </div> + <div> + <div className="flex items-center gap-2"> + <CardTitle className="text-base">飞书 / 钉钉 / 企微</CardTitle> + <Badge variant="secondary" className="text-xs">即将支持</Badge> + </div> + <CardDescription>推送到企业协作平台</CardDescription> + </div> + </div> + <Switch disabled /> + </div> + </CardHeader> + </Card> + </TabsContent> + + {/* 通知偏好 Tab */} + <TabsContent value="preferences" className="mt-4"> + <Card> + <CardHeader> + <CardTitle className="text-base">通知分类</CardTitle> + <CardDescription>选择你想要接收的通知类型</CardDescription> + </CardHeader> + <CardContent className="space-y-1"> + {NOTIFICATION_CATEGORIES.map((category) => ( + <FormField + key={category.key} + control={form.control} + name={`categories.${category.key}`} + render={({ field }) => ( + <FormItem className="flex items-center justify-between py-3 border-b last:border-b-0"> + <div className="flex items-center gap-3"> + <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-muted"> + <category.icon className="h-4 w-4 text-muted-foreground" /> + </div> + <div> + <FormLabel className="text-sm font-medium cursor-pointer"> + {category.label} + </FormLabel> + <FormDescription className="text-xs"> + {category.description} + </FormDescription> + </div> + </div> + <FormControl> + <Switch + checked={field.value} + onCheckedChange={field.onChange} + disabled={isLoading || updateMutation.isPending} + /> + </FormControl> + </FormItem> + )} + /> + ))} + </CardContent> + </Card> + </TabsContent> + + {/* 保存按钮 */} + <div className="flex justify-end mt-6"> + <Button type="submit" disabled={updateMutation.isPending || isLoading}> + 保存设置 + </Button> + </div> + </form> + </Form> + </Tabs> + </div> + ) +} diff --git a/frontend/app/settings/workers/page.tsx b/frontend/app/settings/workers/page.tsx new file mode 100644 index 00000000..3ddfbf5e --- /dev/null +++ b/frontend/app/settings/workers/page.tsx @@ -0,0 +1,19 @@ +"use client" + +import { WorkerList } from "@/components/settings/workers" + +export default function WorkersPage() { + return ( + <div className="flex flex-1 flex-col gap-4 p-4"> + <div className="flex items-center justify-between"> + <div> + <h1 className="text-2xl font-bold tracking-tight">扫描节点</h1> + <p className="text-muted-foreground"> + 管理分布式扫描节点,支持远程 VPS 自动部署 + </p> + </div> + </div> + <WorkerList /> + </div> + ) +} diff --git a/frontend/app/target/[id]/details/page.tsx b/frontend/app/target/[id]/details/page.tsx new file mode 100644 index 00000000..dc30f1aa --- /dev/null +++ b/frontend/app/target/[id]/details/page.tsx @@ -0,0 +1,21 @@ +"use client" + +import { useParams, useRouter } from "next/navigation" +import { useEffect } from "react" + +/** + * 目标详情页面(兼容旧路由) + * 自动重定向到域名页面 + */ +export default function TargetDetailsPage() { + const { id } = useParams<{ id: string }>() + const router = useRouter() + + useEffect(() => { + // 重定向到子域名页面 + router.replace(`/target/${id}/subdomain/`) + }, [id, router]) + + return null +} + diff --git a/frontend/app/target/[id]/directories/page.tsx b/frontend/app/target/[id]/directories/page.tsx new file mode 100644 index 00000000..c46f2c83 --- /dev/null +++ b/frontend/app/target/[id]/directories/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import { useParams } from "next/navigation" +import { DirectoriesView } from "@/components/directories/directories-view" + +export default function TargetDirectoriesPage() { + const { id } = useParams<{ id: string }>() + const targetId = Number(id) + + return ( + <div className="px-4 lg:px-6"> + <DirectoriesView targetId={targetId} /> + </div> + ) +} diff --git a/frontend/app/target/[id]/endpoints/page.tsx b/frontend/app/target/[id]/endpoints/page.tsx new file mode 100644 index 00000000..4b371458 --- /dev/null +++ b/frontend/app/target/[id]/endpoints/page.tsx @@ -0,0 +1,20 @@ +"use client" + +import React from "react" +import { useParams } from "next/navigation" +import { EndpointsDetailView } from "@/components/endpoints" + +/** + * 目标端点页面 + * 显示目标下的端点详情 + */ +export default function TargetEndpointsPage() { + const { id } = useParams<{ id: string }>() + + return ( + <div className="px-4 lg:px-6"> + <EndpointsDetailView targetId={parseInt(id)} /> + </div> + ) +} + diff --git a/frontend/app/target/[id]/ip-addresses/page.tsx b/frontend/app/target/[id]/ip-addresses/page.tsx new file mode 100644 index 00000000..60ed5b85 --- /dev/null +++ b/frontend/app/target/[id]/ip-addresses/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import React from "react" +import { useParams } from "next/navigation" +import { IPAddressesView } from "@/components/ip-addresses" + +export default function TargetIPsPage() { + const { id } = useParams<{ id: string }>() + + return ( + <div className="px-4 lg:px-6"> + <IPAddressesView targetId={Number(id)} /> + </div> + ) +} diff --git a/frontend/app/target/[id]/layout.tsx b/frontend/app/target/[id]/layout.tsx new file mode 100644 index 00000000..bb79c554 --- /dev/null +++ b/frontend/app/target/[id]/layout.tsx @@ -0,0 +1,195 @@ +"use client" + +import React from "react" +import { usePathname, useParams } from "next/navigation" +import Link from "next/link" +import { Target } from "lucide-react" +import { Skeleton } from "@/components/ui/skeleton" +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Badge } from "@/components/ui/badge" +import { useTarget } from "@/hooks/use-targets" + +/** + * 目标详情布局 + * 为所有子页面提供共享的目标信息和导航 + */ +export default function TargetLayout({ + children, +}: { + children: React.ReactNode +}) { + const { id } = useParams<{ id: string }>() + const pathname = usePathname() + + // 使用 React Query 获取目标数据 + const { + data: target, + isLoading, + error + } = useTarget(Number(id)) + + // 获取当前激活的 Tab + const getActiveTab = () => { + if (pathname.includes("/subdomain")) return "subdomain" + if (pathname.includes("/endpoints")) return "endpoints" + if (pathname.includes("/websites")) return "websites" + if (pathname.includes("/directories")) return "directories" + if (pathname.includes("/vulnerabilities")) return "vulnerabilities" + if (pathname.includes("/ip-addresses")) return "ip-addresses" + return "" + } + + // Tab 路径映射 + const basePath = `/target/${id}` + const tabPaths = { + subdomain: `${basePath}/subdomain/`, + endpoints: `${basePath}/endpoints/`, + websites: `${basePath}/websites/`, + directories: `${basePath}/directories/`, + vulnerabilities: `${basePath}/vulnerabilities/`, + "ip-addresses": `${basePath}/ip-addresses/`, + } + + // 从目标数据中获取各个tab的数量 + const counts = { + subdomain: (target as any)?.summary?.subdomains || 0, + endpoints: (target as any)?.summary?.endpoints || 0, + websites: (target as any)?.summary?.websites || 0, + directories: (target as any)?.summary?.directories || 0, + vulnerabilities: (target as any)?.summary?.vulnerabilities?.total || 0, + "ip-addresses": (target as any)?.summary?.ips || 0, + } + + // 加载状态 + if (isLoading) { + return ( + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + {/* 页面头部骨架 */} + <div className="flex items-center justify-between px-4 lg:px-6"> + <div className="w-full max-w-xl space-y-2"> + <div className="flex items-center gap-2"> + <Skeleton className="h-6 w-6 rounded-md" /> + <Skeleton className="h-7 w-48" /> + </div> + <Skeleton className="h-4 w-72" /> + </div> + </div> + + {/* Tabs 导航骨架 */} + <div className="flex items-center justify-between px-4 lg:px-6"> + <div className="flex gap-2"> + <Skeleton className="h-9 w-20" /> + <Skeleton className="h-9 w-24" /> + </div> + </div> + </div> + ) + } + + // 错误状态 + if (error) { + return ( + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + <div className="flex items-center justify-center py-12"> + <div className="text-center"> + <Target className="mx-auto text-destructive mb-4" /> + <h3 className="text-lg font-semibold mb-2">加载失败</h3> + <p className="text-muted-foreground"> + {error.message || "获取目标数据时出现错误"} + </p> + </div> + </div> + </div> + ) + } + + if (!target) { + return ( + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + <div className="flex items-center justify-center py-12"> + <div className="text-center"> + <Target className="mx-auto text-muted-foreground mb-4" /> + <h3 className="text-lg font-semibold mb-2">目标不存在</h3> + <p className="text-muted-foreground"> + 未找到ID为 {id} 的目标 + </p> + </div> + </div> + </div> + ) + } + + return ( + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + {/* 页面头部 */} + <div className="flex items-center justify-between px-4 lg:px-6"> + <div> + <h2 className="text-2xl font-bold tracking-tight flex items-center gap-2"> + <Target /> + {target.name} + </h2> + <p className="text-muted-foreground">{target.description || "暂无描述"}</p> + </div> + </div> + + {/* Tabs 导航 - 使用 Link 确保触发进度条 */} + <div className="flex items-center justify-between px-4 lg:px-6"> + <Tabs value={getActiveTab()} className="w-full"> + <TabsList> + <TabsTrigger value="subdomain" asChild> + <Link href={tabPaths.subdomain} className="flex items-center gap-0.5"> + Subdomains + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts.subdomain} + </Badge> + </Link> + </TabsTrigger> + <TabsTrigger value="endpoints" asChild> + <Link href={tabPaths.endpoints} className="flex items-center gap-0.5"> + URLs + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts.endpoints} + </Badge> + </Link> + </TabsTrigger> + <TabsTrigger value="websites" asChild> + <Link href={tabPaths.websites} className="flex items-center gap-0.5"> + Websites + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts.websites} + </Badge> + </Link> + </TabsTrigger> + <TabsTrigger value="directories" asChild> + <Link href={tabPaths.directories} className="flex items-center gap-0.5"> + Directories + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts.directories} + </Badge> + </Link> + </TabsTrigger> + <TabsTrigger value="ip-addresses" asChild> + <Link href={tabPaths["ip-addresses"]} className="flex items-center gap-0.5"> + IP Addresses + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts["ip-addresses"]} + </Badge> + </Link> + </TabsTrigger> + <TabsTrigger value="vulnerabilities" asChild> + <Link href={tabPaths.vulnerabilities} className="flex items-center gap-0.5"> + Vulnerabilities + <Badge className="text-xs bg-chart-5 text-white border-0"> + {counts.vulnerabilities} + </Badge> + </Link> + </TabsTrigger> + </TabsList> + </Tabs> + </div> + + {/* 子页面内容 */} + {children} + </div> + ) +} diff --git a/frontend/app/target/[id]/page.tsx b/frontend/app/target/[id]/page.tsx new file mode 100644 index 00000000..cfa0395e --- /dev/null +++ b/frontend/app/target/[id]/page.tsx @@ -0,0 +1,21 @@ +"use client" + +import { useParams, useRouter } from "next/navigation" +import { useEffect } from "react" + +/** + * 目标详情默认页面 + * 自动重定向到域名页面 + */ +export default function TargetDetailPage() { + const { id } = useParams<{ id: string }>() + const router = useRouter() + + useEffect(() => { + // 重定向到子域名页面 + router.replace(`/target/${id}/subdomain/`) + }, [id, router]) + + return null +} + diff --git a/frontend/app/target/[id]/subdomain/page.tsx b/frontend/app/target/[id]/subdomain/page.tsx new file mode 100644 index 00000000..382f55bf --- /dev/null +++ b/frontend/app/target/[id]/subdomain/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import React from "react" +import { useParams } from "next/navigation" +import { SubdomainsDetailView } from "@/components/subdomains" + +export default function TargetSubdomainPage() { + const { id } = useParams<{ id: string }>() + + return ( + <div className="px-4 lg:px-6"> + <SubdomainsDetailView targetId={parseInt(id)} /> + </div> + ) +} diff --git a/frontend/app/target/[id]/vulnerabilities/page.tsx b/frontend/app/target/[id]/vulnerabilities/page.tsx new file mode 100644 index 00000000..4a3c6e95 --- /dev/null +++ b/frontend/app/target/[id]/vulnerabilities/page.tsx @@ -0,0 +1,20 @@ +"use client" + +import React from "react" +import { useParams } from "next/navigation" +import { VulnerabilitiesDetailView } from "@/components/vulnerabilities" + +/** + * 目标漏洞页面 + * 显示目标下的漏洞详情 + */ +export default function TargetVulnerabilitiesPage() { + const { id } = useParams<{ id: string }>() + + return ( + <div className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"> + <VulnerabilitiesDetailView targetId={parseInt(id)} /> + </div> + ) +} + diff --git a/frontend/app/target/[id]/websites/page.tsx b/frontend/app/target/[id]/websites/page.tsx new file mode 100644 index 00000000..dba9aabb --- /dev/null +++ b/frontend/app/target/[id]/websites/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import { useParams } from "next/navigation" +import { WebSitesView } from "@/components/websites/websites-view" + +export default function WebSitesPage() { + const { id } = useParams<{ id: string }>() + const targetId = Number(id) + + return ( + <div className="px-4 lg:px-6"> + <WebSitesView targetId={targetId} /> + </div> + ) +} diff --git a/frontend/app/target/page.tsx b/frontend/app/target/page.tsx new file mode 100644 index 00000000..e5c07dc0 --- /dev/null +++ b/frontend/app/target/page.tsx @@ -0,0 +1,26 @@ +import { AllTargetsDetailView } from "@/components/target/all-targets-detail-view" +import { Target } from "lucide-react" + +export default function AllTargetsPage() { + return ( + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + {/* 页面头部 */} + <div className="flex items-center justify-between px-4 lg:px-6"> + <div> + <h2 className="text-2xl font-bold tracking-tight flex items-center gap-2"> + <Target /> + 目标 + </h2> + <p className="text-muted-foreground"> + 管理系统中的所有目标信息 + </p> + </div> + </div> + + {/* 内容区域 */} + <div className="px-4 lg:px-6"> + <AllTargetsDetailView /> + </div> + </div> + ) +} diff --git a/frontend/app/tools/config/custom/page.tsx b/frontend/app/tools/config/custom/page.tsx new file mode 100644 index 00000000..38132a09 --- /dev/null +++ b/frontend/app/tools/config/custom/page.tsx @@ -0,0 +1,8 @@ +/** + * 自定义工具页面 + * 展示和管理自定义扫描脚本和工具 + */ +export default function CustomToolsPage() { + // 工具配置功能已下线,此页面保留占位避免历史链接报错 + return null +} diff --git a/frontend/app/tools/config/opensource/page.tsx b/frontend/app/tools/config/opensource/page.tsx new file mode 100644 index 00000000..3d7bf50e --- /dev/null +++ b/frontend/app/tools/config/opensource/page.tsx @@ -0,0 +1,8 @@ +/** + * 开源工具页面 + * 展示和管理开源扫描工具 + */ +export default function OpensourceToolsPage() { + // 工具配置功能已下线,此页面保留占位避免历史链接报错 + return null +} diff --git a/frontend/app/tools/config/page.tsx b/frontend/app/tools/config/page.tsx new file mode 100644 index 00000000..69973b0e --- /dev/null +++ b/frontend/app/tools/config/page.tsx @@ -0,0 +1,10 @@ +"use client" + +/** + * 工具配置页面 + * 展示和管理扫描工具集(开源工具和自定义工具) + */ +export default function ToolConfigPage() { + // 工具配置功能已下线,此页面保留占位避免历史链接报错 + return null +} diff --git a/frontend/app/tools/nuclei/[repoId]/page.tsx b/frontend/app/tools/nuclei/[repoId]/page.tsx new file mode 100644 index 00000000..6469302b --- /dev/null +++ b/frontend/app/tools/nuclei/[repoId]/page.tsx @@ -0,0 +1,371 @@ +"use client" + +import { useEffect, useMemo, useState } from "react" +import Editor from "@monaco-editor/react" +import Link from "next/link" +import { useParams } from "next/navigation" +import { + ChevronDown, + ChevronRight, + FileText, + Folder, + ArrowLeft, + Search, + RefreshCw, + AlertTriangle, + Tag, + User, +} from "lucide-react" +import { useTheme } from "next-themes" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { + useNucleiRepoTree, + useNucleiRepoContent, + useRefreshNucleiRepo, + useNucleiRepo, +} from "@/hooks/use-nuclei-repos" +import type { NucleiTemplateTreeNode } from "@/types/nuclei.types" +import { cn } from "@/lib/utils" + +interface FlattenedNode extends NucleiTemplateTreeNode { + level: number +} + +/** 解析 YAML 内容提取模板信息 */ +function parseTemplateInfo(content: string) { + const info: { + id?: string + name?: string + severity?: string + tags?: string[] + author?: string + } = {} + + // 简单正则提取,不用完整 YAML 解析 + const idMatch = content.match(/^id:\s*(.+)$/m) + if (idMatch) info.id = idMatch[1].trim() + + const nameMatch = content.match(/^\s*name:\s*(.+)$/m) + if (nameMatch) info.name = nameMatch[1].trim() + + const severityMatch = content.match(/^\s*severity:\s*(.+)$/m) + if (severityMatch) info.severity = severityMatch[1].trim().toLowerCase() + + const tagsMatch = content.match(/^\s*tags:\s*(.+)$/m) + if (tagsMatch) info.tags = tagsMatch[1].split(",").map((t) => t.trim()) + + const authorMatch = content.match(/^\s*author:\s*(.+)$/m) + if (authorMatch) info.author = authorMatch[1].trim() + + return info +} + +/** 严重程度对应的颜色 */ +function getSeverityColor(severity?: string) { + switch (severity) { + case "critical": + return "bg-red-100 text-red-700 border-red-200" + case "high": + return "bg-orange-100 text-orange-700 border-orange-200" + case "medium": + return "bg-yellow-100 text-yellow-700 border-yellow-200" + case "low": + return "bg-blue-100 text-blue-700 border-blue-200" + case "info": + return "bg-gray-100 text-gray-700 border-gray-200" + default: + return "bg-gray-100 text-gray-600 border-gray-200" + } +} + +export default function NucleiRepoDetailPage() { + const params = useParams() + const repoId = params?.repoId as string + + const [selectedPath, setSelectedPath] = useState<string | null>(null) + const [expandedPaths, setExpandedPaths] = useState<string[]>([]) + const [searchQuery, setSearchQuery] = useState("") + const [editorValue, setEditorValue] = useState<string>("") + + const { theme } = useTheme() + + const numericRepoId = repoId ? Number(repoId) : null + + const { data: tree, isLoading, isError } = useNucleiRepoTree(numericRepoId) + const { data: templateContent, isLoading: isLoadingContent } = useNucleiRepoContent(numericRepoId, selectedPath) + const { data: repoDetail } = useNucleiRepo(numericRepoId) + const refreshMutation = useRefreshNucleiRepo() + + // 展开的节点和过滤后的节点 + const nodes: FlattenedNode[] = useMemo(() => { + const result: FlattenedNode[] = [] + const expandedSet = new Set(expandedPaths) + const query = searchQuery.toLowerCase().trim() + + const visit = (items: NucleiTemplateTreeNode[] | undefined, level: number) => { + if (!items) return + for (const item of items) { + const isFolder = item.type === "folder" + const isFile = item.type === "file" + const isTemplateFile = + isFile && (item.name.endsWith(".yaml") || item.name.endsWith(".yml")) + + if (!isFolder && !isTemplateFile) { + continue + } + + // 搜索过滤 + if (query && isFile && !item.name.toLowerCase().includes(query)) { + continue + } + + result.push({ ...item, level }) + + if (isFolder && item.children && item.children.length > 0) { + // 搜索时展开所有文件夹,否则按 expandedPaths + if (query || expandedSet.has(item.path)) { + visit(item.children, level + 1) + } + } + } + } + + visit(tree, 0) + return result + }, [tree, expandedPaths, searchQuery]) + + useEffect(() => { + if (!tree || tree.length === 0) return + if (expandedPaths.length > 0) return + + const rootFolders = tree + .filter((item) => item.type === "folder") + .map((item) => item.path) + + if (rootFolders.length > 0) { + setExpandedPaths(rootFolders) + } + }, [tree, expandedPaths]) + + useEffect(() => { + if (templateContent) { + setEditorValue(templateContent.content) + } else { + setEditorValue("") + } + }, [templateContent?.path]) + + const toggleFolder = (path: string) => { + setExpandedPaths((prev) => + prev.includes(path) ? prev.filter((p) => p !== path) : [...prev, path] + ) + } + + const repoDisplayName = repoDetail?.name || `仓库 #${repoId}` + + // 解析当前模板信息 + const templateInfo = useMemo(() => { + if (!templateContent?.content) return null + return parseTemplateInfo(templateContent.content) + }, [templateContent?.content]) + + return ( + <div className="flex flex-col h-full"> + {/* 顶部:返回 + 标题 + 搜索 + 同步 */} + <div className="flex items-center gap-4 px-4 py-4 lg:px-6"> + <Link href="/tools/nuclei/"> + <Button variant="ghost" size="sm" className="gap-1.5"> + <ArrowLeft className="h-4 w-4" /> + 返回 + </Button> + </Link> + <h1 className="text-xl font-bold truncate">{repoDisplayName}</h1> + <div className="flex items-center gap-2 flex-1 max-w-md ml-auto"> + <div className="relative flex-1"> + <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> + <Input + placeholder="搜索模板..." + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + className="pl-8" + /> + </div> + </div> + <Button + variant="outline" + size="sm" + onClick={() => numericRepoId && refreshMutation.mutate(numericRepoId)} + disabled={refreshMutation.isPending || !numericRepoId} + > + <RefreshCw className={cn("h-4 w-4 mr-1.5", refreshMutation.isPending && "animate-spin")} /> + {refreshMutation.isPending ? "同步中..." : "同步"} + </Button> + </div> + + <Separator /> + + {/* 主体:左侧目录 + 右侧内容 */} + <div className="flex flex-1 min-h-0"> + {/* 左侧:模板目录 */} + <div className="w-72 lg:w-80 border-r flex flex-col"> + <div className="px-4 py-3 border-b"> + <h2 className="text-sm font-medium text-muted-foreground"> + 模板目录 {nodes.filter((n) => n.type === "file").length > 0 && + `(${nodes.filter((n) => n.type === "file").length} 个模板)`} + </h2> + </div> + <ScrollArea className="flex-1"> + {isLoading ? ( + <div className="p-4 text-sm text-muted-foreground">加载中...</div> + ) : isError || nodes.length === 0 ? ( + <div className="p-4 text-sm text-muted-foreground"> + {searchQuery ? "未找到匹配的模板" : "暂无模板或加载失败"} + </div> + ) : ( + <div className="p-2"> + {nodes.map((node) => { + const isFolder = node.type === "folder" + const isFile = node.type === "file" + const isActive = isFile && node.path === selectedPath + const isExpanded = isFolder && expandedPaths.includes(node.path) + + return ( + <button + key={node.path} + type="button" + onClick={() => { + if (isFolder) { + toggleFolder(node.path) + } else if (isFile) { + setSelectedPath(node.path) + } + }} + className={cn( + "flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-sm transition-colors", + isFolder && "font-medium", + isActive + ? "bg-primary/10 text-primary" + : "hover:bg-muted" + )} + style={{ paddingLeft: 8 + node.level * 16 }} + > + {isFolder ? ( + <> + {isExpanded ? ( + <ChevronDown className="h-3.5 w-3.5 shrink-0" /> + ) : ( + <ChevronRight className="h-3.5 w-3.5 shrink-0" /> + )} + <Folder className="h-4 w-4 shrink-0 text-muted-foreground" /> + </> + ) : ( + <> + <span className="w-3.5" /> + <FileText className="h-4 w-4 shrink-0 text-muted-foreground" /> + </> + )} + <span className="truncate">{node.name}</span> + </button> + ) + })} + </div> + )} + </ScrollArea> + </div> + + {/* 右侧:模板内容 */} + <div className="flex-1 flex flex-col min-w-0"> + {selectedPath && templateContent ? ( + <> + {/* 模板头部 */} + <div className="px-6 py-4 border-b"> + <div className="flex items-start gap-3"> + <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 shrink-0"> + <FileText className="h-5 w-5 text-primary" /> + </div> + <div className="min-w-0 flex-1"> + <h2 className="text-lg font-semibold truncate"> + {templateContent.name} + </h2> + <p className="text-xs text-muted-foreground truncate mt-0.5"> + {templateContent.path} + </p> + </div> + {templateInfo?.severity && ( + <Badge + variant="outline" + className={cn("shrink-0 capitalize", getSeverityColor(templateInfo.severity))} + > + {templateInfo.severity} + </Badge> + )} + </div> + </div> + + {/* 代码编辑器 */} + <div className="flex-1 min-h-0"> + <Editor + height="100%" + defaultLanguage="yaml" + value={editorValue} + options={{ + minimap: { enabled: false }, + fontSize: 13, + lineNumbers: "on", + scrollBeyondLastLine: false, + automaticLayout: true, + readOnly: true, + padding: { top: 16 }, + }} + theme={theme === "dark" ? "vs-dark" : "light"} + /> + </div> + + {/* 模板信息 */} + {templateInfo && (templateInfo.tags || templateInfo.author) && ( + <div className="px-6 py-3 border-t flex items-center gap-4 text-sm"> + {templateInfo.tags && templateInfo.tags.length > 0 && ( + <div className="flex items-center gap-2"> + <Tag className="h-4 w-4 text-muted-foreground" /> + <div className="flex gap-1 flex-wrap"> + {templateInfo.tags.slice(0, 5).map((tag) => ( + <Badge key={tag} variant="secondary" className="text-xs"> + {tag} + </Badge> + ))} + {templateInfo.tags.length > 5 && ( + <Badge variant="secondary" className="text-xs"> + +{templateInfo.tags.length - 5} + </Badge> + )} + </div> + </div> + )} + {templateInfo.author && ( + <div className="flex items-center gap-1.5 text-muted-foreground"> + <User className="h-4 w-4" /> + <span>{templateInfo.author}</span> + </div> + )} + </div> + )} + </> + ) : ( + // 未选中状态 + <div className="flex-1 flex items-center justify-center"> + <div className="text-center text-muted-foreground"> + <FileText className="h-12 w-12 mx-auto mb-3 opacity-50" /> + <p className="text-sm">选择左侧模板查看内容</p> + <p className="text-xs mt-1">或使用搜索快速定位</p> + </div> + </div> + )} + </div> + </div> + </div> + ) +} diff --git a/frontend/app/tools/nuclei/page.tsx b/frontend/app/tools/nuclei/page.tsx new file mode 100644 index 00000000..729b0ae0 --- /dev/null +++ b/frontend/app/tools/nuclei/page.tsx @@ -0,0 +1,496 @@ +"use client" + +import Link from "next/link" +import { useState, useMemo, type FormEvent } from "react" +import { GitBranch, Search, RefreshCw, Settings, Trash2, FolderOpen } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Input } from "@/components/ui/input" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Label } from "@/components/ui/label" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { + useNucleiRepos, + useCreateNucleiRepo, + useDeleteNucleiRepo, + useRefreshNucleiRepo, + useUpdateNucleiRepo, + type NucleiRepo, +} from "@/hooks/use-nuclei-repos" +import { cn } from "@/lib/utils" +import { MasterDetailSkeleton } from "@/components/ui/master-detail-skeleton" + +/** 格式化时间显示 */ +function formatDateTime(isoString: string | null) { + if (!isoString) return "-" + try { + return new Date(isoString).toLocaleString("zh-CN") + } catch { + return isoString + } +} + +export default function NucleiReposPage() { + const [selectedId, setSelectedId] = useState<number | null>(null) + const [searchQuery, setSearchQuery] = useState("") + const [createDialogOpen, setCreateDialogOpen] = useState(false) + const [newName, setNewName] = useState("") + const [newRepoUrl, setNewRepoUrl] = useState("") + + const [editDialogOpen, setEditDialogOpen] = useState(false) + const [editingRepo, setEditingRepo] = useState<NucleiRepo | null>(null) + const [editRepoUrl, setEditRepoUrl] = useState("") + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [repoToDelete, setRepoToDelete] = useState<NucleiRepo | null>(null) + + // API Hooks + const { data: repos, isLoading, isError } = useNucleiRepos() + const createMutation = useCreateNucleiRepo() + const deleteMutation = useDeleteNucleiRepo() + const refreshMutation = useRefreshNucleiRepo() + const updateMutation = useUpdateNucleiRepo() + + // 过滤仓库列表 + const filteredRepos = useMemo(() => { + if (!repos) return [] + if (!searchQuery.trim()) return repos + const query = searchQuery.toLowerCase() + return repos.filter( + (r) => + r.name.toLowerCase().includes(query) || + r.repoUrl?.toLowerCase().includes(query) + ) + }, [repos, searchQuery]) + + // 选中的仓库 + const selectedRepo = useMemo(() => { + if (!selectedId || !repos) return null + return repos.find((r) => r.id === selectedId) || null + }, [selectedId, repos]) + + const resetCreateForm = () => { + setNewName("") + setNewRepoUrl("") + } + + const resetEditForm = () => { + setEditingRepo(null) + setEditRepoUrl("") + } + + const handleCreateSubmit = (event: FormEvent) => { + event.preventDefault() + const name = newName.trim() + const repoUrl = newRepoUrl.trim() + if (!name || !repoUrl) return + + createMutation.mutate( + { name, repoUrl }, + { + onSuccess: () => { + resetCreateForm() + setCreateDialogOpen(false) + }, + } + ) + } + + const handleRefresh = (repoId: number) => { + refreshMutation.mutate(repoId) + } + + const handleDelete = (repo: NucleiRepo) => { + setRepoToDelete(repo) + setDeleteDialogOpen(true) + } + + const confirmDelete = () => { + if (!repoToDelete) return + deleteMutation.mutate(repoToDelete.id, { + onSuccess: () => { + if (selectedId === repoToDelete.id) { + setSelectedId(null) + } + setDeleteDialogOpen(false) + setRepoToDelete(null) + }, + }) + } + + const openEditDialog = (repo: NucleiRepo) => { + setEditingRepo(repo) + setEditRepoUrl(repo.repoUrl || "") + setEditDialogOpen(true) + } + + const handleEditSubmit = (event: FormEvent) => { + event.preventDefault() + if (!editingRepo) return + const repoUrl = editRepoUrl.trim() + if (!repoUrl) return + + updateMutation.mutate( + { id: editingRepo.id, repoUrl }, + { + onSuccess: () => { + resetEditForm() + setEditDialogOpen(false) + }, + } + ) + } + + // 加载状态 + if (isLoading) { + return <MasterDetailSkeleton title="Nuclei 模板仓库" listItemCount={3} /> + } + + return ( + <div className="flex flex-col h-full"> + {/* 顶部:标题 + 搜索 + 新增按钮 */} + <div className="flex items-center justify-between gap-4 px-4 py-4 lg:px-6"> + <h1 className="text-2xl font-bold shrink-0">Nuclei 模板仓库</h1> + <div className="flex items-center gap-2 flex-1 max-w-md"> + <div className="relative flex-1"> + <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> + <Input + placeholder="搜索仓库..." + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + className="pl-8" + /> + </div> + </div> + <Button onClick={() => setCreateDialogOpen(true)}> + 新增模板仓库 + </Button> + </div> + + <Separator /> + + {/* 主体:左侧列表 + 右侧详情 */} + <div className="flex flex-1 min-h-0"> + {/* 左侧:仓库列表 */} + <div className="w-72 lg:w-80 border-r flex flex-col"> + <div className="px-4 py-3 border-b"> + <h2 className="text-sm font-medium text-muted-foreground"> + 仓库列表 ({filteredRepos.length}) + </h2> + </div> + <ScrollArea className="flex-1"> + {isLoading ? ( + <div className="p-4 text-sm text-muted-foreground">加载中...</div> + ) : isError ? ( + <div className="p-4 text-sm text-red-500">加载失败</div> + ) : filteredRepos.length === 0 ? ( + <div className="p-4 text-sm text-muted-foreground"> + {searchQuery ? "未找到匹配的仓库" : "暂无仓库,请先新增"} + </div> + ) : ( + <div className="p-2"> + {filteredRepos.map((repo) => ( + <button + key={repo.id} + onClick={() => setSelectedId(repo.id)} + className={cn( + "w-full text-left rounded-lg px-3 py-2.5 transition-colors", + selectedId === repo.id + ? "bg-primary/10 text-primary" + : "hover:bg-muted" + )} + > + <div className="flex items-center gap-2"> + <span className="font-medium text-sm truncate flex-1"> + {repo.name} + </span> + {repo.lastSyncedAt ? ( + <Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 text-xs shrink-0"> + 已同步 + </Badge> + ) : ( + <Badge variant="outline" className="text-xs shrink-0"> + 未同步 + </Badge> + )} + </div> + <div className="text-xs text-muted-foreground mt-0.5 truncate"> + {repo.lastSyncedAt + ? `同步于 ${formatDateTime(repo.lastSyncedAt)}` + : "尚未同步"} + </div> + </button> + ))} + </div> + )} + </ScrollArea> + </div> + + {/* 右侧:仓库详情 */} + <div className="flex-1 flex flex-col min-w-0"> + {selectedRepo ? ( + <> + {/* 详情头部 */} + <div className="px-6 py-4 border-b"> + <div className="flex items-start gap-3"> + <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 shrink-0"> + <GitBranch className="h-5 w-5 text-primary" /> + </div> + <div className="min-w-0 flex-1"> + <div className="flex items-center gap-2"> + <h2 className="text-lg font-semibold truncate"> + {selectedRepo.name} + </h2> + {selectedRepo.lastSyncedAt ? ( + <Badge variant="outline" className="bg-green-50 text-green-700 border-green-200"> + 已同步 + </Badge> + ) : ( + <Badge variant="outline">未同步</Badge> + )} + </div> + </div> + </div> + </div> + + {/* 详情内容 */} + <ScrollArea className="flex-1"> + <div className="p-6 space-y-6"> + {/* 统计信息 */} + <div className="rounded-lg border"> + <div className="grid grid-cols-2 divide-x"> + <div className="p-4"> + <div className="text-xs text-muted-foreground">状态</div> + <div className="text-lg font-semibold mt-1"> + {selectedRepo.lastSyncedAt ? "已同步" : "未同步"} + </div> + </div> + <div className="p-4"> + <div className="text-xs text-muted-foreground">最后同步</div> + <div className="text-lg font-semibold mt-1"> + {selectedRepo.lastSyncedAt + ? new Date(selectedRepo.lastSyncedAt).toLocaleString("zh-CN") + : "-"} + </div> + </div> + </div> + <Separator /> + <div className="p-4 space-y-3"> + <div className="text-sm"> + <span className="text-muted-foreground">Git 地址</span> + <div className="font-mono text-xs mt-1 break-all bg-muted p-2 rounded"> + {selectedRepo.repoUrl} + </div> + </div> + {selectedRepo.localPath && ( + <div className="text-sm"> + <span className="text-muted-foreground">本地路径</span> + <div className="font-mono text-xs mt-1 break-all bg-muted p-2 rounded"> + {selectedRepo.localPath} + </div> + </div> + )} + {selectedRepo.commitHash && ( + <div className="text-sm"> + <span className="text-muted-foreground">Commit</span> + <div className="font-mono text-xs mt-1 break-all bg-muted p-2 rounded"> + {selectedRepo.commitHash} + </div> + </div> + )} + </div> + </div> + </div> + </ScrollArea> + + {/* 操作按钮 */} + <div className="px-6 py-4 border-t flex items-center gap-2"> + <Button + variant="outline" + size="sm" + onClick={() => handleRefresh(selectedRepo.id)} + disabled={refreshMutation.isPending} + > + <RefreshCw className={cn("h-4 w-4 mr-1.5", refreshMutation.isPending && "animate-spin")} /> + {refreshMutation.isPending ? "同步中..." : "同步仓库"} + </Button> + <Button + variant="outline" + size="sm" + onClick={() => openEditDialog(selectedRepo)} + > + <Settings className="h-4 w-4 mr-1.5" /> + 编辑配置 + </Button> + <Link href={`/tools/nuclei/${selectedRepo.id}/`}> + <Button size="sm"> + <FolderOpen className="h-4 w-4 mr-1.5" /> + 管理模板 + </Button> + </Link> + <div className="flex-1" /> + <Button + variant="outline" + size="sm" + className="text-destructive hover:text-destructive" + onClick={() => handleDelete(selectedRepo)} + disabled={deleteMutation.isPending} + > + <Trash2 className="h-4 w-4 mr-1.5" /> + 删除 + </Button> + </div> + </> + ) : ( + // 未选中状态 + <div className="flex-1 flex items-center justify-center"> + <div className="text-center text-muted-foreground"> + <GitBranch className="h-12 w-12 mx-auto mb-3 opacity-50" /> + <p className="text-sm">选择左侧仓库查看详情</p> + </div> + </div> + )} + </div> + </div> + + <Dialog open={createDialogOpen} onOpenChange={(open) => { + setCreateDialogOpen(open) + if (!open) { + resetCreateForm() + } + }}> + <DialogContent className="sm:max-w-md"> + <DialogHeader> + <DialogTitle>新增 Nuclei 模板仓库</DialogTitle> + </DialogHeader> + <form className="space-y-4" onSubmit={handleCreateSubmit}> + <div className="space-y-2"> + <Label htmlFor="nuclei-repo-name">仓库名称</Label> + <Input + id="nuclei-repo-name" + type="text" + placeholder="例如:默认 Nuclei 官方模板" + value={newName} + onChange={(event) => setNewName(event.target.value)} + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="nuclei-repo-url">Git 仓库地址</Label> + <Input + id="nuclei-repo-url" + type="text" + placeholder="例如:https://github.com/projectdiscovery/nuclei-templates.git" + value={newRepoUrl} + onChange={(event) => setNewRepoUrl(event.target.value)} + /> + </div> + + {/* 目前只支持公开仓库,这里不再提供认证方式和凭据配置 */} + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => setCreateDialogOpen(false)} + disabled={createMutation.isPending} + > + 取消 + </Button> + <Button + type="submit" + disabled={!newName.trim() || !newRepoUrl.trim() || createMutation.isPending} + > + {createMutation.isPending ? "创建中..." : "确认新增"} + </Button> + </DialogFooter> + </form> + </DialogContent> + </Dialog> + + <Dialog + open={editDialogOpen} + onOpenChange={(open) => { + setEditDialogOpen(open) + if (!open) { + resetEditForm() + } + }} + > + <DialogContent className="sm:max-w-md"> + <DialogHeader> + <DialogTitle>编辑 Nuclei 仓库配置</DialogTitle> + </DialogHeader> + <form className="space-y-4" onSubmit={handleEditSubmit}> + <div className="space-y-1 text-sm text-muted-foreground"> + <span className="font-medium">仓库名称:</span> + <span>{editingRepo?.name ?? ""}</span> + </div> + + <div className="space-y-2"> + <Label htmlFor="edit-nuclei-repo-url">Git 仓库地址</Label> + <Input + id="edit-nuclei-repo-url" + type="text" + placeholder="例如:https://github.com/projectdiscovery/nuclei-templates.git" + value={editRepoUrl} + onChange={(event) => setEditRepoUrl(event.target.value)} + /> + </div> + + {/* 编辑时也不再支持配置认证方式/凭据,仅允许修改 Git 地址 */} + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => setEditDialogOpen(false)} + disabled={updateMutation.isPending} + > + 取消 + </Button> + <Button + type="submit" + disabled={!editRepoUrl.trim() || updateMutation.isPending} + > + {updateMutation.isPending ? "保存中..." : "保存配置"} + </Button> + </DialogFooter> + </form> + </DialogContent> + </Dialog> + + {/* 删除确认弹窗 */} + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除</AlertDialogTitle> + <AlertDialogDescription> + 确定要删除仓库「{repoToDelete?.name}」吗?此操作无法撤销。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + disabled={deleteMutation.isPending} + > + {deleteMutation.isPending ? "删除中..." : "删除"} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </div> + ) +} diff --git a/frontend/app/tools/page.tsx b/frontend/app/tools/page.tsx new file mode 100644 index 00000000..16aa627c --- /dev/null +++ b/frontend/app/tools/page.tsx @@ -0,0 +1,125 @@ +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { PackageOpen, Settings, ArrowRight } from "lucide-react" +import Link from "next/link" + +/** + * 工具概览页面 + * 显示开源工具和自定义工具的入口 + */ +export default function ToolsPage() { + // 功能模块 + const modules = [ + { + title: "字典管理", + description: "管理目录扫描等使用的字典文件", + href: "/tools/wordlists/", + icon: PackageOpen, + status: "available", + stats: { + total: "-", + active: "-", + }, + }, + { + title: "Nuclei 模板", + description: "浏览本地 Nuclei 模板结构及内容", + href: "/tools/nuclei/", + icon: Settings, + status: "available", + stats: { + total: "-", + active: "-", + }, + }, + ] + + return ( + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + {/* 页面头部 */} + <div className="flex items-center justify-between px-4 lg:px-6"> + <div> + <h2 className="text-2xl font-bold tracking-tight">工具</h2> + <p className="text-muted-foreground"> + 管理与扫描相关的辅助资源,如字典等 + </p> + </div> + </div> + + {/* 统计卡片 */} + <div className="px-4 lg:px-6"> + <div className="grid gap-4 md:grid-cols-2"> + {modules.map((module) => ( + <Card key={module.title} className="relative hover:shadow-lg transition-shadow"> + <CardHeader> + <div className="flex items-center justify-between"> + <div className="flex items-center space-x-2"> + <module.icon className="h-5 w-5" /> + <CardTitle className="text-lg">{module.title}</CardTitle> + </div> + {module.status === "coming-soon" && ( + <span className="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded-full"> + 即将上线 + </span> + )} + </div> + <CardDescription>{module.description}</CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-4"> + {/* 统计信息 */} + <div className="flex items-center gap-6 text-sm"> + <div> + <span className="text-muted-foreground">总数:</span> + <span className="font-semibold ml-1">{module.stats.total}</span> + </div> + <div> + <span className="text-muted-foreground">活跃:</span> + <span className="font-semibold ml-1 text-green-600">{module.stats.active}</span> + </div> + </div> + + {/* 操作按钮 */} + {module.status === "available" ? ( + <Link href={module.href}> + <Button className="w-full"> + 进入管理 + <ArrowRight className="h-4 w-4" /> + </Button> + </Link> + ) : ( + <Button disabled className="w-full"> + 敬请期待 + </Button> + )} + </div> + </CardContent> + </Card> + ))} + </div> + </div> + + {/* 快速操作 */} + <div className="px-4 lg:px-6"> + <Card> + <CardHeader> + <CardTitle>快速操作</CardTitle> + <CardDescription> + 常用的工具操作 + </CardDescription> + </CardHeader> + <CardContent> + <div className="flex flex-wrap gap-2"> + <Link href="/tools/wordlists/"> + <Button variant="outline" size="sm"> + <PackageOpen className="h-4 w-4" /> + 字典管理 + </Button> + </Link> + </div> + </CardContent> + </Card> + </div> + </div> + ) +} diff --git a/frontend/app/tools/wordlists/page.tsx b/frontend/app/tools/wordlists/page.tsx new file mode 100644 index 00000000..3ee9b004 --- /dev/null +++ b/frontend/app/tools/wordlists/page.tsx @@ -0,0 +1,291 @@ +"use client" + +import { useState, useMemo } from "react" +import { FileText, Search, Copy, Download, Trash2, Pencil } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { useWordlists, useDeleteWordlist } from "@/hooks/use-wordlists" +import { WordlistEditDialog } from "@/components/tools/wordlist-edit-dialog" +import { WordlistUploadDialog } from "@/components/tools/wordlist-upload-dialog" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { toast } from "sonner" +import { cn } from "@/lib/utils" +import type { Wordlist } from "@/types/wordlist.types" +import { MasterDetailSkeleton } from "@/components/ui/master-detail-skeleton" + +export default function WordlistsPage() { + const [selectedId, setSelectedId] = useState<number | null>(null) + const [searchQuery, setSearchQuery] = useState("") + const [editingWordlist, setEditingWordlist] = useState<Wordlist | null>(null) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [wordlistToDelete, setWordlistToDelete] = useState<Wordlist | null>(null) + + const { data, isLoading } = useWordlists({ page: 1, pageSize: 100 }) + const deleteMutation = useDeleteWordlist() + + // 过滤字典列表 + const filteredWordlists = useMemo(() => { + if (!data?.results) return [] + if (!searchQuery.trim()) return data.results + const query = searchQuery.toLowerCase() + return data.results.filter( + (w) => + w.name.toLowerCase().includes(query) || + w.description?.toLowerCase().includes(query) + ) + }, [data?.results, searchQuery]) + + // 选中的字典 + const selectedWordlist = useMemo(() => { + if (!selectedId || !data?.results) return null + return data.results.find((w) => w.id === selectedId) || null + }, [selectedId, data?.results]) + + const handleEdit = (wordlist: Wordlist) => { + setEditingWordlist(wordlist) + setIsEditDialogOpen(true) + } + + const handleCopyId = (id: number) => { + navigator.clipboard.writeText(String(id)) + toast.success("ID 已复制到剪贴板") + } + + const handleDelete = (wordlist: Wordlist) => { + setWordlistToDelete(wordlist) + setDeleteDialogOpen(true) + } + + const confirmDelete = () => { + if (!wordlistToDelete) return + deleteMutation.mutate(wordlistToDelete.id, { + onSuccess: () => { + if (selectedId === wordlistToDelete.id) { + setSelectedId(null) + } + setDeleteDialogOpen(false) + setWordlistToDelete(null) + }, + }) + } + + const formatFileSize = (bytes?: number) => { + if (bytes === undefined) return "-" + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` + } + + // 加载状态 + if (isLoading) { + return <MasterDetailSkeleton title="字典管理" listItemCount={5} /> + } + + return ( + <div className="flex flex-col h-full"> + {/* 顶部:标题 + 搜索 + 上传按钮 */} + <div className="flex items-center justify-between gap-4 px-4 py-4 lg:px-6"> + <h1 className="text-2xl font-bold shrink-0">字典管理</h1> + <div className="flex items-center gap-2 flex-1 max-w-md"> + <div className="relative flex-1"> + <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> + <Input + placeholder="搜索字典..." + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + className="pl-8" + /> + </div> + </div> + <WordlistUploadDialog /> + </div> + + <Separator /> + + {/* 主体:左侧列表 + 右侧详情 */} + <div className="flex flex-1 min-h-0"> + {/* 左侧:字典列表 */} + <div className="w-72 lg:w-80 border-r flex flex-col"> + <div className="px-4 py-3 border-b"> + <h2 className="text-sm font-medium text-muted-foreground"> + 字典列表 ({filteredWordlists.length}) + </h2> + </div> + <ScrollArea className="flex-1"> + {isLoading ? ( + <div className="p-4 text-sm text-muted-foreground">加载中...</div> + ) : filteredWordlists.length === 0 ? ( + <div className="p-4 text-sm text-muted-foreground"> + {searchQuery ? "未找到匹配的字典" : "暂无字典,请先上传"} + </div> + ) : ( + <div className="p-2"> + {filteredWordlists.map((wordlist) => ( + <button + key={wordlist.id} + onClick={() => setSelectedId(wordlist.id)} + className={cn( + "w-full text-left rounded-lg px-3 py-2.5 transition-colors", + selectedId === wordlist.id + ? "bg-primary/10 text-primary" + : "hover:bg-muted" + )} + > + <div className="font-medium text-sm truncate"> + {wordlist.name} + </div> + <div className="text-xs text-muted-foreground mt-0.5"> + {wordlist.lineCount?.toLocaleString() ?? "-"} 行 · {formatFileSize(wordlist.fileSize)} + </div> + </button> + ))} + </div> + )} + </ScrollArea> + </div> + + {/* 右侧:字典详情 */} + <div className="flex-1 flex flex-col min-w-0"> + {selectedWordlist ? ( + <> + {/* 详情头部 */} + <div className="px-6 py-4 border-b"> + <div className="flex items-start gap-3"> + <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 shrink-0"> + <FileText className="h-5 w-5 text-primary" /> + </div> + <div className="min-w-0 flex-1"> + <h2 className="text-lg font-semibold truncate"> + {selectedWordlist.name} + </h2> + {selectedWordlist.description && ( + <p className="text-sm text-muted-foreground mt-0.5"> + {selectedWordlist.description} + </p> + )} + </div> + </div> + </div> + + {/* 详情内容 */} + <ScrollArea className="flex-1"> + <div className="p-6 space-y-6"> + {/* 基本信息 */} + <div className="rounded-lg border"> + <div className="grid grid-cols-2 divide-x"> + <div className="p-4"> + <div className="text-xs text-muted-foreground">行数</div> + <div className="text-lg font-semibold mt-1"> + {selectedWordlist.lineCount?.toLocaleString() ?? "-"} + </div> + </div> + <div className="p-4"> + <div className="text-xs text-muted-foreground">大小</div> + <div className="text-lg font-semibold mt-1"> + {formatFileSize(selectedWordlist.fileSize)} + </div> + </div> + </div> + <Separator /> + <div className="p-4 space-y-3"> + <div className="flex justify-between text-sm"> + <span className="text-muted-foreground">ID</span> + <span className="font-mono">{selectedWordlist.id}</span> + </div> + <div className="flex justify-between text-sm"> + <span className="text-muted-foreground">更新时间</span> + <span> + {new Date(selectedWordlist.updatedAt).toLocaleString("zh-CN")} + </span> + </div> + {selectedWordlist.fileHash && ( + <div className="text-sm"> + <span className="text-muted-foreground">Hash</span> + <div className="font-mono text-xs mt-1 break-all bg-muted p-2 rounded"> + {selectedWordlist.fileHash} + </div> + </div> + )} + </div> + </div> + </div> + </ScrollArea> + + {/* 操作按钮 */} + <div className="px-6 py-4 border-t flex items-center gap-2"> + <Button + variant="outline" + size="sm" + onClick={() => handleEdit(selectedWordlist)} + > + <Pencil className="h-4 w-4 mr-1.5" /> + 编辑内容 + </Button> + <div className="flex-1" /> + <Button + variant="outline" + size="sm" + className="text-destructive hover:text-destructive" + onClick={() => handleDelete(selectedWordlist)} + disabled={deleteMutation.isPending} + > + <Trash2 className="h-4 w-4 mr-1.5" /> + 删除 + </Button> + </div> + </> + ) : ( + // 未选中状态 + <div className="flex-1 flex items-center justify-center"> + <div className="text-center text-muted-foreground"> + <FileText className="h-12 w-12 mx-auto mb-3 opacity-50" /> + <p className="text-sm">选择左侧字典查看详情</p> + </div> + </div> + )} + </div> + </div> + + {/* 编辑弹窗 */} + <WordlistEditDialog + wordlist={editingWordlist} + open={isEditDialogOpen} + onOpenChange={setIsEditDialogOpen} + /> + + {/* 删除确认弹窗 */} + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除</AlertDialogTitle> + <AlertDialogDescription> + 确定要删除字典「{wordlistToDelete?.name}」吗?此操作无法撤销。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + disabled={deleteMutation.isPending} + > + {deleteMutation.isPending ? "删除中..." : "删除"} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </div> + ) +} diff --git a/frontend/app/vulnerabilities/page.tsx b/frontend/app/vulnerabilities/page.tsx new file mode 100644 index 00000000..6cadf5d2 --- /dev/null +++ b/frontend/app/vulnerabilities/page.tsx @@ -0,0 +1,27 @@ +"use client" + +import React from "react" +import { VulnerabilitiesDetailView } from "@/components/vulnerabilities" + +/** + * 全部漏洞页面 + * 显示系统中所有漏洞 + */ +export default function VulnerabilitiesPage() { + return ( + <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> + {/* 页面头部 */} + <div className="flex items-center justify-between px-4 lg:px-6"> + <div> + <h2 className="text-2xl font-bold tracking-tight">漏洞管理</h2> + <p className="text-muted-foreground">查看和管理所有扫描发现的漏洞</p> + </div> + </div> + + {/* 漏洞列表 */} + <div className="px-4 lg:px-6"> + <VulnerabilitiesDetailView /> + </div> + </div> + ) +} diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 00000000..b7b9791c --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx new file mode 100644 index 00000000..89095ab8 --- /dev/null +++ b/frontend/components/app-sidebar.tsx @@ -0,0 +1,258 @@ +"use client" // 标记为客户端组件,可以使用浏览器 API 和交互功能 + +// 导入 React 库 +import type * as React from "react" +// 导入 Tabler Icons 图标库中的各种图标 +import { + IconDashboard, // 仪表板图标 + IconHelp, // 帮助图标 + IconInnerShadowTop, // 内阴影图标 + IconListDetails, // 列表详情图标 + IconSettings, // 设置图标 + IconUsers, // 用户图标 + IconChevronRight, // 右箭头图标 + IconRadar, // 雷达扫描图标 + IconTool, // 工具图标 + IconFlask, // 实验瓶图标 + IconServer, // 服务器图标 + IconBug, // 漏洞图标 +} from "@tabler/icons-react" +// 导入路径名 hook +import { usePathname } from "next/navigation" +// 导入 Link 组件 +import Link from "next/link" + +// 导入自定义导航组件 +import { NavSystem } from "@/components/nav-system" +import { NavSecondary } from "@/components/nav-secondary" +import { NavUser } from "@/components/nav-user" +// 导入侧边栏 UI 组件 +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarRail, +} from "@/components/ui/sidebar" +// 导入折叠组件 +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" + +// 定义侧边栏的数据结构 +const data = { + // 用户信息 + user: { + name: "admin", + email: "admin@admin.com", + avatar: "", + }, + // 主导航菜单项 + navMain: [ + { + title: "仪表盘", // 仪表板 + url: "/dashboard/", + icon: IconDashboard, + }, + { + title: "组织", // 组织 + url: "/organization/", + icon: IconUsers, + }, + { + title: "目标", // 目标 + url: "/target/", + icon: IconListDetails, + }, + { + title: "漏洞", // 漏洞 + url: "/vulnerabilities/", + icon: IconBug, + }, + { + title: "扫描", // 扫描 + url: "/scan/", + icon: IconRadar, + items: [ + { + title: "扫描历史", // 扫描历史 + url: "/scan/history/", + }, + { + title: "定时扫描", // 定时扫描 + url: "/scan/scheduled/", + }, + { + title: "扫描引擎", // 扫描引擎 + url: "/scan/engine/", + }, + ], + }, + { + title: "工具", // 工具 + url: "/tools/", + icon: IconTool, + items: [ + { + title: "字典管理", // 字典管理 + url: "/tools/wordlists/", + }, + { + title: "Nuclei 模板", // Nuclei 模板 + url: "/tools/nuclei/", + }, + ], + }, + // 测试中心相关菜单已移除 + ], + // 次要导航菜单项 + navSecondary: [ + { + title: "Get Help", // 获取帮助 + url: "#", + icon: IconHelp, + }, + ], + // 系统设置相关菜单项 + documents: [ + { + name: "扫描节点", + url: "/settings/workers/", + icon: IconServer, + }, + { + name: "通知设置", // 通知设置 + url: "/settings/notifications/", + icon: IconSettings, + }, + ], +} + +/** + * 应用侧边栏组件 + * 显示应用的主要导航菜单,包括用户信息、主菜单、文档和次要菜单 + * 支持子菜单的展开和折叠功能 + * @param props - Sidebar 组件的所有属性 + */ +export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { + const pathname = usePathname() + const normalize = (p: string) => (p !== "/" && p.endsWith("/") ? p.slice(0, -1) : p) + const current = normalize(pathname) + + return ( + // collapsible="icon" 表示侧边栏可以折叠为仅图标模式 + <Sidebar collapsible="icon" {...props}> + {/* 侧边栏头部 */} + <SidebarHeader> + <SidebarMenu> + <SidebarMenuItem> + {/* + 侧边栏菜单按钮,作为链接使用 + data-[slot=sidebar-menu-button]:!p-1.5 设置内边距 + */} + <SidebarMenuButton + asChild + className="data-[slot=sidebar-menu-button]:!p-1.5" + > + <Link href="/"> + {/* 公司 Logo 图标 */} + <IconInnerShadowTop className="!size-5" /> + {/* 公司名称 */} + <span className="text-base font-semibold">XingRin</span> + </Link> + </SidebarMenuButton> + </SidebarMenuItem> + </SidebarMenu> + </SidebarHeader> + + {/* 侧边栏主要内容区域 */} + <SidebarContent> + {/* 主导航菜单 */} + <SidebarGroup> + <SidebarGroupLabel>主要功能</SidebarGroupLabel> + <SidebarGroupContent> + <SidebarMenu> + {data.navMain.map((item) => { + const navUrl = normalize(item.url) + const isActive = navUrl === "/" ? current === "/" : current === navUrl || current.startsWith(navUrl + "/") + const hasSubItems = item.items && item.items.length > 0 + + if (!hasSubItems) { + // 无子菜单的普通菜单项 + return ( + <SidebarMenuItem key={item.title}> + <SidebarMenuButton asChild isActive={isActive}> + <Link href={item.url}> + <item.icon /> + <span>{item.title}</span> + </Link> + </SidebarMenuButton> + </SidebarMenuItem> + ) + } + + // 有子菜单的折叠菜单项 + return ( + <Collapsible + key={item.title} + defaultOpen={isActive} + className="group/collapsible" + > + <SidebarMenuItem> + <CollapsibleTrigger asChild> + <SidebarMenuButton isActive={isActive}> + <item.icon /> + <span>{item.title}</span> + <IconChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" /> + </SidebarMenuButton> + </CollapsibleTrigger> + <CollapsibleContent> + <SidebarMenuSub> + {item.items?.map((subItem) => ( + <SidebarMenuSubItem key={subItem.title}> + <SidebarMenuSubButton + asChild + isActive={current === normalize(subItem.url)} + > + <Link href={subItem.url}> + <span>{subItem.title}</span> + </Link> + </SidebarMenuSubButton> + </SidebarMenuSubItem> + ))} + </SidebarMenuSub> + </CollapsibleContent> + </SidebarMenuItem> + </Collapsible> + ) + })} + </SidebarMenu> + </SidebarGroupContent> + </SidebarGroup> + + {/* 系统设置导航菜单 */} + <NavSystem items={data.documents} /> + {/* 次要导航菜单,使用 mt-auto 推到底部 */} + <NavSecondary items={data.navSecondary} className="mt-auto" /> + </SidebarContent> + + {/* 侧边栏底部 */} + <SidebarFooter> + {/* 用户信息组件 */} + <NavUser user={data.user} /> + </SidebarFooter> + <SidebarRail /> + </Sidebar> + ) +} diff --git a/frontend/components/auth/auth-guard.tsx b/frontend/components/auth/auth-guard.tsx new file mode 100644 index 00000000..f9dd4aa3 --- /dev/null +++ b/frontend/components/auth/auth-guard.tsx @@ -0,0 +1,65 @@ +"use client" + +import React from "react" +import { useRouter, usePathname } from "next/navigation" +import { useAuth } from "@/hooks/use-auth" +import { LoadingState } from "@/components/loading-spinner" + +// 不需要登录的公开路由 +const PUBLIC_ROUTES = ["/login"] +// 通过环境变量跳过认证 (pnpm dev:noauth) +const SKIP_AUTH = process.env.NEXT_PUBLIC_SKIP_AUTH === 'true' + +interface AuthGuardProps { + children: React.ReactNode +} + +/** + * 认证守卫组件 + * 保护需要登录的路由 + */ +export function AuthGuard({ children }: AuthGuardProps) { + const router = useRouter() + const pathname = usePathname() + const { data: auth, isLoading } = useAuth() + + // 检查是否是公开路由 + const isPublicRoute = PUBLIC_ROUTES.some((route) => + pathname.startsWith(route) + ) + + React.useEffect(() => { + // 跳过认证模式不处理 + if (SKIP_AUTH) return + // 加载中或公开路由不处理 + if (isLoading || isPublicRoute) return + + // 未登录跳转登录页 + if (!auth?.authenticated) { + router.push("/login/") + } + }, [auth, isLoading, isPublicRoute, router]) + + // 跳过认证模式 + if (SKIP_AUTH) { + return <>{children}</> + } + + // 加载中显示 loading + if (isLoading) { + return <LoadingState message="loading..." /> + } + + // 公开路由直接渲染 + if (isPublicRoute) { + return <>{children}</> + } + + // 未登录不渲染内容(等待跳转) + if (!auth?.authenticated) { + return <LoadingState message="loading..." /> + } + + // 已登录渲染内容 + return <>{children}</> +} diff --git a/frontend/components/auth/auth-layout.tsx b/frontend/components/auth/auth-layout.tsx new file mode 100644 index 00000000..7c0d7f00 --- /dev/null +++ b/frontend/components/auth/auth-layout.tsx @@ -0,0 +1,81 @@ +"use client" + +import React from "react" +import { usePathname } from "next/navigation" +import { AppSidebar } from "@/components/app-sidebar" +import { SiteHeader } from "@/components/site-header" +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" +import { Toaster } from "@/components/ui/sonner" +import { LoadingState } from "@/components/loading-spinner" +import { Suspense } from "react" +import { useAuth } from "@/hooks/use-auth" +import { useRouter } from "next/navigation" + +// 不需要登录的公开路由 +const PUBLIC_ROUTES = ["/login"] + +interface AuthLayoutProps { + children: React.ReactNode +} + +/** + * 认证布局组件 + * 根据登录状态和路由决定是否显示侧边栏 + */ +export function AuthLayout({ children }: AuthLayoutProps) { + const pathname = usePathname() + const router = useRouter() + const { data: auth, isLoading } = useAuth() + + // 检查是否是公开路由(登录页) + const isPublicRoute = PUBLIC_ROUTES.some((route) => + pathname.startsWith(route) + ) + + // 未登录跳转登录页(useEffect 必须在所有条件返回之前) + React.useEffect(() => { + if (!isLoading && !auth?.authenticated && !isPublicRoute) { + router.push("/login/") + } + }, [auth, isLoading, isPublicRoute, router]) + + // 如果是登录页,直接渲染内容(不带侧边栏) + if (isPublicRoute) { + return ( + <> + {children} + <Toaster /> + </> + ) + } + + // 加载中或未登录 + if (isLoading || !auth?.authenticated) { + return <LoadingState message="loading..." /> + } + + // 已登录显示完整布局(带侧边栏) + return ( + <SidebarProvider + style={ + { + "--sidebar-width": "calc(var(--spacing) * 70)", + "--header-height": "calc(var(--spacing) * 11)", + } as React.CSSProperties + } + > + <AppSidebar /> + <SidebarInset className="flex min-h-0 flex-col h-svh"> + <SiteHeader /> + <div className="flex flex-col flex-1 min-h-0 overflow-y-auto"> + <div className="@container/main flex-1 min-h-0 flex flex-col gap-2"> + <Suspense fallback={<LoadingState message="页面加载中..." />}> + {children} + </Suspense> + <Toaster /> + </div> + </div> + </SidebarInset> + </SidebarProvider> + ) +} diff --git a/frontend/components/auth/change-password-dialog.tsx b/frontend/components/auth/change-password-dialog.tsx new file mode 100644 index 00000000..89d1ea68 --- /dev/null +++ b/frontend/components/auth/change-password-dialog.tsx @@ -0,0 +1,119 @@ +"use client" + +import React from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { useChangePassword } from "@/hooks/use-auth" +import { getErrorMessage } from "@/lib/api-client" + +interface ChangePasswordDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function ChangePasswordDialog({ open, onOpenChange }: ChangePasswordDialogProps) { + const [oldPassword, setOldPassword] = React.useState("") + const [newPassword, setNewPassword] = React.useState("") + const [confirmPassword, setConfirmPassword] = React.useState("") + const [error, setError] = React.useState("") + + const { mutate: changePassword, isPending } = useChangePassword() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + setError("") + + if (newPassword !== confirmPassword) { + setError("新密码与确认密码不一致") + return + } + + if (newPassword.length < 4) { + setError("新密码长度至少 4 位") + return + } + + changePassword( + { oldPassword, newPassword }, + { + onSuccess: () => { + onOpenChange(false) + setOldPassword("") + setNewPassword("") + setConfirmPassword("") + }, + onError: (err: unknown) => { + setError(getErrorMessage(err)) + }, + } + ) + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[400px]"> + <DialogHeader> + <DialogTitle>修改密码</DialogTitle> + <DialogDescription> + 请输入当前密码和新密码 + </DialogDescription> + </DialogHeader> + <form onSubmit={handleSubmit}> + <div className="grid gap-4 py-4"> + <div className="grid gap-2"> + <Label htmlFor="oldPassword">当前密码</Label> + <Input + id="oldPassword" + type="password" + value={oldPassword} + onChange={(e) => setOldPassword(e.target.value)} + required + autoFocus + /> + </div> + <div className="grid gap-2"> + <Label htmlFor="newPassword">新密码</Label> + <Input + id="newPassword" + type="password" + value={newPassword} + onChange={(e) => setNewPassword(e.target.value)} + required + /> + </div> + <div className="grid gap-2"> + <Label htmlFor="confirmPassword">确认新密码</Label> + <Input + id="confirmPassword" + type="password" + value={confirmPassword} + onChange={(e) => setConfirmPassword(e.target.value)} + required + /> + </div> + {error && ( + <p className="text-sm text-destructive">{error}</p> + )} + </div> + <DialogFooter> + <Button type="button" variant="outline" onClick={() => onOpenChange(false)}> + 取消 + </Button> + <Button type="submit" disabled={isPending}> + {isPending ? "保存中..." : "保存"} + </Button> + </DialogFooter> + </form> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/auth/index.ts b/frontend/components/auth/index.ts new file mode 100644 index 00000000..d420b5a6 --- /dev/null +++ b/frontend/components/auth/index.ts @@ -0,0 +1,3 @@ +export { AuthGuard } from "./auth-guard" +export { AuthLayout } from "./auth-layout" +export { ChangePasswordDialog } from "./change-password-dialog" diff --git a/frontend/components/color-theme-switcher.tsx b/frontend/components/color-theme-switcher.tsx new file mode 100644 index 00000000..e084ba70 --- /dev/null +++ b/frontend/components/color-theme-switcher.tsx @@ -0,0 +1,62 @@ +"use client" + +import { useColorTheme, COLOR_THEMES, ColorThemeId } from "@/hooks/use-color-theme" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Button } from "@/components/ui/button" +import { IconPalette, IconCheck } from "@tabler/icons-react" + +/** + * 颜色主题切换器 + */ +export function ColorThemeSwitcher() { + const { theme, setTheme, mounted } = useColorTheme() + + if (!mounted) { + return ( + <Button variant="ghost" size="icon" className="h-8 w-8"> + <IconPalette className="h-4 w-4" /> + </Button> + ) + } + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" size="icon" className="h-8 w-8"> + <IconPalette className="h-4 w-4" /> + <span className="sr-only">切换主题色</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {COLOR_THEMES.map((t) => ( + <DropdownMenuItem + key={t.id} + onClick={() => { + console.log('切换主题到:', t.id) + setTheme(t.id as ColorThemeId) + }} + className="flex items-center gap-2" + > + {/* 颜色预览色块 */} + <div className="flex items-center gap-1"> + {t.colors.map((c, i) => ( + <span + key={i} + className="h-4 w-4 rounded border border-black/10 dark:border-white/20" + style={{ backgroundColor: c }} + /> + ))} + </div> + <span>{t.name}</span> + {theme === t.id && <IconCheck className="ml-auto h-4 w-4" />} + </DropdownMenuItem> + ))} + </DropdownMenuContent> + </DropdownMenu> + ) +} diff --git a/frontend/components/dashboard/asset-distribution-chart.tsx b/frontend/components/dashboard/asset-distribution-chart.tsx new file mode 100644 index 00000000..1b952ad5 --- /dev/null +++ b/frontend/components/dashboard/asset-distribution-chart.tsx @@ -0,0 +1,125 @@ +"use client" + +import { Bar, BarChart, Cell, LabelList, XAxis, YAxis } from "recharts" +import { useAssetStatistics } from "@/hooks/use-dashboard" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart" +import { Skeleton } from "@/components/ui/skeleton" + +// 使用 CSS 变量,跟随主题变化 +const COLORS = { + subdomain: "var(--chart-1)", + ip: "var(--chart-2)", + endpoint: "var(--chart-3)", + website: "var(--chart-4)", +} + +const chartConfig = { + count: { + label: "数量", + }, + subdomain: { + label: "子域名", + color: COLORS.subdomain, + }, + ip: { + label: "IP地址", + color: COLORS.ip, + }, + endpoint: { + label: "端点", + color: COLORS.endpoint, + }, + website: { + label: "网站", + color: COLORS.website, + }, +} satisfies ChartConfig + +export function AssetDistributionChart() { + const { data, isLoading } = useAssetStatistics() + + const chartData = [ + { name: "子域名", count: data?.totalSubdomains ?? 0, fill: COLORS.subdomain }, + { name: "IP地址", count: data?.totalIps ?? 0, fill: COLORS.ip }, + { name: "端点", count: data?.totalEndpoints ?? 0, fill: COLORS.endpoint }, + { name: "网站", count: data?.totalWebsites ?? 0, fill: COLORS.website }, + ] + + const total = chartData.reduce((sum, item) => sum + item.count, 0) + + return ( + <Card> + <CardHeader> + <CardTitle>资产分布</CardTitle> + <CardDescription>各类资产数量统计</CardDescription> + </CardHeader> + <CardContent> + {isLoading ? ( + <div className="space-y-4"> + <Skeleton className="h-8 w-full" /> + <Skeleton className="h-8 w-4/5" /> + <Skeleton className="h-8 w-3/5" /> + <Skeleton className="h-8 w-2/5" /> + </div> + ) : ( + <> + <ChartContainer config={chartConfig} className="aspect-auto h-[160px] w-full"> + <BarChart + accessibilityLayer + data={chartData} + layout="vertical" + margin={{ left: 0, right: 30 }} + > + <YAxis + dataKey="name" + type="category" + tickLine={false} + tickMargin={10} + axisLine={false} + width={50} + /> + <XAxis dataKey="count" type="number" hide /> + <ChartTooltip + cursor={false} + content={<ChartTooltipContent hideLabel />} + /> + <Bar + dataKey="count" + layout="vertical" + radius={4} + > + {chartData.map((entry, index) => ( + <Cell key={`cell-${index}`} fill={entry.fill} /> + ))} + <LabelList + dataKey="count" + position="right" + offset={8} + className="fill-foreground" + fontSize={12} + /> + </Bar> + </BarChart> + </ChartContainer> + <div className="mt-3 pt-3 border-t flex items-center justify-end gap-1.5 text-sm"> + <span className="text-muted-foreground">资产总计:</span> + <span className="font-semibold">{total}</span> + </div> + </> + )} + </CardContent> + </Card> + ) +} diff --git a/frontend/components/dashboard/asset-trend-chart.tsx b/frontend/components/dashboard/asset-trend-chart.tsx new file mode 100644 index 00000000..5f6abaf2 --- /dev/null +++ b/frontend/components/dashboard/asset-trend-chart.tsx @@ -0,0 +1,297 @@ +"use client" + +import { useState, useMemo } from "react" +import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts" +import { useStatisticsHistory } from "@/hooks/use-dashboard" +import type { StatisticsHistoryItem } from "@/types/dashboard.types" + +/** + * 填充缺失的日期数据,确保始终返回完整的 days 天 + * 以最早一条记录的日期为基准,往前补齐,缺失的日期填充 0 + */ +function fillMissingDates(data: StatisticsHistoryItem[] | undefined, days: number): StatisticsHistoryItem[] { + if (!data || data.length === 0) return [] + + // 构建日期到数据的映射 + const dataMap = new Map(data.map(item => [item.date, item])) + + // 找到最早的日期 + const earliestDate = new Date(data[0].date) + + // 生成完整的日期列表(从最早日期往前 days-1 天开始) + const result: StatisticsHistoryItem[] = [] + const startDate = new Date(earliestDate) + startDate.setDate(startDate.getDate() - (days - data.length)) + + for (let i = 0; i < days; i++) { + const currentDate = new Date(startDate) + currentDate.setDate(startDate.getDate() + i) + const dateStr = currentDate.toISOString().split('T')[0] + + const existing = dataMap.get(dateStr) + if (existing) { + result.push(existing) + } else { + // 缺失的日期填充 0 + result.push({ + date: dateStr, + totalTargets: 0, + totalSubdomains: 0, + totalIps: 0, + totalEndpoints: 0, + totalWebsites: 0, + totalVulns: 0, + totalAssets: 0, + }) + } + } + + return result +} +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + ChartConfig, + ChartContainer, +} from "@/components/ui/chart" +import { Skeleton } from "@/components/ui/skeleton" + +const chartConfig = { + totalSubdomains: { + label: "子域名", + color: "#3b82f6", // 蓝色 + }, + totalIps: { + label: "IP", + color: "#f97316", // 橙色 + }, + totalEndpoints: { + label: "端点", + color: "#eab308", // 黄色 + }, + totalWebsites: { + label: "网站", + color: "#22c55e", // 绿色 + }, +} satisfies ChartConfig + +// 数据系列的 key 类型 +type SeriesKey = 'totalSubdomains' | 'totalIps' | 'totalEndpoints' | 'totalWebsites' + +// 所有系列 +const ALL_SERIES: SeriesKey[] = ['totalSubdomains', 'totalIps', 'totalEndpoints', 'totalWebsites'] + +export function AssetTrendChart() { + const { data: rawData, isLoading } = useStatisticsHistory(7) + const [activeData, setActiveData] = useState<StatisticsHistoryItem | null>(null) + + // 可见系列状态(默认全部显示) + const [visibleSeries, setVisibleSeries] = useState<Set<SeriesKey>>(new Set(ALL_SERIES)) + + // 当前悬停的折线 + const [hoveredLine, setHoveredLine] = useState<SeriesKey | null>(null) + + // 切换系列可见性 + const toggleSeries = (key: SeriesKey) => { + setVisibleSeries(prev => { + const next = new Set(prev) + if (next.has(key)) { + // 至少保留一个可见 + if (next.size > 1) { + next.delete(key) + } + } else { + next.add(key) + } + return next + }) + } + + // 填充缺失的日期,确保始终显示7天 + const data = useMemo(() => fillMissingDates(rawData, 7), [rawData]) + + // 格式化日期显示 + const formatDate = (dateStr: string) => { + const date = new Date(dateStr) + return `${date.getMonth() + 1}/${date.getDate()}` + } + + // 获取最新数据(使用原始数据中的最新值) + const latest = rawData && rawData.length > 0 ? rawData[rawData.length - 1] : null + + // 显示的数据:悬停时显示悬停数据,否则显示最新数据 + const displayData = activeData || latest + + return ( + <Card> + <CardHeader> + <CardTitle>资产趋势</CardTitle> + <CardDescription>近 7 天各类资产变化 · 点击折线或图例可隐藏/显示</CardDescription> + </CardHeader> + <CardContent> + {isLoading ? ( + <div className="space-y-4"> + <Skeleton className="h-[180px] w-full" /> + </div> + ) : !rawData || rawData.length === 0 ? ( + <div className="flex items-center justify-center h-[180px] text-muted-foreground"> + 暂无历史数据 + </div> + ) : ( + <> + <ChartContainer config={chartConfig} className="aspect-auto h-[160px] w-full"> + <LineChart + accessibilityLayer + data={data} + margin={{ left: 0, right: 12, top: 12, bottom: 0 }} + onMouseMove={(state) => { + if (state?.activePayload?.[0]?.payload) { + setActiveData(state.activePayload[0].payload) + } + }} + onMouseLeave={() => setActiveData(null)} + > + <CartesianGrid vertical={false} strokeDasharray="3 3" /> + <XAxis + dataKey="date" + tickLine={false} + axisLine={false} + tickMargin={8} + tickFormatter={formatDate} + fontSize={12} + /> + <YAxis + tickLine={false} + axisLine={false} + tickMargin={8} + width={40} + fontSize={12} + /> + {visibleSeries.has('totalSubdomains') && ( + <Line + dataKey="totalSubdomains" + type="monotone" + stroke="var(--color-totalSubdomains)" + strokeWidth={hoveredLine === 'totalSubdomains' ? 4 : 2} + dot={{ r: 3, fill: "var(--color-totalSubdomains)" }} + style={{ cursor: 'pointer', transition: 'stroke-width 0.15s' }} + onClick={() => toggleSeries('totalSubdomains')} + onMouseEnter={() => setHoveredLine('totalSubdomains')} + onMouseLeave={() => setHoveredLine(null)} + /> + )} + {visibleSeries.has('totalIps') && ( + <Line + dataKey="totalIps" + type="monotone" + stroke="var(--color-totalIps)" + strokeWidth={hoveredLine === 'totalIps' ? 4 : 2} + dot={{ r: 3, fill: "var(--color-totalIps)" }} + style={{ cursor: 'pointer', transition: 'stroke-width 0.15s' }} + onClick={() => toggleSeries('totalIps')} + onMouseEnter={() => setHoveredLine('totalIps')} + onMouseLeave={() => setHoveredLine(null)} + /> + )} + {visibleSeries.has('totalEndpoints') && ( + <Line + dataKey="totalEndpoints" + type="monotone" + stroke="var(--color-totalEndpoints)" + strokeWidth={hoveredLine === 'totalEndpoints' ? 4 : 2} + dot={{ r: 3, fill: "var(--color-totalEndpoints)" }} + style={{ cursor: 'pointer', transition: 'stroke-width 0.15s' }} + onClick={() => toggleSeries('totalEndpoints')} + onMouseEnter={() => setHoveredLine('totalEndpoints')} + onMouseLeave={() => setHoveredLine(null)} + /> + )} + {visibleSeries.has('totalWebsites') && ( + <Line + dataKey="totalWebsites" + type="monotone" + stroke="var(--color-totalWebsites)" + strokeWidth={hoveredLine === 'totalWebsites' ? 4 : 2} + dot={{ r: 3, fill: "var(--color-totalWebsites)" }} + style={{ cursor: 'pointer', transition: 'stroke-width 0.15s' }} + onClick={() => toggleSeries('totalWebsites')} + onMouseEnter={() => setHoveredLine('totalWebsites')} + onMouseLeave={() => setHoveredLine(null)} + /> + )} + </LineChart> + </ChartContainer> + <div className="mt-3 pt-3 border-t flex flex-wrap items-center justify-between gap-x-4 gap-y-1.5 text-sm"> + <span className="text-muted-foreground text-xs"> + {activeData ? formatDate(activeData.date) : "当前"} + </span> + <div className="flex items-center gap-3"> + <button + type="button" + onClick={() => toggleSeries('totalSubdomains')} + className={`flex items-center gap-1.5 px-2 py-1 rounded-md transition-all hover:bg-muted ${ + !visibleSeries.has('totalSubdomains') ? 'opacity-40' : '' + }`} + > + <div + className={`h-2.5 w-2.5 rounded-full ${!visibleSeries.has('totalSubdomains') ? 'bg-muted-foreground' : ''}`} + style={{ backgroundColor: visibleSeries.has('totalSubdomains') ? "#3b82f6" : undefined }} + /> + <span className={`text-muted-foreground ${!visibleSeries.has('totalSubdomains') ? 'line-through' : ''}`}>子域名</span> + <span className="font-medium">{displayData?.totalSubdomains ?? 0}</span> + </button> + <button + type="button" + onClick={() => toggleSeries('totalIps')} + className={`flex items-center gap-1.5 px-2 py-1 rounded-md transition-all hover:bg-muted ${ + !visibleSeries.has('totalIps') ? 'opacity-40' : '' + }`} + > + <div + className={`h-2.5 w-2.5 rounded-full ${!visibleSeries.has('totalIps') ? 'bg-muted-foreground' : ''}`} + style={{ backgroundColor: visibleSeries.has('totalIps') ? "#f97316" : undefined }} + /> + <span className={`text-muted-foreground ${!visibleSeries.has('totalIps') ? 'line-through' : ''}`}>IP</span> + <span className="font-medium">{displayData?.totalIps ?? 0}</span> + </button> + <button + type="button" + onClick={() => toggleSeries('totalEndpoints')} + className={`flex items-center gap-1.5 px-2 py-1 rounded-md transition-all hover:bg-muted ${ + !visibleSeries.has('totalEndpoints') ? 'opacity-40' : '' + }`} + > + <div + className={`h-2.5 w-2.5 rounded-full ${!visibleSeries.has('totalEndpoints') ? 'bg-muted-foreground' : ''}`} + style={{ backgroundColor: visibleSeries.has('totalEndpoints') ? "#eab308" : undefined }} + /> + <span className={`text-muted-foreground ${!visibleSeries.has('totalEndpoints') ? 'line-through' : ''}`}>端点</span> + <span className="font-medium">{displayData?.totalEndpoints ?? 0}</span> + </button> + <button + type="button" + onClick={() => toggleSeries('totalWebsites')} + className={`flex items-center gap-1.5 px-2 py-1 rounded-md transition-all hover:bg-muted ${ + !visibleSeries.has('totalWebsites') ? 'opacity-40' : '' + }`} + > + <div + className={`h-2.5 w-2.5 rounded-full ${!visibleSeries.has('totalWebsites') ? 'bg-muted-foreground' : ''}`} + style={{ backgroundColor: visibleSeries.has('totalWebsites') ? "#22c55e" : undefined }} + /> + <span className={`text-muted-foreground ${!visibleSeries.has('totalWebsites') ? 'line-through' : ''}`}>网站</span> + <span className="font-medium">{displayData?.totalWebsites ?? 0}</span> + </button> + </div> + </div> + </> + )} + </CardContent> + </Card> + ) +} diff --git a/frontend/components/dashboard/dashboard-activity-tabs.tsx b/frontend/components/dashboard/dashboard-activity-tabs.tsx new file mode 100644 index 00000000..e9f1fd2a --- /dev/null +++ b/frontend/components/dashboard/dashboard-activity-tabs.tsx @@ -0,0 +1,30 @@ +"use client" + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { VulnerabilitiesDetailView } from "@/components/vulnerabilities/vulnerabilities-detail-view" +import { ScanHistoryList } from "@/components/scan/history" +import { IconBug, IconRadar } from "@tabler/icons-react" + +export function DashboardActivityTabs() { + return ( + <Tabs defaultValue="vulnerabilities" className="w-full"> + <TabsList className="mb-4"> + <TabsTrigger value="vulnerabilities" className="gap-1.5"> + <IconBug className="h-4 w-4" /> + 漏洞 + </TabsTrigger> + <TabsTrigger value="scans" className="gap-1.5"> + <IconRadar className="h-4 w-4" /> + 扫描历史 + </TabsTrigger> + </TabsList> + + <TabsContent value="vulnerabilities" className="mt-0"> + <VulnerabilitiesDetailView /> + </TabsContent> + <TabsContent value="scans" className="mt-0"> + <ScanHistoryList /> + </TabsContent> + </Tabs> + ) +} diff --git a/frontend/components/dashboard/dashboard-data-table.tsx b/frontend/components/dashboard/dashboard-data-table.tsx new file mode 100644 index 00000000..e30acd44 --- /dev/null +++ b/frontend/components/dashboard/dashboard-data-table.tsx @@ -0,0 +1,474 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { IconLayoutColumns, IconBug, IconRadar, IconChevronLeft, IconChevronRight, IconChevronsLeft, IconChevronsRight, IconSearch, IconLoader2, IconChevronDown } from "@tabler/icons-react" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Badge } from "@/components/ui/badge" +import { Skeleton } from "@/components/ui/skeleton" +import { useAllVulnerabilities } from "@/hooks/use-vulnerabilities" +import { useScans } from "@/hooks/use-scans" +import { VulnerabilityDetailDialog } from "@/components/vulnerabilities/vulnerability-detail-dialog" +import { createVulnerabilityColumns } from "@/components/vulnerabilities/vulnerabilities-columns" +import { createScanHistoryColumns } from "@/components/scan/history/scan-history-columns" +import { ScanProgressDialog, buildScanProgressData, type ScanProgressData } from "@/components/scan/scan-progress-dialog" +import { getScan } from "@/services/scan.service" +import { useRouter } from "next/navigation" +import type { Vulnerability } from "@/types/vulnerability.types" +import type { ScanRecord } from "@/types/scan.types" + +function formatTime(dateStr: string) { + const date = new Date(dateStr) + return date.toLocaleString("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }) +} + +export function DashboardDataTable() { + const router = useRouter() + const [activeTab, setActiveTab] = React.useState("scans") + const [vulnColumnVisibility, setVulnColumnVisibility] = React.useState<VisibilityState>({}) + const [scanColumnVisibility, setScanColumnVisibility] = React.useState<VisibilityState>({}) + + // 漏洞详情弹窗 + const [selectedVuln, setSelectedVuln] = React.useState<Vulnerability | null>(null) + const [vulnDialogOpen, setVulnDialogOpen] = React.useState(false) + + // 扫描进度弹窗 + const [progressData, setProgressData] = React.useState<ScanProgressData | null>(null) + const [progressDialogOpen, setProgressDialogOpen] = React.useState(false) + + // 分页状态 + const [vulnPagination, setVulnPagination] = React.useState({ pageIndex: 0, pageSize: 10 }) + const [scanPagination, setScanPagination] = React.useState({ pageIndex: 0, pageSize: 10 }) + + // 服务端搜索状态 + const [vulnSearchQuery, setVulnSearchQuery] = React.useState("") + const [scanSearchQuery, setScanSearchQuery] = React.useState("") + const [localVulnSearch, setLocalVulnSearch] = React.useState("") + const [localScanSearch, setLocalScanSearch] = React.useState("") + const [isVulnSearching, setIsVulnSearching] = React.useState(false) + const [isScanSearching, setIsScanSearching] = React.useState(false) + + // 获取漏洞数据 + const vulnQuery = useAllVulnerabilities({ + page: vulnPagination.pageIndex + 1, + pageSize: vulnPagination.pageSize, + search: vulnSearchQuery || undefined, + }) + + // 获取扫描数据 + const scanQuery = useScans({ + page: scanPagination.pageIndex + 1, + pageSize: scanPagination.pageSize, + search: scanSearchQuery || undefined, + }) + + // 当请求完成时重置搜索状态 + React.useEffect(() => { + if (!vulnQuery.isFetching && isVulnSearching) { + setIsVulnSearching(false) + } + }, [vulnQuery.isFetching, isVulnSearching]) + + React.useEffect(() => { + if (!scanQuery.isFetching && isScanSearching) { + setIsScanSearching(false) + } + }, [scanQuery.isFetching, isScanSearching]) + + // 搜索处理 + const handleVulnSearch = () => { + setIsVulnSearching(true) + setVulnSearchQuery(localVulnSearch) + setVulnPagination(prev => ({ ...prev, pageIndex: 0 })) + } + + const handleScanSearch = () => { + setIsScanSearching(true) + setScanSearchQuery(localScanSearch) + setScanPagination(prev => ({ ...prev, pageIndex: 0 })) + } + + const vulnerabilities = vulnQuery.data?.vulnerabilities ?? [] + const scans = scanQuery.data?.results ?? [] + + // 格式化日期 + const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + } + + // 点击漏洞行 + const handleVulnRowClick = React.useCallback((vuln: Vulnerability) => { + setSelectedVuln(vuln) + setVulnDialogOpen(true) + }, []) + + // 漏洞列定义 - 复用 vulnerabilities 页面的列 + const vulnColumns = React.useMemo( + () => createVulnerabilityColumns({ + formatDate, + handleViewDetail: handleVulnRowClick, + }), + [handleVulnRowClick] + ) + + // 扫描进度查看 + const handleViewProgress = React.useCallback(async (scan: ScanRecord) => { + try { + const fullScan = await getScan(scan.id) + const data = buildScanProgressData(fullScan) + setProgressData(data) + setProgressDialogOpen(true) + } catch (error) { + console.error("获取扫描详情失败:", error) + } + }, []) + + // 扫描列定义 - 复用 scan-history 页面的列 + const scanColumns = React.useMemo( + () => createScanHistoryColumns({ + formatDate, + navigate: (path: string) => router.push(path), + handleDelete: () => {}, // Dashboard 不需要删除功能 + handleStop: () => {}, // Dashboard 不需要停止功能 + handleViewProgress, + }), + [router, handleViewProgress] + ) + + // 漏洞表格 + const vulnTable = useReactTable({ + data: vulnerabilities, + columns: vulnColumns, + getCoreRowModel: getCoreRowModel(), + onColumnVisibilityChange: setVulnColumnVisibility, + state: { + columnVisibility: vulnColumnVisibility, + }, + manualPagination: true, + pageCount: vulnQuery.data?.pagination?.totalPages ?? -1, + }) + + // 扫描表格 + const scanTable = useReactTable({ + data: scans, + columns: scanColumns, + getCoreRowModel: getCoreRowModel(), + onColumnVisibilityChange: setScanColumnVisibility, + state: { + columnVisibility: scanColumnVisibility, + }, + manualPagination: true, + pageCount: scanQuery.data?.totalPages ?? -1, + }) + + const currentTable = activeTab === "vulnerabilities" ? vulnTable : scanTable + const currentLocalSearch = activeTab === "vulnerabilities" ? localVulnSearch : localScanSearch + const setCurrentLocalSearch = activeTab === "vulnerabilities" ? setLocalVulnSearch : setLocalScanSearch + const handleCurrentSearch = activeTab === "vulnerabilities" ? handleVulnSearch : handleScanSearch + const isCurrentSearching = activeTab === "vulnerabilities" ? isVulnSearching : isScanSearching + const isLoading = activeTab === "vulnerabilities" ? vulnQuery.isLoading : scanQuery.isLoading + const pagination = activeTab === "vulnerabilities" ? vulnPagination : scanPagination + const setPagination = activeTab === "vulnerabilities" ? setVulnPagination : setScanPagination + const totalPages = activeTab === "vulnerabilities" + ? (vulnQuery.data?.pagination?.totalPages ?? 1) + : (scanQuery.data?.totalPages ?? 1) + + return ( + <> + <VulnerabilityDetailDialog + vulnerability={selectedVuln} + open={vulnDialogOpen} + onOpenChange={setVulnDialogOpen} + /> + {progressData && ( + <ScanProgressDialog + open={progressDialogOpen} + onOpenChange={setProgressDialogOpen} + data={progressData} + /> + )} + + <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> + {/* Tab + 搜索框 + Columns 在同一行 */} + <div className="flex items-center justify-between gap-4 mb-4"> + <TabsList> + <TabsTrigger value="scans" className="gap-1.5"> + <IconRadar className="h-4 w-4" /> + 扫描历史 + </TabsTrigger> + <TabsTrigger value="vulnerabilities" className="gap-1.5"> + <IconBug className="h-4 w-4" /> + 漏洞 + </TabsTrigger> + </TabsList> + + <div className="flex items-center gap-2"> + <Input + placeholder={activeTab === "vulnerabilities" ? "搜索漏洞类型..." : "搜索目标名称..."} + value={currentLocalSearch} + onChange={(e) => setCurrentLocalSearch(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleCurrentSearch()} + className="h-8 w-[200px]" + /> + <Button variant="outline" size="sm" onClick={handleCurrentSearch} disabled={isCurrentSearching} className="h-8"> + {isCurrentSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {currentTable + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {column.id} + </DropdownMenuCheckboxItem> + ))} + </DropdownMenuContent> + </DropdownMenu> + </div> + </div> + + {/* 表格内容 */} + <TabsContent value="vulnerabilities" className="mt-0"> + {isLoading ? ( + <div className="space-y-2"> + {[...Array(5)].map((_, i) => <Skeleton key={i} className="h-12 w-full" />)} + </div> + ) : ( + <div className="rounded-md border"> + <Table> + <TableHeader> + {vulnTable.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => ( + <TableHead key={header.id}> + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + </TableHead> + ))} + </TableRow> + ))} + </TableHeader> + <TableBody> + {vulnTable.getRowModel().rows?.length ? ( + vulnTable.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + className="hover:bg-muted/50" + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell colSpan={vulnColumns.length} className="h-24 text-center"> + 暂无漏洞数据 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + )} + </TabsContent> + + <TabsContent value="scans" className="mt-0"> + {scanQuery.isLoading ? ( + <div className="space-y-2"> + {[...Array(5)].map((_, i) => <Skeleton key={i} className="h-12 w-full" />)} + </div> + ) : ( + <div className="rounded-md border"> + <Table> + <TableHeader> + {scanTable.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => ( + <TableHead key={header.id}> + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + </TableHead> + ))} + </TableRow> + ))} + </TableHeader> + <TableBody> + {scanTable.getRowModel().rows?.length ? ( + scanTable.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + className="hover:bg-muted/50" + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell colSpan={scanColumns.length} className="h-24 text-center"> + 暂无扫描记录 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + )} + </TabsContent> + + {/* 分页控制 */} + <div className="flex items-center justify-between px-2 py-4"> + {/* 选中行信息 */} + <div className="flex-1 text-sm text-muted-foreground"> + {currentTable.getFilteredSelectedRowModel().rows.length} of{" "} + {activeTab === "vulnerabilities" + ? (vulnQuery.data?.pagination?.total ?? 0) + : (scanQuery.data?.total ?? 0)} row(s) selected + </div> + + {/* 分页控制器 */} + <div className="flex items-center space-x-6 lg:space-x-8"> + {/* 每页显示数量选择 */} + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + Rows per page + </Label> + <Select + value={`${pagination.pageSize}`} + onValueChange={(value) => { + setPagination(prev => ({ ...prev, pageIndex: 0, pageSize: Number(value) })) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={pagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 页码信息 */} + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + Page {pagination.pageIndex + 1} of {totalPages} + </div> + + {/* 分页按钮 */} + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => setPagination(prev => ({ ...prev, pageIndex: 0 }))} + disabled={pagination.pageIndex === 0} + > + <span className="sr-only">Go to first page</span> + <IconChevronsLeft className="h-4 w-4" /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => setPagination(prev => ({ ...prev, pageIndex: Math.max(0, prev.pageIndex - 1) }))} + disabled={pagination.pageIndex === 0} + > + <span className="sr-only">Go to previous page</span> + <IconChevronLeft className="h-4 w-4" /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => setPagination(prev => ({ ...prev, pageIndex: prev.pageIndex + 1 }))} + disabled={pagination.pageIndex >= totalPages - 1} + > + <span className="sr-only">Go to next page</span> + <IconChevronRight className="h-4 w-4" /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => setPagination(prev => ({ ...prev, pageIndex: totalPages - 1 }))} + disabled={pagination.pageIndex >= totalPages - 1} + > + <span className="sr-only">Go to last page</span> + <IconChevronsRight className="h-4 w-4" /> + </Button> + </div> + </div> + </div> + </Tabs> + </> + ) +} diff --git a/frontend/components/dashboard/dashboard-scan-history.tsx b/frontend/components/dashboard/dashboard-scan-history.tsx new file mode 100644 index 00000000..56d31ac1 --- /dev/null +++ b/frontend/components/dashboard/dashboard-scan-history.tsx @@ -0,0 +1,78 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { ScanHistoryDataTable } from "@/components/scan/history/scan-history-data-table" +import { createScanHistoryColumns } from "@/components/scan/history/scan-history-columns" +import { useScans } from "@/hooks/use-scans" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" +import type { ScanRecord } from "@/types/scan.types" +import type { ColumnDef } from "@tanstack/react-table" + +export function DashboardScanHistory() { + const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 5 }) + const [searchQuery, setSearchQuery] = React.useState("") + const [isSearching, setIsSearching] = React.useState(false) + const router = useRouter() + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, pageIndex: 0 })) + } + + const { data, isLoading, isFetching } = useScans({ + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + status: 'running', + search: searchQuery || undefined, + }) + + React.useEffect(() => { + if (!isFetching && isSearching) { + setIsSearching(false) + } + }, [isFetching, isSearching]) + + const formatDate = React.useCallback((dateString: string) => new Date(dateString).toLocaleString("zh-CN", { hour12: false }), []) + const navigate = React.useCallback((path: string) => router.push(path), [router]) + const handleDelete = React.useCallback(() => {}, []) + const handleStop = React.useCallback((scan: ScanRecord) => { + // 仪表盘列表暂时不提供停止逻辑,实现时可在此调用对应的停止扫描接口 + }, []) + + const columns = React.useMemo( + () => createScanHistoryColumns({ formatDate, navigate, handleDelete, handleStop }) as ColumnDef<ScanRecord>[], + [formatDate, navigate, handleDelete, handleStop] + ) + + if (isLoading && !data) { + return ( + <DataTableSkeleton + withPadding={false} + toolbarButtonCount={2} + rows={4} + columns={3} + /> + ) + } + + const paginationInfo = data + ? { total: data.total, page: data.page, pageSize: data.pageSize, totalPages: data.totalPages } + : undefined + + return ( + <ScanHistoryDataTable + data={data?.results ?? []} + columns={columns} + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + hidePagination + pagination={pagination} + setPagination={setPagination} + paginationInfo={paginationInfo} + onPaginationChange={setPagination} + /> + ) +} diff --git a/frontend/components/dashboard/dashboard-scheduled-scans.tsx b/frontend/components/dashboard/dashboard-scheduled-scans.tsx new file mode 100644 index 00000000..a2b564ec --- /dev/null +++ b/frontend/components/dashboard/dashboard-scheduled-scans.tsx @@ -0,0 +1,81 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { ScheduledScanDataTable } from "@/components/scan/scheduled/scheduled-scan-data-table" +import { createScheduledScanColumns } from "@/components/scan/scheduled/scheduled-scan-columns" +import { useScheduledScans } from "@/hooks/use-scheduled-scans" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" + +export function DashboardScheduledScans() { + const [pagination, setPagination] = React.useState({ page: 1, pageSize: 10 }) + const [searchQuery, setSearchQuery] = React.useState("") + const [isSearching, setIsSearching] = React.useState(false) + const router = useRouter() + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, page: 1 })) + } + + const { data, isLoading, isFetching } = useScheduledScans({ + page: pagination.page, + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }) + + React.useEffect(() => { + if (!isFetching && isSearching) { + setIsSearching(false) + } + }, [isFetching, isSearching]) + + const formatDate = (dateString: string) => new Date(dateString).toLocaleString("zh-CN", { hour12: false }) + const handleView = () => router.push(`/scan/scheduled/`) + const handleEdit = () => router.push(`/scan/scheduled/`) + const handleDelete = () => {} + const handleToggleStatus = () => {} + + const columns = React.useMemo( + () => + createScheduledScanColumns({ + formatDate, + handleView, + handleEdit, + handleDelete, + handleToggleStatus, + }), + [formatDate, handleView, handleEdit] + ) + + if (isLoading && !data) { + return ( + <DataTableSkeleton + withPadding={false} + toolbarButtonCount={2} + rows={4} + columns={3} + /> + ) + } + + const list = data?.results ?? [] + + return ( + <ScheduledScanDataTable + data={list} + columns={columns} + searchPlaceholder="搜索任务名称..." + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + page={pagination.page} + pageSize={pagination.pageSize} + total={data?.total || 0} + totalPages={data?.totalPages || 1} + onPageChange={(page) => setPagination((prev) => ({ ...prev, page }))} + onPageSizeChange={(pageSize) => setPagination({ page: 1, pageSize })} + /> + ) +} diff --git a/frontend/components/dashboard/dashboard-stat-cards.tsx b/frontend/components/dashboard/dashboard-stat-cards.tsx new file mode 100644 index 00000000..19b720f0 --- /dev/null +++ b/frontend/components/dashboard/dashboard-stat-cards.tsx @@ -0,0 +1,124 @@ +"use client" + +import { useAssetStatistics } from "@/hooks/use-dashboard" +import { Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Skeleton } from "@/components/ui/skeleton" +import { IconTarget, IconStack2, IconBug, IconPlayerPlay, IconTrendingUp, IconTrendingDown } from "@tabler/icons-react" + +function TrendBadge({ change }: { change: number }) { + if (change === 0) return null + + const isPositive = change > 0 + return ( + <Badge + variant="outline" + className={isPositive + ? "text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-950" + : "text-destructive border-destructive/30 bg-destructive/15" + } + > + {isPositive ? <IconTrendingUp className="size-3 mr-1" /> : <IconTrendingDown className="size-3 mr-1" />} + {isPositive ? '+' : ''}{change} + </Badge> + ) +} + +function StatCard({ + title, + value, + change, + icon, + footer, + loading, +}: { + title: string + value: string | number + change?: number + icon: React.ReactNode + footer: string + loading?: boolean +}) { + return ( + <Card className="@container/card"> + <CardHeader> + <CardDescription className="flex items-center gap-2"> + {icon} + {title} + </CardDescription> + {loading ? ( + <Skeleton className="h-8 w-24" /> + ) : ( + <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl"> + {typeof value === 'number' ? value.toLocaleString() : value} + </CardTitle> + )} + {!loading && change !== undefined && ( + <CardAction> + <TrendBadge change={change} /> + </CardAction> + )} + </CardHeader> + <CardFooter className="flex-col items-start gap-1.5 text-sm"> + <div className="text-muted-foreground">{footer}</div> + </CardFooter> + </Card> + ) +} + +function formatUpdateTime(dateStr: string | null) { + if (!dateStr) return '暂无数据' + const date = new Date(dateStr) + return date.toLocaleString('zh-CN', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) +} + +export function DashboardStatCards() { + const { data, isLoading } = useAssetStatistics() + + return ( + <div className="flex flex-col gap-2 px-4 lg:px-6"> + <div className="grid grid-cols-1 gap-4 @xl/main:grid-cols-2 @5xl/main:grid-cols-4"> + <StatCard + title="发现资产" + value={data?.totalAssets ?? 0} + change={data?.changeAssets} + icon={<IconStack2 className="size-4" />} + loading={isLoading} + footer="子域名 + IP + 端点 + 网站" + /> + <StatCard + title="发现漏洞" + value={data?.totalVulns ?? 0} + change={data?.changeVulns} + icon={<IconBug className="size-4" />} + loading={isLoading} + footer="所有扫描发现的漏洞" + /> + <StatCard + title="监控目标" + value={data?.totalTargets ?? 0} + change={data?.changeTargets} + icon={<IconTarget className="size-4" />} + loading={isLoading} + footer="已添加的目标总数" + /> + <StatCard + title="正在扫描" + value={data?.runningScans ?? 0} + icon={<IconPlayerPlay className="size-4" />} + loading={isLoading} + footer="当前运行中的任务" + /> + </div> + <div className="flex items-center gap-3 mt-1 -mb-2 text-xs text-muted-foreground"> + <div className="flex-1 border-t" /> + <span>统计更新于 {formatUpdateTime(data?.updatedAt ?? null)}</span> + </div> + </div> + ) +} diff --git a/frontend/components/dashboard/recent-vulnerabilities.tsx b/frontend/components/dashboard/recent-vulnerabilities.tsx new file mode 100644 index 00000000..e19af948 --- /dev/null +++ b/frontend/components/dashboard/recent-vulnerabilities.tsx @@ -0,0 +1,126 @@ +"use client" + +import { useQuery } from "@tanstack/react-query" +import Link from "next/link" +import { useRouter } from "next/navigation" +import { VulnerabilityService } from "@/services/vulnerability.service" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { Skeleton } from "@/components/ui/skeleton" +import { IconExternalLink } from "@tabler/icons-react" +import type { VulnerabilitySeverity } from "@/types/vulnerability.types" + +// 统一的漏洞严重程度颜色配置(与图表一致) +const severityConfig: Record<VulnerabilitySeverity, { label: string; className: string }> = { + critical: { label: "严重", className: "bg-red-600 text-white hover:bg-red-600" }, + high: { label: "高危", className: "bg-orange-500 text-white hover:bg-orange-500" }, + medium: { label: "中危", className: "bg-yellow-500 text-white hover:bg-yellow-500" }, + low: { label: "低危", className: "bg-blue-500 text-white hover:bg-blue-500" }, + info: { label: "信息", className: "bg-gray-500 text-white hover:bg-gray-500" }, +} + +function formatTime(dateStr: string) { + const date = new Date(dateStr) + return date.toLocaleString("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }) +} + +export function RecentVulnerabilities() { + const router = useRouter() + const { data, isLoading } = useQuery({ + queryKey: ["dashboard", "recent-vulnerabilities"], + queryFn: () => VulnerabilityService.getAllVulnerabilities({ page: 1, pageSize: 5 }), + }) + + const vulnerabilities = data?.results ?? [] + + return ( + <Card> + <CardHeader className="flex flex-row items-center justify-between"> + <div> + <CardTitle>最近漏洞</CardTitle> + <CardDescription>最近发现的安全漏洞</CardDescription> + </div> + <Link + href="/vulnerabilities/" + className="text-sm text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1" + > + 查看全部 + <IconExternalLink className="h-3.5 w-3.5" /> + </Link> + </CardHeader> + <CardContent> + {isLoading ? ( + <div className="space-y-3"> + {[...Array(5)].map((_, i) => ( + <Skeleton key={i} className="h-10 w-full" /> + ))} + </div> + ) : vulnerabilities.length === 0 ? ( + <div className="text-center text-muted-foreground py-8"> + 暂无漏洞数据 + </div> + ) : ( + <div className="rounded-md border"> + <Table> + <TableHeader> + <TableRow> + <TableHead>Status</TableHead> + <TableHead>Source</TableHead> + <TableHead>类型</TableHead> + <TableHead>URL</TableHead> + <TableHead>发现时间</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {vulnerabilities.map((vuln: any) => ( + <TableRow + key={vuln.id} + className="cursor-pointer hover:bg-muted/50" + onClick={() => router.push(`/vulnerabilities/?id=${vuln.id}`)} + > + <TableCell> + <Badge className={severityConfig[vuln.severity as VulnerabilitySeverity]?.className}> + {severityConfig[vuln.severity as VulnerabilitySeverity]?.label ?? vuln.severity} + </Badge> + </TableCell> + <TableCell> + <Badge variant="outline">{vuln.source}</Badge> + </TableCell> + <TableCell className="font-medium max-w-[120px] truncate"> + {vuln.vulnType} + </TableCell> + <TableCell className="text-muted-foreground text-xs max-w-[200px] truncate"> + {vuln.url} + </TableCell> + <TableCell className="text-muted-foreground text-xs whitespace-nowrap"> + {formatTime(vuln.discoveredAt)} + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + )} + </CardContent> + </Card> + ) +} diff --git a/frontend/components/dashboard/vuln-severity-chart.tsx b/frontend/components/dashboard/vuln-severity-chart.tsx new file mode 100644 index 00000000..8911de55 --- /dev/null +++ b/frontend/components/dashboard/vuln-severity-chart.tsx @@ -0,0 +1,147 @@ +"use client" + +import { Pie, PieChart, Cell, Label } from "recharts" +import { useAssetStatistics } from "@/hooks/use-dashboard" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart" +import { Skeleton } from "@/components/ui/skeleton" + +// 漏洞严重程度使用固定语义化颜色 +const chartConfig = { + count: { + label: "数量", + }, + critical: { + label: "严重", + color: "#dc2626", // 红色 + }, + high: { + label: "高危", + color: "#f97316", // 橙色 + }, + medium: { + label: "中危", + color: "#eab308", // 黄色 + }, + low: { + label: "低危", + color: "#3b82f6", // 蓝色 + }, + info: { + label: "信息", + color: "#6b7280", // 灰色 + }, +} satisfies ChartConfig + +export function VulnSeverityChart() { + const { data, isLoading } = useAssetStatistics() + + const vulnData = data?.vulnBySeverity + const allData = [ + { severity: "critical", count: vulnData?.critical ?? 0, fill: chartConfig.critical.color }, + { severity: "high", count: vulnData?.high ?? 0, fill: chartConfig.high.color }, + { severity: "medium", count: vulnData?.medium ?? 0, fill: chartConfig.medium.color }, + { severity: "low", count: vulnData?.low ?? 0, fill: chartConfig.low.color }, + { severity: "info", count: vulnData?.info ?? 0, fill: chartConfig.info.color }, + ] + // 饼图只显示有数据的 + const chartData = allData.filter(item => item.count > 0) + + const total = allData.reduce((sum, item) => sum + item.count, 0) + + return ( + <Card> + <CardHeader> + <CardTitle>漏洞分布</CardTitle> + <CardDescription>按严重程度统计</CardDescription> + </CardHeader> + <CardContent> + {isLoading ? ( + <div className="flex items-center justify-center h-[180px]"> + <Skeleton className="h-[120px] w-[120px] rounded-full" /> + </div> + ) : total === 0 ? ( + <div className="flex items-center justify-center h-[180px] text-muted-foreground"> + 暂无漏洞数据 + </div> + ) : ( + <div className="flex flex-col items-center gap-4"> + <ChartContainer config={chartConfig} className="aspect-square h-[140px]"> + <PieChart> + <ChartTooltip + content={<ChartTooltipContent nameKey="severity" hideLabel />} + /> + <Pie + data={chartData} + dataKey="count" + nameKey="severity" + innerRadius={45} + outerRadius={70} + paddingAngle={2} + > + {chartData.map((entry) => ( + <Cell key={entry.severity} fill={entry.fill} /> + ))} + <Label + content={({ viewBox }) => { + if (viewBox && "cx" in viewBox && "cy" in viewBox) { + return ( + <text + x={viewBox.cx} + y={viewBox.cy} + textAnchor="middle" + dominantBaseline="middle" + > + <tspan + x={viewBox.cx} + y={viewBox.cy} + className="fill-foreground text-2xl font-bold" + > + {total} + </tspan> + <tspan + x={viewBox.cx} + y={(viewBox.cy || 0) + 18} + className="fill-muted-foreground text-xs" + > + 漏洞 + </tspan> + </text> + ) + } + }} + /> + </Pie> + </PieChart> + </ChartContainer> + <div className="mt-3 pt-3 border-t flex flex-wrap justify-end gap-x-4 gap-y-1.5 text-sm"> + {allData.map((item) => ( + <div key={item.severity} className="flex items-center gap-1.5"> + <div + className="h-2.5 w-2.5 rounded-full" + style={{ backgroundColor: item.fill }} + /> + <span className={item.count > 0 ? "text-foreground" : "text-muted-foreground"}> + {chartConfig[item.severity as keyof typeof chartConfig]?.label} + </span> + <span className={item.count > 0 ? "font-medium" : "text-muted-foreground"}>{item.count}</span> + </div> + ))} + </div> + </div> + )} + </CardContent> + </Card> + ) +} diff --git a/frontend/components/directories/directories-columns.tsx b/frontend/components/directories/directories-columns.tsx new file mode 100644 index 00000000..f5c9a3f6 --- /dev/null +++ b/frontend/components/directories/directories-columns.tsx @@ -0,0 +1,427 @@ +"use client" + +import React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { Checkbox } from "@/components/ui/checkbox" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { IconDots, IconEye } from "@tabler/icons-react" +import { Copy, Check, ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react" +import { toast } from "sonner" +import type { Directory } from "@/types/directory.types" + +/** + * 可复制单元格组件 + */ +function CopyableCell({ + value, + maxWidth = "400px", + truncateLength = 50, + successMessage = "已复制", + className = "font-medium" +}: { + value: string + maxWidth?: string + truncateLength?: number + successMessage?: string + className?: string +}) { + const [copied, setCopied] = React.useState(false) + const isLong = value.length > truncateLength + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(value) + setCopied(true) + toast.success(successMessage) + setTimeout(() => setCopied(false), 2000) + } catch (err) { + toast.error("复制失败") + } + } + + const displayValue = isLong ? `${value.substring(0, truncateLength)}...` : value + + return ( + <TooltipProvider> + <div className="flex items-center gap-2 group"> + <Tooltip> + <TooltipTrigger asChild> + <div + className={`truncate ${className}`} + style={{ maxWidth }} + > + {displayValue} + </div> + </TooltipTrigger> + {isLong && ( + <TooltipContent side="bottom" className="max-w-md break-all"> + <p>{value}</p> + </TooltipContent> + )} + </Tooltip> + <Button + variant="ghost" + size="sm" + className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity" + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3 w-3 text-green-500" /> + ) : ( + <Copy className="h-3 w-3" /> + )} + </Button> + </div> + </TooltipProvider> + ) +} + +/** + * HTTP 状态码徽章组件 + */ +function StatusBadge({ status }: { status: number | null }) { + if (!status) return <span className="text-muted-foreground">-</span> + + let variant: "default" | "secondary" | "destructive" | "outline" = "default" + let className = "" + + if (status >= 200 && status < 300) { + className = "bg-green-500/10 text-green-700 dark:text-green-400 hover:bg-green-500/20" + } else if (status >= 300 && status < 400) { + className = "bg-blue-500/10 text-blue-700 dark:text-blue-400 hover:bg-blue-500/20" + } else if (status >= 400 && status < 500) { + className = "bg-yellow-500/10 text-yellow-700 dark:text-yellow-400 hover:bg-yellow-500/20" + } else if (status >= 500) { + className = "bg-red-500/10 text-red-700 dark:text-red-400 hover:bg-red-500/20" + } + + return ( + <Badge variant={variant} className={className}> + {status} + </Badge> + ) +} + +/** + * 格式化持续时间(纳秒转毫秒) + */ +function formatDuration(nanoseconds: number | null): string { + if (nanoseconds === null) return "-" + const milliseconds = nanoseconds / 1000000 + return `${milliseconds.toFixed(2)} ms` +} + +/** + * 创建目录表格列定义 + */ +export function createDirectoryColumns({ + formatDate, + onViewDetail, +}: { + formatDate: (date: string) => string + onViewDetail?: (directory: Directory) => void +}): ColumnDef<Directory>[] { + return [ + // 选择列 + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="全选" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="选择行" + /> + ), + enableSorting: false, + enableHiding: false, + }, + // URL 列 + { + accessorKey: "url", + size: 300, + minSize: 200, + maxSize: 400, + header: ({ column }) => { + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="h-8 px-2 lg:px-3" + > + URL + {column.getIsSorted() === "asc" ? ( + <ChevronUp className="ml-2 h-4 w-4" /> + ) : column.getIsSorted() === "desc" ? ( + <ChevronDown className="ml-2 h-4 w-4" /> + ) : ( + <ChevronsUpDown className="ml-2 h-4 w-4" /> + )} + </Button> + ) + }, + cell: ({ row }) => { + const url = row.getValue("url") as string + if (!url) return <span className="text-muted-foreground text-sm">-</span> + + const maxLength = 40 + const isLong = url.length > maxLength + const displayUrl = isLong ? url.substring(0, maxLength) + "..." : url + + return ( + <div className="flex items-center gap-1 w-[280px] min-w-[280px]"> + <span className="text-sm font-mono truncate"> + {displayUrl} + </span> + {isLong && ( + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整 URL</h4> + <div className="text-xs break-all bg-muted p-2 rounded max-h-48 overflow-y-auto font-mono"> + {url} + </div> + </div> + </PopoverContent> + </Popover> + )} + </div> + ) + }, + }, + // Status 列 + { + accessorKey: "status", + header: ({ column }) => { + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="h-8 px-2 lg:px-3" + > + Status + {column.getIsSorted() === "asc" ? ( + <ChevronUp className="ml-2 h-4 w-4" /> + ) : column.getIsSorted() === "desc" ? ( + <ChevronDown className="ml-2 h-4 w-4" /> + ) : ( + <ChevronsUpDown className="ml-2 h-4 w-4" /> + )} + </Button> + ) + }, + cell: ({ row }) => <StatusBadge status={row.getValue("status")} />, + }, + // Content Length 列 + { + accessorKey: "contentLength", + header: ({ column }) => { + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="h-8 px-2 lg:px-3" + > + Length + {column.getIsSorted() === "asc" ? ( + <ChevronUp className="ml-2 h-4 w-4" /> + ) : column.getIsSorted() === "desc" ? ( + <ChevronDown className="ml-2 h-4 w-4" /> + ) : ( + <ChevronsUpDown className="ml-2 h-4 w-4" /> + )} + </Button> + ) + }, + cell: ({ row }) => { + const length = row.getValue("contentLength") as number | null + return <span>{length !== null ? length.toLocaleString() : "-"}</span> + }, + }, + // Words 列 + { + accessorKey: "words", + header: ({ column }) => { + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="h-8 px-2 lg:px-3" + > + Words + {column.getIsSorted() === "asc" ? ( + <ChevronUp className="ml-2 h-4 w-4" /> + ) : column.getIsSorted() === "desc" ? ( + <ChevronDown className="ml-2 h-4 w-4" /> + ) : ( + <ChevronsUpDown className="ml-2 h-4 w-4" /> + )} + </Button> + ) + }, + cell: ({ row }) => { + const words = row.getValue("words") as number | null + return <span>{words !== null ? words.toLocaleString() : "-"}</span> + }, + }, + // Lines 列 + { + accessorKey: "lines", + header: ({ column }) => { + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="h-8 px-2 lg:px-3" + > + Lines + {column.getIsSorted() === "asc" ? ( + <ChevronUp className="ml-2 h-4 w-4" /> + ) : column.getIsSorted() === "desc" ? ( + <ChevronDown className="ml-2 h-4 w-4" /> + ) : ( + <ChevronsUpDown className="ml-2 h-4 w-4" /> + )} + </Button> + ) + }, + cell: ({ row }) => { + const lines = row.getValue("lines") as number | null + return <span>{lines !== null ? lines.toLocaleString() : "-"}</span> + }, + }, + // Content Type 列 + { + accessorKey: "contentType", + header: "Content Type", + cell: ({ row }) => { + const contentType = row.getValue("contentType") as string + return contentType ? ( + <Badge variant="outline">{contentType}</Badge> + ) : ( + <span className="text-muted-foreground">-</span> + ) + }, + }, + // Duration 列 + { + accessorKey: "duration", + header: ({ column }) => { + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="h-8 px-2 lg:px-3" + > + Duration + {column.getIsSorted() === "asc" ? ( + <ChevronUp className="ml-2 h-4 w-4" /> + ) : column.getIsSorted() === "desc" ? ( + <ChevronDown className="ml-2 h-4 w-4" /> + ) : ( + <ChevronsUpDown className="ml-2 h-4 w-4" /> + )} + </Button> + ) + }, + cell: ({ row }) => { + const duration = row.getValue("duration") as number | null + return <span className="text-muted-foreground">{formatDuration(duration)}</span> + }, + }, + // Discovered At 列 + { + accessorKey: "discoveredAt", + header: ({ column }) => { + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="h-8 px-2 lg:px-3" + > + Discovered At + {column.getIsSorted() === "asc" ? ( + <ChevronUp className="ml-2 h-4 w-4" /> + ) : column.getIsSorted() === "desc" ? ( + <ChevronDown className="ml-2 h-4 w-4" /> + ) : ( + <ChevronsUpDown className="ml-2 h-4 w-4" /> + )} + </Button> + ) + }, + cell: ({ row }) => { + const date = row.getValue("discoveredAt") as string + return <span className="text-muted-foreground">{formatDate(date)}</span> + }, + }, + // 操作列 + { + id: "actions", + cell: ({ row }) => { + const directory = row.original + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="h-8 w-8 p-0"> + <span className="sr-only">打开菜单</span> + <IconDots className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuLabel>操作</DropdownMenuLabel> + <DropdownMenuItem + onClick={() => { + navigator.clipboard.writeText(directory.url) + toast.success("URL 已复制") + }} + > + 复制 URL + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem onClick={() => onViewDetail?.(directory)}> + <IconEye className="mr-2 h-4 w-4" /> + 查看详细 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) + }, + }, + ] +} diff --git a/frontend/components/directories/directories-data-table.tsx b/frontend/components/directories/directories-data-table.tsx new file mode 100644 index 00000000..69860bdf --- /dev/null +++ b/frontend/components/directories/directories-data-table.tsx @@ -0,0 +1,481 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconTrash, + IconDownload, + IconSearch, + IconLoader2, +} from "@tabler/icons-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import type { Directory } from "@/types/directory.types" +import type { PaginationInfo } from "@/types/common.types" + +interface DirectoriesDataTableProps { + data: Directory[] + columns: ColumnDef<Directory>[] + searchPlaceholder?: string + searchColumn?: string + searchValue?: string + onSearch?: (value: string) => void + isSearching?: boolean + pagination?: { pageIndex: number; pageSize: number } + setPagination?: React.Dispatch<React.SetStateAction<{ pageIndex: number; pageSize: number }>> + paginationInfo?: PaginationInfo + onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void + onBulkDelete?: () => void + onSelectionChange?: (selectedRows: Directory[]) => void + // 下载回调函数 + onDownloadAll?: () => void + onDownloadSelected?: () => void +} + +export function DirectoriesDataTable({ + data = [], + columns, + searchPlaceholder = "搜索URL...", + searchColumn = "url", + searchValue, + onSearch, + isSearching = false, + pagination, + setPagination, + paginationInfo, + onPaginationChange, + onBulkDelete, + onSelectionChange, + onDownloadAll, + onDownloadSelected, +}: DirectoriesDataTableProps) { + const [sorting, setSorting] = React.useState<SortingState>([]) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({}) + const [internalPagination, setInternalPagination] = React.useState({ + pageIndex: 0, + pageSize: 10, + }) + + // 本地搜索输入状态(只在回车或点击按钮时触发搜索) + const [localSearchValue, setLocalSearchValue] = React.useState(searchValue ?? "") + + React.useEffect(() => { + setLocalSearchValue(searchValue ?? "") + }, [searchValue]) + + const handleSearchSubmit = () => { + if (onSearch) { + onSearch(localSearchValue) + } else { + table.getColumn(searchColumn)?.setFilterValue(localSearchValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearchSubmit() + } + } + + const useServerPagination = !!paginationInfo && !!pagination && !!setPagination + const tablePagination = useServerPagination ? pagination : internalPagination + const setTablePagination = useServerPagination ? setPagination : setInternalPagination + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination: tablePagination, + }, + getRowId: (row) => row.id.toString(), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = + typeof updater === "function" ? updater(tablePagination) : updater + setTablePagination?.(nextPagination as { pageIndex: number; pageSize: number }) + onPaginationChange?.(nextPagination) + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + manualPagination: useServerPagination, + pageCount: useServerPagination + ? paginationInfo?.totalPages ?? -1 + : Math.ceil(data.length / tablePagination.pageSize) || 1, + }) + + const totalItems = useServerPagination + ? paginationInfo?.total ?? data.length + : table.getFilteredRowModel().rows.length + + // 处理选中行变化 + React.useEffect(() => { + if (onSelectionChange) { + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + onSelectionChange(selectedRows) + } + }, [rowSelection, onSelectionChange, table]) + + return ( + <div className="w-full space-y-4"> + {/* 工具栏 */} + <div className="flex flex-col gap-4 @container/toolbar"> + {/* 第一行:搜索和列控制 */} + <div className="flex flex-col gap-4 @xl/toolbar:flex-row @xl/toolbar:items-center @xl/toolbar:justify-between"> + <div className="flex flex-1 items-center gap-2"> + <Input + placeholder={searchPlaceholder} + value={localSearchValue} + onChange={(e) => setLocalSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="h-8 w-full @xl/toolbar:max-w-sm" + /> + <Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}> + {isSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + + <div className="flex items-center gap-2"> + {/* 列可见性控制 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns className="mr-2 h-4 w-4" /> + Columns + <IconChevronDown className="ml-2 h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-[200px]"> + <DropdownMenuLabel>Toggle Columns</DropdownMenuLabel> + <DropdownMenuSeparator /> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + const columnTitle = { + url: "URL", + status: "Status", + contentLength: "Length", + words: "Words", + lines: "Lines", + contentType: "Content Type", + duration: "Duration", + discoveredAt: "Discovered At", + }[column.id] || column.id + + return ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {columnTitle} + </DropdownMenuCheckboxItem> + ) + })} + </DropdownMenuContent> + </DropdownMenu> + + {/* 下载按钮 */} + {(onDownloadAll || onDownloadSelected) && ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconDownload /> + Download + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-64"> + <DropdownMenuLabel>Download Options</DropdownMenuLabel> + <DropdownMenuSeparator /> + {onDownloadAll && ( + <DropdownMenuItem onClick={onDownloadAll}> + <IconDownload className="h-4 w-4" /> + Download All Directories + </DropdownMenuItem> + )} + {onDownloadSelected && ( + <DropdownMenuItem + onClick={onDownloadSelected} + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + > + <IconDownload className="h-4 w-4" /> + Download Selected Directories + </DropdownMenuItem> + )} + </DropdownMenuContent> + </DropdownMenu> + )} + + {/* 批量删除按钮 */} + {onBulkDelete && ( + <Button + onClick={onBulkDelete} + size="sm" + variant="outline" + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + className={ + table.getFilteredSelectedRowModel().rows.length === 0 + ? "text-muted-foreground" + : "text-destructive hover:text-destructive hover:bg-destructive/10" + } + > + <IconTrash /> + Delete + </Button> + )} + </div> + </div> + </div> + + {/* 表格 */} + <div className="rounded-md border"> + <Table> + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + 暂无数据 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {/* 分页控制 */} + <div className="flex flex-col gap-4 @container/pagination"> + <div className="flex flex-col gap-4 @xl/pagination:flex-row @xl/pagination:items-center @xl/pagination:justify-between"> + <div className="flex items-center gap-4 text-sm text-muted-foreground"> + <div> + {table.getFilteredSelectedRowModel().rows.length > 0 && ( + <span> + 已选择 {table.getFilteredSelectedRowModel().rows.length} /{" "} + {totalItems} 条 + </span> + )} + {table.getFilteredSelectedRowModel().rows.length === 0 && ( + <span>共 {totalItems} 条</span> + )} + </div> + </div> + + <div className="flex flex-col gap-4 @sm/pagination:flex-row @sm/pagination:items-center"> + {/* 每页显示条数 */} + <div className="flex items-center gap-2"> + <Label htmlFor="pageSize" className="text-sm text-muted-foreground whitespace-nowrap"> + 每页显示 + </Label> + <Select + value={`${tablePagination.pageSize}`} + onValueChange={(value) => { + const newPageSize = Number(value) + const newPagination = { + pageSize: newPageSize, + pageIndex: 0, + } + setTablePagination(newPagination) + onPaginationChange?.(newPagination) + }} + > + <SelectTrigger id="pageSize" className="h-9 w-[70px]"> + <SelectValue placeholder={tablePagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 分页按钮 */} + <div className="flex items-center gap-2"> + <div className="flex items-center justify-center text-sm font-medium whitespace-nowrap"> + 第 {tablePagination.pageIndex + 1} /{" "} + {table.getPageCount() || 1} 页 + </div> + <div className="flex items-center gap-1"> + <Button + variant="outline" + size="icon" + className="h-9 w-9" + onClick={() => { + const newPagination = { + ...tablePagination, + pageIndex: 0, + } + setTablePagination(newPagination) + onPaginationChange?.(newPagination) + }} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">跳转到第一页</span> + <IconChevronsLeft className="h-4 w-4" /> + </Button> + <Button + variant="outline" + size="icon" + className="h-9 w-9" + onClick={() => { + const newPagination = { + ...tablePagination, + pageIndex: tablePagination.pageIndex - 1, + } + setTablePagination(newPagination) + onPaginationChange?.(newPagination) + }} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Previous page</span> + <IconChevronLeft className="h-4 w-4" /> + </Button> + <Button + variant="outline" + size="icon" + className="h-9 w-9" + onClick={() => { + const newPagination = { + ...tablePagination, + pageIndex: tablePagination.pageIndex + 1, + } + setTablePagination(newPagination) + onPaginationChange?.(newPagination) + }} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Next page</span> + <IconChevronRight className="h-4 w-4" /> + </Button> + <Button + variant="outline" + size="icon" + className="h-9 w-9" + onClick={() => { + const newPagination = { + ...tablePagination, + pageIndex: table.getPageCount() - 1, + } + setTablePagination(newPagination) + onPaginationChange?.(newPagination) + }} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">跳转到最后一页</span> + <IconChevronsRight className="h-4 w-4" /> + </Button> + </div> + </div> + </div> + </div> + </div> + </div> + ) +} diff --git a/frontend/components/directories/directories-view.tsx b/frontend/components/directories/directories-view.tsx new file mode 100644 index 00000000..388ca333 --- /dev/null +++ b/frontend/components/directories/directories-view.tsx @@ -0,0 +1,208 @@ +"use client" + +import React, { useCallback, useMemo, useState, useEffect } from "react" +import { AlertTriangle } from "lucide-react" +import { DirectoriesDataTable } from "./directories-data-table" +import { createDirectoryColumns } from "./directories-columns" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" +import { Button } from "@/components/ui/button" +import { useTargetDirectories, useScanDirectories } from "@/hooks/use-directories" +import { DirectoryService } from "@/services/directory.service" +import type { Directory } from "@/types/directory.types" +import { toast } from "sonner" + +export function DirectoriesView({ + targetId, + scanId, +}: { + targetId?: number + scanId?: number +}) { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + const [selectedDirectories, setSelectedDirectories] = useState<Directory[]>([]) + + const [searchQuery, setSearchQuery] = useState("") + const [isSearching, setIsSearching] = useState(false) + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, pageIndex: 0 })) + } + + const targetQuery = useTargetDirectories( + targetId || 0, + { + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }, + { enabled: !!targetId } + ) + + const scanQuery = useScanDirectories( + scanId || 0, + { + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }, + { enabled: !!scanId } + ) + + const activeQuery = targetId ? targetQuery : scanQuery + const { data, isLoading, isFetching, error, refetch } = activeQuery + + useEffect(() => { + if (!isFetching && isSearching) { + setIsSearching(false) + } + }, [isFetching, isSearching]) + + const formatDate = useCallback((dateString: string) => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + }, []) + + const handleViewDetail = (directory: Directory) => { + // TODO: 实现查看目录详细功能 + console.log('查看目录详细:', directory) + } + + const columns = useMemo( + () => + createDirectoryColumns({ + formatDate, + onViewDetail: handleViewDetail, + }), + [formatDate] + ) + + const directories: Directory[] = useMemo(() => { + if (!data?.results) return [] + return data.results + }, [data]) + + const paginationInfo = data + ? { + total: data.total, + page: data.page, + pageSize: data.pageSize, + totalPages: data.totalPages, + } + : undefined + + const handleSelectionChange = useCallback((selectedRows: Directory[]) => { + setSelectedDirectories(selectedRows) + }, []) + + // 处理下载所有目录 + const handleDownloadAll = async () => { + try { + let blob: Blob | null = null + + if (scanId) { + const data = await DirectoryService.exportDirectoriesByScanId(scanId) + blob = data + } else if (targetId) { + const data = await DirectoryService.exportDirectoriesByTargetId(targetId) + blob = data + } else { + if (!directories || directories.length === 0) { + return + } + const content = directories.map((item) => item.url).join("\n") + blob = new Blob([content], { type: "text/plain;charset=utf-8" }) + } + + if (!blob) return + + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + const prefix = scanId ? `scan-${scanId}` : targetId ? `target-${targetId}` : "directories" + a.href = url + a.download = `${prefix}-directories-${Date.now()}.txt` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } catch (error) { + console.error("下载目录列表失败", error) + toast.error("下载目录列表失败,请稍后重试") + } + } + + // 处理下载选中的目录 + const handleDownloadSelected = () => { + if (selectedDirectories.length === 0) { + return + } + const content = selectedDirectories.map((item) => item.url).join("\n") + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + const prefix = scanId ? `scan-${scanId}` : targetId ? `target-${targetId}` : "directories" + a.href = url + a.download = `${prefix}-directories-selected-${Date.now()}.txt` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + if (error) { + return ( + <div className="flex flex-col items-center justify-center py-12"> + <div className="rounded-full bg-destructive/10 p-3 mb-4"> + <AlertTriangle className="h-10 w-10 text-destructive" /> + </div> + <h3 className="text-lg font-semibold mb-2">加载失败</h3> + <p className="text-muted-foreground text-center mb-4"> + 加载目录数据时出现错误,请重试 + </p> + <Button onClick={() => refetch()}>重新加载</Button> + </div> + ) + } + + if (isLoading && !data) { + return ( + <DataTableSkeleton + toolbarButtonCount={2} + rows={6} + columns={5} + /> + ) + } + + return ( + <> + <DirectoriesDataTable + data={directories} + columns={columns} + searchPlaceholder="搜索URL..." + searchColumn="url" + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + pagination={pagination} + setPagination={setPagination} + paginationInfo={paginationInfo} + onPaginationChange={setPagination} + onSelectionChange={handleSelectionChange} + onDownloadAll={handleDownloadAll} + onDownloadSelected={handleDownloadSelected} + /> + </> + ) +} diff --git a/frontend/components/disk/disk-stat-cards.tsx b/frontend/components/disk/disk-stat-cards.tsx new file mode 100644 index 00000000..f6fdacca --- /dev/null +++ b/frontend/components/disk/disk-stat-cards.tsx @@ -0,0 +1,39 @@ +"use client" + +import { useDiskStats } from '@/hooks/use-disk' +import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Skeleton } from '@/components/ui/skeleton' +import { IconDatabase } from '@tabler/icons-react' +import { formatBytes } from '@/lib/utils' + +function StatCard({ title, value, icon, loading }: { title: string; value: string | number; icon: React.ReactNode; loading?: boolean }) { + return ( + <Card className="@container/card"> + <CardHeader> + <CardDescription className="flex items-center gap-2"> + {icon} + {title} + </CardDescription> + {loading ? ( + <Skeleton className="h-8 w-24" /> + ) : ( + <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl"> + {value} + </CardTitle> + )} + </CardHeader> + </Card> + ) +} + +export function DiskStatCards() { + const { data, isLoading } = useDiskStats() + + return ( + <div className="grid grid-cols-1 gap-4 px-4 lg:px-6 @xl/main:grid-cols-3"> + <StatCard title="总容量" value={formatBytes(data?.totalBytes ?? 0)} icon={<IconDatabase />} loading={isLoading} /> + <StatCard title="已使用" value={formatBytes(data?.usedBytes ?? 0)} icon={<IconDatabase />} loading={isLoading} /> + <StatCard title="可用空间" value={formatBytes(data?.freeBytes ?? 0)} icon={<IconDatabase />} loading={isLoading} /> + </div> + ) +} diff --git a/frontend/components/endpoints/endpoints-columns.tsx b/frontend/components/endpoints/endpoints-columns.tsx new file mode 100644 index 00000000..239ffa68 --- /dev/null +++ b/frontend/components/endpoints/endpoints-columns.tsx @@ -0,0 +1,586 @@ +"use client" + +import React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { MoreHorizontal, Eye, Trash2, ChevronsUpDown, ChevronUp, ChevronDown, Copy, Check } from "lucide-react" +import type { Endpoint } from "@/types/endpoint.types" +import { toast } from "sonner" + +function CopyableCell({ + value, + maxWidth = "500px", + truncateLength = 60, + successMessage = "已复制", + className = "font-mono" +}: { + value: string | undefined | null + maxWidth?: string + truncateLength?: number + successMessage?: string + className?: string +}) { + const [copied, setCopied] = React.useState(false) + + if (!value) { + return <span className="text-muted-foreground text-sm">-</span> + } + + const isLong = value.length > truncateLength + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(value) + setCopied(true) + toast.success(successMessage) + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error('复制失败') + } + } + + return ( + <div className="group inline-flex items-center gap-1" style={{ maxWidth }}> + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <div className={`text-sm truncate cursor-default ${className}`}> + {value} + </div> + </TooltipTrigger> + <TooltipContent + side="top" + align="start" + sideOffset={5} + className={`text-xs ${className} ${isLong ? 'max-w-[500px] break-all' : 'whitespace-nowrap'}`} + > + {value} + </TooltipContent> + </Tooltip> + </TooltipProvider> + + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className={`h-6 w-6 flex-shrink-0 hover:bg-accent transition-opacity ${ + copied ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' + }`} + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">{copied ? '已复制!' : '点击复制'}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + ) +} + +interface CreateColumnsProps { + formatDate: (dateString: string) => string + navigate: (path: string) => void + handleDelete: (endpoint: Endpoint) => void +} + +function EndpointRowActions({ + onView, + onDelete, +}: { + onView: () => void + onDelete: () => void +}) { + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" + > + <MoreHorizontal /> + <span className="sr-only">打开菜单</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem onClick={onView}> + <Eye /> + 查看详情 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={onDelete} + className="text-destructive focus:text-destructive" + > + <Trash2 /> + 删除 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) +} + +function DataTableColumnHeader({ + column, + title, +}: { + column: { getCanSort: () => boolean; getIsSorted: () => false | "asc" | "desc"; toggleSorting: (desc?: boolean) => void } + title: string +}) { + if (!column.getCanSort()) { + return <div className="-ml-3 font-medium">{title}</div> + } + + const isSorted = column.getIsSorted() + + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted" + > + {title} + {isSorted === "asc" ? ( + <ChevronUp /> + ) : isSorted === "desc" ? ( + <ChevronDown /> + ) : ( + <ChevronsUpDown /> + )} + </Button> + ) +} + +function HttpStatusBadge({ statusCode }: { statusCode: number | null | undefined }) { + if (statusCode === null || statusCode === undefined) { + return ( + <Badge variant="outline" className="text-muted-foreground px-2 py-1 font-mono"> + - + </Badge> + ) + } + + const getStatusVariant = (code: number): "default" | "secondary" | "destructive" | "outline" => { + if (code >= 200 && code < 300) { + return "outline" + } else if (code >= 300 && code < 400) { + return "secondary" + } else if (code >= 400 && code < 500) { + return "default" + } else if (code >= 500) { + return "destructive" + } else { + return "secondary" + } + } + + const variant = getStatusVariant(statusCode) + + return ( + <Badge variant={variant} className="px-2 py-1 font-mono tabular-nums"> + {statusCode} + </Badge> + ) +} + +export function createEndpointColumns({ + formatDate, + navigate, + handleDelete, +}: CreateColumnsProps): ColumnDef<Endpoint>[] { + return [ + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "url", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="URL" /> + ), + size: 300, + minSize: 200, + maxSize: 400, + cell: ({ row }) => { + const url = row.getValue("url") as string | undefined + + if (!url) { + return <span className="text-muted-foreground text-sm">-</span> + } + + const maxLength = 40 + const isLong = url.length > maxLength + const displayUrl = isLong ? url.substring(0, maxLength) + "..." : url + + return ( + <div className="flex items-center gap-1 w-[280px] min-w-[280px]"> + <span className="text-sm font-mono truncate"> + {displayUrl} + </span> + {isLong && ( + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整 URL</h4> + <div className="text-xs break-all bg-muted p-2 rounded max-h-48 overflow-y-auto font-mono"> + {url} + </div> + </div> + </PopoverContent> + </Popover> + )} + </div> + ) + }, + }, + { + accessorKey: "title", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Title" /> + ), + cell: ({ row }) => { + const title = row.getValue("title") as string | null | undefined + if (!title) return <span className="text-sm">-</span> + + const maxLength = 30 + const isLong = title.length > maxLength + const displayText = isLong ? title.substring(0, maxLength) : title + + if (!isLong) { + return <span className="text-sm">{title}</span> + } + + return ( + <div className="flex items-center gap-1"> + <span className="text-sm">{displayText}</span> + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整标题</h4> + <div className="text-sm break-all bg-muted p-2 rounded max-h-32 overflow-y-auto"> + {title} + </div> + </div> + </PopoverContent> + </Popover> + </div> + ) + }, + }, + { + accessorKey: "statusCode", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Status" /> + ), + cell: ({ row }) => { + const status = row.getValue("statusCode") as number | null | undefined + return <HttpStatusBadge statusCode={status} /> + }, + }, + { + accessorKey: "contentLength", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Content Length" /> + ), + cell: ({ row }) => { + const len = row.getValue("contentLength") as number | null | undefined + if (len === null || len === undefined) { + return <span className="text-muted-foreground text-sm">-</span> + } + return <span className="font-mono tabular-nums">{new Intl.NumberFormat().format(len)}</span> + }, + }, + { + accessorKey: "location", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Location" /> + ), + cell: ({ row }) => { + const location = row.getValue("location") as string | undefined + if (!location) return <span className="text-sm text-muted-foreground">-</span> + + const maxLength = 50 + const isLong = location.length > maxLength + const displayText = isLong ? location.substring(0, maxLength) : location + + if (!isLong) { + return <span className="text-sm">{location}</span> + } + + return ( + <div className="flex items-center gap-1"> + <span className="text-sm">{displayText}</span> + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整 Location</h4> + <div className="text-sm break-all bg-muted p-2 rounded max-h-32 overflow-y-auto"> + {location} + </div> + </div> + </PopoverContent> + </Popover> + </div> + ) + }, + }, + { + accessorKey: "webserver", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Web Server" /> + ), + cell: ({ row }) => { + const webserver = row.getValue("webserver") as string | undefined + if (!webserver) return <span className="text-sm text-muted-foreground">-</span> + + const maxLength = 20 + const isLong = webserver.length > maxLength + const displayText = isLong ? webserver.substring(0, maxLength) : webserver + + if (!isLong) { + return <span className="text-sm">{webserver}</span> + } + + return ( + <div className="flex items-center gap-1"> + <span className="text-sm">{displayText}</span> + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整 Web Server</h4> + <div className="text-sm break-all bg-muted p-2 rounded max-h-32 overflow-y-auto"> + {webserver} + </div> + </div> + </PopoverContent> + </Popover> + </div> + ) + }, + }, + { + accessorKey: "contentType", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Content Type" /> + ), + cell: ({ row }) => { + const ct = row.getValue("contentType") as string | null | undefined + return ct ? <span className="text-sm">{ct}</span> : <span className="text-muted-foreground text-sm">-</span> + }, + }, + { + accessorKey: "tech", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Technologies" /> + ), + cell: ({ row }) => { + const tech = (row.getValue("tech") as string[] | null | undefined) || [] + if (!tech.length) return <span className="text-sm text-muted-foreground">-</span> + + const displayTech = tech.slice(0, 2) + const hasMore = tech.length > 2 + + return ( + <div className="flex flex-wrap gap-1 max-w-[200px]"> + {displayTech.map((t, index) => ( + <Badge key={index} variant="outline" className="text-xs"> + {t} + </Badge> + ))} + {hasMore && ( + <Popover> + <PopoverTrigger asChild> + <Badge variant="secondary" className="text-xs cursor-pointer hover:bg-muted"> + +{tech.length - 2} + </Badge> + </PopoverTrigger> + <PopoverContent className="w-80 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">所有技术栈 ({tech.length})</h4> + <div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto"> + {tech.map((technology, index) => ( + <Badge + key={index} + variant="outline" + className="text-xs" + > + {technology} + </Badge> + ))} + </div> + </div> + </PopoverContent> + </Popover> + )} + </div> + ) + }, + }, + { + accessorKey: "bodyPreview", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Body Preview" /> + ), + cell: ({ row }) => { + const bodyPreview = row.getValue("bodyPreview") as string | undefined + if (!bodyPreview) return <span className="text-sm text-muted-foreground">-</span> + + const maxLength = 30 + const isLong = bodyPreview.length > maxLength + const displayText = isLong ? bodyPreview.substring(0, maxLength) : bodyPreview + + if (!isLong) { + return <span className="text-sm">{bodyPreview}</span> + } + + return ( + <div className="flex items-center gap-1"> + <span className="text-sm">{displayText}</span> + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整响应体预览</h4> + <div className="text-sm break-all bg-muted p-2 rounded max-h-32 overflow-y-auto"> + {bodyPreview} + </div> + </div> + </PopoverContent> + </Popover> + </div> + ) + }, + }, + { + accessorKey: "vhost", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="VHost" /> + ), + cell: ({ row }) => { + const vhost = row.getValue("vhost") as boolean | null | undefined + if (vhost === null || vhost === undefined) return <span className="text-sm text-muted-foreground">-</span> + return <span className="text-sm font-mono">{vhost ? "true" : "false"}</span> + }, + }, + { + accessorKey: "tags", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Tags" /> + ), + cell: ({ row }) => { + const tags = (row.getValue("tags") as string[] | null | undefined) || [] + if (!tags.length) { + return <span className="text-muted-foreground text-sm">-</span> + } + return ( + <div className="flex flex-wrap gap-1"> + {tags.map((tag, idx) => ( + <Badge + key={idx} + variant={/xss|sqli|idor|rce|ssrf|lfi|rfi|xxe|csrf|open.?redirect|interesting/i.test(tag) ? "destructive" : "secondary"} + className="text-xs" + > + {tag} + </Badge> + ))} + </div> + ) + }, + enableSorting: false, + }, + { + accessorKey: "responseTime", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Response Time" /> + ), + cell: ({ row }) => { + const rt = row.getValue("responseTime") as number | null | undefined + if (rt === null || rt === undefined) { + return <span className="text-muted-foreground text-sm">-</span> + } + const formatted = `${rt.toFixed(4)}s` + return <span className="font-mono text-emerald-600 dark:text-emerald-400">{formatted}</span> + }, + }, + { + accessorKey: "discoveredAt", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Discovered At" /> + ), + cell: ({ row }) => { + const discoveredAt = row.getValue("discoveredAt") as string | undefined + return <div className="text-sm">{discoveredAt ? formatDate(discoveredAt) : "-"}</div> + }, + }, + ] +} diff --git a/frontend/components/endpoints/endpoints-data-table.tsx b/frontend/components/endpoints/endpoints-data-table.tsx new file mode 100644 index 00000000..cb315ab8 --- /dev/null +++ b/frontend/components/endpoints/endpoints-data-table.tsx @@ -0,0 +1,407 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconPlus, + IconDownload, + IconSearch, + IconLoader2, +} from "@tabler/icons-react" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import type { Endpoint } from "@/types/endpoint.types" + +interface EndpointsDataTableProps<TData extends { id: number | string }, TValue> { + columns: ColumnDef<TData, TValue>[] + data: TData[] + searchPlaceholder?: string + searchColumn?: string + searchValue?: string + onSearch?: (value: string) => void + isSearching?: boolean + onAddNew?: () => void + addButtonText?: string + onSelectionChange?: (selectedRows: TData[]) => void + pagination?: { pageIndex: number; pageSize: number } + onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void + totalCount?: number + totalPages?: number + onDownloadAll?: () => void + onDownloadSelected?: () => void +} + +export function EndpointsDataTable<TData extends { id: number | string }, TValue>({ + columns, + data, + searchPlaceholder = "搜索主机名...", + searchColumn = "url", + searchValue, + onSearch, + isSearching = false, + onAddNew, + addButtonText = "Add", + onSelectionChange, + pagination: externalPagination, + onPaginationChange, + totalCount, + totalPages, + onDownloadAll, + onDownloadSelected, +}: EndpointsDataTableProps<TData, TValue>) { + const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({}) + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [sorting, setSorting] = React.useState<SortingState>([]) + const [internalPagination, setInternalPagination] = React.useState<{ pageIndex: number, pageSize: number }>({ + pageIndex: 0, + pageSize: 10, + }) + + // 本地搜索输入状态(只在回车或点击按钮时触发搜索) + const [localSearchValue, setLocalSearchValue] = React.useState(searchValue ?? "") + + React.useEffect(() => { + setLocalSearchValue(searchValue ?? "") + }, [searchValue]) + + const handleSearchSubmit = () => { + if (onSearch) { + onSearch(localSearchValue) + } else { + table.getColumn(searchColumn)?.setFilterValue(localSearchValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearchSubmit() + } + } + + const pagination = externalPagination || internalPagination + + const handlePaginationChange = React.useCallback((updaterOrValue: { pageIndex: number; pageSize: number } | ((prev: { pageIndex: number; pageSize: number }) => { pageIndex: number; pageSize: number })) => { + if (onPaginationChange) { + if (typeof updaterOrValue === 'function') { + const newPagination = updaterOrValue(pagination) + onPaginationChange(newPagination) + } else { + onPaginationChange(updaterOrValue) + } + } else { + setInternalPagination(updaterOrValue) + } + }, [onPaginationChange, pagination]) + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination, + }, + getRowId: (row) => row.id.toString(), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: handlePaginationChange, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: externalPagination ? undefined : getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + manualPagination: !!externalPagination, + pageCount: totalPages, + }) + + React.useEffect(() => { + if (onSelectionChange) { + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + onSelectionChange(selectedRows) + } + }, [rowSelection, onSelectionChange, table]) + + return ( + <div className="w-full space-y-4"> + <div className="flex items-center justify-between"> + <div className="flex items-center space-x-2"> + <Input + placeholder={searchPlaceholder} + value={localSearchValue} + onChange={(e) => setLocalSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="h-8 max-w-sm" + /> + <Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}> + {isSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + + <div className="flex items-center space-x-2"> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + return ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {column.id === "id" && "ID"} + {column.id === "url" && "URL"} + {column.id === "endpoint" && "Endpoint"} + {column.id === "method" && "Method"} + {column.id === "statusCode" && "Status"} + {column.id === "title" && "Title"} + {column.id === "contentLength" && "Size"} + {column.id === "contentType" && "Content Type"} + {column.id === "responseTime" && "Response time"} + {column.id === "tags" && "Tags"} + {column.id === "createdAt" && "Created At"} + {column.id === "updatedAt" && "Updated At"} + {!["id", "url", "endpoint", "method", "statusCode", "title", "contentLength", "contentType", "responseTime", "tags", "createdAt", "updatedAt"].includes(column.id) && column.id} + </DropdownMenuCheckboxItem> + ) + })} + </DropdownMenuContent> + </DropdownMenu> + + {(onDownloadAll || onDownloadSelected) && ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconDownload /> + Download + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-64"> + <DropdownMenuLabel>Download Options</DropdownMenuLabel> + <DropdownMenuSeparator /> + {onDownloadAll && ( + <DropdownMenuItem onClick={onDownloadAll}> + <IconDownload className="h-4 w-4" /> + Download All Endpoints + </DropdownMenuItem> + )} + {onDownloadSelected && ( + <DropdownMenuItem + onClick={onDownloadSelected} + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + > + <IconDownload className="h-4 w-4" /> + Download Selected Endpoints + </DropdownMenuItem> + )} + </DropdownMenuContent> + </DropdownMenu> + )} + + {onAddNew && ( + <Button onClick={onAddNew} size="sm"> + <IconPlus /> + {addButtonText} + </Button> + )} + </div> + </div> + + <div className="rounded-md border"> + <Table> + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id} colSpan={header.colSpan}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + No results + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + <div className="flex items-center justify-between px-2"> + <div className="flex-1 text-sm text-muted-foreground"> + {table.getFilteredSelectedRowModel().rows.length} of{" "} + {externalPagination ? totalCount : table.getFilteredRowModel().rows.length} row(s) selected + </div> + + <div className="flex items-center space-x-6 lg:space-x-8"> + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + Rows per page + </Label> + <Select + value={`${table.getState().pagination.pageSize}`} + onValueChange={(value) => { + table.setPageSize(Number(value)) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={table.getState().pagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + Page {table.getState().pagination.pageIndex + 1} of{" "} + {externalPagination ? totalPages : table.getPageCount()} + </div> + + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(0)} + disabled={externalPagination ? pagination.pageIndex === 0 : !table.getCanPreviousPage()} + > + <span className="sr-only">Go to first page</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={externalPagination ? pagination.pageIndex === 0 : !table.getCanPreviousPage()} + > + <span className="sr-only">Go to previous page</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={externalPagination ? pagination.pageIndex >= (totalPages || 1) - 1 : !table.getCanNextPage()} + > + <span className="sr-only">Go to next page</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex((totalPages || table.getPageCount()) - 1)} + disabled={externalPagination ? pagination.pageIndex >= (totalPages || 1) - 1 : !table.getCanNextPage()} + > + <span className="sr-only">Go to last page</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + </div> + ) +} diff --git a/frontend/components/endpoints/endpoints-detail-view.tsx b/frontend/components/endpoints/endpoints-detail-view.tsx new file mode 100644 index 00000000..71914c9f --- /dev/null +++ b/frontend/components/endpoints/endpoints-detail-view.tsx @@ -0,0 +1,276 @@ +"use client" + +import React, { useState, useMemo } from "react" +import { AlertTriangle } from "lucide-react" +import { useRouter } from "next/navigation" +import { useTargetEndpoints } from "@/hooks/use-targets" +import { useDeleteEndpoint, useScanEndpoints } from "@/hooks/use-endpoints" +import { EndpointsDataTable } from "./endpoints-data-table" +import { createEndpointColumns } from "./endpoints-columns" +import { LoadingSpinner } from "@/components/loading-spinner" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import type { Endpoint } from "@/types/endpoint.types" +import { EndpointService } from "@/services/endpoint.service" +import { toast } from "sonner" + +/** + * 目标端点详情视图组件 + * 用于显示和管理目标下的端点列表 + */ +export function EndpointsDetailView({ + targetId, + scanId, +}: { + targetId?: number + scanId?: number +}) { + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [endpointToDelete, setEndpointToDelete] = useState<Endpoint | null>(null) + const [selectedEndpoints, setSelectedEndpoints] = useState<Endpoint[]>([]) + + // 分页状态管理 + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10 + }) + + const [searchQuery, setSearchQuery] = useState("") + const [isSearching, setIsSearching] = useState(false) + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, pageIndex: 0 })) + } + + // 删除相关 hooks + const deleteEndpoint = useDeleteEndpoint() + + // 使用 React Query 获取端点数据:优先按 targetId,其次按 scanId(历史快照) + const targetEndpointsQuery = useTargetEndpoints(targetId || 0, { + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }, { enabled: !!targetId }) + + const scanEndpointsQuery = useScanEndpoints(scanId || 0, { + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }, { enabled: !!scanId }) + + const { + data, + isLoading, + isFetching, + error, + refetch, + } = targetId ? targetEndpointsQuery : scanEndpointsQuery + + React.useEffect(() => { + if (!isFetching && isSearching) { + setIsSearching(false) + } + }, [isFetching, isSearching]) + + // 辅助函数 - 格式化日期 + const formatDate = React.useCallback((dateString: string): string => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + }, []) + + // 导航函数 + const router = useRouter() + const navigate = React.useCallback((path: string) => { + router.push(path) + }, [router]) + + // 处理删除端点 + const handleDeleteEndpoint = React.useCallback((endpoint: Endpoint) => { + setEndpointToDelete(endpoint) + setDeleteDialogOpen(true) + }, []) + + // 确认删除端点 + const confirmDelete = async () => { + if (!endpointToDelete) return + + setDeleteDialogOpen(false) + setEndpointToDelete(null) + + deleteEndpoint.mutate(endpointToDelete.id) + } + + // 处理分页变化 + const handlePaginationChange = (newPagination: { pageIndex: number; pageSize: number }) => { + setPagination(newPagination) + } + + const handleSelectionChange = React.useCallback((selectedRows: Endpoint[]) => { + setSelectedEndpoints(selectedRows) + }, []) + + // 创建列定义 + const endpointColumns = useMemo( + () => + createEndpointColumns({ + formatDate, + navigate, + handleDelete: handleDeleteEndpoint, + }), + [formatDate, navigate, handleDeleteEndpoint] + ) + + // 下载所有端点 URL + const handleDownloadAll = async () => { + try { + let blob: Blob | null = null + + if (scanId) { + const data = await EndpointService.exportEndpointsByScanId(scanId) + blob = data + } else if (targetId) { + const data = await EndpointService.exportEndpointsByTargetId(targetId) + blob = data + } else { + const endpoints: Endpoint[] = (data as any)?.endpoints || [] + if (!endpoints || endpoints.length === 0) { + return + } + const content = endpoints.map((item) => item.url).join("\n") + blob = new Blob([content], { type: "text/plain;charset=utf-8" }) + } + + if (!blob) return + + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + const prefix = scanId ? `scan-${scanId}` : targetId ? `target-${targetId}` : "endpoints" + a.href = url + a.download = `${prefix}-endpoints-${Date.now()}.txt` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } catch (error) { + console.error("下载端点列表失败", error) + toast.error("下载端点列表失败,请稍后重试") + } + } + + // 下载选中的端点 URL + const handleDownloadSelected = () => { + if (selectedEndpoints.length === 0) { + return + } + const content = selectedEndpoints.map((item) => item.url).join("\n") + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + const prefix = scanId ? `scan-${scanId}` : targetId ? `target-${targetId}` : "endpoints" + a.href = url + a.download = `${prefix}-endpoints-selected-${Date.now()}.txt` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + // 错误状态 + if (error) { + return ( + <div className="flex flex-col items-center justify-center py-12"> + <div className="rounded-full bg-destructive/10 p-3 mb-4"> + <AlertTriangle className="h-10 w-10 text-destructive" /> + </div> + <h3 className="text-lg font-semibold mb-2">加载失败</h3> + <p className="text-muted-foreground text-center mb-4"> + {error.message || "加载端点数据时出现错误,请重试"} + </p> + <button + onClick={() => refetch()} + className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" + > + 重新加载 + </button> + </div> + ) + } + + // 加载状态(仅首次加载时显示骨架屏) + if (isLoading && !data) { + return ( + <DataTableSkeleton + toolbarButtonCount={2} + rows={6} + columns={5} + /> + ) + } + + return ( + <> + <EndpointsDataTable + data={data?.endpoints || []} + columns={endpointColumns} + searchPlaceholder="搜索主机名..." + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + pagination={pagination} + onPaginationChange={handlePaginationChange} + totalCount={data?.pagination?.total || 0} + totalPages={data?.pagination?.totalPages || 1} + onSelectionChange={handleSelectionChange} + onDownloadAll={handleDownloadAll} + onDownloadSelected={handleDownloadSelected} + /> + + {/* 删除确认对话框 */} + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除</AlertDialogTitle> + <AlertDialogDescription> + 此操作无法撤销。这将永久删除该端点及其相关数据。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + disabled={deleteEndpoint.isPending} + > + {deleteEndpoint.isPending ? ( + <> + <LoadingSpinner /> + 删除中... + </> + ) : ( + "删除" + )} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </> + ) +} diff --git a/frontend/components/endpoints/index.ts b/frontend/components/endpoints/index.ts new file mode 100644 index 00000000..2bbb7988 --- /dev/null +++ b/frontend/components/endpoints/index.ts @@ -0,0 +1,3 @@ +export { EndpointsDetailView } from './endpoints-detail-view' +export { EndpointsDataTable } from './endpoints-data-table' +export { createEndpointColumns } from './endpoints-columns' diff --git a/frontend/components/ip-addresses/index.ts b/frontend/components/ip-addresses/index.ts new file mode 100644 index 00000000..49c4ba61 --- /dev/null +++ b/frontend/components/ip-addresses/index.ts @@ -0,0 +1 @@ +export { IPAddressesView } from "./ip-addresses-view" diff --git a/frontend/components/ip-addresses/ip-addresses-columns.tsx b/frontend/components/ip-addresses/ip-addresses-columns.tsx new file mode 100644 index 00000000..0219c517 --- /dev/null +++ b/frontend/components/ip-addresses/ip-addresses-columns.tsx @@ -0,0 +1,334 @@ +"use client" + +import React from "react" +import { Column, ColumnDef } from "@tanstack/react-table" +import { ChevronUp, ChevronDown, ChevronsUpDown, Copy, Check, MoreHorizontal, Trash2 } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { Badge } from "@/components/ui/badge" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import type { IPAddress } from "@/types/ip-address.types" +import { toast } from "sonner" + +/** + * 可复制单元格组件 + */ +function CopyableCell({ + value, + maxWidth = "400px", + truncateLength = 50, + successMessage = "已复制", + className = "font-medium" +}: { + value: string + maxWidth?: string + truncateLength?: number + successMessage?: string + className?: string +}) { + const [copied, setCopied] = React.useState(false) + const isLong = value.length > truncateLength + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(value) + setCopied(true) + toast.success(successMessage) + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error('复制失败') + } + } + + return ( + <div className="group inline-flex items-center gap-1" style={{ maxWidth }}> + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <div className={`text-sm truncate cursor-default ${className}`}> + {value} + </div> + </TooltipTrigger> + <TooltipContent + side="top" + align="start" + sideOffset={5} + className={`text-xs ${isLong ? 'max-w-[500px] break-all' : 'whitespace-nowrap'}`} + > + {value} + </TooltipContent> + </Tooltip> + </TooltipProvider> + + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className={`h-6 w-6 flex-shrink-0 hover:bg-accent transition-opacity ${ + copied ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' + }`} + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">{copied ? '已复制!' : '点击复制'}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + ) +} + +interface DataTableColumnHeaderProps<TData, TValue> { + column: Column<TData, TValue> + title: string +} + +function DataTableColumnHeader<TData, TValue>({ + column, + title, +}: DataTableColumnHeaderProps<TData, TValue>) { + if (!column?.getCanSort()) { + return <div className="-ml-3 font-medium">{title}</div> + } + + const isSorted = column.getIsSorted() + + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted" + > + {title} + {isSorted === "asc" ? ( + <ChevronUp /> + ) : isSorted === "desc" ? ( + <ChevronDown /> + ) : ( + <ChevronsUpDown /> + )} + </Button> + ) +} + +export function createIPAddressColumns(params: { + formatDate: (value: string) => string +}) { + const { formatDate } = params + + const columns: ColumnDef<IPAddress>[] = [ + // 选择列 + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + // IP 地址列 + { + accessorKey: "ip", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="IP 地址" /> + ), + cell: ({ row }) => { + const ip = row.original.ip + if (!ip) return <span className="text-muted-foreground text-sm">-</span> + + const maxLength = 40 + const isLong = ip.length > maxLength + const displayIp = isLong ? ip.substring(0, maxLength) + "..." : ip + + return ( + <div className="flex items-center gap-1 max-w-[350px]"> + <span className="text-sm font-mono"> + {displayIp} + </span> + {isLong && ( + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整 IP 地址</h4> + <div className="text-xs break-all bg-muted p-2 rounded max-h-48 overflow-y-auto font-mono"> + {ip} + </div> + </div> + </PopoverContent> + </Popover> + )} + </div> + ) + }, + }, + // 关联主机名列 + { + accessorKey: "hosts", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="关联主机" /> + ), + cell: ({ getValue }) => { + const hosts = getValue<string[]>() + if (!hosts || hosts.length === 0) { + return <span className="text-muted-foreground">-</span> + } + + // 显示前3个主机名 + const displayHosts = hosts.slice(0, 3) + const hasMore = hosts.length > 3 + + return ( + <div className="flex flex-col gap-1"> + {displayHosts.map((host, index) => ( + <span key={index} className="text-sm font-mono">{host}</span> + ))} + {hasMore && ( + <Badge variant="secondary" className="text-xs w-fit"> + +{hosts.length - 3} more + </Badge> + )} + </div> + ) + }, + }, + // 发现时间列 + { + accessorKey: "discoveredAt", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="发现时间" /> + ), + cell: ({ getValue }) => { + const value = getValue<string | undefined>() + return value ? formatDate(value) : "-" + }, + }, + // 开放端口列 + { + accessorKey: "ports", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="开放端口" /> + ), + cell: ({ getValue }) => { + const ports = getValue<number[]>() + if (!ports || ports.length === 0) { + return <span className="text-muted-foreground">-</span> + } + + // 常见端口颜色映射 + const getPortVariant = (portNumber: number) => { + const commonPorts = [80, 443, 22, 21, 25, 53, 110, 143, 993, 995] + const webPorts = [80, 443, 8080, 8443, 3000, 8000, 8888] + const sshPorts = [22] + + if (sshPorts.includes(portNumber)) return "destructive" + if (webPorts.includes(portNumber)) return "default" + if (commonPorts.includes(portNumber)) return "secondary" + return "outline" + } + + // 按端口重要性排序:常见端口优先 + const sortedPorts = [...ports].sort((a, b) => { + const commonPorts = [80, 443, 22, 21, 25, 53, 110, 143, 993, 995] + const webPorts = [80, 443, 8080, 8443, 3000, 8000, 8888] + + const aScore = webPorts.includes(a) ? 3 : + commonPorts.includes(a) ? 2 : 1 + const bScore = webPorts.includes(b) ? 3 : + commonPorts.includes(b) ? 2 : 1 + + if (aScore !== bScore) return bScore - aScore + return a - b + }) + + // 显示前8个端口 + const displayPorts = sortedPorts.slice(0, 8) + const hasMore = sortedPorts.length > 8 + + return ( + <div className="flex flex-wrap gap-1 max-w-xs"> + {displayPorts.map((port, index) => ( + <Badge + key={index} + variant={getPortVariant(port)} + className="text-xs font-mono" + > + {port} + </Badge> + ))} + {hasMore && ( + <Popover> + <PopoverTrigger asChild> + <Badge variant="outline" className="text-xs cursor-pointer hover:bg-muted"> + +{ports.length - 8} + </Badge> + </PopoverTrigger> + <PopoverContent className="w-80 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">所有开放端口 ({sortedPorts.length})</h4> + <div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto"> + {sortedPorts.map((port, index) => ( + <Badge + key={index} + variant={getPortVariant(port)} + className="text-xs font-mono" + > + {port} + </Badge> + ))} + </div> + </div> + </PopoverContent> + </Popover> + )} + </div> + ) + }, + }, + ] + + return columns +} diff --git a/frontend/components/ip-addresses/ip-addresses-data-table.tsx b/frontend/components/ip-addresses/ip-addresses-data-table.tsx new file mode 100644 index 00000000..280541c4 --- /dev/null +++ b/frontend/components/ip-addresses/ip-addresses-data-table.tsx @@ -0,0 +1,411 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconTrash, + IconDownload, + IconSearch, + IconLoader2, +} from "@tabler/icons-react" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import type { IPAddress } from "@/types/ip-address.types" +import type { PaginationInfo } from "@/types/common.types" + +interface IPAddressesDataTableProps { + data: IPAddress[] + columns: ColumnDef<IPAddress>[] + searchPlaceholder?: string + searchColumn?: string + searchValue?: string + onSearch?: (value: string) => void + isSearching?: boolean + pagination?: { pageIndex: number; pageSize: number } + setPagination?: React.Dispatch<React.SetStateAction<{ pageIndex: number; pageSize: number }>> + paginationInfo?: PaginationInfo + onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void + onBulkDelete?: () => void // 批量删除回调函数 + onSelectionChange?: (selectedRows: IPAddress[]) => void // 选中行变化回调 + // 下载回调函数 + onDownloadAll?: () => void // 下载所有 IP 地址 + onDownloadSelected?: () => void // 下载选中的 IP 地址 +} + +export function IPAddressesDataTable({ + data = [], + columns, + searchPlaceholder = "搜索 IP 地址...", + searchColumn = "ip", + searchValue, + onSearch, + isSearching = false, + pagination, + setPagination, + paginationInfo, + onPaginationChange, + onBulkDelete, + onSelectionChange, + onDownloadAll, + onDownloadSelected, +}: IPAddressesDataTableProps) { + const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({}) + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [sorting, setSorting] = React.useState<SortingState>([]) + const [internalPagination, setInternalPagination] = React.useState({ + pageIndex: 0, + pageSize: 10, + }) + + // 本地搜索输入状态(只在回车或点击按钮时触发搜索) + const [localSearchValue, setLocalSearchValue] = React.useState(searchValue ?? "") + + React.useEffect(() => { + setLocalSearchValue(searchValue ?? "") + }, [searchValue]) + + const handleSearchSubmit = () => { + if (onSearch) { + onSearch(localSearchValue) + } else { + table.getColumn(searchColumn)?.setFilterValue(localSearchValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearchSubmit() + } + } + + const useServerPagination = !!paginationInfo && !!pagination && !!setPagination + const tablePagination = useServerPagination ? pagination : internalPagination + const setTablePagination = useServerPagination ? setPagination : setInternalPagination + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination: tablePagination, + }, + getRowId: (row) => row.ip, // IP 地址本身就是唯一标识 + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = + typeof updater === "function" ? updater(tablePagination) : updater + setTablePagination?.(nextPagination as { pageIndex: number; pageSize: number }) + onPaginationChange?.(nextPagination) + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + manualPagination: useServerPagination, + pageCount: useServerPagination + ? paginationInfo?.totalPages ?? -1 + : Math.ceil(data.length / tablePagination.pageSize) || 1, + }) + + const totalItems = useServerPagination + ? paginationInfo?.total ?? data.length + : table.getFilteredRowModel().rows.length + + // 处理选中行变化 + React.useEffect(() => { + if (onSelectionChange) { + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + onSelectionChange(selectedRows) + } + }, [rowSelection, onSelectionChange, table]) + + return ( + <div className="w-full space-y-4"> + {/* 工具栏 */} + <div className="flex items-center justify-between"> + {/* 搜索框 */} + <div className="flex items-center space-x-2"> + <Input + placeholder={searchPlaceholder} + value={localSearchValue} + onChange={(e) => setLocalSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="h-8 max-w-sm" + /> + <Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}> + {isSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + + {/* 右侧操作按钮 */} + <div className="flex items-center space-x-2"> + {/* 列显示控制 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {column.id === "select" && "Select"} + {column.id === "ip" && "IP Address"} + {column.id === "subdomain" && "Subdomain"} + {column.id === "createdAt" && "Created At"} + {column.id === "ports" && "Ports"} + {column.id === "reversePointer" && "Reverse Pointer"} + {column.id === "actions" && "Actions"} + {!["select", "ip", "subdomain", "createdAt", "ports", "reversePointer", "actions"].includes(column.id) && column.id} + </DropdownMenuCheckboxItem> + ))} + </DropdownMenuContent> + </DropdownMenu> + + {/* 下载按钮 */} + {(onDownloadAll || onDownloadSelected) && ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconDownload /> + Download + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-64"> + <DropdownMenuLabel>Download Options</DropdownMenuLabel> + <DropdownMenuSeparator /> + {onDownloadAll && ( + <DropdownMenuItem onClick={onDownloadAll}> + <IconDownload className="h-4 w-4" /> + Download All IP Addresses + </DropdownMenuItem> + )} + {onDownloadSelected && ( + <DropdownMenuItem + onClick={onDownloadSelected} + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + > + <IconDownload className="h-4 w-4" /> + Download Selected IP Addresses + </DropdownMenuItem> + )} + </DropdownMenuContent> + </DropdownMenu> + )} + + {/* 批量删除按钮 */} + {onBulkDelete && ( + <Button + onClick={onBulkDelete} + size="sm" + variant="outline" + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + className={ + table.getFilteredSelectedRowModel().rows.length === 0 + ? "text-muted-foreground" + : "text-destructive hover:text-destructive hover:bg-destructive/10" + } + > + <IconTrash /> + Delete + </Button> + )} + </div> + </div> + + <div className="rounded-md border"> + <Table> + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => ( + <TableHead key={header.id}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ))} + </TableRow> + ))} + </TableHeader> + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + className="group" + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell colSpan={columns.length} className="h-24 text-center"> + 暂无数据 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {/* 分页控制 */} + <div className="flex items-center justify-between px-2"> + {/* 选中行信息 */} + <div className="flex-1 text-sm text-muted-foreground"> + {table.getFilteredSelectedRowModel().rows.length} of{" "} + {paginationInfo ? paginationInfo.total : table.getFilteredRowModel().rows.length} row(s) selected + </div> + + {/* 分页控制器 */} + <div className="flex items-center space-x-6 lg:space-x-8"> + {/* 每页显示数量选择 */} + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + Rows per page + </Label> + <Select + value={`${table.getState().pagination.pageSize}`} + onValueChange={(value) => { + table.setPageSize(Number(value)) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={table.getState().pagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 页码信息 */} + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + </div> + + {/* 分页按钮 */} + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to first page</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to previous page</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to next page</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to last page</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + </div> + ) +} diff --git a/frontend/components/ip-addresses/ip-addresses-view.tsx b/frontend/components/ip-addresses/ip-addresses-view.tsx new file mode 100644 index 00000000..006634a9 --- /dev/null +++ b/frontend/components/ip-addresses/ip-addresses-view.tsx @@ -0,0 +1,199 @@ +"use client" + +import React, { useCallback, useMemo, useState, useEffect } from "react" +import { AlertTriangle } from "lucide-react" +import { IPAddressesDataTable } from "./ip-addresses-data-table" +import { createIPAddressColumns } from "./ip-addresses-columns" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" +import { Button } from "@/components/ui/button" +import { useTargetIPAddresses, useScanIPAddresses } from "@/hooks/use-ip-addresses" +import type { IPAddress } from "@/types/ip-address.types" +import { IPAddressService } from "@/services/ip-address.service" +import { toast } from "sonner" + +export function IPAddressesView({ + targetId, + scanId, +}: { + targetId?: number + scanId?: number +}) { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + const [selectedIPAddresses, setSelectedIPAddresses] = useState<IPAddress[]>([]) + + const [searchQuery, setSearchQuery] = useState("") + const [isSearching, setIsSearching] = useState(false) + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, pageIndex: 0 })) + } + + const targetQuery = useTargetIPAddresses( + targetId || 0, + { + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }, + { enabled: !!targetId } + ) + + const scanQuery = useScanIPAddresses( + scanId || 0, + { + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }, + { enabled: !!scanId } + ) + + const activeQuery = targetId ? targetQuery : scanQuery + const { data, isLoading, isFetching, error, refetch } = activeQuery + + useEffect(() => { + if (!isFetching && isSearching) { + setIsSearching(false) + } + }, [isFetching, isSearching]) + + const formatDate = useCallback((dateString: string) => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + }, []) + + const columns = useMemo( + () => + createIPAddressColumns({ + formatDate, + }), + [formatDate] + ) + + const ipAddresses: IPAddress[] = useMemo(() => { + return data?.results ?? [] + }, [data]) + + const paginationInfo = data + ? { + total: data.total, + page: data.page, + pageSize: data.pageSize, + totalPages: data.totalPages, + } + : undefined + const handleSelectionChange = useCallback((selectedRows: IPAddress[]) => { + setSelectedIPAddresses(selectedRows) + }, []) + + // 处理下载所有 IP 地址 + const handleDownloadAll = async () => { + try { + let blob: Blob | null = null + + if (scanId) { + const data = await IPAddressService.exportIPAddressesByScanId(scanId) + blob = data + } else if (targetId) { + const data = await IPAddressService.exportIPAddressesByTargetId(targetId) + blob = data + } else { + if (!ipAddresses || ipAddresses.length === 0) { + return + } + const content = ipAddresses.map((item) => item.ip).join("\n") + blob = new Blob([content], { type: "text/plain;charset=utf-8" }) + } + + if (!blob) return + + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + const prefix = scanId ? `scan-${scanId}` : targetId ? `target-${targetId}` : "ip-addresses" + a.href = url + a.download = `${prefix}-ip-addresses-${Date.now()}.txt` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } catch (error) { + console.error("下载 IP 地址列表失败", error) + toast.error("下载 IP 地址列表失败,请稍后重试") + } + } + + // 处理下载选中的 IP 地址 + const handleDownloadSelected = () => { + if (selectedIPAddresses.length === 0) { + return + } + const content = selectedIPAddresses.map((item) => item.ip).join("\n") + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + const prefix = scanId ? `scan-${scanId}` : targetId ? `target-${targetId}` : "ip-addresses" + a.href = url + a.download = `${prefix}-ip-addresses-selected-${Date.now()}.txt` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + if (error) { + return ( + <div className="flex flex-col items-center justify-center py-12"> + <div className="rounded-full bg-destructive/10 p-3 mb-4"> + <AlertTriangle className="h-10 w-10 text-destructive" /> + </div> + <h3 className="text-lg font-semibold mb-2">加载失败</h3> + <p className="text-muted-foreground text-center mb-4"> + {error.message || "加载 IP 地址数据时出现错误,请重试"} + </p> + <Button onClick={() => refetch()}>重新加载</Button> + </div> + ) + } + + if (isLoading && !data) { + return ( + <DataTableSkeleton + toolbarButtonCount={1} + rows={6} + columns={4} + /> + ) + } + + return ( + <> + <IPAddressesDataTable + data={ipAddresses} + columns={columns} + searchPlaceholder="搜索IP地址..." + searchColumn="ip" + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + pagination={pagination} + setPagination={setPagination} + paginationInfo={paginationInfo} + onSelectionChange={handleSelectionChange} + onDownloadAll={handleDownloadAll} + onDownloadSelected={handleDownloadSelected} + /> + </> + ) +} diff --git a/frontend/components/loading-spinner.tsx b/frontend/components/loading-spinner.tsx new file mode 100644 index 00000000..9a8f0201 --- /dev/null +++ b/frontend/components/loading-spinner.tsx @@ -0,0 +1,92 @@ +"use client" + +import React from "react" +import { cn } from "@/lib/utils" +import { Spinner } from "@/components/ui/spinner" + +interface LoadingSpinnerProps { + size?: "sm" | "md" | "lg" + className?: string +} + +/** + * 统一的加载动画组件 + * + * 特性: + * - 三种尺寸:sm(16px), md(24px), lg(32px) + * - 支持自定义样式 + * - 使用 Tailwind CSS 动画 + */ +export function LoadingSpinner({ size = "sm", className }: LoadingSpinnerProps) { + const sizeMap = { + sm: "size-4", + md: "size-6", + lg: "size-8" + } + + return <Spinner className={cn(sizeMap[size], className)} /> +} + +interface LoadingStateProps { + message?: string + size?: "sm" | "md" | "lg" + className?: string +} + +/** + * 带文字的加载状态组件 + * + * 用于页面级别的加载状态显示 + */ +export function LoadingState({ + message = "加载中...", + size = "md", + className +}: LoadingStateProps) { + const sizeMap = { + sm: "size-4", + md: "size-6", + lg: "size-8" + } + + return ( + <div className={cn("flex items-center justify-center min-h-[200px] h-screen w-full", className)}> + <div className="flex flex-col items-center space-y-4"> + <Spinner className={sizeMap[size]} /> + <p className="text-sm text-muted-foreground">{message}</p> + </div> + </div> + ) +} + + +interface LoadingOverlayProps { + isLoading: boolean + message?: string + children: React.ReactNode +} + +/** + * 加载遮罩组件 + * + * 在现有内容上显示加载遮罩 + */ +export function LoadingOverlay({ + isLoading, + message = "加载中...", + children +}: LoadingOverlayProps) { + return ( + <div className="relative"> + {children} + {isLoading && ( + <div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50"> + <div className="flex flex-col items-center space-y-2"> + <LoadingSpinner size="lg" /> + <p className="text-sm text-muted-foreground">{message}</p> + </div> + </div> + )} + </div> + ) +} diff --git a/frontend/components/nav-secondary.tsx b/frontend/components/nav-secondary.tsx new file mode 100644 index 00000000..1bdeb2fb --- /dev/null +++ b/frontend/components/nav-secondary.tsx @@ -0,0 +1,60 @@ +"use client" // 标记为客户端组件,可以使用浏览器 API 和交互功能 + +// 导入 React 库 +import * as React from "react" +// 导入图标类型 +import { type Icon } from "@tabler/icons-react" + +// 导入侧边栏相关组件 +import { + SidebarGroup, // 侧边栏组 + SidebarGroupContent, // 侧边栏组内容 + SidebarMenu, // 侧边栏菜单 + SidebarMenuButton, // 侧边栏菜单按钮 + SidebarMenuItem, // 侧边栏菜单项 +} from '@/components/ui/sidebar' + +/** + * 次要导航组件 + * 显示次要的导航菜单项,通常用于设置、帮助等功能 + * + * @param {Object} props - 组件属性 + * @param {Array} props.items - 导航项数组 + * @param {string} props.items[].title - 导航项标题 + * @param {string} props.items[].url - 导航项链接 + * @param {Icon} props.items[].icon - 导航项图标 + * @param {...any} props - 其他传递给 SidebarGroup 的属性 + */ +export function NavSecondary({ + items, + ...props // 其他属性传递给 SidebarGroup +}: { + items: { + title: string // 导航项标题 + url: string // 导航项URL + icon: Icon // 导航项图标 + }[] +} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) { + return ( + <SidebarGroup {...props}> {/* 传递所有其他属性 */} + {/* 侧边栏组内容 */} + <SidebarGroupContent> + {/* 侧边栏菜单 */} + <SidebarMenu> + {/* 遍历次要导航项 */} + {items.map((item) => ( + <SidebarMenuItem key={item.title}> + {/* 导航菜单按钮,使用 asChild 渲染为链接 */} + <SidebarMenuButton asChild> + <a href={item.url}> {/* 导航链接 */} + <item.icon /> {/* 导航项图标 */} + <span>{item.title}</span> {/* 导航项标题 */} + </a> + </SidebarMenuButton> + </SidebarMenuItem> + ))} + </SidebarMenu> + </SidebarGroupContent> + </SidebarGroup> + ) +} diff --git a/frontend/components/nav-system.tsx b/frontend/components/nav-system.tsx new file mode 100644 index 00000000..da7e6cbf --- /dev/null +++ b/frontend/components/nav-system.tsx @@ -0,0 +1,50 @@ +"use client" + +import { type Icon } from "@tabler/icons-react" +import Link from "next/link" +import { usePathname } from "next/navigation" + +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" + +export function NavSystem({ + items, +}: { + items: { + name: string + url: string + icon: Icon + }[] +}) { + const pathname = usePathname() + const normalize = (p: string) => (p !== "/" && p.endsWith("/") ? p.slice(0, -1) : p) + const current = normalize(pathname) + + return ( + <SidebarGroup className="group-data-[collapsible=icon]:hidden"> + <SidebarGroupLabel>系统设置</SidebarGroupLabel> + <SidebarMenu> + {items.map((item) => { + const navUrl = normalize(item.url) + const isActive = current === navUrl || current.startsWith(navUrl + "/") + + return ( + <SidebarMenuItem key={item.name}> + <SidebarMenuButton asChild isActive={isActive}> + <Link href={item.url}> + <item.icon /> + <span>{item.name}</span> + </Link> + </SidebarMenuButton> + </SidebarMenuItem> + ) + })} + </SidebarMenu> + </SidebarGroup> + ) +} diff --git a/frontend/components/nav-user.tsx b/frontend/components/nav-user.tsx new file mode 100644 index 00000000..d8a8d879 --- /dev/null +++ b/frontend/components/nav-user.tsx @@ -0,0 +1,140 @@ +"use client" // 标记为客户端组件,可以使用浏览器 API 和交互功能 + +import React from "react" +// 导入图标组件 +import { + IconDotsVertical, // 垂直三点图标 + IconKey, // 钥匙图标 + IconLogout, // 登出图标 +} from "@tabler/icons-react" + +// 导入头像相关组件 +import { + Avatar, // 头像容器 + AvatarFallback, // 头像备用显示 + AvatarImage, // 头像图片 +} from '@/components/ui/avatar' +// 导入下拉菜单相关组件 +import { + DropdownMenu, // 下拉菜单容器 + DropdownMenuContent, // 下拉菜单内容 + DropdownMenuItem, // 下拉菜单项 + DropdownMenuLabel, // 下拉菜单标签 + DropdownMenuSeparator, // 下拉菜单分隔线 + DropdownMenuTrigger, // 下拉菜单触发器 +} from '@/components/ui/dropdown-menu' +// 导入侧边栏相关组件 +import { + SidebarMenu, // 侧边栏菜单 + SidebarMenuButton, // 侧边栏菜单按钮 + SidebarMenuItem, // 侧边栏菜单项 + useSidebar, // 侧边栏Hook +} from '@/components/ui/sidebar' +import { useAuth, useLogout } from '@/hooks/use-auth' +import { ChangePasswordDialog } from '@/components/auth/change-password-dialog' + +/** + * 用户导航组件 + * 显示用户信息和用户相关的操作菜单 + * + * @param {Object} props - 组件属性 + * @param {Object} props.user - 用户信息 + * @param {string} props.user.name - 用户名称 + * @param {string} props.user.email - 用户邮箱 + * @param {string} props.user.avatar - 用户头像URL + */ +export function NavUser({ + user, +}: { + user: { + name: string // 用户名称 + email: string // 用户邮箱 + avatar: string // 用户头像URL + } +}) { + const { isMobile } = useSidebar() // 获取移动端状态 + const { data: auth } = useAuth() + const { mutate: logout, isPending: isLoggingOut } = useLogout() + const [showChangePassword, setShowChangePassword] = React.useState(false) + + // 使用真实用户名(如果已登录) + const displayName = auth?.user?.username || user.name + + return ( + <> + <ChangePasswordDialog + open={showChangePassword} + onOpenChange={setShowChangePassword} + /> + <SidebarMenu> + <SidebarMenuItem> + {/* 用户下拉菜单 */} + <DropdownMenu> + {/* 下拉菜单触发器 */} + <DropdownMenuTrigger asChild> + <SidebarMenuButton + size="lg" // 大尺寸 + className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" // 打开时的样式 + > + {/* 用户头像 */} + <Avatar className="h-8 w-8 rounded-lg grayscale"> {/* 8x8尺寸,圆角,灰度 */} + <AvatarImage src={user.avatar} alt={user.name} /> {/* 用户头像图片 */} + <AvatarFallback className="rounded-lg">CN</AvatarFallback> {/* 备用显示 */} + </Avatar> + {/* 用户信息区域 */} + <div className="grid flex-1 text-left text-sm leading-tight"> + <span className="truncate font-medium">{displayName}</span> {/* 用户名称 */} + <span className="text-muted-foreground truncate text-xs"> {/* 用户邮箱 */} + {/* {user.email} */} + </span> + </div> + {/* 三点菜单图标 */} + <IconDotsVertical className="ml-auto size-4" /> {/* 自动左边距,4x4尺寸 */} + </SidebarMenuButton> + </DropdownMenuTrigger> + {/* 下拉菜单内容 */} + <DropdownMenuContent + className="rounded-lg" // 圆角 + side={isMobile ? "bottom" : "right"} // 移动端下方,桌面端右侧 + align="end" // 端对齐 + sideOffset={4} // 偏移4像素 + > + {/* 用户信息标签 */} + <DropdownMenuLabel className="p-0 font-normal"> {/* 无内边距,正常字体 */} + <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> + {/* 用户头像 */} + <Avatar className="h-8 w-8 rounded-lg"> + <AvatarImage src={user.avatar} alt={user.name} /> {/* 用户头像图片 */} + <AvatarFallback className="rounded-lg">CN</AvatarFallback> {/* 备用显示 */} + </Avatar> + {/* 用户信息 */} + <div className="grid flex-1 text-left text-sm leading-tight"> + <span className="truncate font-medium">{displayName}</span> {/* 用户名称 */} + <span className="text-muted-foreground truncate text-xs"> {/* 用户邮箱 */} + {/* {user.email} */} + </span> + </div> + </div> + </DropdownMenuLabel> + {/* 分隔线 */} + <DropdownMenuSeparator /> + {/* 修改密码 */} + <DropdownMenuItem onClick={() => setShowChangePassword(true)}> + <IconKey /> + 修改密码 + </DropdownMenuItem> + {/* 登出选项 */} + <DropdownMenuItem + onClick={() => logout()} + disabled={isLoggingOut} + > + <IconLogout /> + {isLoggingOut ? '登出中...' : '退出登录'} + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + </SidebarMenuItem> + </SidebarMenu> + </> + ) +} diff --git a/frontend/components/notifications/index.ts b/frontend/components/notifications/index.ts new file mode 100644 index 00000000..17ca12c1 --- /dev/null +++ b/frontend/components/notifications/index.ts @@ -0,0 +1,5 @@ +/** + * 通知中心模块导出 + */ + +export { NotificationDrawer } from './notification-drawer' diff --git a/frontend/components/notifications/notification-drawer.tsx b/frontend/components/notifications/notification-drawer.tsx new file mode 100644 index 00000000..5cd62e5a --- /dev/null +++ b/frontend/components/notifications/notification-drawer.tsx @@ -0,0 +1,391 @@ +"use client" + +import * as React from "react" +import { Bell, AlertTriangle, Activity, Info, Server, BellOff, Wifi, WifiOff, CheckCheck, Loader2 } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Skeleton } from "@/components/ui/skeleton" +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet" +import { cn } from "@/lib/utils" +import { transformBackendNotification, useNotificationSSE } from "@/hooks/use-notification-sse" +import { useMarkAllAsRead, useNotifications } from "@/hooks/use-notifications" +import type { Notification, NotificationType, NotificationSeverity } from "@/types/notification.types" + +/** + * 通知抽屉组件 + * 从右侧滑出的侧边面板,显示详细的通知信息 + */ +// 筛选标签配置 +const filterTabs: { value: NotificationType | 'all'; label: string; icon?: React.ReactNode }[] = [ + { value: 'all', label: '全部' }, + { value: 'scan', label: '扫描', icon: <Activity className="h-3 w-3" /> }, + { value: 'vulnerability', label: '漏洞', icon: <AlertTriangle className="h-3 w-3" /> }, + { value: 'asset', label: '资产', icon: <Server className="h-3 w-3" /> }, + { value: 'system', label: '系统', icon: <Info className="h-3 w-3" /> }, +] + +// 分类标题映射 +const categoryTitleMap: Record<NotificationType, string> = { + scan: '扫描任务', + vulnerability: '漏洞发现', + asset: '资产发现', + system: '系统消息', +} + +/** 连接状态指示器 */ +function ConnectionStatus({ isConnected }: { isConnected: boolean }) { + return ( + <div className="flex items-center gap-1.5"> + <span className="relative flex h-2 w-2"> + {isConnected && ( + <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" /> + )} + <span className={cn( + "relative inline-flex h-2 w-2 rounded-full", + isConnected ? "bg-emerald-500" : "bg-gray-400" + )} /> + </span> + <span className="text-xs text-muted-foreground"> + {isConnected ? "实时" : "离线"} + </span> + </div> + ) +} + +/** 通知骨架屏 */ +function NotificationSkeleton() { + return ( + <div className="space-y-2"> + {[1, 2, 3].map((i) => ( + <div key={i} className="rounded-md border p-3"> + <div className="flex items-start gap-2.5"> + <Skeleton className="h-5 w-5 rounded-full" /> + <div className="flex-1 space-y-2"> + <Skeleton className="h-4 w-3/4" /> + <Skeleton className="h-3 w-full" /> + <Skeleton className="h-3 w-1/2" /> + </div> + </div> + </div> + ))} + </div> + ) +} + +/** 时间分组辅助函数 */ +function getTimeGroup(dateStr?: string): 'today' | 'yesterday' | 'earlier' { + if (!dateStr) return 'earlier' + const date = new Date(dateStr) + const now = new Date() + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000) + + if (date >= today) return 'today' + if (date >= yesterday) return 'yesterday' + return 'earlier' +} + +const timeGroupLabels = { + today: '今天', + yesterday: '昨天', + earlier: '更早', +} + +export function NotificationDrawer() { + const [open, setOpen] = React.useState(false) + const [activeFilter, setActiveFilter] = React.useState<NotificationType | 'all'>('all') + const queryParams = React.useMemo(() => ({ pageSize: 100 }), []) + const { data: notificationResponse, isLoading: isHistoryLoading } = useNotifications(queryParams) + const { mutate: markAllAsRead, isPending: isMarkingAll } = useMarkAllAsRead() + + // SSE 实时通知 + const { notifications: sseNotifications, isConnected } = useNotificationSSE() + + const [historyNotifications, setHistoryNotifications] = React.useState<Notification[]>([]) + + React.useEffect(() => { + if (!notificationResponse?.results) return + const backendNotifications = notificationResponse.results ?? [] + setHistoryNotifications(backendNotifications.map(transformBackendNotification)) + }, [notificationResponse]) + + // 合并 SSE 和 API 通知,SSE 优先 + const allNotifications = React.useMemo(() => { + const seen = new Set<number>() + const merged: Notification[] = [] + + for (const notification of sseNotifications) { + if (!seen.has(notification.id)) { + merged.push(notification) + seen.add(notification.id) + } + } + + for (const notification of historyNotifications) { + if (!seen.has(notification.id)) { + merged.push(notification) + seen.add(notification.id) + } + } + + return merged.sort((a, b) => { + const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0 + const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0 + return bTime - aTime + }) + }, [historyNotifications, sseNotifications]) + + // 未读通知数量 + const unreadCount = allNotifications.filter(n => n.unread).length + + const unreadByType = React.useMemo<Record<NotificationType | 'all', number>>(() => { + const counts: Record<NotificationType | 'all', number> = { + all: 0, + scan: 0, + vulnerability: 0, + asset: 0, + system: 0, + } + + allNotifications.forEach(notification => { + if (!notification.unread) return + counts.all += 1 + if (counts[notification.type] !== undefined) { + counts[notification.type] += 1 + } + }) + + return counts + }, [allNotifications]) + + // 筛选后的通知列表 + const filteredNotifications = React.useMemo(() => { + if (activeFilter === 'all') return allNotifications + return allNotifications.filter(n => n.type === activeFilter) + }, [allNotifications, activeFilter]) + + // 获取通知图标 + const severityIconClassMap: Record<NotificationSeverity, string> = { + critical: "text-purple-500", + high: "text-red-500", + medium: "text-amber-500", + low: "text-gray-500", + } + + const getNotificationIcon = (type: NotificationType, severity?: NotificationSeverity) => { + const severityClass = severity ? severityIconClassMap[severity] : "text-gray-500" + + if (type === "vulnerability") { + return <AlertTriangle className={cn("h-5 w-5", severityClass)} /> + } + if (type === "scan") { + return <Activity className={cn("h-5 w-5", severityClass)} /> + } + if (type === "asset") { + return <Server className={cn("h-5 w-5", severityClass)} /> + } + return <Info className={cn("h-5 w-5", severityClass)} /> + } + + const severityCardClassMap: Record<NotificationSeverity, string> = { + critical: "border-purple-300 bg-purple-50 hover:bg-purple-100 dark:border-purple-500/60 dark:bg-purple-500/10 dark:hover:bg-purple-500/20", + high: "border-red-300 bg-red-50 hover:bg-red-100 dark:border-red-500/60 dark:bg-red-500/10 dark:hover:bg-red-500/20", + medium: "border-amber-300 bg-amber-50 hover:bg-amber-100 dark:border-amber-500/60 dark:bg-amber-500/10 dark:hover:bg-amber-500/20", + low: "border-gray-300 bg-gray-50 hover:bg-gray-100 dark:border-gray-500/60 dark:bg-gray-500/10 dark:hover:bg-gray-500/20", + } + + const getNotificationCardClasses = (severity?: NotificationSeverity) => { + if (!severity) { + return "border-border bg-card hover:bg-accent/50" + } + return cn("border-border", severityCardClassMap[severity] ?? "") + } + + const handleMarkAll = React.useCallback(() => { + if (allNotifications.length === 0 || isMarkingAll) return + markAllAsRead(undefined, { + onSuccess: () => { + setHistoryNotifications(prev => prev.map(notification => ({ ...notification, unread: false }))) + }, + }) + }, [allNotifications.length, isMarkingAll, markAllAsRead]) + + // 按时间分组通知 + const groupedNotifications = React.useMemo(() => { + const groups: Record<'today' | 'yesterday' | 'earlier', Notification[]> = { + today: [], + yesterday: [], + earlier: [], + } + + filteredNotifications.forEach(notification => { + const group = getTimeGroup(notification.createdAt) + groups[group].push(notification) + }) + + return groups + }, [filteredNotifications]) + + // 渲染单个通知卡片 + const renderNotificationCard = (notification: Notification) => ( + <div + key={notification.id} + className={cn( + "group relative rounded-lg border p-3 transition-all duration-200 overflow-hidden", + "hover:shadow-sm hover:scale-[1.01]", + getNotificationCardClasses(notification.severity) + )} + > + {notification.unread && ( + <span className="absolute right-2 bottom-2 h-2 w-2 rounded-full bg-primary" aria-hidden /> + )} + <div className="flex items-start gap-3"> + <div className={cn( + "mt-0.5 p-1.5 rounded-full shrink-0", + notification.severity === 'critical' && "bg-purple-100 dark:bg-purple-500/20", + notification.severity === 'high' && "bg-red-100 dark:bg-red-500/20", + notification.severity === 'medium' && "bg-amber-100 dark:bg-amber-500/20", + (!notification.severity || notification.severity === 'low') && "bg-muted" + )}> + {getNotificationIcon(notification.type, notification.severity)} + </div> + <div className="flex-1 min-w-0 overflow-hidden"> + {/* 分类标题 + 时间 */} + <div className="flex items-center justify-between gap-2 mb-1"> + <span className="text-xs font-medium text-muted-foreground"> + {categoryTitleMap[notification.type]} + </span> + <span className="text-xs text-muted-foreground tabular-nums shrink-0"> + {notification.time} + </span> + </div> + {/* 通知标题 */} + <p className="text-sm font-semibold leading-snug truncate"> + {notification.title} + </p> + {/* 通知描述 - 支持换行显示 */} + <p className="text-xs text-muted-foreground mt-1 whitespace-pre-line break-all line-clamp-4"> + {notification.description} + </p> + </div> + </div> + </div> + ) + + // 渲染通知列表(带时间分组) + const renderNotificationList = () => { + const hasAny = filteredNotifications.length > 0 + + if (!hasAny) { + return ( + <div className="flex flex-col items-center justify-center h-40 text-muted-foreground"> + <BellOff className="h-10 w-10 mb-2 opacity-50" /> + <p className="text-sm">暂无通知</p> + </div> + ) + } + + return ( + <div className="space-y-4"> + {(['today', 'yesterday', 'earlier'] as const).map(group => { + const items = groupedNotifications[group] + if (items.length === 0) return null + + return ( + <div key={group}> + <h3 className="sticky top-0 z-10 text-xs font-medium text-muted-foreground mb-2 px-1 py-1 backdrop-blur bg-background/90"> + {timeGroupLabels[group]} + </h3> + <div className="space-y-2"> + {items.map(renderNotificationCard)} + </div> + </div> + ) + })} + </div> + ) + } + + return ( + <Sheet open={open} onOpenChange={setOpen}> + <SheetTrigger asChild> + <Button variant="ghost" size="icon" className="relative group"> + <Bell className="h-5 w-5" /> + {unreadCount > 0 && ( + <> + <span className="absolute -top-0.5 -right-0.5 h-4 w-4 rounded-full bg-destructive animate-ping opacity-75" /> + <Badge + variant="destructive" + className="absolute -top-0.5 -right-0.5 h-4 min-w-4 rounded-full p-0 text-[10px] flex items-center justify-center" + > + {unreadCount > 99 ? '99+' : unreadCount} + </Badge> + </> + )} + <span className="sr-only">通知</span> + </Button> + </SheetTrigger> + <SheetContent className="w-full sm:max-w-[440px] p-0 flex flex-col gap-0"> + <SheetHeader className="border-b px-4 py-1.5"> + <div className="flex items-center justify-between gap-2"> + <SheetTitle className="text-sm font-semibold">通知</SheetTitle> + <div className="flex items-center gap-2"> + <button + onClick={handleMarkAll} + disabled={isMarkingAll || allNotifications.length === 0} + className="text-xs text-primary hover:text-primary/80 hover:underline underline-offset-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:no-underline transition-colors" + title="全部标记为已读" + > + {isMarkingAll ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "全部已读"} + </button> + </div> + </div> + </SheetHeader> + + {/* 分类筛选标签 */} + <div className="flex gap-1 px-3 py-1.5 border-b overflow-x-auto"> + {filterTabs.map((tab) => ( + <button + key={tab.value} + onClick={() => setActiveFilter(tab.value)} + className={cn( + "inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium transition-all whitespace-nowrap", + activeFilter === tab.value + ? "bg-primary text-primary-foreground shadow-sm" + : "bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground" + )} + > + {tab.icon} + {tab.label} + {unreadByType[tab.value] > 0 && ( + <span + className={cn( + "ml-1 h-1.5 w-1.5 rounded-full", + activeFilter === tab.value ? "bg-primary-foreground" : "bg-primary" + )} + /> + )} + </button> + ))} + </div> + + <ScrollArea className="flex-1"> + <div className="p-3"> + {isHistoryLoading && allNotifications.length === 0 ? ( + <NotificationSkeleton /> + ) : ( + renderNotificationList() + )} + </div> + </ScrollArea> + </SheetContent> + </Sheet> + ) +} diff --git a/frontend/components/organization/add-organization-dialog.tsx b/frontend/components/organization/add-organization-dialog.tsx new file mode 100644 index 00000000..8fce8fcd --- /dev/null +++ b/frontend/components/organization/add-organization-dialog.tsx @@ -0,0 +1,384 @@ +"use client" + +import React, { useState, useRef, useMemo } from "react" +import { Plus, Building2, Target } from "lucide-react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" + +// 导入 UI 组件 +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { LoadingSpinner } from "@/components/loading-spinner" +import { TargetValidator } from "@/lib/target-validator" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" + +// 导入 React Query Hook +import { useCreateOrganization } from "@/hooks/use-organizations" +import { useBatchCreateTargets } from "@/hooks/use-targets" + +// 导入类型定义 +import type { Organization } from "@/types/organization.types" + +// 表单验证 Schema +const formSchema = z.object({ + name: z.string() + .min(2, { message: "组织名称至少需要 2 个字符" }) + .max(50, { message: "组织名称不能超过 50 个字符" }), + description: z.string().max(200, { message: "描述不能超过 200 个字符" }).optional(), + targets: z.string().optional(), +}) + +type FormValues = z.infer<typeof formSchema> + +// 组件属性类型定义 +interface AddOrganizationDialogProps { + onAdd?: (organization: Organization) => void // 添加成功回调函数(可选) + open?: boolean // 外部控制对话框开关状态 + onOpenChange?: (open: boolean) => void // 外部控制对话框开关回调 +} + +/** + * 添加组织对话框组件(使用 React Query) + * + * 功能特性: + * 1. 自动管理提交状态 + * 2. 自动错误处理和成功提示 + * 3. 自动刷新相关数据 + * 4. 更好的用户体验 + */ +export function AddOrganizationDialog({ + onAdd, + open: externalOpen, + onOpenChange: externalOnOpenChange +}: AddOrganizationDialogProps) { + // 对话框开关状态 - 支持外部控制 + const [internalOpen, setInternalOpen] = useState(false) + const open = externalOpen !== undefined ? externalOpen : internalOpen + const setOpen = externalOnOpenChange || setInternalOpen + + // 行号列和输入框的 ref(用于同步滚动) + const lineNumbersRef = useRef<HTMLDivElement | null>(null) + const textareaRef = useRef<HTMLTextAreaElement | null>(null) + + // 使用 React Query 的创建组织和目标 mutation + const createOrganization = useCreateOrganization() + const batchCreateTargets = useBatchCreateTargets() + + // 初始化表单 + const form = useForm<FormValues>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + description: "", + targets: "", + }, + }) + + // 监听表单值变化 + const targetsText = form.watch("targets") || "" + + // 实时验证目标 + const targetValidation = useMemo(() => { + const lines = targetsText + .split("\n") + .map((s) => s.trim()) + .filter((s) => s.length > 0) + + if (lines.length === 0) { + return { + count: 0, + invalid: [] + } + } + + const results = TargetValidator.validateTargetBatch(lines) + const invalid = results + .filter((r) => !r.isValid) + .map((r) => ({ index: r.index, originalTarget: r.originalTarget, error: r.error || "目标格式无效", type: r.type })) + + return { + count: lines.length, + invalid + } + }, [targetsText]) + + // 同步输入框和行号列的滚动 + const handleTextareaScroll = (e: React.UIEvent<HTMLTextAreaElement>) => { + if (lineNumbersRef.current) { + lineNumbersRef.current.scrollTop = e.currentTarget.scrollTop + } + } + + // 处理表单提交 + const onSubmit = (values: FormValues) => { + // 检查是否有无效目标 + if (targetValidation.invalid.length > 0) { + return + } + + // 先创建组织 + createOrganization.mutate( + { + name: values.name.trim(), + description: values.description?.trim() || "", + }, + { + onSuccess: (newOrganization) => { + // 如果有目标,则批量创建目标 + if (values.targets && values.targets.trim()) { + const targetList = values.targets + .split("\n") + .map(line => line.trim()) + .filter(line => line.length > 0) + .map(name => ({ + name, + })) + + if (targetList.length > 0) { + // 批量创建目标并关联到新组织(后端会自动检测目标类型) + batchCreateTargets.mutate( + { + targets: targetList, + organizationId: newOrganization.id, + }, + { + onSuccess: () => { + // 重置表单 + form.reset() + + // 关闭对话框 + setOpen(false) + + // 调用外部回调 + if (onAdd) { + onAdd(newOrganization) + } + } + } + ) + } else { + // 没有目标,直接完成 + form.reset() + setOpen(false) + if (onAdd) { + onAdd(newOrganization) + } + } + } else { + // 没有目标,直接完成 + form.reset() + setOpen(false) + if (onAdd) { + onAdd(newOrganization) + } + } + } + } + ) + } + + // 处理对话框关闭 + const handleOpenChange = (newOpen: boolean) => { + if (!createOrganization.isPending && !batchCreateTargets.isPending) { + setOpen(newOpen) + if (!newOpen) { + // 关闭时重置表单 + form.reset() + } + } + } + + // 表单验证 + const isFormValid = form.formState.isValid && targetValidation.invalid.length === 0 + const isSubmitting = createOrganization.isPending || batchCreateTargets.isPending + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + {/* 触发按钮 - 仅在非外部控制时显示 */} + {externalOpen === undefined && ( + <DialogTrigger asChild> + <Button size="sm"> + <Plus /> + 添加组织 + </Button> + </DialogTrigger> + )} + + {/* 对话框内容 */} + <DialogContent className="sm:max-w-[650px] max-h-[90vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle className="flex items-center space-x-2"> + <Building2 /> + <span>添加新组织</span> + </DialogTitle> + <DialogDescription> + 填写组织信息以添加到系统中。可以同时添加目标。标有 * 的字段为必填项。 + </DialogDescription> + </DialogHeader> + + {/* 表单 */} + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + <div className="grid gap-4 py-4"> + {/* 组织名称输入框 */} + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel> + 组织名称 <span className="text-destructive">*</span> + </FormLabel> + <FormControl> + <Input + placeholder="请输入组织名称" + disabled={isSubmitting} + maxLength={50} + {...field} + /> + </FormControl> + <FormDescription> + {field.value.length}/50 字符 + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 组织描述输入框 */} + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>组织描述</FormLabel> + <FormControl> + <Textarea + placeholder="请输入组织描述(可选)" + disabled={isSubmitting} + rows={3} + maxLength={200} + {...field} + /> + </FormControl> + <FormDescription> + {(field.value || "").length}/200 字符 + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 目标输入框 - 支持多行,带行号 */} + <FormField + control={form.control} + name="targets" + render={({ field }) => ( + <FormItem> + <FormLabel className="flex items-center space-x-2"> + <Target className="h-4 w-4" /> + <span>添加目标(可选)</span> + </FormLabel> + <FormControl> + <div className="relative border rounded-md overflow-hidden bg-background"> + <div className="flex h-[324px]"> + {/* 行号列 - 固定显示15行 */} + <div className="flex-shrink-0 w-12 bg-muted/30 border-r select-none overflow-hidden"> + <div + ref={lineNumbersRef} + className="py-3 px-2 text-right font-mono text-xs text-muted-foreground leading-[1.4] h-full overflow-y-auto scrollbar-hide" + > + {Array.from({ length: Math.max(field.value?.split('\n').length || 1, 15) }, (_, i) => ( + <div key={i + 1} className="h-[20px]"> + {i + 1} + </div> + ))} + </div> + </div> + {/* 输入框 - 固定高度显示15行 */} + <Textarea + {...field} + ref={(e) => { + field.ref(e) + textareaRef.current = e + }} + onScroll={handleTextareaScroll} + placeholder={`请输入目标,每行一个\n支持域名、IP、CIDR\n例如:\nexample.com\n192.168.1.1\n10.0.0.0/8`} + disabled={isSubmitting} + className="font-mono h-full overflow-y-auto resize-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 leading-[1.4] text-sm py-3" + style={{ lineHeight: '20px' }} + /> + </div> + </div> + </FormControl> + <FormDescription> + {targetValidation.count} 个目标 + {targetValidation.invalid.length > 0 && ( + <span className="text-destructive ml-2"> + | {targetValidation.invalid.length} 个无效 + </span> + )} + </FormDescription> + {targetValidation.invalid.length > 0 && ( + <div className="text-xs text-destructive"> + 例如 第 {targetValidation.invalid[0].index + 1} 行: "{targetValidation.invalid[0].originalTarget}" - {targetValidation.invalid[0].error} + </div> + )} + <FormMessage /> + </FormItem> + )} + /> + </div> + + {/* 对话框底部按钮 */} + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={isSubmitting} + > + 取消 + </Button> + <Button + type="submit" + disabled={isSubmitting || !isFormValid} + > + {isSubmitting ? ( + <> + <LoadingSpinner/> + {createOrganization.isPending ? "创建组织中..." : "批量创建目标中..."} + </> + ) : ( + <> + <Plus /> + 创建组织 + </> + )} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/organization/edit-organization-dialog.tsx b/frontend/components/organization/edit-organization-dialog.tsx new file mode 100644 index 00000000..7888a00e --- /dev/null +++ b/frontend/components/organization/edit-organization-dialog.tsx @@ -0,0 +1,254 @@ +"use client" + +import React, { useEffect } from "react" +import { Edit, Building2 } from "lucide-react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" + +// 导入 UI 组件 +import { Button } from "@/components/ui/button" +import { LoadingSpinner } from "@/components/loading-spinner" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" + +// 导入 React Query Hook +import { useUpdateOrganization } from "@/hooks/use-organizations" + +// 导入类型定义 +import type { Organization } from "@/types/organization.types" + +// 表单验证 Schema +const formSchema = z.object({ + name: z.string() + .min(2, { message: "组织名称至少需要 2 个字符" }) + .max(50, { message: "组织名称不能超过 50 个字符" }), + description: z.string().max(200, { message: "描述不能超过 200 个字符" }).optional(), +}) + +type FormValues = z.infer<typeof formSchema> + +// 组件属性类型定义 +interface EditOrganizationDialogProps { + organization: Organization // 要编辑的组织数据 + open: boolean // 对话框开关状态 + onOpenChange: (open: boolean) => void // 对话框状态变化回调 + onEdit: (organization: Organization) => void // 编辑成功回调函数 +} + +/** + * 编辑组织对话框组件 + * 提供编辑现有组织的表单界面 + * + * 功能特性: + * 1. 预填充现有数据 + * 2. 表单验证 + * 3. 错误处理 + * 4. 加载状态 + * 5. 变更检测 + */ +export function EditOrganizationDialog({ + organization, + open, + onOpenChange, + onEdit +}: EditOrganizationDialogProps) { + // 使用 React Query 的更新组织 mutation + const updateOrganization = useUpdateOrganization() + + // 初始化表单 + const form = useForm<FormValues>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: organization?.name || "", + description: organization?.description || "", + }, + }) + + // 当组织数据变化时重置表单 + useEffect(() => { + if (organization) { + form.reset({ + name: organization.name || "", + description: organization.description || "", + }) + } + }, [organization, form]) + + // 检查表单是否有变更 + const hasChanges = form.formState.isDirty + + // 处理表单提交 + const onSubmit = (values: FormValues) => { + updateOrganization.mutate( + { + id: Number(organization.id), + data: { + name: values.name.trim(), + description: values.description?.trim() || "", + } + }, + { + onSuccess: (updatedOrganization) => { + // 调用成功回调 + onEdit(updatedOrganization) + + // 关闭对话框 + onOpenChange(false) + } + } + ) + } + + // 处理对话框关闭 + const handleOpenChange = (newOpen: boolean) => { + if (!updateOrganization.isPending) { + onOpenChange(newOpen) + } + } + + // 重置表单到原始状态 + const handleReset = () => { + form.reset({ + name: organization.name || "", + description: organization.description || "", + }) + } + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + {/* 对话框内容 */} + <DialogContent className="sm:max-w-[425px]"> + <DialogHeader> + <DialogTitle className="flex items-center space-x-2"> + <Building2 /> + <span>编辑组织</span> + </DialogTitle> + <DialogDescription> + 修改组织的基本信息。标有 * 的字段为必填项。 + </DialogDescription> + </DialogHeader> + + {/* 表单 */} + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + <div className="grid gap-4 py-4"> + {/* 组织名称输入框 */} + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel> + 组织名称 <span className="text-destructive">*</span> + </FormLabel> + <FormControl> + <Input + placeholder="请输入组织名称" + disabled={updateOrganization.isPending} + maxLength={50} + {...field} + /> + </FormControl> + <FormDescription> + {field.value.length}/50 字符 + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 组织描述输入框 */} + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>组织描述</FormLabel> + <FormControl> + <Textarea + placeholder="请输入组织描述(可选)" + disabled={updateOrganization.isPending} + rows={3} + maxLength={200} + {...field} + /> + </FormControl> + <FormDescription> + {(field.value || "").length}/200 字符 + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 变更提示 */} + {hasChanges && ( + <div className="text-xs text-amber-600 bg-amber-50 dark:bg-amber-950/20 p-2 rounded"> + 检测到变更,点击更新保存修改 + </div> + )} + </div> + + {/* 对话框底部按钮 */} + <DialogFooter className="gap-2"> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={updateOrganization.isPending} + > + 取消 + </Button> + + {hasChanges && ( + <Button + type="button" + variant="ghost" + onClick={handleReset} + disabled={updateOrganization.isPending} + > + 重置 + </Button> + )} + + <Button + type="submit" + disabled={updateOrganization.isPending || !form.formState.isValid || !hasChanges} + > + {updateOrganization.isPending ? ( + <> + <LoadingSpinner/> + 更新中... + </> + ) : ( + <> + <Edit/> + 更新组织 + </> + )} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/organization/index.ts b/frontend/components/organization/index.ts new file mode 100644 index 00000000..9680db02 --- /dev/null +++ b/frontend/components/organization/index.ts @@ -0,0 +1,10 @@ +/** + * Organization Components - 统一导出 + */ +export { OrganizationList } from './organization-list' +export { OrganizationDataTable } from './organization-data-table' +export { createOrganizationColumns } from './organization-columns' +export { OrganizationDetailView } from './organization-detail-view' +export { AddOrganizationDialog } from './add-organization-dialog' +export { EditOrganizationDialog } from './edit-organization-dialog' + diff --git a/frontend/components/organization/organization-columns.tsx b/frontend/components/organization/organization-columns.tsx new file mode 100644 index 00000000..0d28bfb8 --- /dev/null +++ b/frontend/components/organization/organization-columns.tsx @@ -0,0 +1,299 @@ +"use client" // 标记为客户端组件,可以使用浏览器 API 和交互功能 + +// 导入表格相关类型和组件 +import { ColumnDef } from "@tanstack/react-table" +import { Checkbox } from "@/components/ui/checkbox" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +// 导入图标组件 +import { MoreHorizontal, Play, Calendar, Edit, Trash2, ChevronsUpDown, ChevronUp, ChevronDown, Eye } from "lucide-react" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +// 导入 Next.js Link 组件 +import Link from "next/link" + +// 导入类型定义 +import type { Organization } from "@/types/organization.types" + +// 列创建函数的参数类型 +interface CreateColumnsProps { + formatDate: (dateString: string) => string // 日期格式化函数 + navigate: (path: string) => void // 导航函数 + handleEdit: (org: Organization) => void // 编辑处理函数 + handleDelete: (org: Organization) => void // 删除处理函数 + handleInitiateScan: (org: Organization) => void // 发起扫描处理函数 + handleScheduleScan: (org: Organization) => void // 计划扫描处理函数 +} + +/** + * 组织行操作组件 + * 提供计划扫描、编辑、删除等操作 + */ +function OrganizationRowActions({ + onScheduleScan, + onEdit, + onDelete +}: { + onScheduleScan: () => void + onEdit: () => void + onDelete: () => void +}) { + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" + > + <MoreHorizontal /> + <span className="sr-only">打开菜单</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem onClick={onScheduleScan}> + <Calendar /> + Schedule Scan + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem onClick={onEdit}> + <Edit /> + Edit Organization + </DropdownMenuItem> + <DropdownMenuItem + onClick={onDelete} + className="text-destructive focus:text-destructive" + > + <Trash2 /> + Delete + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) +} + +/** + * 数据表格列头组件 + * 支持排序功能的列头,参考 shadcn/ui 示例设计 + */ +function DataTableColumnHeader({ + column, + title +}: { + column: { getCanSort: () => boolean; getIsSorted: () => false | "asc" | "desc"; toggleSorting: (desc?: boolean) => void } + title: string +}) { + if (!column.getCanSort()) { + return <div className="font-medium">{title}</div> + } + + const isSorted = column.getIsSorted() + + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted" + > + {title} + {isSorted === "asc" ? ( + <ChevronUp /> + ) : isSorted === "desc" ? ( + <ChevronDown /> + ) : ( + <ChevronsUpDown /> + )} + </Button> + ) +} + +/** + * 创建组织表格列定义 + * + * @param formatDate - 日期格式化函数 + * @param navigate - 页面导航函数 + * @param handleEdit - 编辑处理函数 + * @param handleDelete - 删除处理函数 + * @returns 表格列定义数组 + */ +export const createOrganizationColumns = ({ + formatDate, + navigate, + handleEdit, + handleDelete, + handleInitiateScan, + handleScheduleScan, +}: CreateColumnsProps): ColumnDef<Organization>[] => [ + // 选择列 - 支持单选和全选 + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, // 禁用排序 + enableHiding: false, // 禁用隐藏 + }, + + // 组织名称列 + { + accessorKey: "name", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Organization" /> + ), + cell: ({ row }) => { + const organization = row.original + return ( + <Link + href={`/organization/${organization.id}`} + className="font-medium hover:text-primary hover:underline transition-colors block" + > + {row.getValue("name")} + </Link> + ) + }, + }, + + // 组织描述列 + { + accessorKey: "description", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Description" /> + ), + cell: ({ row }) => { + const description = row.getValue("description") as string + + if (!description) { + return <span className="text-muted-foreground">-</span> + } + + return ( + <div className="max-w-md"> + <span className="block truncate text-muted-foreground"> + {description} + </span> + </div> + ) + }, + }, + + // Total Targets 列 + { + accessorKey: "targetCount", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Total Targets" /> + ), + cell: ({ row }) => { + const targetCount = row.original.targetCount ?? 0 + return ( + <div className="text-sm"> + <Badge variant="secondary" className="text-xs"> + {targetCount} + </Badge> + </div> + ) + }, + }, + + // Added 列(创建时间) + { + accessorKey: "createdAt", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Added" /> + ), + cell: ({ row }) => { + const createdAt = row.getValue("createdAt") as string | undefined + // 检查是否为零值时间 + const isZeroTime = createdAt && ( + createdAt === "0001-01-01T00:00:00Z" || + createdAt.startsWith("0001-01-01") + ) + + return ( + <div className="text-sm text-muted-foreground"> + {createdAt && !isZeroTime ? formatDate(createdAt) : ( + <span className="text-muted-foreground">-</span> + )} + </div> + ) + }, + }, + + // 操作列 + { + id: "actions", + cell: ({ row }) => ( + <div className="flex items-center gap-1"> + {/* Target Summary 按钮 */} + <TooltipProvider delayDuration={300}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className="h-8 w-8" + onClick={() => navigate(`/organization/${row.original.id}`)} + > + <Eye className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">Target Summary</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + + {/* Initiate Scan 按钮 */} + <TooltipProvider delayDuration={300}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className="h-8 w-8" + onClick={() => handleInitiateScan(row.original)} + > + <Play className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">Initiate Scan</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + + {/* 更多操作菜单 */} + <OrganizationRowActions + onScheduleScan={() => handleScheduleScan(row.original)} + onEdit={() => handleEdit(row.original)} + onDelete={() => handleDelete(row.original)} + /> + </div> + ), + enableSorting: false, // 禁用排序 + enableHiding: false, // 禁用隐藏 + }, +] diff --git a/frontend/components/organization/organization-data-table.tsx b/frontend/components/organization/organization-data-table.tsx new file mode 100644 index 00000000..08e5eb89 --- /dev/null +++ b/frontend/components/organization/organization-data-table.tsx @@ -0,0 +1,391 @@ +"use client" // 标记为客户端组件,可以使用浏览器 API 和交互功能 + +// 导入 React 库和 Hooks +import * as React from "react" +// 导入表格相关组件和类型 +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +// 导入图标组件 +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconPlus, + IconTrash, + IconSearch, + IconLoader2, +} from "@tabler/icons-react" + +// 导入 UI 组件 +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +// 导入类型定义 +import type { Organization, OrganizationDataTableProps } from "@/types/organization.types" +export function OrganizationDataTable({ + data, + columns, + onAddNew, + onBulkDelete, + onSelectionChange, + searchPlaceholder = "搜索组织名称...", + searchColumn = "name", + searchValue, + onSearch, + isSearching, + pagination: externalPagination, + setPagination: setExternalPagination, + paginationInfo, + onPaginationChange, +}: OrganizationDataTableProps) { + // 表格状态管理 + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [sorting, setSorting] = React.useState<SortingState>([ + { + id: "createdAt", + desc: true, // 降序排列,最新的在前 + }, + ]) + + // 使用外部分页状态或内部默认状态 + const [internalPagination, setInternalPagination] = React.useState({ + pageIndex: 0, + pageSize: 10, + }) + + const pagination = externalPagination || internalPagination + const setPagination = setExternalPagination || setInternalPagination + + // 本地搜索输入状态(只在回车或点击按钮时触发搜索) + const [localSearchValue, setLocalSearchValue] = React.useState(searchValue ?? "") + + React.useEffect(() => { + setLocalSearchValue(searchValue ?? "") + }, [searchValue]) + + const handleSearchSubmit = () => { + if (onSearch) { + onSearch(localSearchValue) + } else { + table.getColumn(searchColumn)?.setFilterValue(localSearchValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearchSubmit() + } + } + + // 创建表格实例 + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination, + }, + getRowId: (row) => row.id.toString(), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const newPagination = typeof updater === 'function' ? updater(pagination) : updater + setPagination(newPagination) + // 如果有外部分页变化回调,则调用它 + if (onPaginationChange) { + onPaginationChange(newPagination) + } + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + // 如果有外部分页信息,则禁用内部分页,使用手动分页 + ...(paginationInfo ? { + manualPagination: true, + pageCount: paginationInfo.totalPages, + } : { + getPaginationRowModel: getPaginationRowModel(), + }), + }) + + // 监听选中行变化,通知父组件 + React.useEffect(() => { + if (onSelectionChange) { + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + onSelectionChange(selectedRows) + } + }, [rowSelection]) + + return ( + <div className="w-full space-y-4"> + {/* 工具栏 */} + <div className="flex items-center justify-between"> + {/* 搜索框 */} + <div className="flex items-center space-x-2"> + <Input + placeholder={searchPlaceholder} + value={localSearchValue} + onChange={(e) => setLocalSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="h-8 max-w-sm" + /> + <Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}> + {isSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + + {/* 右侧操作按钮 */} + <div className="flex items-center space-x-2"> + {/* 列显示控制 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + return ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {column.id === "name" && "Organization"} + {column.id === "description" && "Description"} + {column.id === "targetCount" && "Total Targets"} + {column.id === "createdAt" && "Added"} + {!["name", "description", "targetCount", "createdAt"].includes(column.id) && column.id} + </DropdownMenuCheckboxItem> + ) + })} + </DropdownMenuContent> + </DropdownMenu> + + {/* 批量删除按钮 */} + {onBulkDelete && ( + <Button + onClick={onBulkDelete} + size="sm" + variant="outline" + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + className={ + table.getFilteredSelectedRowModel().rows.length === 0 + ? "text-muted-foreground" + : "text-destructive hover:text-destructive hover:bg-destructive/10" + } + > + <IconTrash/> + Delete + </Button> + )} + + {/* 添加新组织按钮 */} + {onAddNew && ( + <Button onClick={onAddNew} size="sm"> + <IconPlus/> + Add Organization + </Button> + )} + </div> + </div> + + {/* 表格容器 */} + <div className="rounded-md border"> + <Table> + {/* 表头 */} + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id} colSpan={header.colSpan}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + + {/* 表体 */} + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + No results + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {/* 分页控制 */} + <div className="flex items-center justify-between px-2"> + {/* 选中行信息 */} + <div className="flex-1 text-sm text-muted-foreground"> + {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected + </div> + + {/* 分页控制器 */} + <div className="flex items-center space-x-6 lg:space-x-8"> + {/* 每页显示数量选择 */} + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + Rows per page + </Label> + <Select + value={`${table.getState().pagination.pageSize}`} + onValueChange={(value) => { + table.setPageSize(Number(value)) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={table.getState().pagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 页码信息 */} + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + </div> + + {/* 分页按钮 */} + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to first page</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to previous page</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to next page</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to last page</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + </div> + ) +} diff --git a/frontend/components/organization/organization-detail-view.tsx b/frontend/components/organization/organization-detail-view.tsx new file mode 100644 index 00000000..117e38ed --- /dev/null +++ b/frontend/components/organization/organization-detail-view.tsx @@ -0,0 +1,352 @@ +"use client" + +import React, { useState, useMemo } from "react" +import { useRouter } from "next/navigation" +import { Building2, AlertTriangle } from "lucide-react" +import { TargetsDataTable } from "./targets/targets-data-table" +import { createTargetColumns } from "./targets/targets-columns" +import { AddTargetDialog } from "./targets/add-target-dialog" +import { LoadingSpinner } from "@/components/loading-spinner" +import { Badge } from "@/components/ui/badge" +import { Skeleton } from "@/components/ui/skeleton" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { useOrganization, useOrganizationTargets, useUnlinkTargetsFromOrganization } from "@/hooks/use-organizations" +import type { Target } from "@/types/target.types" +import { toast } from "sonner" + +/** + * 组织详情视图组件 + * 显示组织的统计信息和目标列表 + */ +export function OrganizationDetailView({ + organizationId +}: { + organizationId: string +}) { + const [selectedTargets, setSelectedTargets] = useState<Target[]>([]) + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [targetToDelete, setTargetToDelete] = useState<Target | null>(null) + const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false) + + // 分页状态 + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + + // 搜索状态 + const [searchQuery, setSearchQuery] = useState("") + const [isSearching, setIsSearching] = useState(false) + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, pageIndex: 0 })) + } + + // 使用解除关联 mutation + const unlinkTargets = useUnlinkTargetsFromOrganization() + + // 使用 React Query 获取组织基本信息 + const { + data: organization, + isLoading: isLoadingOrg, + error: orgError, + } = useOrganization(parseInt(organizationId)) + + // 使用 React Query 获取组织的目标列表 + const { + data: targetsData, + isLoading: isLoadingTargets, + isFetching: isFetchingTargets, + error: targetsError, + refetch + } = useOrganizationTargets( + parseInt(organizationId), + { + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: searchQuery || undefined, + } + ) + + // 当请求完成时重置搜索状态 + React.useEffect(() => { + if (!isFetchingTargets && isSearching) { + setIsSearching(false) + } + }, [isFetchingTargets, isSearching]) + + const isLoading = isLoadingOrg || isLoadingTargets + const error = orgError || targetsError + + // 辅助函数 - 格式化日期 + const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + } + + // 导航函数 + const router = useRouter() + const navigate = (path: string) => { + router.push(path) + } + + // 处理解除关联目标 + const handleDeleteTarget = (target: Target) => { + setTargetToDelete(target) + setDeleteDialogOpen(true) + } + + // 确认解除关联目标 + const confirmDelete = async () => { + if (!targetToDelete) return + + setDeleteDialogOpen(false) + const targetId = targetToDelete.id + setTargetToDelete(null) + + // 调用解除关联 API + unlinkTargets.mutate({ + organizationId: parseInt(organizationId), + targetIds: [targetId] + }) + } + + // 处理批量解除关联 + const handleBulkDelete = () => { + if (selectedTargets.length === 0) { + return + } + setBulkDeleteDialogOpen(true) + } + + // 确认批量解除关联 + const confirmBulkDelete = async () => { + if (selectedTargets.length === 0) return + + const targetIds = selectedTargets.map(target => target.id) + + setBulkDeleteDialogOpen(false) + setSelectedTargets([]) + + // 调用批量解除关联 API + unlinkTargets.mutate({ + organizationId: parseInt(organizationId), + targetIds + }) + } + + // 处理添加目标 + const handleAddTarget = () => { + setIsAddDialogOpen(true) + } + + // 处理添加成功 + const handleAddSuccess = () => { + setIsAddDialogOpen(false) + refetch() + } + + // 处理分页变化 + const handlePaginationChange = (newPagination: { pageIndex: number; pageSize: number }) => { + setPagination(newPagination) + setSelectedTargets([]) + } + + // 创建列定义 + const targetColumns = useMemo( + () => + createTargetColumns({ + formatDate, + navigate, + handleDelete: handleDeleteTarget, + }), + [formatDate, navigate, handleDeleteTarget] + ) + + // 错误状态 + if (error) { + return ( + <div className="flex flex-col items-center justify-center py-12"> + <div className="rounded-full bg-destructive/10 p-3 mb-4"> + <AlertTriangle className="h-10 w-10 text-destructive" /> + </div> + <h3 className="text-lg font-semibold mb-2">加载失败</h3> + <p className="text-muted-foreground text-center mb-4"> + {error.message || "加载数据时出现错误,请重试"} + </p> + <button + onClick={() => refetch()} + className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" + > + 重新加载 + </button> + </div> + ) + } + + // 加载状态 + if (isLoading) { + return ( + <div className="flex flex-col gap-4 px-4 lg:px-6"> + {/* 页面头部骨架 */} + <div className="space-y-2"> + <div className="flex items-center gap-2"> + <Skeleton className="h-8 w-8 rounded-md" /> + <Skeleton className="h-8 w-64" /> + </div> + <Skeleton className="h-4 w-96" /> + </div> + + {/* 表格骨架 */} + <Skeleton className="h-96 w-full" /> + </div> + ) + } + + if (!organization) { + return ( + <div className="flex flex-col items-center justify-center py-12"> + <Building2 className="mx-auto text-muted-foreground mb-4 h-12 w-12" /> + <h3 className="text-lg font-semibold mb-2">组织不存在</h3> + <p className="text-muted-foreground">未找到ID为 {organizationId} 的组织</p> + </div> + ) + } + + // 计算统计数据 + const stats = { + totalTargets: targetsData?.total || 0, + } + + return ( + <> + {/* 页面头部 - 简洁版 */} + <div className="px-4 lg:px-6"> + <div className="flex items-start justify-between"> + <div className="space-y-1"> + <h2 className="text-2xl font-bold tracking-tight flex items-center gap-2"> + <Building2 className="h-6 w-6" /> + {organization.name} + </h2> + <p className="text-muted-foreground"> + {organization.description || "暂无描述"} + </p> + <div className="flex items-center gap-4 text-sm text-muted-foreground pt-1"> + <span>创建于 {formatDate(organization.createdAt)}</span> + <span>·</span> + <span>{stats.totalTargets} 个目标</span> + </div> + </div> + </div> + </div> + + {/* 目标列表 */} + <div className="px-4 lg:px-6"> + <TargetsDataTable + data={targetsData?.results || []} + columns={targetColumns} + onAddNew={handleAddTarget} + onBulkDelete={handleBulkDelete} + onSelectionChange={setSelectedTargets} + searchPlaceholder="搜索目标名称..." + searchColumn="name" + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + addButtonText="关联目标" + pagination={pagination} + setPagination={setPagination} + paginationInfo={targetsData ? { + total: targetsData.total, + page: targetsData.page, + pageSize: targetsData.pageSize, + totalPages: targetsData.totalPages, + } : undefined} + onPaginationChange={handlePaginationChange} + /> + </div> + + {/* 添加目标对话框 */} + <AddTargetDialog + organizationId={parseInt(organizationId)} + organizationName={organization.name} + onAdd={handleAddSuccess} + open={isAddDialogOpen} + onOpenChange={setIsAddDialogOpen} + /> + + {/* 解除关联确认对话框 */} + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认解除关联</AlertDialogTitle> + <AlertDialogDescription> + 确定要解除目标 "{targetToDelete?.name}" 与此组织的关联吗?此操作只会解除关联关系,目标本身不会被删除。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + 确认解除 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + + {/* 批量解除关联确认对话框 */} + <AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认批量解除关联</AlertDialogTitle> + <AlertDialogDescription> + 此操作将解除以下 {selectedTargets.length} 个目标与此组织的关联。目标本身不会被删除,仍可正常使用。 + </AlertDialogDescription> + </AlertDialogHeader> + <div className="mt-2 p-2 bg-muted rounded-md max-h-96 overflow-y-auto"> + <ul className="text-sm space-y-1"> + {selectedTargets.map((target) => ( + <li key={target.id} className="flex items-center"> + <span className="font-medium">{target.name}</span> + {target.description && ( + <span className="text-muted-foreground ml-2">- {target.description}</span> + )} + </li> + ))} + </ul> + </div> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmBulkDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + 确认解除 {selectedTargets.length} 个关联 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </> + ) +} diff --git a/frontend/components/organization/organization-list.tsx b/frontend/components/organization/organization-list.tsx new file mode 100644 index 00000000..f5a07a29 --- /dev/null +++ b/frontend/components/organization/organization-list.tsx @@ -0,0 +1,374 @@ +"use client" + +import React, { useState, useMemo, useCallback, useEffect } from "react" +import { useRouter } from "next/navigation" +import { Trash2, Plus, Building2 } from "lucide-react" + +// 导入 UI 组件 +import { Button } from "@/components/ui/button" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { LoadingSpinner } from "@/components/loading-spinner" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" + +// 导入数据表格组件 +import { OrganizationDataTable } from "./organization-data-table" +import { createOrganizationColumns } from "./organization-columns" + +// 导入业务组件 +import { AddOrganizationDialog } from "./add-organization-dialog" +import { EditOrganizationDialog } from "./edit-organization-dialog" +import { InitiateScanDialog } from "@/components/scan/initiate-scan-dialog" +import { CreateScheduledScanDialog } from "@/components/scan/scheduled/create-scheduled-scan-dialog" + +// 导入 React Query Hooks +import { + useOrganizations, + useDeleteOrganization, + useBatchDeleteOrganizations, + useUpdateOrganization, +} from "@/hooks/use-organizations" + +// 导入类型定义 +import type { Organization } from "@/types/organization.types" + +/** + * 组织列表组件(使用 React Query) + * + * 功能特性: + * 1. 统一的 Loading 状态管理 + * 2. 自动缓存和重新验证 + * 3. 乐观更新 + * 4. 自动错误处理 + * 5. 更好的用户体验 + */ +export function OrganizationList() { + // 状态管理 + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [editDialogOpen, setEditDialogOpen] = useState(false) + const [addDialogOpen, setAddDialogOpen] = useState(false) + const [initiateScanDialogOpen, setInitiateScanDialogOpen] = useState(false) + const [scheduleScanDialogOpen, setScheduleScanDialogOpen] = useState(false) + const [organizationToDelete, setOrganizationToDelete] = useState<Organization | null>(null) + const [organizationToEdit, setOrganizationToEdit] = useState<Organization | null>(null) + const [organizationToScan, setOrganizationToScan] = useState<Organization | null>(null) + const [organizationToSchedule, setOrganizationToSchedule] = useState<Organization | null>(null) + const [selectedOrganizations, setSelectedOrganizations] = useState<Organization[]>([]) + const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false) + + // 分页状态 + const [pagination, setPagination] = useState({ + pageIndex: 0, // 0-based for react-table + pageSize: 10, + }) + + const [searchQuery, setSearchQuery] = useState("") + const [isSearching, setIsSearching] = useState(false) + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, pageIndex: 0 })) + } + + // 使用 React Query 获取组织数据 + const { + data, + isLoading, + isFetching, + error, + refetch + } = useOrganizations({ + page: pagination.pageIndex + 1, // 转换为 1-based + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }, { enabled: true }) + + useEffect(() => { + if (!isFetching && isSearching) { + setIsSearching(false) + } + }, [isFetching, isSearching]) + + // Mutations + const deleteOrganization = useDeleteOrganization() + const batchDeleteOrganizations = useBatchDeleteOrganizations() + const updateOrganization = useUpdateOrganization() + + // 辅助函数 - 格式化日期 + const formatDate = useCallback((dateString: string): string => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + }, []) + + // 处理删除操作 + const handleDelete = useCallback((org: Organization) => { + setOrganizationToDelete(org) + setDeleteDialogOpen(true) + }, []) + + // 处理编辑操作 + const handleEdit = useCallback((org: Organization) => { + setOrganizationToEdit(org) + setEditDialogOpen(true) + }, []) + + // 处理发起扫描操作 + const handleInitiateScan = useCallback((org: Organization) => { + setOrganizationToScan(org) + setInitiateScanDialogOpen(true) + }, []) + + // 处理计划扫描操作 + const handleScheduleScan = useCallback((org: Organization) => { + setOrganizationToSchedule(org) + setScheduleScanDialogOpen(true) + }, []) + + // 导航到详情页面(使用 Next.js 客户端路由) + const router = useRouter() + const navigate = useCallback((path: string) => { + router.push(path) + }, [router]) + + // 创建列定义 + const columns = useMemo(() => + createOrganizationColumns({ + formatDate, + navigate, + handleEdit, + handleDelete, + handleInitiateScan, + handleScheduleScan, + }), + [formatDate, navigate, handleEdit, handleDelete, handleInitiateScan, handleScheduleScan] + ) + + // 确认删除组织 + const confirmDelete = async () => { + if (!organizationToDelete) return + + setDeleteDialogOpen(false) + setOrganizationToDelete(null) + + // 使用 React Query 的删除 mutation(自动乐观更新) + deleteOrganization.mutate(Number(organizationToDelete.id)) + } + + // 编辑组织成功回调 + const handleOrganizationEdited = (updatedOrganization: Organization) => { + // 只需要关闭对话框,React Query 已经在 dialog 中处理了更新 + setEditDialogOpen(false) + setOrganizationToEdit(null) + } + + // 批量删除处理函数 + const handleBulkDelete = () => { + if (selectedOrganizations.length === 0) { + return + } + setBulkDeleteDialogOpen(true) + } + + // 确认批量删除 + const confirmBulkDelete = async () => { + if (selectedOrganizations.length === 0) return + + const deletedIds = selectedOrganizations.map(org => Number(org.id)) + + setBulkDeleteDialogOpen(false) + setSelectedOrganizations([]) + + // 使用 React Query 的批量删除 mutation(自动乐观更新) + batchDeleteOrganizations.mutate(deletedIds) + } + + // 处理分页变化 + const handlePaginationChange = (newPagination: { pageIndex: number; pageSize: number }) => { + setPagination(newPagination) + } + + // 错误状态 + if (error) { + return ( + <div className="flex flex-col items-center justify-center py-12"> + <div className="rounded-full bg-destructive/10 p-3 mb-4"> + <Trash2 className="text-destructive" /> + </div> + <h3 className="text-lg font-semibold mb-2">加载失败</h3> + <p className="text-muted-foreground text-center mb-4"> + {error.message || "加载组织数据时出现错误,请重试"} + </p> + <Button variant="outline" onClick={() => refetch()}> + 重新加载 + </Button> + </div> + ) + } + + // 加载状态 + if (isLoading) { + return <OrganizationListSkeleton /> + } + + // 数据为空检查 + if (!data) { + return <OrganizationListSkeleton /> + } + + return ( + <div className="space-y-4"> + {/* 主要内容 */} + <OrganizationDataTable + data={data.organizations} + columns={columns} + onAddNew={() => setAddDialogOpen(true)} + onBulkDelete={handleBulkDelete} + onSelectionChange={setSelectedOrganizations} + searchPlaceholder="搜索组织名称..." + searchColumn="name" + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + pagination={pagination} + setPagination={setPagination} + paginationInfo={data.pagination} + onPaginationChange={handlePaginationChange} + /> + + {/* 删除确认对话框 */} + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除</AlertDialogTitle> + <AlertDialogDescription> + 此操作将永久删除组织 "{organizationToDelete?.name}" 并解除其与域名的关联。域名本身不会被删除,仍可正常使用。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + disabled={deleteOrganization.isPending} + > + {deleteOrganization.isPending ? ( + <> + <LoadingSpinner/> + 删除中... + </> + ) : ( + "删除" + )} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + + {/* 编辑组织对话框 */} + {organizationToEdit && ( + <EditOrganizationDialog + organization={organizationToEdit} + open={editDialogOpen} + onOpenChange={setEditDialogOpen} + onEdit={handleOrganizationEdited} + /> + )} + + {/* 批量删除确认对话框 */} + <AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认批量删除</AlertDialogTitle> + <AlertDialogDescription> + 此操作将永久删除以下 {selectedOrganizations.length} 个组织并解除其与域名的关联。域名本身不会被删除,仍可正常使用。 + </AlertDialogDescription> + </AlertDialogHeader> + {/* 组织列表容器 - 固定最大高度并支持滚动 */} + <div className="mt-2 p-2 bg-muted rounded-md max-h-96 overflow-y-auto"> + <ul className="text-sm space-y-1"> + {selectedOrganizations.map((org) => ( + <li key={org.id} className="flex items-center"> + <span className="font-medium">{org.name}</span> + {org.description && ( + <span className="ml-2 text-muted-foreground">- {org.description}</span> + )} + </li> + ))} + </ul> + </div> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmBulkDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + disabled={batchDeleteOrganizations.isPending} + > + {batchDeleteOrganizations.isPending ? ( + <> + <LoadingSpinner/> + 删除中... + </> + ) : ( + `删除 ${selectedOrganizations.length} 个组织` + )} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + + {/* 添加组织对话框 */} + <AddOrganizationDialog + open={addDialogOpen} + onOpenChange={setAddDialogOpen} + onAdd={() => { + // React Query 会自动刷新数据,不需要手动处理 + setAddDialogOpen(false) + }} + /> + + {/* 发起扫描对话框 */} + <InitiateScanDialog + organization={organizationToScan} + organizationId={organizationToScan?.id} + open={initiateScanDialogOpen} + onOpenChange={setInitiateScanDialogOpen} + onSuccess={() => { + setOrganizationToScan(null) + }} + /> + + {/* 定时扫描对话框 */} + <CreateScheduledScanDialog + open={scheduleScanDialogOpen} + onOpenChange={setScheduleScanDialogOpen} + presetOrganizationId={organizationToSchedule?.id} + presetOrganizationName={organizationToSchedule?.name} + onSuccess={() => { + setOrganizationToSchedule(null) + }} + /> + </div> + ) +} + +function OrganizationListSkeleton() { + return ( + <DataTableSkeleton toolbarButtonCount={2} rows={6} columns={4} /> + ) +} diff --git a/frontend/components/organization/targets/add-target-dialog.tsx b/frontend/components/organization/targets/add-target-dialog.tsx new file mode 100644 index 00000000..5d6f75fe --- /dev/null +++ b/frontend/components/organization/targets/add-target-dialog.tsx @@ -0,0 +1,4 @@ +"use client" + +export { LinkTargetDialog as AddTargetDialog } from "./link-target-dialog" + diff --git a/frontend/components/organization/targets/index.ts b/frontend/components/organization/targets/index.ts new file mode 100644 index 00000000..d5241751 --- /dev/null +++ b/frontend/components/organization/targets/index.ts @@ -0,0 +1,9 @@ +/** + * Organization Targets Components - 统一导出 + */ +export { TargetsDataTable } from './targets-data-table' +export { createTargetColumns } from './targets-columns' +export { TargetsDetailView } from './targets-detail-view' +export { AddTargetDialog } from './add-target-dialog' +export { LinkTargetDialog } from './link-target-dialog' + diff --git a/frontend/components/organization/targets/link-target-dialog.tsx b/frontend/components/organization/targets/link-target-dialog.tsx new file mode 100644 index 00000000..a1a38c94 --- /dev/null +++ b/frontend/components/organization/targets/link-target-dialog.tsx @@ -0,0 +1,343 @@ +"use client" + +import React, { useState, useRef, useMemo } from "react" +import { Plus, Target, Building2, Loader2 } from "lucide-react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" + +// 导入 UI 组件 +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { LoadingSpinner } from "@/components/loading-spinner" +import { TargetValidator } from "@/lib/target-validator" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" + +// 导入 React Query Hooks +import { useBatchCreateTargets } from "@/hooks/use-targets" + +// 导入类型定义 +import type { BatchCreateResponse } from "@/types/api-response.types" + +// 表单验证 Schema +const formSchema = z.object({ + targets: z.string() + .min(1, { message: "请输入至少一个目标" }) + .refine( + (val) => { + const lines = val.split('\n').map(l => l.trim()).filter(l => l.length > 0) + return lines.length > 0 + }, + { message: "请输入至少一个目标" } + ), +}) + +type FormValues = z.infer<typeof formSchema> + +// 组件属性类型定义 +interface LinkTargetDialogProps { + organizationId: number // 组织ID(固定,不可修改) + organizationName: string // 组织名称 + onAdd?: (result: BatchCreateResponse) => void // 添加成功回调,返回批量创建的统计信息 + open?: boolean // 外部控制对话框开关状态 + onOpenChange?: (open: boolean) => void // 外部控制对话框开关回调 +} + +/** + * 关联目标对话框组件(使用 React Query) + * + * 功能特性: + * 1. 批量输入目标并关联到组织 + * 2. 自动创建不存在的目标 + * 3. 自动管理提交状态 + * 4. 自动错误处理和成功提示 + * 5. 固定组织ID,不可修改 + */ +export function LinkTargetDialog({ + organizationId, + organizationName, + onAdd, + open: externalOpen, + onOpenChange: externalOnOpenChange, +}: LinkTargetDialogProps) { + // 对话框开关状态 - 支持外部控制 + const [internalOpen, setInternalOpen] = useState(false) + const open = externalOpen !== undefined ? externalOpen : internalOpen + const setOpen = externalOnOpenChange || setInternalOpen + + // 行号列和输入框的 ref(用于同步滚动) + const lineNumbersRef = useRef<HTMLDivElement | null>(null) + const textareaRef = useRef<HTMLTextAreaElement | null>(null) + + // 使用 React Query 的批量创建目标 mutation + const batchCreateTargets = useBatchCreateTargets() + + // 初始化表单 + const form = useForm<FormValues>({ + resolver: zodResolver(formSchema), + defaultValues: { + targets: "", + }, + }) + + // 监听表单值变化 + const targetsText = form.watch("targets") + + // 实时验证目标 + const targetValidation = useMemo(() => { + const lines = targetsText + .split("\n") + .map((s) => s.trim()) + .filter((s) => s.length > 0) + + if (lines.length === 0) { + return { + count: 0, + invalid: [] + } + } + + const results = TargetValidator.validateTargetBatch(lines) + const invalid = results + .filter((r) => !r.isValid) + .map((r) => ({ index: r.index, originalTarget: r.originalTarget, error: r.error || "目标格式无效", type: r.type })) + + return { + count: lines.length, + invalid + } + }, [targetsText]) + + + // 处理表单提交 + const onSubmit = (values: FormValues) => { + // 检查是否有无效目标 + if (targetValidation.invalid.length > 0) { + return + } + + // 解析目标列表(每行一个目标) + const targetList = values.targets + .split("\n") + .map(line => line.trim()) + .filter(line => line.length > 0) + .map(name => ({ + name, + })) + + if (targetList.length === 0) { + return + } + + // 使用 React Query mutation + batchCreateTargets.mutate( + { + targets: targetList, + organizationId: organizationId, + }, + { + onSuccess: (batchCreateResult) => { + // 重置表单 + form.reset() + + // 关闭对话框 + setOpen(false) + + // 调用外部回调(如果提供) + if (onAdd) { + // 将批量创建结果适配为通用的 BatchCreateResponse 结构 + const adaptedResult: BatchCreateResponse = { + message: batchCreateResult.message, + requestedCount: + batchCreateResult.createdCount + + batchCreateResult.reusedCount + + batchCreateResult.failedCount, + createdCount: batchCreateResult.createdCount, + existedCount: batchCreateResult.reusedCount, + skippedCount: 0, + skippedDomains: batchCreateResult.failedTargets.map((item) => ({ + name: item.name, + reason: item.reason, + })), + } + + onAdd(adaptedResult) + } + } + } + ) + } + + // 处理对话框关闭 + const handleOpenChange = (newOpen: boolean) => { + if (!batchCreateTargets.isPending) { + setOpen(newOpen) + if (!newOpen) { + // 关闭时重置表单 + form.reset() + } + } + } + + // 表单验证 + const isFormValid = form.formState.isValid && targetValidation.invalid.length === 0 + + // 同步输入框和行号列的滚动 + const handleTextareaScroll = (e: React.UIEvent<HTMLTextAreaElement>) => { + if (lineNumbersRef.current) { + lineNumbersRef.current.scrollTop = e.currentTarget.scrollTop + } + } + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + {/* 触发按钮 - 仅在非外部控制时显示 */} + {externalOpen === undefined && ( + <DialogTrigger asChild> + <Button size="sm" variant="secondary"> + <Plus /> + 添加目标 + </Button> + </DialogTrigger> + )} + + {/* 对话框内容 */} + <DialogContent className="sm:max-w-[650px] max-h-[90vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle className="flex items-center space-x-2"> + <Target /> + <span>添加目标到组织</span> + </DialogTitle> + <DialogDescription> + 输入目标并关联到 "{organizationName}"。支持批量添加,每行一个目标。标有 * 的字段为必填项。 + </DialogDescription> + </DialogHeader> + + {/* 表单 */} + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + <div className="grid gap-4 py-4"> + {/* 目标输入框 - 支持多行,带行号 */} + <FormField + control={form.control} + name="targets" + render={({ field }) => ( + <FormItem> + <FormLabel> + 目标 <span className="text-destructive">*</span> + </FormLabel> + <FormControl> + <div className="relative border rounded-md overflow-hidden bg-background"> + <div className="flex h-[324px]"> + {/* 行号列 - 固定显示15行 */} + <div className="flex-shrink-0 w-12 bg-muted/30 border-r select-none overflow-hidden"> + <div + ref={lineNumbersRef} + className="py-3 px-2 text-right font-mono text-xs text-muted-foreground leading-[1.4] h-full overflow-y-auto scrollbar-hide" + > + {Array.from({ length: Math.max(field.value.split('\n').length, 15) }, (_, i) => ( + <div key={i + 1} className="h-[20px]"> + {i + 1} + </div> + ))} + </div> + </div> + {/* 输入框 - 固定高度显示15行 */} + <Textarea + {...field} + ref={(e) => { + field.ref(e) + textareaRef.current = e + }} + onScroll={handleTextareaScroll} + placeholder={`请输入目标,每行一个\n支持域名、IP、CIDR\n例如:\nexample.com\n192.168.1.1\n10.0.0.0/8`} + disabled={batchCreateTargets.isPending} + className="font-mono h-full overflow-y-auto resize-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 leading-[1.4] text-sm py-3" + style={{ lineHeight: '20px' }} + /> + </div> + </div> + </FormControl> + <FormDescription> + {targetValidation.count} 个目标 + {targetValidation.invalid.length > 0 && ( + <span className="text-destructive ml-2"> + | {targetValidation.invalid.length} 个无效 + </span> + )} + </FormDescription> + {targetValidation.invalid.length > 0 && ( + <div className="text-xs text-destructive"> + 例如 第 {targetValidation.invalid[0].index + 1} 行: "{targetValidation.invalid[0].originalTarget}" - {targetValidation.invalid[0].error} + </div> + )} + <FormMessage /> + </FormItem> + )} + /> + + {/* 所属组织(只读显示) */} + <div className="grid gap-2"> + <Label className="flex items-center space-x-2"> + <Building2 /> + <span>所属组织</span> + </Label> + <div className="flex items-center gap-2 px-3 py-2 border rounded-md bg-muted/50"> + <Building2 className="h-4 w-4 text-muted-foreground" /> + <span className="font-medium">{organizationName}</span> + </div> + </div> + </div> + + {/* 对话框底部按钮 */} + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={batchCreateTargets.isPending} + > + 取消 + </Button> + <Button + type="submit" + disabled={batchCreateTargets.isPending || !isFormValid} + > + {batchCreateTargets.isPending ? ( + <> + <LoadingSpinner/> + 创建中... + </> + ) : ( + <> + <Plus /> + 创建目标 + </> + )} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +} + diff --git a/frontend/components/organization/targets/targets-columns.tsx b/frontend/components/organization/targets/targets-columns.tsx new file mode 100644 index 00000000..a638dc39 --- /dev/null +++ b/frontend/components/organization/targets/targets-columns.tsx @@ -0,0 +1,320 @@ +"use client" + +import React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { Checkbox } from "@/components/ui/checkbox" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { MoreHorizontal, Eye, Trash2, ChevronsUpDown, ChevronUp, ChevronDown, Copy, Check } from "lucide-react" +import type { Target } from "@/types/target.types" +import { toast } from "sonner" + +/** + * 可复制单元格组件 + */ +function CopyableCell({ + value, + maxWidth = "400px", + truncateLength = 50, + successMessage = "已复制", + className = "font-medium" +}: { + value: string + maxWidth?: string + truncateLength?: number + successMessage?: string + className?: string +}) { + const [copied, setCopied] = React.useState(false) + const isLong = value.length > truncateLength + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(value) + setCopied(true) + toast.success(successMessage) + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error('复制失败') + } + } + + return ( + <div className="group inline-flex items-center gap-1" style={{ maxWidth }}> + <div className={`text-sm truncate cursor-default ${className}`}> + {value} + </div> + + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className={`h-6 w-6 flex-shrink-0 hover:bg-accent transition-opacity ${ + copied ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' + }`} + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">{copied ? '已复制!' : '点击复制'}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + ) +} + +// 列创建函数的参数类型 +interface CreateColumnsProps { + formatDate: (dateString: string) => string + navigate: (path: string) => void + handleDelete: (target: Target) => void +} + +/** + * 目标行操作组件 + */ +function TargetRowActions({ + target, + onView, + onDelete, +}: { + target: Target + onView: () => void + onDelete: () => void +}) { + return ( + <div className="flex items-center gap-1"> + <TooltipProvider delayDuration={300}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className="h-8 w-8" + onClick={onView} + > + <Eye className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">查看详情</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + + <TooltipProvider delayDuration={300}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className="h-8 w-8 text-destructive hover:text-destructive" + onClick={onDelete} + > + <Trash2 className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">解除关联</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + ) +} + +/** + * 目标名称单元格组件 + */ +function TargetNameCell({ + name, + targetId, + navigate +}: { + name: string + targetId: number + navigate: (path: string) => void +}) { + const [copied, setCopied] = React.useState(false) + + const handleCopy = async (e: React.MouseEvent) => { + e.stopPropagation() + try { + await navigator.clipboard.writeText(name) + setCopied(true) + toast.success("已复制目标名称") + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error('复制失败') + } + } + + return ( + <div className="group inline-flex items-center gap-1 max-w-[400px]"> + <button + onClick={() => navigate(`/target/${targetId}/subdomain/`)} + className="text-sm font-medium hover:text-primary hover:underline underline-offset-2 transition-colors cursor-pointer truncate" + > + {name} + </button> + <Button + variant="ghost" + size="icon" + className={`h-6 w-6 flex-shrink-0 hover:bg-accent transition-opacity ${ + copied ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' + }`} + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + </div> + ) +} + +/** + * 数据表格列头组件 + */ +function DataTableColumnHeader({ + column, + title, +}: { + column: { getCanSort: () => boolean; getIsSorted: () => false | "asc" | "desc"; toggleSorting: (desc?: boolean) => void } + title: string +}) { + if (!column.getCanSort()) { + return <div className="-ml-3 font-medium">{title}</div> + } + + const isSorted = column.getIsSorted() + + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted" + > + {title} + {isSorted === "asc" ? ( + <ChevronUp /> + ) : isSorted === "desc" ? ( + <ChevronDown /> + ) : ( + <ChevronsUpDown /> + )} + </Button> + ) +} + +/** + * 创建目标表格列定义 + */ +export const createTargetColumns = ({ + formatDate, + navigate, + handleDelete, +}: CreateColumnsProps): ColumnDef<Target>[] => [ + // 选择列 + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + + // 目标名称列 + { + accessorKey: "name", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="目标名称" /> + ), + cell: ({ row }) => ( + <TargetNameCell + name={row.getValue("name") as string} + targetId={row.original.id} + navigate={navigate} + /> + ), + }, + + // 类型列 + { + accessorKey: "type", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="类型" /> + ), + cell: ({ row }) => { + const type = row.getValue("type") as string | null + if (!type) { + return <span className="text-sm text-muted-foreground">-</span> + } + const typeMap: Record<string, { label: string; variant: "default" | "secondary" | "outline" }> = { + domain: { label: "域名", variant: "default" }, + ip: { label: "IP", variant: "secondary" }, + cidr: { label: "CIDR", variant: "outline" }, + } + const typeInfo = typeMap[type] || { label: type, variant: "secondary" as const } + return ( + <Badge variant={typeInfo.variant}> + {typeInfo.label} + </Badge> + ) + }, + }, + + // 操作列 + { + id: "actions", + cell: ({ row }) => ( + <TargetRowActions + target={row.original} + onView={() => navigate(`/target/${row.original.id}/subdomain/`)} + onDelete={() => handleDelete(row.original)} + /> + ), + enableSorting: false, + enableHiding: false, + }, +] + diff --git a/frontend/components/organization/targets/targets-data-table.tsx b/frontend/components/organization/targets/targets-data-table.tsx new file mode 100644 index 00000000..24dbdda1 --- /dev/null +++ b/frontend/components/organization/targets/targets-data-table.tsx @@ -0,0 +1,386 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconPlus, + IconTrash, + IconSearch, + IconLoader2, +} from "@tabler/icons-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import type { Target } from "@/types/target.types" +import type { PaginationInfo } from "@/types/common.types" + +interface TargetsDataTableProps { + data: Target[] + columns: ColumnDef<Target>[] + onAddNew?: () => void + onAddHover?: () => void + onBulkDelete?: () => void + onSelectionChange?: (selectedRows: Target[]) => void + searchPlaceholder?: string + searchColumn?: string + searchValue?: string + onSearch?: (value: string) => void + isSearching?: boolean + addButtonText?: string + pagination?: { pageIndex: number; pageSize: number } + setPagination?: React.Dispatch<React.SetStateAction<{ pageIndex: number; pageSize: number }>> + paginationInfo?: PaginationInfo + onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void +} + +/** + * 目标数据表格组件 + * 专门用于显示和管理目标数据的表格 + * 包含搜索、分页、列显示控制等功能 + */ +export function TargetsDataTable({ + data = [], + columns, + onAddNew, + onAddHover, + onBulkDelete, + onSelectionChange, + searchPlaceholder = "搜索目标名称...", + searchColumn = "name", + searchValue, + onSearch, + isSearching = false, + addButtonText = "添加目标", + pagination: externalPagination, + setPagination: setExternalPagination, + paginationInfo, + onPaginationChange, +}: TargetsDataTableProps) { + // 搜索本地状态 + const [localSearchValue, setLocalSearchValue] = React.useState(searchValue || "") + + React.useEffect(() => { + setLocalSearchValue(searchValue || "") + }, [searchValue]) + + const handleSearchSubmit = () => { + if (onSearch) { + onSearch(localSearchValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter") { + handleSearchSubmit() + } + } + + const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({}) + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [sorting, setSorting] = React.useState<SortingState>([]) + + const [internalPagination, setInternalPagination] = React.useState<{ pageIndex: number, pageSize: number }>({ + pageIndex: 0, + pageSize: 10, + }) + + const pagination = externalPagination || internalPagination + const setPagination = setExternalPagination || setInternalPagination + + const validData = React.useMemo(() => { + const filtered = (data || []).filter(item => item && typeof item.id !== 'undefined' && item.id !== null) + return filtered + }, [data]) + + const table = useReactTable({ + data: validData, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination, + }, + pageCount: paginationInfo?.totalPages ?? -1, + manualPagination: !!paginationInfo, + getRowId: (row) => row.id.toString(), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const newPagination = typeof updater === 'function' ? updater(pagination) : updater + setPagination(newPagination) + if (onPaginationChange) { + onPaginationChange(newPagination) + } + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + React.useEffect(() => { + if (onSelectionChange) { + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + onSelectionChange(selectedRows) + } + }, [rowSelection, onSelectionChange, table]) + + return ( + <div className="w-full space-y-4"> + {/* 工具栏 */} + <div className="flex items-center justify-between"> + {/* 搜索框 */} + <div className="flex items-center space-x-2"> + <Input + placeholder={searchPlaceholder} + value={localSearchValue} + onChange={(e) => setLocalSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="h-8 max-w-sm" + /> + <Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}> + {isSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + + {/* 右侧操作按钮 */} + <div className="flex items-center space-x-2"> + {/* 列显示控制 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + 列 + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + return ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {column.id === "id" && "ID"} + {column.id === "name" && "目标名称"} + {column.id === "type" && "类型"} + {column.id === "createdAt" && "创建时间"} + {column.id === "updatedAt" && "更新时间"} + {!["id", "name", "type", "createdAt", "updatedAt"].includes(column.id) && column.id} + </DropdownMenuCheckboxItem> + ) + })} + </DropdownMenuContent> + </DropdownMenu> + {/* 添加新目标按钮 */} + {onAddNew && ( + <Button onClick={onAddNew} onMouseEnter={onAddHover} size="sm"> + <IconPlus /> + {addButtonText} + </Button> + )} + </div> + </div> + + {/* 表格容器 */} + <div className="rounded-md border"> + <Table> + {/* 表头 */} + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id} colSpan={header.colSpan}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + + {/* 表体 */} + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + className="group" + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + 暂无数据 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + {/* 分页控制 */} + <div className="flex items-center justify-between px-2"> + {/* 选中行信息 */} + <div className="flex-1 text-sm text-muted-foreground"> + 已选择 {table.getFilteredSelectedRowModel().rows.length} 个,共{" "} + {paginationInfo ? paginationInfo.total : table.getFilteredRowModel().rows.length} 个 + </div> + + {/* 分页控制器 */} + <div className="flex items-center space-x-6 lg:space-x-8"> + {/* 每页显示数量选择 */} + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + 每页显示 + </Label> + <Select + value={`${table.getState().pagination.pageSize}`} + onValueChange={(value) => { + table.setPageSize(Number(value)) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={table.getState().pagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 页码信息 */} + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + 第 {table.getState().pagination.pageIndex + 1} 页,共{" "} + {table.getPageCount()} 页 + </div> + + {/* 分页按钮 */} + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">第一页</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">上一页</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">下一页</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">最后一页</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + </div> + ) +} + diff --git a/frontend/components/organization/targets/targets-detail-view.tsx b/frontend/components/organization/targets/targets-detail-view.tsx new file mode 100644 index 00000000..8c04b336 --- /dev/null +++ b/frontend/components/organization/targets/targets-detail-view.tsx @@ -0,0 +1,296 @@ +"use client" + +import React, { useState, useMemo } from "react" +import { AlertTriangle } from "lucide-react" +import { useRouter } from "next/navigation" +import { toast } from "sonner" +import { TargetsDataTable } from "./targets-data-table" +import { createTargetColumns } from "./targets-columns" +import { AddTargetDialog } from "./add-target-dialog" +import { LoadingSpinner } from "@/components/loading-spinner" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { useOrganization, useUnlinkTargetsFromOrganization } from "@/hooks/use-organizations" +import { useTargets } from "@/hooks/use-targets" +import type { Target } from "@/types/target.types" + +/** + * 组织目标详情视图组件(使用 React Query) + * 用于显示和管理组织下的目标列表 + * 支持通过组织ID获取数据 + */ +export function OrganizationTargetsDetailView({ + organizationId +}: { + organizationId: string +}) { + const [selectedTargets, setSelectedTargets] = useState<Target[]>([]) + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [targetToDelete, setTargetToDelete] = useState<Target | null>(null) + const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false) + + // 使用解除关联 mutation + const unlinkTargets = useUnlinkTargetsFromOrganization() + + // 分页状态 + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + + // 使用 React Query 获取组织基本信息 + const { + data: organization, + isLoading: isLoadingOrg, + error: orgError, + } = useOrganization(parseInt(organizationId)) + + // 使用 React Query 获取目标列表(过滤组织) + const { + data: targetsData, + isLoading: isLoadingTargets, + error: targetsError, + refetch + } = useTargets({ + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + organizationId: parseInt(organizationId), + }) + + const isLoading = isLoadingOrg || isLoadingTargets + const error = orgError || targetsError + + // 辅助函数 - 格式化日期 + const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + } + + // 导航函数(使用 Next.js 客户端路由) + const router = useRouter() + const navigate = (path: string) => { + router.push(path) + } + + // 处理解除关联目标 + const handleDeleteTarget = (target: Target) => { + setTargetToDelete(target) + setDeleteDialogOpen(true) + } + + // 确认解除关联目标 + const confirmDelete = async () => { + if (!targetToDelete) return + + setDeleteDialogOpen(false) + const targetId = targetToDelete.id + setTargetToDelete(null) + + // 调用解除关联 API + unlinkTargets.mutate({ + organizationId: parseInt(organizationId), + targetIds: [targetId] + }) + } + + // 处理批量解除关联 + const handleBulkDelete = () => { + if (selectedTargets.length === 0) { + return + } + setBulkDeleteDialogOpen(true) + } + + // 确认批量解除关联 + const confirmBulkDelete = async () => { + if (selectedTargets.length === 0) return + + const targetIds = selectedTargets.map(target => target.id) + + setBulkDeleteDialogOpen(false) + setSelectedTargets([]) + + // 调用批量解除关联 API + unlinkTargets.mutate({ + organizationId: parseInt(organizationId), + targetIds + }) + } + + // 处理添加目标 + const handleAddTarget = () => { + setIsAddDialogOpen(true) + } + + // 处理添加成功 + const handleAddSuccess = () => { + setIsAddDialogOpen(false) + // 刷新目标列表 + refetch() + } + + // 处理分页变化 + const handlePaginationChange = (newPagination: { pageIndex: number; pageSize: number }) => { + setPagination(newPagination) + // 清空选中状态 + setSelectedTargets([]) + } + + // 创建列定义 + const targetColumns = useMemo( + () => + createTargetColumns({ + formatDate, + navigate, + handleDelete: handleDeleteTarget, + }), + [formatDate, navigate, handleDeleteTarget] + ) + + // 错误状态 + if (error) { + return ( + <div className="flex flex-col items-center justify-center py-12"> + <div className="rounded-full bg-destructive/10 p-3 mb-4"> + <AlertTriangle className="h-10 w-10 text-destructive" /> + </div> + <h3 className="text-lg font-semibold mb-2">加载失败</h3> + <p className="text-muted-foreground text-center mb-4"> + {error.message || "加载目标数据时出现错误,请重试"} + </p> + <button + onClick={() => refetch()} + className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" + > + 重新加载 + </button> + </div> + ) + } + + // 加载状态 + if (isLoading) { + return ( + <DataTableSkeleton + toolbarButtonCount={3} + rows={6} + columns={4} + /> + ) + } + + if (!organization) { + return ( + <div className="flex flex-col items-center justify-center py-12"> + <p className="text-muted-foreground">组织不存在</p> + </div> + ) + } + + return ( + <> + <TargetsDataTable + data={targetsData?.targets || []} + columns={targetColumns} + onAddNew={handleAddTarget} + onBulkDelete={handleBulkDelete} + onSelectionChange={setSelectedTargets} + searchPlaceholder="搜索目标名称..." + searchColumn="name" + addButtonText="关联目标" + pagination={pagination} + setPagination={setPagination} + paginationInfo={targetsData ? { + total: targetsData.total, + page: targetsData.page, + pageSize: targetsData.pageSize, + totalPages: targetsData.totalPages, + } : undefined} + onPaginationChange={handlePaginationChange} + /> + + {/* 添加目标对话框 */} + <AddTargetDialog + organizationId={parseInt(organizationId)} + organizationName={organization.name} + onAdd={handleAddSuccess} + open={isAddDialogOpen} + onOpenChange={setIsAddDialogOpen} + /> + + {/* 解除关联确认对话框 */} + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认解除关联</AlertDialogTitle> + <AlertDialogDescription> + 确定要解除目标 "{targetToDelete?.name}" 与此组织的关联吗?此操作只会解除关联关系,目标本身不会被删除。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + 确认解除 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + + {/* 批量解除关联确认对话框 */} + <AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认批量解除关联</AlertDialogTitle> + <AlertDialogDescription> + 此操作将解除以下 {selectedTargets.length} 个目标与此组织的关联。目标本身不会被删除,仍可正常使用。 + </AlertDialogDescription> + </AlertDialogHeader> + {/* 目标列表容器 - 固定最大高度并支持滚动 */} + <div className="mt-2 p-2 bg-muted rounded-md max-h-96 overflow-y-auto"> + <ul className="text-sm space-y-1"> + {selectedTargets.map((target) => ( + <li key={target.id} className="flex items-center"> + <span className="font-medium">{target.name}</span> + {target.description && ( + <span className="text-muted-foreground ml-2">- {target.description}</span> + )} + </li> + ))} + </ul> + </div> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmBulkDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + 确认解除 {selectedTargets.length} 个关联 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </> + ) +} + +export { OrganizationTargetsDetailView as TargetsDetailView } diff --git a/frontend/components/providers/index.ts b/frontend/components/providers/index.ts new file mode 100644 index 00000000..37145127 --- /dev/null +++ b/frontend/components/providers/index.ts @@ -0,0 +1,6 @@ +/** + * Provider Components - 统一导出 + */ +export { ThemeProvider } from './theme-provider' +export { QueryProvider } from './query-provider' + diff --git a/frontend/components/providers/query-provider.tsx b/frontend/components/providers/query-provider.tsx new file mode 100644 index 00000000..fcfe1437 --- /dev/null +++ b/frontend/components/providers/query-provider.tsx @@ -0,0 +1,72 @@ +"use client" + +import React from "react" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { ReactQueryDevtools } from "@tanstack/react-query-devtools" + +// 创建 QueryClient 实例 +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // 数据立即过期,切换页面时重新请求 + staleTime: 0, + // 缓存时间(5分钟)- 保留短期缓存用于快速返回 + gcTime: 5 * 60 * 1000, + // 重试配置 + retry: (failureCount, error: unknown) => { + // 4xx 错误不重试 + const err = error as { response?: { status?: number } } + if (err?.response?.status && err.response.status >= 400 && err.response.status < 500) { + return false + } + // 最多重试 3 次 + return failureCount < 3 + }, + // 重试延迟(指数退避) + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + // 窗口重新获得焦点时自动刷新 - 用户切回来能看到最新数据 + refetchOnWindowFocus: true, + // 网络重连时不自动刷新 - 避免网络波动导致的大量请求 + refetchOnReconnect: false, + }, + mutations: { + // 变更操作重试配置 + retry: (failureCount, error: unknown) => { + // 4xx 错误不重试 + const err = error as { response?: { status?: number } } + if (err?.response?.status && err.response.status >= 400 && err.response.status < 500) { + return false + } + // 最多重试 2 次 + return failureCount < 2 + }, + }, + }, +}) + +interface QueryProviderProps { + children: React.ReactNode +} + +/** + * React Query Provider 组件 + * + * 功能: + * 1. 提供全局的 QueryClient 实例 + * 2. 配置默认的查询和变更选项 + * 3. 开发环境下启用 DevTools + */ +export function QueryProvider({ children }: QueryProviderProps) { + return ( + <QueryClientProvider client={queryClient}> + {children} + {/* 只在开发环境显示 DevTools */} + {process.env.NODE_ENV === 'development' && ( + <ReactQueryDevtools + initialIsOpen={false} + buttonPosition="bottom-right" + /> + )} + </QueryClientProvider> + ) +} diff --git a/frontend/components/providers/theme-provider.tsx b/frontend/components/providers/theme-provider.tsx new file mode 100644 index 00000000..1d24e0a4 --- /dev/null +++ b/frontend/components/providers/theme-provider.tsx @@ -0,0 +1,16 @@ +"use client" + +import * as React from "react" +import { ThemeProvider as NextThemesProvider } from "next-themes" + +/** + * 主题提供者组件 + * 基于 next-themes 实现系统主题自动切换 + * 支持亮色、暗色和跟随系统三种模式 + */ +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps<typeof NextThemesProvider>) { + return <NextThemesProvider {...props}>{children}</NextThemesProvider> +} diff --git a/frontend/components/route-prefetch.tsx b/frontend/components/route-prefetch.tsx new file mode 100644 index 00000000..981e46ca --- /dev/null +++ b/frontend/components/route-prefetch.tsx @@ -0,0 +1,13 @@ +'use client' + +import { useRoutePrefetch } from '@/hooks/use-route-prefetch' + +/** + * 路由预加载组件 + * 在应用启动后自动预加载常用页面的 JS/CSS 资源 + * 这是一个不可见的组件,只用于执行预加载逻辑 + */ +export function RoutePrefetch() { + useRoutePrefetch() + return null +} diff --git a/frontend/components/route-progress.tsx b/frontend/components/route-progress.tsx new file mode 100644 index 00000000..bd322bf8 --- /dev/null +++ b/frontend/components/route-progress.tsx @@ -0,0 +1,113 @@ +"use client" + +import { useEffect, useState, useCallback, useRef } from "react" +import { usePathname, useSearchParams } from "next/navigation" +import { cn } from "@/lib/utils" + +/** + * 路由加载进度条组件 + * + * 监听 Next.js App Router 的路由变化,显示顶部进度条动画 + */ +export function RouteProgress() { + const pathname = usePathname() + const searchParams = useSearchParams() + const [progress, setProgress] = useState(0) + const [isVisible, setIsVisible] = useState(false) + const isFirstRender = useRef(true) + + const intervalRef = useRef<NodeJS.Timeout | null>(null) + + const startProgress = useCallback(() => { + setIsVisible(true) + setProgress(0) + + // 使用 interval 平滑递增 + let currentProgress = 0 + intervalRef.current = setInterval(() => { + currentProgress += Math.random() * 10 + 5 // 每次增加 5-15% + if (currentProgress >= 90) { + currentProgress = 90 // 最多到 90%,等待完成 + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + } + setProgress(currentProgress) + }, 100) + }, []) + + const completeProgress = useCallback(() => { + // 清除进行中的 interval + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + + setProgress(100) + // 完成后短暂显示 100%,然后隐藏 + setTimeout(() => { + setIsVisible(false) + setProgress(0) + }, 300) + }, []) + + useEffect(() => { + // 跳过首次渲染 + if (isFirstRender.current) { + isFirstRender.current = false + return + } + + // 路由变化时触发进度条 + startProgress() + + // 页面加载完成后结束进度条 + const timer = setTimeout(() => completeProgress(), 300) + + return () => { + clearTimeout(timer) + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + } + }, [pathname, searchParams, startProgress, completeProgress]) + + if (!isVisible) return null + + return ( + <div + className={cn( + "fixed top-0 left-0 right-0 z-[99999] h-[3px]", + "pointer-events-none" + )} + > + {/* 进度条背景 */} + <div className="absolute inset-0 bg-primary/10" /> + + {/* 进度条 */} + <div + className={cn( + "h-full bg-primary transition-all duration-200 ease-out", + "shadow-[0_0_10px_rgba(99,102,241,0.5)]" + )} + style={{ width: `${progress}%` }} + /> + + {/* 发光效果 */} + <div + className={cn( + "absolute top-0 right-0 h-full w-24", + "bg-gradient-to-r from-transparent to-primary/50", + "opacity-50 blur-sm", + "transition-all duration-200" + )} + style={{ + transform: `translateX(${progress < 100 ? '0' : '100%'})`, + left: `${Math.max(0, progress - 10)}%` + }} + /> + </div> + ) +} diff --git a/frontend/components/scan/engine/engine-columns.tsx b/frontend/components/scan/engine/engine-columns.tsx new file mode 100644 index 00000000..6a50e9b9 --- /dev/null +++ b/frontend/components/scan/engine/engine-columns.tsx @@ -0,0 +1,324 @@ +"use client" + +import React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + MoreHorizontal, + Trash2, + ChevronsUpDown, + ChevronUp, + ChevronDown, + Check, + Edit, + X as XIcon, +} from "lucide-react" +import { + IconTarget, + IconBolt, + IconSettings, +} from "@tabler/icons-react" +import * as yaml from "js-yaml" +import type { ScanEngine } from "@/types/engine.types" + +/** + * 解析引擎的 YAML 配置并检测功能是否启用 + * + * 判断逻辑: + * - 如果 YAML 中存在该配置项(即使是空对象 {}),则认为启用了该功能 + * - 空对象表示使用默认配置启用该功能 + */ +function parseEngineFeatures(engine: ScanEngine) { + // 如果引擎有 configuration 字段,解析 YAML + if (engine.configuration) { + try { + const config = yaml.load(engine.configuration) as any + return { + subdomain_discovery: !!config?.subdomain_discovery, + port_scan: !!config?.port_scan, + site_scan: !!config?.site_scan, + directory_scan: !!config?.directory_scan, + url_fetch: !!config?.url_fetch || !!config?.fetch_url, // 兼容 fetch_url + osint: !!config?.osint, + vulnerability_scan: !!config?.vulnerability_scan, + waf_detection: !!config?.waf_detection, + screenshot: !!config?.screenshot, + } + } catch (error) { + console.error("Failed to parse YAML configuration:", error) + } + } + + // 无配置时,所有功能默认为禁用 + return { + subdomain_discovery: false, + port_scan: false, + site_scan: false, + directory_scan: false, + url_fetch: false, + osint: false, + vulnerability_scan: false, + waf_detection: false, + screenshot: false, + } +} + +/** + * 功能支持状态组件 + */ +function FeatureStatus({ enabled }: { enabled?: boolean }) { + if (enabled) { + return ( + <div className="flex justify-center"> + <Check className="h-5 w-5 text-chart-4" /> + </div> + ) + } + return ( + <div className="flex justify-center"> + <XIcon className="h-5 w-5 text-destructive" /> + </div> + ) +} + +/** + * 数据表格列头组件 + */ +function DataTableColumnHeader({ + column, + title, +}: { + column: { + getCanSort: () => boolean + getIsSorted: () => false | "asc" | "desc" + toggleSorting: (desc?: boolean) => void + } + title: string +}) { + if (!column.getCanSort()) { + return <div className="-ml-3 font-medium">{title}</div> + } + + const isSorted = column.getIsSorted() + + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted" + > + {title} + {isSorted === "asc" ? ( + <ChevronUp /> + ) : isSorted === "desc" ? ( + <ChevronDown /> + ) : ( + <ChevronsUpDown /> + )} + </Button> + ) +} + +// 列创建函数的参数类型 +interface CreateColumnsProps { + formatDate: (dateString: string) => string + handleEdit: (engine: ScanEngine) => void + handleDelete: (engine: ScanEngine) => void +} + +/** + * 引擎行操作组件 + */ +function EngineRowActions({ + engine, + onEdit, + onDelete, +}: { + engine: ScanEngine + onEdit: () => void + onDelete: () => void +}) { + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" + > + <MoreHorizontal /> + <span className="sr-only">打开菜单</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem onClick={onEdit}> + <Edit /> + 编辑引擎 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={onDelete} + className="text-destructive focus:text-destructive" + > + <Trash2 /> + 删除 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) +} + +/** + * 创建引擎表格列定义 + */ +export const createEngineColumns = ({ + formatDate, + handleEdit, + handleDelete, +}: CreateColumnsProps): ColumnDef<ScanEngine>[] => [ + // 引擎名称列 - 可点击编辑 + { + accessorKey: "name", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="引擎名称" /> + ), + cell: ({ row }) => { + const name = row.getValue("name") as string + return ( + <Tooltip> + <TooltipTrigger asChild> + <button + onClick={() => handleEdit(row.original)} + className="max-w-[300px] truncate font-medium text-left hover:text-primary hover:underline underline-offset-2 cursor-pointer transition-colors" + > + {name} + </button> + </TooltipTrigger> + <TooltipContent>编辑引擎</TooltipContent> + </Tooltip> + ) + }, + }, + + // Subdomain Discovery + { + id: "subdomain_discovery", + header: "Subdomain Discovery", + cell: ({ row }) => { + const features = parseEngineFeatures(row.original) + return <FeatureStatus enabled={features.subdomain_discovery} /> + }, + enableSorting: false, + }, + + // Port Scan + { + id: "port_scan", + header: "Port Scan", + cell: ({ row }) => { + const features = parseEngineFeatures(row.original) + return <FeatureStatus enabled={features.port_scan} /> + }, + enableSorting: false, + }, + + // Site Scan (原 HTTP Crawl) + { + id: "site_scan", + header: "Site Scan", + cell: ({ row }) => { + const features = parseEngineFeatures(row.original) + return <FeatureStatus enabled={features.site_scan} /> + }, + enableSorting: false, + }, + + // Directory Scan + { + id: "directory_scan", + header: "Directory Scan", + cell: ({ row }) => { + const features = parseEngineFeatures(row.original) + return <FeatureStatus enabled={features.directory_scan} /> + }, + enableSorting: false, + }, + + // URL Fetch + { + id: "url_fetch", + header: "URL Fetch", + cell: ({ row }) => { + const features = parseEngineFeatures(row.original) + return <FeatureStatus enabled={features.url_fetch} /> + }, + enableSorting: false, + }, + + // OSINT + { + id: "osint", + header: "OSINT", + cell: ({ row }) => { + const features = parseEngineFeatures(row.original) + return <FeatureStatus enabled={features.osint} /> + }, + enableSorting: false, + }, + + // Vulnerability Scan + { + id: "vulnerability_scan", + header: "Vulnerability Scan", + cell: ({ row }) => { + const features = parseEngineFeatures(row.original) + return <FeatureStatus enabled={features.vulnerability_scan} /> + }, + enableSorting: false, + }, + + // WAF Detection + { + id: "waf_detection", + header: "WAF Detection", + cell: ({ row }) => { + const features = parseEngineFeatures(row.original) + return <FeatureStatus enabled={features.waf_detection} /> + }, + enableSorting: false, + }, + + // Screenshot + { + id: "screenshot", + header: "Screenshot", + cell: ({ row }) => { + const features = parseEngineFeatures(row.original) + return <FeatureStatus enabled={features.screenshot} /> + }, + enableSorting: false, + }, + + // 操作列 + { + id: "actions", + cell: ({ row }) => ( + <EngineRowActions + engine={row.original} + onEdit={() => handleEdit(row.original)} + onDelete={() => handleDelete(row.original)} + /> + ), + enableSorting: false, + enableHiding: false, + }, +] + diff --git a/frontend/components/scan/engine/engine-create-dialog.tsx b/frontend/components/scan/engine/engine-create-dialog.tsx new file mode 100644 index 00000000..dfe3775b --- /dev/null +++ b/frontend/components/scan/engine/engine-create-dialog.tsx @@ -0,0 +1,291 @@ +"use client" + +import React, { useState } from "react" +import { FileCode, Save, X, AlertCircle, CheckCircle2 } from "lucide-react" +import Editor from "@monaco-editor/react" +import * as yaml from "js-yaml" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" +import { toast } from "sonner" +import { useTheme } from "next-themes" + +interface EngineCreateDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onSave?: (name: string, yamlContent: string) => Promise<void> +} + +/** + * 新建引擎弹窗 + */ +export function EngineCreateDialog({ + open, + onOpenChange, + onSave, +}: EngineCreateDialogProps) { + const [engineName, setEngineName] = useState("") + const [yamlContent, setYamlContent] = useState("") + const [isSubmitting, setIsSubmitting] = useState(false) + const [isEditorReady, setIsEditorReady] = useState(false) + const [yamlError, setYamlError] = useState<{ message: string; line?: number; column?: number } | null>(null) + const { theme } = useTheme() + const editorRef = React.useRef<any>(null) + + // 默认 YAML 模板 + const defaultYaml = `# 请在此处编写引擎配置 YAML +# 可以参考 engine_config_example.yaml 文件中的配置示例`; + + // 当对话框打开时,重置表单 + React.useEffect(() => { + if (open) { + setEngineName("") + setYamlContent(defaultYaml) + setYamlError(null) + } + }, [open]) + + // 验证 YAML 语法 + const validateYaml = (content: string) => { + if (!content.trim()) { + setYamlError(null) + return true + } + + try { + yaml.load(content) + setYamlError(null) + return true + } catch (error) { + const yamlError = error as yaml.YAMLException + setYamlError({ + message: yamlError.message, + line: yamlError.mark?.line ? yamlError.mark.line + 1 : undefined, + column: yamlError.mark?.column ? yamlError.mark.column + 1 : undefined, + }) + return false + } + } + + // 处理编辑器内容变化 + const handleEditorChange = (value: string | undefined) => { + const newValue = value || "" + setYamlContent(newValue) + validateYaml(newValue) + } + + // 处理编辑器挂载 + const handleEditorDidMount = (editor: any) => { + editorRef.current = editor + setIsEditorReady(true) + } + + // 处理保存 + const handleSave = async () => { + // 验证引擎名称 + if (!engineName.trim()) { + toast.error("请输入引擎名称") + return + } + + // YAML 验证 + if (!yamlContent.trim()) { + toast.error("配置内容不能为空") + return + } + + if (!validateYaml(yamlContent)) { + toast.error("YAML 语法错误", { + description: yamlError?.message, + }) + return + } + + setIsSubmitting(true) + try { + if (onSave) { + await onSave(engineName, yamlContent) + } else { + // TODO: 调用实际的 API 创建引擎 + await new Promise(resolve => setTimeout(resolve, 1000)) + } + + toast.success("引擎创建成功", { + description: `引擎 "${engineName}" 已成功创建`, + }) + onOpenChange(false) + } catch (error) { + console.error("Failed to create engine:", error) + toast.error("引擎创建失败", { + description: error instanceof Error ? error.message : "未知错误", + }) + } finally { + setIsSubmitting(false) + } + } + + // 处理关闭 + const handleClose = () => { + if (engineName.trim() || yamlContent !== defaultYaml) { + const confirmed = window.confirm("您有未保存的更改,确定要关闭吗?") + if (!confirmed) return + } + onOpenChange(false) + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-6xl max-w-[calc(100%-2rem)] h-[90vh] flex flex-col p-0"> + <div className="flex flex-col h-full"> + <DialogHeader className="px-6 pt-6 pb-4 border-b"> + <DialogTitle className="flex items-center gap-2"> + <FileCode className="h-5 w-5" /> + 新建扫描引擎 + </DialogTitle> + <DialogDescription> + 创建新的扫描引擎配置,使用 Monaco Editor 编辑 YAML 配置文件,支持语法高亮、自动补全和错误提示。 + </DialogDescription> + </DialogHeader> + + <div className="flex-1 overflow-hidden px-6 py-4"> + <div className="flex flex-col h-full gap-4"> + {/* 引擎名称输入 */} + <div className="space-y-2"> + <Label htmlFor="engine-name"> + 引擎名称 <span className="text-destructive">*</span> + </Label> + <Input + id="engine-name" + value={engineName} + onChange={(e) => setEngineName(e.target.value)} + placeholder="请输入引擎名称,例如:全面扫描引擎" + disabled={isSubmitting} + className="max-w-md" + /> + </div> + + {/* YAML 编辑器 */} + <div className="flex flex-col flex-1 min-h-0 gap-2"> + <div className="flex items-center justify-between"> + <Label>YAML 配置</Label> + {/* 语法验证状态 */} + <div className="flex items-center gap-2"> + {yamlContent.trim() && ( + yamlError ? ( + <div className="flex items-center gap-1 text-xs text-destructive"> + <AlertCircle className="h-3.5 w-3.5" /> + <span>语法错误</span> + </div> + ) : ( + <div className="flex items-center gap-1 text-xs text-green-600 dark:text-green-400"> + <CheckCircle2 className="h-3.5 w-3.5" /> + <span>语法正确</span> + </div> + ) + )} + </div> + </div> + + {/* Monaco Editor */} + <div className={`border rounded-md overflow-hidden flex-1 ${yamlError ? 'border-destructive' : ''}`}> + <Editor + height="100%" + defaultLanguage="yaml" + value={yamlContent} + onChange={handleEditorChange} + onMount={handleEditorDidMount} + theme={theme === "dark" ? "vs-dark" : "light"} + options={{ + minimap: { enabled: false }, + fontSize: 13, + lineNumbers: "on", + wordWrap: "off", + scrollBeyondLastLine: false, + automaticLayout: true, + tabSize: 2, + insertSpaces: true, + formatOnPaste: true, + formatOnType: true, + folding: true, + foldingStrategy: "indentation", + showFoldingControls: "always", + bracketPairColorization: { + enabled: true, + }, + padding: { + top: 16, + bottom: 16, + }, + readOnly: isSubmitting, + }} + loading={ + <div className="flex items-center justify-center h-full"> + <div className="flex flex-col items-center gap-2"> + <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" /> + <p className="text-sm text-muted-foreground">加载编辑器...</p> + </div> + </div> + } + /> + </div> + + {/* 错误信息显示 */} + {yamlError && ( + <div className="flex items-start gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-md"> + <AlertCircle className="h-4 w-4 text-destructive mt-0.5 flex-shrink-0" /> + <div className="flex-1 text-xs"> + <p className="font-semibold text-destructive mb-1"> + {yamlError.line && yamlError.column + ? `第 ${yamlError.line} 行,第 ${yamlError.column} 列` + : "YAML 语法错误"} + </p> + <p className="text-muted-foreground">{yamlError.message}</p> + </div> + </div> + )} + </div> + </div> + </div> + + <DialogFooter className="px-6 py-4 border-t gap-2"> + <Button + type="button" + variant="outline" + onClick={handleClose} + disabled={isSubmitting} + > + <X className="h-4 w-4" /> + 取消 + </Button> + <Button + type="button" + onClick={handleSave} + disabled={isSubmitting || !engineName.trim() || !!yamlError || !isEditorReady} + > + {isSubmitting ? ( + <> + <div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" /> + 创建中... + </> + ) : ( + <> + <Save className="h-4 w-4" /> + 创建引擎 + </> + )} + </Button> + </DialogFooter> + </div> + </DialogContent> + </Dialog> + ) +} + diff --git a/frontend/components/scan/engine/engine-data-table.tsx b/frontend/components/scan/engine/engine-data-table.tsx new file mode 100644 index 00000000..8d00beee --- /dev/null +++ b/frontend/components/scan/engine/engine-data-table.tsx @@ -0,0 +1,327 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconPlus, +} from "@tabler/icons-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import type { ScanEngine } from "@/types/engine.types" + +// 组件属性类型定义 +interface EngineDataTableProps { + data: ScanEngine[] + columns: ColumnDef<ScanEngine>[] + onAddNew?: () => void + searchPlaceholder?: string + searchColumn?: string + addButtonText?: string +} + +/** + * 扫描引擎数据表格组件 + * 用于显示和管理扫描引擎数据 + * 包含搜索、分页、列显示控制等功能 + */ +export function EngineDataTable({ + data = [], + columns, + onAddNew, + searchPlaceholder = "搜索引擎名称...", + searchColumn = "name", + addButtonText = "新建引擎", +}: EngineDataTableProps) { + // 表格状态管理 + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [sorting, setSorting] = React.useState<SortingState>([]) + const [pagination, setPagination] = React.useState<{ + pageIndex: number + pageSize: number + }>({ + pageIndex: 0, + pageSize: 10, + }) + + // 过滤有效数据 + const validData = React.useMemo(() => { + return (data || []).filter( + (item) => item && typeof item.id !== "undefined" && item.id !== null + ) + }, [data]) + + // 创建表格实例 + const table = useReactTable({ + data: validData, + columns, + state: { + sorting, + columnVisibility, + columnFilters, + pagination, + }, + getRowId: (row) => row.id.toString(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + + return ( + <div className="w-full space-y-4"> + {/* 工具栏 */} + <div className="flex items-center justify-between"> + {/* 搜索框 */} + <div className="flex items-center space-x-2"> + <Input + placeholder={searchPlaceholder} + value={ + (table.getColumn(searchColumn)?.getFilterValue() as string) ?? "" + } + onChange={(event) => + table.getColumn(searchColumn)?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> + </div> + + {/* 右侧操作按钮 */} + <div className="flex items-center space-x-2"> + {/* 列显示控制 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + const columnNameMap: Record<string, string> = { + name: "引擎名称", + type: "类型", + description: "描述", + tools: "关联工具", + updated_at: "更新时间", + } + + return ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => + column.toggleVisibility(!!value) + } + > + {columnNameMap[column.id] || column.id} + </DropdownMenuCheckboxItem> + ) + })} + </DropdownMenuContent> + </DropdownMenu> + + {/* 添加新记录按钮 */} + {onAddNew ? ( + <Button onClick={onAddNew} size="sm"> + <IconPlus className="h-4 w-4 mr-1" /> + {addButtonText} + </Button> + ) : null} + </div> + </div> + + {/* 表格容器 */} + <div className="rounded-md border"> + <Table> + {/* 表头 */} + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id} colSpan={header.colSpan}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + + {/* 表体 */} + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + className="group" + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + 暂无数据 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {/* 分页控制 */} + <div className="flex items-center justify-end px-2"> + {/* 分页控制器 */} + <div className="flex items-center space-x-6 lg:space-x-8"> + {/* 每页显示数量选择 */} + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + Rows per page + </Label> + <Select + value={`${table.getState().pagination.pageSize}`} + onValueChange={(value) => { + table.setPageSize(Number(value)) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue + placeholder={table.getState().pagination.pageSize} + /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 页码信息 */} + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + </div> + + {/* 分页按钮 */} + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to first page</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to previous page</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to next page</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to last page</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + </div> + ) +} + diff --git a/frontend/components/scan/engine/engine-edit-dialog.tsx b/frontend/components/scan/engine/engine-edit-dialog.tsx new file mode 100644 index 00000000..06451d3f --- /dev/null +++ b/frontend/components/scan/engine/engine-edit-dialog.tsx @@ -0,0 +1,367 @@ +"use client" + +import React, { useState, useEffect, useRef } from "react" +import { FileCode, Save, X, AlertCircle, CheckCircle2, AlertTriangle } from "lucide-react" +import Editor from "@monaco-editor/react" +import * as yaml from "js-yaml" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { toast } from "sonner" +import { useTheme } from "next-themes" +import type { ScanEngine } from "@/types/engine.types" + +interface EngineEditDialogProps { + engine: ScanEngine | null + open: boolean + onOpenChange: (open: boolean) => void + onSave?: (engineId: number, yamlContent: string) => Promise<void> +} + +/** + * 引擎配置编辑弹窗 + * 使用 Monaco Editor 提供 VSCode 级别的编辑体验 + */ +export function EngineEditDialog({ + engine, + open, + onOpenChange, + onSave, +}: EngineEditDialogProps) { + const [yamlContent, setYamlContent] = useState("") + const [isSubmitting, setIsSubmitting] = useState(false) + const [hasChanges, setHasChanges] = useState(false) + const [isEditorReady, setIsEditorReady] = useState(false) + const [yamlError, setYamlError] = useState<{ message: string; line?: number; column?: number } | null>(null) + const { theme } = useTheme() + const editorRef = useRef<any>(null) + + // 生成示例 YAML 配置 + const generateSampleYaml = (engine: ScanEngine) => { + return `# 引擎名称: ${engine.name} + +# ==================== 子域名发现 ==================== +subdomain_discovery: + tools: + subfinder: + enabled: true + timeout: 600 # 10 分钟(必需) + + amass_passive: + enabled: true + timeout: 600 # 10 分钟(必需) + + amass_active: + enabled: true + timeout: 1800 # 30 分钟(必需) + + sublist3r: + enabled: true + timeout: 900 # 15 分钟(必需) + + oneforall: + enabled: true + timeout: 1200 # 20 分钟(必需) + + +# ==================== 端口扫描 ==================== +port_scan: + tools: + naabu_active: + enabled: true + timeout: auto # 自动计算 + threads: 5 + top-ports: 100 + rate: 10 + + naabu_passive: + enabled: true + timeout: auto + + +# ==================== 站点扫描 ==================== +site_scan: + tools: + httpx: + enabled: true + timeout: auto # 自动计算 + + +# ==================== 目录扫描 ==================== +directory_scan: + tools: + ffuf: + enabled: true + timeout: auto # 自动计算超时时间 + wordlist: ~/Desktop/dirsearch_dicc.txt # 词表文件路径(必需) + delay: 0.1-2.0 + threads: 10 + request_timeout: 10 + match_codes: 200,201,301,302,401,403 + + +# ==================== URL 获取 ==================== +url_fetch: + tools: + waymore: + enabled: true + timeout: auto + + katana: + enabled: true + timeout: auto + depth: 5 + threads: 10 + rate-limit: 30 + random-delay: 1 + retry: 2 + request-timeout: 12 + + uro: + enabled: true + timeout: auto + + httpx: + enabled: true + timeout: auto +` + } + + // 当引擎改变时,更新 YAML 内容 + useEffect(() => { + if (engine && open) { + // TODO: 从后端 API 获取实际的 YAML 配置 + // 如果引擎有配置则使用,否则使用示例配置 + const content = engine.configuration || generateSampleYaml(engine) + setYamlContent(content) + setHasChanges(false) + setYamlError(null) + } + }, [engine, open]) + + // 验证 YAML 语法 + const validateYaml = (content: string) => { + if (!content.trim()) { + setYamlError(null) + return true + } + + try { + yaml.load(content) + setYamlError(null) + return true + } catch (error) { + const yamlError = error as yaml.YAMLException + setYamlError({ + message: yamlError.message, + line: yamlError.mark?.line ? yamlError.mark.line + 1 : undefined, + column: yamlError.mark?.column ? yamlError.mark.column + 1 : undefined, + }) + return false + } + } + + // 处理编辑器内容变化 + const handleEditorChange = (value: string | undefined) => { + const newValue = value || "" + setYamlContent(newValue) + setHasChanges(true) + validateYaml(newValue) + } + + // 处理编辑器挂载 + const handleEditorDidMount = (editor: any) => { + editorRef.current = editor + setIsEditorReady(true) + } + + // 处理保存 + const handleSave = async () => { + if (!engine) return + + // YAML 验证 + if (!yamlContent.trim()) { + toast.error("配置内容不能为空") + return + } + + if (!validateYaml(yamlContent)) { + toast.error("YAML 语法错误", { + description: yamlError?.message, + }) + return + } + + setIsSubmitting(true) + try { + if (onSave) { + await onSave(engine.id, yamlContent) + } else { + // TODO: 调用实际的 API 保存 YAML 配置 + await new Promise(resolve => setTimeout(resolve, 1000)) + } + + toast.success("配置保存成功", { + description: `引擎 "${engine.name}" 的配置已更新`, + }) + setHasChanges(false) + onOpenChange(false) + } catch (error) { + console.error("Failed to save YAML config:", error) + toast.error("配置保存失败", { + description: error instanceof Error ? error.message : "未知错误", + }) + } finally { + setIsSubmitting(false) + } + } + + // 处理关闭 + const handleClose = () => { + if (hasChanges) { + const confirmed = window.confirm("您有未保存的更改,确定要关闭吗?") + if (!confirmed) return + } + onOpenChange(false) + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-6xl max-w-[calc(100%-2rem)] h-[90vh] flex flex-col p-0"> + <div className="flex flex-col h-full"> + <DialogHeader className="px-6 pt-6 pb-4 border-b"> + <DialogTitle className="flex items-center gap-2"> + <FileCode className="h-5 w-5" /> + 编辑引擎配置 - {engine?.name} + </DialogTitle> + <DialogDescription> + 使用 Monaco Editor 编辑引擎的 YAML 配置文件,支持语法高亮、自动补全和错误提示。 + </DialogDescription> + </DialogHeader> + + <div className="flex-1 overflow-hidden px-6 py-4"> + <div className="flex flex-col h-full gap-2"> + <div className="flex items-center justify-between"> + <Label>YAML 配置</Label> + {/* 语法验证状态 */} + <div className="flex items-center gap-2"> + {yamlContent.trim() && ( + yamlError ? ( + <div className="flex items-center gap-1 text-xs text-destructive"> + <AlertCircle className="h-3.5 w-3.5" /> + <span>语法错误</span> + </div> + ) : ( + <div className="flex items-center gap-1 text-xs text-green-600 dark:text-green-400"> + <CheckCircle2 className="h-3.5 w-3.5" /> + <span>语法正确</span> + </div> + ) + )} + </div> + </div> + + {/* Monaco Editor */} + <div className={`border rounded-md overflow-hidden h-full ${yamlError ? 'border-destructive' : ''}`}> + <Editor + height="100%" + defaultLanguage="yaml" + value={yamlContent} + onChange={handleEditorChange} + onMount={handleEditorDidMount} + theme={theme === "dark" ? "vs-dark" : "light"} + options={{ + minimap: { enabled: false }, + fontSize: 13, + lineNumbers: "on", + wordWrap: "off", + scrollBeyondLastLine: false, + automaticLayout: true, + tabSize: 2, + insertSpaces: true, + formatOnPaste: true, + formatOnType: true, + folding: true, + foldingStrategy: "indentation", + showFoldingControls: "always", + bracketPairColorization: { + enabled: true, + }, + padding: { + top: 16, + bottom: 16, + }, + readOnly: isSubmitting, + }} + loading={ + <div className="flex items-center justify-center h-full"> + <div className="flex flex-col items-center gap-2"> + <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" /> + <p className="text-sm text-muted-foreground">加载编辑器...</p> + </div> + </div> + } + /> + </div> + + {/* 错误信息显示 */} + {yamlError && ( + <div className="flex items-start gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-md"> + <AlertCircle className="h-4 w-4 text-destructive mt-0.5 flex-shrink-0" /> + <div className="flex-1 text-xs"> + <p className="font-semibold text-destructive mb-1"> + {yamlError.line && yamlError.column + ? `第 ${yamlError.line} 行,第 ${yamlError.column} 列` + : "YAML 语法错误"} + </p> + <p className="text-muted-foreground">{yamlError.message}</p> + </div> + </div> + )} + <p className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400"> + <AlertTriangle className="h-3.5 w-3.5" /> + 您有未保存的更改 + </p> + </div> + </div> + + <DialogFooter className="px-6 py-4 border-t gap-2"> + <Button + type="button" + variant="outline" + onClick={handleClose} + disabled={isSubmitting} + > + <X className="h-4 w-4" /> + 取消 + </Button> + <Button + type="button" + onClick={handleSave} + disabled={isSubmitting || !hasChanges || !!yamlError || !isEditorReady} + > + {isSubmitting ? ( + <> + <div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" /> + 保存中... + </> + ) : ( + <> + <Save className="h-4 w-4" /> + 保存配置 + </> + )} + </Button> + </DialogFooter> + </div> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/scan/engine/index.ts b/frontend/components/scan/engine/index.ts new file mode 100644 index 00000000..34aeacd3 --- /dev/null +++ b/frontend/components/scan/engine/index.ts @@ -0,0 +1,7 @@ +/** + * Scan Engine Components - 统一导出 + */ +export { EngineDataTable } from './engine-data-table' +export { createEngineColumns } from './engine-columns' +export { EngineEditDialog } from './engine-edit-dialog' +export { EngineCreateDialog } from './engine-create-dialog' diff --git a/frontend/components/scan/history/index.ts b/frontend/components/scan/history/index.ts new file mode 100644 index 00000000..a157f0f9 --- /dev/null +++ b/frontend/components/scan/history/index.ts @@ -0,0 +1,7 @@ +/** + * Scan History Components - 统一导出 + */ +export { ScanHistoryList } from './scan-history-list' +export { ScanHistoryDataTable } from './scan-history-data-table' +export { createScanHistoryColumns } from './scan-history-columns' + diff --git a/frontend/components/scan/history/scan-history-columns.tsx b/frontend/components/scan/history/scan-history-columns.tsx new file mode 100644 index 00000000..90083811 --- /dev/null +++ b/frontend/components/scan/history/scan-history-columns.tsx @@ -0,0 +1,710 @@ +"use client" + +import React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { Checkbox } from "@/components/ui/checkbox" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import type { ScanRecord, ScanStatus } from "@/types/scan.types" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + MoreHorizontal, + Eye, + Trash2, + ChevronsUpDown, + ChevronUp, + ChevronDown, + Copy, + Check, + CircleArrowRight, + StopCircle, +} from "lucide-react" +import { + IconClock, + IconCircleCheck, + IconCircleX, + IconLoader, + IconWorld, + IconBrowser, + IconServer, + IconLink, + IconBug, +} from "@tabler/icons-react" +import { toast } from "sonner" + +/** + * 可复制单元格组件 + */ +function CopyableCell({ + value, + maxWidth = "300px", + truncateLength = 40, + successMessage = "已复制", + className = "font-medium" +}: { + value: string + maxWidth?: string + truncateLength?: number + successMessage?: string + className?: string +}) { + const [copied, setCopied] = React.useState(false) + const isLong = value.length > truncateLength + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(value) + setCopied(true) + toast.success(successMessage) + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error('复制失败') + } + } + + return ( + <div className="group inline-flex items-center gap-1" style={{ maxWidth }}> + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <div className={`text-sm truncate cursor-default ${className}`}> + {value} + </div> + </TooltipTrigger> + <TooltipContent + side="top" + align="start" + sideOffset={5} + className={`text-xs ${isLong ? 'max-w-[500px] break-all' : 'whitespace-nowrap'}`} + > + {value} + </TooltipContent> + </Tooltip> + </TooltipProvider> + + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className={`h-6 w-6 flex-shrink-0 hover:bg-accent transition-opacity ${ + copied ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' + }`} + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3.5 w-3.5 text-chart-4" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">{copied ? '已复制!' : '点击复制'}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + ) +} + +/** + * 状态徽章组件 + * 使用 shadcn Badge 的标准 variant + * Running/Initiated 状态可点击查看进度详情 + */ +function StatusBadge({ + status, + onClick +}: { + status: ScanStatus + onClick?: () => void +}) { + const config: Record<ScanStatus, { + icon: React.ComponentType<{ className?: string }> + label: string + variant: "secondary" | "default" | "outline" | "destructive" + className?: string + }> = { + cancelled: { + icon: IconCircleX, + label: "Cancelled", + variant: "outline", + className: "bg-gray-500/15 text-gray-600 border-gray-500/30 hover:bg-gray-500/25 dark:text-gray-400 transition-colors", + }, + completed: { + icon: IconCircleCheck, + label: "Completed", + variant: "outline", + className: "bg-emerald-500/15 text-emerald-600 border-emerald-500/30 hover:bg-emerald-500/25 dark:text-emerald-400 transition-colors", + }, + failed: { + icon: IconCircleX, + label: "Failed", + variant: "outline", + className: "bg-red-500/15 text-red-600 border-red-500/30 hover:bg-red-500/25 dark:text-red-400 transition-colors", + }, + initiated: { + icon: IconClock, + label: "Initiated", + variant: "outline", + className: "bg-amber-500/15 text-amber-600 border-amber-500/30 hover:bg-amber-500/25 dark:text-amber-400 transition-colors", + }, + running: { + icon: IconLoader, + label: "Running", + variant: "outline", + className: "bg-blue-500/15 text-blue-600 border-blue-500/30 hover:bg-blue-500/25 dark:text-blue-400 transition-colors", + }, + } + + const { icon: Icon, label, variant, className } = config[status] + + const badge = ( + <Badge variant={variant} className={className}> + {(status === "running" || status === "initiated") ? ( + <span className="relative flex h-2 w-2"> + <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-current opacity-75" /> + <span className="relative inline-flex h-2 w-2 rounded-full bg-current" /> + </span> + ) : ( + <Icon className="h-3.5 w-3.5" /> + )} + {label} + {onClick && <span className="ml-0.5 text-xs opacity-60">›</span>} + </Badge> + ) + + if (onClick) { + return ( + <button + onClick={onClick} + className="cursor-pointer hover:scale-105 transition-transform" + title="点击查看进度详情" + > + {badge} + </button> + ) + } + + return badge +} + +// 列创建函数的参数类型 +interface CreateColumnsProps { + formatDate: (dateString: string) => string + navigate: (path: string) => void + handleDelete: (scan: ScanRecord) => void + handleStop: (scan: ScanRecord) => void + handleViewProgress?: (scan: ScanRecord) => void +} + +/** + * 扫描记录行操作组件 + */ +function ScanRowActions({ + scan, + onView, + onDelete, + onStop, +}: { + scan: ScanRecord + onView: () => void + onDelete: () => void + onStop: () => void +}) { + // 只有在运行中或初始化状态时才显示停止选项(cancelling 状态下不允许再次停止) + const canStop = scan.status === 'running' || scan.status === 'initiated' + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" + > + <MoreHorizontal /> + <span className="sr-only">打开菜单</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem onClick={onView}> + <Eye /> + View Results + </DropdownMenuItem> + {canStop && ( + <DropdownMenuItem + onClick={onStop} + className="text-chart-2 focus:text-chart-2" + > + <StopCircle /> + Stop Scan + </DropdownMenuItem> + )} + {canStop && <DropdownMenuSeparator />} + <DropdownMenuItem + onClick={onDelete} + className="text-destructive focus:text-destructive" + > + <Trash2 /> + delete + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) +} + +/** + * 数据表格列头组件 + */ +function DataTableColumnHeader({ + column, + title, +}: { + column: { getCanSort: () => boolean; getIsSorted: () => false | "asc" | "desc"; toggleSorting: (desc?: boolean) => void } + title: string +}) { + if (!column.getCanSort()) { + return <div className="-ml-3 font-medium">{title}</div> + } + + const isSorted = column.getIsSorted() + + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted" + > + {title} + {isSorted === "asc" ? ( + <ChevronUp /> + ) : isSorted === "desc" ? ( + <ChevronDown /> + ) : ( + <ChevronsUpDown /> + )} + </Button> + ) +} + +/** + * 创建扫描历史表格列定义 + */ +export const createScanHistoryColumns = ({ + formatDate, + navigate, + handleDelete, + handleStop, + handleViewProgress, +}: CreateColumnsProps): ColumnDef<ScanRecord>[] => [ + // 选择列 + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + + // Target 列 + { + accessorKey: "targetName", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Target" /> + ), + cell: ({ row }) => { + const targetName = row.getValue("targetName") as string + const targetId = row.original.target + + const maxLength = 30 + const isLong = targetName.length > maxLength + const displayText = isLong ? targetName.substring(0, maxLength) : targetName + + return ( + <div className="group inline-flex items-center gap-1 max-w-[250px]"> + <div className="flex items-center gap-1"> + {targetId ? ( + <Tooltip> + <TooltipTrigger asChild> + <button + onClick={() => navigate(`/target/${targetId}/details`)} + className="text-sm font-medium hover:text-primary hover:underline underline-offset-2 transition-colors cursor-pointer" + > + {displayText} + </button> + </TooltipTrigger> + <TooltipContent>目标详情</TooltipContent> + </Tooltip> + ) : ( + <span className="text-sm font-medium"> + {displayText} + </span> + )} + {isLong && ( + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整目标名称</h4> + <div className="text-sm break-all bg-muted p-2 rounded max-h-32 overflow-y-auto"> + {targetName} + </div> + </div> + </PopoverContent> + </Popover> + )} + </div> + {targetId && ( + <Button + variant="ghost" + size="icon" + className="h-6 w-6 flex-shrink-0 hover:bg-accent opacity-0 group-hover:opacity-100 transition-opacity" + onClick={(e) => { + e.stopPropagation() + navigate(`/target/${targetId}/details`) + }} + > + <CircleArrowRight className="h-3.5 w-3.5 text-muted-foreground" /> + </Button> + )} + </div> + ) + }, + }, + + // Summary 列 + { + accessorKey: "summary", + header: "Summary", + cell: ({ row }) => { + const summary = (row.getValue("summary") as { + subdomains: number + websites: number + endpoints: number + ips: number + vulnerabilities: { + total: number + critical: number + high: number + medium: number + low: number + } + }) || {} + + const subdomains = summary?.subdomains ?? 0 + const websites = summary?.websites ?? 0 + const endpoints = summary?.endpoints ?? 0 + const ips = summary?.ips ?? 0 + const vulns = summary?.vulnerabilities?.total ?? 0 + + const badges: React.ReactNode[] = [] + + if (subdomains > 0) { + badges.push( + <TooltipProvider delayDuration={300} key="subdomains"> + <Tooltip> + <TooltipTrigger asChild> + <Badge + variant="outline" + className="bg-blue-500/15 text-blue-600 border-blue-500/30 hover:bg-blue-500/25 dark:text-blue-400 transition-colors gap-1" + > + <IconWorld className="h-3 w-3" /> + {subdomains} + </Badge> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">Subdomains</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ) + } + + if (websites > 0) { + badges.push( + <TooltipProvider delayDuration={300} key="websites"> + <Tooltip> + <TooltipTrigger asChild> + <Badge + variant="outline" + className="bg-emerald-500/15 text-emerald-600 border-emerald-500/30 hover:bg-emerald-500/25 dark:text-emerald-400 transition-colors gap-1" + > + <IconBrowser className="h-3 w-3" /> + {websites} + </Badge> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">Websites</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ) + } + + if (ips > 0) { + badges.push( + <TooltipProvider delayDuration={300} key="ips"> + <Tooltip> + <TooltipTrigger asChild> + <Badge + variant="outline" + className="bg-orange-500/15 text-orange-600 border-orange-500/30 hover:bg-orange-500/25 dark:text-orange-400 transition-colors gap-1" + > + <IconServer className="h-3 w-3" /> + {ips} + </Badge> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">IP Addresses</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ) + } + + if (endpoints > 0) { + badges.push( + <TooltipProvider delayDuration={300} key="endpoints"> + <Tooltip> + <TooltipTrigger asChild> + <Badge + variant="outline" + className="bg-violet-500/15 text-violet-600 border-violet-500/30 hover:bg-violet-500/25 dark:text-violet-400 transition-colors gap-1" + > + <IconLink className="h-3 w-3" /> + {endpoints} + </Badge> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">Endpoints</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ) + } + + if (vulns > 0) { + badges.push( + <TooltipProvider delayDuration={300} key="vulnerabilities"> + <Tooltip> + <TooltipTrigger asChild> + <Badge + variant="outline" + className="gap-1 bg-red-500/15 text-red-600 border-red-500/30 hover:bg-red-500/25 dark:text-red-400 transition-colors" + > + <IconBug className="h-3 w-3" /> + {vulns} + </Badge> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs font-medium"> + {summary?.vulnerabilities?.critical ?? 0} Critical, {summary?.vulnerabilities?.high ?? 0} High, {summary?.vulnerabilities?.medium ?? 0} Medium Vulnerabilities + </p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ) + } + + return ( + <div className="flex items-center gap-1.5"> + {badges.length > 0 ? ( + badges + ) : ( + <Badge + variant="outline" + className="gap-0 bg-muted/70 text-muted-foreground/80 border-border/40 px-1.5 py-0.5 rounded-full justify-center" + > + <span className="text-[11px] font-medium leading-none">-</span> + <span className="sr-only">No summary</span> + </Badge> + )} + </div> + ) + }, + enableSorting: false, + }, + + // Engine Name 列 + { + accessorKey: "engineName", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Engine Name" /> + ), + cell: ({ row }) => { + const engineName = row.getValue("engineName") as string + return ( + <Badge variant="secondary"> + {engineName} + </Badge> + ) + }, + }, + + // Created At 列 + { + accessorKey: "createdAt", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Created At" /> + ), + cell: ({ row }) => { + const createdAt = row.getValue("createdAt") as string + return ( + <div className="text-sm text-muted-foreground"> + {formatDate(createdAt)} + </div> + ) + }, + }, + + // Status 列 + { + accessorKey: "status", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Status" /> + ), + cell: ({ row }) => { + const status = row.getValue("status") as ScanStatus + return ( + <StatusBadge + status={status} + onClick={handleViewProgress ? () => handleViewProgress(row.original) : undefined} + /> + ) + }, + }, + + // Progress 列 + { + accessorKey: "progress", + header: "Progress", + cell: ({ row }) => { + const progress = row.getValue("progress") as number + const status = row.original.status + const currentStage = row.original.currentStage + + // 如果状态是completed,显示100% + const displayProgress = status === "completed" ? 100 : progress + + return ( + <div className="flex items-center gap-2 min-w-[120px]"> + <div className="flex-1 h-2 bg-primary/10 rounded-full overflow-hidden border border-border"> + <div + className={`h-full transition-all ${ + status === "completed" ? "bg-emerald-500/80" : + status === "failed" ? "bg-red-500/80" : + status === "running" ? "bg-blue-500/80 progress-striped" : + status === "cancelled" ? "bg-gray-500/80" : + status === "initiated" ? "bg-amber-500/80 progress-striped" : + "bg-muted-foreground/80" + }`} + style={{ width: `${displayProgress}%` }} + /> + </div> + <span className="text-xs text-muted-foreground font-mono w-10"> + {displayProgress}% + </span> + </div> + ) + }, + enableSorting: false, + }, + + // 操作列 + { + id: "actions", + cell: ({ row }) => { + const scan = row.original + const canStop = scan.status === 'running' || scan.status === 'initiated' + + return ( + <div className="flex items-center gap-1"> + {/* View Results 按钮 - 直接显示 */} + <Button + variant="ghost" + size="sm" + className="h-8 px-2 text-xs" + onClick={() => navigate(`/scan/history/${scan.id}/`)} + > + <Eye className="h-3.5 w-3.5 mr-1" /> + 查看 + </Button> + + {/* 更多操作菜单 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" + > + <MoreHorizontal className="h-4 w-4" /> + <span className="sr-only">打开菜单</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {canStop && ( + <> + <DropdownMenuItem + onClick={() => handleStop(scan)} + className="text-chart-2 focus:text-chart-2" + > + <StopCircle /> + 停止扫描 + </DropdownMenuItem> + <DropdownMenuSeparator /> + </> + )} + <DropdownMenuItem + onClick={() => handleDelete(scan)} + className="text-destructive focus:text-destructive" + > + <Trash2 /> + 删除 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + </div> + ) + }, + enableSorting: false, + enableHiding: false, + }, +] diff --git a/frontend/components/scan/history/scan-history-data-table.tsx b/frontend/components/scan/history/scan-history-data-table.tsx new file mode 100644 index 00000000..19922c4f --- /dev/null +++ b/frontend/components/scan/history/scan-history-data-table.tsx @@ -0,0 +1,424 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconPlus, + IconTrash, + IconSearch, + IconLoader2, +} from "@tabler/icons-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import type { ScanRecord } from "@/types/scan.types" +import type { PaginationInfo } from "@/types/common.types" + +// 组件属性类型定义 +interface ScanHistoryDataTableProps { + data: ScanRecord[] + columns: ColumnDef<ScanRecord>[] + onAddNew?: () => void + onBulkDelete?: () => void + onSelectionChange?: (selectedRows: ScanRecord[]) => void + searchPlaceholder?: string + searchColumn?: string + searchValue?: string + onSearch?: (value: string) => void + isSearching?: boolean + addButtonText?: string + // 服务端分页支持 + pagination?: { pageIndex: number; pageSize: number } + setPagination?: React.Dispatch<React.SetStateAction<{ pageIndex: number; pageSize: number }>> + paginationInfo?: PaginationInfo + onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void + hideToolbar?: boolean + hidePagination?: boolean +} + +/** + * 扫描历史数据表格组件 + * 专门用于显示和管理扫描历史数据的表格 + * 包含搜索、分页、列显示控制等功能 + */ +export function ScanHistoryDataTable({ + data = [], + columns, + onAddNew, + onBulkDelete, + onSelectionChange, + searchPlaceholder = "搜索目标名称...", + searchColumn = "targetName", + searchValue, + onSearch, + isSearching = false, + addButtonText = "新建扫描", + pagination: externalPagination, + setPagination: setExternalPagination, + paginationInfo, + onPaginationChange, + hideToolbar = false, + hidePagination = false, +}: ScanHistoryDataTableProps) { + // 搜索本地状态 + const [localSearchValue, setLocalSearchValue] = React.useState(searchValue || "") + + // 同步外部搜索值 + React.useEffect(() => { + setLocalSearchValue(searchValue || "") + }, [searchValue]) + + const handleSearchSubmit = () => { + if (onSearch) { + onSearch(localSearchValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter") { + handleSearchSubmit() + } + } + // 表格状态管理 + const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({}) + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [sorting, setSorting] = React.useState<SortingState>([]) + + // 使用外部分页状态或内部默认状态 + const [internalPagination, setInternalPagination] = React.useState<{ pageIndex: number, pageSize: number }>({ + pageIndex: 0, + pageSize: 10, + }) + + const pagination = externalPagination || internalPagination + const setPagination = setExternalPagination || setInternalPagination + + // 过滤有效数据 + const validData = React.useMemo(() => { + return (data || []).filter(item => item && typeof item.id !== 'undefined' && item.id !== null) + }, [data]) + + // 创建表格实例 + const table = useReactTable({ + data: validData, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination, + }, + // 服务端分页配置 + pageCount: paginationInfo?.totalPages ?? -1, + manualPagination: !!paginationInfo, + getRowId: (row) => row.id.toString(), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const newPagination = typeof updater === 'function' ? updater(pagination) : updater + setPagination(newPagination) + if (onPaginationChange) { + onPaginationChange(newPagination) + } + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + // 监听选中行变化 + React.useEffect(() => { + if (onSelectionChange) { + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + onSelectionChange(selectedRows) + } + }, [rowSelection, onSelectionChange, table]) + + return ( + <div className="w-full"> + {!hideToolbar && ( + <> + <div className="flex items-center justify-between"> + <div className="flex items-center space-x-2"> + <Input + placeholder={searchPlaceholder} + value={localSearchValue} + onChange={(e) => setLocalSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="h-8 max-w-sm" + /> + <Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}> + {isSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + + <div className="flex items-center space-x-2"> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + const columnNameMap: Record<string, string> = { + domainName: "Domain Name", + summary: "Summary", + scanEngine: "Scan Engine Used", + lastScan: "Last Scan", + status: "Status", + progress: "Progress", + } + + return ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {columnNameMap[column.id] || column.id} + </DropdownMenuCheckboxItem> + ) + })} + </DropdownMenuContent> + </DropdownMenu> + + {onBulkDelete && ( + <Button + onClick={onBulkDelete} + size="sm" + variant="outline" + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + className={ + table.getFilteredSelectedRowModel().rows.length === 0 + ? "text-muted-foreground" + : "text-destructive hover:text-destructive hover:bg-destructive/10" + } + > + <IconTrash /> + Delete + </Button> + )} + + {onAddNew && ( + <Button onClick={onAddNew} size="sm"> + <IconPlus /> + {addButtonText} + </Button> + )} + </div> + </div> + <div + className="border-b mt-4" + style={{ borderColor: "var(--sidebar-border)" }} + /> + </> + )} + + {/* 表格容器 */} + <div className="overflow-x-auto"> + <Table className="w-full"> + {/* 表头 */} + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow + key={headerGroup.id} + className="border-b" + style={{ borderColor: "var(--sidebar-border)" }} + > + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id} colSpan={header.colSpan}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + + {/* 表体 */} + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + className="group border-b [&>td]:py-2 last:border-b-0" + style={{ borderColor: "var(--sidebar)" }} + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow + className="border-b" + style={{ borderColor: "var(--sidebar)" }} + > + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + No results + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {!hidePagination && ( + <div className="border-t border-border pt-4 flex items-center justify-between px-2"> + <div className="flex-1 text-sm text-muted-foreground"> + {table.getFilteredSelectedRowModel().rows.length} of{" "} + {paginationInfo ? paginationInfo.total : table.getFilteredRowModel().rows.length} row(s) selected + </div> + + <div className="flex items-center space-x-6 lg:space-x-8"> + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + Rows per page + </Label> + <Select + value={`${table.getState().pagination.pageSize}`} + onValueChange={(value) => { + table.setPageSize(Number(value)) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={table.getState().pagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + </div> + + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to first page</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to previous page</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to next page</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to last page</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + )} + </div> + ) +} diff --git a/frontend/components/scan/history/scan-history-list.tsx b/frontend/components/scan/history/scan-history-list.tsx new file mode 100644 index 00000000..e680f38c --- /dev/null +++ b/frontend/components/scan/history/scan-history-list.tsx @@ -0,0 +1,362 @@ +"use client" + +import React, { useState, useMemo } from "react" +import { useRouter } from "next/navigation" +import { ScanHistoryDataTable } from "./scan-history-data-table" +import { createScanHistoryColumns } from "./scan-history-columns" +import type { ScanRecord } from "@/types/scan.types" +import type { ColumnDef } from "@tanstack/react-table" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { toast } from "sonner" +import { useScans } from "@/hooks/use-scans" +import { deleteScan, bulkDeleteScans, stopScan, getScan } from "@/services/scan.service" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { ScanProgressDialog, buildScanProgressData, type ScanProgressData } from "@/components/scan/scan-progress-dialog" + +/** + * 扫描历史列表组件 + * 用于显示和管理扫描历史记录 + */ +export function ScanHistoryList() { + const queryClient = useQueryClient() + const [selectedScans, setSelectedScans] = useState<ScanRecord[]>([]) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [scanToDelete, setScanToDelete] = useState<ScanRecord | null>(null) + const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false) + const [stopDialogOpen, setStopDialogOpen] = useState(false) + const [scanToStop, setScanToStop] = useState<ScanRecord | null>(null) + + // 进度弹窗状态 + const [progressDialogOpen, setProgressDialogOpen] = useState(false) + const [progressData, setProgressData] = useState<ScanProgressData | null>(null) + + // 分页状态 + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + + // 搜索状态 + const [searchQuery, setSearchQuery] = useState("") + const [isSearching, setIsSearching] = useState(false) + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, pageIndex: 0 })) + } + + // 获取扫描列表数据 + const { data, isLoading, isFetching, error } = useScans({ + page: pagination.pageIndex + 1, // API 页码从 1 开始 + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }) + + // 当请求完成时重置搜索状态 + React.useEffect(() => { + if (!isFetching && isSearching) { + setIsSearching(false) + } + }, [isFetching, isSearching]) + + // 扫描列表数据 + const scans = data?.results || [] + + // 删除单个扫描的 mutation + const deleteMutation = useMutation({ + mutationFn: deleteScan, + onSuccess: () => { + // 刷新列表数据 + queryClient.invalidateQueries({ queryKey: ['scans'] }) + }, + }) + + // 批量删除的 mutation + const bulkDeleteMutation = useMutation({ + mutationFn: bulkDeleteScans, + onSuccess: () => { + // 刷新列表数据 + queryClient.invalidateQueries({ queryKey: ['scans'] }) + // 清空选中项 + setSelectedScans([]) + }, + }) + + // 停止扫描的 mutation + const stopMutation = useMutation({ + mutationFn: stopScan, + onSuccess: () => { + // 刷新列表数据 + queryClient.invalidateQueries({ queryKey: ['scans'] }) + }, + }) + + // 辅助函数 - 格式化日期 + const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + } + + // 导航函数 + const router = useRouter() + const navigate = (path: string) => { + router.push(path) + } + + // 处理删除扫描记录 + const handleDeleteScan = (scan: ScanRecord) => { + setScanToDelete(scan) + setDeleteDialogOpen(true) + } + + // 确认删除扫描记录 + const confirmDelete = async () => { + if (!scanToDelete) return + + setDeleteDialogOpen(false) + + try { + await deleteMutation.mutateAsync(scanToDelete.id) + toast.success(`已删除扫描记录: ${scanToDelete.targetName}`) + } catch (error) { + toast.error("删除失败,请重试") + console.error('删除失败:', error) + } finally { + setScanToDelete(null) + } + } + + // 处理批量删除 + const handleBulkDelete = () => { + if (selectedScans.length === 0) { + return + } + setBulkDeleteDialogOpen(true) + } + + // 处理停止扫描 + const handleStopScan = (scan: ScanRecord) => { + setScanToStop(scan) + setStopDialogOpen(true) + } + + // 确认停止扫描 + const confirmStop = async () => { + if (!scanToStop) return + + setStopDialogOpen(false) + + try { + await stopMutation.mutateAsync(scanToStop.id) + toast.success(`已停止扫描任务: ${scanToStop.targetName}`) + } catch (error) { + toast.error("停止失败,请重试") + console.error('停止扫描失败:', error) + } finally { + setScanToStop(null) + } + } + + // 查看扫描进度(获取单个扫描的最新数据) + const handleViewProgress = async (scan: ScanRecord) => { + try { + // 获取单个扫描的最新数据,而不是刷新整个列表 + const freshScan = await getScan(scan.id) + const progressData = buildScanProgressData(freshScan) + setProgressData(progressData) + setProgressDialogOpen(true) + } catch (error) { + // 如果获取失败,使用当前数据 + const progressData = buildScanProgressData(scan) + setProgressData(progressData) + setProgressDialogOpen(true) + } + } + + // 确认批量删除 + const confirmBulkDelete = async () => { + if (selectedScans.length === 0) return + + const deletedIds = selectedScans.map(scan => scan.id) + + setBulkDeleteDialogOpen(false) + + try { + const result = await bulkDeleteMutation.mutateAsync(deletedIds) + toast.success(result.message || `已删除 ${result.deletedCount} 个扫描记录`) + } catch (error) { + toast.error("批量删除失败,请重试") + console.error('批量删除失败:', error) + } + } + + + // 处理分页变化 + const handlePaginationChange = (newPagination: { pageIndex: number; pageSize: number }) => { + setPagination(newPagination) + } + + // 创建列定义 + const scanColumns = useMemo( + () => + createScanHistoryColumns({ + formatDate, + navigate, + handleDelete: handleDeleteScan, + handleStop: handleStopScan, + handleViewProgress, + }), + [navigate] + ) + + // 错误处理 + if (error) { + return ( + <div className="flex flex-col items-center justify-center py-12"> + <p className="text-destructive mb-4">加载扫描历史失败</p> + <button + onClick={() => queryClient.invalidateQueries({ queryKey: ['scans'] })} + className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" + > + 重试 + </button> + </div> + ) + } + + // 加载状态 + if (isLoading) { + return ( + <DataTableSkeleton + toolbarButtonCount={2} + rows={6} + columns={6} + withPadding={false} + /> + ) + } + + return ( + <> + <ScanHistoryDataTable + data={scans} + columns={scanColumns as ColumnDef<ScanRecord>[]} + onBulkDelete={handleBulkDelete} + onSelectionChange={setSelectedScans} + searchPlaceholder="搜索目标名称..." + searchColumn="targetName" + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + pagination={pagination} + setPagination={setPagination} + paginationInfo={{ + total: data?.total || 0, + page: data?.page || 1, + pageSize: data?.pageSize || 10, + totalPages: data?.totalPages || 1, + }} + onPaginationChange={handlePaginationChange} + /> + + {/* 删除确认对话框 */} + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除</AlertDialogTitle> + <AlertDialogDescription> + 此操作无法撤销。这将永久删除扫描记录 "{scanToDelete?.targetName}" 及其相关数据。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + 删除 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + + {/* 批量删除确认对话框 */} + <AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认批量删除</AlertDialogTitle> + <AlertDialogDescription> + 此操作无法撤销。这将永久删除以下 {selectedScans.length} 个扫描记录及其相关数据。 + </AlertDialogDescription> + </AlertDialogHeader> + {/* 扫描记录列表容器 */} + <div className="mt-2 p-2 bg-muted rounded-md max-h-96 overflow-y-auto"> + <ul className="text-sm space-y-1"> + {selectedScans.map((scan) => ( + <li key={scan.id} className="flex items-center justify-between"> + <span className="font-medium">{scan.targetName}</span> + <span className="text-muted-foreground text-xs">{scan.engineName}</span> + </li> + ))} + </ul> + </div> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmBulkDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + 删除 {selectedScans.length} 个记录 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + + {/* 停止扫描确认对话框 */} + <AlertDialog open={stopDialogOpen} onOpenChange={setStopDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认停止扫描</AlertDialogTitle> + <AlertDialogDescription> + 确定要停止扫描任务 "{scanToStop?.targetName}" 吗?扫描将会中止,已收集的数据将会保留。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmStop} + className="bg-chart-2 text-white hover:bg-chart-2/90" + > + 停止扫描 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + + {/* 扫描进度弹窗 */} + <ScanProgressDialog + open={progressDialogOpen} + onOpenChange={setProgressDialogOpen} + data={progressData} + /> + </> + ) +} diff --git a/frontend/components/scan/history/scan-history-stat-cards.tsx b/frontend/components/scan/history/scan-history-stat-cards.tsx new file mode 100644 index 00000000..e0762e3f --- /dev/null +++ b/frontend/components/scan/history/scan-history-stat-cards.tsx @@ -0,0 +1,87 @@ +"use client" + +import { Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Skeleton } from "@/components/ui/skeleton" +import { + IconRadar, + IconPlayerPlay, + IconBug, + IconStack2 +} from "@tabler/icons-react" +import { useScanStatistics } from "@/hooks/use-scans" + +function StatCard({ + title, + value, + icon, + loading, + footer, +}: { + title: string + value: string | number + icon: React.ReactNode + loading?: boolean + footer: string +}) { + return ( + <Card className="@container/card"> + <CardHeader> + <CardDescription className="flex items-center gap-2"> + {icon} + {title} + </CardDescription> + {loading ? ( + <Skeleton className="h-8 w-24" /> + ) : ( + <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl"> + {value} + </CardTitle> + )} + <CardAction> + <Badge variant="outline">全部</Badge> + </CardAction> + </CardHeader> + <CardFooter className="flex-col items-start gap-1.5 text-sm"> + <div className="text-muted-foreground">{footer}</div> + </CardFooter> + </Card> + ) +} + +export function ScanHistoryStatCards() { + const { data, isLoading } = useScanStatistics() + + return ( + <div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs @xl/main:grid-cols-2 @5xl/main:grid-cols-4"> + <StatCard + title="总扫描数" + value={data?.total ?? 0} + icon={<IconRadar className="size-4" />} + loading={isLoading} + footer="所有扫描任务" + /> + <StatCard + title="进行中" + value={data?.running ?? 0} + icon={<IconPlayerPlay className="size-4" />} + loading={isLoading} + footer="正在执行的扫描" + /> + <StatCard + title="发现漏洞" + value={data?.totalVulns ?? 0} + icon={<IconBug className="size-4" />} + loading={isLoading} + footer="已完成扫描发现" + /> + <StatCard + title="发现资产" + value={data?.totalAssets ?? 0} + icon={<IconStack2 className="size-4" />} + loading={isLoading} + footer="子域名 + IP + 端点 + 网站" + /> + </div> + ) +} diff --git a/frontend/components/scan/initiate-scan-dialog.tsx b/frontend/components/scan/initiate-scan-dialog.tsx new file mode 100644 index 00000000..fe4c41d9 --- /dev/null +++ b/frontend/components/scan/initiate-scan-dialog.tsx @@ -0,0 +1,327 @@ +"use client" + +import React, { useState } from "react" +import { + Play, + ChevronDown, + ChevronUp, +} from "lucide-react" + +// 导入 UI 组件 +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { LoadingSpinner } from "@/components/loading-spinner" +import { cn } from "@/lib/utils" +import { CAPABILITY_CONFIG, getEngineIcon, parseEngineCapabilities } from "@/lib/engine-config" + +// 导入类型定义 +import type { Organization } from "@/types/organization.types" +import type { ScanEngine } from "@/types/engine.types" + +// 导入扫描服务和Toast +import { initiateScan } from "@/services/scan.service" +import { toast } from "sonner" + +// 导入引擎 hooks +import { useEngines } from "@/hooks/use-engines" + +// 组件属性类型定义 +interface InitiateScanDialogProps { + organization?: Organization | null // 选中的组织(可选,用于显示信息) + organizationId?: number // 组织ID(用于发起扫描) + targetId?: number // 目标ID(用于发起扫描,与organizationId二选一) + targetName?: string // 目标名称(可选,如果提供则显示为目标扫描) + open: boolean // 对话框开关状态 + onOpenChange: (open: boolean) => void // 对话框开关回调 + onSuccess?: () => void // 扫描发起成功的回调 +} + +/** + * 发起扫描对话框组件 + * + * 功能特性: + * 1. 选择扫描引擎 + * 2. 展示引擎详细信息 + * 3. 发起扫描操作 + */ +export function InitiateScanDialog({ + organization, + organizationId, + targetId, + targetName, + open, + onOpenChange, + onSuccess, +}: InitiateScanDialogProps) { + const [selectedEngineId, setSelectedEngineId] = useState<string>("") + const [isSubmitting, setIsSubmitting] = useState(false) + const [expandedEngineId, setExpandedEngineId] = useState<string | null>(null) + + // 从后端获取引擎列表 + const { data: engines, isLoading, error } = useEngines() + + // 切换展开/收起 + const toggleExpand = (engineId: string) => { + setExpandedEngineId( + expandedEngineId === engineId ? null : engineId + ) + } + + // 处理发起扫描 + const handleInitiate = async () => { + if (!selectedEngineId) return + + // 验证必须有 organizationId 或 targetId + if (!organizationId && !targetId) { + toast.error("参数错误", { + description: "必须提供组织ID或目标ID", + }) + return + } + + setIsSubmitting(true) + + try { + // 调用 API 发起扫描 + const response = await initiateScan({ + organizationId, + targetId, + engineId: Number(selectedEngineId), + }) + + // 显示成功消息 + toast.success("扫描已发起", { + description: response.message || `成功创建 ${response.count} 个扫描任务`, + }) + + // 调用成功回调 + if (onSuccess) { + onSuccess() + } + + // 关闭对话框 + onOpenChange(false) + + // 重置选择 + setSelectedEngineId("") + } catch (error) { + console.error("Failed to initiate scan:", error) + toast.error("发起扫描失败", { + description: error instanceof Error ? error.message : "未知错误", + }) + } finally { + setIsSubmitting(false) + } + } + + // 处理对话框关闭 + const handleOpenChange = (newOpen: boolean) => { + if (!isSubmitting) { + onOpenChange(newOpen) + if (!newOpen) { + // 关闭时重置所有状态 + setSelectedEngineId("") + setExpandedEngineId(null) + } + } + } + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + <DialogContent className="sm:max-w-[650px]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Play className="h-5 w-5" /> + 发起扫描 + </DialogTitle> + <DialogDescription> + {targetName ? ( + <>为目标 <span className="font-semibold text-foreground">{targetName}</span> 选择扫描引擎并开始安全扫描</> + ) : ( + <>为组织 <span className="font-semibold text-foreground">{organization?.name}</span> 选择扫描引擎并开始安全扫描</> + )} + </DialogDescription> + </DialogHeader> + + <div className="py-4"> + {/* 引擎列表容器 - 固定最大高度,预留滚动条空间 */} + <div className="max-h-[500px] overflow-y-auto" style={{ scrollbarGutter: 'stable' }}> + {isLoading ? ( + <div className="flex items-center justify-center py-8"> + <LoadingSpinner /> + <span className="ml-2 text-sm text-muted-foreground"> + 加载引擎中... + </span> + </div> + ) : error ? ( + <div className="py-8 text-center"> + <p className="text-sm text-destructive mb-2">加载引擎失败</p> + <p className="text-xs text-muted-foreground"> + {error instanceof Error ? error.message : '未知错误'} + </p> + </div> + ) : !engines || engines.length === 0 ? ( + <div className="py-8 text-center text-sm text-muted-foreground"> + 暂无可用引擎 + </div> + ) : ( + <RadioGroup + value={selectedEngineId} + onValueChange={(value) => { + setSelectedEngineId(value) + // 选中时自动展开该引擎详情 + setExpandedEngineId(value) + }} + disabled={isSubmitting} + className="space-y-2" + > + {engines.map((engine) => { + const capabilities = parseEngineCapabilities(engine.configuration || '') + + return ( + <Collapsible + key={engine.id} + open={expandedEngineId === engine.id.toString()} + onOpenChange={() => toggleExpand(engine.id.toString())} + > + <div + className={cn( + "rounded-lg border transition-all", + selectedEngineId === engine.id.toString() + ? "border-primary bg-primary/5 ring-1 ring-primary/20" + : "border-border hover:border-muted-foreground/50 hover:bg-muted/30" + )} + > + {/* 引擎主信息 */} + <div className="flex items-center gap-3 p-4"> + {/* Radio 按钮 */} + <RadioGroupItem + value={engine.id.toString()} + id={`engine-${engine.id}`} + className="mt-0.5" + /> + + {/* 引擎图标 - 根据能力动态显示 */} + {(() => { + const primaryCap = capabilities[0] + const EngineIcon = getEngineIcon(capabilities) + const iconConfig = primaryCap ? CAPABILITY_CONFIG[primaryCap] : null + return ( + <div className={cn( + "flex h-9 w-9 items-center justify-center rounded-lg", + iconConfig?.color || "bg-muted text-muted-foreground" + )}> + <EngineIcon className="h-4 w-4" /> + </div> + ) + })()} + + {/* 引擎名称 */} + <label + htmlFor={`engine-${engine.id}`} + className="flex-1 cursor-pointer" + > + <div className="flex items-center gap-2"> + <span className="font-medium">{engine.name}</span> + </div> + {/* 能力数量预览 */} + <p className="text-xs text-muted-foreground mt-0.5"> + {capabilities.length > 0 + ? `${capabilities.length} 项扫描能力` + : "点击展开查看详情"} + </p> + </label> + + {/* 展开按钮 */} + <CollapsibleTrigger asChild> + <Button + variant="ghost" + size="sm" + className="h-8 w-8 p-0" + > + {expandedEngineId === engine.id.toString() ? ( + <ChevronUp className="h-4 w-4" /> + ) : ( + <ChevronDown className="h-4 w-4" /> + )} + </Button> + </CollapsibleTrigger> + </div> + + {/* 可展开的详情内容 */} + <CollapsibleContent> + <div className="border-t px-4 py-3 space-y-3"> + {/* 能力标签 */} + {capabilities.length > 0 ? ( + <div className="flex flex-wrap gap-2"> + {capabilities.map((capKey) => { + const config = CAPABILITY_CONFIG[capKey] + return ( + <Badge + key={capKey} + variant="outline" + className={cn("text-xs font-normal", config?.color)} + > + {config?.label || capKey} + </Badge> + ) + })} + </div> + ) : ( + <p className="text-sm text-muted-foreground"> + 暂无能力信息 + </p> + )} + </div> + </CollapsibleContent> + </div> + </Collapsible> + ) + })} + </RadioGroup> + )} + </div> + </div> + + {/* 底部按钮 */} + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={isSubmitting} + > + 取消 + </Button> + <Button + type="button" + onClick={handleInitiate} + disabled={!selectedEngineId || isSubmitting} + > + {isSubmitting ? ( + <> + <LoadingSpinner /> + 发起扫描中... + </> + ) : ( + <> + <Play /> + 开始扫描 + </> + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/scan/quick-scan-dialog.tsx b/frontend/components/scan/quick-scan-dialog.tsx new file mode 100644 index 00000000..a4e6b7dd --- /dev/null +++ b/frontend/components/scan/quick-scan-dialog.tsx @@ -0,0 +1,490 @@ +"use client" + +import * as React from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { Badge } from "@/components/ui/badge" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { cn } from "@/lib/utils" +import { toast } from "sonner" +import { + Zap, Target, Settings, Check, ChevronRight, ChevronLeft, Loader2, ChevronDown, ChevronUp +} from "lucide-react" +import { getEngines } from "@/services/engine.service" +import { quickScan } from "@/services/scan.service" +import { CAPABILITY_CONFIG, getEngineIcon, parseEngineCapabilities } from "@/lib/engine-config" +import type { ScanEngine } from "@/types/engine.types" + +// 步骤定义 +const STEPS = [ + { id: 1, title: "输入目标", icon: Target }, + { id: 2, title: "选择引擎", icon: Settings }, + { id: 3, title: "确认", icon: Check }, +] as const + +interface QuickScanDialogProps { + trigger?: React.ReactNode +} + +export function QuickScanDialog({ trigger }: QuickScanDialogProps) { + const [open, setOpen] = React.useState(false) + const [step, setStep] = React.useState(1) + const [isLoading, setIsLoading] = React.useState(false) + const [isSubmitting, setIsSubmitting] = React.useState(false) + + // 表单数据 + const [targetInput, setTargetInput] = React.useState("") + const [selectedEngineId, setSelectedEngineId] = React.useState<string>("") + const [expandedEngineId, setExpandedEngineId] = React.useState<string | null>(null) + const [engines, setEngines] = React.useState<ScanEngine[]>([]) + + // 行号列和输入框的 ref(用于同步滚动) + const lineNumbersRef = React.useRef<HTMLDivElement | null>(null) + const textareaRef = React.useRef<HTMLTextAreaElement | null>(null) + + // 同步输入框和行号列的滚动 + const handleTextareaScroll = (e: React.UIEvent<HTMLTextAreaElement>) => { + if (lineNumbersRef.current) { + lineNumbersRef.current.scrollTop = e.currentTarget.scrollTop + } + } + + // 解析目标列表(多行) + const parseTargets = (input: string): string[] => { + return input + .split(/[\n,;]+/) + .map(t => t.trim()) + .filter(t => t.length > 0) + } + + + // 加载引擎列表 + React.useEffect(() => { + if (open && step === 2 && engines.length === 0) { + setIsLoading(true) + getEngines() + .then((data) => { + setEngines(data) + }) + .catch(() => { + toast.error("获取引擎列表失败") + }) + .finally(() => { + setIsLoading(false) + }) + } + }, [open, step, engines.length]) + + // 重置表单 + const resetForm = () => { + setStep(1) + setTargetInput("") + setSelectedEngineId("") + setExpandedEngineId(null) + } + + // 关闭弹框 + const handleClose = (isOpen: boolean) => { + setOpen(isOpen) + if (!isOpen) { + resetForm() + } + } + + // 验证单个目标 + const validateSingleTarget = (target: string): boolean => { + if (!target.trim()) return false + const domainPattern = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/ + const ipPattern = /^(\d{1,3}\.){3}\d{1,3}$/ + const cidrPattern = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/ + return domainPattern.test(target) || ipPattern.test(target) || cidrPattern.test(target) + } + + // 验证所有目标 + const validateTargets = (): { valid: boolean; targets: string[]; invalid: string[] } => { + const targets = parseTargets(targetInput) + if (targets.length === 0) { + return { valid: false, targets: [], invalid: [] } + } + const invalid = targets.filter(t => !validateSingleTarget(t)) + return { valid: invalid.length === 0, targets, invalid } + } + + // 下一步 + const handleNext = () => { + if (step === 1) { + const { valid, targets, invalid } = validateTargets() + if (targets.length === 0) { + toast.error("请输入至少一个目标") + return + } + if (!valid) { + toast.error(`以下目标格式无效:${invalid.slice(0, 3).join(", ")}${invalid.length > 3 ? "..." : ""}`) + return + } + } + if (step === 2) { + if (!selectedEngineId) { + toast.error("请选择扫描引擎") + return + } + } + setStep(step + 1) + } + + // 上一步 + const handlePrev = () => { + setStep(step - 1) + } + + // 提交扫描 + const handleSubmit = async () => { + const targets = parseTargets(targetInput) + if (targets.length === 0) return + + setIsSubmitting(true) + try { + // 调用快速扫描接口,一次性提交所有目标 + const response = await quickScan({ + targets: targets.map(name => ({ name })), + engineId: Number(selectedEngineId), + }) + + const { targetStats, scans } = response + + if (scans.length > 0) { + toast.success(response.message || `已创建 ${scans.length} 个扫描任务`, { + description: targetStats.failed > 0 + ? `${targetStats.created} 个目标成功,${targetStats.failed} 个失败` + : undefined + }) + handleClose(false) + } else { + toast.error("创建扫描任务失败", { + description: targetStats.failed > 0 + ? `${targetStats.failed} 个目标处理失败` + : undefined + }) + } + } catch (error: any) { + toast.error(error?.response?.data?.detail || error?.response?.data?.error || "创建扫描任务失败") + } finally { + setIsSubmitting(false) + } + } + + // 获取选中的引擎 + const selectedEngine = engines.find(e => String(e.id) === selectedEngineId) + const parsedTargets = parseTargets(targetInput) + + return ( + <Dialog open={open} onOpenChange={handleClose}> + <DialogTrigger asChild> + {trigger || ( + <Button variant="ghost" size="sm" className="gap-1.5 group"> + <Zap className="h-4 w-4 transition-transform group-hover:scale-125 group-hover:rotate-12" /> + 快速扫描 + </Button> + )} + </DialogTrigger> + <DialogContent className="sm:max-w-[550px]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Zap className="h-5 w-5 text-primary" /> + 快速扫描 + </DialogTitle> + </DialogHeader> + + {/* 步骤指示器 */} + <div className="flex items-center justify-between px-2 py-4"> + {STEPS.map((s, index) => ( + <React.Fragment key={s.id}> + <div className="flex flex-col items-center gap-1.5"> + <div + className={cn( + "flex h-10 w-10 items-center justify-center rounded-full border-2 transition-colors", + step === s.id && "border-primary bg-primary text-primary-foreground", + step > s.id && "border-primary bg-primary/10 text-primary", + step < s.id && "border-muted-foreground/30 text-muted-foreground" + )} + > + {step > s.id ? ( + <Check className="h-5 w-5" /> + ) : ( + <s.icon className="h-5 w-5" /> + )} + </div> + <span + className={cn( + "text-xs font-medium", + step >= s.id ? "text-foreground" : "text-muted-foreground" + )} + > + {s.title} + </span> + </div> + {index < STEPS.length - 1 && ( + <div + className={cn( + "h-0.5 flex-1 mx-2 rounded-full transition-colors", + step > s.id ? "bg-primary" : "bg-muted-foreground/30" + )} + /> + )} + </React.Fragment> + ))} + </div> + + {/* 步骤内容 */} + <div className="min-h-[200px] py-4"> + {/* 第一步:输入目标 */} + {step === 1 && ( + <div className="space-y-4"> + <div className="space-y-2"> + <Label htmlFor="target">目标列表</Label> + <div className="flex border rounded-md overflow-hidden h-[180px]"> + {/* 行号列 - 固定宽度 */} + <div className="flex-shrink-0 w-10 border-r bg-muted/50"> + <div + ref={lineNumbersRef} + className="py-2 px-1.5 text-right font-mono text-xs text-muted-foreground leading-[1.4] h-full overflow-y-auto scrollbar-hide" + > + {Array.from({ length: Math.max(targetInput.split('\n').length, 8) }, (_, i) => ( + <div key={i + 1} className="h-[20px]"> + {i + 1} + </div> + ))} + </div> + </div> + {/* 输入框区域 - 占据剩余空间 */} + <div className="flex-1 overflow-hidden"> + <Textarea + ref={textareaRef} + id="target" + value={targetInput} + onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setTargetInput(e.target.value)} + onScroll={handleTextareaScroll} + placeholder={`请输入目标,每行一个 +支持域名、IP、CIDR +例如: +example.com +192.168.1.1 +10.0.0.0/8`} + className="font-mono h-full overflow-y-auto resize-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 leading-[1.4] text-sm py-2" + style={{ lineHeight: '20px' }} + autoFocus + /> + </div> + </div> + <p className="text-xs text-muted-foreground"> + {parsedTargets.length > 0 ? ( + <span className="text-primary">{parsedTargets.length} 个目标</span> + ) : ( + "0 个目标" + )} + </p> + </div> + </div> + )} + + {/* 第二步:选择引擎 */} + {step === 2 && ( + <div className="space-y-2"> + <Label>扫描引擎</Label> + <div className="max-h-[300px] overflow-y-auto" style={{ scrollbarGutter: 'stable' }}> + {isLoading ? ( + <div className="flex items-center justify-center py-8"> + <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> + </div> + ) : engines.length === 0 ? ( + <div className="py-8 text-center text-sm text-muted-foreground"> + 暂无可用引擎 + </div> + ) : ( + <RadioGroup + value={selectedEngineId} + onValueChange={(value: string) => { + setSelectedEngineId(value) + setExpandedEngineId(value) + }} + disabled={isSubmitting} + className="space-y-2" + > + {engines.map((engine) => { + const capabilities = parseEngineCapabilities(engine.configuration || '') + + return ( + <Collapsible + key={engine.id} + open={expandedEngineId === engine.id.toString()} + onOpenChange={() => setExpandedEngineId( + expandedEngineId === engine.id.toString() ? null : engine.id.toString() + )} + > + <div + className={cn( + "rounded-lg border transition-all", + selectedEngineId === engine.id.toString() + ? "border-primary bg-primary/5 ring-1 ring-primary/20" + : "border-border hover:border-muted-foreground/50 hover:bg-muted/30" + )} + > + {/* 引擎主信息 */} + <div className="flex items-center gap-3 p-4"> + {/* Radio 按钮 */} + <RadioGroupItem + value={engine.id.toString()} + id={`engine-${engine.id}`} + className="mt-0.5" + /> + + {/* 引擎图标 - 根据能力动态显示 */} + {(() => { + const primaryCap = capabilities[0] + const EngineIcon = getEngineIcon(capabilities) + const iconConfig = primaryCap ? CAPABILITY_CONFIG[primaryCap] : null + return ( + <div className={cn( + "flex h-9 w-9 items-center justify-center rounded-lg", + iconConfig?.color || "bg-muted text-muted-foreground" + )}> + <EngineIcon className="h-4 w-4" /> + </div> + ) + })()} + + {/* 引擎名称 */} + <label + htmlFor={`engine-${engine.id}`} + className="flex-1 cursor-pointer" + > + <div className="flex items-center gap-2"> + <span className="font-medium">{engine.name}</span> + </div> + {/* 能力数量预览 */} + <p className="text-xs text-muted-foreground mt-0.5"> + {capabilities.length > 0 + ? `${capabilities.length} 项扫描能力` + : "点击展开查看详情"} + </p> + </label> + + {/* 展开按钮 */} + <CollapsibleTrigger asChild> + <Button variant="ghost" size="sm" className="h-8 w-8 p-0"> + {expandedEngineId === engine.id.toString() ? ( + <ChevronUp className="h-4 w-4" /> + ) : ( + <ChevronDown className="h-4 w-4" /> + )} + </Button> + </CollapsibleTrigger> + </div> + + {/* 可展开的详情内容 */} + <CollapsibleContent> + <div className="border-t px-4 py-3 space-y-3"> + {/* 能力标签 */} + {capabilities.length > 0 ? ( + <div className="flex flex-wrap gap-2"> + {capabilities.map((capKey) => { + const config = CAPABILITY_CONFIG[capKey] + return ( + <Badge + key={capKey} + variant="outline" + className={cn("text-xs font-normal", config?.color)} + > + {config?.label || capKey} + </Badge> + ) + })} + </div> + ) : ( + <p className="text-sm text-muted-foreground"> + 暂无能力信息 + </p> + )} + </div> + </CollapsibleContent> + </div> + </Collapsible> + ) + })} + </RadioGroup> + )} + </div> + </div> + )} + + {/* 第三步:确认 */} + {step === 3 && ( + <div className="space-y-4"> + <div className="rounded-lg border bg-muted/50 p-4 space-y-3"> + <div> + <span className="text-sm text-muted-foreground">目标</span> + <div className="mt-1 max-h-[100px] overflow-y-auto"> + {parsedTargets.map((target, idx) => ( + <div key={idx} className="font-mono text-sm">{target}</div> + ))} + </div> + <span className="text-xs text-muted-foreground">共 {parsedTargets.length} 个目标</span> + </div> + <div className="flex items-center justify-between pt-2 border-t"> + <span className="text-sm text-muted-foreground">引擎</span> + <Badge variant="secondary">{selectedEngine?.name}</Badge> + </div> + </div> + <p className="text-sm text-muted-foreground text-center"> + 确认以上信息无误后,点击开始扫描 + </p> + </div> + )} + </div> + + {/* 操作按钮 */} + <div className="flex justify-between pt-4 border-t"> + <Button + variant="outline" + onClick={handlePrev} + disabled={step === 1} + className={cn(step === 1 && "invisible")} + > + <ChevronLeft className="h-4 w-4 mr-1" /> + 上一步 + </Button> + + {step < 3 ? ( + <Button onClick={handleNext}> + 下一步 + <ChevronRight className="h-4 w-4 ml-1" /> + </Button> + ) : ( + <Button onClick={handleSubmit} disabled={isSubmitting}> + {isSubmitting ? ( + <> + <Loader2 className="h-4 w-4 mr-2 animate-spin" /> + 创建中... + </> + ) : ( + <> + <Zap className="h-4 w-4 mr-2" /> + 开始扫描 + </> + )} + </Button> + )} + </div> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/scan/scan-progress-dialog.tsx b/frontend/components/scan/scan-progress-dialog.tsx new file mode 100644 index 00000000..cc0969f3 --- /dev/null +++ b/frontend/components/scan/scan-progress-dialog.tsx @@ -0,0 +1,379 @@ +"use client" + +import * as React from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { + IconCircleCheck, + IconLoader, + IconClock, + IconCircleX, + IconPlayerStop, +} from "@tabler/icons-react" +import { cn } from "@/lib/utils" +import type { ScanStage, ScanRecord, StageProgress, StageStatus } from "@/types/scan.types" + +/** 阶段名称中文映射(支持驼峰和下划线两种格式) */ +const STAGE_LABELS: Record<string, string> = { + // 驼峰命名(后端返回格式) + subdomainDiscovery: "子域名发现", + portScan: "端口扫描", + siteScan: "站点扫描", + directoryScan: "目录扫描", + urlFetch: "URL 抓取", + vulnScan: "漏洞扫描", + // 下划线命名(engine_config 格式) + subdomain_discovery: "子域名发现", + port_scan: "端口扫描", + site_scan: "站点扫描", + directory_scan: "目录扫描", + url_fetch: "URL 抓取", + vuln_scan: "漏洞扫描", +} + +/** 获取阶段中文名称 */ +function getStageName(stage: string): string { + return STAGE_LABELS[stage] || stage +} + +/** + * 扫描阶段详情 + */ +interface StageDetail { + stage: ScanStage // 阶段名称(来自 engine_config key) + status: StageStatus + duration?: string // 耗时,如 "2m30s" + detail?: string // 额外信息,如 "发现 120 个子域名" + resultCount?: number // 结果数量 +} + +/** + * 扫描进度数据 + */ +export interface ScanProgressData { + id: number + targetName: string + engineName: string + status: string + progress: number + currentStage?: ScanStage + startedAt?: string + stages: StageDetail[] +} + +interface ScanProgressDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + data: ScanProgressData | null +} + +/** 扫描状态配置(与 scan-history 状态颜色一致) */ +const SCAN_STATUS_CONFIG: Record<string, { label: string; className: string }> = { + running: { label: "扫描中", className: "bg-blue-500/15 text-blue-600 border-blue-500/30 dark:text-blue-400" }, + cancelled: { label: "已取消", className: "bg-gray-500/15 text-gray-600 border-gray-500/30 dark:text-gray-400" }, + completed: { label: "已完成", className: "bg-emerald-500/15 text-emerald-600 border-emerald-500/30 dark:text-emerald-400" }, + failed: { label: "失败", className: "bg-red-500/15 text-red-600 border-red-500/30 dark:text-red-400" }, + initiated: { label: "等待中", className: "bg-amber-500/15 text-amber-600 border-amber-500/30 dark:text-amber-400" }, +} + +/** + * 闪烁点动效(与 scan-history 一致) + */ +function PulsingDot({ className }: { className?: string }) { + return ( + <span className={cn("relative flex h-3 w-3", className)}> + <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-current opacity-75" /> + <span className="relative inline-flex h-3 w-3 rounded-full bg-current" /> + </span> + ) +} + +/** + * 扫描状态图标(用于标题,与 scan-history 状态列动效一致) + */ +function ScanStatusIcon({ status }: { status: string }) { + switch (status) { + case "running": + return <PulsingDot className="text-blue-500" /> + case "completed": + return <IconCircleCheck className="h-5 w-5 text-emerald-500" /> + case "cancelled": + return <IconCircleX className="h-5 w-5 text-gray-500" /> + case "failed": + return <IconCircleX className="h-5 w-5 text-red-500" /> + case "initiated": + return <PulsingDot className="text-amber-500" /> + default: + return <PulsingDot className="text-muted-foreground" /> + } +} + +/** + * 扫描状态徽章 + */ +function ScanStatusBadge({ status }: { status: string }) { + const config = SCAN_STATUS_CONFIG[status] || { label: status, className: "bg-muted text-muted-foreground" } + return ( + <Badge variant="outline" className={config.className}> + {config.label} + </Badge> + ) +} + +/** + * 阶段状态图标 + */ +function StageStatusIcon({ status }: { status: StageStatus }) { + switch (status) { + case "completed": + return <IconCircleCheck className="h-5 w-5 text-emerald-500" /> + case "running": + return <PulsingDot className="text-blue-500" /> + case "failed": + return <IconCircleX className="h-5 w-5 text-destructive" /> + case "cancelled": + return <IconCircleX className="h-5 w-5 text-orange-500" /> + default: + return <IconClock className="h-5 w-5 text-muted-foreground" /> + } +} + +/** + * 单个阶段行 + */ +function StageRow({ stage }: { stage: StageDetail }) { + return ( + <div + className={cn( + "flex items-center justify-between py-3 px-4 rounded-lg transition-colors", + stage.status === "running" && "bg-blue-500/10 border border-blue-500/20", + stage.status === "completed" && "bg-muted/50", + stage.status === "failed" && "bg-destructive/10", + stage.status === "cancelled" && "bg-orange-500/10", + )} + > + <div className="flex items-center gap-3"> + <StageStatusIcon status={stage.status} /> + <div> + <span className="font-medium">{getStageName(stage.stage)}</span> + {stage.detail && ( + <p className="text-xs text-muted-foreground mt-0.5"> + {stage.detail} + </p> + )} + </div> + </div> + + <div className="flex items-center gap-3 text-right"> + {/* 状态/耗时 */} + {stage.status === "running" && ( + <Badge variant="outline" className="bg-blue-500/15 text-blue-600 border-blue-500/30 dark:text-blue-400"> + 进行中 + </Badge> + )} + {stage.status === "completed" && stage.duration && ( + <span className="text-sm text-muted-foreground font-mono"> + {stage.duration} + </span> + )} + {stage.status === "pending" && ( + <span className="text-sm text-muted-foreground">等待中</span> + )} + {stage.status === "failed" && ( + <Badge variant="outline" className="bg-destructive/20 text-destructive border-destructive/30"> + 失败 + </Badge> + )} + {stage.status === "cancelled" && ( + <Badge variant="outline" className="bg-orange-500/20 text-orange-500 border-orange-500/30"> + 已取消 + </Badge> + )} + </div> + </div> + ) +} + +/** + * 扫描进度弹窗 + */ +export function ScanProgressDialog({ + open, + onOpenChange, + data, +}: ScanProgressDialogProps) { + if (!data) return null + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[500px]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <ScanStatusIcon status={data.status} /> + 扫描进度 + </DialogTitle> + </DialogHeader> + + {/* 基本信息 */} + <div className="space-y-2"> + <div className="flex items-center justify-between text-sm"> + <span className="text-muted-foreground">目标</span> + <span className="font-medium">{data.targetName}</span> + </div> + <div className="flex items-center justify-between text-sm"> + <span className="text-muted-foreground">引擎</span> + <Badge variant="secondary">{data.engineName}</Badge> + </div> + {data.startedAt && ( + <div className="flex items-center justify-between text-sm"> + <span className="text-muted-foreground">开始时间</span> + <span className="font-mono text-xs">{formatDateTime(data.startedAt)}</span> + </div> + )} + <div className="flex items-center justify-between text-sm"> + <span className="text-muted-foreground">状态</span> + <ScanStatusBadge status={data.status} /> + </div> + </div> + + <Separator /> + + {/* 总进度 */} + <div className="space-y-2"> + <div className="flex items-center justify-between text-sm"> + <span className="font-medium">总进度</span> + <span className="font-mono text-muted-foreground">{data.progress}%</span> + </div> + + <div className="h-2 bg-primary/10 rounded-full overflow-hidden border border-border"> + <div + className={`h-full transition-all ${ + data.status === "completed" ? "bg-emerald-500/80" : + data.status === "failed" ? "bg-red-500/80" : + data.status === "running" ? "bg-blue-500/80 progress-striped" : + data.status === "cancelled" ? "bg-gray-500/80" : + data.status === "cancelling" ? "bg-orange-500/80 progress-striped" : + data.status === "initiated" ? "bg-amber-500/80 progress-striped" : + "bg-muted-foreground/80" + }`} + style={{ width: `${data.status === "completed" ? 100 : data.progress}%` }} + /> + </div> + </div> + + <Separator /> + + {/* 阶段列表 */} + <div className="space-y-2 max-h-[300px] overflow-y-auto"> + {data.stages.map((stage) => ( + <StageRow key={stage.stage} stage={stage} /> + ))} + </div> + </DialogContent> + </Dialog> + ) +} + +/** + * 格式化时长(秒 -> 可读字符串) + */ +function formatDuration(seconds?: number): string | undefined { + if (seconds === undefined || seconds === null) return undefined + if (seconds < 1) return "<1s" + if (seconds < 60) return `${Math.round(seconds)}s` + const minutes = Math.floor(seconds / 60) + const secs = Math.round(seconds % 60) + return secs > 0 ? `${minutes}m ${secs}s` : `${minutes}m` +} + +/** + * 格式化日期时间(ISO 字符串 -> 可读格式) + */ +function formatDateTime(isoString?: string): string { + if (!isoString) return "" + try { + const date = new Date(isoString) + return date.toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + } catch { + return isoString + } +} + +/** 从 summary 中获取阶段对应的结果数量 */ +function getStageResultCount(stageName: string, summary: ScanRecord["summary"]): number | undefined { + if (!summary) return undefined + switch (stageName) { + case "subdomain_discovery": + case "subdomainDiscovery": + return summary.subdomains + case "site_scan": + case "siteScan": + return summary.websites + case "directory_scan": + case "directoryScan": + return summary.directories + case "url_fetch": + case "urlFetch": + return summary.endpoints + case "vuln_scan": + case "vulnScan": + return summary.vulnerabilities?.total + default: + return undefined + } +} + +/** + * 从 ScanRecord 构建 ScanProgressData + * + * 阶段名称直接来自 engine_config 的 key,无需映射 + * 阶段顺序按 order 字段排序,与 Flow 执行顺序一致 + */ +export function buildScanProgressData(scan: ScanRecord): ScanProgressData { + const stages: StageDetail[] = [] + + if (scan.stageProgress) { + // 按 order 排序后遍历 + const sortedEntries = Object.entries(scan.stageProgress) + .sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0)) + + for (const [stageName, progress] of sortedEntries) { + const resultCount = progress.status === "completed" + ? getStageResultCount(stageName, scan.summary) + : undefined + + stages.push({ + stage: stageName, + status: progress.status, + duration: formatDuration(progress.duration), + detail: progress.detail || progress.error || progress.reason, + resultCount, + }) + } + } + + return { + id: scan.id, + targetName: scan.targetName, + engineName: scan.engineName, + status: scan.status, + progress: scan.progress, + currentStage: scan.currentStage, + startedAt: scan.createdAt, + stages, + } +} diff --git a/frontend/components/scan/scheduled/create-scheduled-scan-dialog.tsx b/frontend/components/scan/scheduled/create-scheduled-scan-dialog.tsx new file mode 100644 index 00000000..2a0b4140 --- /dev/null +++ b/frontend/components/scan/scheduled/create-scheduled-scan-dialog.tsx @@ -0,0 +1,741 @@ +"use client" + +import React from "react" +import { toast } from "sonner" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Badge } from "@/components/ui/badge" +import { Checkbox } from "@/components/ui/checkbox" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { Separator } from "@/components/ui/separator" +import { cn } from "@/lib/utils" +import { + IconX, + IconLoader2, + IconChevronRight, + IconChevronLeft, + IconCheck, + IconBuilding, + IconTarget, + IconClock, + IconInfoCircle, + IconSearch, +} from "@tabler/icons-react" +import { CronExpressionParser } from "cron-parser" +import cronstrue from "cronstrue/i18n" +import { useStep } from "@/hooks/use-step" +import { useCreateScheduledScan } from "@/hooks/use-scheduled-scans" +import { useTargets } from "@/hooks/use-targets" +import { useEngines } from "@/hooks/use-engines" +import { useOrganizations } from "@/hooks/use-organizations" +import type { CreateScheduledScanRequest } from "@/types/scheduled-scan.types" +import type { ScanEngine } from "@/types/engine.types" +import type { Target } from "@/types/target.types" +import type { Organization } from "@/types/organization.types" + +interface CreateScheduledScanDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onSuccess?: () => void + // 预设值(从组织/目标页面点击定时扫描时传入) + presetOrganizationId?: number + presetOrganizationName?: string + presetTargetId?: number + presetTargetName?: string +} + +// 常用 cron 表达式预设 +const CRON_PRESETS = [ + { label: "每小时", value: "0 * * * *" }, + { label: "每天凌晨2点", value: "0 2 * * *" }, + { label: "每天凌晨4点", value: "0 4 * * *" }, + { label: "每周一凌晨2点", value: "0 2 * * 1" }, + { label: "每月1号凌晨2点", value: "0 2 1 * *" }, +] + +// 完整步骤配置(从定时扫描页面进入) +const FULL_STEPS = [ + { id: 1, title: "基本信息", icon: IconInfoCircle }, + { id: 2, title: "扫描模式", icon: IconBuilding }, + { id: 3, title: "选择目标", icon: IconTarget }, + { id: 4, title: "调度设置", icon: IconClock }, +] + +// 简化步骤配置(从组织/目标页面进入,已预设目标) +const PRESET_STEPS = [ + { id: 1, title: "基本信息", icon: IconInfoCircle }, + { id: 2, title: "调度设置", icon: IconClock }, +] + +// 选择模式 +type SelectionMode = "organization" | "target" + +export function CreateScheduledScanDialog({ + open, + onOpenChange, + onSuccess, + presetOrganizationId, + presetOrganizationName, + presetTargetId, + presetTargetName, +}: CreateScheduledScanDialogProps) { + const { mutate: createScheduledScan, isPending } = useCreateScheduledScan() + const { data: enginesData } = useEngines() + + // 服务端搜索状态 + const [orgSearchInput, setOrgSearchInput] = React.useState("") + const [targetSearchInput, setTargetSearchInput] = React.useState("") + const [orgSearch, setOrgSearch] = React.useState("") + const [targetSearch, setTargetSearch] = React.useState("") + + // 搜索处理函数 + const handleOrgSearch = () => setOrgSearch(orgSearchInput) + const handleTargetSearch = () => setTargetSearch(targetSearchInput) + + // 服务端搜索请求 + const { data: organizationsData, isFetching: isOrgFetching } = useOrganizations({ + pageSize: 50, + search: orgSearch || undefined + }) + const { data: targetsData, isFetching: isTargetFetching } = useTargets({ + pageSize: 50, + search: targetSearch || undefined + }) + + // 判断是否有预设值(简化模式) + const hasPreset = !!(presetOrganizationId || presetTargetId) + const steps = hasPreset ? PRESET_STEPS : FULL_STEPS + const totalSteps = steps.length + + // 步骤控制 + const [currentStep, { goToNextStep, goToPrevStep, reset: resetStep }] = useStep(totalSteps) + + // 表单状态 + const [name, setName] = React.useState("") + const [engineId, setEngineId] = React.useState<number | null>(null) + const [selectionMode, setSelectionMode] = React.useState<SelectionMode>("organization") + const [selectedOrgId, setSelectedOrgId] = React.useState<number | null>(null) + const [selectedTargetId, setSelectedTargetId] = React.useState<number | null>(null) + const [cronExpression, setCronExpression] = React.useState("0 2 * * *") + + // 预设值处理:当打开对话框且有预设值时,自动填充 + React.useEffect(() => { + if (open) { + if (presetOrganizationId) { + // 预设组织模式 + setSelectionMode("organization") + setSelectedOrgId(presetOrganizationId) + setName(presetOrganizationName ? `${presetOrganizationName} - 定时扫描` : "") + } else if (presetTargetId) { + // 预设目标模式 + setSelectionMode("target") + setSelectedTargetId(presetTargetId) + setName(presetTargetName ? `${presetTargetName} - 定时扫描` : "") + } + } + }, [open, presetOrganizationId, presetOrganizationName, presetTargetId, presetTargetName]) + + // 数据 + const targets: Target[] = targetsData?.targets || [] + const engines: ScanEngine[] = enginesData || [] + const organizations: Organization[] = organizationsData?.organizations || [] + + // 重置表单 + const resetForm = () => { + setName("") + setEngineId(null) + setSelectionMode("organization") + setSelectedOrgId(null) + setSelectedTargetId(null) + setCronExpression("0 2 * * *") + resetStep() + } + + // 处理弹窗关闭 + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen) { + resetForm() + } + onOpenChange(isOpen) + } + + // 处理组织选择(单选) + const handleOrgSelect = (orgId: number) => { + setSelectedOrgId(selectedOrgId === orgId ? null : orgId) + } + + // 处理目标选择(单选) + const handleTargetSelect = (targetId: number) => { + setSelectedTargetId(selectedTargetId === targetId ? null : targetId) + } + + // 验证当前步骤 + const validateCurrentStep = (): boolean => { + // 简化模式(有预设值):只有2步 + if (hasPreset) { + switch (currentStep) { + case 1: // 基本信息 + if (!name.trim()) { + toast.error("请输入任务名称") + return false + } + if (!engineId) { + toast.error("请选择扫描引擎") + return false + } + return true + case 2: // 调度设置 + const parts = cronExpression.trim().split(/\s+/) + if (parts.length !== 5) { + toast.error("Cron 表达式格式错误,需要 5 个部分:分 时 日 月 周") + return false + } + return true + default: + return true + } + } + + // 完整模式:4步 + switch (currentStep) { + case 1: + if (!name.trim()) { + toast.error("请输入任务名称") + return false + } + if (!engineId) { + toast.error("请选择扫描引擎") + return false + } + return true + case 2: + // 只选择模式,无需验证 + return true + case 3: + if (selectionMode === "organization") { + if (!selectedOrgId) { + toast.error("请选择一个组织") + return false + } + } else { + if (!selectedTargetId) { + toast.error("请选择一个扫描目标") + return false + } + } + return true + case 4: + const cronParts = cronExpression.trim().split(/\s+/) + if (cronParts.length !== 5) { + toast.error("Cron 表达式格式错误,需要 5 个部分:分 时 日 月 周") + return false + } + return true + default: + return true + } + } + + // 下一步 + const handleNext = () => { + if (validateCurrentStep()) { + goToNextStep() + } + } + + // 提交表单 + const handleSubmit = () => { + if (!validateCurrentStep()) return + + // 根据扫描模式构建请求 + const request: CreateScheduledScanRequest = { + name: name.trim(), + engineId: engineId!, + cronExpression: cronExpression.trim(), + } + + if (selectionMode === "organization" && selectedOrgId) { + // 组织扫描模式 + request.organizationId = selectedOrgId + } else if (selectedTargetId) { + // 目标扫描模式 + request.targetId = selectedTargetId + } + + createScheduledScan(request, { + onSuccess: () => { + resetForm() + onOpenChange(false) + onSuccess?.() + }, + }) + } + + // 获取 cron 描述(使用 cronstrue) + const getCronDescription = (cron: string): string => { + try { + const parts = cron.trim().split(/\s+/) + if (parts.length !== 5) return "无效的表达式" + + return cronstrue.toString(cron, { locale: "zh_CN" }) + } catch { + return "无效的表达式" + } + } + + // 计算下次执行时间(使用 cron-parser v5) + const getNextExecutions = (cron: string, count: number = 3): string[] => { + try { + const parts = cron.trim().split(/\s+/) + if (parts.length !== 5) return [] + + // cron-parser v5 API + const interval = CronExpressionParser.parse(cron, { + currentDate: new Date(), + tz: "Asia/Shanghai", + }) + + const results: string[] = [] + for (let i = 0; i < count; i++) { + const next = interval.next() + const date = next.toDate() + results.push(date.toLocaleString("zh-CN")) + } + + return results + } catch (e) { + console.error("cron parse error:", e) + return [] + } + } + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + <DialogContent className="max-w-2xl max-h-[90vh] overflow-hidden flex flex-col"> + <DialogHeader> + <DialogTitle>新建定时扫描</DialogTitle> + <DialogDescription> + 配置定时扫描任务,设置执行计划 + </DialogDescription> + </DialogHeader> + + {/* 步骤指示器 */} + <div className="flex items-center justify-between px-2 py-4"> + {steps.map((step, index) => ( + <React.Fragment key={step.id}> + <div className="flex flex-col items-center gap-2"> + <div + className={cn( + "flex h-10 w-10 items-center justify-center rounded-full border-2 transition-colors", + currentStep > step.id + ? "border-primary bg-primary text-primary-foreground" + : currentStep === step.id + ? "border-primary text-primary" + : "border-muted text-muted-foreground" + )} + > + {currentStep > step.id ? ( + <IconCheck className="h-5 w-5" /> + ) : ( + <step.icon className="h-5 w-5" /> + )} + </div> + <span + className={cn( + "text-xs font-medium", + currentStep >= step.id + ? "text-foreground" + : "text-muted-foreground" + )} + > + {step.title} + </span> + </div> + {index < steps.length - 1 && ( + <div + className={cn( + "h-0.5 flex-1 mx-2", + currentStep > step.id ? "bg-primary" : "bg-muted" + )} + /> + )} + </React.Fragment> + ))} + </div> + + <Separator /> + + {/* 步骤内容 */} + <div className="flex-1 overflow-y-auto py-4 px-1"> + {/* 步骤 1: 基本信息 */} + {currentStep === 1 && ( + <div className="space-y-6"> + <div className="space-y-2"> + <Label htmlFor="name">任务名称 *</Label> + <Input + id="name" + placeholder="例如:每日安全巡检" + value={name} + onChange={(e) => setName(e.target.value)} + /> + <p className="text-xs text-muted-foreground"> + 为定时任务设置一个易于识别的名称 + </p> + </div> + + <div className="space-y-2"> + <Label>扫描引擎 *</Label> + <Select + value={engineId?.toString() || ""} + onValueChange={(v) => setEngineId(Number(v))} + > + <SelectTrigger> + <SelectValue placeholder="选择扫描引擎" /> + </SelectTrigger> + <SelectContent> + {engines.map((engine) => ( + <SelectItem key={engine.id} value={engine.id.toString()}> + {engine.name} + </SelectItem> + ))} + </SelectContent> + </Select> + <p className="text-xs text-muted-foreground"> + 选择要使用的扫描引擎配置 + </p> + </div> + </div> + )} + + {/* 步骤 2: 扫描模式(完整模式)或 调度设置(简化模式) */} + {currentStep === 2 && !hasPreset && ( + <div className="space-y-6"> + <div className="space-y-3"> + <Label>选择扫描模式</Label> + <div className="grid grid-cols-2 gap-4"> + <div + className={cn( + "flex flex-col items-center gap-3 p-4 border-2 rounded-lg cursor-pointer transition-colors", + selectionMode === "organization" + ? "border-primary bg-primary/5" + : "border-muted hover:border-muted-foreground/50" + )} + onClick={() => { + setSelectionMode("organization") + setSelectedTargetId(null) + }} + > + <IconBuilding className="h-8 w-8" /> + <div className="text-center"> + <p className="font-medium">组织扫描</p> + <p className="text-xs text-muted-foreground"> + 选择组织,执行时动态获取其下所有目标 + </p> + </div> + {selectionMode === "organization" && ( + <IconCheck className="h-5 w-5 text-primary" /> + )} + </div> + + <div + className={cn( + "flex flex-col items-center gap-3 p-4 border-2 rounded-lg cursor-pointer transition-colors", + selectionMode === "target" + ? "border-primary bg-primary/5" + : "border-muted hover:border-muted-foreground/50" + )} + onClick={() => { + setSelectionMode("target") + setSelectedOrgId(null) + }} + > + <IconTarget className="h-8 w-8" /> + <div className="text-center"> + <p className="font-medium">目标扫描</p> + <p className="text-xs text-muted-foreground"> + 选择固定的目标列表进行扫描 + </p> + </div> + {selectionMode === "target" && ( + <IconCheck className="h-5 w-5 text-primary" /> + )} + </div> + </div> + </div> + + <p className="text-sm text-muted-foreground"> + {selectionMode === "organization" + ? "组织扫描:每次执行时会动态获取组织下的所有目标,新增的目标也会被扫描" + : "目标扫描:扫描固定的目标列表,后续新增的目标不会被扫描" + } + </p> + </div> + )} + + {/* 步骤 3: 选择目标(仅完整模式,单选) */} + {currentStep === 3 && !hasPreset && ( + <div className="space-y-4"> + {selectionMode === "organization" ? ( + // 组织扫描模式:选择单个组织 + <> + <Label>选择组织</Label> + <div className="flex items-center gap-2 mb-2"> + <Input + placeholder="搜索组织名称..." + value={orgSearchInput} + onChange={(e) => setOrgSearchInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleOrgSearch()} + className="h-9 flex-1" + /> + <Button + type="button" + variant="outline" + size="icon" + className="h-9 w-9" + onClick={handleOrgSearch} + disabled={isOrgFetching} + > + {isOrgFetching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + <Command className="border rounded-lg" shouldFilter={false}> + <CommandList className="max-h-[250px]"> + {organizations.length === 0 ? ( + <CommandEmpty>未找到组织</CommandEmpty> + ) : ( + <CommandGroup> + {organizations.map((org) => ( + <CommandItem + key={org.id} + value={org.id.toString()} + onSelect={() => handleOrgSelect(org.id)} + className="flex items-center justify-between" + > + <div className="flex items-center gap-2"> + <Checkbox + checked={selectedOrgId === org.id} + onCheckedChange={() => handleOrgSelect(org.id)} + /> + <span>{org.name}</span> + </div> + <span className="text-xs text-muted-foreground"> + {org.targetCount || 0} 个目标 + </span> + </CommandItem> + ))} + </CommandGroup> + )} + </CommandList> + </Command> + + {selectedOrgId && ( + <div className="space-y-2"> + <p className="text-sm text-muted-foreground"> + 已选择组织,执行时将动态扫描该组织下所有目标 + </p> + <Badge variant="secondary"> + {organizations.find((o) => o.id === selectedOrgId)?.name} + <IconX + className="h-3 w-3 ml-1 cursor-pointer" + onClick={() => setSelectedOrgId(null)} + /> + </Badge> + </div> + )} + </> + ) : ( + // 目标扫描模式:选择单个目标 + <> + <Label>选择扫描目标</Label> + <div className="flex items-center gap-2 mb-2"> + <Input + placeholder="搜索目标名称..." + value={targetSearchInput} + onChange={(e) => setTargetSearchInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleTargetSearch()} + className="h-9 flex-1" + /> + <Button + type="button" + variant="outline" + size="icon" + className="h-9 w-9" + onClick={handleTargetSearch} + disabled={isTargetFetching} + > + {isTargetFetching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + <Command className="border rounded-lg" shouldFilter={false}> + <CommandList className="max-h-[250px]"> + {targets.length === 0 ? ( + <CommandEmpty>未找到目标</CommandEmpty> + ) : ( + <CommandGroup> + {targets.map((target) => ( + <CommandItem + key={target.id} + value={target.id.toString()} + onSelect={() => handleTargetSelect(target.id)} + className="flex items-center justify-between" + > + <div className="flex items-center gap-2"> + <Checkbox + checked={selectedTargetId === target.id} + onCheckedChange={() => handleTargetSelect(target.id)} + /> + <span>{target.name}</span> + </div> + {target.organizations && target.organizations.length > 0 && ( + <span className="text-xs text-muted-foreground"> + {target.organizations.map((o) => o.name).join(", ")} + </span> + )} + </CommandItem> + ))} + </CommandGroup> + )} + </CommandList> + </Command> + + {selectedTargetId && ( + <div className="space-y-2"> + <p className="text-sm text-muted-foreground"> + 已选择目标 + </p> + <Badge variant="outline"> + {targets.find((t) => t.id === selectedTargetId)?.name} + <IconX + className="h-3 w-3 ml-1 cursor-pointer" + onClick={() => setSelectedTargetId(null)} + /> + </Badge> + </div> + )} + </> + )} + </div> + )} + + {/* 调度设置:完整模式步骤4 或 简化模式步骤2 */} + {((currentStep === 4 && !hasPreset) || (currentStep === 2 && hasPreset)) && ( + <div className="space-y-6"> + <div className="space-y-2"> + <Label>Cron 表达式 *</Label> + <Input + placeholder="分 时 日 月 周(如:0 2 * * *)" + value={cronExpression} + onChange={(e) => setCronExpression(e.target.value)} + className="font-mono" + /> + <p className="text-xs text-muted-foreground"> + 格式:分(0-59) 时(0-23) 日(1-31) 月(1-12) 周(0-6,0=周日) + </p> + </div> + + <div className="space-y-2"> + <Label className="text-muted-foreground">快捷选择</Label> + <div className="flex flex-wrap gap-2"> + {CRON_PRESETS.map((preset) => ( + <Badge + key={preset.value} + variant={ + cronExpression === preset.value ? "default" : "outline" + } + className="cursor-pointer" + onClick={() => setCronExpression(preset.value)} + > + {preset.label} + </Badge> + ))} + </div> + </div> + + <div className="rounded-lg border bg-muted/50 p-4 space-y-3"> + <div className="flex items-center gap-2"> + <IconClock className="h-4 w-4 text-muted-foreground" /> + <span className="font-medium">执行预览</span> + {cronExpression.trim().split(/\s+/).length === 5 && ( + <Badge variant="secondary" className="ml-auto"> + <IconCheck className="h-3 w-3 mr-1" /> + 有效 + </Badge> + )} + </div> + <p className="text-sm">{getCronDescription(cronExpression)}</p> + <Separator /> + <div className="space-y-1"> + <p className="text-xs text-muted-foreground">下次执行时间:</p> + {getNextExecutions(cronExpression).map((time, i) => ( + <p key={i} className="text-sm"> + • {time} + {i === 0 && ( + <span className="text-muted-foreground ml-2">(即将执行)</span> + )} + </p> + ))} + </div> + </div> + </div> + )} + </div> + + <Separator /> + + {/* 底部按钮 */} + <div className="flex justify-between pt-4"> + <Button + variant="outline" + onClick={goToPrevStep} + disabled={currentStep === 1} + > + <IconChevronLeft className="h-4 w-4 mr-1" /> + 上一步 + </Button> + + {currentStep < totalSteps ? ( + <Button onClick={handleNext}> + 下一步 + <IconChevronRight className="h-4 w-4 ml-1" /> + </Button> + ) : ( + <Button onClick={handleSubmit} disabled={isPending}> + {isPending && <IconLoader2 className="h-4 w-4 mr-1 animate-spin" />} + 创建任务 + </Button> + )} + </div> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/scan/scheduled/edit-scheduled-scan-dialog.tsx b/frontend/components/scan/scheduled/edit-scheduled-scan-dialog.tsx new file mode 100644 index 00000000..121f3cba --- /dev/null +++ b/frontend/components/scan/scheduled/edit-scheduled-scan-dialog.tsx @@ -0,0 +1,274 @@ +"use client" + +import React from "react" +import { toast } from "sonner" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Badge } from "@/components/ui/badge" +import { IconX, IconLoader2 } from "@tabler/icons-react" +import { useUpdateScheduledScan } from "@/hooks/use-scheduled-scans" +import { useTargets } from "@/hooks/use-targets" +import { useEngines } from "@/hooks/use-engines" +import type { ScheduledScan, UpdateScheduledScanRequest } from "@/types/scheduled-scan.types" +import type { ScanEngine } from "@/types/engine.types" +import type { Target } from "@/types/target.types" + +interface EditScheduledScanDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + scheduledScan: ScheduledScan | null + onSuccess?: () => void +} + +// 常用 cron 表达式预设 +const CRON_PRESETS = [ + { label: "每分钟", value: "* * * * *" }, + { label: "每5分钟", value: "*/5 * * * *" }, + { label: "每小时", value: "0 * * * *" }, + { label: "每天凌晨2点", value: "0 2 * * *" }, + { label: "每天凌晨4点", value: "0 4 * * *" }, + { label: "每周一凌晨2点", value: "0 2 * * 1" }, + { label: "每月1号凌晨2点", value: "0 2 1 * *" }, +] + +export function EditScheduledScanDialog({ + open, + onOpenChange, + scheduledScan, + onSuccess, +}: EditScheduledScanDialogProps) { + const { mutate: updateScheduledScan, isPending } = useUpdateScheduledScan() + const { data: targetsData } = useTargets() + const { data: enginesData } = useEngines() + + // 表单状态 + const [name, setName] = React.useState("") + const [engineId, setEngineId] = React.useState<number | null>(null) + const [selectedTargetId, setSelectedTargetId] = React.useState<number | null>(null) + const [cronExpression, setCronExpression] = React.useState("") + + // 当 scheduledScan 变化时,初始化表单 + React.useEffect(() => { + if (scheduledScan && open) { + setName(scheduledScan.name) + setEngineId(scheduledScan.engine) + setSelectedTargetId(scheduledScan.targetId || null) + setCronExpression(scheduledScan.cronExpression || "0 2 * * *") + } + }, [scheduledScan, open]) + + // 处理目标选择(单选) + const handleTargetSelect = (targetId: number) => { + setSelectedTargetId(selectedTargetId === targetId ? null : targetId) + } + + // 验证 cron 表达式 + const validateCron = (cron: string): boolean => { + const parts = cron.trim().split(/\s+/) + return parts.length === 5 + } + + // 提交表单 + const handleSubmit = () => { + if (!scheduledScan) return + + if (!name.trim()) { + toast.error("请输入任务名称") + return + } + if (!engineId) { + toast.error("请选择扫描引擎") + return + } + // 目标扫描模式才需要验证目标 + if (scheduledScan.scanMode === 'target' && !selectedTargetId) { + toast.error("请选择一个扫描目标") + return + } + if (!validateCron(cronExpression)) { + toast.error("Cron 表达式格式错误,需要 5 个部分:分 时 日 月 周") + return + } + + const request: UpdateScheduledScanRequest = { + name: name.trim(), + engineId: engineId, + cronExpression: cronExpression.trim(), + } + + // 只有目标扫描模式才更新 targetId + if (scheduledScan.scanMode === 'target' && selectedTargetId) { + request.targetId = selectedTargetId + } + + updateScheduledScan( + { id: scheduledScan.id, data: request }, + { + onSuccess: () => { + onOpenChange(false) + onSuccess?.() + }, + } + ) + } + + const targets: Target[] = targetsData?.targets || [] + const engines: ScanEngine[] = enginesData || [] + + if (!scheduledScan) return null + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle>编辑定时扫描</DialogTitle> + <DialogDescription> + 修改定时扫描任务配置 + </DialogDescription> + </DialogHeader> + + <div className="grid gap-6 py-4"> + {/* 基本信息 */} + <div className="grid gap-4"> + <div className="grid gap-2"> + <Label htmlFor="edit-name">任务名称 *</Label> + <Input + id="edit-name" + placeholder="例如:每日安全巡检" + value={name} + onChange={(e) => setName(e.target.value)} + /> + </div> + + </div> + + {/* 扫描引擎 */} + <div className="grid gap-2"> + <Label>扫描引擎 *</Label> + <Select + value={engineId?.toString() || ""} + onValueChange={(v) => setEngineId(Number(v))} + > + <SelectTrigger> + <SelectValue placeholder="选择扫描引擎" /> + </SelectTrigger> + <SelectContent> + {engines.map((engine) => ( + <SelectItem key={engine.id} value={engine.id.toString()}> + {engine.name} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 扫描目标/组织 */} + <div className="grid gap-2"> + <Label>扫描范围</Label> + {scheduledScan.scanMode === 'organization' ? ( + // 组织扫描模式:显示组织信息,不可编辑 + <div className="border rounded-md p-3 bg-muted/50"> + <div className="flex items-center gap-2"> + <Badge variant="secondary">组织扫描</Badge> + <span className="font-medium">{scheduledScan.organizationName}</span> + </div> + <p className="text-xs text-muted-foreground mt-2"> + 组织扫描模式下,执行时将动态获取该组织下所有目标 + </p> + </div> + ) : ( + // 目标扫描模式:可编辑目标(单选) + <> + <div className="border rounded-md p-3 max-h-[150px] overflow-y-auto"> + {targets.length === 0 ? ( + <p className="text-sm text-muted-foreground">暂无可用目标</p> + ) : ( + <div className="flex flex-wrap gap-2"> + {targets.map((target) => ( + <Badge + key={target.id} + variant={selectedTargetId === target.id ? "default" : "outline"} + className="cursor-pointer" + onClick={() => handleTargetSelect(target.id)} + > + {target.name} + {selectedTargetId === target.id && ( + <IconX className="h-3 w-3 ml-1" /> + )} + </Badge> + ))} + </div> + )} + </div> + {selectedTargetId && ( + <p className="text-xs text-muted-foreground"> + 已选择: {targets.find(t => t.id === selectedTargetId)?.name} + </p> + )} + </> + )} + </div> + + {/* Cron 表达式 */} + <div className="grid gap-4"> + <div className="grid gap-2"> + <Label>Cron 表达式 *</Label> + <Input + placeholder="分 时 日 月 周(如:0 2 * * *)" + value={cronExpression} + onChange={(e) => setCronExpression(e.target.value)} + className="font-mono" + /> + <p className="text-xs text-muted-foreground"> + 格式:分(0-59) 时(0-23) 日(1-31) 月(1-12) 周(0-6,0=周日) + </p> + </div> + + {/* 快捷预设 */} + <div className="grid gap-2"> + <Label className="text-xs text-muted-foreground">快捷选择</Label> + <div className="flex flex-wrap gap-2"> + {CRON_PRESETS.map((preset) => ( + <Badge + key={preset.value} + variant={cronExpression === preset.value ? "default" : "outline"} + className="cursor-pointer" + onClick={() => setCronExpression(preset.value)} + > + {preset.label} + </Badge> + ))} + </div> + </div> + </div> + </div> + + <DialogFooter> + <Button variant="outline" onClick={() => onOpenChange(false)}> + 取消 + </Button> + <Button onClick={handleSubmit} disabled={isPending}> + {isPending && <IconLoader2 className="h-4 w-4 animate-spin" />} + 保存修改 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/scan/scheduled/index.ts b/frontend/components/scan/scheduled/index.ts new file mode 100644 index 00000000..86e0d3a5 --- /dev/null +++ b/frontend/components/scan/scheduled/index.ts @@ -0,0 +1,8 @@ +/** + * Scheduled Scan Components - 统一导出 + */ +export { ScheduledScanDataTable } from './scheduled-scan-data-table' +export { createScheduledScanColumns } from './scheduled-scan-columns' +export { CreateScheduledScanDialog } from './create-scheduled-scan-dialog' +export { EditScheduledScanDialog } from './edit-scheduled-scan-dialog' + diff --git a/frontend/components/scan/scheduled/scheduled-scan-columns.tsx b/frontend/components/scan/scheduled/scheduled-scan-columns.tsx new file mode 100644 index 00000000..e6728580 --- /dev/null +++ b/frontend/components/scan/scheduled/scheduled-scan-columns.tsx @@ -0,0 +1,475 @@ +"use client" + +import React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Switch } from "@/components/ui/switch" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + MoreHorizontal, + Eye, + Trash2, + ChevronsUpDown, + ChevronUp, + ChevronDown, + Copy, + Check, + Edit, + Clock, +} from "lucide-react" +import { + IconClock, + IconCalendar, + IconCalendarRepeat, + IconCalendarTime, + IconAdjustments, +} from "@tabler/icons-react" +import { toast } from "sonner" +import type { ScheduledScan } from "@/types/scheduled-scan.types" + +/** + * 可复制单元格组件 + */ +function CopyableCell({ + value, + maxWidth = "300px", + truncateLength = 40, + successMessage = "已复制", + className = "font-medium", +}: { + value: string + maxWidth?: string + truncateLength?: number + successMessage?: string + className?: string +}) { + const [copied, setCopied] = React.useState(false) + const isLong = value.length > truncateLength + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(value) + setCopied(true) + toast.success(successMessage) + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error("复制失败") + } + } + + return ( + <div className="group inline-flex items-center gap-1" style={{ maxWidth }}> + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <div className={`text-sm truncate cursor-default ${className}`}> + {value} + </div> + </TooltipTrigger> + <TooltipContent + side="top" + align="start" + sideOffset={5} + className={`text-xs ${ + isLong ? "max-w-[500px] break-all" : "whitespace-nowrap" + }`} + > + {value} + </TooltipContent> + </Tooltip> + </TooltipProvider> + + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className={`h-6 w-6 flex-shrink-0 hover:bg-accent transition-opacity ${ + copied ? "opacity-100" : "opacity-0 group-hover:opacity-100" + }`} + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">{copied ? "已复制!" : "点击复制"}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + ) +} + +/** + * 解析 Cron 表达式为人类可读格式 + */ +function parseCronExpression(cron: string): string { + if (!cron) return '-' + + const parts = cron.split(' ') + if (parts.length !== 5) return cron + + const [minute, hour, day, month, weekday] = parts + + // 每分钟 + if (minute === '*' && hour === '*' && day === '*' && month === '*' && weekday === '*') { + return '每分钟' + } + + // 每N分钟 + if (minute.startsWith('*/') && hour === '*') { + return `每 ${minute.slice(2)} 分钟` + } + + // 每小时 + if (minute !== '*' && hour === '*' && day === '*') { + return `每小时 ${minute}分` + } + + // 每N小时 + if (hour.startsWith('*/')) { + return `每 ${hour.slice(2)} 小时 ${minute}分` + } + + // 每天 + if (day === '*' && month === '*' && weekday === '*') { + return `每天 ${hour.padStart(2, '0')}:${minute.padStart(2, '0')}` + } + + // 每周 + const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] + if (day === '*' && month === '*' && weekday !== '*') { + const dayName = weekdays[parseInt(weekday)] || weekday + return `每${dayName} ${hour.padStart(2, '0')}:${minute.padStart(2, '0')}` + } + + // 每月 + if (day !== '*' && month === '*' && weekday === '*') { + return `每月${day}号 ${hour.padStart(2, '0')}:${minute.padStart(2, '0')}` + } + + return cron +} + +/** + * 数据表格列头组件 + */ +function DataTableColumnHeader({ + column, + title, +}: { + column: { + getCanSort: () => boolean + getIsSorted: () => false | "asc" | "desc" + toggleSorting: (desc?: boolean) => void + } + title: string +}) { + if (!column.getCanSort()) { + return <div className="-ml-3 font-medium">{title}</div> + } + + const isSorted = column.getIsSorted() + + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted" + > + {title} + {isSorted === "asc" ? ( + <ChevronUp /> + ) : isSorted === "desc" ? ( + <ChevronDown /> + ) : ( + <ChevronsUpDown /> + )} + </Button> + ) +} + +// 列创建函数的参数类型 +interface CreateColumnsProps { + formatDate: (dateString: string) => string + handleView: (scan: ScheduledScan) => void + handleEdit: (scan: ScheduledScan) => void + handleDelete: (scan: ScheduledScan) => void + handleToggleStatus: (scan: ScheduledScan, enabled: boolean) => void +} + +/** + * 定时扫描行操作组件 + */ +function ScheduledScanRowActions({ + scan, + onView, + onEdit, + onDelete, +}: { + scan: ScheduledScan + onView: () => void + onEdit: () => void + onDelete: () => void +}) { + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" + > + <MoreHorizontal /> + <span className="sr-only">打开菜单</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem onClick={onView}> + <Eye /> + 查看详情 + </DropdownMenuItem> + <DropdownMenuItem onClick={onEdit}> + <Edit /> + 编辑任务 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={onDelete} + className="text-destructive focus:text-destructive" + > + <Trash2 /> + 删除 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) +} + +/** + * 创建定时扫描表格列定义 + */ +export const createScheduledScanColumns = ({ + formatDate, + handleView, + handleEdit, + handleDelete, + handleToggleStatus, +}: CreateColumnsProps): ColumnDef<ScheduledScan>[] => [ + // 任务名称列 + { + accessorKey: "name", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="任务名称" /> + ), + cell: ({ row }) => { + const name = row.getValue("name") as string + if (!name) return <span className="text-muted-foreground text-sm">-</span> + + const maxLength = 35 + const isLong = name.length > maxLength + const displayName = isLong ? name.substring(0, maxLength) + "..." : name + + return ( + <div className="flex items-center gap-1 max-w-[300px]"> + <span className="text-sm font-medium"> + {displayName} + </span> + {isLong && ( + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整任务名称</h4> + <div className="text-xs break-all bg-muted p-2 rounded max-h-48 overflow-y-auto"> + {name} + </div> + </div> + </PopoverContent> + </Popover> + )} + </div> + ) + }, + }, + + // 扫描引擎列 + { + accessorKey: "engineName", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="扫描引擎" /> + ), + cell: ({ row }) => { + const engineName = row.getValue("engineName") as string + return ( + <Badge variant="secondary"> + {engineName} + </Badge> + ) + }, + }, + + // Cron 表达式列 + { + accessorKey: "cronExpression", + header: "调度时间", + cell: ({ row }) => { + const cron = row.original.cronExpression + return ( + <div className="flex flex-col gap-1"> + <span className="text-sm"> + {parseCronExpression(cron)} + </span> + <code className="text-xs text-muted-foreground font-mono"> + {cron} + </code> + </div> + ) + }, + enableSorting: false, + }, + + // 目标列(根据 scanMode 显示组织或目标) + { + accessorKey: "scanMode", + header: "目标", + cell: ({ row }) => { + const scanMode = row.original.scanMode + const organizationName = row.original.organizationName + const targetName = row.original.targetName + + // 组织扫描模式 + if (scanMode === 'organization' && organizationName) { + return ( + <Badge variant="secondary" className="text-xs"> + 组织: {organizationName} + </Badge> + ) + } + + // 目标扫描模式:显示单个目标 + if (!targetName) { + return <div className="text-sm text-muted-foreground">-</div> + } + return ( + <Badge variant="outline" className="text-xs font-mono font-normal"> + {targetName} + </Badge> + ) + }, + enableSorting: false, + }, + + // 启用状态列 + { + accessorKey: "isEnabled", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="状态" /> + ), + cell: ({ row }) => { + const isEnabled = row.getValue("isEnabled") as boolean + const scan = row.original + return ( + <div className="flex items-center gap-2"> + <Switch + checked={isEnabled} + onCheckedChange={(checked: boolean) => + handleToggleStatus(scan, checked) + } + /> + <span className="text-sm text-muted-foreground"> + {isEnabled ? "启用" : "禁用"} + </span> + </div> + ) + }, + }, + + // 下次执行时间列 + { + accessorKey: "nextRunTime", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="下次执行" /> + ), + cell: ({ row }) => { + const nextRunTime = row.getValue("nextRunTime") as string | undefined + return ( + <div className="text-sm text-muted-foreground"> + {nextRunTime ? formatDate(nextRunTime) : "-"} + </div> + ) + }, + }, + + // 执行次数列 + { + accessorKey: "runCount", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="执行次数" /> + ), + cell: ({ row }) => { + const count = row.getValue("runCount") as number + return ( + <div className="text-sm text-muted-foreground font-mono">{count}</div> + ) + }, + }, + + // 上次执行时间列 + { + accessorKey: "lastRunTime", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="上次执行" /> + ), + cell: ({ row }) => { + const lastRunTime = row.getValue("lastRunTime") as string | undefined + return ( + <div className="text-sm text-muted-foreground"> + {lastRunTime ? formatDate(lastRunTime) : "-"} + </div> + ) + }, + }, + + // 操作列 + { + id: "actions", + cell: ({ row }) => ( + <ScheduledScanRowActions + scan={row.original} + onView={() => handleView(row.original)} + onEdit={() => handleEdit(row.original)} + onDelete={() => handleDelete(row.original)} + /> + ), + enableSorting: false, + enableHiding: false, + }, +] diff --git a/frontend/components/scan/scheduled/scheduled-scan-data-table.tsx b/frontend/components/scan/scheduled/scheduled-scan-data-table.tsx new file mode 100644 index 00000000..814fca06 --- /dev/null +++ b/frontend/components/scan/scheduled/scheduled-scan-data-table.tsx @@ -0,0 +1,375 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconPlus, + IconSearch, + IconLoader2, +} from "@tabler/icons-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import type { ScheduledScan } from "@/types/scheduled-scan.types" + +// 组件属性类型定义 +interface ScheduledScanDataTableProps { + data: ScheduledScan[] + columns: ColumnDef<ScheduledScan>[] + onAddNew?: () => void + searchPlaceholder?: string + searchColumn?: string + searchValue?: string + onSearch?: (value: string) => void + isSearching?: boolean + addButtonText?: string + // 服务端分页相关 + page?: number + pageSize?: number + total?: number + totalPages?: number + onPageChange?: (page: number) => void + onPageSizeChange?: (pageSize: number) => void +} + +/** + * 定时扫描数据表格组件 + * 用于显示和管理定时扫描任务数据 + * 包含搜索、分页、列显示控制等功能 + */ +export function ScheduledScanDataTable({ + data = [], + columns, + onAddNew, + searchPlaceholder = "搜索任务名称...", + searchColumn = "name", + searchValue, + onSearch, + isSearching = false, + addButtonText = "新建定时扫描", + // 服务端分页 + page = 1, + pageSize = 10, + total = 0, + totalPages = 1, + onPageChange, + onPageSizeChange, +}: ScheduledScanDataTableProps) { + // 搜索本地状态 + const [localSearchValue, setLocalSearchValue] = React.useState(searchValue || "") + + React.useEffect(() => { + setLocalSearchValue(searchValue || "") + }, [searchValue]) + + const handleSearchSubmit = () => { + if (onSearch) { + onSearch(localSearchValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter") { + handleSearchSubmit() + } + } + + // 表格状态管理 + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [sorting, setSorting] = React.useState<SortingState>([]) + + // 服务端分页:不使用 TanStack Table 的分页,而是手动控制 + const isServerPagination = !!onPageChange + + // 过滤有效数据 + const validData = React.useMemo(() => { + return (data || []).filter( + (item) => item && typeof item.id !== "undefined" && item.id !== null + ) + }, [data]) + + // 创建表格实例 + const table = useReactTable({ + data: validData, + columns, + state: { + sorting, + columnVisibility, + columnFilters, + // 服务端分页时不使用内部分页状态 + ...(isServerPagination ? {} : { + pagination: { pageIndex: page - 1, pageSize } + }), + }, + getRowId: (row) => row.id.toString(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + // 服务端分页时不使用客户端分页 + ...(isServerPagination ? {} : { getPaginationRowModel: getPaginationRowModel() }), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + // 服务端分页:告诉 table 总行数 + ...(isServerPagination ? { manualPagination: true, pageCount: totalPages } : {}), + }) + + + return ( + <div className="w-full space-y-4"> + {/* 工具栏 */} + <div className="flex items-center justify-between"> + {/* 搜索框 */} + <div className="flex items-center space-x-2"> + <Input + placeholder={searchPlaceholder} + value={localSearchValue} + onChange={(e) => setLocalSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="h-8 max-w-sm" + /> + <Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}> + {isSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + + {/* 右侧操作按钮 */} + <div className="flex items-center space-x-2"> + {/* 列显示控制 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + const columnNameMap: Record<string, string> = { + name: "任务名称", + engine_name: "扫描引擎", + frequency: "执行频率", + target_domains: "目标域名", + is_enabled: "状态", + next_run_time: "下次执行", + run_count: "执行次数", + last_run_time: "上次执行", + } + + return ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => + column.toggleVisibility(!!value) + } + > + {columnNameMap[column.id] || column.id} + </DropdownMenuCheckboxItem> + ) + })} + </DropdownMenuContent> + </DropdownMenu> + + {/* 添加新记录按钮 */} + {onAddNew && ( + <Button onClick={onAddNew} size="sm"> + <IconPlus /> + {addButtonText} + </Button> + )} + </div> + </div> + + {/* 表格容器 */} + <div className="rounded-md border"> + <Table> + {/* 表头 */} + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id} colSpan={header.colSpan}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + + {/* 表体 */} + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + className="group" + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + 暂无数据 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {/* 分页控制 */} + <div className="flex items-center justify-end px-2"> + {/* 分页控制器 */} + <div className="flex items-center space-x-6 lg:space-x-8"> + {/* 每页显示数量选择 */} + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + Rows per page + </Label> + <Select + value={`${pageSize}`} + onValueChange={(value) => { + if (onPageSizeChange) { + onPageSizeChange(Number(value)) + } + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((size) => ( + <SelectItem key={size} value={`${size}`}> + {size} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 页码信息 */} + <div className="flex w-[120px] items-center justify-center text-sm font-medium"> + Page {page} of {totalPages} ({total} items) + </div> + + {/* 分页按钮 */} + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => onPageChange?.(1)} + disabled={page <= 1} + > + <span className="sr-only">Go to first page</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => onPageChange?.(page - 1)} + disabled={page <= 1} + > + <span className="sr-only">Go to previous page</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => onPageChange?.(page + 1)} + disabled={page >= totalPages} + > + <span className="sr-only">Go to next page</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => onPageChange?.(totalPages)} + disabled={page >= totalPages} + > + <span className="sr-only">Go to last page</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + </div> + ) +} diff --git a/frontend/components/settings/workers/deploy-terminal-dialog.tsx b/frontend/components/settings/workers/deploy-terminal-dialog.tsx new file mode 100644 index 00000000..532d12ea --- /dev/null +++ b/frontend/components/settings/workers/deploy-terminal-dialog.tsx @@ -0,0 +1,384 @@ +"use client" + +import { useState, useEffect, useRef, useCallback } from "react" +import { + Dialog, + DialogContent, +} from "@/components/ui/dialog" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import { IconRocket, IconEye, IconTrash, IconRefresh } from "@tabler/icons-react" +import type { WorkerNode } from "@/types/worker.types" + +interface DeployTerminalDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + worker: WorkerNode | null + onDeployComplete?: () => void +} + +// 自动根据当前页面 URL 生成 WebSocket URL +const getWsBaseUrl = () => { + if (typeof window === 'undefined') return 'ws://localhost:8888' + + // 优先使用环境变量 + if (process.env.NEXT_PUBLIC_WS_URL) { + return process.env.NEXT_PUBLIC_WS_URL + } + + // 根据当前页面协议和域名自动生成 + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const host = window.location.host + return `${protocol}//${host}` +} + +export function DeployTerminalDialog({ + open, + onOpenChange, + worker, + onDeployComplete, +}: DeployTerminalDialogProps) { + const [isConnected, setIsConnected] = useState(false) + const [error, setError] = useState<string | null>(null) + // 本地 worker 状态,用于实时更新按钮显示 + const [localStatus, setLocalStatus] = useState<string | null>(null) + const [uninstallDialogOpen, setUninstallDialogOpen] = useState(false) + + // 使用本地状态或传入的 worker 状态 + const currentStatus = localStatus || worker?.status + const terminalRef = useRef<HTMLDivElement>(null) + const terminalInstanceRef = useRef<any>(null) + const fitAddonRef = useRef<any>(null) + const wsRef = useRef<WebSocket | null>(null) + + // 初始化 xterm + const initTerminal = useCallback(async () => { + if (!terminalRef.current || terminalInstanceRef.current) return + + const { Terminal } = await import('@xterm/xterm') + const { FitAddon } = await import('@xterm/addon-fit') + const { WebLinksAddon } = await import('@xterm/addon-web-links') + + const terminal = new Terminal({ + cursorBlink: true, + fontSize: 12, // 减小字体 + fontFamily: 'Menlo, Monaco, "Courier New", monospace', + theme: { + background: '#1a1b26', + foreground: '#a9b1d6', + cursor: '#c0caf5', + black: '#32344a', + red: '#f7768e', + green: '#9ece6a', + yellow: '#e0af68', + blue: '#7aa2f7', + magenta: '#ad8ee6', + cyan: '#449dab', + white: '#787c99', + }, + }) + + const fitAddon = new FitAddon() + terminal.loadAddon(fitAddon) + terminal.loadAddon(new WebLinksAddon()) + + terminal.open(terminalRef.current) + fitAddon.fit() + + terminalInstanceRef.current = terminal + fitAddonRef.current = fitAddon + + // 显示连接提示 + terminal.writeln('\x1b[90m正在建立 SSH 连接...\x1b[0m') + + // 监听窗口大小变化 + const handleResize = () => fitAddon.fit() + window.addEventListener('resize', handleResize) + + // 自动连接 WebSocket + connectWs() + + return () => { + window.removeEventListener('resize', handleResize) + } + }, [worker]) + + // 连接 WebSocket + const connectWs = useCallback(() => { + if (!worker || !terminalInstanceRef.current) return + + const terminal = terminalInstanceRef.current + // 如果已有连接先关闭 + if (wsRef.current) { + wsRef.current.close() + } + + const ws = new WebSocket(`${getWsBaseUrl()}/ws/workers/${worker.id}/deploy/`) + ws.binaryType = 'arraybuffer' + wsRef.current = ws + + ws.onopen = () => { + terminal.writeln('\x1b[32m✓ WebSocket 已连接\x1b[0m') + // 后端会自动开始 SSH 连接 + } + + ws.onmessage = (event) => { + if (event.data instanceof ArrayBuffer) { + // 二进制数据 - 终端输出 + const decoder = new TextDecoder() + terminal.write(decoder.decode(event.data)) + } else { + // JSON 消息 + try { + const data = JSON.parse(event.data) + if (data.type === 'connected') { + setIsConnected(true) + setError(null) + // 绑定终端输入 + terminal.onData((data: string) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'input', data })) + } + }) + // 发送终端大小 + ws.send(JSON.stringify({ + type: 'resize', + cols: terminal.cols, + rows: terminal.rows, + })) + } else if (data.type === 'error') { + terminal.writeln(`\x1b[31m✗ ${data.message}\x1b[0m`) + setError(data.message) + } else if (data.type === 'status') { + // 更新本地状态以实时显示正确的按钮 + setLocalStatus(data.status) + // 任何状态变化都刷新父组件列表 + onDeployComplete?.() + } + } catch { + // 忽略解析错误 + } + } + } + + ws.onclose = () => { + terminal.writeln('') + terminal.writeln('\x1b[33m连接已关闭\x1b[0m') + setIsConnected(false) + } + + ws.onerror = () => { + terminal.writeln('\x1b[31m✗ WebSocket 连接失败\x1b[0m') + setError('连接失败') + } + }, [worker, onDeployComplete]) + + // 发送终端大小变化 + useEffect(() => { + if (!isConnected || !wsRef.current || !terminalInstanceRef.current) return + + const terminal = terminalInstanceRef.current + const handleResize = () => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify({ + type: 'resize', + cols: terminal.cols, + rows: terminal.rows, + })) + } + } + + terminal.onResize?.(handleResize) + }, [isConnected]) + + // 打开时初始化 + useEffect(() => { + if (open && worker) { + // 延迟初始化,确保 DOM 已渲染 + const timer = setTimeout(initTerminal, 100) + return () => clearTimeout(timer) + } + }, [open, worker, initTerminal]) + + // 关闭时清理 + const handleClose = () => { + if (wsRef.current) { + wsRef.current.close() + wsRef.current = null + } + if (terminalInstanceRef.current) { + terminalInstanceRef.current.dispose() + terminalInstanceRef.current = null + } + fitAddonRef.current = null + setIsConnected(false) + setError(null) + setLocalStatus(null) // 重置本地状态 + // 关闭时刷新父组件列表,确保状态同步 + onDeployComplete?.() + onOpenChange(false) + } + + // 执行部署脚本(后台运行) + const handleDeploy = () => { + if (!wsRef.current || !isConnected) return + setLocalStatus('deploying') // 立即更新为部署中状态 + onDeployComplete?.() // 刷新父组件列表 + wsRef.current.send(JSON.stringify({ type: 'deploy' })) + } + + // 查看部署进度(attach 到 tmux 会话) + const handleAttach = () => { + if (!wsRef.current || !isConnected) return + wsRef.current.send(JSON.stringify({ type: 'attach' })) + } + + // 卸载 Agent(打开确认弹窗) + const handleUninstallClick = () => { + if (!wsRef.current || !isConnected) return + setUninstallDialogOpen(true) + } + + // 确认卸载 + const handleUninstallConfirm = () => { + if (!wsRef.current || !isConnected) return + setUninstallDialogOpen(false) + wsRef.current.send(JSON.stringify({ type: 'uninstall' })) + } + + return ( + <Dialog open={open} onOpenChange={handleClose}> + <DialogContent className="w-[50vw] max-w-[50vw] h-[80vh] flex flex-col p-0 gap-0 overflow-hidden [&>button]:hidden"> + {/* 终端标题栏 - macOS 风格 */} + <div className="flex items-center justify-between px-4 py-3 bg-[#1a1b26] border-b border-[#32344a]"> + <div className="flex items-center gap-3"> + {/* 红黄绿按钮 */} + <div className="flex items-center gap-1.5"> + <button + onClick={handleClose} + className="w-3 h-3 rounded-full bg-[#ff5f56] hover:bg-[#ff5f56]/80 transition-colors" + title="关闭" + /> + <div className="w-3 h-3 rounded-full bg-[#ffbd2e]" /> + <div className="w-3 h-3 rounded-full bg-[#27c93f]" /> + </div> + {/* 标题 */} + <span className="text-sm text-[#a9b1d6] font-medium"> + {worker?.username}@{worker?.ipAddress} + </span> + </div> + <div className="flex items-center gap-1.5"> + <span className={`w-2 h-2 rounded-full ${isConnected ? 'bg-[#9ece6a]' : 'bg-[#f7768e]'}`} /> + <span className="text-xs text-[#a9b1d6]">{isConnected ? '已连接' : '未连接'}</span> + </div> + </div> + + {/* xterm 终端容器 */} + <div + ref={terminalRef} + className="flex-1 overflow-hidden bg-[#1a1b26]" + /> + + {/* 底部操作栏 - 根据状态显示不同按钮 */} + <div className="flex items-center justify-between px-4 py-3 bg-[#1a1b26] border-t border-[#32344a]"> + {/* 左侧:状态提示 */} + <div className="text-xs text-[#565f89]"> + {!isConnected && '等待连接...'} + {isConnected && currentStatus === 'pending' && '节点未部署,点击右侧按钮开始部署扫描环境'} + {isConnected && currentStatus === 'deploying' && '正在部署中,点击查看进度'} + {isConnected && currentStatus === 'online' && '节点运行正常'} + {isConnected && currentStatus === 'offline' && '节点离线,可尝试重新部署'} + </div> + + {/* 右侧:操作按钮 */} + <div className="flex items-center gap-2"> + {!isConnected && ( + <button + onClick={connectWs} + className="inline-flex items-center px-3 py-1.5 text-sm rounded-md bg-[#32344a] text-[#a9b1d6] hover:bg-[#414868] transition-colors" + > + <IconRefresh className="mr-1.5 h-4 w-4" /> + 重新连接 + </button> + )} + {isConnected && worker && ( + <> + {/* 未部署 -> 显示"开始部署" */} + {currentStatus === 'pending' && ( + <button + onClick={handleDeploy} + className="inline-flex items-center px-3 py-1.5 text-sm rounded-md bg-[#7aa2f7] text-[#1a1b26] hover:bg-[#7aa2f7]/80 transition-colors" + > + <IconRocket className="mr-1.5 h-4 w-4" /> + 开始部署 + </button> + )} + + {/* 部署中 -> 显示"查看进度" */} + {currentStatus === 'deploying' && ( + <button + onClick={handleAttach} + className="inline-flex items-center px-3 py-1.5 text-sm rounded-md bg-[#7aa2f7] text-[#1a1b26] hover:bg-[#7aa2f7]/80 transition-colors" + > + <IconEye className="mr-1.5 h-4 w-4" /> + 查看进度 + </button> + )} + + {/* 已部署(online/offline) -> 显示"重新部署"和"卸载" */} + {(currentStatus === 'online' || currentStatus === 'offline') && ( + <> + <button + onClick={handleDeploy} + className="inline-flex items-center px-3 py-1.5 text-sm rounded-md bg-[#32344a] text-[#a9b1d6] hover:bg-[#414868] transition-colors" + > + <IconRocket className="mr-1.5 h-4 w-4" /> + 重新部署 + </button> + <button + onClick={handleUninstallClick} + className="inline-flex items-center px-3 py-1.5 text-sm rounded-md bg-[#32344a] text-[#f7768e] hover:bg-[#414868] transition-colors" + > + <IconTrash className="mr-1.5 h-4 w-4" /> + 卸载 + </button> + </> + )} + </> + )} + </div> + </div> + </DialogContent> + + {/* 卸载确认弹窗 */} + <AlertDialog open={uninstallDialogOpen} onOpenChange={setUninstallDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认卸载</AlertDialogTitle> + <AlertDialogDescription> + 确定要在远程主机上卸载 Agent 并删除相关容器吗?此操作不会卸载 Docker。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={handleUninstallConfirm} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + 卸载 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </Dialog> + ) +} diff --git a/frontend/components/settings/workers/index.ts b/frontend/components/settings/workers/index.ts new file mode 100644 index 00000000..97383dd1 --- /dev/null +++ b/frontend/components/settings/workers/index.ts @@ -0,0 +1,3 @@ +export { WorkerList } from './worker-list' +export { WorkerDialog } from './worker-dialog' +export { DeployTerminalDialog } from './deploy-terminal-dialog' diff --git a/frontend/components/settings/workers/worker-dialog.tsx b/frontend/components/settings/workers/worker-dialog.tsx new file mode 100644 index 00000000..949ab559 --- /dev/null +++ b/frontend/components/settings/workers/worker-dialog.tsx @@ -0,0 +1,241 @@ +"use client" + +import { useEffect } from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { useCreateWorker, useUpdateWorker } from "@/hooks/use-workers" +import type { WorkerNode } from "@/types/worker.types" + +// 表单验证 Schema +const formSchema = z.object({ + name: z.string().min(1, "请输入节点名称").max(100, "名称不能超过100个字符"), + ipAddress: z.string() + .min(1, "请输入 IP 地址") + .regex( + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, + "请输入有效的 IP 地址" + ), + sshPort: z.coerce.number().int().min(1).max(65535), + username: z.string().min(1, "请输入用户名"), + password: z.string().optional(), +}) + +// 显式定义表单类型以解决类型推断问题 +type FormValues = { + name: string + ipAddress: string + sshPort: number + username: string + password?: string +} + +interface WorkerDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + worker?: WorkerNode | null +} + +export function WorkerDialog({ open, onOpenChange, worker }: WorkerDialogProps) { + const createWorker = useCreateWorker() + const updateWorker = useUpdateWorker() + const isEditing = !!worker + + const form = useForm<FormValues>({ + resolver: zodResolver(formSchema) as any, // 绕过类型检查问题 + defaultValues: { + name: "", + ipAddress: "", + sshPort: 22, + username: "root", + password: "", + }, + }) + + // 填充表单数据 + useEffect(() => { + if (open && worker) { + form.reset({ + name: worker.name, + ipAddress: worker.ipAddress, + sshPort: worker.sshPort, + username: worker.username, + password: "", // 编辑时不回显密码 + }) + } else if (open && !worker) { + form.reset({ + name: "", + ipAddress: "", + sshPort: 22, + username: "root", + password: "", + }) + } + }, [open, worker, form]) + + const onSubmit = async (values: FormValues) => { + try { + if (isEditing && worker) { + await updateWorker.mutateAsync({ + id: worker.id, + data: { + name: values.name, + sshPort: values.sshPort, + username: values.username, + password: values.password || undefined, // 如果为空则不传 + } + }) + } else { + if (!values.password) { + form.setError("password", { message: "请输入 SSH 密码" }) + return + } + await createWorker.mutateAsync({ + name: values.name, + ipAddress: values.ipAddress, + sshPort: values.sshPort, + username: values.username, + password: values.password, + }) + } + form.reset() + onOpenChange(false) + } catch (error) { + // 错误已在 hook 中处理 + } + } + + const isPending = createWorker.isPending || updateWorker.isPending + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[425px]"> + <DialogHeader> + <DialogTitle>{isEditing ? "编辑扫描节点" : "添加扫描节点"}</DialogTitle> + <DialogDescription> + {isEditing + ? "修改节点的 SSH 连接信息" + : "输入远程 VPS 的 SSH 连接信息,添加后可通过「管理部署」一键部署扫描环境"} + </DialogDescription> + </DialogHeader> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>节点名称</FormLabel> + <FormControl> + <Input placeholder="例如: VPS-US-1" {...field} /> + </FormControl> + <FormDescription> + 用于识别节点的名称 + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="ipAddress" + render={({ field }) => ( + <FormItem> + <FormLabel>IP 地址</FormLabel> + <FormControl> + <Input + placeholder="例如: 192.168.1.100" + {...field} + disabled={isEditing} // 编辑时 IP 禁用 + /> + </FormControl> + {isEditing && ( + <FormDescription>IP 地址不可修改</FormDescription> + )} + <FormMessage /> + </FormItem> + )} + /> + <div className="grid grid-cols-2 gap-4"> + <FormField + control={form.control} + name="sshPort" + render={({ field }) => ( + <FormItem> + <FormLabel>SSH 端口</FormLabel> + <FormControl> + <Input type="number" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="username" + render={({ field }) => ( + <FormItem> + <FormLabel>用户名</FormLabel> + <FormControl> + <Input {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + <FormField + control={form.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormLabel>SSH 密码</FormLabel> + <FormControl> + <Input type="password" placeholder={isEditing ? "留空保持不变" : "输入 SSH 密码"} {...field} /> + </FormControl> + <FormDescription> + {isEditing ? "如需修改密码请输入新密码" : "密码仅用于部署,不会明文存储"} + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + > + 取消 + </Button> + <Button type="submit" disabled={isPending}> + {isPending + ? (isEditing ? "保存中..." : "创建中...") + : (isEditing ? "保存修改" : "创建节点")} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/settings/workers/worker-list.tsx b/frontend/components/settings/workers/worker-list.tsx new file mode 100644 index 00000000..19d28204 --- /dev/null +++ b/frontend/components/settings/workers/worker-list.tsx @@ -0,0 +1,379 @@ +"use client" + +import { useState } from "react" +import { + IconPlus, + IconServer, + IconTerminal2, + IconTrash, + IconEdit, + IconCloud, + IconCloudOff, + IconClock, +} from "@tabler/icons-react" +import { Button } from "@/components/ui/button" +import { Status, StatusIndicator, StatusLabel } from "@/components/ui/shadcn-io/status" +import { Badge } from "@/components/ui/badge" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { + Banner, + BannerIcon, + BannerTitle, + BannerAction, + BannerClose, +} from "@/components/ui/shadcn-io/banner" +import { Skeleton } from "@/components/ui/skeleton" +import { useWorkers, useDeleteWorker } from "@/hooks/use-workers" +import type { WorkerNode, WorkerStatus } from "@/types/worker.types" +import { WorkerDialog } from "./worker-dialog" +import { DeployTerminalDialog } from "./deploy-terminal-dialog" +import { Rocket } from "lucide-react" + +// 后端状态 -> shadcn 状态映射 +const STATUS_MAP: Record<WorkerStatus, 'online' | 'offline' | 'maintenance' | 'degraded'> = { + online: 'online', + offline: 'offline', + pending: 'maintenance', + deploying: 'degraded', +} + +// 状态中文标签 +const STATUS_LABEL: Record<WorkerStatus, string> = { + online: '运行中', + offline: '离线', + pending: '等待部署', + deploying: '部署中', +} + +// 统计卡片组件 +function StatsCards({ workers }: { workers: WorkerNode[] }) { + const total = workers.length + const online = workers.filter(w => w.status === 'online').length + const offline = workers.filter(w => w.status === 'offline').length + const pending = workers.filter(w => w.status === 'pending').length + + const stats = [ + { label: '总节点', value: total, icon: IconServer, color: 'text-foreground' }, + { label: '在线', value: online, icon: IconCloud, color: 'text-emerald-600' }, + { label: '离线', value: offline, icon: IconCloudOff, color: 'text-red-500' }, + { label: '等待部署', value: pending, icon: IconClock, color: 'text-amber-500' }, + ] + + return ( + <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"> + {stats.map((stat) => ( + <Card key={stat.label} className="p-4"> + <div className="flex items-center gap-3"> + <div className={`p-2 rounded-lg bg-muted ${stat.color}`}> + <stat.icon className="h-5 w-5" /> + </div> + <div> + <p className="text-2xl font-bold">{stat.value}</p> + <p className="text-xs text-muted-foreground">{stat.label}</p> + </div> + </div> + </Card> + ))} + </div> + ) +} + +// 快速开始引导横幅 +function QuickStartBanner() { + const [helpOpen, setHelpOpen] = useState(false) + + const steps = [ + { step: 1, title: '添加扫描节点', desc: '点击"添加节点"按钮,填写 VPS 服务器的 SSH 连接信息(IP、端口、用户名、密码)' }, + { step: 2, title: '部署扫描环境', desc: '点击"管理部署"按钮,系统会自动通过 SSH 在远程服务器上安装 Docker 和心跳agent' }, + { step: 3, title: '自动任务分发', desc: '部署完成后节点会自动上报心跳,扫描任务将根据负载自动分发到各节点并行执行' }, + ] + + return ( + <> + <Banner inset className="mb-6"> + <BannerIcon icon={Rocket} /> + <BannerTitle> + <span className="font-medium">分布式扫描:</span> + <span className="opacity-90">添加 VPS 节点 → 一键部署 → 任务自动分发</span> + </BannerTitle> + <BannerAction onClick={() => setHelpOpen(true)}> + 了解更多 + </BannerAction> + <BannerClose /> + </Banner> + + <AlertDialog open={helpOpen} onOpenChange={setHelpOpen}> + <AlertDialogContent className="max-w-lg"> + <AlertDialogHeader> + <AlertDialogTitle className="flex items-center gap-2"> + <Rocket className="h-5 w-5" /> + 什么是分布式扫描? + </AlertDialogTitle> + <AlertDialogDescription asChild> + <div className="space-y-4 text-left"> + <p> + 分布式扫描允许你将扫描任务分发到多个远程服务器(扫描节点)上并行执行,显著提高扫描效率。 + </p> + <div className="space-y-3"> + {steps.map((item) => ( + <div key={item.step} className="flex gap-3"> + <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground text-xs font-medium"> + {item.step} + </div> + <div> + <p className="font-medium text-sm text-foreground">{item.title}</p> + <p className="text-xs text-muted-foreground">{item.desc}</p> + </div> + </div> + ))} + </div> + </div> + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogAction>知道了</AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </> + ) +} + +// Worker 卡片视图组件 +function WorkerCardView({ + workers, + onEdit, + onManage, + onDelete +}: { + workers: WorkerNode[] + onEdit: (w: WorkerNode) => void + onManage: (w: WorkerNode) => void + onDelete: (w: WorkerNode) => void +}) { + return ( + <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"> + {workers.map((worker) => ( + <Card key={worker.id}> + <CardHeader className="pb-2"> + <div className="flex items-start justify-between"> + <div className="flex items-center gap-3"> + <div className="p-2 rounded-lg bg-muted"> + <IconServer className="h-5 w-5 text-muted-foreground" /> + </div> + <div className="min-w-0"> + <div className="flex items-center gap-2"> + <CardTitle className="text-base">{worker.name}</CardTitle> + {worker.isLocal && ( + <Badge variant="secondary" className="text-xs">本地</Badge> + )} + </div> + <Status status={STATUS_MAP[worker.status]} className="mt-1"> + <StatusIndicator /> + <StatusLabel>{STATUS_LABEL[worker.status]}</StatusLabel> + </Status> + </div> + </div> + {/* 本地节点不显示编辑和删除按钮 */} + {!worker.isLocal && ( + <div className="flex gap-1"> + <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => onEdit(worker)} title="编辑"> + <IconEdit className="h-4 w-4" /> + </Button> + <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => onDelete(worker)} title="删除"> + <IconTrash className="h-4 w-4 text-destructive" /> + </Button> + </div> + )} + </div> + </CardHeader> + <CardContent className="space-y-3"> + {/* 所有节点都显示 CPU 和内存 */} + <div className="grid grid-cols-2 gap-2 text-sm"> + <div className="text-center p-2 rounded-lg bg-muted"> + <p className="text-xs text-muted-foreground">CPU</p> + <p className="font-mono font-medium"> + {worker.info?.cpuPercent != null ? `${worker.info.cpuPercent.toFixed(1)}%` : '-'} + </p> + </div> + <div className="text-center p-2 rounded-lg bg-muted"> + <p className="text-xs text-muted-foreground">内存</p> + <p className="font-mono font-medium"> + {worker.info?.memoryPercent != null ? `${worker.info.memoryPercent.toFixed(1)}%` : '-'} + </p> + </div> + </div> + + {/* 远程节点:额外显示连接信息和管理按钮 */} + {!worker.isLocal && ( + <> + <div className="flex items-center gap-2 text-xs text-muted-foreground"> + <span className="font-mono">{worker.ipAddress}:{worker.sshPort}</span> + <span>•</span> + <span>{worker.username}</span> + </div> + + <Button variant="outline" size="sm" className="w-full" onClick={() => onManage(worker)}> + <IconTerminal2 className="h-4 w-4 mr-1.5" /> + 管理部署 + </Button> + </> + )} + </CardContent> + </Card> + ))} + </div> + ) +} + +// 空状态组件 +function EmptyState({ onAdd }: { onAdd: () => void }) { + return ( + <div className="flex flex-col items-center justify-center py-16 text-center"> + <div className="p-4 rounded-full bg-muted mb-4"> + <IconServer className="h-12 w-12 text-muted-foreground" /> + </div> + <h3 className="text-lg font-semibold mb-2">暂无扫描节点</h3> + <p className="text-sm text-muted-foreground mb-6 max-w-md"> + 添加远程 VPS 服务器作为扫描节点,开始使用分布式扫描功能,提升扫描效率 + </p> + <Button onClick={onAdd}> + <IconPlus className="h-4 w-4 mr-2" /> + 添加第一个节点 + </Button> + </div> + ) +} + +export function WorkerList() { + const [page, setPage] = useState(1) + const [pageSize] = useState(10) + const [workerDialogOpen, setWorkerDialogOpen] = useState(false) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [deployDialogOpen, setDeployDialogOpen] = useState(false) + const [selectedWorker, setSelectedWorker] = useState<WorkerNode | null>(null) + const [workerToDeploy, setWorkerToDeploy] = useState<WorkerNode | null>(null) + const [workerToDelete, setWorkerToDelete] = useState<WorkerNode | null>(null) + + const { data, isLoading, refetch } = useWorkers(page, pageSize) + const deleteWorker = useDeleteWorker() + + const workers = data?.results || [] + const hasWorkers = workers.length > 0 + + const handleAdd = () => { + setSelectedWorker(null) + setWorkerDialogOpen(true) + } + + const handleEdit = (worker: WorkerNode) => { + setSelectedWorker(worker) + setWorkerDialogOpen(true) + } + + const handleManage = (worker: WorkerNode) => { + setWorkerToDeploy(worker) + setDeployDialogOpen(true) + } + + const handleDeleteClick = (worker: WorkerNode) => { + setWorkerToDelete(worker) + setDeleteDialogOpen(true) + } + + const handleDeleteConfirm = () => { + if (workerToDelete) { + deleteWorker.mutate(workerToDelete.id) + setDeleteDialogOpen(false) + setWorkerToDelete(null) + } + } + + return ( + <div className="space-y-4"> + {/* 快速开始引导横幅 */} + <QuickStartBanner /> + + {/* 统计卡片 - 只在有 Worker 时显示 */} + {hasWorkers && <StatsCards workers={workers} />} + + {/* 主内容卡片 */} + <Card> + <CardHeader> + <div className="flex items-center justify-between"> + <div> + <CardTitle className="flex items-center gap-2"> + <IconServer className="h-5 w-5" /> + Worker 节点 + </CardTitle> + <CardDescription>管理分布式扫描节点,支持远程部署和监控</CardDescription> + </div> + <div className="flex items-center gap-2"> + <Button size="sm" onClick={handleAdd}> + <IconPlus className="mr-1 h-4 w-4" />添加节点 + </Button> + </div> + </div> + </CardHeader> + <CardContent> + {isLoading ? ( + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> + {[...Array(3)].map((_, i) => <Skeleton key={i} className="h-48 w-full rounded-lg" />)} + </div> + ) : !hasWorkers ? ( + <EmptyState onAdd={handleAdd} /> + ) : ( + <WorkerCardView + workers={workers} + onEdit={handleEdit} + onManage={handleManage} + onDelete={handleDeleteClick} + /> + )} + </CardContent> + </Card> + + {/* 弹窗 */} + <WorkerDialog + open={workerDialogOpen} + onOpenChange={setWorkerDialogOpen} + worker={selectedWorker} + /> + <DeployTerminalDialog + open={deployDialogOpen} + onOpenChange={setDeployDialogOpen} + worker={workerToDeploy} + onDeployComplete={() => refetch()} + /> + + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除</AlertDialogTitle> + <AlertDialogDescription>确定要删除 Worker 节点 "{workerToDelete?.name}" 吗?此操作不可恢复。</AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction onClick={handleDeleteConfirm} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">删除</AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </div> + ) +} diff --git a/frontend/components/site-header.tsx b/frontend/components/site-header.tsx new file mode 100644 index 00000000..c68c9389 --- /dev/null +++ b/frontend/components/site-header.tsx @@ -0,0 +1,57 @@ +import { Button } from "@/components/ui/button" +// 导入分隔线组件 +import { Separator } from "@/components/ui/separator" +// 导入侧边栏触发器组件 +import { SidebarTrigger } from "@/components/ui/sidebar" +// 导入通知抽屉组件 +import { NotificationDrawer } from "@/components/notifications" +// 导入主题切换组件 +import { ThemeToggle } from "@/components/theme-toggle" +// 导入颜色主题切换组件 +import { ColorThemeSwitcher } from "@/components/color-theme-switcher" +// 导入快速扫描组件 +import { QuickScanDialog } from "@/components/scan/quick-scan-dialog" + +/** + * 网站头部组件 + * 显示在页面顶部,包含侧边栏切换按钮、页面标题和外部链接 + */ +export function SiteHeader() { + return ( + // header 元素,使用 flex 布局水平排列内容 + <header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"> + {/* 内容容器,占据整个宽度 */} + <div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6"> + {/* 侧边栏切换按钮,带有负左边距以对齐 */} + <SidebarTrigger className="-ml-1" /> + + {/* 右侧按钮区域,使用 ml-auto 推到最右边 */} + <div className="ml-auto flex items-center gap-2"> + {/* 快速扫描按钮 */} + <QuickScanDialog /> + + {/* 通知抽屉按钮 */} + <NotificationDrawer /> + + {/* 颜色主题切换按钮 */} + <ColorThemeSwitcher /> + + {/* 亮暗模式切换按钮 */} + <ThemeToggle /> + + {/* GitHub 链接按钮,在小屏幕上隐藏 */} + <Button variant="ghost" asChild size="sm" className="hidden sm:flex"> + <a + href="https://github.com/yyhuni" + rel="noopener noreferrer" // 安全属性,防止新窗口访问原窗口 + target="_blank" // 在新标签页打开 + className="dark:text-foreground" // 深色模式下的文字颜色 + > + GitHub + </a> + </Button> + </div> + </div> + </header> + ) +} diff --git a/frontend/components/subdomains/index.ts b/frontend/components/subdomains/index.ts new file mode 100644 index 00000000..a8cb7727 --- /dev/null +++ b/frontend/components/subdomains/index.ts @@ -0,0 +1,3 @@ +export { SubdomainsDetailView } from "./subdomains-detail-view" +export { SubdomainsDataTable } from "./subdomains-data-table" +export { createSubdomainColumns } from "./subdomains-columns" diff --git a/frontend/components/subdomains/subdomains-columns.tsx b/frontend/components/subdomains/subdomains-columns.tsx new file mode 100644 index 00000000..32885bdc --- /dev/null +++ b/frontend/components/subdomains/subdomains-columns.tsx @@ -0,0 +1,278 @@ +"use client" + +import React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { Checkbox } from "@/components/ui/checkbox" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { MoreHorizontal, Eye, ChevronsUpDown, ChevronUp, ChevronDown, Copy, Check } from "lucide-react" +import { Badge } from "@/components/ui/badge" +import type { Subdomain } from "@/types/subdomain.types" +import { toast } from "sonner" + +/** + * 可复制单元格组件 + */ +function CopyableCell({ + value, + maxWidth = "400px", + truncateLength = 50, + successMessage = "已复制", + className = "font-medium" +}: { + value: string + maxWidth?: string + truncateLength?: number + successMessage?: string + className?: string +}) { + const [copied, setCopied] = React.useState(false) + const isLong = value.length > truncateLength + + const handleCopy = async () => { + try { + // 优先使用 Clipboard API,不支持时用 fallback + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(value) + } else { + // Fallback: 使用临时 textarea + const textArea = document.createElement('textarea') + textArea.value = value + textArea.style.position = 'fixed' + textArea.style.left = '-9999px' + textArea.style.top = '-9999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + document.execCommand('copy') + document.body.removeChild(textArea) + } + setCopied(true) + toast.success(successMessage) + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error('复制失败') + } + } + + return ( + <div className="group inline-flex items-center gap-1" style={{ maxWidth }}> + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <div className={`text-sm truncate cursor-default ${className}`}> + {value} + </div> + </TooltipTrigger> + <TooltipContent + side="top" + align="start" + sideOffset={5} + className={`text-xs ${isLong ? 'max-w-[500px] break-all' : 'whitespace-nowrap'}`} + > + {value} + </TooltipContent> + </Tooltip> + </TooltipProvider> + + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className={`h-6 w-6 flex-shrink-0 hover:bg-accent transition-opacity ${ + copied ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' + }`} + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">{copied ? '已复制!' : '点击复制'}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + ) +} + +// 列创建函数的参数类型 +interface CreateColumnsProps { + formatDate: (dateString: string) => string + navigate: (path: string) => void + onViewDetail: (subdomain: Subdomain) => void +} + +/** + * 域名行操作组件 + */ +function SubdomainRowActions({ + subdomain, + onViewDetail, +}: { + subdomain: Subdomain + onViewDetail: () => void +}) { + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" + > + <MoreHorizontal /> + <span className="sr-only">打开菜单</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-52"> + <DropdownMenuItem onClick={onViewDetail}> + <Eye className="mr-2 h-4 w-4" /> + 查看详细 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) +} + +/** + * 数据表格列头组件 + */ +function DataTableColumnHeader({ + column, + title, +}: { + column: { getCanSort: () => boolean; getIsSorted: () => false | "asc" | "desc"; toggleSorting: (desc?: boolean) => void } + title: string +}) { + if (!column.getCanSort()) { + return <div className="-ml-3 font-medium">{title}</div> + } + + const isSorted = column.getIsSorted() + + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted" + > + {title} + {isSorted === "asc" ? ( + <ChevronUp /> + ) : isSorted === "desc" ? ( + <ChevronDown /> + ) : ( + <ChevronsUpDown /> + )} + </Button> + ) +} + +/** + * 创建目标域名表格列定义 + */ +export const createSubdomainColumns = ({ + formatDate, + navigate, + onViewDetail, +}: CreateColumnsProps): ColumnDef<Subdomain>[] => [ + // 选择列 + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + + // 子域名列 + { + accessorKey: "name", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Subdomain" /> + ), + cell: ({ row }) => { + const name = row.getValue("name") as string + if (!name) return <span className="text-muted-foreground text-sm">-</span> + + const maxLength = 40 + const isLong = name.length > maxLength + const displayName = isLong ? name.substring(0, maxLength) + "..." : name + + return ( + <div className="flex items-center gap-1 max-w-[350px]"> + <span className="text-sm font-medium"> + {displayName} + </span> + {isLong && ( + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整子域名</h4> + <div className="text-xs break-all bg-muted p-2 rounded max-h-48 overflow-y-auto font-mono"> + {name} + </div> + </div> + </PopoverContent> + </Popover> + )} + </div> + ) + }, + }, + + // 发现时间列 + { + accessorKey: "discoveredAt", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="发现时间" /> + ), + cell: ({ getValue }) => { + const value = getValue<string | undefined>() + return value ? formatDate(value) : "-" + }, + }, + +] diff --git a/frontend/components/subdomains/subdomains-data-table.tsx b/frontend/components/subdomains/subdomains-data-table.tsx new file mode 100644 index 00000000..a887689c --- /dev/null +++ b/frontend/components/subdomains/subdomains-data-table.tsx @@ -0,0 +1,485 @@ +"use client" // 标记为客户端组件 + +// 导入 React 库和 Hooks +import * as React from "react" +// 导入表格相关组件和类型 +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +// 导入图标组件 +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconPlus, + IconTrash, + IconDownload, + IconSearch, + IconLoader2, +} from "@tabler/icons-react" + +// 导入 UI 组件 +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +// 导入子域名类型定义 +import type { Subdomain } from "@/types/subdomain.types" +import type { PaginationInfo } from "@/types/common.types" + +// 组件属性类型定义 +interface SubdomainsDataTableProps { + data: Subdomain[] // 子域名数据数组 + columns: ColumnDef<Subdomain>[] // 列定义数组 + onAddNew?: () => void // 添加新域名的回调函数 + onBulkDelete?: () => void // 批量删除回调函数 + onSelectionChange?: (selectedRows: Subdomain[]) => void // 选中行变化回调 + searchPlaceholder?: string // 搜索框占位符 + searchColumn?: string // 搜索的列名 + searchValue?: string // 受控:搜索框当前值(服务端搜索) + onSearch?: (value: string) => void // 受控:搜索框变更回调(服务端搜索) + isSearching?: boolean // 搜索中状态(显示加载动画) + addButtonText?: string // 添加按钮文本 + // 下载回调函数 + onDownloadAll?: () => void // 下载所有子域名 + onDownloadInteresting?: () => void // 下载有趣的子域名 + onDownloadImportant?: () => void // 下载重要的子域名 + onDownloadSelected?: () => void // 下载选中的子域名 + // 服务端分页支持 + pagination?: { pageIndex: number; pageSize: number } + setPagination?: React.Dispatch<React.SetStateAction<{ pageIndex: number; pageSize: number }>> + paginationInfo?: PaginationInfo + onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void +} + +/** + * 目标域名数据表格组件 + * 专门用于显示和管理目标域名数据的表格 + * 包含搜索、分页、列显示控制等功能 + */ +export function SubdomainsDataTable({ + data = [], + columns, + onAddNew, + onBulkDelete, + onSelectionChange, + searchPlaceholder = "搜索子域名...", + searchColumn = "name", + searchValue, + onSearch, + isSearching = false, + addButtonText = "Add", + onDownloadAll, + onDownloadInteresting, + onDownloadImportant, + onDownloadSelected, + pagination: externalPagination, + setPagination: setExternalPagination, + paginationInfo, + onPaginationChange, +}: SubdomainsDataTableProps) { + // 表格状态管理 + // 选中行状态,key为行id,value为true或false + const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({}) + // 列可见性状态,key为列id,value为true或false + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + // 列过滤状态,key为列id,value为过滤条件对象数组 + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + // 排序状态,key为列id,value为true或false + const [sorting, setSorting] = React.useState<SortingState>([]) + + // 使用外部分页状态或内部默认状态 + const [internalPagination, setInternalPagination] = React.useState<{ pageIndex: number, pageSize: number }>({ + pageIndex: 0, + pageSize: 10, + }) + + const pagination = externalPagination || internalPagination + const setPagination = setExternalPagination || setInternalPagination + + // 本地搜索输入状态(只在回车或点击按钮时触发搜索) + const [localSearchValue, setLocalSearchValue] = React.useState(searchValue ?? "") + + // 同步外部 searchValue 到本地状态 + React.useEffect(() => { + setLocalSearchValue(searchValue ?? "") + }, [searchValue]) + + const handleSearchSubmit = () => { + if (onSearch) { + onSearch(localSearchValue) + } else { + table.getColumn(searchColumn)?.setFilterValue(localSearchValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearchSubmit() + } + } + + // 过滤有效数据,确保每个行都有有效的 id + const validData = React.useMemo(() => { + const filtered = (data || []).filter(item => item && typeof item.id !== 'undefined' && item.id !== null) + console.log('数据表格接收到的原始数据:', data) + console.log('过滤后的有效数据:', filtered) + console.log('有效数据数量:', filtered.length) + return filtered + }, [data]) + + // 创建表格实例 + const table = useReactTable({ + data: validData, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination, + }, + // 服务端分页配置 + pageCount: paginationInfo?.totalPages ?? -1, + manualPagination: !!paginationInfo, // 如果有paginationInfo,使用服务端分页 + getRowId: (row) => row.id.toString(), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const newPagination = typeof updater === 'function' ? updater(pagination) : updater + setPagination(newPagination) + // 如果有外部分页回调,调用它 + if (onPaginationChange) { + onPaginationChange(newPagination) + } + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + // 监听选中行变化,通知父组件 + React.useEffect(() => { + if (onSelectionChange) { + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + onSelectionChange(selectedRows) + } + }, [rowSelection, onSelectionChange, table]) + + return ( + <div className="w-full space-y-4"> + {/* 工具栏 */} + <div className="flex items-center justify-between"> + {/* 搜索框 */} + <div className="flex items-center space-x-2"> + <Input + placeholder={searchPlaceholder} + value={localSearchValue} + onChange={(e) => setLocalSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="h-8 max-w-sm" + /> + <Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}> + {isSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + + {/* 右侧操作按钮 */} + <div className="flex items-center space-x-2"> + {/* 列显示控制 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + return ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {column.id === "id" && "ID"} + {column.id === "name" && "Subdomain"} + {column.id === "status" && "Status"} + {column.id === "title" && "Title"} + {column.id === "ip" && "IP"} + {column.id === "ports" && "Ports"} + {column.id === "contentLength" && "Content Length"} + {column.id === "screenshot" && "Screenshot"} + {column.id === "responseTime" && "Response Time"} + {column.id === "assetId" && "Target ID"} + {column.id === "asset" && "Target"} + {column.id === "createdAt" && "Created At"} + {column.id === "updatedAt" && "Updated At"} + {!["id", "name", "status", "title", "ip", "ports", "contentLength", "screenshot", "responseTime", "assetId", "asset", "createdAt", "updatedAt"].includes(column.id) && column.id} + </DropdownMenuCheckboxItem> + ) + })} + </DropdownMenuContent> + </DropdownMenu> + + {/* 下载按钮 */} + {(onDownloadAll || onDownloadInteresting || onDownloadImportant || onDownloadSelected) && ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconDownload /> + Download + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-64"> + <DropdownMenuLabel>Download Options</DropdownMenuLabel> + <DropdownMenuSeparator /> + {onDownloadAll && ( + <DropdownMenuItem onClick={onDownloadAll}> + <IconDownload className="h-4 w-4" /> + Download All Subdomains + </DropdownMenuItem> + )} + {onDownloadSelected && ( + <DropdownMenuItem + onClick={onDownloadSelected} + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + > + <IconDownload className="h-4 w-4" /> + Download Selected Subdomains + </DropdownMenuItem> + )} + {onDownloadImportant && ( + <DropdownMenuItem onClick={onDownloadImportant}> + <IconDownload className="h-4 w-4" /> + Download Important Subdomains + </DropdownMenuItem> + )} + </DropdownMenuContent> + </DropdownMenu> + )} + + {/* 批量删除按钮 */} + {onBulkDelete && ( + <Button + onClick={onBulkDelete} + size="sm" + variant="outline" + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + className={ + table.getFilteredSelectedRowModel().rows.length === 0 + ? "text-muted-foreground" + : "text-destructive hover:text-destructive hover:bg-destructive/10" + } + > + <IconTrash /> + Delete + </Button> + )} + + {/* 添加新域名按钮 */} + {onAddNew && ( + <Button onClick={onAddNew} size="sm"> + <IconPlus /> + {addButtonText} + </Button> + )} + </div> + </div> + + {/* 表格容器 */} + <div className="rounded-md border"> + <Table> + {/* 表头 */} + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id} colSpan={header.colSpan}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + + {/* 表体 */} + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + className="group" + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + No results + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + {/* 分页控制 */} + <div className="flex items-center justify-between px-2"> + {/* 选中行信息 */} + <div className="flex-1 text-sm text-muted-foreground"> + {table.getFilteredSelectedRowModel().rows.length} of{" "} + {paginationInfo ? paginationInfo.total : table.getFilteredRowModel().rows.length} row(s) selected + </div> + + {/* 分页控制器 */} + <div className="flex items-center space-x-6 lg:space-x-8"> + {/* 每页显示数量选择 */} + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + Rows per page + </Label> + <Select + value={`${table.getState().pagination.pageSize}`} + onValueChange={(value) => { + table.setPageSize(Number(value)) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={table.getState().pagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 页码信息 */} + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + </div> + + {/* 分页按钮 */} + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to first page</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to previous page</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to next page</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to last page</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + </div> + ) +} diff --git a/frontend/components/subdomains/subdomains-detail-view.tsx b/frontend/components/subdomains/subdomains-detail-view.tsx new file mode 100644 index 00000000..9e624b2e --- /dev/null +++ b/frontend/components/subdomains/subdomains-detail-view.tsx @@ -0,0 +1,271 @@ +"use client" + +import React, { useState, useMemo } from "react" +import { AlertTriangle } from "lucide-react" +import { useRouter } from "next/navigation" +import { useTarget } from "@/hooks/use-targets" +import { + useTargetSubdomains, + useScanSubdomains +} from "@/hooks/use-subdomains" +import { SubdomainsDataTable } from "./subdomains-data-table" +import { createSubdomainColumns } from "./subdomains-columns" +import { LoadingSpinner } from "@/components/loading-spinner" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" +import { SubdomainService } from "@/services/subdomain.service" +import type { Subdomain } from "@/types/subdomain.types" + +/** + * 子域名详情视图组件 + * 支持两种模式: + * 1. targetId: 显示目标下的所有子域名 + * 2. scanId: 显示扫描历史中的子域名 + */ +export function SubdomainsDetailView({ + targetId, + scanId +}: { + targetId?: number + scanId?: number +}) { + const [selectedSubdomains, setSelectedSubdomains] = useState<Subdomain[]>([]) + + // 分页状态 + const [pagination, setPagination] = useState({ + pageIndex: 0, // 0-based for react-table + pageSize: 10, + }) + + // 搜索状态(服务端搜索) + const [searchQuery, setSearchQuery] = useState("") + const [isSearching, setIsSearching] = useState(false) + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, pageIndex: 0 })) + } + + // 根据 targetId 或 scanId 获取子域名数据(传入分页和搜索参数) + const targetSubdomainsQuery = useTargetSubdomains( + targetId || 0, + { + page: pagination.pageIndex + 1, // 转换为 1-based + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }, + { enabled: !!targetId } + ) + const scanSubdomainsQuery = useScanSubdomains( + scanId || 0, + { + page: pagination.pageIndex + 1, // 转换为 1-based + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }, + { enabled: !!scanId } + ) + + // 选择当前使用的查询结果 + const activeQuery = targetId ? targetSubdomainsQuery : scanSubdomainsQuery + const { data: subdomainsData, isLoading, isFetching, error, refetch } = activeQuery + + // 当请求完成时重置搜索状态 + React.useEffect(() => { + if (!isFetching && isSearching) { + setIsSearching(false) + } + }, [isFetching, isSearching]) + + // 获取目标信息(仅在 targetId 模式下) + const { data: targetData } = useTarget(targetId || 0) + + // 辅助函数 - 格式化日期 + const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + } + + // 导航函数 + const router = useRouter() + const navigate = (path: string) => { + router.push(path) + } + + // 处理查看详细 + const handleViewDetail = (subdomain: Subdomain) => { + // TODO: 实现查看子域名详细功能 + console.log('查看子域名详细:', subdomain) + // 可以跳转到详情页或打开对话框 + } + + // 处理分页变化 + const handlePaginationChange = (newPagination: { pageIndex: number; pageSize: number }) => { + setPagination(newPagination) + } + + // 处理标记重要子域名 + const handleMarkImportant = (domain: Subdomain) => { + // TODO: 实现标记重要子域名功能 + console.log('标记重要子域名:', domain) + // 可以在这里调用 API 更新域名的 isImportant 状态 + } + + // 处理下载所有子域名 + const handleDownloadAll = async () => { + try { + let blob: Blob | null = null + + if (scanId) { + const data = await SubdomainService.exportSubdomainsByScanId(scanId) + blob = data + } else if (targetId) { + const data = await SubdomainService.exportSubdomainsByTargetId(targetId) + blob = data + } else { + if (!subdomains || subdomains.length === 0) { + return + } + const content = subdomains.map((item) => item.name).join("\n") + blob = new Blob([content], { type: "text/plain;charset=utf-8" }) + } + + if (!blob) return + + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + const prefix = scanId ? `scan-${scanId}` : targetId ? `target-${targetId}` : "subdomains" + a.href = url + a.download = `${prefix}-subdomains-${Date.now()}.txt` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } catch (error) { + console.error("下载子域名失败", error) + } + } + + // 处理下载选中的子域名 + const handleDownloadSelected = () => { + if (selectedSubdomains.length === 0) { + return + } + const content = selectedSubdomains.map((item) => item.name).join("\n") + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = `subdomains-selected-${scanId ?? targetId ?? "all"}-${Date.now()}.txt` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + // 处理下载有趣的子域名 + const handleDownloadInteresting = () => { + // TODO: 实现下载有趣的子域名功能 + console.log('下载有趣的子域名') + // 可以根据某些规则筛选有趣的子域名并下载 + } + + // 处理下载重要的子域名 + const handleDownloadImportant = () => { + // TODO: 实现下载重要的子域名功能 + console.log('下载重要的子域名') + // const a = document.createElement('a') + // a.href = url + // a.download = `selected-subdomains-${Date.now()}.txt` + // a.click() + // URL.revokeObjectURL(url) + } + + // 创建列定义 + const subdomainColumns = useMemo( + () => + createSubdomainColumns({ + formatDate, + navigate, + onViewDetail: handleViewDetail, + }), + [formatDate, navigate] + ) + + // 转换后端数据格式为前端 Subdomain 类型(必须在条件渲染之前调用) + // 注意:后端使用 djangorestframework-camel-case 自动转换字段名为 camelCase + const subdomains: Subdomain[] = useMemo(() => { + if (!subdomainsData?.results) return [] + return subdomainsData.results.map((item: any) => ({ + id: item.id, + name: item.name, + discoveredAt: item.discoveredAt, // 发现时间(后端已转换为 camelCase) + })) + }, [subdomainsData]) + + // 错误状态 + if (error) { + return ( + <div className="flex flex-col items-center justify-center py-12"> + <div className="rounded-full bg-destructive/10 p-3 mb-4"> + <AlertTriangle className="h-10 w-10 text-destructive" /> + </div> + <h3 className="text-lg font-semibold mb-2">加载失败</h3> + <p className="text-muted-foreground text-center mb-4"> + {error.message || "加载域名数据时出现错误,请重试"} + </p> + <button + onClick={() => refetch()} + className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" + > + 重新加载 + </button> + </div> + ) + } + + // 加载状态(仅首次加载时显示骨架屏,搜索时不显示) + if (isLoading && !subdomainsData) { + return ( + <DataTableSkeleton + toolbarButtonCount={2} + rows={6} + columns={5} + /> + ) + } + + return ( + <> + <SubdomainsDataTable + data={subdomains} + columns={subdomainColumns} + onSelectionChange={setSelectedSubdomains} + searchPlaceholder="搜索子域名..." + searchColumn="name" + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + onDownloadAll={handleDownloadAll} + onDownloadSelected={handleDownloadSelected} + onDownloadInteresting={handleDownloadInteresting} + pagination={pagination} + setPagination={setPagination} + paginationInfo={{ + total: subdomainsData?.total || 0, + page: subdomainsData?.page || 1, + pageSize: subdomainsData?.pageSize || 10, + totalPages: subdomainsData?.totalPages || 1, + }} + onPaginationChange={handlePaginationChange} + /> + </> + ) +} diff --git a/frontend/components/target/add-target-dialog.tsx b/frontend/components/target/add-target-dialog.tsx new file mode 100644 index 00000000..cece2538 --- /dev/null +++ b/frontend/components/target/add-target-dialog.tsx @@ -0,0 +1,515 @@ +"use client" + +import React, { useState, useRef } from "react" +import { Plus, Target as TargetIcon, Building2, Loader2, Check, ChevronsUpDown } from "lucide-react" +import { IconChevronLeft, IconChevronRight, IconChevronsLeft, IconChevronsRight } from "@tabler/icons-react" + +// 导入 UI 组件 +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { cn } from "@/lib/utils" +import { LoadingSpinner } from "@/components/loading-spinner" +import { TargetValidator } from "@/lib/target-validator" + +// 导入 React Query Hooks +import { useOrganizations } from "@/hooks/use-organizations" +import { useBatchCreateTargets } from "@/hooks/use-targets" +import { toast } from "sonner" +import type { BatchCreateTargetsRequest } from "@/types/target.types" + +// 组件属性类型定义 +interface AddTargetDialogProps { + onAdd?: () => void // 添加成功回调 + open?: boolean // 外部控制对话框开关状态 + onOpenChange?: (open: boolean) => void // 外部控制对话框开关回调 + prefetchEnabled?: boolean // 是否提前预取组织列表 +} + +/** + * 添加目标对话框组件(支持选择组织) + * + * 功能特性: + * 1. 批量输入目标 + * 2. 可选择所属组织 + * 3. 自动创建不存在的目标 + * 4. 自动管理提交状态 + * 5. 自动错误处理和成功提示 + */ +export function AddTargetDialog({ + onAdd, + open: externalOpen, + onOpenChange: externalOnOpenChange, + prefetchEnabled, +}: AddTargetDialogProps) { + // 对话框开关状态 - 支持外部控制 + const [internalOpen, setInternalOpen] = useState(false) + const open = externalOpen !== undefined ? externalOpen : internalOpen + const setOpen = externalOnOpenChange || setInternalOpen + const [orgPickerOpen, setOrgPickerOpen] = useState(false) + + // 表单数据状态 + const [formData, setFormData] = useState({ + targets: "", // 目标列表,每行一个 + organizationId: "", // 选择的组织ID + }) + + // 组织选择器状态 + const [orgSearchQuery, setOrgSearchQuery] = useState("") + const [orgPage, setOrgPage] = useState(1) + const [orgPageSize, setOrgPageSize] = useState(20) // 默认每页20条 + const pageSizeOptions = [20, 50, 200, 500, 1000] + + // 验证错误状态 + const [invalidTargets, setInvalidTargets] = useState<Array<{ index: number; originalTarget: string; error: string; type?: string }>>([]) + + // 使用批量创建目标 mutation + const batchCreateTargets = useBatchCreateTargets() + + // 行号列和输入框的 ref(用于同步滚动) + const lineNumbersRef = useRef<HTMLDivElement | null>(null) + const textareaRef = useRef<HTMLTextAreaElement | null>(null) + + // 获取组织列表(支持分页) + const shouldEnableOrgsQuery = Boolean(prefetchEnabled || orgPickerOpen) + const { data: organizationsData, isLoading: isLoadingOrganizations } = useOrganizations( + { + page: orgPage, + pageSize: orgPageSize, // 动态每页数量 + }, + { enabled: shouldEnableOrgsQuery } + ) + + // 处理输入框变化 + const handleInputChange = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })) + + if (field === "targets") { + const lines = value + .split("\n") + .map((s) => s.trim()) + .filter((s) => s.length > 0) + + if (lines.length === 0) { + setInvalidTargets([]) + return + } + + const results = TargetValidator.validateTargetBatch(lines) + const invalid = results + .filter((r) => !r.isValid) + .map((r) => ({ index: r.index, originalTarget: r.originalTarget, error: r.error || "目标格式无效", type: r.type })) + setInvalidTargets(invalid) + } + } + + // 计算目标数量 + const targetCount = formData.targets + .split("\n") + .map(line => line.trim()) + .filter(line => line.length > 0).length + + // 处理表单提交 + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + // 表单验证 + if (!formData.targets.trim()) { + return + } + + if (invalidTargets.length > 0) { + return + } + + // 解析目标列表(每行一个目标) + const targetList = formData.targets + .split("\n") + .map(line => line.trim()) + .filter(line => line.length > 0) + .map(name => ({ + name, + })) + + if (targetList.length === 0) { + return + } + + // 组装请求数据(组织为可选字段) + const payload: BatchCreateTargetsRequest = { + targets: targetList, + } + + if (formData.organizationId) { + payload.organizationId = parseInt(formData.organizationId, 10) + } + + // 调用批量创建 API + batchCreateTargets.mutate( + payload, + { + onSuccess: (batchCreateResult) => { + // 重置表单 + setFormData({ + targets: "", + organizationId: "", + }) + setInvalidTargets([]) + setOrgSearchQuery("") + setOrgPage(1) + setOrgPageSize(20) + + // 关闭对话框 + setOpen(false) + + // 调用外部回调(如果提供) + if (onAdd) { + onAdd() + } + } + } + ) + } + + // 处理对话框关闭 + const handleOpenChange = (newOpen: boolean) => { + if (!batchCreateTargets.isPending) { + setOpen(newOpen) + if (!newOpen) { + // 关闭时重置表单 + setFormData({ + targets: "", + organizationId: "", + }) + setInvalidTargets([]) + setOrgSearchQuery("") + setOrgPage(1) + setOrgPageSize(20) // 重置为默认值 + } + } + } + + // 表单验证 + const isFormValid = formData.targets.trim().length > 0 && invalidTargets.length === 0 + + // 同步输入框和行号列的滚动 + const handleTextareaScroll = (e: React.UIEvent<HTMLTextAreaElement>) => { + if (lineNumbersRef.current) { + lineNumbersRef.current.scrollTop = e.currentTarget.scrollTop + } + } + + // 获取选中的组织名称 + const [selectedOrgName, setSelectedOrgName] = useState("") + const selectedOrganization = organizationsData?.organizations.find( + org => org.id.toString() === formData.organizationId + ) + + // 更新选中组织的名称 + React.useEffect(() => { + if (selectedOrganization) { + setSelectedOrgName(selectedOrganization.name) + } + }, [selectedOrganization]) + + // 过滤组织列表 + const filteredOrganizations = React.useMemo(() => { + if (!organizationsData?.organizations) return [] + if (!orgSearchQuery) return organizationsData.organizations + return organizationsData.organizations.filter(org => + org.name.toLowerCase().includes(orgSearchQuery.toLowerCase()) + ) + }, [organizationsData?.organizations, orgSearchQuery]) + + // 处理组织选择 + const handleSelectOrganization = (orgId: string, orgName: string) => { + handleInputChange("organizationId", orgId) + setSelectedOrgName(orgName) + setOrgPickerOpen(false) + setOrgSearchQuery("") + setOrgPage(1) + setOrgPageSize(20) // 重置为默认值 + } + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + {/* 触发按钮 - 仅在非外部控制时显示 */} + {externalOpen === undefined && ( + <DialogTrigger asChild> + <Button size="sm"> + <Plus /> + 添加目标 + </Button> + </DialogTrigger> + )} + + {/* 对话框内容 */} + <DialogContent className="sm:max-w-[650px] max-h-[90vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle className="flex items-center space-x-2"> + <TargetIcon /> + <span>添加目标</span> + </DialogTitle> + <DialogDescription> + 输入目标并关联到组织。支持批量添加,每行一个目标。标有 * 的字段为必填项。 + </DialogDescription> + </DialogHeader> + + {/* 表单 */} + <form onSubmit={handleSubmit}> + <div className="grid gap-4 py-4"> + {/* 目标输入框(支持多行) */} + <div className="grid gap-2"> + <Label htmlFor="targets"> + 目标列表 <span className="text-destructive">*</span> + </Label> + <div className="flex border rounded-md overflow-hidden h-[180px]"> + {/* 行号列 - 固定宽度 */} + <div className="flex-shrink-0 w-12 border-r bg-muted/50"> + <div + ref={lineNumbersRef} + className="py-3 px-2 text-right font-mono text-xs text-muted-foreground leading-[1.4] h-full overflow-y-auto scrollbar-hide" + > + {Array.from({ length: Math.max(formData.targets.split('\n').length, 8) }, (_, i) => ( + <div key={i + 1} className="h-[20px]"> + {i + 1} + </div> + ))} + </div> + </div> + {/* 输入框区域 - 占据剩余空间 */} + <div className="flex-1 overflow-hidden"> + {/* 输入框 - 固定高度显示8行 */} + <Textarea + ref={textareaRef} + id="targets" + value={formData.targets} + onChange={(e) => handleInputChange("targets", e.target.value)} + onScroll={handleTextareaScroll} + placeholder={`请输入目标,每行一个 +支持域名、IP、CIDR +例如: +example.com +192.168.1.1 +10.0.0.0/8`} + disabled={batchCreateTargets.isPending} + className="font-mono h-full overflow-y-auto resize-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 leading-[1.4] text-sm py-3" + style={{ lineHeight: '20px' }} + /> + </div> + </div> + <div className="text-xs text-muted-foreground"> + {targetCount} 个目标 + </div> + {invalidTargets.length > 0 && ( + <div className="text-xs text-destructive"> + {invalidTargets.length} 个无效目标,例如 第 {invalidTargets[0].index + 1} 行: "{invalidTargets[0].originalTarget}" - {invalidTargets[0].error} + </div> + )} + </div> + + {/* 所属组织(可选择、可搜索、分页) */} + <div className="grid gap-2"> + <Label htmlFor="organization"> + 关联组织(可选) + </Label> + <Button + variant="outline" + role="combobox" + className="w-full justify-between" + onClick={() => setOrgPickerOpen(true)} + disabled={batchCreateTargets.isPending || isLoadingOrganizations} + > + {isLoadingOrganizations ? ( + <span className="flex items-center gap-2"> + <Loader2 className="h-4 w-4 animate-spin" /> + 加载中... + </span> + ) : formData.organizationId ? ( + <span className="flex items-center gap-2"> + <Building2 className="h-4 w-4" /> + <span className="truncate">{selectedOrgName}</span> + </span> + ) : ( + "请选择组织" + )} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + <CommandDialog + open={orgPickerOpen} + onOpenChange={(o) => { + setOrgPickerOpen(o) + if (!o) { + setOrgSearchQuery("") + setOrgPage(1) + setOrgPageSize(20) + } + }} + > + <CommandInput + placeholder="搜索组织..." + value={orgSearchQuery} + onValueChange={(v) => setOrgSearchQuery(v)} + /> + <CommandList className="max-h-[300px] overflow-y-auto overscroll-contain"> + {isLoadingOrganizations ? ( + <div className="py-6 text-center text-sm"> + <Loader2 className="mx-auto h-4 w-4 animate-spin" /> + </div> + ) : filteredOrganizations.length === 0 ? ( + <CommandEmpty>未找到组织</CommandEmpty> + ) : ( + <CommandGroup> + <div className="grid grid-cols-2 gap-1 p-1"> + {filteredOrganizations.map((org) => ( + <CommandItem + key={org.id} + value={org.id.toString()} + onSelect={() => handleSelectOrganization(org.id.toString(), org.name)} + className="cursor-pointer" + > + <Check + className={cn( + "mr-1 h-3.5 w-3.5 flex-shrink-0", + formData.organizationId === org.id.toString() + ? "opacity-100" + : "opacity-0" + )} + /> + <Building2 className="mr-1 h-3.5 w-3.5 flex-shrink-0" /> + <span className="font-medium text-sm truncate">{org.name}</span> + </CommandItem> + ))} + </div> + </CommandGroup> + )} + </CommandList> + {organizationsData && ( + <div className="flex items-center justify-between border-t p-2 bg-muted/50"> + <div className="text-xs text-muted-foreground"> + 共 {organizationsData.pagination.total} 个组织 · 第 {organizationsData.pagination.page} / {organizationsData.pagination.totalPages} 页 + </div> + <div className="flex items-center gap-2"> + <div className="flex items-center gap-1"> + <span className="text-xs text-muted-foreground">每页:</span> + <Select value={orgPageSize.toString()} onValueChange={(value) => { + setOrgPageSize(Number(value)) + setOrgPage(1) + }}> + <SelectTrigger className="h-7 w-16 text-xs"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + {pageSizeOptions.map((size) => ( + <SelectItem key={size} value={size.toString()}> + {size} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => setOrgPage(1)} + disabled={orgPage === 1 || isLoadingOrganizations} + > + <span className="sr-only">第一页</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => setOrgPage(prev => Math.max(1, prev - 1))} + disabled={orgPage === 1 || isLoadingOrganizations} + > + <span className="sr-only">上一页</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => setOrgPage(prev => Math.min(organizationsData.pagination.totalPages, prev + 1))} + disabled={orgPage === organizationsData.pagination.totalPages || isLoadingOrganizations} + > + <span className="sr-only">下一页</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => setOrgPage(organizationsData.pagination.totalPages)} + disabled={orgPage === organizationsData.pagination.totalPages || isLoadingOrganizations} + > + <span className="sr-only">最后一页</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + )} + </CommandDialog> + </div> + </div> + + {/* 对话框底部按钮 */} + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={batchCreateTargets.isPending} + > + 取消 + </Button> + <Button + type="submit" + disabled={batchCreateTargets.isPending || !isFormValid} + > + {batchCreateTargets.isPending ? ( + <> + <LoadingSpinner/> + 创建中... + </> + ) : ( + <> + <Plus /> + 创建目标 + </> + )} + </Button> + </DialogFooter> + </form> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/target/all-targets-columns.tsx b/frontend/components/target/all-targets-columns.tsx new file mode 100644 index 00000000..4e13724c --- /dev/null +++ b/frontend/components/target/all-targets-columns.tsx @@ -0,0 +1,406 @@ +"use client" + +import React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { Checkbox } from "@/components/ui/checkbox" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { MoreHorizontal, Eye, Trash2, ChevronsUpDown, ChevronUp, ChevronDown, Play, Calendar, Copy, Check } from "lucide-react" +import { Badge } from "@/components/ui/badge" +import { toast } from "sonner" +import type { Target } from "@/types/target.types" + +/** + * 复制到剪贴板(兼容 HTTP 环境) + */ +async function copyToClipboard(text: string): Promise<boolean> { + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text) + } else { + // Fallback: 使用临时 textarea + const textArea = document.createElement('textarea') + textArea.value = text + textArea.style.position = 'fixed' + textArea.style.left = '-9999px' + textArea.style.top = '-9999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + document.execCommand('copy') + document.body.removeChild(textArea) + } + return true + } catch { + return false + } +} + +/** + * 目标名称单元格组件 + */ +function TargetNameCell({ + name, + targetId, + navigate +}: { + name: string + targetId: number + navigate: (path: string) => void +}) { + const [copied, setCopied] = React.useState(false) + + const handleCopy = async (e: React.MouseEvent) => { + e.stopPropagation() + const success = await copyToClipboard(name) + if (success) { + setCopied(true) + toast.success("已复制目标名称") + setTimeout(() => setCopied(false), 2000) + } else { + toast.error("复制失败") + } + } + + return ( + <div className="group inline-flex items-center gap-1 max-w-[350px]"> + <Tooltip> + <TooltipTrigger asChild> + <span + onClick={() => navigate(`/target/${targetId}/details`)} + className="text-sm font-medium hover:text-primary hover:underline underline-offset-2 transition-colors cursor-pointer truncate" + > + {name} + </span> + </TooltipTrigger> + <TooltipContent>目标详情</TooltipContent> + </Tooltip> + <TooltipProvider delayDuration={300}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className={`h-6 w-6 flex-shrink-0 hover:bg-accent transition-opacity ${ + copied ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' + }`} + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">{copied ? '已复制!' : '点击复制'}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + ) +} + +// 列创建函数的参数类型 +interface CreateColumnsProps { + formatDate: (dateString: string) => string + navigate: (path: string) => void + handleDelete: (target: Target) => void + handleInitiateScan: (target: Target) => void + handleScheduleScan: (target: Target) => void +} + +/** + * 目标行操作组件 + */ +function TargetRowActions({ + target, + onView, + onInitiateScan, + onScheduleScan, + onDelete, +}: { + target: Target + onView: () => void + onInitiateScan: () => void + onScheduleScan: () => void + onDelete: () => void +}) { + return ( + <div className="flex items-center gap-1"> + {/* Target Summary 按钮 */} + <TooltipProvider delayDuration={300}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className="h-8 w-8" + onClick={onView} + > + <Eye className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">Target Summary</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + + {/* Initiate Scan 按钮 */} + <TooltipProvider delayDuration={300}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className="h-8 w-8" + onClick={onInitiateScan} + > + <Play className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">Initiate Scan</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + + {/* 更多操作菜单 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + className="h-8 w-8 p-0 data-[state=open]:bg-muted" + > + <MoreHorizontal className="h-4 w-4" /> + <span className="sr-only">更多操作</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-48"> + <DropdownMenuItem onClick={onScheduleScan}> + <Calendar /> + Schedule Scan + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={onDelete} + className="text-destructive focus:text-destructive" + > + <Trash2 /> + Delete + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + </div> + ) +} + +/** + * 数据表格列头组件 + */ +function DataTableColumnHeader({ + column, + title, +}: { + column: { getCanSort: () => boolean; getIsSorted: () => false | "asc" | "desc"; toggleSorting: (desc?: boolean) => void } + title: string +}) { + if (!column.getCanSort()) { + return <div className="-ml-3 font-medium">{title}</div> + } + + const isSorted = column.getIsSorted() + + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted" + > + {title} + {isSorted === "asc" ? ( + <ChevronUp /> + ) : isSorted === "desc" ? ( + <ChevronDown /> + ) : ( + <ChevronsUpDown /> + )} + </Button> + ) +} + +/** + * 创建所有目标表格列定义 + */ +export const createAllTargetsColumns = ({ + formatDate, + navigate, + handleDelete, + handleInitiateScan, + handleScheduleScan, +}: CreateColumnsProps): ColumnDef<Target>[] => [ + // 选择列 + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + + // 目标名称列 + { + accessorKey: "name", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Target" /> + ), + cell: ({ row }) => ( + <TargetNameCell + name={row.getValue("name") as string} + targetId={row.original.id} + navigate={navigate} + /> + ), + }, + + // 所属组织列 + { + accessorKey: "organizations", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Organization" /> + ), + cell: ({ row }) => { + const organizations = row.getValue("organizations") as Array<{ id: number; name: string }> | undefined + if (!organizations || organizations.length === 0) { + return <span className="text-sm text-muted-foreground">-</span> + } + + const displayOrgs = organizations.slice(0, 2) + const remainingCount = organizations.length - 2 + + return ( + <div className="flex flex-wrap gap-1"> + {displayOrgs.map((org) => ( + <Badge + key={org.id} + variant="secondary" + className="text-xs" + title={org.name} + > + {org.name} + </Badge> + ))} + {remainingCount > 0 && ( + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Badge + variant="outline" + className="text-xs cursor-default" + > + +{remainingCount} + </Badge> + </TooltipTrigger> + <TooltipContent + side="top" + align="start" + sideOffset={5} + className="max-w-sm" + > + <div className="flex flex-col gap-1"> + {organizations.slice(2).map((org) => ( + <div key={org.id} className="text-xs"> + {org.name} + </div> + ))} + </div> + </TooltipContent> + </Tooltip> + </TooltipProvider> + )} + </div> + ) + }, + enableSorting: false, + }, + + // 创建时间列 + { + accessorKey: "createdAt", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Added On" /> + ), + cell: ({ row }) => { + const createdAt = row.getValue("createdAt") as string + return ( + <div className="text-sm text-muted-foreground"> + {formatDate(createdAt)} + </div> + ) + }, + }, + + // 最后扫描时间列 + { + accessorKey: "lastScannedAt", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Last Scanned" /> + ), + cell: ({ row }) => { + const lastScannedAt = row.original.lastScannedAt + if (!lastScannedAt) { + return <span className="text-sm text-muted-foreground">-</span> + } + return ( + <div className="text-sm text-muted-foreground"> + {formatDate(lastScannedAt)} + </div> + ) + }, + }, + + // 操作列 + { + id: "actions", + cell: ({ row }) => ( + <TargetRowActions + target={row.original} + onView={() => navigate(`/target/${row.original.id}/details`)} + onInitiateScan={() => handleInitiateScan(row.original)} + onScheduleScan={() => handleScheduleScan(row.original)} + onDelete={() => handleDelete(row.original)} + /> + ), + enableSorting: false, + enableHiding: false, + }, +] diff --git a/frontend/components/target/all-targets-detail-view.tsx b/frontend/components/target/all-targets-detail-view.tsx new file mode 100644 index 00000000..7a531222 --- /dev/null +++ b/frontend/components/target/all-targets-detail-view.tsx @@ -0,0 +1,299 @@ +"use client" + +import React, { useState, useCallback } from "react" +import { useRouter } from "next/navigation" +import { createAllTargetsColumns } from "@/components/target/all-targets-columns" +import { TargetsDataTable } from "@/components/target/targets-data-table" +import { AddTargetDialog } from "@/components/target/add-target-dialog" +import { InitiateScanDialog } from "@/components/scan/initiate-scan-dialog" +import { CreateScheduledScanDialog } from "@/components/scan/scheduled/create-scheduled-scan-dialog" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { formatDate } from "@/lib/utils" +import { LoadingSpinner } from "@/components/loading-spinner" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" +import { useTargets, useDeleteTarget, useBatchDeleteTargets } from "@/hooks/use-targets" +import type { Target } from "@/types/target.types" +import type { Organization } from "@/types/organization.types" + +/** + * 所有目标详情视图组件 + * 显示系统中所有目标的列表,支持搜索、分页、删除等操作 + */ +export function AllTargetsDetailView() { + const router = useRouter() + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }) + const [searchQuery, setSearchQuery] = useState("") + const [selectedTargets, setSelectedTargets] = useState<Target[]>([]) + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [targetToDelete, setTargetToDelete] = useState<Target | null>(null) + const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false) + const [shouldPrefetchOrgs, setShouldPrefetchOrgs] = useState(false) + const [initiateScanDialogOpen, setInitiateScanDialogOpen] = useState(false) + const [scheduleScanDialogOpen, setScheduleScanDialogOpen] = useState(false) + const [targetToScan, setTargetToScan] = useState<Target | null>(null) + const [targetToSchedule, setTargetToSchedule] = useState<Target | null>(null) + + // 处理分页状态变化 + const handlePaginationChange = React.useCallback((newPagination: { pageIndex: number, pageSize: number }) => { + setPagination(newPagination) + }, []) + + const [isSearching, setIsSearching] = React.useState(false) + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, pageIndex: 0 })) + } + + // 使用 API hooks + const { data, isLoading, isFetching, error } = useTargets(pagination.pageIndex + 1, pagination.pageSize, undefined, searchQuery || undefined) + const deleteTargetMutation = useDeleteTarget() + const batchDeleteMutation = useBatchDeleteTargets() + + const targets = data?.results || [] + const totalCount = data?.total || 0 + + React.useEffect(() => { + if (!isFetching && isSearching) { + setIsSearching(false) + } + }, [isFetching, isSearching]) + + // 处理添加目标 + const handleAddTarget = useCallback(() => { + setIsAddDialogOpen(true) + }, []) + + // 处理删除单个目标 + const handleDeleteTarget = useCallback((target: Target) => { + setTargetToDelete(target) + setDeleteDialogOpen(true) + }, []) + + // 确认删除目标 + const confirmDelete = async () => { + if (!targetToDelete) return + + try { + await deleteTargetMutation.mutateAsync(targetToDelete.id) + setDeleteDialogOpen(false) + setTargetToDelete(null) + } catch (error) { + // 错误已在 hook 中处理 + console.error('删除失败:', error) + } + } + + // 处理批量删除 + const handleBatchDelete = useCallback(() => { + if (selectedTargets.length === 0) return + setBulkDeleteDialogOpen(true) + }, [selectedTargets]) + + // 确认批量删除 + const confirmBulkDelete = async () => { + if (selectedTargets.length === 0) return + + try { + await batchDeleteMutation.mutateAsync({ + ids: selectedTargets.map((t) => t.id), + }) + setBulkDeleteDialogOpen(false) + setSelectedTargets([]) + } catch (error) { + // 错误已在 hook 中处理 + console.error('批量删除失败:', error) + } + } + + // 处理发起扫描 + const handleInitiateScan = useCallback((target: Target) => { + setTargetToScan(target) + setInitiateScanDialogOpen(true) + }, []) + + // 处理定时扫描 + const handleScheduleScan = useCallback((target: Target) => { + setTargetToSchedule(target) + setScheduleScanDialogOpen(true) + }, []) + + // 创建表格列 + const columns = createAllTargetsColumns({ + formatDate, + navigate: (path: string) => router.push(path), + handleDelete: handleDeleteTarget, + handleInitiateScan, + handleScheduleScan, + }) + + // 加载中 + if (isLoading) { + return ( + <DataTableSkeleton + toolbarButtonCount={2} + rows={6} + columns={5} + /> + ) + } + + // 错误处理 + if (error) { + return ( + <div className="flex items-center justify-center h-64"> + <div className="text-center"> + <p className="text-destructive mb-2">加载失败</p> + <p className="text-sm text-muted-foreground">{error.message}</p> + </div> + </div> + ) + } + + return ( + <> + <TargetsDataTable + data={targets} + columns={columns} + onAddNew={handleAddTarget} + onAddHover={() => setShouldPrefetchOrgs(true)} + onBulkDelete={handleBatchDelete} + onSelectionChange={setSelectedTargets} + searchPlaceholder="搜索目标名称..." + searchColumn="name" + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + addButtonText="添加目标" + // 分页相关属性 + pagination={pagination} + onPaginationChange={handlePaginationChange} + totalCount={totalCount} + manualPagination={true} + /> + + {/* 添加目标对话框 */} + <AddTargetDialog + onAdd={() => { + setIsAddDialogOpen(false) + }} + open={isAddDialogOpen} + onOpenChange={setIsAddDialogOpen} + prefetchEnabled={shouldPrefetchOrgs} + /> + + {/* 删除确认对话框 */} + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除目标</AlertDialogTitle> + <AlertDialogDescription> + 此操作无法撤销。这将永久删除目标 "{targetToDelete?.name}" 及其所有关联数据。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + disabled={deleteTargetMutation.isPending} + > + {deleteTargetMutation.isPending ? ( + <> + <LoadingSpinner/> + 删除中... + </> + ) : ( + "确认删除" + )} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + + {/* 发起扫描对话框 */} + <InitiateScanDialog + organization={ + targetToScan?.organizations && targetToScan.organizations.length > 0 + ? { + id: targetToScan.organizations[0].id, + name: targetToScan.organizations[0].name, + targetCount: 1, // 当前目标 + } as Organization + : null + } + targetId={targetToScan?.id} + targetName={targetToScan?.name} + open={initiateScanDialogOpen} + onOpenChange={setInitiateScanDialogOpen} + onSuccess={() => { + setTargetToScan(null) + }} + /> + + {/* 定时扫描对话框 */} + <CreateScheduledScanDialog + open={scheduleScanDialogOpen} + onOpenChange={setScheduleScanDialogOpen} + presetTargetId={targetToSchedule?.id} + presetTargetName={targetToSchedule?.name} + onSuccess={() => { + setTargetToSchedule(null) + }} + /> + + {/* 批量删除确认对话框 */} + <AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认批量删除目标</AlertDialogTitle> + <AlertDialogDescription> + 此操作无法撤销。这将永久删除以下 {selectedTargets.length} 个目标及其所有关联数据。 + </AlertDialogDescription> + </AlertDialogHeader> + {/* 目标列表容器 - 固定最大高度并支持滚动 */} + <div className="mt-2 p-2 bg-muted rounded-md max-h-96 overflow-y-auto"> + <ul className="text-sm space-y-1"> + {selectedTargets.map((target) => ( + <li key={target.id} className="flex items-center"> + <span className="font-medium">{target.name}</span> + {target.description && ( + <span className="text-muted-foreground ml-2">- {target.description}</span> + )} + </li> + ))} + </ul> + </div> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmBulkDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + disabled={batchDeleteMutation.isPending} + > + {batchDeleteMutation.isPending ? ( + <> + <LoadingSpinner/> + 删除中... + </> + ) : ( + `确认删除 ${selectedTargets.length} 个目标` + )} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </> + ) +} diff --git a/frontend/components/target/index.ts b/frontend/components/target/index.ts new file mode 100644 index 00000000..f9de89f8 --- /dev/null +++ b/frontend/components/target/index.ts @@ -0,0 +1,8 @@ +/** + * Target Components - 统一导出 + */ +export { TargetsDataTable } from './targets-data-table' +export { createAllTargetsColumns } from './all-targets-columns' +export { AllTargetsDetailView } from './all-targets-detail-view' +export { AddTargetDialog } from './add-target-dialog' + diff --git a/frontend/components/target/targets-data-table.tsx b/frontend/components/target/targets-data-table.tsx new file mode 100644 index 00000000..9a391ca5 --- /dev/null +++ b/frontend/components/target/targets-data-table.tsx @@ -0,0 +1,415 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconPlus, + IconTrash, + IconSearch, + IconLoader2, +} from "@tabler/icons-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import type { Target } from "@/types/target.types" + +interface TargetsDataTableProps { + data: Target[] + columns: ColumnDef<Target>[] + onAddNew?: () => void + onAddHover?: () => void + onBulkDelete?: () => void + onSelectionChange?: (selectedRows: Target[]) => void + searchPlaceholder?: string + searchColumn?: string + searchValue?: string + onSearch?: (value: string) => void + isSearching?: boolean + addButtonText?: string + // 分页相关属性 + pagination?: { pageIndex: number, pageSize: number } + onPaginationChange?: (pagination: { pageIndex: number, pageSize: number }) => void + totalCount?: number + manualPagination?: boolean +} + +/** + * 目标数据表格组件 + * 专门用于显示和管理目标数据的表格 + * 包含搜索、分页、列显示控制等功能 + */ +export function TargetsDataTable({ + data = [], + columns, + onAddNew, + onAddHover, + onBulkDelete, + onSelectionChange, + searchPlaceholder = "搜索目标名称...", + searchColumn = "name", + searchValue, + onSearch, + isSearching = false, + addButtonText = "添加目标", + pagination: externalPagination, + onPaginationChange, + totalCount, + manualPagination = false, +}: TargetsDataTableProps) { + const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({}) + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [sorting, setSorting] = React.useState<SortingState>([]) + // 使用外部分页状态或内部状态 + const [internalPagination, setInternalPagination] = React.useState<{ pageIndex: number, pageSize: number }>({ + pageIndex: 0, + pageSize: 10, + }) + + // 本地搜索输入状态(只在回车或点击按钮时触发搜索) + const [localSearchValue, setLocalSearchValue] = React.useState(searchValue ?? "") + + React.useEffect(() => { + setLocalSearchValue(searchValue ?? "") + }, [searchValue]) + + const handleSearchSubmit = () => { + if (onSearch) { + onSearch(localSearchValue) + } else { + table.getColumn(searchColumn)?.setFilterValue(localSearchValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearchSubmit() + } + } + + const pagination = externalPagination || internalPagination + + // 处理分页状态变化 + const handlePaginationChange = React.useCallback((updaterOrValue: any) => { + if (onPaginationChange) { + // 如果是函数,先计算新值 + const newPagination = typeof updaterOrValue === 'function' + ? updaterOrValue(pagination) + : updaterOrValue + onPaginationChange(newPagination) + } else { + // 使用内部状态 + setInternalPagination(updaterOrValue) + } + }, [onPaginationChange, pagination]) + + const validData = React.useMemo(() => { + const filtered = (data || []).filter(item => item && typeof item.id !== 'undefined' && item.id !== null) + return filtered + }, [data]) + + const table = useReactTable({ + data: validData, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination, + }, + getRowId: (row) => row.id.toString(), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: handlePaginationChange, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: manualPagination ? undefined : getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + // 手动分页配置 + manualPagination, + pageCount: manualPagination && totalCount ? Math.ceil(totalCount / pagination.pageSize) : undefined, + }) + + React.useEffect(() => { + if (onSelectionChange) { + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + onSelectionChange(selectedRows) + } + }, [rowSelection, onSelectionChange, table]) + + return ( + <div className="w-full space-y-4"> + {/* 工具栏 */} + <div className="flex items-center justify-between"> + {/* 搜索框 */} + <div className="flex items-center space-x-2"> + <Input + placeholder={searchPlaceholder} + value={localSearchValue} + onChange={(e) => setLocalSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="h-8 max-w-sm" + /> + <Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}> + {isSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + + {/* 右侧操作按钮 */} + <div className="flex items-center space-x-2"> + {/* 列显示控制 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + return ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {column.id === "id" && "ID"} + {column.id === "name" && "目标名称"} + {column.id === "type" && "类型"} + {column.id === "organizations" && "所属组织"} + {column.id === "domainCount" && "域名数"} + {column.id === "endpointCount" && "URL 数"} + {!["id", "name", "type", "organizations", "domainCount", "endpointCount"].includes(column.id) && column.id} + </DropdownMenuCheckboxItem> + ) + })} + </DropdownMenuContent> + </DropdownMenu> + + {/* 批量删除按钮 */} + {onBulkDelete && ( + <Button + onClick={onBulkDelete} + size="sm" + variant="outline" + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + className={ + table.getFilteredSelectedRowModel().rows.length === 0 + ? "text-muted-foreground" + : "text-destructive hover:text-destructive hover:bg-destructive/10" + } + > + <IconTrash /> + 删除 + </Button> + )} + + {/* 添加新目标按钮 */} + {onAddNew && ( + <Button onClick={onAddNew} onMouseEnter={onAddHover} size="sm"> + <IconPlus /> + {addButtonText} + </Button> + )} + </div> + </div> + + {/* 表格容器 */} + <div className="rounded-md border"> + <Table> + {/* 表头 */} + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id} colSpan={header.colSpan}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + + {/* 表体 */} + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + className="group" + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + 暂无数据 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + {/* 分页控制 */} + <div className="flex items-center justify-between px-2"> + {/* 选中行信息 */} + <div className="flex-1 text-sm text-muted-foreground"> + 已选择 {table.getFilteredSelectedRowModel().rows.length} 个,共{" "} + {manualPagination && totalCount ? totalCount : table.getFilteredRowModel().rows.length} 个 + </div> + + {/* 分页控制器 */} + <div className="flex items-center space-x-6 lg:space-x-8"> + {/* 每页显示数量选择 */} + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + 每页显示 + </Label> + <Select + value={`${table.getState().pagination.pageSize}`} + onValueChange={(value) => { + table.setPageSize(Number(value)) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={table.getState().pagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 页码信息 */} + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + 第 {table.getState().pagination.pageIndex + 1} 页,共{" "} + {manualPagination && totalCount ? Math.ceil(totalCount / pagination.pageSize) : table.getPageCount()} 页 + </div> + + {/* 分页按钮 */} + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">First page</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Previous page</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Next page</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Last page</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + </div> + ) +} diff --git a/frontend/components/theme-toggle.tsx b/frontend/components/theme-toggle.tsx new file mode 100644 index 00000000..1df0ff53 --- /dev/null +++ b/frontend/components/theme-toggle.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import { flushSync } from "react-dom" +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" +import { cn } from "@/lib/utils" + +/** + * 主题切换组件 - 滑动开关样式 + 圆形扩展动画 + */ +export function ThemeToggle() { + const { setTheme, resolvedTheme } = useTheme() + const [mounted, setMounted] = React.useState(false) + const [isToggled, setIsToggled] = React.useState(false) + const buttonRef = React.useRef<HTMLButtonElement>(null) + + React.useEffect(() => { + setMounted(true) + }, []) + + React.useEffect(() => { + if (mounted) { + setIsToggled(resolvedTheme === "dark") + } + }, [resolvedTheme, mounted]) + + const handleToggle = async () => { + const newIsDark = !isToggled + const newTheme = newIsDark ? "dark" : "light" + + // 不支持 View Transitions 或用户偏好减少动画,直接切换 + if ( + !buttonRef.current || + !('startViewTransition' in document) || + window.matchMedia('(prefers-reduced-motion: reduce)').matches + ) { + setIsToggled(newIsDark) + setTheme(newTheme) + return + } + + // 1. 先让滑块滑动(不触发主题切换) + setIsToggled(newIsDark) + + // 2. 等待滑块动画完成(100ms) + await new Promise(r => setTimeout(r, 100)) + + // 获取按钮位置 + const { top, left, width, height } = buttonRef.current.getBoundingClientRect() + const x = left + width / 2 + const y = top + height / 2 + const right = window.innerWidth - left + const bottom = window.innerHeight - top + const maxRadius = Math.hypot(Math.max(left, right), Math.max(top, bottom)) + + // 禁用默认的 View Transition 动画 + const style = document.createElement('style') + style.textContent = ` + ::view-transition-old(root), + ::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; + } + ` + document.head.appendChild(style) + + // 3. 滑块滑完后,启动 View Transition 切换主题 + const transition = (document as any).startViewTransition(() => { + flushSync(() => { + setTheme(newTheme) + }) + }) + + await transition.ready + + document.documentElement.animate( + { + clipPath: [ + `circle(0px at ${x}px ${y}px)`, + `circle(${maxRadius}px at ${x}px ${y}px)`, + ], + }, + { + duration: 500, + easing: 'ease-out', + pseudoElement: '::view-transition-new(root)', + } + ) + + transition.finished.then(() => { + style.remove() + }) + } + + if (!mounted) { + return ( + <div className="w-11 h-6 rounded-full bg-gray-200 dark:bg-primary" /> + ) + } + + return ( + <button + ref={buttonRef} + onClick={handleToggle} + className={cn( + "relative w-11 h-6 rounded-full transition-colors duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring", + isToggled ? "bg-primary" : "bg-gray-200" + )} + aria-label={isToggled ? "切换到亮色模式" : "切换到暗色模式"} + > + <div + className={cn( + "absolute top-0.5 left-0.5 w-5 h-5 rounded-full shadow-md flex items-center justify-center", + "transition-all ease-in-out", + isToggled ? "translate-x-5 bg-primary-foreground" : "translate-x-0 bg-white" + )} + style={{ transitionDuration: "100ms" }} + > + <Sun + className={cn( + "h-3 w-3 absolute transition-all duration-200 text-primary", + isToggled ? "opacity-0 rotate-90 scale-0" : "opacity-100 rotate-0 scale-100" + )} + /> + <Moon + className={cn( + "h-3 w-3 absolute transition-all duration-200 text-primary", + isToggled ? "opacity-100 rotate-0 scale-100" : "opacity-0 -rotate-90 scale-0" + )} + /> + </div> + </button> + ) +} diff --git a/frontend/components/tools/commands/commands-columns.tsx b/frontend/components/tools/commands/commands-columns.tsx new file mode 100644 index 00000000..49efc072 --- /dev/null +++ b/frontend/components/tools/commands/commands-columns.tsx @@ -0,0 +1,333 @@ +"use client" + +import React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { Command } from "@/types/command.types" +import { Checkbox } from "@/components/ui/checkbox" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { MoreHorizontal, Eye, Trash2, ChevronsUpDown, ChevronUp, ChevronDown, Copy, Check } from "lucide-react" +import { Badge } from "@/components/ui/badge" +import { formatDate } from "@/lib/utils" +import { toast } from "sonner" + +/** + * 可复制单元格组件 + */ +function CopyableCell({ + value, + maxWidth = "400px", + truncateLength = 50, + successMessage = "已复制", + className = "font-medium" +}: { + value: string + maxWidth?: string + truncateLength?: number + successMessage?: string + className?: string +}) { + const [copied, setCopied] = React.useState(false) + const isLong = value.length > truncateLength + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(value) + setCopied(true) + toast.success(successMessage) + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error('复制失败') + } + } + + return ( + <div className="group inline-flex items-center gap-1" style={{ maxWidth }}> + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <div className={`text-sm truncate cursor-default ${className}`}> + {value} + </div> + </TooltipTrigger> + <TooltipContent + side="top" + align="start" + sideOffset={5} + className={`text-xs ${isLong ? 'max-w-[500px] break-all' : 'whitespace-nowrap'}`} + > + {value} + </TooltipContent> + </Tooltip> + </TooltipProvider> + + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className={`h-6 w-6 flex-shrink-0 hover:bg-accent transition-opacity ${ + copied ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' + }`} + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">{copied ? '已复制!' : '点击复制'}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + ) +} + +/** + * 数据表格列头组件 + */ +function DataTableColumnHeader({ + column, + title, +}: { + column: { getCanSort: () => boolean; getIsSorted: () => false | "asc" | "desc"; toggleSorting: (desc?: boolean) => void } + title: string +}) { + if (!column.getCanSort()) { + return <div className="-ml-3 font-medium">{title}</div> + } + + const isSorted = column.getIsSorted() + + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted" + > + {title} + {isSorted === "asc" ? ( + <ChevronUp /> + ) : isSorted === "desc" ? ( + <ChevronDown /> + ) : ( + <ChevronsUpDown /> + )} + </Button> + ) +} + +/** + * 命令表格列定义 + */ +export const commandColumns: ColumnDef<Command>[] = [ + // 选择列 + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + + // 名称列 + { + accessorKey: "displayName", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="名称" /> + ), + cell: ({ row }) => { + const displayName = row.getValue("displayName") as string + const name = row.original.name + return ( + <div className="flex flex-col max-w-[200px]"> + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <span className="font-medium truncate cursor-default">{displayName || name}</span> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">{displayName || name}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + {displayName && name && name.length > 20 && ( + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整命令名称</h4> + <div className="text-sm break-all bg-muted p-2 rounded max-h-32 overflow-y-auto font-mono"> + {name} + </div> + </div> + </PopoverContent> + </Popover> + )} + {displayName && name && name.length <= 20 && ( + <span className="text-xs text-muted-foreground font-mono">{name}</span> + )} + </div> + ) + }, + }, + + // 所属工具列 + { + accessorKey: "tool", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="所属工具" /> + ), + cell: ({ row }) => { + const tool = row.original.tool + return ( + <div className="flex items-center gap-2"> + {tool ? ( + <Badge variant="outline">{tool.name}</Badge> + ) : ( + <span className="text-muted-foreground text-sm">-</span> + )} + </div> + ) + }, + }, + + // 命令模板列 + { + accessorKey: "commandTemplate", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="命令模板" /> + ), + cell: ({ row }) => { + const template = row.getValue("commandTemplate") as string + return <CopyableCell value={template} maxWidth="500px" truncateLength={60} successMessage="已复制命令模板" className="font-mono text-xs" /> + }, + }, + + // 描述列 + { + accessorKey: "description", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="描述" /> + ), + cell: ({ row }) => { + const description = row.getValue("description") as string + return ( + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <div className="max-w-[300px] truncate text-sm cursor-default"> + {description || "-"} + </div> + </TooltipTrigger> + <TooltipContent side="top" align="start"> + <p className="text-xs max-w-[400px]">{description || "暂无描述"}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ) + }, + }, + + // 更新时间列 + { + accessorKey: "updatedAt", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="更新时间" /> + ), + cell: ({ row }) => ( + <div className="text-sm text-muted-foreground"> + {formatDate(row.getValue("updatedAt"))} + </div> + ), + }, + + // 操作列 + { + id: "actions", + cell: ({ row }) => { + const command = row.original + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" + > + <MoreHorizontal /> + <span className="sr-only">打开菜单</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem + onClick={async () => { + try { + await navigator.clipboard.writeText(command.commandTemplate) + toast.success('已复制命令模板') + } catch { + toast.error('复制失败') + } + }} + > + <Copy /> + 复制命令模板 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem> + <Eye /> + 查看详情 + </DropdownMenuItem> + <DropdownMenuItem className="text-destructive focus:text-destructive"> + <Trash2 /> + 删除 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) + }, + enableSorting: false, + enableHiding: false, + }, +] diff --git a/frontend/components/tools/commands/commands-data-table.tsx b/frontend/components/tools/commands/commands-data-table.tsx new file mode 100644 index 00000000..e5b71d8d --- /dev/null +++ b/frontend/components/tools/commands/commands-data-table.tsx @@ -0,0 +1,313 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconPlus, + IconTrash, +} from "@tabler/icons-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + + +interface CommandsDataTableProps<TData, TValue> { + columns: ColumnDef<TData, TValue>[] + data: TData[] + onBulkDelete?: (selectedIds: number[]) => void + onAdd?: () => void +} + +export function CommandsDataTable<TData, TValue>({ + columns, + data, + onBulkDelete, + onAdd, +}: CommandsDataTableProps<TData, TValue>) { + const [sorting, setSorting] = React.useState<SortingState>([]) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + + // 获取选中的行 + const selectedRows = table.getFilteredSelectedRowModel().rows + + // 处理批量删除 + const handleBulkDelete = () => { + if (onBulkDelete && selectedRows.length > 0) { + const selectedIds = selectedRows.map((row) => (row.original as { id: number }).id) + onBulkDelete(selectedIds) + } + } + + return ( + <div className="space-y-4"> + {/* 工具栏 */} + <div className="flex items-center justify-between"> + {/* 搜索框 */} + <div className="flex items-center space-x-2"> + <Input + placeholder="搜索命令名称..." + value={(table.getColumn("displayName")?.getFilterValue() as string) ?? ""} + onChange={(event) => + table.getColumn("displayName")?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> + </div> + + {/* 右侧操作按钮 */} + <div className="flex items-center space-x-2"> + {/* 列显示控制 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + return ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {column.id === "id" && "ID"} + {column.id === "displayName" && "名称"} + {column.id === "tool" && "所属工具"} + {column.id === "commandTemplate" && "命令模板"} + {column.id === "description" && "描述"} + {column.id === "updatedAt" && "更新时间"} + {!["id", "displayName", "tool", "commandTemplate", "description", "updatedAt"].includes(column.id) && column.id} + </DropdownMenuCheckboxItem> + ) + })} + </DropdownMenuContent> + </DropdownMenu> + + {/* 批量删除按钮 */} + {onBulkDelete && ( + <Button + onClick={handleBulkDelete} + size="sm" + variant="outline" + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + className={ + table.getFilteredSelectedRowModel().rows.length === 0 + ? "text-muted-foreground" + : "text-destructive hover:text-destructive hover:bg-destructive/10" + } + > + <IconTrash /> + Delete + </Button> + )} + + {/* 添加命令按钮 */} + {onAdd && ( + <Button onClick={onAdd} size="sm"> + <IconPlus /> + Add + </Button> + )} + </div> + </div> + + {/* 表格 */} + <div className="rounded-md border"> + <Table> + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell colSpan={columns.length} className="h-24 text-center"> + 暂无数据 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {/* 分页控制 */} + <div className="flex items-center justify-between px-2"> + {/* 选中行信息 */} + <div className="flex-1 text-sm text-muted-foreground"> + {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected + </div> + + {/* 分页控制器 */} + <div className="flex items-center space-x-6 lg:space-x-8"> + {/* 每页显示数量选择 */} + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + Rows per page + </Label> + <Select + value={`${table.getState().pagination.pageSize}`} + onValueChange={(value) => { + table.setPageSize(Number(value)) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={table.getState().pagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 页码信息 */} + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + </div> + + {/* 分页按钮 */} + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to first page</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to previous page</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to next page</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to last page</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + </div> + ) +} diff --git a/frontend/components/tools/commands/index.ts b/frontend/components/tools/commands/index.ts new file mode 100644 index 00000000..284a44fb --- /dev/null +++ b/frontend/components/tools/commands/index.ts @@ -0,0 +1,5 @@ +/** + * Tool Commands Components - 统一导出 + */ +export { CommandsDataTable } from './commands-data-table' +export { commandColumns } from './commands-columns' diff --git a/frontend/components/tools/config/add-custom-tool-dialog.tsx b/frontend/components/tools/config/add-custom-tool-dialog.tsx new file mode 100644 index 00000000..a99dba1b --- /dev/null +++ b/frontend/components/tools/config/add-custom-tool-dialog.tsx @@ -0,0 +1,323 @@ +"use client" + +import React, { useState } from "react" +import { Wrench } from "lucide-react" +import { IconPlus } from "@tabler/icons-react" + +// 导入 UI 组件 +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { LoadingSpinner } from "@/components/loading-spinner" +import { IconX } from "@tabler/icons-react" +import { CategoryNameMap, type Tool } from "@/types/tool.types" +import { useCreateTool, useUpdateTool } from "@/hooks/use-tools" + +// 组件属性类型定义 +interface AddCustomToolDialogProps { + tool?: Tool // 要编辑的工具数据(可选,有值时为编辑模式) + onAdd?: (tool: Tool) => void // 添加成功回调函数(可选) + open?: boolean // 外部控制对话框开关状态 + onOpenChange?: (open: boolean) => void // 外部控制对话框开关回调 +} + +/** + * 添加/编辑自定义工具对话框组件 + */ +export function AddCustomToolDialog({ + tool, + onAdd, + open: externalOpen, + onOpenChange: externalOnOpenChange +}: AddCustomToolDialogProps) { + // 判断是编辑模式还是添加模式 + const isEditMode = !!tool + + // 对话框开关状态 - 支持外部控制 + const [internalOpen, setInternalOpen] = useState(false) + const open = externalOpen !== undefined ? externalOpen : internalOpen + const setOpen = externalOnOpenChange || setInternalOpen + + // 表单数据状态 - 如果是编辑模式,使用工具数据初始化 + const [formData, setFormData] = useState({ + name: tool?.name || "", + description: tool?.description || "", + directory: tool?.directory || "", + categoryNames: tool?.categoryNames || [] as string[], + }) + + // 使用预定义的分类列表 + const availableCategories = Object.keys(CategoryNameMap) + + // 使用 React Query 的创建和更新工具 mutation + const createTool = useCreateTool() + const updateTool = useUpdateTool() + + // 当 tool 变化时更新表单数据 + React.useEffect(() => { + if (tool) { + setFormData({ + name: tool.name || "", + description: tool.description || "", + directory: tool.directory || "", + categoryNames: tool.categoryNames || [], + }) + } + }, [tool]) + + // 处理表单提交 + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + // 表单验证 + if (!formData.name.trim() || !formData.directory.trim()) { + return + } + + const toolData = { + name: formData.name.trim(), + type: 'custom' as const, // 自定义工具 + description: formData.description.trim() || undefined, + directory: formData.directory.trim(), + categoryNames: formData.categoryNames.length > 0 ? formData.categoryNames : undefined, + } + + const onSuccessCallback = (response: { tool?: Tool }) => { + // 重置表单 + setFormData({ + name: "", + description: "", + directory: "", + categoryNames: [], + }) + + // 关闭对话框 + setOpen(false) + + // 调用外部回调(如果提供) + if (onAdd && response?.tool) { + onAdd(response.tool) + } + } + + // 根据模式选择创建或更新 + if (isEditMode && tool?.id) { + // 编辑模式:调用更新 API + updateTool.mutate( + { id: tool.id, data: toolData }, + { onSuccess: onSuccessCallback } + ) + } else { + // 创建模式:调用创建 API + createTool.mutate(toolData, { onSuccess: onSuccessCallback }) + } + } + + // 处理对话框关闭 - 重置表单 + const handleOpenChange = (newOpen: boolean) => { + // 正在提交时不允许关闭 + if (!createTool.isPending && !updateTool.isPending) { + setOpen(newOpen) + if (!newOpen) { + // 对话框关闭时重置表单 + setFormData({ + name: "", + description: "", + directory: "", + categoryNames: [], + }) + } + } + } + + // 处理分类标签点击切换 + const handleCategoryToggle = (categoryName: string) => { + setFormData((prev) => { + const isSelected = prev.categoryNames.includes(categoryName) + return { + ...prev, + categoryNames: isSelected + ? prev.categoryNames.filter(c => c !== categoryName) + : [...prev.categoryNames, categoryName] + } + }) + } + + // 移除分类标签 + const handleCategoryRemove = (categoryName: string) => { + setFormData((prev) => ({ + ...prev, + categoryNames: prev.categoryNames.filter(c => c !== categoryName) + })) + } + + // 表单验证 - 检查必填字段 + const isFormValid = + formData.name.trim().length > 0 && + formData.directory.trim().length > 0 + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + {/* 触发按钮 - 仅在非外部控制时显示 */} + {externalOpen === undefined && ( + <DialogTrigger asChild> + <Button> + <IconPlus className="h-5 w-5" /> + 添加工具 + </Button> + </DialogTrigger> + )} + + {/* 对话框内容 */} + <DialogContent className="sm:max-w-[500px]"> + <DialogHeader> + <DialogTitle className="flex items-center space-x-2"> + <Wrench /> + <span>{isEditMode ? "编辑自定义工具" : "添加自定义工具"}</span> + </DialogTitle> + <DialogDescription> + 配置自定义扫描工具的基本信息。标有 * 的字段为必填项。 + </DialogDescription> + </DialogHeader> + + {/* 表单 */} + <form onSubmit={handleSubmit}> + <div className="grid gap-6 py-4"> + {/* 工具名称 */} + <div className="grid gap-2"> + <Label htmlFor="name"> + 工具名称 <span className="text-red-500">*</span> + </Label> + <Input + id="name" + placeholder="例如:自定义端口扫描" + value={formData.name} + onChange={(e) => setFormData({ ...formData, name: e.target.value })} + disabled={createTool.isPending || updateTool.isPending} + required + /> + </div> + + {/* 工具描述 */} + <div className="grid gap-2"> + <Label htmlFor="description">工具描述</Label> + <Textarea + id="description" + placeholder="描述工具的功能和用途..." + value={formData.description} + onChange={(e) => setFormData({ ...formData, description: e.target.value })} + disabled={createTool.isPending || updateTool.isPending} + rows={3} + /> + </div> + + {/* 工具路径 */} + <div className="grid gap-2"> + <Label htmlFor="directory"> + 工具路径 <span className="text-red-500">*</span> + </Label> + <Input + id="directory" + placeholder="例如:/opt/security-tools/port-scanner" + value={formData.directory} + onChange={(e) => setFormData({ ...formData, directory: e.target.value })} + disabled={createTool.isPending || updateTool.isPending} + required + /> + <p className="text-xs text-muted-foreground"> + 脚本或工具所在的目录路径 + </p> + </div> + + {/* 分类标签 */} + <div className="grid gap-2"> + <Label>分类标签</Label> + + {/* 已选择的标签 */} + {formData.categoryNames.length > 0 && ( + <div className="flex flex-wrap gap-2 p-3 border rounded-md bg-muted/50"> + {formData.categoryNames.map((categoryName) => ( + <Badge + key={categoryName} + variant="default" + className="flex items-center gap-1 px-2 py-1" + > + {CategoryNameMap[categoryName] || categoryName} + <button + type="button" + onClick={() => handleCategoryRemove(categoryName)} + disabled={createTool.isPending || updateTool.isPending} + className="ml-1 hover:bg-primary/20 rounded-full p-0.5" + > + <IconX className="h-3 w-3" /> + </button> + </Badge> + ))} + </div> + )} + + {/* 可选择的标签 */} + <div className="flex flex-wrap gap-2 p-3 border rounded-md"> + {availableCategories.length > 0 ? ( + availableCategories.map((categoryName) => { + const isSelected = formData.categoryNames.includes(categoryName) + return ( + <Badge + key={categoryName} + variant={isSelected ? "secondary" : "outline"} + className="cursor-pointer hover:bg-secondary/80 transition-colors" + onClick={() => handleCategoryToggle(categoryName)} + > + {CategoryNameMap[categoryName] || categoryName} + </Badge> + ) + }) + ) : ( + <p className="text-sm text-muted-foreground">暂无可用分类</p> + )} + </div> + </div> + </div> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={createTool.isPending || updateTool.isPending} + > + 取消 + </Button> + <Button + type="submit" + disabled={createTool.isPending || updateTool.isPending || !isFormValid} + > + {(createTool.isPending || updateTool.isPending) ? ( + <> + <LoadingSpinner/> + {isEditMode ? "保存中..." : "创建中..."} + </> + ) : ( + <> + <IconPlus className="h-5 w-5" /> + {isEditMode ? "保存修改" : "创建工具"} + </> + )} + </Button> + </DialogFooter> + </form> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/tools/config/add-tool-dialog.tsx b/frontend/components/tools/config/add-tool-dialog.tsx new file mode 100644 index 00000000..a35c1cc3 --- /dev/null +++ b/frontend/components/tools/config/add-tool-dialog.tsx @@ -0,0 +1,526 @@ +"use client" + +import React, { useState, useEffect } from "react" +import { Wrench, AlertTriangle } from "lucide-react" +import { IconPlus } from "@tabler/icons-react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" + +// 导入 UI 组件 +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { LoadingSpinner } from "@/components/loading-spinner" +import { IconX } from "@tabler/icons-react" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" + +// 导入 React Query Hook +import { useCreateTool, useUpdateTool } from "@/hooks/use-tools" + +// 导入类型定义 +import type { Tool } from "@/types/tool.types" +import { CategoryNameMap } from "@/types/tool.types" + +// 表单验证 Schema +const formSchema = z.object({ + name: z.string() + .min(2, { message: "工具名称至少需要 2 个字符" }) + .max(255, { message: "工具名称不能超过 255 个字符" }), + repoUrl: z.string().optional().or(z.literal("")), + version: z.string().max(100).optional().or(z.literal("")), + description: z.string().max(1000).optional().or(z.literal("")), + categoryNames: z.array(z.string()), + installCommand: z.string().min(1, { message: "安装命令不能为空" }), + updateCommand: z.string().min(1, { message: "更新命令不能为空" }), + versionCommand: z.string().min(1, { message: "版本查询命令不能为空" }), +}) + +type FormValues = z.infer<typeof formSchema> + +// 组件属性类型定义 +interface AddToolDialogProps { + tool?: Tool // 要编辑的工具数据(可选,有值时为编辑模式) + onAdd?: (tool: Tool) => void // 添加成功回调函数(可选) + open?: boolean // 外部控制对话框开关状态 + onOpenChange?: (open: boolean) => void // 外部控制对话框开关回调 +} + +/** + * 根据工具名称和安装命令自动生成版本查询命令 + */ +function generateVersionCommand(toolName: string, installCommand: string): string { + if (!toolName) return "" + + const lowerName = toolName.toLowerCase().trim() + const lowerInstall = installCommand.toLowerCase() + + // Python 工具 + if (lowerInstall.includes("python") || lowerInstall.includes(".py")) { + return `python ${lowerName}.py -v` + } + + // Go 工具 + if (lowerInstall.includes("go install") || lowerInstall.includes("go get")) { + return `${lowerName} -version` + } + + // 默认尝试常见的版本命令 + return `${lowerName} --version` +} + +/** + * 添加工具对话框组件(使用 React Query) + * + * 功能特性: + * 1. 自动管理提交状态 + * 2. 自动错误处理和成功提示 + * 3. 自动刷新相关数据 + * 4. 支持多分类标签选择 + * 5. 支持安装、更新、版本命令配置 + */ +export function AddToolDialog({ + tool, + onAdd, + open: externalOpen, + onOpenChange: externalOnOpenChange +}: AddToolDialogProps) { + // 判断是编辑模式还是添加模式 + const isEditMode = !!tool + + // 对话框开关状态 - 支持外部控制 + const [internalOpen, setInternalOpen] = useState(false) + const open = externalOpen !== undefined ? externalOpen : internalOpen + const setOpen = externalOnOpenChange || setInternalOpen + + // 使用预定义的分类列表 + const availableCategories = Object.keys(CategoryNameMap) + + // 使用 React Query 的创建和更新工具 mutation + const createTool = useCreateTool() + const updateTool = useUpdateTool() + + // 初始化表单 + const form = useForm<FormValues>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: tool?.name || "", + repoUrl: tool?.repoUrl || "", + version: tool?.version || "", + description: tool?.description || "", + categoryNames: tool?.categoryNames || [], + installCommand: tool?.installCommand || "", + updateCommand: tool?.updateCommand || "", + versionCommand: tool?.versionCommand || "", + }, + }) + + // 当 tool 变化时重置表单 + useEffect(() => { + if (tool) { + form.reset({ + name: tool.name || "", + repoUrl: tool.repoUrl || "", + version: tool.version || "", + description: tool.description || "", + categoryNames: tool.categoryNames || [], + installCommand: tool.installCommand || "", + updateCommand: tool.updateCommand || "", + versionCommand: tool.versionCommand || "", + }) + } + }, [tool, form]) + + // 监听表单值变化 + const watchName = form.watch("name") + const watchInstallCommand = form.watch("installCommand") + const watchVersionCommand = form.watch("versionCommand") + const watchCategoryNames = form.watch("categoryNames") + + // 自动生成版本命令 + useEffect(() => { + if (watchName && watchInstallCommand && !watchVersionCommand) { + const generatedCmd = generateVersionCommand(watchName, watchInstallCommand) + form.setValue("versionCommand", generatedCmd) + } + }, [watchName, watchInstallCommand, watchVersionCommand, form]) + + // 处理表单提交 + const onSubmit = (values: FormValues) => { + const toolData = { + name: values.name.trim(), + type: 'opensource' as const, + repoUrl: values.repoUrl?.trim() || undefined, + version: values.version?.trim() || undefined, + description: values.description?.trim() || undefined, + categoryNames: values.categoryNames.length > 0 ? values.categoryNames : undefined, + installCommand: values.installCommand.trim(), + updateCommand: values.updateCommand.trim(), + versionCommand: values.versionCommand.trim(), + } + + const onSuccessCallback = (response: { tool?: Tool }) => { + // 重置表单 + form.reset() + + // 关闭对话框 + setOpen(false) + + // 调用外部回调 + if (onAdd && response?.tool) { + onAdd(response.tool) + } + } + + // 根据模式选择创建或更新 + if (isEditMode && tool?.id) { + updateTool.mutate( + { id: tool.id, data: toolData }, + { onSuccess: onSuccessCallback } + ) + } else { + createTool.mutate(toolData, { onSuccess: onSuccessCallback }) + } + } + + // 处理分类标签点击 + const handleCategoryToggle = (categoryName: string) => { + const current = form.getValues("categoryNames") + const isSelected = current.includes(categoryName) + form.setValue( + "categoryNames", + isSelected + ? current.filter(c => c !== categoryName) + : [...current, categoryName] + ) + } + + // 移除分类标签 + const handleCategoryRemove = (categoryName: string) => { + const current = form.getValues("categoryNames") + form.setValue("categoryNames", current.filter(c => c !== categoryName)) + } + + // 处理对话框关闭 + const handleOpenChange = (newOpen: boolean) => { + if (!createTool.isPending && !updateTool.isPending) { + setOpen(newOpen) + if (!newOpen) { + form.reset() + } + } + } + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + {/* 触发按钮 - 仅在非外部控制时显示 */} + {externalOpen === undefined && ( + <DialogTrigger asChild> + <Button> + <IconPlus className="h-5 w-5" /> + 添加工具 + </Button> + </DialogTrigger> + )} + + {/* 对话框内容 */} + <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle className="flex items-center space-x-2"> + <Wrench /> + <span>{isEditMode ? "编辑工具" : "添加新工具"}</span> + </DialogTitle> + <DialogDescription> + 配置扫描工具的基本信息和执行命令。标有 * 的字段为必填项。 + </DialogDescription> + </DialogHeader> + + {/* 表单 */} + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + <div className="grid gap-6 py-4"> + {/* 基本信息部分 */} + <div className="space-y-4"> + <h3 className="text-sm font-semibold text-muted-foreground">基本信息</h3> + + {/* 工具名称 */} + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>工具名称 <span className="text-destructive">*</span></FormLabel> + <FormControl> + <Input + placeholder="例如: Nuclei, Subfinder, HTTPX" + disabled={createTool.isPending || updateTool.isPending} + maxLength={255} + {...field} + /> + </FormControl> + <FormDescription>{field.value.length}/255 字符</FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 仓库地址 */} + <FormField + control={form.control} + name="repoUrl" + render={({ field }) => ( + <FormItem> + <FormLabel>仓库地址</FormLabel> + <FormControl> + <Input + type="url" + placeholder="https://github.com/projectdiscovery/nuclei" + disabled={createTool.isPending || updateTool.isPending} + maxLength={512} + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 版本号 */} + <FormField + control={form.control} + name="version" + render={({ field }) => ( + <FormItem> + <FormLabel>当前版本</FormLabel> + <FormControl> + <Input + placeholder="v3.0.0" + disabled={createTool.isPending || updateTool.isPending} + maxLength={100} + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 工具描述 */} + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>工具描述</FormLabel> + <FormControl> + <Textarea + placeholder="描述工具的功能、特点和使用场景..." + disabled={createTool.isPending || updateTool.isPending} + rows={3} + maxLength={1000} + {...field} + /> + </FormControl> + <FormDescription>{(field.value || "").length}/1000 字符</FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 分类标签 */} + <div className="grid gap-2"> + <FormLabel>分类标签</FormLabel> + + {/* 已选择的标签 */} + {watchCategoryNames.length > 0 && ( + <div className="flex flex-wrap gap-2 p-3 border rounded-md bg-muted/50"> + {watchCategoryNames.map((categoryName) => ( + <Badge + key={categoryName} + variant="default" + className="flex items-center gap-1 px-2 py-1" + > + {CategoryNameMap[categoryName] || categoryName} + <button + type="button" + onClick={() => handleCategoryRemove(categoryName)} + disabled={createTool.isPending || updateTool.isPending} + className="ml-1 hover:bg-primary/20 rounded-full p-0.5" + > + <IconX className="h-3 w-3" /> + </button> + </Badge> + ))} + </div> + )} + + {/* 可选择的标签 */} + <div className="flex flex-wrap gap-2 p-3 border rounded-md"> + {availableCategories.length > 0 ? ( + availableCategories.map((categoryName) => { + const isSelected = watchCategoryNames.includes(categoryName) + return ( + <Badge + key={categoryName} + variant={isSelected ? "secondary" : "outline"} + className="cursor-pointer hover:bg-secondary/80 transition-colors" + onClick={() => handleCategoryToggle(categoryName)} + > + {CategoryNameMap[categoryName] || categoryName} + </Badge> + ) + }) + ) : ( + <p className="text-sm text-muted-foreground">暂无可用分类</p> + )} + </div> + </div> + </div> + + {/* 命令配置部分 */} + <div className="space-y-4"> + <h3 className="text-sm font-semibold text-muted-foreground">命令配置</h3> + + {/* 安装命令 */} + <FormField + control={form.control} + name="installCommand" + render={({ field }) => ( + <FormItem> + <FormLabel>安装命令 <span className="text-destructive">*</span></FormLabel> + <FormControl> + <Textarea + placeholder="git clone https://github.com/user/tool 或 go install -v github.com/tool@latest" + disabled={createTool.isPending || updateTool.isPending} + rows={3} + className="font-mono text-sm" + {...field} + /> + </FormControl> + <FormDescription className="space-y-1"> + <span className="block"><strong>示例:</strong></span> + <span className="block">• 使用 git: <code className="bg-muted px-1 py-0.5 rounded">git clone https://github.com/user/tool</code></span> + <span className="block">• 使用 go: <code className="bg-muted px-1 py-0.5 rounded">go install -v github.com/tool@latest</code></span> + <span className="flex items-center gap-1 text-amber-600"> + <AlertTriangle className="h-3.5 w-3.5" /> + 注意:go get 已不再支持,请使用 go install + </span> + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 更新命令 */} + <FormField + control={form.control} + name="updateCommand" + render={({ field }) => ( + <FormItem> + <FormLabel>更新命令 <span className="text-destructive">*</span></FormLabel> + <FormControl> + <Textarea + placeholder="git pull 或 go install -v github.com/tool@latest" + disabled={createTool.isPending || updateTool.isPending} + rows={2} + className="font-mono text-sm" + {...field} + /> + </FormControl> + <FormDescription className="space-y-1"> + <span className="block">• 使用 git clone 安装的工具,推荐使用 <code className="bg-muted px-1 py-0.5 rounded">git pull</code></span> + <span className="block">• 使用 go install 安装的工具,推荐使用相同的安装命令</span> + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 版本查询命令 */} + <FormField + control={form.control} + name="versionCommand" + render={({ field }) => ( + <FormItem> + <FormLabel> + 版本查询命令 <span className="text-destructive">*</span> + {field.value && ( + <span className="ml-2 text-xs text-muted-foreground font-normal"> + 已自动生成 + </span> + )} + </FormLabel> + <FormControl> + <Input + placeholder="toolname --version" + disabled={createTool.isPending || updateTool.isPending} + maxLength={500} + className="font-mono text-sm" + {...field} + /> + </FormControl> + <FormDescription className="space-y-1"> + <span className="block">系统会使用此命令检查工具版本并提示更新。常见格式:</span> + <span className="block">• <code className="bg-muted px-1 py-0.5 rounded">toolname -v</code></span> + <span className="block">• <code className="bg-muted px-1 py-0.5 rounded">toolname -V</code></span> + <span className="block">• <code className="bg-muted px-1 py-0.5 rounded">toolname --version</code></span> + <span className="block">• <code className="bg-muted px-1 py-0.5 rounded">python tool_name.py -v</code></span> + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + </div> + </div> + + {/* 对话框底部按钮 */} + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={createTool.isPending || updateTool.isPending} + > + 取消 + </Button> + <Button + type="submit" + disabled={createTool.isPending || updateTool.isPending || !form.formState.isValid} + > + {(createTool.isPending || updateTool.isPending) ? ( + <> + <LoadingSpinner /> + {isEditMode ? "保存中..." : "创建中..."} + </> + ) : ( + <> + <IconPlus className="h-5 w-5" /> + {isEditMode ? "保存修改" : "创建工具"} + </> + )} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/tools/config/custom-tools-list.tsx b/frontend/components/tools/config/custom-tools-list.tsx new file mode 100644 index 00000000..350d4860 --- /dev/null +++ b/frontend/components/tools/config/custom-tools-list.tsx @@ -0,0 +1,218 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { IconEdit, IconTrash, IconFolder } from "@tabler/icons-react" +import { AddCustomToolDialog } from "@/components/tools/config/add-custom-tool-dialog" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { LoadingSpinner } from "@/components/loading-spinner" +import { CardGridSkeleton } from "@/components/ui/card-grid-skeleton" +import { CategoryNameMap, type Tool } from "@/types/tool.types" +import { useTools, useDeleteTool } from "@/hooks/use-tools" + +/** + * 自定义工具列表组件 + * 展示和管理自定义扫描脚本和工具 + */ +export function CustomToolsList() { + const [editingTool, setEditingTool] = useState<Tool | null>(null) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [toolToDelete, setToolToDelete] = useState<Tool | null>(null) + + // 获取工具列表(只获取自定义工具) + const { data, isLoading, error } = useTools({ + page: 1, + pageSize: 100, + }) + + // 过滤出自定义工具 + const customTools = (data?.tools || []).filter((tool: Tool) => tool.type === 'custom') + + // 删除工具 mutation + const deleteTool = useDeleteTool() + + const handleEditTool = (tool: Tool) => { + setEditingTool(tool) + setIsEditDialogOpen(true) + } + + const handleEditDialogClose = (open: boolean) => { + setIsEditDialogOpen(open) + if (!open) { + setEditingTool(null) + } + } + + const handleDeleteTool = (toolId: number) => { + const tool = customTools.find((t: Tool) => t.id === toolId) + if (!tool) return + setToolToDelete(tool) + } + + const confirmDelete = async () => { + if (!toolToDelete) return + + try { + await deleteTool.mutateAsync(toolToDelete.id) + // 删除成功后关闭对话框 + setToolToDelete(null) + } catch (error) { + // 错误已在 hook 中处理 + } + } + + // 加载状态 + if (isLoading) { + return <CardGridSkeleton cards={4} /> + } + + // 错误状态 + if (error) { + return ( + <div className="flex items-center justify-center min-h-[400px]"> + <div className="text-center"> + <p className="text-destructive">加载失败: {error.message}</p> + </div> + </div> + ) + } + + return ( + <div className="flex flex-col gap-4"> + {/* 工具列表 */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> + {customTools.map((tool: Tool) => ( + <Card key={tool.id} className="flex flex-col h-full hover:shadow-lg transition-shadow"> + <CardHeader> + <CardTitle className="text-lg truncate" title={tool.name}>{tool.name}</CardTitle> + <CardDescription className="line-clamp-2" title={tool.description || '暂无描述'}> + {tool.description || '暂无描述'} + </CardDescription> + + {/* 分类标签 */} + <div className="flex flex-wrap gap-1 mt-2"> + {tool.categoryNames && tool.categoryNames.length > 0 ? ( + <div + className="flex flex-wrap gap-1" + title={tool.categoryNames.map(c => CategoryNameMap[c] || c).join('、')} + > + {tool.categoryNames.slice(0, 3).map((category: string) => ( + <Badge key={category} variant="secondary" className="text-xs whitespace-nowrap"> + {CategoryNameMap[category] || category} + </Badge> + ))} + {tool.categoryNames.length > 3 && ( + <Badge variant="secondary" className="text-xs"> + +{tool.categoryNames.length - 3} + </Badge> + )} + </div> + ) : ( + <Badge variant="outline" className="text-xs text-muted-foreground"> + 未分类 + </Badge> + )} + </div> + </CardHeader> + <CardContent className="flex-1"> + <div className="space-y-4"> + {/* 工具目录 */} + <div className="bg-muted rounded-md p-3"> + <div className="flex items-center gap-2 text-sm text-muted-foreground mb-1"> + <IconFolder className="h-4 w-4" /> + <span>目录</span> + </div> + <code + className="text-sm font-mono break-all line-clamp-2" + title={tool.directory} + > + {tool.directory} + </code> + </div> + + {/* 最后更新时间 */} + <div className="text-sm text-muted-foreground"> + 最后更新:{new Date(tool.updatedAt).toLocaleDateString('zh-CN')} + </div> + </div> + </CardContent> + <CardFooter className="flex gap-2 pt-0"> + <Button + variant="outline" + className="flex-1" + onClick={() => handleEditTool(tool)} + > + <IconEdit className="h-4 w-4" /> + 编辑 + </Button> + <Button + variant="outline" + className="flex-1" + onClick={() => handleDeleteTool(tool.id)} + > + <IconTrash className="h-4 w-4" /> + 删除 + </Button> + </CardFooter> + </Card> + ))} + </div> + + {/* 空状态 */} + {customTools.length === 0 && ( + <div className="text-center py-12"> + <p className="text-muted-foreground">暂无自定义工具</p> + </div> + )} + + + + {/* 编辑工具对话框 */} + <AddCustomToolDialog + tool={editingTool || undefined} + open={isEditDialogOpen} + onOpenChange={handleEditDialogClose} + /> + + {/* 删除确认对话框 */} + <AlertDialog open={!!toolToDelete} onOpenChange={(open) => !open && setToolToDelete(null)}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除</AlertDialogTitle> + <AlertDialogDescription> + 此操作无法撤销。这将永久删除自定义工具 "{toolToDelete?.name}" 及其相关配置。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel disabled={deleteTool.isPending}>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + disabled={deleteTool.isPending} + > + {deleteTool.isPending ? ( + <> + <LoadingSpinner/> + 删除中... + </> + ) : ( + "删除" + )} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </div> + ) +} diff --git a/frontend/components/tools/config/index.ts b/frontend/components/tools/config/index.ts new file mode 100644 index 00000000..4152e4e9 --- /dev/null +++ b/frontend/components/tools/config/index.ts @@ -0,0 +1,9 @@ +/** + * Tool Config Components - 统一导出 + */ +export { ToolCard } from './tool-card' +export { OpensourceToolsList } from './opensource-tools-list' +export { CustomToolsList } from './custom-tools-list' +export { AddToolDialog } from './add-tool-dialog' +export { AddCustomToolDialog } from './add-custom-tool-dialog' + diff --git a/frontend/components/tools/config/opensource-tools-list.tsx b/frontend/components/tools/config/opensource-tools-list.tsx new file mode 100644 index 00000000..b7180fff --- /dev/null +++ b/frontend/components/tools/config/opensource-tools-list.tsx @@ -0,0 +1,171 @@ +"use client" + +import { useState } from "react" +import { ToolCard } from "@/components/tools/config/tool-card" +import { AddToolDialog } from "@/components/tools/config/add-tool-dialog" +import { useTools, useDeleteTool } from "@/hooks/use-tools" +import type { Tool } from "@/types/tool.types" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { LoadingSpinner } from "@/components/loading-spinner" +import { CardGridSkeleton } from "@/components/ui/card-grid-skeleton" + +/** + * 开源工具列表组件 + * 展示和管理开源扫描工具 + */ +export function OpensourceToolsList() { + const [checkingToolId, setCheckingToolId] = useState<number | null>(null) + const [editingTool, setEditingTool] = useState<Tool | null>(null) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [toolToDelete, setToolToDelete] = useState<Tool | null>(null) + + // 获取工具列表(只获取开源工具) + const { data, isLoading, error } = useTools({ + page: 1, + pageSize: 100, + }) + + // 过滤出开源工具 + const tools = (data?.tools || []).filter((tool: Tool) => tool.type === 'opensource') + + // 删除工具 mutation + const deleteTool = useDeleteTool() + + // 处理检查更新 + const handleCheckUpdate = async (toolId: number) => { + try { + setCheckingToolId(toolId) + console.log("检查工具更新:", toolId) + + // TODO: 调用后端 API 检查更新 + // 模拟异步操作 + await new Promise(resolve => setTimeout(resolve, 2000)) + + console.log("检查完成:", toolId) + } catch (error) { + console.error("检查更新失败:", error) + } finally { + setCheckingToolId(null) + } + } + + // 处理编辑工具 + const handleEditTool = (tool: Tool) => { + setEditingTool(tool) + setIsEditDialogOpen(true) + } + + // 编辑对话框关闭回调 + const handleEditDialogClose = (open: boolean) => { + setIsEditDialogOpen(open) + if (!open) { + setEditingTool(null) + } + } + + // 处理删除工具 + const handleDeleteTool = (toolId: number) => { + const tool = tools.find((t: Tool) => t.id === toolId) + if (!tool) return + setToolToDelete(tool) + } + + // 确认删除工具 + const confirmDelete = async () => { + if (!toolToDelete) return + + try { + await deleteTool.mutateAsync(toolToDelete.id) + // 删除成功后关闭对话框 + setToolToDelete(null) + } catch (error) { + // 错误已在 hook 中处理 + } + } + + // 加载状态 + if (isLoading) { + return <CardGridSkeleton cards={4} /> + } + + // 错误状态 + if (error) { + return ( + <div className="flex items-center justify-center min-h-[400px]"> + <div className="text-center"> + <p className="text-destructive">加载失败: {error.message}</p> + </div> + </div> + ) + } + + return ( + <div className="flex flex-col gap-4"> + {/* 工具列表 */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> + {tools.map((tool: Tool) => ( + <ToolCard + key={tool.id} + tool={tool} + onCheckUpdate={handleCheckUpdate} + onEdit={handleEditTool} + onDelete={handleDeleteTool} + isChecking={checkingToolId === tool.id} + /> + ))} + </div> + + {/* 空状态 */} + {tools.length === 0 && ( + <div className="text-center py-12"> + <p className="text-muted-foreground">暂无工具</p> + </div> + )} + + {/* 编辑工具对话框 */} + <AddToolDialog + tool={editingTool || undefined} + open={isEditDialogOpen} + onOpenChange={handleEditDialogClose} + /> + + {/* 删除确认对话框 */} + <AlertDialog open={!!toolToDelete} onOpenChange={(open) => !open && setToolToDelete(null)}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除</AlertDialogTitle> + <AlertDialogDescription> + 此操作无法撤销。这将永久删除开源工具 "{toolToDelete?.name}" 及其相关配置。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel disabled={deleteTool.isPending}>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + disabled={deleteTool.isPending} + > + {deleteTool.isPending ? ( + <> + <LoadingSpinner/> + 删除中... + </> + ) : ( + "删除" + )} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </div> + ) +} diff --git a/frontend/components/tools/config/tool-card.tsx b/frontend/components/tools/config/tool-card.tsx new file mode 100644 index 00000000..ab59787f --- /dev/null +++ b/frontend/components/tools/config/tool-card.tsx @@ -0,0 +1,134 @@ +"use client" + +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { IconBrandGithub, IconScale, IconRefresh, IconEdit, IconTrash } from "@tabler/icons-react" +import type { Tool } from "@/types/tool.types" +import { CategoryNameMap } from "@/types/tool.types" +import Link from "next/link" + +interface ToolCardProps { + tool: Tool + onCheckUpdate?: (toolId: number) => void | Promise<void> + onEdit?: (tool: Tool) => void // 编辑工具回调 + onDelete?: (toolId: number) => void // 删除工具回调 + isChecking?: boolean // 是否正在检查更新 +} + +/** + * 高亮描述文本组件 + * 简单显示描述文本,保持简洁 + */ +function HighlightedDescription({ description }: { description: string }) { + return <p className="line-clamp-4">{description}</p> +} + +/** + * 工具卡片组件 + * 显示单个扫描工具的信息 + */ +export function ToolCard({ tool, onCheckUpdate, onEdit, onDelete, isChecking = false }: ToolCardProps) { + // 从 name 生成首字母大写的 displayName + const displayName = tool.name.charAt(0).toUpperCase() + tool.name.slice(1) + + return ( + <Card className="flex flex-col h-full hover:shadow-lg transition-shadow"> + <CardHeader className=" space-y-2"> + {/* 工具名称 */} + <CardTitle + className="text-center text-2xl font-bold truncate px-2" + title={displayName} + > + {displayName} + </CardTitle> + + {/* GitHub/仓库链接 */} + <div className="flex items-center justify-center"> + {tool.repoUrl && ( + <Link + href={tool.repoUrl} + target="_blank" + rel="noopener noreferrer" + className="flex items-center gap-1 text-sm text-primary hover:underline" + > + <IconBrandGithub className="h-4 w-4" /> + <span>Repository</span> + </Link> + )} + </div> + + {/* 分类标签(居中显示,最多3个)*/} + <div className="flex items-center justify-center pt-1"> + {tool.categoryNames && tool.categoryNames.length > 0 ? ( + <div + className="flex flex-wrap gap-1 justify-center max-w-full" + title={tool.categoryNames.map(c => CategoryNameMap[c] || c).join('、')} + > + {tool.categoryNames.slice(0, 3).map((categoryName: string) => ( + <Badge key={categoryName} variant="secondary" className="text-xs whitespace-nowrap"> + {CategoryNameMap[categoryName] || categoryName} + </Badge> + ))} + {tool.categoryNames.length > 3 && ( + <Badge variant="secondary" className="text-xs"> + +{tool.categoryNames.length - 3} + </Badge> + )} + </div> + ) : ( + <Badge variant="outline" className="text-xs text-muted-foreground"> + 未分类 + </Badge> + )} + </div> + </CardHeader> + + <CardContent className="flex-1 flex flex-col"> + {/* 当前安装版本 */} + <div className="mb-2"> + <div className="text-xs text-muted-foreground text-center mb-1"> + Current Installed Version + </div> + <div className="text-center font-semibold text-base"> + {tool.version || 'N/A'} + </div> + </div> + + {/* 工具描述 */} + <CardDescription + className="flex-1 text-center line-clamp-3 text-sm leading-snug" + title={tool.description || '暂无描述'} + > + {tool.description || '暂无描述'} + </CardDescription> + </CardContent> + + <CardFooter className="flex gap-2"> + <Button + variant="default" + className="flex-1" + onClick={() => onCheckUpdate?.(tool.id)} + disabled={isChecking} + > + <IconRefresh className={isChecking ? "animate-spin h-4 w-4" : "h-4 w-4"} /> + {isChecking ? "检查中..." : "Check Update"} + </Button> + <Button + size="sm" + variant="outline" + onClick={() => onEdit?.(tool)} + > + <IconEdit className="h-4 w-4" /> + </Button> + <Button + size="sm" + variant="outline" + onClick={() => onDelete?.(tool.id)} + > + <IconTrash className="h-4 w-4" /> + </Button> + </CardFooter> + </Card> + ) +} diff --git a/frontend/components/tools/index.ts b/frontend/components/tools/index.ts new file mode 100644 index 00000000..badb44e7 --- /dev/null +++ b/frontend/components/tools/index.ts @@ -0,0 +1,5 @@ +export { ToolCard } from './config/tool-card' +export { OpensourceToolsList } from './config/opensource-tools-list' +export { CustomToolsList } from './config/custom-tools-list' +export { AddToolDialog } from './config/add-tool-dialog' +export { AddCustomToolDialog } from './config/add-custom-tool-dialog' diff --git a/frontend/components/tools/wordlist-edit-dialog.tsx b/frontend/components/tools/wordlist-edit-dialog.tsx new file mode 100644 index 00000000..e6536a63 --- /dev/null +++ b/frontend/components/tools/wordlist-edit-dialog.tsx @@ -0,0 +1,219 @@ +"use client" + +import React, { useState, useEffect, useRef } from "react" +import { FileText, Save, X, AlertTriangle } from "lucide-react" +import Editor from "@monaco-editor/react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { useTheme } from "next-themes" +import { useWordlistContent, useUpdateWordlistContent } from "@/hooks/use-wordlists" +import type { Wordlist } from "@/types/wordlist.types" + +interface WordlistEditDialogProps { + wordlist: Wordlist | null + open: boolean + onOpenChange: (open: boolean) => void +} + +/** + * 字典编辑弹窗 + * 使用 Monaco Editor 提供 VSCode 级别的编辑体验 + */ +export function WordlistEditDialog({ + wordlist, + open, + onOpenChange, +}: WordlistEditDialogProps) { + const [content, setContent] = useState("") + const [hasChanges, setHasChanges] = useState(false) + const [isEditorReady, setIsEditorReady] = useState(false) + const { theme } = useTheme() + const editorRef = useRef<any>(null) + + // 获取字典内容 + const { data: originalContent, isLoading } = useWordlistContent( + open && wordlist ? wordlist.id : null + ) + const updateMutation = useUpdateWordlistContent() + + // 当获取到内容时,更新编辑器 + useEffect(() => { + if (originalContent !== undefined && open) { + setContent(originalContent) + setHasChanges(false) + } + }, [originalContent, open]) + + // 当弹窗关闭时重置状态 + useEffect(() => { + if (!open) { + setContent("") + setHasChanges(false) + setIsEditorReady(false) + } + }, [open]) + + // 处理编辑器内容变化 + const handleEditorChange = (value: string | undefined) => { + const newValue = value || "" + setContent(newValue) + setHasChanges(newValue !== originalContent) + } + + // 处理编辑器挂载 + const handleEditorDidMount = (editor: any) => { + editorRef.current = editor + setIsEditorReady(true) + } + + // 处理保存 + const handleSave = async () => { + if (!wordlist) return + + updateMutation.mutate( + { id: wordlist.id, content }, + { + onSuccess: () => { + setHasChanges(false) + onOpenChange(false) + }, + } + ) + } + + // 处理关闭 + const handleClose = () => { + if (hasChanges) { + const confirmed = window.confirm("您有未保存的更改,确定要关闭吗?") + if (!confirmed) return + } + onOpenChange(false) + } + + // 计算行数 + const lineCount = content.split("\n").length + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-6xl max-w-[calc(100%-2rem)] h-[90vh] flex flex-col p-0"> + <div className="flex flex-col h-full"> + <DialogHeader className="px-6 pt-6 pb-4 border-b"> + <DialogTitle className="flex items-center gap-2"> + <FileText className="h-5 w-5" /> + 编辑字典 - {wordlist?.name} + </DialogTitle> + <DialogDescription> + 编辑字典内容,每行一个条目。保存后会自动更新行数、文件大小和 Hash 值。 + </DialogDescription> + </DialogHeader> + + <div className="flex-1 overflow-hidden px-6 py-4"> + <div className="flex flex-col h-full gap-2"> + <div className="flex items-center justify-between"> + <Label>字典内容</Label> + <div className="flex items-center gap-4 text-xs text-muted-foreground"> + <span>共 {lineCount.toLocaleString()} 行</span> + {wordlist?.fileHash && ( + <span title={wordlist.fileHash}> + Hash: {wordlist.fileHash.slice(0, 12)}... + </span> + )} + </div> + </div> + + {/* Monaco Editor */} + <div className="border rounded-md overflow-hidden h-full"> + {isLoading ? ( + <div className="flex items-center justify-center h-full"> + <div className="flex flex-col items-center gap-2"> + <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" /> + <p className="text-sm text-muted-foreground">加载字典内容...</p> + </div> + </div> + ) : ( + <Editor + height="100%" + defaultLanguage="plaintext" + value={content} + onChange={handleEditorChange} + onMount={handleEditorDidMount} + theme={theme === "dark" ? "vs-dark" : "light"} + options={{ + minimap: { enabled: false }, + fontSize: 13, + lineNumbers: "on", + wordWrap: "off", + scrollBeyondLastLine: false, + automaticLayout: true, + tabSize: 2, + insertSpaces: true, + folding: false, + padding: { + top: 16, + bottom: 16, + }, + readOnly: updateMutation.isPending, + }} + loading={ + <div className="flex items-center justify-center h-full"> + <div className="flex flex-col items-center gap-2"> + <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" /> + <p className="text-sm text-muted-foreground">加载编辑器...</p> + </div> + </div> + } + /> + )} + </div> + + {/* 未保存提示 */} + {hasChanges && ( + <p className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400"> + <AlertTriangle className="h-3.5 w-3.5" /> + 您有未保存的更改 + </p> + )} + </div> + </div> + + <DialogFooter className="px-6 py-4 border-t gap-2"> + <Button + type="button" + variant="outline" + onClick={handleClose} + disabled={updateMutation.isPending} + > + <X className="h-4 w-4" /> + 取消 + </Button> + <Button + type="button" + onClick={handleSave} + disabled={updateMutation.isPending || !hasChanges || !isEditorReady} + > + {updateMutation.isPending ? ( + <> + <div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" /> + 保存中... + </> + ) : ( + <> + <Save className="h-4 w-4" /> + 保存字典 + </> + )} + </Button> + </DialogFooter> + </div> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/tools/wordlist-upload-dialog.tsx b/frontend/components/tools/wordlist-upload-dialog.tsx new file mode 100644 index 00000000..eb17828c --- /dev/null +++ b/frontend/components/tools/wordlist-upload-dialog.tsx @@ -0,0 +1,223 @@ +"use client" + +import { useState, type FormEvent } from "react" +import { Upload, X, FileText } from "lucide-react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { useUploadWordlist } from "@/hooks/use-wordlists" +import { cn } from "@/lib/utils" + +interface WordlistUploadDialogProps { + trigger?: React.ReactNode +} + +export function WordlistUploadDialog({ trigger }: WordlistUploadDialogProps) { + const [open, setOpen] = useState(false) + const [name, setName] = useState("") + const [description, setDescription] = useState("") + const [file, setFile] = useState<File | null>(null) + const [isDragActive, setIsDragActive] = useState(false) + + const uploadMutation = useUploadWordlist() + + const resetForm = () => { + setName("") + setDescription("") + setFile(null) + } + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + setIsDragActive(true) + } + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault() + setIsDragActive(false) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + setIsDragActive(false) + const droppedFile = e.dataTransfer.files[0] + if (droppedFile && droppedFile.name.endsWith(".txt")) { + setFile(droppedFile) + if (!name) { + setName(droppedFile.name.replace(/\.[^/.]+$/, "")) + } + } + } + + const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { + const selectedFile = e.target.files?.[0] + if (selectedFile) { + setFile(selectedFile) + if (!name) { + setName(selectedFile.name.replace(/\.[^/.]+$/, "")) + } + } + } + + const handleSubmit = (e: FormEvent) => { + e.preventDefault() + if (!name || !file) return + + uploadMutation.mutate( + { name, description: description || undefined, file }, + { + onSuccess: () => { + resetForm() + setOpen(false) + }, + } + ) + } + + const removeFile = () => { + setFile(null) + } + + const formatFileSize = (bytes: number) => { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` + } + + return ( + <Dialog open={open} onOpenChange={(v) => { setOpen(v); if (!v) resetForm() }}> + <DialogTrigger asChild> + {trigger || ( + <Button> + <Upload className="mr-2 h-4 w-4" /> + 上传字典 + </Button> + )} + </DialogTrigger> + <DialogContent className="sm:max-w-lg"> + <DialogHeader> + <DialogTitle>上传字典</DialogTitle> + <DialogDescription> + 上传字典文件,后端保存后由各个 Worker 按需下载使用 + </DialogDescription> + </DialogHeader> + + <form onSubmit={handleSubmit} className="space-y-4"> + {/* 拖拽上传区域 */} + <div + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + className={cn( + "relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-6 transition-colors", + isDragActive + ? "border-primary bg-primary/5" + : "border-muted-foreground/25 hover:border-muted-foreground/50", + file && "border-solid border-muted-foreground/25" + )} + > + {file ? ( + // 已选择文件 + <div className="flex w-full items-center gap-3"> + <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10"> + <FileText className="h-5 w-5 text-primary" /> + </div> + <div className="flex-1 min-w-0"> + <p className="truncate font-medium text-sm">{file.name}</p> + <p className="text-xs text-muted-foreground"> + {formatFileSize(file.size)} + </p> + </div> + <Button + type="button" + variant="ghost" + size="icon" + className="h-8 w-8 shrink-0" + onClick={removeFile} + > + <X className="h-4 w-4" /> + </Button> + </div> + ) : ( + // 空状态 + <> + <div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted"> + <Upload className="h-6 w-6 text-muted-foreground" /> + </div> + <div className="mt-3 text-center"> + <p className="text-sm font-medium">拖拽文件到此处</p> + <p className="mt-1 text-xs text-muted-foreground"> + 或{" "} + <label className="cursor-pointer text-primary hover:underline"> + 选择文件 + <input + type="file" + accept=".txt" + className="hidden" + onChange={handleFileSelect} + /> + </label> + </p> + <p className="mt-2 text-xs text-muted-foreground"> + 支持 .txt 文件,最大 50MB + </p> + </div> + </> + )} + </div> + + {/* 名称和描述 */} + <div className="grid gap-4 sm:grid-cols-2"> + <div className="space-y-2"> + <Label htmlFor="name"> + 名称 <span className="text-destructive">*</span> + </Label> + <Input + id="name" + value={name} + onChange={(e) => setName(e.target.value)} + placeholder="例如:常用目录字典" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="description">描述(可选)</Label> + <Input + id="description" + value={description} + onChange={(e) => setDescription(e.target.value)} + placeholder="例如:基于 dirsearch" + /> + </div> + </div> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => setOpen(false)} + disabled={uploadMutation.isPending} + > + 取消 + </Button> + <Button + type="submit" + disabled={uploadMutation.isPending || !file || !name} + > + {uploadMutation.isPending ? "上传中..." : "上传字典"} + </Button> + </DialogFooter> + </form> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/ui/alert-dialog.tsx b/frontend/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..462bdcbe --- /dev/null +++ b/frontend/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client" + +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" + +function AlertDialog({ + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) { + return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} /> +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) { + return ( + <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) { + return ( + <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} /> + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) { + return ( + <AlertDialogPrimitive.Overlay + data-slot="alert-dialog-overlay" + className={cn( + "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-background/80 backdrop-blur-sm", + className + )} + {...props} + /> + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) { + return ( + <AlertDialogPortal> + <AlertDialogOverlay /> + <AlertDialogPrimitive.Content + data-slot="alert-dialog-content" + className={cn( + "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", + className + )} + {...props} + /> + </AlertDialogPortal> + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + <div + data-slot="alert-dialog-header" + className={cn("flex flex-col gap-2 text-center sm:text-left", className)} + {...props} + /> + ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + <div + data-slot="alert-dialog-footer" + className={cn( + "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", + className + )} + {...props} + /> + ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) { + return ( + <AlertDialogPrimitive.Title + data-slot="alert-dialog-title" + className={cn("text-lg font-semibold", className)} + {...props} + /> + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) { + return ( + <AlertDialogPrimitive.Description + data-slot="alert-dialog-description" + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) { + return ( + <AlertDialogPrimitive.Action + className={cn(buttonVariants(), className)} + {...props} + /> + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) { + return ( + <AlertDialogPrimitive.Cancel + className={cn(buttonVariants({ variant: "outline" }), className)} + {...props} + /> + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/frontend/components/ui/alert.tsx b/frontend/components/ui/alert.tsx new file mode 100644 index 00000000..5afd41d1 --- /dev/null +++ b/frontend/components/ui/alert.tsx @@ -0,0 +1,59 @@ +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 px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> +>(({ className, variant, ...props }, ref) => ( + <div + ref={ref} + role="alert" + className={cn(alertVariants({ variant }), className)} + {...props} + /> +)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes<HTMLHeadingElement> +>(({ className, ...props }, ref) => ( + <h5 + ref={ref} + className={cn("mb-1 font-medium leading-none tracking-tight", className)} + {...props} + /> +)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes<HTMLParagraphElement> +>(({ className, ...props }, ref) => ( + <div + ref={ref} + className={cn("text-sm [&_p]:leading-relaxed", className)} + {...props} + /> +)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/frontend/components/ui/avatar.tsx b/frontend/components/ui/avatar.tsx new file mode 100644 index 00000000..71e428b4 --- /dev/null +++ b/frontend/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps<typeof AvatarPrimitive.Root>) { + return ( + <AvatarPrimitive.Root + data-slot="avatar" + className={cn( + "relative flex size-8 shrink-0 overflow-hidden rounded-full", + className + )} + {...props} + /> + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps<typeof AvatarPrimitive.Image>) { + return ( + <AvatarPrimitive.Image + data-slot="avatar-image" + className={cn("aspect-square size-full", className)} + {...props} + /> + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { + return ( + <AvatarPrimitive.Fallback + data-slot="avatar-fallback" + className={cn( + "bg-muted flex size-full items-center justify-center rounded-full", + className + )} + {...props} + /> + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/frontend/components/ui/badge.tsx b/frontend/components/ui/badge.tsx new file mode 100644 index 00000000..fd3a406b --- /dev/null +++ b/frontend/components/ui/badge.tsx @@ -0,0 +1,46 @@ +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 badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps<typeof badgeVariants> & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + <Comp + data-slot="badge" + className={cn(badgeVariants({ variant }), className)} + {...props} + /> + ) +} + +export { Badge, badgeVariants } diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx new file mode 100644 index 00000000..de9387ca --- /dev/null +++ b/frontend/components/ui/button.tsx @@ -0,0 +1,60 @@ +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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-primary/10 hover:text-primary dark:hover:bg-primary/20", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps<typeof buttonVariants> & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + <Comp + data-slot="button" + className={cn(buttonVariants({ variant, size, className }))} + {...props} + /> + ) +} + +export { Button, buttonVariants } diff --git a/frontend/components/ui/calendar.tsx b/frontend/components/ui/calendar.tsx new file mode 100644 index 00000000..6f304b5b --- /dev/null +++ b/frontend/components/ui/calendar.tsx @@ -0,0 +1,216 @@ +"use client" + +import * as React from "react" +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react" +import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, + ...props +}: React.ComponentProps<typeof DayPicker> & { + buttonVariant?: React.ComponentProps<typeof Button>["variant"] +}) { + const defaultClassNames = getDefaultClassNames() + + return ( + <DayPicker + showOutsideDays={showOutsideDays} + className={cn( + "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", + String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }} + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "flex gap-4 flex-col md:flex-row relative", + defaultClassNames.months + ), + month: cn("flex flex-col w-full gap-4", defaultClassNames.month), + nav: cn( + "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_next + ), + month_caption: cn( + "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", + defaultClassNames.dropdown_root + ), + dropdown: cn( + "absolute bg-popover inset-0 opacity-0", + defaultClassNames.dropdown + ), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", + defaultClassNames.weekday + ), + week: cn("flex w-full mt-2", defaultClassNames.week), + week_number_header: cn( + "select-none w-(--cell-size)", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] select-none text-muted-foreground", + defaultClassNames.week_number + ), + day: cn( + "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", + props.showWeekNumber + ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md" + : "[&:first-child[data-selected=true]_button]:rounded-l-md", + defaultClassNames.day + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), + today: cn( + "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", + defaultClassNames.today + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( + <div + data-slot="calendar" + ref={rootRef} + className={cn(className)} + {...props} + /> + ) + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + <ChevronLeftIcon className={cn("size-4", className)} {...props} /> + ) + } + + if (orientation === "right") { + return ( + <ChevronRightIcon + className={cn("size-4", className)} + {...props} + /> + ) + } + + return ( + <ChevronDownIcon className={cn("size-4", className)} {...props} /> + ) + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + <td {...props}> + <div className="flex size-(--cell-size) items-center justify-center text-center"> + {children} + </div> + </td> + ) + }, + ...components, + }} + {...props} + /> + ) +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps<typeof DayButton>) { + const defaultClassNames = getDefaultClassNames() + + const ref = React.useRef<HTMLButtonElement>(null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) + + return ( + <Button + ref={ref} + variant="ghost" + size="icon" + data-day={day.date.toLocaleDateString()} + data-selected-single={ + modifiers.selected && + !modifiers.range_start && + !modifiers.range_end && + !modifiers.range_middle + } + data-range-start={modifiers.range_start} + data-range-end={modifiers.range_end} + data-range-middle={modifiers.range_middle} + className={cn( + "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70", + defaultClassNames.day, + className + )} + {...props} + /> + ) +} + +export { Calendar, CalendarDayButton } diff --git a/frontend/components/ui/card-grid-skeleton.tsx b/frontend/components/ui/card-grid-skeleton.tsx new file mode 100644 index 00000000..23ed75ae --- /dev/null +++ b/frontend/components/ui/card-grid-skeleton.tsx @@ -0,0 +1,67 @@ +import { cn } from "@/lib/utils" +import { Skeleton } from "@/components/ui/skeleton" + +interface CardGridSkeletonProps { + cards?: number + actionButtonCount?: number + showToolbar?: boolean + withPadding?: boolean + className?: string +} + +/** + * 通用卡片网格骨架屏 + * 适用于工具列表等卡片布局 + */ +export function CardGridSkeleton({ + cards = 4, + actionButtonCount = 2, + showToolbar = true, + withPadding = true, + className, +}: CardGridSkeletonProps) { + const containerClass = cn( + "flex flex-col gap-4", + withPadding && "px-4 lg:px-6", + className + ) + + return ( + <div className={containerClass}> + {showToolbar && ( + <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> + <Skeleton className="h-9 w-full sm:max-w-sm" /> + <div className="flex items-center gap-2"> + {Array.from({ length: actionButtonCount }).map((_, index) => ( + <Skeleton key={index} className="h-9 w-24" /> + ))} + </div> + </div> + )} + + <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> + {Array.from({ length: cards }).map((_, index) => ( + <div key={index} className="rounded-lg border bg-card p-4 shadow-sm space-y-4"> + <div className="space-y-2"> + <Skeleton className="h-5 w-3/4" /> + <Skeleton className="h-4 w-full" /> + </div> + <div className="space-y-2"> + <Skeleton className="h-4 w-1/2" /> + <Skeleton className="h-16 w-full" /> + </div> + <div className="flex flex-wrap gap-2"> + {Array.from({ length: 3 }).map((_, badgeIndex) => ( + <Skeleton key={badgeIndex} className="h-6 w-16 rounded-full" /> + ))} + </div> + <div className="flex gap-2"> + <Skeleton className="h-9 w-full" /> + <Skeleton className="h-9 w-full" /> + </div> + </div> + ))} + </div> + </div> + ) +} diff --git a/frontend/components/ui/card.tsx b/frontend/components/ui/card.tsx new file mode 100644 index 00000000..d05bbc6c --- /dev/null +++ b/frontend/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card" + className={cn( + "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", + className + )} + {...props} + /> + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-header" + className={cn( + "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", + className + )} + {...props} + /> + ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-title" + className={cn("leading-none font-semibold", className)} + {...props} + /> + ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-description" + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> + ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-action" + className={cn( + "col-start-2 row-span-2 row-start-1 self-start justify-self-end", + className + )} + {...props} + /> + ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-content" + className={cn("px-6", className)} + {...props} + /> + ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-footer" + className={cn("flex items-center px-6 [.border-t]:pt-6", className)} + {...props} + /> + ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/frontend/components/ui/chart.tsx b/frontend/components/ui/chart.tsx new file mode 100644 index 00000000..0a15587b --- /dev/null +++ b/frontend/components/ui/chart.tsx @@ -0,0 +1,357 @@ +"use client" + +import * as React from "react" +import * as RechartsPrimitive from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record<keyof typeof THEMES, string> } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext<ChartContextProps | null>(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a <ChartContainer />") + } + + return context +} + +function ChartContainer({ + id, + className, + children, + config, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] +}) { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + <ChartContext.Provider value={{ config }}> + <div + data-slot="chart" + data-chart={chartId} + className={cn( + "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_line]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_line]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector]:stroke-transparent [&_.recharts-surface]:outline-hidden", + className + )} + {...props} + > + <ChartStyle id={chartId} config={config} /> + <RechartsPrimitive.ResponsiveContainer> + {children} + </RechartsPrimitive.ResponsiveContainer> + </div> + </ChartContext.Provider> + ) +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( + <style + dangerouslySetInnerHTML={{ + __html: Object.entries(THEMES) + .map( + ([theme, prefix]) => ` +${prefix} [data-chart=${id}] { +${colorConfig + .map(([key, itemConfig]) => { + const color = + itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || + itemConfig.color + return color ? ` --color-${key}: ${color};` : null + }) + .join("\n")} +} +` + ) + .join("\n"), + }} + /> + ) +} + +const ChartTooltip = RechartsPrimitive.Tooltip + +function ChartTooltipContent({ + active, + payload, + className, + indicator = "dot", + hideLabel = false, + hideIndicator = false, + label, + labelFormatter, + labelClassName, + formatter, + color, + nameKey, + labelKey, +}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> & + React.ComponentProps<"div"> & { + hideLabel?: boolean + hideIndicator?: boolean + indicator?: "line" | "dot" | "dashed" + nameKey?: string + labelKey?: string + }) { + 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 = getPayloadConfigFromPayload(config, item, key) + const value = + !labelKey && typeof label === "string" + ? config[label as keyof typeof config]?.label || label + : itemConfig?.label + + if (labelFormatter) { + return ( + <div className={cn("font-medium", labelClassName)}> + {labelFormatter(value, payload)} + </div> + ) + } + + if (!value) { + return null + } + + return <div className={cn("font-medium", labelClassName)}>{value}</div> + }, [ + label, + labelFormatter, + payload, + hideLabel, + labelClassName, + config, + labelKey, + ]) + + if (!active || !payload?.length) { + return null + } + + const nestLabel = payload.length === 1 && indicator !== "dot" + + return ( + <div + className={cn( + "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl", + className + )} + > + {!nestLabel ? tooltipLabel : null} + <div className="grid gap-1.5"> + {payload + .filter((item) => item.type !== "none") + .map((item, index) => { + const key = `${nameKey || item.name || item.dataKey || "value"}` + const itemConfig = getPayloadConfigFromPayload(config, item, key) + const indicatorColor = color || item.payload.fill || item.color + + return ( + <div + key={item.dataKey} + className={cn( + "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5", + indicator === "dot" && "items-center" + )} + > + {formatter && item?.value !== undefined && item.name ? ( + formatter(item.value, item.name, item, index, item.payload) + ) : ( + <> + {itemConfig?.icon ? ( + <itemConfig.icon /> + ) : ( + !hideIndicator && ( + <div + className={cn( + "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", + { + "h-2.5 w-2.5": indicator === "dot", + "w-1": indicator === "line", + "w-0 border-[1.5px] border-dashed bg-transparent": + indicator === "dashed", + "my-0.5": nestLabel && indicator === "dashed", + } + )} + style={ + { + "--color-bg": indicatorColor, + "--color-border": indicatorColor, + } as React.CSSProperties + } + /> + ) + )} + <div + className={cn( + "flex flex-1 justify-between leading-none", + nestLabel ? "items-end" : "items-center" + )} + > + <div className="grid gap-1.5"> + {nestLabel ? tooltipLabel : null} + <span className="text-muted-foreground"> + {itemConfig?.label || item.name} + </span> + </div> + {item.value && ( + <span className="text-foreground font-mono font-medium tabular-nums"> + {item.value.toLocaleString()} + </span> + )} + </div> + </> + )} + </div> + ) + })} + </div> + </div> + ) +} + +const ChartLegend = RechartsPrimitive.Legend + +function ChartLegendContent({ + className, + hideIcon = false, + payload, + verticalAlign = "bottom", + nameKey, +}: React.ComponentProps<"div"> & + Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { + hideIcon?: boolean + nameKey?: string + }) { + const { config } = useChart() + + if (!payload?.length) { + return null + } + + return ( + <div + className={cn( + "flex items-center justify-center gap-4", + verticalAlign === "top" ? "pb-3" : "pt-3", + className + )} + > + {payload + .filter((item) => item.type !== "none") + .map((item) => { + const key = `${nameKey || item.dataKey || "value"}` + const itemConfig = getPayloadConfigFromPayload(config, item, key) + + return ( + <div + key={item.value} + className={cn( + "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3" + )} + > + {itemConfig?.icon && !hideIcon ? ( + <itemConfig.icon /> + ) : ( + <div + className="h-2 w-2 shrink-0 rounded-[2px]" + style={{ + backgroundColor: item.color, + }} + /> + )} + {itemConfig?.label} + </div> + ) + })} + </div> + ) +} + +// Helper to extract item config from a payload. +function getPayloadConfigFromPayload( + config: ChartConfig, + payload: unknown, + key: string +) { + if (typeof payload !== "object" || payload === null) { + return undefined + } + + const payloadPayload = + "payload" in payload && + typeof payload.payload === "object" && + payload.payload !== null + ? payload.payload + : undefined + + let configLabelKey: string = key + + if ( + key in payload && + typeof payload[key as keyof typeof payload] === "string" + ) { + configLabelKey = payload[key as keyof typeof payload] as string + } else if ( + payloadPayload && + key in payloadPayload && + typeof payloadPayload[key as keyof typeof payloadPayload] === "string" + ) { + configLabelKey = payloadPayload[ + key as keyof typeof payloadPayload + ] as string + } + + return configLabelKey in config + ? config[configLabelKey] + : config[key as keyof typeof config] +} + +export { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + ChartLegend, + ChartLegendContent, + ChartStyle, +} diff --git a/frontend/components/ui/checkbox.tsx b/frontend/components/ui/checkbox.tsx new file mode 100644 index 00000000..a6f9b437 --- /dev/null +++ b/frontend/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps<typeof CheckboxPrimitive.Root>) { + return ( + <CheckboxPrimitive.Root + data-slot="checkbox" + className={cn( + "peer border-input bg-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-sm border shadow-xs transition-all outline-none focus-visible:ring-[1px] cursor-pointer disabled:cursor-not-allowed disabled:opacity-50", + className + )} + {...props} + > + <CheckboxPrimitive.Indicator + data-slot="checkbox-indicator" + className="flex items-center justify-center text-current transition-none" + > + <CheckIcon className="size-3.5" /> + </CheckboxPrimitive.Indicator> + </CheckboxPrimitive.Root> + ) +} + +export { Checkbox } diff --git a/frontend/components/ui/collapsible.tsx b/frontend/components/ui/collapsible.tsx new file mode 100644 index 00000000..ae9fad04 --- /dev/null +++ b/frontend/components/ui/collapsible.tsx @@ -0,0 +1,33 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +function Collapsible({ + ...props +}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { + return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} /> +} + +function CollapsibleTrigger({ + ...props +}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) { + return ( + <CollapsiblePrimitive.CollapsibleTrigger + data-slot="collapsible-trigger" + {...props} + /> + ) +} + +function CollapsibleContent({ + ...props +}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) { + return ( + <CollapsiblePrimitive.CollapsibleContent + data-slot="collapsible-content" + {...props} + /> + ) +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/frontend/components/ui/command.tsx b/frontend/components/ui/command.tsx new file mode 100644 index 00000000..e2aaa292 --- /dev/null +++ b/frontend/components/ui/command.tsx @@ -0,0 +1,158 @@ +"use client" + +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { MagnifyingGlassIcon } from "@radix-ui/react-icons" +import { Command as CommandPrimitive } from "cmdk" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef<typeof CommandPrimitive>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive> +>(({ className, ...props }, ref) => ( + <CommandPrimitive + ref={ref} + className={cn( + "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", + className + )} + {...props} + /> +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps { + contentClassName?: string +} + +const CommandDialog = ({ children, contentClassName, ...props }: CommandDialogProps) => { + return ( + <Dialog {...props}> + <DialogContent className={cn("overflow-hidden p-0 sm:max-w-[500px]", contentClassName)}> + <DialogTitle className="sr-only">Command Dialog</DialogTitle> + <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> + {children} + </Command> + </DialogContent> + </Dialog> + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Input>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> +>(({ className, ...props }, ref) => ( + <div className="flex items-center border-b px-3" cmdk-input-wrapper=""> + <MagnifyingGlassIcon className="shrink-0 opacity-50" /> + <CommandPrimitive.Input + ref={ref} + className={cn( + "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", + className + )} + {...props} + /> + </div> +)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.List>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.List + ref={ref} + className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} + {...props} + /> +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Empty>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> +>((props, ref) => ( + <CommandPrimitive.Empty + ref={ref} + className="py-6 text-center text-sm" + {...props} + /> +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Group>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Group + ref={ref} + className={cn( + "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", + className + )} + {...props} + /> +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Separator + ref={ref} + className={cn("-mx-1 h-px bg-border", className)} + {...props} + /> +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Item + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50", + className + )} + {...props} + /> +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes<HTMLSpanElement>) => { + return ( + <span + className={cn( + "ml-auto text-xs tracking-widest text-muted-foreground", + className + )} + {...props} + /> + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandSeparator, + CommandShortcut, +} diff --git a/frontend/components/ui/data-table-skeleton.tsx b/frontend/components/ui/data-table-skeleton.tsx new file mode 100644 index 00000000..d6f21fb7 --- /dev/null +++ b/frontend/components/ui/data-table-skeleton.tsx @@ -0,0 +1,110 @@ +import { cn } from "@/lib/utils" +import { Skeleton } from "@/components/ui/skeleton" + +interface DataTableSkeletonProps { + statsCount?: number + toolbarButtonCount?: number + rows?: number + columns?: number + withSearch?: boolean + paginationButtonCount?: number + withPadding?: boolean + className?: string +} + +/** + * 通用表格页面骨架屏 + * 可配置统计卡片、工具栏按钮、表格列数等 + */ +export function DataTableSkeleton({ + statsCount = 0, + toolbarButtonCount = 2, + rows = 5, + columns = 4, + withSearch = true, + paginationButtonCount = 3, + withPadding = true, + className, +}: DataTableSkeletonProps) { + const containerClass = cn( + "space-y-4", + withPadding && "px-4 lg:px-6", + className + ) + + const toolbarNeeded = withSearch || toolbarButtonCount > 0 + + return ( + <div className={containerClass}> + {statsCount > 0 && ( + <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4"> + {Array.from({ length: statsCount }).map((_, index) => ( + <div key={index} className="rounded-lg border bg-card p-4 shadow-sm space-y-2"> + <Skeleton className="h-4 w-24" /> + <Skeleton className="h-8 w-20" /> + </div> + ))} + </div> + )} + + {toolbarNeeded && ( + <> + <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> + {withSearch && <Skeleton className="h-9 w-full sm:max-w-sm" />} + {toolbarButtonCount > 0 && ( + <div className="flex items-center gap-2"> + {Array.from({ length: toolbarButtonCount }).map((_, index) => ( + <Skeleton key={index} className="h-9 w-24" /> + ))} + </div> + )} + </div> + <div + className="border-b mt-4" + style={{ borderColor: "var(--sidebar-border)" }} + /> + </> + )} + + <div className="overflow-x-auto"> + <div + className="hidden md:flex items-center gap-4 border-b px-2 py-3" + style={{ borderColor: "var(--sidebar-border)" }} + > + {Array.from({ length: columns }).map((_, index) => ( + <Skeleton key={index} className="h-5 flex-1" /> + ))} + </div> + + <div> + {Array.from({ length: rows }).map((_, rowIndex) => ( + <div + key={rowIndex} + className={cn( + "flex flex-col gap-3 border-b px-2 py-3 md:flex-row md:items-center md:justify-between", + rowIndex === rows - 1 && "border-b-0" + )} + style={{ borderColor: rowIndex === rows - 1 ? "transparent" : "var(--sidebar)" }} + > + {Array.from({ length: Math.max(columns, 3) }).map((_, colIndex) => ( + <Skeleton + key={colIndex} + className="h-5 w-full md:w-1/4" + /> + ))} + </div> + ))} + </div> + </div> + + <div className="border-t border-border pt-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> + <Skeleton className="h-5 w-48" /> + <div className="flex items-center gap-2"> + {Array.from({ length: paginationButtonCount }).map((_, index) => ( + <Skeleton key={index} className="h-8 w-10 rounded-full" /> + ))} + </div> + </div> + </div> + ) +} diff --git a/frontend/components/ui/datetime-picker.tsx b/frontend/components/ui/datetime-picker.tsx new file mode 100644 index 00000000..c9c6f96a --- /dev/null +++ b/frontend/components/ui/datetime-picker.tsx @@ -0,0 +1,108 @@ +"use client" + +import * as React from "react" +import { ChevronDownIcon } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { Calendar } from "@/components/ui/calendar" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" + +interface DateTimePickerProps { + value?: Date + onChange?: (date: Date | undefined) => void + label?: string + placeholder?: string + minDate?: Date +} + +export function DateTimePicker({ + value, + onChange, + label = "执行时间", + placeholder = "选择日期时间", + minDate, +}: DateTimePickerProps) { + const [open, setOpen] = React.useState(false) + const [date, setDate] = React.useState<Date | undefined>(value) + const [time, setTime] = React.useState<string>( + value ? `${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}` : "02:00" + ) + + // 合并日期和时间 + const updateDateTime = React.useCallback((newDate: Date | undefined, newTime: string) => { + if (!newDate) { + onChange?.(undefined) + return + } + + const [hours, minutes] = newTime.split(":").map(Number) + const dateTime = new Date(newDate) + dateTime.setHours(hours || 0, minutes || 0, 0, 0) + onChange?.(dateTime) + }, [onChange]) + + const handleDateChange = (newDate: Date | undefined) => { + setDate(newDate) + updateDateTime(newDate, time) + setOpen(false) + } + + const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setTime(e.target.value) + updateDateTime(date, e.target.value) + } + + // 格式化显示 + const displayDate = date + ? date.toLocaleDateString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }) + : placeholder + + return ( + <div className="flex flex-col gap-3"> + {label && ( + <Label className="px-1">{label}</Label> + )} + <div className="flex gap-3"> + {/* 日期选择 */} + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + className="flex-1 justify-between font-normal" + > + {displayDate} + <ChevronDownIcon className="h-4 w-4 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-auto overflow-hidden p-0" align="start"> + <Calendar + mode="single" + selected={date} + captionLayout="dropdown" + onSelect={handleDateChange} + disabled={minDate ? { before: minDate } : undefined} + /> + </PopoverContent> + </Popover> + + {/* 时间选择 */} + <Input + type="time" + value={time} + onChange={handleTimeChange} + className="w-28 bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" + /> + </div> + </div> + ) +} diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx new file mode 100644 index 00000000..b10ed411 --- /dev/null +++ b/frontend/components/ui/dialog.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Root>) { + return <DialogPrimitive.Root data-slot="dialog" {...props} /> +} + +function DialogTrigger({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Trigger>) { + return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> +} + +function DialogPortal({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Portal>) { + return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> +} + +function DialogClose({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Close>) { + return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps<typeof DialogPrimitive.Overlay>) { + return ( + <DialogPrimitive.Overlay + data-slot="dialog-overlay" + className={cn( + "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-background/80 backdrop-blur-sm", + className + )} + {...props} + /> + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps<typeof DialogPrimitive.Content> & { + showCloseButton?: boolean +}) { + return ( + <DialogPortal data-slot="dialog-portal"> + <DialogOverlay /> + <DialogPrimitive.Content + data-slot="dialog-content" + className={cn( + "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200", + className + )} + {...props} + > + {children} + {showCloseButton && ( + <DialogPrimitive.Close + data-slot="dialog-close" + className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" + > + <XIcon /> + <span className="sr-only">Close</span> + </DialogPrimitive.Close> + )} + </DialogPrimitive.Content> + </DialogPortal> + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="dialog-header" + className={cn("flex flex-col gap-2 text-center sm:text-left", className)} + {...props} + /> + ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="dialog-footer" + className={cn( + "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", + className + )} + {...props} + /> + ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps<typeof DialogPrimitive.Title>) { + return ( + <DialogPrimitive.Title + data-slot="dialog-title" + className={cn("text-lg leading-none font-semibold", className)} + {...props} + /> + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps<typeof DialogPrimitive.Description>) { + return ( + <DialogPrimitive.Description + data-slot="dialog-description" + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/frontend/components/ui/drawer.tsx b/frontend/components/ui/drawer.tsx new file mode 100644 index 00000000..918cd3b7 --- /dev/null +++ b/frontend/components/ui/drawer.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@/lib/utils" + +function Drawer({ + ...props +}: React.ComponentProps<typeof DrawerPrimitive.Root>) { + return <DrawerPrimitive.Root data-slot="drawer" {...props} /> +} + +function DrawerTrigger({ + ...props +}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) { + return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} /> +} + +function DrawerPortal({ + ...props +}: React.ComponentProps<typeof DrawerPrimitive.Portal>) { + return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} /> +} + +function DrawerClose({ + ...props +}: React.ComponentProps<typeof DrawerPrimitive.Close>) { + return <DrawerPrimitive.Close data-slot="drawer-close" {...props} /> +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) { + return ( + <DrawerPrimitive.Overlay + data-slot="drawer-overlay" + className={cn( + "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-background/80 backdrop-blur-sm", + className + )} + {...props} + /> + ) +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps<typeof DrawerPrimitive.Content>) { + return ( + <DrawerPortal data-slot="drawer-portal"> + <DrawerOverlay /> + <DrawerPrimitive.Content + data-slot="drawer-content" + className={cn( + "group/drawer-content bg-background fixed z-50 flex h-auto flex-col", + "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b", + "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t", + "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm", + "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm", + className + )} + {...props} + > + <div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" /> + {children} + </DrawerPrimitive.Content> + </DrawerPortal> + ) +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="drawer-header" + className={cn( + "flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left", + className + )} + {...props} + /> + ) +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="drawer-footer" + className={cn("mt-auto flex flex-col gap-2 p-4", className)} + {...props} + /> + ) +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps<typeof DrawerPrimitive.Title>) { + return ( + <DrawerPrimitive.Title + data-slot="drawer-title" + className={cn("text-foreground font-semibold", className)} + {...props} + /> + ) +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps<typeof DrawerPrimitive.Description>) { + return ( + <DrawerPrimitive.Description + data-slot="drawer-description" + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> + ) +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/frontend/components/ui/dropdown-menu.tsx b/frontend/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..1b579000 --- /dev/null +++ b/frontend/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { + return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { + return ( + <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { + return ( + <DropdownMenuPrimitive.Trigger + data-slot="dropdown-menu-trigger" + {...props} + /> + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { + return ( + <DropdownMenuPrimitive.Portal> + <DropdownMenuPrimitive.Content + data-slot="dropdown-menu-content" + sideOffset={sideOffset} + className={cn( + "bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", + className + )} + {...props} + /> + </DropdownMenuPrimitive.Portal> + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { + return ( + <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + <DropdownMenuPrimitive.Item + data-slot="dropdown-menu-item" + data-inset={inset} + data-variant={variant} + className={cn( + "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-1.5 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + className + )} + {...props} + /> + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { + return ( + <DropdownMenuPrimitive.CheckboxItem + data-slot="dropdown-menu-checkbox-item" + className={cn( + "focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-1.5 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + className + )} + checked={checked} + {...props} + > + <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <CheckIcon className="size-4" /> + </DropdownMenuPrimitive.ItemIndicator> + </span> + {children} + </DropdownMenuPrimitive.CheckboxItem> + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { + return ( + <DropdownMenuPrimitive.RadioGroup + data-slot="dropdown-menu-radio-group" + {...props} + /> + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { + return ( + <DropdownMenuPrimitive.RadioItem + data-slot="dropdown-menu-radio-item" + className={cn( + "focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-1.5 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + className + )} + {...props} + > + <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <CircleIcon className="size-2 fill-current" /> + </DropdownMenuPrimitive.ItemIndicator> + </span> + {children} + </DropdownMenuPrimitive.RadioItem> + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { + inset?: boolean +}) { + return ( + <DropdownMenuPrimitive.Label + data-slot="dropdown-menu-label" + data-inset={inset} + className={cn( + "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", + className + )} + {...props} + /> + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { + return ( + <DropdownMenuPrimitive.Separator + data-slot="dropdown-menu-separator" + className={cn("bg-border -mx-1 my-1 h-px", className)} + {...props} + /> + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + <span + data-slot="dropdown-menu-shortcut" + className={cn( + "text-muted-foreground ml-auto text-xs tracking-widest", + className + )} + {...props} + /> + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { + return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { + inset?: boolean +}) { + return ( + <DropdownMenuPrimitive.SubTrigger + data-slot="dropdown-menu-sub-trigger" + data-inset={inset} + className={cn( + "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8", + className + )} + {...props} + > + {children} + <ChevronRightIcon className="ml-auto size-4" /> + </DropdownMenuPrimitive.SubTrigger> + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { + return ( + <DropdownMenuPrimitive.SubContent + data-slot="dropdown-menu-sub-content" + className={cn( + "bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", + className + )} + {...props} + /> + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/frontend/components/ui/dropzone.tsx b/frontend/components/ui/dropzone.tsx new file mode 100644 index 00000000..05ac5067 --- /dev/null +++ b/frontend/components/ui/dropzone.tsx @@ -0,0 +1,200 @@ +'use client' + +import { UploadIcon } from 'lucide-react' +import type { ReactNode } from 'react' +import { createContext, useContext } from 'react' +import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone' +import { useDropzone } from 'react-dropzone' +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' + +type DropzoneContextType = { + src?: File[] + accept?: DropzoneOptions['accept'] + maxSize?: DropzoneOptions['maxSize'] + minSize?: DropzoneOptions['minSize'] + maxFiles?: DropzoneOptions['maxFiles'] +} + +const renderBytes = (bytes: number) => { + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + let size = bytes + let unitIndex = 0 + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024 + unitIndex++ + } + + return `${size.toFixed(2)}${units[unitIndex]}` +} + +const DropzoneContext = createContext<DropzoneContextType | undefined>( + undefined +) + +export type DropzoneProps = Omit<DropzoneOptions, 'onDrop'> & { + src?: File[] + className?: string + onDrop?: ( + acceptedFiles: File[], + fileRejections: FileRejection[], + event: DropEvent + ) => void + children?: ReactNode +} + +export const Dropzone = ({ + accept, + maxFiles = 1, + maxSize, + minSize, + onDrop, + onError, + disabled, + src, + className, + children, + ...props +}: DropzoneProps) => { + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + accept, + maxFiles, + maxSize, + minSize, + onError, + disabled, + onDrop: (acceptedFiles, fileRejections, event) => { + if (fileRejections.length > 0) { + const message = fileRejections.at(0)?.errors.at(0)?.message + onError?.(new Error(message)) + return + } + + onDrop?.(acceptedFiles, fileRejections, event) + }, + ...props, + }) + + return ( + <DropzoneContext.Provider + key={JSON.stringify(src)} + value={{ src, accept, maxSize, minSize, maxFiles }} + > + <Button + className={cn( + 'relative h-auto w-full flex-col overflow-hidden p-8', + isDragActive && 'outline-none ring-2 ring-ring', + className + )} + disabled={disabled} + type="button" + variant="outline" + {...getRootProps()} + > + <input {...getInputProps()} disabled={disabled} /> + {children} + </Button> + </DropzoneContext.Provider> + ) +} + +const useDropzoneContext = () => { + const context = useContext(DropzoneContext) + + if (!context) { + throw new Error('useDropzoneContext must be used within a Dropzone') + } + + return context +} + +export type DropzoneContentProps = { + children?: ReactNode + className?: string +} + +const maxLabelItems = 3 + +export const DropzoneContent = ({ + children, + className, +}: DropzoneContentProps) => { + const { src } = useDropzoneContext() + + if (!src || src.length === 0) { + return null + } + + if (children) { + return children + } + + return ( + <div className={cn('flex flex-col items-center justify-center', className)}> + <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> + <UploadIcon size={16} /> + </div> + <p className="my-2 w-full truncate font-medium text-sm"> + {src.length > maxLabelItems + ? `${src.slice(0, maxLabelItems).map((file) => file.name).join(', ')} 等 ${src.length - maxLabelItems} 个文件` + : src.map((file) => file.name).join(', ')} + </p> + <p className="w-full text-wrap text-muted-foreground text-xs"> + 拖拽或点击替换文件 + </p> + </div> + ) +} + +export type DropzoneEmptyStateProps = { + children?: ReactNode + className?: string +} + +export const DropzoneEmptyState = ({ + children, + className, +}: DropzoneEmptyStateProps) => { + const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext() + + if (src && src.length > 0) { + return null + } + + if (children) { + return children + } + + let caption = '' + + if (accept) { + caption += '支持 ' + caption += Object.keys(accept).join(', ') + } + + if (minSize && maxSize) { + caption += ` 大小在 ${renderBytes(minSize)} 到 ${renderBytes(maxSize)} 之间` + } else if (minSize) { + caption += ` 最小 ${renderBytes(minSize)}` + } else if (maxSize) { + caption += ` 最大 ${renderBytes(maxSize)}` + } + + return ( + <div className={cn('flex flex-col items-center justify-center', className)}> + <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> + <UploadIcon size={16} /> + </div> + <p className="my-2 w-full truncate text-wrap font-medium text-sm"> + 上传{maxFiles === 1 ? '文件' : '文件'} + </p> + <p className="w-full truncate text-wrap text-muted-foreground text-xs"> + 拖拽文件到此处,或点击选择 + </p> + {caption && ( + <p className="text-wrap text-muted-foreground text-xs mt-1">{caption}</p> + )} + </div> + ) +} diff --git a/frontend/components/ui/field.tsx b/frontend/components/ui/field.tsx new file mode 100644 index 00000000..235d00ef --- /dev/null +++ b/frontend/components/ui/field.tsx @@ -0,0 +1,248 @@ +"use client" + +import { useMemo } from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( + <fieldset + data-slot="field-set" + className={cn( + "flex flex-col gap-6", + "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + <legend + data-slot="field-legend" + data-variant={variant} + className={cn( + "mb-3 font-medium", + "data-[variant=legend]:text-base", + "data-[variant=label]:text-sm", + className + )} + {...props} + /> + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="field-group" + className={cn( + "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4", + className + )} + {...props} + /> + ) +} + +const fieldVariants = cva( + "group/field flex w-full gap-3 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], + horizontal: [ + "flex-row items-center", + "[&>[data-slot=field-label]]:flex-auto", + "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + responsive: [ + "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto", + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) { + return ( + <div + role="group" + data-slot="field" + data-orientation={orientation} + className={cn(fieldVariants({ orientation }), className)} + {...props} + /> + ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="field-content" + className={cn( + "group/field-content flex flex-1 flex-col gap-1.5 leading-snug", + className + )} + {...props} + /> + ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps<typeof Label>) { + return ( + <Label + data-slot="field-label" + className={cn( + "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50", + "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4", + "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10", + className + )} + {...props} + /> + ) +} + +function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="field-label" + className={cn( + "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50", + className + )} + {...props} + /> + ) +} + +function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( + <p + data-slot="field-description" + className={cn( + "text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance", + "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5", + "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...props} + /> + ) +} + +function FieldSeparator({ + children, + className, + ...props +}: React.ComponentProps<"div"> & { + children?: React.ReactNode +}) { + return ( + <div + data-slot="field-separator" + data-content={!!children} + className={cn( + "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", + className + )} + {...props} + > + <Separator className="absolute inset-0 top-1/2" /> + {children && ( + <span + className="bg-background text-muted-foreground relative mx-auto block w-fit px-2" + data-slot="field-separator-content" + > + {children} + </span> + )} + </div> + ) +} + +function FieldError({ + className, + children, + errors, + ...props +}: React.ComponentProps<"div"> & { + errors?: Array<{ message?: string } | undefined> +}) { + const content = useMemo(() => { + if (children) { + return children + } + + if (!errors?.length) { + return null + } + + const uniqueErrors = [ + ...new Map(errors.map((error) => [error?.message, error])).values(), + ] + + if (uniqueErrors?.length == 1) { + return uniqueErrors[0]?.message + } + + return ( + <ul className="ml-4 flex list-disc flex-col gap-1"> + {uniqueErrors.map( + (error, index) => + error?.message && <li key={index}>{error.message}</li> + )} + </ul> + ) + }, [children, errors]) + + if (!content) { + return null + } + + return ( + <div + role="alert" + data-slot="field-error" + className={cn("text-destructive text-sm font-normal", className)} + {...props} + > + {content} + </div> + ) +} + +export { + Field, + FieldLabel, + FieldDescription, + FieldError, + FieldGroup, + FieldLegend, + FieldSeparator, + FieldSet, + FieldContent, + FieldTitle, +} diff --git a/frontend/components/ui/form.tsx b/frontend/components/ui/form.tsx new file mode 100644 index 00000000..524b986b --- /dev/null +++ b/frontend/components/ui/form.tsx @@ -0,0 +1,167 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, +> = { + name: TName +} + +const FormFieldContext = React.createContext<FormFieldContextValue>( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, +>({ + ...props +}: ControllerProps<TFieldValues, TName>) => { + return ( + <FormFieldContext.Provider value={{ name: props.name }}> + <Controller {...props} /> + </FormFieldContext.Provider> + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within <FormField>") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext<FormItemContextValue>( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + <FormItemContext.Provider value={{ id }}> + <div + data-slot="form-item" + className={cn("grid gap-2", className)} + {...props} + /> + </FormItemContext.Provider> + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps<typeof LabelPrimitive.Root>) { + const { error, formItemId } = useFormField() + + return ( + <Label + data-slot="form-label" + data-error={!!error} + className={cn("data-[error=true]:text-destructive", className)} + htmlFor={formItemId} + {...props} + /> + ) +} + +function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { + const { error, formItemId, formDescriptionId, formMessageId } = useFormField() + + return ( + <Slot + data-slot="form-control" + id={formItemId} + aria-describedby={ + !error + ? `${formDescriptionId}` + : `${formDescriptionId} ${formMessageId}` + } + aria-invalid={!!error} + {...props} + /> + ) +} + +function FormDescription({ className, ...props }: React.ComponentProps<"p">) { + const { formDescriptionId } = useFormField() + + return ( + <p + data-slot="form-description" + id={formDescriptionId} + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> + ) +} + +function FormMessage({ className, ...props }: React.ComponentProps<"p">) { + const { error, formMessageId } = useFormField() + const body = error ? String(error?.message ?? "") : props.children + + if (!body) { + return null + } + + return ( + <p + data-slot="form-message" + id={formMessageId} + className={cn("text-destructive text-sm", className)} + {...props} + > + {body} + </p> + ) +} + +export { + useFormField, + Form, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, + FormField, +} diff --git a/frontend/components/ui/input.tsx b/frontend/components/ui/input.tsx new file mode 100644 index 00000000..1c5e463b --- /dev/null +++ b/frontend/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + <input + type={type} + data-slot="input" + className={cn( + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-background dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-all outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", + "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + className + )} + {...props} + /> + ) +} + +export { Input } diff --git a/frontend/components/ui/label.tsx b/frontend/components/ui/label.tsx new file mode 100644 index 00000000..d6d8f576 --- /dev/null +++ b/frontend/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps<typeof LabelPrimitive.Root>) { + return ( + <LabelPrimitive.Root + data-slot="label" + className={cn( + "flex items-center gap-1.5 text-sm leading-none font-medium select-none cursor-pointer group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", + className + )} + {...props} + /> + ) +} + +export { Label } diff --git a/frontend/components/ui/master-detail-skeleton.tsx b/frontend/components/ui/master-detail-skeleton.tsx new file mode 100644 index 00000000..ed91aaa0 --- /dev/null +++ b/frontend/components/ui/master-detail-skeleton.tsx @@ -0,0 +1,97 @@ +import { Skeleton } from "@/components/ui/skeleton" +import { Separator } from "@/components/ui/separator" + +interface MasterDetailSkeletonProps { + /** 左侧列表项数量 */ + listItemCount?: number + /** 是否显示搜索框 */ + withSearch?: boolean + /** 页面标题 */ + title?: string +} + +/** + * 主从布局骨架屏 + * 适用于扫描引擎、字典管理、Nuclei 模板等页面 + */ +export function MasterDetailSkeleton({ + listItemCount = 5, + withSearch = true, + title, +}: MasterDetailSkeletonProps) { + return ( + <div className="flex flex-col h-full"> + {/* 顶部 */} + <div className="flex items-center justify-between gap-4 px-4 py-4 lg:px-6"> + {title ? ( + <h1 className="text-2xl font-bold shrink-0">{title}</h1> + ) : ( + <Skeleton className="h-8 w-32" /> + )} + {withSearch && ( + <div className="flex items-center gap-2 flex-1 max-w-md"> + <Skeleton className="h-9 w-full" /> + </div> + )} + <Skeleton className="h-9 w-24" /> + </div> + + <Separator /> + + {/* 主体 */} + <div className="flex flex-1 min-h-0"> + {/* 左侧列表 */} + <div className="w-72 lg:w-80 border-r flex flex-col"> + <div className="px-4 py-3 border-b"> + <Skeleton className="h-4 w-24" /> + </div> + <div className="p-2 space-y-2"> + {Array.from({ length: listItemCount }).map((_, index) => ( + <div key={index} className="rounded-lg px-3 py-2.5 space-y-1.5"> + <Skeleton className="h-4 w-3/4" /> + <Skeleton className="h-3 w-1/2" /> + </div> + ))} + </div> + </div> + + {/* 右侧详情 */} + <div className="flex-1 flex flex-col min-w-0"> + <div className="px-6 py-4 border-b"> + <div className="flex items-start gap-3"> + <Skeleton className="h-10 w-10 rounded-lg" /> + <div className="flex-1 space-y-2"> + <Skeleton className="h-5 w-48" /> + <Skeleton className="h-4 w-32" /> + </div> + </div> + </div> + <div className="flex-1 p-6 space-y-6"> + <div className="rounded-lg border p-4 space-y-3"> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-2"> + <Skeleton className="h-3 w-16" /> + <Skeleton className="h-6 w-24" /> + </div> + <div className="space-y-2"> + <Skeleton className="h-3 w-16" /> + <Skeleton className="h-6 w-24" /> + </div> + </div> + <Separator /> + <div className="space-y-3"> + <Skeleton className="h-4 w-full" /> + <Skeleton className="h-4 w-3/4" /> + </div> + </div> + </div> + <div className="px-6 py-4 border-t flex items-center gap-2"> + <Skeleton className="h-8 w-24" /> + <div className="flex-1" /> + <Skeleton className="h-8 w-20" /> + </div> + </div> + </div> + </div> + ) +} diff --git a/frontend/components/ui/popover.tsx b/frontend/components/ui/popover.tsx new file mode 100644 index 00000000..5c7befe1 --- /dev/null +++ b/frontend/components/ui/popover.tsx @@ -0,0 +1,35 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverAnchor = PopoverPrimitive.Anchor + +const PopoverContent = React.forwardRef< + React.ElementRef<typeof PopoverPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & { + container?: HTMLElement | null + } +>(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => ( + <PopoverPrimitive.Portal container={container ?? undefined}> + <PopoverPrimitive.Content + ref={ref} + align={align} + sideOffset={sideOffset} + className={cn( + "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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", + className + )} + {...props} + /> + </PopoverPrimitive.Portal> +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/frontend/components/ui/progress.tsx b/frontend/components/ui/progress.tsx new file mode 100644 index 00000000..e7a416c3 --- /dev/null +++ b/frontend/components/ui/progress.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +function Progress({ + className, + value, + ...props +}: React.ComponentProps<typeof ProgressPrimitive.Root>) { + return ( + <ProgressPrimitive.Root + data-slot="progress" + className={cn( + "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", + className + )} + {...props} + > + <ProgressPrimitive.Indicator + data-slot="progress-indicator" + className="bg-primary h-full w-full flex-1 transition-all" + style={{ transform: `translateX(-${100 - (value || 0)}%)` }} + /> + </ProgressPrimitive.Root> + ) +} + +export { Progress } diff --git a/frontend/components/ui/radio-group.tsx b/frontend/components/ui/radio-group.tsx new file mode 100644 index 00000000..b5a08274 --- /dev/null +++ b/frontend/components/ui/radio-group.tsx @@ -0,0 +1,45 @@ +"use client" + +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function RadioGroup({ + className, + ...props +}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) { + return ( + <RadioGroupPrimitive.Root + data-slot="radio-group" + className={cn("grid gap-3", className)} + {...props} + /> + ) +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) { + return ( + <RadioGroupPrimitive.Item + data-slot="radio-group-item" + className={cn( + "border-input bg-background text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", + className + )} + {...props} + > + <RadioGroupPrimitive.Indicator + data-slot="radio-group-indicator" + className="relative flex items-center justify-center" + > + <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" /> + </RadioGroupPrimitive.Indicator> + </RadioGroupPrimitive.Item> + ) +} + +export { RadioGroup, RadioGroupItem } diff --git a/frontend/components/ui/scroll-area.tsx b/frontend/components/ui/scroll-area.tsx new file mode 100644 index 00000000..0b4a48d8 --- /dev/null +++ b/frontend/components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +"use client" + +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<typeof ScrollAreaPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> +>(({ className, children, ...props }, ref) => ( + <ScrollAreaPrimitive.Root + ref={ref} + className={cn("relative overflow-hidden", className)} + {...props} + > + <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> + {children} + </ScrollAreaPrimitive.Viewport> + <ScrollBar /> + <ScrollAreaPrimitive.Corner /> + </ScrollAreaPrimitive.Root> +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, + React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> +>(({ className, orientation = "vertical", ...props }, ref) => ( + <ScrollAreaPrimitive.ScrollAreaScrollbar + ref={ref} + orientation={orientation} + className={cn( + "flex touch-none select-none transition-colors", + orientation === "vertical" && + "h-full w-2.5 border-l border-l-transparent p-[1px]", + orientation === "horizontal" && + "h-2.5 flex-col border-t border-t-transparent p-[1px]", + className + )} + {...props} + > + <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> + </ScrollAreaPrimitive.ScrollAreaScrollbar> +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/frontend/components/ui/select.tsx b/frontend/components/ui/select.tsx new file mode 100644 index 00000000..d4d9f6be --- /dev/null +++ b/frontend/components/ui/select.tsx @@ -0,0 +1,185 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Root>) { + return <SelectPrimitive.Root data-slot="select" {...props} /> +} + +function SelectGroup({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Group>) { + return <SelectPrimitive.Group data-slot="select-group" {...props} /> +} + +function SelectValue({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Value>) { + return <SelectPrimitive.Value data-slot="select-value" {...props} /> +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { + size?: "sm" | "default" +}) { + return ( + <SelectPrimitive.Trigger + data-slot="select-trigger" + data-size={size} + className={cn( + "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-background dark:bg-input/30 hover:bg-accent/50 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + className + )} + {...props} + > + {children} + <SelectPrimitive.Icon asChild> + <ChevronDownIcon className="size-4 opacity-50" /> + </SelectPrimitive.Icon> + </SelectPrimitive.Trigger> + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps<typeof SelectPrimitive.Content>) { + return ( + <SelectPrimitive.Portal> + <SelectPrimitive.Content + data-slot="select-content" + className={cn( + "bg-popover text-popover-foreground 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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md", + position === "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", + className + )} + position={position} + {...props} + > + <SelectScrollUpButton /> + <SelectPrimitive.Viewport + className={cn( + "p-1", + position === "popper" && + "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1" + )} + > + {children} + </SelectPrimitive.Viewport> + <SelectScrollDownButton /> + </SelectPrimitive.Content> + </SelectPrimitive.Portal> + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Label>) { + return ( + <SelectPrimitive.Label + data-slot="select-label" + className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)} + {...props} + /> + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Item>) { + return ( + <SelectPrimitive.Item + data-slot="select-item" + className={cn( + "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-1.5 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-1.5", + className + )} + {...props} + > + <span className="absolute right-2 flex size-3.5 items-center justify-center"> + <SelectPrimitive.ItemIndicator> + <CheckIcon className="size-4" /> + </SelectPrimitive.ItemIndicator> + </span> + <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> + </SelectPrimitive.Item> + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Separator>) { + return ( + <SelectPrimitive.Separator + data-slot="select-separator" + className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)} + {...props} + /> + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { + return ( + <SelectPrimitive.ScrollUpButton + data-slot="select-scroll-up-button" + className={cn( + "flex cursor-default items-center justify-center py-1", + className + )} + {...props} + > + <ChevronUpIcon className="size-4" /> + </SelectPrimitive.ScrollUpButton> + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) { + return ( + <SelectPrimitive.ScrollDownButton + data-slot="select-scroll-down-button" + className={cn( + "flex cursor-default items-center justify-center py-1", + className + )} + {...props} + > + <ChevronDownIcon className="size-4" /> + </SelectPrimitive.ScrollDownButton> + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/frontend/components/ui/separator.tsx b/frontend/components/ui/separator.tsx new file mode 100644 index 00000000..275381ca --- /dev/null +++ b/frontend/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { + return ( + <SeparatorPrimitive.Root + data-slot="separator" + decorative={decorative} + orientation={orientation} + className={cn( + "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", + className + )} + {...props} + /> + ) +} + +export { Separator } diff --git a/frontend/components/ui/shadcn-io/banner/index.tsx b/frontend/components/ui/shadcn-io/banner/index.tsx new file mode 100644 index 00000000..6f711ed1 --- /dev/null +++ b/frontend/components/ui/shadcn-io/banner/index.tsx @@ -0,0 +1,142 @@ +'use client'; + +import { useControllableState } from '@radix-ui/react-use-controllable-state'; +import { type LucideIcon, XIcon } from 'lucide-react'; +import { + type ComponentProps, + createContext, + type HTMLAttributes, + type MouseEventHandler, + useContext, +} from 'react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +type BannerContextProps = { + show: boolean; + setShow: (show: boolean) => void; +}; + +export const BannerContext = createContext<BannerContextProps>({ + show: true, + setShow: () => {}, +}); + +export type BannerProps = HTMLAttributes<HTMLDivElement> & { + visible?: boolean; + defaultVisible?: boolean; + onClose?: () => void; + inset?: boolean; +}; + +export const Banner = ({ + children, + visible, + defaultVisible = true, + onClose, + className, + inset = false, + ...props +}: BannerProps) => { + const [show, setShow] = useControllableState({ + defaultProp: defaultVisible, + prop: visible, + onChange: onClose, + }); + + if (!show) { + return null; + } + + return ( + <BannerContext.Provider value={{ show, setShow }}> + <div + className={cn( + 'flex w-full items-center justify-between gap-2 bg-primary px-4 py-2 text-primary-foreground', + inset && 'rounded-lg', + className + )} + {...(props as any)} + > + {children} + </div> + </BannerContext.Provider> + ); +}; + +export type BannerIconProps = HTMLAttributes<HTMLDivElement> & { + icon: LucideIcon; +}; + +export const BannerIcon = ({ + icon: Icon, + className, + ...props +}: BannerIconProps) => ( + <div + className={cn( + 'rounded-full border border-background/20 bg-background/10 p-1 shadow-sm', + className + )} + {...(props as any)} + > + <Icon size={16} /> + </div> +); + +export type BannerTitleProps = HTMLAttributes<HTMLParagraphElement>; + +export const BannerTitle = ({ className, ...props }: BannerTitleProps) => ( + <p className={cn('flex-1 text-sm', className)} {...(props as any)} /> +); + +export type BannerActionProps = ComponentProps<typeof Button>; + +export const BannerAction = ({ + variant = 'outline', + size = 'sm', + className, + ...props +}: BannerActionProps) => ( + <Button + className={cn( + 'shrink-0 bg-transparent hover:bg-background/10 hover:text-background', + className + )} + size={size} + variant={variant} + {...(props as any)} + /> +); + +export type BannerCloseProps = ComponentProps<typeof Button>; + +export const BannerClose = ({ + variant = 'ghost', + size = 'icon', + onClick, + className, + ...props +}: BannerCloseProps) => { + const { setShow } = useContext(BannerContext); + + const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => { + setShow(false); + onClick?.(e); + }; + + return ( + <Button + className={cn( + 'shrink-0 bg-transparent hover:bg-background/10 hover:text-background', + className + )} + onClick={handleClick} + size={size} + variant={variant} + {...(props as any)} + > + <XIcon size={18} /> + </Button> + ); +}; diff --git a/frontend/components/ui/shadcn-io/status/index.tsx b/frontend/components/ui/shadcn-io/status/index.tsx new file mode 100644 index 00000000..576b4c74 --- /dev/null +++ b/frontend/components/ui/shadcn-io/status/index.tsx @@ -0,0 +1,62 @@ +import type { ComponentProps, HTMLAttributes } from 'react'; +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; + +export type StatusProps = ComponentProps<typeof Badge> & { + status: 'online' | 'offline' | 'maintenance' | 'degraded'; +}; + +export const Status = ({ className, status, ...props }: StatusProps) => ( + <Badge + className={cn('flex items-center gap-2', 'group', status, className)} + variant="secondary" + {...(props as any)} + /> +); + +export type StatusIndicatorProps = HTMLAttributes<HTMLSpanElement>; + +export const StatusIndicator = ({ + className, + ...props +}: StatusIndicatorProps) => ( + <span className={cn('relative flex h-2 w-2', className)} {...(props as any)}> + <span + className={cn( + 'absolute inline-flex h-full w-full animate-ping rounded-full opacity-75', + 'group-[.online]:bg-emerald-500', + 'group-[.offline]:bg-red-500', + 'group-[.maintenance]:bg-blue-500', + 'group-[.degraded]:bg-amber-500' + )} + /> + <span + className={cn( + 'relative inline-flex h-2 w-2 rounded-full', + 'group-[.online]:bg-emerald-500', + 'group-[.offline]:bg-red-500', + 'group-[.maintenance]:bg-blue-500', + 'group-[.degraded]:bg-amber-500' + )} + /> + </span> +); + +export type StatusLabelProps = HTMLAttributes<HTMLSpanElement>; + +export const StatusLabel = ({ + className, + children, + ...props +}: StatusLabelProps) => ( + <span className={cn('text-muted-foreground', className)} {...(props as any)}> + {children ?? ( + <> + <span className="hidden group-[.online]:block">Online</span> + <span className="hidden group-[.offline]:block">Offline</span> + <span className="hidden group-[.maintenance]:block">Maintenance</span> + <span className="hidden group-[.degraded]:block">Degraded</span> + </> + )} + </span> +); diff --git a/frontend/components/ui/sheet.tsx b/frontend/components/ui/sheet.tsx new file mode 100644 index 00000000..7bb8ea06 --- /dev/null +++ b/frontend/components/ui/sheet.tsx @@ -0,0 +1,134 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { + return <SheetPrimitive.Root data-slot="sheet" {...props} /> +} + +function SheetTrigger({ + ...props +}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { + return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> +} + +function SheetClose({ + ...props +}: React.ComponentProps<typeof SheetPrimitive.Close>) { + return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> +} + +function SheetPortal({ + ...props +}: React.ComponentProps<typeof SheetPrimitive.Portal>) { + return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps<typeof SheetPrimitive.Overlay>) { + return ( + <SheetPrimitive.Overlay + data-slot="sheet-overlay" + className={cn( + "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-background/80 backdrop-blur-sm", + className + )} + {...props} + /> + ) +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps<typeof SheetPrimitive.Content> & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + <SheetPortal> + <SheetOverlay /> + <SheetPrimitive.Content + data-slot="sheet-content" + className={cn( + "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", + side === "right" && + "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", + side === "left" && + "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", + side === "top" && + "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", + side === "bottom" && + "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", + className + )} + {...props} + > + {children} + </SheetPrimitive.Content> + </SheetPortal> + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sheet-header" + className={cn("flex flex-col gap-1.5 p-4", className)} + {...props} + /> + ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sheet-footer" + className={cn("mt-auto flex flex-col gap-2 p-4", className)} + {...props} + /> + ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps<typeof SheetPrimitive.Title>) { + return ( + <SheetPrimitive.Title + data-slot="sheet-title" + className={cn("text-foreground font-semibold", className)} + {...props} + /> + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps<typeof SheetPrimitive.Description>) { + return ( + <SheetPrimitive.Description + data-slot="sheet-description" + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/frontend/components/ui/sidebar.tsx b/frontend/components/ui/sidebar.tsx new file mode 100644 index 00000000..28dbd480 --- /dev/null +++ b/frontend/components/ui/sidebar.tsx @@ -0,0 +1,726 @@ +"use client" + +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, VariantProps } from "class-variance-authority" +import { PanelLeftIcon } from "lucide-react" + +import { useIsMobile } from "@/hooks/use-mobile" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Skeleton } from "@/components/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +const SIDEBAR_COOKIE_NAME = "sidebar_state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = "14rem" +const SIDEBAR_WIDTH_MOBILE = "16rem" +const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +type SidebarContextProps = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext<SidebarContextProps | null>(null) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void +}) { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = React.useMemo<SidebarContextProps>( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + <SidebarContext.Provider value={contextValue}> + <TooltipProvider delayDuration={0}> + <div + data-slot="sidebar-wrapper" + style={ + { + "--sidebar-width": SIDEBAR_WIDTH, + "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, + ...style, + } as React.CSSProperties + } + className={cn( + "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", + className + )} + {...props} + > + {children} + </div> + </TooltipProvider> + </SidebarContext.Provider> + ) +} + +function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props +}: React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( + <div + data-slot="sidebar" + className={cn( + "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", + className + )} + {...props} + > + {children} + </div> + ) + } + + if (isMobile) { + return ( + <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> + <SheetContent + data-sidebar="sidebar" + data-slot="sidebar" + data-mobile="true" + className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" + style={ + { + "--sidebar-width": SIDEBAR_WIDTH_MOBILE, + } as React.CSSProperties + } + side={side} + > + <SheetHeader className="sr-only"> + <SheetTitle>Sidebar</SheetTitle> + <SheetDescription>Displays the mobile sidebar.</SheetDescription> + </SheetHeader> + <div className="flex h-full w-full flex-col">{children}</div> + </SheetContent> + </Sheet> + ) + } + + return ( + <div + className="group peer text-sidebar-foreground hidden md:block" + data-state={state} + data-collapsible={state === "collapsed" ? collapsible : ""} + data-variant={variant} + data-side={side} + data-slot="sidebar" + > + {/* This is what handles the sidebar gap on desktop */} + <div + data-slot="sidebar-gap" + className={cn( + "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear", + "group-data-[collapsible=offcanvas]:w-0", + "group-data-[side=right]:rotate-180", + variant === "floating" || variant === "inset" + ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" + : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)" + )} + /> + <div + data-slot="sidebar-container" + className={cn( + "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex", + side === "left" + ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" + : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", + // Adjust the padding for floating and inset variants. + variant === "floating" || variant === "inset" + ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" + : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", + className + )} + {...props} + > + <div + data-sidebar="sidebar" + data-slot="sidebar-inner" + className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm" + > + {children} + </div> + </div> + </div> + ) +} + +function SidebarTrigger({ + className, + onClick, + ...props +}: React.ComponentProps<typeof Button>) { + const { toggleSidebar } = useSidebar() + + return ( + <Button + data-sidebar="trigger" + data-slot="sidebar-trigger" + variant="ghost" + size="icon" + className={cn("size-7", className)} + onClick={(event) => { + onClick?.(event) + toggleSidebar() + }} + {...props} + > + <PanelLeftIcon /> + <span className="sr-only">Toggle Sidebar</span> + </Button> + ) +} + +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar() + + return ( + <button + data-sidebar="rail" + data-slot="sidebar-rail" + aria-label="Toggle Sidebar" + tabIndex={-1} + onClick={toggleSidebar} + title="Toggle Sidebar" + className={cn( + "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex", + "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", + "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", + "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full", + "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", + "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", + className + )} + {...props} + /> + ) +} + +function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { + return ( + <main + data-slot="sidebar-inset" + className={cn( + "bg-background relative flex w-full flex-1 flex-col", + "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", + className + )} + {...props} + /> + ) +} + +function SidebarInput({ + className, + ...props +}: React.ComponentProps<typeof Input>) { + return ( + <Input + data-slot="sidebar-input" + data-sidebar="input" + className={cn("bg-background h-8 w-full shadow-none", className)} + {...props} + /> + ) +} + +function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-header" + data-sidebar="header" + className={cn("flex flex-col gap-2 p-2", className)} + {...props} + /> + ) +} + +function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-footer" + data-sidebar="footer" + className={cn("flex flex-col gap-2 p-2", className)} + {...props} + /> + ) +} + +function SidebarSeparator({ + className, + ...props +}: React.ComponentProps<typeof Separator>) { + return ( + <Separator + data-slot="sidebar-separator" + data-sidebar="separator" + className={cn("bg-sidebar-border mx-2 w-auto", className)} + {...props} + /> + ) +} + +function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-content" + data-sidebar="content" + className={cn( + "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", + className + )} + {...props} + /> + ) +} + +function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-group" + data-sidebar="group" + className={cn("relative flex w-full min-w-0 flex-col p-2", className)} + {...props} + /> + ) +} + +function SidebarGroupLabel({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { asChild?: boolean }) { + const Comp = asChild ? Slot : "div" + + return ( + <Comp + data-slot="sidebar-group-label" + data-sidebar="group-label" + className={cn( + "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + className + )} + {...props} + /> + ) +} + +function SidebarGroupAction({ + className, + asChild = false, + ...props +}: React.ComponentProps<"button"> & { asChild?: boolean }) { + const Comp = asChild ? Slot : "button" + + return ( + <Comp + data-slot="sidebar-group-action" + data-sidebar="group-action" + className={cn( + "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 md:after:hidden", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ) +} + +function SidebarGroupContent({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-group-content" + data-sidebar="group-content" + className={cn("w-full text-sm", className)} + {...props} + /> + ) +} + +function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { + return ( + <ul + data-slot="sidebar-menu" + data-sidebar="menu" + className={cn("flex w-full min-w-0 flex-col gap-1", className)} + {...props} + /> + ) +} + +function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { + return ( + <li + data-slot="sidebar-menu-item" + data-sidebar="menu-item" + className={cn("group/menu-item relative", className)} + {...props} + /> + ) +} + +const sidebarMenuButtonVariants = cva( + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + { + variants: { + variant: { + default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", + outline: + "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", + }, + size: { + default: "h-8 text-sm", + sm: "h-7 text-xs", + lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function SidebarMenuButton({ + asChild = false, + isActive = false, + variant = "default", + size = "default", + tooltip, + className, + ...props +}: React.ComponentProps<"button"> & { + asChild?: boolean + isActive?: boolean + tooltip?: string | React.ComponentProps<typeof TooltipContent> +} & VariantProps<typeof sidebarMenuButtonVariants>) { + const Comp = asChild ? Slot : "button" + const { isMobile, state } = useSidebar() + + const button = ( + <Comp + data-slot="sidebar-menu-button" + data-sidebar="menu-button" + data-size={size} + data-active={isActive} + className={cn(sidebarMenuButtonVariants({ variant, size }), className)} + {...props} + /> + ) + + if (!tooltip) { + return button + } + + if (typeof tooltip === "string") { + tooltip = { + children: tooltip, + } + } + + return ( + <Tooltip> + <TooltipTrigger asChild>{button}</TooltipTrigger> + <TooltipContent + side="right" + align="center" + hidden={state !== "collapsed" || isMobile} + {...tooltip} + /> + </Tooltip> + ) +} + +function SidebarMenuAction({ + className, + asChild = false, + showOnHover = false, + ...props +}: React.ComponentProps<"button"> & { + asChild?: boolean + showOnHover?: boolean +}) { + const Comp = asChild ? Slot : "button" + + return ( + <Comp + data-slot="sidebar-menu-action" + data-sidebar="menu-action" + className={cn( + "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 md:after:hidden", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + showOnHover && + "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", + className + )} + {...props} + /> + ) +} + +function SidebarMenuBadge({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-menu-badge" + data-sidebar="menu-badge" + className={cn( + "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none", + "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ) +} + +function SidebarMenuSkeleton({ + className, + showIcon = false, + ...props +}: React.ComponentProps<"div"> & { + showIcon?: boolean +}) { + // Random width between 50 to 90%. + const width = React.useMemo(() => { + return `${Math.floor(Math.random() * 40) + 50}%` + }, []) + + return ( + <div + data-slot="sidebar-menu-skeleton" + data-sidebar="menu-skeleton" + className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} + {...props} + > + {showIcon && ( + <Skeleton + className="size-4 rounded-md" + data-sidebar="menu-skeleton-icon" + /> + )} + <Skeleton + className="h-4 max-w-(--skeleton-width) flex-1" + data-sidebar="menu-skeleton-text" + style={ + { + "--skeleton-width": width, + } as React.CSSProperties + } + /> + </div> + ) +} + +function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { + return ( + <ul + data-slot="sidebar-menu-sub" + data-sidebar="menu-sub" + className={cn( + "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ) +} + +function SidebarMenuSubItem({ + className, + ...props +}: React.ComponentProps<"li">) { + return ( + <li + data-slot="sidebar-menu-sub-item" + data-sidebar="menu-sub-item" + className={cn("group/menu-sub-item relative", className)} + {...props} + /> + ) +} + +function SidebarMenuSubButton({ + asChild = false, + size = "md", + isActive = false, + className, + ...props +}: React.ComponentProps<"a"> & { + asChild?: boolean + size?: "sm" | "md" + isActive?: boolean +}) { + const Comp = asChild ? Slot : "a" + + return ( + <Comp + data-slot="sidebar-menu-sub-button" + data-sidebar="menu-sub-button" + data-size={size} + data-active={isActive} + className={cn( + "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", + size === "sm" && "text-xs", + size === "md" && "text-sm", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ) +} + +export { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupAction, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarInput, + SidebarInset, + SidebarMenu, + SidebarMenuAction, + SidebarMenuBadge, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSkeleton, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarProvider, + SidebarRail, + SidebarSeparator, + SidebarTrigger, + useSidebar, +} diff --git a/frontend/components/ui/skeleton.tsx b/frontend/components/ui/skeleton.tsx new file mode 100644 index 00000000..32ea0ef7 --- /dev/null +++ b/frontend/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="skeleton" + className={cn("bg-accent animate-pulse rounded-md", className)} + {...props} + /> + ) +} + +export { Skeleton } diff --git a/frontend/components/ui/sonner.tsx b/frontend/components/ui/sonner.tsx new file mode 100644 index 00000000..2b4fcec7 --- /dev/null +++ b/frontend/components/ui/sonner.tsx @@ -0,0 +1,41 @@ +"use client" + +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from "lucide-react" +import { useTheme } from "next-themes" +import { Toaster as Sonner, ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + <Sonner + theme={theme as ToasterProps["theme"]} + position="top-center" + className="toaster group" + icons={{ + success: <CircleCheckIcon className="size-4" />, + info: <InfoIcon className="size-4" />, + warning: <TriangleAlertIcon className="size-4" />, + error: <OctagonXIcon className="size-4" />, + loading: <Loader2Icon className="size-4 animate-spin" />, + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as React.CSSProperties + } + {...props} + /> + ) +} + +export { Toaster } \ No newline at end of file diff --git a/frontend/components/ui/spinner.tsx b/frontend/components/ui/spinner.tsx new file mode 100644 index 00000000..a70e713c --- /dev/null +++ b/frontend/components/ui/spinner.tsx @@ -0,0 +1,16 @@ +import { Loader2Icon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Spinner({ className, ...props }: React.ComponentProps<"svg">) { + return ( + <Loader2Icon + role="status" + aria-label="Loading" + className={cn("size-4 animate-spin", className)} + {...props} + /> + ) +} + +export { Spinner } diff --git a/frontend/components/ui/switch.tsx b/frontend/components/ui/switch.tsx new file mode 100644 index 00000000..5f4117f0 --- /dev/null +++ b/frontend/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef<typeof SwitchPrimitives.Root>, + React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> +>(({ className, ...props }, ref) => ( + <SwitchPrimitives.Root + className={cn( + "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm 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", + className + )} + {...props} + ref={ref} + > + <SwitchPrimitives.Thumb + className={cn( + "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0" + )} + /> + </SwitchPrimitives.Root> +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/frontend/components/ui/table.tsx b/frontend/components/ui/table.tsx new file mode 100644 index 00000000..51b74dd5 --- /dev/null +++ b/frontend/components/ui/table.tsx @@ -0,0 +1,116 @@ +"use client" + +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( + <div + data-slot="table-container" + className="relative w-full overflow-x-auto" + > + <table + data-slot="table" + className={cn("w-full caption-bottom text-sm", className)} + {...props} + /> + </div> + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + <thead + data-slot="table-header" + className={cn("[&_tr]:border-b", className)} + {...props} + /> + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + <tbody + data-slot="table-body" + className={cn("[&_tr:last-child]:border-0", className)} + {...props} + /> + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + <tfoot + data-slot="table-footer" + className={cn( + "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + <tr + data-slot="table-row" + className={cn( + "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", + className + )} + {...props} + /> + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( + <th + data-slot="table-head" + className={cn( + "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + <td + data-slot="table-cell" + className={cn( + "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( + <caption + data-slot="table-caption" + className={cn("text-muted-foreground mt-4 text-sm", className)} + {...props} + /> + ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/frontend/components/ui/tabs.tsx b/frontend/components/ui/tabs.tsx new file mode 100644 index 00000000..5f081d31 --- /dev/null +++ b/frontend/components/ui/tabs.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Root>) { + return ( + <TabsPrimitive.Root + data-slot="tabs" + className={cn("flex flex-col gap-2", className)} + {...props} + /> + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.List>) { + return ( + <TabsPrimitive.List + data-slot="tabs-list" + className={cn( + "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]", + className + )} + {...props} + /> + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Trigger>) { + return ( + <TabsPrimitive.Trigger + data-slot="tabs-trigger" + className={cn( + "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap cursor-pointer transition-[color,box-shadow] focus-visible:ring-[1px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + className + )} + {...props} + /> + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Content>) { + return ( + <TabsPrimitive.Content + data-slot="tabs-content" + className={cn("flex-1 outline-none", className)} + {...props} + /> + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/frontend/components/ui/textarea.tsx b/frontend/components/ui/textarea.tsx new file mode 100644 index 00000000..89fc486f --- /dev/null +++ b/frontend/components/ui/textarea.tsx @@ -0,0 +1,18 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { + return ( + <textarea + data-slot="textarea" + className={cn( + "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-background dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border px-3 py-2 text-base shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + className + )} + {...props} + /> + ) +} + +export { Textarea } diff --git a/frontend/components/ui/toggle-group.tsx b/frontend/components/ui/toggle-group.tsx new file mode 100644 index 00000000..5eed401b --- /dev/null +++ b/frontend/components/ui/toggle-group.tsx @@ -0,0 +1,73 @@ +"use client" + +import * as React from "react" +import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" +import { type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { toggleVariants } from "@/components/ui/toggle" + +const ToggleGroupContext = React.createContext< + VariantProps<typeof toggleVariants> +>({ + size: "default", + variant: "default", +}) + +function ToggleGroup({ + className, + variant, + size, + children, + ...props +}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> & + VariantProps<typeof toggleVariants>) { + return ( + <ToggleGroupPrimitive.Root + data-slot="toggle-group" + data-variant={variant} + data-size={size} + className={cn( + "group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs", + className + )} + {...props} + > + <ToggleGroupContext.Provider value={{ variant, size }}> + {children} + </ToggleGroupContext.Provider> + </ToggleGroupPrimitive.Root> + ) +} + +function ToggleGroupItem({ + className, + children, + variant, + size, + ...props +}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & + VariantProps<typeof toggleVariants>) { + const context = React.useContext(ToggleGroupContext) + + return ( + <ToggleGroupPrimitive.Item + data-slot="toggle-group-item" + data-variant={context.variant || variant} + data-size={context.size || size} + className={cn( + toggleVariants({ + variant: context.variant || variant, + size: context.size || size, + }), + "min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l", + className + )} + {...props} + > + {children} + </ToggleGroupPrimitive.Item> + ) +} + +export { ToggleGroup, ToggleGroupItem } diff --git a/frontend/components/ui/toggle.tsx b/frontend/components/ui/toggle.tsx new file mode 100644 index 00000000..bef3bf6c --- /dev/null +++ b/frontend/components/ui/toggle.tsx @@ -0,0 +1,47 @@ +"use client" + +import * as React from "react" +import * as TogglePrimitive from "@radix-ui/react-toggle" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const toggleVariants = cva( + "inline-flex items-center justify-center gap-1.5 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground cursor-pointer disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[1px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", + { + variants: { + variant: { + default: "bg-transparent", + outline: + "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-9 px-2 min-w-9", + sm: "h-8 px-1.5 min-w-8", + lg: "h-10 px-2.5 min-w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Toggle({ + className, + variant, + size, + ...props +}: React.ComponentProps<typeof TogglePrimitive.Root> & + VariantProps<typeof toggleVariants>) { + return ( + <TogglePrimitive.Root + data-slot="toggle" + className={cn(toggleVariants({ variant, size, className }))} + {...props} + /> + ) +} + +export { Toggle, toggleVariants } diff --git a/frontend/components/ui/tooltip.tsx b/frontend/components/ui/tooltip.tsx new file mode 100644 index 00000000..4bb8e3c1 --- /dev/null +++ b/frontend/components/ui/tooltip.tsx @@ -0,0 +1,57 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { + return ( + <TooltipPrimitive.Provider + data-slot="tooltip-provider" + delayDuration={delayDuration} + {...props} + /> + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps<typeof TooltipPrimitive.Root>) { + return <TooltipPrimitive.Root data-slot="tooltip" {...props} /> +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { + return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} /> +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps<typeof TooltipPrimitive.Content>) { + return ( + <TooltipPrimitive.Portal> + <TooltipPrimitive.Content + data-slot="tooltip-content" + sideOffset={sideOffset} + className={cn( + "bg-foreground text-background 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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", + className + )} + {...props} + > + {children} + <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> + </TooltipPrimitive.Content> + </TooltipPrimitive.Portal> + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/frontend/components/vulnerabilities/index.ts b/frontend/components/vulnerabilities/index.ts new file mode 100644 index 00000000..2f482b09 --- /dev/null +++ b/frontend/components/vulnerabilities/index.ts @@ -0,0 +1,4 @@ +export { VulnerabilitiesDetailView } from './vulnerabilities-detail-view' +export { VulnerabilitiesDataTable } from './vulnerabilities-data-table' +export { createVulnerabilityColumns } from './vulnerabilities-columns' +export { VulnerabilityDetailDialog } from './vulnerability-detail-dialog' diff --git a/frontend/components/vulnerabilities/vulnerabilities-columns.tsx b/frontend/components/vulnerabilities/vulnerabilities-columns.tsx new file mode 100644 index 00000000..4bb58977 --- /dev/null +++ b/frontend/components/vulnerabilities/vulnerabilities-columns.tsx @@ -0,0 +1,248 @@ +"use client" + +import { ColumnDef } from "@tanstack/react-table" +import { Eye, Copy, Check } from "lucide-react" +import { Checkbox } from "@/components/ui/checkbox" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { useState } from "react" +import { toast } from "sonner" +import type { Vulnerability, VulnerabilitySeverity } from "@/types/vulnerability.types" + +/** + * 复制到剪贴板(兼容 HTTP 环境) + */ +async function copyToClipboard(text: string): Promise<boolean> { + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text) + } else { + // Fallback: 使用临时 textarea + const textArea = document.createElement('textarea') + textArea.value = text + textArea.style.position = 'fixed' + textArea.style.left = '-9999px' + textArea.style.top = '-9999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + document.execCommand('copy') + document.body.removeChild(textArea) + } + return true + } catch { + return false + } +} + +/** URL 弹窗组件 */ +function UrlPopover({ url }: { url: string }) { + const [copied, setCopied] = useState(false) + const [open, setOpen] = useState(false) + + const handleCopy = async (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + const success = await copyToClipboard(url) + if (success) { + setCopied(true) + toast.success("URL 已复制") + setTimeout(() => setCopied(false), 2000) + } else { + toast.error("复制失败") + } + } + + return ( + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent + className="w-auto max-w-[450px] p-0" + align="start" + onInteractOutside={(e) => { + // 复制中不关闭弹窗 + if (copied) e.preventDefault() + }} + > + <div className="group relative"> + <div className="text-xs break-all bg-muted/30 px-3 py-2.5 font-mono text-muted-foreground select-all max-h-40 overflow-y-auto"> + {url} + </div> + <Button + type="button" + variant="secondary" + size="sm" + className="absolute top-1.5 right-1.5 h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity shadow-sm" + onMouseDown={(e) => e.preventDefault()} + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3 w-3 text-green-500" /> + ) : ( + <Copy className="h-3 w-3" /> + )} + </Button> + </div> + </PopoverContent> + </Popover> + ) +} + +// 统一的漏洞严重程度颜色配置(与图表一致) +const severityConfig: Record<VulnerabilitySeverity, { label: string; className: string }> = { + critical: { label: "严重", className: "bg-red-600 text-white hover:bg-red-600" }, + high: { label: "高危", className: "bg-orange-500 text-white hover:bg-orange-500" }, + medium: { label: "中危", className: "bg-yellow-500 text-white hover:bg-yellow-500" }, + low: { label: "低危", className: "bg-blue-500 text-white hover:bg-blue-500" }, + info: { label: "信息", className: "bg-gray-500 text-white hover:bg-gray-500" }, +} + +interface ColumnActions { + formatDate: (date: string) => string + handleViewDetail: (vulnerability: Vulnerability) => void +} + +export function createVulnerabilityColumns({ + formatDate, + handleViewDetail, +}: ColumnActions): ColumnDef<Vulnerability>[] { + return [ + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="全选" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="选择行" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "severity", + header: "Status", + cell: ({ row }) => { + const severity = row.getValue("severity") as VulnerabilitySeverity + const config = severityConfig[severity] + return ( + <Badge className={config.className}> + {config.label} + </Badge> + ) + }, + }, + { + accessorKey: "source", + header: "Source", + cell: ({ row }) => { + const source = row.getValue("source") as string + return ( + <Badge variant="outline"> + {source} + </Badge> + ) + }, + }, + { + accessorKey: "vulnType", + header: "类型", + cell: ({ row }) => { + const vulnType = row.getValue("vulnType") as string + const vulnerability = row.original + return ( + <Tooltip> + <TooltipTrigger asChild> + <span + className="font-medium cursor-pointer hover:text-primary hover:underline underline-offset-2 transition-colors" + onClick={() => handleViewDetail(vulnerability)} + > + {vulnType} + </span> + </TooltipTrigger> + <TooltipContent>漏洞详情</TooltipContent> + </Tooltip> + ) + }, + }, + { + accessorKey: "url", + header: "URL", + size: 300, + minSize: 200, + maxSize: 400, + cell: ({ row }) => { + const url = row.original.url + if (!url) return <span className="text-muted-foreground">-</span> + + const maxLength = 40 + const isLong = url.length > maxLength + const displayUrl = isLong ? url.substring(0, maxLength) + "..." : url + + return ( + <div className="flex items-center gap-1 w-[280px] min-w-[280px]"> + <a + href={url} + target="_blank" + rel="noopener noreferrer" + className="text-sm text-blue-600 hover:underline truncate" + onClick={(e) => e.stopPropagation()} + > + {displayUrl} + </a> + {isLong && <UrlPopover url={url} />} + </div> + ) + }, + }, + { + accessorKey: "discoveredAt", + header: "发现时间", + cell: ({ row }) => { + const discoveredAt = row.getValue("discoveredAt") as string + return ( + <span className="text-sm text-muted-foreground"> + {formatDate(discoveredAt)} + </span> + ) + }, + }, + { + id: "actions", + header: "", + cell: ({ row }) => { + const vulnerability = row.original + + return ( + <div className="text-right"> + <Button + variant="ghost" + size="sm" + className="h-8 px-2" + onClick={() => handleViewDetail(vulnerability)} + > + <Eye className="h-4 w-4 mr-1" /> + 详情 + </Button> + </div> + ) + }, + }, + ] +} diff --git a/frontend/components/vulnerabilities/vulnerabilities-data-table.tsx b/frontend/components/vulnerabilities/vulnerabilities-data-table.tsx new file mode 100644 index 00000000..36107f5c --- /dev/null +++ b/frontend/components/vulnerabilities/vulnerabilities-data-table.tsx @@ -0,0 +1,425 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconTrash, + IconDownload, + IconSearch, + IconLoader2, +} from "@tabler/icons-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import type { Vulnerability } from "@/types/vulnerability.types" +import type { PaginationInfo } from "@/types/common.types" + +interface VulnerabilitiesDataTableProps { + data: Vulnerability[] + columns: ColumnDef<Vulnerability>[] + searchPlaceholder?: string + searchColumn?: string + searchValue?: string + onSearch?: (value: string) => void + isSearching?: boolean + pagination?: { pageIndex: number; pageSize: number } + setPagination?: React.Dispatch<React.SetStateAction<{ pageIndex: number; pageSize: number }>> + paginationInfo?: PaginationInfo + onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void + onBulkDelete?: () => void // 批量删除回调函数 + onSelectionChange?: (selectedRows: Vulnerability[]) => void // 选中行变化回调 + // 下载回调函数 + onDownloadAll?: () => void // 下载所有漏洞 + onDownloadSelected?: () => void // 下载选中的漏洞 +} + +export function VulnerabilitiesDataTable({ + data = [], + columns, + searchPlaceholder = "搜索漏洞类型...", + searchColumn = "title", + searchValue, + onSearch, + isSearching = false, + pagination, + setPagination, + paginationInfo, + onPaginationChange, + onBulkDelete, + onSelectionChange, + onDownloadAll, + onDownloadSelected, +}: VulnerabilitiesDataTableProps) { + const [sorting, setSorting] = React.useState<SortingState>([]) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const [internalPagination, setInternalPagination] = React.useState({ + pageIndex: 0, + pageSize: 10, + }) + + // 本地搜索输入状态(只在回车或点击按钮时触发搜索) + const [localSearchValue, setLocalSearchValue] = React.useState(searchValue ?? "") + + React.useEffect(() => { + setLocalSearchValue(searchValue ?? "") + }, [searchValue]) + + const handleSearchSubmit = () => { + if (onSearch) { + onSearch(localSearchValue) + } else { + table.getColumn(searchColumn)?.setFilterValue(localSearchValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearchSubmit() + } + } + + const useServerPagination = !!paginationInfo && !!pagination && !!setPagination + const tablePagination = useServerPagination ? pagination : internalPagination + const setTablePagination = useServerPagination ? setPagination : setInternalPagination + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination: tablePagination, + }, + getRowId: (row) => row.id.toString(), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = + typeof updater === "function" ? updater(tablePagination) : updater + setTablePagination?.(nextPagination as { pageIndex: number; pageSize: number }) + onPaginationChange?.(nextPagination) + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + manualPagination: useServerPagination, + pageCount: useServerPagination + ? paginationInfo?.totalPages ?? -1 + : Math.ceil(data.length / tablePagination.pageSize) || 1, + }) + + const totalItems = useServerPagination + ? paginationInfo?.total ?? data.length + : table.getFilteredRowModel().rows.length + + // 处理选中行变化 + React.useEffect(() => { + if (onSelectionChange) { + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + onSelectionChange(selectedRows) + } + }, [rowSelection, onSelectionChange, table]) + + return ( + <div className="w-full space-y-4"> + {/* 工具栏 */} + <div className="flex items-center justify-between"> + {/* 搜索框 */} + <div className="flex items-center space-x-2"> + <Input + placeholder={searchPlaceholder} + value={localSearchValue} + onChange={(e) => setLocalSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="h-8 max-w-sm" + /> + <Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}> + {isSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + + {/* 右侧操作按钮 */} + <div className="flex items-center space-x-2"> + {/* 列显示控制 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {column.id === "select" && "Select"} + {column.id === "title" && "Title"} + {column.id === "severity" && "Severity"} + {column.id === "status" && "Status"} + {column.id === "url" && "URL"} + {column.id === "createdAt" && "Created At"} + {column.id === "actions" && "Actions"} + {!["select", "title", "severity", "status", "url", "createdAt", "actions"].includes(column.id) && column.id} + </DropdownMenuCheckboxItem> + ))} + </DropdownMenuContent> + </DropdownMenu> + + {/* 下载按钮 */} + {(onDownloadAll || onDownloadSelected) && ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconDownload /> + Download + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-64"> + <DropdownMenuLabel>Download Options</DropdownMenuLabel> + <DropdownMenuSeparator /> + {onDownloadAll && ( + <DropdownMenuItem onClick={onDownloadAll}> + <IconDownload className="h-4 w-4" /> + Download All Vulnerabilities + </DropdownMenuItem> + )} + {onDownloadSelected && ( + <DropdownMenuItem + onClick={onDownloadSelected} + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + > + <IconDownload className="h-4 w-4" /> + Download Selected Vulnerabilities + </DropdownMenuItem> + )} + </DropdownMenuContent> + </DropdownMenu> + )} + + {/* 批量删除按钮 */} + {onBulkDelete && ( + <Button + onClick={onBulkDelete} + size="sm" + variant="outline" + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + className={ + table.getFilteredSelectedRowModel().rows.length === 0 + ? "text-muted-foreground" + : "text-destructive hover:text-destructive hover:bg-destructive/10" + } + > + <IconTrash /> + Delete + </Button> + )} + </div> + </div> + + <div className="rounded-md border"> + <Table> + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + 暂无数据 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {/* 分页控制 */} + <div className="flex items-center justify-between px-2"> + {/* 选中行信息 */} + <div className="flex-1 text-sm text-muted-foreground"> + {table.getFilteredSelectedRowModel().rows.length} of{" "} + {paginationInfo ? paginationInfo.total : table.getFilteredRowModel().rows.length} row(s) selected + </div> + + {/* 分页控制器 */} + <div className="flex items-center space-x-6 lg:space-x-8"> + {/* 每页显示数量选择 */} + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + Rows per page + </Label> + <Select + value={`${table.getState().pagination.pageSize}`} + onValueChange={(value) => { + table.setPageSize(Number(value)) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={table.getState().pagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 页码信息 */} + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + </div> + + {/* 分页按钮 */} + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to first page</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to previous page</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to next page</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to last page</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + </div> + ) +} diff --git a/frontend/components/vulnerabilities/vulnerabilities-detail-view.tsx b/frontend/components/vulnerabilities/vulnerabilities-detail-view.tsx new file mode 100644 index 00000000..ded38678 --- /dev/null +++ b/frontend/components/vulnerabilities/vulnerabilities-detail-view.tsx @@ -0,0 +1,276 @@ +"use client" + +import React, { useState, useMemo } from "react" +import { VulnerabilitiesDataTable } from "./vulnerabilities-data-table" +import { createVulnerabilityColumns } from "./vulnerabilities-columns" +import { VulnerabilityDetailDialog } from "./vulnerability-detail-dialog" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import type { Vulnerability } from "@/types/vulnerability.types" +import { useScanVulnerabilities, useTargetVulnerabilities, useAllVulnerabilities } from "@/hooks/use-vulnerabilities" + +interface VulnerabilitiesDetailViewProps { + /** 扫描历史页面使用:按 scan 维度查看漏洞 */ + scanId?: number + /** 目标详情页面使用:按 target 维度查看漏洞 */ + targetId?: number +} + +export function VulnerabilitiesDetailView({ + scanId, + targetId, +}: VulnerabilitiesDetailViewProps) { + const [selectedVulnerabilities, setSelectedVulnerabilities] = useState<Vulnerability[]>([]) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [vulnerabilityToDelete, setVulnerabilityToDelete] = useState<Vulnerability | null>(null) + const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [detailDialogOpen, setDetailDialogOpen] = useState(false) + const [selectedVulnerability, setSelectedVulnerability] = useState<Vulnerability | null>(null) + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + + const [searchQuery, setSearchQuery] = useState("") + const [isSearching, setIsSearching] = useState(false) + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, pageIndex: 0 })) + } + + const paginationParams = { + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: searchQuery || undefined, + } + + // 按 scan 维度加载(扫描历史页面) + const scanQuery = useScanVulnerabilities( + scanId ?? 0, + paginationParams, + { enabled: !!scanId }, + ) + + // 按 target 维度加载(目标详情页面) + const targetQuery = useTargetVulnerabilities( + targetId ?? 0, + paginationParams, + { enabled: !!targetId && !scanId }, + ) + + // 获取所有漏洞(全局漏洞页面) + const allQuery = useAllVulnerabilities( + paginationParams, + { enabled: !scanId && !targetId }, + ) + + // 根据参数选择使用哪个 query + const activeQuery = scanId ? scanQuery : targetId ? targetQuery : allQuery + const isQueryLoading = activeQuery.isLoading + + // 当请求完成时重置搜索状态 + React.useEffect(() => { + if (!activeQuery.isFetching && isSearching) { + setIsSearching(false) + } + }, [activeQuery.isFetching, isSearching]) + + const vulnerabilities = activeQuery.data?.vulnerabilities ?? [] + const paginationInfo = activeQuery.data?.pagination ?? { + total: vulnerabilities.length, + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + totalPages: 1, + } + + const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + } + + const navigate = (path: string) => { + console.log("导航到:", path) + } + + const handleViewDetail = (vulnerability: Vulnerability) => { + setSelectedVulnerability(vulnerability) + setDetailDialogOpen(true) + } + + const handleDeleteVulnerability = (vulnerability: Vulnerability) => { + setVulnerabilityToDelete(vulnerability) + setDeleteDialogOpen(true) + } + + const confirmDelete = async () => { + if (!vulnerabilityToDelete) return + + setDeleteDialogOpen(false) + setIsLoading(true) + + setTimeout(() => { + console.log("删除漏洞:", vulnerabilityToDelete.id) + setVulnerabilityToDelete(null) + setIsLoading(false) + }, 1000) + } + + const handleBulkDelete = () => { + if (selectedVulnerabilities.length === 0) { + return + } + setBulkDeleteDialogOpen(true) + } + + const confirmBulkDelete = async () => { + if (selectedVulnerabilities.length === 0) return + + const deletedIds = selectedVulnerabilities.map(vulnerability => vulnerability.id) + + setBulkDeleteDialogOpen(false) + setIsLoading(true) + + setTimeout(() => { + console.log("批量删除漏洞:", deletedIds) + setSelectedVulnerabilities([]) + setIsLoading(false) + }, 1000) + } + + const handlePaginationChange = (newPagination: { pageIndex: number; pageSize: number }) => { + setPagination(newPagination) + } + + // 处理下载所有漏洞 + const handleDownloadAll = () => { + // TODO: 实现下载所有漏洞功能 + console.log('下载所有漏洞') + } + + // 处理下载选中的漏洞 + const handleDownloadSelected = () => { + // TODO: 实现下载选中的漏洞功能 + console.log('下载选中的漏洞:', selectedVulnerabilities) + if (selectedVulnerabilities.length === 0) { + return + } + } + + const vulnerabilityColumns = useMemo( + () => + createVulnerabilityColumns({ + formatDate, + handleViewDetail, + }), + [handleViewDetail] + ) + + if ((isLoading || isQueryLoading) && !activeQuery.data) { + return ( + <DataTableSkeleton + toolbarButtonCount={2} + rows={6} + columns={6} + /> + ) + } + + return ( + <> + <VulnerabilityDetailDialog + vulnerability={selectedVulnerability} + open={detailDialogOpen} + onOpenChange={setDetailDialogOpen} + /> + + <VulnerabilitiesDataTable + data={vulnerabilities} + columns={vulnerabilityColumns} + searchPlaceholder="搜索漏洞类型..." + searchColumn="vulnType" + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + pagination={pagination} + setPagination={setPagination} + paginationInfo={{ + total: paginationInfo.total, + page: paginationInfo.page, + pageSize: paginationInfo.pageSize, + totalPages: paginationInfo.totalPages, + }} + onPaginationChange={handlePaginationChange} + onSelectionChange={setSelectedVulnerabilities} + /> + + <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认删除</AlertDialogTitle> + <AlertDialogDescription> + 此操作无法撤销。这将永久删除漏洞 "{vulnerabilityToDelete?.vulnType}" 及其相关数据。 + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + 删除 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + + <AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>确认批量删除</AlertDialogTitle> + <AlertDialogDescription> + 此操作无法撤销。这将永久删除以下 {selectedVulnerabilities.length} 个漏洞及其相关数据。 + </AlertDialogDescription> + </AlertDialogHeader> + <div className="mt-2 p-2 bg-muted rounded-md max-h-96 overflow-y-auto"> + <ul className="text-sm space-y-1"> + {selectedVulnerabilities.map((vulnerability) => ( + <li key={vulnerability.id} className="flex items-center"> + <span className="font-medium">{vulnerability.vulnType}</span> + </li> + ))} + </ul> + </div> + <AlertDialogFooter> + <AlertDialogCancel>取消</AlertDialogCancel> + <AlertDialogAction + onClick={confirmBulkDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + 删除 {selectedVulnerabilities.length} 个漏洞 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </> + ) +} diff --git a/frontend/components/vulnerabilities/vulnerability-detail-dialog.tsx b/frontend/components/vulnerabilities/vulnerability-detail-dialog.tsx new file mode 100644 index 00000000..ac1e49c2 --- /dev/null +++ b/frontend/components/vulnerabilities/vulnerability-detail-dialog.tsx @@ -0,0 +1,395 @@ +"use client" + +import React from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Button } from "@/components/ui/button" +import { Copy, Check, Info, FileCode, Terminal, Database, ExternalLink } from "lucide-react" +import { toast } from "sonner" +import type { Vulnerability, VulnerabilitySeverity } from "@/types/vulnerability.types" + +const severityConfig: Record<VulnerabilitySeverity, { label: string; variant: "default" | "secondary" | "destructive" | "outline"; color: string }> = { + critical: { label: "严重", variant: "destructive", color: "bg-red-500" }, + high: { label: "高危", variant: "destructive", color: "bg-orange-500" }, + medium: { label: "中危", variant: "default", color: "bg-yellow-500" }, + low: { label: "低危", variant: "secondary", color: "bg-blue-500" }, + info: { label: "信息", variant: "outline", color: "bg-gray-500" }, +} + +interface VulnerabilityDetailDialogProps { + vulnerability: Vulnerability | null + open: boolean + onOpenChange: (open: boolean) => void +} + +/** + * 漏洞详情对话框组件 - Tab 分页式 + */ +export function VulnerabilityDetailDialog({ + vulnerability, + open, + onOpenChange, +}: VulnerabilityDetailDialogProps) { + const [copiedField, setCopiedField] = React.useState<string | null>(null) + const dialogRef = React.useRef<HTMLDivElement>(null) + + // 兼容 HTTP 环境的复制实现(解决 Dialog focus trap 问题) + const copyToClipboard = React.useCallback(async (text: string): Promise<boolean> => { + const value = text ?? "" + // 1) 优先尝试 Clipboard API(无条件先试,部分浏览器在 HTTP 也放行) + if (navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(value) + return true + } catch { + // 继续走 fallback + } + } + + // 2) 兼容 HTTP 的 textarea 方案 + // 关键:将 textarea 添加到 Dialog 内部,避免 focus trap 阻止焦点 + try { + const container = dialogRef.current || document.body + const textArea = document.createElement("textarea") + textArea.value = value + textArea.style.position = "fixed" + textArea.style.left = "-9999px" + textArea.style.top = "-9999px" + textArea.style.opacity = "0" + container.appendChild(textArea) + textArea.focus() + textArea.select() + textArea.setSelectionRange(0, textArea.value.length) + const success = document.execCommand("copy") + container.removeChild(textArea) + return success + } catch { + return false + } + }, []) + + // 处理复制(兼容 HTTP 环境)- 必须在条件返回之前声明 + const handleCopy = React.useCallback(async (text: string, field: string) => { + const success = await copyToClipboard(text) + if (success) { + setCopiedField(field) + setTimeout(() => setCopiedField(null), 2000) + toast.success("已复制") + } else { + toast.error("复制失败") + } + }, [copyToClipboard]) + + if (!vulnerability) return null + + const raw = vulnerability.rawOutput || {} + + const severityConf = severityConfig[vulnerability.severity] || severityConfig.info + + const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + } + + // 从 rawOutput 提取信息 + const curlCommand = raw["curl-command"] as string | undefined + const request = raw.request as string | undefined + const response = raw.response as string | undefined + const cweId = raw.cwe || raw.info?.classification?.["cwe-id"]?.join(", ") + const cveId = raw.info?.classification?.["cve-id"] + const references = raw.info?.reference as string[] | undefined + const payload = raw.payload as string | undefined + const param = raw.param as string | undefined + const evidence = raw.evidence as string | undefined + const method = raw.method as string | undefined + const infoDesc = raw.info?.description as string | undefined + + // 判断来源 + const isNuclei = vulnerability.source === "nuclei" + const isDalfox = vulnerability.source === "dalfox" + + // 代码块组件(hover 显示复制图标按钮) + const CodeBlock = ({ content, field, maxHeight = "max-h-64" }: { content: string; field: string; maxHeight?: string }) => ( + <div className="relative group"> + <Button + variant="ghost" + size="icon" + className={`absolute right-2 top-2 z-10 h-6 w-6 hover:bg-accent transition-opacity ${ + copiedField === field ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' + }`} + onClick={() => handleCopy(content, field)} + > + {copiedField === field ? ( + <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + <div className={`bg-muted p-4 rounded-lg overflow-auto ${maxHeight}`}> + <pre className="text-xs font-mono whitespace-pre-wrap break-all">{content}</pre> + </div> + </div> + ) + + // 内容框组件(hover 显示复制图标按钮) + const ContentBox = ({ title, content, field }: { title: string; content: string; field: string }) => ( + <div className="p-4 rounded-lg border bg-card relative group"> + <Button + variant="ghost" + size="icon" + className={`absolute right-3 top-3 z-10 h-6 w-6 hover:bg-accent transition-opacity ${ + copiedField === field ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' + }`} + onClick={() => handleCopy(content, field)} + > + {copiedField === field ? ( + <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + <h3 className="font-semibold text-sm mb-2">{title}</h3> + <p className="text-sm text-muted-foreground break-all">{content}</p> + </div> + ) + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent ref={dialogRef} className="max-w-4xl max-h-[85vh] p-0 gap-0 overflow-hidden"> + {/* 头部 - pr-10 给关闭按钮留出空间 */} + <div className="px-6 py-4 border-b bg-muted/30 pr-12"> + <DialogHeader> + <div className="flex items-center gap-3"> + <Badge variant={severityConf.variant} className="text-sm px-3 py-1"> + {severityConf.label} + </Badge> + <DialogTitle className="flex-1 text-lg font-semibold truncate"> + {vulnerability.vulnType} + </DialogTitle> + <Badge variant="outline" className="text-xs shrink-0"> + {vulnerability.source.toUpperCase()} + </Badge> + </div> + {vulnerability.description && ( + <p className="text-sm text-muted-foreground mt-2">{vulnerability.description}</p> + )} + </DialogHeader> + </div> + + {/* Tab 内容 */} + <Tabs defaultValue="overview" className="flex-1 flex flex-col"> + <div className="px-6 pt-2 border-b"> + <TabsList className="h-10"> + <TabsTrigger value="overview" className="gap-2"> + <Info className="h-4 w-4" /> + 概览 + </TabsTrigger> + <TabsTrigger value="evidence" className="gap-2"> + <FileCode className="h-4 w-4" /> + {isNuclei ? "请求/响应" : "证据"} + </TabsTrigger> + {curlCommand && ( + <TabsTrigger value="reproduce" className="gap-2"> + <Terminal className="h-4 w-4" /> + 复现 + </TabsTrigger> + )} + <TabsTrigger value="raw" className="gap-2"> + <Database className="h-4 w-4" /> + 原始数据 + </TabsTrigger> + </TabsList> + </div> + + <ScrollArea className="flex-1 px-6 py-4"> + {/* 概览 Tab */} + <TabsContent value="overview" className="mt-0 space-y-6"> + {/* 基本信息卡片 */} + <div className="grid gap-4 md:grid-cols-2"> + <div className="space-y-4 p-4 rounded-lg border bg-card"> + <h3 className="font-semibold text-sm flex items-center gap-2"> + <Info className="h-4 w-4" /> + 基本信息 + </h3> + <div className="space-y-3 text-sm"> + <div className="flex justify-between"> + <span className="text-muted-foreground">ID</span> + <span className="font-mono">{vulnerability.id}</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">发现时间</span> + <span>{formatDate(vulnerability.discoveredAt)}</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">严重性</span> + <Badge variant={severityConf.variant}>{severityConf.label}</Badge> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">来源</span> + <Badge variant="outline">{vulnerability.source.toUpperCase()}</Badge> + </div> + {method && ( + <div className="flex justify-between"> + <span className="text-muted-foreground">HTTP 方法</span> + <Badge variant="outline">{method}</Badge> + </div> + )} + {param && ( + <div className="flex justify-between"> + <span className="text-muted-foreground">参数</span> + <code className="text-xs bg-muted px-2 py-1 rounded">{param}</code> + </div> + )} + </div> + </div> + + <div className="space-y-4 p-4 rounded-lg border bg-card"> + <h3 className="font-semibold text-sm">漏洞分类</h3> + <div className="space-y-3 text-sm"> + {cweId && ( + <div className="flex justify-between"> + <span className="text-muted-foreground">CWE</span> + <code className="text-xs bg-muted px-2 py-1 rounded">{cweId}</code> + </div> + )} + {cveId && ( + <div className="flex justify-between"> + <span className="text-muted-foreground">CVE</span> + <code className="text-xs bg-muted px-2 py-1 rounded">{cveId}</code> + </div> + )} + {vulnerability.cvssScore && ( + <div className="flex justify-between"> + <span className="text-muted-foreground">CVSS</span> + <Badge variant="destructive">{vulnerability.cvssScore}</Badge> + </div> + )} + {!cweId && !cveId && !vulnerability.cvssScore && ( + <p className="text-muted-foreground text-xs">暂无分类信息</p> + )} + </div> + </div> + </div> + + {/* URL */} + <ContentBox title="漏洞 URL" content={vulnerability.url} field="url" /> + + {/* 详细描述 */} + {infoDesc && ( + <div className="p-4 rounded-lg border bg-card"> + <h3 className="font-semibold text-sm mb-2">详细描述</h3> + <p className="text-sm text-muted-foreground">{infoDesc}</p> + </div> + )} + + {/* 参考链接 */} + {references && references.length > 0 && ( + <div className="p-4 rounded-lg border bg-card"> + <h3 className="font-semibold text-sm mb-3">参考链接</h3> + <ul className="space-y-2"> + {references.map((ref: string, index: number) => ( + <li key={index}> + <a + href={ref} + target="_blank" + rel="noopener noreferrer" + className="text-sm text-blue-600 hover:underline break-all flex items-center gap-1" + > + {ref} + <ExternalLink className="h-3 w-3 flex-shrink-0" /> + </a> + </li> + ))} + </ul> + </div> + )} + </TabsContent> + + {/* 证据/请求响应 Tab */} + <TabsContent value="evidence" className="mt-0 space-y-4"> + {isDalfox && ( + <> + {payload && ( + <div> + <h3 className="font-semibold text-sm mb-2">Payload</h3> + <CodeBlock content={payload} field="payload" /> + </div> + )} + {evidence && ( + <div> + <h3 className="font-semibold text-sm mb-2">Evidence</h3> + <CodeBlock content={evidence} field="evidence" /> + </div> + )} + </> + )} + + {isNuclei && ( + <> + {request && ( + <div> + <h3 className="font-semibold text-sm mb-2">HTTP Request</h3> + <CodeBlock content={request} field="request" maxHeight="max-h-80" /> + </div> + )} + {response && ( + <div> + <h3 className="font-semibold text-sm mb-2">HTTP Response</h3> + <CodeBlock content={response} field="response" maxHeight="max-h-80" /> + </div> + )} + </> + )} + + {!payload && !evidence && !request && !response && ( + <p className="text-muted-foreground text-sm">暂无证据数据</p> + )} + </TabsContent> + + {/* 复现 Tab */} + {curlCommand && ( + <TabsContent value="reproduce" className="mt-0 space-y-4"> + <div> + <h3 className="font-semibold text-sm mb-2">CURL 命令</h3> + <p className="text-xs text-muted-foreground mb-3"> + 复制以下命令到终端执行,即可复现此漏洞 + </p> + <CodeBlock content={curlCommand} field="curl" maxHeight="max-h-40" /> + </div> + </TabsContent> + )} + + {/* 原始数据 Tab */} + <TabsContent value="raw" className="mt-0"> + <div> + <h3 className="font-semibold text-sm mb-2">原始输出 (JSON)</h3> + <p className="text-xs text-muted-foreground mb-3"> + 扫描工具的完整输出数据 + </p> + <CodeBlock + content={JSON.stringify(raw, null, 2)} + field="raw" + maxHeight="max-h-[50vh]" + /> + </div> + </TabsContent> + </ScrollArea> + </Tabs> + </DialogContent> + </Dialog> + ) +} diff --git a/frontend/components/websites/websites-columns.tsx b/frontend/components/websites/websites-columns.tsx new file mode 100644 index 00000000..457b24c3 --- /dev/null +++ b/frontend/components/websites/websites-columns.tsx @@ -0,0 +1,519 @@ +"use client" + +import React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { Checkbox } from "@/components/ui/checkbox" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { IconDots, IconEye, IconExternalLink } from "@tabler/icons-react" +import { Copy, Check, ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react" +import { toast } from "sonner" +import type { WebSite } from "@/types/website.types" + +/** + * 可复制单元格组件 + */ +function CopyableCell({ + value, + maxWidth = "400px", + truncateLength = 50, + successMessage = "已复制", + className = "font-medium" +}: { + value: string + maxWidth?: string + truncateLength?: number + successMessage?: string + className?: string +}) { + const [copied, setCopied] = React.useState(false) + const isLong = value.length > truncateLength + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(value) + setCopied(true) + toast.success(successMessage) + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error('复制失败') + } + } + + return ( + <div className="group inline-flex items-center gap-1" style={{ maxWidth }}> + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <div className={`text-sm truncate cursor-default ${className}`}> + {value} + </div> + </TooltipTrigger> + <TooltipContent + side="top" + align="start" + sideOffset={5} + className={`text-xs ${isLong ? 'max-w-[500px] break-all' : 'whitespace-nowrap'}`} + > + {value} + </TooltipContent> + </Tooltip> + </TooltipProvider> + + <TooltipProvider delayDuration={500} skipDelayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className={`h-6 w-6 flex-shrink-0 hover:bg-accent transition-opacity ${ + copied ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' + }`} + onClick={handleCopy} + > + {copied ? ( + <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> + ) : ( + <Copy className="h-3.5 w-3.5 text-muted-foreground" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent side="top"> + <p className="text-xs">{copied ? '已复制!' : '点击复制'}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + ) +} + +/** + * 数据表格列头组件 - 支持排序 + */ +function DataTableColumnHeader({ + column, + title, +}: { + column: { getCanSort: () => boolean; getIsSorted: () => false | "asc" | "desc"; toggleSorting: (desc?: boolean) => void } + title: string +}) { + if (!column.getCanSort()) { + return <div className="-ml-3 font-medium">{title}</div> + } + + const isSorted = column.getIsSorted() + + return ( + <Button + variant="ghost" + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted" + > + {title} + {isSorted === "asc" ? ( + <ChevronUp className="h-4 w-4" /> + ) : isSorted === "desc" ? ( + <ChevronDown className="h-4 w-4" /> + ) : ( + <ChevronsUpDown className="h-4 w-4" /> + )} + </Button> + ) +} + +interface CreateWebSiteColumnsProps { + formatDate: (dateString: string) => string + onViewDetail?: (website: WebSite) => void +} + +export function createWebSiteColumns({ + formatDate, + onViewDetail, +}: CreateWebSiteColumnsProps): ColumnDef<WebSite>[] { + return [ + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "url", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="URL" /> + ), + size: 300, + minSize: 200, + maxSize: 400, + cell: ({ row }) => { + const url = row.getValue("url") as string + if (!url) return <span className="text-muted-foreground text-sm">-</span> + + const maxLength = 40 + const isLong = url.length > maxLength + const displayUrl = isLong ? url.substring(0, maxLength) + "..." : url + + return ( + <div className="flex items-center gap-1 w-[280px] min-w-[280px]"> + <span className="text-sm font-mono truncate"> + {displayUrl} + </span> + {isLong && ( + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整 URL</h4> + <div className="text-xs break-all bg-muted p-2 rounded max-h-48 overflow-y-auto font-mono"> + {url} + </div> + </div> + </PopoverContent> + </Popover> + )} + </div> + ) + }, + }, + { + accessorKey: "title", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Title" /> + ), + cell: ({ row }) => { + const title = row.getValue("title") as string + if (!title) return "-" + + const maxLength = 30 + const isLong = title.length > maxLength + const displayText = isLong ? title.substring(0, maxLength) : title + + if (!isLong) { + return <span className="text-sm">{title}</span> + } + + return ( + <div className="flex items-center gap-1"> + <span className="text-sm">{displayText}</span> + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整标题</h4> + <div className="text-sm break-all bg-muted p-2 rounded max-h-32 overflow-y-auto"> + {title} + </div> + </div> + </PopoverContent> + </Popover> + </div> + ) + }, + }, + { + accessorKey: "statusCode", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Status" /> + ), + cell: ({ row }) => { + const statusCode = row.getValue("statusCode") as number + if (!statusCode) return "-" + + let variant: "default" | "secondary" | "destructive" | "outline" = "default" + if (statusCode >= 200 && statusCode < 300) { + variant = "default" + } else if (statusCode >= 300 && statusCode < 400) { + variant = "secondary" + } else if (statusCode >= 400) { + variant = "destructive" + } + + return <Badge variant={variant}>{statusCode}</Badge> + }, + }, + { + accessorKey: "contentLength", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Content Length" /> + ), + cell: ({ row }) => { + const contentLength = row.getValue("contentLength") as number + if (!contentLength) return "-" + return contentLength.toString() + }, + }, + { + accessorKey: "location", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Location" /> + ), + cell: ({ row }) => { + const location = row.getValue("location") as string + if (!location) return "-" + + const maxLength = 50 + const isLong = location.length > maxLength + const displayText = isLong ? location.substring(0, maxLength) : location + + if (!isLong) { + return <span className="text-sm">{location}</span> + } + + return ( + <div className="flex items-center gap-1"> + <span className="text-sm">{displayText}</span> + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整 Location</h4> + <div className="text-sm break-all bg-muted p-2 rounded max-h-32 overflow-y-auto"> + {location} + </div> + </div> + </PopoverContent> + </Popover> + </div> + ) + }, + }, + { + accessorKey: "webserver", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Web Server" /> + ), + cell: ({ row }) => { + const webserver = row.getValue("webserver") as string + if (!webserver) return "-" + + const maxLength = 20 + const isLong = webserver.length > maxLength + const displayText = isLong ? webserver.substring(0, maxLength) : webserver + + if (!isLong) { + return <span className="text-sm">{webserver}</span> + } + + return ( + <div className="flex items-center gap-1"> + <span className="text-sm">{displayText}</span> + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整 Web Server</h4> + <div className="text-sm break-all bg-muted p-2 rounded max-h-32 overflow-y-auto"> + {webserver} + </div> + </div> + </PopoverContent> + </Popover> + </div> + ) + }, + }, + { + accessorKey: "contentType", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Content Type" /> + ), + cell: ({ row }) => { + const contentType = row.getValue("contentType") as string + if (!contentType) return "-" + + const maxLength = 25 + const isLong = contentType.length > maxLength + const displayText = isLong ? contentType.substring(0, maxLength) : contentType + + if (!isLong) { + return <span className="text-sm">{contentType}</span> + } + + return ( + <div className="flex items-center gap-1"> + <span className="text-sm">{displayText}</span> + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整 Content Type</h4> + <div className="text-sm break-all bg-muted p-2 rounded max-h-32 overflow-y-auto"> + {contentType} + </div> + </div> + </PopoverContent> + </Popover> + </div> + ) + }, + }, + { + accessorKey: "tech", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Technologies" /> + ), + cell: ({ row }) => { + const tech = row.getValue("tech") as string[] + if (!tech || tech.length === 0) return "-" + + // 显示前2个技术,如果有更多就显示省略 + const displayTech = tech.slice(0, 2) + const hasMore = tech.length > 2 + + return ( + <div className="flex flex-wrap gap-1 max-w-[200px]"> + {displayTech.map((technology, index) => ( + <Badge key={index} variant="outline" className="text-xs"> + {technology} + </Badge> + ))} + {hasMore && ( + <Popover> + <PopoverTrigger asChild> + <Badge variant="secondary" className="text-xs cursor-pointer hover:bg-muted"> + +{tech.length - 2} + </Badge> + </PopoverTrigger> + <PopoverContent className="w-80 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">所有技术栈 ({tech.length})</h4> + <div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto"> + {tech.map((technology, index) => ( + <Badge + key={index} + variant="outline" + className="text-xs" + > + {technology} + </Badge> + ))} + </div> + </div> + </PopoverContent> + </Popover> + )} + </div> + ) + }, + }, + { + accessorKey: "bodyPreview", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Body Preview" /> + ), + cell: ({ row }) => { + const bodyPreview = row.getValue("bodyPreview") as string + if (!bodyPreview) return "-" + + const maxLength = 30 + const isLong = bodyPreview.length > maxLength + const displayText = isLong ? bodyPreview.substring(0, maxLength) : bodyPreview + + if (!isLong) { + return <span className="text-sm">{bodyPreview}</span> + } + + return ( + <div className="flex items-center gap-1"> + <span className="text-sm">{displayText}</span> + <Popover> + <PopoverTrigger asChild> + <span className="inline-flex items-center rounded border bg-muted px-1.5 text-[10px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground flex-shrink-0 transition-colors"> + ··· + </span> + </PopoverTrigger> + <PopoverContent className="w-96 p-3"> + <div className="space-y-2"> + <h4 className="font-medium text-sm">完整响应体预览</h4> + <div className="text-sm break-all bg-muted p-2 rounded max-h-32 overflow-y-auto"> + {bodyPreview} + </div> + </div> + </PopoverContent> + </Popover> + </div> + ) + }, + }, + { + accessorKey: "vhost", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="VHost" /> + ), + cell: ({ row }) => { + const vhost = row.getValue("vhost") as boolean | null + if (vhost === null) return "-" + return ( + <Badge variant={vhost ? "default" : "secondary"} className="text-xs"> + {vhost ? "true" : "false"} + </Badge> + ) + }, + }, + { + accessorKey: "discoveredAt", + header: ({ column }) => ( + <DataTableColumnHeader column={column} title="Discovered At" /> + ), + cell: ({ row }) => { + const discoveredAt = row.getValue("discoveredAt") as string + return <div className="text-sm">{discoveredAt ? formatDate(discoveredAt) : "-"}</div> + }, + }, + ] +} diff --git a/frontend/components/websites/websites-data-table.tsx b/frontend/components/websites/websites-data-table.tsx new file mode 100644 index 00000000..c0c2f64b --- /dev/null +++ b/frontend/components/websites/websites-data-table.tsx @@ -0,0 +1,423 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconTrash, + IconDownload, + IconSearch, + IconLoader2, +} from "@tabler/icons-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import type { WebSite } from "@/types/website.types" +import type { PaginationInfo } from "@/types/common.types" + +interface WebSitesDataTableProps { + data: WebSite[] + columns: ColumnDef<WebSite>[] + searchPlaceholder?: string + searchColumn?: string + searchValue?: string + onSearch?: (value: string) => void + isSearching?: boolean + pagination?: { pageIndex: number; pageSize: number } + setPagination?: React.Dispatch<React.SetStateAction<{ pageIndex: number; pageSize: number }>> + paginationInfo?: PaginationInfo + onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void + onBulkDelete?: () => void // 批量删除回调函数 + onSelectionChange?: (selectedRows: WebSite[]) => void // 选中行变化回调 + // 下载回调函数 + onDownloadAll?: () => void // 下载所有网站 + onDownloadSelected?: () => void // 下载选中的网站 +} + +export function WebSitesDataTable({ + data = [], + columns, + searchPlaceholder = "搜索主机名...", + searchColumn = "url", + searchValue, + onSearch, + isSearching = false, + pagination, + setPagination, + paginationInfo, + onPaginationChange, + onBulkDelete, + onSelectionChange, + onDownloadAll, + onDownloadSelected, +}: WebSitesDataTableProps) { + const [sorting, setSorting] = React.useState<SortingState>([]) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({}) + const [internalPagination, setInternalPagination] = React.useState({ + pageIndex: 0, + pageSize: 10, + }) + + // 本地搜索输入状态(只在回车或点击按钮时触发搜索) + const [localSearchValue, setLocalSearchValue] = React.useState(searchValue ?? "") + + React.useEffect(() => { + setLocalSearchValue(searchValue ?? "") + }, [searchValue]) + + const handleSearchSubmit = () => { + if (onSearch) { + onSearch(localSearchValue) + } else { + table.getColumn(searchColumn)?.setFilterValue(localSearchValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearchSubmit() + } + } + + const useServerPagination = !!paginationInfo && !!pagination && !!setPagination + const tablePagination = useServerPagination ? pagination : internalPagination + const setTablePagination = useServerPagination ? setPagination : setInternalPagination + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination: tablePagination, + }, + getRowId: (row) => row.id.toString(), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = + typeof updater === "function" ? updater(tablePagination) : updater + setTablePagination?.(nextPagination as { pageIndex: number; pageSize: number }) + onPaginationChange?.(nextPagination) + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + manualPagination: useServerPagination, + pageCount: useServerPagination + ? paginationInfo?.totalPages ?? -1 + : Math.ceil(data.length / tablePagination.pageSize) || 1, + }) + + const totalItems = useServerPagination + ? paginationInfo?.total ?? data.length + : table.getFilteredRowModel().rows.length + + // 处理选中行变化 + React.useEffect(() => { + if (onSelectionChange) { + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + onSelectionChange(selectedRows) + } + }, [rowSelection, onSelectionChange, table]) + + return ( + <div className="w-full space-y-4"> + {/* 工具栏 */} + <div className="flex items-center justify-between"> + {/* 搜索框 */} + <div className="flex items-center space-x-2"> + <Input + placeholder={searchPlaceholder} + value={localSearchValue} + onChange={(e) => setLocalSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="h-8 max-w-sm" + /> + <Button variant="outline" size="sm" onClick={handleSearchSubmit} disabled={isSearching}> + {isSearching ? ( + <IconLoader2 className="h-4 w-4 animate-spin" /> + ) : ( + <IconSearch className="h-4 w-4" /> + )} + </Button> + </div> + + {/* 右侧操作按钮 */} + <div className="flex items-center space-x-2"> + {/* 列显示控制 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconLayoutColumns /> + Columns + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {column.id === "select" && "Select"} + {column.id === "url" && "URL"} + {column.id === "title" && "Title"} + {column.id === "statusCode" && "Status"} + {column.id === "contentLength" && "Content Length"} + {column.id === "location" && "Location"} + {column.id === "webserver" && "Web Server"} + {column.id === "contentType" && "Content Type"} + {column.id === "tech" && "Technologies"} + {column.id === "bodyPreview" && "Body Preview"} + {column.id === "vhost" && "VHost"} + {column.id === "discoveredAt" && "Discovered At"} + {column.id === "actions" && "Actions"} + {!["select", "url", "title", "statusCode", "contentLength", "location", "webserver", "contentType", "tech", "bodyPreview", "vhost", "discoveredAt", "actions"].includes(column.id) && column.id} + </DropdownMenuCheckboxItem> + ))} + </DropdownMenuContent> + </DropdownMenu> + + {/* 下载按钮 */} + {(onDownloadAll || onDownloadSelected) && ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm"> + <IconDownload /> + Download + <IconChevronDown /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-64"> + <DropdownMenuLabel>Download Options</DropdownMenuLabel> + <DropdownMenuSeparator /> + {onDownloadAll && ( + <DropdownMenuItem onClick={onDownloadAll}> + <IconDownload className="h-4 w-4" /> + Download All Websites + </DropdownMenuItem> + )} + {onDownloadSelected && ( + <DropdownMenuItem + onClick={onDownloadSelected} + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + > + <IconDownload className="h-4 w-4" /> + Download Selected Websites + </DropdownMenuItem> + )} + </DropdownMenuContent> + </DropdownMenu> + )} + + {/* 批量删除按钮 */} + {onBulkDelete && ( + <Button + onClick={onBulkDelete} + size="sm" + variant="outline" + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + className={ + table.getFilteredSelectedRowModel().rows.length === 0 + ? "text-muted-foreground" + : "text-destructive hover:text-destructive hover:bg-destructive/10" + } + > + <IconTrash /> + Delete + </Button> + )} + </div> + </div> + + <div className="rounded-md border"> + <Table> + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => ( + <TableHead key={header.id}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ))} + </TableRow> + ))} + </TableHeader> + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + className="group" + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell colSpan={columns.length} className="h-24 text-center"> + 暂无数据 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {/* 分页控制 */} + <div className="flex items-center justify-between px-2"> + {/* 选中行信息 */} + <div className="flex-1 text-sm text-muted-foreground"> + {table.getFilteredSelectedRowModel().rows.length} of{" "} + {paginationInfo ? paginationInfo.total : table.getFilteredRowModel().rows.length} row(s) selected + </div> + + {/* 分页控制器 */} + <div className="flex items-center space-x-6 lg:space-x-8"> + {/* 每页显示数量选择 */} + <div className="flex items-center space-x-2"> + <Label htmlFor="rows-per-page" className="text-sm font-medium"> + Rows per page + </Label> + <Select + value={`${table.getState().pagination.pageSize}`} + onValueChange={(value) => { + table.setPageSize(Number(value)) + }} + > + <SelectTrigger className="h-8 w-[90px]" id="rows-per-page"> + <SelectValue placeholder={table.getState().pagination.pageSize} /> + </SelectTrigger> + <SelectContent side="top"> + {[10, 20, 50, 100, 200, 500, 1000].map((pageSize) => ( + <SelectItem key={pageSize} value={`${pageSize}`}> + {pageSize} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 页码信息 */} + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + </div> + + {/* 分页按钮 */} + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to first page</span> + <IconChevronsLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">Go to previous page</span> + <IconChevronLeft /> + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to next page</span> + <IconChevronRight /> + </Button> + <Button + variant="outline" + className="hidden h-8 w-8 p-0 lg:flex" + onClick={() => table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">Go to last page</span> + <IconChevronsRight /> + </Button> + </div> + </div> + </div> + </div> + ) +} diff --git a/frontend/components/websites/websites-view.tsx b/frontend/components/websites/websites-view.tsx new file mode 100644 index 00000000..7cddfe09 --- /dev/null +++ b/frontend/components/websites/websites-view.tsx @@ -0,0 +1,209 @@ +"use client" + +import React, { useCallback, useMemo, useState, useEffect } from "react" +import { AlertTriangle } from "lucide-react" +import { WebSitesDataTable } from "./websites-data-table" +import { createWebSiteColumns } from "./websites-columns" +import { DataTableSkeleton } from "@/components/ui/data-table-skeleton" +import { Button } from "@/components/ui/button" +import { useTargetWebSites, useScanWebSites } from "@/hooks/use-websites" +import { WebsiteService } from "@/services/website.service" +import type { WebSite } from "@/types/website.types" +import { toast } from "sonner" + +export function WebSitesView({ + targetId, + scanId, +}: { + targetId?: number + scanId?: number +}) { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + const [selectedWebSites, setSelectedWebSites] = useState<WebSite[]>([]) + + const [searchQuery, setSearchQuery] = useState("") + const [isSearching, setIsSearching] = useState(false) + + const handleSearchChange = (value: string) => { + setIsSearching(true) + setSearchQuery(value) + setPagination((prev) => ({ ...prev, pageIndex: 0 })) + } + + const targetQuery = useTargetWebSites( + targetId || 0, + { + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }, + { enabled: !!targetId } + ) + + const scanQuery = useScanWebSites( + scanId || 0, + { + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: searchQuery || undefined, + }, + { enabled: !!scanId } + ) + + const activeQuery = targetId ? targetQuery : scanQuery + const { data, isLoading, isFetching, error, refetch } = activeQuery + + // 当请求完成时重置搜索状态 + useEffect(() => { + if (!isFetching && isSearching) { + setIsSearching(false) + } + }, [isFetching, isSearching]) + + const formatDate = useCallback((dateString: string) => { + return new Date(dateString).toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + }, []) + + const handleViewDetail = (website: WebSite) => { + // TODO: 实现查看网站详细功能 + console.log('查看网站详细:', website) + } + + const columns = useMemo( + () => + createWebSiteColumns({ + formatDate, + onViewDetail: handleViewDetail, + }), + [formatDate] + ) + + const websites: WebSite[] = useMemo(() => { + if (!data?.results) return [] + return data.results + }, [data]) + + const paginationInfo = data + ? { + total: data.total, + page: data.page, + pageSize: data.pageSize, + totalPages: data.totalPages, + } + : undefined + + const handleSelectionChange = useCallback((selectedRows: WebSite[]) => { + setSelectedWebSites(selectedRows) + }, []) + + // 处理下载所有网站 + const handleDownloadAll = async () => { + try { + let blob: Blob | null = null + + if (scanId) { + const data = await WebsiteService.exportWebsitesByScanId(scanId) + blob = data + } else if (targetId) { + const data = await WebsiteService.exportWebsitesByTargetId(targetId) + blob = data + } else { + if (!websites || websites.length === 0) { + return + } + const content = websites.map((item) => item.url).join("\n") + blob = new Blob([content], { type: "text/plain;charset=utf-8" }) + } + + if (!blob) return + + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + const prefix = scanId ? `scan-${scanId}` : targetId ? `target-${targetId}` : "websites" + a.href = url + a.download = `${prefix}-websites-${Date.now()}.txt` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } catch (error) { + console.error("下载网站列表失败", error) + toast.error("下载网站列表失败,请稍后重试") + } + } + + // 处理下载选中的网站 + const handleDownloadSelected = () => { + if (selectedWebSites.length === 0) { + return + } + const content = selectedWebSites.map((item) => item.url).join("\n") + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + const prefix = scanId ? `scan-${scanId}` : targetId ? `target-${targetId}` : "websites" + a.href = url + a.download = `${prefix}-websites-selected-${Date.now()}.txt` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + if (error) { + return ( + <div className="flex flex-col items-center justify-center py-12"> + <div className="rounded-full bg-destructive/10 p-3 mb-4"> + <AlertTriangle className="h-10 w-10 text-destructive" /> + </div> + <h3 className="text-lg font-semibold mb-2">加载失败</h3> + <p className="text-muted-foreground text-center mb-4"> + 加载网站数据时出现错误,请重试 + </p> + <Button onClick={() => refetch()}>重新加载</Button> + </div> + ) + } + + if (isLoading && !data) { + return ( + <DataTableSkeleton + toolbarButtonCount={2} + rows={6} + columns={5} + /> + ) + } + + return ( + <> + <WebSitesDataTable + data={websites} + columns={columns} + searchPlaceholder="搜索主机名..." + searchColumn="url" + searchValue={searchQuery} + onSearch={handleSearchChange} + isSearching={isSearching} + pagination={pagination} + setPagination={setPagination} + paginationInfo={paginationInfo} + onPaginationChange={setPagination} + onSelectionChange={handleSelectionChange} + onDownloadAll={handleDownloadAll} + onDownloadSelected={handleDownloadSelected} + /> + </> + ) +} diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 00000000..719cea2b --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,25 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), + { + ignores: [ + "node_modules/**", + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ], + }, +]; + +export default eslintConfig; diff --git a/frontend/hooks/use-auth.ts b/frontend/hooks/use-auth.ts new file mode 100644 index 00000000..583e86f4 --- /dev/null +++ b/frontend/hooks/use-auth.ts @@ -0,0 +1,80 @@ +/** + * 认证相关 hooks + */ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { useRouter } from 'next/navigation' +import { toast } from 'sonner' +import { login, logout, getMe, changePassword } from '@/services/auth.service' +import { getErrorMessage } from '@/lib/api-client' +import type { LoginRequest, ChangePasswordRequest } from '@/types/auth.types' + +/** + * 获取当前用户信息 + */ +export function useAuth() { + const skipAuth = process.env.NEXT_PUBLIC_SKIP_AUTH === 'true' + + return useQuery({ + queryKey: ['auth', 'me'], + queryFn: skipAuth + ? () => Promise.resolve({ authenticated: true } as Awaited<ReturnType<typeof getMe>>) + : getMe, + staleTime: 1000 * 60 * 5, // 5 分钟内不重新请求 + retry: false, + }) +} + +/** + * 用户登录 + */ +export function useLogin() { + const queryClient = useQueryClient() + const router = useRouter() + + return useMutation({ + mutationFn: (data: LoginRequest) => login(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['auth', 'me'] }) + toast.success('登录成功') + router.push('/dashboard/') + }, + onError: (error: unknown) => { + toast.error(getErrorMessage(error)) + }, + }) +} + +/** + * 用户登出 + */ +export function useLogout() { + const queryClient = useQueryClient() + const router = useRouter() + + return useMutation({ + mutationFn: logout, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['auth', 'me'] }) + toast.success('已登出') + router.push('/login/') + }, + onError: (error: unknown) => { + toast.error(getErrorMessage(error)) + }, + }) +} + +/** + * 修改密码 + */ +export function useChangePassword() { + return useMutation({ + mutationFn: (data: ChangePasswordRequest) => changePassword(data), + onSuccess: () => { + toast.success('密码修改成功') + }, + onError: (error: unknown) => { + toast.error(getErrorMessage(error)) + }, + }) +} diff --git a/frontend/hooks/use-color-theme.ts b/frontend/hooks/use-color-theme.ts new file mode 100644 index 00000000..fa2c9259 --- /dev/null +++ b/frontend/hooks/use-color-theme.ts @@ -0,0 +1,74 @@ +/** + * 颜色主题切换 hook + * 管理主题色(不是亮暗模式) + */ +import { useEffect, useState, useCallback } from 'react' + +// 可用的颜色主题(colors 数组用于预览) +export const COLOR_THEMES = [ + { id: 'vercel', name: 'Vercel', color: '#000000', colors: ['#000000', '#ffffff', '#666666', '#999999'] }, + { id: 'violet-bloom', name: 'Violet Bloom', color: '#7c3aed', colors: ['#7c3aed', '#8b5cf6', '#a78bfa', '#c4b5fd'] }, + { id: 'bubblegum', name: 'Bubblegum', color: '#d946a8', colors: ['#d946a8', '#ec4899', '#f472b6', '#f9a8d4'] }, + { id: 'quantum-rose', name: 'Quantum Rose', color: '#e11d48', colors: ['#e11d48', '#f43f5e', '#fb7185', '#fda4af'] }, + { id: 'clean-slate', name: 'Clean Slate', color: '#3b82f6', colors: ['#3b82f6', '#60a5fa', '#93c5fd', '#bfdbfe'] }, + { id: 'cosmic-night', name: 'Cosmic Night', color: '#6366f1', colors: ['#6366f1', '#818cf8', '#a5b4fc', '#c7d2fe'] }, + { id: 'candyland', name: 'Candyland', color: '#f5a5b8', colors: ['#f5a5b8', '#9dd5f5', '#f9e87c', '#f5a5c8'] }, +] as const + +export type ColorThemeId = typeof COLOR_THEMES[number]['id'] + +const STORAGE_KEY = 'color-theme' + +/** + * 获取当前颜色主题 + */ +function getStoredTheme(): ColorThemeId { + if (typeof window === 'undefined') return 'vercel' + return (localStorage.getItem(STORAGE_KEY) as ColorThemeId) || 'vercel' +} + +/** + * 应用颜色主题到 DOM + */ +function applyTheme(themeId: ColorThemeId) { + if (typeof window === 'undefined') return + + const root = document.documentElement + root.setAttribute('data-theme', themeId) + + console.log('应用主题:', themeId, '当前 html:', root.getAttribute('data-theme'), root.className) +} + +/** + * 颜色主题 hook + */ +export function useColorTheme() { + const [theme, setThemeState] = useState<ColorThemeId>('vercel') + const [mounted, setMounted] = useState(false) + + // 初始化 + useEffect(() => { + const stored = getStoredTheme() + setThemeState(stored) + applyTheme(stored) + setMounted(true) + }, []) + + // 切换主题 + const setTheme = useCallback((newTheme: ColorThemeId) => { + setThemeState(newTheme) + localStorage.setItem(STORAGE_KEY, newTheme) + applyTheme(newTheme) + }, []) + + // 获取当前主题信息 + const currentTheme = COLOR_THEMES.find(t => t.id === theme) || COLOR_THEMES[0] + + return { + theme, + setTheme, + themes: COLOR_THEMES, + currentTheme, + mounted, + } +} diff --git a/frontend/hooks/use-commands.ts b/frontend/hooks/use-commands.ts new file mode 100644 index 00000000..1682c45d --- /dev/null +++ b/frontend/hooks/use-commands.ts @@ -0,0 +1,362 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { CommandService } from "@/services/command.service" +import type { + GetCommandsRequest, + CreateCommandRequest, + UpdateCommandRequest, + GetCommandsResponse, + Command, +} from "@/types/command.types" +import { toast } from "sonner" + +// 假数据 +const MOCK_COMMANDS: Command[] = [ + { + id: 1, + createdAt: "2024-01-15T10:30:00Z", + updatedAt: "2024-01-15T10:30:00Z", + toolId: 1, + tool: { + id: 1, + name: "subfinder", + type: "opensource", + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + repoUrl: "https://github.com/projectdiscovery/subfinder", + version: "v2.6.5", + description: "Fast passive subdomain enumeration tool", + categoryNames: ["subdomain", "recon"], + directory: "", + installCommand: "go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest", + updateCommand: "go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest", + versionCommand: "subfinder -version", + }, + name: "subdomain_scan", + displayName: "子域名扫描", + description: "使用 subfinder 进行子域名扫描", + commandTemplate: "subfinder -d {{domain}} -o {{output}}", + }, + { + id: 2, + createdAt: "2024-01-16T11:20:00Z", + updatedAt: "2024-01-16T11:20:00Z", + toolId: 2, + tool: { + id: 2, + name: "nmap", + type: "opensource", + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + repoUrl: "https://github.com/nmap/nmap", + version: "7.94", + description: "Network exploration tool and security / port scanner", + categoryNames: ["port", "network"], + directory: "", + installCommand: "brew install nmap", + updateCommand: "brew upgrade nmap", + versionCommand: "nmap --version", + }, + name: "port_scan", + displayName: "端口扫描", + description: "使用 nmap 进行端口扫描", + commandTemplate: "nmap -sV -p- {{target}} -oX {{output}}", + }, + { + id: 3, + createdAt: "2024-01-17T09:15:00Z", + updatedAt: "2024-01-17T09:15:00Z", + toolId: 1, + tool: { + id: 1, + name: "subfinder", + type: "opensource", + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + repoUrl: "https://github.com/projectdiscovery/subfinder", + version: "v2.6.5", + description: "Fast passive subdomain enumeration tool", + categoryNames: ["subdomain", "recon"], + directory: "", + installCommand: "go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest", + updateCommand: "go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest", + versionCommand: "subfinder -version", + }, + name: "fast_subdomain_scan", + displayName: "快速子域名扫描", + description: "使用 subfinder 快速扫描常见子域名", + commandTemplate: "subfinder -d {{domain}} -silent -o {{output}}", + }, + { + id: 4, + createdAt: "2024-01-18T14:45:00Z", + updatedAt: "2024-01-18T14:45:00Z", + toolId: 3, + tool: { + id: 3, + name: "nuclei", + type: "opensource", + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + repoUrl: "https://github.com/projectdiscovery/nuclei", + version: "v3.1.4", + description: "Fast and customisable vulnerability scanner", + categoryNames: ["vulnerability", "scanner"], + directory: "", + installCommand: "go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest", + updateCommand: "go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest", + versionCommand: "nuclei -version", + }, + name: "vulnerability_scan", + displayName: "漏洞扫描", + description: "使用 nuclei 进行漏洞扫描", + commandTemplate: "nuclei -u {{target}} -severity critical,high,medium -o {{output}}", + }, + { + id: 5, + createdAt: "2024-01-19T16:00:00Z", + updatedAt: "2024-01-19T16:00:00Z", + toolId: 4, + tool: { + id: 4, + name: "katana", + type: "opensource", + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + repoUrl: "https://github.com/projectdiscovery/katana", + version: "v1.0.4", + description: "Next-generation crawling and spidering framework", + categoryNames: ["crawler", "recon"], + directory: "", + installCommand: "go install github.com/projectdiscovery/katana/cmd/katana@latest", + updateCommand: "go install github.com/projectdiscovery/katana/cmd/katana@latest", + versionCommand: "katana -version", + }, + name: "web_crawl", + displayName: "网页爬取", + description: "使用 katana 爬取网页链接", + commandTemplate: "katana -u {{target}} -d 3 -o {{output}}", + }, + { + id: 6, + createdAt: "2024-01-20T10:30:00Z", + updatedAt: "2024-01-20T10:30:00Z", + toolId: 2, + tool: { + id: 2, + name: "nmap", + type: "opensource", + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + repoUrl: "https://github.com/nmap/nmap", + version: "7.94", + description: "Network exploration tool and security / port scanner", + categoryNames: ["port", "network"], + directory: "", + installCommand: "brew install nmap", + updateCommand: "brew upgrade nmap", + versionCommand: "nmap --version", + }, + name: "service_detect", + displayName: "服务识别", + description: "使用 nmap 进行服务版本识别", + commandTemplate: "nmap -sV {{target}} -oX {{output}}", + }, + { + id: 7, + createdAt: "2024-01-21T13:20:00Z", + updatedAt: "2024-01-21T13:20:00Z", + toolId: 5, + tool: { + id: 5, + name: "dirsearch", + type: "opensource", + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + repoUrl: "https://github.com/maurosoria/dirsearch", + version: "v0.4.3", + description: "Web path scanner", + categoryNames: ["directory", "recon"], + directory: "", + installCommand: "pip3 install dirsearch", + updateCommand: "pip3 install --upgrade dirsearch", + versionCommand: "dirsearch --version", + }, + name: "dir_scan", + displayName: "目录扫描", + description: "使用 dirsearch 进行目录扫描", + commandTemplate: "dirsearch -u {{target}} -e php,html,js -o {{output}}", + }, + { + id: 8, + createdAt: "2024-01-22T15:10:00Z", + updatedAt: "2024-01-22T15:10:00Z", + toolId: 3, + tool: { + id: 3, + name: "nuclei", + type: "opensource", + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + repoUrl: "https://github.com/projectdiscovery/nuclei", + version: "v3.1.4", + description: "Fast and customisable vulnerability scanner", + categoryNames: ["vulnerability", "scanner"], + directory: "", + installCommand: "go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest", + updateCommand: "go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest", + versionCommand: "nuclei -version", + }, + name: "full_vulnerability_scan", + displayName: "完整漏洞扫描", + description: "使用 nuclei 进行全面漏洞扫描", + commandTemplate: "nuclei -u {{target}} -t nuclei-templates/ -o {{output}}", + }, +] + +/** + * 获取命令列表(使用假数据) + */ +export function useCommands(params: GetCommandsRequest = {}) { + return useQuery({ + queryKey: ["commands", params], + queryFn: async (): Promise<GetCommandsResponse> => { + // 模拟网络延迟 + await new Promise(resolve => setTimeout(resolve, 500)) + + const page = params.page || 1 + const pageSize = params.pageSize || 10 + + // 如果有 toolId 过滤 + let filteredCommands = MOCK_COMMANDS + if (params.toolId) { + filteredCommands = MOCK_COMMANDS.filter(cmd => cmd.toolId === params.toolId) + } + + const totalCount = filteredCommands.length + const totalPages = Math.ceil(totalCount / pageSize) + const startIndex = (page - 1) * pageSize + const endIndex = startIndex + pageSize + const commands = filteredCommands.slice(startIndex, endIndex) + + return { + commands, + page, + pageSize, + total: totalCount, + totalPages, + // 兼容字段(向后兼容) + page_size: pageSize, + total_count: totalCount, + total_pages: totalPages, + } + }, + }) +} + +/** + * 获取单个命令(使用假数据) + */ +export function useCommand(id: number) { + return useQuery({ + queryKey: ["command", id], + queryFn: async (): Promise<Command | undefined> => { + // 模拟网络延迟 + await new Promise(resolve => setTimeout(resolve, 300)) + return MOCK_COMMANDS.find(cmd => cmd.id === id) + }, + enabled: !!id, + }) +} + +/** + * 创建命令 + */ +export function useCreateCommand() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: CreateCommandRequest) => CommandService.createCommand(data), + onSuccess: () => { + toast.success("命令创建成功") + queryClient.invalidateQueries({ queryKey: ["commands"] }) + }, + onError: (error: any) => { + console.error("创建命令失败:", error) + toast.error("命令创建失败") + }, + }) +} + +/** + * 更新命令 + */ +export function useUpdateCommand() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: UpdateCommandRequest }) => + CommandService.updateCommand(id, data), + onSuccess: () => { + toast.success("命令更新成功") + queryClient.invalidateQueries({ queryKey: ["commands"] }) + queryClient.invalidateQueries({ queryKey: ["command"] }) + }, + onError: (error: any) => { + console.error("更新命令失败:", error) + toast.error("命令更新失败") + }, + }) +} + +/** + * 删除命令 + */ +export function useDeleteCommand() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: number) => CommandService.deleteCommand(id), + onSuccess: () => { + toast.success("命令删除成功") + queryClient.invalidateQueries({ queryKey: ["commands"] }) + }, + onError: (error: any) => { + console.error("删除命令失败:", error) + toast.error("命令删除失败") + }, + }) +} + +/** + * 批量删除命令(使用假数据) + */ +export function useBatchDeleteCommands() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (ids: number[]) => { + // 模拟网络延迟 + await new Promise(resolve => setTimeout(resolve, 800)) + + // 从假数据中过滤掉被删除的命令 + const deletedCount = ids.filter(id => + MOCK_COMMANDS.some(cmd => cmd.id === id) + ).length + + // 模拟删除(实际上不会真的删除假数据) + return { + data: { + deletedCount: deletedCount + } + } + }, + onSuccess: (response) => { + toast.success(`成功删除 ${response.data?.deletedCount} 个命令`) + queryClient.invalidateQueries({ queryKey: ["commands"] }) + }, + onError: (error: any) => { + console.error("批量删除命令失败:", error) + toast.error("批量删除命令失败") + }, + }) +} diff --git a/frontend/hooks/use-dashboard.ts b/frontend/hooks/use-dashboard.ts new file mode 100644 index 00000000..c8e74a79 --- /dev/null +++ b/frontend/hooks/use-dashboard.ts @@ -0,0 +1,29 @@ +import { useQuery } from '@tanstack/react-query' +import { getDashboardStats, getAssetStatistics, getStatisticsHistory } from '@/services/dashboard.service' + +export function useDashboardStats() { + return useQuery({ + queryKey: ['dashboard', 'stats'], + queryFn: () => getDashboardStats(), + }) +} + +/** + * 获取资产统计数据(预聚合) + */ +export function useAssetStatistics() { + return useQuery({ + queryKey: ['asset', 'statistics'], + queryFn: getAssetStatistics, + }) +} + +/** + * 获取统计历史数据(用于折线图) + */ +export function useStatisticsHistory(days: number = 7) { + return useQuery({ + queryKey: ['asset', 'statistics', 'history', days], + queryFn: () => getStatisticsHistory(days), + }) +} diff --git a/frontend/hooks/use-directories.ts b/frontend/hooks/use-directories.ts new file mode 100644 index 00000000..9fb1ed59 --- /dev/null +++ b/frontend/hooks/use-directories.ts @@ -0,0 +1,163 @@ +import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query' +import { toast } from 'sonner' +import type { Directory, DirectoryListResponse } from '@/types/directory.types' + +// API 服务函数 +const directoryService = { + // 获取目标的目录列表 + getTargetDirectories: async ( + targetId: number, + params: { page: number; pageSize: number; search?: string } + ): Promise<DirectoryListResponse> => { + const searchParam = params.search ? `&search=${encodeURIComponent(params.search)}` : '' + const response = await fetch( + `/api/targets/${targetId}/directories/?page=${params.page}&pageSize=${params.pageSize}${searchParam}` + ) + if (!response.ok) { + throw new Error('获取目录列表失败') + } + return response.json() + }, + + // 获取扫描的目录列表 + getScanDirectories: async ( + scanId: number, + params: { page: number; pageSize: number; search?: string } + ): Promise<DirectoryListResponse> => { + const searchParam = params.search ? `&search=${encodeURIComponent(params.search)}` : '' + const response = await fetch( + `/api/scans/${scanId}/directories/?page=${params.page}&pageSize=${params.pageSize}${searchParam}` + ) + if (!response.ok) { + throw new Error('获取目录列表失败') + } + return response.json() + }, + + // 批量删除目录(支持单个或多个) + bulkDeleteDirectories: async (ids: number[]): Promise<{ + message: string + deletedCount: number + requestedIds: number[] + cascadeDeleted: Record<string, number> + }> => { + const response = await fetch('/api/directories/bulk-delete/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ ids }), + }) + if (!response.ok) { + throw new Error('批量删除目录失败') + } + return response.json() + }, + + // 删除单个目录(使用单独的 DELETE API) + deleteDirectory: async (directoryId: number): Promise<{ + message: string + directoryId: number + directoryUrl: string + deletedCount: number + deletedDirectories: string[] + detail: { + phase1: string + phase2: string + } + }> => { + const response = await fetch(`/api/directories/${directoryId}/`, { + method: 'DELETE', + }) + if (!response.ok) { + throw new Error('删除目录失败') + } + return response.json() + }, +} + +// 获取目标的目录列表 +export function useTargetDirectories( + targetId: number, + params: { page: number; pageSize: number; search?: string }, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: ['target-directories', targetId, params], + queryFn: () => directoryService.getTargetDirectories(targetId, params), + enabled: options?.enabled ?? true, + placeholderData: keepPreviousData, + }) +} + +// 获取扫描的目录列表 +export function useScanDirectories( + scanId: number, + params: { page: number; pageSize: number; search?: string }, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: ['scan-directories', scanId, params], + queryFn: () => directoryService.getScanDirectories(scanId, params), + enabled: options?.enabled ?? true, + placeholderData: keepPreviousData, + }) +} + +// 删除单个目录(使用单独的 DELETE API) +export function useDeleteDirectory() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: directoryService.deleteDirectory, + onMutate: (id) => { + toast.loading('正在删除目录...', { id: `delete-directory-${id}` }) + }, + onSuccess: (response, id) => { + toast.dismiss(`delete-directory-${id}`) + + // 显示删除信息(单个删除 API 返回两阶段信息) + const { directoryUrl, detail } = response + toast.success(`目录 "${directoryUrl}" 已成功删除`, { + description: `${detail.phase1};${detail.phase2}`, + duration: 4000 + }) + + // 刷新相关查询 + queryClient.invalidateQueries({ queryKey: ['target-directories'] }) + queryClient.invalidateQueries({ queryKey: ['scan-directories'] }) + queryClient.invalidateQueries({ queryKey: ['targets'] }) + queryClient.invalidateQueries({ queryKey: ['scans'] }) + }, + onError: (error: Error, id) => { + toast.dismiss(`delete-directory-${id}`) + toast.error(error.message || '删除目录失败') + }, + }) +} + +// 批量删除目录(使用统一的批量删除接口) +export function useBulkDeleteDirectories() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: directoryService.bulkDeleteDirectories, + onMutate: () => { + toast.loading('正在批量删除目录...', { id: 'bulk-delete-directories' }) + }, + onSuccess: (response) => { + toast.dismiss('bulk-delete-directories') + toast.success(`成功删除 ${response.deletedCount} 个目录`) + + // 刷新相关查询 + queryClient.invalidateQueries({ queryKey: ['target-directories'] }) + queryClient.invalidateQueries({ queryKey: ['scan-directories'] }) + queryClient.invalidateQueries({ queryKey: ['targets'] }) + queryClient.invalidateQueries({ queryKey: ['scans'] }) + }, + onError: (error: Error) => { + toast.dismiss('bulk-delete-directories') + toast.error(error.message || '批量删除目录失败') + }, + }) +} diff --git a/frontend/hooks/use-disk.ts b/frontend/hooks/use-disk.ts new file mode 100644 index 00000000..91f9cdbd --- /dev/null +++ b/frontend/hooks/use-disk.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query' +import { getDiskStats } from '@/services/disk.service' + +export function useDiskStats() { + return useQuery({ + queryKey: ['system', 'disk-stats'], + queryFn: () => getDiskStats(), + refetchInterval: 5000, + }) +} diff --git a/frontend/hooks/use-endpoints.ts b/frontend/hooks/use-endpoints.ts new file mode 100644 index 00000000..71d7244f --- /dev/null +++ b/frontend/hooks/use-endpoints.ts @@ -0,0 +1,221 @@ +"use client" + +import { useMutation, useQuery, useQueryClient, keepPreviousData } from "@tanstack/react-query" +import { toast } from "sonner" +import { EndpointService } from "@/services/endpoint.service" +import type { + Endpoint, + CreateEndpointRequest, + UpdateEndpointRequest, + GetEndpointsRequest, + GetEndpointsResponse, + BatchDeleteEndpointsRequest, + BatchDeleteEndpointsResponse +} from "@/types/endpoint.types" + +// Query Keys +export const endpointKeys = { + all: ['endpoints'] as const, + lists: () => [...endpointKeys.all, 'list'] as const, + list: (params: GetEndpointsRequest) => + [...endpointKeys.lists(), params] as const, + details: () => [...endpointKeys.all, 'detail'] as const, + detail: (id: number) => [...endpointKeys.details(), id] as const, + byTarget: (targetId: number, params: GetEndpointsRequest) => + [...endpointKeys.all, 'target', targetId, params] as const, + bySubdomain: (subdomainId: number, params: GetEndpointsRequest) => + [...endpointKeys.all, 'subdomain', subdomainId, params] as const, + byScan: (scanId: number, params: GetEndpointsRequest) => + [...endpointKeys.all, 'scan', scanId, params] as const, +} + +// 获取单个 Endpoint 详情 +export function useEndpoint(id: number) { + return useQuery({ + queryKey: endpointKeys.detail(id), + queryFn: () => EndpointService.getEndpointById(id), + select: (response) => { + // RESTful 标准:直接返回数据 + return response as Endpoint + }, + enabled: !!id, + }) +} + +// 获取 Endpoint 列表 +export function useEndpoints(params?: GetEndpointsRequest) { + const defaultParams: GetEndpointsRequest = { + page: 1, + pageSize: 10, + ...params + } + + return useQuery({ + queryKey: endpointKeys.list(defaultParams), + queryFn: () => EndpointService.getEndpoints(defaultParams), + select: (response) => { + // RESTful 标准:直接返回数据 + return response as GetEndpointsResponse + }, + }) +} + +// 根据目标ID获取 Endpoint 列表(使用专用路由) +export function useEndpointsByTarget(targetId: number, params?: Omit<GetEndpointsRequest, 'targetId'>) { + const defaultParams: GetEndpointsRequest = { + page: 1, + pageSize: 10, + ...params + } + + return useQuery({ + queryKey: endpointKeys.byTarget(targetId, defaultParams), + queryFn: () => EndpointService.getEndpointsByTargetId(targetId, defaultParams), + select: (response) => { + // RESTful 标准:直接返回数据 + return response as GetEndpointsResponse + }, + enabled: !!targetId, + placeholderData: keepPreviousData, + }) +} + +// 根据扫描ID获取 Endpoint 列表(历史快照) +export function useScanEndpoints(scanId: number, params?: Omit<GetEndpointsRequest, 'targetId'>, options?: { enabled?: boolean }) { + const defaultParams: GetEndpointsRequest = { + page: 1, + pageSize: 10, + ...params, + } + + return useQuery({ + queryKey: endpointKeys.byScan(scanId, defaultParams), + queryFn: () => EndpointService.getEndpointsByScanId(scanId, defaultParams), + enabled: options?.enabled !== undefined ? options.enabled : !!scanId, + select: (response: any) => { + // 后端使用通用分页格式:results/total/page/pageSize/totalPages + return { + endpoints: response.results || [], + pagination: { + total: response.total || 0, + page: response.page || 1, + pageSize: response.pageSize || response.page_size || defaultParams.pageSize || 10, + totalPages: response.totalPages || response.total_pages || 0, + }, + } + }, + placeholderData: keepPreviousData, + }) +} + +// 创建 Endpoint(完全自动化) +export function useCreateEndpoint() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: { + endpoints: Array<CreateEndpointRequest> + }) => EndpointService.createEndpoints(data), + onMutate: async () => { + toast.loading('正在创建端点...', { id: 'create-endpoint' }) + }, + onSuccess: (response) => { + // 关闭加载提示 + toast.dismiss('create-endpoint') + + const { createdCount, existedCount } = response + + // 打印后端响应 + console.log('创建端点成功') + console.log('后端响应:', response) + + // 前端自己构造成功提示消息 + if (existedCount > 0) { + toast.warning( + `成功创建 ${createdCount} 个端点(${existedCount} 个已存在)` + ) + } else { + toast.success(`成功创建 ${createdCount} 个端点`) + } + + // 刷新所有端点相关查询(通配符匹配) + queryClient.invalidateQueries({ queryKey: ['endpoints'] }) + }, + onError: (error: any) => { + // 关闭加载提示 + toast.dismiss('create-endpoint') + + console.error('创建端点失败:', error) + console.error('后端响应:', error?.response?.data || error) + + // 前端自己构造错误提示 + toast.error('创建端点失败,请查看控制台日志') + }, + }) +} + +// 删除单个 Endpoint +export function useDeleteEndpoint() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: number) => EndpointService.deleteEndpoint(id), + onMutate: (id) => { + toast.loading('正在删除端点...', { id: `delete-endpoint-${id}` }) + }, + onSuccess: (response, id) => { + toast.dismiss(`delete-endpoint-${id}`) + + // 打印后端响应 + console.log('删除端点成功') + + toast.success('删除成功') + + // 刷新所有端点相关查询(通配符匹配) + queryClient.invalidateQueries({ queryKey: ['endpoints'] }) + }, + onError: (error: any, id) => { + toast.dismiss(`delete-endpoint-${id}`) + + console.error('删除端点失败:', error) + console.error('后端响应:', error?.response?.data || error) + + // 前端自己构造错误提示 + toast.error('删除端点失败,请查看控制台日志') + }, + }) +} + +// 批量删除 Endpoint +export function useBatchDeleteEndpoints() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: BatchDeleteEndpointsRequest) => EndpointService.batchDeleteEndpoints(data), + onMutate: () => { + toast.loading('正在批量删除端点...', { id: 'batch-delete-endpoints' }) + }, + onSuccess: (response) => { + toast.dismiss('batch-delete-endpoints') + + // 打印后端响应 + console.log('批量删除端点成功') + console.log('后端响应:', response) + + const { deletedCount } = response + toast.success(`成功删除 ${deletedCount} 个端点`) + + // 刷新所有端点相关查询(通配符匹配) + queryClient.invalidateQueries({ queryKey: ['endpoints'] }) + }, + onError: (error: any) => { + toast.dismiss('batch-delete-endpoints') + + console.error('批量删除端点失败:', error) + console.error('后端响应:', error?.response?.data || error) + + // 前端自己构造错误提示 + toast.error('批量删除失败,请查看控制台日志') + }, + }) +} diff --git a/frontend/hooks/use-engines.ts b/frontend/hooks/use-engines.ts new file mode 100644 index 00000000..716e0ef7 --- /dev/null +++ b/frontend/hooks/use-engines.ts @@ -0,0 +1,94 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' +import { + getEngines, + getEngine, + createEngine, + updateEngine, + deleteEngine, +} from '@/services/engine.service' +import type { ScanEngine } from '@/types/engine.types' + +/** + * 获取引擎列表 + */ +export function useEngines() { + return useQuery({ + queryKey: ['engines'], + queryFn: getEngines, + }) +} + +/** + * 获取引擎详情 + */ +export function useEngine(id: number) { + return useQuery({ + queryKey: ['engines', id], + queryFn: () => getEngine(id), + enabled: !!id, + }) +} + +/** + * 创建引擎 + */ +export function useCreateEngine() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: createEngine, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['engines'] }) + toast.success('引擎创建成功') + }, + onError: (error: any) => { + toast.error('引擎创建失败', { + description: error?.response?.data?.error || error.message, + }) + }, + }) +} + +/** + * 更新引擎 + */ +export function useUpdateEngine() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: Parameters<typeof updateEngine>[1] }) => + updateEngine(id, data), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['engines'] }) + queryClient.invalidateQueries({ queryKey: ['engines', variables.id] }) + toast.success('引擎更新成功') + }, + onError: (error: any) => { + toast.error('引擎更新失败', { + description: error?.response?.data?.error || error.message, + }) + }, + }) +} + +/** + * 删除引擎 + */ +export function useDeleteEngine() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: deleteEngine, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['engines'] }) + toast.success('引擎删除成功') + }, + onError: (error: any) => { + toast.error('引擎删除失败', { + description: error?.response?.data?.error || error.message, + }) + }, + }) +} + diff --git a/frontend/hooks/use-ip-addresses.ts b/frontend/hooks/use-ip-addresses.ts new file mode 100644 index 00000000..a4d76efe --- /dev/null +++ b/frontend/hooks/use-ip-addresses.ts @@ -0,0 +1,53 @@ +"use client" + +import { useQuery, keepPreviousData } from "@tanstack/react-query" +import { IPAddressService } from "@/services/ip-address.service" +import type { GetIPAddressesParams, GetIPAddressesResponse } from "@/types/ip-address.types" + +const ipAddressKeys = { + all: ["ip-addresses"] as const, + target: (targetId: number, params: GetIPAddressesParams) => + [...ipAddressKeys.all, "target", targetId, params] as const, + scan: (scanId: number, params: GetIPAddressesParams) => + [...ipAddressKeys.all, "scan", scanId, params] as const, +} + +function normalizeParams(params?: GetIPAddressesParams): Required<GetIPAddressesParams> { + return { + page: params?.page ?? 1, + pageSize: params?.pageSize ?? 10, + search: params?.search ?? "", + } +} + +export function useTargetIPAddresses( + targetId: number, + params?: GetIPAddressesParams, + options?: { enabled?: boolean } +) { + const normalizedParams = normalizeParams(params) + + return useQuery({ + queryKey: ipAddressKeys.target(targetId, normalizedParams), + queryFn: () => IPAddressService.getTargetIPAddresses(targetId, normalizedParams), + enabled: options?.enabled ?? !!targetId, + select: (response: GetIPAddressesResponse) => response, + placeholderData: keepPreviousData, + }) +} + +export function useScanIPAddresses( + scanId: number, + params?: GetIPAddressesParams, + options?: { enabled?: boolean } +) { + const normalizedParams = normalizeParams(params) + + return useQuery({ + queryKey: ipAddressKeys.scan(scanId, normalizedParams), + queryFn: () => IPAddressService.getScanIPAddresses(scanId, normalizedParams), + enabled: options?.enabled ?? !!scanId, + select: (response: GetIPAddressesResponse) => response, + placeholderData: keepPreviousData, + }) +} diff --git a/frontend/hooks/use-mobile.ts b/frontend/hooks/use-mobile.ts new file mode 100644 index 00000000..d1acfa62 --- /dev/null +++ b/frontend/hooks/use-mobile.ts @@ -0,0 +1,20 @@ +import * as React from "react" + +const MOBILE_BREAKPOINT = 768 + +export function useIsMobile() { + // 初始值设为 false,确保 SSR 和客户端初始渲染一致 + const [isMobile, setIsMobile] = React.useState<boolean>(false) + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + } + mql.addEventListener("change", onChange) + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + return () => mql.removeEventListener("change", onChange) + }, []) + + return isMobile +} diff --git a/frontend/hooks/use-notification-settings.ts b/frontend/hooks/use-notification-settings.ts new file mode 100644 index 00000000..045891c1 --- /dev/null +++ b/frontend/hooks/use-notification-settings.ts @@ -0,0 +1,26 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { NotificationSettingsService } from '@/services/notification-settings.service' +import type { UpdateNotificationSettingsRequest } from '@/types/notification-settings.types' +import { toast } from 'sonner' + +export function useNotificationSettings() { + return useQuery({ + queryKey: ['notification-settings'], + queryFn: () => NotificationSettingsService.getSettings(), + }) +} + +export function useUpdateNotificationSettings() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (data: UpdateNotificationSettingsRequest) => + NotificationSettingsService.updateSettings(data), + onSuccess: (res) => { + qc.invalidateQueries({ queryKey: ['notification-settings'] }) + toast.success(res?.message || '已保存通知设置') + }, + onError: () => { + toast.error('保存失败,请重试') + }, + }) +} diff --git a/frontend/hooks/use-notification-sse.ts b/frontend/hooks/use-notification-sse.ts new file mode 100644 index 00000000..5748cdc5 --- /dev/null +++ b/frontend/hooks/use-notification-sse.ts @@ -0,0 +1,309 @@ +/** + * WebSocket 实时通知 Hook + */ + +import { useCallback, useEffect, useState, useRef } from 'react' +import { useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' +import type { BackendNotification, Notification, BackendNotificationLevel, NotificationSeverity } from '@/types/notification.types' +import { getBackendBaseUrl } from '@/lib/env' + +const severityMap: Record<BackendNotificationLevel, NotificationSeverity> = { + critical: 'critical', + high: 'high', + medium: 'medium', + low: 'low', +} + +const inferNotificationType = (message: string, category?: string) => { + // 优先使用后端返回的 category + if (category === 'scan' || category === 'vulnerability' || category === 'asset' || category === 'system') { + return category + } + // 后备:通过消息内容推断 + if (message?.includes('扫描') || message?.includes('任务')) { + return 'scan' as const + } + if (message?.includes('漏洞')) { + return 'vulnerability' as const + } + return 'system' as const +} + +const formatTimeAgo = (date: Date): string => { + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffMins = Math.floor(diffMs / (1000 * 60)) + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) + + if (diffMins < 1) return '刚刚' + if (diffMins < 60) return `${diffMins} 分钟前` + if (diffHours < 24) return `${diffHours} 小时前` + return date.toLocaleDateString() +} + +export const transformBackendNotification = (backendNotification: BackendNotification): Notification => { + const createdAtRaw = backendNotification.createdAt ?? backendNotification.created_at + const createdDate = createdAtRaw ? new Date(createdAtRaw) : new Date() + const isRead = backendNotification.isRead ?? backendNotification.is_read + const notification: Notification = { + id: backendNotification.id, + type: inferNotificationType(backendNotification.message, backendNotification.category), + title: backendNotification.title, + description: backendNotification.message, + time: formatTimeAgo(createdDate), + unread: isRead === true ? false : true, + severity: severityMap[backendNotification.level] ?? undefined, + createdAt: createdDate.toISOString(), + } + return notification +} + +export function useNotificationSSE() { + const [isConnected, setIsConnected] = useState(false) + const [notifications, setNotifications] = useState<Notification[]>([]) + const wsRef = useRef<WebSocket | null>(null) + const queryClient = useQueryClient() + const reconnectTimerRef = useRef<NodeJS.Timeout | null>(null) + const heartbeatTimerRef = useRef<NodeJS.Timeout | null>(null) + const isConnectingRef = useRef(false) + const reconnectAttempts = useRef(0) + const maxReconnectAttempts = 10 + const baseReconnectDelay = 1000 // 1秒 + + const markNotificationsAsRead = useCallback((ids?: number[]) => { + setNotifications(prev => prev.map(notification => { + if (!ids || ids.includes(notification.id)) { + return { ...notification, unread: false } + } + return notification + })) + }, []) + + // 启动心跳 + const startHeartbeat = useCallback(() => { + // 清除旧的心跳定时器 + if (heartbeatTimerRef.current) { + clearInterval(heartbeatTimerRef.current) + } + + // 每 30 秒发送一次心跳 + heartbeatTimerRef.current = setInterval(() => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + console.log('[HEARTBEAT] 发送心跳 ping') + wsRef.current.send(JSON.stringify({ type: 'ping' })) + } + }, 30000) // 30秒 + }, []) + + // 停止心跳 + const stopHeartbeat = useCallback(() => { + if (heartbeatTimerRef.current) { + clearInterval(heartbeatTimerRef.current) + heartbeatTimerRef.current = null + } + }, []) + + // 计算重连延迟(指数退避) + const getReconnectDelay = useCallback(() => { + const delay = Math.min(baseReconnectDelay * Math.pow(2, reconnectAttempts.current), 30000) + return delay + }, []) + + // 连接 WebSocket + const connect = useCallback(() => { + // 防止重复连接 + if (isConnectingRef.current) { + console.log('[SKIP] 已在连接中,跳过') + return + } + + // 如果已经连接,跳过 + if (wsRef.current?.readyState === WebSocket.OPEN) { + console.log('[SKIP] 已连接,跳过') + return + } + + isConnectingRef.current = true + + // 关闭旧连接 + if (wsRef.current && wsRef.current.readyState !== WebSocket.CLOSED) { + wsRef.current.close() + } + + // 清除重连定时器 + if (reconnectTimerRef.current) { + clearTimeout(reconnectTimerRef.current) + reconnectTimerRef.current = null + } + + try { + // 构造 WebSocket URL + const backendUrl = getBackendBaseUrl() + const wsProtocol = backendUrl.startsWith('https') ? 'wss' : 'ws' + const wsHost = backendUrl.replace(/^https?:\/\//, '') + const wsUrl = `${wsProtocol}://${wsHost}/ws/notifications/` + + console.log('[CONNECTING] 正在连接 WebSocket:', wsUrl, `(尝试 ${reconnectAttempts.current + 1})`) + + const ws = new WebSocket(wsUrl) + wsRef.current = ws + + ws.onopen = () => { + console.log('[SUCCESS] WebSocket 连接已建立') + setIsConnected(true) + isConnectingRef.current = false + reconnectAttempts.current = 0 // 重置重连计数 + // 启动心跳 + startHeartbeat() + } + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + console.log('[MESSAGE] WebSocket 消息接收:', data) + + if (data.type === 'connected') { + console.log('[SUCCESS] WebSocket 连接成功') + return + } + + if (data.type === 'pong') { + // 心跳响应 + console.log('[HEARTBEAT] 心跳响应') + return + } + + if (data.type === 'error') { + console.error('[ERROR] WebSocket 错误:', data.message) + toast.error(`通知连接错误: ${data.message}`) + return + } + + // 处理通知消息 + if (data.type === 'notification') { + console.log('[NOTIFICATION] 处理通知消息 (type=notification)') + // 移除 type 字段,获取实际的通知数据 + const { type, ...payload } = data as any + + if (payload.id && payload.title && payload.message) { + console.log('[TRANSFORM] 转换通知:', payload) + const notification = transformBackendNotification(payload as BackendNotification) + console.log('[UPDATE] 更新通知列表,新通知:', notification) + setNotifications(prev => { + const updated = [notification, ...prev.slice(0, 49)] + console.log('[STATS] 通知列表已更新,总数:', updated.length) + return updated + }) + + queryClient.invalidateQueries({ queryKey: ['notifications'] }) + } else { + console.warn('[WARN] 通知数据不完整:', payload) + } + return + } + + // 备用处理:直接检查通知字段 + if (data.id && data.title && data.message) { + console.log('[NOTIFICATION] 处理通知消息 (直接字段)') + const notification = transformBackendNotification(data as BackendNotification) + + // 添加到通知列表 + console.log('[UPDATE] 更新通知列表,新通知:', notification) + setNotifications(prev => { + const updated = [notification, ...prev.slice(0, 49)] + console.log('[STATS] 通知列表已更新,总数:', updated.length) + return updated + }) + + // 刷新通知查询 + queryClient.invalidateQueries({ queryKey: ['notifications'] }) + } + } catch (error) { + console.error('[ERROR] 解析 WebSocket 消息失败:', error, '原始数据:', event.data) + } + } + + ws.onerror = () => { + // WebSocket onerror 接收的是 Event 对象,不是 Error + // 实际的错误信息通常不可用,只能记录连接状态 + console.warn('[WARN] WebSocket 连接错误,将自动重连') + setIsConnected(false) + isConnectingRef.current = false + } + + ws.onclose = (event) => { + console.log('[CLOSED] WebSocket 连接已关闭:', event.code, event.reason) + setIsConnected(false) + isConnectingRef.current = false + // 停止心跳 + stopHeartbeat() + + // 自动重连(非正常关闭时) + if (event.code !== 1000) { // 1000 = 正常关闭 + if (reconnectAttempts.current < maxReconnectAttempts) { + const delay = getReconnectDelay() + reconnectAttempts.current++ + console.log(`[RECONNECT] ${delay / 1000}秒后尝试重连... (第 ${reconnectAttempts.current} 次)`) + reconnectTimerRef.current = setTimeout(() => { + connect() + }, delay) + } else { + console.error('[ERROR] 已达到最大重连次数,停止重连') + } + } + } + } catch (error) { + console.error('[ERROR] 创建 WebSocket 失败:', error instanceof Error ? error.message : String(error)) + setIsConnected(false) + isConnectingRef.current = false + } + }, [queryClient, startHeartbeat, stopHeartbeat, getReconnectDelay]) + + // 断开连接 + const disconnect = useCallback(() => { + // 停止心跳 + stopHeartbeat() + + // 清除重连定时器 + if (reconnectTimerRef.current) { + clearTimeout(reconnectTimerRef.current) + reconnectTimerRef.current = null + } + + // 重置重连计数 + reconnectAttempts.current = 0 + isConnectingRef.current = false + + if (wsRef.current) { + wsRef.current.close(1000, 'User disconnect') // 1000 = 正常关闭 + wsRef.current = null + } + setIsConnected(false) + }, [stopHeartbeat]) + + // 清空通知 + const clearNotifications = () => { + setNotifications([]) + } + + // 组件挂载时连接,卸载时断开 + // 注意:不依赖 connect/disconnect 避免无限循环 + useEffect(() => { + connect() + + return () => { + disconnect() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return { + isConnected, + notifications, + connect, + disconnect, + clearNotifications, + markNotificationsAsRead, + } +} diff --git a/frontend/hooks/use-notifications.ts b/frontend/hooks/use-notifications.ts new file mode 100644 index 00000000..04a5f800 --- /dev/null +++ b/frontend/hooks/use-notifications.ts @@ -0,0 +1,49 @@ +/** + * 通知相关的 React Query hooks + */ + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { NotificationService } from '@/services/notification.service' +import type { + GetNotificationsRequest, +} from '@/types/notification.types' +import { toast } from 'sonner' + +/** + * 获取通知列表 + */ +export function useNotifications(params?: GetNotificationsRequest) { + return useQuery({ + queryKey: ['notifications', params], + queryFn: () => NotificationService.getNotifications(params), + }) +} + +/** + * 获取未读通知数量 + */ +export function useUnreadCount() { + return useQuery({ + queryKey: ['notifications', 'unread-count'], + queryFn: () => NotificationService.getUnreadCount(), + refetchInterval: 30000, // 每 30 秒自动刷新 + }) +} + +/** + * 标记所有通知为已读 + */ +export function useMarkAllAsRead() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: () => NotificationService.markAllAsRead(), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['notifications'] }) + }, + onError: (error: any) => { + console.error('标记全部已读失败:', error) + }, + }) +} + diff --git a/frontend/hooks/use-nuclei-git-settings.ts b/frontend/hooks/use-nuclei-git-settings.ts new file mode 100644 index 00000000..d526f0ef --- /dev/null +++ b/frontend/hooks/use-nuclei-git-settings.ts @@ -0,0 +1,28 @@ +"use client" + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { toast } from "sonner" +import { NucleiGitService } from "@/services/nuclei-git.service" +import type { UpdateNucleiGitSettingsRequest } from "@/types/nuclei-git.types" + +export function useNucleiGitSettings() { + return useQuery({ + queryKey: ["nuclei", "git", "settings"], + queryFn: () => NucleiGitService.getSettings(), + }) +} + +export function useUpdateNucleiGitSettings() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: (data: UpdateNucleiGitSettingsRequest) => NucleiGitService.updateSettings(data), + onSuccess: (res) => { + qc.invalidateQueries({ queryKey: ["nuclei", "git", "settings"] }) + toast.success(res?.message || "Git 仓库配置已保存") + }, + onError: () => { + toast.error("保存 Git 仓库配置失败,请重试") + }, + }) +} diff --git a/frontend/hooks/use-nuclei-repos.ts b/frontend/hooks/use-nuclei-repos.ts new file mode 100644 index 00000000..1595343d --- /dev/null +++ b/frontend/hooks/use-nuclei-repos.ts @@ -0,0 +1,117 @@ +/** + * Nuclei 模板仓库相关 Hooks + */ + +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { nucleiRepoApi } from "../services/nuclei-repo.api" +import type { NucleiTemplateTreeNode, NucleiTemplateContent } from "@/types/nuclei.types" + +// ==================== 仓库 CRUD ==================== + +export interface NucleiRepo { + id: number + name: string + repoUrl: string + localPath: string + commitHash: string | null + lastSyncedAt: string | null + createdAt: string + updatedAt: string +} + +/** 获取仓库列表 */ +export function useNucleiRepos() { + return useQuery<NucleiRepo[]>({ + queryKey: ["nuclei-repos"], + queryFn: nucleiRepoApi.listRepos, + }) +} + +/** 获取单个仓库详情 */ +export function useNucleiRepo(repoId: number | null) { + return useQuery<NucleiRepo>({ + queryKey: ["nuclei-repos", repoId], + queryFn: () => nucleiRepoApi.getRepo(repoId!), + enabled: !!repoId, + }) +} + +/** 创建仓库 */ +export function useCreateNucleiRepo() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: nucleiRepoApi.createRepo, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["nuclei-repos"] }) + }, + }) +} + +/** 更新仓库 */ +export function useUpdateNucleiRepo() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: { + id: number + repoUrl?: string + }) => nucleiRepoApi.updateRepo(data.id, data), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ queryKey: ["nuclei-repos"] }) + queryClient.invalidateQueries({ queryKey: ["nuclei-repos", variables.id] }) + }, + }) +} + +/** 删除仓库 */ +export function useDeleteNucleiRepo() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: nucleiRepoApi.deleteRepo, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["nuclei-repos"] }) + }, + }) +} + +// ==================== Git 同步 ==================== + +/** 刷新仓库(Git clone/pull) */ +export function useRefreshNucleiRepo() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: nucleiRepoApi.refreshRepo, + onSuccess: (_data, repoId) => { + // 刷新仓库列表(last_synced_at 会更新) + queryClient.invalidateQueries({ queryKey: ["nuclei-repos"] }) + queryClient.invalidateQueries({ queryKey: ["nuclei-repos", repoId] }) + queryClient.invalidateQueries({ queryKey: ["nuclei-repo-tree", repoId] }) + }, + }) +} + +// ==================== 模板只读 ==================== + +/** 获取仓库模板目录树 */ +export function useNucleiRepoTree(repoId: number | null) { + return useQuery({ + queryKey: ["nuclei-repo-tree", repoId], + queryFn: async () => { + const res = await nucleiRepoApi.getTemplateTree(repoId!) + return (res.roots ?? []) as NucleiTemplateTreeNode[] + }, + enabled: !!repoId, + }) +} + +/** 获取模板文件内容 */ +export function useNucleiRepoContent(repoId: number | null, path: string | null) { + return useQuery<NucleiTemplateContent>({ + queryKey: ["nuclei-repo-content", repoId, path], + queryFn: () => nucleiRepoApi.getTemplateContent(repoId!, path!), + enabled: !!repoId && !!path, + }) +} diff --git a/frontend/hooks/use-nuclei-templates.ts b/frontend/hooks/use-nuclei-templates.ts new file mode 100644 index 00000000..66d2644d --- /dev/null +++ b/frontend/hooks/use-nuclei-templates.ts @@ -0,0 +1,81 @@ +"use client" + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { toast } from "sonner" +import { getNucleiTemplateTree, getNucleiTemplateContent, refreshNucleiTemplates, saveNucleiTemplate, uploadNucleiTemplate } from "@/services/nuclei.service" +import type { NucleiTemplateTreeNode, NucleiTemplateContent, UploadNucleiTemplatePayload, SaveNucleiTemplatePayload } from "@/types/nuclei.types" + +export function useNucleiTemplateTree() { + return useQuery<NucleiTemplateTreeNode[]>({ + queryKey: ["nuclei", "templates", "tree"], + queryFn: () => getNucleiTemplateTree(), + }) +} + +export function useNucleiTemplateContent(path: string | null) { + return useQuery<NucleiTemplateContent>({ + queryKey: ["nuclei", "templates", "content", path], + queryFn: () => getNucleiTemplateContent(path as string), + enabled: !!path, + }) +} + +export function useRefreshNucleiTemplates() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: () => refreshNucleiTemplates(), + onMutate: () => { + toast.loading("正在更新 Nuclei 官方模板...", { id: "refresh-nuclei-templates" }) + }, + onSuccess: () => { + toast.dismiss("refresh-nuclei-templates") + toast.success("模板更新完成") + queryClient.invalidateQueries({ queryKey: ["nuclei", "templates", "tree"] }) + }, + onError: () => { + toast.dismiss("refresh-nuclei-templates") + toast.error("模板更新失败") + }, + }) +} + +export function useUploadNucleiTemplate() { + const queryClient = useQueryClient() + + return useMutation<void, Error, UploadNucleiTemplatePayload>({ + mutationFn: (payload) => uploadNucleiTemplate(payload), + onMutate: () => { + toast.loading("正在上传模板...", { id: "upload-nuclei-template" }) + }, + onSuccess: () => { + toast.dismiss("upload-nuclei-template") + toast.success("模板上传成功") + queryClient.invalidateQueries({ queryKey: ["nuclei", "templates", "tree"] }) + }, + onError: (error) => { + toast.dismiss("upload-nuclei-template") + toast.error(error.message || "模板上传失败") + }, + }) +} + +export function useSaveNucleiTemplate() { + const queryClient = useQueryClient() + + return useMutation<void, Error, SaveNucleiTemplatePayload>({ + mutationFn: (payload) => saveNucleiTemplate(payload), + onMutate: () => { + toast.loading("正在保存模板...", { id: "save-nuclei-template" }) + }, + onSuccess: (_data, variables) => { + toast.dismiss("save-nuclei-template") + toast.success("模板保存成功") + queryClient.invalidateQueries({ queryKey: ["nuclei", "templates", "content", variables.path] }) + }, + onError: (error) => { + toast.dismiss("save-nuclei-template") + toast.error(error.message || "模板保存失败") + }, + }) +} diff --git a/frontend/hooks/use-organizations.ts b/frontend/hooks/use-organizations.ts new file mode 100644 index 00000000..4323775b --- /dev/null +++ b/frontend/hooks/use-organizations.ts @@ -0,0 +1,357 @@ +import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query' +import { toast } from 'sonner' +import { OrganizationService } from '@/services/organization.service' +import type { Organization, CreateOrganizationRequest, UpdateOrganizationRequest } from '@/types/organization.types' + +// Query Keys - 统一管理查询键 +export const organizationKeys = { + all: ['organizations'] as const, + lists: () => [...organizationKeys.all, 'list'] as const, + list: (params?: any) => [...organizationKeys.lists(), params] as const, + details: () => [...organizationKeys.all, 'detail'] as const, + detail: (id: number) => [...organizationKeys.details(), id] as const, +} + +/** + * 获取组织列表的 Hook + * + * 功能: + * - 自动管理加载状态 + * - 自动错误处理 + * - 支持分页 + * - 自动缓存和重新验证 + * - 支持条件查询(enabled 选项) + */ +// 后端固定按更新时间降序排列,不支持自定义排序 +export function useOrganizations( + params: { + page?: number + pageSize?: number + search?: string + } = {}, + options?: { + enabled?: boolean + } +) { + return useQuery({ + queryKey: ['organizations', { + page: params.page || 1, + pageSize: params.pageSize || 10, + search: params.search || undefined, + }], + queryFn: () => OrganizationService.getOrganizations(params || {}), + select: (response) => { + // 处理 DRF 分页响应格式 + const page = params.page || 1 + const pageSize = params.pageSize || 10 + const total = response.total || response.count || 0 + const totalPages = Math.ceil(total / pageSize) + + return { + organizations: response.results || [], + pagination: { + total, + page, + pageSize, + totalPages, + } + } + }, + enabled: options?.enabled !== undefined ? options.enabled : true, + placeholderData: keepPreviousData, + }) +} + +/** + * 获取单个组织详情的 Hook + */ +export function useOrganization(id: number) { + return useQuery({ + queryKey: organizationKeys.detail(id), + queryFn: () => OrganizationService.getOrganizationById(id), + enabled: !!id, // 只有当 id 存在时才执行查询 + }) +} + +/** + * 获取组织的目标列表 Hook + */ +export function useOrganizationTargets( + id: number, + params?: { + page?: number + pageSize?: number + sortBy?: string + sortOrder?: 'asc' | 'desc' + search?: string + }, + options?: { + enabled?: boolean + } +) { + return useQuery({ + queryKey: [...organizationKeys.detail(id), 'targets', params], + queryFn: () => OrganizationService.getOrganizationTargets(id, params), + enabled: options?.enabled !== undefined ? (options.enabled && !!id) : !!id, + placeholderData: keepPreviousData, + }) +} + +/** + * 创建组织的 Mutation Hook + * + * 功能: + * - 自动管理提交状态 + * - 成功后自动刷新列表 + * - 自动显示成功/失败提示 + */ +export function useCreateOrganization() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: CreateOrganizationRequest) => + OrganizationService.createOrganization(data), + onMutate: () => { + // 显示创建开始的提示 + toast.loading('正在创建组织...', { id: 'create-organization' }) + }, + onSuccess: () => { + // 关闭加载提示 + toast.dismiss('create-organization') + + // 刷新所有组织相关查询(通配符匹配) + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + + // 显示成功提示 + toast.success('创建成功') + }, + onError: (error: any) => { + // 关闭加载提示 + toast.dismiss('create-organization') + + console.error('创建组织失败:', error) + console.error('后端响应:', error?.response?.data || error) + + // 前端自己构造错误提示 + toast.error('创建组织失败,请查看控制台日志') + }, + }) +} + +/** + * 更新组织的 Mutation Hook + */ +export function useUpdateOrganization() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: UpdateOrganizationRequest }) => + OrganizationService.updateOrganization({ id, ...data }), + onMutate: ({ id, data }) => { + // 显示更新开始的提示 + toast.loading('正在更新组织...', { id: `update-${id}` }) + }, + onSuccess: ({ id }) => { + // 关闭加载提示 + toast.dismiss(`update-${id}`) + + // 刷新所有组织相关查询(通配符匹配) + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + + // 显示成功提示 + toast.success('更新成功') + }, + onError: (error: any, { id }) => { + // 关闭加载提示 + toast.dismiss(`update-${id}`) + + console.error('更新组织失败:', error) + console.error('后端响应:', error?.response?.data || error) + + // 前端自己构造错误提示 + toast.error('更新组织失败,请查看控制台日志') + }, + }) +} + +/** + * 删除组织的 Mutation Hook(乐观更新) + */ +export function useDeleteOrganization() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: number) => OrganizationService.deleteOrganization(id), + onMutate: async (deletedId) => { + // 显示删除开始的提示 + toast.loading('正在删除组织...', { id: `delete-${deletedId}` }) + + // 取消正在进行的查询 + await queryClient.cancelQueries({ queryKey: ['organizations'] }) + + // 获取当前数据作为备份 + const previousData = queryClient.getQueriesData({ queryKey: ['organizations'] }) + + // 乐观更新:从所有列表查询中移除该组织 + queryClient.setQueriesData( + { queryKey: ['organizations'] }, + (old: any) => { + if (old?.organizations) { + return { + ...old, + organizations: old.organizations.filter((org: Organization) => org.id !== deletedId) + } + } + return old + } + ) + + // 返回备份数据用于回滚 + return { previousData, deletedId } + }, + onSuccess: (response, deletedId, context) => { + // 关闭加载提示 + toast.dismiss(`delete-${deletedId}`) + + // 显示删除信息(单个删除 API 返回两阶段信息) + const { organizationName, detail } = response + toast.success(`组织 "${organizationName}" 已成功删除`, { + description: `${detail.phase1};${detail.phase2}`, + duration: 4000 + }) + }, + onError: (error: any, deletedId, context) => { + // 关闭加载提示 + toast.dismiss(`delete-${deletedId}`) + + // 回滚乐观更新 + if (context?.previousData) { + context.previousData.forEach(([queryKey, data]) => { + queryClient.setQueryData(queryKey, data) + }) + } + + console.error('删除组织失败:', error) + console.error('后端响应:', error?.response?.data || error) + + // 前端自己构造错误提示 + toast.error('删除组织失败,请查看控制台日志') + }, + onSettled: () => { + // 无论成功失败都刷新数据 + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + // 刷新目标查询,因为删除组织会解除目标的关联关系,需要更新目标的 organizations 字段 + queryClient.invalidateQueries({ queryKey: ['targets'] }) + }, + }) +} + +/** + * 批量删除组织的 Mutation Hook(乐观更新) + */ +export function useBatchDeleteOrganizations() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (ids: number[]) => + OrganizationService.batchDeleteOrganizations(ids), + onMutate: async (deletedIds) => { + // 显示批量删除开始的提示 + toast.loading('正在批量删除组织...', { id: 'batch-delete' }) + + // 取消正在进行的查询 + await queryClient.cancelQueries({ queryKey: ['organizations'] }) + + // 获取当前数据作为备份 + const previousData = queryClient.getQueriesData({ queryKey: ['organizations'] }) + + // 乐观更新:从所有列表查询中移除这些组织 + queryClient.setQueriesData( + { queryKey: ['organizations'] }, + (old: any) => { + if (old?.organizations) { + return { + ...old, + organizations: old.organizations.filter( + (org: Organization) => !deletedIds.includes(org.id) + ) + } + } + return old + } + ) + + // 返回备份数据用于回滚 + return { previousData, deletedIds } + }, + onSuccess: (response, deletedIds) => { + // 关闭加载提示 + toast.dismiss('batch-delete') + + // 打印后端响应 + console.log('批量删除组织成功') + console.log('后端响应:', response) + + // 显示删除成功信息 + const { deletedOrganizationCount } = response + toast.success(`成功删除 ${deletedOrganizationCount} 个组织`) + }, + onError: (error: any, deletedIds, context) => { + // 关闭加载提示 + toast.dismiss('batch-delete') + + // 回滚乐观更新 + if (context?.previousData) { + context.previousData.forEach(([queryKey, data]) => { + queryClient.setQueryData(queryKey, data) + }) + } + + console.error('批量删除组织失败:', error) + console.error('后端响应:', error?.response?.data || error) + + // 前端自己构造错误提示 + toast.error('批量删除失败,请查看控制台日志') + }, + onSettled: () => { + // 无论成功失败都刷新数据 + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + // 刷新目标查询,因为删除组织会解除目标的关联关系,需要更新目标的 organizations 字段 + queryClient.invalidateQueries({ queryKey: ['targets'] }) + }, + }) +} + + + +/** + * 解除组织与目标关联的 Mutation Hook(批量) + */ +export function useUnlinkTargetsFromOrganization() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: { organizationId: number; targetIds: number[] }) => + OrganizationService.unlinkTargetsFromOrganization(data), + onMutate: ({ organizationId, targetIds }) => { + toast.loading('正在解除关联...', { id: `unlink-${organizationId}` }) + }, + onSuccess: (response, { organizationId }) => { + toast.dismiss(`unlink-${organizationId}`) + toast.success(response.message || '已成功解除关联') + + // 刷新所有目标和组织相关查询 + queryClient.invalidateQueries({ queryKey: ['targets'] }) + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + }, + onError: (error: any, { organizationId }) => { + toast.dismiss(`unlink-${organizationId}`) + + console.error('解除关联失败:', error) + console.error('后端响应:', error?.response?.data || error) + + const errorMessage = error?.response?.data?.error || '解除关联失败' + toast.error(errorMessage) + }, + }) +} diff --git a/frontend/hooks/use-route-prefetch.ts b/frontend/hooks/use-route-prefetch.ts new file mode 100644 index 00000000..223394a6 --- /dev/null +++ b/frontend/hooks/use-route-prefetch.ts @@ -0,0 +1,105 @@ +import { useRouter } from 'next/navigation' +import { useEffect } from 'react' + +/** + * 路由预加载 Hook + * 在页面加载完成后,后台预加载其他页面的 JS/CSS 资源 + * 不会发送 API 请求,只加载页面组件 + * @param currentPath 当前页面路径(可选),如果提供则会智能预加载相关动态路由 + */ +export function useRoutePrefetch(currentPath?: string) { + const router = useRouter() + + useEffect(() => { + console.log('[START] 路由预加载 Hook 已挂载,开始预加载...') + + // 使用 requestIdleCallback 在浏览器空闲时预加载,不影响当前页面渲染 + const prefetchRoutes = () => { + const routes = [ + // 仪表盘 + '/dashboard/', + // 资产管理 + '/assets/organization/', + '/assets/domain/', + '/assets/endpoint/', + '/assets/website/', + // 扫描 + '/scan/tools/', + '/scan/history/', + // 目标 + '/targets/', + // 漏洞 + '/vulnerabilities/', + // 设置 + '/settings/workers/', + '/settings/notification/', + ] + + routes.forEach((route) => { + console.log(` -> 预加载: ${route}`) + router.prefetch(route) + }) + + // 如果提供了当前路径,智能预加载相关动态路由 + if (currentPath) { + // 如果是域名详情页(如 /assets/domain/146),预加载子路由 + const domainIdMatch = currentPath.match(/\/assets\/domain\/(\d+)/) + if (domainIdMatch) { + const domainId = domainIdMatch[1] + router.prefetch(`/assets/domain/${domainId}/endpoints`) + console.log(` -> 智能预加载域名子路由: /assets/domain/${domainId}/endpoints`) + } + } + + console.log('[DONE] 所有路由预加载请求已发送') + } + + // 使用 requestIdleCallback 在浏览器空闲时执行,如果不支持则立即执行 + if (typeof window !== 'undefined' && 'requestIdleCallback' in window) { + const idleId = window.requestIdleCallback(prefetchRoutes) + return () => window.cancelIdleCallback(idleId) + } else { + prefetchRoutes() + return + } + }, [router, currentPath]) +} + +/** + * 智能路由预加载 Hook + * 根据当前路径,预加载用户可能访问的下一个页面 + * @param currentPath 当前页面路径 + */ +export function useSmartRoutePrefetch(currentPath: string) { + const router = useRouter() + + useEffect(() => { + const timer = setTimeout(() => { + if (currentPath.includes('/assets/organization')) { + // 在组织页面,预加载域名页面 + router.prefetch('/assets/domain') + } else if (currentPath.includes('/assets/domain')) { + // 在域名页面,预加载端点页面 + router.prefetch('/assets/endpoint') + + // 如果是域名详情页(如 /assets/domain/146),预加载子路由 + const domainIdMatch = currentPath.match(/\/assets\/domain\/(\d+)$/) + if (domainIdMatch) { + const domainId = domainIdMatch[1] + router.prefetch(`/assets/domain/${domainId}/endpoints`) + console.log(` -> 预加载域名子路由: /assets/domain/${domainId}/endpoints`) + } + } else if (currentPath.includes('/assets/scan')) { + // 在扫描页面,预加载资产页面 + router.prefetch('/assets/organization') + router.prefetch('/assets/domain') + } else if (currentPath === '/') { + // 在首页,预加载主要页面 + router.prefetch('/dashboard') + router.prefetch('/assets/organization') + } + }, 1500) // 1.5 秒后预加载 + + return () => clearTimeout(timer) + }, [currentPath, router]) +} diff --git a/frontend/hooks/use-scans.ts b/frontend/hooks/use-scans.ts new file mode 100644 index 00000000..1dcbb9a2 --- /dev/null +++ b/frontend/hooks/use-scans.ts @@ -0,0 +1,33 @@ +import { useQuery, keepPreviousData } from '@tanstack/react-query' +import { getScans, getScan, getScanStatistics } from '@/services/scan.service' +import type { GetScansParams } from '@/types/scan.types' + +export function useScans(params: GetScansParams = { page: 1, pageSize: 10 }) { + return useQuery({ + queryKey: ['scans', params], + queryFn: () => getScans(params), + placeholderData: keepPreviousData, + }) +} + +export function useRunningScans(page = 1, pageSize = 10) { + return useScans({ page, pageSize, status: 'running' }) +} + +export function useScan(id: number) { + return useQuery({ + queryKey: ['scan', id], + queryFn: () => getScan(id), + enabled: !!id, + }) +} + +/** + * 获取扫描统计数据 + */ +export function useScanStatistics() { + return useQuery({ + queryKey: ['scan-statistics'], + queryFn: getScanStatistics, + }) +} diff --git a/frontend/hooks/use-scheduled-scans.ts b/frontend/hooks/use-scheduled-scans.ts new file mode 100644 index 00000000..8a9f5bb1 --- /dev/null +++ b/frontend/hooks/use-scheduled-scans.ts @@ -0,0 +1,109 @@ +import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query' +import { toast } from 'sonner' +import { + getScheduledScans, + getScheduledScan, + createScheduledScan, + updateScheduledScan, + deleteScheduledScan, + toggleScheduledScan, +} from '@/services/scheduled-scan.service' +import type { CreateScheduledScanRequest, UpdateScheduledScanRequest } from '@/types/scheduled-scan.types' + +/** + * 获取定时扫描列表 + */ +export function useScheduledScans(params: { page?: number; pageSize?: number; search?: string } = { page: 1, pageSize: 10 }) { + return useQuery({ + queryKey: ['scheduled-scans', params], + queryFn: () => getScheduledScans(params), + placeholderData: keepPreviousData, + }) +} + +/** + * 获取定时扫描详情 + */ +export function useScheduledScan(id: number) { + return useQuery({ + queryKey: ['scheduled-scan', id], + queryFn: () => getScheduledScan(id), + enabled: !!id, + }) +} + +/** + * 创建定时扫描 + */ +export function useCreateScheduledScan() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: CreateScheduledScanRequest) => createScheduledScan(data), + onSuccess: (result) => { + toast.success(result.message) + queryClient.invalidateQueries({ queryKey: ['scheduled-scans'] }) + }, + onError: (error: Error) => { + toast.error(`创建失败: ${error.message}`) + }, + }) +} + +/** + * 更新定时扫描 + */ +export function useUpdateScheduledScan() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: UpdateScheduledScanRequest }) => + updateScheduledScan(id, data), + onSuccess: (result) => { + toast.success(result.message) + queryClient.invalidateQueries({ queryKey: ['scheduled-scans'] }) + queryClient.invalidateQueries({ queryKey: ['scheduled-scan'] }) + }, + onError: (error: Error) => { + toast.error(`更新失败: ${error.message}`) + }, + }) +} + +/** + * 删除定时扫描 + */ +export function useDeleteScheduledScan() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: number) => deleteScheduledScan(id), + onSuccess: (result) => { + toast.success(result.message) + queryClient.invalidateQueries({ queryKey: ['scheduled-scans'] }) + }, + onError: (error: Error) => { + toast.error(`删除失败: ${error.message}`) + }, + }) +} + +/** + * 切换定时扫描启用状态 + */ +export function useToggleScheduledScan() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ id, isEnabled }: { id: number; isEnabled: boolean }) => + toggleScheduledScan(id, isEnabled), + onSuccess: (result) => { + toast.success(result.message) + queryClient.invalidateQueries({ queryKey: ['scheduled-scans'] }) + }, + onError: (error: Error) => { + toast.error(`操作失败: ${error.message}`) + }, + }) +} + diff --git a/frontend/hooks/use-step.ts b/frontend/hooks/use-step.ts new file mode 100644 index 00000000..218e6687 --- /dev/null +++ b/frontend/hooks/use-step.ts @@ -0,0 +1,71 @@ +"use client" + +import { useCallback, useState } from 'react' +import type { Dispatch, SetStateAction } from 'react' + +type UseStepActions = { + goToNextStep: () => void + goToPrevStep: () => void + reset: () => void + canGoToNextStep: boolean + canGoToPrevStep: boolean + setStep: Dispatch<SetStateAction<number>> +} + +type SetStepCallbackType = (step: number | ((step: number) => number)) => void + +/** + * 步骤导航 Hook + * @param maxStep 最大步骤数 + * @returns [currentStep, actions] + */ +export function useStep(maxStep: number): [number, UseStepActions] { + const [currentStep, setCurrentStep] = useState(1) + + const canGoToNextStep = currentStep + 1 <= maxStep + const canGoToPrevStep = currentStep - 1 > 0 + + const setStep = useCallback<SetStepCallbackType>( + step => { + const newStep = step instanceof Function ? step(currentStep) : step + + if (newStep >= 1 && newStep <= maxStep) { + setCurrentStep(newStep) + return + } + + throw new Error('Step not valid') + }, + [maxStep, currentStep], + ) + + const goToNextStep = useCallback(() => { + if (canGoToNextStep) { + setCurrentStep(step => step + 1) + } + }, [canGoToNextStep]) + + const goToPrevStep = useCallback(() => { + if (canGoToPrevStep) { + setCurrentStep(step => step - 1) + } + }, [canGoToPrevStep]) + + const reset = useCallback(() => { + setCurrentStep(1) + }, []) + + return [ + currentStep, + { + goToNextStep, + goToPrevStep, + canGoToNextStep, + canGoToPrevStep, + setStep, + reset, + }, + ] +} + +export type { UseStepActions } diff --git a/frontend/hooks/use-subdomains.ts b/frontend/hooks/use-subdomains.ts new file mode 100644 index 00000000..10eecffe --- /dev/null +++ b/frontend/hooks/use-subdomains.ts @@ -0,0 +1,296 @@ +"use client" + +import { useMutation, useQuery, useQueryClient, keepPreviousData } from "@tanstack/react-query" +import { toast } from "sonner" +import { SubdomainService } from "@/services/subdomain.service" +import { OrganizationService } from "@/services/organization.service" +import type { Subdomain, GetSubdomainsResponse, GetAllSubdomainsParams } from "@/types/subdomain.types" +import type { PaginationParams } from "@/types/common.types" + +// Query Keys +export const subdomainKeys = { + all: ['subdomains'] as const, + lists: () => [...subdomainKeys.all, 'list'] as const, + list: (params: PaginationParams & { organizationId?: string }) => + [...subdomainKeys.lists(), params] as const, + details: () => [...subdomainKeys.all, 'detail'] as const, + detail: (id: number) => [...subdomainKeys.details(), id] as const, +} + +// 获取单个子域名详情 +export function useSubdomain(id: number) { + return useQuery({ + queryKey: subdomainKeys.detail(id), + queryFn: () => SubdomainService.getSubdomainById(id), + enabled: !!id, + }) +} + +// 获取组织的子域名列表 +export function useOrganizationSubdomains( + organizationId: number, + params?: { page?: number; pageSize?: number }, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: ['organizations', 'detail', organizationId, 'subdomains', { + page: params?.page, + pageSize: params?.pageSize, + }], + queryFn: () => SubdomainService.getSubdomainsByOrgId(organizationId, { + page: params?.page || 1, + pageSize: params?.pageSize || 10, + }), + enabled: options?.enabled !== undefined ? options.enabled : true, + select: (response) => ({ + domains: response.domains || [], + pagination: { + total: response.total || 0, + page: response.page || 1, + pageSize: response.pageSize || 10, + totalPages: response.totalPages || 0, + } + }), + }) +} + +// 创建子域名(绑定到资产) +export function useCreateSubdomain() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: { domains: Array<{ name: string }>; assetId: number }) => + SubdomainService.createSubdomains(data), + onMutate: async () => { + toast.loading('正在创建子域名...', { id: 'create-subdomain' }) + }, + onSuccess: (response) => { + toast.dismiss('create-subdomain') + const { createdCount, existedCount, skippedCount = 0 } = response + if (skippedCount > 0 && existedCount > 0) { + toast.warning(`成功创建 ${createdCount} 个子域名(${existedCount} 个已存在,${skippedCount} 个已跳过)`) + } else if (skippedCount > 0) { + toast.warning(`成功创建 ${createdCount} 个子域名(${skippedCount} 个已跳过)`) + } else if (existedCount > 0) { + toast.warning(`成功创建 ${createdCount} 个子域名(${existedCount} 个已存在)`) + } else { + toast.success(`成功创建 ${createdCount} 个子域名`) + } + queryClient.invalidateQueries({ queryKey: ['subdomains'] }) + queryClient.invalidateQueries({ queryKey: ['assets'] }) + }, + onError: (error: any) => { + toast.dismiss('create-subdomain') + console.error('创建子域名失败:', error) + console.error('后端响应:', error?.response?.data || error) + toast.error('创建子域名失败,请查看控制台日志') + }, + }) +} + +// 从组织中移除子域名 +export function useDeleteSubdomainFromOrganization() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: { organizationId: number; targetId: number }) => + OrganizationService.unlinkTargetsFromOrganization({ + organizationId: data.organizationId, + targetIds: [data.targetId], + }), + onMutate: ({ organizationId, targetId }) => { + toast.loading('正在移除子域名...', { id: `delete-${organizationId}-${targetId}` }) + }, + onSuccess: (_response, { organizationId }) => { + toast.dismiss(`delete-${organizationId}`) + toast.success('子域名已成功移除') + queryClient.invalidateQueries({ queryKey: ['subdomains'] }) + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + }, + onError: (error: any, { organizationId, targetId }) => { + toast.dismiss(`delete-${organizationId}-${targetId}`) + console.error('移除子域名失败:', error) + console.error('后端响应:', error?.response?.data || error) + toast.error('移除子域名失败,请查看控制台日志') + }, + }) +} + +// 批量从组织中移除子域名 +export function useBatchDeleteSubdomainsFromOrganization() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: { organizationId: number; domainIds: number[] }) => + SubdomainService.batchDeleteSubdomainsFromOrganization(data), + onMutate: ({ organizationId }) => { + toast.loading('正在批量移除子域名...', { id: `batch-delete-${organizationId}` }) + }, + onSuccess: (response, { organizationId }) => { + toast.dismiss(`batch-delete-${organizationId}`) + const successCount = response.successCount || 0 + const failedCount = response.failedCount || 0 + if (failedCount > 0) { + toast.warning(`批量移除完成(成功:${successCount},失败:${failedCount})`) + } else { + toast.success(`成功移除 ${successCount} 个子域名`) + } + queryClient.invalidateQueries({ queryKey: ['subdomains'] }) + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + }, + onError: (error: any, { organizationId }) => { + toast.dismiss(`batch-delete-${organizationId}`) + console.error('批量移除子域名失败:', error) + console.error('后端响应:', error?.response?.data || error) + toast.error('批量移除失败,请查看控制台日志') + }, + }) +} + +// 删除单个子域名(使用单独的 DELETE API) +export function useDeleteSubdomain() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: number) => SubdomainService.deleteSubdomain(id), + onMutate: (id) => { + toast.loading('正在删除子域名...', { id: `delete-subdomain-${id}` }) + }, + onSuccess: (response, id) => { + toast.dismiss(`delete-subdomain-${id}`) + + // 显示删除信息(单个删除 API 返回两阶段信息) + const { subdomainName, detail } = response + toast.success(`子域名 "${subdomainName}" 已成功删除`, { + description: `${detail.phase1};${detail.phase2}`, + duration: 4000 + }) + + queryClient.invalidateQueries({ queryKey: ['subdomains'] }) + queryClient.invalidateQueries({ queryKey: ['targets'] }) + queryClient.invalidateQueries({ queryKey: ['scans'] }) + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + }, + onError: (error: any, id) => { + toast.dismiss(`delete-subdomain-${id}`) + console.error('删除子域名失败:', error) + console.error('后端响应:', error?.response?.data || error) + toast.error('删除子域名失败,请查看控制台日志') + }, + }) +} + +// 批量删除子域名(使用统一的批量删除接口) +export function useBatchDeleteSubdomains() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (ids: number[]) => SubdomainService.batchDeleteSubdomains(ids), + onMutate: () => { + toast.loading('正在批量删除子域名...', { id: 'batch-delete-subdomains' }) + }, + onSuccess: (response) => { + toast.dismiss('batch-delete-subdomains') + + // 显示级联删除信息 + const cascadeInfo = Object.entries(response.cascadeDeleted || {}) + .filter(([key, count]) => key !== 'asset.Subdomain' && count > 0) + .map(([key, count]) => { + const modelName = key.split('.')[1] + return `${modelName}: ${count}` + }) + .join(', ') + + if (cascadeInfo) { + toast.success(`成功删除 ${response.deletedCount} 个子域名(级联删除: ${cascadeInfo})`) + } else { + toast.success(`成功删除 ${response.deletedCount} 个子域名`) + } + + queryClient.invalidateQueries({ queryKey: ['subdomains'] }) + queryClient.invalidateQueries({ queryKey: ['targets'] }) + queryClient.invalidateQueries({ queryKey: ['scans'] }) + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + }, + onError: (error: any) => { + toast.dismiss('batch-delete-subdomains') + console.error('批量删除子域名失败:', error) + console.error('后端响应:', error?.response?.data || error) + toast.error('批量删除失败,请查看控制台日志') + }, + }) +} + +// 更新子域名 +export function useUpdateSubdomain() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: { name?: string; description?: string } }) => + SubdomainService.updateSubdomain({ id, ...data }), + onMutate: ({ id }) => { + toast.loading('正在更新子域名...', { id: `update-subdomain-${id}` }) + }, + onSuccess: (_response, { id }) => { + toast.dismiss(`update-subdomain-${id}`) + toast.success('更新成功') + queryClient.invalidateQueries({ queryKey: ['subdomains'] }) + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + }, + onError: (error: any, { id }) => { + toast.dismiss(`update-subdomain-${id}`) + console.error('更新子域名失败:', error) + console.error('后端响应:', error?.response?.data || error) + toast.error('更新子域名失败,请查看控制台日志') + }, + }) +} + +// 获取所有子域名列表 +export function useAllSubdomains( + params: GetAllSubdomainsParams = {}, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: ['subdomains', 'all', { page: params.page, pageSize: params.pageSize }], + queryFn: () => SubdomainService.getAllSubdomains(params), + select: (response) => ({ + domains: response.domains || [], + pagination: { + total: response.total || 0, + page: response.page || 1, + pageSize: response.pageSize || 10, + totalPages: response.totalPages || 0, + } + }), + enabled: options?.enabled !== undefined ? options.enabled : true, + }) +} + +// 获取目标的子域名列表 +export function useTargetSubdomains( + targetId: number, + params?: { page?: number; pageSize?: number; search?: string }, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: ['targets', targetId, 'subdomains', { page: params?.page, pageSize: params?.pageSize, search: params?.search }], + queryFn: () => SubdomainService.getSubdomainsByTargetId(targetId, params), + enabled: options?.enabled !== undefined ? options.enabled : !!targetId, + placeholderData: keepPreviousData, + }) +} + +// 获取扫描的子域名列表 +export function useScanSubdomains( + scanId: number, + params?: { page?: number; pageSize?: number; search?: string }, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: ['scans', scanId, 'subdomains', { page: params?.page, pageSize: params?.pageSize, search: params?.search }], + queryFn: () => SubdomainService.getSubdomainsByScanId(scanId, params), + enabled: options?.enabled !== undefined ? options.enabled : !!scanId, + placeholderData: keepPreviousData, + }) +} diff --git a/frontend/hooks/use-targets.ts b/frontend/hooks/use-targets.ts new file mode 100644 index 00000000..11f458a9 --- /dev/null +++ b/frontend/hooks/use-targets.ts @@ -0,0 +1,301 @@ +/** + * Targets Hooks - 目标管理相关 hooks + */ +import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query' +import { toast } from 'sonner' +import { + getTargets, + getTargetById, + createTarget, + updateTarget, + deleteTarget, + batchDeleteTargets, + batchCreateTargets, + getTargetOrganizations, + linkTargetOrganizations, + unlinkTargetOrganizations, + getTargetEndpoints, +} from '@/services/target.service' +import type { + CreateTargetRequest, + UpdateTargetRequest, + BatchDeleteTargetsRequest, + BatchCreateTargetsRequest, +} from '@/types/target.types' + +/** + * 获取所有目标列表 + * 支持两种调用方式: + * 1. useTargets(page, pageSize, organizationId) - 直接传参数 + * 2. useTargets({ page, pageSize, organizationId }) - 传对象(已废弃,为了兼容性保留) + */ +export function useTargets( + pageOrParams: number | { page?: number; pageSize?: number; organizationId?: number; search?: string } = 1, + pageSize = 10, + organizationId?: number, + search?: string +) { + // 处理参数:支持对象参数或独立参数 + let actualPage: number + let actualPageSize: number + let actualOrgId: number | undefined + let actualSearch: string | undefined + + if (typeof pageOrParams === 'object') { + // 对象参数方式(兼容旧代码) + actualPage = pageOrParams.page || 1 + actualPageSize = pageOrParams.pageSize || 10 + actualOrgId = pageOrParams.organizationId + actualSearch = pageOrParams.search + } else { + // 独立参数方式 + actualPage = pageOrParams + actualPageSize = pageSize + actualOrgId = organizationId + actualSearch = search + } + + return useQuery({ + queryKey: ['targets', { page: actualPage, pageSize: actualPageSize, organizationId: actualOrgId, search: actualSearch }], + queryFn: () => getTargets(actualPage, actualPageSize, actualSearch), + select: (response) => { + // 如果指定了 organizationId,过滤结果 + if (actualOrgId) { + const filteredResults = response.results.filter(target => + target.organizations?.some(org => org.id === actualOrgId) + ) + return { + ...response, + results: filteredResults, + total: filteredResults.length, + // 为兼容性添加额外字段 + count: filteredResults.length, // 兼容字段 + targets: filteredResults, + page: actualPage, + pageSize: actualPageSize, + totalPages: Math.ceil(filteredResults.length / actualPageSize), + } + } + + // 否则直接返回原始响应,并添加兼容字段 + return { + ...response, + targets: response.results, + // 后端返回 total,不是 count + count: response.total, // 兼容字段,使用 total 值 + // 保持原有字段 + total: response.total, + page: response.page, + pageSize: response.pageSize, + totalPages: response.totalPages, + } + }, + placeholderData: keepPreviousData, + }) +} + +/** + * 获取单个目标详情 + */ +export function useTarget(id: number) { + return useQuery({ + queryKey: ['targets', id], + queryFn: () => getTargetById(id), + enabled: !!id, + }) +} + +/** + * 创建目标 + */ +export function useCreateTarget() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: CreateTargetRequest) => createTarget(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['targets'] }) + toast.success('目标创建成功') + }, + onError: (error: Error) => { + toast.error(`创建失败: ${error.message}`) + }, + }) +} + +/** + * 更新目标 + */ +export function useUpdateTarget() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: UpdateTargetRequest }) => + updateTarget(id, data), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['targets'] }) + queryClient.invalidateQueries({ queryKey: ['targets', variables.id] }) + toast.success('目标更新成功') + }, + onError: (error: Error) => { + toast.error(`更新失败: ${error.message}`) + }, + }) +} + +/** + * 删除目标(使用单独的 DELETE API) + */ +export function useDeleteTarget() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: number) => deleteTarget(id), + onMutate: (id) => { + toast.loading('正在删除目标...', { id: `delete-target-${id}` }) + }, + onSuccess: (response, id) => { + toast.dismiss(`delete-target-${id}`) + + // 显示删除信息(单个删除 API 返回两阶段信息) + const { targetName, detail } = response + toast.success(`目标 "${targetName}" 已成功删除`, { + description: `${detail.phase1};${detail.phase2}`, + duration: 4000 + }) + + queryClient.invalidateQueries({ queryKey: ['targets'] }) + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + }, + onError: (error: Error, id) => { + toast.dismiss(`delete-target-${id}`) + toast.error(`删除失败: ${error.message}`) + }, + }) +} + +/** + * 批量删除目标 + */ +export function useBatchDeleteTargets() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: BatchDeleteTargetsRequest) => batchDeleteTargets(data), + onSuccess: (response) => { + queryClient.invalidateQueries({ queryKey: ['targets'] }) + toast.success(`成功删除 ${response.deletedCount} 个目标`) + }, + onError: (error: Error) => { + toast.error(`批量删除失败: ${error.message}`) + }, + }) +} + +/** + * 批量创建目标 + */ +export function useBatchCreateTargets() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: BatchCreateTargetsRequest) => batchCreateTargets(data), + onSuccess: (response) => { + queryClient.invalidateQueries({ queryKey: ['targets'] }) + queryClient.invalidateQueries({ queryKey: ['organizations'] }) + toast.success(response.message) + }, + onError: (error: Error) => { + toast.error(`批量创建失败: ${error.message}`) + }, + }) +} + +/** + * 获取目标的组织列表 + */ +export function useTargetOrganizations(targetId: number, page = 1, pageSize = 10) { + return useQuery({ + queryKey: ['targets', targetId, 'organizations', page, pageSize], + queryFn: () => getTargetOrganizations(targetId, page, pageSize), + enabled: !!targetId, + }) +} + +/** + * 关联目标与组织 + */ +export function useLinkTargetOrganizations() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ targetId, organizationIds }: { targetId: number; organizationIds: number[] }) => + linkTargetOrganizations(targetId, organizationIds), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['targets', variables.targetId, 'organizations'] }) + queryClient.invalidateQueries({ queryKey: ['targets', variables.targetId] }) + toast.success('组织关联成功') + }, + onError: (error: Error) => { + toast.error(`关联失败: ${error.message}`) + }, + }) +} + +/** + * 取消目标与组织的关联 + */ +export function useUnlinkTargetOrganizations() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ targetId, organizationIds }: { targetId: number; organizationIds: number[] }) => + unlinkTargetOrganizations(targetId, organizationIds), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['targets', variables.targetId, 'organizations'] }) + queryClient.invalidateQueries({ queryKey: ['targets', variables.targetId] }) + toast.success('取消关联成功') + }, + onError: (error: Error) => { + toast.error(`取消关联失败: ${error.message}`) + }, + }) +} + +/** + * 获取目标的端点列表 + */ +export function useTargetEndpoints( + targetId: number, + params?: { + page?: number + pageSize?: number + search?: string + }, + options?: { + enabled?: boolean + } +) { + return useQuery({ + queryKey: ['targets', 'detail', targetId, 'endpoints', { + page: params?.page, + pageSize: params?.pageSize, + search: params?.search, + }], + queryFn: () => getTargetEndpoints(targetId, params?.page || 1, params?.pageSize || 10, params?.search), + enabled: options?.enabled !== undefined ? options.enabled : !!targetId, + select: (response: any) => { + // RESTful 标准:直接返回数据 + return { + endpoints: response.endpoints || [], + pagination: { + total: response.total || 0, + page: response.page || 1, + pageSize: response.pageSize || 10, + totalPages: response.totalPages || 0, + } + } + }, + }) +} + diff --git a/frontend/hooks/use-tools.ts b/frontend/hooks/use-tools.ts new file mode 100644 index 00000000..5f1fb652 --- /dev/null +++ b/frontend/hooks/use-tools.ts @@ -0,0 +1,137 @@ +"use client" + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { toast } from "sonner" +import { ToolService } from "@/services/tool.service" +import type { Tool, GetToolsParams, CreateToolRequest, UpdateToolRequest } from "@/types/tool.types" + +// Query Keys +export const toolKeys = { + all: ['tools'] as const, + lists: () => [...toolKeys.all, 'list'] as const, + list: (params: GetToolsParams) => [...toolKeys.lists(), params] as const, +} + +// 获取工具列表 +export function useTools(params: GetToolsParams = {}) { + return useQuery({ + queryKey: toolKeys.list(params), + queryFn: () => ToolService.getTools(params), + select: (response) => { + // RESTful 标准:直接返回数据 + return { + tools: response.tools || [], + pagination: { + total: response.total || 0, + page: response.page || 1, + pageSize: response.pageSize || 10, + totalPages: response.totalPages || 0, + } + } + }, + }) +} + +// 创建工具 +export function useCreateTool() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: CreateToolRequest) => ToolService.createTool(data), + onMutate: async () => { + toast.loading('正在创建工具...', { id: 'create-tool' }) + }, + onSuccess: (response) => { + toast.dismiss('create-tool') + + // 打印后端响应 + console.log('创建工具成功') + console.log('后端响应:', response) + + toast.success('创建成功') + + // 刷新工具列表和分类列表 + queryClient.invalidateQueries({ + queryKey: toolKeys.all, + refetchType: 'active' + }) + }, + onError: (error: any) => { + toast.dismiss('create-tool') + + console.error('创建工具失败:', error) + console.error('后端响应:', error?.response?.data || error) + + // 前端自己构造错误提示 + toast.error('创建工具失败,请查看控制台日志') + }, + }) +} + +// 更新工具 +export function useUpdateTool() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: UpdateToolRequest }) => + ToolService.updateTool(id, data), + onMutate: async () => { + toast.loading('正在更新工具...', { id: 'update-tool' }) + }, + onSuccess: (response) => { + toast.dismiss('update-tool') + + console.log('更新工具成功') + console.log('后端响应:', response) + + toast.success('更新成功') + + // 刷新工具列表 + queryClient.invalidateQueries({ + queryKey: toolKeys.all, + refetchType: 'active' + }) + }, + onError: (error: any) => { + toast.dismiss('update-tool') + + console.error('更新工具失败:', error) + console.error('后端响应:', error?.response?.data || error) + + toast.error('更新工具失败,请查看控制台日志') + }, + }) +} + +// 删除工具 +export function useDeleteTool() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: number) => ToolService.deleteTool(id), + onMutate: async () => { + toast.loading('正在删除工具...', { id: 'delete-tool' }) + }, + onSuccess: (response) => { + toast.dismiss('delete-tool') + + console.log('删除工具成功') + + toast.success('删除成功') + + // 刷新工具列表 + queryClient.invalidateQueries({ + queryKey: toolKeys.all, + refetchType: 'active' + }) + }, + onError: (error: any) => { + toast.dismiss('delete-tool') + + console.error('删除工具失败:', error) + console.error('后端响应:', error?.response?.data || error) + + toast.error('删除工具失败,请查看控制台日志') + }, + }) +} diff --git a/frontend/hooks/use-vulnerabilities.ts b/frontend/hooks/use-vulnerabilities.ts new file mode 100644 index 00000000..30f622f4 --- /dev/null +++ b/frontend/hooks/use-vulnerabilities.ts @@ -0,0 +1,231 @@ +"use client" + +import { useQuery, keepPreviousData } from "@tanstack/react-query" + +import { VulnerabilityService } from "@/services/vulnerability.service" +import type { + Vulnerability, + VulnerabilitySeverity, + GetVulnerabilitiesParams, +} from "@/types/vulnerability.types" +import type { PaginationInfo } from "@/types/common.types" + +export const vulnerabilityKeys = { + all: ["vulnerabilities"] as const, + list: (params: GetVulnerabilitiesParams) => + [...vulnerabilityKeys.all, "list", params] as const, + byScan: (scanId: number, params: GetVulnerabilitiesParams) => + [...vulnerabilityKeys.all, "scan", scanId, params] as const, + byTarget: (targetId: number, params: GetVulnerabilitiesParams) => + [...vulnerabilityKeys.all, "target", targetId, params] as const, +} + +/** 获取所有漏洞 */ +export function useAllVulnerabilities( + params?: GetVulnerabilitiesParams, + options?: { enabled?: boolean }, +) { + const defaultParams: GetVulnerabilitiesParams = { + page: 1, + pageSize: 10, + ...params, + } + + return useQuery({ + queryKey: vulnerabilityKeys.list(defaultParams), + queryFn: () => VulnerabilityService.getAllVulnerabilities(defaultParams), + enabled: options?.enabled ?? true, + select: (response: any) => { + const items = (response?.results ?? []) as any[] + + const vulnerabilities: Vulnerability[] = items.map((item) => { + let severity = (item.severity || "info") as + | VulnerabilitySeverity + | "unknown" + if (severity === "unknown") { + severity = "info" + } + + let cvssScore: number | undefined + if (typeof item.cvssScore === "number") { + cvssScore = item.cvssScore + } else if (item.cvssScore != null) { + const num = Number(item.cvssScore) + cvssScore = Number.isNaN(num) ? undefined : num + } + + const discoveredAt: string = item.discoveredAt + + return { + id: item.id, + vulnType: item.vulnType || "unknown", + url: item.url || "", + description: item.description || "", + severity: severity as VulnerabilitySeverity, + source: item.source || "scan", + cvssScore, + rawOutput: item.rawOutput || {}, + discoveredAt, + } + }) + + const pagination: PaginationInfo = { + total: response?.total ?? 0, + page: response?.page ?? defaultParams.page ?? 1, + pageSize: + response?.pageSize ?? + response?.page_size ?? + defaultParams.pageSize ?? + 10, + totalPages: + response?.totalPages ?? + response?.total_pages ?? + 0, + } + + return { vulnerabilities, pagination } + }, + placeholderData: keepPreviousData, + }) +} + +export function useScanVulnerabilities( + scanId: number, + params?: GetVulnerabilitiesParams, + options?: { enabled?: boolean }, +) { + const defaultParams: GetVulnerabilitiesParams = { + page: 1, + pageSize: 10, + ...params, + } + + return useQuery({ + queryKey: vulnerabilityKeys.byScan(scanId, defaultParams), + queryFn: () => + VulnerabilityService.getVulnerabilitiesByScanId(scanId, defaultParams), + enabled: options?.enabled !== undefined ? options.enabled : !!scanId, + select: (response: any) => { + const items = (response?.results ?? []) as any[] + + const vulnerabilities: Vulnerability[] = items.map((item) => { + let severity = (item.severity || "info") as + | VulnerabilitySeverity + | "unknown" + if (severity === "unknown") { + severity = "info" + } + + let cvssScore: number | undefined + if (typeof item.cvssScore === "number") { + cvssScore = item.cvssScore + } else if (item.cvssScore != null) { + const num = Number(item.cvssScore) + cvssScore = Number.isNaN(num) ? undefined : num + } + + const discoveredAt: string = item.discoveredAt + + return { + id: item.id, + vulnType: item.vulnType || "unknown", + url: item.url || "", + description: item.description || "", + severity: severity as VulnerabilitySeverity, + source: item.source || "scan", + cvssScore, + rawOutput: item.rawOutput || {}, + discoveredAt, + } + }) + + const pagination: PaginationInfo = { + total: response?.total ?? 0, + page: response?.page ?? defaultParams.page ?? 1, + pageSize: + response?.pageSize ?? + response?.page_size ?? + defaultParams.pageSize ?? + 10, + totalPages: + response?.totalPages ?? + response?.total_pages ?? + 0, + } + + return { vulnerabilities, pagination } + }, + placeholderData: keepPreviousData, + }) +} + +export function useTargetVulnerabilities( + targetId: number, + params?: GetVulnerabilitiesParams, + options?: { enabled?: boolean }, +) { + const defaultParams: GetVulnerabilitiesParams = { + page: 1, + pageSize: 10, + ...params, + } + + return useQuery({ + queryKey: vulnerabilityKeys.byTarget(targetId, defaultParams), + queryFn: () => + VulnerabilityService.getVulnerabilitiesByTargetId(targetId, defaultParams), + enabled: options?.enabled !== undefined ? options.enabled : !!targetId, + select: (response: any) => { + const items = (response?.results ?? []) as any[] + + const vulnerabilities: Vulnerability[] = items.map((item) => { + let severity = (item.severity || "info") as + | VulnerabilitySeverity + | "unknown" + if (severity === "unknown") { + severity = "info" + } + + let cvssScore: number | undefined + if (typeof item.cvssScore === "number") { + cvssScore = item.cvssScore + } else if (item.cvssScore != null) { + const num = Number(item.cvssScore) + cvssScore = Number.isNaN(num) ? undefined : num + } + + const discoveredAt: string = item.discoveredAt + + return { + id: item.id, + vulnType: item.vulnType || "unknown", + url: item.url || "", + description: item.description || "", + severity: severity as VulnerabilitySeverity, + source: item.source || "scan", + target: item.target ?? targetId, + cvssScore, + rawOutput: item.rawOutput || {}, + discoveredAt, + } + }) + + const pagination: PaginationInfo = { + total: response?.total ?? 0, + page: response?.page ?? defaultParams.page ?? 1, + pageSize: + response?.pageSize ?? + response?.page_size ?? + defaultParams.pageSize ?? + 10, + totalPages: + response?.totalPages ?? + response?.total_pages ?? + 0, + } + + return { vulnerabilities, pagination } + }, + placeholderData: keepPreviousData, + }) +} diff --git a/frontend/hooks/use-websites.ts b/frontend/hooks/use-websites.ts new file mode 100644 index 00000000..167e3de1 --- /dev/null +++ b/frontend/hooks/use-websites.ts @@ -0,0 +1,177 @@ +import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query' +import { toast } from 'sonner' +import type { WebSite, WebSiteListResponse } from '@/types/website.types' + +// API 服务函数 +const websiteService = { + // 获取目标的网站列表 + getTargetWebSites: async ( + targetId: number, + params: { page: number; pageSize: number; search?: string } + ): Promise<WebSiteListResponse> => { + const searchParam = params.search ? `&search=${encodeURIComponent(params.search)}` : '' + const response = await fetch( + `/api/targets/${targetId}/websites/?page=${params.page}&pageSize=${params.pageSize}${searchParam}` + ) + if (!response.ok) { + throw new Error('获取网站列表失败') + } + return response.json() + }, + + // 获取扫描的网站列表 + getScanWebSites: async ( + scanId: number, + params: { page: number; pageSize: number; search?: string } + ): Promise<WebSiteListResponse> => { + const searchParam = params.search ? `&search=${encodeURIComponent(params.search)}` : '' + const response = await fetch( + `/api/scans/${scanId}/websites/?page=${params.page}&pageSize=${params.pageSize}${searchParam}` + ) + if (!response.ok) { + throw new Error('获取网站列表失败') + } + return response.json() + }, + + // 批量删除网站(支持单个或多个) + bulkDeleteWebSites: async (ids: number[]): Promise<{ + message: string + deletedCount: number + requestedIds: number[] + cascadeDeleted: Record<string, number> + }> => { + const response = await fetch('/api/websites/bulk-delete/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ ids }), + }) + if (!response.ok) { + throw new Error('批量删除网站失败') + } + return response.json() + }, + + // 删除单个网站(使用单独的 DELETE API) + deleteWebSite: async (websiteId: number): Promise<{ + message: string + websiteId: number + websiteUrl: string + deletedCount: number + deletedWebSites: string[] + detail: { + phase1: string + phase2: string + } + }> => { + const response = await fetch(`/api/websites/${websiteId}/`, { + method: 'DELETE', + }) + if (!response.ok) { + throw new Error('删除网站失败') + } + return response.json() + }, +} + +// 获取目标的网站列表 +export function useTargetWebSites( + targetId: number, + params: { page: number; pageSize: number; search?: string }, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: ['target-websites', targetId, params], + queryFn: () => websiteService.getTargetWebSites(targetId, params), + enabled: options?.enabled ?? true, + placeholderData: keepPreviousData, + }) +} + +// 获取扫描的网站列表 +export function useScanWebSites( + scanId: number, + params: { page: number; pageSize: number; search?: string }, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: ['scan-websites', scanId, params], + queryFn: () => websiteService.getScanWebSites(scanId, params), + enabled: options?.enabled ?? true, + placeholderData: keepPreviousData, + }) +} + +// 删除单个网站(使用单独的 DELETE API) +export function useDeleteWebSite() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: websiteService.deleteWebSite, + onMutate: (id) => { + toast.loading('正在删除网站...', { id: `delete-website-${id}` }) + }, + onSuccess: (response, id) => { + toast.dismiss(`delete-website-${id}`) + + // 显示删除信息(单个删除 API 返回两阶段信息) + const { websiteUrl, detail } = response + toast.success(`网站 "${websiteUrl}" 已成功删除`, { + description: `${detail.phase1};${detail.phase2}`, + duration: 4000 + }) + + // 刷新相关查询 + queryClient.invalidateQueries({ queryKey: ['target-websites'] }) + queryClient.invalidateQueries({ queryKey: ['scan-websites'] }) + queryClient.invalidateQueries({ queryKey: ['targets'] }) + queryClient.invalidateQueries({ queryKey: ['scans'] }) + }, + onError: (error: Error, id) => { + toast.dismiss(`delete-website-${id}`) + toast.error(error.message || '删除网站失败') + }, + }) +} + +// 批量删除网站(使用统一的批量删除接口) +export function useBulkDeleteWebSites() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: websiteService.bulkDeleteWebSites, + onMutate: () => { + toast.loading('正在批量删除网站...', { id: 'bulk-delete-websites' }) + }, + onSuccess: (response) => { + toast.dismiss('bulk-delete-websites') + + // 显示级联删除信息 + const cascadeInfo = Object.entries(response.cascadeDeleted || {}) + .filter(([key, count]) => key !== 'asset.WebSite' && count > 0) + .map(([key, count]) => { + const modelName = key.split('.')[1] + return `${modelName}: ${count}` + }) + .join(', ') + + if (cascadeInfo) { + toast.success(`成功删除 ${response.deletedCount} 个网站(级联删除: ${cascadeInfo})`) + } else { + toast.success(`成功删除 ${response.deletedCount} 个网站`) + } + + // 刷新相关查询 + queryClient.invalidateQueries({ queryKey: ['target-websites'] }) + queryClient.invalidateQueries({ queryKey: ['scan-websites'] }) + queryClient.invalidateQueries({ queryKey: ['targets'] }) + queryClient.invalidateQueries({ queryKey: ['scans'] }) + }, + onError: (error: Error) => { + toast.dismiss('bulk-delete-websites') + toast.error(error.message || '批量删除网站失败') + }, + }) +} diff --git a/frontend/hooks/use-wordlists.ts b/frontend/hooks/use-wordlists.ts new file mode 100644 index 00000000..af0ba612 --- /dev/null +++ b/frontend/hooks/use-wordlists.ts @@ -0,0 +1,96 @@ +"use client" + +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { toast } from "sonner" +import { + getWordlists, + uploadWordlist, + deleteWordlist, + getWordlistContent, + updateWordlistContent, +} from "@/services/wordlist.service" +import type { GetWordlistsResponse, Wordlist } from "@/types/wordlist.types" + +// 获取字典列表 +export function useWordlists(params?: { page?: number; pageSize?: number }) { + const page = params?.page ?? 1 + const pageSize = params?.pageSize ?? 10 + + return useQuery<GetWordlistsResponse>({ + queryKey: ["wordlists", { page, pageSize }], + queryFn: () => getWordlists(page, pageSize), + }) +} + +// 上传字典 +export function useUploadWordlist() { + const queryClient = useQueryClient() + + return useMutation<{}, Error, { name: string; description?: string; file: File }>({ + mutationFn: (payload) => uploadWordlist(payload), + onMutate: () => { + toast.loading("正在上传字典...", { id: "upload-wordlist" }) + }, + onSuccess: () => { + toast.dismiss("upload-wordlist") + toast.success("字典上传成功") + queryClient.invalidateQueries({ queryKey: ["wordlists"] }) + }, + onError: (error) => { + toast.dismiss("upload-wordlist") + toast.error(`上传失败: ${error.message}`) + }, + }) +} + +// 删除字典 +export function useDeleteWordlist() { + const queryClient = useQueryClient() + + return useMutation<void, Error, number>({ + mutationFn: (id: number) => deleteWordlist(id), + onMutate: (id) => { + toast.loading("正在删除字典...", { id: `delete-wordlist-${id}` }) + }, + onSuccess: (_data, id) => { + toast.dismiss(`delete-wordlist-${id}`) + toast.success("字典删除成功") + queryClient.invalidateQueries({ queryKey: ["wordlists"] }) + }, + onError: (error, id) => { + toast.dismiss(`delete-wordlist-${id}`) + toast.error(`删除失败: ${error.message}`) + }, + }) +} + +// 获取字典内容 +export function useWordlistContent(id: number | null) { + return useQuery<string>({ + queryKey: ["wordlist-content", id], + queryFn: () => getWordlistContent(id!), + enabled: id !== null, + }) +} + +// 更新字典内容 +export function useUpdateWordlistContent() { + const queryClient = useQueryClient() + + return useMutation<Wordlist, Error, { id: number; content: string }>({ + mutationFn: ({ id, content }) => updateWordlistContent(id, content), + onMutate: () => { + toast.loading("正在保存...", { id: "update-wordlist-content" }) + }, + onSuccess: (data) => { + toast.dismiss("update-wordlist-content") + toast.success("字典保存成功") + queryClient.invalidateQueries({ queryKey: ["wordlists"] }) + queryClient.invalidateQueries({ queryKey: ["wordlist-content", data.id] }) + }, + onError: (error) => { + toast.dismiss("update-wordlist-content") + toast.error(`保存失败: ${error.message}`) + }, + }) +} diff --git a/frontend/hooks/use-workers.ts b/frontend/hooks/use-workers.ts new file mode 100644 index 00000000..d78fa818 --- /dev/null +++ b/frontend/hooks/use-workers.ts @@ -0,0 +1,155 @@ +/** + * Worker 节点管理 Hooks + */ + +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { workerService } from '@/services/worker.service' +import type { CreateWorkerRequest, UpdateWorkerRequest } from '@/types/worker.types' +import { toast } from 'sonner' + +// Query Keys +export const workerKeys = { + all: ['workers'] as const, + lists: () => [...workerKeys.all, 'list'] as const, + list: (page: number, pageSize: number) => [...workerKeys.lists(), { page, pageSize }] as const, + details: () => [...workerKeys.all, 'detail'] as const, + detail: (id: number) => [...workerKeys.details(), id] as const, +} + +/** + * 获取 Worker 列表 + */ +export function useWorkers(page = 1, pageSize = 10) { + return useQuery({ + queryKey: workerKeys.list(page, pageSize), + queryFn: () => workerService.getWorkers(page, pageSize), + }) +} + +/** + * 获取单个 Worker 详情 + */ +export function useWorker(id: number) { + return useQuery({ + queryKey: workerKeys.detail(id), + queryFn: () => workerService.getWorker(id), + enabled: id > 0, + }) +} + +/** + * 创建 Worker + */ +export function useCreateWorker() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (data: CreateWorkerRequest) => workerService.createWorker(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: workerKeys.lists() }) + toast.success('Worker 节点创建成功') + }, + onError: (error: Error) => { + toast.error(`创建失败: ${error.message}`) + }, + }) +} + +/** + * 更新 Worker + */ +export function useUpdateWorker() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: UpdateWorkerRequest }) => + workerService.updateWorker(id, data), + onSuccess: (_: unknown, { id }: { id: number; data: UpdateWorkerRequest }) => { + queryClient.invalidateQueries({ queryKey: workerKeys.lists() }) + queryClient.invalidateQueries({ queryKey: workerKeys.detail(id) }) + toast.success('Worker 节点更新成功') + }, + onError: (error: Error) => { + toast.error(`更新失败: ${error.message}`) + }, + }) +} + +/** + * 删除 Worker + */ +export function useDeleteWorker() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: number) => workerService.deleteWorker(id), + onSuccess: () => { + // 立即刷新活跃的列表查询 + queryClient.invalidateQueries({ + queryKey: workerKeys.lists(), + refetchType: 'active', + }) + toast.success('Worker 节点已删除') + }, + onError: (error: Error) => { + toast.error(`删除失败: ${error.message}`) + }, + }) +} + +/** + * 部署 Worker + */ +export function useDeployWorker() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: number) => workerService.deployWorker(id), + onSuccess: (_: unknown, id: number) => { + queryClient.invalidateQueries({ queryKey: workerKeys.detail(id) }) + queryClient.invalidateQueries({ queryKey: workerKeys.lists() }) + toast.success('部署已启动') + }, + onError: (error: Error) => { + toast.error(`部署失败: ${error.message}`) + }, + }) +} + +/** + * 重启 Worker + */ +export function useRestartWorker() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: number) => workerService.restartWorker(id), + onSuccess: (_: unknown, id: number) => { + queryClient.invalidateQueries({ queryKey: workerKeys.detail(id) }) + queryClient.invalidateQueries({ queryKey: workerKeys.lists() }) + toast.success('Worker 正在重启') + }, + onError: (error: Error) => { + toast.error(`重启失败: ${error.message}`) + }, + }) +} + +/** + * 停止 Worker + */ +export function useStopWorker() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: number) => workerService.stopWorker(id), + onSuccess: (_: unknown, id: number) => { + queryClient.invalidateQueries({ queryKey: workerKeys.detail(id) }) + queryClient.invalidateQueries({ queryKey: workerKeys.lists() }) + toast.success('Worker 已停止') + }, + onError: (error: Error) => { + toast.error(`停止失败: ${error.message}`) + }, + }) +} diff --git a/frontend/lib/api-client.ts b/frontend/lib/api-client.ts new file mode 100644 index 00000000..cd2a3457 --- /dev/null +++ b/frontend/lib/api-client.ts @@ -0,0 +1,276 @@ +/** + * API 客户端配置文件 + * + * 核心功能: + * 1. 统一的 HTTP 请求封装 + * 2. 统一错误处理 + * 3. 请求/响应日志记录 + * + * 命名规范说明: + * - 前端(TypeScript/React):驼峰命名 camelCase + * 例如:pageSize, createdAt, organizationId + * + * - 后端(Django/Python):下划线命名 snake_case(模型字段) + * 例如:page_size, created_at, organization_id + * + * - API JSON 格式:驼峰命名 camelCase(已由后端自动转换) + * 例如:pageSize, createdAt, organizationId + * + * 命名转换机制: + * ══════════════════════════════════════════════════════════════════════ + * 【后端处理】Django REST Framework + djangorestframework-camel-case + * ══════════════════════════════════════════════════════════════════════ + * + * 1. 前端发送请求(camelCase): + * { pageSize: 10, sortBy: "name" } + * + * 2. Django 接收并自动转换为 snake_case: + * { page_size: 10, sort_by: "name" } + * + * 3. Django 处理后端逻辑(使用 snake_case 模型字段) + * + * 4. Django 返回数据时自动转换为 camelCase: + * { pageSize: 10, createdAt: "2024-01-01" } + * + * 5. 前端直接使用(camelCase): + * response.data.pageSize // [OK] 直接使用 + * + * [NOTE] 关键点:命名转换由后端统一处理,前端无需转换 + */ + +import axios, { AxiosRequestConfig } from 'axios'; + +/** + * 创建 axios 实例 + * 配置基础 URL、超时时间和默认请求头 + */ +const apiClient = axios.create({ + baseURL: '/api', // API 基础路径 + timeout: 30000, // 30秒超时 + headers: { + 'Content-Type': 'application/json', + }, +}); + +/** + * 请求拦截器:处理请求前的准备工作 + * + * 工作流程: + * 1. 确保 URL 格式正确(Django 要求末尾斜杠) + * 2. 记录请求日志(开发调试用) + * + * 注意事项: + * - 命名转换由后端处理,前端无需转换 + * - 前端直接使用 camelCase 命名即可 + */ +apiClient.interceptors.request.use( + (config) => { + // 只在开发环境输出调试日志 + if (process.env.NODE_ENV === 'development') { + console.log('[REQUEST] API Request:', { + method: config.method?.toUpperCase(), + url: config.url, + baseURL: config.baseURL, + fullURL: `${config.baseURL}${config.url}`, + data: config.data, + params: config.params + }); + } + + return config; + }, + (error) => { + if (process.env.NODE_ENV === 'development') { + console.error('[ERROR] Request Error:', error); + } + return Promise.reject(error); + } +); + +/** + * 响应拦截器:处理响应数据 + * + * 工作流程: + * 1. 记录响应日志(开发调试用) + * 2. 返回响应数据(后端已转换为 camelCase) + * + * 注意事项: + * - 后端已自动将 snake_case 转换为 camelCase + * - 前端直接使用即可,无需额外转换 + */ +apiClient.interceptors.response.use( + (response) => { + // 只在开发环境输出调试日志 + if (process.env.NODE_ENV === 'development') { + console.log('[RESPONSE] API Response:', { + status: response.status, + statusText: response.statusText, + url: response.config.url, + data: response.data + }); + } + + return response; + }, + (error) => { + // 只在开发环境输出错误日志 + if (process.env.NODE_ENV === 'development') { + // 检查是否是 Axios 错误 + if (axios.isAxiosError(error)) { + console.error('[ERROR] API Error:', { + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method, + data: error.response?.data, + message: error.message, + code: error.code + }); + } else if (error instanceof Error) { + // 普通 Error 对象 + console.error('[ERROR] API Error:', error.message, error.stack); + } else { + // 未知错误类型 + console.error('[ERROR] API Error: Unknown error', String(error)); + } + } + + return Promise.reject(error); + } +); + +// 导出默认的 axios 实例(一般不直接使用) +export default apiClient; + +/** + * 导出常用的 HTTP 方法 + * + * 使用示例: + * + * 1. GET 请求: + * api.get('/organizations', { + * params: { pageSize: 10, sortBy: 'name' } // 使用 camelCase + * }) + * 后端接收:page_size=10&sort_by=name(自动转换) + * + * 2. POST 请求: + * api.post('/organizations/create', { + * organizationName: 'test', // 使用 camelCase + * createdAt: '2024-01-01' + * }) + * 后端接收:organization_name, created_at(自动转换) + * + * 3. 响应数据(已经是 camelCase): + * const response = await api.get('/organizations') + * response.data.pageSize // [OK] 直接使用 camelCase + * response.data.createdAt // [OK] 直接使用 camelCase + * + * 类型参数: + * - T: 响应数据的类型(可选) + * - config: axios 配置对象(可选) + */ +export const api = { + /** + * GET 请求 + * @param url - 请求路径(相对于 baseURL) + * @param config - axios 配置,建议使用 params 传递查询参数 + * @returns Promise<AxiosResponse<T>> + */ + get: <T = unknown>(url: string, config?: AxiosRequestConfig) => apiClient.get<T>(url, config), + + /** + * POST 请求 + * @param url - 请求路径(相对于 baseURL) + * @param data - 请求体数据(会自动转换为 snake_case) + * @param config - axios 配置(可选) + * @returns Promise<AxiosResponse<T>> + */ + post: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) => apiClient.post<T>(url, data, config), + + /** + * PUT 请求 + * @param url - 请求路径(相对于 baseURL) + * @param data - 请求体数据(会自动转换为 snake_case) + * @param config - axios 配置(可选) + * @returns Promise<AxiosResponse<T>> + */ + put: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) => apiClient.put<T>(url, data, config), + + /** + * PATCH 请求(部分更新) + * @param url - 请求路径(相对于 baseURL) + * @param data - 请求体数据(会自动转换为 snake_case) + * @param config - axios 配置(可选) + * @returns Promise<AxiosResponse<T>> + */ + patch: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) => apiClient.patch<T>(url, data, config), + + /** + * DELETE 请求 + * @param url - 请求路径(相对于 baseURL) + * @param config - axios 配置(可选) + * @returns Promise<AxiosResponse<T>> + */ + delete: <T = unknown>(url: string, config?: AxiosRequestConfig) => apiClient.delete<T>(url, config), +}; + +/** + * 错误处理工具函数 + * + * 功能:从错误对象中提取用户友好的错误消息 + * + * 错误优先级: + * 1. 请求取消 + * 2. 请求超时 + * 3. 后端返回的错误消息 + * 4. axios 错误消息 + * 5. 未知错误 + * + * 使用示例: + * try { + * await api.get('/organizations') + * } catch (error) { + * const message = getErrorMessage(error) + * toast.error(message) + * } + * + * @param error - 错误对象(可以是任意类型) + * @returns 用户友好的错误消息字符串 + */ +export const getErrorMessage = (error: unknown): string => { + // 请求被取消(用户主动取消或组件卸载) + if (axios.isCancel(error)) { + return '请求已被取消'; + } + + // 类型守卫:检查是否为错误对象 + const err = error as { + code?: string; + response?: { data?: { message?: string; error?: string; detail?: string } }; + message?: string + } + + // 请求超时(超过 30 秒) + if (err.code === 'ECONNABORTED') { + return '请求超时,请稍后重试'; + } + + // 后端返回的错误消息(支持多种格式) + if (err.response?.data?.error) { + return err.response.data.error; + } + if (err.response?.data?.message) { + return err.response.data.message; + } + if (err.response?.data?.detail) { + return err.response.data.detail; + } + + // axios 自身的错误消息 + if (err.message) { + return err.message; + } + + // 兜底错误消息 + return '发生未知错误'; +}; diff --git a/frontend/lib/domain-validator.ts b/frontend/lib/domain-validator.ts new file mode 100644 index 00000000..b4e03d2c --- /dev/null +++ b/frontend/lib/domain-validator.ts @@ -0,0 +1,214 @@ +import validator from 'validator' +import { parse as parseDomain } from 'tldts' + +/** + * 域名验证工具类 + * 使用 validator.js 进行可靠的域名验证 + */ + +export interface DomainValidationResult { + isValid: boolean + error?: string +} + +export class DomainValidator { + /** + * 验证域名格式(如 example.com) + * @param domain - 要验证的域名字符串 + * @returns 验证结果 + */ + static validateDomain(domain: string): DomainValidationResult { + // 1. 检查是否为空 + if (!domain || domain.trim().length === 0) { + return { + isValid: false, + error: '域名不能为空' + } + } + + const trimmedDomain = domain.trim() + + // 2. 检查是否包含空格 + if (trimmedDomain.includes(' ')) { + return { + isValid: false, + error: '域名不能包含空格' + } + } + + // 3. 检查长度(使用 validator 包) + if (!validator.isLength(trimmedDomain, { min: 1, max: 253 })) { + return { + isValid: false, + error: '域名长度不能超过 253 个字符' + } + } + + // 4. 使用 tldts 做域名语义校验(优先) + const info = parseDomain(trimmedDomain) + if (!info.domain || info.isIp === true) { + return { + isValid: false, + error: '域名格式无效' + } + } + + // 5. 使用 validator.js 的 isFQDN 兜底,确保严格性 + if (!validator.isFQDN(trimmedDomain, { + require_tld: true, + allow_underscores: false, + allow_trailing_dot: false, + allow_numeric_tld: false, + allow_wildcard: false, + })) { + return { + isValid: false, + error: '域名格式无效' + } + } + + return { isValid: true } + } + + /** + * 验证子域名格式(如 www.example.com, api.test.org) + * @param subdomain - 要验证的子域名字符串 + * @returns 验证结果 + */ + static validateSubdomain(subdomain: string): DomainValidationResult { + // 先进行基本域名验证 + const basicValidation = this.validateDomain(subdomain) + if (!basicValidation.isValid) { + return basicValidation + } + + // 子域名必须至少包含 3 个部分(如 www.example.com) + const labels = subdomain.trim().split('.') + if (labels.length < 3) { + return { + isValid: false, + error: '子域名必须至少包含 3 个部分(如 www.example.com)' + } + } + + return { + isValid: true + } + } + + /** + * 批量验证域名列表 + * @param domains - 域名字符串数组 + * @returns 验证结果数组 + */ + static validateDomainBatch(domains: string[]): Array<DomainValidationResult & { index: number; originalDomain: string }> { + return domains.map((domain, index) => ({ + ...this.validateDomain(domain), + index, + originalDomain: domain + })) + } + + /** + * 批量验证子域名列表 + * @param subdomains - 子域名字符串数组 + * @returns 验证结果数组 + */ + static validateSubdomainBatch(subdomains: string[]): Array<DomainValidationResult & { index: number; originalDomain: string }> { + return subdomains.map((subdomain, index) => ({ + ...this.validateSubdomain(subdomain), + index, + originalDomain: subdomain + })) + } + + /** + * 规范化域名(转换为小写) + */ + static normalize(domain: string): string | null { + const result = this.validateDomain(domain) + if (!result.isValid) { + return null + } + return domain.trim().toLowerCase() + } + + /** + * 从子域名中提取根域名(使用 PSL - Public Suffix List) + * @param subdomain - 子域名(如 www.example.com, blog.github.io) + * @returns 根域名(如 example.com, blog.github.io)或 null + * + * 示例: + * - www.example.com → example.com + * - api.test.example.com → example.com + * - blog.github.io → blog.github.io (正确处理公共后缀) + * - www.bbc.co.uk → bbc.co.uk (正确处理多级 TLD) + */ + static extractRootDomain(subdomain: string): string | null { + const trimmed = subdomain.trim().toLowerCase() + if (!trimmed) return null + + // 使用 tldts 解析域名 + const parsed = parseDomain(trimmed) + if (!parsed.domain) { + return null + } + return parsed.domain + } + + /** + * 将子域名列表按根域名分组 + * @param subdomains - 子域名列表 + * @returns { grouped: Map<根域名, 子域名[]>, invalid: 无效的子域名[] } + */ + static groupSubdomainsByRootDomain(subdomains: string[]): { + grouped: Map<string, string[]> + invalid: string[] + } { + const grouped = new Map<string, string[]>() + const invalid: string[] = [] + + for (const subdomain of subdomains) { + const rootDomain = this.extractRootDomain(subdomain) + + if (!rootDomain) { + invalid.push(subdomain) + continue + } + + if (!grouped.has(rootDomain)) { + grouped.set(rootDomain, []) + } + + grouped.get(rootDomain)!.push(subdomain) + } + + return { grouped, invalid } + } + + /** + * 检查子域名是否属于指定的根域名 + * @param subdomain - 子域名(如 www.example.com, api.example.com) + * @param rootDomain - 根域名(如 example.com) + * @returns 是否属于该根域名 + * + * 示例: + * - isSubdomainOf('www.example.com', 'example.com') → true + * - isSubdomainOf('api.test.example.com', 'example.com') → true + * - isSubdomainOf('www.test.com', 'example.com') → false + */ + static isSubdomainOf(subdomain: string, rootDomain: string): boolean { + const trimmedSubdomain = subdomain.trim().toLowerCase() + const trimmedRootDomain = rootDomain.trim().toLowerCase() + + if (!trimmedSubdomain || !trimmedRootDomain) { + return false + } + + // 提取子域名的根域名 + const extractedRoot = this.extractRootDomain(trimmedSubdomain) + + // 比较提取的根域名与目标根域名 + return extractedRoot === trimmedRootDomain + } +} diff --git a/frontend/lib/endpoint-validator.ts b/frontend/lib/endpoint-validator.ts new file mode 100644 index 00000000..48f514a4 --- /dev/null +++ b/frontend/lib/endpoint-validator.ts @@ -0,0 +1,223 @@ +import validator from 'validator' +import { isIP } from 'is-ip' + +/** + * Endpoint 验证工具类 + * 提供严格的 URL 格式验证 + * 使用 validator.js 进行可靠的 URL 验证 + */ + +export interface EndpointValidationResult { + isValid: boolean + error?: string + url?: URL +} + +export class EndpointValidator { + /** + * 验证 Endpoint 是否为有效的 HTTP/HTTPS URL + * @param urlString - 要验证的 URL 字符串 + * @returns 验证结果 + */ + static validate(urlString: string): EndpointValidationResult { + // 1. 检查是否为空 + if (!urlString || urlString.trim().length === 0) { + return { + isValid: false, + error: 'Endpoint 不能为空' + } + } + + const trimmedUrl = urlString.trim() + + // 2. 检查是否包含空格 + if (trimmedUrl.includes(' ')) { + return { + isValid: false, + error: 'Endpoint 不能包含空格' + } + } + + // 3. 使用 validator.js 进行严格验证 + if (!validator.isURL(trimmedUrl, { + protocols: ['http', 'https'], + require_protocol: true, + require_valid_protocol: true, + require_host: true, + allow_underscores: false, + allow_trailing_dot: false, + allow_protocol_relative_urls: false, + })) { + return { + isValid: false, + error: 'Endpoint 格式无效,必须是有效的 HTTP/HTTPS URL' + } + } + + // 4. 尝试解析 URL(双重验证) + let parsedUrl: URL + try { + parsedUrl = new URL(trimmedUrl) + } catch (error) { + return { + isValid: false, + error: 'Endpoint 格式无效,无法解析' + } + } + + // 5. 验证协议 + if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { + return { + isValid: false, + error: '只支持 HTTP 和 HTTPS 协议' + } + } + + // 6. 验证主机名 + if (!parsedUrl.hostname || parsedUrl.hostname.length === 0) { + return { + isValid: false, + error: 'Endpoint 必须包含有效的主机名' + } + } + + // 7. 检查主机名格式(域名或 IP) + if (!this.isValidHostname(parsedUrl.hostname)) { + return { + isValid: false, + error: '主机名格式无效' + } + } + + // 8. 检查端口号(如果有) + if (parsedUrl.port && !this.isValidPort(parsedUrl.port)) { + return { + isValid: false, + error: '端口号无效(必须是 1-65535)' + } + } + + // 9. 检查路径(可选,但如果有必须有效) + if (parsedUrl.pathname && parsedUrl.pathname.includes('..')) { + return { + isValid: false, + error: 'Endpoint 路径不能包含 ".."' + } + } + + // 10. 检查是否包含危险字符 + if (this.containsDangerousCharacters(trimmedUrl)) { + return { + isValid: false, + error: 'Endpoint 包含不安全的字符' + } + } + + return { + isValid: true, + url: parsedUrl + } + } + + /** + * 批量验证 Endpoint 列表 + * @param urls - URL 字符串数组 + * @returns 验证结果数组 + */ + static validateBatch(urls: string[]): Array<EndpointValidationResult & { index: number; originalUrl: string }> { + return urls.map((url, index) => ({ + ...this.validate(url), + index, + originalUrl: url + })) + } + + /** + * 验证主机名是否有效(域名或 IP 地址) + */ + private static isValidHostname(hostname: string): boolean { + // 1) IP 校验(支持 IPv4/IPv6) + if (isIP(hostname)) { + return true + } + + // 2) 域名校验(使用 validator 的 FQDN 校验) + return validator.isFQDN(hostname, { + require_tld: true, + allow_underscores: false, + allow_trailing_dot: false, + allow_numeric_tld: false, + allow_wildcard: false, + }) + } + + /** + * 验证端口号是否有效 + */ + private static isValidPort(port: string): boolean { + const portNum = parseInt(port, 10) + return !isNaN(portNum) && portNum >= 1 && portNum <= 65535 + } + + /** + * 检查 URL 是否包含危险字符 + */ + private static containsDangerousCharacters(url: string): boolean { + // 检查是否包含控制字符 + const controlCharRegex = /[\x00-\x1F\x7F]/ + if (controlCharRegex.test(url)) { + return true + } + + // 检查是否包含 JavaScript 协议 + if (url.toLowerCase().includes('javascript:')) { + return true + } + + // 检查是否包含 data 协议 + if (url.toLowerCase().includes('data:')) { + return true + } + + return false + } + + /** + * 格式化 Endpoint(规范化) + */ + static normalize(urlString: string): string | null { + const result = this.validate(urlString) + if (!result.isValid || !result.url) { + return null + } + + // 返回规范化的 URL + return result.url.href + } + + /** + * 提取 Endpoint 的各个部分 + */ + static parse(urlString: string): { + protocol: string + hostname: string + port: string + pathname: string + search: string + hash: string + } | null { + const result = this.validate(urlString) + if (!result.isValid || !result.url) { + return null + } + + return { + protocol: result.url.protocol, + hostname: result.url.hostname, + port: result.url.port, + pathname: result.url.pathname, + search: result.url.search, + hash: result.url.hash + } + } +} diff --git a/frontend/lib/engine-config.ts b/frontend/lib/engine-config.ts new file mode 100644 index 00000000..596875b5 --- /dev/null +++ b/frontend/lib/engine-config.ts @@ -0,0 +1,80 @@ +import { + Globe, + Network, + Monitor, + FolderSearch, + Link, + ShieldAlert, + Shield, + Camera, + Search, + Cpu, +} from "lucide-react" +import type { LucideIcon } from "lucide-react" + +/** 统一的能力标签颜色(使用全局 CSS 变量) */ +const CAPABILITY_COLOR = "bg-primary/10 text-primary border-primary/20" + +/** + * 引擎能力配置(使用全局 CSS 颜色) + * 用于发起扫描、快速扫描等引擎选择界面 + */ +export const CAPABILITY_CONFIG: Record<string, { + label: string + color: string + icon: LucideIcon +}> = { + subdomain_discovery: { label: "子域名发现", color: CAPABILITY_COLOR, icon: Globe }, + port_scan: { label: "端口扫描", color: CAPABILITY_COLOR, icon: Network }, + site_scan: { label: "站点扫描", color: CAPABILITY_COLOR, icon: Monitor }, + directory_scan: { label: "目录扫描", color: CAPABILITY_COLOR, icon: FolderSearch }, + url_fetch: { label: "URL 抓取", color: CAPABILITY_COLOR, icon: Link }, + vuln_scan: { label: "漏洞扫描", color: CAPABILITY_COLOR, icon: ShieldAlert }, + waf_detection: { label: "WAF 检测", color: CAPABILITY_COLOR, icon: Shield }, + screenshot: { label: "截图", color: CAPABILITY_COLOR, icon: Camera }, + osint: { label: "OSINT", color: CAPABILITY_COLOR, icon: Search }, +} + +/** + * 根据引擎能力获取主图标 + * 按优先级返回第一个匹配的能力图标 + */ +export function getEngineIcon(capabilities: string[]): LucideIcon { + const priorityOrder = [ + 'vuln_scan', + 'subdomain_discovery', + 'port_scan', + 'site_scan', + 'directory_scan', + 'url_fetch', + 'waf_detection', + 'screenshot', + 'osint' + ] + + for (const key of priorityOrder) { + if (capabilities.includes(key)) { + return CAPABILITY_CONFIG[key].icon + } + } + return Cpu +} + +/** + * 解析引擎配置以获取能力列表 + */ +export function parseEngineCapabilities(configuration: string): string[] { + if (!configuration) return [] + + try { + const capabilities: string[] = [] + Object.keys(CAPABILITY_CONFIG).forEach((key) => { + if (configuration.includes(key)) { + capabilities.push(key) + } + }) + return capabilities + } catch { + return [] + } +} diff --git a/frontend/lib/env.ts b/frontend/lib/env.ts new file mode 100644 index 00000000..666fa749 --- /dev/null +++ b/frontend/lib/env.ts @@ -0,0 +1,37 @@ +/** + * 环境变量与运行时配置工具 + */ + +const DEFAULT_DEV_BACKEND_URL = 'http://localhost:8888' + +const stripTrailingSlash = (url: string) => url.replace(/\/+$/, '') + +/** + * 获取后端基础地址(用于绕过 Next.js 代理,保证 SSE 等长连接可用) + */ +export function getBackendBaseUrl(): string { + const envUrl = process.env.NEXT_PUBLIC_BACKEND_URL?.trim() + if (envUrl) { + return stripTrailingSlash(envUrl) + } + + if (typeof window !== 'undefined') { + const origin = window.location.origin + // 本地开发时,默认后端运行在 8888 端口 + if (window.location.hostname === 'localhost' && window.location.port === '3000') { + return stripTrailingSlash(DEFAULT_DEV_BACKEND_URL) + } + return stripTrailingSlash(origin) + } + + return stripTrailingSlash(DEFAULT_DEV_BACKEND_URL) +} + +/** + * 拼接后端 API 地址(会自动处理多余斜杠) + */ +export function buildBackendUrl(path: string): string { + const base = getBackendBaseUrl() + const normalizedPath = path.startsWith('/') ? path : `/${path}` + return `${base}${normalizedPath}` +} diff --git a/frontend/lib/error-handler.ts b/frontend/lib/error-handler.ts new file mode 100644 index 00000000..bcd689d2 --- /dev/null +++ b/frontend/lib/error-handler.ts @@ -0,0 +1,130 @@ +/** + * 统一的错误处理工具 + * + * 根据项目规则24: + * - 成功提示:前端自己构造 + * - 错误提示:前端自己构造,提供更具体的错误原因 + * - 控制台日志:打印完整的后端响应体 + */ + +import { toast } from "sonner" + +/** + * API 错误信息接口 + */ +export interface ApiError { + response?: { + data?: unknown + } + message?: string +} + +/** + * 处理 mutation 错误(通用) + * @param error 错误对象 + * @param userMessage 前端自定义的用户友好错误消息 + * @param toastId 可选的 toast ID(用于关闭加载提示) + */ +export function handleMutationError( + error: unknown, + userMessage: string, + toastId?: string +) { + // 关闭加载提示(如果有) + if (toastId) { + toast.dismiss(toastId) + } + + // 控制台打印详细错误信息 + console.error('操作失败:', error) + console.error('后端响应:', (error as ApiError)?.response?.data || error) + + // 显示前端自定义的用户友好错误消息 + toast.error(userMessage) +} + +/** + * 处理 query 错误(通用) + * @param error 错误对象 + * @param userMessage 前端自定义的用户友好错误消息 + */ +export function handleQueryError(error: unknown, userMessage: string) { + // 控制台打印详细错误信息 + console.error('查询失败:', error) + console.error('后端响应:', (error as ApiError)?.response?.data || error) + + // 显示前端自定义的用户友好错误消息 + toast.error(userMessage) +} + +/** + * 处理成功响应(通用) + * @param response 后端响应 + * @param successMessage 前端自定义的成功消息 + * @param toastId 可选的 toast ID(用于关闭加载提示) + */ +export function handleSuccess( + response: unknown, + successMessage: string, + toastId?: string +) { + // 关闭加载提示(如果有) + if (toastId) { + toast.dismiss(toastId) + } + + // 控制台打印成功信息 + console.log('操作成功') + console.log('后端响应:', response) + + // 显示前端自定义的成功消息 + toast.success(successMessage) +} + +/** + * 处理警告响应(部分成功场景) + * @param response 后端响应 + * @param warningMessage 前端自定义的警告消息 + * @param toastId 可选的 toast ID(用于关闭加载提示) + */ +export function handleWarning( + response: unknown, + warningMessage: string, + toastId?: string +) { + // 关闭加载提示(如果有) + if (toastId) { + toast.dismiss(toastId) + } + + // 控制台打印信息(仅在开发环境) + if (process.env.NODE_ENV === 'development') { + console.log('操作部分成功') + console.log('后端响应:', response) + } + + // 显示前端自定义的警告消息 + toast.warning(warningMessage) +} + +/** + * 检查响应是否成功 + * @param response API 响应 + * @returns 是否成功 + */ +export function isSuccessResponse(response: unknown): boolean { + return (response as { state?: string })?.state === 'success' +} + +/** + * 从响应中提取数据 + * @param response API 响应 + * @param defaultValue 默认值 + * @returns 响应数据 + */ +export function extractData<T>(response: unknown, defaultValue: T): T { + if (isSuccessResponse(response) && (response as { data?: T }).data) { + return (response as { data: T }).data + } + return defaultValue +} diff --git a/frontend/lib/target-validator.ts b/frontend/lib/target-validator.ts new file mode 100644 index 00000000..be25c7bf --- /dev/null +++ b/frontend/lib/target-validator.ts @@ -0,0 +1,217 @@ +import validator from 'validator' +import { parse as parseDomain } from 'tldts' + +/** + * 目标验证工具类 + * 支持验证三种目标类型:域名、IP、CIDR + */ + +export interface TargetValidationResult { + isValid: boolean + error?: string + type?: 'domain' | 'ip' | 'cidr' +} + +export class TargetValidator { + /** + * 验证域名格式(如 example.com) + */ + static validateDomain(domain: string): TargetValidationResult { + if (!domain || domain.trim().length === 0) { + return { + isValid: false, + error: '目标不能为空' + } + } + + const trimmedDomain = domain.trim() + + if (trimmedDomain.includes(' ')) { + return { + isValid: false, + error: '目标不能包含空格' + } + } + + if (!validator.isLength(trimmedDomain, { min: 1, max: 253 })) { + return { + isValid: false, + error: '目标长度不能超过 253 个字符' + } + } + + const info = parseDomain(trimmedDomain) + if (!info.domain || info.isIp === true) { + return { + isValid: false, + error: '域名格式无效' + } + } + + if (!validator.isFQDN(trimmedDomain, { + require_tld: true, + allow_underscores: false, + allow_trailing_dot: false, + allow_numeric_tld: false, + allow_wildcard: false, + })) { + return { + isValid: false, + error: '域名格式无效' + } + } + + return { isValid: true, type: 'domain' } + } + + /** + * 验证 IPv4 地址(如 192.168.1.1) + */ + static validateIPv4(ip: string): TargetValidationResult { + if (!ip || ip.trim().length === 0) { + return { + isValid: false, + error: '目标不能为空' + } + } + + const trimmedIP = ip.trim() + + if (!validator.isIP(trimmedIP, 4)) { + return { + isValid: false, + error: 'IPv4 地址格式无效' + } + } + + return { isValid: true, type: 'ip' } + } + + /** + * 验证 IPv6 地址(如 2001:db8::1) + */ + static validateIPv6(ip: string): TargetValidationResult { + if (!ip || ip.trim().length === 0) { + return { + isValid: false, + error: '目标不能为空' + } + } + + const trimmedIP = ip.trim() + + if (!validator.isIP(trimmedIP, 6)) { + return { + isValid: false, + error: 'IPv6 地址格式无效' + } + } + + return { isValid: true, type: 'ip' } + } + + /** + * 验证 IP 地址(IPv4 或 IPv6) + */ + static validateIP(ip: string): TargetValidationResult { + if (!ip || ip.trim().length === 0) { + return { + isValid: false, + error: '目标不能为空' + } + } + + const trimmedIP = ip.trim() + + if (!validator.isIP(trimmedIP)) { + return { + isValid: false, + error: 'IP 地址格式无效' + } + } + + return { isValid: true, type: 'ip' } + } + + /** + * 验证 CIDR 网段(如 10.0.0.0/8, 192.168.0.0/16) + */ + static validateCIDR(cidr: string): TargetValidationResult { + if (!cidr || cidr.trim().length === 0) { + return { + isValid: false, + error: '目标不能为空' + } + } + + const trimmedCIDR = cidr.trim() + + // 检查是否包含 / + if (!trimmedCIDR.includes('/')) { + return { + isValid: false, + error: 'CIDR 格式无效,应包含 /' + } + } + + const [ip, prefix] = trimmedCIDR.split('/') + + // 验证 IP 部分 + if (!validator.isIP(ip.trim())) { + return { + isValid: false, + error: 'CIDR 中的 IP 地址格式无效' + } + } + + // 验证前缀长度 + const prefixNum = parseInt(prefix, 10) + if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 32) { + return { + isValid: false, + error: 'CIDR 前缀长度必须在 0-32 之间' + } + } + + return { isValid: true, type: 'cidr' } + } + + /** + * 自动检测目标类型并验证 + * 支持:域名、IPv4、IPv6、CIDR + */ + static validateTarget(target: string): TargetValidationResult { + if (!target || target.trim().length === 0) { + return { + isValid: false, + error: '目标不能为空' + } + } + + const trimmedTarget = target.trim() + + // 1. 先尝试 CIDR 验证(包含 /) + if (trimmedTarget.includes('/')) { + return this.validateCIDR(trimmedTarget) + } + + // 2. 尝试 IP 验证 + if (validator.isIP(trimmedTarget)) { + return this.validateIP(trimmedTarget) + } + + // 3. 尝试域名验证 + return this.validateDomain(trimmedTarget) + } + + /** + * 批量验证目标列表 + */ + static validateTargetBatch(targets: string[]): Array<TargetValidationResult & { index: number; originalTarget: string }> { + return targets.map((target, index) => ({ + ...this.validateTarget(target), + index, + originalTarget: target + })) + } +} diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts new file mode 100644 index 00000000..1a3fc98c --- /dev/null +++ b/frontend/lib/utils.ts @@ -0,0 +1,41 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +/** + * 格式化日期时间 + * @param date 日期字符串或 Date 对象 + * @returns 格式化后的日期时间字符串 + */ +export function formatDate(date: string | Date | null | undefined): string { + if (!date) return "-" + + try { + const d = typeof date === "string" ? new Date(date) : date + return new Intl.DateTimeFormat("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }).format(d) + } catch (error) { + return "-" + } +} + +export function formatBytes(bytes: number, decimals = 2): string { + if (!Number.isFinite(bytes) || bytes < 0) return "-" + if (bytes === 0) return "0 B" + const k = 1024 + const dm = decimals < 0 ? 0 : decimals + const sizes = ["B", "KB", "MB", "GB", "TB", "PB"] + const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1) + const val = bytes / Math.pow(k, i) + return `${parseFloat(val.toFixed(dm))} ${sizes[i]}` +} diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 00000000..0ab8863e --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,27 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + // Docker 部署使用 standalone 模式 + output: 'standalone', + // 禁用 Next.js 自动添加/移除末尾斜杠的行为 + // 让我们手动控制 URL 格式 + skipTrailingSlashRedirect: true, + // 生产构建时不因为 ESLint 报错而中断(保留开发环境的 lint) + eslint: { + ignoreDuringBuilds: true, + }, + + async rewrites() { + // Docker 环境使用 server 服务名,本地开发使用 localhost + const apiHost = process.env.API_HOST || 'localhost'; + return [ + // 只匹配带斜杠的 API 路径 + { + source: '/api/:path*/', + destination: `http://${apiHost}:8888/api/:path*/`, + }, + ]; + }, +}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..c83d43c1 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,9217 @@ +{ + "name": "xingrin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "xingrin", + "version": "0.1.0", + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@hookform/resolvers": "^5.2.2", + "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@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-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-use-controllable-state": "^1.2.2", + "@tabler/icons-react": "^3.35.0", + "@tanstack/react-query": "^5.90.2", + "@tanstack/react-table": "^8.21.3", + "@xterm/addon-fit": "^0.10.0", + "@xterm/addon-web-links": "^0.11.0", + "@xterm/xterm": "^5.5.0", + "axios": "^1.12.2", + "camelcase-keys": "^10.0.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "cron-parser": "^5.4.0", + "cronstrue": "^3.9.0", + "date-fns": "^4.1.0", + "geist": "^1.5.1", + "is-ip": "^5.0.1", + "js-yaml": "^4.1.0", + "lottie-react": "^2.4.1", + "lucide-react": "^0.544.0", + "next": "15.5.7", + "next-themes": "^0.4.6", + "nextjs-toploader": "^3.9.17", + "psl": "^1.15.0", + "react": "19.1.2", + "react-day-picker": "^9.11.2", + "react-dom": "19.1.2", + "react-dropzone": "^14.3.8", + "react-hook-form": "^7.65.0", + "recharts": "2.15.4", + "snakecase-keys": "^9.0.2", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "tldts": "^6.1.86", + "validator": "^13.15.15", + "vaul": "^1.1.2", + "zod": "^4.1.12" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@tanstack/react-query-devtools": "^5.90.2", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.19.19", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/validator": "^13.15.3", + "eslint": "^9", + "eslint-config-next": "15.5.7", + "msw": "^2.11.6", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", + "typescript": "^5" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/@date-fns/tz": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", + "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", + "license": "MIT" + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", + "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "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.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "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.0", + "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/js": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", + "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.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "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/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "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/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.5.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", + "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.19", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.19.tgz", + "integrity": "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz", + "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.1", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz", + "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz", + "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@monaco-editor/loader": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.6.1.tgz", + "integrity": "sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", + "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.7.tgz", + "integrity": "sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "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-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-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@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-is-hydrated": "0.1.0", + "@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-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "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-presence": "1.1.5", + "@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-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-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-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-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "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-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-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "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-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", + "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-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-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@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-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "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-direction": "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", + "@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-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-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "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-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-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-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@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-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "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-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@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-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-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "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/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", + "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "dev": true, + "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/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tabler/icons": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.35.0.tgz", + "integrity": "sha512-yYXe+gJ56xlZFiXwV9zVoe3FWCGuZ/D7/G4ZIlDtGxSx5CGQK110wrnT29gUj52kEZoxqF7oURTk97GQxELOFQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.35.0.tgz", + "integrity": "sha512-XG7t2DYf3DyHT5jxFNp5xyLVbL4hMJYJhiSdHADzAjLRYfL7AnjlRfiHDHeXxkb2N103rEIvTsBRazxXtAUz2g==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.35.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", + "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.1", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", + "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.5.1" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", + "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", + "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", + "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", + "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", + "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", + "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", + "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", + "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", + "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", + "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.5", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", + "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", + "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.14.tgz", + "integrity": "sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", + "postcss": "^8.4.41", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.5.tgz", + "integrity": "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.90.1.tgz", + "integrity": "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.5.tgz", + "integrity": "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.90.2.tgz", + "integrity": "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.90.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.90.2", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "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/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", + "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", + "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz", + "integrity": "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/type-utils": "8.45.0", + "@typescript-eslint/utils": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0", + "graphemer": "^1.4.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.45.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.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz", + "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.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.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.45.0.tgz", + "integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.45.0", + "@typescript-eslint/types": "^8.45.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.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz", + "integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.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.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz", + "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==", + "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.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz", + "integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.0", + "@typescript-eslint/utils": "8.45.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.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz", + "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==", + "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.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz", + "integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.45.0", + "@typescript-eslint/tsconfig-utils": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.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": ">=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/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "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/@typescript-eslint/typescript-estree/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==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/utils": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz", + "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.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.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz", + "integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.45.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/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/addon-web-links": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz", + "integrity": "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, + "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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-10.0.0.tgz", + "integrity": "sha512-dzb1nrmD6llsF6eMZWSpQjVfe1FX4WOkR7HPdX1sMxM5u+1MlnbXodNJ/E6NRArYgMtcwMhiqqemGp/QJV/vrw==", + "license": "MIT", + "dependencies": { + "camelcase": "^8.0.0", + "map-obj": "5.0.2", + "quick-lru": "^7.1.0", + "type-fest": "^4.41.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001747", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz", + "integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==", + "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/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone-regexp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", + "integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==", + "license": "MIT", + "dependencies": { + "is-regexp": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cron-parser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.4.0.tgz", + "integrity": "sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA==", + "license": "MIT", + "dependencies": { + "luxon": "^3.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cronstrue": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-3.9.0.tgz", + "integrity": "sha512-T3S35zmD0Ai2B4ko6+mEM+k9C6tipe2nB9RLiGT6QL2Wn0Vsn2cCZAC8Oeuf4CaE00GZWVdpYitbpWCNlIWqdA==", + "license": "MIT", + "bin": { + "cronstrue": "bin/cli.js" + } + }, + "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/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "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/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, + "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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dompurify": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", + "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.36.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "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-config-next": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.7.tgz", + "integrity": "sha512-nU/TRGHHeG81NeLW5DeQT5t6BDUqbpsNQTvef1ld/tqHT+/zTx60/TIhKnmPISTTe++DVo+DLxDmk4rnwHaZVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.5.7", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/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/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "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/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/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/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": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "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-equals": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz", + "integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "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.4" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, + "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==", + "dev": true, + "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/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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/function-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz", + "integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/geist": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/geist/-/geist-1.5.1.tgz", + "integrity": "sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==", + "license": "SIL OPEN FONT LICENSE", + "peerDependencies": { + "next": ">=13.2.0" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, + "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/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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/ip-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-ip": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.1.tgz", + "integrity": "sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==", + "license": "MIT", + "dependencies": { + "ip-regex": "^5.0.0", + "super-regex": "^0.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "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/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lottie-react": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lottie-react/-/lottie-react-2.4.1.tgz", + "integrity": "sha512-LQrH7jlkigIIv++wIyrOYFLHSKQpEY4zehPicL9bQsrt1rnoKRYCYgpCUe5maqylNtacy58/sQDZTkwMcTRxZw==", + "license": "MIT", + "dependencies": { + "lottie-web": "^5.10.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lottie-web": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.13.0.tgz", + "integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==", + "license": "MIT" + }, + "node_modules/lucide-react": { + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", + "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/map-obj": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.2.tgz", + "integrity": "sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/monaco-editor": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", + "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.1.7", + "marked": "14.0.0" + } + }, + "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/msw": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.11.6.tgz", + "integrity": "sha512-MCYMykvmiYScyUm7I6y0VCxpNq1rgd5v7kG8ks5dKtvmxRUUPjribX6mUoUNBbM5/3PhUyoelEWiKXGOz84c+w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.40.0", + "@open-draft/deferred-promise": "^2.2.0", + "@types/statuses": "^2.0.4", + "cookie": "^1.0.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.7.0", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.0", + "type-fest": "^4.26.1", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.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/napi-postinstall": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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/next": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.7", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/nextjs-toploader": { + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/nextjs-toploader/-/nextjs-toploader-3.9.17.tgz", + "integrity": "sha512-9OF0KSSLtoSAuNg2LZ3aTl4hR9mBDj5L9s9DZiFCbMlXehyICGjkIz5dVGzuATU2bheJZoBdFgq9w07AKSuQQw==", + "license": "MIT", + "dependencies": { + "nprogress": "^0.2.0", + "prop-types": "^15.8.1" + }, + "funding": { + "url": "https://buymeacoffee.com/thesgj" + }, + "peerDependencies": { + "next": ">= 6.0.0", + "react": ">= 16.0.0", + "react-dom": ">= 16.0.0" + } + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "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-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "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": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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/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/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "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==", + "dev": true, + "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/quick-lru": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.2.0.tgz", + "integrity": "sha512-fG4L8TlD1CacJiGMGPxM1/K8l/GaKL2eFQZ6DWAjxZYxSf07DkumbC/Mhh+u/NHvxkfQVL25By0pxBS8QE9ZrQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.2.tgz", + "integrity": "sha512-MdWVitvLbQULD+4DP8GYjZUrepGW7d+GQkNVqJEzNxE+e9WIa4egVFE/RDfVb1u9u/Jw7dNMmPB4IqxzbFYJ0w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "9.11.2", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.2.tgz", + "integrity": "sha512-TD/xMUGg2oiKX8jUR21MST5pj+7Y36097YtnDHQFlIcZOu3mbLLw2B2JqEByEGrR3HHveWYnKlyls6WqJgohAg==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0", + "date-fns-jalali": "^4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.2.tgz", + "integrity": "sha512-dEoydsCp50i7kS1xHOmPXq4zQYoGWedUsvqv9H6zdif2r7yLHygyfP9qou71TulRN0d6ng9EbRVsQhSqfUc19g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.2" + } + }, + "node_modules/react-dropzone": { + "version": "14.3.8", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", + "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.65.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", + "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "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-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rettime": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", + "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "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/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" + } + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/snakecase-keys": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-9.0.2.tgz", + "integrity": "sha512-Tr4gONsDj1Pa6HJH9D3b411r6tuRyCGgb1l7YpzDFp/thjVSWs7rcbNjyTyRqJi5SUV23sFpzf9epIJRbLR6Yw==", + "license": "MIT", + "dependencies": { + "change-case": "^5.4.4", + "map-obj": "^5.0.2", + "type-fest": "^4.15.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "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/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/super-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz", + "integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==", + "license": "MIT", + "dependencies": { + "clone-regexp": "^3.0.0", + "function-timeout": "^0.1.0", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "license": "MIT", + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "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/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tough-cookie/node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tough-cookie/node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "dev": true, + "license": "MIT" + }, + "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/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.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/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "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/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, + "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/validator": { + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "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/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/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..f096c342 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,98 @@ +{ + "name": "xingrin", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "dev:noauth": "NEXT_PUBLIC_SKIP_AUTH=true next dev --turbopack", + "build": "next build --turbopack", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@hookform/resolvers": "^5.2.2", + "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@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-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-use-controllable-state": "^1.2.2", + "@tabler/icons-react": "^3.35.0", + "@tanstack/react-query": "^5.90.2", + "@tanstack/react-table": "^8.21.3", + "@xterm/addon-fit": "^0.10.0", + "@xterm/addon-web-links": "^0.11.0", + "@xterm/xterm": "^5.5.0", + "axios": "^1.12.2", + "camelcase-keys": "^10.0.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "cron-parser": "^5.4.0", + "cronstrue": "^3.9.0", + "date-fns": "^4.1.0", + "geist": "^1.5.1", + "is-ip": "^5.0.1", + "js-yaml": "^4.1.0", + "lottie-react": "^2.4.1", + "lucide-react": "^0.544.0", + "next": "15.5.7", + "next-themes": "^0.4.6", + "nextjs-toploader": "^3.9.17", + "psl": "^1.15.0", + "react": "19.1.2", + "react-day-picker": "^9.11.2", + "react-dom": "19.1.2", + "react-dropzone": "^14.3.8", + "react-hook-form": "^7.65.0", + "recharts": "2.15.4", + "snakecase-keys": "^9.0.2", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "tldts": "^6.1.86", + "validator": "^13.15.15", + "vaul": "^1.1.2", + "zod": "^4.1.12" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@tanstack/react-query-devtools": "^5.90.2", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.19.19", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/validator": "^13.15.3", + "eslint": "^9", + "eslint-config-next": "15.5.7", + "msw": "^2.11.6", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", + "typescript": "^5" + }, + "msw": { + "workerDirectory": [ + "public" + ] + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 00000000..0deb6405 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,6413 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@dnd-kit/modifiers': + specifier: ^9.0.0 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.1.2) + '@hookform/resolvers': + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.65.0(react@19.1.2)) + '@monaco-editor/react': + specifier: ^4.7.0 + version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-avatar': + specifier: ^1.1.10 + version: 1.1.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-collapsible': + specifier: ^1.1.12 + version: 1.1.12(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.16 + version: 2.1.16(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-icons': + specifier: ^1.3.2 + version: 1.3.2(react@19.1.2) + '@radix-ui/react-label': + specifier: ^2.1.7 + version: 2.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-progress': + specifier: ^1.1.7 + version: 1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-radio-group': + specifier: ^1.3.8 + version: 1.3.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-scroll-area': + specifier: ^1.2.10 + version: 1.2.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-separator': + specifier: ^1.1.7 + version: 1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toggle': + specifier: ^1.1.10 + version: 1.1.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toggle-group': + specifier: ^1.1.11 + version: 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': + specifier: ^1.2.2 + version: 1.2.2(@types/react@19.2.0)(react@19.1.2) + '@tabler/icons-react': + specifier: ^3.35.0 + version: 3.35.0(react@19.1.2) + '@tanstack/react-query': + specifier: ^5.90.2 + version: 5.90.2(react@19.1.2) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@xterm/addon-fit': + specifier: ^0.10.0 + version: 0.10.0(@xterm/xterm@5.5.0) + '@xterm/addon-web-links': + specifier: ^0.11.0 + version: 0.11.0(@xterm/xterm@5.5.0) + '@xterm/xterm': + specifier: ^5.5.0 + version: 5.5.0 + axios: + specifier: ^1.12.2 + version: 1.12.2 + camelcase-keys: + specifier: ^10.0.0 + version: 10.0.0 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + cron-parser: + specifier: ^5.4.0 + version: 5.4.0 + cronstrue: + specifier: ^3.9.0 + version: 3.9.0 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + geist: + specifier: ^1.5.1 + version: 1.5.1(next@15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2)) + is-ip: + specifier: ^5.0.1 + version: 5.0.1 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 + lottie-react: + specifier: ^2.4.1 + version: 2.4.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + lucide-react: + specifier: ^0.544.0 + version: 0.544.0(react@19.1.2) + next: + specifier: 15.5.7 + version: 15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + nextjs-toploader: + specifier: ^3.9.17 + version: 3.9.17(next@15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + psl: + specifier: ^1.15.0 + version: 1.15.0 + react: + specifier: 19.1.2 + version: 19.1.2 + react-day-picker: + specifier: ^9.11.2 + version: 9.11.2(react@19.1.2) + react-dom: + specifier: 19.1.2 + version: 19.1.2(react@19.1.2) + react-dropzone: + specifier: ^14.3.8 + version: 14.3.8(react@19.1.2) + react-hook-form: + specifier: ^7.65.0 + version: 7.65.0(react@19.1.2) + recharts: + specifier: 2.15.4 + version: 2.15.4(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + snakecase-keys: + specifier: ^9.0.2 + version: 9.0.2 + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 + tldts: + specifier: ^6.1.86 + version: 6.1.86 + validator: + specifier: ^13.15.15 + version: 13.15.15 + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + zod: + specifier: ^4.1.12 + version: 4.1.12 + devDependencies: + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.1 + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.14 + '@tanstack/react-query-devtools': + specifier: ^5.90.2 + version: 5.90.2(@tanstack/react-query@5.90.2(react@19.1.2))(react@19.1.2) + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 + '@types/node': + specifier: ^20.19.19 + version: 20.19.19 + '@types/react': + specifier: ^19 + version: 19.2.0 + '@types/react-dom': + specifier: ^19 + version: 19.2.0(@types/react@19.2.0) + '@types/validator': + specifier: ^13.15.3 + version: 13.15.3 + eslint: + specifier: ^9 + version: 9.36.0(jiti@2.6.1) + eslint-config-next: + specifier: 15.5.7 + version: 15.5.7(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + msw: + specifier: ^2.11.6 + version: 2.11.6(@types/node@20.19.19)(typescript@5.9.3) + tailwindcss: + specifier: ^4 + version: 4.1.14 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + + '@emnapi/core@1.5.0': + resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.36.0': + resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.4': + resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.4': + resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.3': + resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.3': + resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.3': + resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.3': + resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.3': + resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.3': + resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.3': + resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.4': + resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.4': + resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.4': + resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.4': + resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.4': + resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.4': + resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.4': + resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.4': + resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.4': + resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.4': + resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.4': + resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@inquirer/ansi@1.0.1': + resolution: {integrity: sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.19': + resolution: {integrity: sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.0': + resolution: {integrity: sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.14': + resolution: {integrity: sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.9': + resolution: {integrity: sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@monaco-editor/loader@1.7.0': + resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@mswjs/interceptors@0.40.0': + resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} + engines: {node: '>=18'} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@next/env@15.5.7': + resolution: {integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==} + + '@next/eslint-plugin-next@15.5.7': + resolution: {integrity: sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==} + + '@next/swc-darwin-arm64@15.5.7': + resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.5.7': + resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.5.7': + resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.5.7': + resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.5.7': + resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.5.7': + resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.5.7': + resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.5.7': + resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + 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 + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + 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 + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + 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 + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + 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 + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + 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 + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + 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 + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + 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 + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + 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 + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + 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 + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + 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 + + '@radix-ui/react-icons@1.3.2': + resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==} + peerDependencies: + react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + 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 + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + 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 + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + 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 + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + 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 + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + 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 + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + 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 + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + 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 + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + 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 + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + 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 + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + 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 + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + 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 + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + 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 + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + 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 + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + 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 + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + 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 + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + 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 + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + 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 + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + 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 + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + 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 + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/eslint-patch@1.12.0': + resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tabler/icons-react@3.35.0': + resolution: {integrity: sha512-XG7t2DYf3DyHT5jxFNp5xyLVbL4hMJYJhiSdHADzAjLRYfL7AnjlRfiHDHeXxkb2N103rEIvTsBRazxXtAUz2g==} + peerDependencies: + react: '>= 16' + + '@tabler/icons@3.35.0': + resolution: {integrity: sha512-yYXe+gJ56xlZFiXwV9zVoe3FWCGuZ/D7/G4ZIlDtGxSx5CGQK110wrnT29gUj52kEZoxqF7oURTk97GQxELOFQ==} + + '@tailwindcss/node@4.1.14': + resolution: {integrity: sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==} + + '@tailwindcss/oxide-android-arm64@4.1.14': + resolution: {integrity: sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + resolution: {integrity: sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.14': + resolution: {integrity: sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + resolution: {integrity: sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + resolution: {integrity: sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + resolution: {integrity: sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + resolution: {integrity: sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + resolution: {integrity: sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.14': + resolution: {integrity: sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.14': + resolution: {integrity: sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==} + + '@tanstack/query-core@5.90.2': + resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} + + '@tanstack/query-devtools@5.90.1': + resolution: {integrity: sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==} + + '@tanstack/react-query-devtools@5.90.2': + resolution: {integrity: sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==} + peerDependencies: + '@tanstack/react-query': ^5.90.2 + react: ^18 || ^19 + + '@tanstack/react-query@5.90.2': + resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@20.19.19': + resolution: {integrity: sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==} + + '@types/react-dom@19.2.0': + resolution: {integrity: sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.0': + resolution: {integrity: sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/validator@13.15.3': + resolution: {integrity: sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==} + + '@typescript-eslint/eslint-plugin@8.45.0': + resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.45.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.45.0': + resolution: {integrity: sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.45.0': + resolution: {integrity: sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.45.0': + resolution: {integrity: sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.45.0': + resolution: {integrity: sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.45.0': + resolution: {integrity: sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.45.0': + resolution: {integrity: sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.45.0': + resolution: {integrity: sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.45.0': + resolution: {integrity: sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.45.0': + resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@xterm/addon-fit@0.10.0': + resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + + '@xterm/addon-web-links@0.11.0': + resolution: {integrity: sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + + '@xterm/xterm@5.5.0': + resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + engines: {node: '>=4'} + + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-keys@10.0.0: + resolution: {integrity: sha512-dzb1nrmD6llsF6eMZWSpQjVfe1FX4WOkR7HPdX1sMxM5u+1MlnbXodNJ/E6NRArYgMtcwMhiqqemGp/QJV/vrw==} + engines: {node: '>=20'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + caniuse-lite@1.0.30001747: + resolution: {integrity: sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-regexp@3.0.0: + resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-hrtime@5.0.0: + resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} + engines: {node: '>=12'} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + cron-parser@5.4.0: + resolution: {integrity: sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA==} + engines: {node: '>=18'} + + cronstrue@3.9.0: + resolution: {integrity: sha512-T3S35zmD0Ai2B4ko6+mEM+k9C6tipe2nB9RLiGT6QL2Wn0Vsn2cCZAC8Oeuf4CaE00GZWVdpYitbpWCNlIWqdA==} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.1: + resolution: {integrity: sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@15.5.7: + resolution: {integrity: sha512-nU/TRGHHeG81NeLW5DeQT5t6BDUqbpsNQTvef1ld/tqHT+/zTx60/TIhKnmPISTTe++DVo+DLxDmk4rnwHaZVw==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.36.0: + resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@5.3.2: + resolution: {integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function-timeout@0.1.1: + resolution: {integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==} + engines: {node: '>=14.16'} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + geist@1.5.1: + resolution: {integrity: sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==} + peerDependencies: + next: '>=13.2.0' + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + graphql@16.11.0: + resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + ip-regex@5.0.0: + resolution: {integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-ip@5.0.1: + resolution: {integrity: sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==} + engines: {node: '>=14.16'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lottie-react@2.4.1: + resolution: {integrity: sha512-LQrH7jlkigIIv++wIyrOYFLHSKQpEY4zehPicL9bQsrt1rnoKRYCYgpCUe5maqylNtacy58/sQDZTkwMcTRxZw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + lottie-web@5.13.0: + resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==} + + lucide-react@0.544.0: + resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + map-obj@5.0.2: + resolution: {integrity: sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + monaco-editor@0.55.1: + resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.11.6: + resolution: {integrity: sha512-MCYMykvmiYScyUm7I6y0VCxpNq1rgd5v7kG8ks5dKtvmxRUUPjribX6mUoUNBbM5/3PhUyoelEWiKXGOz84c+w==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.3: + resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + next@15.5.7: + resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + nextjs-toploader@3.9.17: + resolution: {integrity: sha512-9OF0KSSLtoSAuNg2LZ3aTl4hR9mBDj5L9s9DZiFCbMlXehyICGjkIz5dVGzuATU2bheJZoBdFgq9w07AKSuQQw==} + peerDependencies: + next: '>= 6.0.0' + react: '>= 16.0.0' + react-dom: '>= 16.0.0' + + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@7.2.0: + resolution: {integrity: sha512-fG4L8TlD1CacJiGMGPxM1/K8l/GaKL2eFQZ6DWAjxZYxSf07DkumbC/Mhh+u/NHvxkfQVL25By0pxBS8QE9ZrQ==} + engines: {node: '>=18'} + + react-day-picker@9.11.2: + resolution: {integrity: sha512-TD/xMUGg2oiKX8jUR21MST5pj+7Y36097YtnDHQFlIcZOu3mbLLw2B2JqEByEGrR3HHveWYnKlyls6WqJgohAg==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8.0' + + react-dom@19.1.2: + resolution: {integrity: sha512-dEoydsCp50i7kS1xHOmPXq4zQYoGWedUsvqv9H6zdif2r7yLHygyfP9qou71TulRN0d6ng9EbRVsQhSqfUc19g==} + peerDependencies: + react: ^19.1.2 + + react-dropzone@14.3.8: + resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + + react-hook-form@7.65.0: + resolution: {integrity: sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} + 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 + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + 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 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@19.1.2: + resolution: {integrity: sha512-MdWVitvLbQULD+4DP8GYjZUrepGW7d+GQkNVqJEzNxE+e9WIa4egVFE/RDfVb1u9u/Jw7dNMmPB4IqxzbFYJ0w==} + engines: {node: '>=0.10.0'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + rettime@0.7.0: + resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.34.4: + resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + snakecase-keys@9.0.2: + resolution: {integrity: sha512-Tr4gONsDj1Pa6HJH9D3b411r6tuRyCGgb1l7YpzDFp/thjVSWs7rcbNjyTyRqJi5SUV23sFpzf9epIJRbLR6Yw==} + engines: {node: '>=22'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + super-regex@0.2.0: + resolution: {integrity: sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==} + engines: {node: '>=14.16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + + tailwindcss@4.1.14: + resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tar@7.5.1: + resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} + engines: {node: '>=18'} + + time-span@5.1.0: + resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} + engines: {node: '>=12'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts-core@7.0.17: + resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tldts@7.0.17: + resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + 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 + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + 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 + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + validator@13.15.15: + resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} + engines: {node: '>= 0.10'} + + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/runtime@7.28.4': {} + + '@date-fns/tz@1.4.1': {} + + '@dnd-kit/accessibility@3.1.1(react@19.1.2)': + dependencies: + react: 19.1.2 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.1.2) + '@dnd-kit/utilities': 3.2.2(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + tslib: 2.8.1 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@dnd-kit/utilities': 3.2.2(react@19.1.2) + react: 19.1.2 + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@dnd-kit/utilities': 3.2.2(react@19.1.2) + react: 19.1.2 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.1.2)': + dependencies: + react: 19.1.2 + tslib: 2.8.1 + + '@emnapi/core@1.5.0': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.6.1))': + dependencies: + eslint: 9.36.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.36.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.6(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + + '@floating-ui/utils@0.2.10': {} + + '@hookform/resolvers@5.2.2(react-hook-form@7.65.0(react@19.1.2))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.65.0(react@19.1.2) + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.3 + optional: true + + '@img/sharp-darwin-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.3': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.3': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + optional: true + + '@img/sharp-linux-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.3 + optional: true + + '@img/sharp-linux-arm@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.3 + optional: true + + '@img/sharp-linux-ppc64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.3 + optional: true + + '@img/sharp-linux-s390x@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.3 + optional: true + + '@img/sharp-linux-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.3 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + optional: true + + '@img/sharp-wasm32@0.34.4': + dependencies: + '@emnapi/runtime': 1.5.0 + optional: true + + '@img/sharp-win32-arm64@0.34.4': + optional: true + + '@img/sharp-win32-ia32@0.34.4': + optional: true + + '@img/sharp-win32-x64@0.34.4': + optional: true + + '@inquirer/ansi@1.0.1': {} + + '@inquirer/confirm@5.1.19(@types/node@20.19.19)': + dependencies: + '@inquirer/core': 10.3.0(@types/node@20.19.19) + '@inquirer/type': 3.0.9(@types/node@20.19.19) + optionalDependencies: + '@types/node': 20.19.19 + + '@inquirer/core@10.3.0(@types/node@20.19.19)': + dependencies: + '@inquirer/ansi': 1.0.1 + '@inquirer/figures': 1.0.14 + '@inquirer/type': 3.0.9(@types/node@20.19.19) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.19 + + '@inquirer/figures@1.0.14': {} + + '@inquirer/type@3.0.9(@types/node@20.19.19)': + optionalDependencies: + '@types/node': 20.19.19 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@monaco-editor/loader@1.7.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@monaco-editor/loader': 1.7.0 + monaco-editor: 0.55.1 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + + '@mswjs/interceptors@0.40.0': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/env@15.5.7': {} + + '@next/eslint-plugin-next@15.5.7': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@15.5.7': + optional: true + + '@next/swc-darwin-x64@15.5.7': + optional: true + + '@next/swc-linux-arm64-gnu@15.5.7': + optional: true + + '@next/swc-linux-arm64-musl@15.5.7': + optional: true + + '@next/swc-linux-x64-gnu@15.5.7': + optional: true + + '@next/swc-linux-x64-musl@15.5.7': + optional: true + + '@next/swc-win32-arm64-msvc@15.5.7': + optional: true + + '@next/swc-win32-x64-msvc@15.5.7': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.0)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.0)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + aria-hidden: 1.2.6 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.7.1(@types/react@19.2.0)(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.0)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.0)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-icons@1.3.2(react@19.1.2)': + dependencies: + react: 19.1.2 + + '@radix-ui/react-id@1.1.1(@types/react@19.2.0)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.0)(react@19.1.2) + aria-hidden: 1.2.6 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.7.1(@types/react@19.2.0)(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + aria-hidden: 1.2.6 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.7.1(@types/react@19.2.0)(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/rect': 1.1.1 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + aria-hidden: 1.2.6 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.7.1(@types/react@19.2.0)(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.0)(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.0)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.0)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.0)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.0)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.0)(react@19.1.2)': + dependencies: + react: 19.1.2 + use-sync-external-store: 1.6.0(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.0)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.0)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.0)(react@19.1.2)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.0)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.0)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.2.0 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + + '@radix-ui/rect@1.1.1': {} + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.12.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tabler/icons-react@3.35.0(react@19.1.2)': + dependencies: + '@tabler/icons': 3.35.0 + react: 19.1.2 + + '@tabler/icons@3.35.0': {} + + '@tailwindcss/node@4.1.14': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.1 + magic-string: 0.30.19 + source-map-js: 1.2.1 + tailwindcss: 4.1.14 + + '@tailwindcss/oxide-android-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide@4.1.14': + dependencies: + detect-libc: 2.1.1 + tar: 7.5.1 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-x64': 4.1.14 + '@tailwindcss/oxide-freebsd-x64': 4.1.14 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.14 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.14 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-x64-musl': 4.1.14 + '@tailwindcss/oxide-wasm32-wasi': 4.1.14 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.14 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.14 + + '@tailwindcss/postcss@4.1.14': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.14 + '@tailwindcss/oxide': 4.1.14 + postcss: 8.5.6 + tailwindcss: 4.1.14 + + '@tanstack/query-core@5.90.2': {} + + '@tanstack/query-devtools@5.90.1': {} + + '@tanstack/react-query-devtools@5.90.2(@tanstack/react-query@5.90.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@tanstack/query-devtools': 5.90.1 + '@tanstack/react-query': 5.90.2(react@19.1.2) + react: 19.1.2 + + '@tanstack/react-query@5.90.2(react@19.1.2)': + dependencies: + '@tanstack/query-core': 5.90.2 + react: 19.1.2 + + '@tanstack/react-table@8.21.3(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + + '@tanstack/table-core@8.21.3': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/js-yaml@4.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@20.19.19': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.2.0(@types/react@19.2.0)': + dependencies: + '@types/react': 19.2.0 + + '@types/react@19.2.0': + dependencies: + csstype: 3.1.3 + + '@types/statuses@2.0.6': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@types/validator@13.15.3': {} + + '@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/type-utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.45.0 + eslint: 9.36.0(jiti@2.6.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.45.0 + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.45.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.45.0': + dependencies: + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 + + '@typescript-eslint/tsconfig-utils@8.45.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.45.0': {} + + '@typescript-eslint/typescript-estree@8.45.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.45.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + eslint: 9.36.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.45.0': + dependencies: + '@typescript-eslint/types': 8.45.0 + eslint-visitor-keys: 4.2.1 + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + + '@xterm/addon-web-links@0.11.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + + '@xterm/xterm@5.5.0': {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + asynckit@0.4.0: {} + + attr-accept@2.2.5: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.10.3: {} + + axios@1.12.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase-keys@10.0.0: + dependencies: + camelcase: 8.0.0 + map-obj: 5.0.2 + quick-lru: 7.2.0 + type-fest: 4.41.0 + + camelcase@8.0.0: {} + + caniuse-lite@1.0.30001747: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + change-case@5.4.4: {} + + chownr@3.0.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-width@4.1.0: {} + + client-only@0.0.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-regexp@3.0.0: + dependencies: + is-regexp: 3.1.0 + + clsx@2.1.1: {} + + cmdk@1.1.1(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.0)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + concat-map@0.0.1: {} + + convert-hrtime@5.0.0: {} + + cookie@1.0.2: {} + + cron-parser@5.4.0: + dependencies: + luxon: 3.7.2 + + cronstrue@3.9.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + date-fns-jalali@4.1.0-0: {} + + date-fns@4.1.0: {} + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js-light@2.5.1: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + detect-libc@2.1.1: {} + + detect-node-es@1.1.0: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.4 + csstype: 3.1.3 + + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-next@15.5.7(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 15.5.7 + '@rushstack/eslint-patch': 1.12.0 + '@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.36.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.36.0(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.36.0(jiti@2.6.1)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.36.0(jiti@2.6.1)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.1) + get-tsconfig: 4.10.1 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.36.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.36.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.36.0(jiti@2.6.1)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.10.3 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.36.0(jiti@2.6.1) + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@5.2.0(eslint@9.36.0(jiti@2.6.1)): + dependencies: + eslint: 9.36.0(jiti@2.6.1) + + eslint-plugin-react@7.37.5(eslint@9.36.0(jiti@2.6.1)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.36.0(jiti@2.6.1) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.36.0(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.36.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + 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.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + eventemitter3@4.0.7: {} + + fast-deep-equal@3.1.3: {} + + fast-equals@5.3.2: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-selector@2.1.2: + dependencies: + tslib: 2.8.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + function-bind@1.1.2: {} + + function-timeout@0.1.1: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + geist@1.5.1(next@15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2)): + dependencies: + next: 15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + + generator-function@2.0.1: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + graphql@16.11.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + headers-polyfill@4.0.3: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + internmap@2.0.3: {} + + ip-regex@5.0.0: {} + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-ip@5.0.1: + dependencies: + ip-regex: 5.0.0 + super-regex: 0.2.0 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-node-process@1.2.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-regexp@3.1.0: {} + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.1.1 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lottie-react@2.4.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + lottie-web: 5.13.0 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + + lottie-web@5.13.0: {} + + lucide-react@0.544.0(react@19.1.2): + dependencies: + react: 19.1.2 + + luxon@3.7.2: {} + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + map-obj@5.0.2: {} + + marked@14.0.0: {} + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + + monaco-editor@0.55.1: + dependencies: + dompurify: 3.2.7 + marked: 14.0.0 + + ms@2.1.3: {} + + msw@2.11.6(@types/node@20.19.19)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.19(@types/node@20.19.19) + '@mswjs/interceptors': 0.40.0 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.0.2 + graphql: 16.11.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.7.0 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 4.41.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@2.0.0: {} + + nanoid@3.3.11: {} + + napi-postinstall@0.3.3: {} + + natural-compare@1.4.0: {} + + next-themes@0.4.6(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + + next@15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + '@next/env': 15.5.7 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001747 + postcss: 8.4.31 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + styled-jsx: 5.1.6(react@19.1.2) + optionalDependencies: + '@next/swc-darwin-arm64': 15.5.7 + '@next/swc-darwin-x64': 15.5.7 + '@next/swc-linux-arm64-gnu': 15.5.7 + '@next/swc-linux-arm64-musl': 15.5.7 + '@next/swc-linux-x64-gnu': 15.5.7 + '@next/swc-linux-x64-musl': 15.5.7 + '@next/swc-win32-arm64-msvc': 15.5.7 + '@next/swc-win32-x64-msvc': 15.5.7 + sharp: 0.34.4 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + nextjs-toploader@3.9.17(next@15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + next: 15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + nprogress: 0.2.0 + prop-types: 15.8.1 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + + nprogress@0.2.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + outvariant@1.4.3: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@6.3.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-from-env@1.1.0: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + quick-lru@7.2.0: {} + + react-day-picker@9.11.2(react@19.1.2): + dependencies: + '@date-fns/tz': 1.4.1 + date-fns: 4.1.0 + date-fns-jalali: 4.1.0-0 + react: 19.1.2 + + react-dom@19.1.2(react@19.1.2): + dependencies: + react: 19.1.2 + scheduler: 0.26.0 + + react-dropzone@14.3.8(react@19.1.2): + dependencies: + attr-accept: 2.2.5 + file-selector: 2.1.2 + prop-types: 15.8.1 + react: 19.1.2 + + react-hook-form@7.65.0(react@19.1.2): + dependencies: + react: 19.1.2 + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.0)(react@19.1.2): + dependencies: + react: 19.1.2 + react-style-singleton: 2.2.3(@types/react@19.2.0)(react@19.1.2) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.0 + + react-remove-scroll@2.7.1(@types/react@19.2.0)(react@19.1.2): + dependencies: + react: 19.1.2 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.0)(react@19.1.2) + react-style-singleton: 2.2.3(@types/react@19.2.0)(react@19.1.2) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.0)(react@19.1.2) + use-sidecar: 1.1.3(@types/react@19.2.0)(react@19.1.2) + optionalDependencies: + '@types/react': 19.2.0 + + react-smooth@4.0.4(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + fast-equals: 5.3.2 + prop-types: 15.8.1 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-transition-group: 4.4.5(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + + react-style-singleton@2.2.3(@types/react@19.2.0)(react@19.1.2): + dependencies: + get-nonce: 1.0.1 + react: 19.1.2 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.0 + + react-transition-group@4.4.5(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + '@babel/runtime': 7.28.4 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + + react@19.1.2: {} + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-directory@2.1.1: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rettime@0.7.0: {} + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.26.0: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + sharp@0.34.4: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.1 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.4 + '@img/sharp-darwin-x64': 0.34.4 + '@img/sharp-libvips-darwin-arm64': 1.2.3 + '@img/sharp-libvips-darwin-x64': 1.2.3 + '@img/sharp-libvips-linux-arm': 1.2.3 + '@img/sharp-libvips-linux-arm64': 1.2.3 + '@img/sharp-libvips-linux-ppc64': 1.2.3 + '@img/sharp-libvips-linux-s390x': 1.2.3 + '@img/sharp-libvips-linux-x64': 1.2.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + '@img/sharp-linux-arm': 0.34.4 + '@img/sharp-linux-arm64': 0.34.4 + '@img/sharp-linux-ppc64': 0.34.4 + '@img/sharp-linux-s390x': 0.34.4 + '@img/sharp-linux-x64': 0.34.4 + '@img/sharp-linuxmusl-arm64': 0.34.4 + '@img/sharp-linuxmusl-x64': 0.34.4 + '@img/sharp-wasm32': 0.34.4 + '@img/sharp-win32-arm64': 0.34.4 + '@img/sharp-win32-ia32': 0.34.4 + '@img/sharp-win32-x64': 0.34.4 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + snakecase-keys@9.0.2: + dependencies: + change-case: 5.4.4 + map-obj: 5.0.2 + type-fest: 4.41.0 + + sonner@2.0.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + + source-map-js@1.2.1: {} + + stable-hash@0.0.5: {} + + state-local@1.0.7: {} + + statuses@2.0.2: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.0 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.6(react@19.1.2): + dependencies: + client-only: 0.0.1 + react: 19.1.2 + + super-regex@0.2.0: + dependencies: + clone-regexp: 3.0.0 + function-timeout: 0.1.1 + time-span: 5.1.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@3.3.1: {} + + tailwindcss@4.1.14: {} + + tapable@2.3.0: {} + + tar@7.5.1: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + time-span@5.1.0: + dependencies: + convert-hrtime: 5.0.0 + + tiny-invariant@1.3.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tldts-core@6.1.86: {} + + tldts-core@7.0.17: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tldts@7.0.17: + dependencies: + tldts-core: 7.0.17 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.17 + + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tw-animate-css@1.4.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.41.0: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.3 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + until-async@3.0.2: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.0)(react@19.1.2): + dependencies: + react: 19.1.2 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.0 + + use-sidecar@1.1.3(@types/react@19.2.0)(react@19.1.2): + dependencies: + detect-node-es: 1.1.0 + react: 19.1.2 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.0 + + use-sync-external-store@1.6.0(react@19.1.2): + dependencies: + react: 19.1.2 + + validator@13.15.15: {} + + vaul@1.1.2(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + + yallist@5.0.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + zod@4.1.12: {} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 00000000..c7bcb4b1 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/frontend/public/animations/Security000-Purple.json b/frontend/public/animations/Security000-Purple.json new file mode 100644 index 00000000..1ca270b4 --- /dev/null +++ b/frontend/public/animations/Security000-Purple.json @@ -0,0 +1 @@ +{"v":"5.7.7","fr":30,"ip":0,"op":91,"w":3710,"h":3710,"nm":"Cloud Computing Security","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Arm R","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[328.648,176.87,0],"ix":2,"l":2},"a":{"a":0,"k":[161.776,-9.236,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.759,0.536],[-0.538,-0.76],[0,0],[-0.462,0.908],[-0.832,-0.427],[0.423,-0.829],[76.776,-32.874]],"o":[[0,0],[-0.538,-0.759],[0.756,-0.543],[0,0],[74.91,-32.841],[0.421,-0.825],[0.828,0.421],[-0.47,0.92],[0,0]],"v":[[-58.1,64.348],[-67.705,50.766],[-67.302,48.419],[-64.956,48.82],[-56.926,60.176],[64.822,-63.188],[67.086,-63.921],[67.82,-61.656],[-56.849,63.812]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[135.083,156.696],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.498,0],[0.309,0.243],[-0.576,0.73],[-18.069,45.788],[-0.873,-0.358],[0.34,-0.864],[0.534,-0.674]],"o":[[-0.365,0],[-0.729,-0.575],[0.531,-0.673],[0.341,-0.865],[0.865,0.342],[-18.253,46.248],[-0.332,0.421]],"v":[[-35.824,58.802],[-36.867,58.44],[-37.144,56.076],[34.247,-57.497],[36.433,-58.444],[37.381,-56.261],[-34.501,58.161]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[37.97,59.052],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Arm L Line","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[68.407,217.677,0],"ix":2,"l":2},"a":{"a":0,"k":[11.949,61.788,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.003,0.926],[-0.199,0.68],[-0.896,-0.26],[0.261,-0.89],[-0.168,-50.776],[0.931,-0.003]],"o":[[-0.927,0],[-0.17,-51.272],[0.261,-0.888],[0.892,0.26],[-0.199,0.675],[0.003,0.93],[0,0]],"v":[[-9.858,61.442],[-11.541,59.766],[8.218,-60.041],[10.308,-61.182],[11.45,-59.095],[-8.174,59.753],[-9.853,61.442]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[11.961,61.692],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Laptop","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-223.819,363.13,0],"ix":2,"l":2},"a":{"a":0,"k":[245.616,162.77,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-5.759,0],[-0.187,0.011],[0,0],[0.303,6.004],[5.984,-0.404],[0,0],[-0.303,-6.005]],"o":[[0.186,0],[0,0],[6.004,-0.302],[-0.303,-6.006],[0,0],[-6.003,0.301],[0.295,5.816]],"v":[[-121.448,17.218],[-120.889,17.204],[121.989,4.928],[132.312,-6.492],[120.889,-16.815],[-121.989,-4.538],[-132.311,6.883]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[355.694,307.307],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.4,-12.92],[-7.234,3.585],[6.401,12.92],[7.235,-3.585]],"o":[[6.401,12.921],[7.235,-3.584],[-6.401,-12.921],[-7.235,3.583]],"v":[[-13.1,6.489],[11.589,23.394],[13.099,-6.489],[-11.589,-23.394]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[103.245,120.161],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[12.149,0.831],[0,0],[-3.906,-11.127],[0,0],[-9.275,-0.937],[0,0]],"o":[[0,0],[-3.497,-11.664],[0,0],[-11.766,-0.805],[0,0],[3.088,8.796],[0,0],[0,0]],"v":[[124.006,153.273],[38.272,-132.642],[12.263,-153.412],[-103.489,-161.333],[-120.1,-139.516],[-24.257,133.531],[-3.997,149.48],[109.989,162.137]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.256,162.388],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Head","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[-5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":45,"s":[10]},{"t":90,"s":[-5]}],"ix":10},"p":{"a":0,"k":[43.714,48.794,0],"ix":2,"l":2},"a":{"a":0,"k":[193.754,259.215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-17.56,-15.287],[20.117,-6.413]],"o":[[0,0],[17.561,15.288],[0,0]],"v":[[-23.341,11.767],[5.78,-20.03],[-18.915,35.318]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.672988712086,0.661611998315,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[234.411,154.694],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.401,2.671],[6.167,10.95],[32.401,-17.44],[28.652,15.254],[-73.827,-13.867],[0.21,2.85],[-28.66,9.083],[-6.05,-7.307],[-1.697,2.215],[-32.121,-14.156],[0.437,-10.154],[-2.992,1.377],[-12.485,-15.643],[7.399,-4.917],[0.009,-1.455],[24.587,-27.671],[1.241,2.955],[0,0],[2.162,-0.697]],"o":[[-2.571,0.829],[-1.22,-8.123],[0,0],[-32.4,17.438],[-27.934,-14.873],[2.809,0.528],[-0.908,-12.302],[26.821,-8.502],[1.78,2.151],[8.035,-10.49],[26.456,11.659],[-0.142,3.291],[8.102,-3.732],[17.259,21.625],[-1.212,0.806],[-0.07,11.247],[-2.129,2.396],[0,0],[-0.879,-2.094],[0,0]],"v":[[55.473,54.233],[49.809,50.743],[39.376,15.596],[10.201,21.492],[-99.516,49.294],[-81.92,-26.563],[-76.738,-31.123],[-47.765,-80.811],[-0.211,-69.898],[6.57,-70.006],[68.545,-88.665],[98.679,-50.556],[104.868,-46.413],[138.488,-36.441],[123.684,8.854],[121.709,12.45],[95.301,100.425],[87.996,99.164],[69.065,54.057],[63.688,51.586]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[155.997,103.071],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-33.801,-27.059],[-22.243,66.335],[0,0]],"o":[[0,0],[33.8,27.059],[0,0],[0,0]],"v":[[-84.106,-42.045],[-37.792,73.005],[84.106,13.355],[39.787,-100.064]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.672988712086,0.661611998315,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[166.69,183.974],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Neck","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[209.141,90.844,0],"ix":2,"l":2},"a":{"a":0,"k":[53.835,111.217,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[3.351,-2.295],[43.932,16.219],[0.435,5.338],[0,0]],"o":[[0,0],[1.167,3.889],[-12.253,8.391],[-5.025,-1.855],[0,0],[0,0]],"v":[[27.352,-60.18],[52.802,24.703],[49.177,35.099],[-39.032,43.961],[-47.936,32.075],[-53.969,-41.871]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.672988712086,0.661611998315,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[54.218,60.43],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Hand R","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[46.053,161.17,0],"ix":2,"l":2},"a":{"a":0,"k":[443.962,45.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[32.906,26.019],[148.712,-20.133],[28.58,-12.509],[0,0],[0,0],[-90.219,109.483]],"o":[[0,0],[0,0],[-28.579,12.51],[0,0],[0,0],[-7.26,-38.311]],"v":[[168.95,-101.529],[-96.078,56.33],[-163.091,33.801],[-230.856,101.529],[-39.91,101.529],[230.856,-14.316]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.672988712086,0.661611998315,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[231.106,101.779],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Body","parent":29,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[974.912,340.998,0],"ix":2,"l":2},"a":{"a":0,"k":[200.98,309.541,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[53.365,54.575],[-8.998,110.579],[19.144,11.357],[-3.134,6.491],[-6.376,1.591],[-100.101,-136.922],[194.9,-187.332]],"o":[[0,0],[0,0],[-6.199,-3.677],[14.075,-29.158],[34.656,-8.649],[42.96,58.763],[-62.714,10.209]],"v":[[-206.824,214.413],[-155.961,-9.968],[-205.733,-47.649],[-211.139,-65.595],[-105.572,-210.831],[171.314,-150.136],[-32.111,276.85]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.7490196078431373,0.12156862745098039,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[214.523,287.308],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Hand L","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[66.852,244.246,0],"ix":2,"l":2},"a":{"a":0,"k":[336.185,15.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[90.614,6.968],[180.388,-72.992],[0,0],[0,0],[0,0],[-68.166,138.854]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[2.252,-4.588]],"v":[[102.949,-139.437],[-114.256,118.061],[-178.421,110.499],[-193.562,134.641],[-43.049,139.437],[153.773,-50.376]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.672988712086,0.661611998315,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[193.812,139.687],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Leg R","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[182.412,504.738,0],"ix":2,"l":2},"a":{"a":0,"k":[725.756,0.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[35.424,-246.39],[-35.303,-3.101],[-76.992,136.348],[0,0],[-52.336,80.538]],"o":[[0,0],[0,0],[0,0],[0,0],[94.404,-167.181],[89.852,19.83],[0,0]],"v":[[215.844,-216.45],[-126.146,-212.747],[-356.873,195.227],[-327.45,216.45],[-132.381,2.982],[149.05,-88.944],[362.753,-157.03]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[363.003,216.7],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shooes R","parent":9,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49.042,401.617,0],"ix":2,"l":2},"a":{"a":0,"k":[205.872,0.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-9.004,10.234],[-9.322,8.168],[-31.723,4.226],[-25.487,14.805],[61.347,1.698]],"o":[[6.033,-6.856],[-3.224,19.326],[32.629,-4.346],[-31.945,27.245],[-13.626,-0.377]],"v":[[-72.35,-0.251],[-49.295,-23.15],[-16.119,11.119],[81.355,-28.081],[-60.542,26.383]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[81.604,88.936],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-3.224,19.326],[-70.393,8.628],[0,0],[27.733,-23.651],[32.628,-4.346]],"o":[[30.603,-26.815],[0,0],[0,0],[-25.487,14.805],[-31.723,4.225]],"v":[[-85.169,13.52],[64.588,-52.015],[88.393,-37.454],[45.481,8.589],[-51.992,47.79]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[117.478,52.265],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Leg L","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[44.798,576.06,0],"ix":2,"l":2},"a":{"a":0,"k":[553.395,396.937,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[41.854,108.787],[0,0],[147.011,24.975],[-107.785,-244.076],[-18.089,11.553],[-2.36,5.806],[0,0],[-33.233,-32.271]],"o":[[0,0],[0,0],[0,0],[8.671,19.634],[5.41,-3.456],[0,0],[0,0],[33.233,32.271]],"v":[[252.335,70.993],[179.305,70.993],[-191.853,-201.531],[-186.403,173.174],[-135.393,189.978],[-123.738,176.085],[-132.405,-32.316],[92.025,167.179]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[294.439,201.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shooes L","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[138.183,375.875,0],"ix":2,"l":2},"a":{"a":0,"k":[150.157,19.634,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-15.575,22.49],[-47.84,17.664],[-17.702,12.71],[-0.229,-3.836],[108.4,-17.122],[-2.934,7.699]],"o":[[18.854,6.103],[46.284,-17.088],[0.395,3.445],[0,0],[-8.137,1.285],[5.264,-13.815]],"v":[[-70.536,-18.196],[4.449,-3.047],[102.844,-54.884],[103.799,-44.005],[-89.213,53.599],[-100.866,39.062]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[104.05,117.285],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-77.566,38.613],[-4.244,-71.199],[108.399,-17.122],[-2.933,7.699]],"o":[[0,0],[0,0],[-8.139,1.285],[13.96,-36.633]],"v":[[59.542,-81.688],[98.393,-8.657],[-90.254,80.403],[-75.944,17.152]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[109.457,81.937],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Security","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2457.188,2064.126,0],"ix":2,"l":2},"a":{"a":0,"k":[594.999,698.885,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[11.36,0],[0,0],[-2.653,11.048],[0,0],[-1.62,30.689],[-39.725,2.467],[0,-45.417],[23.133,-13.829]],"o":[[2.653,11.046],[0,0],[-11.362,0],[0,0],[-24.27,-14.509],[2.099,-39.745],[46.034,-2.86],[0,28.91],[0,0]],"v":[[61.135,105.571],[44.055,127.239],[-44.057,127.239],[-61.137,105.569],[-40.576,19.985],[-79.091,-52.331],[-5.059,-127.081],[79.201,-48.036],[40.573,19.985]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[26.183,0],[0,0],[0,-26.183],[0,0],[-26.184,0],[0,0],[0,26.184],[0,0]],"o":[[0,0],[-26.184,0],[0,0],[0,26.184],[0,0],[26.183,0],[0,0],[0,-26.183]],"v":[[213.644,-211.374],[-213.645,-211.374],[-261.056,-163.965],[-261.056,163.963],[-213.645,211.375],[213.644,211.375],[261.054,163.963],[261.054,-163.965]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[595.385,765.304],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[12.552,0],[0,0],[0,12.552],[0,0],[30.926,0],[0,0],[0,-30.926],[0,0],[12.552,0],[0,0],[0,12.552],[0,0],[-69.989,0],[0,0],[0,-69.99],[0,0]],"o":[[0,0],[-12.552,0],[0,0],[0,-30.926],[0,0],[-30.927,0],[0,0],[0,12.552],[0,0],[-12.551,0],[0,0],[0,-69.99],[0,0],[69.99,0],[0,0],[0,12.552]],"v":[[182.316,117.926],[157.039,117.926],[134.312,95.199],[134.312,8.801],[78.315,-47.196],[-78.311,-47.196],[-134.308,8.801],[-134.308,95.199],[-157.035,117.926],[-182.312,117.926],[-205.038,95.199],[-205.038,8.801],[-78.311,-117.926],[78.315,-117.926],[205.043,8.801],[205.043,95.199]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[595.383,415.613],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.461,1.262],[0,0],[0,0],[-119.819,160.696],[28.283,322.694],[3.826,0.284],[161.56,107.203],[2.644,-2.373],[133.637,0],[0,0],[1.488,-1.517],[-0.04,-2.096],[0,0],[-64.907,-177.691],[-213.257,-136.323]],"o":[[0,0],[0,0],[55.505,-28.528],[160.519,-215.29],[-0.338,-3.834],[-122.739,-9.046],[-2.962,-1.965],[-147.168,131.873],[0,0],[-2.256,0],[-1.466,1.49],[0,0],[2.807,159.984],[72.602,198.749],[2.335,1.493]],"v":[[-9.529,619.339],[-8.286,621.759],[-9.529,619.339],[307.827,346.411],[507.111,-464.352],[499.926,-471.459],[-14.402,-618.888],[-23.921,-618.199],[-505.446,-427.299],[-505.449,-427.299],[-511.193,-424.947],[-513.406,-419.384],[-513.023,-397.997],[-442.123,121.001],[-17.308,618.956]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[2.069,0],[2.173,1.387],[73.026,199.913],[2.82,160.406],[0,0],[-2.498,2.546],[-3.613,0],[0,0],[-146.1,130.913],[-5.032,-3.337],[-122.208,-9.006],[-0.572,-6.506],[161.342,-216.391],[55.842,-28.704]],"o":[[-2.498,0],[-214.297,-136.99],[-65.221,-178.547],[0,0],[-0.067,-3.568],[2.519,-2.563],[0,0],[132.713,0],[4.485,-4.02],[160.528,106.514],[6.501,0.481],[28.406,324.065],[-120.502,161.609],[-1.888,0.97]],"v":[[-13.073,625.63],[-20.241,623.542],[-447.234,122.866],[-518.465,-397.896],[-518.847,-419.283],[-515.074,-428.763],[-505.563,-432.74],[-505.558,-432.74],[-27.55,-622.253],[-11.394,-623.42],[500.325,-476.884],[512.53,-464.828],[312.19,349.662],[-7.043,624.18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[595.381,702.828],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":4,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-8.301,7.774],[-11.33,-1.019],[-147.757,151.182],[-11.761,-8.789],[-94.558,-5.2],[-2.021,-19.487],[112.155,-53.124],[11.517,7.172],[7.73,335.497]],"o":[[8.3,-7.773],[121.174,10.897],[10.26,-10.499],[164.326,122.816],[19.56,1.076],[85.993,830.543],[-12.26,5.807],[-558.973,-348.175],[-0.262,-11.37]],"v":[[-596.251,-477.4],[-565.401,-488.019],[-70.391,-686.672],[-29.668,-682.448],[501.605,-528.999],[539.056,-493.263],[-27.866,691.364],[-66.077,689.156],[-608.87,-447.309]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.7490196078431373,0.12156862745098039,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[625.299,697.421],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Security 4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[100]},{"t":90,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2457.188,2064.126,0],"ix":2,"l":2},"a":{"a":0,"k":[594.999,698.885,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[100,100,100]},{"t":90,"s":[130,130,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-8.301,7.774],[-11.33,-1.019],[-147.757,151.182],[-11.761,-8.789],[-94.558,-5.2],[-2.021,-19.487],[112.155,-53.124],[11.517,7.172],[7.73,335.497]],"o":[[8.3,-7.773],[121.174,10.897],[10.26,-10.499],[164.326,122.816],[19.56,1.076],[85.993,830.543],[-12.26,5.807],[-558.973,-348.175],[-0.262,-11.37]],"v":[[29.048,220.021],[59.899,209.402],[554.909,10.749],[595.631,14.973],[1126.905,168.422],[1164.356,204.158],[597.433,1388.785],[559.223,1386.577],[16.43,250.112]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.7490196078431373,0.12156862745098039,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":60,"op":240,"st":60,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Security 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[100]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2457.188,2064.126,0],"ix":2,"l":2},"a":{"a":0,"k":[594.999,698.885,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[100,100,100]},{"t":60,"s":[130,130,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-8.301,7.774],[-11.33,-1.019],[-147.757,151.182],[-11.761,-8.789],[-94.558,-5.2],[-2.021,-19.487],[112.155,-53.124],[11.517,7.172],[7.73,335.497]],"o":[[8.3,-7.773],[121.174,10.897],[10.26,-10.499],[164.326,122.816],[19.56,1.076],[85.993,830.543],[-12.26,5.807],[-558.973,-348.175],[-0.262,-11.37]],"v":[[29.048,220.021],[59.899,209.402],[554.909,10.749],[595.631,14.973],[1126.905,168.422],[1164.356,204.158],[597.433,1388.785],[559.223,1386.577],[16.43,250.112]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.7490196078431373,0.12156862745098039,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":30,"op":210,"st":30,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Security 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":30,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2457.188,2064.126,0],"ix":2,"l":2},"a":{"a":0,"k":[594.999,698.885,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"t":30,"s":[130,130,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-8.301,7.774],[-11.33,-1.019],[-147.757,151.182],[-11.761,-8.789],[-94.558,-5.2],[-2.021,-19.487],[112.155,-53.124],[11.517,7.172],[7.73,335.497]],"o":[[8.3,-7.773],[121.174,10.897],[10.26,-10.499],[164.326,122.816],[19.56,1.076],[85.993,830.543],[-12.26,5.807],[-558.973,-348.175],[-0.262,-11.37]],"v":[[29.048,220.021],[59.899,209.402],[554.909,10.749],[595.631,14.973],[1126.905,168.422],[1164.356,204.158],[597.433,1388.785],[559.223,1386.577],[16.43,250.112]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.7490196078431373,0.12156862745098039,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Setting","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":90,"s":[-360]}],"ix":10},"p":{"a":0,"k":[2549.826,1119.711,0],"ix":2,"l":2},"a":{"a":0,"k":[231.589,234.578,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[24.477,0],[2.728,0.214],[17.863,20.883],[-0.004,23.671],[-23.754,20.317],[-36.874,-43.112],[-0.285,-0.338],[-3.048,-5.395],[-0.01,-0.02],[-0.043,-0.079],[-0.079,-0.138],[-0.042,-0.075],[-0.132,-0.243],[0,0],[4.304,5.036],[32.978,-8.111],[11.352,-9.709],[0.003,-26.196],[-14.929,-17.454],[-38.877,33.259],[-5.31,15.863],[0,0],[14.173,-12.122]],"o":[[-2.709,0],[-27.397,-2.134],[-16.556,-19.352],[0,-29.047],[43.106,-36.882],[0.29,0.335],[3.998,4.801],[0.012,0.023],[0.046,0.082],[0.079,0.141],[0.043,0.078],[0.136,0.243],[0,0],[-3.12,-5.773],[-23.55,-27.525],[-13.602,3.347],[-21.42,18.326],[0,21.347],[33.256,38.871],[12.779,-10.931],[0,0],[-5.892,17.596],[-18.799,16.083]],"v":[[2.813,102.865],[-5.347,102.546],[-75.533,66.852],[-100.187,0.046],[-64.23,-78.201],[80.823,-66.898],[81.684,-65.883],[92.288,-50.539],[92.324,-50.476],[92.459,-50.236],[92.695,-49.815],[92.823,-49.582],[93.221,-48.849],[84.337,-44.049],[73.15,-60.336],[-19.671,-90.07],[-57.667,-70.528],[-90.094,0.039],[-67.86,60.289],[62.958,70.481],[90.607,29.523],[100.188,32.732],[69.52,78.155]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[225.761,236.073],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.181,0],[6.027,-8.512],[-13.931,-9.856],[-8.127,1.397],[-4.774,6.746],[1.393,8.147],[6.746,4.774]],"o":[[-9.712,0],[-9.856,13.933],[6.749,4.777],[8.15,-1.394],[4.777,-6.749],[-1.395,-8.151],[-5.418,-3.837]],"v":[[0.032,-30.941],[-25.274,-17.882],[-17.889,25.262],[5.21,30.506],[25.252,17.881],[30.496,-5.221],[17.871,-25.263]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[2.327,0],[7.01,4.961],[-13.072,18.476],[-10.806,1.848],[-8.945,-6.332],[-1.848,-10.806],[6.336,-8.949],[10.806,-1.848]],"o":[[-8.42,0],[-18.476,-13.075],[6.335,-8.953],[10.81,-1.851],[8.953,6.336],[1.851,10.81],[-6.335,8.952],[-2.341,0.401]],"v":[[-0.096,41.059],[-23.722,33.508],[-33.519,-23.714],[-6.935,-40.462],[23.703,-33.509],[40.451,-6.928],[33.497,23.714],[6.917,40.461]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[229.278,236.047],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[1.087,0.472],[0,0],[7.466,14.426],[0,0],[0.77,0.901],[0,0],[1.101,-0.434],[0,0],[15.044,4.782],[0,0],[1.182,0.092],[0,0],[0.471,-1.087],[0,0],[14.426,-7.466],[0,0],[0.9,-0.77],[0,0],[-0.435,-1.103],[0,0],[4.782,-15.044],[0,0],[0.093,-1.18],[0,0],[-1.088,-0.472],[0,0],[-7.466,-14.427],[0,0],[-0.77,-0.901],[0,0],[-1.102,0.435],[0,0],[-15.043,-4.782],[0,0],[-1.181,-0.093],[0,0],[-0.473,1.086],[0,0],[-14.427,7.466],[0,0],[-0.9,0.771],[0,0],[0.433,1.102],[0,0],[-4.783,15.043],[0,0],[-0.093,1.183]],"o":[[0.092,-1.181],[0,0],[-2.395,-15.603],[0,0],[0.6,-1.022],[0,0],[-0.772,-0.9],[0,0],[-13.097,-9.61],[0,0],[-0.298,-1.146],[0,0],[-1.181,-0.093],[0,0],[-15.603,2.395],[0,0],[-1.022,-0.601],[0,0],[-0.9,0.769],[0,0],[-9.61,13.097],[0,0],[-1.145,0.299],[0,0],[-0.091,1.181],[0,0],[2.394,15.602],[0,0],[-0.6,1.022],[0,0],[0.77,0.9],[0,0],[13.097,9.611],[0,0],[0.298,1.147],[0,0],[1.181,0.091],[0,0],[15.602,-2.395],[0,0],[1.02,0.599],[0,0],[0.902,-0.771],[0,0],[9.61,-13.097],[0,0],[1.147,-0.298],[0,0]],"v":[[179.051,-6.382],[177.389,-9.145],[146.387,-22.612],[131.626,-67.973],[148.734,-97.099],[148.45,-100.312],[122.096,-131.12],[118.966,-131.899],[87.542,-119.509],[45.014,-141.117],[36.511,-173.832],[34.038,-175.9],[-6.382,-179.05],[-9.144,-177.387],[-22.612,-146.386],[-67.974,-131.625],[-97.1,-148.733],[-100.311,-148.449],[-131.121,-122.094],[-131.9,-118.964],[-119.509,-87.542],[-141.118,-45.012],[-173.832,-36.51],[-175.901,-34.039],[-179.051,6.383],[-177.387,9.146],[-146.386,22.613],[-131.626,67.975],[-148.734,97.101],[-148.451,100.312],[-122.095,131.123],[-118.966,131.899],[-87.542,119.51],[-45.014,141.118],[-36.511,173.832],[-34.039,175.903],[6.382,179.052],[9.146,177.389],[22.612,146.388],[67.974,131.626],[97.1,148.736],[100.312,148.45],[131.12,122.096],[131.9,118.968],[119.508,87.542],[141.118,45.015],[173.83,36.511],[175.901,34.039]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[229.268,236.046],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.171,0],[0.259,1.66],[0.927,4.303],[-1.817,0.391],[-0.392,-1.815],[-0.701,-4.474],[1.838,-0.286]],"o":[[-1.631,0],[-0.678,-4.349],[-0.392,-1.818],[1.849,-0.391],[0.953,4.428],[0.285,1.838],[-0.176,0.026]],"v":[[1.26,10.139],[-2.064,7.292],[-4.484,-5.747],[-1.904,-9.748],[2.098,-7.167],[4.59,6.253],[1.781,10.1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[457.995,192.225],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.174,0],[0.259,1.656],[-1.838,0.289],[-9.012,0.381],[-0.075,-1.861],[1.857,-0.079],[8.615,-1.362]],"o":[[-1.627,0],[-0.293,-1.838],[8.866,-1.404],[1.861,-0.039],[0.079,1.858],[-8.755,0.368],[-0.177,0.029]],"v":[[-62.934,-64.322],[-66.254,-67.162],[-63.457,-71.012],[-36.511,-73.702],[-33.006,-70.48],[-36.229,-66.975],[-62.406,-64.364]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[1.489,0],[0.289,0.076],[8.654,1.258],[-0.27,1.838],[-1.851,-0.315],[-8.697,-2.308],[0.477,-1.798]],"o":[[-0.286,0],[-8.443,-2.242],[-1.841,-0.267],[0.266,-1.845],[8.909,1.296],[1.794,0.476],[-0.401,1.506]],"v":[[33.057,-59.397],[32.188,-59.509],[6.423,-64.785],[3.575,-68.599],[7.39,-71.447],[33.918,-66.018],[36.308,-61.899]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.502,0],[0.588,1.184],[-1.664,0.828],[-8.49,3.058],[-0.631,-1.749],[1.749,-0.628],[7.808,-3.883]],"o":[[-1.24,0],[-0.829,-1.666],[8.045,-3.998],[1.758,-0.624],[0.631,1.749],[-8.245,2.969],[-0.484,0.24]],"v":[[-129.458,-43.1],[-132.475,-44.968],[-130.959,-49.481],[-106.045,-60.111],[-101.74,-58.082],[-103.764,-53.775],[-127.961,-43.452]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[1.118,0],[0.558,0.352],[7.87,3.78],[-0.806,1.676],[-1.687,-0.815],[-7.608,-4.806],[0.994,-1.571]],"o":[[-0.614,0],[-7.391,-4.671],[-1.677,-0.806],[0.801,-1.677],[8.104,3.893],[1.571,0.996],[-0.64,1.013]],"v":[[97.031,-31.471],[95.237,-31.991],[72.24,-44.727],[70.662,-49.218],[75.152,-50.797],[98.833,-37.685],[99.882,-33.04]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.834,0],[0.661,0.694],[-1.343,1.282],[-1.394,1.276],[-5.662,4.298],[-1.124,-1.486],[1.483,-1.124],[5.109,-4.662],[1.325,-1.266]],"o":[[-0.888,0],[-1.282,-1.348],[1.366,-1.298],[5.26,-4.8],[1.48,-1.127],[1.128,1.479],[-5.497,4.179],[-1.358,1.243],[-0.655,0.621]],"v":[[-186.581,-2.921],[-189.017,-3.963],[-188.906,-8.724],[-184.77,-12.587],[-168.312,-26.297],[-163.597,-25.652],[-164.241,-20.938],[-180.229,-7.616],[-184.256,-3.851]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0.773,0],[0.667,0.783],[1.802,1.976],[4.446,4.155],[-1.269,1.358],[-1.361,-1.272],[-4.226,-4.632],[-1.768,-2.077],[1.414,-1.207]],"o":[[-0.949,0],[-1.716,-2.018],[-4.109,-4.501],[-1.357,-1.273],[1.272,-1.361],[4.57,4.278],[1.853,2.029],[1.204,1.417],[-0.634,0.539]],"v":[[149.74,14.346],[147.176,13.162],[141.898,7.172],[129.01,-5.873],[128.854,-10.634],[133.614,-10.792],[146.871,2.635],[152.303,8.796],[151.923,13.544]],"c":true},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ind":6,"ty":"sh","ix":7,"ks":{"a":0,"k":{"i":[[0.442,0],[0.546,1.282],[4.334,7.599],[-1.615,0.92],[-0.924,-1.614],[-3.518,-8.282],[1.713,-0.727]],"o":[[-1.307,0],[-3.413,-8.042],[-0.92,-1.614],[1.614,-0.921],[4.458,7.825],[0.726,1.709],[-0.428,0.181]],"v":[[186.473,73.741],[183.374,71.69],[171.702,48.12],[172.962,43.528],[177.553,44.787],[189.573,69.06],[187.789,73.475]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[257.198,73.991],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":9,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.118,0],[0.559,0.353],[-0.992,1.575],[-2.633,3.686],[-1.51,-1.079],[1.082,-1.513],[2.35,-3.732]],"o":[[-0.614,0],[-1.572,-0.989],[2.417,-3.839],[1.082,-1.509],[1.515,1.081],[-2.558,3.58],[-0.638,1.015]],"v":[[-3.747,9.181],[-5.538,8.661],[-6.591,4.019],[1.024,-7.32],[5.719,-8.102],[6.501,-3.407],[-0.896,7.61]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[38.461,105.809],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.286,0],[0.4,1.509],[0.904,4.442],[-1.826,0.372],[-0.365,-1.824],[-1.127,-4.254],[1.798,-0.476]],"o":[[-1.489,0],[-1.161,-4.376],[-0.372,-1.822],[1.821,-0.371],[0.881,4.314],[0.477,1.795],[-0.29,0.076]],"v":[[1.499,10.068],[-1.752,7.564],[-4.862,-5.728],[-2.235,-9.697],[1.732,-7.07],[4.757,5.841],[2.364,9.957]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[5.484,287.625],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.621,0],[0.641,1.007],[3.998,8.239],[-1.674,0.812],[-0.812,-1.67],[-4.79,-7.48],[1.568,-1.003]],"o":[[-1.107,0],[-4.934,-7.699],[-0.812,-1.674],[1.679,-0.809],[3.883,7.999],[1.003,1.565],[-0.562,0.361]],"v":[[-177.388,-37.602],[-180.225,-39.153],[-193.685,-63.169],[-192.126,-67.667],[-187.629,-66.108],[-174.557,-42.782],[-175.576,-38.134]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.799,0],[0.664,0.743],[-1.388,1.237],[0,0],[-5.309,5.967],[-1.39,-1.242],[1.237,-1.387],[6.096,-5.56],[0,0]],"o":[[-0.924,0],[-1.239,-1.387],[0,0],[5.921,-5.402],[1.233,-1.397],[1.391,1.237],[-5.461,6.145],[0,0],[-0.644,0.572]],"v":[[171.568,10.561],[169.056,9.436],[169.326,4.682],[171.305,2.896],[188.23,-14.235],[192.981,-14.515],[193.26,-9.764],[175.841,7.875],[173.81,9.706]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.965,0],[0.628,0.525],[6.309,6.654],[-1.348,1.279],[-1.275,-1.347],[-6.8,-5.678],[1.194,-1.427]],"o":[[-0.76,0],[-7,-5.843],[-1.28,-1.351],[1.351,-1.276],[6.132,6.471],[1.426,1.19],[-0.664,0.799]],"v":[[-130.452,15.636],[-132.606,14.854],[-152.664,-3.981],[-152.539,-8.741],[-147.779,-8.617],[-128.292,9.686],[-127.866,14.427]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0.457,0],[0.558,1.249],[-1.696,0.759],[-7.627,4.553],[-0.957,-1.598],[1.597,-0.954],[8.357,-3.735]],"o":[[-1.289,0],[-0.76,-1.7],[8.117,-3.626],[1.588,-0.957],[0.953,1.598],[-7.855,4.691],[-0.447,0.2]],"v":[[111.992,49.148],[108.915,47.156],[110.614,42.708],[134.344,30.379],[138.961,31.542],[137.797,36.158],[113.363,48.855]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[1.328,0],[0.417,0.168],[8.022,4.419],[-0.897,1.628],[-1.633,-0.891],[-8.219,-3.34],[0.7,-1.723]],"o":[[-0.424,0],[-8.463,-3.439],[-1.628,-0.898],[0.898,-1.627],[7.792,4.29],[1.719,0.7],[-0.532,1.306]],"v":[[-69.627,52.17],[-70.896,51.923],[-95.738,40.081],[-97.063,35.507],[-92.49,34.183],[-68.358,45.683],[-66.507,50.072]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0.125,0],[0.194,1.719],[-1.848,0.207],[-8.627,2.006],[-0.421,-1.815],[1.811,-0.42],[9.12,-1.023]],"o":[[-1.692,0],[-0.208,-1.848],[8.86,-0.993],[1.795,-0.44],[0.421,1.812],[-8.884,2.068],[-0.128,0.012]],"v":[[43.489,67.762],[40.146,64.771],[43.118,61.05],[69.472,56.531],[73.512,59.047],[70.998,63.088],[43.867,67.744]],"c":true},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ind":6,"ty":"sh","ix":7,"ks":{"a":0,"k":{"i":[[1.738,0],[0.089,0.006],[8.959,1.769],[-0.358,1.822],[-1.815,-0.361],[-8.87,-0.697],[0.146,-1.851]],"o":[[-0.089,0],[-9.13,-0.72],[-1.825,-0.361],[0.364,-1.824],[8.709,1.723],[1.855,0.148],[-0.137,1.766]],"v":[[-0.542,68.477],[-0.809,68.467],[-28.074,64.715],[-30.727,60.76],[-26.772,58.106],[-0.284,61.752],[2.81,65.373]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[213.007,400.392],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":9,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.15,0],[0.539,0.319],[-0.946,1.601],[-2.002,3.879],[-1.66,-0.848],[0.851,-1.654],[2.331,-3.933]],"o":[[-0.582,0],[-1.601,-0.947],[2.263,-3.82],[0.852,-1.65],[1.651,0.852],[-2.062,3.995],[-0.628,1.062]],"v":[[-3.264,9.49],[-4.978,9.02],[-6.158,4.407],[0.27,-7.195],[4.806,-8.642],[6.253,-4.105],[-0.365,7.84]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[433.012,346.702],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Setting","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":90,"s":[360]}],"ix":10},"p":{"a":0,"k":[1976.14,908.807,0],"ix":2,"l":2},"a":{"a":0,"k":[447.134,454.057,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[46.518,0],[5.185,0.404],[33.943,39.682],[0,46.518],[-0.405,5.188],[-39.686,33.945],[-52.063,-4.057],[-33.942,-39.682],[-6.573,-12.168],[0,0],[8.601,10.05],[49.371,3.847],[37.635,-32.186],[3.849,-49.371],[0,-4.882],[-28.984,-33.882],[-49.372,-3.847],[-37.628,32.189],[-10.61,31.697],[0,0],[26.932,-23.037]],"o":[[-5.146,0],[-52.065,-4.054],[-30.565,-35.728],[0,-5.148],[4.057,-52.06],[39.676,-33.943],[52.065,4.054],[9.065,10.593],[0,0],[-6.234,-11.54],[-32.189,-37.635],[-49.371,-3.814],[-37.63,32.193],[-0.382,4.918],[0,44.114],[32.19,37.632],[49.407,3.85],[25.542,-21.85],[0,0],[-11.191,33.43],[-35.728,30.563]],"v":[[5.391,195.51],[-10.107,194.905],[-143.48,127.08],[-190.434,0.316],[-189.829,-15.189],[-122.001,-148.56],[20.264,-194.907],[153.636,-127.082],[177.203,-92.778],[168.319,-87.978],[145.962,-120.516],[19.482,-184.84],[-115.439,-140.887],[-179.762,-14.403],[-180.335,0.3],[-135.806,120.517],[-9.325,184.838],[125.595,140.885],[180.855,59.034],[190.435,62.243],[132.158,148.558]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[435.86,456.42],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13.338,0],[3.709,-0.635],[10.034,-14.179],[-29.271,-20.712],[-17.133,2.923],[-10.034,14.179],[2.929,17.122],[14.179,10.034]],"o":[[-3.686,0],[-17.122,2.929],[-20.716,29.271],[14.18,10.037],[17.122,-2.929],[10.037,-14.181],[-2.933,-17.122],[-11.11,-7.861]],"v":[[0.115,-65.049],[-10.994,-64.099],[-53.106,-37.568],[-37.591,53.084],[10.952,64.101],[53.06,37.57],[64.081,-10.97],[37.546,-53.082]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[4.258,0],[12.832,9.081],[-23.93,33.817],[-19.782,3.386],[-16.379,-11.589],[-3.383,-19.779],[11.593,-16.379],[19.782,-3.386]],"o":[[-15.409,0.004],[-33.817,-23.93],[11.593,-16.382],[19.782,-3.377],[16.383,11.596],[3.387,19.782],[-11.591,16.382],[-4.283,0.733]],"v":[[-0.181,75.151],[-43.424,61.329],[-61.352,-43.4],[-12.697,-74.054],[43.378,-61.328],[74.032,-12.676],[61.305,43.402],[12.654,74.056]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[442.639,456.41],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.115,0.919],[0,0],[14.534,28.085],[0,0],[1.499,1.753],[0,0],[2.144,-0.845],[0,0],[29.287,9.31],[0,0],[2.298,0.179],[0,0],[0.918,-2.115],[0,0],[28.085,-14.534],[0,0],[1.754,-1.499],[0,0],[-0.846,-2.146],[0,0],[9.309,-29.285],[0,0],[0.179,-2.299],[0,0],[-2.116,-0.919],[0,0],[-14.534,-28.086],[0,0],[-1.499,-1.752],[0,0],[-2.145,0.846],[0,0],[-29.286,-9.309],[0,0],[-2.3,-0.179],[0,0],[-0.92,2.117],[0,0],[-28.085,14.535],[0,0],[-1.752,1.499],[0,0],[0.845,2.146],[0,0],[-9.311,29.284],[0,0],[-0.18,2.3]],"o":[[0.179,-2.299],[0,0],[-4.662,-30.373],[0,0],[1.169,-1.989],[0,0],[-1.5,-1.753],[0,0],[-25.497,-18.708],[0,0],[-0.58,-2.232],[0,0],[-2.301,-0.179],[0,0],[-30.374,4.662],[0,0],[-1.989,-1.169],[0,0],[-1.752,1.499],[0,0],[-18.71,25.496],[0,0],[-2.231,0.581],[0,0],[-0.18,2.299],[0,0],[4.662,30.373],[0,0],[-1.167,1.988],[0,0],[1.499,1.753],[0,0],[25.495,18.71],[0,0],[0.579,2.233],[0,0],[2.299,0.178],[0,0],[30.373,-4.663],[0,0],[1.987,1.168],[0,0],[1.753,-1.499],[0,0],[18.709,-25.496],[0,0],[2.232,-0.58],[0,0]],"v":[[348.565,-12.424],[345.329,-17.802],[284.976,-44.021],[256.242,-132.327],[289.545,-189.028],[288.993,-195.281],[237.687,-255.256],[231.597,-256.772],[170.421,-232.653],[87.629,-274.719],[71.076,-338.403],[66.265,-342.432],[-12.423,-348.563],[-17.802,-345.328],[-44.02,-284.977],[-132.326,-256.24],[-189.027,-289.545],[-195.28,-288.992],[-255.257,-237.686],[-256.772,-231.594],[-232.651,-170.419],[-274.717,-87.63],[-338.403,-71.076],[-342.433,-66.264],[-348.564,12.425],[-345.328,17.803],[-284.976,44.021],[-256.241,132.329],[-289.547,189.03],[-288.993,195.281],[-237.686,255.259],[-231.595,256.772],[-170.42,232.652],[-87.628,274.719],[-71.076,338.403],[-66.263,342.433],[12.425,348.564],[17.804,345.328],[44.021,284.978],[132.328,256.24],[189.029,289.547],[195.28,288.993],[255.256,237.687],[256.774,231.596],[232.652,170.422],[274.718,87.631],[338.403,71.076],[342.433,66.265]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[442.617,456.41],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.171,0],[0.26,1.66],[0.822,4.382],[-1.828,0.342],[-0.342,-1.825],[-0.697,-4.461],[1.838,-0.285]],"o":[[-1.631,0],[-0.685,-4.396],[-0.339,-1.828],[1.819,-0.316],[0.829,4.448],[0.285,1.838],[-0.176,0.027]],"v":[[1.151,10.129],[-2.172,7.282],[-4.428,-5.885],[-1.736,-9.813],[2.193,-7.121],[4.482,6.243],[1.673,10.09]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[889.192,377.526],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.079,0],[0.123,1.772],[-1.855,0.128],[-9.294,0.079],[0,0],[-0.017,-1.848],[1.857,-0.016],[9.104,-0.637]],"o":[[-1.752,0],[-0.13,-1.857],[9.245,-0.651],[0,0],[1.848,0],[0.016,1.86],[-9.16,0.079],[-0.082,0.007]],"v":[[-74.354,-157.223],[-77.708,-160.353],[-74.587,-163.946],[-46.649,-165.044],[-46.619,-165.044],[-43.252,-161.707],[-46.59,-158.311],[-74.114,-157.233]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[1.634,0],[0.172,0.026],[9.108,0.829],[-0.167,1.851],[-1.89,-0.174],[-9.173,-1.407],[0.283,-1.837]],"o":[[-0.168,0],[-9.041,-1.387],[-1.85,-0.168],[0.171,-1.851],[9.242,0.842],[1.838,0.283],[-0.253,1.667]],"v":[[25.987,-153.094],[25.472,-153.133],[-1.874,-156.47],[-4.921,-160.129],[-1.262,-163.177],[26.492,-159.787],[29.31,-155.951]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.257,0],[0.364,1.548],[-1.808,0.428],[-9.13,1.562],[-0.312,-1.832],[1.831,-0.313],[8.92,-2.104]],"o":[[-1.525,0],[-0.424,-1.808],[9.054,-2.133],[1.874,-0.296],[0.316,1.834],[-8.995,1.538],[-0.26,0.059]],"v":[[-146.325,-146.301],[-149.6,-148.895],[-147.095,-152.946],[-119.689,-158.515],[-115.803,-155.766],[-118.55,-151.88],[-145.549,-146.39]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[1.421,0],[0.345,0.112],[8.856,2.282],[-0.463,1.798],[-1.773,-0.461],[-8.82,-2.86],[0.571,-1.768]],"o":[[-0.341,0],[-8.689,-2.818],[-1.802,-0.463],[0.464,-1.802],[8.991,2.317],[1.77,0.572],[-0.461,1.424]],"v":[[96.795,-136.3],[95.757,-136.464],[69.311,-144.148],[66.888,-148.247],[70.988,-150.67],[97.835,-142.869],[100,-138.628]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.43,0],[0.539,1.296],[-1.716,0.717],[-8.798,3.032],[-0.605,-1.759],[1.756,-0.605],[8.426,-3.505]],"o":[[-1.319,0],[-0.713,-1.715],[8.551,-3.554],[1.765,-0.601],[0.605,1.759],[-8.666,2.988],[-0.421,0.174]],"v":[[-215.586,-123.912],[-218.697,-125.987],[-216.879,-130.389],[-190.731,-140.318],[-186.451,-138.233],[-188.536,-133.952],[-214.294,-124.168]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[1.22,0],[0.494,0.256],[8.374,3.682],[-0.75,1.704],[-1.706,-0.756],[-8.238,-4.251],[0.852,-1.651]],"o":[[-0.519,0],[-8.117,-4.185],[-1.703,-0.747],[0.743,-1.703],[8.499,3.735],[1.655,0.852],[-0.598,1.16]],"v":[[163.963,-108.306],[162.424,-108.68],[137.574,-120.535],[135.847,-124.971],[140.283,-126.697],[165.508,-114.664],[166.959,-110.13]],"c":true},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ind":6,"ty":"sh","ix":7,"ks":{"a":0,"k":{"i":[[0.609,0],[0.638,1.026],[-1.578,0.983],[-8.177,4.416],[-0.881,-1.63],[1.634,-0.884],[7.759,-4.829]],"o":[[-1.124,0],[-0.983,-1.578],[7.874,-4.901],[1.643,-0.891],[0.884,1.638],[-8.058,4.349],[-0.552,0.345]],"v":[[-280.312,-90.578],[-283.172,-92.166],[-282.093,-96.802],[-257.902,-110.847],[-253.34,-109.486],[-254.7,-104.922],[-278.536,-91.088]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ind":7,"ty":"sh","ix":8,"ks":{"a":0,"k":{"i":[[1.029,0],[0.605,0.447],[7.656,4.985],[-1.016,1.558],[-1.569,-1.016],[-7.45,-5.537],[1.108,-1.493]],"o":[[-0.698,0],[-7.338,-5.455],[-1.559,-1.012],[1.012,-1.556],[7.769,5.059],[1.493,1.108],[-0.661,0.888]],"v":[[225.708,-69.803],[223.701,-70.467],[201.106,-86.196],[200.122,-90.854],[204.781,-91.837],[227.719,-75.872],[228.412,-71.161]],"c":true},"ix":2},"nm":"Path 8","mn":"ADBE Vector Shape - Group","hd":false},{"ind":8,"ty":"sh","ix":9,"ks":{"a":0,"k":{"i":[[0.785,0],[0.665,0.759],[-1.4,1.222],[-7.338,5.665],[-1.135,-1.467],[1.47,-1.137],[6.901,-6.03]],"o":[[-0.938,0],[-1.222,-1.4],[7.007,-6.119],[1.483,-1.131],[1.134,1.473],[-7.233,5.579],[-0.638,0.559]],"v":[[-338.812,-47.223],[-341.348,-48.374],[-341.029,-53.124],[-319.413,-70.882],[-314.687,-70.273],[-315.296,-65.549],[-336.597,-48.055]],"c":true},"ix":2},"nm":"Path 9","mn":"ADBE Vector Shape - Group","hd":false},{"ind":9,"ty":"sh","ix":10,"ks":{"a":0,"k":{"i":[[0.844,0],[0.66,0.681],[6.776,6.178],[-1.253,1.374],[-1.371,-1.256],[-6.437,-6.638],[1.335,-1.295]],"o":[[-0.878,0],[-6.343,-6.539],[-1.374,-1.252],[1.252,-1.381],[6.878,6.273],[1.295,1.335],[-0.654,0.635]],"v":[[280.414,-21.832],[277.998,-22.855],[258.229,-42.019],[258.008,-46.776],[262.765,-46.996],[282.83,-27.543],[282.757,-22.783]],"c":true},"ix":2},"nm":"Path 10","mn":"ADBE Vector Shape - Group","hd":false},{"ind":10,"ty":"sh","ix":11,"ks":{"a":0,"k":{"i":[[0.97,0],[0.628,0.523],[-1.187,1.431],[-6.371,6.79],[-1.361,-1.272],[1.272,-1.355],[5.82,-7.01]],"o":[[-0.756,0],[-1.43,-1.186],[5.908,-7.114],[1.273,-1.357],[1.354,1.276],[-6.283,6.69],[-0.667,0.802]],"v":[[-389.644,4.88],[-391.791,4.104],[-392.231,-0.637],[-373.725,-21.593],[-368.964,-21.744],[-368.814,-16.983],[-387.05,3.664]],"c":true},"ix":2},"nm":"Path 11","mn":"ADBE Vector Shape - Group","hd":false},{"ind":11,"ty":"sh","ix":12,"ks":{"a":0,"k":{"i":[[0.668,0],[0.655,0.937],[5.701,7.164],[-1.453,1.157],[-1.158,-1.453],[-5.316,-7.595],[1.525,-1.068]],"o":[[-1.061,0],[-5.237,-7.479],[-1.157,-1.456],[1.46,-1.164],[5.786,7.276],[1.064,1.522],[-0.585,0.411]],"v":[[326.809,34.262],[324.047,32.825],[307.564,10.755],[308.099,6.024],[312.831,6.56],[329.565,28.966],[328.736,33.654]],"c":true},"ix":2},"nm":"Path 12","mn":"ADBE Vector Shape - Group","hd":false},{"ind":12,"ty":"sh","ix":13,"ks":{"a":0,"k":{"i":[[0.49,0],[0.579,1.203],[4.458,7.963],[-1.624,0.911],[-0.908,-1.621],[-4.03,-8.384],[1.674,-0.806]],"o":[[-1.256,0],[-3.972,-8.259],[-0.907,-1.621],[1.624,-0.901],[4.521,8.081],[0.806,1.677],[-0.47,0.227]],"v":[[363.576,97.09],[360.538,95.183],[347.837,70.736],[349.133,66.153],[353.716,67.448],[366.607,92.264],[365.033,96.758]],"c":true},"ix":2},"nm":"Path 13","mn":"ADBE Vector Shape - Group","hd":false},{"ind":13,"ty":"sh","ix":14,"ks":{"a":0,"k":{"i":[[0.312,0],[0.431,1.469],[3.109,8.6],[-1.749,0.634],[-0.632,-1.749],[-2.613,-8.91],[1.785,-0.523]],"o":[[-1.457,0],[-2.571,-8.775],[-0.632,-1.747],[1.742,-0.639],[3.159,8.726],[0.523,1.785],[-0.317,0.092]],"v":[[389.664,165.044],[386.432,162.624],[377.868,136.441],[379.889,132.131],[384.201,134.152],[392.896,160.73],[390.611,164.909]],"c":true},"ix":2},"nm":"Path 14","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[487.88,165.294],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":16,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.118,0],[0.559,0.353],[-0.993,1.572],[-2.525,3.758],[-1.542,-1.042],[1.036,-1.542],[2.37,-3.762]],"o":[[-0.615,0],[-1.571,-0.989],[2.404,-3.813],[1.035,-1.542],[1.545,1.039],[-2.492,3.706],[-0.639,1.015]],"v":[[-3.669,9.241],[-5.461,8.722],[-6.513,4.08],[0.881,-7.282],[5.553,-8.199],[6.47,-3.528],[-0.818,7.671]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[67.549,208.315],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.256,0],[0.364,1.552],[0.901,4.425],[-1.821,0.369],[-0.368,-1.828],[-1.019,-4.34],[1.809,-0.424]],"o":[[-1.529,0],[-1.033,-4.406],[-0.371,-1.822],[1.819,-0.368],[0.888,4.36],[0.424,1.812],[-0.259,0.06]],"v":[[1.422,10.09],[-1.852,7.493],[-4.756,-5.753],[-2.129,-9.722],[1.839,-7.095],[4.703,5.954],[2.194,10.001]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[5.377,550.784],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.424,0],[0.533,1.309],[2.824,8.337],[-1.762,0.595],[-0.598,-1.762],[-3.285,-8.085],[1.722,-0.7]],"o":[[-1.329,0],[-3.331,-8.203],[-0.595,-1.759],[1.746,-0.595],[2.782,8.209],[0.7,1.722],[-0.414,0.168]],"v":[[-389.817,-124.474],[-392.937,-126.574],[-402.212,-151.501],[-400.101,-155.77],[-395.834,-153.658],[-386.697,-129.105],[-388.551,-124.72]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.589,0],[0.631,1.052],[4.074,7.795],[-1.65,0.862],[-0.862,-1.65],[-4.494,-7.487],[1.594,-0.957]],"o":[[-1.144,0],[-4.563,-7.598],[-0.858,-1.647],[1.644,-0.857],[4.011,7.677],[0.957,1.595],[-0.543,0.326]],"v":[[-358.867,-62.533],[-361.757,-64.167],[-374.773,-87.365],[-373.346,-91.909],[-368.802,-90.482],[-355.984,-67.633],[-357.137,-63.013]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[1.003,0],[0.615,0.48],[-1.141,1.467],[-4.912,7.137],[-1.526,-1.052],[1.053,-1.528],[5.444,-6.989]],"o":[[-0.724,0],[-1.466,-1.141],[5.362,-6.888],[1.059,-1.532],[1.531,1.055],[-4.988,7.25],[-0.664,0.851]],"v":[[383.376,-17.693],[381.311,-18.403],[380.723,-23.127],[396.205,-44.263],[400.887,-45.132],[401.754,-40.45],[386.036,-18.991]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0.76,0],[0.667,0.799],[5.254,7.108],[-1.493,1.108],[-1.105,-1.49],[-5.559,-6.674],[1.431,-1.19]],"o":[[-0.966,0],[-5.642,-6.773],[-1.105,-1.496],[1.506,-1.105],[5.175,7.006],[1.19,1.427],[-0.628,0.523]],"v":[[-318.754,-6.094],[-321.344,-7.306],[-337.763,-28.227],[-337.056,-32.938],[-332.345,-32.23],[-316.17,-11.614],[-316.604,-6.873]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.829,0],[0.664,0.7],[-1.352,1.276],[-5.961,6.316],[-1.352,-1.269],[1.277,-1.351],[6.427,-6.066]],"o":[[-0.893,0],[-1.275,-1.355],[6.335,-5.977],[1.279,-1.348],[1.351,1.279],[-6.052,6.407],[-0.652,0.611]],"v":[[336.779,33.507],[334.33,32.451],[334.469,27.69],[353.001,9.167],[357.762,9.03],[357.899,13.791],[339.091,32.59]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0.931,0],[0.641,0.565],[6.276,6.244],[-1.312,1.318],[-1.318,-1.313],[-6.493,-5.747],[1.233,-1.394]],"o":[[-0.795,0],[-6.589,-5.833],[-1.318,-1.312],[1.312,-1.315],[6.185,6.147],[1.391,1.233],[-0.668,0.75]],"v":[[-270.543,43.613],[-272.775,42.769],[-292.166,24.571],[-292.179,19.811],[-287.419,19.797],[-268.311,37.724],[-268.021,42.479]],"c":true},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ind":6,"ty":"sh","ix":7,"ks":{"a":0,"k":{"i":[[0.657,0],[0.655,0.947],[-1.529,1.056],[-6.862,5.352],[-1.144,-1.467],[1.466,-1.144],[7.269,-5.017]],"o":[[-1.069,0],[-1.054,-1.532],[7.164,-4.941],[1.465,-1.141],[1.141,1.466],[-6.967,5.427],[-0.586,0.401]],"v":[[282.921,77.082],[280.145,75.628],[281.004,70.944],[302.141,55.432],[306.868,56.018],[306.283,60.745],[284.831,76.487]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ind":7,"ty":"sh","ix":8,"ks":{"a":0,"k":{"i":[[1.108,0],[0.562,0.361],[7.13,5.188],[-1.095,1.506],[-1.502,-1.091],[-7.325,-4.698],[1.003,-1.565]],"o":[[-0.621,0],[-7.433,-4.767],[-1.503,-1.094],[1.098,-1.503],[7.019,5.112],[1.565,1.002],[-0.644,1.003]],"v":[[-215.349,85.356],[-217.164,84.824],[-239.112,69.819],[-239.852,65.115],[-235.148,64.375],[-213.528,79.156],[-212.512,83.809]],"c":true},"ix":2},"nm":"Path 8","mn":"ADBE Vector Shape - Group","hd":false},{"ind":8,"ty":"sh","ix":9,"ks":{"a":0,"k":{"i":[[0.49,0],[0.582,1.206],[-1.677,0.809],[-7.607,4.238],[-0.907,-1.624],[1.624,-0.904],[7.953,-3.83]],"o":[[-1.253,0],[-0.806,-1.674],[7.831,-3.771],[1.634,-0.901],[0.905,1.624],[-7.723,4.3],[-0.47,0.227]],"v":[[223.052,111.882],[220.014,109.976],[221.589,105.48],[244.855,93.411],[249.435,94.717],[248.13,99.296],[224.508,111.55]],"c":true},"ix":2},"nm":"Path 9","mn":"ADBE Vector Shape - Group","hd":false},{"ind":9,"ty":"sh","ix":10,"ks":{"a":0,"k":{"i":[[1.295,0],[0.441,0.194],[7.835,4.021],[-0.849,1.654],[-1.66,-0.849],[-7.973,-3.515],[0.749,-1.7]],"o":[[-0.454,0],[-8.094,-3.567],[-1.654,-0.848],[0.851,-1.647],[7.716,3.961],[1.703,0.749],[-0.556,1.259]],"v":[[-154.369,118.093],[-155.724,117.806],[-179.727,106.368],[-181.183,101.834],[-176.65,100.378],[-153.008,111.646],[-151.285,116.083]],"c":true},"ix":2},"nm":"Path 10","mn":"ADBE Vector Shape - Group","hd":false},{"ind":10,"ty":"sh","ix":11,"ks":{"a":0,"k":{"i":[[0.322,0],[0.441,1.454],[-1.779,0.539],[-8.146,3.001],[-0.643,-1.746],[1.746,-0.645],[8.465,-2.565]],"o":[[-1.446,0],[-0.54,-1.778],[8.34,-2.529],[1.739,-0.641],[0.642,1.746],[-8.268,3.047],[-0.327,0.098]],"v":[[158.534,137.023],[155.312,134.633],[157.558,130.435],[182.402,122.101],[186.725,124.095],[184.73,128.419],[159.511,136.878]],"c":true},"ix":2},"nm":"Path 11","mn":"ADBE Vector Shape - Group","hd":false},{"ind":11,"ty":"sh","ix":12,"ks":{"a":0,"k":{"i":[[1.485,0],[0.289,0.079],[8.377,2.771],[-0.585,1.766],[-1.778,-0.579],[-8.403,-2.246],[0.48,-1.795]],"o":[[-0.289,0],[-8.532,-2.279],[-1.766,-0.582],[0.582,-1.762],[8.252,2.728],[1.799,0.48],[-0.401,1.506]],"v":[[-89.049,141.051],[-89.92,140.936],[-115.4,133.324],[-117.537,129.07],[-113.283,126.934],[-88.185,134.432],[-85.801,138.552]],"c":true},"ix":2},"nm":"Path 12","mn":"ADBE Vector Shape - Group","hd":false},{"ind":12,"ty":"sh","ix":13,"ks":{"a":0,"k":{"i":[[0.154,0],[0.237,1.679],[-1.842,0.259],[-8.522,1.716],[-0.368,-1.821],[1.822,-0.368],[8.746,-1.233]],"o":[[-1.654,0],[-0.26,-1.842],[8.616,-1.213],[1.805,-0.366],[0.364,1.822],[-8.65,1.743],[-0.161,0.023]],"v":[[90.913,151.932],[87.582,149.036],[90.446,145.232],[116.274,140.817],[120.239,143.453],[117.602,147.418],[91.386,151.899]],"c":true},"ix":2},"nm":"Path 13","mn":"ADBE Vector Shape - Group","hd":false},{"ind":13,"ty":"sh","ix":14,"ks":{"a":0,"k":{"i":[[1.696,0],[0.122,0.014],[8.686,1.456],[-0.306,1.831],[-1.824,-0.318],[-8.663,-0.937],[0.201,-1.848]],"o":[[-0.122,0],[-8.794,-0.953],[-1.834,-0.31],[0.306,-1.835],[8.562,1.437],[1.848,0.2],[-0.187,1.726]],"v":[[-20.99,153.738],[-21.356,153.718],[-47.7,150.088],[-50.465,146.212],[-46.589,143.447],[-20.632,147.024],[-17.647,150.732]],"c":true},"ix":2},"nm":"Path 14","mn":"ADBE Vector Shape - Group","hd":false},{"ind":14,"ty":"sh","ix":15,"ks":{"a":0,"k":{"i":[[6.848,0],[1.989,0.027],[-0.022,1.861],[-1.838,0],[0,0],[-8.693,0.394],[-0.082,-1.86],[1.858,-0.085]],"o":[[-1.989,0],[-1.857,-0.026],[0.027,-1.841],[0,0],[8.709,0.119],[1.732,-0.119],[0.085,1.858],[-6.839,0.309]],"v":[[27.733,156.365],[21.769,156.324],[18.448,152.912],[21.815,149.592],[21.861,149.592],[48.058,149.171],[51.572,152.383],[48.36,155.897]],"c":true},"ix":2},"nm":"Path 15","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[417.68,751.499],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":17,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.184,0],[0.517,0.289],[-0.904,1.624],[-2.039,3.952],[-1.66,-0.848],[0.851,-1.653],[2.2,-3.959]],"o":[[-0.552,0],[-1.627,-0.904],[2.167,-3.899],[0.852,-1.65],[1.651,0.852],[-2.072,4.012],[-0.615,1.107]],"v":[[-3.179,9.536],[-4.811,9.112],[-6.119,4.536],[0.188,-7.241],[4.723,-8.688],[6.171,-4.151],[-0.234,7.804]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[842.422,666.525],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Lock","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1896.529,2431.411,0],"ix":2,"l":2},"a":{"a":0,"k":[86.599,112.545,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.757,0],[0,0],[-0.878,3.654],[0,0],[-0.536,10.15],[-13.139,0.816],[0,-15.022],[7.652,-4.574]],"o":[[0.878,3.653],[0,0],[-3.758,0],[0,0],[-8.028,-4.799],[0.694,-13.148],[15.227,-0.946],[0,9.563],[0,0]],"v":[[20.222,34.92],[14.573,42.088],[-14.573,42.088],[-20.222,34.919],[-13.422,6.61],[-26.161,-17.308],[-1.673,-42.035],[26.198,-15.89],[13.421,6.61]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.661,0],[0,0],[0,-8.66],[0,0],[-8.661,0],[0,0],[0,8.661],[0,0]],"o":[[0,0],[-8.661,0],[0,0],[0,8.661],[0,0],[8.661,0],[0,0],[0,-8.66]],"v":[[70.667,-69.917],[-70.667,-69.917],[-86.349,-54.234],[-86.349,54.234],[-70.667,69.917],[70.667,69.917],[86.349,54.234],[86.349,-54.234]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.599,154.925],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.151,0],[0,0],[0,4.152],[0,0],[10.23,0],[0,0],[0,-10.229],[0,0],[4.152,0],[0,0],[0,4.152],[0,0],[-23.151,0],[0,0],[0,-23.151],[0,0]],"o":[[0,0],[-4.152,0],[0,0],[0,-10.229],[0,0],[-10.23,0],[0,0],[0,4.152],[0,0],[-4.151,0],[0,0],[0,-23.151],[0,0],[23.151,0],[0,0],[0,4.152]],"v":[[60.304,39.006],[51.943,39.006],[44.425,31.49],[44.425,2.911],[25.903,-15.61],[-25.904,-15.61],[-44.426,2.911],[-44.426,31.49],[-51.944,39.006],[-60.305,39.006],[-67.822,31.49],[-67.822,2.911],[-25.904,-39.006],[25.903,-39.006],[67.822,2.911],[67.822,31.49]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.6,39.257],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"loading Line 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1667.146,2764.849,0],"ix":2,"l":2},"a":{"a":0,"k":[-0.004,24.662,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-60,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-15,"s":[0,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":75,"s":[0,100,100]},{"t":120,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.946,0],[0,0],[0,2.946],[0,0],[-2.946,0],[0,0],[0,-2.946],[0,0]],"o":[[0,0],[-2.946,0],[0,0],[0,-2.946],[0,0],[2.946,0],[0,0],[0,2.946]],"v":[[157.29,24.661],[-157.295,24.661],[-162.628,19.327],[-162.628,-19.328],[-157.295,-24.661],[157.29,-24.661],[162.624,-19.328],[162.624,19.327]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[162.624,24.662],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"loading Line 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1667.146,2684.435,0],"ix":2,"l":2},"a":{"a":0,"k":[-0.004,24.665,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-75,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-30,"s":[0,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[0,100,100]},{"t":105,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.946,0],[0,0],[0,2.945],[0,0],[-2.945,0],[0,0],[0,-2.946],[0,0]],"o":[[0,0],[-2.945,0],[0,0],[0,-2.946],[0,0],[2.946,0],[0,0],[0,2.945]],"v":[[269.114,24.661],[-269.12,24.661],[-274.453,19.328],[-274.453,-19.328],[-269.12,-24.661],[269.114,-24.661],[274.448,-19.328],[274.448,19.328]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[274.448,24.665],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"loading Line 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1665.449,2605.613,0],"ix":2,"l":2},"a":{"a":0,"k":[-0.001,24.663,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-90,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-45,"s":[0,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":45,"s":[0,100,100]},{"t":90,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.945,0],[0,0],[0,2.945],[0,0],[-2.946,0],[0,0],[0,-2.946],[0,0]],"o":[[0,0],[-2.946,0],[0,0],[0,-2.946],[0,0],[2.945,0],[0,0],[0,2.945]],"v":[[269.117,24.663],[-269.117,24.663],[-274.451,19.33],[-274.451,-19.326],[-269.117,-24.659],[269.117,-24.659],[274.45,-19.326],[274.45,19.33]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[274.45,24.661],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"Doc Line 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1022.39,2843.612,0],"ix":2,"l":2},"a":{"a":0,"k":[0,24.661,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.945,0],[0,0],[0,2.945],[0,0],[-2.945,0],[0,0],[0,-2.946],[0,0]],"o":[[0,0],[-2.945,0],[0,0],[0,-2.946],[0,0],[2.945,0],[0,0],[0,2.945]],"v":[[185.561,24.661],[-185.562,24.661],[-190.895,19.328],[-190.895,-19.328],[-185.562,-24.661],[185.561,-24.661],[190.894,-19.328],[190.894,19.328]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[190.894,24.661],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"Doc Line 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1021.638,2764.849,0],"ix":2,"l":2},"a":{"a":0,"k":[-0.002,24.662,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.945,0],[0,0],[0,2.946],[0,0],[-2.945,0],[0,0],[0,-2.946],[0,0]],"o":[[0,0],[-2.945,0],[0,0],[0,-2.946],[0,0],[2.945,0],[0,0],[0,2.946]],"v":[[185.561,24.661],[-185.562,24.661],[-190.896,19.327],[-190.896,-19.328],[-185.562,-24.661],[185.561,-24.661],[190.894,-19.328],[190.894,19.327]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[190.894,24.662],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":25,"ty":4,"nm":"Doc Line 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1019.402,2684.435,0],"ix":2,"l":2},"a":{"a":0,"k":[0.002,24.665,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.946,0],[0,0],[0,2.945],[0,0],[-2.945,0],[0,0],[0,-2.946],[0,0]],"o":[[0,0],[-2.945,0],[0,0],[0,-2.946],[0,0],[2.946,0],[0,0],[0,2.945]],"v":[[185.562,24.661],[-185.561,24.661],[-190.894,19.328],[-190.894,-19.328],[-185.561,-24.661],[185.562,-24.661],[190.895,-19.328],[190.895,19.328]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[190.896,24.665],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"Doc Line 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1018.651,2605.671,0],"ix":2,"l":2},"a":{"a":0,"k":[0.001,24.661,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.946,0],[0,0],[0,2.945],[0,0],[-2.945,0],[0,0],[0,-2.946],[0,0]],"o":[[0,0],[-2.945,0],[0,0],[0,-2.946],[0,0],[2.946,0],[0,0],[0,2.945]],"v":[[376.457,49.322],[5.334,49.322],[0.001,43.989],[0.001,5.333],[5.334,0],[376.457,0],[381.79,5.333],[381.79,43.989]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":27,"ty":4,"nm":"Document Bg","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1226.408,2697.161,0],"ix":2,"l":2},"a":{"a":0,"k":[317.795,404.108,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[20.658,0],[0,0],[0,20.658],[0,0],[-21.204,-0.811],[0,0],[0,-20.101],[0,0]],"o":[[0,0],[-20.658,0],[0,0],[0,-21.219],[0,0],[20.087,0.769],[0,0],[0,20.658]],"v":[[280.141,403.467],[-280.14,403.467],[-317.545,366.063],[-317.545,-365.277],[-278.71,-402.655],[281.571,-381.215],[317.546,-343.837],[317.546,366.063]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[317.795,403.716],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":28,"ty":4,"nm":"Laptop","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1619.189,2712.431,0],"ix":2,"l":2},"a":{"a":0,"k":[895.075,527.973,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-3.109,0],[0,0],[0,3.109],[0,0],[3.111,0],[0,0],[0,-3.11],[0,0]],"o":[[0,0],[3.11,0],[0,0],[0,-3.11],[0,0],[-3.109,0],[0,0],[0,3.109]],"v":[[-889.171,38.323],[889.171,38.323],[894.825,32.669],[894.825,-32.671],[889.171,-38.324],[-889.171,-38.324],[-894.825,-32.671],[-894.825,32.669]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[895.075,1016.896],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.121,0],[0,0],[-0.033,2.147],[0,0],[8.681,8.317],[1.084,-0.051],[0,0],[-0.058,2.148],[-2.288,-0.194],[0,0],[-18.241,-17.477],[-0.078,-16.875],[0,0]],"o":[[0,0],[-2.15,-0.031],[0,0],[-0.068,-14.611],[-16.203,-15.524],[0,0],[-2.148,-0.054],[0.056,-2.148],[0,0],[2.826,-0.167],[10.262,9.833],[0,0],[-0.032,2.129]],"v":[[253.636,159.681],[253.577,159.681],[249.743,155.731],[253.354,-90.248],[240.171,-124.842],[197.158,-139.523],[-257.295,-151.71],[-261.08,-155.703],[-257.085,-159.487],[197.098,-147.3],[245.556,-130.464],[261.138,-90.21],[257.525,155.847]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[1392.359,234.571],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.121,0],[0,0],[-0.033,2.152],[0,0],[-2.121,0],[0,0],[0.032,-2.151],[0,0]],"o":[[0,0],[-2.17,-0.031],[0,0],[0.03,-2.129],[0,0],[2.169,0.031],[0,0],[-0.032,2.129]],"v":[[-0.468,36.004],[-0.507,36.004],[-4.361,32.053],[-3.419,-32.17],[0.47,-36.004],[0.508,-36.004],[4.362,-32.055],[3.421,32.169]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[1644.152,515.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-14.606,0],[0,0],[0,14.605],[0,0],[25.327,-1.012],[0,0],[9.639,-9.614],[-0.341,-13.61]],"o":[[0,14.605],[0,0],[14.607,0],[0,0],[-1.404,-47.656],[0,0],[-13.61,-0.364],[-9.639,9.615],[0,0]],"v":[[-749.365,433.825],[-722.808,460.383],[732.375,460.383],[758.933,433.825],[770.852,-358.1],[702.34,-421.872],[-719.505,-460.019],[-755.912,-445.526],[-770.511,-409.143]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.7490196078431373,0.12156862745098039,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[900.476,494.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-15.439,0],[0,0],[0,15.424],[0,0],[33.304,-2.263],[0,0],[13.718,-13.673],[-0.458,-19.355]],"o":[[0,15.424],[0,0],[15.438,0],[0,0],[0,-15.423],[0,0],[-19.371,-0.501],[-13.717,13.674],[0,0]],"v":[[-782.729,463.232],[-754.658,491.276],[763.332,491.276],[791.403,463.232],[804.035,-409.116],[752.557,-452.389],[-730.99,-490.775],[-782.811,-470.148],[-803.577,-418.425]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[906.126,491.527],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":29,"ty":4,"nm":"Cloud","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[1855,2079.442,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[1855,2129.442,0],"to":[0,0,0],"ti":[0,0,0]},{"t":90,"s":[1855,2079.442,0]}],"ix":2,"l":2},"a":{"a":0,"k":[1457.664,834.54,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-72.82,310.808],[-163.923,40.348],[-91.391,-44.95],[-8.398,33.377],[-338.381,0],[0,-411.403],[0.187,-14.317],[-19.556,-3.28],[-6.856,-163.896],[183.708,0],[0,0]],"o":[[39.377,-168.069],[112.699,-27.742],[30.312,14.908],[80.789,-321.076],[401.242,0],[0,10.921],[1.421,30.764],[157.204,16.775],[7.871,188.186],[0,0],[-290.889,0]],"v":[[-1202.679,156.969],[-870.615,-183.385],[-558.399,-151.289],[-483.669,-186.636],[219.832,-744.911],[946.344,-0.001],[945.944,40.154],[980.021,87.262],[1267.629,400.812],[942.41,744.911],[-756.852,744.911]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.379145573635,0.416115405513,0.963032382142,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[1431.885,923.919],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-81.558,348.1],[-183.592,45.19],[-102.357,-50.344],[-9.407,37.383],[-378.982,0],[0,-460.765],[0.208,-16.034],[-21.901,-3.674],[-7.678,-183.561],[205.75,0],[0,0]],"o":[[44.102,-188.235],[126.221,-31.069],[33.948,16.697],[90.482,-359.599],[449.386,0],[0,12.232],[1.591,34.456],[176.069,18.789],[8.817,210.766],[0,0],[-325.792,0]],"v":[[-1346.983,175.804],[-975.075,-205.389],[-625.399,-169.442],[-541.701,-209.03],[246.208,-834.291],[1059.891,0],[1059.443,44.973],[1097.609,97.731],[1419.724,448.905],[1055.484,834.291],[-847.662,834.291]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.21568627450980393,0.7568627450980392,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[1428.791,834.541],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":30,"ty":4,"nm":"Background Shape","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1876.955,1891.635,0],"ix":2,"l":2},"a":{"a":0,"k":[1473.364,1375.013,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":45,"s":[110,110,100]},{"t":90,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[168.339,-402.166],[596.684,93.25],[235.668,213.24],[-365.966,572.834],[-453.695,-11.821],[-329.144,-275.374],[-96.098,-152.478]],"o":[[-258.302,617.093],[-97.093,-15.175],[-453.675,-410.503],[266.36,-416.923],[97.727,2.547],[48.346,40.447],[225.657,358.046]],"v":[[1262.172,445.366],[-280.871,1314.907],[-879.814,1041.292],[-1064.545,-754.895],[132.986,-1396.336],[934.384,-1102.721],[1185.457,-813.682]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9450980392156862,0.9333333333333333,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[1430.761,1408.407],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js new file mode 100644 index 00000000..2f658e91 --- /dev/null +++ b/frontend/public/mockServiceWorker.js @@ -0,0 +1,349 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.11.6' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise<Client | undefined>} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise<Response>} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array<Transferable>} transferrables + * @returns {Promise<any>} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/frontend/services/auth.service.ts b/frontend/services/auth.service.ts new file mode 100644 index 00000000..45221f49 --- /dev/null +++ b/frontend/services/auth.service.ts @@ -0,0 +1,44 @@ +/** + * 认证服务 + */ +import { api } from '@/lib/api-client' +import type { + LoginRequest, + LoginResponse, + MeResponse, + LogoutResponse, + ChangePasswordRequest, + ChangePasswordResponse +} from '@/types/auth.types' + +/** + * 用户登录 + */ +export async function login(data: LoginRequest): Promise<LoginResponse> { + const res = await api.post<LoginResponse>('/auth/login/', data) + return res.data +} + +/** + * 用户登出 + */ +export async function logout(): Promise<LogoutResponse> { + const res = await api.post<LogoutResponse>('/auth/logout/') + return res.data +} + +/** + * 获取当前用户信息 + */ +export async function getMe(): Promise<MeResponse> { + const res = await api.get<MeResponse>('/auth/me/') + return res.data +} + +/** + * 修改密码 + */ +export async function changePassword(data: ChangePasswordRequest): Promise<ChangePasswordResponse> { + const res = await api.post<ChangePasswordResponse>('/auth/change-password/', data) + return res.data +} diff --git a/frontend/services/command.service.ts b/frontend/services/command.service.ts new file mode 100644 index 00000000..f53c5802 --- /dev/null +++ b/frontend/services/command.service.ts @@ -0,0 +1,89 @@ +import { api } from "@/lib/api-client" +import type { + Command, + GetCommandsRequest, + GetCommandsResponse, + CreateCommandRequest, + UpdateCommandRequest, + CommandResponseData, + BatchDeleteCommandsResponseData, +} from "@/types/command.types" + +/** + * 命令服务 + */ +export class CommandService { + /** + * 获取命令列表 + */ + static async getCommands( + params: GetCommandsRequest = {} + ): Promise<GetCommandsResponse> { + const response = await api.get<GetCommandsResponse>( + "/commands/", + { params } + ) + return response.data + } + + /** + * 获取单个命令 + */ + static async getCommandById(id: number): Promise<CommandResponseData> { + const response = await api.get<CommandResponseData>( + `/commands/${id}/` + ) + return response.data + } + + /** + * 创建命令 + */ + static async createCommand( + data: CreateCommandRequest + ): Promise<CommandResponseData> { + const response = await api.post<CommandResponseData>( + "/commands/create/", + data + ) + return response.data + } + + /** + * 更新命令 + */ + static async updateCommand( + id: number, + data: UpdateCommandRequest + ): Promise<CommandResponseData> { + const response = await api.put<CommandResponseData>( + `/commands/${id}/`, + data + ) + return response.data + } + + /** + * 删除命令 + */ + static async deleteCommand( + id: number + ): Promise<void> { + await api.delete( + `/commands/${id}/` + ) + } + + /** + * 批量删除命令 + */ + static async batchDeleteCommands( + ids: number[] + ): Promise<BatchDeleteCommandsResponseData> { + const response = await api.post<BatchDeleteCommandsResponseData>( + "/commands/batch-delete/", + { ids } + ) + return response.data + } +} diff --git a/frontend/services/dashboard.service.ts b/frontend/services/dashboard.service.ts new file mode 100644 index 00000000..7e487ba9 --- /dev/null +++ b/frontend/services/dashboard.service.ts @@ -0,0 +1,25 @@ +import { api } from '@/lib/api-client' +import type { DashboardStats, AssetStatistics, StatisticsHistoryItem } from '@/types/dashboard.types' + +export async function getDashboardStats(): Promise<DashboardStats> { + const res = await api.get<DashboardStats>('/dashboard/stats/') + return res.data +} + +/** + * 获取资产统计数据(预聚合) + */ +export async function getAssetStatistics(): Promise<AssetStatistics> { + const res = await api.get<AssetStatistics>('/assets/statistics/') + return res.data +} + +/** + * 获取统计历史数据(用于折线图) + */ +export async function getStatisticsHistory(days: number = 7): Promise<StatisticsHistoryItem[]> { + const res = await api.get<StatisticsHistoryItem[]>('/assets/statistics/history/', { + params: { days } + }) + return res.data +} diff --git a/frontend/services/directory.service.ts b/frontend/services/directory.service.ts new file mode 100644 index 00000000..28dcdf95 --- /dev/null +++ b/frontend/services/directory.service.ts @@ -0,0 +1,20 @@ +import { api } from "@/lib/api-client" + +/** 目录相关 API 服务 */ +export class DirectoryService { + /** 按目标导出所有目录 URL(文本文件,一行一个) */ + static async exportDirectoriesByTargetId(targetId: number): Promise<Blob> { + const response = await api.get<Blob>(`/targets/${targetId}/directories/export/`, { + responseType: "blob", + }) + return response.data + } + + /** 按扫描任务导出所有目录 URL(文本文件,一行一个) */ + static async exportDirectoriesByScanId(scanId: number): Promise<Blob> { + const response = await api.get<Blob>(`/scans/${scanId}/directories/export/`, { + responseType: "blob", + }) + return response.data + } +} diff --git a/frontend/services/disk.service.ts b/frontend/services/disk.service.ts new file mode 100644 index 00000000..7a24ac6b --- /dev/null +++ b/frontend/services/disk.service.ts @@ -0,0 +1,7 @@ +import { api } from '@/lib/api-client' +import type { DiskStats } from '@/types/disk.types' + +export async function getDiskStats(): Promise<DiskStats> { + const res = await api.get<DiskStats>('/system/disk-stats/') + return res.data +} diff --git a/frontend/services/endpoint.service.ts b/frontend/services/endpoint.service.ts new file mode 100644 index 00000000..c8378a97 --- /dev/null +++ b/frontend/services/endpoint.service.ts @@ -0,0 +1,115 @@ +import { api } from "@/lib/api-client" +import type { + Endpoint, + CreateEndpointRequest, + UpdateEndpointRequest, + GetEndpointsRequest, + GetEndpointsResponse, + BatchDeleteEndpointsRequest, + BatchDeleteEndpointsResponse +} from "@/types/endpoint.types" + +export class EndpointService { + + /** + * 获取单个 Endpoint 详情 + * @param id - Endpoint ID + * @returns Promise<Endpoint> + */ + static async getEndpointById(id: number): Promise<Endpoint> { + const response = await api.get<Endpoint>(`/endpoints/${id}/`) + return response.data + } + + /** + * 获取 Endpoint 列表 + * @param params - 查询参数 + * @returns Promise<GetEndpointsResponse> + */ + static async getEndpoints(params: GetEndpointsRequest): Promise<GetEndpointsResponse> { + // api-client.ts 会自动将 params 对象的驼峰转换为下划线 + const response = await api.get<GetEndpointsResponse>('/endpoints/', { + params + }) + return response.data + } + + /** + * 根据目标ID获取 Endpoint 列表(专用路由) + * @param targetId - 目标ID + * @param params - 其他查询参数 + * @returns Promise<GetEndpointsResponse> + */ + static async getEndpointsByTargetId(targetId: number, params: GetEndpointsRequest): Promise<GetEndpointsResponse> { + // api-client.ts 会自动将 params 对象的驼峰转换为下划线 + const response = await api.get<GetEndpointsResponse>(`/targets/${targetId}/endpoints/`, { + params + }) + return response.data + } + + /** + * 根据扫描ID获取 Endpoint 列表(历史快照) + * @param scanId - 扫描任务 ID + * @param params - 分页等查询参数 + */ + static async getEndpointsByScanId( + scanId: number, + params: GetEndpointsRequest, + ): Promise<any> { + const response = await api.get(`/scans/${scanId}/endpoints/`, { + params, + }) + return response.data + } + + /** + * 批量创建 Endpoint + * @param data - 创建请求对象 + * @param data.endpoints - Endpoint 数据数组 + * @returns Promise<CreateEndpointsResponse> + */ + static async createEndpoints(data: { endpoints: Array<CreateEndpointRequest> }): Promise<any> { + // api-client.ts 会自动将请求体的驼峰转换为下划线 + const response = await api.post('/endpoints/create/', data) + return response.data + } + + /** + * 删除 Endpoint + * @param id - Endpoint ID + * @returns Promise<void> + */ + static async deleteEndpoint(id: number): Promise<void> { + await api.delete(`/endpoints/${id}/`) + } + + /** + * 批量删除 Endpoint + * @param data - 批量删除请求对象 + * @param data.endpointIds - Endpoint ID 列表 + * @returns Promise<BatchDeleteEndpointsResponse> + */ + static async batchDeleteEndpoints(data: BatchDeleteEndpointsRequest): Promise<BatchDeleteEndpointsResponse> { + // api-client.ts 会自动将请求体的驼峰转换为下划线 + const response = await api.post<BatchDeleteEndpointsResponse>('/endpoints/batch-delete/', data) + return response.data + } + + /** 按目标导出所有端点 URL(文本文件,一行一个) */ + static async exportEndpointsByTargetId(targetId: number): Promise<Blob> { + const response = await api.get<Blob>(`/targets/${targetId}/endpoints/export/`, { + responseType: 'blob', + }) + return response.data + } + + /** 按扫描任务导出所有端点 URL(文本文件,一行一个) */ + static async exportEndpointsByScanId(scanId: number): Promise<Blob> { + const response = await api.get<Blob>(`/scans/${scanId}/endpoints/export/`, { + responseType: 'blob', + }) + return response.data + } + +} diff --git a/frontend/services/engine.service.ts b/frontend/services/engine.service.ts new file mode 100644 index 00000000..07a7ee8b --- /dev/null +++ b/frontend/services/engine.service.ts @@ -0,0 +1,57 @@ +import apiClient from '@/lib/api-client' +import type { ScanEngine } from '@/types/engine.types' + +/** + * 引擎 API 服务 + */ + +/** + * 获取引擎列表 + */ +export async function getEngines(): Promise<ScanEngine[]> { + const response = await apiClient.get('/engines/') + // 后端返回分页数据: { results: [...], total, page, pageSize, totalPages } + // 这里暂时返回 results 数组 + return response.data.results || response.data +} + +/** + * 获取引擎详情 + */ +export async function getEngine(id: number): Promise<ScanEngine> { + const response = await apiClient.get(`/engines/${id}/`) + return response.data +} + +/** + * 创建引擎 + */ +export async function createEngine(data: { + name: string + configuration: string +}): Promise<ScanEngine> { + const response = await apiClient.post('/engines/', data) + return response.data +} + +/** + * 更新引擎 + */ +export async function updateEngine( + id: number, + data: Partial<{ + name: string + configuration: string + }> +): Promise<ScanEngine> { + const response = await apiClient.patch(`/engines/${id}/`, data) + return response.data +} + +/** + * 删除引擎 + */ +export async function deleteEngine(id: number): Promise<void> { + await apiClient.delete(`/engines/${id}/`) +} + diff --git a/frontend/services/ip-address.service.ts b/frontend/services/ip-address.service.ts new file mode 100644 index 00000000..a41efeff --- /dev/null +++ b/frontend/services/ip-address.service.ts @@ -0,0 +1,48 @@ +import { api } from "@/lib/api-client" +import type { GetIPAddressesParams, GetIPAddressesResponse } from "@/types/ip-address.types" + +export class IPAddressService { + static async getTargetIPAddresses( + targetId: number, + params?: GetIPAddressesParams + ): Promise<GetIPAddressesResponse> { + const response = await api.get<GetIPAddressesResponse>(`/targets/${targetId}/ip-addresses/`, { + params: { + page: params?.page || 1, + pageSize: params?.pageSize || 10, + ...(params?.search && { search: params.search }), + }, + }) + return response.data + } + + static async getScanIPAddresses( + scanId: number, + params?: GetIPAddressesParams + ): Promise<GetIPAddressesResponse> { + const response = await api.get<GetIPAddressesResponse>(`/scans/${scanId}/ip-addresses/`, { + params: { + page: params?.page || 1, + pageSize: params?.pageSize || 10, + ...(params?.search && { search: params.search }), + }, + }) + return response.data + } + + /** 按目标导出所有 IP 地址(文本文件,一行一个) */ + static async exportIPAddressesByTargetId(targetId: number): Promise<Blob> { + const response = await api.get<Blob>(`/targets/${targetId}/ip-addresses/export/`, { + responseType: 'blob', + }) + return response.data + } + + /** 按扫描任务导出所有 IP 地址(文本文件,一行一个) */ + static async exportIPAddressesByScanId(scanId: number): Promise<Blob> { + const response = await api.get<Blob>(`/scans/${scanId}/ip-addresses/export/`, { + responseType: 'blob', + }) + return response.data + } +} diff --git a/frontend/services/notification-settings.service.ts b/frontend/services/notification-settings.service.ts new file mode 100644 index 00000000..94bff776 --- /dev/null +++ b/frontend/services/notification-settings.service.ts @@ -0,0 +1,20 @@ +import { api } from '@/lib/api-client' +import type { + GetNotificationSettingsResponse, + UpdateNotificationSettingsRequest, + UpdateNotificationSettingsResponse, +} from '@/types/notification-settings.types' + +export class NotificationSettingsService { + static async getSettings(): Promise<GetNotificationSettingsResponse> { + const res = await api.get<GetNotificationSettingsResponse>('/settings/notifications/') + return res.data + } + + static async updateSettings( + data: UpdateNotificationSettingsRequest + ): Promise<UpdateNotificationSettingsResponse> { + const res = await api.put<UpdateNotificationSettingsResponse>('/settings/notifications/', data) + return res.data + } +} diff --git a/frontend/services/notification.service.ts b/frontend/services/notification.service.ts new file mode 100644 index 00000000..554e20df --- /dev/null +++ b/frontend/services/notification.service.ts @@ -0,0 +1,53 @@ +/** + * 通知服务 + * 处理所有与通知相关的 API 请求 + */ + +import api from '@/lib/api-client' +import type { ApiResponse } from '@/types/api-response.types' +import type { + Notification, + GetNotificationsRequest, + GetNotificationsResponse, +} from '@/types/notification.types' + +export class NotificationService { + /** + * 获取通知列表 + */ + static async getNotifications( + params: GetNotificationsRequest = {} + ): Promise<GetNotificationsResponse> { + const response = await api.get<GetNotificationsResponse | ApiResponse<GetNotificationsResponse>>('/notifications/', { + params, + }) + const payload = response.data + + if ( + payload && + typeof payload === 'object' && + 'data' in payload && + (payload as ApiResponse<GetNotificationsResponse>).data + ) { + return (payload as ApiResponse<GetNotificationsResponse>).data as GetNotificationsResponse + } + + return payload as GetNotificationsResponse + } + + /** + * 标记所有通知为已读 + */ + static async markAllAsRead(): Promise<ApiResponse<null>> { + const response = await api.post<ApiResponse<null>>('/notifications/mark-all-as-read/') + return response.data + } + + /** + * 获取未读通知数量 + */ + static async getUnreadCount(): Promise<ApiResponse<{ count: number }>> { + const response = await api.get<ApiResponse<{ count: number }>>('/notifications/unread-count/') + return response.data + } +} diff --git a/frontend/services/nuclei-git.service.ts b/frontend/services/nuclei-git.service.ts new file mode 100644 index 00000000..9015cc07 --- /dev/null +++ b/frontend/services/nuclei-git.service.ts @@ -0,0 +1,20 @@ +import { api } from "@/lib/api-client" +import type { + GetNucleiGitSettingsResponse, + UpdateNucleiGitSettingsRequest, + UpdateNucleiGitSettingsResponse, +} from "@/types/nuclei-git.types" + +export class NucleiGitService { + static async getSettings(): Promise<GetNucleiGitSettingsResponse> { + const res = await api.get<GetNucleiGitSettingsResponse>("/settings/nuclei-templates-git/") + return res.data + } + + static async updateSettings( + data: UpdateNucleiGitSettingsRequest + ): Promise<UpdateNucleiGitSettingsResponse> { + const res = await api.put<UpdateNucleiGitSettingsResponse>("/settings/nuclei-templates-git/", data) + return res.data + } +} diff --git a/frontend/services/nuclei-repo.api.ts b/frontend/services/nuclei-repo.api.ts new file mode 100644 index 00000000..076eb903 --- /dev/null +++ b/frontend/services/nuclei-repo.api.ts @@ -0,0 +1,113 @@ +/** + * Nuclei 模板仓库 API + */ + +import { api } from "@/lib/api-client" + +const BASE_URL = "/nuclei/repos/" + +export interface NucleiRepoResponse { + id: number + name: string + repoUrl: string + localPath: string + commitHash: string | null + lastSyncedAt: string | null + createdAt: string + updatedAt: string +} + +export interface CreateRepoPayload { + name: string + repoUrl: string +} + +export interface UpdateRepoPayload { + repoUrl?: string +} + +export interface TemplateTreeResponse { + roots: Array<{ + type: "folder" | "file" + name: string + path: string + children?: Array<{ + type: "folder" | "file" + name: string + path: string + children?: unknown[] + }> + }> +} + +export interface TemplateContentResponse { + path: string + name: string + content: string +} + +/** 分页响应格式 */ +interface PaginatedResponse<T> { + results: T[] + total: number + page: number + pageSize: number + totalPages: number +} + +export const nucleiRepoApi = { + /** 获取仓库列表 */ + listRepos: async (): Promise<NucleiRepoResponse[]> => { + const response = await api.get<PaginatedResponse<NucleiRepoResponse>>(BASE_URL) + // 后端返回分页格式,取 results 数组 + return response.data.results + }, + + /** 获取单个仓库 */ + getRepo: async (repoId: number): Promise<NucleiRepoResponse> => { + const response = await api.get<NucleiRepoResponse>(`${BASE_URL}${repoId}/`) + return response.data + }, + + /** 创建仓库 */ + createRepo: async (payload: CreateRepoPayload): Promise<NucleiRepoResponse> => { + const response = await api.post<NucleiRepoResponse>(BASE_URL, payload) + return response.data + }, + + /** 更新仓库 */ + updateRepo: async (repoId: number, payload: UpdateRepoPayload): Promise<NucleiRepoResponse> => { + const response = await api.put<NucleiRepoResponse>(`${BASE_URL}${repoId}/`, payload) + return response.data + }, + + /** 删除仓库 */ + deleteRepo: async (repoId: number): Promise<void> => { + await api.delete(`${BASE_URL}${repoId}/`) + }, + + /** 刷新仓库(Git clone/pull) */ + refreshRepo: async (repoId: number): Promise<{ message: string; result: unknown }> => { + const response = await api.post<{ message: string; result: unknown }>( + `${BASE_URL}${repoId}/refresh/` + ) + return response.data + }, + + /** 获取模板目录树 */ + getTemplateTree: async (repoId: number): Promise<TemplateTreeResponse> => { + const response = await api.get<TemplateTreeResponse>( + `${BASE_URL}${repoId}/templates/tree/` + ) + return response.data + }, + + /** 获取模板内容 */ + getTemplateContent: async (repoId: number, path: string): Promise<TemplateContentResponse> => { + const response = await api.get<TemplateContentResponse>( + `${BASE_URL}${repoId}/templates/content/`, + { params: { path } } + ) + return response.data + }, +} diff --git a/frontend/services/nuclei.service.ts b/frontend/services/nuclei.service.ts new file mode 100644 index 00000000..32d45286 --- /dev/null +++ b/frontend/services/nuclei.service.ts @@ -0,0 +1,40 @@ +import { api } from "@/lib/api-client" +import type { + NucleiTemplateTreeNode, + NucleiTemplateContent, + NucleiTemplateTreeResponse, + UploadNucleiTemplatePayload, + SaveNucleiTemplatePayload, +} from "@/types/nuclei.types" + +export async function getNucleiTemplateTree(): Promise<NucleiTemplateTreeNode[]> { + const response = await api.get<NucleiTemplateTreeResponse>("/nuclei/templates/tree/") + return response.data.roots || [] +} + +export async function getNucleiTemplateContent(path: string): Promise<NucleiTemplateContent> { + const response = await api.get<NucleiTemplateContent>("/nuclei/templates/content/", { + params: { path }, + }) + return response.data +} + +export async function refreshNucleiTemplates(): Promise<void> { + await api.post("/nuclei/templates/refresh/") +} + +export async function saveNucleiTemplate(payload: SaveNucleiTemplatePayload): Promise<void> { + await api.post("/nuclei/templates/save/", payload) +} + +export async function uploadNucleiTemplate(payload: UploadNucleiTemplatePayload): Promise<void> { + const formData = new FormData() + formData.append("scope", payload.scope) + formData.append("file", payload.file) + + await api.post("/nuclei/templates/upload/", formData, { + headers: { + "Content-Type": undefined, + }, + }) +} diff --git a/frontend/services/organization.service.ts b/frontend/services/organization.service.ts new file mode 100644 index 00000000..4b4359c7 --- /dev/null +++ b/frontend/services/organization.service.ts @@ -0,0 +1,183 @@ +import { api } from "@/lib/api-client" +import type { Organization, OrganizationsResponse } from "@/types/organization.types" + + +export class OrganizationService { + // ========== 组织基础操作 ========== + + /** + * 获取组织列表 + * @param params - 查询参数对象 + * @param params.page - 当前页码,1-based + * @param params.pageSize - 分页大小 + * @returns Promise<OrganizationsResponse<Organization>> + * @description 后端固定按更新时间降序排列,不支持自定义排序 + */ + static async getOrganizations(params?: { + page?: number + pageSize?: number + search?: string + }): Promise<OrganizationsResponse<Organization>> { + const response = await api.get<OrganizationsResponse<Organization>>( + '/organizations/', + { params } + ) + return response.data + } + + /** + * 获取单个组织详情 + * @param id - 组织ID + * @returns Promise<Organization> + */ + static async getOrganizationById(id: string | number): Promise<Organization> { + const response = await api.get<Organization>(`/organizations/${id}/`) + return response.data + } + + /** + * 获取组织的目标列表 + * @param id - 组织ID + * @param params - 查询参数 + * @returns Promise<any> + */ + static async getOrganizationTargets(id: string | number, params?: { + page?: number + pageSize?: number + search?: string + }): Promise<any> { + const response = await api.get<any>( + `/organizations/${id}/targets/`, + { params } + ) + return response.data + } + + /** + * 创建新组织 + * @param data - 组织信息对象 + * @param data.name - 组织名称 + * @param data.description - 组织描述 + * @returns Promise<Organization> - 创建成功后的组织信息对象 + */ + static async createOrganization(data: { + name: string + description: string + }): Promise<Organization> { + const response = await api.post<Organization>('/organizations/', data) + return response.data + } + + /** + * 更新组织信息 + * @param data - 组织信息对象 + * @param data.id - 组织ID,number或string类型 + * @param data.name - 组织名称 + * @param data.description - 组织描述 + * @returns Promise<Organization> - 更新成功后的组织信息对象 + */ + static async updateOrganization(data: { + id: string | number + name: string + description: string + }): Promise<Organization> { + const response = await api.put<Organization>(`/organizations/${data.id}/`, { + name: data.name, + description: data.description + }) + return response.data + } + /** + * 删除组织(使用单独的 DELETE API) + * + * @param id - 组织ID,number类型 + * @returns Promise<删除响应> + */ + static async deleteOrganization(id: number): Promise<{ + message: string + organizationId: number + organizationName: string + deletedCount: number + deletedOrganizations: string[] + detail: { + phase1: string + phase2: string + } + }> { + const response = await api.delete<{ + message: string + organizationId: number + organizationName: string + deletedCount: number + deletedOrganizations: string[] + detail: { + phase1: string + phase2: string + } + }>(`/organizations/${id}/`) + return response.data + } + + /** + * 批量删除组织 + * @param organizationIds - 组织ID数组,number类型 + * @returns Promise<{ message: string; deletedOrganizationCount: number }> + * + * 注意: 删除组织不会删除域名实体,只会解除关联关系 + */ + static async batchDeleteOrganizations(organizationIds: number[]): Promise<{ + message: string + deletedOrganizationCount: number + }> { + const response = await api.post<{ + message: string + deletedOrganizationCount: number + }>('/organizations/batch_delete/', { + organizationIds // [OK] 使用驼峰命名,拦截器会自动转换为 organization_ids + }) + return response.data + } + + // ========== 组织与目标关联操作 ========== + + /** + * 关联目标到组织(单个) + * @param data - 关联请求对象 + * @param data.organizationId - 组织ID + * @param data.targetId - 目标ID + * @returns Promise<{ message: string }> + */ + static async linkTargetToOrganization(data: { + organizationId: number + targetId: number + }): Promise<{ message: string }> { + const response = await api.post<{ message: string }>( + `/organizations/${data.organizationId}/targets/`, + { + targetId: data.targetId // 拦截器会转换为 target_id + } + ) + return response.data + } + + /** + * 从组织中移除目标(批量) + * @param data - 移除请求对象 + * @param data.organizationId - 组织ID + * @param data.targetIds - 目标ID数组 + * @returns Promise<{ unlinkedCount: number; message: string }> + */ + static async unlinkTargetsFromOrganization(data: { + organizationId: number + targetIds: number[] + }): Promise<{ unlinkedCount: number; message: string }> { + const response = await api.post<{ unlinkedCount: number; message: string }>( + `/organizations/${data.organizationId}/unlink_targets/`, + { + targetIds: data.targetIds // 拦截器会转换为 target_ids + } + ) + return response.data + } + +} \ No newline at end of file diff --git a/frontend/services/scan.service.ts b/frontend/services/scan.service.ts new file mode 100644 index 00000000..176c83f7 --- /dev/null +++ b/frontend/services/scan.service.ts @@ -0,0 +1,100 @@ +import { api } from '@/lib/api-client' +import type { + GetScansParams, + GetScansResponse, + InitiateScanRequest, + InitiateScanResponse, + QuickScanRequest, + QuickScanResponse, + ScanRecord +} from '@/types/scan.types' + +/** + * 获取扫描列表 + */ +export async function getScans(params?: GetScansParams): Promise<GetScansResponse> { + const res = await api.get<GetScansResponse>('/scans/', { params }) + return res.data +} + +/** + * 获取单个扫描详情 + * @param id - 扫描ID + * @returns 扫描详情 + */ +export async function getScan(id: number): Promise<ScanRecord> { + const res = await api.get<ScanRecord>(`/scans/${id}/`) + return res.data +} + +/** + * 发起扫描任务(针对已存在的目标/组织) + * @param data - 扫描请求参数 + * @returns 扫描任务信息 + */ +export async function initiateScan(data: InitiateScanRequest): Promise<InitiateScanResponse> { + const res = await api.post<InitiateScanResponse>('/scans/initiate/', data) + return res.data +} + +/** + * 快速扫描(自动创建目标并立即扫描) + * @param data - 快速扫描请求参数 + * @returns 扫描任务信息 + */ +export async function quickScan(data: QuickScanRequest): Promise<QuickScanResponse> { + const res = await api.post<QuickScanResponse>('/scans/quick/', data) + return res.data +} + +/** + * 删除单个扫描记录 + * @param id - 扫描ID + */ +export async function deleteScan(id: number): Promise<void> { + await api.delete(`/scans/${id}/`) +} + +/** + * 批量删除扫描记录 + * @param ids - 扫描ID数组 + * @returns 删除结果 + */ +export async function bulkDeleteScans(ids: number[]): Promise<{ message: string; deletedCount: number }> { + const res = await api.post<{ message: string; deletedCount: number }>('/scans/bulk-delete/', { ids }) + return res.data +} + +/** + * 停止扫描任务 + * @param id - 扫描ID + * @returns 操作结果 + */ +export async function stopScan(id: number): Promise<{ message: string; revokedTaskCount: number }> { + const res = await api.post<{ message: string; revokedTaskCount: number }>(`/scans/${id}/stop/`) + return res.data +} + +/** + * 扫描统计数据类型 + */ +export interface ScanStatistics { + total: number + running: number + completed: number + failed: number + totalVulns: number + totalSubdomains: number + totalEndpoints: number + totalWebsites: number + totalAssets: number +} + +/** + * 获取扫描统计数据 + * @returns 统计数据 + */ +export async function getScanStatistics(): Promise<ScanStatistics> { + const res = await api.get<ScanStatistics>('/scans/statistics/') + return res.data +} diff --git a/frontend/services/scheduled-scan.service.ts b/frontend/services/scheduled-scan.service.ts new file mode 100644 index 00000000..463cfa66 --- /dev/null +++ b/frontend/services/scheduled-scan.service.ts @@ -0,0 +1,68 @@ +import { api } from '@/lib/api-client' +import type { + GetScheduledScansResponse, + ScheduledScan, + CreateScheduledScanRequest, + UpdateScheduledScanRequest +} from '@/types/scheduled-scan.types' + +/** + * 获取定时扫描列表 + */ +export async function getScheduledScans(params?: { page?: number; pageSize?: number; search?: string }): Promise<GetScheduledScansResponse> { + const res = await api.get<GetScheduledScansResponse>('/scheduled-scans/', { params }) + return res.data +} + +/** + * 获取定时扫描详情 + */ +export async function getScheduledScan(id: number): Promise<ScheduledScan> { + const res = await api.get<ScheduledScan>(`/scheduled-scans/${id}/`) + return res.data +} + +/** + * 创建定时扫描 + */ +export async function createScheduledScan(data: CreateScheduledScanRequest): Promise<{ + message: string + scheduledScan: ScheduledScan +}> { + const res = await api.post<{ message: string; scheduledScan: ScheduledScan }>('/scheduled-scans/', data) + return res.data +} + +/** + * 更新定时扫描 + */ +export async function updateScheduledScan(id: number, data: UpdateScheduledScanRequest): Promise<{ + message: string + scheduledScan: ScheduledScan +}> { + const res = await api.put<{ message: string; scheduledScan: ScheduledScan }>(`/scheduled-scans/${id}/`, data) + return res.data +} + +/** + * 删除定时扫描 + */ +export async function deleteScheduledScan(id: number): Promise<{ message: string; id: number }> { + const res = await api.delete<{ message: string; id: number }>(`/scheduled-scans/${id}/`) + return res.data +} + +/** + * 切换定时扫描启用状态 + */ +export async function toggleScheduledScan(id: number, isEnabled: boolean): Promise<{ + message: string + scheduledScan: ScheduledScan +}> { + const res = await api.post<{ message: string; scheduledScan: ScheduledScan }>( + `/scheduled-scans/${id}/toggle/`, + { isEnabled } + ) + return res.data +} + diff --git a/frontend/services/subdomain.service.ts b/frontend/services/subdomain.service.ts new file mode 100644 index 00000000..ba75a5fc --- /dev/null +++ b/frontend/services/subdomain.service.ts @@ -0,0 +1,225 @@ +import { api } from "@/lib/api-client" +import type { Subdomain, GetSubdomainsParams, GetSubdomainsResponse, GetAllSubdomainsParams, GetAllSubdomainsResponse, GetSubdomainByIDResponse, BatchCreateSubdomainsResponse } from "@/types/subdomain.types" + +export class SubdomainService { + // ========== 子域名基础操作 ========== + + /** + * 批量创建子域名(绑定到资产) + */ + static async createSubdomains(data: { + domains: Array<{ + name: string + }> + assetId: number + }): Promise<BatchCreateSubdomainsResponse> { + const response = await api.post<BatchCreateSubdomainsResponse>('/domains/create/', { + domains: data.domains, + assetId: data.assetId // [OK] 驼峰,拦截器转换为 asset_id + }) + return response.data + } + + /** + * 获取单个子域名详情 + */ + static async getSubdomainById(id: string | number): Promise<GetSubdomainByIDResponse> { + const response = await api.get<GetSubdomainByIDResponse>(`/domains/${id}/`) + return response.data + } + + /** + * 更新子域名信息(PATCH) + */ + static async updateSubdomain(data: { + id: number + name?: string + description?: string + }): Promise<Subdomain> { + const requestBody: any = {} + if (data.name !== undefined) requestBody.name = data.name + if (data.description !== undefined) requestBody.description = data.description + const response = await api.patch<Subdomain>(`/domains/${data.id}/`, requestBody) + return response.data + } + + /** 批量删除子域名(支持单个或多个,使用统一接口) */ + static async bulkDeleteSubdomains( + ids: number[] + ): Promise<{ + message: string + deletedCount: number + requestedIds: number[] + cascadeDeleted: Record<string, number> + }> { + const response = await api.post<{ + message: string + deletedCount: number + requestedIds: number[] + cascadeDeleted: Record<string, number> + }>( + `/assets/subdomains/bulk-delete/`, + { ids } + ) + return response.data + } + + /** 删除单个子域名(使用单独的 DELETE API) */ + static async deleteSubdomain(id: number): Promise<{ + message: string + subdomainId: number + subdomainName: string + deletedCount: number + deletedSubdomains: string[] + detail: { + phase1: string + phase2: string + } + }> { + const response = await api.delete<{ + message: string + subdomainId: number + subdomainName: string + deletedCount: number + deletedSubdomains: string[] + detail: { + phase1: string + phase2: string + } + }>(`/assets/subdomains/${id}/`) + return response.data + } + + /** 批量删除子域名(别名,兼容旧代码) */ + static async batchDeleteSubdomains(ids: number[]): Promise<{ + message: string + deletedCount: number + requestedIds: number[] + cascadeDeleted: Record<string, number> + }> { + return this.bulkDeleteSubdomains(ids) + } + + /** 批量从组织中移除子域名 */ + static async batchDeleteSubdomainsFromOrganization(data: { + organizationId: number + domainIds: number[] + }): Promise<{ + message: string + successCount: number + failedCount: number + }> { + const response = await api.post<any>( + `/organizations/${data.organizationId}/domains/batch-remove/`, + { + domainIds: data.domainIds, // 拦截器转换为 domain_ids + } + ) + return response.data + } + + /** 获取组织的子域名列表(服务端分页) */ + static async getSubdomainsByOrgId( + organizationId: number, + params?: { + page?: number + pageSize?: number + } + ): Promise<GetSubdomainsResponse> { + const response = await api.get<GetSubdomainsResponse>( + `/organizations/${organizationId}/domains/`, + { + params: { + page: params?.page || 1, + pageSize: params?.pageSize || 10, + } + } + ) + return response.data + } + + /** 获取所有子域名列表(服务端分页) */ + static async getAllSubdomains(params?: GetAllSubdomainsParams): Promise<GetAllSubdomainsResponse> { + const response = await api.get<GetAllSubdomainsResponse>('/domains/', { + params: { + page: params?.page || 1, + pageSize: params?.pageSize || 10, + } + }) + return response.data + } + + /** 获取目标的子域名列表(支持分页和搜索) */ + static async getSubdomainsByTargetId( + targetId: number, + params?: { + page?: number + pageSize?: number + search?: string + } + ): Promise<any> { + const response = await api.get(`/targets/${targetId}/subdomains/`, { + params: { + page: params?.page || 1, + pageSize: params?.pageSize || 10, + ...(params?.search && { search: params.search }), + } + }) + return response.data + } + + /** 获取扫描的子域名列表(支持分页) */ + static async getSubdomainsByScanId( + scanId: number, + params?: { + page?: number + pageSize?: number + search?: string + } + ): Promise<{ + results: Array<{ + id: number + name: string + createdAt: string // 后端自动转换为 camelCase + cname: string[] + isCdn: boolean // 后端自动转换为 camelCase + cdnName: string // 后端自动转换为 camelCase + ports: Array<{ + number: number + serviceName: string + description: string + isUncommon: boolean + }> + ipAddresses: string[] // IP地址列表 + }> + total: number + page: number + pageSize: number // 后端自动转换为 camelCase + totalPages: number // 后端自动转换为 camelCase + }> { + const response = await api.get(`/scans/${scanId}/subdomains/`, { + params: { + page: params?.page || 1, + pageSize: params?.pageSize || 10, + ...(params?.search && { search: params.search }), + } + }) + return response.data as any + } + + /** 按目标导出所有子域名名称(文本文件,一行一个) */ + static async exportSubdomainsByTargetId(targetId: number): Promise<Blob> { + const response = await api.get<Blob>(`/targets/${targetId}/subdomains/export/`, { + responseType: 'blob', + }) + return response.data + } + + /** 按扫描任务导出所有子域名名称(文本文件,一行一个) */ + static async exportSubdomainsByScanId(scanId: number): Promise<Blob> { + const response = await api.get<Blob>(`/scans/${scanId}/subdomains/export/`, { + responseType: 'blob', + }) + return response.data + } +} diff --git a/frontend/services/target.service.ts b/frontend/services/target.service.ts new file mode 100644 index 00000000..6ae3a2fb --- /dev/null +++ b/frontend/services/target.service.ts @@ -0,0 +1,150 @@ +/** + * Target Service - 目标管理 API + */ +import { api } from '@/lib/api-client' +import type { + Target, + TargetsResponse, + CreateTargetRequest, + UpdateTargetRequest, + BatchDeleteTargetsRequest, + BatchDeleteTargetsResponse, + BatchCreateTargetsRequest, + BatchCreateTargetsResponse, +} from '@/types/target.types' + +/** + * 获取所有目标列表(分页) + */ +export async function getTargets(page = 1, pageSize = 10, search?: string): Promise<TargetsResponse> { + const response = await api.get<TargetsResponse>('/targets/', { + params: { + page, + pageSize, + ...(search && { search }), + }, + }) + return response.data +} + +/** + * 获取单个目标详情 + */ +export async function getTargetById(id: number): Promise<Target> { + const response = await api.get<Target>(`/targets/${id}/`) + return response.data +} + +/** + * 创建目标 + */ +export async function createTarget(data: CreateTargetRequest): Promise<Target> { + const response = await api.post<Target>('/targets/', data) + return response.data +} + +/** + * 更新目标 + */ +export async function updateTarget(id: number, data: UpdateTargetRequest): Promise<Target> { + const response = await api.patch<Target>(`/targets/${id}/`, data) + return response.data +} + +/** + * 删除单个目标(使用单独的 DELETE API) + */ +export async function deleteTarget(id: number): Promise<{ + message: string + targetId: number + targetName: string + deletedCount: number + deletedTargets: string[] + detail: { + phase1: string + phase2: string + } +}> { + const response = await api.delete<{ + message: string + targetId: number + targetName: string + deletedCount: number + deletedTargets: string[] + detail: { + phase1: string + phase2: string + } + }>(`/targets/${id}/`) + return response.data +} + +/** + * 批量删除目标 + */ +export async function batchDeleteTargets( + data: BatchDeleteTargetsRequest +): Promise<BatchDeleteTargetsResponse> { + const response = await api.post<BatchDeleteTargetsResponse>('/targets/bulk-delete/', data) + return response.data +} + +/** + * 批量创建目标 + */ +export async function batchCreateTargets( + data: BatchCreateTargetsRequest +): Promise<BatchCreateTargetsResponse> { + const response = await api.post<BatchCreateTargetsResponse>('/targets/batch_create/', data) + return response.data +} + +/** + * 获取目标的组织列表 + */ +export async function getTargetOrganizations(id: number, page = 1, pageSize = 10) { + const response = await api.get(`/targets/${id}/organizations/`, { params: { page, pageSize } }) + return response.data +} + +/** + * 为目标关联组织 + */ +export async function linkTargetOrganizations( + id: number, + organizationIds: number[] +): Promise<{ message: string }> { + const response = await api.post<{ message: string }>(`/targets/${id}/organizations/`, { organizationIds }) + return response.data +} + +/** + * 取消目标与组织的关联 + */ +export async function unlinkTargetOrganizations( + id: number, + organizationIds: number[] +): Promise<{ message: string }> { + const response = await api.post<{ message: string }>(`/targets/${id}/organizations/unlink/`, { organizationIds }) + return response.data +} + +/** + * 获取目标的端点列表 + */ +export async function getTargetEndpoints( + id: number, + page = 1, + pageSize = 10, + search?: string +): Promise<any> { + const response = await api.get(`/targets/${id}/endpoints/`, { + params: { + page, + pageSize, + ...(search && { search }), + }, + }) + return response.data +} + diff --git a/frontend/services/tool.service.ts b/frontend/services/tool.service.ts new file mode 100644 index 00000000..5d2dcff3 --- /dev/null +++ b/frontend/services/tool.service.ts @@ -0,0 +1,54 @@ +import { api } from "@/lib/api-client" +import type { Tool, GetToolsResponse, CreateToolRequest, UpdateToolRequest, GetToolsParams } from "@/types/tool.types" + +export class ToolService { + /** + * 获取工具列表 + * @param params - 查询参数对象 + * @param params.page - 当前页码,1-based + * @param params.pageSize - 分页大小 + * @returns Promise<GetToolsResponse> + * @description 后端固定按更新时间降序排列,不支持自定义排序 + */ + static async getTools(params?: GetToolsParams): Promise<GetToolsResponse> { + const response = await api.get<GetToolsResponse>( + '/tools/', + { params } + ) + return response.data + } + + /** + * 创建新工具 + * @param data - 工具信息对象 + * @param data.name - 工具名称 + * @param data.repoUrl - 仓库地址 + * @param data.version - 版本号 + * @param data.description - 工具描述 + * @returns Promise<{ tool: Tool }> + */ + static async createTool(data: CreateToolRequest): Promise<{ tool: Tool }> { + const response = await api.post<{ tool: Tool }>('/tools/create/', data) + return response.data + } + + /** + * 更新工具 + * @param id - 工具ID + * @param data - 更新的工具信息(所有字段可选) + * @returns Promise<{ tool: Tool }> + */ + static async updateTool(id: number, data: UpdateToolRequest): Promise<{ tool: Tool }> { + const response = await api.put<{ tool: Tool }>(`/tools/${id}/`, data) + return response.data + } + + /** + * 删除工具 + * @param id - 工具ID + * @returns Promise<void> + */ + static async deleteTool(id: number): Promise<void> { + await api.delete(`/tools/${id}/`) + } +} diff --git a/frontend/services/vulnerability.service.ts b/frontend/services/vulnerability.service.ts new file mode 100644 index 00000000..9d17b1ef --- /dev/null +++ b/frontend/services/vulnerability.service.ts @@ -0,0 +1,36 @@ +import { api } from "@/lib/api-client" +import type { GetVulnerabilitiesParams } from "@/types/vulnerability.types" + +export class VulnerabilityService { + /** 获取所有漏洞列表(全局漏洞页使用) */ + static async getAllVulnerabilities( + params: GetVulnerabilitiesParams, + ): Promise<any> { + const response = await api.get(`/assets/vulnerabilities/`, { + params, + }) + return response.data + } + + /** 按扫描任务获取漏洞快照列表(扫描历史页使用) */ + static async getVulnerabilitiesByScanId( + scanId: number, + params: GetVulnerabilitiesParams, + ): Promise<any> { + const response = await api.get(`/scans/${scanId}/vulnerabilities/`, { + params, + }) + return response.data + } + + /** 按目标获取漏洞资产列表(目标详情页使用) */ + static async getVulnerabilitiesByTargetId( + targetId: number, + params: GetVulnerabilitiesParams, + ): Promise<any> { + const response = await api.get(`/targets/${targetId}/vulnerabilities/`, { + params, + }) + return response.data + } +} diff --git a/frontend/services/website.service.ts b/frontend/services/website.service.ts new file mode 100644 index 00000000..9c7d00b8 --- /dev/null +++ b/frontend/services/website.service.ts @@ -0,0 +1,23 @@ +import { api } from "@/lib/api-client" + +/** + * 网站相关 API 服务 + * 所有前端调用的网站接口都应该集中在这里 + */ +export class WebsiteService { + /** 按目标导出所有网站 URL(文本文件,一行一个) */ + static async exportWebsitesByTargetId(targetId: number): Promise<Blob> { + const response = await api.get<Blob>(`/targets/${targetId}/websites/export/`, { + responseType: "blob", + }) + return response.data + } + + /** 按扫描任务导出所有网站 URL(文本文件,一行一个) */ + static async exportWebsitesByScanId(scanId: number): Promise<Blob> { + const response = await api.get<Blob>(`/scans/${scanId}/websites/export/`, { + responseType: "blob", + }) + return response.data + } +} diff --git a/frontend/services/wordlist.service.ts b/frontend/services/wordlist.service.ts new file mode 100644 index 00000000..27176c02 --- /dev/null +++ b/frontend/services/wordlist.service.ts @@ -0,0 +1,54 @@ +import apiClient from "@/lib/api-client" +import type { GetWordlistsResponse, Wordlist } from "@/types/wordlist.types" + +// 字典(Wordlist) API 服务 + +// 获取字典列表 +export async function getWordlists(page = 1, pageSize = 10): Promise<GetWordlistsResponse> { + const response = await apiClient.get<GetWordlistsResponse>("/wordlists/", { + params: { + page, + pageSize, + }, + }) + return response.data +} + +// 上传字典文件 +export async function uploadWordlist(payload: { + name: string + description?: string + file: File +}): Promise<Wordlist> { + const formData = new FormData() + formData.append("name", payload.name) + if (payload.description) { + formData.append("description", payload.description) + } + formData.append("file", payload.file) + + const response = await apiClient.post<Wordlist>("/wordlists/", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + + return response.data +} + +// 删除字典 +export async function deleteWordlist(id: number): Promise<void> { + await apiClient.delete(`/wordlists/${id}/`) +} + +// 获取字典内容 +export async function getWordlistContent(id: number): Promise<string> { + const response = await apiClient.get<{ content: string }>(`/wordlists/${id}/content/`) + return response.data.content +} + +// 更新字典内容 +export async function updateWordlistContent(id: number, content: string): Promise<Wordlist> { + const response = await apiClient.put<Wordlist>(`/wordlists/${id}/content/`, { content }) + return response.data +} diff --git a/frontend/services/worker.service.ts b/frontend/services/worker.service.ts new file mode 100644 index 00000000..7fc459c4 --- /dev/null +++ b/frontend/services/worker.service.ts @@ -0,0 +1,90 @@ +/** + * Worker 节点管理 API 服务 + */ + +import apiClient from '@/lib/api-client' +import type { + WorkerNode, + WorkersResponse, + CreateWorkerRequest, + UpdateWorkerRequest, +} from '@/types/worker.types' + +const BASE_URL = '/workers' + +export const workerService = { + /** + * 获取 Worker 列表 + */ + async getWorkers(page = 1, pageSize = 10): Promise<WorkersResponse> { + const response = await apiClient.get<WorkersResponse>( + `${BASE_URL}/?page=${page}&page_size=${pageSize}` + ) + return response.data + }, + + /** + * 获取单个 Worker 详情 + */ + async getWorker(id: number): Promise<WorkerNode> { + const response = await apiClient.get<WorkerNode>(`${BASE_URL}/${id}/`) + return response.data + }, + + /** + * 创建 Worker 节点 + */ + async createWorker(data: CreateWorkerRequest): Promise<WorkerNode> { + const response = await apiClient.post<WorkerNode>(`${BASE_URL}/`, { + name: data.name, + ip_address: data.ipAddress, + ssh_port: data.sshPort ?? 22, + username: data.username ?? 'root', + password: data.password, + }) + return response.data + }, + + /** + * 更新 Worker 节点 + */ + async updateWorker(id: number, data: UpdateWorkerRequest): Promise<WorkerNode> { + const response = await apiClient.patch<WorkerNode>(`${BASE_URL}/${id}/`, { + name: data.name, + ssh_port: data.sshPort, + username: data.username, + password: data.password, + }) + return response.data + }, + + /** + * 删除 Worker 节点 + */ + async deleteWorker(id: number): Promise<void> { + await apiClient.delete(`${BASE_URL}/${id}/`) + }, + + /** + * 部署 Worker 节点(占位实现,当前仅用于消除前端类型错误) + */ + async deployWorker(id: number): Promise<never> { + return Promise.reject(new Error(`Worker deploy is not implemented for id=${id}`)) + }, + + /** + * 重启 Worker + */ + async restartWorker(id: number): Promise<{ message: string }> { + const response = await apiClient.post<{ message: string }>(`${BASE_URL}/${id}/restart/`) + return response.data + }, + + /** + * 停止 Worker + */ + async stopWorker(id: number): Promise<{ message: string }> { + const response = await apiClient.post<{ message: string }>(`${BASE_URL}/${id}/stop/`) + return response.data + }, +} diff --git a/frontend/styles/themes/bubblegum.css b/frontend/styles/themes/bubblegum.css new file mode 100644 index 00000000..2b00e0f5 --- /dev/null +++ b/frontend/styles/themes/bubblegum.css @@ -0,0 +1,97 @@ +/** + * Bubblegum 主题 - 粉色泡泡糖风格 + */ +[data-theme="bubblegum"] { + --background: oklch(0.9399 0.0203 345.6985); + --foreground: oklch(0.4712 0 0); + --card: oklch(0.9498 0.0500 86.8891); + --card-foreground: oklch(0.4712 0 0); + --popover: oklch(1.0000 0 0); + --popover-foreground: oklch(0.4712 0 0); + --primary: oklch(0.6209 0.1801 348.1385); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.8095 0.0694 198.1863); + --secondary-foreground: oklch(0.3211 0 0); + --muted: oklch(0.8800 0.0504 212.0952); + --muted-foreground: oklch(0.5795 0 0); + --accent: oklch(0.9195 0.0801 87.6670); + --accent-foreground: oklch(0.3211 0 0); + --destructive: oklch(0.7091 0.1697 21.9551); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.6209 0.1801 348.1385); + --input: oklch(0.9189 0 0); + --ring: oklch(0.7002 0.1597 350.7532); + --chart-1: oklch(0.7002 0.1597 350.7532); + --chart-2: oklch(0.8189 0.0799 212.0892); + --chart-3: oklch(0.9195 0.0801 87.6670); + --chart-4: oklch(0.7998 0.1110 348.1791); + --chart-5: oklch(0.6197 0.1899 353.9091); + --sidebar: oklch(0.9140 0.0424 343.0913); + --sidebar-foreground: oklch(0.3211 0 0); + --sidebar-primary: oklch(0.6559 0.2118 354.3084); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.8228 0.1095 346.0184); + --sidebar-accent-foreground: oklch(0.3211 0 0); + --sidebar-border: oklch(0.9464 0.0327 307.1745); + --sidebar-ring: oklch(0.6559 0.2118 354.3084); + --font-sans: Poppins, sans-serif; + --font-serif: Lora, serif; + --font-mono: Fira Code, monospace; + --radius: 0.4rem; + --shadow-color: hsl(325.78 58.18% 56.86% / 0.5); + --shadow-2xs: 3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 0.50); + --shadow-xs: 3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 0.50); + --shadow-sm: 3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 1.00); + --shadow: 3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 1.00); + --shadow-md: 3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 1.00); + --shadow-lg: 3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 1.00); + --shadow-xl: 3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 1.00); + --shadow-2xl: 3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 1.00); + --tracking-normal: 0em; + --spacing: 0.25rem; +} + +.dark[data-theme="bubblegum"], +[data-theme="bubblegum"].dark { + --background: oklch(0.2497 0.0305 234.1628); + --foreground: oklch(0.9306 0.0197 349.0785); + --card: oklch(0.2902 0.0299 233.5352); + --card-foreground: oklch(0.9306 0.0197 349.0785); + --popover: oklch(0.2902 0.0299 233.5352); + --popover-foreground: oklch(0.9306 0.0197 349.0785); + --primary: oklch(0.9195 0.0801 87.6670); + --primary-foreground: oklch(0.2497 0.0305 234.1628); + --secondary: oklch(0.7794 0.0803 4.1330); + --secondary-foreground: oklch(0.2497 0.0305 234.1628); + --muted: oklch(0.2713 0.0086 255.5780); + --muted-foreground: oklch(0.7794 0.0803 4.1330); + --accent: oklch(0.6699 0.0988 356.9762); + --accent-foreground: oklch(0.9306 0.0197 349.0785); + --destructive: oklch(0.6702 0.1806 350.3599); + --destructive-foreground: oklch(0.2497 0.0305 234.1628); + --border: oklch(0.3907 0.0399 242.2181); + --input: oklch(0.3093 0.0305 232.0027); + --ring: oklch(0.6998 0.0896 201.8672); + --chart-1: oklch(0.6998 0.0896 201.8672); + --chart-2: oklch(0.7794 0.0803 4.1330); + --chart-3: oklch(0.6699 0.0988 356.9762); + --chart-4: oklch(0.4408 0.0702 217.0848); + --chart-5: oklch(0.2713 0.0086 255.5780); + --sidebar: oklch(0.2303 0.0270 235.9743); + --sidebar-foreground: oklch(0.9670 0.0029 264.5419); + --sidebar-primary: oklch(0.6559 0.2118 354.3084); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.8228 0.1095 346.0184); + --sidebar-accent-foreground: oklch(0.2781 0.0296 256.8480); + --sidebar-border: oklch(0.3729 0.0306 259.7328); + --sidebar-ring: oklch(0.6559 0.2118 354.3084); + --shadow-color: #324859; + --shadow-2xs: 3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 0.50); + --shadow-xs: 3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 0.50); + --shadow-sm: 3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 1.00); + --shadow: 3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 1.00); + --shadow-md: 3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 1.00); + --shadow-lg: 3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 1.00); + --shadow-xl: 3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 1.00); + --shadow-2xl: 3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 1.00); +} diff --git a/frontend/styles/themes/candyland.css b/frontend/styles/themes/candyland.css new file mode 100644 index 00000000..2e6c97fb --- /dev/null +++ b/frontend/styles/themes/candyland.css @@ -0,0 +1,108 @@ +[data-theme="candyland"] { + --background: oklch(0.9809 0.0025 228.7836); + --foreground: oklch(0.3211 0 0); + --card: oklch(1.0000 0 0); + --card-foreground: oklch(0.3211 0 0); + --popover: oklch(1.0000 0 0); + --popover-foreground: oklch(0.3211 0 0); + --primary: oklch(0.8677 0.0735 7.0855); + --primary-foreground: oklch(0 0 0); + --secondary: oklch(0.8148 0.0819 225.7537); + --secondary-foreground: oklch(0 0 0); + --muted: oklch(0.8828 0.0285 98.1033); + --muted-foreground: oklch(0.5382 0 0); + --accent: oklch(0.9680 0.2110 109.7692); + --accent-foreground: oklch(0 0 0); + --destructive: oklch(0.6368 0.2078 25.3313); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.8699 0 0); + --input: oklch(0.8699 0 0); + --ring: oklch(0.8677 0.0735 7.0855); + --chart-1: oklch(0.8677 0.0735 7.0855); + --chart-2: oklch(0.8148 0.0819 225.7537); + --chart-3: oklch(0.9680 0.2110 109.7692); + --chart-4: oklch(0.8027 0.1355 349.2347); + --chart-5: oklch(0.7395 0.2268 142.8504); + --sidebar: oklch(0.9809 0.0025 228.7836); + --sidebar-foreground: oklch(0.3211 0 0); + --sidebar-primary: oklch(0.8677 0.0735 7.0855); + --sidebar-primary-foreground: oklch(0 0 0); + --sidebar-accent: oklch(0.9680 0.2110 109.7692); + --sidebar-accent-foreground: oklch(0 0 0); + --sidebar-border: oklch(0.8699 0 0); + --sidebar-ring: oklch(0.8677 0.0735 7.0855); + --font-sans: Poppins, sans-serif; + --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; + --font-mono: Roboto Mono, monospace; + --radius: 0.5rem; + --shadow-x: 0; + --shadow-y: 1px; + --shadow-blur: 3px; + --shadow-spread: 0px; + --shadow-opacity: 0.1; + --shadow-color: oklch(0 0 0); + --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); + --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); + --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); + --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); + --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); + --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); + --tracking-normal: 0em; + --spacing: 0.25rem; +} + +.dark[data-theme="candyland"], +[data-theme="candyland"].dark { + --background: oklch(0.2303 0.0125 264.2926); + --foreground: oklch(0.9219 0 0); + --card: oklch(0.3210 0.0078 223.6661); + --card-foreground: oklch(0.9219 0 0); + --popover: oklch(0.3210 0.0078 223.6661); + --popover-foreground: oklch(0.9219 0 0); + --primary: oklch(0.8027 0.1355 349.2347); + --primary-foreground: oklch(0 0 0); + --secondary: oklch(0.7395 0.2268 142.8504); + --secondary-foreground: oklch(0 0 0); + --muted: oklch(0.3867 0 0); + --muted-foreground: oklch(0.7155 0 0); + --accent: oklch(0.8148 0.0819 225.7537); + --accent-foreground: oklch(0 0 0); + --destructive: oklch(0.6368 0.2078 25.3313); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.3867 0 0); + --input: oklch(0.3867 0 0); + --ring: oklch(0.8027 0.1355 349.2347); + --chart-1: oklch(0.8027 0.1355 349.2347); + --chart-2: oklch(0.7395 0.2268 142.8504); + --chart-3: oklch(0.8148 0.0819 225.7537); + --chart-4: oklch(0.9680 0.2110 109.7692); + --chart-5: oklch(0.8652 0.1768 90.3816); + --sidebar: oklch(0.2303 0.0125 264.2926); + --sidebar-foreground: oklch(0.9219 0 0); + --sidebar-primary: oklch(0.8027 0.1355 349.2347); + --sidebar-primary-foreground: oklch(0 0 0); + --sidebar-accent: oklch(0.8148 0.0819 225.7537); + --sidebar-accent-foreground: oklch(0 0 0); + --sidebar-border: oklch(0.3867 0 0); + --sidebar-ring: oklch(0.8027 0.1355 349.2347); + --font-sans: Poppins, sans-serif; + --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; + --font-mono: Roboto Mono, monospace; + --radius: 0.5rem; + --shadow-x: 0; + --shadow-y: 1px; + --shadow-blur: 3px; + --shadow-spread: 0px; + --shadow-opacity: 0.1; + --shadow-color: oklch(0 0 0); + --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); + --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); + --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); + --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); + --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); + --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); +} diff --git a/frontend/styles/themes/clean-slate.css b/frontend/styles/themes/clean-slate.css new file mode 100644 index 00000000..d0b0e8ae --- /dev/null +++ b/frontend/styles/themes/clean-slate.css @@ -0,0 +1,96 @@ +/** + * Clean Slate 主题 - 简洁蓝灰风格 + */ +[data-theme="clean-slate"] { + --background: oklch(0.9842 0.0034 247.8575); + --foreground: oklch(0.2795 0.0368 260.0310); + --card: oklch(1.0000 0 0); + --card-foreground: oklch(0.2795 0.0368 260.0310); + --popover: oklch(1.0000 0 0); + --popover-foreground: oklch(0.2795 0.0368 260.0310); + --primary: oklch(0.5854 0.2041 277.1173); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.9276 0.0058 264.5313); + --secondary-foreground: oklch(0.3729 0.0306 259.7328); + --muted: oklch(0.9670 0.0029 264.5419); + --muted-foreground: oklch(0.5510 0.0234 264.3637); + --accent: oklch(0.9299 0.0334 272.7879); + --accent-foreground: oklch(0.3729 0.0306 259.7328); + --destructive: oklch(0.6368 0.2078 25.3313); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.8717 0.0093 258.3382); + --input: oklch(0.8717 0.0093 258.3382); + --ring: oklch(0.5854 0.2041 277.1173); + --chart-1: oklch(0.5854 0.2041 277.1173); + --chart-2: oklch(0.5106 0.2301 276.9656); + --chart-3: oklch(0.4568 0.2146 277.0229); + --chart-4: oklch(0.3984 0.1773 277.3662); + --chart-5: oklch(0.3588 0.1354 278.6973); + --sidebar: oklch(0.9670 0.0029 264.5419); + --sidebar-foreground: oklch(0.2795 0.0368 260.0310); + --sidebar-primary: oklch(0.5854 0.2041 277.1173); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.9299 0.0334 272.7879); + --sidebar-accent-foreground: oklch(0.3729 0.0306 259.7328); + --sidebar-border: oklch(0.8717 0.0093 258.3382); + --sidebar-ring: oklch(0.5854 0.2041 277.1173); + --font-sans: Inter, sans-serif; + --font-serif: Merriweather, serif; + --font-mono: JetBrains Mono, monospace; + --radius: 0.5rem; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); + --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); + --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10); + --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10); + --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10); + --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10); + --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10); + --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25); + --tracking-normal: 0em; + --spacing: 0.25rem; +} + +[data-theme="clean-slate"].dark { + --background: oklch(0.2077 0.0398 265.7549); + --foreground: oklch(0.9288 0.0126 255.5078); + --card: oklch(0.2795 0.0368 260.0310); + --card-foreground: oklch(0.9288 0.0126 255.5078); + --popover: oklch(0.2795 0.0368 260.0310); + --popover-foreground: oklch(0.9288 0.0126 255.5078); + --primary: oklch(0.6801 0.1583 276.9349); + --primary-foreground: oklch(0.2077 0.0398 265.7549); + --secondary: oklch(0.3351 0.0331 260.9120); + --secondary-foreground: oklch(0.8717 0.0093 258.3382); + --muted: oklch(0.2427 0.0381 259.9437); + --muted-foreground: oklch(0.7137 0.0192 261.3246); + --accent: oklch(0.3729 0.0306 259.7328); + --accent-foreground: oklch(0.8717 0.0093 258.3382); + --destructive: oklch(0.6368 0.2078 25.3313); + --destructive-foreground: oklch(0.2077 0.0398 265.7549); + --border: oklch(0.4461 0.0263 256.8018); + --input: oklch(0.4461 0.0263 256.8018); + --ring: oklch(0.6801 0.1583 276.9349); + --chart-1: oklch(0.6801 0.1583 276.9349); + --chart-2: oklch(0.5854 0.2041 277.1173); + --chart-3: oklch(0.5106 0.2301 276.9656); + --chart-4: oklch(0.4568 0.2146 277.0229); + --chart-5: oklch(0.3984 0.1773 277.3662); + --sidebar: oklch(0.2795 0.0368 260.0310); + --sidebar-foreground: oklch(0.9288 0.0126 255.5078); + --sidebar-primary: oklch(0.6801 0.1583 276.9349); + --sidebar-primary-foreground: oklch(0.2077 0.0398 265.7549); + --sidebar-accent: oklch(0.3729 0.0306 259.7328); + --sidebar-accent-foreground: oklch(0.8717 0.0093 258.3382); + --sidebar-border: oklch(0.4461 0.0263 256.8018); + --sidebar-ring: oklch(0.6801 0.1583 276.9349); + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); + --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); + --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10); + --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10); + --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10); + --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10); + --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10); + --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25); +} diff --git a/frontend/styles/themes/cosmic-night.css b/frontend/styles/themes/cosmic-night.css new file mode 100644 index 00000000..69e8cf98 --- /dev/null +++ b/frontend/styles/themes/cosmic-night.css @@ -0,0 +1,96 @@ +/** + * Cosmic Night 主题 - 宇宙紫夜风格 + */ +[data-theme="cosmic-night"] { + --background: oklch(0.9730 0.0133 286.1503); + --foreground: oklch(0.3015 0.0572 282.4176); + --card: oklch(1.0000 0 0); + --card-foreground: oklch(0.3015 0.0572 282.4176); + --popover: oklch(1.0000 0 0); + --popover-foreground: oklch(0.3015 0.0572 282.4176); + --primary: oklch(0.5417 0.1790 288.0332); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.9174 0.0435 292.6901); + --secondary-foreground: oklch(0.4143 0.1039 288.1742); + --muted: oklch(0.9580 0.0133 286.1454); + --muted-foreground: oklch(0.5426 0.0465 284.7435); + --accent: oklch(0.9221 0.0373 262.1410); + --accent-foreground: oklch(0.3015 0.0572 282.4176); + --destructive: oklch(0.6861 0.2061 14.9941); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.9115 0.0216 285.9625); + --input: oklch(0.9115 0.0216 285.9625); + --ring: oklch(0.5417 0.1790 288.0332); + --chart-1: oklch(0.5417 0.1790 288.0332); + --chart-2: oklch(0.7042 0.1602 288.9880); + --chart-3: oklch(0.5679 0.2113 276.7065); + --chart-4: oklch(0.6356 0.1922 281.8054); + --chart-5: oklch(0.4509 0.1758 279.3838); + --sidebar: oklch(0.9580 0.0133 286.1454); + --sidebar-foreground: oklch(0.3015 0.0572 282.4176); + --sidebar-primary: oklch(0.5417 0.1790 288.0332); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.9221 0.0373 262.1410); + --sidebar-accent-foreground: oklch(0.3015 0.0572 282.4176); + --sidebar-border: oklch(0.9115 0.0216 285.9625); + --sidebar-ring: oklch(0.5417 0.1790 288.0332); + --font-sans: Inter, sans-serif; + --font-serif: Georgia, serif; + --font-mono: JetBrains Mono, monospace; + --radius: 0.5rem; + --shadow-color: hsl(240 30% 25%); + --shadow-2xs: 0px 4px 10px 0px hsl(240 30% 25% / 0.06); + --shadow-xs: 0px 4px 10px 0px hsl(240 30% 25% / 0.06); + --shadow-sm: 0px 4px 10px 0px hsl(240 30% 25% / 0.12); + --shadow: 0px 4px 10px 0px hsl(240 30% 25% / 0.12); + --shadow-md: 0px 4px 10px 0px hsl(240 30% 25% / 0.12); + --shadow-lg: 0px 4px 10px 0px hsl(240 30% 25% / 0.12); + --shadow-xl: 0px 4px 10px 0px hsl(240 30% 25% / 0.12); + --shadow-2xl: 0px 4px 10px 0px hsl(240 30% 25% / 0.30); + --tracking-normal: 0em; + --spacing: 0.25rem; +} + +[data-theme="cosmic-night"].dark { + --background: oklch(0.1743 0.0227 283.7998); + --foreground: oklch(0.9185 0.0257 285.8834); + --card: oklch(0.2284 0.0384 282.9324); + --card-foreground: oklch(0.9185 0.0257 285.8834); + --popover: oklch(0.2284 0.0384 282.9324); + --popover-foreground: oklch(0.9185 0.0257 285.8834); + --primary: oklch(0.7162 0.1597 290.3962); + --primary-foreground: oklch(0.1743 0.0227 283.7998); + --secondary: oklch(0.3139 0.0736 283.4591); + --secondary-foreground: oklch(0.8367 0.0849 285.9111); + --muted: oklch(0.2710 0.0621 281.4377); + --muted-foreground: oklch(0.7166 0.0462 285.1741); + --accent: oklch(0.3354 0.0828 280.9705); + --accent-foreground: oklch(0.9185 0.0257 285.8834); + --destructive: oklch(0.6861 0.2061 14.9941); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.3261 0.0597 282.5832); + --input: oklch(0.3261 0.0597 282.5832); + --ring: oklch(0.7162 0.1597 290.3962); + --chart-1: oklch(0.7162 0.1597 290.3962); + --chart-2: oklch(0.6382 0.1047 274.9117); + --chart-3: oklch(0.7482 0.1235 244.7492); + --chart-4: oklch(0.7124 0.0977 186.6761); + --chart-5: oklch(0.7546 0.1831 346.8124); + --sidebar: oklch(0.2284 0.0384 282.9324); + --sidebar-foreground: oklch(0.9185 0.0257 285.8834); + --sidebar-primary: oklch(0.7162 0.1597 290.3962); + --sidebar-primary-foreground: oklch(0.1743 0.0227 283.7998); + --sidebar-accent: oklch(0.3354 0.0828 280.9705); + --sidebar-accent-foreground: oklch(0.9185 0.0257 285.8834); + --sidebar-border: oklch(0.3261 0.0597 282.5832); + --sidebar-ring: oklch(0.7162 0.1597 290.3962); + --shadow-color: hsl(240 30% 25%); + --shadow-2xs: 0px 4px 10px 0px hsl(240 30% 25% / 0.06); + --shadow-xs: 0px 4px 10px 0px hsl(240 30% 25% / 0.06); + --shadow-sm: 0px 4px 10px 0px hsl(240 30% 25% / 0.12); + --shadow: 0px 4px 10px 0px hsl(240 30% 25% / 0.12); + --shadow-md: 0px 4px 10px 0px hsl(240 30% 25% / 0.12); + --shadow-lg: 0px 4px 10px 0px hsl(240 30% 25% / 0.12); + --shadow-xl: 0px 4px 10px 0px hsl(240 30% 25% / 0.12); + --shadow-2xl: 0px 4px 10px 0px hsl(240 30% 25% / 0.30); +} diff --git a/frontend/styles/themes/index.css b/frontend/styles/themes/index.css new file mode 100644 index 00000000..fe29f4f4 --- /dev/null +++ b/frontend/styles/themes/index.css @@ -0,0 +1,8 @@ +/** + * 主题文件索引 + * 所有颜色主题都在这里引入 + */ +@import "./bubblegum.css"; +@import "./quantum-rose.css"; +@import "./clean-slate.css"; +@import "./cosmic-night.css"; diff --git a/frontend/styles/themes/quantum-rose.css b/frontend/styles/themes/quantum-rose.css new file mode 100644 index 00000000..07bda9ae --- /dev/null +++ b/frontend/styles/themes/quantum-rose.css @@ -0,0 +1,96 @@ +/** + * Quantum Rose 主题 - 玫瑰量子风格 + */ +[data-theme="quantum-rose"] { + --background: oklch(0.9692 0.0192 343.9344); + --foreground: oklch(0.4426 0.1653 352.3762); + --card: oklch(0.9837 0.0107 339.3288); + --card-foreground: oklch(0.4426 0.1653 352.3762); + --popover: oklch(0.9837 0.0107 339.3288); + --popover-foreground: oklch(0.4426 0.1653 352.3762); + --primary: oklch(0.6002 0.2414 0.1348); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.9230 0.0701 326.1273); + --secondary-foreground: oklch(0.4426 0.1653 352.3762); + --muted: oklch(0.9429 0.0363 344.2604); + --muted-foreground: oklch(0.5740 0.1732 352.0544); + --accent: oklch(0.8766 0.0828 344.8849); + --accent-foreground: oklch(0.4426 0.1653 352.3762); + --destructive: oklch(0.5831 0.1911 6.3410); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.8881 0.0747 344.3866); + --input: oklch(0.9230 0.0701 326.1273); + --ring: oklch(0.6002 0.2414 0.1348); + --chart-1: oklch(0.6002 0.2414 0.1348); + --chart-2: oklch(0.5979 0.1750 345.0378); + --chart-3: oklch(0.6009 0.1243 311.7958); + --chart-4: oklch(0.5849 0.1178 283.2937); + --chart-5: oklch(0.6479 0.1871 267.9684); + --sidebar: oklch(0.9629 0.0227 345.7485); + --sidebar-foreground: oklch(0.4426 0.1653 352.3762); + --sidebar-primary: oklch(0.6002 0.2414 0.1348); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.8766 0.0828 344.8849); + --sidebar-accent-foreground: oklch(0.4426 0.1653 352.3762); + --sidebar-border: oklch(0.9311 0.0448 343.3135); + --sidebar-ring: oklch(0.6002 0.2414 0.1348); + --font-sans: Poppins, sans-serif; + --font-serif: Playfair Display, serif; + --font-mono: Space Mono, monospace; + --radius: 0.5rem; + --shadow-color: hsl(330 70% 30% / 0.12); + --shadow-2xs: 0px 3px 0px 0px hsl(330 70% 30% / 0.09); + --shadow-xs: 0px 3px 0px 0px hsl(330 70% 30% / 0.09); + --shadow-sm: 0px 3px 0px 0px hsl(330 70% 30% / 0.18); + --shadow: 0px 3px 0px 0px hsl(330 70% 30% / 0.18); + --shadow-md: 0px 3px 0px 0px hsl(330 70% 30% / 0.18); + --shadow-lg: 0px 3px 0px 0px hsl(330 70% 30% / 0.18); + --shadow-xl: 0px 3px 0px 0px hsl(330 70% 30% / 0.18); + --shadow-2xl: 0px 3px 0px 0px hsl(330 70% 30% / 0.45); + --tracking-normal: 0em; + --spacing: 0.25rem; +} + +[data-theme="quantum-rose"].dark { + --background: oklch(0.1808 0.0535 313.7159); + --foreground: oklch(0.8624 0.1307 326.6356); + --card: oklch(0.2398 0.0661 313.2337); + --card-foreground: oklch(0.8624 0.1307 326.6356); + --popover: oklch(0.2398 0.0661 313.2337); + --popover-foreground: oklch(0.8624 0.1307 326.6356); + --primary: oklch(0.7543 0.2319 332.0212); + --primary-foreground: oklch(0.1608 0.0493 327.5673); + --secondary: oklch(0.3184 0.0915 319.6465); + --secondary-foreground: oklch(0.8624 0.1307 326.6356); + --muted: oklch(0.2701 0.0770 312.3525); + --muted-foreground: oklch(0.7116 0.1623 327.1132); + --accent: oklch(0.3558 0.1201 325.7655); + --accent-foreground: oklch(0.8624 0.1307 326.6356); + --destructive: oklch(0.6539 0.2441 7.1740); + --destructive-foreground: oklch(0.9821 0 0); + --border: oklch(0.3280 0.1202 313.5393); + --input: oklch(0.3184 0.0915 319.6465); + --ring: oklch(0.7543 0.2319 332.0212); + --chart-1: oklch(0.7543 0.2319 332.0212); + --chart-2: oklch(0.6508 0.2159 317.6331); + --chart-3: oklch(0.6249 0.2233 292.7656); + --chart-4: oklch(0.6067 0.1649 278.7172); + --chart-5: oklch(0.6235 0.2019 268.0521); + --sidebar: oklch(0.1941 0.0504 311.3983); + --sidebar-foreground: oklch(0.8624 0.1307 326.6356); + --sidebar-primary: oklch(0.7543 0.2319 332.0212); + --sidebar-primary-foreground: oklch(0.1608 0.0493 327.5673); + --sidebar-accent: oklch(0.3558 0.1201 325.7655); + --sidebar-accent-foreground: oklch(0.8624 0.1307 326.6356); + --sidebar-border: oklch(0.3280 0.1202 313.5393); + --sidebar-ring: oklch(0.7543 0.2319 332.0212); + --shadow-color: hsl(300 80% 50% / 0.25); + --shadow-2xs: 0px 3px 0px 0px hsl(300 80% 50% / 0.09); + --shadow-xs: 0px 3px 0px 0px hsl(300 80% 50% / 0.09); + --shadow-sm: 0px 3px 0px 0px hsl(300 80% 50% / 0.18); + --shadow: 0px 3px 0px 0px hsl(300 80% 50% / 0.18); + --shadow-md: 0px 3px 0px 0px hsl(300 80% 50% / 0.18); + --shadow-lg: 0px 3px 0px 0px hsl(300 80% 50% / 0.18); + --shadow-xl: 0px 3px 0px 0px hsl(300 80% 50% / 0.18); + --shadow-2xl: 0px 3px 0px 0px hsl(300 80% 50% / 0.45); +} diff --git a/frontend/styles/themes/vercel.css b/frontend/styles/themes/vercel.css new file mode 100644 index 00000000..887f6a73 --- /dev/null +++ b/frontend/styles/themes/vercel.css @@ -0,0 +1,108 @@ +[data-theme="vercel"] { + --background: oklch(0.9900 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --popover: oklch(0.9900 0 0); + --popover-foreground: oklch(0 0 0); + --primary: oklch(0 0 0); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.9400 0 0); + --secondary-foreground: oklch(0 0 0); + --muted: oklch(0.9700 0 0); + --muted-foreground: oklch(0.4400 0 0); + --accent: oklch(0.9400 0 0); + --accent-foreground: oklch(0 0 0); + --destructive: oklch(0.6300 0.1900 23.0300); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.9200 0 0); + --input: oklch(0.9400 0 0); + --ring: oklch(0 0 0); + --chart-1: oklch(0.8100 0.1700 75.3500); + --chart-2: oklch(0.5500 0.2200 264.5300); + --chart-3: oklch(0.7200 0 0); + --chart-4: oklch(0.9200 0 0); + --chart-5: oklch(0.5600 0 0); + --sidebar: oklch(0.9900 0 0); + --sidebar-foreground: oklch(0 0 0); + --sidebar-primary: oklch(0 0 0); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.9400 0 0); + --sidebar-accent-foreground: oklch(0 0 0); + --sidebar-border: oklch(0.9400 0 0); + --sidebar-ring: oklch(0 0 0); + --font-sans: Geist, sans-serif; + --font-serif: Georgia, serif; + --font-mono: Geist Mono, monospace; + --radius: 0.5rem; + --shadow-x: 0px; + --shadow-y: 1px; + --shadow-blur: 2px; + --shadow-spread: 0px; + --shadow-opacity: 0.18; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18); + --shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18); + --shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18); + --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45); + --tracking-normal: 0em; + --spacing: 0.25rem; +} + +.dark[data-theme="vercel"], +[data-theme="vercel"].dark { + --background: oklch(0 0 0); + --foreground: oklch(1 0 0); + --card: oklch(0.1400 0 0); + --card-foreground: oklch(1 0 0); + --popover: oklch(0.1800 0 0); + --popover-foreground: oklch(1 0 0); + --primary: oklch(1 0 0); + --primary-foreground: oklch(0 0 0); + --secondary: oklch(0.2500 0 0); + --secondary-foreground: oklch(1 0 0); + --muted: oklch(0.2300 0 0); + --muted-foreground: oklch(0.7200 0 0); + --accent: oklch(0.3200 0 0); + --accent-foreground: oklch(1 0 0); + --destructive: oklch(0.6900 0.2000 23.9100); + --destructive-foreground: oklch(0 0 0); + --border: oklch(0.2600 0 0); + --input: oklch(0.3200 0 0); + --ring: oklch(0.7200 0 0); + --chart-1: oklch(0.8100 0.1700 75.3500); + --chart-2: oklch(0.5800 0.2100 260.8400); + --chart-3: oklch(0.5600 0 0); + --chart-4: oklch(0.4400 0 0); + --chart-5: oklch(0.9200 0 0); + --sidebar: oklch(0.1800 0 0); + --sidebar-foreground: oklch(1 0 0); + --sidebar-primary: oklch(1 0 0); + --sidebar-primary-foreground: oklch(0 0 0); + --sidebar-accent: oklch(0.3200 0 0); + --sidebar-accent-foreground: oklch(1 0 0); + --sidebar-border: oklch(0.3200 0 0); + --sidebar-ring: oklch(0.7200 0 0); + --font-sans: Geist, sans-serif; + --font-serif: Georgia, serif; + --font-mono: Geist Mono, monospace; + --radius: 0.5rem; + --shadow-x: 0px; + --shadow-y: 1px; + --shadow-blur: 2px; + --shadow-spread: 0px; + --shadow-opacity: 0.18; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18); + --shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18); + --shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18); + --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45); +} \ No newline at end of file diff --git a/frontend/styles/themes/violet-bloom.css b/frontend/styles/themes/violet-bloom.css new file mode 100644 index 00000000..52755c32 --- /dev/null +++ b/frontend/styles/themes/violet-bloom.css @@ -0,0 +1,108 @@ +[data-theme="violet-bloom"] { + --background: oklch(0.9940 0 0); + --foreground: oklch(0 0 0); + --card: oklch(0.9940 0 0); + --card-foreground: oklch(0 0 0); + --popover: oklch(0.9911 0 0); + --popover-foreground: oklch(0 0 0); + --primary: oklch(0.5393 0.2713 286.7462); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.9540 0.0063 255.4755); + --secondary-foreground: oklch(0.1344 0 0); + --muted: oklch(0.9702 0 0); + --muted-foreground: oklch(0.4386 0 0); + --accent: oklch(0.9393 0.0288 266.3680); + --accent-foreground: oklch(0.5445 0.1903 259.4848); + --destructive: oklch(0.6290 0.1902 23.0704); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.9300 0.0094 286.2156); + --input: oklch(0.9401 0 0); + --ring: oklch(0 0 0); + --chart-1: oklch(0.7459 0.1483 156.4499); + --chart-2: oklch(0.5393 0.2713 286.7462); + --chart-3: oklch(0.7336 0.1758 50.5517); + --chart-4: oklch(0.5828 0.1809 259.7276); + --chart-5: oklch(0.5590 0 0); + --sidebar: oklch(0.9777 0.0051 247.8763); + --sidebar-foreground: oklch(0 0 0); + --sidebar-primary: oklch(0 0 0); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.9401 0 0); + --sidebar-accent-foreground: oklch(0 0 0); + --sidebar-border: oklch(0.9401 0 0); + --sidebar-ring: oklch(0 0 0); + --font-sans: Plus Jakarta Sans, sans-serif; + --font-serif: Lora, serif; + --font-mono: IBM Plex Mono, monospace; + --radius: 1.4rem; + --shadow-x: 0px; + --shadow-y: 2px; + --shadow-blur: 3px; + --shadow-spread: 0px; + --shadow-opacity: 0.16; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 2px 3px 0px hsl(0 0% 0% / 0.08); + --shadow-xs: 0px 2px 3px 0px hsl(0 0% 0% / 0.08); + --shadow-sm: 0px 2px 3px 0px hsl(0 0% 0% / 0.16), 0px 1px 2px -1px hsl(0 0% 0% / 0.16); + --shadow: 0px 2px 3px 0px hsl(0 0% 0% / 0.16), 0px 1px 2px -1px hsl(0 0% 0% / 0.16); + --shadow-md: 0px 2px 3px 0px hsl(0 0% 0% / 0.16), 0px 2px 4px -1px hsl(0 0% 0% / 0.16); + --shadow-lg: 0px 2px 3px 0px hsl(0 0% 0% / 0.16), 0px 4px 6px -1px hsl(0 0% 0% / 0.16); + --shadow-xl: 0px 2px 3px 0px hsl(0 0% 0% / 0.16), 0px 8px 10px -1px hsl(0 0% 0% / 0.16); + --shadow-2xl: 0px 2px 3px 0px hsl(0 0% 0% / 0.40); + --tracking-normal: -0.025em; + --spacing: 0.27rem; +} + +.dark[data-theme="violet-bloom"], +[data-theme="violet-bloom"].dark { + --background: oklch(0.2223 0.0060 271.1393); + --foreground: oklch(0.9551 0 0); + --card: oklch(0.2568 0.0076 274.6528); + --card-foreground: oklch(0.9551 0 0); + --popover: oklch(0.2568 0.0076 274.6528); + --popover-foreground: oklch(0.9551 0 0); + --primary: oklch(0.6132 0.2294 291.7437); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.2940 0.0130 272.9312); + --secondary-foreground: oklch(0.9551 0 0); + --muted: oklch(0.2940 0.0130 272.9312); + --muted-foreground: oklch(0.7058 0 0); + --accent: oklch(0.2795 0.0368 260.0310); + --accent-foreground: oklch(0.7857 0.1153 246.6596); + --destructive: oklch(0.7106 0.1661 22.2162); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.3289 0.0092 268.3843); + --input: oklch(0.3289 0.0092 268.3843); + --ring: oklch(0.6132 0.2294 291.7437); + --chart-1: oklch(0.8003 0.1821 151.7110); + --chart-2: oklch(0.6132 0.2294 291.7437); + --chart-3: oklch(0.8077 0.1035 19.5706); + --chart-4: oklch(0.6691 0.1569 260.1063); + --chart-5: oklch(0.7058 0 0); + --sidebar: oklch(0.2011 0.0039 286.0396); + --sidebar-foreground: oklch(0.9551 0 0); + --sidebar-primary: oklch(0.6132 0.2294 291.7437); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.2940 0.0130 272.9312); + --sidebar-accent-foreground: oklch(0.6132 0.2294 291.7437); + --sidebar-border: oklch(0.3289 0.0092 268.3843); + --sidebar-ring: oklch(0.6132 0.2294 291.7437); + --font-sans: Plus Jakarta Sans, sans-serif; + --font-serif: Lora, serif; + --font-mono: IBM Plex Mono, monospace; + --radius: 1.4rem; + --shadow-x: 0px; + --shadow-y: 2px; + --shadow-blur: 3px; + --shadow-spread: 0px; + --shadow-opacity: 0.16; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 2px 3px 0px hsl(0 0% 0% / 0.08); + --shadow-xs: 0px 2px 3px 0px hsl(0 0% 0% / 0.08); + --shadow-sm: 0px 2px 3px 0px hsl(0 0% 0% / 0.16), 0px 1px 2px -1px hsl(0 0% 0% / 0.16); + --shadow: 0px 2px 3px 0px hsl(0 0% 0% / 0.16), 0px 1px 2px -1px hsl(0 0% 0% / 0.16); + --shadow-md: 0px 2px 3px 0px hsl(0 0% 0% / 0.16), 0px 2px 4px -1px hsl(0 0% 0% / 0.16); + --shadow-lg: 0px 2px 3px 0px hsl(0 0% 0% / 0.16), 0px 4px 6px -1px hsl(0 0% 0% / 0.16); + --shadow-xl: 0px 2px 3px 0px hsl(0 0% 0% / 0.16), 0px 8px 10px -1px hsl(0 0% 0% / 0.16); + --shadow-2xl: 0px 2px 3px 0px hsl(0 0% 0% / 0.40); +} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..d8b93235 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/frontend/types/api-response.types.ts b/frontend/types/api-response.types.ts new file mode 100644 index 00000000..052449d3 --- /dev/null +++ b/frontend/types/api-response.types.ts @@ -0,0 +1,21 @@ +// 通用API响应类型 +export interface ApiResponse<T = any> { + code: string; // HTTP状态码,如 "200", "400", "500" + state: string; // 业务状态,如 "success", "error" + message: string; // 响应消息 + data?: T; // 响应数据 +} + +// 通用批量创建响应数据(对应后端 BaseBatchCreateResponseData) +// 适用于:域名、端点等批量创建操作 +export interface BatchCreateResponse { + message: string // 详细说明,如 "成功处理 5 个域名,新创建 3 个,2 个已存在,1 个已跳过" + requestedCount: number // 请求创建的总数量 + createdCount: number // 新创建的数量 + existedCount: number // 已存在的数量 + skippedCount?: number // 跳过的数量(可选) + skippedDomains?: Array<{ // 跳过的域名列表(可选) + name: string + reason: string + }> +} diff --git a/frontend/types/auth.types.ts b/frontend/types/auth.types.ts new file mode 100644 index 00000000..1123fd22 --- /dev/null +++ b/frontend/types/auth.types.ts @@ -0,0 +1,45 @@ +/** + * 认证相关类型定义 + */ + +// 用户信息 +export interface User { + id: number + username: string + isStaff: boolean + isSuperuser: boolean +} + +// 登录请求 +export interface LoginRequest { + username: string + password: string +} + +// 登录响应 +export interface LoginResponse { + message: string + user: User +} + +// 获取当前用户响应 +export interface MeResponse { + authenticated: boolean + user: User | null +} + +// 登出响应 +export interface LogoutResponse { + message: string +} + +// 修改密码请求 +export interface ChangePasswordRequest { + oldPassword: string + newPassword: string +} + +// 修改密码响应 +export interface ChangePasswordResponse { + message: string +} diff --git a/frontend/types/command.types.ts b/frontend/types/command.types.ts new file mode 100644 index 00000000..da27ca4d --- /dev/null +++ b/frontend/types/command.types.ts @@ -0,0 +1,75 @@ +import { Tool } from "./tool.types" + +/** + * 命令模型 + */ +export interface Command { + id: number + createdAt: string + updatedAt: string + toolId: number + tool?: Tool + name: string + displayName: string + description: string + commandTemplate: string +} + +/** + * 获取命令列表请求参数 + */ +export interface GetCommandsRequest { + page?: number + pageSize?: number + toolId?: number +} + +/** + * 获取命令列表响应 + */ +export interface GetCommandsResponse { + commands: Command[] + page: number + pageSize: number // 后端返回 camelCase 格式 + total: number // 统一使用 total 字段 + totalPages: number // 后端返回 camelCase 格式 + // 兼容字段(向后兼容) + page_size?: number + total_count?: number + total_pages?: number +} + +/** + * 创建命令请求 + */ +export interface CreateCommandRequest { + toolId: number + name: string + displayName?: string + description?: string + commandTemplate: string +} + +/** + * 更新命令请求 + */ +export interface UpdateCommandRequest { + name?: string + displayName?: string + description?: string + commandTemplate?: string +} + +/** + * 命令响应数据 + */ +export interface CommandResponseData { + command: Command +} + +/** + * 批量删除命令响应数据 + */ +export interface BatchDeleteCommandsResponseData { + deletedCount: number +} diff --git a/frontend/types/common.types.ts b/frontend/types/common.types.ts new file mode 100644 index 00000000..0a49da16 --- /dev/null +++ b/frontend/types/common.types.ts @@ -0,0 +1,19 @@ +// 通用类型定义 + +// 分页信息接口 +export interface PaginationInfo { + total: number + page: number + pageSize: number + totalPages: number +} + +// 分页和排序参数接口 +export interface PaginationParams { + page?: number + pageSize?: number + sortBy?: string // 排序字段:id, name, created_at, updated_at(使用下划线命名) + sortOrder?: "asc" | "desc" // 排序方向:asc, desc + search?: string // 搜索关键词 +} + diff --git a/frontend/types/dashboard.types.ts b/frontend/types/dashboard.types.ts new file mode 100644 index 00000000..aac49613 --- /dev/null +++ b/frontend/types/dashboard.types.ts @@ -0,0 +1,53 @@ +export interface DashboardStats { + totalTargets: number + totalSubdomains: number + totalEndpoints: number + totalVulnerabilities: number +} + +/** + * 资产统计数据(预聚合) + */ +export interface VulnBySeverity { + critical: number + high: number + medium: number + low: number + info: number +} + +export interface AssetStatistics { + totalTargets: number + totalSubdomains: number + totalIps: number + totalEndpoints: number + totalWebsites: number + totalVulns: number + totalAssets: number + runningScans: number + updatedAt: string | null + // 变化值 + changeTargets: number + changeSubdomains: number + changeIps: number + changeEndpoints: number + changeWebsites: number + changeVulns: number + changeAssets: number + // 漏洞严重程度分布 + vulnBySeverity: VulnBySeverity +} + +/** + * 统计历史数据(用于折线图) + */ +export interface StatisticsHistoryItem { + date: string + totalTargets: number + totalSubdomains: number + totalIps: number + totalEndpoints: number + totalWebsites: number + totalVulns: number + totalAssets: number +} diff --git a/frontend/types/directory.types.ts b/frontend/types/directory.types.ts new file mode 100644 index 00000000..fca7a101 --- /dev/null +++ b/frontend/types/directory.types.ts @@ -0,0 +1,30 @@ +/** + * Directory 相关类型定义 + */ + +export interface Directory { + id: number + url: string + status: number | null + contentLength: number | null // 后端返回 contentLength + words: number | null + lines: number | null + contentType: string + duration: number | null + websiteUrl: string // 后端返回 websiteUrl + discoveredAt: string // 后端返回 discoveredAt +} + +export interface DirectoryFilters { + url?: string + status?: number + contentType?: string +} + +export interface DirectoryListResponse { + results: Directory[] + total: number + page: number + pageSize: number + totalPages: number +} diff --git a/frontend/types/disk.types.ts b/frontend/types/disk.types.ts new file mode 100644 index 00000000..2e6530aa --- /dev/null +++ b/frontend/types/disk.types.ts @@ -0,0 +1,6 @@ +export interface DiskStats { + totalBytes: number + usedBytes: number + freeBytes: number + usedPercent: number +} diff --git a/frontend/types/endpoint.types.ts b/frontend/types/endpoint.types.ts new file mode 100644 index 00000000..27c5f923 --- /dev/null +++ b/frontend/types/endpoint.types.ts @@ -0,0 +1,100 @@ +// Endpoint 专用数据类型定义 +// 注意:后端返回 snake_case,但 api-client.ts 会自动转换为 camelCase + +import type { BatchCreateResponse } from './api-response.types' + +export interface Endpoint { + id: number + url: string + + // HTTP 元信息(部分场景可为空) + method?: string + statusCode: number | null // 后端: status_code (指针类型,可能为 null) + title: string + contentLength: number | null // 后端: content_length (指针类型,可能为 null) + contentType?: string | null // 后端: content_type (可选) + responseTime?: number | null // 后端: response_time (单位秒,可选) + tags?: string[] | null // 后端: tags/matched_gf_patterns 映射(可选) + + // 站点/端点维度的附加信息(资产表和快照表都会使用) + host?: string + location?: string + webserver?: string + bodyPreview?: string + tech?: string[] + vhost?: boolean | null + discoveredAt?: string + + // 旧版域名关联字段(在部分接口中可能不存在) + domainId?: number // 后端: domain_id + subdomainId?: number // 后端: subdomain_id + domain?: string + subdomain?: string + updatedAt?: string // 后端: updated_at +} + +// Endpoint 列表请求参数 +// 后端固定按更新时间降序排列 +export interface GetEndpointsRequest { + page?: number + pageSize?: number + search?: string +} + +// Endpoint 列表响应数据 +// 注意:后端返回 snake_case,但 api-client.ts 会自动转换为 camelCase +export interface GetEndpointsResponse { + endpoints: Endpoint[] + total: number + page: number + pageSize: number // 后端返回 camelCase 格式 + totalPages: number // 后端返回 camelCase 格式 + // 兼容字段(向后兼容) + page_size?: number + total_pages?: number +} + +// 创建 Endpoint 请求参数 +export interface CreateEndpointRequest { + url: string // 必填 + method?: string // 可选 + statusCode?: number | null // 可选 + title?: string // 可选 + contentLength?: number | null // 可选 + contentType?: string | null // 可选 + responseTime?: number | null // 可选 + tags?: string[] | null // 可选 + domain?: string // 可选 + subdomain?: string // 可选 +} + +// 创建 Endpoint 响应(继承通用批量创建响应) +export interface CreateEndpointsResponse extends BatchCreateResponse { + // 继承的字段:message, requestedCount, createdCount, existedCount +} + +// 更新 Endpoint 请求参数 +export interface UpdateEndpointRequest { + id: number + url?: string + method?: string + statusCode?: number + title?: string + contentLength?: number + contentType?: string | null + responseTime?: number | null + tags?: string[] | null + domain?: string + subdomain?: string +} + +// 批量删除 Endpoint 请求参数 +export interface BatchDeleteEndpointsRequest { + endpointIds: number[] +} + +// 批量删除 Endpoint 响应数据 +export interface BatchDeleteEndpointsResponse { + message: string + deletedCount: number +} diff --git a/frontend/types/engine.types.ts b/frontend/types/engine.types.ts new file mode 100644 index 00000000..5e6c2e68 --- /dev/null +++ b/frontend/types/engine.types.ts @@ -0,0 +1,27 @@ +/** + * 扫描引擎类型定义 + * + * 后端实际返回字段: id, name, configuration, created_at, updated_at + */ + +// 扫描引擎接口 +export interface ScanEngine { + id: number + name: string + configuration?: string // YAML 配置内容 + createdAt: string + updatedAt: string +} + +// 创建引擎请求 +export interface CreateEngineRequest { + name: string + configuration: string +} + +// 更新引擎请求 +export interface UpdateEngineRequest { + name?: string + configuration?: string +} + diff --git a/frontend/types/ip-address.types.ts b/frontend/types/ip-address.types.ts new file mode 100644 index 00000000..e67ecf28 --- /dev/null +++ b/frontend/types/ip-address.types.ts @@ -0,0 +1,27 @@ +export interface Port { + number: number + serviceName: string + description: string + isUncommon: boolean +} + +export interface IPAddress { + ip: string // IP 地址(唯一标识) + hosts: string[] // 关联的主机名列表 + ports: number[] // 关联的端口列表 + discoveredAt: string // 首次发现时间 +} + +export interface GetIPAddressesParams { + page?: number + pageSize?: number + search?: string +} + +export interface GetIPAddressesResponse { + results: IPAddress[] + total: number + page: number + pageSize: number + totalPages: number +} diff --git a/frontend/types/notification-settings.types.ts b/frontend/types/notification-settings.types.ts new file mode 100644 index 00000000..3db6f585 --- /dev/null +++ b/frontend/types/notification-settings.types.ts @@ -0,0 +1,30 @@ +export interface DiscordSettings { + enabled: boolean + webhookUrl: string +} + +/** 通知分类 - 与后端 NotificationCategory 对应 */ +export type NotificationCategory = 'scan' | 'vulnerability' | 'asset' | 'system' + +/** 按分类的通知开关 */ +export interface NotificationCategories { + scan: boolean // 扫描任务 + vulnerability: boolean // 漏洞发现 + asset: boolean // 资产发现 + system: boolean // 系统消息 +} + +export interface NotificationSettings { + discord: DiscordSettings + categories: NotificationCategories +} + +export type GetNotificationSettingsResponse = NotificationSettings + +export type UpdateNotificationSettingsRequest = NotificationSettings + +export interface UpdateNotificationSettingsResponse { + message: string + discord: DiscordSettings + categories: NotificationCategories +} diff --git a/frontend/types/notification.types.ts b/frontend/types/notification.types.ts new file mode 100644 index 00000000..c7c0a5a0 --- /dev/null +++ b/frontend/types/notification.types.ts @@ -0,0 +1,60 @@ +/** + * 通知类型定义 + */ + +// 通知类型枚举(与后端 NotificationCategory 对应) +export type NotificationType = "vulnerability" | "scan" | "asset" | "system" + +// 严重等级(与后端 NotificationLevel 对应) +export type NotificationSeverity = "low" | "medium" | "high" | "critical" + +// 后端通知级别(与后端保持一致) +export type BackendNotificationLevel = NotificationSeverity + +// 后端通知数据格式 +export interface BackendNotification { + id: number + category?: NotificationType + title: string + message: string + level: BackendNotificationLevel + created_at?: string + createdAt?: string + read_at?: string | null + readAt?: string | null + is_read?: boolean + isRead?: boolean +} + +// 通知接口 +export interface Notification { + id: number + type: NotificationType + title: string + description: string + detail?: string + time: string + unread: boolean + severity?: NotificationSeverity + createdAt?: string +} + +// 获取通知列表请求参数 +export interface GetNotificationsRequest { + page?: number + pageSize?: number + type?: NotificationType + unread?: boolean +} + +// 获取通知列表响应 +export interface GetNotificationsResponse { + results: BackendNotification[] + total: number + page: number + pageSize: number // 后端返回 camelCase 格式 + totalPages: number // 后端返回 camelCase 格式 + // 兼容字段(向后兼容) + page_size?: number + total_pages?: number +} diff --git a/frontend/types/nuclei-git.types.ts b/frontend/types/nuclei-git.types.ts new file mode 100644 index 00000000..62e72bf4 --- /dev/null +++ b/frontend/types/nuclei-git.types.ts @@ -0,0 +1,16 @@ +export type NucleiGitAuthType = "none" | "token" + +export interface NucleiGitSettings { + repoUrl: string + authType: NucleiGitAuthType + authToken: string +} + +export type GetNucleiGitSettingsResponse = NucleiGitSettings + +export type UpdateNucleiGitSettingsRequest = NucleiGitSettings + +export interface UpdateNucleiGitSettingsResponse { + message: string + settings: NucleiGitSettings +} diff --git a/frontend/types/nuclei.types.ts b/frontend/types/nuclei.types.ts new file mode 100644 index 00000000..2020e5d7 --- /dev/null +++ b/frontend/types/nuclei.types.ts @@ -0,0 +1,36 @@ +export type NucleiTemplateNodeType = "folder" | "file" + +export interface NucleiTemplateTreeNode { + type: NucleiTemplateNodeType + name: string + path: string + children?: NucleiTemplateTreeNode[] + templateId?: string + severity?: string + tags?: string[] +} + +export interface NucleiTemplateTreeResponse { + roots: NucleiTemplateTreeNode[] +} + +export interface NucleiTemplateContent { + path: string + name: string + templateId?: string + severity?: string + tags?: string[] + content: string +} + +export type NucleiTemplateScope = "custom" | "public" + +export interface UploadNucleiTemplatePayload { + scope: NucleiTemplateScope + file: File +} + +export interface SaveNucleiTemplatePayload { + path: string + content: string +} diff --git a/frontend/types/organization.types.ts b/frontend/types/organization.types.ts new file mode 100644 index 00000000..1ce65a62 --- /dev/null +++ b/frontend/types/organization.types.ts @@ -0,0 +1,83 @@ +import { ColumnDef } from "@tanstack/react-table" +import { PaginationInfo } from "./common.types" + +// 组织统计数据 +export interface OrganizationStats { + totalDomains?: number // 域名总数 + totalEndpoints?: number // 端点总数 + totalTargets?: number // 目标总数 +} + +// 组织相关类型定义(匹配后端 Organization 模型) +export interface Organization { + id: number + name: string + description: string + createdAt: string // 后端 created_at 由 Django 自动转换为 camelCase + updatedAt: string // 后端 updated_at + // 关联数据(通过 serializer 添加) + targets?: Array<{ + id: number + name: string + }> + // 统计数据(可选,通过聚合查询获取) + stats?: OrganizationStats + targetCount?: number // 目标数量(用于列表展示) + domainCount?: number // 域名数量(用于列表展示) + endpointCount?: number // 端点数量(用于列表展示) +} + +// 组织列表响应类型(匹配后端实际响应格式) +export interface OrganizationsResponse<T = Organization> { + results: T[] // 组织数据列表 + total: number // 总记录数(后端实际字段) + page: number // 当前页码 + pageSize: number // 每页大小 + totalPages: number // 总页数 + // 兼容字段 + count?: number // DRF 标准字段(向后兼容) + next?: string | null // 下一页链接(DRF 标准字段) + previous?: string | null // 上一页链接(DRF 标准字段) + organizations?: T[] + pagination?: { + total: number + page: number + pageSize: number + totalPages: number + } +} + + +// 创建组织请求类型 +export interface CreateOrganizationRequest { + name: string + description: string +} + +// 更新组织请求类型 +export interface UpdateOrganizationRequest { + name: string + description: string +} + +// 组织数据表格组件属性类型定义 +export interface OrganizationDataTableProps { + data: Organization[] // 组织数据数组 + columns: ColumnDef<Organization>[] // 列定义数组 + onAddNew?: () => void // 添加新组织的回调函数 + onBulkDelete?: () => void // 批量删除回调函数 + onSelectionChange?: (selectedRows: Organization[]) => void // 选中行变化回调 + searchPlaceholder?: string // 搜索框占位符 + searchColumn?: string // 搜索的列名 + searchValue?: string // 受控:搜索框当前值(服务端搜索) + onSearch?: (value: string) => void // 受控:搜索框变更回调(服务端搜索) + isSearching?: boolean // 搜索中状态(显示加载动画) + // 添加分页相关属性 + pagination?: { + pageIndex: number + pageSize: number + } + setPagination?: (pagination: { pageIndex: number; pageSize: number }) => void + paginationInfo?: PaginationInfo + onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void +} diff --git a/frontend/types/psl.d.ts b/frontend/types/psl.d.ts new file mode 100644 index 00000000..b428aa3d --- /dev/null +++ b/frontend/types/psl.d.ts @@ -0,0 +1,15 @@ +declare module 'psl' { + export interface ParsedDomain { + input: string + tld: string | null + sld: string | null + domain: string | null + subdomain: string | null + listed: boolean + error?: Error + } + + export function parse(domain: string): ParsedDomain + export function get(domain: string): string | null + export function isValid(domain: string): boolean +} diff --git a/frontend/types/scan.types.ts b/frontend/types/scan.types.ts new file mode 100644 index 00000000..189f3869 --- /dev/null +++ b/frontend/types/scan.types.ts @@ -0,0 +1,125 @@ +/** + * 扫描任务状态枚举 + * 与后端 ScanStatus 保持一致 + */ +export type ScanStatus = "cancelled" | "completed" | "failed" | "initiated" | "running" + +/** + * 扫描阶段(动态,来自 engine_config 的 key) + */ +export type ScanStage = string + +/** + * 阶段进度状态 + */ +export type StageStatus = "pending" | "running" | "completed" | "failed" | "cancelled" + +/** + * 单个阶段的进度信息 + */ +export interface StageProgressItem { + status: StageStatus + order: number // 执行顺序(从 0 开始) + startedAt?: string // ISO 时间字符串 + duration?: number // 执行时长(秒) + detail?: string // 完成详情 + error?: string // 错误信息 + reason?: string // 跳过原因 +} + +/** + * 各阶段进度字典(动态 key) + */ +export type StageProgress = Record<string, StageProgressItem> + +export interface ScanRecord { + id: number + target?: number // 目标ID(对应后端 target) + targetName: string // 目标名称(对应后端 targetName) + summary: { + subdomains: number + websites: number + directories: number + endpoints: number + ips: number + vulnerabilities: { + total: number + critical: number + high: number + medium: number + low: number + } + } + engine?: number // 引擎ID(对应后端 engine) + engineName: string // 引擎名称(对应后端 engineName) + createdAt: string // 创建时间(对应后端 createdAt) + status: ScanStatus + progress: number // 0-100 + currentStage?: ScanStage // 当前扫描阶段(仅 running 状态有值) + stageProgress?: StageProgress // 各阶段进度详情 +} + +export interface GetScansParams { + page?: number + pageSize?: number + status?: ScanStatus + search?: string +} + +export interface GetScansResponse { + results: ScanRecord[] // 对应后端 results 字段 + total: number + page: number + pageSize: number + totalPages: number +} + +/** + * 发起扫描请求参数(用于已存在的目标/组织) + */ +export interface InitiateScanRequest { + organizationId?: number // 组织ID(二选一) + targetId?: number // 目标ID(二选一) + engineId: number // 扫描引擎ID(必填) +} + +/** + * 快速扫描请求参数(自动创建目标并扫描) + */ +export interface QuickScanRequest { + targets: { name: string }[] // 目标列表 + engineId: number // 扫描引擎ID(必填) +} + +/** + * 快速扫描响应 + */ +export interface QuickScanResponse { + message: string + targetStats: { + created: number + failed: number + } + scans: ScanTask[] +} + +/** + * 单个扫描任务信息 + */ +export interface ScanTask { + id: number + target: number // 目标ID + engine: number // 引擎ID + status: ScanStatus + createdAt: string + updatedAt: string +} + +/** + * 发起扫描响应 + */ +export interface InitiateScanResponse { + message: string // 成功消息 + count: number // 创建的扫描任务数量 + scans: ScanTask[] // 扫描任务列表 +} diff --git a/frontend/types/scheduled-scan.types.ts b/frontend/types/scheduled-scan.types.ts new file mode 100644 index 00000000..384cd03d --- /dev/null +++ b/frontend/types/scheduled-scan.types.ts @@ -0,0 +1,58 @@ +/** + * 定时扫描类型定义 + */ + +// 定时扫描状态 +export type ScheduledScanStatus = "active" | "paused" | "expired" + +// 扫描模式 +export type ScanMode = 'organization' | 'target' + +// 定时扫描接口 +export interface ScheduledScan { + id: number + name: string + engine: number // 关联的扫描引擎ID + engineName: string // 关联的扫描引擎名称 + organizationId: number | null // 组织 ID(组织扫描模式) + organizationName: string | null // 组织名称 + targetId: number | null // 目标 ID(目标扫描模式) + targetName: string | null // 目标名称(目标扫描模式) + scanMode: ScanMode // 扫描模式 + cronExpression: string // Cron 表达式 + isEnabled: boolean // 是否启用 + nextRunTime?: string // 下次执行时间 + lastRunTime?: string // 上次执行时间 + runCount: number // 已执行次数 + createdAt: string + updatedAt: string +} + +// 创建定时扫描请求(organizationId 和 targetId 互斥) +export interface CreateScheduledScanRequest { + name: string + engineId: number + organizationId?: number // 组织扫描模式 + targetId?: number // 目标扫描模式 + cronExpression: string // Cron 表达式,格式:分 时 日 月 周 + isEnabled?: boolean +} + +// 更新定时扫描请求(organizationId 和 targetId 互斥) +export interface UpdateScheduledScanRequest { + name?: string + engineId?: number + organizationId?: number // 组织扫描模式(设置后清空 targetId) + targetId?: number // 目标扫描模式(设置后清空 organizationId) + cronExpression?: string + isEnabled?: boolean +} + +// API 响应 +export interface GetScheduledScansResponse { + results: ScheduledScan[] + total: number + page: number + pageSize: number + totalPages: number +} diff --git a/frontend/types/subdomain.types.ts b/frontend/types/subdomain.types.ts new file mode 100644 index 00000000..67f4f111 --- /dev/null +++ b/frontend/types/subdomain.types.ts @@ -0,0 +1,71 @@ +import { ColumnDef } from "@tanstack/react-table" +import { PaginationParams, PaginationInfo } from "./common.types" +import type { Organization } from "./organization.types" +import type { BatchCreateResponse } from "./api-response.types" +import type { Port } from "./ip-address.types" + +// 子域名相关类型定义(由原 domain.types.ts 重命名而来) + +// 基础子域名类型(与前端驼峰命名规范一致) +// 注意:后端返回 snake_case,但响应拦截器会自动转换为 camelCase +export interface Subdomain { + id: number + name: string + discoveredAt: string // 发现时间 +} + +// 获取子域名列表请求参数 +export interface GetSubdomainsParams extends PaginationParams { + organizationId: number +} + +// 获取子域名列表响应(字段 domains 保持与后端一致) +export interface GetSubdomainsResponse { + domains: Subdomain[] + total: number + page: number + pageSize: number // [OK] 使用驼峰命名 + totalPages: number // [OK] 使用驼峰命名 +} + +// 获取所有子域名请求参数 +// 后端固定按更新时间降序排列,不支持自定义排序 +export interface GetAllSubdomainsParams { + page?: number + pageSize?: number + search?: string +} + +// 获取所有子域名响应(字段 domains 保持与后端一致) +export interface GetAllSubdomainsResponse { + domains: Subdomain[] + total: number + page: number + pageSize: number + totalPages: number +} + +// 获取单个子域名详情响应(后端直接返回对象) +export type GetSubdomainByIDResponse = Subdomain + +// 子域名数据表格组件属性类型定义 +export interface SubdomainDataTableProps { + data: Subdomain[] // 子域名数据数组 + columns: ColumnDef<Subdomain>[] // 列定义数组 + onAddNew?: () => void // 添加新子域名的回调函数 + onBulkDelete?: () => void // 批量删除回调函数 + onSelectionChange?: (selectedRows: Subdomain[]) => void // 选中行变化回调 + searchPlaceholder?: string // 搜索框占位符 + searchColumn?: string // 搜索的列名 + // 添加分页相关属性 + pagination?: { + pageIndex: number + pageSize: number + } + setPagination?: (pagination: { pageIndex: number; pageSize: number }) => void + paginationInfo?: PaginationInfo + onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void +} + +// 子域名批量创建响应(复用通用类型) +export type BatchCreateSubdomainsResponse = BatchCreateResponse diff --git a/frontend/types/target.types.ts b/frontend/types/target.types.ts new file mode 100644 index 00000000..2eff0818 --- /dev/null +++ b/frontend/types/target.types.ts @@ -0,0 +1,116 @@ +/** + * Target 类型定义 + */ + +/** + * 目标类型 + */ +export type TargetType = 'domain' | 'ip' | 'cidr' + +/** + * 目标基础信息(用于列表显示) + */ +export interface Target { + id: number + name: string + type: TargetType // 后端字段:type + description?: string + createdAt: string // 后端字段:created_at,自动转换为 createdAt + lastScannedAt?: string // 后端字段:last_scanned_at,自动转换为 lastScannedAt + // 关联数据(通过 serializer 添加) + organizations?: Array<{ + id: number + name: string + }> +} + +/** + * 目标详情信息(包含统计数据) + */ +export interface TargetDetail extends Target { + summary: { + subdomains: number + websites: number + endpoints: number + ips: number + vulnerabilities: { + total: number + critical: number + high: number + medium: number + low: number + } + } +} + +/** + * 目标列表响应类型 + */ +export interface TargetsResponse { + results: Target[] + total: number // 后端返回 total,不是 count + page: number // 当前页码 + pageSize: number // 每页大小 + totalPages: number // 总页数 + // 兼容字段(为了向后兼容) + count?: number // 可选,等同于 total + next?: string | null + previous?: string | null +} + +/** + * 创建目标的请求参数 + */ +export interface CreateTargetRequest { + name: string + description?: string +} + +/** + * 更新目标的请求参数 + */ +export interface UpdateTargetRequest { + name?: string + description?: string +} + +/** + * 批量删除目标的请求参数 + */ +export interface BatchDeleteTargetsRequest { + ids: number[] +} + +/** + * 批量删除目标的响应 + */ +export interface BatchDeleteTargetsResponse { + deletedCount: number + failedIds?: number[] +} + +/** + * 批量创建目标的请求参数 + */ +export interface BatchCreateTargetsRequest { + targets: Array<{ + name: string + description?: string + }> + organizationId?: number // 可选:关联到指定组织 +} + +/** + * 批量创建目标的响应 + */ +export interface BatchCreateTargetsResponse { + createdCount: number + reusedCount: number + failedCount: number + failedTargets: Array<{ + name: string + reason: string + }> + message: string +} + diff --git a/frontend/types/tool.types.ts b/frontend/types/tool.types.ts new file mode 100644 index 00000000..f659e2f9 --- /dev/null +++ b/frontend/types/tool.types.ts @@ -0,0 +1,89 @@ +// 工具类型枚举 +export type ToolType = 'opensource' | 'custom' + +// 工具类型定义(匹配前端 camelCase 转换后的格式) +// 注意:后端返回 snake_case,api-client.ts 自动转换为 camelCase +export interface Tool { + id: number + name: string // 工具名称 + type: ToolType // 工具类型:opensource/custom(后端: type) + repoUrl: string // 仓库地址(后端: repo_url) + version: string // 版本号 + description: string // 工具描述 + categoryNames: string[] // 分类标签数组(后端: category_names) + directory: string // 工具路径(后端: directory) + installCommand: string // 安装命令(后端: install_command) + updateCommand: string // 更新命令(后端: update_command) + versionCommand: string // 版本查询命令(后端: version_command) + createdAt: string // 后端: created_at + updatedAt: string // 后端: updated_at +} + +// 工具分类名称到中文的映射 +// 所有分类参考后端模型设计文档 +export const CategoryNameMap: Record<string, string> = { + subdomain: '子域名扫描', + vulnerability: '漏洞扫描', + port: '端口扫描', + directory: '目录扫描', + dns: 'DNS解析', + http: 'HTTP探测', + crawler: '网页爬虫', + recon: '信息收集', + fuzzer: '模糊测试', + wordlist: '字典生成', + screenshot: '截图工具', + exploit: '漏洞利用', + network: '网络扫描', + other: '其他', +} + +// 工具列表响应类型(api-client.ts 会自动转换为 camelCase) +export interface GetToolsResponse { + tools: Tool[] + total: number + page: number + pageSize: number // 后端返回 camelCase 格式 + totalPages: number // 后端返回 camelCase 格式 + // 兼容字段(向后兼容) + page_size?: number + total_pages?: number +} + +// 创建工具请求类型 +export interface CreateToolRequest { + name: string + type: ToolType // 工具类型(必填) + repoUrl?: string + version?: string + description?: string + categoryNames?: string[] // 分类标签数组 + directory?: string // 工具路径(自定义工具必填) + installCommand?: string // 安装命令(开源工具必填) + updateCommand?: string // 更新命令(开源工具必填) + versionCommand?: string // 版本查询命令(开源工具必填) +} + +// 更新工具请求类型 +export interface UpdateToolRequest { + name?: string + type?: ToolType // 工具类型(用于验证命令字段) + repoUrl?: string + version?: string + description?: string + categoryNames?: string[] // 分类标签数组 + directory?: string // 工具路径 + installCommand?: string // 安装命令 + updateCommand?: string // 更新命令 + versionCommand?: string // 版本查询命令 +} + +// 工具查询参数 +// 后端固定按更新时间降序排列,不支持自定义排序 +export interface GetToolsParams { + page?: number + pageSize?: number +} + +// 工具过滤类型 +export type ToolFilter = 'all' | 'default' | 'custom' diff --git a/frontend/types/vulnerability.types.ts b/frontend/types/vulnerability.types.ts new file mode 100644 index 00000000..d7872c61 --- /dev/null +++ b/frontend/types/vulnerability.types.ts @@ -0,0 +1,106 @@ +import { ColumnDef } from "@tanstack/react-table" +import { PaginationParams, PaginationInfo } from "./common.types" +import type { BatchCreateResponse } from "./api-response.types" + +// 漏洞相关类型定义 + +// 漏洞严重程度 +export type VulnerabilitySeverity = "critical" | "high" | "medium" | "low" | "info" + +// 漏洞状态 +export type VulnerabilityStatus = "open" | "in_progress" | "resolved" | "false_positive" | "accepted" + +// 工具原始输出(JSON) +export interface VulnerabilityRawOutput { + // Dalfox 字段 + type?: string // R=Reflected, S=Stored + inject_type?: string // 注入类型 + method?: string // HTTP 方法 + data?: string // URL + param?: string // 参数名 + payload?: string // payload + evidence?: string // 证据 + cwe?: string // CWE + message_str?: string // 消息 + + // Nuclei 字段 + "template-id"?: string + "template-path"?: string + "matched-at"?: string + host?: string + request?: string + response?: string + "curl-command"?: string + ip?: string + info?: { + name?: string + description?: string + severity?: string + tags?: string[] + reference?: string[] + classification?: { + "cve-id"?: string + "cwe-id"?: string[] + } + } + + // 其他字段 + [key: string]: unknown +} + +// 基础漏洞类型(字段名匹配后端 DRF 序列化器输出 - 驼峰格式) +export interface Vulnerability { + id: number + target?: number // 关联的目标ID + url: string // 漏洞所在的URL + vulnType: string // 漏洞类型(如 xss-reflected, template-id) + severity: VulnerabilitySeverity + source: string // 漏洞来源(dalfox, nuclei) + cvssScore?: number // CVSS评分 + description?: string // 简化描述 + rawOutput?: VulnerabilityRawOutput // 工具原始输出 + discoveredAt: string // 发现时间 +} + +// 获取漏洞列表请求参数 +export interface GetVulnerabilitiesParams extends PaginationParams { + targetId?: number + domainId?: number + endpointId?: number + severity?: VulnerabilitySeverity + status?: VulnerabilityStatus +} + +// 获取漏洞列表响应 +export interface GetVulnerabilitiesResponse { + vulnerabilities: Vulnerability[] + total: number + page: number + pageSize: number + totalPages: number +} + +// 获取单个漏洞详情响应 +export type GetVulnerabilityByIDResponse = Vulnerability + +// 漏洞数据表格组件属性类型定义 +export interface VulnerabilityDataTableProps { + data: Vulnerability[] + columns: ColumnDef<Vulnerability>[] + onAddNew?: () => void + onBulkDelete?: () => void + onSelectionChange?: (selectedRows: Vulnerability[]) => void + searchPlaceholder?: string + searchColumn?: string + pagination?: { + pageIndex: number + pageSize: number + } + setPagination?: (pagination: { pageIndex: number; pageSize: number }) => void + paginationInfo?: PaginationInfo + onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void +} + +// 漏洞批量创建响应 +export type BatchCreateVulnerabilitiesResponse = BatchCreateResponse + diff --git a/frontend/types/website.types.ts b/frontend/types/website.types.ts new file mode 100644 index 00000000..1dcd28f6 --- /dev/null +++ b/frontend/types/website.types.ts @@ -0,0 +1,44 @@ +/** + * WebSite 相关类型定义 + */ + +export interface WebSite { + id: number + scan?: number + target?: number + url: string + location: string + title: string + webserver: string + contentType: string + statusCode: number + contentLength: number + bodyPreview: string + tech: string[] + vhost: boolean | null + subdomain: string + discoveredAt: string +} + +export interface Technology { + id: number + name: string + version?: string + category?: string +} + +export interface WebSiteFilters { + url?: string + title?: string + statusCode?: number + webserver?: string + contentType?: string +} + +export interface WebSiteListResponse { + results: WebSite[] + total: number + page: number + pageSize: number + totalPages: number +} diff --git a/frontend/types/wordlist.types.ts b/frontend/types/wordlist.types.ts new file mode 100644 index 00000000..7cfe827b --- /dev/null +++ b/frontend/types/wordlist.types.ts @@ -0,0 +1,23 @@ +// 字典(Wordlist)相关类型 + +import type { PaginationInfo } from "@/types/common.types" + +// 字典基础信息 +export interface Wordlist { + id: number + name: string + description?: string + // 文件大小(字节),可选,由后端返回 + fileSize?: number + // 行数,便于估算耗时,可选,由后端返回 + lineCount?: number + // 文件 SHA-256 哈希,用于缓存校验 + fileHash?: string + createdAt: string + updatedAt: string +} + +// 获取字典列表响应(遵循统一分页结构) +export interface GetWordlistsResponse extends PaginationInfo { + results: Wordlist[] +} diff --git a/frontend/types/worker.types.ts b/frontend/types/worker.types.ts new file mode 100644 index 00000000..ec9da908 --- /dev/null +++ b/frontend/types/worker.types.ts @@ -0,0 +1,49 @@ +/** + * Worker 节点相关类型定义 + */ + +// Worker 状态枚举(前后端统一) +export type WorkerStatus = 'pending' | 'deploying' | 'online' | 'offline' + +// Worker 节点 +export interface WorkerNode { + id: number + name: string + ipAddress: string + sshPort: number + username: string + status: WorkerStatus + isLocal: boolean // 是否为本地节点(Docker 容器内) + createdAt: string + updatedAt?: string + info?: { + cpuPercent?: number + memoryPercent?: number + } +} + +// 创建 Worker 请求 +export interface CreateWorkerRequest { + name: string + ipAddress: string + sshPort?: number + username?: string + password: string +} + +// 更新 Worker 请求 +export interface UpdateWorkerRequest { + name?: string + sshPort?: number + username?: string + password?: string +} + +// Worker 列表响应 +export interface WorkersResponse { + results: WorkerNode[] + total: number + page: number + pageSize: number +} + diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..ab28e2dd --- /dev/null +++ b/install.sh @@ -0,0 +1,381 @@ +#!/bin/bash +set -e + +# ============================================================================== +# 用法: +# sudo ./install.sh 生产模式(拉取 Docker Hub 镜像) +# sudo ./install.sh --dev 开发模式(本地构建 + 调试日志) +# sudo ./install.sh --no-frontend 安装并只启动后端 +# sudo ./install.sh --dev --no-frontend 开发模式 + 只启动后端 +# ============================================================================== + +# 解析参数 +START_ARGS="" +DEV_MODE=false +for arg in "$@"; do + case $arg in + --dev) + DEV_MODE=true + START_ARGS="$START_ARGS --dev" + ;; + --no-frontend) + START_ARGS="$START_ARGS --no-frontend" + ;; + esac +done + +# ============================================================================== +# 颜色定义 +# ============================================================================== +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +RESET='\033[0m' + +# ============================================================================== +# 日志函数 +# ============================================================================== +info() { + echo -e "${BLUE}[INFO]${RESET} $1" +} + +success() { + echo -e "${GREEN}[OK]${RESET} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${RESET} $1" +} + +error() { + echo -e "${RED}[ERROR]${RESET} $1" +} + +step() { + echo -e "\n${BOLD}${CYAN}>>> $1${RESET}" +} + +header() { + echo -e "${BOLD}${BLUE}============================================================${RESET}" + echo -e "${BOLD}${BLUE} $1${RESET}" + echo -e "${BOLD}${BLUE}============================================================${RESET}" +} + +# ============================================================================== +# 权限检查 +# ============================================================================== +if [ "$EUID" -ne 0 ]; then + error "请使用 sudo 运行此脚本" + echo -e " 正确用法: ${BOLD}sudo ./install.sh${RESET}" + exit 1 +fi + +# 获取真实用户(通过 sudo 运行时 $SUDO_USER 是真实用户) +REAL_USER="${SUDO_USER:-$USER}" +REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6) + +# 项目根目录 +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$ROOT_DIR" + +# 显示标题 +header "XingRin 一键安装脚本 (Ubuntu)" +info "当前用户: ${BOLD}$REAL_USER${RESET}" +info "项目路径: ${BOLD}$ROOT_DIR${RESET}" + +# ============================================================================== +# 工具函数 +# ============================================================================== + +# 生成随机字符串 +generate_random_string() { + local length="${1:-32}" + if command -v openssl >/dev/null 2>&1; then + openssl rand -hex "$length" 2>/dev/null | cut -c1-"$length" + else + date +%s%N | sha256sum | cut -c1-"$length" + fi +} + +# 更新 .env 文件中的某个键 +update_env_var() { + local file="$1" + local key="$2" + local value="$3" + if grep -q "^$key=" "$file"; then + sed -i -e "s|^$key=.*|$key=$value|" "$file" + else + echo "$key=$value" >> "$file" + fi +} + +# 用于保存生成的密码,方便最后显示 +GENERATED_DB_PASSWORD="" +GENERATED_DJANGO_KEY="" + +# 生成自签 HTTPS 证书(无域名场景) +generate_self_signed_cert() { + local ssl_dir="$DOCKER_DIR/nginx/ssl" + local fullchain="$ssl_dir/fullchain.pem" + local privkey="$ssl_dir/privkey.pem" + + if [ -f "$fullchain" ] && [ -f "$privkey" ]; then + success "检测到已有 HTTPS 证书,跳过自签" + return + fi + + info "未检测到 HTTPS 证书,正在生成自签证书(localhost)..." + mkdir -p "$ssl_dir" + if openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout "$privkey" \ + -out "$fullchain" \ + -subj "/C=CN/ST=NA/L=NA/O=XingRin/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" >/dev/null 2>&1; then + success "自签证书已生成: $ssl_dir" + else + warn "自签证书生成失败,请检查 openssl 是否可用,或手动放置证书到 $ssl_dir" + fi +} + +# 自动为 docker/.env 填充敏感变量 +auto_fill_docker_env_secrets() { + local env_file="$1" + info "自动生成 DJANGO_SECRET_KEY 和 DB_PASSWORD..." + GENERATED_DJANGO_KEY="$(generate_random_string 64)" + GENERATED_DB_PASSWORD="$(generate_random_string 32)" + update_env_var "$env_file" "DJANGO_SECRET_KEY" "$GENERATED_DJANGO_KEY" + update_env_var "$env_file" "DB_PASSWORD" "$GENERATED_DB_PASSWORD" + success "密钥生成完成" +} + +# 显示安装总结信息 +show_summary() { + echo + if [ "$1" == "success" ]; then + header "服务已成功启动!" + else + header "安装完成 Summary" + fi + + if [ -f "$DOCKER_DIR/.env" ]; then + # 从 .env 读取配置用于显示 + DB_HOST=$(grep "^DB_HOST=" "$DOCKER_DIR/.env" | cut -d= -f2) + DB_USER=$(grep "^DB_USER=" "$DOCKER_DIR/.env" | cut -d= -f2) + DB_PASSWORD=$(grep "^DB_PASSWORD=" "$DOCKER_DIR/.env" | cut -d= -f2) + + echo -e "${YELLOW}数据库配置:${RESET}" + echo -e "------------------------------------------------------------" + echo -e " 服务器地址: ${DB_HOST:-未知}" + echo -e " 用户名: ${DB_USER:-未知}" + echo -e " 密码: ${DB_PASSWORD:-未知}" + echo -e "------------------------------------------------------------" + echo + fi + + # 获取访问地址 + PUBLIC_HOST=$(grep "^PUBLIC_HOST=" "$DOCKER_DIR/.env" 2>/dev/null | cut -d= -f2) + if [ -n "$PUBLIC_HOST" ] && [ "$PUBLIC_HOST" != "server" ]; then + ACCESS_HOST="$PUBLIC_HOST" + else + ACCESS_HOST="localhost" + fi + + echo -e "${GREEN}访问地址:${RESET}" + printf " %-16s %s\n" "XingRin:" "https://${ACCESS_HOST}/" + echo -e " ${YELLOW}(HTTP 会自动跳转到 HTTPS)${RESET}" + echo + + echo -e "${YELLOW}默认登录账号:${RESET}" + printf " %-16s %s\n" "用户名:" "admin" + printf " %-16s %s\n" "密码:" "admin" + echo -e "${YELLOW} [!] 请首次登录后修改密码!${RESET}" + echo + + if [ "$1" != "success" ]; then + echo -e "${GREEN}后续启动命令:${RESET}" + echo -e " ./start.sh # 启动所有服务" + echo -e " ./start.sh --no-frontend # 只启动后端" + echo -e " ./stop.sh # 停止所有服务" + echo -e " ./update.sh # 更新系统" + echo + fi +} + +# ============================================================================== +# 安装流程 +# ============================================================================== + +step "[1/3] 检查基础命令" +MISSING_CMDS=() +for cmd in git curl jq openssl; do + if ! command -v "$cmd" >/dev/null 2>&1; then + MISSING_CMDS+=("$cmd") + warn "未安装: $cmd" + else + success "已安装: $cmd" + fi +done + +if [ ${#MISSING_CMDS[@]} -gt 0 ]; then + info "正在安装缺失命令: ${MISSING_CMDS[*]}..." + apt update -qq + apt install -y "${MISSING_CMDS[@]}" + success "基础命令安装完成" +fi + +step "[2/3] 检查 Docker 环境" +if command -v docker >/dev/null 2>&1; then + success "已安装: docker" +else + info "正在安装 Docker..." + curl -fsSL https://get.docker.com | sh + usermod -aG docker "$REAL_USER" + success "Docker 安装完成" +fi + +# 检查 docker compose +if docker compose version >/dev/null 2>&1; then + success "已安装: docker compose" +else + info "正在安装 docker-compose-plugin..." + apt install -y docker-compose-plugin + success "docker compose 安装完成" +fi + +step "[3/3] 初始化配置" +DOCKER_DIR="$ROOT_DIR/docker" +if [ ! -d "$DOCKER_DIR" ]; then + error "未找到 docker 目录,请确认项目结构。" + exit 1 +fi + +if [ -f "$DOCKER_DIR/.env.example" ]; then + cp "$DOCKER_DIR/.env.example" "$DOCKER_DIR/.env" + success "已创建: docker/.env" + auto_fill_docker_env_secrets "$DOCKER_DIR/.env" + + # 开发模式:开启调试日志 + if [ "$DEV_MODE" = true ]; then + info "开发模式:开启调试配置..." + update_env_var "$DOCKER_DIR/.env" "DEBUG" "True" + update_env_var "$DOCKER_DIR/.env" "LOG_LEVEL" "INFO" + update_env_var "$DOCKER_DIR/.env" "ENABLE_COMMAND_LOGGING" "true" + success "已开启: DEBUG=True, LOG_LEVEL=INFO, ENABLE_COMMAND_LOGGING=true" + fi + + # 询问数据库配置 + echo "" + echo -n -e "${BOLD}${CYAN}[?] 是否使用远程 PostgreSQL 数据库?(y/N) ${RESET}" + read -r use_remote_db + echo + + if [[ $use_remote_db =~ ^[Yy]$ ]]; then + echo -e "${CYAN} 请输入远程 PostgreSQL 配置:${RESET}" + + # 服务器地址(必填) + echo -n -e " ${CYAN}服务器地址: ${RESET}" + read -r db_host + if [ -z "$db_host" ]; then + error "服务器地址不能为空" + exit 1 + fi + + # 端口(可选) + echo -n -e " ${CYAN}端口 [5432]: ${RESET}" + read -r db_port + db_port=${db_port:-5432} + + # 用户名(必填) + echo -n -e " ${CYAN}用户名: ${RESET}" + read -r db_user + if [ -z "$db_user" ]; then + error "用户名不能为空" + exit 1 + fi + + # 密码(必填) + echo -n -e " ${CYAN}密码: ${RESET}" + read -r db_password + if [ -z "$db_password" ]; then + error "密码不能为空" + exit 1 + fi + + # 验证远程 PostgreSQL 连接(使用官方 postgres 镜像中的 psql) + echo + info "正在验证远程 PostgreSQL 连接..." + # 使用 postgres 默认库验证连接(每个 PostgreSQL 都有这个库) + if ! docker run --rm \ + -e PGPASSWORD="$db_password" \ + postgres:15 \ + psql "postgresql://$db_user@$db_host:$db_port/postgres" -c 'SELECT 1' >/dev/null 2>&1; then + echo + error "无法连接到远程 PostgreSQL,请检查 IP/端口/用户名/密码是否正确" + echo " 尝试连接: postgresql://$db_user@$db_host:$db_port/postgres" + exit 1 + fi + success "远程 PostgreSQL 连接验证通过" + + # 尝试创建业务数据库(如果不存在) + info "检查并创建数据库..." + db_name=$(grep "^DB_NAME=" "$DOCKER_DIR/.env" | cut -d= -f2) + db_name=${db_name:-xingrin} + prefect_db=$(grep "^PREFECT_DB_NAME=" "$DOCKER_DIR/.env" | cut -d= -f2) + prefect_db=${prefect_db:-prefect} + + docker run --rm -e PGPASSWORD="$db_password" postgres:15 \ + psql "postgresql://$db_user@$db_host:$db_port/postgres" \ + -c "CREATE DATABASE $db_name;" 2>/dev/null || true + docker run --rm -e PGPASSWORD="$db_password" postgres:15 \ + psql "postgresql://$db_user@$db_host:$db_port/postgres" \ + -c "CREATE DATABASE $prefect_db;" 2>/dev/null || true + success "数据库准备完成" + + sed -i "s/^DB_HOST=.*/DB_HOST=$db_host/" "$DOCKER_DIR/.env" + sed -i "s/^DB_PORT=.*/DB_PORT=$db_port/" "$DOCKER_DIR/.env" + sed -i "s/^DB_USER=.*/DB_USER=$db_user/" "$DOCKER_DIR/.env" + sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=$db_password/" "$DOCKER_DIR/.env" + success "已配置远程数据库: $db_user@$db_host:$db_port" + else + info "使用本地 PostgreSQL 容器" + fi + + # 是否为远程 VPS 部署(需要从其它机器 / Worker 访问本系统) + echo "" + echo -n -e "${BOLD}${CYAN}[?] 当前是否为远程 VPS 部署?(y/N) ${RESET}" + read -r set_public_host + echo + if [[ $set_public_host =~ ^[Yy]$ ]]; then + echo -n -e " ${CYAN}请输入当前远程 vps 的外网 IP 地址(例如 10.1.1.1): ${RESET}" + read -r public_host + if [ -z "$public_host" ]; then + warn "未输入外网ip地址,将保持 .env 中已有的 PUBLIC_HOST(请确保 Worker 能访问该地址)" + else + update_env_var "$DOCKER_DIR/.env" "PUBLIC_HOST" "$public_host" + success "已配置对外访问地址: $public_host" + fi + else + info "检测为本机 docker 部署,将 PUBLIC_HOST 设置为 server(容器内部访问后端服务名)" + update_env_var "$DOCKER_DIR/.env" "PUBLIC_HOST" "server" + fi +else + error "未找到 docker/.env.example" + exit 1 +fi + +# 准备 HTTPS 证书(无域名也可使用自签) +generate_self_signed_cert + +# ============================================================================== +# 启动服务 +# ============================================================================== +step "正在启动服务..." +"$ROOT_DIR/start.sh" $START_ARGS + +# ============================================================================== +# 完成总结 +# ============================================================================== +show_summary "success" diff --git a/restart.sh b/restart.sh new file mode 100755 index 00000000..38615419 --- /dev/null +++ b/restart.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# 重启服务(Docker 部署) +cd "$(dirname "$0")" +exec ./docker/restart.sh diff --git a/start.sh b/start.sh new file mode 100755 index 00000000..7fc18d26 --- /dev/null +++ b/start.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# 启动服务(所有服务均在 Docker 中运行) +# +# 用法: +# ./start.sh 生产模式 - 拉取 Docker Hub 镜像启动 +# ./start.sh --dev 开发模式 - 本地构建镜像启动 +# ./start.sh --no-frontend 只启动后端(前端手动启动) +# ./start.sh --dev --no-frontend 开发模式 + 只启动后端 + +cd "$(dirname "$0")" +exec ./docker/start.sh "$@" diff --git a/stop.sh b/stop.sh new file mode 100755 index 00000000..66de13f7 --- /dev/null +++ b/stop.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# 停止服务(所有服务均在 Docker 中运行) +cd "$(dirname "$0")" +exec ./docker/stop.sh "$@" diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 00000000..d713a0e4 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,186 @@ +#!/bin/bash +set -e + +# ============================================================================== +# 颜色定义 +# ============================================================================== +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +RESET='\033[0m' + +# ============================================================================== +# 日志函数 +# ============================================================================== +info() { + echo -e "${BLUE}[INFO]${RESET} $1" +} + +success() { + echo -e "${GREEN}[OK]${RESET} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${RESET} $1" +} + +error() { + echo -e "${RED}[ERROR]${RESET} $1" +} + +step() { + echo -e "\n${BOLD}${CYAN}>>> $1${RESET}" +} + +header() { + echo -e "${BOLD}${BLUE}============================================================${RESET}" + echo -e "${BOLD}${BLUE} $1${RESET}" + echo -e "${BOLD}${BLUE}============================================================${RESET}" +} + +# ============================================================================== +# 权限检查 +# ============================================================================== +if [ "$EUID" -ne 0 ]; then + error "请使用 sudo 运行此脚本" + echo -e " 正确用法: ${BOLD}sudo ./uninstall.sh${RESET}" + exit 1 +fi + +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +DOCKER_DIR="$ROOT_DIR/docker" + +header "XingRin 一键卸载脚本 (Ubuntu)" +info "项目路径: ${BOLD}$ROOT_DIR${RESET}" + +if [ ! -d "$DOCKER_DIR" ]; then + error "未找到 docker 目录,请确认项目结构。" + exit 1 +fi + +# ============================================================================== +# 1. 停止并删除全部容器/网络 +# ============================================================================== +step "[1/6] 是否停止并删除全部容器/网络?(Y/n)" +read -r ans_stop +ans_stop=${ans_stop:-Y} + +if [[ $ans_stop =~ ^[Yy]$ ]]; then + info "正在停止并删除容器/网络..." + cd "$DOCKER_DIR" + if command -v docker compose >/dev/null 2>&1; then + COMPOSE_CMD="docker compose" + else + COMPOSE_CMD="docker-compose" + fi + + # 先强制停止并删除可能占用网络的容器(xingrin-agent 等) + docker rm -f xingrin-agent xingrin-watchdog 2>/dev/null || true + + # 停止两种模式的容器 + [ -f "docker-compose.yml" ] && ${COMPOSE_CMD} -f docker-compose.yml down 2>/dev/null || true + [ -f "docker-compose.dev.yml" ] && ${COMPOSE_CMD} -f docker-compose.dev.yml down 2>/dev/null || true + + # 手动删除网络(以防 compose 未能删除) + docker network rm xingrin_network 2>/dev/null || true + + success "容器和网络已停止/删除(如存在)。" +else + warn "已跳过停止/删除容器/网络。" +fi + +# ============================================================================== +# 2. 删除扫描日志和结果目录 +# ============================================================================== +LOGS_DIR="$ROOT_DIR/backend/logs" +RESULTS_DIR="$ROOT_DIR/backend/results" + +step "[2/6] 是否删除扫描日志和结果目录 ($LOGS_DIR, $RESULTS_DIR)?(Y/n)" +read -r ans_logs +ans_logs=${ans_logs:-Y} + +if [[ $ans_logs =~ ^[Yy]$ ]]; then + info "正在删除日志和结果目录..." + rm -rf "$LOGS_DIR" "$RESULTS_DIR" + success "已删除日志和结果目录。" +else + warn "已保留日志和结果目录。" +fi + +# ============================================================================== +# 3. 删除 /opt/xingrin/tools 和 /opt/xingrin/wordlists +# ============================================================================== +TOOLS_DIR="/opt/xingrin/tools" +WORDLISTS_DIR="/opt/xingrin/wordlists" + +step "[3/6] 是否删除工具目录和字典目录 ($TOOLS_DIR, $WORDLISTS_DIR)?(Y/n)" +read -r ans_tools +ans_tools=${ans_tools:-Y} + +if [[ $ans_tools =~ ^[Yy]$ ]]; then + info "正在删除工具和字典目录..." + rm -rf "$TOOLS_DIR" "$WORDLISTS_DIR" + success "已删除 /opt/xingrin/tools 和 /opt/xingrin/wordlists。" +else + warn "已保留 /opt/xingrin/tools 和 /opt/xingrin/wordlists。" +fi + +# ============================================================================== +# 4. 删除 docker/.env 配置文件 +# ============================================================================== +ENV_FILE="$DOCKER_DIR/.env" + +step "[4/6] 是否删除配置文件 ($ENV_FILE)?(Y/n)" +echo -e " ${YELLOW}注意:删除后下次安装将生成新的随机密码。${RESET}" +read -r ans_env +ans_env=${ans_env:-Y} + +if [[ $ans_env =~ ^[Yy]$ ]]; then + info "正在删除配置文件..." + rm -f "$ENV_FILE" + success "已删除 $ENV_FILE。" +else + warn "已保留 $ENV_FILE。" +fi + +# ============================================================================== +# 5. 删除本地 Postgres 容器及数据卷(如果使用本地 DB) +# ============================================================================== +step "[5/6] 若使用本地 PostgreSQL 容器:是否删除数据库容器和 volume?(Y/n)" +read -r ans_db +ans_db=${ans_db:-Y} + +if [[ $ans_db =~ ^[Yy]$ ]]; then + info "尝试删除与 XingRin 相关的 Postgres 容器和数据卷..." + # docker-compose 项目名为 docker,常见资源名如下(忽略不存在的情况): + # - 容器: docker-postgres-1 + # - 数据卷: docker_postgres_data(对应 compose 中的 postgres_data 卷) + docker rm -f docker-postgres-1 2>/dev/null || true + docker volume rm docker_postgres_data 2>/dev/null || true + success "本地 Postgres 容器及数据卷已尝试删除(不存在会自动忽略)。" +else + warn "已保留本地 Postgres 容器和 volume。" +fi + +step "[6/6] 是否删除与 XingRin 相关的 Docker 镜像?(y/N)" +read -r ans_images +ans_images=${ans_images:-N} + +if [[ $ans_images =~ ^[Yy]$ ]]; then + info "正在删除 Docker 镜像..." + # 直接删除相关镜像,避免 compose 警告 + docker rmi yyhuni/xingrin-server:latest 2>/dev/null || true + docker rmi yyhuni/xingrin-frontend:latest 2>/dev/null || true + docker rmi yyhuni/xingrin-nginx:latest 2>/dev/null || true + docker rmi yyhuni/xingrin-agent:latest 2>/dev/null || true + docker rmi yyhuni/xingrin-worker:latest 2>/dev/null || true + docker rmi redis:7-alpine 2>/dev/null || true + success "Docker 镜像已删除(如存在)。" +else + warn "已保留 Docker 镜像。" +fi + +success "卸载流程已完成。" diff --git a/update.sh b/update.sh new file mode 100755 index 00000000..ddeff2ca --- /dev/null +++ b/update.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# 更新服务(停止 → 拉取 → 合并配置 → 启动) +# +# 用法: +# ./update.sh 生产模式更新(拉取 Docker Hub 镜像) +# ./update.sh --dev 开发模式更新(本地构建镜像) +# ./update.sh --no-frontend 更新后只启动后端 +# ./update.sh --dev --no-frontend 开发环境更新后只启动后端 + +cd "$(dirname "$0")" + +# 解析参数判断模式 +DEV_MODE=false +for arg in "$@"; do + case $arg in + --dev) DEV_MODE=true ;; + esac +done + +# 颜色定义 +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# 合并 .env 新配置项(保留用户已有值) +merge_env_config() { + local example_file="docker/.env.example" + local env_file="docker/.env" + + if [ ! -f "$example_file" ] || [ ! -f "$env_file" ]; then + return + fi + + local new_keys=0 + + while IFS= read -r line || [ -n "$line" ]; do + [[ -z "$line" || "$line" =~ ^# ]] && continue + local key="${line%%=*}" + [[ -z "$key" || "$key" == "$line" ]] && continue + + if ! grep -q "^${key}=" "$env_file"; then + echo "$line" >> "$env_file" + echo -e " ${GREEN}+${NC} 新增: $key" + ((new_keys++)) + fi + done < "$example_file" + + if [ $new_keys -gt 0 ]; then + echo -e " ${GREEN}OK${NC} 已添加 $new_keys 个新配置项" + else + echo -e " ${GREEN}OK${NC} 配置已是最新" + fi +} + +echo "" +echo -e "${BOLD}${BLUE}╔════════════════════════════════════════╗${NC}" +if [ "$DEV_MODE" = true ]; then + echo -e "${BOLD}${BLUE}║ 开发环境更新(本地构建) ║${NC}" +else + echo -e "${BOLD}${BLUE}║ 生产环境更新(Docker Hub) ║${NC}" +fi +echo -e "${BOLD}${BLUE}╚════════════════════════════════════════╝${NC}" +echo "" + +echo -e "${CYAN}[1/4]${NC} 停止服务..." +./stop.sh 2>&1 | sed 's/^/ /' + +echo "" +echo -e "${CYAN}[2/4]${NC} 拉取代码..." +git pull --rebase 2>&1 | sed 's/^/ /' + +echo "" +echo -e "${CYAN}[3/4]${NC} 检查配置更新..." +merge_env_config + +echo "" +echo -e "${CYAN}[4/4]${NC} 启动服务..." +./start.sh "$@" + +echo "" +echo -e "${BOLD}${GREEN}════════════════════════════════════════${NC}" +echo -e "${BOLD}${GREEN} 更新完成!${NC}" +echo -e "${BOLD}${GREEN}════════════════════════════════════════${NC}" +echo ""