- Introduced `VexStatusChipComponent` to display VEX status with color coding and tooltips. - Implemented integration tests for reachability drift detection, covering various scenarios including drift detection, determinism, and error handling. - Enhanced `ScannerToSignalsReachabilityTests` with a null implementation of `ICallGraphSyncService` for better test isolation. - Updated project references to include the new Reachability Drift library.
14 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/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
-
L1: In-Memory Overlay Cache
- Per-request deduplication
- TTL: Request lifetime
- Key:
{tenantId}:{componentPurl}:{advisoryId}
-
L2: Redis Distributed Cache
- Shared across Policy Engine instances
- TTL: 5 minutes (configurable)
- Key:
rf:{tenantId}:{sha256(purl+advisoryId)}
-
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 IDbatch_size: Number of items requestedcache_hits: Items found in cachecache_misses: Items not in cachestore_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" # or "memory"
RedisConnectionString: "${REDIS_URL}"
KeyPrefix: "rf:"
10. Validation Rules
Subject.Purlmust be a valid Package URLReachabilityFact.Confidencemust be 0.0-1.0ReachabilityFact.Statemust be a valid enum valueTimestampmust be valid UTC ISO-8601- At least one of
CveId,GhsaId, orVulnerabilityIdmust be present
Changelog
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2025-12-19 | Initial release |