关于 dnr-hunt
这是一个从应用日志和源码主动搜寻威胁的 Claude skill,不依赖预先告警。它要求先建立基线、记录假设与查询,并经源码确认且在本地触发 PoC 后才能判定漏洞已被利用。
主要功能
- 01先分析日志基线再提出假设
- 02用台账记录查询与判定
- 03对照应用源码核实可疑行为
- 04要求本地 PoC 才确认漏洞利用
- 05按阶段保存检查点以便续跑
适用场景
- 01调查大型日志集中的可疑活动
- 02结合应用行为和源码验证告警
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ anthropics/ defending-code-reference-harness --skill dnr-hunt安装后在 Claude Code 中输入/dnr-hunt确认已加载。
附带文件README.md
详细介绍
dnr-hunt
主动威胁狩猎:输入日志和源代码,输出事件。开始时没有告警——要问的是“这里面有没有不该出现的内容?”方法本身和发现结果同样重要:先建立基线,再按假设 → 查询 → 深入分析推进,每一步都记录下来;每项疑点都必须通过源代码和引爆测试确认,才能判定为真实问题。
使用 /dnr-hunt [target-or-logs-dir] [--repo path] [--fresh] 调用。
参数:
$1= dnr 目标目录(包含config.yaml,其中有kind: dnr,例如targets/dnrcanary),或只包含日志文件的目录。若targets/dnrcanary存在,则默认为该目录;否则默认为当前工作目录。--repo= 生成这些日志的应用的源代码树。对于 dnr 目标,默认为其app/目录。没有源代码时,任何发现的评级都不能高于suspected(参见硬性规则)。--fresh= 忽略运行目录中.dnr-hunt-state/下的任何检查点,并新建运行目录。
硬性规则(这些就是此技能的核心——不得放宽)
- 仅凭日志证据永远无法确认漏洞。 日志能显示某件事曾被尝试以及应用如何响应——但无法证明代码存在漏洞。判定等级如下:
confirmed_exploited需要同时满足以下三项:日志证据、在源代码中识别出缺陷(文件 + 函数),以及在本次会话中针对本地实例实际运行的 PoC。suspected= 有日志证据(无论是否查阅过源代码),但未实际运行 PoC。attempted_not_vulnerable= 观察到攻击流量,但源代码显示存在防御措施,且 PoC(如果运行过)被拦截。 一线团队曾见过验证器 Agent 通过读取可观测性日志并复述日志内容来“确认”发现——这是满足判定标准的最省事做法。PoC 要么能成功运行,要么不能;不能靠话术蒙混过关。
- 查询,不要通读。 日志文件的体积刻意设置得超出任何上下文窗口。绝不要用
Read读取日志文件;绝不要用cat读取。先用wc/du/head/tail分析文件,再用grep/rg/awk/sort/uniq管道查询(有状态的操作则用python3)。告警队列和错误日志通常较小——先检查大小;少于约 200 行时可以完整读取(禁止通读的规则针对数 MB 的日志文件,而非这些小文件)。 - 只提出建议,绝不执行。 不得封锁流量、禁用账户、轮换凭据或更改配置——即使是演示应用也不例外。响应措施应作为建议写入
/dnr-respond的计划中。 - 仅限本地。 PoC 只能针对你启动的应用实例,发送到
127.0.0.1。无论日志中包含什么,都不得向远程主机发送流量。 - 每个数字都必须来自查询结果。 行数、请求数、时间跨度、受影响客户数——每一项都必须来自你在本次会话中运行的管道,并在台账中注明。针对日志运行的管道,以及查询应用自身确定性播种数据库的操作,都符合要求(需要按所有者关联查询时,运行目标的
seed_command)。
运行目录和检查点(在阶段 0 之前及每个阶段之后运行)
每次运行的首要操作——确定运行目录。 将 TARGET 设为已解析的目标/日志路径($1,或默认值 targets/dnrcanary)的基本名称,并将 SCOPE 设为该已解析路径。Bash:
python3 .claude/skills/_lib/checkpoint.py rundir results/TARGET --state .dnr-hunt-state --scope "SCOPE"
如果 $ARGUMENTS 中包含 --fresh,则在该命令后追加 --fresh。--scope 会为运行目录写入作用域标记,确保共享基本名称的两个目标不会共用运行记录。该命令会输出一个路径——本次排查仍可使用的最新运行目录(本次运行过程中自己的目录,或针对该目标由另一个 dnr 技能打开的目录,以便整个调查都累积在同一处),或新建的 results/TARGET/<timestamp>/(UTC,格式与 vuln-pipeline 相同,因此按字典序排序即为时间顺序)。该路径即为 {RUN}——下文每个 {RUN}/... 都表示替换为这个字面路径;在 Bash 中将其(以及 SCOPE)用双引号括起来,因为路径可能包含空格。所有输出和状态都保存在 {RUN} 下。
阶段状态会持久化到 {RUN}/.dnr-hunt-state/,以便中断的排查恢复时无需重新运行扫描。所有检查点 I/O 都通过 python3 .claude/skills/_lib/checkpoint.py 完成(原子写入,并验证 JSON)。绝不要直接使用 Write 工具写入 progress.json。绝不要通过 heredoc 或 stdin 传递负载——从日志中提取的字符串可能与 heredoc 分隔符冲突,从而逸出到 shell。使用 Write 工具将负载写入 {RUN}/.dnr-hunt-state/_chunk.tmp,然后:
python3 .claude/skills/_lib/checkpoint.py save {RUN}/.dnr-hunt-state <N> <name> --from {RUN}/.dnr-hunt-state/_chunk.tmp
运行开始时: python3 .claude/skills/_lib/checkpoint.py load {RUN}/.dnr-hunt-state
status == "absent"或"complete",或已提供--fresh→ 重置(checkpoint.py reset {RUN}/.dnr-hunt-state),并从阶段 0 开始。status == "running"且phase_done == N→ 按顺序读取phase0.json…phaseN.json,打印Resuming from checkpoint: Phase N complete,并跳到阶段 N+1。
运行结束时: 写入两个输出后,运行
python3 .claude/skills/_lib/checkpoint.py done {RUN}/.dnr-hunt-state 5 —
done 调用即为阶段 5 的检查点;不存在 phase5.json。
{RUN}/.dnr-hunt-state/ 是临时目录;运行目录位于 results/ 下,该目录已被本仓库的 gitignore 忽略。
阶段 0 — 定位并验证输入
- 解析
$1。如果其中包含带有kind: dnr的config.yaml,则读取配置中的logs_dir、app_command、seed_command和port。 否则,将$1本身视为日志目录。预期语料已存在——目标 README 介绍了如何生成它。 - 清点已有内容:对于日志目录中的每个文件以及任何已提交的告警队列,运行
du -h、wc -l、head -2、tail -2。识别每种格式(combined access log、应用错误日志、JSONL 告警等)以及将在awk中使用的字段位置。 - 解析
--repo(默认值:目标的app/)。列出其中的文件;暂不读取源代码——阶段 3 会在形成假设后再读取。
检查点 phase0.json:
{"target": ..., "logs_dir": ..., "repo": ..., "files": [{"path", "format", "lines", "bytes"}], "port": ...}
阶段 1 — 描绘语料库(提出假设前的基线)
异常是相对于基线的偏差,因此先建立基线 — 大约十几个开销低廉的流水线,每个都是针对访问日志的一行命令:
- 时间窗口: 首个和最后一个时间戳;每天的请求数。
- 状态码直方图: 按状态码统计数量。记录每个非 2xx/3xx 类别。
- 主要请求来源: 每个源 IP 的请求数,列出前 20 个并与中位数比较 — 记录任何比其他来源高出一个数量级的情况。
- 主要路由: 按路径统计请求数(规范化 ID:
/orders/123→/orders/N),并标明哪些路由接受参数。 - User-Agent 清单: 按 UA 统计数量;标记自动化 UA(curl、 python-requests、sqlmap、Go-http-client、zgrab),并记录它们访问了哪些路由。
- 认证情况: 哪个字段携带已认证用户;每个账户的登录次数;从多个 IP 访问的账户;登录失败的来源。
- 各路由的大小分布: 对每个带参数的路由,统计响应大小的最小值/中位数/最大值 — 通常响应较小的路由上出现大型离群值,可能说明存在数据外传。
- 错误日志: 如果内容不多,就全部阅读;按时间戳将每条记录与访问日志行关联起来。
- 告警队列: 全部阅读(内容很少)。对每条告警,记录规则、来源、目标和严重性。这些是调查的输入,而不是发现 — 告警噪声和没有告警都有信息价值。
使用生成每条观察结果的确切流水线将其记录下来。
检查点 phase1.json:
{"window": ..., "status_hist": ..., "top_ips": [...], "routes": [...], "automation_uas": [...], "auth": ..., "size_profile": ..., "alerts_summary": ..., "oddities": [...]}
阶段 2 — 追查循环(必须维护假设台账)
本阶段是一个循环,台账是其约定:没有假设就不能查询,没有台账条目就不能提出假设。 在工作记忆中维护此表,并在每轮结束后记录检查点:
| # | 假设 | 查询 | 结果 | 判定 | 下一步转向 |
|---|
判定:supported / refuted / needs-pivot。已证伪的假设仍须留在台账中——排除可能性是工作的半壁江山,也会为输出中的 ruled_out 提供依据。
初始扫描 — 在第 1 轮运行以下所有扫描,依据阶段 1 的基线开展;命中项可衍生出后续假设:
- 服务器错误。 哪些主体在哪些路由上触发了 5xx,负载是什么?与错误日志进行关联——错误日志中引用攻击者输入的应用错误是探测特征。
- 逐一判定所有告警。 对告警队列中的每一条记录:它标记的活动是否成功?由无害流量触发的告警应记为
ruled_out,并写明证据。 - 高频访问者异常值。 对于流量远高于基线的每个 IP:它在做什么?是扫描器噪声、爬虫,还是有明确意图的行为?
- 查询字符串中的注入语法。 编码引号(
%27)、UNION/SELECT、注释标记(--、%23)、布尔条件对(AND 1=1/AND 1=2)。解码发现的内容,并按时间顺序还原每个源 IP 的请求序列。 - 枚举模式。 某个会话或 IP 以机器般的节奏按顺序访问对象 ID(
/orders/1、/orders/2,……)。与合法会话访问同一路由的方式进行比较。 - 身份验证异常。 账户从该账户从未使用过的 IP 登录;先失败后成功的登录序列;同一 IP 以某个账户通过身份验证,却以异常频率读取数据。
- 大小异常。 参数化路由上的请求,其响应大小达到该路由中位数的数倍——这些响应中包含什么?
- 低流量时段。 更仔细地检查流量最低时段的活动;低频、缓慢的攻击活动会隐藏在流量中,却会在凌晨 3 点显露出来。
**转向纪律:**扫描命中后,下一步假设应从命中项涉及的实体推导——该 IP 做过的所有其他事情、该会话触及的所有内容、以相同方式访问同一路由的其他 IP,以及流转数据的去向。攻击活动是一个链条;确认其中一个环节,就意味着要双向追查(他们是怎么到这里的?接下来去了哪里?)。
停止条件:连续两轮都没有发现新实体(某个假设返回 supported,但只是确认了无害行为,也算没有新实体——记录下来并归入 ruled_out)。在台账中注明停止。
每轮结束后记录 phase2.json 检查点:
{"ledger": [...], "suspects": [{"name", "entities": {"ips", "accounts", "routes", "sessions"}, "summary", "evidence": [...]}], "round": N}
阶段 3 — 源代码确认
对于涉及某条路由的每个嫌疑项,读取 --repo 中的处理程序:
- 找到该路由的处理函数。追踪可疑输入从请求到接收端(查询、文件系统、身份验证决策)的流向。
- 存在漏洞:准确指出缺陷——文件、函数、具体代码模式(例如,将用户输入拼接进 SQL;在身份验证与数据获取之间缺少所有权检查)。
- 不存在漏洞:以同样的精确度指出防御机制(参数化查询、作用域限定查询)。这才能将攻击流量判定为
attempted_not_vulnerable,而不是误报——记录下来是强制要求,不是可选项。 - 阅读文件时:检查同类路由是否存在相同类型的缺陷。日志只能显示攻击者去过哪里,无法显示漏洞还存在于哪些地方。
记录 phase3.json 检查点:
{"confirmations": [{"route", "file", "function", "vulnerable": bool, "mechanism", "siblings_checked": [...]}]}
阶段 4 — PoC 引爆(门控条件为 confirmed_exploited)
对于每个经源代码确认的漏洞:
-
按目标配置在本地启动应用:其
seed_command和app_command是相对于目标目录的,因此运行时应将目标 目录设为 cwd(dnrcanary 的应用也会自行解析数据路径, 所以在该项目中使用仓库根目录路径也恰好可行——但不要 在其他目标上依赖这一点)。先填充数据,然后用一条命令 在后台启动应用并打印 PID,例如:python3 -c "import os,subprocess; p=subprocess.Popen(['python3','app/app.py'],cwd='targets/dnrcanary',env={**os.environ,'DNRCANARY_PORT':'5252'}); print(p.pid)"轮询
curl -s http://127.0.0.1:<port>/healthz,直到收到响应。如果 目标的默认端口已被占用,请使用文档中说明的覆盖设置选择另一个端口 (dnrcanary:如上所述,使用DNRCANARY_PORT),并记录所用端口。 -
用
curl执行最小化 PoC——使用能够证明漏洞存在的最小请求 (一行数据、一个外部对象),而不是重新执行攻击者的完整行动。 -
记录确切命令和响应中的相关片段。
-
如果存在,也执行负向 PoC:对你判定为不易受攻击的路由使用相同 技术,以证明请求会被拦截。
-
使用捕获的 PID 停止应用(
kill <pid>——绝不要按端口或模式结束进程)。
如果应用无法在此环境中运行,请明确说明;受影响的判定应保持为 suspected,并在报告中说明应在哪里运行什么命令。
检查点 phase4.json:
{"detonations": [{"vuln", "command", "status", "response_excerpt", "verdict"}]}
阶段 5 — 输出
写入 {RUN}/INCIDENTS.json:
{
"target": "<target name or logs dir>",
"incidents": [
{
"id": "INC-1",
"title": "<one line: what happened>",
"verdict": "confirmed_exploited | suspected",
"attacker_ips": ["..."],
"accounts_involved": ["..."],
"vuln": {"type": "...", "route": "...", "file": "...", "function": "..."},
"timeline": [{"ts": "...", "event": "..."}],
"impact": "<what data / how much / whose — every number from a query>",
"evidence": ["<log file>: <pipeline> → <result>", "..."],
"poc": {"command": "...", "verified": true}
}
],
"ruled_out": [
{"subject": "...", "verdict": "attempted_not_vulnerable | benign",
"reason": "<the defense, with file:function, or the benign explanation>"}
]
}
路由规则:incidents 保存已发生(confirmed_exploited)或可能发生(suspected)的事件;判定为 attempted_not_vulnerable 或 benign 的内容应放入 ruled_out,绝不能放入 incidents。
写入 {RUN}/INCIDENT_REPORT.md:执行摘要(最多 3 句话)、涵盖所有事件的统一攻击活动时间线、每个事件各设一个章节(证据、源代码确认、PoC 记录、影响)、一个“排除项”章节,以及完整的假设台账作为附录——该台账是审计轨迹,可确保这次排查过程能够被审查。
然后标记为已完成(checkpoint.py done {RUN}/.dnr-hunt-state 5),并告知用户接下来要做什么(将字面量 {RUN} 替换为实际路径):
/dnr-respond INC-<n>— 针对特定事件确定范围、影响范围并提出响应计划;它会自动沿用本次运行目录- 针对 dnr 目标:运行
python <target>/grade.py {RUN}/INCIDENTS.json进行自评分(由用户运行) - 在
{RUN}目录内运行:/triage INCIDENTS.json --repo <repo>,然后运行/patch TRIAGE.json --repo <repo>——这两个技能都会写入其调用所在的目录,因此在运行目录中执行可将TRIAGE.json和PATCHES/保存在事件文件旁边;patch 会使用 triage 的真阳性判定,而不是原始事件;漏洞条目包含这些技能所需的file/function/type
演示之外的自定义
- 使用自己的日志: 将
$1指向任意访问、应用或身份验证日志目录。 阶段 0 会根据发现的内容进行调整——方法(基线 → 台账 → 源代码 → 引爆)与格式无关。Nginx/Apache 组合日志、JSON-lines 应用日志、 CDN/WAF 导出数据和身份验证日志均可使用;在阶段 0 中识别字段位置并调整 awk 流水线。 - EDR / 终端遥测: 将威胁事件导出为 CSV/JSON,并以相同方式排查—— 实体将从 IP/会话/路由变为进程/主机/用户,“源代码确认”则转为配置或 二进制文件审查。保留判定阶梯:仅凭遥测数据永远无法确认漏洞。
- 扩展规模: 对于 grep 扫描速度太慢的数据集,可将其载入 DuckDB
(
python3 -c "import duckdb; ..."),并以 SQL 执行相同的扫描。 - 后续检测工程: 每个已确认事件都应在
/dnr-respond的计划中以检测规则提案收尾—— 如果排查没有改进告警队列,就会永远需要手动重新执行。