theHarvester 新增发现模块(Discovery Module)完整接入指南:从 Provider 契约确认到源码注册与测试
【免费下载链接】theHarvesterE-mails, subdomains and names Harvester - OSINT项目地址: https://gitcode.com/GitHub_Trending/th/theHarvester
本指南以 theHarvester 官方文档 How to add a new module 为核心骨架,系统讲解如何为这个开源 OSINT 工具新增一个发现源(Provider)。你将掌握一套可复制的六步流程:确认 Provider 契约、实现异步适配器、注册 Source 目录与工厂、配置 API Key、编写离线测试、更新操作员文档,并深入理解SourceExecutionReport状态机、AsyncFetcher会话生命周期等底层机制,最终让新模块能够被theHarvester -s <source>正常调用并产出规范化证据。
0. 前置准备:分支、环境与贡献规范
开始实现前,请先阅读仓库根目录的 CONTRIBUTING.md,其中包含分支策略、开发环境搭建、测试运行方式与 Pull Request 要求。在动手写 Provider 之前,还应在项目的 Issues 与已有 Pull Request 中搜索确认:是否已经有人实现过同一 Provider,或仓库是否已将其标记为失效源(例如 tests/lib/test_source_catalog.py 中就有test_dead_threatcrowd_source_is_not_selectable、test_invalid_bitbucket_domain_source_is_not_selectable、test_removed_inert_sources_are_not_supported等契约测试,专门防止失效源重新进入可选列表)。
1. 第一步:确认 Provider 契约(Provider Contract)
在写任何代码之前,先阅读 Provider 官方的 API 文档和条款,明确以下四个关键问题:
- 认证字段:需要什么凭据?是 API Key、Token、账号密码组合,还是完全匿名?认证方式直接决定适配器如何构造请求头以及是否需要接入第 4 步的凭据体系。
- 请求约束:请求速率限制(rate limit)、分页方式(offset / cursor / page number)、重试策略、终止行为分别是什么?例如百度搜索以
pn参数按 10 条一页翻页(见 theHarvester/discovery/baidusearch.py 的 URL 生成逻辑)。 - 稳定响应字段:哪些字段可以稳定地映射为 hosts(主机名)、emails(邮箱)、IPs、ASNs、URLs 或 people?这是决定
ResultRoute声明与 getter 实现的依据。 - 最小请求序列:查询单个域名最少需要哪几步请求(首页请求、分页、可能的轮询)?
同时注意一个规范红线:不要把 Provider 的价格或配额写入仓库文档,应当链接到 Provider 自有文档。仓库文档只描述技术集成方式。
2. 第二步:实现适配器(Adapter)
适配器统一放在theHarvester/discovery/目录下(目录下已存在 baidusearch.py、crtsh.py、virustotal.py 等 50 余个现成实现)。应当复用项目共享的 fetcher、配置、parser 与结果归一化能力,而不是另起炉灶。
2.1 适配器的标准形态
一个标准的适配器通常提供三部分:
- 初始化器(initializer):接收目标(word/domain)与结果上限 limit,初始化本地结果集合;
- 异步
process()方法:返回SourceExecutionReport | None,是整个 Provider 会话的入口; - 只实现实际支持的 getter:如
get_hostnames()、get_emails()、get_ips()、get_asns()、get_urls()、get_results()等。
以最简单的 theHarvester/discovery/subdomaincenter.py 为例,它展示了最精简的形态:__init__中保存self.word、初始化self.results = set();do_search()用AsyncFetcher.fetch_all()发起单次 GET 请求;get_hostnames()直接返回结果集合;process(proxy=False)记录代理标志后调用do_search()。
稍微复杂的 theHarvester/discovery/baidusearch.py 则展示了更完整的形态:process()内部先尝试 Playwright 无头浏览器(模拟真实浏览器、携带Core.get_browser_user_agent()),失败或缺少playwright库时回退到纯 HTTP 请求;get_emails()与get_hostnames()通过共享 parser theHarvester/parsers/myparser.py 从累积的 HTML 中解析邮箱与子域名。
需要强调的纪律:Provider 没有提供的字段绝不能伪造返回;所有结果在返回前必须做归一化(normalize)与去重(deduplicate)。归一化工作最终由 runner 侧统一执行,见下文第 2.2 节。
2.2process()返回值契约:理解SourceExecutionReport状态机
process()只能返回以下五种结果之一,这是适配器与运行器之间的核心契约:
| 返回值 | 含义 |
|---|---|
None | Provider 会话正常完成,包括合法返回零结果的情况。 |
SourceExecutionReport('completed', reason) | 源在自然结束前成功停止,例如已达请求的结果上限。 |
SourceExecutionReport('failed', reason) | Provider 或传输层故障导致源结束。 |
SourceExecutionReport('rate-limited', reason) | 终端级限流导致源结束。 |
SourceExecutionReport('partial', reason) | Provider 明确确认覆盖不完整。 |
在源码层面,theHarvester/lib/source_execution.py 定义了SourceReportStatus = Literal['completed', 'partial', 'failed', 'rate-limited']和冻结数据类SourceExecutionReport:status必须是上述四种之一,stop_reason必须是非空字符串,否则在__post_init__中直接抛出ValueError。
注意:适配器上不得定义可变的execution_status或stop_reason字段。运行器在构造适配器后立即通过_reject_removed_execution_fields()检查这两个字段是否存在,一旦发现直接抛ValueError(见 theHarvester/lib/source_runner.py)。
运行器run_source()(theHarvester/lib/source_runner.py)对返回值的最终化逻辑如下:
process()返回None→ 状态为completed;若结果数为 0 且无 stop_reason,则自动记录为completed+no-results;- 返回
SourceExecutionReport→ 采用其状态与 stop_reason; - 若结果数 > 0 但状态不是
completed,则自动提升(promote)为partial,保留已归一化的证据; - 传输层代理失败(
AsyncFetcher.proxy_transport_failed()为真)→ 强制failed+transport-error; MissingKeyError→skipped+missing-credentials;asyncio.CancelledError→ 保留已收集结果,有结果记partial否则记failed,并通过commit_cancelled回调提交后重新抛出;- 其他异常 → 有结果记
partial否则记failed,异常类型写入 stop_reason。
因此适配器作者只需对 Provider 会话本身负责,最终状态由运行器统一裁决,这正是"不要定义可变执行字段"的根本原因。
2.3 拥有 Provider 会话(Own the Provider Conversation)
"Provider 会话"指一次源执行相关的完整请求序列:首次请求、分页、重试或轮询、最终响应处理。规范要求这个序列有唯一明确的属主,并遵守以下纪律:
- 复用同一个
AsyncFetcher.open_session():连接池、请求头、cookie jar 与选定的代理身份在会话期间保持稳定。运行器会为一次执行固定一个被选中的代理(通过AsyncFetcher.proxy_scope()上下文管理器传入),适配器要用传入的 proxy 标志打开会话,并把借来的 session 以session=参数传给共享 fetch 方法,只允许最外层属主关闭它。 - cookie jar 策略:当后续请求可能依赖前面响应建立的 cookie 时(典型如搜索引擎的会话 cookie),保留默认 cookie jar;而像接管检测(takeover)这类刻意相互独立的探测,则使用
aiohttp.DummyCookieJar(),避免一个目标影响另一个目标。 - 作用域隔离:一个 session 只服务于一个 Provider 和一个已授权目标,绝不在不同源执行或无关目标之间共享 cookie、认证状态或代理身份。
- 取消安全:关闭每个属主 session、response、task、connector 时必须保留取消语义——既要覆盖成功完成路径,也要覆盖中断路径,且都要有对应测试。
open_session在core.py中本身就通过drain_tasks_after_cancellation处理了"关闭 session 期间再被取消"的竞态。 - 生命周期即生命周期阶段:把 session 的构建与销毁视为适配器生命周期阶段。除非 Provider 契约明确改变,否则保留现有 TLS 与超时策略;普通生命周期失败返回
SourceExecutionReport,而真正的取消(CancelledError)要让它原样传播。 - 扩展共享 fetcher 接口需谨慎:在扩展
AsyncFetcher这类共享接口前,审计所有位置参数调用点以及每个"属主 vs 借用"分支,新加的可选参数绝不能改变既有调用的语义。open_session的完整签名(见 theHarvester/lib/core.py)为headers、proxy、request_timeout、cookie_jar、verify,新增适配器应尽量只通过这些既有参数表达需求。
完成检查(completion check):文档要求一个特定的离线测试——后一页的结果依赖于前一页建立的状态(证明浏览器/会话状态在翻页间被保留),外加一条清理断言证明 Provider 会话最终被关闭。这个测试模式在 tests/discovery/test_baidusearch.py 中体现得淋漓尽致:PageResponse.requires_prior_navigation标志会让 FakePage 在"后页丢失浏览器状态"时直接断言失败(见 tests/discovery/test_baidusearch.py),而test_cancellation_survives_cleanup_failures则验证即使 page/context/browser/manager 四层 close 全部抛异常,CancelledError依然原样传播且四个资源都被关闭(tests/discovery/test_baidusearch.py)。
3. 第三步:注册 Source(目录 + 工厂双登记)
适配器写完后需要注册才能被 CLI 识别,注册涉及两个文件、两处条目:
theHarvester/lib/source_catalog.py:新增一条
_spec(...)目录条目。目录(catalog)负责提供 CLI 帮助文本、源选择与活动分类(activity classification)。SourceSpec冻结数据类包含name、routes、activity、retains_unresolved_hostnames四个字段;ResultRoute枚举定义了SUBDOMAINS / EMAILS / IPS / ASNS / PEOPLE / URLS / BREACHES七种结果路由(theHarvester/lib/source_catalog.py);ActivityClass枚举定义了PASSIVE('P0') / DNS('P1') / DIRECT('P2')三个活动等级(theHarvester/lib/source_catalog.py)。例如:_spec('mynewsource', ResultRoute.SUBDOMAINS, ResultRoute.EMAILS, activity=ActivityClass.PASSIVE),目录还支撑
resolve_sources()的能力选择器:all会展开为全部 PASSIVE 源,subdomains/emails等能力关键字会按capabilities展开(theHarvester/lib/source_catalog.py)。theHarvester/lib/source_runner.py:在
SOURCE_FACTORIES字典中新增一条工厂条目,用SourceRequest构造适配器实例,例如:'mynewsource': lambda request: mynewsource.SearchMyNewSource(request.target, request.limit),create_source()(theHarvester/lib/source_runner.py)通过目录中的规范名称查表构造适配器;_ROUTE_GETTERS(theHarvester/lib/source_runner.py)把ResultRoute映射到具体的 getter 名(如SUBDOMAINS → get_hostnames),运行器随后收集所有声明的结果路由并随完成的 run 一起持久化。对于特殊源(如 builtwith 的框架/语言/服务器/CMS/分析栈 getter、hudsonrock 的 infostealer、shodan 的 host 证据),运行器还有专门的_collect_observations分支(theHarvester/lib/source_runner.py)。
最后一条纪律:保持公开的源标识符稳定,且在目录、工厂、CLI、README 矩阵等所有位置使用完全相同的拼写(大小写敏感的源名如securityTrails会被get_source_spec()做 casefold 归一化处理,但规范名本身要一致)。
4. 第四步:按需添加凭据
如果源接受 API Key,按以下四步接入项目统一的凭据体系:
- 在 theHarvester/data/api-keys.yaml 中添加空的凭据字段。文件顶层是
apikeys:映射,每个 Provider 一个二级映射,字段按需定义。多字段示例:censys需要token+organization_id,fofa需要key+email,tomba需要key+secret;单字段示例:shodan、virustotal都只需要key。 - 在
Core._API_KEY_FIELDS注册这些字段(theHarvester/lib/core.py)。该ClassVar字典把 Provider 名映射到字段元组,Core.api_key_fields()与Core._api_key_value()都依赖它。 - 添加匹配的
Core访问器供适配器使用。例如Core.shodan_key()内部调用_api_key_value('shodan')返回单个值;Core.fofa_key()返回(key, email)二元组;Core.censys_key()则是从api_keys()中安全取token与organization_id(theHarvester/lib/core.py)。 - 缺少必需凭据时要清晰失败。运行器会捕获
MissingKeyError并把该源标记为skipped/missing-credentials,不会中断整个采集(见 theHarvester/lib/source_runner.py);如果 key 是可选的,则保留文档中说明的无 key 行为(如 baidu 不要求凭据即可匿名搜索)。
安全红线:绝不在日志中打印凭据,绝不在测试、示例、提交、Issue 或 Pull Request 中包含真实 key。仓库的api-keys.yaml只存放空字段占位,用户运行时会自动复制到主目录配置路径(Core._read_config会按配置目录查找,找不到则从数据目录创建默认文件)。
5. 第五步:添加聚焦的测试覆盖
文档推荐以 tests/discovery/test_baidusearch.py 为模板——它是一个小而完整的示例,可用pytest的monkeypatch替换网络获取层(包括伪造 Playwright API、伪造AsyncFetcher.open_session/fetch、伪造asyncio.sleep),并断言归一化后的结果。pytestmark = pytest.mark.provider_contract('baidu')把它挂入 Provider 契约测试组。
需要覆盖的典型用例(对应 baidu 测试中的具体测试):
- 成功解析:如
test_process_queries_site_first_and_reuses_one_browser,断言三页 URL 顺序、domcontentloaded等待策略、60 秒超时、headless=True启动参数、代理注入、每页间 1.0 秒延迟,以及邮箱/主机名解析结果; - 缺少必需凭据:断言抛出/记录
MissingKeyError,最终状态为skipped; - 非成功响应 / 超时 / 空响应 / 畸形响应:如
test_http_429_is_reported(rate-limited+http-429)、test_empty_response_is_reported(failed+no-response)、test_http_fallback_reports_malformed_response(failed+invalid-response)。注意项目在 theHarvester/discovery/provider_response.py 提供了共享的provider_http_error()分类器:非FetcherResponse→transport-error,401/403 →access-denied,429 →rate-limited+http-429,其余非 2xx →http-{status},可直接复用; - 分页与终止:如
test_unlimited_stops_when_provider_repeats_a_page,验证无 limit 时遇到重复页返回partial+repeated-page并停止; - 执行报告:不完整工作返回
SourceExecutionReport,正常完成返回None; - 归一化与去重:如
test_later_captcha_preserves_partial_results验证部分结果被保留。
硬性约束:测试不得依赖外部网络访问,不得使用真实 Provider 凭据。所有网络行为都必须被 mock 或 stub 替换。
6. 第六步:更新操作员文档
- 在仓库根目录 README.md 的 Source 矩阵中新增该源,注明其结果路由(routes)、活动类别(activity class)与凭据要求。README 矩阵契约测试会逐项对照目录条目校验这些值是否一致(相关契约测试见 tests/lib/test_source_catalog.py 与 tests/test_readme.py)。
- 在 Pull Request 描述中链接 Provider 官方 API 文档,并解释任何对共享传输行为的有意例外(例如某源必须禁用重定向、必须使用特定 UA、必须用无头浏览器而非普通 HTTP 等)。
7. 自检清单与工作流全景
把以上六步串起来,一次完整的"新增发现模块"工作流是:
- 阅读 CONTRIBUTING.md,检查 Issues/PR 避免重复劳动;
- 确认 Provider 契约(认证、限流、分页、稳定字段、最小请求序列);
- 在 theHarvester/discovery/ 实现适配器(初始化器 +
process()+ 按需 getter),严格遵守SourceExecutionReport状态契约与会话生命周期纪律; - 在 theHarvester/lib/source_catalog.py 加目录条目、在 theHarvester/lib/source_runner.py 加工厂条目,保证标识符拼写一致;
- 按需在 theHarvester/data/api-keys.yaml 与
Core._API_KEY_FIELDS注册凭据字段并添加访问器; - 参考 tests/discovery/test_baidusearch.py 编写全离线的聚焦测试;
- 更新 README.md 矩阵,在 PR 中链接 Provider 文档并说明传输行为例外。
运行新源时,用户通过 CLI 指定源名(如python theHarvester/theHarvester.py -d example.com -s mynewsource)或使用all/ 能力选择器,运行器会按目录条目校验、构造适配器、执行会话、收集并归一化证据、以SourceExecution记录执行状态(completed/partial/failed/rate-limited/skipped)持久化到 run 中——这正是"目录驱动、运行器拥有最终状态"这一架构设计的完整闭环。
【免费下载链接】theHarvesterE-mails, subdomains and names Harvester - OSINT项目地址: https://gitcode.com/GitHub_Trending/th/theHarvester
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考