ADR 0004: Reachability, runtime facts first, static import analysis as fallback¶
- Status: accepted
- Date: 2026-09-26
Context¶
The research question is how much reachability enrichment cuts actionable alerts without missing exploitable findings. Runtime facts (modules loaded in a running container) are the most reliable signal, but most pipelines do not collect them.
Decision¶
- If a
runtimestage reports the modules loaded in a downstream container, that evidence decides the result. The package is reachable if it was loaded anywhere and unreachable if it was observed nowhere. - Otherwise
tracegate.reachmakes a conservative static estimate. A package counts as reachable if the app sources import it (AST imports,importlib.import_module, dotted-path strings such as DjangoINSTALLED_APPS), or if it is named in an entrypoint file (Dockerfile, Procfile, shell, ini, toml, yaml), or if a reachable package requires it according to the pip-compile# viaannotations. - Unknown reachability is treated as reachable. Being unreachable only downgrades a finding from block to warn. It never suppresses the finding.
Consequences¶
- Any import on any code path counts, including cold ones. The estimate therefore over-approximates reachability, and the reduction it reports is a lower bound.
- Two cases produce a false "unreached": packages loaded only through entry-point plugins, and
packages loaded through computed import strings. For pip-compile repos the benchmark lists every
direct dependency that was marked unreached, so these cases can be audited by hand
(
results/lineage_real_repos.json). - The mapping from distribution name to import name is a curated table plus heuristics, because
wheel
top_level.txtmetadata is not fetched. A wrong mapping can only cause a false "unreached", and that outcome is still reported as a warning.
Amendment (after the v1.1.0 audit)¶
An audit of every static downgrade in the per-snapshot reachability benchmark (45 findings on 17
pins of warehouse and netbox) found all 17 pins loaded: each is required by a package the
snapshot's sources import (google-cloud-bigquery -> google-auth -> rsa / pyasn1,
webauthn -> cbor2, django-rest-swagger -> PyYAML). Those requirement files record no
dependency edges, so a third failure mode dominates: a transitive dependency of an imported
package looks exactly like an unused pin. Two changes follow:
via_graphalso reads the older inline layout (pyyaml==3.11 # via pymlconf).- Without any pip-compile
# viaannotation in the manifest, a pin that is not imported gets the statusunknown, which never downgrades.unreachednow requires recorded dependency edges.
On the benchmark's 36 snapshots the static rule therefore downgrades nothing. Evidence and
verdicts: results/reachability_audit.json.