内容
<p align="center">
<img src="assets/logo.svg" alt="bugwarden logo" width="180">
</p>
# bugwarden
**bugwarden** 是一个模型上下文协议(MCP)服务器,使用 Rust 编写,具有操作员控制的安全防护。它为 LLM 客户端暴露了一个 Bugzilla 实例,允许查询 bug、搜索、阅读评论和历史记录,以及(在允许的情况下)更新 bug。策略文件决定了模型对每个 bug 的允许操作,模型既看不到也不能更改该文件。
Bugzilla REST API 已经通过 API 密钥强制执行**用户**权限。它无法强制执行 AI 代理代表用户执行操作的**更严格**的权限集。bugwarden 位于两者之间:操作员编写一个小的 TOML 策略(“禁止的安全漏洞不可见”,“在安全产品上,代理只能读取摘要和留下评论”,“一周内的内容都不存在”),每个工具调用在触摸 Bugzilla 或返回数据之前都会根据该策略进行检查。
## 功能
- **完整的 Bugzilla 工具表面**:bug 详情、历史记录、评论、附件元数据和内容、快速搜索、评论/状态/字段/指派/CC/依赖更新、重复标记、服务器信息、快速搜索语法文档和 bug 摘要提示工具。
- **防护策略引擎**:按 bug 的 `allow` / `deny` / `restrict` 决策与产品、组件、组、关键词、状态、严重程度、优先级、白板和 bug 年龄相匹配 - 具有细粒度的 11 能力词汇用于 `restrict`。
- **无存在预言**:策略拒绝的 bug 与不存在的 bug 无区别。
- **静默搜索过滤**:拒绝的 bug 根本不会出现在搜索结果中;仅限于摘要的 bug 会被编辑。
- **最小年龄隔离区**:`min_bug_age_days` 使最近提交的 bug(最有可能包含尚未分类的敏感数据)不可见。
- **只读模式和工具禁用**:从 MCP 工具列表中完全删除写入工具 - 客户端永远不会看到它们,而是看到它们出错。
- **两种传输方式**:可流式的 HTTP(每个请求的 API 密钥头,适用于共享部署)和 stdio(通过桌面 MCP 客户端的子进程启动)。
- 单个静态二进制文件,异步贯穿始终(tokio + [rmcp](https://crates.io/crates/rmcp))。
## 安全模型
### 防护概念
防护策略在**启动时**从通过 `--policy`(或 `BUGWARDEN_POLICY`)传递的 TOML 文件加载。它位于操作员的文件系统上。MCP 客户端 - 即模型 - 没有工具来读取它,列出其规则或修改它。`mcp_server_info` 故意只公开粗略的事实:规则数量,默认操作,`min_bug_age_days`,服务器是否是只读的,以及哪些工具被禁用。规则名称和匹配标准永远不会被泄露。
每个使用 bug id 的工具首先获取 bug 的分类元数据(产品、组、关键词、创建时间等)并在**任何副作用发生或数据返回之前**评估策略。唯一的例外是 `bug_url`,它在本地计算 URL 字符串并且不联系任何人。
### 不变性
- **统一拒绝**。被拒绝的 bug 和不存在的 bug 产生完全相同的响应:`Bug {id} 无法通过此服务器访问`。任何文字或细节差异都不能用作机密 bug 的存在预言。
- **静默搜索过滤**。搜索结果通过策略进行后过滤;客户端永远不会被告知有多少结果被删除或发生了过滤。(服务器端调试日志确实为操作员记录了它。)
- **关闭失败**。如果分类获取失败,如果 bug 在响应中不存在,或者如果由于 bug 对象没有携带规则要求的字段而无法确定规则,则 bug 被视为被拒绝 - 永远不会被允许。
- **私人评论门**。仅当策略设置 `allow_private_comments = true` **且**单个调用选择加入 `include_private = true` 时,才返回私人评论。两者中有一个单独的不足。
- **自定义字段不能走私写入**。`update_bug_fields.custom_fields` 仅接受以 `cf_` 开头的键;其他任何内容(例如 `groups`、`cc`、`assigned_to`)在联系 Bugzilla 之前都会被拒绝。
- **API 密钥永远不会泄漏**。Bugzilla API 密钥永远不会被写入日志、错误消息或工具结果中;HTTP 错误会被净化,以便通过 URL 查询参数传递的密钥不会出现在错误文本中。
- **CLI 只能收紧**。`--read-only` OR 进入策略的只读标志;没有 CLI 开关可以放松策略禁止的操作。
### 故意省略和严格的默认值
- **没有 header-echo 工具**。传入的请求头 - 包括 API 密钥头 - 永远不会暴露给模型。
- **私人评论默认关闭**。默认策略具有 `allow_private_comments = false`,因此需要策略文件来启用它们。
- **`update_bug_fields` 自定义字段仅限于 `cf_*` 键**,如上所述。
## 安装 / 构建
```bash
git clone https://github.com/plusky/bugwarden
cd bugwarden
cargo build --release
# 二进制文件位于 target/release/bugwarden
```
存储库通过 `rust-toolchain.toml` 固定其 Rust 工具链;`cargo` 会自动拾取它(rustup 管理的安装)。如果您在构建时不使用固定版本,则可以使用任何最近的稳定 Rust 版本。
## 用法
> **注意:** 一些 Bugzilla 部署保护其交互式主机免受反机器人挑战,拒绝 API 客户端的身份验证请求。 如果工具因“响应正文不是有效的 JSON”而失败,请检查实例是否提供专用的 API 主机(例如 `apibugzilla.suse.com` 而不是 `bugzilla.suse.com`),并将 `--bugzilla-server` 指向该主机。
### HTTP 传输(默认)
服务器监听 `http://<host>:<port>/mcp`。每个客户端请求都在 HTTP 头中携带 Bugzilla API 密钥(默认头名称:`ApiKey`),因此一个服务器可以使用多个用户及其自己的密钥提供服务:
```bash
bugwarden \
--bugzilla-server https://bugzilla.opensuse.org \
--policy /etc/bugwarden/policy.toml \
--host 127.0.0.1 --port 8000
```
MCP 客户端配置(确切格式因客户端而异):
```json
{
"mcpServers": {
"bugzilla": {
"url": "http://127.0.0.1:8000/mcp",
"headers": {
"ApiKey": "您的 Bugzilla API 密钥"
}
}
}
}
```
可以通过 `--api-key-header` 配置头部名称。对于拒绝 `api_key` 查询参数并要求 `Authorization: Bearer` 的 Bugzilla 实例(例如 Red Hat Bugzilla),请添加 `--use-auth-header` - 这仅影响服务器到 Bugzilla 的身份验证,而不是面向客户端的头部。
### stdio 传输
适用于以子进程形式启动服务器并通过 stdin/stdout 进行通信的 MCP 客户端。这里没有每个请求的 HTTP 头,因此必须通过 `--api-key` 或 `BUGZILLA_API_KEY`(以 `BUGZILLA_API_KEY` 开头)提供 API 密钥:
```bash
BUGZILLA_API_KEY=您的_api_key \
bugwarden \
--bugzilla-server https://bugzilla.opensuse.org \
--transport stdio \
--policy /etc/bugwarden/policy.toml
```
MCP 客户端配置:
```json
{
"mcpServers": {
"bugzilla": {
"command": "/usr/local/bin/bugwarden",
"args": [
"--bugzilla-server", "https://bugzilla.opensuse.org",
"--transport", "stdio",
"--policy", "/etc/bugwarden/policy.toml"
],
"env": {
"BUGZILLA_API_KEY": "您的 Bugzilla API 密钥"
}
}
}
}
```
## CLI 参考
命令行参数优先于环境变量。
| 标志 | 环境变量 | 默认值 | 描述 |
|------|---------------------|---------|-------------|
| `--bugzilla-server <URL>` | `BUGZILLA_SERVER` | *必填* | Bugzilla 服务器的基准 URL(例如 `https://bugzilla.opensuse.org`) |
| `--transport <http\|stdio>` | `MCP_TRANSPORT` | `http` | MCP 传输。`stdio` 用于 MCP 客户端的子进程启动;`http` 在 `/mcp` 处暴露一个网络端点 |
| `--host <ADDRESS>` | `MCP_HOST` | `127.0.0.1` | 监听地址(仅限 http 传输) |
| `--port <PORT>` | `MCP_PORT` | `8000` | 监听端口(仅限 http 传输) |
| `--api-key-header <NAME>` | `MCP_API_KEY_HEADER` | `ApiKey` | 客户端在其中发送 Bugzilla API 密钥的 HTTP 头名称(仅限 http 传输) |
| `--api-key <KEY>` | `BUGZILLA_API_KEY` | — | Bugzilla API 密钥。**必填**,用于 `--transport stdio`;使用 `http` 时会被忽略,并带有警告(客户端按请求发送密钥) |
| `--use-auth-header` | — | `false` | 使用 `Authorization: Bearer <key>` 而不是 `api_key` 查询参数向 Bugzilla 进行身份验证 |
| `--read-only` | `MCP_READ_ONLY` | `false` | 禁用所有写入工具。仅限收紧:与策略的 `global.read_only` 进行 OR 操作;不能重新启用策略禁止的写入 |
| `--policy <PATH>` | `BUGWARDEN_POLICY` | — | 防护策略 TOML 的路径。没有它,将应用允许所有(私人评论关闭)的策略 |
## 策略文件参考
策略是严格的 TOML:**任何位置的未知键都是启动错误**,而没有能力的 `restrict` 规则、`allow`/`deny` 规则 *具有* 能力,或 `default_action = "restrict"`。在 Unix 上,如果策略文件是组或他人可写的,bugwarden 会在启动时记录警告。
一个完整、注释的示例位于 [`examples/policy.toml`](examples/policy.toml) 中。
### 顶级
| 键 | 类型 | 默认值 | 描述 |
|-----|------|---------|-------------|
| `default_action` | `"allow"` \| `"deny"` | `"allow"` | 应用于没有匹配规则的 bug。不能为 `"restrict"`(一个 catch-all `restrict` 规则表示这一点) |
### `[global]`
| 键 | 类型 | 默认值 | 描述 |
|-----|------|---------|-------------|
| `min_bug_age_days` | 整数 | `0`(禁用) | 创建时间少于 N 天的 bug **不可见** - 与不存在的 bug 完全一样,先于任何规则进行评估。创建时间缺失或无法解析的 bug 被拒绝(关闭失败) |
| `allow_private_comments` | 布尔值 | `false` | **所有** 私有内容的总开关:评论、附件元数据和附件下载。即使为 `true`,每个调用还必须传递 `include_private = true`。在附件下载中,*缺失* 的隐私标志被视为私有 |
| `read_only` | 布尔值 | `false` | 从每个授予中剥夺写入能力,并从工具列表中删除写入工具。 `--read-only` 标志与此处的 OR 操作 |
| `disabled_tools` | 字符串数组 | `[]` | 完全从工具列表中删除的工具名称 |
| `max_attachment_bytes` | 整数 | `2097152`(2 MiB) | `download_attachment` 可以返回的最大附件(解码大小)。`0` 删除上限。附件内容以 base64 嵌入在工具结果中,并进入模型的上下文 - 故意提升 |
### `[[rule]]`
规则**从上到下**进行评估;**第一个匹配的规则获胜**,后续规则被忽略。如果没有规则匹配,则应用 `default_action`。将最具体的规则(通常是最严格的规则)放在前面。
| 键 | 类型 | 默认值 | 描述 |
|-----|------|---------|-------------|
| `name` | 字符串 | *必填* | 规则标识符(仅限服务器端日志,不显示给客户端) |
| `description` | 字符串 | `""` | 操作员的自由格式文档 |
| `match` | 表 | `{}`(匹配每个 bug) | 匹配条件,见下文 |
| `action` | `"allow"` \| `"deny"` \| `"restrict"` | *必填* | `allow` 授予所有能力,`deny` 授予无,`restrict` 授予恰好 `capabilities` |
| `capabilities` | 能力字符串数组 | `[]` | 仅限 `action = "restrict"`,其中至少需要一个。必须为空/不存在于 `allow` 和 `deny` |
#### `match` 条件
一个匹配器中的所有条件都必须成立(**与**)。在单个列表中,任何元素都可以匹配(**或**)。空匹配器匹配每个 bug - 没有 `match` 的规则是一个 catch-all。要表示“条件 A **或** 条件 B”,请编写两个连续的规则。
| 键 | 类型 | 匹配对象 |
|-----|------|-----------------|
| `products` | glob 数组 | bug 的产品 |
| `components` | glob 数组 | bug 的任何组件 |
| `groups` | glob 数组 | bug 的任何组名 |
| `keywords` | glob 数组 | bug 的任何关键词 |
| `statuses` | glob 数组 | bug 的状态 |
| `severities` | glob 数组 | bug 的严重程度 |
| `priorities` | glob 数组 | bug 的优先级 |
| `whiteboard_contains` | 字符串数组 | 白板中的不区分大小写的子字符串搜索 |
| `summary_contains` | 字符串数组 | bug 的单行摘要中的不区分大小写的子字符串搜索 |
| `group_restricted` | 布尔值 | `true` 匹配只能通过至少一个 Bugzilla 组可读的 bug,`false` 匹配世界可读的 bug |
| `younger_than_days` | 整数 | 匹配创建时间在最近 N 天内的 bug |
#### 不可读的元数据
每个条件都需要一个字段,而 bug 对象可能不具备该字段 - 缺失、`null`、意外类型或包含解析器无法读取的元素的列表。这样的字段是 **未知**,而咨询它的规则是不可判定的:它既不成立也不失败。
bugwarden 通过**拒绝 bug** 来解决不可判定的规则,无论规则的动作如何。`deny` 规则拒绝,因为 bug 可能正是它被编写来捕获的内容。`allow` 或 `restrict` 规则也拒绝 - 它可能不会在无法检查的数据上授予访问权限,也不能简单地跳过,因为跳过会将 bug 交给后续规则或 `default_action`。因此,不可读的元数据永远不会为 bug 购买比可读元数据更多的访问权限。
这两种情况故意不执行。已经明确失败的条件优先于未知条件,因此被另一个条件排除的规则保持排除。并且,只有规则实际咨询的字段才重要 - 缺失的白板与不提及白板的规则无关。存在但为空(`""`、`[]`)的字段是知识,而不是无知,并正常匹配。
#### 通配符语法
通配符匹配整个值,不区分大小写。`*` 匹配任何(可能为空)子字符串;其他字符都是字面意义的。没有其他元字符。示例:`embargo*`、`*security*`、`SUSE *`。
#### 能力
存在十一项能力。`read` 意味着 `summary`;其他能力不意味着任何其他能力。
| 能力 | 类型 | 授予 |
|------------|------|--------|
| `read` | 读取 | 完整的 bug 详情(意味着 `summary`) |
| `summary` | 读取 | 红色的摘要视图(id、摘要、状态、解析、产品、组件、严重程度、优先级、创建时间/上次更改时间) |
| `comments` | 读取 | 读取评论(也需要 `summarize_bug`) |
| `history` | 读取 | 读取 bug 的更改历史记录 |
| `attachments` | 读取 | 列出附件元数据和下载附件内容 |
| `comment` | 写入 | 添加评论 |
| `status` | 写入 | 更改状态/解析、标记重复项 |
| `fields` | 写入 | 更改优先级、严重程度、解析、`cf_*` 自定义字段 |
| `assign` | 写入 | 更改指派人 |
| `cc` | 写入 | 修改 CC 列表 |
| `deps` | 写入 | 更改阻止/依赖于 |
当服务器为只读时(策略或 CLI),会从每个授予中剥夺六个写入能力,包括来自 `allow` 规则和默认操作的写入能力。
## 工具参考
| 工具 | 功能 | 所需能力 |
|------|--------------|---------------------|
| `bug_info` | 一组 bug id 的详细信息。对于每个 id:使用 `read` 授予完整的详细信息,使用 `summary` 授予编辑后的摘要,否则是一个统一的“不可访问”条目 | `read` / `summary` |
| `bug_history` | bug 的更改历史记录,可选仅限较新的条目 | `history` |
| `bug_comments` | bug 上的评论;私有评论仅限于私人评论门槛 | `comments` |
| `bugs_quicksearch` | Bugzilla [快速搜索](https://bugzilla.readthedocs.io/en/latest/using/finding.html#quicksearch);结果会被策略过滤(拒绝的会被删除,摘要仅限编辑) | 每个结果:`read` / `summary` |
| `summarize_bug` | 根据 bug 的公共评论生成摘要提示 | `comments` |
| `list_attachments` | 附件元数据(永远不会是附件内容) | `attachments` |
| `download_attachment` | 附件内容(光栅图像作为图像内容,其他作为 base64 blob 资源),上限为 `max_attachment_bytes`;私有附件需要双重选择加入,并且在下载时,*缺失* 的隐私标志被视为私有 | 附件的 `attachments` |
| `add_comment` | 向 bug 添加评论 | `comment` |
| `update_bug_status` | 更改状态/解析(CLOSED 需要解析) | `status` |
| `assign_bug` | 设置指派人 | `assign` |
| `update_bug_fields` | 更新优先级/严重程度/解析和 `cf_*` 自定义字段 | `fields` |
| `update_bug_dependencies` | 添加/删除阻止/依赖项 | `deps` |
| `add_cc_to_bug` | 将电子邮件添加到 CC 列表 | `cc` |
| `mark_as_duplicate` | 关闭 bug 作为另一个 bug 的 DUPLICATE | bug 的 `status` **和** 副本目标的至少 `summary` |
| `bug_url` | 在本地计算 `{server}/show_bug.cgi?id={id}` | 无(不联系任何人) |
| `bugzilla_server_info` | Bugzilla 版本、扩展、时区、时间、参数 | 无 |
| `quicksearch_syntax` | Bugzilla 的快速搜索语法文档(HTML) | 无 |
| `mcp_server_info` | bugwarden 版本、Bugzilla URL、传输、粗略策略摘要 | 无 |
## 路线图
- **OBS / openSUSE 打包**,通过 zypper 安装。
## 许可证
Apache License 2.0。详见 [LICENSE](LICENSE) 获取详细信息。