GuillaumeRoss/fleebag
GitHub: GuillaumeRoss/fleebag
将开源密钥扫描器 bagel 打包为 macOS PKG,结合 Fleet/osquery 实现开发者设备上的自动化密钥检测与集中化合规管理。
Stars: 3 | Forks: 0
# fleebag
通过 macOS PKG 将 [bagel](https://github.com/boostsecurityio/bagel) 部署到开发者 Mac 上,并在 [Fleet](https://github.com/fleetdm/fleet) 中展示密钥扫描结果。
 
## 概述
**fleebag** 将 [bagel](https://github.com/boostsecurityio/bagel) 开源密钥扫描器打包到 macOS 安装程序 (`.pkg`) 中,通过 LaunchAgent 在开发者笔记本电脑上自动运行。扫描结果以 JSON 格式写入,并由 Fleet 的 osquery agent 进行查询,让您可以在整个设备队列中查看检测到的密钥并执行合规策略——所有这些都无需手动操作每台机器。
```
flowchart TD
GH["boostsecurityio/bagel\nGitHub releases"] -->|fetched at build| BUILD["build-pkg.sh"]
BUILD -->|pkgbuild| PKG["fleebag-VERSION.pkg"]
PKG -->|MDM / Fleet software / manual install| MAC
subgraph MAC["Developer Mac"]
LA["LaunchAgent\nzip.recyclebin.fleebag\nevery 4 h + at login"] --> SCAN["fleebag-scan\n/usr/local/libexec/fleebag-scan"]
CFG["/etc/fleebag/bagel.yaml"] --> SCAN
BAGEL["/usr/local/bin/bagel"] --> SCAN
SCAN -->|atomic write| RESULTS["~/Library/Logs/fleebag/results.json"]
end
subgraph FLEET["Fleet"]
OQ["osquery agent\nparse_json virtual table"] -->|reads| RESULTS
OQ --> FINDINGS["bagel_findings.sql\nlive query"]
OQ --> POLICY["fleebag policy\ncompliance check"]
end
```
- **bagel** ([boostsecurityio/bagel](https://github.com/boostsecurityio/bagel)) — 开源工作站密钥扫描器
- **Fleet** ([fleetdm/fleet](https://github.com/fleetdm/fleet)) — 基于 osquery 的设备管理和策略平台
## 前置条件
- macOS 12 (Monterey) 或更高版本
- Xcode Command Line Tools — 提供 `pkgbuild`:
xcode-select --install
- `python3` — macOS 预装
- `curl` — macOS 预装
- 可访问 `api.github.com` 和 `github.com` 的网络连接(用于获取最新的 bagel 版本)
- 已部署 fleetd agent 的 Fleet 实例(查询所需的 `parse_json` 虚拟表必需)
- 可选:`osqueryi` 用于本地查询测试 (`brew install osquery`)
## 构建 PKG
在仓库根目录下运行:
```
bash scripts/build-pkg.sh
```
该脚本将:
1. 检测您 Mac 的架构(`arm64` 或 `x86_64`)
2. 从 GitHub API 获取最新的 bagel 版本元数据
3. 下载并解压特定架构的 `bagel_Darwin_.tar.gz` 压缩包
4. 运行 `pkgbuild` 组装安装程序
**bagel 二进制文件不存储在此仓库中** — 它始终在构建时从最新的 GitHub 发布版本中获取。
预期输出:
```
build/fleebag-.pkg
```
### 分发 PKG
`.pkg` 可以通过以下方式分发:
- **MDM**(例如 Jamf、Mosyle、Kandji)— 上传并划定作用范围到您的开发者群体
- **Fleet 软件管理** — 通过 Fleet 内置的软件部署上传(Settings → Software)
- **手动安装** — `sudo installer -pkg build/fleebag-.pkg -target /`
### 签名 bagel 二进制文件
从上游 GitHub 发布版本下载的 `bagel` 二进制文件仅带有临时的链接器签名(`flags=adhoc`、`Identifier=a.out`、`TeamIdentifier=not set`)。这意味着:
- 该二进制文件没有 macOS 可以验证的稳定代码签名身份。
- PPPC 描述文件的 `CodeRequirement` 只能表示为绑定到该确切构建版本的 `cdhash` — 每次更新 bagel 时它都会失效。
- 某些 MDM 和 endpoint 安全工具会将临时签名的二进制文件视为未经验证的第三方代码。
**推荐的做法**是在打包之前,使用您自己的 **Developer ID Application** 证书对 bagel 二进制文件进行重新签名。这为其提供了一个稳定的、锚定证书的身份,并且在版本更新后依然有效(只要您使用相同的团队证书):
```
# 列出你 keychain 中的 Developer ID Application identities
security find-identity -v -p codesigning | grep "Developer ID Application"
# 重新对 binary 进行签名(将 TEAMID 和 org name 替换为你自己的)
codesign --force --options runtime \
--sign "Developer ID Application: Your Name (TEAMID)" \
pkg/payload/usr/local/bin/bagel
# 验证
codesign -dv --verbose=4 pkg/payload/usr/local/bin/bagel
```
重新签名后,派生出您将粘贴到 PPPC 描述文件中的 `CodeRequirement`:
```
codesign -dr - pkg/payload/usr/local/bin/bagel 2>&1 | grep 'designated =>'
```
正确签名的二进制文件的示例输出:
```
designated => identifier "bagel" and anchor apple generic and
certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and
certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and
certificate leaf[subject.OU] = "XXXXXXXXXX"
```
只要您使用相同的 Developer ID 证书对每个新版本进行重新签名,此字符串在未来的 bagel 版本中就是稳定的。将其作为 `CodeRequirement` 值粘贴到 `profiles/fleebag-full-disk-access.mobileconfig` 中。有关详细信息,请参阅[准备描述文件](#preparing-the-profile)。
### 签名 PKG
由 `build-pkg.sh` 生成的 PKG 是未签名的。macOS Gatekeeper 会阻止标准用户或对未签名的 PKG 发出警告,并且某些 MDM 供应商会在部署前验证 PKG 签名。签名**在此仓库中未实现自动化**,因为它需要 Apple Developer ID Installer 证书,该证书特定于您组织的 Apple Developer 账户。
如果您拥有 Developer ID 证书,请在分发之前使用 `productsign` 对构建的 PKG 进行签名:
```
# 列出可用的签名身份以找到你的 Developer ID Installer certificate
security find-identity -v -p basic | grep "Developer ID Installer"
# 对 PKG 进行签名
productsign \
--sign "Developer ID Installer: Your Name (TEAMID)" \
build/fleebag-.pkg \
build/fleebag--signed.pkg
```
签名后,验证签名:
```
pkgutil --check-signature build/fleebag--signed.pkg
```
**公证**(对于在 macOS 13+ 上向 MDM 之外的标准用户分发是必需的)是签名后的一个独立步骤 — 使用 `notarytool` 将已签名的 PKG 提交给 Apple 的公证服务,然后装订票据:
```
xcrun notarytool submit build/fleebag--signed.pkg \
--apple-id "you@example.com" \
--team-id "TEAMID" \
--password "@keychain:AC_PASSWORD" \
--wait
xcrun stapler staple build/fleebag--signed.pkg
```
对于 MDM 部署(Jamf、Mosyle、Kandji、Fleet 软件管理),签名就足够了 — 不需要公证,因为 MDM 会绕过 Gatekeeper 隔离。
## 安装说明
PKG 将安装以下文件:
| 路径 | 描述 |
|---|---|
| `/usr/local/bin/bagel` | bagel 扫描器二进制文件 |
| `/usr/local/libexec/fleebag-scan` | 处理日志记录和原子输出的包装脚本 |
| `/etc/fleebag/bagel.yaml` | Bagel 配置(探测器、隐私控制、输出选项) |
| `/Library/LaunchAgents/zip.recyclebin.fleebag.plist` | LaunchAgent — 每 4 小时及登录时运行 |
postinstall 脚本会设置正确的权限,并在安装后立即为当前用户会话引导启动 LaunchAgent。
**扫描结果**将写入:
```
~/Library/Logs/fleebag/results.json
```
Mac 上的每个用户在其主目录下都有各自的结果文件。
### 手动触发扫描
要立即运行扫描而无需等待 4 小时的间隔:
```
launchctl kickstart -k gui/$(id -u)/zip.recyclebin.fleebag
```
### 检查 LaunchAgent 状态
```
launchctl print gui/$(id -u)/zip.recyclebin.fleebag
```
## 完全磁盘访问权限
macOS TCC(透明度、同意和控制)限制了对敏感位置的读取 — `~/Desktop`、`~/Documents`、`~/Downloads`、`~/Library`、shell 历史记录文件、SSH 密钥、云凭证文件等。如果没有完全磁盘访问权限,bagel 将在需要扫描的大多数位置静默地收到权限被拒绝的错误,从而导致结果不完整或为空。
完全磁盘访问权限必须通过 MDM 配置描述文件(PPPC payload)授予。**它不能由用户为后台 LaunchAgent 进程以交互方式授予。**
示例描述文件位于 `profiles/fleebag-full-disk-access.mobileconfig`。
### 准备描述文件
该描述文件需要两个特定于您环境的值:
**1. UUID** — 生成两个唯一的标识符:
```
uuidgen # run twice; paste results into PAYLOAD-UUID-1 and PAYLOAD-UUID-2
```
**2. CodeRequirement** — bagel 二进制文件的代码签名要求。
您在此处使用的值取决于 bagel 的签名方式:
- **如果您使用自己的 Developer ID Application 证书对 bagel 进行了重新签名**(推荐 — 参见[签名 bagel 二进制文件](#signing-the-bagel-binary)):在安装了重新签名的 bagel 的 Mac 上运行以下命令,并将 `designated => ` 之后的所有内容粘贴到描述文件中。此值在 bagel 版本更新中保持稳定。
codesign -dr - /usr/local/bin/bagel 2>&1 | grep 'designated =>'
示例输出:
designated => identifier "bagel" and anchor apple generic and
certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and
certificate leaf[subject.OU] = "XXXXXXXXXX"
- **如果您直接使用上游临时签名的二进制文件**(仅用于概念验证):唯一有效的 `CodeRequirement` 是二进制文件的 `cdhash`,它随着每次 bagel 发布版本而更改。使用以下命令获取它:
codesign -dr - /usr/local/bin/bagel 2>&1
# 寻找:# designated => cdhash H""
# 使用:cdhash H""
每次安装新版本的 bagel 时,您都必须在描述文件中更新此值 — 并通过 MDM 重新部署。
### 部署描述文件
使用您的 MDM 签名证书对描述文件进行签名(如果您的 MDM 供应商要求),然后上传并将其作用范围划定到与 fleebag PKG 相同的设备群体。必须在 PKG **之前或同时**部署描述文件 — 在 FDA 激活之前,bagel 将无法扫描受保护的路径。
## Fleet 查询
`queries/` 中的两个 SQL 文件已准备好粘贴到 Fleet 中。
### 查询结果 (`queries/bagel_findings.sql`)
**目的:**实时查询 — 返回每台受管 Mac 中的每个密钥检测结果,包括严重性、规则 ID、受影响文件路径以及上次扫描的时间戳。
**在 Fleet 中添加的方法:**Settings → Queries → New query → 粘贴以下 SQL。
```
SELECT
REPLACE(REPLACE(pj.path, '/Library/Logs/fleebag/results.json', ''), '/Users/', '') AS username,
MAX(CASE WHEN pj.key = 'severity' THEN pj.value END) AS severity,
MAX(CASE WHEN pj.key = 'id' THEN pj.value END) AS rule_id,
MAX(CASE WHEN pj.key = 'path' THEN pj.value END) AS file_path,
MAX(CASE WHEN pj.key = 'line' THEN pj.value END) AS line_number,
datetime(f.mtime, 'unixepoch') AS last_scan
FROM parse_json pj
LEFT JOIN file f ON f.path = pj.path
WHERE pj.path LIKE '/Users/%/Library/Logs/fleebag/results.json'
AND pj.parent LIKE 'findings/%'
GROUP BY pj.path, pj.parent
ORDER BY
CASE MAX(CASE WHEN pj.key = 'severity' THEN pj.value END)
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
ELSE 5
END,
pj.path;
```
**输出列:**
| 列 | 描述 |
|---|---|
| `username` | 其结果文件包含该检测结果的 macOS 简短用户名 |
| `severity` | 小写严重性:`critical`、`high`、`medium` 或 `low` |
| `rule_id` | 机器可读的规则名称(例如 `git-ssl-verify-disabled`) |
| `file_path` | 受影响文件或配置的位置(对于某些规则可能为 NULL) |
| `line_number` | 行号(始终为 NULL — 不在 bagel 的 schema 中;保留供将来使用) |
| `last_scan` | 结果文件最后写入时间的 ISO 8601 时间戳 |
### 策略查询 (`queries/bagel_policy.sql`)
**目的:**自动化策略 — 评估设备是否合规(扫描是最新的,未检测到关键密钥)。
**在 Fleet 中添加的方法:**Policies → Add policy → 粘贴以下 SQL。
**Fleet 策略语义:**返回 ≥ 1 行 = **PASS**;返回 0 行 = **FAIL**。
```
WITH local_users AS (
SELECT
username,
directory || '/Library/Logs/fleebag/results.json' AS results_path
FROM users
WHERE uid >= 500
AND directory LIKE '/Users/%'
),
valid_users AS (
SELECT lu.username
FROM local_users lu
JOIN file f
ON f.path = lu.results_path
WHERE
(strftime('%s', 'now') - f.mtime) < 604800
AND NOT EXISTS (
SELECT 1
FROM parse_json pj
WHERE pj.path = lu.results_path
AND pj.key = 'severity'
AND pj.parent LIKE 'findings/%'
AND pj.value = 'critical'
)
)
SELECT
'pass' AS result,
vu.username AS username
FROM valid_users vu
WHERE
(SELECT COUNT(*) FROM valid_users) = (SELECT COUNT(*) FROM local_users)
AND (SELECT COUNT(*) FROM local_users) > 0
LIMIT 1;
```
**通过/失败逻辑:**
| 结果 | 条件 |
|---|---|
| **PASS** | `results.json` 存在,在过去 7 天内写入,并且对于设备上的每个本地用户都不包含 `critical` 严重性的检测结果 |
| **FAIL** | `results.json` 缺失或过期(超过 7 天),或者对于任何本地用户包含一个或多个 `critical` 严重性的检测结果 |
**推荐的 Fleet 修复措施:**
- 通过 Fleet 的内置电子邮件/Slack 通知通知设备所有者
- 使用 Fleet 的自动化规则在您的问题追踪系统中创建工单
- 对于持续失败的情况,通过 MDM 或 Fleet 软件管理重新划定 PKG 的部署范围
## 配置
安装的配置文件是 `/etc/fleebag/bagel.yaml`。它控制运行哪些探测器、隐私设置和输出选项。
### 启用或禁用探测器
在构建 PKG 之前编辑 `pkg/payload/etc/fleebag/bagel.yaml`。在任何探测器下设置 `enabled: false` 以跳过它:
```
probes:
git:
enabled: true
ssh:
enabled: true
npm:
enabled: true
env:
enabled: true
shell_history:
enabled: false # disable if shell history scanning is not desired
cloud:
enabled: true
jetbrains:
enabled: true
gh:
enabled: true
ai_credentials:
enabled: true
ai_chats:
enabled: false # disable if AI chat history scanning is not desired
```
### 隐私控制
要抑制特定路径中的检测结果(例如,密码管理器数据库):
```
privacy:
redact_paths:
- "~/.password-store/**"
exclude_env_prefixes:
- "MY_INTERNAL_"
```
### 调整扫描间隔
LaunchAgent 默认每 4 小时运行一次(`StartInterval: 14400`)。要更改此设置,请在构建 PKG 之前编辑 `pkg/payload/Library/LaunchAgents/zip.recyclebin.fleebag.plist`:
```
StartInterval
14400
```
### 更改输出路径
结果文件路径硬编码在 `pkg/payload/usr/local/libexec/fleebag-scan`(`LOG_DIR` 变量)中。如果您更改它,请更新 `queries/bagel_findings.sql` 和 `queries/bagel_policy.sql` 以使其匹配 — 两个查询都使用相同的模式从用户的主目录派生结果路径。
## 测试
测试套件针对四个测试夹具场景验证 osquery 查询逻辑,而无需 Fleet 实例。
### 前置条件
- `python3`(macOS 标准)
- 用于冒烟测试的 `osqueryi`(可选):`brew install osquery`
### 运行测试
```
./tests/update_timestamps.sh && ./tests/run_tests.sh
```
必须首先运行 `update_timestamps.sh` — 它将测试夹具文件的修改时间设置为模拟最新(当前)和过期(8 天前)的扫描。
### 测试夹具场景
| 测试夹具 | 存在的检测结果 | 策略结果 |
|---|---|---|
| `no_critical_fresh.json` | 是(高/中/低) | **PASS** — 最新扫描,无关键检测结果 |
| `no_critical_stale.json` | 是(高/中/低) | **FAIL** — 扫描已过期(> 7 天) |
| `critical_fresh.json` | 是(包含关键) | **FAIL — 存在关键检测结果 |
| `critical_stale.json` | 是(包含关键) | **FAIL** — 已过期并且有关键检测结果 |
### 预期输出
```
=== Results: 8/8 passed ===
All 8 assertions passed.
```
## 许可证
请参阅 [LICENSE](LICENSE)。
标签:Cutter, Fleet, osquery, 安全助手, 特权提升, 终端设备管理, 自动化部署, 运维安全, 逆向工具