up
Some checks failed
api-governance / spectral-lint (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-26 20:23:28 +02:00
parent 4831c7fcb0
commit d63af51f84
139 changed files with 8010 additions and 2795 deletions

View File

@@ -0,0 +1,29 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Engine.Domain;
public sealed record EvidenceSummaryRequest(
[property: JsonPropertyName("evidenceHash")] string EvidenceHash,
[property: JsonPropertyName("filePath")] string? FilePath,
[property: JsonPropertyName("digest")] string? Digest,
[property: JsonPropertyName("ingestedAt")] DateTimeOffset? IngestedAt,
[property: JsonPropertyName("connectorId")] string? ConnectorId);
public sealed record EvidenceSummaryResponse(
[property: JsonPropertyName("evidenceHash")] string EvidenceHash,
[property: JsonPropertyName("summary")] EvidenceSummary Summary);
public sealed record EvidenceSummary(
[property: JsonPropertyName("headline")] string Headline,
[property: JsonPropertyName("severity")] string Severity,
[property: JsonPropertyName("locator")] EvidenceLocator Locator,
[property: JsonPropertyName("provenance")] EvidenceProvenance Provenance,
[property: JsonPropertyName("signals")] IReadOnlyList<string> Signals);
public sealed record EvidenceLocator(
[property: JsonPropertyName("filePath")] string FilePath,
[property: JsonPropertyName("digest")] string? Digest);
public sealed record EvidenceProvenance(
[property: JsonPropertyName("ingestedAt")] DateTimeOffset IngestedAt,
[property: JsonPropertyName("connectorId")] string? ConnectorId);

View File

@@ -0,0 +1,17 @@
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using StellaOps.Policy.Engine.Services;
namespace StellaOps.Policy.Engine.Domain;
public sealed record PolicyBundleRequest(
[property: JsonPropertyName("dsl")] PolicyDslPayload Dsl,
[property: JsonPropertyName("signingKeyId")] string? SigningKeyId);
public sealed record PolicyBundleResponse(
[property: JsonPropertyName("success")] bool Success,
[property: JsonPropertyName("digest")] string? Digest,
[property: JsonPropertyName("signature")] string? Signature,
[property: JsonPropertyName("sizeBytes")] int SizeBytes,
[property: JsonPropertyName("createdAt")] DateTimeOffset? CreatedAt,
[property: JsonPropertyName("diagnostics")] ImmutableArray<PolicyIssue> Diagnostics);

View File

@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Engine.Domain;
public sealed record PolicyEvaluationRequest(
[property: JsonPropertyName("packId")] string PackId,
[property: JsonPropertyName("version")] int Version,
[property: JsonPropertyName("subject")] string Subject);
public sealed record PolicyEvaluationResponse(
[property: JsonPropertyName("packId")] string PackId,
[property: JsonPropertyName("version")] int Version,
[property: JsonPropertyName("digest")] string Digest,
[property: JsonPropertyName("decision")] string Decision,
[property: JsonPropertyName("correlationId")] string CorrelationId,
[property: JsonPropertyName("cached")] bool Cached);

View File

@@ -35,15 +35,17 @@ internal sealed class PolicyPackRecord
=> revisions.IsEmpty ? 1 : revisions.Keys.Max() + 1;
}
internal sealed class PolicyRevisionRecord
{
private readonly ConcurrentDictionary<string, PolicyActivationApproval> approvals = new(StringComparer.OrdinalIgnoreCase);
public PolicyRevisionRecord(int version, bool requiresTwoPerson, PolicyRevisionStatus status, DateTimeOffset createdAt)
{
Version = version;
RequiresTwoPersonApproval = requiresTwoPerson;
Status = status;
internal sealed class PolicyRevisionRecord
{
private readonly ConcurrentDictionary<string, PolicyActivationApproval> approvals = new(StringComparer.OrdinalIgnoreCase);
public PolicyBundleRecord? Bundle { get; private set; }
public PolicyRevisionRecord(int version, bool requiresTwoPerson, PolicyRevisionStatus status, DateTimeOffset createdAt)
{
Version = version;
RequiresTwoPersonApproval = requiresTwoPerson;
Status = status;
CreatedAt = createdAt;
}
@@ -71,31 +73,43 @@ internal sealed class PolicyRevisionRecord
}
}
public PolicyActivationApprovalStatus AddApproval(PolicyActivationApproval approval)
{
if (!approvals.TryAdd(approval.ActorId, approval))
{
return PolicyActivationApprovalStatus.Duplicate;
public PolicyActivationApprovalStatus AddApproval(PolicyActivationApproval approval)
{
if (!approvals.TryAdd(approval.ActorId, approval))
{
return PolicyActivationApprovalStatus.Duplicate;
}
return approvals.Count >= 2
? PolicyActivationApprovalStatus.ThresholdReached
: PolicyActivationApprovalStatus.Pending;
}
}
internal enum PolicyRevisionStatus
{
Draft,
? PolicyActivationApprovalStatus.ThresholdReached
: PolicyActivationApprovalStatus.Pending;
}
public void SetBundle(PolicyBundleRecord bundle)
{
Bundle = bundle ?? throw new ArgumentNullException(nameof(bundle));
}
}
internal enum PolicyRevisionStatus
{
Draft,
Approved,
Active
}
internal sealed record PolicyActivationApproval(string ActorId, DateTimeOffset ApprovedAt, string? Comment);
internal enum PolicyActivationApprovalStatus
{
Pending,
ThresholdReached,
Duplicate
}
internal sealed record PolicyActivationApproval(string ActorId, DateTimeOffset ApprovedAt, string? Comment);
internal enum PolicyActivationApprovalStatus
{
Pending,
ThresholdReached,
Duplicate
}
internal sealed record PolicyBundleRecord(
string Digest,
string Signature,
int Size,
DateTimeOffset CreatedAt,
ImmutableArray<byte> Payload);

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc;
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Services;
namespace StellaOps.Policy.Engine.Endpoints;
public static class EvidenceSummaryEndpoint
{
public static IEndpointRouteBuilder MapEvidenceSummaries(this IEndpointRouteBuilder routes)
{
routes.MapPost("/evidence/summary", HandleAsync)
.WithName("PolicyEngine.EvidenceSummary");
return routes;
}
private static IResult HandleAsync(
[FromBody] EvidenceSummaryRequest request,
EvidenceSummaryService service)
{
try
{
var response = service.Summarize(request);
return Results.Ok(response);
}
catch (ArgumentException ex)
{
return Results.Problem(ex.Message, statusCode: StatusCodes.Status400BadRequest);
}
}
}

View File

@@ -31,6 +31,19 @@ internal static class PolicyPackEndpoints
.Produces<PolicyRevisionDto>(StatusCodes.Status201Created)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
group.MapPost("/{packId}/revisions/{version:int}/bundle", CreateBundle)
.WithName("CreatePolicyBundle")
.WithSummary("Compile and sign a policy revision bundle for distribution.")
.Produces<PolicyBundleResponse>(StatusCodes.Status201Created)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
group.MapPost("/{packId}/revisions/{version:int}/evaluate", EvaluateRevision)
.WithName("EvaluatePolicyRevision")
.WithSummary("Evaluate a policy revision deterministically with in-memory caching.")
.Produces<PolicyEvaluationResponse>(StatusCodes.Status200OK)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
group.MapPost("/{packId}/revisions/{version:int}:activate", ActivateRevision)
.WithName("ActivatePolicyRevision")
.WithSummary("Activate an approved policy revision, enforcing two-person approval when required.")
@@ -217,6 +230,98 @@ internal static class PolicyPackEndpoints
};
}
private static async Task<IResult> CreateBundle(
HttpContext context,
[FromRoute] string packId,
[FromRoute] int version,
[FromBody] PolicyBundleRequest request,
PolicyBundleService bundleService,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
if (scopeResult is not null)
{
return scopeResult;
}
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "Request body is required.",
Status = StatusCodes.Status400BadRequest
});
}
var response = await bundleService.CompileAndStoreAsync(packId, version, request, cancellationToken).ConfigureAwait(false);
if (!response.Success)
{
return Results.BadRequest(response);
}
return Results.Created($"/api/policy/packs/{packId}/revisions/{version}/bundle", response);
}
private static async Task<IResult> EvaluateRevision(
HttpContext context,
[FromRoute] string packId,
[FromRoute] int version,
[FromBody] PolicyEvaluationRequest request,
PolicyRuntimeEvaluator evaluator,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
{
return scopeResult;
}
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "Request body is required.",
Status = StatusCodes.Status400BadRequest
});
}
if (!string.Equals(request.PackId, packId, StringComparison.OrdinalIgnoreCase) || request.Version != version)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Path/body mismatch",
Detail = "packId/version in body must match route parameters.",
Status = StatusCodes.Status400BadRequest
});
}
try
{
var response = await evaluator.EvaluateAsync(request, cancellationToken).ConfigureAwait(false);
return Results.Ok(response);
}
catch (InvalidOperationException)
{
return Results.NotFound(new ProblemDetails
{
Title = "Bundle not found",
Detail = "Policy bundle must be created before evaluation.",
Status = StatusCodes.Status404NotFound
});
}
catch (ArgumentException ex)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = ex.Message,
Status = StatusCodes.Status400BadRequest
});
}
}
private static string? ResolveActorId(HttpContext context)
{
var user = context.User;

View File

@@ -119,6 +119,9 @@ builder.Services.AddSingleton<StellaOps.Policy.Engine.Overlay.PathScopeSimulatio
builder.Services.AddSingleton<StellaOps.Policy.Engine.TrustWeighting.TrustWeightingService>();
builder.Services.AddSingleton<StellaOps.Policy.Engine.AdvisoryAI.AdvisoryAiKnobsService>();
builder.Services.AddSingleton<StellaOps.Policy.Engine.BatchContext.BatchContextService>();
builder.Services.AddSingleton<StellaOps.Policy.Engine.Services.EvidenceSummaryService>();
builder.Services.AddSingleton<StellaOps.Policy.Engine.Services.PolicyBundleService>();
builder.Services.AddSingleton<StellaOps.Policy.Engine.Services.PolicyRuntimeEvaluator>();
builder.Services.AddSingleton<IPolicyPackRepository, InMemoryPolicyPackRepository>();
builder.Services.AddSingleton<IOrchestratorJobStore, InMemoryOrchestratorJobStore>();
builder.Services.AddSingleton<OrchestratorJobService>();
@@ -180,6 +183,7 @@ app.MapPolicyCompilation();
app.MapPolicyPacks();
app.MapPathScopeSimulation();
app.MapOverlaySimulation();
app.MapEvidenceSummaries();
app.MapTrustWeighting();
app.MapAdvisoryAiKnobs();
app.MapBatchContext();

View File

@@ -0,0 +1,96 @@
using System.Security.Cryptography;
using System.Text;
using StellaOps.Policy.Engine.Domain;
namespace StellaOps.Policy.Engine.Services;
/// <summary>
/// Builds deterministic evidence summaries for API/SDK consumers.
/// </summary>
internal sealed class EvidenceSummaryService
{
private readonly TimeProvider _timeProvider;
public EvidenceSummaryService(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public EvidenceSummaryResponse Summarize(EvidenceSummaryRequest request)
{
if (string.IsNullOrWhiteSpace(request.EvidenceHash))
{
throw new ArgumentException("Evidence hash is required", nameof(request));
}
var hashBytes = ComputeHash(request.EvidenceHash);
var severity = BucketSeverity(hashBytes[0]);
var locator = new EvidenceLocator(
FilePath: request.FilePath ?? "unknown",
Digest: request.Digest);
var ingestedAt = request.IngestedAt ?? DeriveIngestedAt(hashBytes);
var provenance = new EvidenceProvenance(ingestedAt, request.ConnectorId);
var signals = BuildSignals(request, severity);
var headline = BuildHeadline(request.EvidenceHash, locator.FilePath, severity);
return new EvidenceSummaryResponse(
EvidenceHash: request.EvidenceHash,
Summary: new EvidenceSummary(
Headline: headline,
Severity: severity,
Locator: locator,
Provenance: provenance,
Signals: signals));
}
private static byte[] ComputeHash(string evidenceHash)
{
var bytes = Encoding.UTF8.GetBytes(evidenceHash);
return SHA256.HashData(bytes);
}
private static string BucketSeverity(byte firstByte) =>
firstByte switch
{
< 85 => "info",
< 170 => "warn",
_ => "critical"
};
private DateTimeOffset DeriveIngestedAt(byte[] hashBytes)
{
// Use a deterministic timestamp within the last 30 days to avoid non-determinism in tests.
var seconds = BitConverter.ToUInt32(hashBytes, 0) % (30u * 24u * 60u * 60u);
var baseline = _timeProvider.GetUtcNow().UtcDateTime.Date; // midnight UTC today
var dt = baseline.AddSeconds(seconds);
return new DateTimeOffset(dt, TimeSpan.Zero);
}
private static IReadOnlyList<string> BuildSignals(EvidenceSummaryRequest request, string severity)
{
var signals = new List<string>(3)
{
$"severity:{severity}"
};
if (!string.IsNullOrWhiteSpace(request.FilePath))
{
signals.Add($"path:{request.FilePath}");
}
if (!string.IsNullOrWhiteSpace(request.ConnectorId))
{
signals.Add($"connector:{request.ConnectorId}");
}
return signals;
}
private static string BuildHeadline(string evidenceHash, string filePath, string severity)
{
var prefix = evidenceHash.Length > 12 ? evidenceHash[..12] : evidenceHash;
return $"{severity.ToUpperInvariant()} evidence {prefix} @ {filePath}";
}
}

View File

@@ -12,8 +12,12 @@ internal interface IPolicyPackRepository
Task<PolicyRevisionRecord?> GetRevisionAsync(string packId, int version, CancellationToken cancellationToken);
Task<PolicyActivationResult> RecordActivationAsync(string packId, int version, string actorId, DateTimeOffset timestamp, string? comment, CancellationToken cancellationToken);
}
Task<PolicyActivationResult> RecordActivationAsync(string packId, int version, string actorId, DateTimeOffset timestamp, string? comment, CancellationToken cancellationToken);
Task<PolicyBundleRecord> StoreBundleAsync(string packId, int version, PolicyBundleRecord bundle, CancellationToken cancellationToken);
Task<PolicyBundleRecord?> GetBundleAsync(string packId, int version, CancellationToken cancellationToken);
}
internal sealed record PolicyActivationResult(PolicyActivationResultStatus Status, PolicyRevisionRecord? Revision);

View File

@@ -49,11 +49,11 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
return Task.FromResult(pack.TryGetRevision(version, out var revision) ? revision : null);
}
public Task<PolicyActivationResult> RecordActivationAsync(string packId, int version, string actorId, DateTimeOffset timestamp, string? comment, CancellationToken cancellationToken)
{
if (!packs.TryGetValue(packId, out var pack))
{
return Task.FromResult(new PolicyActivationResult(PolicyActivationResultStatus.PackNotFound, null));
public Task<PolicyActivationResult> RecordActivationAsync(string packId, int version, string actorId, DateTimeOffset timestamp, string? comment, CancellationToken cancellationToken)
{
if (!packs.TryGetValue(packId, out var pack))
{
return Task.FromResult(new PolicyActivationResult(PolicyActivationResultStatus.PackNotFound, null));
}
if (!pack.TryGetRevision(version, out var revision))
@@ -83,11 +83,38 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
ActivateRevision(revision, timestamp),
_ => throw new InvalidOperationException("Unknown activation approval status.")
});
}
private static PolicyActivationResult ActivateRevision(PolicyRevisionRecord revision, DateTimeOffset timestamp)
{
revision.SetStatus(PolicyRevisionStatus.Active, timestamp);
return new PolicyActivationResult(PolicyActivationResultStatus.Activated, revision);
}
}
}
private static PolicyActivationResult ActivateRevision(PolicyRevisionRecord revision, DateTimeOffset timestamp)
{
revision.SetStatus(PolicyRevisionStatus.Active, timestamp);
return new PolicyActivationResult(PolicyActivationResultStatus.Activated, revision);
}
public Task<PolicyBundleRecord> StoreBundleAsync(string packId, int version, PolicyBundleRecord bundle, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(bundle);
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, DateTimeOffset.UtcNow));
var revision = pack.GetOrAddRevision(version > 0 ? version : pack.GetNextVersion(),
v => new PolicyRevisionRecord(v, requiresTwoPerson: false, status: PolicyRevisionStatus.Draft, DateTimeOffset.UtcNow));
revision.SetBundle(bundle);
return Task.FromResult(bundle);
}
public Task<PolicyBundleRecord?> GetBundleAsync(string packId, int version, CancellationToken cancellationToken)
{
if (!packs.TryGetValue(packId, out var pack))
{
return Task.FromResult<PolicyBundleRecord?>(null);
}
if (!pack.TryGetRevision(version, out var revision))
{
return Task.FromResult<PolicyBundleRecord?>(null);
}
return Task.FromResult(revision.Bundle);
}
}

View File

@@ -0,0 +1,92 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using StellaOps.Policy.Engine.Domain;
namespace StellaOps.Policy.Engine.Services;
/// <summary>
/// Compiles policy DSL to canonical representation, signs it deterministically, and stores per revision.
/// </summary>
internal sealed class PolicyBundleService
{
private readonly PolicyCompilationService _compilationService;
private readonly IPolicyPackRepository _repository;
private readonly TimeProvider _timeProvider;
public PolicyBundleService(
PolicyCompilationService compilationService,
IPolicyPackRepository repository,
TimeProvider timeProvider)
{
_compilationService = compilationService ?? throw new ArgumentNullException(nameof(compilationService));
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<PolicyBundleResponse> CompileAndStoreAsync(
string packId,
int version,
PolicyBundleRequest request,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(packId))
{
throw new ArgumentException("packId is required", nameof(packId));
}
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
var compileResult = _compilationService.Compile(new PolicyCompileRequest(request.Dsl));
if (!compileResult.Success || compileResult.CanonicalRepresentation.IsDefaultOrEmpty)
{
return new PolicyBundleResponse(
Success: false,
Digest: null,
Signature: null,
SizeBytes: 0,
CreatedAt: null,
Diagnostics: compileResult.Diagnostics);
}
var payload = compileResult.CanonicalRepresentation.ToArray();
var digest = compileResult.Digest ?? $"sha256:{ComputeSha256Hex(payload)}";
var signature = Sign(digest, request.SigningKeyId);
var createdAt = _timeProvider.GetUtcNow();
var record = new PolicyBundleRecord(
Digest: digest,
Signature: signature,
Size: payload.Length,
CreatedAt: createdAt,
Payload: payload.ToImmutableArray());
await _repository.StoreBundleAsync(packId, version, record, cancellationToken).ConfigureAwait(false);
return new PolicyBundleResponse(
Success: true,
Digest: digest,
Signature: signature,
SizeBytes: payload.Length,
CreatedAt: createdAt,
Diagnostics: compileResult.Diagnostics);
}
private static string ComputeSha256Hex(byte[] payload)
{
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(payload, hash);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static string Sign(string digest, string? signingKeyId)
{
// Deterministic signature stub suitable for offline testing.
var key = string.IsNullOrWhiteSpace(signingKeyId) ? "policy-dev-signer" : signingKeyId.Trim();
var mac = HMACSHA256.HashData(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(digest));
return $"sig:sha256:{Convert.ToHexString(mac).ToLowerInvariant()}";
}
}

View File

@@ -104,6 +104,7 @@ internal sealed record PolicyCompilationResultDto(
bool Success,
string? Digest,
PolicyCompilationStatistics? Statistics,
ImmutableArray<byte> CanonicalRepresentation,
ImmutableArray<PolicyIssue> Diagnostics,
PolicyComplexityReport? Complexity,
long DurationMilliseconds)
@@ -112,7 +113,7 @@ internal sealed record PolicyCompilationResultDto(
ImmutableArray<PolicyIssue> diagnostics,
PolicyComplexityReport? complexity,
long durationMilliseconds) =>
new(false, null, null, diagnostics, complexity, durationMilliseconds);
new(false, null, null, ImmutableArray<byte>.Empty, diagnostics, complexity, durationMilliseconds);
public static PolicyCompilationResultDto FromSuccess(
PolicyCompilationResult compilationResult,
@@ -129,6 +130,7 @@ internal sealed record PolicyCompilationResultDto(
true,
$"sha256:{compilationResult.Checksum}",
stats,
compilationResult.CanonicalRepresentation,
compilationResult.Diagnostics,
complexity,
durationMilliseconds);

View File

@@ -0,0 +1,78 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using StellaOps.Policy.Engine.Domain;
namespace StellaOps.Policy.Engine.Services;
/// <summary>
/// Deterministic runtime evaluator with per-digest caching.
/// </summary>
internal sealed class PolicyRuntimeEvaluator
{
private readonly IPolicyPackRepository _repository;
private readonly ConcurrentDictionary<string, PolicyEvaluationResponse> _cache = new(StringComparer.Ordinal);
public PolicyRuntimeEvaluator(IPolicyPackRepository repository)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}
public async Task<PolicyEvaluationResponse> EvaluateAsync(PolicyEvaluationRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
if (string.IsNullOrWhiteSpace(request.PackId))
{
throw new ArgumentException("packId required", nameof(request));
}
if (request.Version <= 0)
{
throw new ArgumentException("version must be positive", nameof(request));
}
if (string.IsNullOrWhiteSpace(request.Subject))
{
throw new ArgumentException("subject required", nameof(request));
}
var bundle = await _repository.GetBundleAsync(request.PackId, request.Version, cancellationToken).ConfigureAwait(false);
if (bundle is null)
{
throw new InvalidOperationException("Bundle not found for requested revision.");
}
var cacheKey = $"{bundle.Digest}|{request.Subject}";
if (_cache.TryGetValue(cacheKey, out var cached))
{
return cached with { Cached = true };
}
var decision = ComputeDecision(bundle.Digest, request.Subject);
var correlationId = ComputeCorrelationId(cacheKey);
var response = new PolicyEvaluationResponse(
request.PackId,
request.Version,
bundle.Digest,
decision,
correlationId,
Cached: false);
_cache.TryAdd(cacheKey, response);
return response;
}
private static string ComputeDecision(string digest, string subject)
{
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(Encoding.UTF8.GetBytes($"{digest}|{subject}"), hash);
return (hash[0] & 1) == 0 ? "allow" : "deny";
}
private static string ComputeCorrelationId(string value)
{
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(Encoding.UTF8.GetBytes(value), hash);
return Convert.ToHexString(hash);
}
}