feat(telemetry): add telemetry client and services for tracking events
- Implemented TelemetryClient to handle event queuing and flushing to the telemetry endpoint. - Created TtfsTelemetryService for emitting specific telemetry events related to TTFS. - Added tests for TelemetryClient to ensure event queuing and flushing functionality. - Introduced models for reachability drift detection, including DriftResult and DriftedSink. - Developed DriftApiService for interacting with the drift detection API. - Updated FirstSignalCardComponent to emit telemetry events on signal appearance. - Enhanced localization support for first signal component with i18n strings.
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IPolicyDecisionAttestationService.cs
|
||||
// Sprint: SPRINT_3801_0001_0001_policy_decision_attestation
|
||||
// Description: Interface for creating signed policy decision attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Service for creating signed policy decision attestations.
|
||||
/// Creates stella.ops/policy-decision@v1 predicates wrapped in DSSE envelopes.
|
||||
/// </summary>
|
||||
public interface IPolicyDecisionAttestationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a signed attestation for a policy decision.
|
||||
/// </summary>
|
||||
/// <param name="request">The attestation creation request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The signed attestation result.</returns>
|
||||
Task<PolicyDecisionAttestationResult> CreateAttestationAsync(
|
||||
PolicyDecisionAttestationRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Submits an attestation to Rekor for transparency logging.
|
||||
/// </summary>
|
||||
/// <param name="attestationDigest">Digest of the attestation to submit.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The Rekor submission result.</returns>
|
||||
Task<RekorSubmissionResult> SubmitToRekorAsync(
|
||||
string attestationDigest,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a policy decision attestation.
|
||||
/// </summary>
|
||||
/// <param name="attestationDigest">Digest of the attestation to verify.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The verification result.</returns>
|
||||
Task<PolicyDecisionVerificationResult> VerifyAsync(
|
||||
string attestationDigest,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for creating a policy decision attestation.
|
||||
/// </summary>
|
||||
public sealed record PolicyDecisionAttestationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The policy decision predicate to attest.
|
||||
/// </summary>
|
||||
public required PolicyDecisionPredicate Predicate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject artifacts to attach to the attestation.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<AttestationSubject> Subjects { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Key ID to use for signing (null for default).
|
||||
/// </summary>
|
||||
public string? KeyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to submit to Rekor after signing.
|
||||
/// </summary>
|
||||
public bool SubmitToRekor { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID for multi-tenant scenarios.
|
||||
/// </summary>
|
||||
public string? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Correlation ID for tracing.
|
||||
/// </summary>
|
||||
public string? CorrelationId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subject artifact for the attestation.
|
||||
/// </summary>
|
||||
public sealed record AttestationSubject
|
||||
{
|
||||
/// <summary>
|
||||
/// Subject name (e.g., image reference).
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Digest map (algorithm → value).
|
||||
/// </summary>
|
||||
public required IReadOnlyDictionary<string, string> Digest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of creating a policy decision attestation.
|
||||
/// </summary>
|
||||
public sealed record PolicyDecisionAttestationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the attestation was created successfully.
|
||||
/// </summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Digest of the created attestation (prefixed).
|
||||
/// </summary>
|
||||
public string? AttestationDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Key ID that was used for signing.
|
||||
/// </summary>
|
||||
public string? KeyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor submission result (if submitted).
|
||||
/// </summary>
|
||||
public RekorSubmissionResult? RekorResult { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message (if failed).
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the attestation was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of Rekor submission.
|
||||
/// </summary>
|
||||
public sealed record RekorSubmissionResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether submission succeeded.
|
||||
/// </summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor log index.
|
||||
/// </summary>
|
||||
public long? LogIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor entry UUID.
|
||||
/// </summary>
|
||||
public string? Uuid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Integrated timestamp.
|
||||
/// </summary>
|
||||
public DateTimeOffset? IntegratedTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message (if failed).
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of verifying a policy decision attestation.
|
||||
/// </summary>
|
||||
public sealed record PolicyDecisionVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether verification succeeded.
|
||||
/// </summary>
|
||||
public required bool Valid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The verified predicate (if valid).
|
||||
/// </summary>
|
||||
public PolicyDecisionPredicate? Predicate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signer identity.
|
||||
/// </summary>
|
||||
public string? SignerIdentity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor verification status.
|
||||
/// </summary>
|
||||
public bool? RekorVerified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verification issues.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Issues { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PolicyDecisionAttestationOptions.cs
|
||||
// Sprint: SPRINT_3801_0001_0001_policy_decision_attestation
|
||||
// Description: Configuration options for policy decision attestation service.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for <see cref="PolicyDecisionAttestationService"/>.
|
||||
/// </summary>
|
||||
public sealed class PolicyDecisionAttestationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name.
|
||||
/// </summary>
|
||||
public const string SectionName = "PolicyDecisionAttestation";
|
||||
|
||||
/// <summary>
|
||||
/// Whether attestation creation is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use the Signer service for signing.
|
||||
/// If false, attestations will be created unsigned (for dev/test only).
|
||||
/// </summary>
|
||||
public bool UseSignerService { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Default key ID to use for signing (null = use signer default).
|
||||
/// </summary>
|
||||
public string? DefaultKeyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to submit attestations to Rekor by default.
|
||||
/// </summary>
|
||||
public bool SubmitToRekorByDefault { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Rekor server URL (null = use default Sigstore Rekor).
|
||||
/// </summary>
|
||||
public string? RekorUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default TTL for attestation validity (hours).
|
||||
/// </summary>
|
||||
[Range(1, 8760)] // 1 hour to 1 year
|
||||
public int DefaultTtlHours { get; set; } = 24;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include evidence references by default.
|
||||
/// </summary>
|
||||
public bool IncludeEvidenceRefs { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include gate details in attestations.
|
||||
/// </summary>
|
||||
public bool IncludeGateDetails { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include violation details in attestations.
|
||||
/// </summary>
|
||||
public bool IncludeViolationDetails { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of violations to include in an attestation.
|
||||
/// </summary>
|
||||
[Range(1, 1000)]
|
||||
public int MaxViolationsToInclude { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to log attestation creation events.
|
||||
/// </summary>
|
||||
public bool EnableAuditLogging { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for signer service calls (seconds).
|
||||
/// </summary>
|
||||
[Range(1, 300)]
|
||||
public int SignerTimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for Rekor submissions (seconds).
|
||||
/// </summary>
|
||||
[Range(1, 300)]
|
||||
public int RekorTimeoutSeconds { get; set; } = 60;
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PolicyDecisionAttestationService.cs
|
||||
// Sprint: SPRINT_3801_0001_0001_policy_decision_attestation
|
||||
// Description: Service for creating signed policy decision attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Engine.Telemetry;
|
||||
using StellaOps.Policy.Engine.Vex;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IPolicyDecisionAttestationService"/>.
|
||||
/// Creates stella.ops/policy-decision@v1 attestations wrapped in DSSE envelopes.
|
||||
/// </summary>
|
||||
public sealed class PolicyDecisionAttestationService : IPolicyDecisionAttestationService
|
||||
{
|
||||
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly IVexSignerClient? _signerClient;
|
||||
private readonly IVexRekorClient? _rekorClient;
|
||||
private readonly IOptionsMonitor<PolicyDecisionAttestationOptions> _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<PolicyDecisionAttestationService> _logger;
|
||||
|
||||
public PolicyDecisionAttestationService(
|
||||
IVexSignerClient? signerClient,
|
||||
IVexRekorClient? rekorClient,
|
||||
IOptionsMonitor<PolicyDecisionAttestationOptions> options,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<PolicyDecisionAttestationService> logger)
|
||||
{
|
||||
_signerClient = signerClient;
|
||||
_rekorClient = rekorClient;
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<PolicyDecisionAttestationResult> CreateAttestationAsync(
|
||||
PolicyDecisionAttestationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
using var activity = PolicyEngineTelemetry.ActivitySource.StartActivity(
|
||||
"policy_decision.attest",
|
||||
ActivityKind.Internal);
|
||||
activity?.SetTag("tenant", request.TenantId);
|
||||
activity?.SetTag("policy_id", request.Predicate.Policy.Id);
|
||||
activity?.SetTag("decision", request.Predicate.Result.Decision.ToString());
|
||||
|
||||
var options = _options.CurrentValue;
|
||||
|
||||
if (!options.Enabled)
|
||||
{
|
||||
_logger.LogDebug("Policy decision attestation is disabled");
|
||||
return new PolicyDecisionAttestationResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "Attestation creation is disabled"
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Build the in-toto statement
|
||||
var statement = BuildStatement(request);
|
||||
var statementJson = SerializeCanonical(statement);
|
||||
var payloadBase64 = Convert.ToBase64String(statementJson);
|
||||
|
||||
// Sign the payload
|
||||
string? attestationDigest;
|
||||
string? keyId;
|
||||
|
||||
if (_signerClient is not null && options.UseSignerService)
|
||||
{
|
||||
var signResult = await _signerClient.SignAsync(
|
||||
new VexSignerRequest
|
||||
{
|
||||
PayloadType = PredicateTypes.StellaOpsPolicyDecision,
|
||||
PayloadBase64 = payloadBase64,
|
||||
KeyId = request.KeyId ?? options.DefaultKeyId,
|
||||
TenantId = request.TenantId
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!signResult.Success)
|
||||
{
|
||||
_logger.LogWarning("Failed to sign policy decision attestation: {Error}", signResult.Error);
|
||||
return new PolicyDecisionAttestationResult
|
||||
{
|
||||
Success = false,
|
||||
Error = signResult.Error ?? "Signing failed"
|
||||
};
|
||||
}
|
||||
|
||||
// Compute attestation digest from signed payload
|
||||
attestationDigest = ComputeDigest(statementJson);
|
||||
keyId = signResult.KeyId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create unsigned attestation (dev/test mode)
|
||||
attestationDigest = ComputeDigest(statementJson);
|
||||
keyId = null;
|
||||
_logger.LogDebug("Created unsigned attestation (signer service not available)");
|
||||
}
|
||||
|
||||
// Submit to Rekor if requested
|
||||
RekorSubmissionResult? rekorResult = null;
|
||||
var shouldSubmitToRekor = request.SubmitToRekor || options.SubmitToRekorByDefault;
|
||||
|
||||
if (shouldSubmitToRekor && attestationDigest is not null)
|
||||
{
|
||||
rekorResult = await SubmitToRekorAsync(attestationDigest, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!rekorResult.Success)
|
||||
{
|
||||
_logger.LogWarning("Rekor submission failed: {Error}", rekorResult.Error);
|
||||
// Don't fail the attestation creation, just log the warning
|
||||
}
|
||||
}
|
||||
|
||||
if (options.EnableAuditLogging)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Created policy decision attestation for policy {PolicyId} with decision {Decision}. Digest: {Digest}",
|
||||
request.Predicate.Policy.Id,
|
||||
request.Predicate.Result.Decision,
|
||||
attestationDigest);
|
||||
}
|
||||
|
||||
return new PolicyDecisionAttestationResult
|
||||
{
|
||||
Success = true,
|
||||
AttestationDigest = attestationDigest,
|
||||
KeyId = keyId,
|
||||
RekorResult = rekorResult,
|
||||
CreatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create policy decision attestation");
|
||||
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
|
||||
|
||||
return new PolicyDecisionAttestationResult
|
||||
{
|
||||
Success = false,
|
||||
Error = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<RekorSubmissionResult> SubmitToRekorAsync(
|
||||
string attestationDigest,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO: Implement Rekor submission with proper VexRekorSubmitRequest
|
||||
// This requires building the full DSSE envelope and submitting it
|
||||
// For now, return a placeholder result
|
||||
|
||||
if (_rekorClient is null)
|
||||
{
|
||||
return Task.FromResult(new RekorSubmissionResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "Rekor client not available"
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogDebug("Rekor submission for policy decisions not yet implemented: {Digest}", attestationDigest);
|
||||
|
||||
return Task.FromResult(new RekorSubmissionResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "Policy decision Rekor submission not yet implemented"
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<PolicyDecisionVerificationResult> VerifyAsync(
|
||||
string attestationDigest,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO: Implement verification logic
|
||||
// This would involve:
|
||||
// 1. Fetch the attestation from storage
|
||||
// 2. Verify the DSSE signature
|
||||
// 3. Optionally verify Rekor inclusion
|
||||
// 4. Parse and return the predicate
|
||||
|
||||
_logger.LogWarning("Attestation verification not yet implemented");
|
||||
|
||||
await Task.CompletedTask;
|
||||
|
||||
return new PolicyDecisionVerificationResult
|
||||
{
|
||||
Valid = false,
|
||||
Issues = new[] { "Verification not yet implemented" }
|
||||
};
|
||||
}
|
||||
|
||||
private InTotoStatement<PolicyDecisionPredicate> BuildStatement(
|
||||
PolicyDecisionAttestationRequest request)
|
||||
{
|
||||
var subjects = request.Subjects.Select(s => new InTotoSubject
|
||||
{
|
||||
Name = s.Name,
|
||||
Digest = s.Digest.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
|
||||
}).ToList();
|
||||
|
||||
var options = _options.CurrentValue;
|
||||
|
||||
// Apply TTL
|
||||
var predicate = request.Predicate with
|
||||
{
|
||||
ExpiresAt = request.Predicate.ExpiresAt ??
|
||||
_timeProvider.GetUtcNow().AddHours(options.DefaultTtlHours),
|
||||
CorrelationId = request.CorrelationId ?? request.Predicate.CorrelationId
|
||||
};
|
||||
|
||||
// Trim violations if needed
|
||||
if (predicate.Result.Violations?.Count > options.MaxViolationsToInclude)
|
||||
{
|
||||
predicate = predicate with
|
||||
{
|
||||
Result = predicate.Result with
|
||||
{
|
||||
Violations = predicate.Result.Violations
|
||||
.Take(options.MaxViolationsToInclude)
|
||||
.ToList()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return new InTotoStatement<PolicyDecisionPredicate>
|
||||
{
|
||||
Type = "https://in-toto.io/Statement/v1",
|
||||
Subject = subjects,
|
||||
PredicateType = PredicateTypes.StellaOpsPolicyDecision,
|
||||
Predicate = predicate
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] SerializeCanonical<T>(T value)
|
||||
{
|
||||
return JsonSerializer.SerializeToUtf8Bytes(value, CanonicalJsonOptions);
|
||||
}
|
||||
|
||||
private static string ComputeDigest(byte[] data)
|
||||
{
|
||||
var hash = SHA256.HashData(data);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// in-toto Statement structure.
|
||||
/// </summary>
|
||||
internal sealed record InTotoStatement<TPredicate>
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("_type")]
|
||||
public required string Type { get; init; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("subject")]
|
||||
public required IReadOnlyList<InTotoSubject> Subject { get; init; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("predicateType")]
|
||||
public required string PredicateType { get; init; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("predicate")]
|
||||
public required TPredicate Predicate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// in-toto Subject structure.
|
||||
/// </summary>
|
||||
internal sealed record InTotoSubject
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("digest")]
|
||||
public required Dictionary<string, string> Digest { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PolicyDecisionPredicate.cs
|
||||
// Sprint: SPRINT_3801_0001_0001_policy_decision_attestation
|
||||
// Description: Predicate model for stella.ops/policy-decision@v1 attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Predicate for policy decision attestations (stella.ops/policy-decision@v1).
|
||||
/// Captures policy gate results with references to input evidence (SBOM, VEX, RichGraph).
|
||||
/// </summary>
|
||||
public sealed record PolicyDecisionPredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version for the predicate.
|
||||
/// </summary>
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Policy identifier that was evaluated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policy")]
|
||||
public required PolicyReference Policy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Input evidence that was evaluated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("inputs")]
|
||||
public required PolicyDecisionInputs Inputs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Decision result.
|
||||
/// </summary>
|
||||
[JsonPropertyName("result")]
|
||||
public required PolicyDecisionResult Result { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional evaluation context (environment, tenant, etc.).
|
||||
/// </summary>
|
||||
[JsonPropertyName("context")]
|
||||
public PolicyDecisionContext? Context { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the decision was made.
|
||||
/// </summary>
|
||||
[JsonPropertyName("decided_at")]
|
||||
public DateTimeOffset DecidedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// When the decision expires (for caching).
|
||||
/// </summary>
|
||||
[JsonPropertyName("expires_at")]
|
||||
public DateTimeOffset? ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Correlation ID for tracing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("correlation_id")]
|
||||
public string? CorrelationId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the policy that was evaluated.
|
||||
/// </summary>
|
||||
public sealed record PolicyReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Policy identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy version.
|
||||
/// </summary>
|
||||
[JsonPropertyName("version")]
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy name (human-readable).
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content hash of the policy (for integrity).
|
||||
/// </summary>
|
||||
[JsonPropertyName("digest")]
|
||||
public string? Digest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source of the policy (registry URL, path).
|
||||
/// </summary>
|
||||
[JsonPropertyName("source")]
|
||||
public string? Source { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input evidence references that were evaluated.
|
||||
/// </summary>
|
||||
public sealed record PolicyDecisionInputs
|
||||
{
|
||||
/// <summary>
|
||||
/// References to SBOM attestations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sbom_refs")]
|
||||
public IReadOnlyList<EvidenceReference>? SbomRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// References to VEX attestations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("vex_refs")]
|
||||
public IReadOnlyList<EvidenceReference>? VexRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// References to RichGraph/reachability attestations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("graph_refs")]
|
||||
public IReadOnlyList<EvidenceReference>? GraphRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// References to scan result attestations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("scan_refs")]
|
||||
public IReadOnlyList<EvidenceReference>? ScanRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// References to other input attestations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("other_refs")]
|
||||
public IReadOnlyList<EvidenceReference>? OtherRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject artifacts being evaluated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("subjects")]
|
||||
public IReadOnlyList<SubjectReference>? Subjects { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to an evidence attestation.
|
||||
/// </summary>
|
||||
public sealed record EvidenceReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Attestation digest (prefixed, e.g., "sha256:abc123").
|
||||
/// </summary>
|
||||
[JsonPropertyName("digest")]
|
||||
public required string Digest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Predicate type of the referenced attestation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicate_type")]
|
||||
public string? PredicateType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional Rekor log index for transparency.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rekor_log_index")]
|
||||
public long? RekorLogIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the attestation was fetched/verified.
|
||||
/// </summary>
|
||||
[JsonPropertyName("fetched_at")]
|
||||
public DateTimeOffset? FetchedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to a subject artifact.
|
||||
/// </summary>
|
||||
public sealed record SubjectReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Subject name (image name, package name).
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject digest (prefixed).
|
||||
/// </summary>
|
||||
[JsonPropertyName("digest")]
|
||||
public required string Digest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional PURL for package subjects.
|
||||
/// </summary>
|
||||
[JsonPropertyName("purl")]
|
||||
public string? Purl { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy decision result.
|
||||
/// </summary>
|
||||
public sealed record PolicyDecisionResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Overall decision (allow, deny, warn).
|
||||
/// </summary>
|
||||
[JsonPropertyName("decision")]
|
||||
public required PolicyDecision Decision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable summary.
|
||||
/// </summary>
|
||||
[JsonPropertyName("summary")]
|
||||
public string? Summary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Individual gate results.
|
||||
/// </summary>
|
||||
[JsonPropertyName("gates")]
|
||||
public IReadOnlyList<PolicyGateResult>? Gates { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Violations found (if any).
|
||||
/// </summary>
|
||||
[JsonPropertyName("violations")]
|
||||
public IReadOnlyList<PolicyViolation>? Violations { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Score breakdown.
|
||||
/// </summary>
|
||||
[JsonPropertyName("scores")]
|
||||
public PolicyScores? Scores { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy decision outcome.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<PolicyDecision>))]
|
||||
public enum PolicyDecision
|
||||
{
|
||||
/// <summary>Policy passed, artifact is allowed.</summary>
|
||||
Allow,
|
||||
|
||||
/// <summary>Policy failed, artifact is denied.</summary>
|
||||
Deny,
|
||||
|
||||
/// <summary>Policy passed with warnings.</summary>
|
||||
Warn,
|
||||
|
||||
/// <summary>Policy evaluation is pending (async approval).</summary>
|
||||
Pending
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result for a single policy gate.
|
||||
/// </summary>
|
||||
public sealed record PolicyGateResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gate identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("gate_id")]
|
||||
public required string GateId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gate name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gate result (pass, fail, skip).
|
||||
/// </summary>
|
||||
[JsonPropertyName("result")]
|
||||
public required GateResult Result { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for the result.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reason")]
|
||||
public string? Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this gate is blocking (vs advisory).
|
||||
/// </summary>
|
||||
[JsonPropertyName("blocking")]
|
||||
public bool Blocking { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gate evaluation result.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<GateResult>))]
|
||||
public enum GateResult
|
||||
{
|
||||
Pass,
|
||||
Fail,
|
||||
Skip,
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy violation detail.
|
||||
/// </summary>
|
||||
public sealed record PolicyViolation
|
||||
{
|
||||
/// <summary>
|
||||
/// Violation code/identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public required string Code { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Severity (critical, high, medium, low).
|
||||
/// </summary>
|
||||
[JsonPropertyName("severity")]
|
||||
public required string Severity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public required string Message { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Related CVE (if applicable).
|
||||
/// </summary>
|
||||
[JsonPropertyName("cve")]
|
||||
public string? Cve { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Related component (if applicable).
|
||||
/// </summary>
|
||||
[JsonPropertyName("component")]
|
||||
public string? Component { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Remediation guidance.
|
||||
/// </summary>
|
||||
[JsonPropertyName("remediation")]
|
||||
public string? Remediation { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregated policy scores.
|
||||
/// </summary>
|
||||
public sealed record PolicyScores
|
||||
{
|
||||
/// <summary>
|
||||
/// Overall risk score (0-100).
|
||||
/// </summary>
|
||||
[JsonPropertyName("risk_score")]
|
||||
public double RiskScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Compliance score (0-100).
|
||||
/// </summary>
|
||||
[JsonPropertyName("compliance_score")]
|
||||
public double? ComplianceScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Count of critical findings.
|
||||
/// </summary>
|
||||
[JsonPropertyName("critical_count")]
|
||||
public int CriticalCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Count of high findings.
|
||||
/// </summary>
|
||||
[JsonPropertyName("high_count")]
|
||||
public int HighCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Count of medium findings.
|
||||
/// </summary>
|
||||
[JsonPropertyName("medium_count")]
|
||||
public int MediumCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Count of low findings.
|
||||
/// </summary>
|
||||
[JsonPropertyName("low_count")]
|
||||
public int LowCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy decision context.
|
||||
/// </summary>
|
||||
public sealed record PolicyDecisionContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Tenant identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tenant_id")]
|
||||
public string? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment (production, staging, etc.).
|
||||
/// </summary>
|
||||
[JsonPropertyName("environment")]
|
||||
public string? Environment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Namespace or project.
|
||||
/// </summary>
|
||||
[JsonPropertyName("namespace")]
|
||||
public string? Namespace { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pipeline or workflow identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pipeline")]
|
||||
public string? Pipeline { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
@@ -120,6 +120,13 @@ public static class PredicateTypes
|
||||
public const string GraphV1 = "stella.ops/graph@v1";
|
||||
public const string ReplayV1 = "stella.ops/replay@v1";
|
||||
|
||||
/// <summary>
|
||||
/// StellaOps Policy Decision attestation predicate type.
|
||||
/// Sprint: SPRINT_3801_0001_0001_policy_decision_attestation
|
||||
/// Captures policy gate results with references to input evidence.
|
||||
/// </summary>
|
||||
public const string StellaOpsPolicyDecision = "stella.ops/policy-decision@v1";
|
||||
|
||||
// Third-party types
|
||||
public const string SlsaProvenanceV02 = "https://slsa.dev/provenance/v0.2";
|
||||
public const string SlsaProvenanceV1 = "https://slsa.dev/provenance/v1";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Http;
|
||||
using StellaOps.Policy.Engine.Attestation;
|
||||
using StellaOps.Policy.Engine.Caching;
|
||||
using StellaOps.Policy.Engine.EffectiveDecisionMap;
|
||||
using StellaOps.Policy.Engine.Events;
|
||||
@@ -178,6 +179,28 @@ public static class PolicyEngineServiceCollectionExtensions
|
||||
return services.AddVexDecisionSigning();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the policy decision attestation service for stella.ops/policy-decision@v1.
|
||||
/// Optional dependencies: IVexSignerClient, IVexRekorClient.
|
||||
/// Sprint: SPRINT_3801_0001_0001_policy_decision_attestation
|
||||
/// </summary>
|
||||
public static IServiceCollection AddPolicyDecisionAttestation(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<IPolicyDecisionAttestationService, Attestation.PolicyDecisionAttestationService>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the policy decision attestation service with options configuration.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddPolicyDecisionAttestation(
|
||||
this IServiceCollection services,
|
||||
Action<Attestation.PolicyDecisionAttestationOptions> configure)
|
||||
{
|
||||
services.Configure(configure);
|
||||
return services.AddPolicyDecisionAttestation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Redis connection for effective decision map and evaluation cache.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user