news 2026/9/14 12:33:30

theHarvester 中的 ASN 组织归属持久化:基于来源证据的规范化 SQLite 关系设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
theHarvester 中的 ASN 组织归属持久化:基于来源证据的规范化 SQLite 关系设计

theHarvester 中的 ASN 组织归属持久化:基于来源证据的规范化 SQLite 关系设计

【免费下载链接】theHarvesterE-mails, subdomains and names Harvester - OSINT项目地址: https://gitcode.com/GitHub_Trending/th/theHarvester

导读

本文围绕 theHarvester 仓库中的架构决策记录 ADR-0005(Persist ASN organization attribution as sourced evidence) 展开,讲解项目如何把"某组织(organization)持有/通告某 ASN"这一信息,从随意的属性标签升级为带来源(sourced)、类型化(typed)、可校验(canonical)的持久化证据。你将掌握:归属观察(AsnAttributionObservation)的数据模型与规范化规则、SQLite 中asn_attributions关系表的存储结构、JSONL 与 API 的投影格式,以及它与 RouteViews 网络纵深(pivot)的衔接边界。读完本文即可在二次开发或审计 thisHarvester 结果数据库时准确理解组织归属数据的写入、读取与校验链路。

本文涉及文件均为仓库现有内容,可对照 theHarvester/lib/asn_attribution.py、theHarvester/lib/database.py、theHarvester/lib/completed_result.py 等路径深入阅读。

背景:为什么组织标签不能作为 ASN 的可变属性

ADR-0005 首先回答了一个数据建模问题:ASN 的组织归属(organization attribution)应该以什么形态存储?

Provider 标签本身并不可靠,ADR 明确列出的现实约束有三点:

  • 可能缺失:某个数据源(source)返回的 ASN 结果里并没有组织名;
  • 会随时间变化:组织改名、重组、运营主体变更都会导致同一 ASN 的标签前后不一致;
  • 跨来源不一致:不同 provider(如 Onyphe、Shodan、URLScan)对同一 ASN 给出的组织名可能互相冲突。

如果把组织标签当作 ASN 结果上的一个"可变属性"直接塞进results.details_json,这些冲突会被静默覆盖,且无法支撑"跨多次运行(cross-run)查询同一 ASN 的所有组织归属"这类需求。因此 ADR-0005 的决策是:归一化(normalized)为独立的、带来源的关系记录,每条记录把 ASN 结果、关联的主机名或 IP 结果、以及产生该证据的 source/action 执行三者显式关联起来。

同时,决策划清了语义边界:组织归属只服务于操作者复核(operator review),它:

  • 不代表所有权(ownership)判定;
  • 不构成授权(authorization)依据;
  • 不能扩展目标范围(target scope);
  • 其组织标签本身不能作为过滤条件——但它精确的 IP 主体(exact IP subject)可以作为自动触发 RouteViews 纵深查证的输入。

决策要点:类型化证据 + 归一化关系行

ADR 的完整决策是:

将每条 ASN 组织归属存储为一个类型化的领域观察(typed domain observation),由一条归一化的 SQLite 关系行支撑,该行关联 run、ASN 结果、相关主机名或 IP 结果、以及 source 执行。SQLAlchemy 行对ResultStore保持私有,观察通过 JSONL 与 API 投影输出,而不是把组织标签当作 ASN 的可变属性。

落到代码上,这一决策由三层结构落实:

  1. 领域对象层:theHarvester/lib/asn_attribution.py 中定义的AsnAttributionObservation冻结数据类;
  2. 存储层:theHarvester/lib/database.py 中定义的_AsnAttributionRowSQLAlchemy 模型(表名asn_attributions),且仅由ResultStore内部使用;
  3. 投影层:JSONL 序列化(completed_result.py)与 API 响应模型(run_models.py)。

领域模型:AsnAttributionObservation 及其规范化规则

AsnAttributionObservation是 frozen + slots 的不可变数据类,字段与约束如下(asn_attribution.py):

字段类型规范化规则
producer_kind'source'/'action'必须是二者之一
producerstr非空、UTF-8 可编码、长度 ≤ 255、不含 Cc/Cf 控制类字符
asnstrnormalize_asn归一为AS<number>形式
organization_labelstr非空、UTF-8 可编码、长度 ≤ 255(MAX_ORGANIZATION_LABEL_LENGTH)、不含控制字符
subject_kind'hostname'/'ip'二者之一
subject_valuestrhostname 经normalize_hostname;IP 必须是规范地址(拒绝 IPv6 scope 标识%
collected_atdatetime必须带时区,统一转为 UTC

__post_init__中所有字段都在构造时完成校验与规范化,任何违规都抛出ValueError,体现了**构造即校验、失败即关闭(fail closed)**的设计。其中:

  • asn的规范化实现在 result_values.py:去掉AS前缀(大小写不敏感)、只接受 ASCII 十进制整数、范围0 ≤ ASN ≤ 4_294_967_295,最终输出AS<number>规范形式;
  • subject的规范化在_normalize_subject(asn_attribution.py):hostname 走域名归一化,IP 通过ipaddress.ip_address生成规范字符串,且明确拒绝含%的 IPv6 scope 形式;
  • _normalize_name校验非空、UTF-8 可编码、长度上限以及 Unicode 控制字符类别(unicodedata.categoryCc/Cf的字符一律拒绝)。

观察对象还提供两个关键方法:

  • sort_key():按(asn, producer_kind, producer, organization_label, subject_kind, subject_value, collected_at)排序,用于去重与稳定排序;
  • detail():输出投影字典,结构固定为:
{ 'type': 'organization-attribution', 'producer_kind': 'source', # 或 'action' 'producer': 'urlscan', 'organization_label': 'Example Transit', 'subject': {'type': 'ip', 'value': '192.0.2.10'}, 'collected_at': '2026-09-13T05:11:54Z', # format_utc 输出的 UTC 时间戳 }

规范化集合与严格解析

模块级函数定义了观察集合的"规范化"含义(asn_attribution.py):

  • canonical_asn_attributions(observations)sorted(set(...), key=sort_key),即去重 + 排序后才算规范化;
  • asn_attribution_details(observations):把规范化集合逐一投影为detail()字典列表;
  • parse_asn_attribution_details(asn, details):反向解析。它对输入做严格全等校验:要求details是非空数组、每个元素必须是恰好包含type / producer_kind / producer / organization_label / subject / collected_at六个键的字典、subject必须恰好包含type / value两个键、collected_at必须是可解析的 UTC 时间戳,最终还要求detail != observation.detail()时直接抛错——即读回来的内容必须与写出的规范化结构完全一致,不允许任何变体或多余字段。

存储层:asn_attributions 关系表的结构与生命周期

表结构与外键

SQLAlchemy 模型_AsnAttributionRow(database.py)对应表asn_attributions

类型说明
run_idText(主键)所属运行
positionint(主键)行内序号
asn_result_positionint外键 →results(run_id, position),指向该 run 内的 ASN 结果
subject_result_positionint外键 →results(run_id, position),指向主机名或 IP 结果
execution_positionint外键 →executions(run_id, position),指向产生两者的 source/action 执行
organization_labelText组织标签原文
collected_atTextISO 格式 UTC 时间戳

三条外键约束都带ondelete='CASCADE':run、被引用的结果或执行被删除时,归属行随之级联删除。行不存 ASN 值本身,只存位置指针——ASN 值由results表中对应行承载,归属行通过asn_result_position引用,这与 ADR 中"关系行 linking the run, ASN result, related hostname or IP result, and source execution"的描述完全一致。

初始化:schema 保持 8,缺表自动创建

ADR 提到"当前未发布(unreleased)的 schema 仍为 8,初始化时若缺表则自动创建"。代码印证于 database.py 的SCHEMA_VERSION = 8:初始化流程(database.py)读取PRAGMA user_version,若版本高于 8 直接报错拒绝;随后执行_Base.metadata.create_all(database.py),asn_attributions作为 metadata 中已注册的表,在数据库为空或旧库缺少该表时会被自动创建,无需提升 schema 版本——这正是"创建额外表"而非"破坏性迁移"的设计意图。

写入:save_run 的三段引用解析

写入逻辑位于ResultStore.save_run(database.py)。写入前先建立两张查找表:

  • result_positions{(kind, value): position},覆盖 run 内全部结果;
  • execution_positions{(producer_kind, name): position},覆盖全部 source 与 action 执行。

随后每条归属观察被映射为一行:

_AsnAttributionRow( run_id=run_id, position=position, asn_result_position=result_positions[('asn', attribution.asn)], subject_result_position=result_positions[(attribution.subject_kind, attribution.subject_value)], execution_position=execution_positions[(attribution.producer_kind, attribution.producer)], organization_label=attribution.organization_label, collected_at=attribution.collected_at.isoformat(), )

注意:归属对象中的asnsubject都是规范化后的值,必须能在本 run 的results中找到对应行,否则KeyError会让整个事务失败——这就是"每条归属必须引用规范的 ASN 结果、精确的主机名/IP 主体、以及产生两者的 source 或 action"的强制保证。

读取:load_run 的反向装配与完整性校验

读取时(database.py),load_runposition顺序取出全部attribution_rows,对每一行执行反向装配:

  1. 通过asn_result_position/subject_result_position/execution_position三处指针,分别查出 ASN 结果、主体结果与执行记录;
  2. 任一指针落空即抛出ResultStoreError('Persisted ASN attribution references missing evidence')
  3. 校验执行记录的producer_kind合法性,反推producer_kind(source/action);
  4. 用 ISO 时间戳重建collected_at,构造AsnAttributionObservation
  5. 全部装配完成后调用canonical_asn_attributions与原始列表比较,顺序不一致同样抛错

也就是说,持久化数据在每次加载时都会被重新规范化并全等校验,任何缺失、重复或非规范化(noncanonical)的关系都会导致 fail closed。这一点在测试 test_completed_persistence.py 中有直接覆盖:通过UPDATE asn_attributions SET asn_result_position = subject_result_positionINSERT INTO asn_attributions ...等方式注入损坏数据后,load_run必须抛错拒绝。

投影层:JSONL 与 API 如何暴露归属观察

ADR 强调 SQLAlchemy 行对ResultStore保持私有,观察通过JSONL 与 API对外投影。

JSONL 序列化

在 completed_result.py 中,CompletedResult.to_jsonl()先把归属按 ASN 分组为attribution_by_asn,当遇到kind == 'asn'且该 ASN 存在归属时,向该行记录写入:

{"type": "asn", "value": "AS64500", "observations": [{"type": "organization-attribution", ...}]}

反向路径from_jsonl(completed_result.py)则调用parse_asn_attribution_details严格解析,失败时以ValueError拒绝整条记录。此外CompletedResult在构造后还有一条全局一致性校验(completed_result.py):所有归属必须已去重排序,collected_at必须落在本次运行的started_at~completed_at区间内,且每条归属的(asn, subject)必须真实存在于 run 的结果集合中、其 producer 必须与执行记录吻合——任何一条不满足都拒绝该结果对象。

API 响应模型

API 侧的投影模型定义在 run_models.py:

  • AsnAttributionSubjectResponsetype限定为'hostname' | 'ip'value为字符串,extra='forbid'
  • AsnAttributionObservationResponsetype限定为'organization-attribution',字段与detail()一一对应,同样extra='forbid'

请求/响应处理在 run_evidence.py:先拒绝含未知字段的输入,再调用parse_asn_attribution_details解析,最后用asn_attribution_details重新规范化输出。这套"解析 → 规范化 → 输出"的往返保证了 API 与 JSONL 两个入口看到的归属结构完全一致。

边界约束:归属不可授权、不可过滤,但可触发 RouteViews 纵深

ADR 的 Consequences 部分明确了两条安全边界:

  1. 归属可以相互冲突地独立可见:不同 provider 标签冲突时,归一化关系保留全部冲突记录,而不是覆盖其中一方;
  2. 归属不能授权目标范围扩展:组织标签本身不是过滤条件,也不会自动扩大扫描授权范围。

唯一的自动化联动是RouteViews 网络纵深(pivot)。在 routeviews.py 中,enrich_routeviews的文档字符串写明:

Domain runs automatically pass harvested IPs that have sourced IP-to-ASN attribution. Bare ASN findings are not expanded into complete prefix inventories; that requires an explicit ASN target.

即:域名类运行中,只有带来源的 IP→ASN 归属所对应的精确 IP 才会被自动送入 RouteViews 查询;孤立的 ASN 结果(无归属关联、无显式 ASN 目标)不会被自动展开成完整前缀清单。归属的"精确 IP 主体"是触发纵深查询的输入,但组织标签不参与过滤决策。对应地,run_models.py中关于该能力的描述为"Enrich discovered IPs with sourced ASN attribution, or an explicitly targeted ASN or IP address"(run_models.py),进一步印证:要么是带来源的自动归属,要么是用户显式指定的 ASN/IP 目标。

源码验证路径小结

关注点验证位置
观察数据类与规范化asn_attribution.py
ASN 归一化规则result_values.py
关系表定义database.py
schema 版本与建表database.py、database.py
写入/读取装配database.py、database.py
JSONL 投影与校验completed_result.py、completed_result.py
API 模型与解析run_models.py、run_evidence.py
RouteViews 联动边界routeviews.py
损坏数据 fail-closed 测试test_completed_persistence.py
来源侧产出归属的测试test_shodan_engine.py、test_onyphe.py、test_urlscan.py、test_source_runner.py

总结

ADR-0005 为 theHarvester 确立了一条清晰的证据持久化原则:组织归属是"来源证据",不是"ASN 属性"。它以不可变领域对象 + 归一化 SQLite 关系行承载,通过外键把 run、ASN 结果、主机名/IP 主体和 source/action 执行钉死在一条记录里;写入与读取两侧都执行去重、排序、引用完整性与全等校验,任何缺失、重复或非规范的关系都会 fail closed。对外,它通过 JSONL 与 API 投影为结构固定的organization-attribution观察;对内,它只作为操作者复核材料与 RouteViews 精确 IP 纵深查证的输入,既不充当过滤条件,也不构成目标范围授权。这种设计在保留跨来源冲突可见性的同时,让 ASN 组织归属成为可查询、可追溯、可复核的结构化证据。

【免费下载链接】theHarvesterE-mails, subdomains and names Harvester - OSINT项目地址: https://gitcode.com/GitHub_Trending/th/theHarvester

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 12:33:00

人机交互实验的具身智能数据采集平台选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 12:32:07

SpringBoot+Vue实现乡村垃圾运输智能管理系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 12:28:49

Unity开发实战记录:从UI细节到数字孪生的踩坑与解法

做Unity开发这几年&#xff0c;我最大的感受是&#xff1a;真正折磨人的从来不是引擎里那些花哨功能&#xff0c;而是一个个具体到发指的细节。按钮点击区域差几像素、WebGL存档写不进去、PLC读回来的温度值是个天文数字、Pico上MR切VR画面闪一下——这些问题单独看都不大&…

作者头像 李华
网站建设 2026/9/14 12:28:18

MCU芯片级功能安全机制:ECC与锁步核的工程化整合

1. SafetyPack不是个软件包&#xff0c;而是MCU芯片级安全机制的系统化封装概念你搜“SafetyPack”时&#xff0c;大概率会一头雾水——GitHub上没有叫这个名字的知名开源库&#xff0c;主流芯片厂商的SDK里也找不到独立的SafetyPack安装包。这不是一个能pip install或make men…

作者头像 李华
网站建设 2026/9/14 12:28:07

YOLO26改进:c3k2与RandomMixingFormer融合实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华