Documentation
¶
Overview ¶
Package reachproof is the coordinator that turns a deterministic reachability result into a CONFIRMED reachability Judgment, reusing the existing audited propose→verify gate rather than any new confirmed-state path. It runs the reachability analysis for an engagement's target, and for each finding mints a ReachabilityClaim at the analyzer's tier (Go call-graph → Tier-2, Python source-import → Tier-1) that supersedes a weaker prior judgment (a stronger prior stands — a Tier-1 import proof never downgrades a Tier-2 call-path proof).
SAFETY (security-reviewed): Two RESERVED, mutually-distinct, non-agent/non-human identities: proposer = the scan, verifier = the engine. The domain self-confirm guard is satisfied because they differ, and it stays meaningful for the agent path (no agent is involved; this coordinator is not agent-reachable). The proof IS the evidence: the verdict carries the call path + a fixed deterministic score. No coverage (build failed) mints NOTHING – the weaker prior judgment stands, never a false "not reachable". Only a SUCCESSFUL build yields reachable / not-reachable judgments. Supersession is append-only: a NEW judgment row + an audit entry naming BOTH sides; the prior judgment is never mutated or deleted.
Index ¶
- Constants
- type CacheKey
- type Coordinator
- func NewCoordinator(a analyzer, r recorder, audit ports.AuditLogger, clock ports.Clock) (*Coordinator, error)
- func NewCoordinatorForLanguage(a analyzer, r recorder, audit ports.AuditLogger, clock ports.Clock, ...) (*Coordinator, error)
- func NewCoordinatorForTier(a analyzer, r recorder, audit ports.AuditLogger, clock ports.Clock, ...) (*Coordinator, error)
- func NewJVMVerdictCoordinator(r recorder, audit ports.AuditLogger, clock ports.Clock) (*Coordinator, error)
- func (c *Coordinator) Record(ctx context.Context, engagementID shared.ID, targetRef string, ...) (int, error)
- func (c *Coordinator) RecordVerdicts(ctx context.Context, engagementID shared.ID, ...) (int, error)
- func (c *Coordinator) WithCache(cache ReachabilityCache, fingerprinter ports.ReachabilitySourceFingerprinter, ...) *Coordinator
- func (c *Coordinator) WithRaiseOnly() *Coordinator
- func (c *Coordinator) WithSkipUnresolvedSubjects() *Coordinator
- type InMemoryCache
- type JVMTier2Coordinator
- type Language
- type ReachabilityCache
Constants ¶
const ( DefaultCacheCapacity = 256 DefaultCacheResultBudget = 200_000 // ~ a few hundred MB worst case of Result+Path slices )
DefaultCacheCapacity bounds the process-local cache by entry count so a long-running server that scans many distinct source trees does not grow without limit. DefaultCacheResultBudget additionally bounds it by content size (total reachability.Result entries across all cached Analyses), so a few engagements with thousands of affected symbols cannot dominate RSS the way a pure entry-count bound would. Eviction under either bound only ever forces a recompute, never an unsound verdict.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CacheKey ¶ added in v0.2.0
type CacheKey struct {
SourceHash string
Symbols []string
Tier judgment.ReachabilityTier
AnalyzerVersion string
CoverageModel string
EnvFingerprint string
}
CacheKey binds EVERYTHING that can change a reachability verdict, so a cached whole-graph result is reused only when every verdict-affecting input is unchanged (EPIC #1042 0.7). The soundness bar is that a STALE-KEY NEGATIVE can never be served: a not_reachable served after an input changed would hide a now-reachable vulnerability. Two protections enforce it: (1) Complete() requires every field to be bound, and an incomplete key is never used (the caller recomputes); (2) Fingerprint() hashes the whole tuple, so any changed input yields a different key and misses the cache.
SourceHash is a content hash (Merkle) of the analyzed source tree; Symbols are the affected symbols queried (an advisory revision that changes them changes the key); Tier is the analysis tier; Analyzer version and CoverageModel version bind the analyzer + coverage semantics; EnvFingerprint is a caller-supplied hash of the remaining verdict-affecting environment (dependency/lock graph, symbol-catalog version, language/toolchain version, build tags + GOOS/GOARCH, entrypoint policy, resolver config, generated-source inputs, sandbox/build mode). The caller MUST fold all of those into EnvFingerprint; if it cannot, it leaves EnvFingerprint empty and the key is incomplete, so nothing is cached (recompute).
func (CacheKey) Complete ¶ added in v0.2.0
Complete reports whether every verdict-affecting input is bound. An incomplete key must NEVER be used to read or write the cache, because a missing input could let a stale negative be served.
func (CacheKey) Fingerprint ¶ added in v0.2.0
Fingerprint is the stable content-addressed key over the whole tuple. Symbols are sorted + de-duplicated so subject ordering never changes the key, and every field is length-prefixed so no two distinct tuples can collide by concatenation. Callers must gate on Complete() first; Fingerprint of an incomplete key is still deterministic but must not be used.
type Coordinator ¶
type Coordinator struct {
// contains filtered or unexported fields
}
Coordinator records deterministic reachability judgments from a call-graph/import analyzer. It implements ports.ReachabilityRecorder (a subject is ports.ReachabilitySubject), so the SCA pipeline can drive it without importing this package. The minted claim's tier reflects the ANALYZER's strength of proof: a Go call-graph analyzer proves Tier-2 (a reached call path); a source-import analyzer (e.g. Python) proves Tier-1 (the vulnerable package is/ isn't imported by first-party code) — a weaker but still deterministic signal. The tier is honest per analyzer; it is NOT inflated to Tier-2 for an import-level proof.
func NewCoordinator ¶
func NewCoordinator(a analyzer, r recorder, audit ports.AuditLogger, clock ports.Clock) (*Coordinator, error)
NewCoordinator validates and returns a Tier-2 coordinator (a call-graph analyzer that proves a reached call path — the Go/govulncheck default).
func NewCoordinatorForLanguage ¶ added in v0.1.8
func NewCoordinatorForLanguage(a analyzer, r recorder, audit ports.AuditLogger, clock ports.Clock, tier judgment.ReachabilityTier, language Language) (*Coordinator, error)
NewCoordinatorForLanguage is NewCoordinatorForTier with an explicit Tier-1 source language, so the sealed proof and the audit trail name the engine that actually produced it.
func NewCoordinatorForTier ¶
func NewCoordinatorForTier(a analyzer, r recorder, audit ports.AuditLogger, clock ports.Clock, tier judgment.ReachabilityTier) (*Coordinator, error)
NewCoordinatorForTier validates and returns a coordinator whose minted judgments carry the given tier, honestly reflecting the analyzer's strength of proof (Tier-2 for a call-graph, Tier-1 for source-import reachability). It refuses an unknown tier rather than mint an unrankable claim.
func NewJVMVerdictCoordinator ¶ added in v0.2.0
func NewJVMVerdictCoordinator(r recorder, audit ports.AuditLogger, clock ports.Clock) (*Coordinator, error)
NewJVMVerdictCoordinator returns a Tier-1.5 coordinator for JVM class-reachability that mints from PRE-COMPUTED per-finding verdicts (via RecordVerdicts) instead of running a symbol analyzer, since the jvmreach tagger computes reachability in-scan at the COMPONENT level (the app's class-reference closure), not by affected symbol. It carries no analyzer; RecordVerdicts is its entry point.
func (*Coordinator) Record ¶
func (c *Coordinator) Record(ctx context.Context, engagementID shared.ID, targetRef string, subjects []ports.ReachabilitySubject) (int, error)
Record runs the analyzer over the engagement target ONCE and mints a deterministic reachability judgment (at the coordinator's tier) per subject. It returns the number of judgments minted. A no-coverage error aborts the whole pass (mints nothing – the weaker prior judgments stand). Per subject, a judgment is minted only when it SUPERSEDES the prior reachability judgment (or there is none) – same-or-stronger prior is left untouched (no churn). Subjects must have DISTINCT FindingIDs (the supersession check reads the stored prior, not in-flight mints) – the post-scan trigger produces one Subject per finding.
func (*Coordinator) RecordVerdicts ¶ added in v0.2.0
func (c *Coordinator) RecordVerdicts(ctx context.Context, engagementID shared.ID, verdicts []ports.JVMReachabilityVerdict) (int, error)
RecordVerdicts mints a Tier-1.5 JVM class-reachability judgment per finding from PRE-COMPUTED verdicts, recording the in-scan jvmreach tags as auditable judgments that feed VEX and (for a REACHABLE verdict) the SLA scorer. It runs no analyzer. A judgment is minted only when it SUPERSEDES the prior (a stronger Tier-2 call-graph proof stands, no churn). Tier-1.5 is never a promotable deterministic proof (IsDeterministicReachabilityProof returns false), so a not-reachable verdict is auditable but NEVER becomes a VEX not_affected - correct for a coarse, reflection-blind signal that must only deprioritize.
func (*Coordinator) WithCache ¶ added in v0.2.0
func (c *Coordinator) WithCache(cache ReachabilityCache, fingerprinter ports.ReachabilitySourceFingerprinter, analyzerVersion, coverageModel string) *Coordinator
WithCache attaches a read-through whole-graph Analysis cache. fingerprinter supplies the per-target source hash + environment fingerprint (called once per Record on targetRef); analyzerVersion and coverageModel are the coordinator's static key inputs. The cache is consulted only when the fingerprint succeeds AND the resulting CacheKey is Complete(), so a fingerprint error or an unbound field disables caching for that run (recompute) rather than risk a stale verdict. A nil cache or nil fingerprinter leaves the coordinator uncached (every Record recomputes), which is the default and always sound.
func (*Coordinator) WithRaiseOnly ¶ added in v0.2.0
func (c *Coordinator) WithRaiseOnly() *Coordinator
WithRaiseOnly makes the coordinator mint only reachable (urgency-raising) claims and never a not-reachable (suppressing) one. Use it for an analyzer whose positive direction is sound but whose negative is not, so a reached finding is prioritised while an un-reached one leaves the prior tier standing (no false suppression).
func (*Coordinator) WithSkipUnresolvedSubjects ¶ added in v0.2.0
func (c *Coordinator) WithSkipUnresolvedSubjects() *Coordinator
WithSkipUnresolvedSubjects makes the coordinator treat a subject the analyzer returned no result for as UNKNOWN (mint nothing, prior tier stands) rather than not-reachable. It is for a build-aware analyzer that omits subjects it cannot prove either way; a false not_affected must never come from an unknown.
type InMemoryCache ¶ added in v0.2.0
type InMemoryCache struct {
// contains filtered or unexported fields
}
InMemoryCache is a process-local ReachabilityCache backed by a map guarded by a RWMutex, with FIFO eviction once it exceeds either its entry-count capacity or its total-Result budget. It is the sound core adapter: correct for a single process and for tests. A cross-process persistent adapter (postgres or file) is the follow-on and implements this same interface. It deep-copies on Put and on Get so a caller mutating a returned Analysis can never corrupt a stored entry, and a later mutation of the argument can never reach back into the store.
It is one process-global instance shared across all tenants and engagements, keyed only by the content-addressed fingerprint (source hash + symbols + tier + versions + env), with no tenant component. This is sound: the cached Analysis is a pure function of those inputs, and per-subject verdicts are re-derived after every read from tenant-scoped prior judgments, so no tenant state is baked into a cached graph and a hit changes wall-clock only. The residual is a weak cross-tenant timing oracle (a fast hit reveals that some tenant already scanned a byte-identical tree with the same symbol set); no verdict, path, or source content crosses, since a key match means the trees are byte-identical. A persistent adapter that wants to close even that oracle can fold a per-tenant salt into the key at the cost of cross-tenant reuse.
func NewInMemoryCache ¶ added in v0.2.0
func NewInMemoryCache() *InMemoryCache
NewInMemoryCache returns an empty in-memory cache bounded by DefaultCacheCapacity and DefaultCacheResultBudget.
func NewInMemoryCacheWithCapacity ¶ added in v0.2.0
func NewInMemoryCacheWithCapacity(capacity, resultBudget int) *InMemoryCache
NewInMemoryCacheWithCapacity returns an empty in-memory cache holding at most capacity entries and resultBudget total Result entries (a non-positive value falls back to the corresponding default).
func (*InMemoryCache) Get ¶ added in v0.2.0
func (c *InMemoryCache) Get(_ context.Context, fingerprint string) (*reachability.Analysis, bool, error)
Get returns a deep copy of the stored Analysis for fingerprint, or (nil, false) on a miss.
func (*InMemoryCache) Put ¶ added in v0.2.0
func (c *InMemoryCache) Put(_ context.Context, fingerprint string, analysis *reachability.Analysis) error
Put stores a deep copy of analysis under fingerprint, evicting oldest entries until both the entry-count capacity and the total-Result budget hold. A nil analysis or empty fingerprint is a no-op, so only a real result under a real key is ever cached. A single Analysis larger than the whole budget is still stored (it is the only entry), because refusing to cache it would just recompute it every time.
type JVMTier2Coordinator ¶ added in v0.2.0
type JVMTier2Coordinator struct {
// contains filtered or unexported fields
}
JVMTier2Coordinator scopes affected symbols to their Maven package before delegating to the generic judgment lifecycle. This prevents a reached same-named method in dependency A from raising a finding for dependency B. A missing/non-Maven PURL is unknown attribution and is therefore skipped, never guessed.
func NewJVMTier2Coordinator ¶ added in v0.2.0
func NewJVMTier2Coordinator(a analyzer, r recorder, audit ports.AuditLogger, clock ports.Clock) (*JVMTier2Coordinator, error)
NewJVMTier2Coordinator returns a Tier-2 coordinator permanently configured RAISE-ONLY. It reuses the standard propose->verify->supersede lifecycle and proof-path sealing, while overriding the generic Tier-2 actors before the coordinator can mint anything. Keeping these JVM actors out of the domain proof allowlist is a second fail-safe beyond raiseOnly: even a persisted claim cannot satisfy the deterministic suppression provenance predicate.
func (*JVMTier2Coordinator) Record ¶ added in v0.2.0
func (c *JVMTier2Coordinator) Record(ctx context.Context, engagementID shared.ID, targetRef string, subjects []ports.ReachabilitySubject) (int, error)
Record implements ports.ReachabilityRecorder. Package identity is encoded only inside the JVM analyzer seam; the generic coordinator still performs exact result-to-finding mapping and never sees an ambiguous unscoped JVM symbol.
type Language ¶ added in v0.1.8
type Language string
Language identifies which source language produced a Tier-1 import proof. Tier-1 identities are per-language for the same reason they are per-tier: an audit reader must not see a JavaScript proof attributed to the Python import engine.
const ( LanguageGo Language = "go" LanguagePython Language = "python" LanguageJavaScript Language = "javascript" LanguageRust Language = "rust" LanguagePHP Language = "php" LanguageRuby Language = "ruby" LanguageDotNet Language = "dotnet" LanguageJVM Language = "jvm" LanguageCPP Language = "cpp" LanguageGoBinary Language = "go-binary" )
type ReachabilityCache ¶ added in v0.2.0
type ReachabilityCache interface {
Get(ctx context.Context, fingerprint string) (*reachability.Analysis, bool, error)
Put(ctx context.Context, fingerprint string, analysis *reachability.Analysis) error
}
ReachabilityCache memoizes a whole-graph reachability Analysis under a verdict-complete fingerprint (see CacheKey). It is a read-through cache for the expensive analyzer run: the coordinator always re-derives per-subject verdicts from the Analysis, so caching the graph result never changes a verdict, it only skips recomputing the graph when every verdict-affecting input is unchanged.
The #1-bar (never hide a real vulnerability) is upheld OUTSIDE this interface, by the caller: the coordinator reads/writes the cache only under a CacheKey.Complete() fingerprint, so a partial key can never map to a stored negative. An implementation therefore does not need to reason about staleness; it only needs to be a correct content-addressed store. Get returns (analysis, true) on a hit and (nil, false) on a miss; a hit that returns a nil analysis is treated by the caller as a miss.