VaseGod/traceback
GitHub: VaseGod/traceback
一款 AI agent 取证重建工具,将海量机器级动作日志转化为可查询、可审计的决策与因果关系时间线,用于事件响应与合规。
Stars: 0 | Forks: 0
# traceback
**一款 agent 取证重建工具。** 将数以万计的机器级 AI-agent 动作转化为一个可查询、可审计的决策与因果关系时间线 —— 用于事件响应与合规。
支持自托管。默认本地运行。确定性优先。
## 为什么会有这个项目
在 2026 年 7 月,一个自主 agent 在约 4.5 天的时间内跨越生产基础设施执行了约 17,600 次操作。没有哪个单独的漏洞利用是精巧的。防御者面临的问题是**体量**:那条唯一成功的路径被掩埋在成千上万条失败的路径中,手动重建是不切实际的,而且商用的 LLM 工具在事件发生期间拒绝了一部分取证工作,因为解码捕获的漏洞利用 payload 看起来与编写一个 payload 一模一样。
由此衍生出三个要求,它们是本仓库中每一个设计决策的支撑基石:
### 1. 默认完全本地化
除非明确启用,否则不允许任何网络流出。模型推理会发送到**用户提供的 OpenAI 兼容端点**(vLLM、llama.cpp、Ollama),以便操作员能够在他们自己的基础设施上运行未经过滤的自托管模型。
如果未设置 `TRACEBACK_ALLOW_EGRESS`,任何尝试连接非 localhost 主机的行为都会引发 `EgressBlocked` —— 这是一个硬性错误,而不是警告。该防护机制会解析 DNS 并检查生成的 IP,因此 `--model-url http://evil.example.com/v1` 以及一个解析到公网 IP 的主机名都会在检查中直接失败并拦截。在所有配置下,摄入、分析、报告和验证阶段都会发起**零**次网络调用。
### 2. 确定性优先,LLM 其次
每一个分析阶段都会在**零模型调用**的情况下产生完整且有用的结果。会话化、解码、机密清单梳理和 kill-chain 重建都是事件存储的纯函数。LLLM 是一个丰富层,用于在此之上添加叙述和假设。糟糕、缓慢或不可用的模型只会降低报告质量;它绝不会破坏 pipeline。`narrate.py` 会捕获它能产生的所有异常类,并回退到标记为 `[unenriched]` 的确定性模板文本。
### 3. 证据完整性
事件存储是仅可追加且通过哈希链链接的。每份报告都可以从存储中重现,并且可以通过 `traceback verify` 检测篡改。这是合规性产物(EU AI Act 第 12 条记录保存要求),因此出处比精巧程度更重要。
## 安装
```
uv sync
uv run traceback --help
# 参见 Decisions:在 macOS 上,editable-install 的 .pth 文件可能会隐藏
chflags nohidden .venv/lib/python3.*/site-packages/*.pth 2>/dev/null || true
```
要求 Python 3.11+。如果系统自带的解释器版本较旧,`uv` 会自动配置一个新的解释器。
## 快速开始
```
# 1. 生成一个合成事件(不需要真实数据)
uv run traceback synth --events 17600 --out ./incident/
# 2. 导入到基于 hash-chained 的 DuckDB 存储中
uv run traceback ingest './incident/*.jsonl' --adapter auto --store ./case.db
# 3. 确定性分析(无 model 调用)
uv run traceback analyze --store ./case.db --no-llm
# 3b. ……或者使用本地 model 进行叙述增强
uv run traceback analyze --store ./case.db \
--model-url http://localhost:8000/v1 --model glm-5.2
# 4. 渲染单文件报告
uv run traceback report --store ./case.db --out ./report.html
# 5. 证明证据链完好无损
uv run traceback verify --store ./case.db
# 6. 对存储执行 Ad-hoc SQL
uv run traceback query --store ./case.db "SELECT boundary, count(*) FROM events GROUP BY 1"
```
`traceback selftest --store ./case.db --truth ./incident/ground_truth.json` 会对已分析的存储进行评分,对比合成的
`ground_truth.json` 并打印验收表格。
## 架构
```
JSONL (heterogeneous)
│
▼
┌─────────────┐ adapters: generic_jsonl │ k8s_audit │ openai_agent_trace
│ ingest │ → CanonicalEvent (ts, actor, action, target, boundary,
└─────────────┘ outcome, payload, reasoning, raw)
│
▼
┌─────────────┐ DuckDB, single file, append-only
│ store │ SHA-256 hash chain: row_hash = H(prev_hash ‖ canonical(event))
└─────────────┘ head hash pinned in meta; verify() recomputes end-to-end
│
├──────────────── DETERMINISTIC ────────────────────────────────┐
▼ │
1. sessionize events → Operations (temporal gaps, session id, │
actor continuity, target prefix, boundary) │
2. decode recursive layered payload decoder → DecodeStep chain │
3. secrets credential inventory over decoded + raw, redacted │
4. killchain causal DAG over operations → successful path │
│ │
└──────────────── OPTIONAL ENRICHMENT ─────────────────────────┘
5. narrate (local OpenAI-compatible endpoint)
│
▼
┌─────────────┐
│ report │ one self-contained HTML file, no CDN, vanilla JS
└─────────────┘
```
### 规范化事件
每一个源都会被统一转换为同一种格式。未知字段会在 `raw` 中原样保留。
```
class CanonicalEvent(BaseModel):
event_id: str # deterministic hash of (source, source_id, ts)
ts: datetime # UTC, required
session_id: str | None # agent session / trajectory id
actor: str # identity that performed the action
actor_type: Literal["agent", "human", "service", "unknown"]
action: str # exec | read | write | auth | network | tool_call
target: str | None # resource touched
boundary: str | None # trust boundary: sandbox, cluster-a, vpn, ci, registry
outcome: Literal["success", "failure", "unknown"]
payload: str | None # raw payload if present
reasoning: str | None # agent's stated reasoning, if the source captured it
source: str # adapter name
raw: dict # untouched original record
```
`reasoning` 和 `boundary` 是核心产出。现有的 SIEM 捕获了*发生了什么*,但没有捕获*为什么发生*以及*跨越了哪个信任边界*。适配器会在源允许的情况下填充这两个字段:`openai_agent_trace` 从助手的思考过程和工具调用原理中提取推理;边界则首先从显式字段推导,然后从 cluster/namespace/host 上下文推导,最后从目标前缀映射推导。
### 五个分析阶段
| # | 阶段 | 输入 → 输出 | 确定性? |
|---|-------|----------------|----------------|
| 1 | `sessionize` | events → `Operation[]` | 是 |
| 2 | `decode` | payload → `DecodeStep[]` + 明文 | 是 |
| 3 | `secrets` | decoded + raw → `SecretFinding[]` | 是 |
| 4 | `killchain` | operations → 因果 DAG + 成功路径 | 是 |
| 5 | `narrate` | path ops → 散文叙述 | **否** — 可选,会降级 |
**1. sessionize —— 是操作,而不是事件。** 仅使用确定性信号将事件聚类为语义连贯的 `Operation`:时间间隔、共享的 `session_id`、共享的目标前缀、参与者连续性、边界转换。压缩比是核心指标 —— 17,600 个事件必须缩减到 200 个操作以下。
**2. decode —— 多层 payload 解码。** 攻击者会链接各种编码方式。一个递归解码器可以按任意顺序检测并解包,达到可配置的深度:base64/base64url、hex、gzip/zlib/bzip2、URL 编码、单字节和重复密钥 XOR(通过频率分析恢复密钥)、按索引重组的分块 payload,以及 JSON/YAML 嵌套。每个被剥离的层都会被记录为一个 `DecodeStep`,以便报告显示完整的解包链条。当输出不再像已知的编码时,解码就会停止。
**3. secrets —— 凭证清单梳理。** 扫描解码后的 payload 和事件目标,查找高熵字符串和已知的凭证格式(AWS 密钥、GitHub `ghp_`/`ghs_`、JWT、私钥标头、kubeconfig 块、`.env` 模式、bearer token、连接字符串、Slack/Stripe token)。每次发现都会报告其出现的位置、所属的边界、首次出现的时间戳,以及一个**脱敏指纹** —— 前 4 位 + 后 4 位 + SHA-256。完整的机密永远不会被写入报告、输出到 stdout 或分析表中。基准测试要求:恢复的有效信息必须明显多于对原始日志进行简单的 regex 扫描。两者的计数会并排显示,以便直观地看到差值。
**4. killchain —— 因果重建。** 构建在操作之上的有向图。出现以下情况时即添加边:
- 一个操作的输出 artifact 出现在后续操作的输入中 *(权重 3.0)*
- 在操作 A 中发现的凭证在操作 B 中被使用 *(权重 3.0)*
- 一个操作在先前边界的成功操作后不久跨越了信任边界,且具有参与者或会话连续性 *(权重 1.0)*
**成功路径** 是通过 `outcome == "success"` 操作的最大权重链条,这些操作将初始访问连接到所达到的最深边界,通过在按时间排序的 DAG 上进行动态规划(DP)来找到。失败的操作会保留在存储中,并被折叠为底噪摘要。
**5. narrate —— 可选的 LLM 层。** 对于成功路径上的每个操作,本地模型会用一段话解释 agent 试图完成的任务,以及为什么这一步紧接上一步,并提供一个总体的事件摘要。请求会被批量处理,受限于严格的 token 预算,并按操作哈希进行缓存,因此重新运行是免费的。模型不可达、JSON 格式错误、超时或输出为空 → 使用确定性模板文本,并在报告中标记 `[unenriched]`。
### 合成事件生成器
该工具具有自我验证功能:`synth/generate.py` 会生成一个真实的事件,以便在没有真实数据的情况下对 pipeline 进行评分。
- 默认在 4.5 个模拟日内生成 17,600 个事件
- ~97% 的噪音:失败的探测、良性的后台服务流量、死胡同式的枚举
- 一条隐藏的、包含 15-25 个操作的成功路径,跨越至少四个信任边界
(`sandbox → registry → cluster-a → vpn → ci`)
- 真实的时间戳抖动和机器速度的爆发
- 成功路径的 payload 被 2-4 层链接的编码包裹
- ~12 个植入的凭证,其中几个**只有**在解码后才能找到
- 基准事实被写入到附带的 `ground_truth.json` 中
## 验收标准
`tests/test_end_to_end.py` 是检验关口。它运行 generate → ingest → analyze → report 流程,并根据基准事实进行断言:
| # | 标准 | 阈值 |
|---|-----------|-----------|
| 1 | 成功路径召回率 (事件级别) | ≥ 0.85 |
| 2 | 成功路径精确率 (事件级别) | ≥ 0.80 |
| 3 | 恢复的植入凭证 | 12 个中的 ≥ 10 个 |
| 4 | 操作压缩比 | ≥ 50:1 |
| 5 | 完整 pipeline 实际运行时间,17,600 个事件,`--no-llm` | < 90 秒 |
| 6 | `verify` 在未篡改的存储上通过 | 为真 |
| 7 | `verify` 在修改单行数据后失败 | 为真 |
测试中的其他不变量:
- 解码器恢复 4 层链条,并为每一层记录一个 `DecodeStep`
- 机密扫描严格优于简单的原始日志 regex 基线
- 渲染的 HTML 中任何地方都不会出现完整的机密值
- 报告是一个不包含外部引用 (`http://`, `https://`, `//cdn`) 的单个文件
- 当未设置 `TRACEBACK_ALLOW_EGRESS` 时,egress guard 会对非 localhost 模型 URL 引发错误
精确率/召回率是在**事件**级别评分的,而不是操作级别:属于提取路径上操作的事件集合会与基准事实路径事件集合进行比较。操作级别的评分会因为聚类的存在而显得更好看,所以更严格的指标才是把关的标准。
## 报告
一个自包含的 HTML 文件。使用 Vanilla JS,内联 CSS,无 CDN,无构建步骤。
- 事件摘要标头:事件数、操作数、压缩比、跨越的边界、暴露的机密、实际时间跨度
- 水平时间线,突出显示成功路径;底噪折叠在切换开关之后
- 点击操作 → 面板显示构成事件、解码链条、捕获的推理、叙述(如果已丰富)
- 视觉上突出显示边界跨越标记 —— 这些是至关重要的时刻
- 脱敏的机密表
- 出处页脚:存储头哈希、事件计数、分析配置、工具版本、生成时间戳
专为首席信息安全官(CISO)在凌晨 2 点阅读而设计:信息密集、风格冷静、没有仪表盘边框、没有渐变色、没有表情符号。标识符使用等宽字体,正文使用比例字体。
## 决策
设计分歧均倾向于更简单的选项,并在此记录。
0. **导入的包名为 `traceback_forensics`,而不是 `traceback`。** CLI 命令仍然是 `traceback`,文件树在其他方面保持如上所述。一个名字真的叫 `traceback` 的包是无法导入的:由于该名称的标准库模块在 `sys.path` 中排在 site-packages 之前,导致 `import traceback.schema` 会引发 `ModuleNotFoundError: 'traceback' is not a package`,并且任何执行 `import traceback` 的依赖项都会获得(在路径冲突中)胜出的那一个。在重命名之前已验证。
1. **分析产物存放在第二个哈希链中,而不是事件链中。** `events` 是不可变的证据链。重新运行 `analyze` 不能使其失效,因此 operations / findings / kill chain 被存放到一个带有自己独立链条和 `run_id` 的 `artifacts` 表中。报告会引用两个头哈希。
2. **仅可追加是由 API 强制执行的,而不是由引擎。** DuckDB 允许 `UPDATE`。存储不暴露任何修改路径,而哈希链使得带外修改变得可检测 —— 这才是实际的安全属性。声称引擎级别的不可变性是不实的。
3. **在事件级别评分精确率/召回率**(见上文)—— 这是两种可用定义中更严格的一种。
4. **`--adapter auto` 按文件而不是按行进行嗅探。** 文件的第一个可解析记录将决定整个文件使用的适配器。混合格式的文件不在考虑范围内。
5. **XOR 恢复被限制在密钥长度 1-8**,通过 χ² 频率评分。更长的密钥需要取证 payload 很少提供的大量密文,而且无限制的搜索会在压缩数据中引入误报。
6. **解码器输出一个候选链条,而不是一个 lattice。** 在每一层,置信度最高的检测器胜出。探索所有排序的时间复杂度是指数级的,而边缘的恢复收益很小。
7. **存储是可重现的单位,而不是输入文件。** 报告引用存储头哈希。重新摄入相同的 JSONL 会产生完全相同的 `event_id` 和完全相同的链条,因此输入级别的可重现性自然就得到了保证。
8. **边界推断是静态映射加上源提示**,而不是学习得来的。操作员可以稍后使用 `--boundary-map` 进行覆盖;v0 发布内置映射。
9. **无 YAML 依赖。** 解码器的 "YAML 嵌套" 情况仅针对 JSON 子集进行处理,避免了为了极少数边缘情况而引入 `pyyaml` 依赖。
10. **叙述缓存是一个位于存储旁边的 JSON 文件 (`.narrate-cache.json`),而不是一张表,这样损坏的缓存可以被删除,而不会触及证据。
11. **XOR 是推断出来的,而不是检测出来的,所以它只作为备选方案运行。** 所有其他解码器都锚定在魔数、严格的字符集或解析器上。XOR 没有标头,因此将它与其他解码器一起运行会使其劫持本属于 base64 或 gzip 的层。只有在没有任何明确无误的匹配时才会尝试它。
12. **因果边的权重必须大于它所支付的跳跃成本。** 边界跨越的权重低于 `NODE_COST`,因此单纯的跨越本身永远无法扩展成功路径。在成功的操作之后不久,大量不相关的操作会在更深的边界中运行;如果仅凭情况就允许它们,路径就会随意漫游到随后发生的任何事情上。如果与 artifact 或凭证边配对,跨越就很容易通过审核。
13. **原始基线仅包含 regex,没有熵启发式。** 原始日志上的熵会标记语料库中的每一个编码块和标识符(在默认事件中有 7,593 个 "机密")。包含它会夸大衡量此阶段的指标数字,而不是真正衡量任何东西,所以基线就是凭证模式扫描真正能找到的东西。
14. **所有成功路径的事件都源自 agent trace。** 服务器端来源只提供噪音。真实的 agent 框架会为每个任务发射一个 trajectory id,因此路径的会话化在某种程度上已经交给我们处理了;k8s 和 gateway 来源根本不带 session id,这就是 gap/prefix/actor 聚类发挥作用的地方。
### 已知限制
* **对于短小、高熵的明文,重复密钥 XOR 并不总是可恢复的。**
一个 4-6 字节的密钥,应用于一个约 200 字节且一半是随机凭证材料的 payload 上,每个密钥位置只留下约 35 个样本,其中一半是均匀的噪音。在这种大小下,频率分析无能为力 —— 这是密文的固有属性,而不是实现上的缺陷。如果存在容器标头或严格的字母表,则可以精确恢复并验证密钥;否则,解码器会停止并报告部分链条,而不是盲目猜测。在默认事件中,这导致 12 个植入凭证中丢失了 2 个,它们都位于同一个 4 层分块 payload 内。
* **连续两层 XOR 等于一层。** 使用长度为 *a* 和 *b* 的密钥进行 XOR,等同于使用长度为 `lcm(a, b)` 的密钥进行单次 XOR;生成器不会连续发射它们,因为声称一个在产物中无法观察到的深度的基准事实等于什么也没衡量。
* **可编辑安装需要在此机器上执行 `chflags nohidden`。** `.pth` 文件落地时设置了 macOS 的 `UF_HIDDEN` 属性,而 CPython 的 `site` 会跳过隐藏的 `.pth` 文件,因此新同步的 venv 可能会出现已安装包但无法导入的情况。`conftest.py` 将 `src` 放在路径上,因此测试永远不会依赖它。
## 非目标
- 没有实时或流式摄入。仅限批处理。
- 没有检测或警报。这是事后取证。
- 没有多用户、身份验证或托管服务。
- 没有云模型提供商。
- 尚未集成 agent 框架。
## 安全姿态
- `traceback` 在 `ingest`、`analyze --no-llm`、`report`、`verify`、`query` 或 `synth` 期间不进行任何出站连接。
- 唯一的网络客户端是 `model/client.py`,并受 egress guard 检查控制。
- 摄入分析产物的时机密就会立即生成指纹;完整值仅保留在证据存储中,这也是它们原本所在的地方。
- 解码器是惰性的:它只解包编码,从不执行、评估或重建 payload 内容。
## License
MIT。
标签:AI智能体, 人工智能, 数字取证, 本地部署, 用户模式Hook绕过, 自动化脚本, 逆向工具