578 lines
17 KiB
Markdown
578 lines
17 KiB
Markdown
# 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:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```yaml
|
|
# 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:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```yaml
|
|
# 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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```yaml
|
|
match:
|
|
reachability:
|
|
pathHash:
|
|
exists: true
|
|
observedAtRuntime: true
|
|
action: block
|
|
```
|
|
|
|
Require fresh runtime evidence:
|
|
|
|
```yaml
|
|
match:
|
|
reachability:
|
|
runtimeEvidenceAge:
|
|
gt: 24h
|
|
action: warn
|
|
message: "Runtime evidence is stale"
|
|
```
|
|
|
|
Block specific node patterns:
|
|
|
|
```yaml
|
|
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 |
|