ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

theHarvester HarvestView 私有控制平面:用本地 SQLite 调度有限运行的架构解析(ADR-0006)

theHarvester HarvestView 私有控制平面:用本地 SQLite 调度有限运行的架构解析(ADR-0006) theHarvester HarvestView 私有控制平面用本地 SQLite 调度有限运行的架构解析ADR-0006【免费下载链接】theHarvesterE-mails, subdomains and names Harvester - OSINT项目地址: https://gitcode.com/GitHub_Trending/th/theHarvester导读本文围绕 theHarvester 仓库中的架构决策记录 ADR-0006Schedule finite runs through a private control plane深入剖析 HarvestView 如何用一个与证据分离的本地 SQLite 控制平面来存储授权目标清单、递归策略、重叠策略、调度声明与派发预留并通过既有持久队列与单一 worker 提交普通有限运行。读完本文你将掌握调度 API 的完整用法、日历递归的时区语义、skip/queue 重叠策略的行为差异以及控制平面与可移植证据为何必须分离的底层原理。一、决策背景为什么需要一个私有控制平面在引入调度能力之前theHarvester 已经通过 ADR-0003Keep run records separate with one isolated worker 建立了一条稳定的运行管线HTTP 应用拥有独立的持久化运行记录提交即获得稳定 ID 并入队一个本地 worker 一次认领一个运行在隔离的子进程中执行有限的枚举核心。生命周期为queued - running - completed|failed、queued - cancelled、running - cancelling - cancelled证据状态complete、partial、failed与编排生命周期状态分开报告。如果直接把这些调度数据塞进现有证据模型会混淆三类性质完全不同的数据操作者意图谁被授权执行、按什么频率执行编排状态运行是否被认领、是否在取消、worker 租约是否有效证据质量最终结果是否完整、部分还是失败。ADR-0006 的答案是把它们拆开控制平面control plane负责何时、对谁、按什么策略发起运行证据平面data plane只保存最终的可移植证据。这是一个从源码结构可以清楚看到的边界——控制平面文件theHarvester/lib/api/schedule_store.py与证据存储theHarvester/lib/api/run_store.py是两套独立的 SQLite 引擎、两张独立的数据表集合。二、决策核心控制平面存储什么证据导出不存储什么ADR-0006 原文明确了控制平面承载五类数据授权的目标清单authorized target inventories递归策略recurrence policy重叠策略overlap policy声明claims即调度器对某次到点 occurrence 的认领租约派发预留dispatch reservations即每个目标对应的稳定 run ID 预留。而可移植的完成运行证据导出不包含调度、声明或派发预留。这一点在 Rest-API.md 的导出章节 中再次确认The export contains canonical completed evidence and screenshot metadata. It excludes queue state, cancellation state, worker leases, and legacy observations. 也就是说把完成证据导出到别处做长期归档或交叉比对时不会把未来的自动化策略、worker 租约这类编排内幕一起带出去——证据文件是干净、稳定、可复现的。从实现看这一分离体现在两个层面物理层面configured_schedule_database()默认把调度库放在运行库的同名 sibling 路径例如runs.sqlite对应runs.schedules.sqlite可用环境变量THEHARVESTER_SCHEDULE_DB覆盖语义层面调度库的schedules表与schedule_dispatches表通过 SQLAlchemy 定义在独立的_SCHEDULE_METADATA上见 schedule_store.py与运行证据表互不引用仅run_id作为字符串关联。三、复用而非再造每次 occurrence 都是普通有限运行ADR-0006 最核心的设计选择是调度不引入第二条执行路径。每次到点的 occurrence调度器做两件事预留稳定的 run ID在派发前先把schedule_dispatches表中每个目标的run_id状态置为reserved此时运行记录尚未创建通过既有队列提交普通有限运行随后用该run_id调用RunStore.create()创建运行记录状态进入queued并唤醒 worker 认领执行。对应代码在ScheduleDispatcher._enqueue_targets()schedule_service.py中reserve_dispatches一次性为所有目标生成{target: str(uuid4())}预留随后对每个预留RunRequest.model_validate({**template, target: target})把调度模板中的target替换为当前目标再run_store.create(run_request, run_idrun_id)。全部创建完成后调用run_worker.wake_worker()唤醒执行 worker。这样带来的直接收益正是 ADR 原文所述生命周期、取消、归属、导出行为全部保持不变。调度的运行与手工提交的运行没有本质区别取消一条调度产生的运行、查看其来源归属、导出其证据走的都是同一条已验证的路径不需要为调度单独维护一套半吊子生命周期。3.1 派发状态机每个派发记录dispatch record有自己的状态镜像见 schedule_models.py 中的DispatchStatereserved - queued - running - cancelling - completed | failed | cancelled调度器的_reconcile_pending()会读取对应运行的真实状态并同步到派发记录上例如运行进入cancelling时派发记录也镜像为cancelling测试test_dispatch_mirrors_cancelling_run_state验证了这一点。这也意味着派发历史是可审计的每个目标、每次 occurrence 到底有没有被真正创建为运行在schedule_dispatches表中一目了然。四、日历递归语义保留本地墙钟时间DST 与月末都有明确答案ADR-0006 原文规定了两条容易出错的语义日历递归保留所选本地墙钟时间Calendar recurrence preserves the selected local wall-clock time当所选日期在当月不存在时月度调度使用该月最后一天monthly schedules use the final day when their selected day does not exist。这两条语义在ScheduleTiming.next_after()中有精确实现schedule_models.pyif self.frequency monthly: ... candidate_day date(year, month, min(local_start.day, monthrange(year, month)[1]))即用min(所选日, 当月最大天数)把 1 月 31 日的月度调度在 2 月折叠为 2 月 28/29 日。测试test_monthly_schedule_uses_the_final_day_of_short_months给出了具体期望值2027-01-31T09:00:00-05:00起算的月度调度2 月落在2027-02-28T14:00:0000:003 月落在2027-03-31T13:00:0000:00注意 3 月美东已进入夏令时UTC 偏移变化但本地 09:00 不变。4.1 各频率的时区行为一览频率计时基准关键行为daily本地墙钟跨 DST 保持本地时刻不变如美东 09:00 全年不变weekly本地墙钟可指定多个 ISO 星期周一1…周日7未指定时默认采用 start_at 的星期monthly本地墙钟所选日不存在时折叠到当月最后一天hourly经过的 UTC 小时数按 UTC 整点推进不受时区/夏令时影响once固定时刻仅执行一次interval必须为 1daily的 DST 处理包含一个细节_wall_candidate()会在本地时间不存在如春季 DST 跳变导致的 02:30 不存在时用resolve_imaginary()向前归一化。测试test_daily_schedule_preserves_local_wall_clock_across_dst与test_recurrence_handles_intervals_dst_and_downtime分别覆盖了秋季回拨与春季跳变两种场景。4.2 停机后的追赶策略只补一次不重放ADR 原文没有直接写但 Rest-API.md 与实现都明确服务停机后恢复时只派发一个逾期 occurrence然后递归推进到下一个未来时间绝不把错过的每个间隔全部重放。实现上这是通过next_future_after(occurrence, now)完成的——它取max(上次 occurrence, 当前时间)作为基准再计算下一个时刻从而把多次错过的间隔折叠为一次。测试test_recurrence_handles_intervals_dst_and_downtime验证了 hourly 调度停机 15 小时后从2026-08-21T04:00继续而非重放。五、重叠策略skip 与 queue 的精确语义ADR-0006 规定默认重叠策略为skip当同一调度的前一批次仍处于活动状态reserved、queued 或 running时跳过本次 occurrence。API 文档给出的另一个选项是queue再提交一个有限批次排在后面。实现位置在ScheduleDispatcher.dispatch_claimed()active await self._reconcile_pending(..., reserved_onlyschedule.overlap_policy queue, ...) if schedule.overlap_policy skip and active: next_run schedule.timing.next_future_after(scheduled_for) await self.schedule_store.complete_claim(..., errorOccurrence skipped because a prior scheduled batch is still active) return注意reserved_only的差别skip模式下把reserved/queued/running/cancelling全部视为活动queue模式只把尚未创建运行的reserved视为待办因此已有运行在跑时仍会追加新批次。测试test_due_schedules_skip_or_queue_while_a_prior_batch_is_active验证了 skip 模式只产生 1 条派发记录并记录跳过错误而 queue 模式产生 3 条派发记录且无错误。六、私有 SQLite 控制平面的安全与存储细节调度库是私有的ADR-0006 用 private local SQLite 强调这一点schedule_store.py 中有多道防线文件权限 0600初始化完成后self.database.chmod(0o600)测试test_schedule_database_rejects_symlinks_and_is_private断言权限位精确等于0o600拒绝符号链接初始化时与每次建立会话前都调用reject_symlink()防止调度库被替换为指向其他文件的链接测试test_schedule_database_rechecks_symlink_before_each_connection验证了会话前复检WAL 模式初始化时执行PRAGMA journal_mode WAL失败即报错外键强制连接后检查PRAGMA foreign_keys必须为 1原子初始化BEGIN IMMEDIATE包裹建表避免并发初始化竞争。调度库的schedules表还带 CheckConstraintenabled IN (0,1)、overlap_policy IN (skip,queue)schedule_dispatches表对(schedule_id, scheduled_for, target)有唯一约束杜绝同一 occurrence 同一目标重复派发。6.1 相关环境变量环境变量作用默认值THEHARVESTER_SCHEDULE_DB覆盖调度库文件路径运行库同名 sibling*.schedules.sqliteTHEHARVESTER_SCHEDULER设为disabled仅用于持久化预览或外部控制启动enabledTHEHARVESTER_RUN_WORKER控制执行 worker 开关调度派发依赖它enabledTHEHARVESTER_API_KEYAPI 认证密钥无必须设置run-now端点会先检查worker_enabled()与worker_available()worker 不可用时返回 503 且不创建任何运行测试test_run_now_rejects_an_unavailable_worker_without_creating_runs。七、调度器运行机制认领、租约与幂等恢复调度器以 asyncio 任务运行在 HarvestView 进程内start_scheduler()/_scheduler_loop()见 schedule_service.py。核心流程查询下一个到期时刻_wait_for_work()读取next_due_at()enabled1且next_run_at最小的记录据此休眠至多 5 秒或等待唤醒事件创建/修改/删除/暂停/恢复调度都会wake_scheduler()认领到期 occurrenceclaim_due(owner_id, lease_seconds60)用带claim_until的原子 UPDATE 抢占到期记录同一 occurrence 只可能被一个调度器实例认领测试test_two_scheduler_instances_cannot_claim_the_same_occurrence验证两个并发认领只有一个成功处理过程中续租处理大批量目标时每 100 个目标续租一次renew_claim租约丢失则立刻抛错、失败关闭测试test_claimed_occurrence_fails_closed_after_claim_loss、test_large_reservation_reconciliation_renews_the_schedule_claim完成或推迟成功派发后complete_claim写入last_run_at并推进next_run_at执行 worker 不可用或异常时defer_claim保留 occurrence 身份并推迟 60 秒重试测试test_deferred_claim_keeps_occurrence_identity。7.1 幂等恢复派发预留的一个重要特性是可恢复且幂等如果调度器在创建运行过程中崩溃已reserved的派发记录与已创建的运行都会保留下次派发时_enqueue_targets会复用既有预留与 run ID而不是重新生成测试test_dispatch_recovery_reuses_reservations_and_run_ids验证了重复dispatch_now第二次返回空run_ids且目标全部进入skipped_targets。reserve_dispatches使用sqlite_insert(...).on_conflict_do_nothing()保证预留的原子幂等。八、实操创建、查看与管理一个调度HarvestView 的调度 API 以/api/v1/schedules为前缀路由定义在 schedules.py全部需要X-API-Key认证。8.1 创建调度以下示例来自 Rest-API.md 的 Schedule finite runs 章节创建一个每周一 09:00美东运行的库存枚举调度curl -s http://127.0.0.1:5000/api/v1/schedules \ -X POST \ -H X-API-Key: $THEHARVESTER_API_KEY \ -H Content-Type: application/json \ -d { name: Weekly external inventory, targets: [example.com, example.org], run: {target: example.com, sources: [crtsh], limit: 500}, timing: { frequency: weekly, start_at: 2026-08-24T09:00:00-04:00, timezone: America/New_York, interval: 1, weekdays: [1] }, enabled: true, overlap_policy: skip } \ | jq请求字段说明与 schedule_models.py 的校验规则对应字段约束与说明name1–120 字符空白折叠后不可为空targets1–10000 个规范化目标去重会与run模板合并校验每个目标都必须是合法的运行目标run完整 RunRequest 模板target会被逐个替换sources可为空纯动作调度timing.frequencyonce/hourly/daily/weekly/monthlytiming.start_at必须带 UTC 偏移timing.timezone合法 IANA 时区名timing.interval1–365频率单位数timing.weekdays仅 weekly 可用ISO 星期 1–7去重overlap_policyskip默认或queueenabled是否立即可派发响应中除了schedule_id、next_run_at外还包含接下来 5 个派生 occurrenceupcoming_occurrencesHarvestView 前端会在每张调度卡片上展示。测试test_schedule_api_exposes_five_upcoming_monthly_occurrences验证了 1 月 31 日起算的月度调度返回 5 个正确日期且 pause 后upcoming_occurrences变为空。8.2 完整 API 一览方法路径说明GET/api/v1/schedules列出调度limit 1–500默认 100POST/api/v1/schedules创建调度201GET/api/v1/schedules/health报告调度器与执行 worker 的 enabled/available 状态GET/api/v1/schedules/{id}读取单个调度PUT/api/v1/schedules/{id}整体替换调度不删除已提交运行与派发历史DELETE/api/v1/schedules/{id}删除调度204不取消已提交运行不删除完成证据POST/api/v1/schedules/{id}/pause暂停未来 occurrence不取消已提交运行POST/api/v1/schedules/{id}/resume恢复未来 occurrencePOST/api/v1/schedules/{id}/run-now不改变递归时机立即多派发一次202worker 不可用返回 503GET/api/v1/schedules/{id}/dispatches列出每个目标的派发预留与生命周期镜像limit 1–5000健康检查示例响应对应 schedules.py 的ScheduleHealthResponse{ scheduler_enabled: true, scheduler_available: true, worker_enabled: true, worker_available: true }8.3 行为要点暂停/删除不会取消已提交运行删除也不会删除完成证据——这是 ADR-0006 preserves lifecycle and cancellation behavior 的操作者友好体现替换调度走 PUT 路由已完成的一次性调度被替换后其last_run_at保留重新启用时按新模板计算next_run_at测试test_replacing_a_completed_once_schedule_resets_occurrence_stateP1/P2 活动的授权不因调度而放宽每个 occurrence 只执行运行模板中显式存储的 provider/DNS/直接活动P1、P2 仍要求操作者对每个列出的目标显式授权见 Rest-API.md。九、网络边界与单操作者设计哲学ADR-0006 末尾明确不引入外部调度器、第二条执行路径或分布式队列直到有可衡量的需求证明需要改变本地单操作者边界。与之呼应的是 ADR-0003 的 A single worker matches the local single-operator product——这是一个刻意保持的本地化、串行化、可审计的产品边界。对操作者而言这意味着调度管理本身零网络活动纯本地 SQLite到点只发生运行模板里显式声明的 provider/DNS/直接请求一次只执行一个运行worker 串行重叠由skip/queue策略显式表达不会因调度产生意外的并发网络洪峰默认绑定127.0.0.1:5000需要远程访问时须自行叠加 TLS、访问控制、请求日志与限流见 Rest-API.md 安全边界章节。十、测试与验证路径调度功能在 tests/lib/test_schedules.py 中有超过三十个测试覆盖是理解语义最直接的入口。值得重点阅读的几类递归语义test_recurrence_handles_intervals_dst_and_downtimeinterval、DST、停机追赶、test_daily_schedule_preserves_local_wall_clock_across_dst、test_monthly_schedule_uses_the_final_day_of_short_months重叠策略test_due_schedules_skip_or_queue_while_a_prior_batch_is_active幂等与恢复test_dispatch_recovery_reuses_reservations_and_run_ids、test_overlap_policy_recovers_current_occurrence_reservations、test_recovered_queued_occurrence_completes_without_a_false_error并发与租约test_two_scheduler_instances_cannot_claim_the_same_occurrence、test_claimed_occurrence_fails_closed_after_claim_loss、test_claim_loss_during_enqueue_leaves_every_target_auditable安全test_schedule_database_rejects_symlinks_and_is_private、test_every_schedule_route_requires_authentication容量test_schedule_store_reserves_the_maximum_target_batch10000 目标全量预留。十一、总结ADR-0006 通过私有本地 SQLite 控制平面 既有持久队列 单一 worker的组合为 HarvestView 增加了可落地的定时枚举能力同时守住了三条底线生命周期与取消行为不被破坏、自动化策略不进证据导出、不引入超出单操作者需求的分布式基础设施。对于需要每周对一批授权域名做 crtsh 枚举并留档这类场景只需一个 POST 请求即可建立长期任务并借助upcoming_occurrences、dispatches与health端点持续观察其状态。【免费下载链接】theHarvesterE-mails, subdomains and names Harvester - OSINT项目地址: https://gitcode.com/GitHub_Trending/th/theHarvester创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表