- 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.
18 KiB
18 KiB
Staleness & Time Anchor Contract v1.0.0
Status: APPROVED Version: 1.0.0 Effective: 2025-12-19 Owner: AirGap Guild + Findings Ledger Guild Sprint: SPRINT_0510_0001_0001 (unblocks LEDGER-AIRGAP-56-002, LEDGER-AIRGAP-57-001)
1. Purpose
This contract defines how air-gapped StellaOps installations maintain trusted time references, calculate data staleness, and enforce freshness policies. It enables deterministic vulnerability triage even when disconnected from external time sources.
2. Schema References
| Schema | Location |
|---|---|
| Time Anchor | docs/schemas/time-anchor.schema.json |
| Ledger Staleness | docs/schemas/ledger-airgap-staleness.schema.json |
| Sealed Mode | docs/schemas/sealed-mode.schema.json |
3. Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ Air-Gapped Environment │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Mirror │───▶│ AirGap │───▶│ AirGap Time │ │
│ │ Bundle │ │ Controller │ │ Service │ │
│ │ (time anchor)│ └──────────────┘ └──────────────────────┘ │
│ └──────────────┘ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ Staleness Calculator │ │
│ │ (drift, budgets, validation) │ │
│ └──────────────────────────────────────────┘ │
│ │ │ │
│ ┌─────────────┴─────────────────────┴───────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Findings Ledger │ │ Policy Engine │ │
│ │ (staleness tracking) │ │ (evaluation gating) │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
4. Core Types
4.1 TimeAnchor
A cryptographically signed time reference:
public sealed record TimeAnchor
{
/// <summary>RFC 3339 timestamp of the anchor.</summary>
public required DateTimeOffset AnchorTime { get; init; }
/// <summary>Source of the time anchor.</summary>
public required TimeSource Source { get; init; }
/// <summary>Format identifier (roughtime-v1, rfc3161-v1).</summary>
public required string Format { get; init; }
/// <summary>SHA-256 digest of the time token.</summary>
public required string TokenDigest { get; init; }
/// <summary>Signing key fingerprint.</summary>
public string? SignatureFingerprint { get; init; }
/// <summary>Verification status.</summary>
public VerificationStatus? Verification { get; init; }
/// <summary>Monotonic counter for replay protection.</summary>
public long? MonotonicCounter { get; init; }
}
public enum TimeSource
{
Roughtime = 0,
Rfc3161 = 1,
HardwareClock = 2,
AttestationTsa = 3,
Manual = 4,
Unknown = 5
}
public sealed record VerificationStatus
{
public required VerificationState Status { get; init; }
public string? Reason { get; init; }
public DateTimeOffset? VerifiedAt { get; init; }
}
public enum VerificationState
{
Unknown = 0,
Passed = 1,
Failed = 2
}
4.2 StalenessBudget
Configuration for acceptable data freshness:
public sealed record StalenessBudget
{
/// <summary>Budget identifier.</summary>
public required string BudgetId { get; init; }
/// <summary>Domain this budget applies to.</summary>
public required string DomainId { get; init; }
/// <summary>Maximum staleness in seconds before data is stale.</summary>
public required TimeSpan FreshnessThreshold { get; init; }
/// <summary>Warning threshold (percentage of freshness threshold).</summary>
public decimal WarningThresholdPercent { get; init; } = 75m;
/// <summary>Critical threshold (percentage of freshness threshold).</summary>
public decimal CriticalThresholdPercent { get; init; } = 90m;
/// <summary>Grace period after threshold before hard enforcement.</summary>
public TimeSpan GracePeriod { get; init; } = TimeSpan.FromDays(1);
/// <summary>Enforcement mode.</summary>
public EnforcementMode EnforcementMode { get; init; } = EnforcementMode.Strict;
}
public enum EnforcementMode
{
Strict = 0, // Block operations when stale
Warn = 1, // Allow but log warnings
Disabled = 2 // No enforcement
}
4.3 StalenessEvaluation
Result of staleness calculation:
public sealed record StalenessEvaluation
{
/// <summary>Domain evaluated.</summary>
public required string DomainId { get; init; }
/// <summary>Current staleness duration.</summary>
public required TimeSpan CurrentStaleness { get; init; }
/// <summary>Configured threshold.</summary>
public required TimeSpan Threshold { get; init; }
/// <summary>Staleness as percentage of threshold.</summary>
public required decimal PercentOfThreshold { get; init; }
/// <summary>Overall status.</summary>
public required StalenessStatus Status { get; init; }
/// <summary>When data will become stale.</summary>
public DateTimeOffset? ProjectedStaleAt { get; init; }
/// <summary>Time anchor used for calculation.</summary>
public required TimeAnchor TimeAnchor { get; init; }
/// <summary>Last bundle import timestamp.</summary>
public required DateTimeOffset LastImportAt { get; init; }
/// <summary>Source timestamp of last bundle.</summary>
public required DateTimeOffset LastSourceTimestamp { get; init; }
}
public enum StalenessStatus
{
Fresh = 0, // < warning threshold
Warning = 1, // >= warning, < critical
Critical = 2, // >= critical, < threshold
Stale = 3, // >= threshold, < threshold + grace
Breached = 4 // >= threshold + grace
}
4.4 BundleProvenance
Provenance record for imported bundles:
public sealed record BundleProvenance
{
/// <summary>Unique bundle identifier.</summary>
public required Guid BundleId { get; init; }
/// <summary>Bundle domain (vex-advisories, vulnerability-feeds, etc.).</summary>
public required string DomainId { get; init; }
/// <summary>When bundle was imported.</summary>
public required DateTimeOffset ImportedAt { get; init; }
/// <summary>Original generation timestamp from source.</summary>
public required DateTimeOffset SourceTimestamp { get; init; }
/// <summary>Source environment identifier.</summary>
public string? SourceEnvironment { get; init; }
/// <summary>SHA-256 digest of bundle contents.</summary>
public required string BundleDigest { get; init; }
/// <summary>SHA-256 digest of bundle manifest.</summary>
public string? ManifestDigest { get; init; }
/// <summary>Staleness at import time.</summary>
public required TimeSpan StalenessAtImport { get; init; }
/// <summary>Time anchor used for staleness calculation.</summary>
public required TimeAnchor TimeAnchor { get; init; }
/// <summary>DSSE attestation covering this bundle.</summary>
public BundleAttestation? Attestation { get; init; }
/// <summary>Exports included in this bundle.</summary>
public ImmutableArray<ExportRecord> Exports { get; init; }
}
5. Staleness Domains
| Domain ID | Description | Default Threshold | Default Grace |
|---|---|---|---|
vulnerability-feeds |
Advisory and CVE data | 7 days | 1 day |
vex-advisories |
VEX statements | 7 days | 1 day |
scanner-signatures |
Scanner detection rules | 14 days | 3 days |
policy-packs |
Policy bundles | 30 days | 7 days |
trust-roots |
Certificate/key roots | 90 days | 14 days |
runtime-evidence |
Runtime observation data | 1 day | 4 hours |
6. Time Anchor Verification
6.1 Roughtime Verification
public interface IRoughtimeVerifier
{
/// <summary>
/// Verifies a Roughtime response against trusted servers.
/// </summary>
Task<TimeAnchorValidationResult> VerifyAsync(
byte[] roughtimeResponse,
RoughtimeRoot[] trustedRoots,
CancellationToken cancellationToken = default);
}
Roughtime provides:
- Sub-second accuracy with 1-2 second uncertainty
- Ed25519 signatures
- Chain of trust via server public keys
- Radius-based uncertainty bounds
6.2 RFC 3161 Verification
public interface IRfc3161Verifier
{
/// <summary>
/// Verifies an RFC 3161 timestamp token.
/// </summary>
Task<TimeAnchorValidationResult> VerifyAsync(
byte[] timestampToken,
Rfc3161Root[] trustedRoots,
CancellationToken cancellationToken = default);
}
RFC 3161 provides:
- X.509 certificate-based trust
- ASN.1/DER encoded tokens
- Hash algorithm binding
- Nonce for uniqueness
6.3 Validation Result
public sealed record TimeAnchorValidationResult
{
public required bool IsValid { get; init; }
public required TimeAnchor? Anchor { get; init; }
public TimeAnchorError? Error { get; init; }
public TimeSpan? Uncertainty { get; init; }
}
public enum TimeAnchorError
{
None = 0,
SignatureInvalid = 1,
RootNotTrusted = 2,
TokenExpired = 3,
TokenMalformed = 4,
CounterReplay = 5,
UncertaintyTooHigh = 6
}
7. API Endpoints
7.1 AirGap Time Service
| Endpoint | Method | Description |
|---|---|---|
GET /api/v1/time/status |
GET | Current anchor metadata and drift |
GET /api/v1/time/anchor |
GET | Active time anchor |
POST /api/v1/time/anchor |
POST | Import new time anchor |
GET /api/v1/time/metrics |
GET | Prometheus metrics |
GET /api/v1/time/health |
GET | Health check |
7.2 Staleness Endpoints
| Endpoint | Method | Description |
|---|---|---|
GET /api/v1/staleness/domains |
GET | List all domain staleness |
GET /api/v1/staleness/domains/{domainId} |
GET | Get domain staleness |
POST /api/v1/staleness/validate |
POST | Validate staleness for context |
GET /api/v1/staleness/config |
GET | Get staleness configuration |
PUT /api/v1/staleness/config |
PUT | Update staleness configuration |
7.3 Response Formats
{
"domainId": "vex-advisories",
"currentStaleness": "PT172800S",
"threshold": "PT604800S",
"percentOfThreshold": 28.57,
"status": "fresh",
"projectedStaleAt": "2025-12-26T10:00:00Z",
"timeAnchor": {
"anchorTime": "2025-12-19T10:00:00Z",
"source": "roughtime",
"format": "roughtime-v1",
"tokenDigest": "sha256:abc123...",
"verification": {
"status": "passed",
"verifiedAt": "2025-12-19T10:00:01Z"
}
},
"lastImportAt": "2025-12-17T10:00:00Z",
"lastSourceTimestamp": "2025-12-17T08:00:00Z"
}
8. Integration Points
8.1 Findings Ledger Integration
The Ledger tracks staleness per projection:
public interface IStalenessValidationService
{
/// <summary>
/// Validates that data is fresh enough for the given context.
/// </summary>
Task<StalenessValidationResult> ValidateAsync(
string tenantId,
string domainId,
StalenessContext context,
CancellationToken cancellationToken = default);
/// <summary>
/// Updates staleness tracking after bundle import.
/// </summary>
Task UpdateStalenessAsync(
string tenantId,
BundleProvenance provenance,
CancellationToken cancellationToken = default);
}
public enum StalenessContext
{
Export = 0, // Generating exports
Query = 1, // Querying data
PolicyEval = 2, // Policy evaluation
Attestation = 3 // Creating attestations
}
8.2 Policy Engine Integration
Policy Engine gates evaluations based on staleness:
public interface ISealedModeService
{
/// <summary>
/// Checks if sealed mode should block the operation.
/// </summary>
Task<SealedModeDecision> CheckAsync(
string tenantId,
SealedModeContext context,
CancellationToken cancellationToken = default);
}
public sealed record SealedModeDecision
{
public required bool IsBlocked { get; init; }
public SealedModeReason? Reason { get; init; }
public ImmutableArray<StalenessEvaluation> StaleDomains { get; init; }
}
public enum SealedModeReason
{
None = 0,
DataStale = 1,
TimeAnchorMissing = 2,
TimeAnchorExpired = 3,
SignatureInvalid = 4
}
9. Telemetry
9.1 Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
airgap_anchor_age_seconds |
gauge | - | Age of current time anchor |
airgap_anchor_drift_seconds |
gauge | - | Drift from anchor time |
airgap_anchor_expiry_seconds |
gauge | - | Seconds until anchor expires |
airgap_staleness_seconds |
gauge | domain |
Current staleness per domain |
airgap_staleness_threshold_seconds |
gauge | domain |
Threshold per domain |
airgap_staleness_percent |
gauge | domain |
Staleness as % of threshold |
airgap_staleness_status |
gauge | domain, status |
Current status (0=fresh, 3=stale) |
airgap_bundle_imports_total |
counter | domain, result |
Bundle imports |
airgap_validation_total |
counter | domain, context, result |
Staleness validations |
9.2 Alerts
# Recommended alerting rules
groups:
- name: airgap-staleness
rules:
- alert: AirGapDataApproachingStale
expr: airgap_staleness_percent > 75
for: 1h
labels:
severity: warning
annotations:
summary: "{{ $labels.domain }} data approaching staleness"
- alert: AirGapDataStale
expr: airgap_staleness_percent >= 100
for: 5m
labels:
severity: critical
annotations:
summary: "{{ $labels.domain }} data is stale"
- alert: AirGapTimeAnchorMissing
expr: airgap_anchor_age_seconds > 86400
for: 5m
labels:
severity: critical
annotations:
summary: "Time anchor is older than 24 hours"
10. Configuration
# etc/airgap.yaml
AirGap:
Time:
Enabled: true
TrustRootsPath: "/etc/stellaops/trust-roots.json"
MaxAnchorAgeHours: 168 # 7 days
MaxUncertaintyMs: 5000 # 5 seconds
Staleness:
DefaultThresholdDays: 7
DefaultGracePeriodDays: 1
EnforcementMode: "Strict" # Strict, Warn, Disabled
Domains:
vulnerability-feeds:
ThresholdDays: 7
GracePeriodDays: 1
vex-advisories:
ThresholdDays: 7
GracePeriodDays: 1
runtime-evidence:
ThresholdDays: 1
GracePeriodHours: 4
Notifications:
- PercentOfThreshold: 75
Severity: warning
Channels: [slack, metric]
- PercentOfThreshold: 90
Severity: critical
Channels: [email, slack, metric]
11. Error Codes
| Code | Description | Resolution |
|---|---|---|
ERR_AIRGAP_STALE |
Data exceeds staleness threshold | Import fresh bundle |
ERR_AIRGAP_NO_BUNDLE |
No bundle imported for domain | Import initial bundle |
ERR_AIRGAP_TIME_ANCHOR_MISSING |
No time anchor available | Import time anchor with bundle |
ERR_AIRGAP_TIME_DRIFT |
Excessive drift detected | Re-verify time anchor |
ERR_AIRGAP_ATTESTATION_INVALID |
Bundle attestation invalid | Verify bundle source |
ERR_AIRGAP_SIGNATURE_INVALID |
Time token signature invalid | Check trust roots |
ERR_AIRGAP_COUNTER_REPLAY |
Monotonic counter replay | Import newer anchor |
Changelog
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2025-12-19 | Initial release |