Reachability Input Contract v1.0.0

Status: APPROVED Version: 1.0.0 Effective: 2025-12-19 Owner: Policy Guild + Signals Guild Sprint: SPRINT_0126_0001_0001 (unblocks POLICY-ENGINE-80-001 through 80-004)


1. Purpose

This contract defines the integration between the Signals service (reachability analysis) and the Policy Engine. It specifies how reachability and exploitability facts flow into policy evaluation, enabling risk-aware decisions based on static analysis, runtime observations, and exploit intelligence.

2. Schema Reference

The canonical JSON schema is at:

docs/modules/policy/schemas/reachability-input.schema.json

3. Data Flow

┌─────────────┐     ┌──────────────┐     ┌───────────────┐     ┌──────────────┐
│ Scanner     │────▶│ Signals      │────▶│ Reachability  │────▶│ Policy       │
│ (callgraph) │     │ Service      │     │ Facts Store   │     │ Engine       │
└─────────────┘     └──────────────┘     └───────────────┘     └──────────────┘
                           │                     ▲
                           │                     │
                    ┌──────▼──────┐              │
                    │ Runtime     │──────────────┘
                    │ Agent       │
                    └─────────────┘

4. Core Types

4.1 ReachabilityInput

The input payload submitted to Policy Engine for evaluation:

public sealed record ReachabilityInput
{
    /// <summary>Subject being evaluated (component + vulnerability).</summary>
    public required Subject Subject { get; init; }

    /// <summary>Static reachability analysis results.</summary>
    public required ImmutableArray<ReachabilityFact> ReachabilityFacts { get; init; }

    /// <summary>Exploitability assessments from KEV, EPSS, vendor advisories.</summary>
    public ImmutableArray<ExploitabilityFact> ExploitabilityFacts { get; init; }

    /// <summary>References to stored callgraphs.</summary>
    public ImmutableArray<CallgraphRef> CallgraphRefs { get; init; }

    /// <summary>Runtime observation facts.</summary>
    public ImmutableArray<RuntimeFact> RuntimeFacts { get; init; }

    /// <summary>Scanner entropy/trust score for confidence weighting.</summary>
    public EntropyScore? EntropyScore { get; init; }

    /// <summary>Input timestamp (UTC).</summary>
    public required DateTimeOffset Timestamp { get; init; }
}

4.2 Subject

public sealed record Subject
{
    /// <summary>Package URL of the component.</summary>
    public required string Purl { get; init; }

    /// <summary>CVE identifier (e.g., CVE-2024-1234).</summary>
    public string? CveId { get; init; }

    /// <summary>GitHub Security Advisory ID.</summary>
    public string? GhsaId { get; init; }

    /// <summary>Internal vulnerability identifier.</summary>
    public string? VulnerabilityId { get; init; }

    /// <summary>Vulnerable symbols/functions in the component.</summary>
    public ImmutableArray<string> AffectedSymbols { get; init; }

    /// <summary>Affected version range (e.g., "<1.2.3").</summary>
    public string? VersionRange { get; init; }
}

4.3 ReachabilityFact

public sealed record ReachabilityFact
{
    /// <summary>Reachability state determination.</summary>
    public required ReachabilityState State { get; init; }

    /// <summary>Confidence score (0.0-1.0).</summary>
    public required decimal Confidence { get; init; }

    /// <summary>Source of determination.</summary>
    public required ReachabilitySource Source { get; init; }

    /// <summary>Analyzer that produced this fact.</summary>
    public string? Analyzer { get; init; }

    /// <summary>Analyzer version.</summary>
    public string? AnalyzerVersion { get; init; }

    /// <summary>Call path from entry point to vulnerable symbol.</summary>
    public CallPath? CallPath { get; init; }

    /// <summary>Entry points that can reach vulnerable code.</summary>
    public ImmutableArray<EntryPoint> EntryPoints { get; init; }

    /// <summary>Supporting evidence.</summary>
    public ReachabilityEvidence? Evidence { get; init; }

    /// <summary>When this fact was evaluated.</summary>
    public DateTimeOffset? EvaluatedAt { get; init; }
}

public enum ReachabilityState
{
    Reachable = 0,
    Unreachable = 1,
    PotentiallyReachable = 2,
    Unknown = 3
}

public enum ReachabilitySource
{
    StaticAnalysis = 0,
    DynamicAnalysis = 1,
    SbomInference = 2,
    Manual = 3,
    External = 4
}

4.4 ExploitabilityFact

public sealed record ExploitabilityFact
{
    /// <summary>Exploitability state.</summary>
    public required ExploitabilityState State { get; init; }

    /// <summary>Confidence score (0.0-1.0).</summary>
    public required decimal Confidence { get; init; }

    /// <summary>Source of determination.</summary>
    public required ExploitabilitySource Source { get; init; }

    /// <summary>EPSS probability score (0.0-1.0).</summary>
    public decimal? EpssScore { get; init; }

    /// <summary>EPSS percentile (0-100).</summary>
    public decimal? EpssPercentile { get; init; }

    /// <summary>Listed in CISA Known Exploited Vulnerabilities.</summary>
    public bool? KevListed { get; init; }

    /// <summary>KEV remediation due date.</summary>
    public DateOnly? KevDueDate { get; init; }

    /// <summary>Exploit maturity level (per CVSS).</summary>
    public ExploitMaturity? ExploitMaturity { get; init; }

    /// <summary>References to known exploits.</summary>
    public ImmutableArray<Uri> ExploitRefs { get; init; }

    /// <summary>Conditions required for exploitation.</summary>
    public ImmutableArray<ExploitCondition> Conditions { get; init; }

    /// <summary>When this fact was evaluated.</summary>
    public DateTimeOffset? EvaluatedAt { get; init; }
}

public enum ExploitabilityState
{
    Exploitable = 0,
    NotExploitable = 1,
    ConditionallyExploitable = 2,
    Unknown = 3
}

public enum ExploitabilitySource
{
    Kev = 0,
    Epss = 1,
    VendorAdvisory = 2,
    InternalAnalysis = 3,
    ExploitDb = 4
}

public enum ExploitMaturity
{
    NotDefined = 0,
    Unproven = 1,
    Poc = 2,
    Functional = 3,
    High = 4
}

4.5 RuntimeFact

public sealed record RuntimeFact
{
    /// <summary>Type of runtime observation.</summary>
    public required RuntimeFactType Type { get; init; }

    /// <summary>Observed symbol/function.</summary>
    public string? Symbol { get; init; }

    /// <summary>Observed module.</summary>
    public string? Module { get; init; }

    /// <summary>Number of times called.</summary>
    public int? CallCount { get; init; }

    /// <summary>Last invocation time.</summary>
    public DateTimeOffset? LastCalled { get; init; }

    /// <summary>When observation was recorded.</summary>
    public required DateTimeOffset ObservedAt { get; init; }

    /// <summary>Observation window duration (e.g., "7d").</summary>
    public string? ObservationWindow { get; init; }

    /// <summary>Environment where observed.</summary>
    public RuntimeEnvironment? Environment { get; init; }
}

public enum RuntimeFactType
{
    FunctionCalled = 0,
    FunctionNotCalled = 1,
    PathExecuted = 2,
    PathNotExecuted = 3,
    ModuleLoaded = 4,
    ModuleNotLoaded = 5
}

public enum RuntimeEnvironment
{
    Production = 0,
    Staging = 1,
    Development = 2,
    Test = 3
}

5. Policy Engine Integration

5.1 ReachabilityFactsJoiningService

The ReachabilityFactsJoiningService provides efficient batch lookups with caching:

public interface IReachabilityFactsJoiningService
{
    /// <summary>
    /// Gets reachability facts for a batch of component-advisory pairs.
    /// Uses cache-first strategy with store fallback.
    /// </summary>
    Task<ReachabilityFactsBatch> GetFactsBatchAsync(
        string tenantId,
        IReadOnlyList<ReachabilityFactsRequest> items,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Enriches signal context with reachability facts.
    /// </summary>
    Task<bool> EnrichSignalsAsync(
        string tenantId,
        string componentPurl,
        string advisoryId,
        IDictionary<string, object?> signals,
        CancellationToken cancellationToken = default);
}

The live Policy Engine host now resolves IReachabilityFactsStore through SignalsBackedReachabilityFactsStore, with the Signals client bound from the ReachabilitySignals configuration section. The overlay cache remains a local transport-pluggable cache in this slice.

5.2 SPL Predicates

Reachability is exposed in SPL (StellaOps Policy Language) via the reachability scope:

# Example SPL rule using reachability predicates
rules:
  - name: "Suppress unreachable critical CVEs"
    when:
      all:
        - severity >= critical
        - reachability.state == "unreachable"
        - reachability.confidence >= 0.9
    then:
      effect: suppress
      justification: "Unreachable code path with high confidence"

  - name: "Escalate reachable with exploit"
    when:
      all:
        - reachability.state == "reachable"
        - exploitability.kev_listed == true
    then:
      effect: escalate
      priority: critical

Available predicates:

PredicateTypeDescription
reachability.statestring“reachable”, “unreachable”, “potentially_reachable”, “unknown”, “computing”
reachability.confidencedecimalConfidence score 0.0-1.0
reachability.scoredecimalComputed risk score
reachability.has_runtime_evidenceboolWhether runtime facts support determination
reachability.is_high_confidenceboolConfidence >= 0.8
reachability.sourcestringSource of determination
reachability.methodstringAnalysis method used
exploitability.statestring“exploitable”, “not_exploitable”, “conditionally_exploitable”, “unknown”
exploitability.epss_scoredecimalEPSS probability 0.0-1.0
exploitability.epss_percentiledecimalEPSS percentile 0-100
exploitability.kev_listedboolIn CISA KEV catalog
exploitability.kev_due_datedateKEV remediation deadline
exploitability.maturitystring“not_defined”, “unproven”, “poc”, “functional”, “high”

computing means call-path evidence is queued or running and must not be treated as a reachability proof. Promote-time evidence cites matchClass=reachability only for completed reachable findings; CVE-id presence matches remain matchClass=denylist.

5.2.1 Promote-time evidence classes

Policy evidence must keep the rule class visible to operators:

matchClassMeaningCan prove reachability?
reachabilityA severity/reachability rule matched a completed reachable finding.Yes
denylistA CVE-id, package, or other presence-based backstop matched.No

The class is semantic, not cosmetic. A base-image denylist rule for busybox, musl, openssl, or another OS package remains matchClass=denylist unless the finding also carries explicit reachability evidence. A rule rationale such as “used by PID 1” or “loaded by libc” does not by itself satisfy reachability.state == "reachable".

5.3 ReachabilityOutput

Policy evaluation produces enriched output:

public sealed record ReachabilityOutput
{
    /// <summary>Subject evaluated.</summary>
    public required Subject Subject { get; init; }

    /// <summary>Effective reachability state after policy rules.</summary>
    public required ReachabilityState EffectiveState { get; init; }

    /// <summary>Effective exploitability after policy rules.</summary>
    public ExploitabilityState? EffectiveExploitability { get; init; }

    /// <summary>Risk adjustment from policy evaluation.</summary>
    public required RiskAdjustment RiskAdjustment { get; init; }

    /// <summary>Policy rule trace.</summary>
    public ImmutableArray<PolicyRuleTrace> PolicyTrace { get; init; }

    /// <summary>When evaluation occurred.</summary>
    public required DateTimeOffset EvaluatedAt { get; init; }
}

public sealed record RiskAdjustment
{
    /// <summary>Risk multiplier (0=suppress, 1=neutral, >1=amplify).</summary>
    public required decimal Factor { get; init; }

    /// <summary>Severity override if rules dictate.</summary>
    public Severity? SeverityOverride { get; init; }

    /// <summary>Justification for adjustment.</summary>
    public string? Justification { get; init; }
}

6. API Endpoints

6.1 Signals Service Endpoints

EndpointMethodDescription
POST /signals/reachability/recomputePOSTRecompute reachability for a subject
GET /signals/facts/{subjectKey}GETGet reachability facts for a subject
POST /signals/runtime-factsPOSTIngest runtime observations

6.2 Policy Engine Endpoints

EndpointMethodDescription
POST /api/policy/evaluatePOSTEvaluate with reachability enrichment
POST /api/policy/simulatePOSTSimulate with reachability overrides
GET /api/policy/reachability/statsGETGet reachability integration metrics

7. Caching Strategy

7.1 Cache Layers

  1. L1: In-Memory Overlay Cache

    • Per-request deduplication
    • TTL: Request lifetime
    • Key: {tenantId}:{componentPurl}:{advisoryId}
  2. L2: Redis Distributed Cache

    • Shared across Policy Engine instances
    • TTL: 5 minutes (configurable)
    • Key: rf:{tenantId}:{sha256(purl+advisoryId)}
  3. L3: Postgres Facts Store

    • Authoritative source
    • Indexed by (tenant_id, component_purl, advisory_id)

7.2 Cache Invalidation

8. Telemetry

8.1 Metrics

MetricTypeLabelsDescription
policy_reachability_applied_totalcounterstateFacts applied to evaluations
policy_reachability_cache_hits_totalcounter-Cache hits
policy_reachability_cache_misses_totalcounter-Cache misses
policy_reachability_cache_hit_ratiogauge-Hit ratio (0.0-1.0)
policy_reachability_lookups_totalcounteroutcomeLookup attempts
policy_reachability_lookup_secondshistogram-Lookup latency

8.2 Traces

Activity: reachability_facts.batch_lookup Tags:

9. Configuration

# etc/policy-engine.yaml
PolicyEngine:
  Reachability:
    Enabled: true
    CacheTtlSeconds: 300
    MaxBatchSize: 1000
    DefaultConfidenceThreshold: 0.7
    HighConfidenceThreshold: 0.9

  ReachabilityCache:
    Type: "redis"  # Valkey (Redis-compatible) or "memory"
    RedisConnectionString: "${REDIS_URL}"
    KeyPrefix: "rf:"

10. Validation Rules

  1. Subject.Purl must be a valid Package URL
  2. ReachabilityFact.Confidence must be 0.0-1.0
  3. ReachabilityFact.State must be a valid enum value
  4. Timestamp must be valid UTC ISO-8601
  5. At least one of CveId, GhsaId, or VulnerabilityId must be present

11. Node Hash and Path Gating Extensions

Sprint: SPRINT_20260112_008_DOCS_path_witness_contracts (PW-DOC-004)

11.1 Extended ReachabilityInput Fields

The following fields extend ReachabilityInput for path-level gating:

public sealed record ReachabilityInput
{
    // ... existing fields ...

    /// <summary>Canonical path hash computed from entry to sink.</summary>
    public string? PathHash { get; init; }

    /// <summary>Top-K node hashes along the path.</summary>
    public ImmutableArray<string> NodeHashes { get; init; }

    /// <summary>Entry point node hash.</summary>
    public string? EntryNodeHash { get; init; }

    /// <summary>Sink (vulnerable symbol) node hash.</summary>
    public string? SinkNodeHash { get; init; }

    /// <summary>When runtime evidence was observed (UTC).</summary>
    public DateTimeOffset? RuntimeEvidenceAt { get; init; }

    /// <summary>Whether path was observed at runtime.</summary>
    public bool ObservedAtRuntime { get; init; }
}

11.2 Node Hash Computation

Node hashes are computed using the canonical recipe:

nodeHash = SHA256(normalize(purl) + ":" + normalize(symbol))

See docs/contracts/witness-v1.md for normalization rules.

11.3 Policy DSL Access

The following fields are exposed in policy evaluation context:

DSL PathTypeDescription
reachability.pathHashstringCanonical path hash
reachability.nodeHashesarrayTop-K node hashes
reachability.entryNodeHashstringEntry point node hash
reachability.sinkNodeHashstringSink node hash
reachability.runtimeEvidenceAtdatetimeRuntime observation timestamp
reachability.observedAtRuntimebooleanWhether confirmed at runtime
reachability.runtimeEvidenceAgedurationAge of runtime evidence

11.4 Path Gating Examples

Block paths confirmed at runtime:

match:
  reachability:
    pathHash:
      exists: true
    observedAtRuntime: true
action: block

Require fresh runtime evidence:

match:
  reachability:
    runtimeEvidenceAge:
      gt: 24h
action: warn
message: "Runtime evidence is stale"

Block specific node patterns:

match:
  reachability:
    nodeHashes:
      contains_any:
        - "sha256:critical-auth-node..."
action: block

11.5 Runtime Evidence Freshness

Runtime evidence age is computed as:

runtimeEvidenceAge = now() - runtimeEvidenceAt

Freshness thresholds can be configured per environment in DeterminizationOptions.


Changelog

VersionDateChanges
1.1.02026-01-14Added node hash, path gating, and runtime evidence fields (SPRINT_20260112_008)
1.0.02025-12-19Initial release