Add SBOM, symbols, traces, and VEX files for CVE-2022-21661 SQLi case
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Created CycloneDX and SPDX SBOM files for both reachable and unreachable images.
- Added symbols.json detailing function entry and sink points in the WordPress code.
- Included runtime traces for function calls in both reachable and unreachable scenarios.
- Developed OpenVEX files indicating vulnerability status and justification for both cases.
- Updated README for evaluator harness to guide integration with scanner output.
This commit is contained in:
master
2025-11-08 20:53:45 +02:00
parent 515975edc5
commit 536f6249a6
837 changed files with 37279 additions and 14675 deletions

View File

@@ -0,0 +1,282 @@
using System;
using System.Collections.Immutable;
namespace StellaOps.Policy.Engine.Compilation;
/// <summary>
/// Computes deterministic complexity metrics for compiled policies.
/// </summary>
internal sealed class PolicyComplexityAnalyzer
{
public PolicyComplexityReport Analyze(PolicyIrDocument document)
{
ArgumentNullException.ThrowIfNull(document);
var metrics = new ComplexityMetrics();
metrics.RuleCount = document.Rules.IsDefault ? 0 : document.Rules.Length;
VisitMetadata(document.Metadata.Values, metrics);
VisitMetadata(document.Settings.Values, metrics);
VisitProfiles(document.Profiles, metrics);
if (!document.Rules.IsDefaultOrEmpty)
{
foreach (var rule in document.Rules)
{
metrics.ConditionCount++;
VisitExpression(rule.When, metrics, depth: 0);
VisitActions(rule.ThenActions, metrics);
VisitActions(rule.ElseActions, metrics);
}
}
var score = CalculateScore(metrics);
var roundedScore = Math.Round(score, 3, MidpointRounding.AwayFromZero);
return new PolicyComplexityReport(
roundedScore,
metrics.RuleCount,
metrics.ActionCount,
metrics.ExpressionCount,
metrics.InvocationCount,
metrics.MemberAccessCount,
metrics.IdentifierCount,
metrics.LiteralCount,
metrics.MaxDepth,
metrics.ProfileCount,
metrics.ProfileBindings,
metrics.ConditionCount,
metrics.ListItems);
}
private static void VisitProfiles(ImmutableArray<PolicyIrProfile> profiles, ComplexityMetrics metrics)
{
if (profiles.IsDefaultOrEmpty)
{
return;
}
foreach (var profile in profiles)
{
metrics.ProfileCount++;
if (!profile.Maps.IsDefaultOrEmpty)
{
foreach (var map in profile.Maps)
{
if (map.Entries.IsDefaultOrEmpty)
{
continue;
}
foreach (var entry in map.Entries)
{
metrics.ProfileBindings++;
metrics.LiteralCount++; // weight values contribute to literal count
}
}
}
if (!profile.Environments.IsDefaultOrEmpty)
{
foreach (var environment in profile.Environments)
{
if (environment.Entries.IsDefaultOrEmpty)
{
continue;
}
foreach (var entry in environment.Entries)
{
metrics.ProfileBindings++;
metrics.ConditionCount++;
VisitExpression(entry.Condition, metrics, depth: 0);
}
}
}
if (!profile.Scalars.IsDefaultOrEmpty)
{
foreach (var scalar in profile.Scalars)
{
metrics.ProfileBindings++;
VisitLiteral(scalar.Value, metrics);
}
}
}
}
private static void VisitMetadata(IEnumerable<PolicyIrLiteral> literals, ComplexityMetrics metrics)
{
foreach (var literal in literals)
{
VisitLiteral(literal, metrics);
}
}
private static void VisitLiteral(PolicyIrLiteral literal, ComplexityMetrics metrics)
{
switch (literal)
{
case PolicyIrListLiteral list when !list.Items.IsDefaultOrEmpty:
foreach (var item in list.Items)
{
VisitLiteral(item, metrics);
}
break;
}
metrics.LiteralCount++;
}
private static void VisitActions(ImmutableArray<PolicyIrAction> actions, ComplexityMetrics metrics)
{
if (actions.IsDefaultOrEmpty)
{
return;
}
foreach (var action in actions)
{
metrics.ActionCount++;
switch (action)
{
case PolicyIrAssignmentAction assignment:
VisitExpression(assignment.Value, metrics, depth: 0);
break;
case PolicyIrAnnotateAction annotate:
VisitExpression(annotate.Value, metrics, depth: 0);
break;
case PolicyIrIgnoreAction ignore when ignore.Until is not null:
VisitExpression(ignore.Until, metrics, depth: 0);
break;
case PolicyIrEscalateAction escalate:
VisitExpression(escalate.To, metrics, depth: 0);
VisitExpression(escalate.When, metrics, depth: 0);
break;
case PolicyIrRequireVexAction require when !require.Conditions.IsEmpty:
foreach (var condition in require.Conditions.Values)
{
VisitExpression(condition, metrics, depth: 0);
}
break;
case PolicyIrWarnAction warn when warn.Message is not null:
VisitExpression(warn.Message, metrics, depth: 0);
break;
case PolicyIrDeferAction defer when defer.Until is not null:
VisitExpression(defer.Until, metrics, depth: 0);
break;
}
}
}
private static void VisitExpression(PolicyExpression? expression, ComplexityMetrics metrics, int depth)
{
if (expression is null)
{
return;
}
metrics.ExpressionCount++;
var currentDepth = depth + 1;
if (currentDepth > metrics.MaxDepth)
{
metrics.MaxDepth = currentDepth;
}
switch (expression)
{
case PolicyLiteralExpression:
metrics.LiteralCount++;
break;
case PolicyListExpression listExpression:
if (!listExpression.Items.IsDefaultOrEmpty)
{
foreach (var item in listExpression.Items)
{
metrics.ListItems++;
VisitExpression(item, metrics, currentDepth);
}
}
break;
case PolicyIdentifierExpression:
metrics.IdentifierCount++;
break;
case PolicyMemberAccessExpression member:
metrics.MemberAccessCount++;
VisitExpression(member.Target, metrics, currentDepth);
break;
case PolicyInvocationExpression invocation:
metrics.InvocationCount++;
VisitExpression(invocation.Target, metrics, currentDepth);
if (!invocation.Arguments.IsDefaultOrEmpty)
{
foreach (var argument in invocation.Arguments)
{
VisitExpression(argument, metrics, currentDepth);
}
}
break;
case PolicyIndexerExpression indexer:
VisitExpression(indexer.Target, metrics, currentDepth);
VisitExpression(indexer.Index, metrics, currentDepth);
break;
case PolicyUnaryExpression unary:
VisitExpression(unary.Operand, metrics, currentDepth);
break;
case PolicyBinaryExpression binary:
VisitExpression(binary.Left, metrics, currentDepth);
VisitExpression(binary.Right, metrics, currentDepth);
break;
default:
break;
}
}
private static double CalculateScore(ComplexityMetrics metrics)
{
return metrics.RuleCount * 5d
+ metrics.ActionCount * 1.5d
+ metrics.ExpressionCount * 0.75d
+ metrics.InvocationCount * 1.5d
+ metrics.MemberAccessCount * 1.0d
+ metrics.IdentifierCount * 0.5d
+ metrics.LiteralCount * 0.25d
+ metrics.ProfileBindings * 0.5d
+ metrics.ConditionCount * 1.25d
+ metrics.MaxDepth * 2d
+ metrics.ListItems * 0.25d;
}
private sealed class ComplexityMetrics
{
public int RuleCount;
public int ActionCount;
public int ExpressionCount;
public int InvocationCount;
public int MemberAccessCount;
public int IdentifierCount;
public int LiteralCount;
public int ProfileCount;
public int ProfileBindings;
public int ConditionCount;
public int MaxDepth;
public int ListItems;
}
}
internal sealed record PolicyComplexityReport(
double Score,
int RuleCount,
int ActionCount,
int ExpressionCount,
int InvocationCount,
int MemberAccessCount,
int IdentifierCount,
int LiteralCount,
int MaxExpressionDepth,
int ProfileCount,
int ProfileBindingCount,
int ConditionCount,
int ListItemCount);

View File

@@ -1,107 +1,150 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Policy;
using StellaOps.Policy.Engine.Services;
using System.Collections.Immutable;
namespace StellaOps.Policy.Engine.Endpoints;
internal static class PolicyCompilationEndpoints
{
private const string CompileRoute = "/api/policy/policies/{policyId}/versions/{version}:compile";
public static IEndpointRouteBuilder MapPolicyCompilation(this IEndpointRouteBuilder endpoints)
{
endpoints.MapPost(CompileRoute, CompilePolicy)
.WithName("CompilePolicy")
.WithSummary("Compile and lint a policy DSL document.")
.WithDescription("Compiles a stella-dsl@1 policy document and returns deterministic digest and statistics.")
.Produces<PolicyCompileResponse>(StatusCodes.Status200OK)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
.RequireAuthorization(); // scopes enforced by policy middleware.
return endpoints;
}
private static IResult CompilePolicy(
[FromRoute] string policyId,
[FromRoute] int version,
[FromBody] PolicyCompileRequest request,
PolicyCompilationService compilationService)
{
if (request is null)
{
return Results.BadRequest(BuildProblem("ERR_POL_001", "Request body missing.", policyId, version));
}
var result = compilationService.Compile(request);
if (!result.Success)
{
return Results.BadRequest(BuildProblem("ERR_POL_001", "Policy compilation failed.", policyId, version, result.Diagnostics));
}
var response = new PolicyCompileResponse(
result.Digest!,
result.Statistics ?? new PolicyCompilationStatistics(0, ImmutableDictionary<string, int>.Empty),
ConvertDiagnostics(result.Diagnostics));
return Results.Ok(response);
}
private static PolicyProblemDetails BuildProblem(string code, string message, string policyId, int version, ImmutableArray<PolicyIssue>? diagnostics = null)
{
var problem = new PolicyProblemDetails
{
Code = code,
Title = "Policy compilation error",
Detail = message,
PolicyId = policyId,
PolicyVersion = version
};
if (diagnostics is { Length: > 0 } diag)
{
problem.Diagnostics = diag;
}
return problem;
}
private static ImmutableArray<PolicyDiagnosticDto> ConvertDiagnostics(ImmutableArray<PolicyIssue> issues)
{
if (issues.IsDefaultOrEmpty)
{
return ImmutableArray<PolicyDiagnosticDto>.Empty;
}
var builder = ImmutableArray.CreateBuilder<PolicyDiagnosticDto>(issues.Length);
foreach (var issue in issues)
{
if (issue.Severity != PolicyIssueSeverity.Warning)
{
continue;
}
builder.Add(new PolicyDiagnosticDto(issue.Code, issue.Message, issue.Path));
}
return builder.ToImmutable();
}
private sealed class PolicyProblemDetails : ProblemDetails
{
public string Code { get; set; } = "ERR_POL_001";
public string? PolicyId { get; set; }
public int PolicyVersion { get; set; }
public ImmutableArray<PolicyIssue> Diagnostics { get; set; } = ImmutableArray<PolicyIssue>.Empty;
}
}
internal sealed record PolicyCompileResponse(
string Digest,
PolicyCompilationStatistics Statistics,
ImmutableArray<PolicyDiagnosticDto> Warnings);
internal sealed record PolicyDiagnosticDto(string Code, string Message, string Path);
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Policy;
using StellaOps.Policy.Engine.Compilation;
using StellaOps.Policy.Engine.Services;
using System.Collections.Immutable;
namespace StellaOps.Policy.Engine.Endpoints;
internal static class PolicyCompilationEndpoints
{
private const string CompileRoute = "/api/policy/policies/{policyId}/versions/{version}:compile";
public static IEndpointRouteBuilder MapPolicyCompilation(this IEndpointRouteBuilder endpoints)
{
endpoints.MapPost(CompileRoute, CompilePolicy)
.WithName("CompilePolicy")
.WithSummary("Compile and lint a policy DSL document.")
.WithDescription("Compiles a stella-dsl@1 policy document and returns deterministic digest and statistics.")
.Produces<PolicyCompileResponse>(StatusCodes.Status200OK)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
.RequireAuthorization(); // scopes enforced by policy middleware.
return endpoints;
}
private static IResult CompilePolicy(
[FromRoute] string policyId,
[FromRoute] int version,
[FromBody] PolicyCompileRequest request,
PolicyCompilationService compilationService)
{
if (request is null)
{
return Results.BadRequest(BuildProblem("ERR_POL_001", "Request body missing.", policyId, version));
}
var result = compilationService.Compile(request);
if (!result.Success)
{
return Results.BadRequest(BuildProblem("ERR_POL_001", "Policy compilation failed.", policyId, version, result.Diagnostics));
}
var response = new PolicyCompileResponse(
result.Digest!,
result.Statistics ?? new PolicyCompilationStatistics(0, ImmutableDictionary<string, int>.Empty),
MapComplexity(result.Complexity),
result.DurationMilliseconds,
ConvertDiagnostics(result.Diagnostics));
return Results.Ok(response);
}
private static PolicyProblemDetails BuildProblem(string code, string message, string policyId, int version, ImmutableArray<PolicyIssue>? diagnostics = null)
{
var problem = new PolicyProblemDetails
{
Code = code,
Title = "Policy compilation error",
Detail = message,
PolicyId = policyId,
PolicyVersion = version
};
if (diagnostics is { Length: > 0 } diag)
{
problem.Diagnostics = diag;
}
return problem;
}
private static ImmutableArray<PolicyDiagnosticDto> ConvertDiagnostics(ImmutableArray<PolicyIssue> issues)
{
if (issues.IsDefaultOrEmpty)
{
return ImmutableArray<PolicyDiagnosticDto>.Empty;
}
var builder = ImmutableArray.CreateBuilder<PolicyDiagnosticDto>(issues.Length);
foreach (var issue in issues)
{
if (issue.Severity != PolicyIssueSeverity.Warning)
{
continue;
}
builder.Add(new PolicyDiagnosticDto(issue.Code, issue.Message, issue.Path));
}
return builder.ToImmutable();
}
private static PolicyComplexityReportDto? MapComplexity(PolicyComplexityReport? report)
{
if (report is null)
{
return null;
}
return new PolicyComplexityReportDto(
report.Score,
report.RuleCount,
report.ActionCount,
report.ExpressionCount,
report.InvocationCount,
report.MemberAccessCount,
report.IdentifierCount,
report.LiteralCount,
report.MaxExpressionDepth,
report.ProfileCount,
report.ProfileBindingCount,
report.ConditionCount,
report.ListItemCount);
}
private sealed class PolicyProblemDetails : ProblemDetails
{
public string Code { get; set; } = "ERR_POL_001";
public string? PolicyId { get; set; }
public int PolicyVersion { get; set; }
public ImmutableArray<PolicyIssue> Diagnostics { get; set; } = ImmutableArray<PolicyIssue>.Empty;
}
}
internal sealed record PolicyCompileResponse(
string Digest,
PolicyCompilationStatistics Statistics,
PolicyComplexityReportDto? Complexity,
long DurationMilliseconds,
ImmutableArray<PolicyDiagnosticDto> Warnings);
internal sealed record PolicyDiagnosticDto(string Code, string Message, string Path);
internal sealed record PolicyComplexityReportDto(
double Score,
int RuleCount,
int ActionCount,
int ExpressionCount,
int InvocationCount,
int MemberAccessCount,
int IdentifierCount,
int LiteralCount,
int MaxExpressionDepth,
int ProfileCount,
int ProfileBindingCount,
int ConditionCount,
int ListItemCount);

View File

@@ -1,267 +1,274 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.Abstractions;
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Services;
namespace StellaOps.Policy.Engine.Endpoints;
internal static class PolicyPackEndpoints
{
public static IEndpointRouteBuilder MapPolicyPacks(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/policy/packs")
.RequireAuthorization()
.WithTags("Policy Packs");
group.MapPost(string.Empty, CreatePack)
.WithName("CreatePolicyPack")
.WithSummary("Create a new policy pack container.")
.Produces<PolicyPackDto>(StatusCodes.Status201Created);
group.MapGet(string.Empty, ListPacks)
.WithName("ListPolicyPacks")
.WithSummary("List policy packs for the current tenant.")
.Produces<IReadOnlyList<PolicyPackSummaryDto>>(StatusCodes.Status200OK);
group.MapPost("/{packId}/revisions", CreateRevision)
.WithName("CreatePolicyRevision")
.WithSummary("Create or update policy revision metadata.")
.Produces<PolicyRevisionDto>(StatusCodes.Status201Created)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
group.MapPost("/{packId}/revisions/{version:int}:activate", ActivateRevision)
.WithName("ActivatePolicyRevision")
.WithSummary("Activate an approved policy revision, enforcing two-person approval when required.")
.Produces<PolicyRevisionActivationResponse>(StatusCodes.Status200OK)
.Produces<PolicyRevisionActivationResponse>(StatusCodes.Status202Accepted)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
return endpoints;
}
private static async Task<IResult> CreatePack(
HttpContext context,
[FromBody] CreatePolicyPackRequest request,
IPolicyPackRepository repository,
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 packId = string.IsNullOrWhiteSpace(request.PackId)
? $"pack-{Guid.NewGuid():n}"
: request.PackId.Trim();
var pack = await repository.CreateAsync(packId, request.DisplayName?.Trim(), cancellationToken).ConfigureAwait(false);
var dto = PolicyPackMapper.ToDto(pack);
return Results.Created($"/api/policy/packs/{dto.PackId}", dto);
}
private static async Task<IResult> ListPacks(
HttpContext context,
IPolicyPackRepository repository,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
{
return scopeResult;
}
var packs = await repository.ListAsync(cancellationToken).ConfigureAwait(false);
var summaries = packs.Select(PolicyPackMapper.ToSummaryDto).ToArray();
return Results.Ok(summaries);
}
private static async Task<IResult> CreateRevision(
HttpContext context,
[FromRoute] string packId,
[FromBody] CreatePolicyRevisionRequest request,
IPolicyPackRepository repository,
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
});
}
if (request.InitialStatus is not (PolicyRevisionStatus.Draft or PolicyRevisionStatus.Approved))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid status",
Detail = "Only Draft or Approved statuses are supported for new revisions.",
Status = StatusCodes.Status400BadRequest
});
}
var revision = await repository.UpsertRevisionAsync(
packId,
request.Version ?? 0,
request.RequiresTwoPersonApproval,
request.InitialStatus,
cancellationToken).ConfigureAwait(false);
return Results.Created(
$"/api/policy/packs/{packId}/revisions/{revision.Version}",
PolicyPackMapper.ToDto(packId, revision));
}
private static async Task<IResult> ActivateRevision(
HttpContext context,
[FromRoute] string packId,
[FromRoute] int version,
[FromBody] ActivatePolicyRevisionRequest request,
IPolicyPackRepository repository,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyActivate);
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 actorId = ResolveActorId(context);
if (actorId is null)
{
return Results.Problem("Actor identity required.", statusCode: StatusCodes.Status401Unauthorized);
}
var result = await repository.RecordActivationAsync(
packId,
version,
actorId,
DateTimeOffset.UtcNow,
request.Comment,
cancellationToken).ConfigureAwait(false);
return result.Status switch
{
PolicyActivationResultStatus.PackNotFound => Results.NotFound(new ProblemDetails
{
Title = "Policy pack not found",
Status = StatusCodes.Status404NotFound
}),
PolicyActivationResultStatus.RevisionNotFound => Results.NotFound(new ProblemDetails
{
Title = "Policy revision not found",
Status = StatusCodes.Status404NotFound
}),
PolicyActivationResultStatus.NotApproved => Results.BadRequest(new ProblemDetails
{
Title = "Revision not approved",
Detail = "Only approved revisions may be activated.",
Status = StatusCodes.Status400BadRequest
}),
PolicyActivationResultStatus.DuplicateApproval => Results.BadRequest(new ProblemDetails
{
Title = "Approval already recorded",
Detail = "This approver has already approved activation.",
Status = StatusCodes.Status400BadRequest
}),
PolicyActivationResultStatus.PendingSecondApproval => Results.Accepted(
$"/api/policy/packs/{packId}/revisions/{version}",
new PolicyRevisionActivationResponse("pending_second_approval", PolicyPackMapper.ToDto(packId, result.Revision!))),
PolicyActivationResultStatus.Activated => Results.Ok(new PolicyRevisionActivationResponse("activated", PolicyPackMapper.ToDto(packId, result.Revision!))),
PolicyActivationResultStatus.AlreadyActive => Results.Ok(new PolicyRevisionActivationResponse("already_active", PolicyPackMapper.ToDto(packId, result.Revision!))),
_ => Results.BadRequest(new ProblemDetails
{
Title = "Activation failed",
Detail = "Unknown activation result.",
Status = StatusCodes.Status400BadRequest
})
};
}
private static string? ResolveActorId(HttpContext context)
{
var user = context.User;
var actor = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? user?.FindFirst(ClaimTypes.Upn)?.Value
?? user?.FindFirst("sub")?.Value;
if (!string.IsNullOrWhiteSpace(actor))
{
return actor;
}
if (context.Request.Headers.TryGetValue("X-StellaOps-Actor", out var header) && !string.IsNullOrWhiteSpace(header))
{
return header.ToString();
}
return null;
}
}
internal static class PolicyPackMapper
{
public static PolicyPackDto ToDto(PolicyPackRecord record)
=> new(record.PackId, record.DisplayName, record.CreatedAt, record.GetRevisions().Select(r => ToDto(record.PackId, r)).ToArray());
public static PolicyPackSummaryDto ToSummaryDto(PolicyPackRecord record)
=> new(record.PackId, record.DisplayName, record.CreatedAt, record.GetRevisions().Select(r => r.Version).ToArray());
public static PolicyRevisionDto ToDto(string packId, PolicyRevisionRecord revision)
=> new(
packId,
revision.Version,
revision.Status.ToString(),
revision.RequiresTwoPersonApproval,
revision.CreatedAt,
revision.ActivatedAt,
revision.Approvals.Select(a => new PolicyActivationApprovalDto(a.ActorId, a.ApprovedAt, a.Comment)).ToArray());
}
internal sealed record CreatePolicyPackRequest(string? PackId, string? DisplayName);
internal sealed record PolicyPackDto(string PackId, string? DisplayName, DateTimeOffset CreatedAt, IReadOnlyList<PolicyRevisionDto> Revisions);
internal sealed record PolicyPackSummaryDto(string PackId, string? DisplayName, DateTimeOffset CreatedAt, IReadOnlyList<int> Versions);
internal sealed record CreatePolicyRevisionRequest(int? Version, bool RequiresTwoPersonApproval, PolicyRevisionStatus InitialStatus = PolicyRevisionStatus.Approved);
internal sealed record PolicyRevisionDto(string PackId, int Version, string Status, bool RequiresTwoPersonApproval, DateTimeOffset CreatedAt, DateTimeOffset? ActivatedAt, IReadOnlyList<PolicyActivationApprovalDto> Approvals);
internal sealed record PolicyActivationApprovalDto(string ActorId, DateTimeOffset ApprovedAt, string? Comment);
internal sealed record ActivatePolicyRevisionRequest(string? Comment);
internal sealed record PolicyRevisionActivationResponse(string Status, PolicyRevisionDto Revision);
using System.Security.Claims;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.Abstractions;
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Services;
namespace StellaOps.Policy.Engine.Endpoints;
internal static class PolicyPackEndpoints
{
public static IEndpointRouteBuilder MapPolicyPacks(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/policy/packs")
.RequireAuthorization()
.WithTags("Policy Packs");
group.MapPost(string.Empty, CreatePack)
.WithName("CreatePolicyPack")
.WithSummary("Create a new policy pack container.")
.Produces<PolicyPackDto>(StatusCodes.Status201Created);
group.MapGet(string.Empty, ListPacks)
.WithName("ListPolicyPacks")
.WithSummary("List policy packs for the current tenant.")
.Produces<IReadOnlyList<PolicyPackSummaryDto>>(StatusCodes.Status200OK);
group.MapPost("/{packId}/revisions", CreateRevision)
.WithName("CreatePolicyRevision")
.WithSummary("Create or update policy revision metadata.")
.Produces<PolicyRevisionDto>(StatusCodes.Status201Created)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
group.MapPost("/{packId}/revisions/{version:int}:activate", ActivateRevision)
.WithName("ActivatePolicyRevision")
.WithSummary("Activate an approved policy revision, enforcing two-person approval when required.")
.Produces<PolicyRevisionActivationResponse>(StatusCodes.Status200OK)
.Produces<PolicyRevisionActivationResponse>(StatusCodes.Status202Accepted)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
return endpoints;
}
private static async Task<IResult> CreatePack(
HttpContext context,
[FromBody] CreatePolicyPackRequest request,
IPolicyPackRepository repository,
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 packId = string.IsNullOrWhiteSpace(request.PackId)
? $"pack-{Guid.NewGuid():n}"
: request.PackId.Trim();
var pack = await repository.CreateAsync(packId, request.DisplayName?.Trim(), cancellationToken).ConfigureAwait(false);
var dto = PolicyPackMapper.ToDto(pack);
return Results.Created($"/api/policy/packs/{dto.PackId}", dto);
}
private static async Task<IResult> ListPacks(
HttpContext context,
IPolicyPackRepository repository,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
{
return scopeResult;
}
var packs = await repository.ListAsync(cancellationToken).ConfigureAwait(false);
var summaries = packs.Select(PolicyPackMapper.ToSummaryDto).ToArray();
return Results.Ok(summaries);
}
private static async Task<IResult> CreateRevision(
HttpContext context,
[FromRoute] string packId,
[FromBody] CreatePolicyRevisionRequest request,
IPolicyPackRepository repository,
IPolicyActivationSettings activationSettings,
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
});
}
if (request.InitialStatus is not (PolicyRevisionStatus.Draft or PolicyRevisionStatus.Approved))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid status",
Detail = "Only Draft or Approved statuses are supported for new revisions.",
Status = StatusCodes.Status400BadRequest
});
}
var requiresTwoPersonApproval = activationSettings.ResolveRequirement(request.RequiresTwoPersonApproval);
var revision = await repository.UpsertRevisionAsync(
packId,
request.Version ?? 0,
requiresTwoPersonApproval,
request.InitialStatus,
cancellationToken).ConfigureAwait(false);
return Results.Created(
$"/api/policy/packs/{packId}/revisions/{revision.Version}",
PolicyPackMapper.ToDto(packId, revision));
}
private static async Task<IResult> ActivateRevision(
HttpContext context,
[FromRoute] string packId,
[FromRoute] int version,
[FromBody] ActivatePolicyRevisionRequest request,
IPolicyPackRepository repository,
IPolicyActivationAuditor auditor,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyActivate);
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 actorId = ResolveActorId(context);
if (actorId is null)
{
return Results.Problem("Actor identity required.", statusCode: StatusCodes.Status401Unauthorized);
}
var result = await repository.RecordActivationAsync(
packId,
version,
actorId,
DateTimeOffset.UtcNow,
request.Comment,
cancellationToken).ConfigureAwait(false);
var tenantId = context.User?.FindFirst(StellaOpsClaimTypes.Tenant)?.Value;
auditor.RecordActivation(packId, version, actorId, tenantId, result, request.Comment);
return result.Status switch
{
PolicyActivationResultStatus.PackNotFound => Results.NotFound(new ProblemDetails
{
Title = "Policy pack not found",
Status = StatusCodes.Status404NotFound
}),
PolicyActivationResultStatus.RevisionNotFound => Results.NotFound(new ProblemDetails
{
Title = "Policy revision not found",
Status = StatusCodes.Status404NotFound
}),
PolicyActivationResultStatus.NotApproved => Results.BadRequest(new ProblemDetails
{
Title = "Revision not approved",
Detail = "Only approved revisions may be activated.",
Status = StatusCodes.Status400BadRequest
}),
PolicyActivationResultStatus.DuplicateApproval => Results.BadRequest(new ProblemDetails
{
Title = "Approval already recorded",
Detail = "This approver has already approved activation.",
Status = StatusCodes.Status400BadRequest
}),
PolicyActivationResultStatus.PendingSecondApproval => Results.Accepted(
$"/api/policy/packs/{packId}/revisions/{version}",
new PolicyRevisionActivationResponse("pending_second_approval", PolicyPackMapper.ToDto(packId, result.Revision!))),
PolicyActivationResultStatus.Activated => Results.Ok(new PolicyRevisionActivationResponse("activated", PolicyPackMapper.ToDto(packId, result.Revision!))),
PolicyActivationResultStatus.AlreadyActive => Results.Ok(new PolicyRevisionActivationResponse("already_active", PolicyPackMapper.ToDto(packId, result.Revision!))),
_ => Results.BadRequest(new ProblemDetails
{
Title = "Activation failed",
Detail = "Unknown activation result.",
Status = StatusCodes.Status400BadRequest
})
};
}
private static string? ResolveActorId(HttpContext context)
{
var user = context.User;
var actor = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? user?.FindFirst(ClaimTypes.Upn)?.Value
?? user?.FindFirst("sub")?.Value;
if (!string.IsNullOrWhiteSpace(actor))
{
return actor;
}
if (context.Request.Headers.TryGetValue("X-StellaOps-Actor", out var header) && !string.IsNullOrWhiteSpace(header))
{
return header.ToString();
}
return null;
}
}
internal static class PolicyPackMapper
{
public static PolicyPackDto ToDto(PolicyPackRecord record)
=> new(record.PackId, record.DisplayName, record.CreatedAt, record.GetRevisions().Select(r => ToDto(record.PackId, r)).ToArray());
public static PolicyPackSummaryDto ToSummaryDto(PolicyPackRecord record)
=> new(record.PackId, record.DisplayName, record.CreatedAt, record.GetRevisions().Select(r => r.Version).ToArray());
public static PolicyRevisionDto ToDto(string packId, PolicyRevisionRecord revision)
=> new(
packId,
revision.Version,
revision.Status.ToString(),
revision.RequiresTwoPersonApproval,
revision.CreatedAt,
revision.ActivatedAt,
revision.Approvals.Select(a => new PolicyActivationApprovalDto(a.ActorId, a.ApprovedAt, a.Comment)).ToArray());
}
internal sealed record CreatePolicyPackRequest(string? PackId, string? DisplayName);
internal sealed record PolicyPackDto(string PackId, string? DisplayName, DateTimeOffset CreatedAt, IReadOnlyList<PolicyRevisionDto> Revisions);
internal sealed record PolicyPackSummaryDto(string PackId, string? DisplayName, DateTimeOffset CreatedAt, IReadOnlyList<int> Versions);
internal sealed record CreatePolicyRevisionRequest(int? Version, bool? RequiresTwoPersonApproval, PolicyRevisionStatus InitialStatus = PolicyRevisionStatus.Approved);
internal sealed record PolicyRevisionDto(string PackId, int Version, string Status, bool RequiresTwoPersonApproval, DateTimeOffset CreatedAt, DateTimeOffset? ActivatedAt, IReadOnlyList<PolicyActivationApprovalDto> Approvals);
internal sealed record PolicyActivationApprovalDto(string ActorId, DateTimeOffset ApprovedAt, string? Comment);
internal sealed record ActivatePolicyRevisionRequest(string? Comment);
internal sealed record PolicyRevisionActivationResponse(string Status, PolicyRevisionDto Revision);

View File

@@ -1,168 +1,227 @@
using System.Collections.ObjectModel;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Policy.Engine.Options;
/// <summary>
/// Root configuration for the Policy Engine host.
/// </summary>
public sealed class PolicyEngineOptions
{
public const string SectionName = "PolicyEngine";
public PolicyEngineAuthorityOptions Authority { get; } = new();
public PolicyEngineStorageOptions Storage { get; } = new();
public PolicyEngineWorkerOptions Workers { get; } = new();
public PolicyEngineResourceServerOptions ResourceServer { get; } = new();
public void Validate()
{
Authority.Validate();
Storage.Validate();
Workers.Validate();
ResourceServer.Validate();
}
}
public sealed class PolicyEngineAuthorityOptions
{
public bool Enabled { get; set; } = true;
public string Issuer { get; set; } = "https://authority.stella-ops.local";
public string ClientId { get; set; } = "policy-engine";
public string? ClientSecret { get; set; }
public IList<string> Scopes { get; } = new List<string>
{
StellaOpsScopes.PolicyRun,
StellaOpsScopes.FindingsRead,
StellaOpsScopes.EffectiveWrite
};
public int BackchannelTimeoutSeconds { get; set; } = 30;
public void Validate()
{
if (!Enabled)
{
return;
}
if (string.IsNullOrWhiteSpace(Issuer))
{
throw new InvalidOperationException("Policy Engine authority configuration requires an issuer.");
}
if (!Uri.TryCreate(Issuer, UriKind.Absolute, out var issuerUri) || !issuerUri.IsAbsoluteUri)
{
throw new InvalidOperationException("Policy Engine authority issuer must be an absolute URI.");
}
if (issuerUri.Scheme != Uri.UriSchemeHttps && !issuerUri.IsLoopback)
{
throw new InvalidOperationException("Policy Engine authority issuer must use HTTPS unless targeting loopback.");
}
if (string.IsNullOrWhiteSpace(ClientId))
{
throw new InvalidOperationException("Policy Engine authority configuration requires a clientId.");
}
if (Scopes.Count == 0)
{
throw new InvalidOperationException("Policy Engine authority configuration requires at least one scope.");
}
if (BackchannelTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Policy Engine authority backchannel timeout must be greater than zero.");
}
}
}
public sealed class PolicyEngineStorageOptions
{
public string ConnectionString { get; set; } = "mongodb://localhost:27017/policy-engine";
public string DatabaseName { get; set; } = "policy_engine";
public int CommandTimeoutSeconds { get; set; } = 30;
public void Validate()
{
if (string.IsNullOrWhiteSpace(ConnectionString))
{
throw new InvalidOperationException("Policy Engine storage configuration requires a MongoDB connection string.");
}
if (string.IsNullOrWhiteSpace(DatabaseName))
{
throw new InvalidOperationException("Policy Engine storage configuration requires a database name.");
}
if (CommandTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Policy Engine storage command timeout must be greater than zero.");
}
}
public TimeSpan CommandTimeout => TimeSpan.FromSeconds(CommandTimeoutSeconds);
}
public sealed class PolicyEngineWorkerOptions
{
public int SchedulerIntervalSeconds { get; set; } = 15;
public int MaxConcurrentEvaluations { get; set; } = 4;
public void Validate()
{
if (SchedulerIntervalSeconds <= 0)
{
throw new InvalidOperationException("Policy Engine worker interval must be greater than zero.");
}
if (MaxConcurrentEvaluations <= 0)
{
throw new InvalidOperationException("Policy Engine worker concurrency must be greater than zero.");
}
}
}
public sealed class PolicyEngineResourceServerOptions
{
public string Authority { get; set; } = "https://authority.stella-ops.local";
public IList<string> Audiences { get; } = new List<string> { "api://policy-engine" };
public IList<string> RequiredScopes { get; } = new List<string> { StellaOpsScopes.PolicyRun };
public IList<string> RequiredTenants { get; } = new List<string>();
public IList<string> BypassNetworks { get; } = new List<string> { "127.0.0.1/32", "::1/128" };
public bool RequireHttpsMetadata { get; set; } = true;
public void Validate()
{
if (string.IsNullOrWhiteSpace(Authority))
{
throw new InvalidOperationException("Resource server configuration requires an Authority URL.");
}
if (!Uri.TryCreate(Authority.Trim(), UriKind.Absolute, out var uri))
{
throw new InvalidOperationException("Resource server Authority URL must be absolute.");
}
if (RequireHttpsMetadata && !uri.IsLoopback && !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Resource server Authority URL must use HTTPS when HTTPS metadata is required.");
}
}
}
using System.Collections.ObjectModel;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Policy.Engine.Options;
/// <summary>
/// Root configuration for the Policy Engine host.
/// </summary>
public sealed class PolicyEngineOptions
{
public const string SectionName = "PolicyEngine";
public PolicyEngineAuthorityOptions Authority { get; } = new();
public PolicyEngineStorageOptions Storage { get; } = new();
public PolicyEngineWorkerOptions Workers { get; } = new();
public PolicyEngineResourceServerOptions ResourceServer { get; } = new();
public PolicyEngineCompilationOptions Compilation { get; } = new();
public PolicyEngineActivationOptions Activation { get; } = new();
public void Validate()
{
Authority.Validate();
Storage.Validate();
Workers.Validate();
ResourceServer.Validate();
Compilation.Validate();
Activation.Validate();
}
}
public sealed class PolicyEngineAuthorityOptions
{
public bool Enabled { get; set; } = true;
public string Issuer { get; set; } = "https://authority.stella-ops.local";
public string ClientId { get; set; } = "policy-engine";
public string? ClientSecret { get; set; }
public IList<string> Scopes { get; } = new List<string>
{
StellaOpsScopes.PolicyRun,
StellaOpsScopes.FindingsRead,
StellaOpsScopes.EffectiveWrite
};
public int BackchannelTimeoutSeconds { get; set; } = 30;
public void Validate()
{
if (!Enabled)
{
return;
}
if (string.IsNullOrWhiteSpace(Issuer))
{
throw new InvalidOperationException("Policy Engine authority configuration requires an issuer.");
}
if (!Uri.TryCreate(Issuer, UriKind.Absolute, out var issuerUri) || !issuerUri.IsAbsoluteUri)
{
throw new InvalidOperationException("Policy Engine authority issuer must be an absolute URI.");
}
if (issuerUri.Scheme != Uri.UriSchemeHttps && !issuerUri.IsLoopback)
{
throw new InvalidOperationException("Policy Engine authority issuer must use HTTPS unless targeting loopback.");
}
if (string.IsNullOrWhiteSpace(ClientId))
{
throw new InvalidOperationException("Policy Engine authority configuration requires a clientId.");
}
if (Scopes.Count == 0)
{
throw new InvalidOperationException("Policy Engine authority configuration requires at least one scope.");
}
if (BackchannelTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Policy Engine authority backchannel timeout must be greater than zero.");
}
}
}
public sealed class PolicyEngineStorageOptions
{
public string ConnectionString { get; set; } = "mongodb://localhost:27017/policy-engine";
public string DatabaseName { get; set; } = "policy_engine";
public int CommandTimeoutSeconds { get; set; } = 30;
public void Validate()
{
if (string.IsNullOrWhiteSpace(ConnectionString))
{
throw new InvalidOperationException("Policy Engine storage configuration requires a MongoDB connection string.");
}
if (string.IsNullOrWhiteSpace(DatabaseName))
{
throw new InvalidOperationException("Policy Engine storage configuration requires a database name.");
}
if (CommandTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Policy Engine storage command timeout must be greater than zero.");
}
}
public TimeSpan CommandTimeout => TimeSpan.FromSeconds(CommandTimeoutSeconds);
}
public sealed class PolicyEngineWorkerOptions
{
public int SchedulerIntervalSeconds { get; set; } = 15;
public int MaxConcurrentEvaluations { get; set; } = 4;
public void Validate()
{
if (SchedulerIntervalSeconds <= 0)
{
throw new InvalidOperationException("Policy Engine worker interval must be greater than zero.");
}
if (MaxConcurrentEvaluations <= 0)
{
throw new InvalidOperationException("Policy Engine worker concurrency must be greater than zero.");
}
}
}
public sealed class PolicyEngineResourceServerOptions
{
public string Authority { get; set; } = "https://authority.stella-ops.local";
public IList<string> Audiences { get; } = new List<string> { "api://policy-engine" };
public IList<string> RequiredScopes { get; } = new List<string> { StellaOpsScopes.PolicyRun };
public IList<string> RequiredTenants { get; } = new List<string>();
public IList<string> BypassNetworks { get; } = new List<string> { "127.0.0.1/32", "::1/128" };
public bool RequireHttpsMetadata { get; set; } = true;
public void Validate()
{
if (string.IsNullOrWhiteSpace(Authority))
{
throw new InvalidOperationException("Resource server configuration requires an Authority URL.");
}
if (!Uri.TryCreate(Authority.Trim(), UriKind.Absolute, out var uri))
{
throw new InvalidOperationException("Resource server Authority URL must be absolute.");
}
if (RequireHttpsMetadata && !uri.IsLoopback && !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Resource server Authority URL must use HTTPS when HTTPS metadata is required.");
}
}
}
public sealed class PolicyEngineCompilationOptions
{
/// <summary>
/// Maximum allowed complexity score for compiled policies. Set to <c>&lt;= 0</c> to disable.
/// </summary>
public double MaxComplexityScore { get; set; } = 750d;
/// <summary>
/// Maximum allowed compilation wall-clock duration in milliseconds. Set to <c>&lt;= 0</c> to disable.
/// </summary>
public int MaxDurationMilliseconds { get; set; } = 1500;
public bool EnforceComplexity => MaxComplexityScore > 0;
public bool EnforceDuration => MaxDurationMilliseconds > 0;
public void Validate()
{
if (MaxComplexityScore < 0)
{
throw new InvalidOperationException("Compilation.maxComplexityScore must be greater than or equal to zero.");
}
if (MaxDurationMilliseconds < 0)
{
throw new InvalidOperationException("Compilation.maxDurationMilliseconds must be greater than or equal to zero.");
}
}
}
public sealed class PolicyEngineActivationOptions
{
/// <summary>
/// Forces two distinct approvals for every activation regardless of the request payload.
/// </summary>
public bool ForceTwoPersonApproval { get; set; } = false;
/// <summary>
/// Default value applied when callers omit <c>requiresTwoPersonApproval</c>.
/// </summary>
public bool DefaultRequiresTwoPersonApproval { get; set; } = false;
/// <summary>
/// Emits structured audit logs for every activation attempt.
/// </summary>
public bool EmitAuditLogs { get; set; } = true;
public void Validate()
{
}
}

View File

@@ -13,7 +13,24 @@ using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.Engine.Workers;
using StellaOps.AirGap.Policy;
var builder = WebApplication.CreateBuilder(args);
var builder = WebApplication.CreateBuilder(args);
var policyEngineConfigFiles = new[]
{
"../etc/policy-engine.yaml",
"../etc/policy-engine.local.yaml",
"policy-engine.yaml",
"policy-engine.local.yaml"
};
var policyEngineActivationConfigFiles = new[]
{
"../etc/policy-engine.activation.yaml",
"../etc/policy-engine.activation.local.yaml",
"/config/policy-engine/activation.yaml",
"policy-engine.activation.yaml",
"policy-engine.activation.local.yaml"
};
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
@@ -25,41 +42,41 @@ builder.Configuration.AddStellaOpsDefaults(options =>
options.ConfigureBuilder = configurationBuilder =>
{
var contentRoot = builder.Environment.ContentRootPath;
foreach (var relative in new[]
{
"../etc/policy-engine.yaml",
"../etc/policy-engine.local.yaml",
"policy-engine.yaml",
"policy-engine.local.yaml"
})
{
var path = Path.Combine(contentRoot, relative);
configurationBuilder.AddYamlFile(path, optional: true);
}
};
});
var bootstrap = StellaOpsConfigurationBootstrapper.Build<PolicyEngineOptions>(options =>
foreach (var relative in policyEngineConfigFiles)
{
var path = Path.Combine(contentRoot, relative);
configurationBuilder.AddYamlFile(path, optional: true);
}
foreach (var relative in policyEngineActivationConfigFiles)
{
var path = Path.Combine(contentRoot, relative);
configurationBuilder.AddYamlFile(path, optional: true);
}
};
});
var bootstrap = StellaOpsConfigurationBootstrapper.Build<PolicyEngineOptions>(options =>
{
options.BasePath = builder.Environment.ContentRootPath;
options.EnvironmentPrefix = "STELLAOPS_POLICY_ENGINE_";
options.BindingSection = PolicyEngineOptions.SectionName;
options.ConfigureBuilder = configurationBuilder =>
{
foreach (var relative in new[]
{
"../etc/policy-engine.yaml",
"../etc/policy-engine.local.yaml",
"policy-engine.yaml",
"policy-engine.local.yaml"
})
{
var path = Path.Combine(builder.Environment.ContentRootPath, relative);
configurationBuilder.AddYamlFile(path, optional: true);
}
};
options.PostBind = static (value, _) => value.Validate();
});
foreach (var relative in policyEngineConfigFiles)
{
var path = Path.Combine(builder.Environment.ContentRootPath, relative);
configurationBuilder.AddYamlFile(path, optional: true);
}
foreach (var relative in policyEngineActivationConfigFiles)
{
var path = Path.Combine(builder.Environment.ContentRootPath, relative);
configurationBuilder.AddYamlFile(path, optional: true);
}
};
options.PostBind = static (value, _) => value.Validate();
});
builder.Configuration.AddConfiguration(bootstrap.Configuration);

View File

@@ -1,3 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Policy.Engine.Tests")]
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Policy.Engine.Tests")]

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Options;
namespace StellaOps.Policy.Engine.Services;
internal interface IPolicyActivationAuditor
{
void RecordActivation(
string packId,
int version,
string actorId,
string? tenantId,
PolicyActivationResult result,
string? comment);
}
internal sealed class PolicyActivationAuditor : IPolicyActivationAuditor
{
private const int CommentLimit = 512;
private readonly PolicyEngineOptions options;
private readonly ILogger<PolicyActivationAuditor> logger;
public PolicyActivationAuditor(
PolicyEngineOptions options,
ILogger<PolicyActivationAuditor> logger)
{
this.options = options ?? throw new ArgumentNullException(nameof(options));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void RecordActivation(
string packId,
int version,
string actorId,
string? tenantId,
PolicyActivationResult result,
string? comment)
{
if (!options.Activation.EmitAuditLogs)
{
return;
}
ArgumentNullException.ThrowIfNull(packId);
ArgumentNullException.ThrowIfNull(actorId);
ArgumentNullException.ThrowIfNull(result);
var normalizedStatus = NormalizeStatus(result.Status);
var scope = new Dictionary<string, object?>
{
["policy.pack_id"] = packId,
["policy.revision"] = version,
["policy.activation.status"] = normalizedStatus,
["policy.activation.actor"] = actorId
};
if (!string.IsNullOrWhiteSpace(tenantId))
{
scope["policy.tenant"] = tenantId;
}
if (!string.IsNullOrWhiteSpace(comment))
{
scope["policy.activation.comment"] = Truncate(comment!, CommentLimit);
}
if (result.Revision is { } revision)
{
scope["policy.activation.requires_two_person"] = revision.RequiresTwoPersonApproval;
scope["policy.activation.approval_count"] = revision.Approvals.Length;
if (revision.Approvals.Length > 0)
{
scope["policy.activation.approvers"] = revision.Approvals
.Select(static approval => approval.ActorId)
.Where(static actor => !string.IsNullOrWhiteSpace(actor))
.ToArray();
}
}
using (logger.BeginScope(scope))
{
logger.LogInformation(
"Policy activation {PackId}/{Revision} completed with status {Status}.",
packId,
version,
normalizedStatus);
}
}
private static string NormalizeStatus(PolicyActivationResultStatus status)
=> status.ToString().ToLowerInvariant();
private static string Truncate(string value, int maxLength)
=> value.Length <= maxLength ? value : value[..maxLength];
}

View File

@@ -0,0 +1,34 @@
using System;
using StellaOps.Policy.Engine.Options;
namespace StellaOps.Policy.Engine.Services;
internal interface IPolicyActivationSettings
{
bool ResolveRequirement(bool? requested);
}
internal sealed class PolicyActivationSettings : IPolicyActivationSettings
{
private readonly PolicyEngineOptions options;
public PolicyActivationSettings(PolicyEngineOptions options)
{
this.options = options ?? throw new ArgumentNullException(nameof(options));
}
public bool ResolveRequirement(bool? requested)
{
if (options.Activation.ForceTwoPersonApproval)
{
return true;
}
if (requested.HasValue)
{
return requested.Value;
}
return options.Activation.DefaultRequiresTwoPersonApproval;
}
}

View File

@@ -1,6 +1,9 @@
using System.Collections.Immutable;
using StellaOps.Policy;
using StellaOps.Policy.Engine.Compilation;
using System;
using System.Collections.Immutable;
using Microsoft.Extensions.Options;
using StellaOps.Policy;
using StellaOps.Policy.Engine.Compilation;
using StellaOps.Policy.Engine.Options;
namespace StellaOps.Policy.Engine.Services;
@@ -8,14 +11,24 @@ namespace StellaOps.Policy.Engine.Services;
/// Provides deterministic compilation for <c>stella-dsl@1</c> policy documents and exposes
/// basic statistics consumed by API/CLI surfaces.
/// </summary>
internal sealed class PolicyCompilationService
{
private readonly PolicyCompiler compiler;
public PolicyCompilationService(PolicyCompiler compiler)
{
this.compiler = compiler ?? throw new ArgumentNullException(nameof(compiler));
}
internal sealed class PolicyCompilationService
{
private readonly PolicyCompiler compiler;
private readonly PolicyComplexityAnalyzer complexityAnalyzer;
private readonly IOptionsMonitor<PolicyEngineOptions> optionsMonitor;
private readonly TimeProvider timeProvider;
public PolicyCompilationService(
PolicyCompiler compiler,
PolicyComplexityAnalyzer complexityAnalyzer,
IOptionsMonitor<PolicyEngineOptions> optionsMonitor,
TimeProvider timeProvider)
{
this.compiler = compiler ?? throw new ArgumentNullException(nameof(compiler));
this.complexityAnalyzer = complexityAnalyzer ?? throw new ArgumentNullException(nameof(complexityAnalyzer));
this.optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
this.timeProvider = timeProvider ?? TimeProvider.System;
}
public PolicyCompilationResultDto Compile(PolicyCompileRequest request)
{
@@ -31,51 +44,96 @@ internal sealed class PolicyCompilationService
if (!string.Equals(request.Dsl.Syntax, "stella-dsl@1", StringComparison.Ordinal))
{
return PolicyCompilationResultDto.FromFailure(
ImmutableArray.Create(PolicyIssue.Error(
PolicyDslDiagnosticCodes.UnsupportedSyntaxVersion,
$"Unsupported syntax '{request.Dsl.Syntax ?? "null"}'. Expected 'stella-dsl@1'.",
"dsl.syntax")));
}
return PolicyCompilationResultDto.FromFailure(
ImmutableArray.Create(PolicyIssue.Error(
PolicyDslDiagnosticCodes.UnsupportedSyntaxVersion,
$"Unsupported syntax '{request.Dsl.Syntax ?? "null"}'. Expected 'stella-dsl@1'.",
"dsl.syntax")),
complexity: null,
durationMilliseconds: 0);
}
var result = compiler.Compile(request.Dsl.Source);
if (!result.Success || result.Document is null)
{
return PolicyCompilationResultDto.FromFailure(result.Diagnostics);
}
return PolicyCompilationResultDto.FromSuccess(result);
}
}
var start = timeProvider.GetTimestamp();
var result = compiler.Compile(request.Dsl.Source);
var elapsed = timeProvider.GetElapsedTime(start, timeProvider.GetTimestamp());
var durationMilliseconds = (long)Math.Ceiling(elapsed.TotalMilliseconds);
if (!result.Success || result.Document is null)
{
return PolicyCompilationResultDto.FromFailure(result.Diagnostics, null, durationMilliseconds);
}
var complexity = complexityAnalyzer.Analyze(result.Document);
var diagnostics = result.Diagnostics.IsDefault ? ImmutableArray<PolicyIssue>.Empty : result.Diagnostics;
var limits = optionsMonitor.CurrentValue?.Compilation ?? new PolicyEngineCompilationOptions();
if (limits.EnforceComplexity && complexity.Score > limits.MaxComplexityScore)
{
var diagnostic = PolicyIssue.Error(
PolicyEngineDiagnosticCodes.CompilationComplexityExceeded,
$"Policy complexity score {complexity.Score:F2} exceeds configured maximum {limits.MaxComplexityScore:F2}. Reduce rule count or expression depth.",
"$.rules");
diagnostics = AppendDiagnostic(diagnostics, diagnostic);
return PolicyCompilationResultDto.FromFailure(diagnostics, complexity, durationMilliseconds);
}
if (limits.EnforceDuration && durationMilliseconds > limits.MaxDurationMilliseconds)
{
var diagnostic = PolicyIssue.Error(
PolicyEngineDiagnosticCodes.CompilationComplexityExceeded,
$"Policy compilation time {durationMilliseconds} ms exceeded limit {limits.MaxDurationMilliseconds} ms.",
"$.dsl");
diagnostics = AppendDiagnostic(diagnostics, diagnostic);
return PolicyCompilationResultDto.FromFailure(diagnostics, complexity, durationMilliseconds);
}
return PolicyCompilationResultDto.FromSuccess(result, complexity, durationMilliseconds);
}
private static ImmutableArray<PolicyIssue> AppendDiagnostic(ImmutableArray<PolicyIssue> diagnostics, PolicyIssue diagnostic)
=> diagnostics.IsDefault
? ImmutableArray.Create(diagnostic)
: diagnostics.Add(diagnostic);
}
internal sealed record PolicyCompileRequest(PolicyDslPayload Dsl);
internal sealed record PolicyDslPayload(string Syntax, string Source);
internal sealed record PolicyCompilationResultDto(
bool Success,
string? Digest,
PolicyCompilationStatistics? Statistics,
ImmutableArray<PolicyIssue> Diagnostics)
{
public static PolicyCompilationResultDto FromFailure(ImmutableArray<PolicyIssue> diagnostics) =>
new(false, null, null, diagnostics);
public static PolicyCompilationResultDto FromSuccess(PolicyCompilationResult compilationResult)
{
if (compilationResult.Document is null)
{
throw new ArgumentException("Compilation result must include a document for success.", nameof(compilationResult));
}
var stats = PolicyCompilationStatistics.Create(compilationResult.Document);
return new PolicyCompilationResultDto(
true,
$"sha256:{compilationResult.Checksum}",
stats,
compilationResult.Diagnostics);
}
}
internal sealed record PolicyCompilationResultDto(
bool Success,
string? Digest,
PolicyCompilationStatistics? Statistics,
ImmutableArray<PolicyIssue> Diagnostics,
PolicyComplexityReport? Complexity,
long DurationMilliseconds)
{
public static PolicyCompilationResultDto FromFailure(
ImmutableArray<PolicyIssue> diagnostics,
PolicyComplexityReport? complexity,
long durationMilliseconds) =>
new(false, null, null, diagnostics, complexity, durationMilliseconds);
public static PolicyCompilationResultDto FromSuccess(
PolicyCompilationResult compilationResult,
PolicyComplexityReport complexity,
long durationMilliseconds)
{
if (compilationResult.Document is null)
{
throw new ArgumentException("Compilation result must include a document for success.", nameof(compilationResult));
}
var stats = PolicyCompilationStatistics.Create(compilationResult.Document);
return new PolicyCompilationResultDto(
true,
$"sha256:{compilationResult.Checksum}",
stats,
compilationResult.Diagnostics,
complexity,
durationMilliseconds);
}
}
internal sealed record PolicyCompilationStatistics(
int RuleCount,

View File

@@ -0,0 +1,6 @@
namespace StellaOps.Policy.Engine.Services;
internal static class PolicyEngineDiagnosticCodes
{
public const string CompilationComplexityExceeded = "ERR_POL_COMPLEXITY";
}

View File

@@ -45,8 +45,8 @@
|----|--------|----------|------------|-------------|---------------|
| POLICY-ENGINE-27-001 | TODO | Policy Guild | POLICY-ENGINE-20-001, REGISTRY-API-27-003 | Extend compile outputs to include rule coverage metadata, symbol table, inline documentation, and rule index for editor autocomplete; persist deterministic hashes. | Compile endpoint returns coverage + symbol table; responses validated with fixtures; hashing deterministic across runs; docs updated. |
| POLICY-ENGINE-27-002 | TODO | Policy Guild, Observability Guild | POLICY-ENGINE-20-002, POLICY-ENGINE-27-001 | Enhance simulate endpoints to emit rule firing counts, heatmap aggregates, sampled explain traces with deterministic ordering, and delta summaries for quick/batch sims. | Simulation outputs include ordered heatmap + sample explains; integration tests verify determinism; telemetry emits `policy_rule_fired_total`. |
| POLICY-ENGINE-27-003 | TODO | Policy Guild, Security Guild | POLICY-ENGINE-20-005 | Implement complexity/time limit enforcement with compiler scoring, configurable thresholds, and structured diagnostics (`ERR_POL_COMPLEXITY`). | Policies exceeding limits return actionable diagnostics; limits configurable per tenant; regression tests cover allow/block cases. |
| POLICY-ENGINE-27-004 | TODO | Policy Guild, QA Guild | POLICY-ENGINE-27-001..003 | Update golden/property tests to cover new coverage metrics, symbol tables, explain traces, and complexity limits; provide fixtures for Registry/Console integration. | Test suites extended; fixtures shared under `StellaOps.Policy.Engine.Tests/Fixtures/policy-studio`; CI ensures determinism across runs. |
| POLICY-ENGINE-27-003 | DONE | Policy Guild, Security Guild | POLICY-ENGINE-20-005 | Implement complexity/time limit enforcement with compiler scoring, configurable thresholds, and structured diagnostics (`ERR_POL_COMPLEXITY`). | Policies exceeding limits return actionable diagnostics; limits configurable per tenant; regression tests cover allow/block cases. |
| POLICY-ENGINE-27-004 | DONE | Policy Guild, QA Guild | POLICY-ENGINE-27-001..003 | Update golden/property tests to cover new coverage metrics, symbol tables, explain traces, and complexity limits; provide fixtures for Registry/Console integration. | Test suites extended; fixtures shared under `StellaOps.Policy.Engine.Tests/Fixtures/policy-studio`; CI ensures determinism across runs. |
## Epic 3: Graph Explorer v1

View File

@@ -1,45 +1,45 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace StellaOps.Policy.Gateway.Contracts;
public sealed record PolicyPackSummaryDto(
string PackId,
string? DisplayName,
DateTimeOffset CreatedAt,
IReadOnlyList<int> Versions);
public sealed record PolicyPackDto(
string PackId,
string? DisplayName,
DateTimeOffset CreatedAt,
IReadOnlyList<PolicyRevisionDto> Revisions);
public sealed record PolicyRevisionDto(
int Version,
string Status,
bool RequiresTwoPersonApproval,
DateTimeOffset CreatedAt,
DateTimeOffset? ActivatedAt,
IReadOnlyList<PolicyActivationApprovalDto> Approvals);
public sealed record PolicyActivationApprovalDto(
string ActorId,
DateTimeOffset ApprovedAt,
string? Comment);
public sealed record PolicyRevisionActivationDto(
string Status,
PolicyRevisionDto Revision);
public sealed record CreatePolicyPackRequest(
[StringLength(200)] string? PackId,
[StringLength(200)] string? DisplayName);
public sealed record CreatePolicyRevisionRequest(
int? Version,
bool RequiresTwoPersonApproval,
string InitialStatus = "Approved");
public sealed record ActivatePolicyRevisionRequest(string? Comment);
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace StellaOps.Policy.Gateway.Contracts;
public sealed record PolicyPackSummaryDto(
string PackId,
string? DisplayName,
DateTimeOffset CreatedAt,
IReadOnlyList<int> Versions);
public sealed record PolicyPackDto(
string PackId,
string? DisplayName,
DateTimeOffset CreatedAt,
IReadOnlyList<PolicyRevisionDto> Revisions);
public sealed record PolicyRevisionDto(
int Version,
string Status,
bool RequiresTwoPersonApproval,
DateTimeOffset CreatedAt,
DateTimeOffset? ActivatedAt,
IReadOnlyList<PolicyActivationApprovalDto> Approvals);
public sealed record PolicyActivationApprovalDto(
string ActorId,
DateTimeOffset ApprovedAt,
string? Comment);
public sealed record PolicyRevisionActivationDto(
string Status,
PolicyRevisionDto Revision);
public sealed record CreatePolicyPackRequest(
[StringLength(200)] string? PackId,
[StringLength(200)] string? DisplayName);
public sealed record CreatePolicyRevisionRequest(
int? Version,
bool? RequiresTwoPersonApproval,
string InitialStatus = "Approved");
public sealed record ActivatePolicyRevisionRequest(string? Comment);

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Options;
using StellaOps.Policy.Engine.Services;
using Xunit;
namespace StellaOps.Policy.Engine.Tests;
public class PolicyActivationAuditorTests
{
[Fact]
public void RecordActivation_WhenDisabled_DoesNothing()
{
var options = new PolicyEngineOptions();
options.Activation.EmitAuditLogs = false;
var logger = new TestLogger<PolicyActivationAuditor>();
var auditor = new PolicyActivationAuditor(options, logger);
var result = new PolicyActivationResult(PolicyActivationResultStatus.PackNotFound, null);
auditor.RecordActivation("pack", 1, "alice", null, result, null);
Assert.Empty(logger.Entries);
}
[Fact]
public void RecordActivation_WhenEnabled_WritesScopedLog()
{
var options = new PolicyEngineOptions();
options.Activation.EmitAuditLogs = true;
var logger = new TestLogger<PolicyActivationAuditor>();
var auditor = new PolicyActivationAuditor(options, logger);
var revision = new PolicyRevisionRecord(1, true, PolicyRevisionStatus.Approved, DateTimeOffset.UtcNow);
revision.AddApproval(new PolicyActivationApproval("alice", DateTimeOffset.UtcNow, "first"));
var result = new PolicyActivationResult(PolicyActivationResultStatus.PendingSecondApproval, revision);
auditor.RecordActivation("pack-a", 3, "bob", "tenant-x", result, "please rollout");
var entry = Assert.Single(logger.Entries);
Assert.Contains("pack-a", entry.Message);
Assert.Contains("pendingsecondapproval", entry.Message.ToLowerInvariant());
var scope = Assert.IsType<Dictionary<string, object?>>(Assert.Single(entry.Scopes));
Assert.Equal("tenant-x", scope["policy.tenant"]);
Assert.Equal("bob", scope["policy.activation.actor"]);
Assert.True((bool)scope["policy.activation.requires_two_person"]!);
Assert.Equal(1, scope["policy.activation.approval_count"]);
}
private sealed record LogEntry(LogLevel Level, string Message, IReadOnlyList<object?> Scopes);
private sealed class TestLogger<T> : ILogger<T>
{
private readonly Stack<object?> scopeStack = new();
public List<LogEntry> Entries { get; } = new();
IDisposable ILogger.BeginScope<TState>(TState state)
{
scopeStack.Push(state);
return new DisposeAction(scopeStack);
}
bool ILogger.IsEnabled(LogLevel logLevel) => true;
void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
Entries.Add(new LogEntry(logLevel, formatter(state, exception), scopeStack.Count > 0 ? new List<object?>(scopeStack) : new List<object?>()));
}
private sealed class DisposeAction : IDisposable
{
private readonly Stack<object?> stack;
public DisposeAction(Stack<object?> stack)
{
this.stack = stack;
}
public void Dispose()
{
if (stack.Count > 0)
{
stack.Pop();
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
using StellaOps.Policy.Engine.Options;
using StellaOps.Policy.Engine.Services;
using Xunit;
namespace StellaOps.Policy.Engine.Tests;
public class PolicyActivationSettingsTests
{
[Fact]
public void ResolveRequirement_WhenForceEnabled_IgnoresRequest()
{
var options = new PolicyEngineOptions();
options.Activation.ForceTwoPersonApproval = true;
var settings = new PolicyActivationSettings(options);
Assert.True(settings.ResolveRequirement(false));
Assert.True(settings.ResolveRequirement(null));
}
[Fact]
public void ResolveRequirement_UsesRequestedValue_WhenProvided()
{
var options = new PolicyEngineOptions();
var settings = new PolicyActivationSettings(options);
Assert.True(settings.ResolveRequirement(true));
Assert.False(settings.ResolveRequirement(false));
}
[Fact]
public void ResolveRequirement_FallsBackToDefault_WhenRequestMissing()
{
var options = new PolicyEngineOptions();
options.Activation.DefaultRequiresTwoPersonApproval = true;
var settings = new PolicyActivationSettings(options);
Assert.True(settings.ResolveRequirement(null));
options.Activation.DefaultRequiresTwoPersonApproval = false;
Assert.False(settings.ResolveRequirement(null));
}
}

View File

@@ -0,0 +1,136 @@
using System;
using Microsoft.Extensions.Options;
using StellaOps.Policy;
using StellaOps.Policy.Engine.Compilation;
using StellaOps.Policy.Engine.Options;
using StellaOps.Policy.Engine.Services;
using Xunit;
namespace StellaOps.Policy.Engine.Tests;
public sealed class PolicyCompilationServiceTests
{
private const string SimplePolicy = """
policy "Sample" syntax "stella-dsl@1" {
rule block_high priority 10 {
when severity.normalized >= "High"
then status := "blocked"
because "Block high severity findings"
}
rule warn_medium priority 20 {
when severity.normalized >= "Medium"
then status := "warn"
because "Warn on medium severity findings"
}
}
""";
[Fact]
public void Compile_ReturnsComplexityReport_WhenWithinLimits()
{
var service = CreateService(maxComplexityScore: 1000, maxDurationMilliseconds: 1000, simulatedDurationMilliseconds: 12.3);
var request = new PolicyCompileRequest(new PolicyDslPayload("stella-dsl@1", SimplePolicy));
var result = service.Compile(request);
Assert.True(result.Success);
Assert.NotNull(result.Digest);
Assert.NotNull(result.Complexity);
Assert.True(result.Complexity!.Score > 0);
Assert.True(result.Complexity.RuleCount >= 2);
Assert.Equal(13, result.DurationMilliseconds);
Assert.True(result.Diagnostics.IsDefaultOrEmpty);
}
[Fact]
public void Compile_Fails_WhenComplexityExceedsThreshold()
{
var service = CreateService(maxComplexityScore: 1, maxDurationMilliseconds: 1000, simulatedDurationMilliseconds: 2);
var request = new PolicyCompileRequest(new PolicyDslPayload("stella-dsl@1", SimplePolicy));
var result = service.Compile(request);
Assert.False(result.Success);
Assert.NotNull(result.Complexity);
Assert.Equal(2, result.DurationMilliseconds);
var diagnostic = Assert.Single(result.Diagnostics);
Assert.Equal(PolicyEngineDiagnosticCodes.CompilationComplexityExceeded, diagnostic.Code);
Assert.Equal(PolicyIssueSeverity.Error, diagnostic.Severity);
}
[Fact]
public void Compile_Fails_WhenDurationExceedsThreshold()
{
var service = CreateService(maxComplexityScore: 1000, maxDurationMilliseconds: 1, simulatedDurationMilliseconds: 5.2);
var request = new PolicyCompileRequest(new PolicyDslPayload("stella-dsl@1", SimplePolicy));
var result = service.Compile(request);
Assert.False(result.Success);
Assert.NotNull(result.Complexity);
Assert.Equal(6, result.DurationMilliseconds);
var diagnostic = Assert.Single(result.Diagnostics);
Assert.Equal(PolicyEngineDiagnosticCodes.CompilationComplexityExceeded, diagnostic.Code);
}
private static PolicyCompilationService CreateService(double maxComplexityScore, int maxDurationMilliseconds, double simulatedDurationMilliseconds)
{
var compiler = new PolicyCompiler();
var analyzer = new PolicyComplexityAnalyzer();
var options = new PolicyEngineOptions();
options.Compilation.MaxComplexityScore = maxComplexityScore;
options.Compilation.MaxDurationMilliseconds = maxDurationMilliseconds;
var optionsMonitor = new StaticOptionsMonitor<PolicyEngineOptions>(options);
var timeProvider = new FakeTimeProvider(simulatedDurationMilliseconds);
return new PolicyCompilationService(compiler, analyzer, optionsMonitor, timeProvider);
}
private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>
where T : class
{
public StaticOptionsMonitor(T value)
{
CurrentValue = value ?? throw new ArgumentNullException(nameof(value));
}
public T CurrentValue { get; }
public T Get(string? name) => CurrentValue;
public IDisposable OnChange(Action<T, string> listener) => Disposable.Instance;
private sealed class Disposable : IDisposable
{
public static readonly Disposable Instance = new();
public void Dispose()
{
}
}
}
private sealed class FakeTimeProvider : TimeProvider
{
private readonly long elapsedCounts;
private readonly long frequency = 1_000_000;
private bool firstCall = true;
public FakeTimeProvider(double milliseconds)
{
elapsedCounts = (long)Math.Round(milliseconds * frequency / 1000d);
}
public override long GetTimestamp()
{
if (firstCall)
{
firstCall = false;
return 0;
}
return elapsedCounts;
}
public override long TimestampFrequency => frequency;
}
}