本章标志着我们从基础的 recon 自动化迈向更高阶的一步:构建一个由 agentic AI 设计驱动、且完全 no-code 的攻击面管理(ASM)系统。我们将超越理论化工作流,开始打造一个实用、贴近真实世界的 ASM 智能体,它的工作方式会反映现代红队与漏洞赏金猎人(bug bounty hunters)的实践。目标是使用进攻性安全社区里强大且开源的工具,并用 n8n 将它们无缝编排在一起——不写传统脚本或 shell 命令。
在本章中,我们会重建并扩展之前的 recon bot,把它转化为一个 agentic ASM 解决方案,对任意目标执行深度侦察。这将涵盖诸如发现(discovery)、子域名枚举、服务发现、基于 HTTP 的资产解析,以及技术栈指纹识别等技术。为了高效实现这些,我们将依赖安全社区在 GitHub 上最常用、已成为“标配”的多种工具,例如 subfinder、aiodnsbrute、assetfinder、shuffledns、httpx、nmap、nuclei 等等。
我们不会把这些工具当作孤立的命令行脚本来介绍,而是展示如何在 n8n 中把它们接入一个更大的 agentic 自动化系统。该系统会动态收集、解析并规范化(normalize)输出,并将数据存入诸如 MongoDB 这类结构化后端以便进一步处理。我们的目标不仅是执行扫描,更是构建一个可长期运行、可循环(loopable)的 ASM 智能体:它可以持续运作,并与大语言模型(LLMs)集成以获得上下文感知。
本章将重点展示这些工具如何被连接起来,以及如何为速度、可靠性与集成进行优化。我们会聚焦那些输出对自动化友好(例如 JSON)、可在容器内运行、并在开源生态中维护良好的工具。无论是通过来源做被动数据采集,还是用 subfinder、dnsx 等工具主动探测端点,我们都会覆盖多种用例,同时保持以 n8n 为中心的模块化架构。
本章将覆盖以下主题:
- 构建一个 agentic AI 的 ASM 工作流
- 集成资产发现、枚举与指纹识别、以及漏洞检测
到本章结束时,你将拥有一个既强大又可扩展的 agentic ASM 系统蓝图:它让你无需编写复杂脚本就能规模化 recon 活动,并为“完全自治的安全智能体代你做决策”打开大门。
构建一个 agentic AI 的 ASM 工作流(Building an agentic AI ASM workflow)
要让 ASM 真正自治,我们需要的不只是自动化脚本;我们需要一个能够选择性触发工作流、维护记忆,并利用结构化工具的推理智能体。本节我们将探索一种模块化的 agentic AI 设计:用开源工具与实时推理来编排发现、枚举与漏洞检测。
下面是核心能力的高层概览,展示一个中心 AI 智能体如何被设计来管理并协调 ASM 涉及的多个任务:
- 基于用户输入或后台触发器动态触发工作流
- 在 Postgres 数据库中存储记忆与上下文
- 运行开源工具进行资产发现、指纹识别与漏洞检测
- 查询数据库中已发现的数据
- 必要时引入人工反馈,以确保准确性与相关性
在这个模型里,我们刻意避免只依赖 AI 本身,以防幻觉(hallucination)与无根据的假设。每个智能体工作流都锚定在可信开源工具产出的真实、可复现数据之上。
目标是在复杂的云原生环境中,确保可追溯性与可靠性,并减少误报(false positives)——这点至关重要。
下图展示了 agentic ASM 系统的高层结构:一个中心 AI 智能体编排发现、枚举、漏洞检测与数据查询工作流:
图 4.1 —— 用于 ASM 的 Agentic AI(Figure 4.1 – Agentic AI for ASM)
配置 AI Agent 节点
我们先检查 AI Agent 节点的配置。正如前几章讨论过的,我们首先选择一个 LLM。本例中,我们将配置 OpenAI API key,并选择 gpt-4o-mini 模型。对这个智能体来说,我们还需要定义一个自定义 system prompt。该 prompt 应当描述工作流逻辑、智能体允许使用的工具,以及它应当如何与用户交互。这些规范对于确保智能体行为一致、并按预期执行任务非常关键。
在 AI Agent 节点的 Parameters 面板中,添加如下 system prompt,用于定义工具使用方式、决策边界与交互模式:
{{ $now }}
## System Prompt: Attack Surface Management (ASM) Agent
You are an **Attack Surface Management (ASM) Agent** responsible for orchestrating a multi-stage scanning workflow on a given domain. Your primary objective is to assess and manage the external attack surface of any domain provided by the user.
You are connected to the following tools:
- **DB Query Tool**: Retrieves historical scan data and metadata from a domain database.
- **Asset Discovery Tool**: Discovers all visible subdomains and infrastructure assets associated with the domain.
- **Enumeration & Fingerprinting Tool**: Enumerates services and fingerprints technologies across discovered assets.
- **Vulnerability Detection Tool**: Scans for security vulnerabilities across the enumerated assets.
---
### Your Behavior
#### Step 1 – Check for Historical Data
When a user gives you a domain (e.g., `"example.com"`), you must first call the **DB Query Tool** using the following input format:
"domain.com"
Based on the tool's response:
If the domain has no record, inform the user:
"There is no historical record for this domain. Would you like me to initiate a new attack surface scan?"
If a record exists, respond:
"The last scan for this domain was on [last_scan_date]. Would you like me to initiate a new scan?"
#### Step 2 – Wait for User Approval
Do not proceed unless the user explicitly approves starting a new scan. If they respond with "yes", continue to Step 3.
#### Step 3 – Execute the Scanning Workflow
Sequentially trigger the following tools in this exact order, using the input:
"domain.com"
1. Asset_Discovery Tool
2. Enumeration_Fingerprinting Tool
3. Vulnerability_Detection Tool
Wait for each tool to complete before moving to the next.
#### Step 4 – Final Report
Once all tools finish running, respond:
"The attack surface scan for [domain] is complete. Assets have been discovered, fingerprinted, and scanned for vulnerabilities."
### Special Instructions
Always use the input format below when calling tools:
"domain": "<domain-name>"
If the user manually asks about database history (e.g., "Check from database"), use the DB Query Tool to answer.
Never skip the database check step.
You only coordinate the tools you do not perform any scanning yourself.
Responses should remain clear, professional, and concise.
如图所示,作为智能体的工具配置,主工作流中(图 4.1)展示的每个工具都代表一个独立、可自包含的工作流,这些工作流会在本章后续部分被定义并实现。作为参数,我们定义了一个名为 domain 的自定义字段。通过把 AI 智能体生成的查询作为该参数的值传入,我们可以确保智能体把自己的 query/input 发送到目标工作流:
图 4.2 —— 将 domain 变量传入工具配置(Figure 4.2 – Passing the domain variable into a tool configuration)
与其把每个阶段拆成独立智能体,本方案中的工作流(Asset Discovery、Enumeration Fingerprinting、Vulnerability Detection)被设计为模块化工具链。它们由核心 AI 智能体触发,并通过 bash 命令、API 调用或脚本执行的组合来运行,从而实现完全透明与可控。
在接下来的小节中,我们会逐一拆解每条工作流,并展示如何用开源工具与中心化的 agentic 编排层来实现它们。
集成资产发现、枚举与指纹识别、以及漏洞检测(Integrating Asset Discovery, Enumeration Fingerprinting, and Vulnerability Detection)
在本节中,我们将开始配置三条由 AI 智能体统一控制的核心工作流:资产发现(Asset Discovery) 、枚举与指纹识别(Enumeration Fingerprinting) 、以及漏洞检测(Vulnerability Detection) 。这些工作流代表任何 ASM 系统的基础阶段,并作为可复用的模块化构件,可针对不同环境进行复用、扩展或微调。
每条工作流都会串联多个开源安全工具,其中许多在进攻性安全与漏洞赏金(bug bounty)生态中被广泛使用。利用开源工具的一大优势,是可以获得标准化、便于 grep 的输出格式,例如 CSV、JSON 或 XML。这使得不同步骤之间能够无缝集成,数据流动顺畅,并支持基于逻辑的分支。
在这些工作流中,我们主要使用 JSON 输出格式,因为它既易于解析,也被大多数现代安全工具原生支持。在某些阶段,尤其是在映射或清洗原始输出时,我们会加入轻量级 JavaScript 函数,在把数据传递到下一步之前先做转换。
少量使用脚本的选择纯属务实:它能避免在工作流 UI 中堆叠过多中间节点,从而减少视觉杂乱。但这不是硬性要求;如果你愿意增加更多节点,完全可以做到全程无代码,只依赖自动化框架的拖拽能力来实现。
接下来,我们会逐条工作流深入讲解,说明:
- 使用了哪些工具
- AI 智能体配置了哪些参数
- 输出如何被解析并转发
- 最终目标是构建健壮、可复用、且自治的工作流,支持持续的资产情报更新,而不需要人在每一步做微观管理。这将为一个可规模化、AI 驱动的 ASM 流水线打下基础,它能跨项目复用,并能同时融入红队与蓝队的运营体系。
现在我们开始设计 Asset Discovery 工作流,它是图 4.1 所示主 agentic 工作流调用的第一个工具。在接下来的小节里,我们会一步一步构建它,然后再进入 ASM 流水线中的其余工具。
阶段 1 —— 资产发现工作流(Stage 1 – Asset Discovery workflow)
如 system prompt 所定义,这是 ASM 流水线中第一个被执行的工作流。
该工作流使用的节点包含多个用于子域名枚举的流行开源工具,并且都支持 JSON 输出:aiodnsbrute、subfinder、assetfinder、shuffledns。这些工具通过 SSH 节点执行,使我们能够输入 SSH 登录凭据,并在一个或多个远程服务器上运行枚举过程,从而支持并行化执行:
图 4.3 —— 资产发现工作流(Figure 4.3 – Asset Discovery workflow)
除此之外,我们还会使用一些工作流节点,例如 Set、Code、Merge、If、MongoDB:
图 4.4 —— Set、Code、Merge、If 与 MongoDB 节点(Figure 4.4 – The Set, Code, Merge, If, and MongoDB nodes)
标记为 Domain1 的 Set 节点用于接收并赋值智能体传入的 domain 参数。这个命名约定是刻意的:通过在开头使用通用节点名,我们能在未来想实现“多智能体协同的集群架构”时,保留结构一致性与兼容性。
下一步是检查 MongoDB:看该 domain 是否已有记录。工作流使用 MongoDB 节点连接数据库并执行查询。
注意 If 节点的两个分支(domain 在数据库中存在或不存在)都会继续进入资产发现工作流。这是刻意设计的:如果记录已存在,我们就刷新并更新发现数据;如果没有历史记录,则从零开始执行完整资产发现。
这种做法确保 ASM 过程具备幂等性(idempotent) 与状态感知(state-aware) 能力,能够以持续且可规模化的方式维护与更新域名情报。它也为 AI 智能体提供上下文:帮助判断一个新发现的子域名或 IP 是否值得进一步扫描,或因先前分析而应被降级处理。
当域名上下文建立、状态校验完成后,工作流将切换到主动侦察阶段。在这个阶段,智能体会通过编排一系列专用工具启动子域名枚举,每个工具都作为独立枚举工作流的一部分执行。这样的设计让智能体能够进行可规模化、可重复的发现,同时在“协调逻辑”和“执行逻辑”之间保持清晰分离。
子域名枚举流程(Subdomain Enumeration flow)
在该工作流阶段,我们启动子域名枚举流程:
图 4.5 —— 子域名枚举过程(Figure 4.5 – The Subdomain Enumeration process)
我们运行的第一个工具是 aiodnsbrute:一个高性能、异步的 DNS 暴力枚举工具,专门用于高效发现子域名。它利用异步 DNS 查询,相比传统暴力工具能实现快得多的解析速度,因此非常适合大规模侦察任务。
你可以从其官方 GitHub 仓库获取该工具:https://github.com/blark/aiodnsbrute。
在我们的工作流中,我们通过 n8n 的 SSH 节点集成 aiodnsbrute。这使我们只需提供有效 SSH 凭据并指定必要命令行参数,就能在一台或多台服务器上远程执行工具。该设置不仅分摊负载,也确保扫描过程不会消耗本地资源,或暴露扫描系统的 IP 地址。
该工具输出结构化、便于 grep 的格式,非常适合下游解析与集成到更大的 ASM 流水线中。执行完成后,结果会被映射并存入 MongoDB,以支持历史追踪,以及与后续阶段(指纹识别、漏洞检测)的关联分析。
下面是 SSH 节点运行 aiodnsbrute 的配置方式:
图 4.6 —— aiodnsbrute 节点(Figure 4.6 – The aiodnsbrute node)
如截图所示,我们将 SSH 节点配置为使用特定表达式与动态输入来执行工具。用于运行 aiodnsbrute 的命令如下:
/home/kali/.local/bin/aiodnsbrute -w 2m-subdomains.txt -r resolvers-1.txt --gethostbyname {{ $json.domain }} --verify -o json -f aiodnsbrute-{{ $json.domain }}.json
我们拆解一下这条命令:
-w 2m-subdomains.txt:定义用于暴力枚举的字典。我们使用的是一个包含 200 万条的子域名字典,覆盖常见与冷门的子域模式。你可以从这里下载:https://wordlists-cdn.assetnote.io/data/manual/2m-subdomains.txt。-r resolvers-1.txt:使用自定义的高质量 DNS resolver 列表,以便在大规模解析时避免限流或被阻断。resolver 质量会显著影响枚举的准确性与速度。可参考:https://github.com/trickest/resolvers/blob/main/resolvers.txt。--gethostbyname {{ $json.domain }}:通过表达式动态注入 AI 智能体传入的 domain 值。在 n8n 中,{{ $json.domain }}用于引用上游节点的 domain 字段,使工作流可对任何目标复用。--verify:确保只返回真正可解析的子域名,从而减少误报噪声。-o json:将输出格式设置为 JSON,便于解析并在 ASM 后续阶段存储与处理。-f aiodnsbrute-{{ $json.domain }}.json:根据输入 domain 动态生成输出文件名。
该配置让资产发现智能体能够在远程服务器上运行高吞吐、准确的子域名枚举任务,并产出结构化结果,随时可用于分析与富集。
完成 aiodnsbrute 的子域名枚举后,我们会用另一个广泛采用的工具 subfinder 来增强覆盖面。该工具属于 ProjectDiscovery 工具集,以速度、可靠性以及聚合数十种被动数据源的能力而闻名。它是侦察与漏洞赏金工作流中的标配,也非常适合作为 aiodnsbrute 这类基于暴力枚举工具的补充。
你可以从 subfinder 官方 GitHub 仓库下载:https://github.com/projectdiscovery/subfinder。
与 aiodnsbrute 类似,我们会在 n8n 里通过 SSH 节点在已安装该工具的远程机器上直接运行 subfinder。这样能让 n8n 工作流保持轻量,同时把高强度侦察任务卸载到专用扫描基础设施上:
图 4.7 —— subfinder 节点(Figure 4.7 – The subfinder node)
如截图所示,我们会在 SSH 节点中执行以下命令:
/usr/bin/subfinder -silent -duc -d {{ $('Merge').item.json.domain }} -rL resolvers.txt -oJ -o subfinder-{{ $('Merge').item.json.domain }}.json
拆解参数含义与使用原因:
-silent:抑制冗余输出,保持 SSH 执行日志更干净。-duc:执行 DNS 解析,验证发现的子域名确实可解析(减少噪声)。-d {{ $('Merge').item.json.domain }}:用 n8n 表达式注入动态 domain 值,引用 Merge 输入中选择的 domain。-rL resolvers.txt:指定自定义 DNS resolver 列表,以获得更快、更可靠的解析。你可以使用社区维护的高质量 resolver 列表来提升速度与稳定性。-oJ:以 JSON 格式输出,便于后续 n8n 节点解析处理。-o subfinder-{{ $('Merge').item.json.domain }}.json:将输出保存为按 domain 命名的文件,便于按目标组织数据。
将 subfinder 与 aiodnsbrute 结合使用,我们同时获得被动与主动的子域名枚举覆盖,确保不遗漏潜在攻击面。后续我们会对输出做规范化,并用于 IP 解析、端口扫描,以及与威胁情报源的关联。
为了进一步扩大子域名枚举覆盖面,我们引入另一个被动工具:Tomnomnom 的 assetfinder。它能利用 crt.sh、ThreatCrowd 等被动数据源快速发现子域名。由于简单且速度快,它在漏洞赏金猎人与侦察工程师群体中广受信任:
图 4.8 —— assetfinder 节点(Figure 4.8 – The assetfinder node)
你可以在其官方 GitHub 仓库找到该工具:https://github.com/tomnomnom/assetfinder。
与之前工具一样,我们在 n8n 中通过 SSH 节点运行 assetfinder。该命令针对“抽取干净子域数据并输出为 JSON”做了定制:
/home/kali/go/bin/assetfinder --subs-only {{ $json.domain }} | jq -R -s -c 'split("\n")[:-1]' | tee assetfinder-{{ $json.domain }}.json
各部分含义如下:
/home/kali/go/bin/assetfinder --subs-only {{ $json.domain }}:执行工具获取该 domain 的子域名,--subs-only将输出限制为有效子域名(不包含 IP 或无关引用)。{{ $json.domain }}:动态注入 domain 值(通常来自上游节点)。| jq -R -s -c 'split("\n")[:-1]':assetfinder 的输出是纯文本(每行一个子域名),我们用 jq 把列表转换为干净的 JSON 数组,[:-1]用于去掉尾部空行。| tee assetfinder-{{ $json.domain }}.json:把 JSON 数组写入按目标 domain 命名的文件,以保留结构化输出,便于日志与后续处理。
将 assetfinder 叠加在 aiodnsbrute 与 subfinder 之上,我们扩展了被动枚举的广度,同时刻意引入多工具之间的结果重叠。重叠有助于交叉验证发现、减少误报,并降低因工具特性限制导致漏资产的风险。稍后我们会统一规范化所有输出,并解析到 IP,为下游端口扫描与富集做准备。
为完成子域名枚举流程的最后一块,我们加入 ProjectDiscovery 开发的 shuffledns:一个强力、偏“进攻性”的 DNS 解析工具。与 assetfinder、subfinder 这类纯被动工具不同,shuffledns 使用高性能 resolver 引擎做暴力枚举 DNS,因此能发现那些不在公开数据源中的子域名,在追求完整性与深覆盖时非常合适。
你可以从官方 GitHub 仓库获取:https://github.com/projectdiscovery/shuffledns。
图 4.9 —— shuffledns 节点(Figure 4.9 – The shuffledns node)
在工作流中,我们同样使用 SSH 节点执行命令:
/home/kali/go/bin/shuffledns -duc -silent -r resolvers.txt -d {{ $('Merge').item.json.domain }} -w 2m-subdomains.txt -j -o shuffledns-{{ $('Merge').item.json.domain }}.json -mode bruteforce
拆解命令参数:
-duc:禁用更新检查,以提升性能并在 CI 类环境中保持一致性。-silent:抑制冗余日志,保持输出干净。-r resolvers.txt:指定要使用的公共 DNS resolver 列表;对绕过本地 resolver 限制与提升解析速度至关重要。-d {{ $('Merge').item.json.domain }}:从上游节点动态指定目标 domain。-w 2m-subdomains.txt:提供一个大字典(200 万条)用于暴力枚举潜在子域。-j:以 JSON 格式输出,便于解析与集成到 n8n 流水线。-o shuffledns-{{ $('Merge').item.json.domain }}.json:按目标 domain 命名输出文件。-mode bruteforce:确保工具严格执行暴力枚举而非被动抓取。
加入 shuffledns 后,我们用四种互补工具完成子域名枚举:
- aiodnsbrute:异步暴力枚举 + DNS 验证
- subfinder:被动、可靠的聚合式枚举
- assetfinder:从已知来源快速获取结果
- shuffledns:深度、基于暴力枚举的发现
每个工具都有不同优势与数据来源,让我们得到更多样、冗余更高的子域数据集。
最后,这套序列对多数 recon 工作流已经有很好的覆盖,但你也应按需要自定义,例如:
- 增加 amass、dnsx 或 chaos-client 等工具
- 引入 permutation / mutation 这类变体生成技术
- 将子域名枚举拆成独立工作流以提升可扩展性
当我们运行多个工具并收集到一份广泛的子域名列表后,无论这些子域名当前是否活跃,我们都拥有了一份有价值的数据集。一个子域名能被侦察工具发现,就意味着它曾经在线、可能仍在线,或未来可能被复用。与其丢掉这份数据,更聪明的做法是把它持久化,供长期使用。
将数据存入 MongoDB(Storing data in MongoDB)
通过把每个工具的输出存入 MongoDB collection,我们构建了一个历史仓库,后续可以用于分析、模式识别与 AI 推理。这让 agentic 系统不仅能评论“当前暴露”,也能评论“过去出现过或重新出现的基础设施”。为此,我们会使用工作流中的两个关键节点:Code4 节点用于转换并准备数据,MongoDB3 节点用于写入数据库。这样不仅让 recon 更聪明,也让威胁情报工作流具备长期演进能力(future-proof)。
在这个阶段,我们把 Code 节点当作灵活的转换层,允许在工作流中注入自定义 JavaScript 或 Python 逻辑。我们用该节点映射并规范化(normalize)子域名枚举阶段采集的数据,把多个工具的输出合并成结构化格式,以便可靠存储与下游分析:
图 4.10 —— 映射采集数据(Figure 4.10 – Mapping the collected data)
尽管本项目的核心理念是尽量减少代码使用,但在某些时刻,少量逻辑片段能显著提升清晰度并降低工作流复杂度。本例中,Code4 节点在把子域名枚举结果写入 MongoDB 之前,扮演了关键的“定型”角色。相较于用多个 Set、Merge、Edit 节点来操作结构,这段简短的 JavaScript 函数把 aiodnsbrute、assetfinder、subfinder 与 shuffledns 的所有输出整合为一个组织良好的 JSON 对象,并确保每条记录包含 _id、时间戳(createdAt、updatedAt),以及一个嵌套的 toolResults 对象,方便后续追踪与查询。它让工作流保持干净、可读、高效,同时不背离 no-code 的精神。
下面是 Code 节点的逻辑:在持久化之前,把 aiodnsbrute、assetfinder、subfinder 与 shuffledns 的输出规范化并合并为一个统一文档:
return [
{
json: {
_id: $('Merge1').first().json._id.$oid,
domain: $('Merge1').first().json.domain,
createdAt: $('Merge1').first().json.createdAt,
updatedAt: new Date().toISOString(),
toolResults: {
aiodnsbrute: {
output: $('cat-aiodnsbrute1').first().json.stdout
},
assetfinder: {
output: $('cat-assetfinder1').first().json.stdout
},
subfinder: {
output: $('cat-subfinder1').first().json.stdout
},
shuffledns: {
output: $('cat-shuffledns1').first().json.stdout
}
}
}
}
];
在从各工具收集完子域名枚举结果后,我们希望以结构化、可持久化的方式保存这些数据。MongoDB3 节点负责把这些信息更新或插入到 MongoDB 数据库中。
MongoDB 节点的关键字段包括 Operation、Collection、Update Key 与 Fields。我们先给出高层概览,然后在图 4.11 后做更详细解释:
- Operation: 定义对 MongoDB 执行的动作类型,例如创建新记录、更新现有记录、删除数据或检索文档。
- Collection: MongoDB 存储文档的逻辑容器,类似关系型数据库中的表;该字段指定操作作用于哪个数据集。
- Update Key: 更新操作时用于唯一匹配文档的字段;MongoDB 用该 key 定位目标记录,并只对该记录应用更改。
- Fields: 定义将被写入或更新的字段,用于精确控制数据模型,确保一致性并避免不必要或重复存储。
图 4.11 —— MongoDB3 节点(Figure 4.11 – The MongoDB3 node)
我们使用预配置的 MongoDB credential(例如 MongoDB account Packt)安全连接数据库。
下面分解这些字段如何填充,并说明各配置决策背后的设计考量:
-
Operation – Update: 该操作检查是否存在
_id匹配的文档;若存在则更新现有记录。当启用 Upsert 时,如果找不到匹配记录,会自动创建新文档。这确保工作流能同时安全处理“首次发现”与“后续更新”,无需单独写 insert/update 逻辑,既避免重复,又维护连续历史记录。 -
Collection – domains: 数据存入
domainscollection,其中每个文档代表一个唯一 domain 及其相关侦察结果。 -
Update Key – _id: 使用
_id字段定位更新目标。该 ID 由 Code4 节点传入,确保精确命中记录。 -
Fields: 我们选择以下字段,以支持持续、可重复的子域名枚举,同时避免随时间产生重复 domain 记录:
createdAt:首次识别该 domain 的时间戳updatedAt:本次更新时间(反映最新扫描)toolResults:包含所有子域名枚举工具输出的 JSON 对象(例如 aiodnsbrute、subfinder、assetfinder、shuffledns)
由于枚举工作流可能对同一 domain 反复运行,createdAt 让我们保留该 domain 首次被发现的时间,从而建立清晰的历史基线;updatedAt 则记录该条目最后一次被修改的时间,使我们能追踪后续运行中是否发现了新子域名,或刷新了已有侦察数据。
toolResults 字段会在每次执行时被有意地存储并更新,因为每轮枚举都可能产出新的或不同的结果。持久化这些原始输出,使得域名记录能被增量富集,并让 agentic 系统能够基于时间维度去推理变化、关联与工具特定发现,而不是把每次运行当成孤立事件。
启用 Upsert 正是为了支持这种模型:它确保一个 domain 只被创建一次,之后在未来运行中持续更新,从而避免重复记录,同时让数据库随着新的侦察数据出现而持续演进。
这一设置让我们能够构建一个丰富、可查询的 domains/subdomains 历史记录:包括哪些工具发现了它们、以及发现时间。这样的数据在 agentic 工作流中价值极高:它可以支持 AI 驱动推理,帮助识别随时间变化的趋势,并确保即便资产暂时不活跃,我们也仍然可见。
在完成多工具的子域名枚举阶段后,我们现在拥有了一份坚实的潜在资产清单,其中一些可能当前仍活跃,另一些可能已废弃或处于休眠状态。无论当前状态如何,这些条目都很有价值,并已被保存在数据库中,用于长期情报与历史追踪。
识别存活域名(Identifying live domains)
我们 agentic 工作流接下来的逻辑步骤,是确定这些子域名里哪些当前仍然存活(live)。为此,我们使用 dnsx 工具,它允许我们以编程方式解析每个子域名,并用额外元数据对结果进行富集,例如关联 IP 地址、是否存在 CDN、以及 ASN 信息。这是一种轻量但有效的方法,能够在不依赖重型基础设施的情况下,以规模化方式验证子域名的可达性。
下面是工作流中用于解析子域名并识别存活资产的 dnsx 节点配置:
图 4.12 —— dnsx 节点(Figure 4.12 – The dnsx node)
我们在 SSH 节点中使用如下命令运行 dnsx,并把存活子域名结果捕获到结构化的 JSON 文件中:
echo "{{ $json.subdomains.join('\n') }}" | /home/kali/go/bin/dnsx -all -cdn -asn -silent -j -o dnsx-{{ $('Merge1').item.json.domain }}.json
这条命令会动态读取子域名列表,将其通过管道传给 dnsx,并执行多项检查,包括 DNS 解析、CDN 与 ASN 检测,然后把结构化输出保存到一个以目标域名命名的文件中。该输出稍后会在 ASM 流水线中用于进一步分析、存储与可视化。
完成这一步后,我们就拥有了一个清晰、可行动的子域名子集:它们当前存活且可达,为后续阶段——例如端口扫描、技术指纹识别与服务枚举——奠定基础。
在 MongoDB 中更新存活子域记录(Updating live subdomain records on MongoDB)
在用 dnsx 成功识别出存活子域名之后,必须把这些富集后的 DNS 信息持久化回我们的中心 MongoDB 数据库。为与前面沿用的架构保持一致,我们在 n8n 工作流中使用 Code 节点 + MongoDB 节点的组合来完成这一点。
dnsx 的输出包含了有价值的 DNS 记录,例如每个存活子域名的 TTL、resolver,以及 A、AAAA、CNAME、MX、NS、TXT、SOA 等记录。这些细节能提供更深层的 DNS 配置可见性,对后续指纹识别、风险分析或历史追踪都可能至关重要。
在这一步中,Code 节点用于把 dnsx 输出按我们在其他工具结果中使用的同一格式进行结构化:将数据嵌套在该域名条目的 toolResults 对象之下。随后 MongoDB 节点执行更新操作:要么插入新的 DNS 数据,要么用最新发现富集已有记录。
如下图所示,这种方式确保 MongoDB 中每个域名记录都会在 ASM 工作流执行过程中持续更新并不断富集,最终形成一个全面且可查询的知识库,既支持实时分析,也支持回溯分析:
图 4.13 —— MongoDB 记录(Figure 4.13 – MongoDB records)
资产发现阶段完成后,我们将不再停留在“识别有哪些东西存在”,而开始聚焦“这些资产到底是什么”。此时,工作流从发现与验证转向更深层分析:存活子域名成为系统化枚举与指纹识别的候选对象。这标志着从表层可见性到上下文情报的明确跃迁,为后续阶段中有意义的安全评估奠定基础。
阶段 2 —— 枚举与指纹识别(Stage 2 – Enumeration Fingerprinting)
在识别出哪些子域名存活且有响应之后,我们的工作流进入一个关键阶段:枚举与指纹识别。子域名发现只能提供表层可见性,而正是这一阶段真正揭示了我们正在面对的资产的身份与性质。在传统渗透测试或红队行动中,这往往意味着熟练分析师从侦察切换到目标画像(target profiling)。在我们的 agentic 自动化中,我们希望模拟同样的复杂度:系统化地挖掘使用了哪些技术、暴露了哪些端口、哪些服务可达,以及在不触发告警的前提下能提取哪些元数据。
这一阶段尤其重要,因为它为我们早期收集到的“浅而广”的数据增加深度。我们不再只是知道一个子域名存在且存活,而是开始提取细节:它是否运行 NGINX 或 Apache,是否通过 HTTP/2 提供内容,是否暴露了管理入口,以及 TLS 配置是否泄露了基础设施背后的信息。这些信息不仅利于后续定向测试,也会成为智能体推理层的输入:它之后可以据此判断哪些资产更有价值、可能更脆弱,或更可能超出范围。某种意义上,这个阶段就是下游模块(如漏洞扫描器、技术栈分析器或 exploit 生成器)的地基。
指纹识别过程被分为两条并行链路:HTTP 服务发现与端口扫描。在 HTTP 发现链路中,我们的目标是扫描存活子域名,识别正在运行的 Web 服务并分析 HTTP 响应行为。使用 httpx 与 get-title 等工具,我们可以获取 server headers、状态码、技术信息与 HTML 标题。这些看似简单的属性其实信息量很大:例如响应可能暴露 WordPress、某个特定 CDN 供应商,或一个自定义 403 页面暗示了访问控制策略。我们通过 n8n 的 SSH 节点在 recon 服务器上直接运行这些工具,保持一切都在 no-code 生态内完成,并用 Code 节点解析输出。处理完成后,这些结果会被存入 MongoDB 中相应的子域条目之下,确保数据在整个工作流中的连续性。
在第二条链路——端口扫描——我们进一步深入基础设施。通过执行 TCP 扫描(通常使用 nmap 等工具),我们希望识别所有对外暴露的端口以及其背后运行的服务。这对发现“意外暴露”尤其有用,例如开发用 SSH 服务、遗忘的 FTP 实例,甚至不安全的管理接口。在我们的工作流中,端口扫描逻辑与 HTTP 扫描流水线保持一致:拉取存活子域名、用远程 SSH 执行工具扫描、解析结果、再把一切写回数据库。这种模块化不仅让系统易扩展,也让它高度可追溯:每个工具、每条结果、每个时间戳都会被记录,从而提供组织攻击面随时间演进的历史视角。
此外,这一阶段采集的所有数据都会集中存入 MongoDB,使得后续阶段的查询、富集与推理变得无缝。例如,后续某个智能体可以查询“所有运行过期 Apache 版本的资产”,或按 CDN 供应商对各子域分组以发现异常。以结构化、可检索格式存储数据,也让外部系统(例如 dashboard 或 SIEM)可以消费并可视化这些洞察。随着多次运行累积数据,它不再只是一个快照,而会变成攻击面的时间序列图谱,对检测漂移、配置错误或策略违规很有价值。
最终,枚举与指纹识别阶段把你的自动化从被动发现转为主动理解。它为智能体提供“情报层”,让它不只是爬行,而是真正侦察。当以这种结构与精度构建时,它为可靠、持续且可规模化的攻击面管理奠定基础,其效果足以媲美人类红队员手工产出的成果:
图 4.14 —— 枚举指纹识别(Figure 4.14 – Enumeration Fingerprint)
作为枚举与指纹识别流程的一部分,我们首先检查对外暴露的 HTTP 服务,因为 Web 响应往往能提供最清晰的指标,揭示底层技术、配置与访问边界。
HTTP 服务发现(HTTP Service Discovery)
枚举与指纹识别阶段的第一条链路聚焦于 HTTP 服务发现。目标是分析存活子域名在 HTTP 与 HTTPS 下的响应方式,并提取关于底层 Web 栈的关键指标。通过检查响应头、状态码、支持的协议与页面元数据,智能体开始推断使用了哪些技术,以及每个资产从外部视角表现出的行为:
图 4.15 —— HTTP 服务发现(Figure 4.15 – HTTP Service Discovery)
在这个工作流中,我们执行一个由父级智能体工作流控制的“工具专用子工作流”。这种设计促进模块化,让每个工具聚焦一个明确任务,而不与更广的编排逻辑紧耦合。该子工作流的第一个节点名为 Domain1,目的简单但关键:接收智能体传入的目标 domain。这样工具就能在有范围(scoped)、具上下文意识的数据上运行,符合 agentic 架构的原则:每个节点/工作流都执行一个有意图、目标驱动的动作。
接收到 domain 后,下一步是获取所有相关子域名。为此,MongoDB1 节点连接中心数据库,查询 domains collection,取回在早期侦察阶段发现并存储的子域名。这个历史数据集确保工具基于干净且可靠的输入集运行,避免不必要地重复前面的 recon 步骤。
但原始数据库结果并不总能直接喂给命令行工具,这就是 Code 节点登场的地方。它负责把从 MongoDB 拉取的子域列表转换为工具可接受的格式字符串或数组——通常是以换行分隔的列表,这正是 httpx 等工具期望的输入形式。没有这一步,数据与工具之间的集成会很脆弱或容易失败。
完成数据准备后,Code 节点的输出会传给 httpx 节点——这是 HTTP 服务发现的核心。httpx 扫描每个子域名上的存活 HTTP 服务,抓取 HTTP 状态码、重定向行为、内容长度、TLS 证书信息、页面标题、server headers 与技术指纹。这些信息对理解技术栈与访问模式非常宝贵,也为下游智能体提供决策所需情报——例如是否要扫描 WordPress 插件或测试过期服务器版本。
这里展示了 httpx 命令的使用方式:
图 4.16 —— httpx 节点(Figure 4.16 – The httpx node)
如工作流所示,httpx 节点获得的数据不仅被采集,还会被有条理地结构化并存储。为了把每条 HTTP 指纹数据与数据库中的对应子域关联起来,我们使用 Loop Over Items 节点,它遍历每一条 httpx 结果,确保更新按“每个子域名”粒度执行。
在循环之后,下游节点(MongoDB2、Code2、MongoDB)构成更新机制的核心:首先,MongoDB2 节点在数据库中定位对应的子域记录;然后 Code2 节点对 httpx 的 raw 输出进行重排与格式化,确保符合我们的数据模型且仅保留相关字段;最后,末尾 MongoDB 节点把结构化数据写回数据库的正确子域条目下。
这种精确的逐项更新方式,让我们可以用最新的 HTTP 元数据不断富集资产清单,使 AI 智能体或人类分析师更容易在后续做出智能决策。同时,它在叠加实时枚举洞察的同时,也保留了历史子域扫描上下文。
下面是 Code2 节点中使用的代码:
return {
json: {
_id: $input.first().json._id ,
"domain": "",
"subdomain": "",
"dns": {
"ttl": $input.first().json.dns?.ttl || 0,
"resolver": $input.first().json.dns?.resolver || [],
"a": $input.first().json.dns?.a || [],
"aaaa": $input.first().json.dns?.aaaa || [],
"cname": $input.first().json.dns?.cname || [],
"mx": $input.first().json.dns?.mx || [],
"ns": $input.first().json.dns?.ns || [],
"txt": $input.first().json.dns?.txt|| [],
"soa": $input.first().json.dns?.soa || [],
"all": $input.first().json.dns?.all || []
},
"http": {
"hash": {
"body_md5": $('Loop Over Items').first().json.hash?.body_md5 || "",
"header_md5": $('Loop Over Items').first().json.hash?.header_md5 || ""
},
"cdn_name": $('Loop Over Items').first().json?.cdn_name || "",
"cdn_type": $('Loop Over Items').first().json?.cdn_type || "",
"port": $('Loop Over Items').first().json.port || 0,
"url": $('Loop Over Items').first().json.url || "",
"scheme": $('Loop Over Items').first().json.scheme || "",
"webserver": $('Loop Over Items').first().json.webserver || "",
"content_type": $('Loop Over Items').first().json.content_type || "",
"method": $('Loop Over Items').first().json.method || "",
"host": $('Loop Over Items').first().json.host || "",
"path": $('Loop Over Items').first().json.path || "",
"time": $('Loop Over Items').first().json.time || "",
"a": $('Loop Over Items').first().json.a || [],
"cname": $('Loop Over Items').first().json.cname || [],
"tech": $('Loop Over Items').first().json.tech || [],
"words": $('Loop Over Items').first().json.words || 0,
"lines": $('Loop Over Items').first().json.lines || 0,
"status_code": $('Loop Over Items').first().json.status_code || 0,
"content_length": $('Loop Over Items').first().json.content_length || 0,
"failed": $('Loop Over Items').first().json.failed || false,
"screenshot_bytes": "",
"headless_body": "",
"stored_response_path": $('Loop Over Items').first().json.stored_response_path || "",
"screenshot_path": $('Loop Over Items').first().json.screenshot_path || "",
"screenshot_path_rel": $('Loop Over Items').first().json.screenshot_path_rel || ""
},
"tls": {
"host": $('Loop Over Items').first().json.tls?.host || "",
"port": $('Loop Over Items').first().json.tls?.port || "",
"probe_status": $('Loop Over Items').first().json.tls?.probe_status || true,
"tls_version": $('Loop Over Items').first().json.tls?.tls_version || "",
"cipher": $('Loop Over Items').first().json.hash?.header_md5 || "",
"not_after": $('Loop Over Items').first().json.tls?.not_after || "",
"not_before": $('Loop Over Items').first().json.tls?.not_before || "",
"subject_dn": $('Loop Over Items').first().json.tls?.subject_dn || "",
"subject_cn": $('Loop Over Items').first().json.tls?.subject_cn || "",
"subject_org": $('Loop Over Items').first().json.tls?.subject_org || [],
"subject_an": $('Loop Over Items').first().json.tls?.subject_an || [],
"serial": $('Loop Over Items').first().json.tls?.serial || "",
"issuer_dn": $('Loop Over Items').first().json.tls?.issuer_dn || "",
"issuer_cn": $('Loop Over Items').first().json.tls?.issuer_cn || "",
"issuer_org": $('Loop Over Items').first().json.tls?.issuer_org || [],
"fingerprint_hash": {
"md5": $('Loop Over Items').first().json.tls?.fingerprint_hash.md5 || "",
"sha1": $('Loop Over Items').first().json.tls?.fingerprint_hash.sha1 || "",
"sha256": $('Loop Over Items').first().json.tls?.fingerprint_hash.sha256 || ""
},
"wildcard_certificate": true,
"tls_connection": $('Loop Over Items').first().json.tls?.tls_connection || "",
"sni": $('Loop Over Items').first().json.tls?.sni || ""
}
}
};
乍看之下,Code2 节点里的代码对不熟悉“no-code 环境中的 JavaScript”的读者可能显得又长又复杂。但它本质只是一个结构化转换:把 httpx 返回的每条结果重塑成适合写入数据库的格式。快速扫一遍就能看到重复模式:每个字段都被一致地抽取并映射。正如前面提到的,这段逻辑完全可以用多个专用 n8n 节点来复刻,但我们选择用单个 Code 节点让整体工作流更紧凑、更易维护、也更容易导航,尤其当处理项数量增长时。
端口扫描(Port Scanning)
在这一阶段,我们的数据库已经包含每个子域名的 HTTP 服务探测详情。但并不是每个存活主机都会暴露 HTTP 服务;有些主机可能运行其他协议,或只在 DNS 层面“看起来存活”。为了覆盖这些情况,我们使用强大的 nmap 工具。
图 4.17 —— 端口扫描(Figure 4.17 – Port scanning)
在这个例子里,我们选择将扫描保持轻量,只针对 80 与 443 端口,但 nmap 支持非常丰富的配置:你可以扫描更广的端口范围、识别运行服务,甚至执行基础漏洞检测。扫描完成后,nmap 会以 XML 格式输出。为了把这份数据集成到 MongoDB 数据库中,我们用 xml2js 解析器把 XML 转成 JSON。之后,我们把结果格式化以匹配数据模型,并通过 MongoDB 节点更新对应的子域记录,确保即便是非 HTTP 但存活或有价值的服务,也能在 ASM 系统中被准确追踪。
阶段 3 —— 漏洞检测(Stage 3 – Vulnerability Detection)
现在我们已经为目标域名完成了全面的子域名发现与指纹识别,是时候进入漏洞扫描阶段了。此时,在工作流层面我们已经清楚:哪些子域名是存活的,哪些子域名承载了 Web 应用。这个示例里,我选择使用 Nuclei——一个强大且可扩展的开源扫描器,能够识别广泛的 Web 漏洞类型。当然,这一步高度可定制;你可以按你的环境需求或评估范围集成多个工具,无论是开源还是商业方案都可以。把这些工具直接接入现有的 agentic 工作流,你就能以很小的代价完成自动化漏洞扫描,并确保发现结果能与此前的侦察结果在上下文上绑定。
下面的工作流展示了漏洞扫描如何被集成进 agentic 流水线:使用之前已枚举的资产作为结构化输入,从而实现具备上下文的漏洞检测:
图 4.18 —— 漏洞检测(Figure 4.18 – Vulnerability detection)
与之前的工作流一样,我们在这一阶段先通过 Domain1 节点从 AI 智能体获取必要输入。该 domain 值被传给 MongoDB1 节点,用于查询数据库并拉取所有相关子域数据。随后我们使用一个 Code 节点提取并准备这些子域关联的 URL 列表。接着通过 Loop Over Items 节点逐条遍历这些 URL,将它们一条条送入 Nuclei 进行漏洞扫描。扫描完成后,结果会进入另一个 Code 节点,以便重排为符合数据库结构的格式。最后,我们用 MongoDB 节点把识别到的漏洞更新回对应的子域记录中。
这种无缝集成确保每一个漏洞都能在上下文上被映射到正确资产,从而维持整个侦察与扫描流水线的完整性与可追溯性。
你可以在这里看到 nuclei 节点所使用的命令:
图 4.19 —— nuclei 节点(Figure 4.19 – The nuclei node)
在这一阶段,我们已经准备好把每个子域的 Nuclei 漏洞扫描结果持久化。为了在数据库中保持干净且结构化的 schema,我们把这些扫描结果组织在每个子域记录内一个专用的 nuclei 数组之下。这确保所有漏洞发现以一致且易查询的格式被封装起来。
为实现这一点,我们使用一个专用 Code 节点,把 Nuclei 的原始扫描输出转换并映射到目标数据库 schema。该节点负责把每条漏洞发现与对应的子域记录对齐,并为结构化存储做好准备。下图展示了 Code3 节点中如何实现该映射:
图 4.20 —— Code3 节点(Figure 4.20 – The Code3 node)
当单条 Nuclei 扫描结果被 Code3 规范化并富集后,工作流进入由 Code4 负责的持久化阶段。Code3 的职责是把单条漏洞发现塑造成一个干净、自包含的 JSON 对象;而 Code4 则聚焦把该对象整合进更大的数据模型。具体来说,Code4 会取 Code3 产出的结构化输出,并将其追加到 MongoDB 中对应子域文档的既有 nuclei 数组里。它不是覆盖旧结果、也不是创建碎片化记录,而是确保每次新的扫描发现都被增量地加入该子域的历史扫描数据中。这样的设计使得 Nuclei 在时间维度上的多次执行可以被汇总在同一个子域实体之下,从而保留扫描历史,并支持纵向分析、复测工作流,以及跨发现的关联分析。
从效果上说,Code4 就是把“短暂的扫描输出”与“数据库中持久、可查询的安全情报”连接起来的桥梁。
图 4.21 —— Code4 节点(Figure 4.21 – The Code4 node)
这种模块化方法为“随时间存储多次扫描结果”提供了灵活性,支持历史对比、复测追踪,甚至在下游与 dashboard 或告警系统集成。它也支持多扫描器并行运行的场景:多个漏洞扫描器都可以向同一个 MongoDB 后端贡献结构化数据。
阶段 4 —— 数据库查询智能体(Stage 4 – Database Query Agent)
这个工作流表示数据库查询逻辑:用于检查某个 domain 是否已经存在于 MongoDB collection 中,并根据查询结果执行更新或插入操作。我们逐步拆解一下发生了什么,让你既理解逻辑也理解其背后的动机。
在自动化流水线里,我们需要避免冗余条目并确保 MongoDB 资产清单的数据完整性。为此,你设计的工作流会在尝试任何插入或更新操作之前,先检查该 domain 是否已经存在于数据库中。这一逻辑至关重要,尤其是在工具与智能体会对相同目标反复运行的情况下。
下面的工作流图展示了 n8n 中实现的、由决策驱动的查询与更新逻辑:
图 4.22 —— 数据库查询工作流(Figure 4.22 – Database query workflow)
工作流从 When Executed by Another Workflow 节点开始,这表明该例程是模块化架构的一部分。下一个节点 Domain1 注入 domain 值,通常来自前一个流程或某个智能体任务。
- MongoDB9 节点对 MongoDB collection 执行 Find 操作,搜索与给定 domain 关联的现有记录。
- 数据库查询结果被送到 If1 节点,这是一个决策点,用于检查是否找到了任何文档(即该 domain 是否已存在于 DB)。
- 流程继续到 Found2 节点,在这里准备更新数据:可能包含刷新后的扫描结果、元数据或新发现资产。
- 随后 MongoDB-Update1 节点对匹配到的 domain 记录执行更新操作。
在下面截图中,你可以看到 ASM 智能体的结构,以及直接集成进其中的工具:
图 4.23 —— ASM 智能体聊天(Figure 4.23 – ASM agent chat)
在这个阶段,架构的设计相对直接:每个工具都在单一智能体的监督下运行。不过,这个设计只是起点;该结构完全可以被显著增强,以支持更高级、更加模块化的能力。
例如,智能体内的每个工具本身都可以被封装成一个专用的子智能体(sub-agent)。这些子智能体不仅能带来更好的模块化,还能让攻击面枚举过程中的角色与职责被更明确地分配。进一步地,这些智能体还可以被配置为使用不同的 LLM 后端,从而让系统能够在成本效率与性能之间根据上下文动态优化:上下文窗口更小、token 使用更低的模型可以被分配给简单解析或执行类任务。
反过来,当任务变得棘手、需要更强推理能力时,你就会希望使用更大的模型。
不要把这套配置当作“教条”。它是一个很扎实的起点,你应该去 hack、去改、去增强。你可以基于本章介绍的技术继续构建这些工作流,或者随着用例与环境演进,引入更高级的基于智能体的设计来进一步扩展。
总结(Summary)
在本章中,我们构建了一个真正的 ASM 引擎,让 AI 智能体坐在安全工具的“驾驶位”上。通过把这些工具组织为相互连接的工作流,我们展示了如何将开源技术组合起来,以实现持续的资产发现、枚举与漏洞扫描。
我们还实现了持久化数据存储与带条件的数据库逻辑,从而维持一个一致、可重复的 ASM 过程。结果是:AI 智能体现在可以通过一次简单的对话交互,为一个新域名启动完整的攻击面分析,为可规模化、可扩展的自治安全工作流奠定基础。
在下一章,我们将处理一个真实世界的合规挑战:PCI-DSS 分段测试。你将学习如何构建一个 agentic 工作流,自动验证你的网络分段是否被正确隔离,从而节省数小时的手工验证,并降低审计“意外翻车”的风险。