reachproof

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 27, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

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

View Source
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

func (k CacheKey) Complete() bool

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

func (k CacheKey) Fingerprint() string

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"
)

func (Language) Valid added in v0.1.8

func (l Language) Valid() bool

Valid reports whether l is a supported Tier-1 language.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL