BenJonesVA/wigle_watcher

GitHub: BenJonesVA/wigle_watcher

一个本地单用户 WiFi 地理定位工具,通过 WiGLE 众包数据库将接入点 BSSID 解析为估算物理坐标并在交互式地图上可视化。

Stars: 1 | Forks: 0

# wigle-watcher [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml) [![Tests: pytest](https://img.shields.io/badge/tests-pytest-0A9EDC.svg)](tests) 导入观察到的 WiFi 接入点信标数据(BSSID/SSID/信道/加密), 通过 [WiGLE.net](https://wigle.net) 众包的 wardriving 数据库解析每个网络的物理位置, 并在交互式地图上查看结果。 ![带有侧边栏网络列表的地图概览](https://static.pigsec.cn/wp-content/uploads/repos/cas/0b/0be20d34c4f6e02b2122cb824ff46bde72429b0341fe53411c9698af18687bb6.png) 这是一个基于你个人的 WiGLE API token 构建的本地单用户工具。 WiGLE 的条款限制批量查询和重新分发其数据——缓存的查询结果会保留在本地 SQLite 数据库中,不会在任何地方重新发布。 此工具有两种模式,都建立在相同的 导入 → 查询 → 地图 pipeline 之上: - **AP mapping** —— 从 beacon 帧中解析你自己(或已授权)接入点的物理位置。 - **Target location profiling** —— 从设备在被动捕获期间探测的 SSID,推断特定设备可能的物理位置(例如家庭、工作场所)。此模式需要针对特定目标的书面且明确范围的授权——使用前请参阅下方的 [法律 / 负责任的使用](#legal--responsible-use) 和 [目标位置分析](#target-location-profiling-probe-requests)。 ## 法律 / 负责任的使用 - **仅限授权使用。** 仅捕获并解析你拥有、运营或获得明确许可以进行评估的网络(例如你自己的家庭/办公室,或范围明确的安全测试任务)。尽管每个底层数据点都是公开的,但大规模将 BSSID 解析为物理位置具有类似监控的性质——未经同意,请勿使用此工具定位、跟踪或监视人员、组织或网络。 - **目标位置分析属于一个不同且风险更高的类别。** 捕获设备的 probe requests 并推断其所有者可能的居住或工作地点,是对*具名个人*的分析,而不仅仅是解析 AP 的静态位置——请务必按此对待。仅在签署并明确范围的测试任务(例如带有书面交战规则的物理安全/要员保护评估)中针对被点名目标运行此模式。正是出于这个原因,每次 probe session 都需要一个 `authorization_reference`——它是一个工作流减速带,而不是技术控制,因此它不能代替实际获得的授权。绝不要将此工具用于旁观者、测试范围之外的同事,或任何未在该授权中点名的人。 - **对其实际能发现的内容设定合理预期。** 当前的 iOS 和大多数最新的 Android 版本会随机化 probe requests 中使用的 MAC 地址,并默认使用广播(空白 SSID)探测,而不是公布已保存的网络名称——这会使得基于 SSID 的简单分析方法对大多数现代且完全更新的手机失效。仍然存在的主要信息泄露是目标的**隐藏**(非广播)已保存网络,因为操作系统仍必须通过名称来宣告它以便找到它。对于没有隐藏家庭 SSID 且使用当代设备的对象,预期此功能几乎或根本不会返回任何结果——这是手机按预期工作,而不是此工具的 bug。 - **位置是估算值,不是精确定位。** 坐标来自 WiGLE 的众包三角测量,而不是 GPS 级别的测量——对于仅被一名贡献者看到的 AP,它可能仅反映该贡献者的手机当时碰巧所在的位置。请勿将地图标记视为绝对真相。 - **尊重 WiGLE 的服务条款。** 批量查询和重新分发 WiGLE 的数据违反其 ToS(请参阅下方的配额部分)。此工具专为针对你自己的 API token 的本地单用户使用而构建——缓存结果保留在你的本地数据库中,不会在任何地方重新发布。 - **无担保。** 这是一个按“原样”提供的个人/研究工具,不保证准确性、可用性或适用于任何特定目的。 ## 本工具不会做什么 (v1) 实时数据包捕获功能并未内置在 Web 应用本身中,而且根据设计也永远不会包含——将 WiFi 适配器置于 monitor 模式并运行 `airodump-ng` 需要 root 权限,而此应用没有任何身份验证(见上文),因此无身份验证的本地 Web 服务器不是持有该权限的合适位置。 `capture.sh`(见下文 [实时捕获](#live-capture-optional))将捕获作为独立的、手动调用的、以 root 权限运行的脚本运行;Web 应用仅从本地磁盘读取它生成的 CSV 文件——它从不启动、停止或配置捕获本身。此工具本质上仍然是一个 *导入 → 查询 → 地图* pipeline;任何其他摄取源(Kismet 等)都可以在以后添加,而无需更改 DB/API/frontend,因为每个摄取源只需生成相同的标准化记录结构。 ## 安装说明 ### 快速设置(基于 Kali/Debian 的 Linux) ``` ./setup.sh # creates .venv, installs dependencies, copies .env.example -> .env ./run.sh # starts uvicorn on 127.0.0.1:8000 ``` `run.sh` 默认仅绑定到 localhost——此应用没有任何身份验证,因此除非你处于信任的网络中,否则不要设置 `HOST=0.0.0.0`。额外的参数(例如用于开发的 `./run.sh --reload`)将直接传递给 uvicorn。重新运行 `setup.sh` 是安全的;它不会破坏现有的 `.venv` 或 `.env`。 ### 手动设置(任何操作系统) ``` python -m venv .venv .venv/Scripts/activate # Windows; use `source .venv/bin/activate` on Linux/macOS pip install -e ".[dev]" cp .env.example .env ``` 编辑 `.env`: - 为了在不消耗 WiGLE 配额的情况下进行试用,请保留 `WIGLE_MOCK=true`(默认值)。这将使用确定的预设坐标,以便可以离线测试整个 pipeline。 - 要使用真实的 API,请设置 `WIGLE_MOCK=false` 并从你的 [WiGLE 账户页面](https://wigle.net/account) 填写 `WIGLE_API_NAME`/`WIGLE_API_TOKEN`——这是“API Token Name”和“API Token”字段,**不是**你的登录密码。 ### 关于 WiGLE 的查询配额 WiGLE 没有为免费账户发布固定的每日查询数量——它是一个基于账户信誉/行为的动态标准(新账户可能受到非常严格的限制,例如每天个位数;随着你使用网站并上传真实数据,限制会逐渐提高)。在运行大批量任务之前,请在你的 WiGLE 账户页面上检查你当前的实际限制,并对 `POST /api/lookup` 的 `limit` 参数保持谨慎。配额在美国/太平洋时间 00:00 重置——此工具报告的 `calls_made_today` 数据仅是通过此工具进行的调用的本地时钟近似计数;它无法看到共享同一 token 的其他工具,而 WiGLE 自身的速率限制响应始终是权威信号,绝不是此计数器。 ## 运行 ``` ./run.sh # Kali/Linux quick setup path # or, with a manually-activated venv: uvicorn app.main:app --reload ``` 打开 `http://localhost:8000`。 ## 实时捕获(可选) 捕获是作为独立的脚本运行的,绝不能从浏览器运行——请参阅 [本工具不会做什么 (v1)](#what-this-does-not-do-v1) 了解为什么存在此边界。这仅限 Linux(Kali / Raspberry Pi OS)。 ``` sudo ./capture.sh wlan0 # put wlan0 into monitor mode, run airodump-ng sudo ./capture.sh wlan0 --kill-conflicting # also kill NetworkManager/wpa_supplicant first ``` - `--kill-conflicting` 运行 `airmon-ng check kill`,这会在**全系统范围内**停止 NetworkManager/wpa_supplicant,而不仅仅是在你指定的接口上——这可能会断开这台机器上的任何其他 WiFi 连接(包括你现在正通过不同适配器连接的连接)。正是出于这个原因,它是可选的;脚本在执行此操作之前会打印警告并暂停。 - CSV 文件将写入 `CAPTURE_DIR`(`.env`,默认为 `./data/captures`)——Ctrl-C 会停止捕获并将接口恢复到 managed 模式。 - AP mapping 和 target-profiling 的摄取路径**都**读取*相同*的文件(一个只解析 AP 表,另一个只解析 station 表),因此一次 `capture.sh` 运行即可同时为两者提供数据。 在 Import 面板中,或在选定 session 的 Target Location Profiling 面板中切换 **“Watch capture folder”**,即可在 `capture.sh` 运行时每约 5 秒自动从 `CAPTURE_DIR` 导入——无需手动重新上传。这是一个纯粹的重新扫描和 upsert(`POST /api/import/scan` / `POST /api/probe-sessions/{id}/scan`)——可以无限期安全地保持开启,与手动上传路径所依赖的相同的幂等导入保证,使得重新导入未更改或仍在增长的文件成为一个 no-op/低成本更新。 浏览器从不自行启动、停止或配置捕获——该切换开关只会重新检查 `capture.sh` 正在写入的磁盘上的文件夹。 **在开始新的 target-profiling 任务之前**,请先清除或归档 `CAPTURE_DIR`: ``` mv data/captures data/captures-archive-$(date +%F) mkdir -p data/captures ``` 监视文件夹的扫描会将当前位于 `CAPTURE_DIR` 中的*每个* CSV 合并到选定的任何 probe session 中——先前目标留下的陈旧文件没有过期时间,因此将它们留在原处有将以前任务的数据合并到新任务中的风险。 ## 使用说明 0. **(可选)选择或创建一个 mapping session** —— “Import”面板的 session 选择器按站点/任务对观察到的网络进行分组(`POST /api/network-sessions`,可选的 `label`/`authorization_reference`/`notes`)。将其保留在“(无 session —— 所有网络)”上,即可将所有内容导入/浏览在一个平面列表中,完全就像此工具在有 session 功能之前的行为一样。选择或创建 session 会将导入、网络列表和导出范围限定为仅该 session——同一个物理 AP 稍后可以在不同的 session 下再次进行映射,而不会将两者混淆。与 `ProbeSession` 不同,这里的 `authorization_reference` 是可选的:AP mapping 解析你自己(或已授权)AP 的静态位置,这是风险低于下方分析模式的类别,因此 session 是用于组织导入,而不是用于门控。 1. **导入** —— 使用 `capture.sh`(上文)或直接使用 `airodump-ng -w capture --output-format csv` 进行捕获(或手动构建一个平面 CSV——格式请参见 `tests/fixtures/manual_sample.csv`),然后通过 Import 面板将其上传(`POST /api/import`,如果选择了 session,则使用 `POST /api/network-sessions/{id}/import`),或者使用“Watch capture folder”完全跳过手动上传步骤。 2. **运行查询** —— 针对未解析的 BSSID 在 WiGLE 中进行解析,缓存优先(除非你传递 `force_refresh`,否则将跳过已解析的 BSSID)。 3. **地图** —— 已解析的网络呈现为标记,按匹配置信度进行着色:绿色代表完全匹配的 BSSID 或单个明确的 SSID 匹配,橙色代表具有多个可能位置的不明确的 SSID 匹配。弹窗将坐标标记为“WiGLE estimate”——这是一种众包的三角测量,而不是精确的 GPS 定位,对于仅被看到一次的 AP,它可能仅反映了一名贡献者的手机所在的位置。 ![显示精确 BSSID 匹配的标记弹窗](https://static.pigsec.cn/wp-content/uploads/repos/cas/b7/b7fa6778734571af7108976edc970fd6cce729f49f3badb65a903a33e482219e.png) 不明确的 SSID 匹配项仍会显示在地图上(这样就不会有任何内容悄无声息地消失),只是以不同方式进行了标记: ![显示不明确 SSID 匹配的标记弹窗](https://static.pigsec.cn/wp-content/uploads/repos/cas/54/548df5c6b860c040e58c3bf959ad7b23780d1329dc458f413f94f7ccfd63a570.png) 侧边栏列表反映了相同的置信度标记,包括尚未解析到位置的网络: ![带有置信度标记的侧边栏网络列表](https://static.pigsec.cn/wp-content/uploads/repos/cas/b1/b1c6025ebb00cc86458bb9c39ecbe5eda9d04771ef27370d5c1101788a6505ae.png) 4. **删除 / 导出 / 重置** —— 侧边栏条目旁边的 ✕ 可删除该网络及其缓存的 WiGLE 查询(`DELETE /api/networks/{bssid}`),用于清除不应被捕获的网络。“Export CSV”(或 `GET /api/networks/export?format=csv|json`,两者都遵循与网络列表相同的 `q`/`encryption`/`resolved`/`session_id` 筛选器)会下载当前筛选后的视图,以便在报告中使用。 有两种级别的重置可用:删除 **session**(`DELETE /api/network-sessions/{id}`,session 选择器旁边的“Delete”按钮)仅删除使用该 session 标记的网络和缓存查询——其他 session,以及在没有选择 session 的情况下导入的任何内容,都不受影响。“Clear all”(`DELETE /api/networks`)是更粗暴的全局版本——它会清除所有观察到的网络、所有缓存查询,*以及所有 session*——将其用于完全重置,或者使用范围限定的 session 删除来仅重置一个站点/任务,而不会影响你的其余地图。 ## 目标位置分析(probe requests) 使用前请阅读上方的 [法律 / 负责任的使用](#legal--responsible-use)。侧边栏的“Target Location Profiling”面板通过 UI 涵盖了以下相同的步骤(session 选择器、导入表单、定位/删除按钮),将排序后的候选位置呈现为单独的可切换地图图层。 ![选择了 session 的目标位置分析面板](https://static.pigsec.cn/wp-content/uploads/repos/cas/16/160a6d794083f3ddc6c638001a43f0c11d082f30ab2d42e6040126baa8583dfd.png) 1. **捕获** —— 与 AP mapping 相同的 `airodump-ng -w capture --output-format csv` 捕获;此模式读取同一文件的 *station* 表(`POST /api/import` 忽略的那个),其中包含每个观察到的设备的 MAC 以及它探测的 SSID。 2. **创建 session** —— `POST /api/probe-sessions`,带有必需的 `authorization_reference`(自由文本——命名目标的任务/工单 ID)和可选的 `label`/`notes`。 3. **导入** —— `POST /api/probe-sessions/{id}/import`,带有 `format=airodump` 和相同的捕获文件。 4. **定位** —— `POST /api/probe-sessions/{id}/locate` 向 WiGLE 查询每个探测到的 SSID 的已知位置,然后在地理空间上对候选点进行聚类(默认半径为 150m,可使用 `radius_m` 覆盖)。一个聚类的 `score` 对落入其中的每个*不同* SSID 的稀有度权重求和——汇聚在同一街区的两个不常见的 SSID 的排名会高于单独出现的一个常见的默认路由器名称。如果捕获拾取了多个设备,请传递 `station_mac` 以限定为一个设备。 5. **删除** —— `DELETE /api/probe-sessions/{id}` 删除该 session 及在其下捕获的所有内容(station、探测到的 SSID)。它不会清除全局的每个 SSID 的 WiGLE 缓存,因为其他 session 可能共享相同的探测 SSID,不应该为此重新查询 WiGLE。 排序后的候选位置在“Target location estimates”地图图层上呈现为紫色标记,其大小由 score 决定——点击其中一个(在地图上或侧边栏列表中)会显示一个弹窗,详细说明其 score 和贡献的 SSID: ![带有弹窗和侧边栏列表的排序后候选位置聚类](https://static.pigsec.cn/wp-content/uploads/repos/cas/d5/d5b249f703c0d549b3408c8e2775a24912f990384136ae3cb891e002ae95d68b.png) ``` curl -X POST http://localhost:8000/api/probe-sessions \ -H "Content-Type: application/json" \ -d '{"authorization_reference": "engagement-1234", "label": "coffee shop"}' # -> {"id": 1, ...} curl -X POST http://localhost:8000/api/probe-sessions/1/import \ -F "format=airodump" \ -F "file=@capture.csv;type=text/csv" curl -X POST http://localhost:8000/api/probe-sessions/1/locate \ -H "Content-Type: application/json" -d '{}' # -> ranked clusters: {"lat", "lon", "score", "contributing_ssids", "point_count"} curl -X DELETE http://localhost:8000/api/probe-sessions/1 # -> 204, session and its stations/probed SSIDs are gone ``` ## 冒烟测试 ``` # 1. With WIGLE_MOCK=true (no real quota spent): uvicorn app.main:app --reload & # 2. Import the bundled fixture curl -X POST http://localhost:8000/api/import \ -F "format=airodump" \ -F "file=@tests/fixtures/airodump_sample.csv;type=text/csv" # 3. Resolve it curl -X POST http://localhost:8000/api/lookup \ -H "Content-Type: application/json" \ -d '{"all_unresolved": true, "limit": 10}' # response includes calls_made_today # 4. Open http://localhost:8000/ — confirm markers render with popups # 5. Re-run step 3 — outcomes should come back empty (all cache hits), # confirming already-resolved BSSIDs are not re-queried. ``` 使用 `WIGLE_MOCK=false` 和你的真实 token 对一个真实的(小规模)捕获重复一次,以在依赖它之前确认实时身份验证/响应解析是否正常工作。 ## 测试 ``` pytest ``` 所有测试都针对 mocked HTTP 和临时的内存 SQLite 数据库运行——没有真实的 WiGLE 调用,也不会写入你本地的 `data/` 目录。 ## 架构 ``` setup.sh, run.sh, capture.sh # bootstrap, serve, and (optional, root/Linux-only) live-capture scripts app/ main.py, config.py, db.py # app wiring, settings, SQLAlchemy engine utils/mac.py # normalize_bssid() — the single DB join-key source of truth models/, schemas/ # ORM models, Pydantic I/O shapes ingest/ # pluggable parsers -> ObservedNetworkRecord (AP mapping) airodump.py, manual_csv.py # implemented kismet.py, live_scapy.py # documented stubs for future ingest sources probe_base.py, probe_airodump.py # pluggable parsers -> ProbeStationRecord (target profiling) services/ ingest_service.py # upsert-by-BSSID import logic; scan_capture_dir() for the watch-folder # feature; delete_network(), clear_networks(), delete_network_session() wigle_client.py # RealWigleClient + MockWigleClient lookup_service.py # cache-first resolve(), BSSID-first/SSID-fallback geojson_service.py # DB -> GeoJSON for the map probe_ingest_service.py # upsert-by-(session, station_mac) import logic; scan_probe_capture_dir() probe_location_service.py # cache-first per-SSID WiGLE search, rarity-weighted clustering routers/ # /api/import, /api/import/scan, /api/networks (+ /export, DELETE # /{bssid}, DELETE bulk-clear), /api/network-sessions (+ .../import, # .../scan, DELETE), /api/lookup, /api/map/geojson, # /api/probe-sessions, .../import, .../scan, .../stations, .../locate, # DELETE /api/probe-sessions/{id} static/ # Leaflet + OpenStreetMap frontend (no API key/billing); # AP markers + a toggleable target-profiling cluster layer, # "Watch capture folder" toggles polling the /scan endpoints ``` 关于迁移的说明:此项目中没有 Alembic —— `db.py` 的 `init_db()` 会调用 `Base.metadata.create_all()`(它仅创建缺失的表),加上一个小型手动编写的步骤,用于将 `observed_networks.session_id` 列添加到在 `NetworkSession` 存在之前创建的数据库中。以后对现有表的任何 schema 更改都需要相同类型的显式、有针对性的迁移。 添加新的摄取源(例如 Kismet、实时的 Scapy 捕获)意味着编写一个发出 `ObservedNetworkRecord`(AP mapping)或 `ProbeStationRecord`(target profiling)对象的解析器——下游的任何内容都不需要更改。
标签:DFIR, ESC4, OSINT, Python, SQLite, WiFi, WiGLE, 主机安全, 地理位置, 安全规则引擎, 无后门