Skip to content

Search

Search the docs, blog, and marketplace.

Experimentalexperimental

Reachability

Check whether your code can reach a vulnerable package. Experimental.

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

This feature is experimental. The flag, output shape, and ecosystem coverage may change without notice. It is not yet ready to gate releases on.

Experimental. Reachability is opt-in (--analyze) and still maturing. The output shape is stable, but ecosystem coverage is growing. The JavaScript, Python, and JVM analyzers work at package precision: they confirm that your code imports the vulnerable package, not that it calls the vulnerable function. Read Unreachable is not safe before you gate CI on it.

Reachability answers one question per vulnerability: can the scanned project reach the vulnerable code at all? Use it to stop chasing high-severity advisories in transitive packages your code never imports. This page explains what the analyzers check, what the results mean, and where they appear.

Quick start

--analyze requires --enrich, because it annotates the vulnerabilities enrichment found:

# Annotate vulnerabilities with reachability
bomly scan --enrich --analyze --json

# Fail only when a finding is both low-or-higher severity and confirmed reachable
bomly scan --enrich --audit --analyze --fail-on low --fail-on reachable

What each analyzer checks

Each analyzer handles one language and reports at one level of precision, called a tier.

EcosystemAnalyzerTier"Reachable" meansPrerequisites
GogovulnchecksymbolApplication code calls the vulnerable function, directly or through other calls. Built on the Go vulnerability database tooling, run in-processA Go module that builds (go on PATH)
JavaScript, TypeScriptjsreachpackageApplication source imports the npm package, directly or through the dependency graph the lockfile describesA package.json with entry points, and a lockfile
PythonpyreachpackageApplication source imports the PyPI distribution, directly or through the dependency graph.py sources under the project root
Java, Kotlin, Scala, GroovyjvmreachpackageApplication source imports a class from the Maven artifact, directly or through the dependency graphSources under the project root, and a Maven, Gradle, or SBT graph

On a project with no applicable analyzer, the scan runs normally and every vulnerability keeps an empty reachability result.

The tiers, from strongest to weakest:

TierMeaning
symbolA call graph from application code reaches the vulnerable function or method
moduleApplication code imports the vulnerable submodule, with no symbol-level evidence
packageApplication code imports, or does not import, the package. Nothing more is known
noneNo analysis ran. A status of unknown always uses this tier

Each analyzer reports one of three statuses per vulnerability, or leaves it empty when the vulnerability is outside its scope:

StatusMeaning
reachableApplication code reaches the vulnerable code at the reported tier
unreachableThe analysis completed and found no path. At the package tier this does not mean safe
unknownThe analyzer applied but could not finish. reason says why

Analyzer failures never stop a scan. A missing toolchain, a broken build, or a cancelled run becomes unknown with a machine-readable reason such as missing-toolchain or build-failed.

Unreachable is not safe

This is the most important thing to know before you gate CI on --fail-on reachable. Static analysis cannot see:

  • Dynamic imports: require(userInput), importlib.import_module(name), Class.forName(string), conditional __import__.
  • Runtime loaders: plugin systems, Python entry points, Django INSTALLED_APPS strings, Spring component scanning, ServiceLoader, OSGi, JPMS layers, reflection.
  • Generated or external code: annotation processors, build-time code generators, shell commands.

A package-tier unreachable says "the application does not import this package". That is useful for deprioritizing dev-only and unused transitive dependencies. It is not evidence that the vulnerability is mitigated. The Go symbol tier is much stronger, because govulncheck builds the real call graph, but even it cannot see reflection.

When an analyzer sees dynamic-import constructs in your source, it sets dynamic_imports_detected to true and lowers the confidence of its results, so you can tell from the output when an unreachable verdict is incomplete.

Package-tier analysis in brief

jsreach, pyreach, and jvmreach all work the same way. They scan application source for import statements to build the set of directly imported packages, then expand that set through the dependency graph the detector resolved. Any package reachable from the imported set is reachable; everything else, such as a dev dependency no source file imports, is unreachable.

Three consequences follow for all three:

  • Imports of a subpath count for the whole package. Importing lodash/get marks all of lodash reachable, even if an advisory affects only lodash/template.
  • The result is only as good as the lockfile or build manifest. A package installed locally but never recorded, or a dependency the build tool did not report, cannot be reached.
  • Python and JVM imports must be mapped to package names. import yaml means the PyYAML distribution, and org.apache.commons covers many Maven artifacts. Both analyzers use curated maps for the well-known cases, so a missing entry can produce a false unreachable for a direct import. If you find one, open an issue naming the import and the package; additions are quick to ship.

The per-analyzer details, including the override maps and workspace handling, are in the reachability internals for contributors.

Gating on reachability

--fail-on is repeatable, and the vulnerability constraints must all match (they combine with AND). An empty reachability result, meaning no analyzer ran on that vulnerability, never matches reachable: the analyzer must have confirmed it.

bomly scan --enrich --audit --fail-on low                                   # severity only
bomly scan --enrich --audit --analyze --fail-on low --fail-on reachable      # severity AND reachable

The full list of --fail-on values, including exploitable, is on the Auditors page.

Where the results appear

OutputWhat you see
JSONA reachability object on each vulnerabilities[] entry and, with --audit, on each findings[] entry: status, tier, analyzer, optional reason, symbols, and call_paths (with function, package, file, line, column). Package-tier analyzers add hops (graph distance from a directly imported package; 0 means directly imported), confidence (high, medium, or low), and dynamic_imports_detected
Text reportA "Reachability" line in the summary and a REACHABILITY column in the findings table, rendered as <status> (<tier>), or — when no analyzer ran
SARIFresult.codeFlows built from the call paths, plus result.properties with reachability, reachability_tier, reachability_reason, analyzer, reachability_confidence, reachability_hops, and reachability_dynamic_imports_detected

confidence is high for a direct import with no dynamic imports seen, medium for one to three hops, and low for four or more hops or when dynamic imports were seen.

Selecting analyzers

--analyzers uses the same selector grammar as the other component flags:

bomly scan --enrich --analyze --analyzers govulncheck            # only govulncheck
bomly scan --enrich --analyze --analyzers -govulncheck           # everything except govulncheck
bomly scan --enrich --analyze --analyzers govulncheck,jsreach    # these two

External analyzer plugins join the same selector once installed and enabled. See Plugins and How to implement an analyzer plugin.

Limitations

LimitationAffectsDetail
unreachable is not safeAll package-tier analyzersSee above
Needs a buildable Go modulegovulncheckWhen the build fails, the status is unknown with one of the reasons below
Dynamic imports and runtime loaders are invisibleAllThe result is a lower bound on what is reachable
Subpath imports count for the whole packagejsreach, pyreach, jvmreachAn advisory on one submodule marks the whole package reachable
Curated name mapspyreach, jvmreachA missing module-to-distribution or package-prefix entry can hide a direct import. Python falls back to name normalization; Java has no naming convention to fall back on
Workspace traversal reads declarations onlyjsreach, jvmreachjsreach follows npm and Yarn workspaces and pnpm-workspace.yaml; jvmreach follows Maven <modules> and Gradle include(...). Computed workspace membership and Gradle composite builds are not followed

govulncheck reason codes when the build fails:

ReasonCause
missing-toolchaingo is not on PATH
no-go-packagesNo .go files, or build constraints exclude everything
module-resolution-failedNo go.mod, a missing go.sum entry, or a module download failed
invalid-go-modgo.mod syntax error
build-failedA compile error
cancelledThe run was cancelled or timed out
runner-errorAny other failure

See also