A compromised npm package does not announce itself. You learn about it from a security blog post — days after its postinstall script already ran on your laptop. The question that follows is always the same: do I have that exact package, and where? Perplexity's security team open-sourced the tool they use to answer it in seconds. It is called Bumblebee, it is a single Go binary, and in about fifteen minutes you will go from install to a real, verified exposure finding on your own machine.

Why this is blowing up now#

Bumblebee collected 2,328 stars in its first five days on GitHub trending and sat past 5,000 stars with 454 forks when I checked it for this tutorial — the strongest new-repo signal in its week, from a credible team (the perplexityai org) solving a specific, defensible problem.

The timing is not an accident. 2026 has been a brutal year for open-source supply-chain attacks: the Shai-Hulud worm waves swept hundreds of npm and PyPI packages in May and June, GlassWorm became the first self-propagating IDE-extension worm on Open VSX, and the Mastra compromise pushed an infostealer through a typosquat dependency's postinstall script across 141 npm packages. Bumblebee ships with maintained exposure catalogs for 13 of these campaigns in its threat_intel/ directory — assembled from public threat-intel reporting, updated by pull request, with the newest (MemTensor, a September 23 credential stealer) added four days before this writing.

What makes it different from yet another scanner: Bumblebee is read-only and offline. It never executes a package manager, never reads your source files, and never touches the network at scan time. It turns the messy on-disk state of a developer machine — lockfiles, extension manifests, MCP configs — into structured NDJSON records, then matches them against an exposure catalog with exact name-and-version matching. SBOMs tell you what shipped; EDR tells you what ran. Bumblebee answers the incident-response question in between: which machines have this exact artifact on disk, right now?

What you'll need#

  • Go 1.25 or newer — only needed for the one-line install; the binary itself has zero non-standard-library dependencies.
  • Linux or macOS. I ran every command below on a Linux machine; macOS is a first-class target (the repo even ships a macOS deployment doc).
  • git — to fetch the maintained exposure catalogs.
  • python3 or jq — the output is NDJSON, one JSON object per line; either tool reads it.

No accounts, no API keys, no GPU, no cloud service, no network access during scans. Cost: $0.

Step 1: Install the binary#

go install github.com/perplexityai/bumblebee/cmd/bumblebee@latest

That is the whole install. Verified on September 27, 2026, @latest resolves to v0.1.2. Make sure $GOBIN (default ~/go/bin) is on your PATH, then confirm:

bumblebee version
# bumblebee v0.1.2
# commit: unknown
# built:  unknown
# go:     go1.25.1

The version output includes the VCS revision and build time when built from a checkout — so a record emitted in production can be traced back to a specific build. To pin a version instead of floating on latest:

go install github.com/perplexityai/bumblebee/cmd/[email protected]

Prefer building from source? It is one step, and the test suite runs clean:

git clone https://github.com/perplexityai/bumblebee.git
cd bumblebee
go build -o bumblebee ./cmd/bumblebee

Step 2: Prove it works with the self-test#

bumblebee selftest
# selftest OK (5 findings in 4ms)

This is the best-designed thirty seconds in the README. The self-test scans fixtures embedded in the binary — deliberately fake package names like [email protected] — and verifies the detector fires. It makes no network calls. A non-zero exit means your install can no longer detect what it should: a pre-deployment smoke test for fleet rollouts, and your first proof that the matching engine works before you trust a single scan.

Expect 5 findings on v0.1.2 (v0.1.1 reported 3 — the fixture set grew with the release). If your count differs from the version you installed, reinstall.

Step 3: Take your first inventory#

Bumblebee is a one-shot scanner with three profiles. Preview what a profile will touch before scanning:

bumblebee roots --profile baseline

On my machine this resolved 8 roots — user package dirs, MCP config locations, global library paths — and noted 120 candidate paths absent. Then run the scan:

bumblebee scan --profile baseline > inventory.ndjson

My run finished in about 1.5 seconds with exit code 0 and produced 95 records: 94 package records and one scan_summary. Each line is a self-contained JSON object. Here is a real one, trimmed:

{
  "record_type": "package",
  "ecosystem": "pypi",
  "package_name": "annotated-types",
  "version": "0.8.0",
  "confidence": "high",
  "source_type": "pypi-dist-info",
  "root_kind": "user_package_root"
}

The three profiles, straight from the docs:

ProfileScansUse for
baselineGlobal/user package roots, language toolchains, editor extensions, browser extensions, MCP configsRecurring lightweight inventory, driven by an external runner
projectConfigured development directories (~/code, ~/src, ~/work)Recurring inventory of known project workspaces
deepExplicit --root paths, including broad roots like $HOMEOn-demand incident or campaign checks, usually with --ecosystem, --exposure-catalog, and --findings-only

Two details worth knowing. First, baseline and project refuse bare-home roots; only deep walks them — a guardrail against accidentally inventorying your entire home directory on a schedule. Second, coverage is broad: the npm family (npm, pnpm, Yarn Classic and Berry, Bun), PyPI, Go modules, RubyGems, Composer, MCP host configs (including ~/.claude.json), agent-skill lock files, editor extensions (VS Code, Cursor, Windsurf, VSCodium), browser extensions (Chromium-family and Firefox), and Homebrew. MCP configs can carry credentials in their env blocks; Bumblebee parses them for the server inventory but never emits those values — a deliberate design choice I verified in the source.

Each record carries a confidence field — high for exact identity and version from canonical metadata, medium when the version or source is partial, low for config references that do not prove an installed version — and a content-addressed record_id that is stable across runs, so receivers can diff inventories over time.

Conceptual diagram: lockfiles and package metadata flow into a scanner lens that emits structured data lines, ending in a shield flagging one matching record
Image: AI-generated illustration for AI Frontier Post.

Step 4: Run a real exposure check#

Inventory is the means; exposure matching is the point. Grab the maintained catalogs:

git clone https://github.com/perplexityai/bumblebee.git
ls bumblebee/threat_intel/

You will see 13 campaign catalogs: the Mastra compromise (141 packages), GlassWorm (243 editor extensions), the Shai-Hulud worm waves, the node-ipc credential stealer, Laravel Lang, the Nx Console VS Code extension compromise, MemTensor, and more. The catalog README carries an honest caveat: review the entries against current advisories before production use.

To see a finding with your own eyes, plant a known-compromised package. The Mastra catalog lists [email protected] — the typosquat dependency that delivered a cross-platform infostealer via postinstall. Create a test project containing it:

mkdir -p /tmp/bb-demo
cat > /tmp/bb-demo/package-lock.json <<'EOF'
{
  "name": "demo-app",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "demo-app",
      "version": "1.0.0",
      "dependencies": { "easy-day-js": "1.11.22", "lodash": "4.17.21" }
    },
    "node_modules/easy-day-js": {
      "version": "1.11.22",
      "resolved": "https://registry.npmjs.org/easy-day-js/-/easy-day-js-1.11.22.tgz"
    },
    "node_modules/lodash": {
      "version": "4.17.21",
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
    }
  }
}
EOF

Now scan that directory against the real Mastra campaign catalog, showing only findings:

bumblebee scan --profile deep --root /tmp/bb-demo \
  --exposure-catalog bumblebee/threat_intel/mastra-2026-06-17.json \
  --findings-only

The output is exactly one finding record plus the scan summary — the innocent lodash is silent, the compromised package is not:

{
  "record_type": "finding",
  "finding_type": "package_exposure",
  "severity": "critical",
  "catalog_id": "mastra-2026-06-17-npm-easy-day-js",
  "catalog_name": "easy-day-js (Mastra npm supply-chain compromise)",
  "ecosystem": "npm",
  "package_name": "easy-day-js",
  "version": "1.11.22",
  "confidence": "high",
  "evidence": "exact name+version match (version=1.11.22)"
}

That is the whole detection loop, verified end to end: lockfile in, structured finding out, no network, no package manager executed. --findings-only requires --exposure-catalog and suppresses the package records — exactly what you want when an advisory drops and you need a yes-or-no answer across machines.

Conceptual illustration: an advisory dossier of package entries matched by glowing lines against packages on a developer laptop, one match flagged red
Image: AI-generated illustration for AI Frontier Post.

Step 5: Write your own exposure catalog#

When the advisory you care about is not in threat_intel/ yet, the catalog format is minimal JSON — an object (bare top-level arrays are rejected) with a schema_version and an entries list:

{
  "schema_version": "0.2.0",
  "entries": [
    {
      "id": "demo-typosquat",
      "name": "demo typosquat (any version)",
      "ecosystem": "npm",
      "package": "demo-typosquat",
      "versions": ["*"],
      "severity": "high"
    }
  ]
}

The "*" in versions is a schema-0.2.0 feature: it matches every version of the package — useful for typosquats, where no version is ever legitimate. I verified it against a planted [email protected]; the evidence string reads "exact name match, catalog entry matches any version (version=9.9.9)". Point --exposure-catalog at a directory to merge several catalogs at once (merged non-recursively; every file must share the same schema_version).

Two honest limits to keep in mind. Matching is exact (ecosystem, name, version) presence — the help text is explicit that the catalog is package-presence criteria only; it is NOT an EDR IOC feed. And Cargo is not matched yet: the TrapDoor catalog documents six Cargo packages that sit inert until Cargo support lands.

Step 6: Put it on a schedule#

Bumblebee is deliberately one-shot — cadence is the runner's responsibility (cron, launchd, systemd, MDM). A sensible starting setup:

# Weekly lightweight inventory, Mondays at 09:00
0 9 * * 1 /home/you/go/bin/bumblebee scan --profile baseline \
  > /var/log/bumblebee/inventory.ndjson 2> /var/log/bumblebee/err.ndjson

When an advisory drops, run the incident check on demand and bound it:

bumblebee scan --profile deep --root "$HOME" \
  --exposure-catalog ./catalogs/ \
  --ecosystem npm,pypi \
  --findings-only \
  --max-duration 10m

For fleets, records can go straight to a central ingest instead of a file: --output http with --http-url, --http-auth bearer|hmac-sha256 (tokens via --http-token-env, never on the command line), --http-gzip, and --http-batch-size — all present in scan --help. Pair with --device-id-env to stamp a stable machine identity from your MDM onto every record, and receivers can use the scan_summary record to decide whether to promote a run to current state.

When to use this vs the alternatives#

ToolQuestion it answersNetwork at scan timeScope
BumblebeeWhich machines have this exact package on disk right now?NoneMachine / fleet inventory
npm audit / pip-auditDoes this project have known-vulnerable dependencies?Yes (advisory DB)One project
Socket.dev / SnykScan, score, and fix dependency risksYes (cloud)Project / org, commercial
OSV-ScannerWhich OSV advisories match this lockfile?Yes (osv.dev)Project lockfiles
EDR / XDRWhat ran or touched the network?YesRuntime behavior

Reach for Bumblebee when an advisory names a package and you need a fleet-wide yes-or-no in minutes, without installing agents, creating accounts, or exfiltrating file lists to a cloud. Reach for something else when you need CVE severity scoring and fix PRs (Bumblebee only does exact presence matching), runtime behavior (that is EDR's job), or what shipped in a build artifact (that is what SBOMs are for). The repo also ships tools/osvcatalog, which converts a local OSV snapshot into a catalog offline — the sanctioned way to keep the offline posture while sourcing advisories from OSV.

The takeaway#

Supply-chain response has a gap between "an advisory names a package" and "we know where it is," and that gap is measured in days of manual grep across machines. Bumblebee closes it with a single static binary: install in one command, self-test in milliseconds, inventory a machine in seconds, and match a real campaign catalog with one deep scan. The 5,000 stars are not for novelty — they are for a tool that does one incident-response job, read-only, with nothing to configure and nothing to phone home.