Files
git.stella-ops.org/docs/modules/findings-ledger/contracts/staleness-time-anchor-contract.md
StellaOps Bot 5fc469ad98 feat: Add VEX Status Chip component and integration tests for reachability drift detection
- 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.
2025-12-20 01:26:42 +02:00

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