Bumblebee is a read-only inventory collector for package, extension, and developer-tool metadata on macOS, Linux, and Windows developer endpoints.
It answers a narrow supply-chain response question: when an advisory names a package, extension, or version, which developer machines show a match in their on-disk metadata right now?
SBOMs help answer what shipped, and EDR helps answer what ran or touched the network, but supply-chain response often needs a different view: messy local state across lockfiles, package-manager metadata, extension manifests, and supported developer-tool configs.
Bumblebee turns that scattered on-disk state into structured NDJSON component records and, when given an exposure catalog, flags exact matches for fast, read-only exposure checks when responders already know what they are looking for.
- Single static binary, Go 1.25+, one dependency.
- Three scan profiles (
baseline,project,deep) for different populations and cadences. - Reads only the lockfiles, package-manager install metadata,
extension manifests, and supported MCP JSON configs listed in
docs/inventory-sources.md. No package
manager execution (
npm ls,pip show,go list, ...) and no source-file reads. MCP host configs can carry environment values and credentials in theirenvblocks; Bumblebee parses these configs for the server inventory it needs but does not emit those values in its records.
| Family | Emitted ecosystem |
Sources |
|---|---|---|
| npm | npm |
package-lock.json, npm-shrinkwrap.json, node_modules/.package-lock.json, node_modules/<pkg>/package.json |
| pnpm | npm |
pnpm-lock.yaml, .pnpm/.../package.json |
| Yarn | npm |
yarn.lock (Classic + Berry) |
| Bun | npm |
bun.lock; bun.lockb presence as diagnostic |
| PyPI (installed) | pypi |
*.dist-info/METADATA, INSTALLER, direct_url.json, *.egg-info/PKG-INFO |
| PyPI (declared) | pypi |
requirements*.txt, Pipfile.lock, poetry.lock, uv.lock, pylock.toml (PEP 751) |
| Go modules | go |
go.sum, go.mod, go.work.sum, vendor/modules.txt |
| RubyGems | rubygems |
Gemfile.lock, installed *.gemspec |
| Composer | packagist |
composer.lock, vendor/composer/installed.json |
| MCP | mcp |
JSON host configs: mcp.json, .mcp.json, claude_desktop_config.json, mcp_config.json, mcp_settings.json, cline_mcp_settings.json, plus ~/.gemini/settings.json (Gemini CLI / Code Assist) and ~/.claude.json (Claude Code user- and project-scoped mcpServers). These are parsed as JSONC (comments and trailing commas accepted), because VS Code, Kiro and GitLab Duo all ship commented mcp.json templates. Non-JSON configs (Codex config.toml, Continue YAML) are not parsed in v0.1. |
| Agent skills | agent-skill |
skills.sh / vercel-labs/skills lock files: global ~/.agents/.skill-lock.json (or $XDG_STATE_HOME/skills/.skill-lock.json) and project-local skills-lock.json. Loose SKILL.md directories without a lock file are not enumerated. |
| Editor extensions | editor-extension |
VS Code, Cursor, Windsurf, VSCodium manifests, including each editor's remote/SSH server tree |
| Browser extensions | browser-extension |
Chromium-family (manifest.json) and Firefox (extensions.json) per profile |
| Homebrew | homebrew |
Formula INSTALL_RECEIPT.json files and cask .metadata install markers |
| NuGet | nuget |
~/.nuget/packages/<id>/<version>/ cache, packages.lock.json, packages.config |
| Cargo | cargo |
Cargo.lock, ~/.cargo/registry/src/<index>/<crate>-<version>/ |
| Maven / Gradle | maven |
*.gradle.lockfile, pom.xml (declared only), ~/.m2/repository/ |
| SwiftPM | swift |
Package.resolved (v1 and v2/v3 layouts) |
| CocoaPods | cocoapods |
Podfile.lock (PODS: section) |
| Dart | pub |
pubspec.lock |
| Elixir | hex |
mix.lock |
Per-ecosystem detail: docs/inventory-sources.md.
Requires Go 1.25+. Exactly one dependency,
pelletier/go-toml/v2 (MIT), which
has no transitive dependencies of its own.
# Install the latest tagged release into $GOBIN.
go install github.com/perplexityai/bumblebee/cmd/bumblebee@latest
# Or pin a specific tag.
go install github.com/perplexityai/bumblebee/cmd/bumblebee@v0.1.5To build from a checkout:
go build -o bumblebee ./cmd/bumblebee
go test ./...Stamp an explicit version at build time:
go build -ldflags "-X main.Version=v0.1.5" -o bumblebee ./cmd/bumblebeebumblebee version prints the version plus the VCS revision, build
time, and Go runtime — so a record emitted in production can be traced
back to a specific build. Version precedence: -ldflags override,
module version recorded by go install, then the in-tree default
tracked in VERSION.
After installing, run a built-in end-to-end check against embedded fixtures:
bumblebee selftest
# selftest OK (19 findings in 1ms)The fixtures live inside the binary, use deliberately fake package
names (bumblebee-selftest-evil@0.0.0), and make no network calls. A
non-zero exit means the local install can no longer detect what it
should — a fast pre-deployment smoke test for fleet rollouts.
Bumblebee is a one-shot scanner: each invocation performs a single scan
and exits. Cadence is the runner's responsibility (cron, launchd, systemd,
MDM, etc.). Each record carries profile and a per-root root_kind so
receivers can keep populations separate.
| Profile | Scans | Use for |
|---|---|---|
baseline |
Common global/user package roots (including the npm -g/Yarn/pnpm/Bun prefixes, per-user Python, conda, Composer, pub, Hex), language toolchains, editor extensions, browser extensions, and MCP configs. Honours GOPATH, CARGO_HOME, NPM_CONFIG_PREFIX and similar relocations. |
Recurring lightweight inventory via an external runner. |
project |
Configured development directories, such as ~/code, ~/src, or ~/work. |
Recurring inventory for known project workspaces. |
deep |
Explicit --root paths, including broad roots like $HOME. |
On-demand incident or campaign checks, usually with --ecosystem, --exposure-catalog, and --findings-only. |
baseline and project refuse bare-home roots; only deep walks them.
On Windows this also covers drive roots and bare <drive>:\Users\<name>
homes. Default roots are platform-aware: on Windows, MCP host configs
under %APPDATA%, Chromium-family extensions under %LOCALAPPDATA%,
and Firefox profiles under %APPDATA%\Mozilla are resolved in place of
their macOS/Linux equivalents.
# Baseline global inventory.
bumblebee scan --profile baseline > inventory.ndjson
# Daily project sweep with explicit roots.
bumblebee scan --profile project \
--root "$HOME/code" \
--root "$HOME/Developer"
# Limit a run to selected emitted ecosystems.
bumblebee scan --profile baseline \
--ecosystem npm,pypi \
--ecosystem go
# On-demand exposure scan against a published advisory.
bumblebee scan --profile deep \
--root "$HOME" \
--exposure-catalog ./catalog.json \
--max-duration 10mPreview the resolved roots without scanning:
bumblebee roots --profile baseline
# prints "<root_kind>\t<path>" lines--root is a filesystem path to scan; repeatable, required for deep,
optional for the other profiles. --ecosystem is repeatable and
comma-separated. --exposure-catalog accepts a JSON file or a directory
of *.json catalogs (merged non-recursively, all files must share
schema_version). --findings-only requires --exposure-catalog and
suppresses package records while keeping findings. --exclude adds a
directory name or suffix path to skip (repeatable). --all-users
expands per-user default roots across every real user home
(/Users/<name> on macOS, <drive>:\Users\<name> on Windows). It is
what makes an agent-run scan see anything: a fleet agent runs as root or
SYSTEM, and SYSTEM's own profile holds no developer trees, so without it
baseline and project resolve to nothing. On Linux it is accepted but
has no effect and is reported as a diagnostic, since there is no single
canonical home parent to enumerate. bumblebee scan --help lists
every flag.
Records are NDJSON, one per line. Diagnostics go to stderr as NDJSON. Each
run ends with a scan_summary record; receivers use it to decide whether
to promote a run to current state. See docs/transport.md
for HTTPS/file output and docs/state-model.md for the
receiver-side current-state model.
Package record:
Example package record
{
"record_type": "package",
"record_id": "package:...",
"schema_version": "0.2.0",
"scanner_name": "bumblebee",
"scanner_version": "v0.1.1",
"run_id": "9b1f0c2e4d5a6b7c8d9e0f1a2b3c4d5e",
"scan_time": "2026-05-15T18:22:01.482Z",
"endpoint": {
"hostname": "alex-mbp",
"os": "darwin",
"arch": "arm64",
"username": "alex",
"uid": "501",
"device_id": "MDM-7F4A2B"
},
"profile": "project",
"ecosystem": "npm",
"package_name": "@tanstack/query-core",
"normalized_name": "@tanstack/query-core",
"version": "5.59.20",
"project_path": "/Users/alex/code/web-app",
"root_kind": "project_root",
"package_manager": "pnpm",
"source_type": "pnpm-lockfile",
"source_file": "/Users/alex/code/web-app/pnpm-lock.yaml",
"has_lifecycle_scripts": false,
"confidence": "high"
}confidence:
high— exact identity and version came from canonical metadata.medium— identity is reliable, but version or source is partial.low— config/path/spec reference only; not proof of an installed exact version.
Finding record (exposure-catalog match):
Example finding record
{
"record_type": "finding",
"record_id": "finding:...",
"schema_version": "0.2.0",
"scanner_name": "bumblebee",
"scanner_version": "v0.1.1",
"run_id": "3a8c7d1e9f0b2a4c6d8e0f1a2b3c4d5e",
"scan_time": "2026-05-15T18:22:01.482Z",
"endpoint": {
"hostname": "alex-mbp",
"os": "darwin",
"arch": "arm64",
"username": "alex",
"uid": "501",
"device_id": "MDM-7F4A2B"
},
"profile": "deep",
"finding_type": "package_exposure",
"severity": "critical",
"catalog_id": "advisory-2026-0042",
"catalog_name": "example-pkg 1.2.3 (compromised release)",
"ecosystem": "npm",
"package_name": "example-pkg",
"normalized_name": "example-pkg",
"version": "1.2.3",
"root_kind": "deep_home_root",
"project_path": "/Users/alex/code/web-app",
"source_type": "pnpm-lockfile",
"source_file": "/Users/alex/code/web-app/pnpm-lock.yaml",
"confidence": "high",
"evidence": "exact name+version match (version=1.2.3)"
}record_id is a content-addressed hash of a canonical identity tuple per
record type, stable across runs. Per-record-type field lists and dedupe
guidance: docs/state-model.md.
Minimal JSON, exact (ecosystem, name, version) matching. An entry may
declare "versions": ["*"] to match every version of the package:
{
"schema_version": "0.2.0",
"entries": [
{
"id": "advisory-2026-0042",
"name": "example-pkg 1.2.3 (compromised release)",
"ecosystem": "npm",
"package": "example-pkg",
"versions": ["1.2.3"],
"severity": "critical"
}
]
}The catalog must be a JSON object with schema_version and entries
keys. Bare top-level arrays are rejected. schema_version 0.1.0
catalogs are still accepted (they cannot use "*"); unsupported future
values are rejected. Multiple catalog files can be loaded together by
pointing --exposure-catalog at a directory; see the flag description
above.
The threat_intel/ directory holds maintained exposure
catalogs built from public threat-intelligence reporting on recent
supply-chain campaigns, assembled with
Perplexity Computer and updated
via PRs as new campaigns are reported. Additional auto-*.json
catalogs are produced daily by cmd/threatintel-fetch from public
feeds (MalExt Sentry, ExtSentry, VSXSentry, OSV malicious-packages,
DataDog malicious-software-packages-dataset). See
threat_intel/README.md for the current
catalog list, review guidance, and the auto-source map.
Operators using a release binary can pull the current snapshot without cloning the repo:
curl -fsSLO https://github.com/perplexityai/bumblebee/releases/latest/download/threat-intel-latest.tar.gz
tar -xzf threat-intel-latest.tar.gz
The daily threat-intel-YYYY-MM-DD release carries the whole directory
as a tarball plus each catalog as an individual asset and a
SHA256SUMS manifest.
- docs/inventory-sources.md — per-ecosystem list of the exact files read and the fields derived from each.
- docs/state-model.md — receiver-side current-state
model,
record_ididentity, and dedupe guidance. - docs/transport.md — stdout/file/HTTPS output modes,
batching, and auth (
bearer,hmac-sha256). - docs/deployment-macos.md —
launchd/MDM scheduling patterns per profile. - docs/schema/ — published JSON Schemas for records and
the exposure-catalog format (
v0.1.0,v0.2.0). Published schema versions are never edited in place; see CONTRIBUTING.md.
| Path | What it is |
|---|---|
cmd/bumblebee |
The scanner CLI (scan, roots, selftest, version). |
cmd/threatintel-fetch |
Daily automation that builds the auto-*.json catalogs from public feeds. |
tools/osvcatalog |
Offline generator that turns OSV data into an exposure catalog. |
internal/ |
Ecosystem parsers, walker, exposure matching, endpoint metadata, output sinks. |
threat_intel/ |
Shipped and auto-generated exposure catalogs. |
See CONTRIBUTING.md for the local build/test loop and the requirements for adding an exposure catalog. Report vulnerabilities privately as described in SECURITY.md.
Apache License 2.0. See LICENSE.