concepts

Detectors

How Bomly discovers projects and turns evidence into a dependency graph.

Detectors turn a project, container, or SBOM into a dependency graph. Every scan starts with a detector.

A detector knows one or more package managers. Given evidence on disk — a lockfile, a manifest, a workflow file, an SBOM document — it produces packages, versions, and the edges between them. Bomly ships native detectors for the most common ecosystems and falls back to Syft for broad coverage of everything else.

When detectors run

  • bomly scan runs detectors to build the graph.
  • bomly explain reuses the same detector planning before walking dependency paths.
  • bomly diff runs detectors for each side of the comparison, unless you pass --sbom to diff two SBOM files directly.
  • Plugin detectors participate in the same planning flow when they declare package-manager evidence.

Detector Chains

A detector chain is the ordered list Bomly tries for a package manager. The first entry is preferred. Later entries are fallbacks Bomly uses when the preferred detector is not ready, is not applicable, or cannot produce graph data.

For example, the npm chain is npm-detectorsyft-detector:

  1. npm-detector parses package-lock.json directly and resolves the full transitive graph.
  2. syft-detector runs only if the native detector cannot produce graph data (for example, no lockfile present), and emits a flat package list.

Per-ecosystem chains are listed in detectors/ecosystems/. The full live list lives in the CLI:

bomly plugin list --detectors
bomly plugin list --detectors --json

Native vs. Syft

Native detectorsSyft-backed
Graph shapeFull transitive graph with edgesFlat package list
SourceLockfile or build-tool outputCataloger heuristics
Best forLocal source trees, monorepos, CIContainer images, ecosystems without a native detector
PerformanceFaster on supported ecosystemsRequired for many container layers

Bomly prefers native detectors because they preserve edges (needed by bomly explain, reachability, and scope filtering). It falls back to Syft transparently when no native detector applies.

Selecting detectors

Use --detectors to restrict or extend the default set with the standard +/- selector grammar:

# Use only the native Go detector
bomly scan --detectors go-detector

# Disable Syft fallback for this run
bomly scan --detectors -syft-detector

# Add an external plugin detector
bomly scan --detectors +bomly.examples.detector.bun-lock

Pass the bare detector name to filter to only that detector, +name to add it on top of defaults, or -name to remove it.

Network behavior

Detectors differ in whether they run subprocesses, and those that do may invoke build tools that download packages from registries. The marketing claim "Bomly is offline-safe by default" is precise: matchers make zero outbound calls without --enrich. Detectors may invoke build tools that download packages on your behalf during normal graph resolution.

Detector classExamplesNetwork during normal scan
Lockfile parsernpm-detector, pnpm-detector, bundler-detector, composer-detector, nuget-detector, github-actions-detector, SBOM ingestNone — pure file parse
Lockfile-first hybridcargo-detector, poetry-detector, uv-detectorNone when the lockfile is present; the build-tool fallback uses --locked / --no-sync to stay offline
pip inspectpip-detector, pipenv-detectorNone — reads the local Python environment
Build-tool primarygo-detector, maven-detector, gradle-detector, sbt-native-detectorMay download uncached artifacts during normal resolution

The build-tool-primary detectors invoke commands you would already run locally (go list, mvn dependency:tree, gradle dependencies, sbt dependencyTree). Whether they hit the network is a property of those tools and your local cache state, not a Bomly choice. To keep these scans fully offline, pre-warm the local cache (go mod download, mvn dependency:go-offline, etc.) or commit a lockfile when the ecosystem supports one.

Per-PM pages under detectors/ecosystems/ document the exact command each detector runs and whether it touches the network.

--install-first {#install-first}

Pass --install-first to let supporting detectors run their normal dependency-install (or cache-warming) command before resolving the graph. The exact command depends on the package manager:

Package managerInstall-first command
gomodgo mod download
npmnpm install
pnpmpnpm install
yarnyarn install
pippython -m pip install -r <requirements> (plus requirements-dev.txt if present)
pipenvpipenv install
poetrypoetry install --no-root
uvuv sync
bundlerbundle install
composercomposer install
mavenmvn dependency:resolve (uses ./mvnw if present)
gradlegradle dependencies --console=plain (uses ./gradlew if present)
cargocargo fetch --locked

⚠️ --install-first downloads packages from each ecosystem's registry and modifies the filesystem (writes to node_modules/, the active virtualenv, vendor/, ~/.m2/repository/, etc.). It is opt-in for that reason. Use it when:

  • You're scanning in CI on a clean checkout where dependencies have not been installed locally.
  • The lockfile is missing or stale and you want a fresh graph.
  • A build-tool primary detector (Go, Maven, Gradle) would otherwise fetch artifacts during the resolution step itself — --install-first is the cleaner place to bear that cost.

Detectors without an Install implementation (e.g. NuGet, GitHub Actions, SBOM ingest, Syft) silently skip the step when --install-first is set. Bomly does not install package managers themselves — only their dependencies.

Each package-manager page under detectors/ecosystems/ lists whether --install-first is supported and the exact command that runs.

Customizing the install command with --install-arg

The default install-first command for each detector is fine for most projects, but real builds often need extra flags (skip dev dependencies, use a legacy resolver, point at a private index, run a clean install). Pass --install-arg — repeatable — to append arguments to whatever the detector's Install() method runs.

⚠️ --install-arg requires exactly one selected detector. Bomly cannot safely apply an arbitrary flag to a chain of mixed package managers, so it rejects the run with exit 4 if zero or more than one detector is in scope. Combine with --detectors <name> to scope to a single detector.

Args are appended to the default command, not replacing it. For example, the npm detector defaults to npm install; with --install-arg --legacy-peer-deps --install-arg --no-audit it runs npm install --legacy-peer-deps --no-audit.

Recipes:

# npm: tolerate peer-dependency conflicts on a legacy project
bomly scan --install-first --detectors npm-detector \
  --install-arg --legacy-peer-deps

# pip: install from a private index in CI
bomly scan --install-first --detectors pip-detector \
  --install-arg --index-url --install-arg https://pypi.example.com/simple

# composer: skip dev dependencies for a production-shaped graph
bomly scan --install-first --detectors composer-detector \
  --install-arg --no-dev

# bundle: deployment mode (frozen lockfile, no version updates)
bomly scan --install-first --detectors bundler-detector \
  --install-arg --deployment

# go: verbose download output for CI debugging
bomly scan --install-first --detectors go-detector \
  --install-arg -x

# mvn: pass a specific Maven profile for the dependency:resolve step
bomly scan --install-first --detectors maven-detector \
  --install-arg -Pproduction

# uv: install only the locked dependencies, no editable updates
bomly scan --install-first --detectors uv-detector \
  --install-arg --frozen

Without --install-first, --install-arg has no effect — the args are only consumed during the install step, not during graph resolution itself.

Discovery and monorepos

For local source trees, Bomly discovers subprojects before running detectors. Every directory containing recognized evidence becomes a subproject; Bomly runs the matching detector chain in each one and consolidates the results into a single graph.

There is no project-root requirement. Pointing bomly scan at a monorepo will scan every workspace in one pass.

SBOM ingest

Bomly treats SPDX 2.3 and CycloneDX SBOMs as first-class input. Use --sbom to ingest an SBOM file directly without re-running ecosystem detectors:

bomly scan --sbom --path ./existing.spdx.json

This is fast and offline. See SBOM formats for the format comparison.

Container images

Bomly resolves container references via the host's registry credentials. Native detectors that work on lockfile contents inside layers still run; everything else falls through to Syft:

bomly scan --container ghcr.io/example/app:latest

See also

  • Ecosystem guides — generated per-ecosystem detector chains, evidence patterns, and PATH requirements
  • Support matrix — generated overview of every supported ecosystem
  • Plugins — author and install external detectors