Files
git.stella-ops.org/docs/modules/policy/contracts/reachability-input-contract.md

17 KiB

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);
}

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:

Predicate Type Description
reachability.state string "reachable", "unreachable", "potentially_reachable", "unknown"
reachability.confidence decimal Confidence score 0.0-1.0
reachability.score decimal Computed risk score
reachability.has_runtime_evidence bool Whether runtime facts support determination
reachability.is_high_confidence bool Confidence >= 0.8
reachability.source string Source of determination
reachability.method string Analysis method used
exploitability.state string "exploitable", "not_exploitable", "conditionally_exploitable", "unknown"
exploitability.epss_score decimal EPSS probability 0.0-1.0
exploitability.epss_percentile decimal EPSS percentile 0-100
exploitability.kev_listed bool In CISA KEV catalog
exploitability.kev_due_date date KEV remediation deadline
exploitability.maturity string "not_defined", "unproven", "poc", "functional", "high"

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

Endpoint Method Description
POST /signals/reachability/recompute POST Recompute reachability for a subject
GET /signals/facts/{subjectKey} GET Get reachability facts for a subject
POST /signals/runtime-facts POST Ingest runtime observations

6.2 Policy Engine Endpoints

Endpoint Method Description
POST /api/policy/evaluate POST Evaluate with reachability enrichment
POST /api/policy/simulate POST Simulate with reachability overrides
GET /api/policy/reachability/stats GET Get 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

  • Facts are invalidated when:
    • New callgraph is ingested
    • Runtime facts are updated
    • Manual override is applied
    • TTL expires

8. Telemetry

8.1 Metrics

Metric Type Labels Description
policy_reachability_applied_total counter state Facts applied to evaluations
policy_reachability_cache_hits_total counter - Cache hits
policy_reachability_cache_misses_total counter - Cache misses
policy_reachability_cache_hit_ratio gauge - Hit ratio (0.0-1.0)
policy_reachability_lookups_total counter outcome Lookup attempts
policy_reachability_lookup_seconds histogram - Lookup latency

8.2 Traces

Activity: reachability_facts.batch_lookup Tags:

  • tenant: Tenant ID
  • batch_size: Number of items requested
  • cache_hits: Items found in cache
  • cache_misses: Items not in cache
  • store_hits: Items fetched from store

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 Path Type Description
reachability.pathHash string Canonical path hash
reachability.nodeHashes array Top-K node hashes
reachability.entryNodeHash string Entry point node hash
reachability.sinkNodeHash string Sink node hash
reachability.runtimeEvidenceAt datetime Runtime observation timestamp
reachability.observedAtRuntime boolean Whether confirmed at runtime
reachability.runtimeEvidenceAge duration Age 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

Version Date Changes
1.1.0 2026-01-14 Added node hash, path gating, and runtime evidence fields (SPRINT_20260112_008)
1.0.0 2025-12-19 Initial release