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) 中展示密钥扫描结果。 ![macOS](https://img.shields.io/badge/macOS-12%2B-blue) ![Fleet](https://img.shields.io/badge/Fleet-osquery-green) ## 概述 **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, 安全助手, 特权提升, 终端设备管理, 自动化部署, 运维安全, 逆向工具