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:
master
2025-12-18 16:19:16 +02:00
parent 00d2c99af9
commit 811f35cba7
114 changed files with 13702 additions and 268 deletions

View File

@@ -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; }
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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";

View File

@@ -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>