Perplexity 把内部用来守护搜索产品、Comet 浏览器和 Computer 智能体的安全工具开源了——Bumblebee,一个纯 Go 编写的只读清单收集器,零非标准库依赖,专为 macOS 和 Linux 开发者终端设计。在供应链投毒事件频发的当下,这种"只读扫描、零侵入"的思路值得每个团队认真看看。
供应链投毒的真正痛点
npm 上的恶意包、PyPI 的仿冒库、Docker Hub 的污染镜像——这些不是新闻,而是每周都在发生的事。传统的安全扫描往往聚焦在"已知漏洞匹配"上,但投毒攻击的逻辑完全不同:攻击者不是利用已有 CVE,而是主动往你的依赖里塞东西。
这意味着你需要的不只是"这个包有没有已知漏洞",而是"我的终端上到底装了什么、从哪来的、有没有不该出现的东西"。Bumblebee 的定位就在这里——它不做漏洞库匹配,而是做清单收集,把开发者终端上的软件资产原样记录下来,供后续审计比对。
只读收集器的设计逻辑
Bumblebee 最关键的设计决策是只读。它不修改任何文件、不安装任何东西、不写入任何系统目录,只是扫描并输出清单。这个选择带来几个直接好处:
- 零运行开销:没有后台进程、没有守护服务、没有定时任务,跑完即走
- 零信任前提:不需要 root 权限,不需要访问系统关键路径的写权限
- 零副作用风险:不会因为扫描动作本身引入新的安全问题
对于 Perplexity 这样同时运行搜索服务、浏览器和 AI 智能体的公司来说,开发者终端是所有代码的入口。一旦终端被投毒(比如恶意 CLI 工具、污染的包管理器插件),下游所有产品都会受影响。只读收集器的思路是:先搞清楚现状,再决定怎么处置。
零非标准库依赖意味着什么
Bumblebee 整个项目只依赖 Go 标准库,没有引入任何第三方包。这不仅是极简主义的审美选择,更是安全工具的自我保护:
- 扫描器本身不会成为供应链攻击的载体——如果你用了一个依赖丰富的扫描工具,攻击者完全可以通过污染扫描器的某个依赖来实现"用安全工具投毒"的讽刺场景
- 构建和分发极简——
go build一条命令出二进制,不需要依赖管理、版本锁定、缓存清理 - 审计成本低——任何人审查 Bumblebee 的依赖树,只需要看 Go 标准库,几分钟就能确认没有可疑引入
这种设计在安全工具领域应该成为默认标准:保护别人的工具,自己首先要是最难被攻破的。
实践:安装、运行与 CI 集成
Bumblebee 的使用方式非常直接。以下是从安装到集成 CI 的完整路径。
安装与本地运行
# 克隆仓库并构建(零第三方依赖,go build 直接出二进制)
git clone https://github.com/perplexity-ai/bumblebee.git
cd bumblebee
go build -o bumblebee .
# 在开发者终端上运行只读扫描
./bumblebee scan
# 输出清单到文件,供后续比对
./bumblebee scan --output manifest-$(date +%Y%m%d).json
构建过程不需要 go mod download 拉任何外部包,因为标准库随 Go 发行版自带。这意味着即使你的网络环境受限,或者你刻意在离线环境中构建,也不会有任何问题。
清单比对:发现异常依赖
Bumblebee 的核心价值不在单次扫描,而在持续比对。你可以这样做:
# 周一扫描,生成基准清单
./bumblebee scan --output baseline.json
# 周五扫描,生成当前清单
./bumblebee scan --output current.json
# 用 jq 快速比对差异(假设输出为 JSON 格式)
diff <(jq -S '.packages | sort_by(.name)' baseline.json) \
<(jq -S '.packages | sort_by(.name)' current.json)
任何本周新增的包、版本变化、来源变更都会在 diff 中暴露出来。如果出现了你不认识的包名或意料之外的版本升级,那就是需要追查的信号。
GitHub Actions 集成
把 Bumblebee 嵌入 CI 流程,让每次 PR 都自动扫描开发者环境的清单变化:
name: Supply Chain Manifest Check
on:
pull_request:
branches: [main]
jobs:
manifest-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Build Bumblebee
run: |
git clone https://github.com/perplexity-ai/bumblebee.git /tmp/bumblebee
cd /tmp/bumblebee
go build -o bumblebee .
- name: Scan and compare
run: |
# 下载仓库中保存的基准清单
BASELINE=manifests/baseline.json
if [ ! -f "$BASELINE" ]; then
echo "No baseline found, creating initial manifest"
/tmp/bumblebee/bumblebee scan --output "$BASELINE"
exit 0
fi
# 当前扫描
/tmp/bumblebee/bumblebee scan --output current.json
# 比对
CHANGES=$(diff <(jq -S '.packages | sort_by(.name)' "$BASELINE") \
<(jq -S '.packages | sort_by(.name)' current.json) || true)
if [ -n "$CHANGES" ]; then
echo "::warning::Manifest changes detected"
echo "$CHANGES"
# 可选:严格模式下 exit 1 阻断 PR
fi
这个 workflow 的关键设计:首次没有基准清单时自动创建,后续每次 PR 都与基准比对。检测到变化时输出 GitHub Actions warning,严格模式下可以直接 exit 1 阻断合并。
自定义只读收集器示例
如果你想在自己的项目里实现类似的只读清单收集逻辑,以下是一个最小化的 Go 示例,同样只依赖标准库:
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"time"
)
// PackageRecord 记录单个包的信息
type PackageRecord struct {
Name string `json:"name"`
Version string `json:"version"`
Source string `json:"source"`
Path string `json:"path,omitempty"`
}
// Manifest 清单结构
type Manifest struct {
Timestamp string `json:"timestamp"`
Platform string `json:"platform"`
Packages []PackageRecord `json:"packages"`
}
// 只读收集 Homebrew 包信息(macOS)
func collectBrewPackages() []PackageRecord {
cmd := exec.Command("brew", "list", "--versions")
output, err := cmd.Output()
if err != nil {
// brew 不存在则跳过,不报错——只读原则
return nil
}
var records []PackageRecord
for _, line := range strings.Split(string(output), "\n") {
if line == "" {
continue
}
parts := strings.Fields(line)
name := parts[0]
version := ""
if len(parts) > 1 {
version = parts[1]
}
records = append(records, PackageRecord{
Name: name,
Version: version,
Source: "homebrew",
})
}
return records
}
// 只读收集 pip 包信息
func collectPipPackages() []PackageRecord {
cmd := exec.Command("pip3", "list", "--format=json")
output, err := cmd.Output()
if err != nil {
return nil
}
var raw []struct {
Name string `json:"name"`
Version string `json:"version"`
}
if json.Unmarshal(output, &raw) != nil {
return nil
}
var records []PackageRecord
for _, p := range raw {
records = append(records, PackageRecord{
Name: p.Name,
Version: p.Version,
Source: "pip",
})
}
return records
}
func main() {
manifest := Manifest{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Platform: runtime.GOOS + "/" + runtime.GOARCH,
}
// 按平台收集,只调用只读命令
manifest.Packages = append(manifest.Packages, collectBrewPackages()...)
manifest.Packages = append(manifest.Packages, collectPipPackages()...)
data, _ := json.MarshalIndent(manifest, "", " ")
fmt.Println(string(data))
// 可选:写入文件
if len(os.Args) > 2 && os.Args[1] == "--output" {
os.WriteFile(os.Args[2], data, 0644)
}
}
这段代码的核心原则与 Bumblebee 一致:只调用只读命令(brew list、pip list),不修改任何状态,依赖不存在时静默跳过而非报错。你可以直接 go run main.go 运行,或扩展它来收集 npm、gem、cargo 等其他包管理器的信息。
采纳建议与边界认知
Bumblebee 解决的是"知道终端上有什么"的问题,不是"判断这些东西是否恶意"的问题。理解这个边界很重要:
适合做的事: - 定期扫描开发者终端,建立软件资产基准 - 在 CI 中自动比对清单变化,快速发现异常新增 - 在入职/换机流程中跑一次扫描,确认初始状态干净 - 配合 SBOM(Software Bill of Materials)流程,补上终端侧的盲区
不适合做的事: - 替代漏洞扫描工具(它不做 CVE 匹配) - 替代运行时安全监控(它只做静态清单收集) - 在 Windows 上使用(目前只支持 macOS 和 Linux)
采纳路径建议: 1. 先在核心开发者的终端上手动跑一周,确认输出格式和覆盖范围符合预期 2. 把基准清单提交到仓库,在 CI 中加入自动比对步骤 3. 逐步扩展到所有开发者终端,考虑用 Git hook 或定时 cron 自动化扫描 4. 当清单比对发现异常时,接入人工审计流程或自动告警通道
供应链安全没有银弹,但"知道自己有什么"是最便宜的起步。Bumblebee 用零依赖、只读、零运行开销的方式把这个起步成本压到了最低——一个二进制文件、一条命令、一份清单,然后你才有可能发现那些不该出现的东西。