Skip to content

Search

Search the docs, blog, and marketplace.

How it works

Detectors

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

Last updated October 25, 2026View source (v0.30.0)

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

A detector knows one or more package managers. It looks for evidence on disk: a lockfile, a manifest, a workflow file, or an SBOM document. From that evidence it produces packages, versions, and the links between them. Bomly ships native detectors for the most common ecosystems. For everything else it falls back to Syft, a bundled tool that lists packages by recognizing files on disk.

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 take part in the same planning 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 a fallback when the preferred detector is not ready, does not apply, or cannot produce graph data.

For example, the npm chain is npm → syft-detector:

  1. npm tries its internal strategies in order. It parses package-lock.json first and falls back to npm ls. Either way it resolves the full transitive graph.
  2. syft-detector runs only if the npm detector cannot produce graph data, for example when there is no lockfile and no npm command. It emits a flat package list.

Per-detector chains are listed in the detector reference. The CLI prints the live list:

bomly plugins list --detectors
bomly plugins list --detectors --json

Native vs. Syft

Native detectorsSyft-backed
Graph shapeFull transitive graph with edgesFlat package list
SourceLockfile or build-tool outputFiles Syft recognizes
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 keep the edges. bomly explain, reachability, and scope filtering all need those edges. When no native detector applies, Bomly falls back to Syft without any extra flag.

Selecting detectors

Use --detectors to restrict or extend the default set with the standard selector grammar. Pass the bare name to use only that detector, +name to add one to the defaults, or -name to remove one:

# 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

Remediation hints

After vulnerability enrichment, a detector may describe how its package manager can fix a dependency it found. Each detector declares that capability for itself and owns the advice for its package manager. Bomly's remediation component checks those read-only hints and chooses the final suggestion. Detectors never choose a fix version and never change manifests or lockfiles.

Each package-manager page lists the strategies its built-in detectors understand. External detector plugins can advertise the same optional capability. Older protocol-v1 plugins keep working without it.

Resolution warnings

A detector can resolve a good graph from a project whose package-manager setup will still break an install somewhere else. Examples are a lockfile format the pinned package manager will not keep, or an install policy that rejects freshly published versions. Detectors record those as resolution warnings on the manifest they resolved. The graph is still produced and the problem is still reported. See CI-readiness warnings for the checks and where the warnings appear.

Network behavior

Matchers only call out when you pass --enrich, but a detector that runs a build tool may download dependencies while it resolves the graph. Network and privacy is the full rule. The table below says which detectors can download during a plain scan.

Detector classExamplesNetwork during a plain scan
Lockfile parsernpm, pnpm, yarn (lockfile strategy), bundler-detector, composer-detector, nuget-detector, github-actions-detector, SBOM ingestNone. The detector only reads files.
Lockfile-first hybridcargo-detector, poetry-detector, uv-detectorNone when the lockfile is present. The build-tool fallback passes --locked or --no-sync to stay offline.
pip inspectpip-detector, pipenv-detectorNone when a lockfile is present. Otherwise the detector may download packages while it fills the isolated environment it inspects.
Build-tool primarygo-detector, maven-detector, gradle-detector, sbt-native-detectorMay download artifacts that are not in the local cache.

The build-tool detectors run commands you would already run locally: go list, mvn dependency:tree, gradle dependencies, and sbt dependencyTree. Whether those commands use the network depends on the tool and on your local cache, not on Bomly. To keep these scans offline, warm the local cache first (for example with go mod download or mvn dependency:go-offline) or commit a lockfile when the ecosystem supports one.

Each page under detectors/ documents the exact command that detector runs and whether it uses the network.

--install-first {#install-first}

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

Package managerInstall-first command
gomodgo mod download
npmnpm i (the short form of npm install)
pnpmpnpm i (the short form of pnpm 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 writes to the filesystem. Depending on the package manager, that means node_modules/, the active virtual environment, vendor/, or ~/.m2/repository/. It is opt-in for that reason. Use it when:

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

Detectors with no install step (for example NuGet, GitHub Actions, SBOM ingest, and Syft) skip the step silently when --install-first is set. Bomly does not install package managers themselves, only their dependencies.

Each page under detectors/ says whether --install-first is supported and which command runs.

Customizing the install command with --install-arg

The default install command suits most projects. Real builds often need extra flags: skip dev dependencies, use a legacy resolver, point at a private index, or run a clean install. Pass --install-arg once per argument to append arguments to the detector's install command.

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

Arguments are appended to the default command, never replacing it. For example, the npm detector runs npm i by default. With --install-arg --legacy-peer-deps --install-arg --no-audit it runs npm i --legacy-peer-deps --no-audit.

The recipes below cover the most common adjustments:

# npm: tolerate peer-dependency conflicts on a legacy project
bomly scan --install-first --detectors npm \
  --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

Bomly applies --install-arg whenever it runs a detector install step. Most detectors only install when --install-first is set. pip-detector may also install into its isolated temporary virtual environment when no readable requirements.lock is present.

Discovery and monorepos

For local source trees, Bomly discovers subprojects before running detectors. Every package manager with recognized evidence in the target directory becomes a subproject. Workspace-aware package managers (npm, pnpm, Yarn, Maven, Gradle, Cargo) expand their own nested modules from the root manifest. Bomly runs the matching detector chain in each subproject and merges the results into a single graph.

To also discover independent projects in nested directories, pass --recursive. See Monorepos for depth and exclusion options.

SBOM ingest

Bomly treats SPDX 2.3 and CycloneDX SBOMs as first-class input. Use --sbom to ingest an SBOM file directly without 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 pulls container images with the host's registry credentials. Native detectors that read lockfile contents inside layers still run. Everything else falls through to Syft:

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

See also