mcpField
D
dnr-hunt

dnr-hunt

github.com/anthropics安全7.7k229 次安装12 周前更新
代码仓库

关于 dnr-hunt

这是一个从应用日志和源码主动搜寻威胁的 Claude skill,不依赖预先告警。它要求先建立基线、记录假设与查询,并经源码确认且在本地触发 PoC 后才能判定漏洞已被利用。

主要功能

  1. 01先分析日志基线再提出假设
  2. 02用台账记录查询与判定
  3. 03对照应用源码核实可疑行为
  4. 04要求本地 PoC 才确认漏洞利用
  5. 05按阶段保存检查点以便续跑

适用场景

  1. 01调查大型日志集中的可疑活动
  2. 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/ 下的任何检查点,并新建运行目录。

硬性规则(这些就是此技能的核心——不得放宽)

  1. 仅凭日志证据永远无法确认漏洞。 日志能显示某件事曾被尝试以及应用如何响应——但无法证明代码存在漏洞。判定等级如下:
    • confirmed_exploited 需要同时满足以下三项:日志证据、在源代码中识别出缺陷(文件 + 函数),以及在本次会话中针对本地实例实际运行的 PoC。
    • suspected = 有日志证据(无论是否查阅过源代码),但未实际运行 PoC。
    • attempted_not_vulnerable = 观察到攻击流量,但源代码显示存在防御措施,且 PoC(如果运行过)被拦截。 一线团队曾见过验证器 Agent 通过读取可观测性日志并复述日志内容来“确认”发现——这是满足判定标准的最省事做法。PoC 要么能成功运行,要么不能;不能靠话术蒙混过关。
  2. 查询,不要通读。 日志文件的体积刻意设置得超出任何上下文窗口。绝不要用 Read 读取日志文件;绝不要用 cat 读取。先用 wc/du/head/tail 分析文件,再用 grep/rg/awk/sort/uniq 管道查询(有状态的操作则用 python3)。告警队列和错误日志通常较小——先检查大小;少于约 200 行时可以完整读取(禁止通读的规则针对数 MB 的日志文件,而非这些小文件)。
  3. 只提出建议,绝不执行。 不得封锁流量、禁用账户、轮换凭据或更改配置——即使是演示应用也不例外。响应措施应作为建议写入 /dnr-respond 的计划中。
  4. 仅限本地。 PoC 只能针对你启动的应用实例,发送到 127.0.0.1。无论日志中包含什么,都不得向远程主机发送流量。
  5. 每个数字都必须来自查询结果。 行数、请求数、时间跨度、受影响客户数——每一项都必须来自你在本次会话中运行的管道,并在台账中注明。针对日志运行的管道,以及查询应用自身确定性播种数据库的操作,都符合要求(需要按所有者关联查询时,运行目标的 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. 解析 $1。如果其中包含带有 kind: dnr 的 config.yaml,则读取配置中的 logs_dir、app_command、seed_command 和 port。 否则,将 $1 本身视为日志目录。预期语料已存在——目标 README 介绍了如何生成它。
  2. 清点已有内容:对于日志目录中的每个文件以及任何已提交的告警队列,运行 du -h、wc -l、head -2、tail -2。识别每种格式(combined access log、应用错误日志、JSONL 告警等)以及将在 awk 中使用的字段位置。
  3. 解析 --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 的基线开展;命中项可衍生出后续假设:

  1. 服务器错误。 哪些主体在哪些路由上触发了 5xx,负载是什么?与错误日志进行关联——错误日志中引用攻击者输入的应用错误是探测特征。
  2. 逐一判定所有告警。 对告警队列中的每一条记录:它标记的活动是否成功?由无害流量触发的告警应记为 ruled_out,并写明证据。
  3. 高频访问者异常值。 对于流量远高于基线的每个 IP:它在做什么?是扫描器噪声、爬虫,还是有明确意图的行为?
  4. 查询字符串中的注入语法。 编码引号(%27)、UNION/SELECT、注释标记(--、%23)、布尔条件对(AND 1=1 / AND 1=2)。解码发现的内容,并按时间顺序还原每个源 IP 的请求序列。
  5. 枚举模式。 某个会话或 IP 以机器般的节奏按顺序访问对象 ID(/orders/1、/orders/2,……)。与合法会话访问同一路由的方式进行比较。
  6. 身份验证异常。 账户从该账户从未使用过的 IP 登录;先失败后成功的登录序列;同一 IP 以某个账户通过身份验证,却以异常频率读取数据。
  7. 大小异常。 参数化路由上的请求,其响应大小达到该路由中位数的数倍——这些响应中包含什么?
  8. 低流量时段。 更仔细地检查流量最低时段的活动;低频、缓慢的攻击活动会隐藏在流量中,却会在凌晨 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)

对于每个经源代码确认的漏洞:

  1. 按目标配置在本地启动应用:其 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),并记录所用端口。

  2. 用 curl 执行最小化 PoC——使用能够证明漏洞存在的最小请求 (一行数据、一个外部对象),而不是重新执行攻击者的完整行动。

  3. 记录确切命令和响应中的相关片段。

  4. 如果存在,也执行负向 PoC:对你判定为不易受攻击的路由使用相同 技术,以证明请求会被拦截。

  5. 使用捕获的 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 的计划中以检测规则提案收尾—— 如果排查没有改进告警队列,就会永远需要手动重新执行。

评论

支持 Markdown