edygert/bhindex

GitHub: edygert/bhindex

一个本地优先的 Black Hat 会议议程元数据采集与全文索引工具,通过 Wayback Machine 获取数据并存入 SQLite 供离线搜索。

Stars: 0 | Forks: 0

# bhindex [![许可证: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/) 本地优先的 **Black Hat** 大会议程元数据采集与索引工具。它将活动元数据抓取到本地 SQLite 数据库中,并提供全文搜索功能——**仅限元数据;它绝不会下载演示文稿或其他二进制文件。** 这是 **第 1 阶段(后端 + CLI)**。计划未来通过 TUI 和 FastAPI 层来封装相同的服务层(参见*架构**)。范围有意限制在 **2016 年及以后**(参见*覆盖范围*)。 ## 为什么使用 Wayback Machine `blackhat.com` 前端由 Cloudflare 托管挑战保护,会对数据中心 IP 上的非浏览器 HTTP 客户端返回 `403`。与其对抗它,bhindex 选择从 **Wayback Machine** 采集数据,它不在 Cloudflare 后面,并且可以在任何网络中工作(包括 CI)。由于过去的 Black Hat 活动是不可变的,存档中已经包含了基本上所有的历史元数据。 ## 采集原理 - **近期活动(2016 年及以后)**发布了一个结构化的 JSON feed,地址为 `…//briefings/schedule/sessions.json`(议程页面本身是一个由此 feed 渲染的 Handlebars 模板)。bhindex 解析该 feed → 活动、议程、演讲者、摘要以及材料链接。 - **2016 和 2017 年的材料** *不* 包含在 feed 中。这些文件位于 `blackhat.com/docs//…/--.pdf`。bhindex 通过 Wayback CDX API 枚举它们,并通过将文件名与演讲者姓氏匹配(主要方式)以及标题 token 重叠(备选方式),将每个文件附加到其对应的议程中。非议程文件(调查、信件)将被跳过。 - **2018 年及以后的材料**直接来自 feed(`bh_files`,指向 `i.blackhat.com`)。 - **手动 / 离线**:您可以自己保存 `sessions.json`(或任何页面),并对其使用 `ingest-file`。 采集过程仅获取 HTML/JSON,并将材料 **URL** 记录为元数据。从采集过程不存在下载二进制文件的代码路径。 ## 安装 需要 Python 3.12+ 和 [uv](https://docs.astral.sh/uv/)。 ``` uv sync # install runtime + dev deps uv run bhindex --help ``` ## 用法 ``` uv run bhindex init-db # create the database + FTS index uv run bhindex harvest us-24 eu-23 asia-24 # harvest one or more editions (metadata only) uv run bhindex harvest us-17 # 2017: materials backfilled from /docs automatically # harvest 显示实时进度监视器以及每个事件的验证报告(异常 / 缺失数据)。 uv run bhindex stats # row counts + per-source/per-event coverage uv run bhindex events # list harvested events uv run bhindex search "kernel exploit" # full-text search; prints a #id per result uv run bhindex show 1234 # full detail for one session (speakers, abstract, links) # 离线 / 手动保存的页面: uv run bhindex ingest-file ./sessions.json --url https://www.blackhat.com/us-24/briefings/schedule/sessions.json ``` 版本的写法与它们在 URL 中显示的一样:`us-24`、`eu-23`、`asia-24`。 数据位于 `~/.local/share/bhindex/` 目录下(通过 `--data-dir` 或 `BHINDEX_DATA_DIR` 覆盖): `bhindex.sqlite3`、`snapshots/`(用于调试和解析器测试的原始 HTML/JSON 捕获)和 `bhindex.log`。 **礼貌策略。** Wayback Machine 的速率限制非常严格,因此 bhindex 以 **5 秒的默认延迟 + 抖动**、一个在多次多版本运行中持续控制节奏的单一客户端,以及遵守 `429`/`5xx` 错误时 `Retry-After` 头部的指数退避策略来控制请求节奏(节流情况会在进度监视器中实时显示)。通过 `BHINDEX_REQUEST_DELAY`、`BHINDEX_MAX_RETRIES`、`BHINDEX_USER_AGENT`(或 `~/.config/bhindex/config.toml`)进行调整。在默认延迟下,完整的 2016–2025 年扫描(30 个版本)大约需要 10–12 分钟 —— 慢一点更友好。 ## 架构 分层后端;依赖规则向内指向。CLI(以及未来的 TUI/FastAPI)**仅**调用服务层 —— 绝不直接调用 repositories、parsers 或 fetchers。 ``` cli ─► services ─► parsers (recent feed parser, /docs materials matcher) │ └─► adapters (HttpClient, WaybackFetcher, FileFetcher) └─► storage (sqlite3 + FTS5, repositories, snapshots) everything ─► core / dto (config, errors, urls, models; pydantic contracts) ``` - `ServiceContainer` 是 DI 的根;`HarvestService`、`SearchService`、`StatsService` 是公开的 API。 - 服务是**同步的**并且兼容 FastAPI:未来的 Web 层将构造相同的容器并调用相同的方法;TUI 从后台工作线程驱动它们。无需更改任何核心逻辑。 - Parsers 是纯函数(HTML/JSON + 基础 URL → DTOs);所有的网络 I/O 都位于 adapters 中。 ## 覆盖范围(实际有效的内容) | 年份 | 议程 + 演讲者 + 摘要 | 材料 | |---|---|---| | 2018–2025 (us/eu/asia) | ✅ feed | ✅ feed (`i.blackhat.com`) | | 2016–2017 | ✅ feed | ✅ 从 `blackhat.com/docs/` 回填 | | ≤2015 | ❌ 无 JSON feed | ❌ | 2016 年之前的旧版 `bh-media-archives` HTML 有意**排除在范围之外** —— 它的标记在不同年份之间差异太大,无法可靠解析,并且目前不需要。 ## 开发 ``` uv run pytest # unit + integration tests (fixtures are real, trimmed captures) uv run ruff check . # lint ``` 测试完全离线运行,针对 `tests/fixtures/` 中的测试夹具数据。集成测试套件固定了第 1 阶段的不变量,即采集仅写入元数据 + HTML 快照,绝不会写入二进制文件。 ## 许可证 [MIT](LICENSE) © 2026 Evan H. Dygert. bhindex 仅索引**元数据**(标题、摘要、演讲者和链接)。它不分发 Black Hat 演示文件;所有材料链接都指向其原始来源。在采集时,请尊重源站点的使用条款和 Internet Archive 的访问政策。</div><div><strong>标签:</strong>Python, SQLite, 元数据管理, 全文搜索, 安全会议资料, 无后门, 逆向工具</div></article></div> <!-- 人机验证 --> <script> (function () { var base = (document.querySelector('base') && document.querySelector('base').getAttribute('href')) || ''; var path = base.replace(/\/?$/, '') + '/cap-wasm/cap_wasm.min.js'; window.CAP_CUSTOM_WASM_URL = new URL(path, window.location.href).href; })(); </script> </body> </html>