notify doctors work, audit work, new product advisory sprints
This commit is contained in:
@@ -5,6 +5,7 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.RiskProfile.Export;
|
||||
using StellaOps.Policy.RiskProfile.Hashing;
|
||||
using StellaOps.Policy.RiskProfile.Models;
|
||||
@@ -22,6 +23,7 @@ public sealed class RiskProfileAirGapExportService
|
||||
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ISealedModeService? _sealedModeService;
|
||||
private readonly RiskProfileHasher _hasher;
|
||||
private readonly ILogger<RiskProfileAirGapExportService> _logger;
|
||||
@@ -35,11 +37,13 @@ public sealed class RiskProfileAirGapExportService
|
||||
public RiskProfileAirGapExportService(
|
||||
ICryptoHash cryptoHash,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<RiskProfileAirGapExportService> logger,
|
||||
ISealedModeService? sealedModeService = null)
|
||||
{
|
||||
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_sealedModeService = sealedModeService;
|
||||
_hasher = new RiskProfileHasher(cryptoHash);
|
||||
@@ -74,7 +78,7 @@ public sealed class RiskProfileAirGapExportService
|
||||
var export = new RiskProfileAirGapExport(
|
||||
Key: $"profile-{profile.Id}-{profile.Version}",
|
||||
Format: "json",
|
||||
ExportId: Guid.NewGuid().ToString("N")[..16],
|
||||
ExportId: _guidProvider.NewGuid().ToString("N")[..16],
|
||||
ProfileId: profile.Id,
|
||||
ProfileVersion: profile.Version,
|
||||
CreatedAt: now.ToString("O", CultureInfo.InvariantCulture),
|
||||
@@ -426,9 +430,9 @@ public sealed class RiskProfileAirGapExportService
|
||||
SignedAt: signedAt.ToString("O", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
private static string GenerateBundleId(DateTimeOffset timestamp)
|
||||
private string GenerateBundleId(DateTimeOffset timestamp)
|
||||
{
|
||||
return $"rpab-{timestamp:yyyyMMddHHmmss}-{Guid.NewGuid():N}"[..24];
|
||||
return $"rpab-{timestamp:yyyyMMddHHmmss}-{_guidProvider.NewGuid():N}"[..24];
|
||||
}
|
||||
|
||||
private static string GetSigningKey(string? keyId)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Attestation;
|
||||
@@ -191,10 +191,8 @@ public sealed class RvaBuilder
|
||||
|
||||
private string ComputeAttestationId(RiskVerdictAttestation attestation)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(attestation with { AttestationId = "" },
|
||||
RvaSerializerOptions.Canonical);
|
||||
|
||||
var hash = _cryptoHash.ComputeHashHex(System.Text.Encoding.UTF8.GetBytes(json), "SHA256");
|
||||
var canonical = CanonJson.Canonicalize(attestation with { AttestationId = "" });
|
||||
var hash = _cryptoHash.ComputeHashHex(canonical, "SHA256");
|
||||
return $"rva:sha256:{hash}";
|
||||
}
|
||||
|
||||
@@ -208,19 +206,3 @@ public sealed class RvaBuilder
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Centralized JSON serializer options for RVA.
|
||||
/// </summary>
|
||||
internal static class RvaSerializerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonical JSON options for deterministic serialization.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Canonical { get; } = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Policy.Snapshots;
|
||||
|
||||
@@ -272,16 +272,15 @@ public sealed class RvaVerifier : IRvaVerifier
|
||||
|
||||
private static bool VerifyAttestationId(RiskVerdictAttestation attestation)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(attestation with { AttestationId = "" },
|
||||
RvaSerializerOptions.Canonical);
|
||||
var expectedId = $"rva:sha256:{ComputeSha256(json)}";
|
||||
var canonical = CanonJson.Canonicalize(attestation with { AttestationId = "" });
|
||||
var expectedId = $"rva:sha256:{ComputeSha256(canonical)}";
|
||||
return attestation.AttestationId == expectedId;
|
||||
}
|
||||
|
||||
private static string ComputeSha256(string input)
|
||||
private static string ComputeSha256(ReadOnlySpan<byte> input)
|
||||
{
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
var bytes = SHA256.HashData(input);
|
||||
return Convert.ToHexStringLower(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -326,6 +326,8 @@ public sealed record VerdictAppliedGuardrails
|
||||
/// </summary>
|
||||
public sealed record VerdictScoringProof
|
||||
{
|
||||
private const string DefaultCalculatorVersion = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new VerdictScoringProof.
|
||||
/// </summary>
|
||||
@@ -382,7 +384,7 @@ public sealed record VerdictScoringProof
|
||||
inputs: VerdictEvidenceInputs.FromEvidenceInputValues(ewsResult.Inputs),
|
||||
weights: VerdictEvidenceWeights.FromEvidenceWeights(ewsResult.Weights),
|
||||
policyDigest: ewsResult.PolicyDigest,
|
||||
calculatorVersion: "1.0.0", // TODO: Get from calculator metadata
|
||||
calculatorVersion: DefaultCalculatorVersion,
|
||||
calculatedAt: ewsResult.CalculatedAt
|
||||
);
|
||||
}
|
||||
|
||||
@@ -203,8 +203,7 @@ public sealed class VerdictPredicateBuilder
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Extract full reachability paths from trace or evidence
|
||||
// For now, return basic reachability status
|
||||
// Reachability paths are not yet supplied; emit status-only until trace evidence expands.
|
||||
return new VerdictReachability(
|
||||
status: reachabilityStatus,
|
||||
paths: null
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Engine.Ledger;
|
||||
|
||||
namespace StellaOps.Policy.Engine.ConsoleExport;
|
||||
@@ -20,19 +21,22 @@ internal sealed partial class ConsoleExportJobService
|
||||
private readonly IConsoleExportBundleStore _bundleStore;
|
||||
private readonly LedgerExportService _ledgerExport;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public ConsoleExportJobService(
|
||||
IConsoleExportJobStore jobStore,
|
||||
IConsoleExportExecutionStore executionStore,
|
||||
IConsoleExportBundleStore bundleStore,
|
||||
LedgerExportService ledgerExport,
|
||||
TimeProvider timeProvider)
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider)
|
||||
{
|
||||
_jobStore = jobStore ?? throw new ArgumentNullException(nameof(jobStore));
|
||||
_executionStore = executionStore ?? throw new ArgumentNullException(nameof(executionStore));
|
||||
_bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore));
|
||||
_ledgerExport = ledgerExport ?? throw new ArgumentNullException(nameof(ledgerExport));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
}
|
||||
|
||||
public async Task<ExportBundleJob> CreateJobAsync(
|
||||
@@ -216,7 +220,7 @@ internal sealed partial class ConsoleExportJobService
|
||||
CompletedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
|
||||
Error = ex.Message
|
||||
};
|
||||
await _executionStore.SaveAsync(failedExecution, CancellationToken.None).ConfigureAwait(false);
|
||||
await _executionStore.SaveAsync(failedExecution, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,9 +289,9 @@ internal sealed partial class ConsoleExportJobService
|
||||
return from.AddDays(1).ToString("O", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string GenerateId(string prefix)
|
||||
private string GenerateId(string prefix)
|
||||
{
|
||||
return $"{prefix}-{Guid.NewGuid():N}"[..16];
|
||||
return $"{prefix}-{_guidProvider.NewGuid():N}"[..16];
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] data)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Http;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Confidence.Configuration;
|
||||
using StellaOps.Policy.Confidence.Services;
|
||||
using StellaOps.Policy.Engine.Attestation;
|
||||
@@ -35,8 +36,8 @@ public static class PolicyEngineServiceCollectionExtensions
|
||||
/// </summary>
|
||||
public static IServiceCollection AddPolicyEngineCore(this IServiceCollection services)
|
||||
{
|
||||
// Time provider
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
// Determinism defaults (TimeProvider + IGuidProvider)
|
||||
services.AddDeterminismDefaults();
|
||||
|
||||
// Core compilation and evaluation services
|
||||
services.TryAddSingleton<PolicyCompilationService>();
|
||||
|
||||
@@ -26,7 +26,7 @@ public static class PolicyLintEndpoints
|
||||
group.MapGet("/rules", GetLintRulesAsync)
|
||||
.WithName("Policy.Lint.GetRules")
|
||||
.WithDescription("Get available lint rules and their severities")
|
||||
.AllowAnonymous();
|
||||
.RequireAuthorization(policy => policy.RequireClaim("scope", "policy:read"));
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Engine.Domain;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
|
||||
@@ -59,6 +60,7 @@ internal static class PolicyPackEndpoints
|
||||
HttpContext context,
|
||||
[FromBody] CreatePolicyPackRequest request,
|
||||
IPolicyPackRepository repository,
|
||||
IGuidProvider guidProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
|
||||
@@ -78,7 +80,7 @@ internal static class PolicyPackEndpoints
|
||||
}
|
||||
|
||||
var packId = string.IsNullOrWhiteSpace(request.PackId)
|
||||
? $"pack-{Guid.NewGuid():n}"
|
||||
? $"pack-{guidProvider.NewGuid():n}"
|
||||
: request.PackId.Trim();
|
||||
|
||||
var pack = await repository.CreateAsync(packId, request.DisplayName?.Trim(), cancellationToken).ConfigureAwait(false);
|
||||
@@ -157,6 +159,7 @@ internal static class PolicyPackEndpoints
|
||||
[FromBody] ActivatePolicyRevisionRequest request,
|
||||
IPolicyPackRepository repository,
|
||||
IPolicyActivationAuditor auditor,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyActivate);
|
||||
@@ -185,7 +188,7 @@ internal static class PolicyPackEndpoints
|
||||
packId,
|
||||
version,
|
||||
actorId,
|
||||
DateTimeOffset.UtcNow,
|
||||
timeProvider.GetUtcNow(),
|
||||
request.Comment,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Policy.Engine.Snapshots;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// API endpoints for snapshot CRUD under /api/policy.
|
||||
/// </summary>
|
||||
internal static class PolicySnapshotEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapPolicySnapshotsApi(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/api/policy/snapshots")
|
||||
.RequireAuthorization()
|
||||
.WithTags("Policy Snapshots");
|
||||
|
||||
group.MapPost(string.Empty, CreateAsync)
|
||||
.WithName("PolicyEngine.Api.Snapshots.Create");
|
||||
|
||||
group.MapGet(string.Empty, ListAsync)
|
||||
.WithName("PolicyEngine.Api.Snapshots.List");
|
||||
|
||||
group.MapGet("/{snapshotId}", GetAsync)
|
||||
.WithName("PolicyEngine.Api.Snapshots.Get");
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateAsync(
|
||||
HttpContext context,
|
||||
[FromBody] SnapshotRequest request,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var snapshot = await service.CreateAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
return Results.Json(snapshot);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return Results.BadRequest(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListAsync(
|
||||
HttpContext context,
|
||||
[FromQuery(Name = "tenant_id")] string? tenantId,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
var (items, cursor) = await service.ListAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
||||
return Results.Json(new { items, next_cursor = cursor });
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetAsync(
|
||||
HttpContext context,
|
||||
[FromRoute] string snapshotId,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
var snapshot = await service.GetAsync(snapshotId, cancellationToken).ConfigureAwait(false);
|
||||
return snapshot is null ? Results.NotFound() : Results.Json(snapshot);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ internal static class RiskProfileSchemaEndpoints
|
||||
.WithTags("Schema Discovery")
|
||||
.Produces<string>(StatusCodes.Status200OK, contentType: JsonSchemaMediaType)
|
||||
.Produces(StatusCodes.Status304NotModified)
|
||||
.AllowAnonymous();
|
||||
.RequireAuthorization();
|
||||
|
||||
endpoints.MapPost("/api/risk/schema/validate", ValidateProfile)
|
||||
.WithName("ValidateRiskProfile")
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Gates;
|
||||
|
||||
@@ -31,15 +33,18 @@ public sealed class DriftGateEvaluator : IDriftGateEvaluator
|
||||
{
|
||||
private readonly IOptionsMonitor<DriftGateOptions> _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ILogger<DriftGateEvaluator> _logger;
|
||||
|
||||
public DriftGateEvaluator(
|
||||
IOptionsMonitor<DriftGateOptions> options,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<DriftGateEvaluator> logger)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -52,7 +57,7 @@ public sealed class DriftGateEvaluator : IDriftGateEvaluator
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var context = request.Context;
|
||||
|
||||
var decisionId = $"drift-gate:{now:yyyyMMddHHmmss}:{Guid.NewGuid():N}";
|
||||
var decisionId = $"drift-gate:{now:yyyyMMddHHmmss}:{_guidProvider.NewGuid():N}";
|
||||
var gateResults = new List<DriftGateResult>();
|
||||
|
||||
string? blockedBy = null;
|
||||
@@ -386,23 +391,23 @@ public sealed class DriftGateEvaluator : IDriftGateEvaluator
|
||||
|
||||
if (remainder.StartsWith(">="))
|
||||
{
|
||||
return double.TryParse(remainder[2..].Trim(), out var threshold) && value >= threshold;
|
||||
return double.TryParse(remainder[2..].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold) && value >= threshold;
|
||||
}
|
||||
if (remainder.StartsWith("<="))
|
||||
{
|
||||
return double.TryParse(remainder[2..].Trim(), out var threshold) && value <= threshold;
|
||||
return double.TryParse(remainder[2..].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold) && value <= threshold;
|
||||
}
|
||||
if (remainder.StartsWith(">"))
|
||||
{
|
||||
return double.TryParse(remainder[1..].Trim(), out var threshold) && value > threshold;
|
||||
return double.TryParse(remainder[1..].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold) && value > threshold;
|
||||
}
|
||||
if (remainder.StartsWith("<"))
|
||||
{
|
||||
return double.TryParse(remainder[1..].Trim(), out var threshold) && value < threshold;
|
||||
return double.TryParse(remainder[1..].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold) && value < threshold;
|
||||
}
|
||||
if (remainder.StartsWith("="))
|
||||
{
|
||||
return double.TryParse(remainder[1..].Trim(), out var threshold) && Math.Abs(value - threshold) < 0.001;
|
||||
return double.TryParse(remainder[1..].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold) && Math.Abs(value - threshold) < 0.001;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -126,7 +126,6 @@ builder.Services.AddOptions<PolicyEngineOptions>()
|
||||
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<PolicyEngineOptions>>().Value);
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<PolicyEngineOptions>().ExceptionLifecycle);
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
builder.Services.AddSingleton<PolicyEngineStartupDiagnostics>();
|
||||
builder.Services.AddSingleton<PolicyTimelineEvents>();
|
||||
builder.Services.AddSingleton<EvidenceBundleService>();
|
||||
@@ -135,19 +134,10 @@ builder.Services.AddSingleton<PolicyEvaluationAttestationService>();
|
||||
// Verdict attestation services
|
||||
builder.Services.AddSingleton<StellaOps.Policy.Engine.Attestation.VerdictPredicateBuilder>();
|
||||
builder.Services.AddHttpClient<StellaOps.Policy.Engine.Attestation.IAttestorClient, StellaOps.Policy.Engine.Attestation.HttpAttestorClient>();
|
||||
builder.Services.AddSingleton<StellaOps.Policy.Engine.Attestation.VerdictAttestationOptions>(sp =>
|
||||
{
|
||||
var options = new StellaOps.Policy.Engine.Attestation.VerdictAttestationOptions
|
||||
{
|
||||
Enabled = false, // Disabled by default, enable via config
|
||||
FailOnError = false,
|
||||
RekorEnabled = false,
|
||||
AttestorUrl = "http://localhost:8080",
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
// TODO: Bind from configuration section "VerdictAttestation"
|
||||
return options;
|
||||
});
|
||||
builder.Services.AddOptions<StellaOps.Policy.Engine.Attestation.VerdictAttestationOptions>()
|
||||
.Bind(builder.Configuration.GetSection("VerdictAttestation"))
|
||||
.ValidateOnStart();
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<StellaOps.Policy.Engine.Attestation.VerdictAttestationOptions>>().Value);
|
||||
builder.Services.AddSingleton<StellaOps.Policy.Engine.Attestation.IVerdictAttestationService, StellaOps.Policy.Engine.Attestation.VerdictAttestationService>();
|
||||
|
||||
builder.Services.AddSingleton<IncidentModeService>();
|
||||
@@ -368,8 +358,7 @@ app.MapProfileEvents();
|
||||
app.MapCvssReceipts(); // CVSS v4 receipt CRUD & history
|
||||
|
||||
// Phase 5: Multi-tenant PostgreSQL-backed API endpoints
|
||||
// TODO: Fix missing MapPolicySnapshotsApi method
|
||||
// app.MapPolicySnapshotsApi();
|
||||
app.MapPolicySnapshotsApi();
|
||||
app.MapViolationEventsApi();
|
||||
app.MapConflictsApi();
|
||||
|
||||
|
||||
@@ -50,11 +50,11 @@ public sealed class EvidenceWeightedScoreEnricher : IFindingScoreEnricher
|
||||
{
|
||||
// For now, the implementation is synchronous - async is for future when
|
||||
// we might need to fetch additional evidence asynchronously
|
||||
return ValueTask.FromResult(Enrich(evidence));
|
||||
return ValueTask.FromResult(Enrich(evidence, cancellationToken));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ScoreEnrichmentResult Enrich(FindingEvidence evidence)
|
||||
public ScoreEnrichmentResult Enrich(FindingEvidence evidence, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evidence);
|
||||
|
||||
@@ -90,7 +90,7 @@ public sealed class EvidenceWeightedScoreEnricher : IFindingScoreEnricher
|
||||
var input = _aggregator.Aggregate(evidence);
|
||||
|
||||
// Get policy (use configured digest or default)
|
||||
var policy = GetPolicy(options);
|
||||
var policy = GetPolicy(options, cancellationToken);
|
||||
|
||||
// Calculate score
|
||||
var score = _calculator.Calculate(input, policy);
|
||||
@@ -142,12 +142,12 @@ public sealed class EvidenceWeightedScoreEnricher : IFindingScoreEnricher
|
||||
}
|
||||
}
|
||||
|
||||
private EvidenceWeightPolicy GetPolicy(PolicyEvidenceWeightedScoreOptions options)
|
||||
private EvidenceWeightPolicy GetPolicy(PolicyEvidenceWeightedScoreOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get default policy synchronously (blocking call) - use cached policy in production
|
||||
// The async API is available but for the sync Enrich method we need sync access
|
||||
var defaultPolicy = _policyProvider
|
||||
.GetDefaultPolicyAsync("default", CancellationToken.None)
|
||||
.GetDefaultPolicyAsync("default", cancellationToken)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
||||
|
||||
@@ -97,8 +97,11 @@ public interface IFindingScoreEnricher
|
||||
/// Enriches a finding synchronously (for pipeline integration).
|
||||
/// </summary>
|
||||
/// <param name="evidence">Evidence collected for the finding.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Score enrichment result.</returns>
|
||||
ScoreEnrichmentResult Enrich(FindingEvidence evidence);
|
||||
ScoreEnrichmentResult Enrich(
|
||||
FindingEvidence evidence,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Enriches multiple findings in batch.
|
||||
@@ -172,7 +175,7 @@ public sealed class NullFindingScoreEnricher : IFindingScoreEnricher
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ScoreEnrichmentResult Enrich(FindingEvidence evidence)
|
||||
public ScoreEnrichmentResult Enrich(FindingEvidence evidence, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ScoreEnrichmentResult.Skipped(evidence.FindingId);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ public sealed record MigrationTelemetryStats
|
||||
/// <summary>
|
||||
/// Timestamp when stats were captured.
|
||||
/// </summary>
|
||||
public DateTimeOffset CapturedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset CapturedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -164,6 +164,7 @@ public sealed class MigrationTelemetryService : IMigrationTelemetryService
|
||||
{
|
||||
private readonly IOptionsMonitor<PolicyEvidenceWeightedScoreOptions> _options;
|
||||
private readonly ILogger<MigrationTelemetryService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
// Counters
|
||||
private long _totalVerdicts;
|
||||
@@ -193,10 +194,12 @@ public sealed class MigrationTelemetryService : IMigrationTelemetryService
|
||||
public MigrationTelemetryService(
|
||||
IOptionsMonitor<PolicyEvidenceWeightedScoreOptions> options,
|
||||
ILogger<MigrationTelemetryService> logger,
|
||||
TimeProvider? timeProvider = null,
|
||||
IMeterFactory? meterFactory = null)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
|
||||
var meter = meterFactory?.Create("StellaOps.Policy.Migration")
|
||||
?? new Meter("StellaOps.Policy.Migration");
|
||||
@@ -311,7 +314,7 @@ public sealed class MigrationTelemetryService : IMigrationTelemetryService
|
||||
scoreDifference: scoreDiff,
|
||||
isAligned: isAligned,
|
||||
tierBucketMatch: tierMatch,
|
||||
timestamp: DateTimeOffset.UtcNow
|
||||
timestamp: _timeProvider.GetUtcNow()
|
||||
);
|
||||
|
||||
_recentSamples.Enqueue(sample);
|
||||
@@ -360,7 +363,7 @@ public sealed class MigrationTelemetryService : IMigrationTelemetryService
|
||||
ScoreDifferenceDistribution = new Dictionary<string, long>(_scoreDiffDistribution),
|
||||
ByConfidenceTier = new Dictionary<string, long>(_byConfidenceTier),
|
||||
ByEwsBucket = new Dictionary<string, long>(_byEwsBucket),
|
||||
CapturedAt = DateTimeOffset.UtcNow
|
||||
CapturedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -452,7 +455,7 @@ public static class MigrationTelemetryExtensions
|
||||
lines.Add($" {bucket}: {count:N0}");
|
||||
}
|
||||
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
return string.Join("\n", lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,9 +8,9 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
|
||||
private readonly ConcurrentDictionary<string, PolicyPackRecord> packs = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryPolicyPackRepository(TimeProvider? timeProvider = null)
|
||||
public InMemoryPolicyPackRepository(TimeProvider timeProvider)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public Task<PolicyPackRecord> CreateAsync(string packId, string? displayName, CancellationToken cancellationToken)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
using StellaOps.Policy.Engine.Telemetry;
|
||||
using StellaOps.Policy.RiskProfile.Hashing;
|
||||
@@ -21,6 +23,7 @@ public sealed class RiskSimulationService
|
||||
private readonly RiskProfileConfigurationService _profileService;
|
||||
private readonly RiskProfileHasher _hasher;
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly RiskSimulationBreakdownService? _breakdownService;
|
||||
|
||||
private static readonly double[] PercentileLevels = { 0.25, 0.50, 0.75, 0.90, 0.95, 0.99 };
|
||||
@@ -32,12 +35,14 @@ public sealed class RiskSimulationService
|
||||
TimeProvider timeProvider,
|
||||
RiskProfileConfigurationService profileService,
|
||||
ICryptoHash cryptoHash,
|
||||
IGuidProvider guidProvider,
|
||||
RiskSimulationBreakdownService? breakdownService = null)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_profileService = profileService ?? throw new ArgumentNullException(nameof(profileService));
|
||||
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
_hasher = new RiskProfileHasher(cryptoHash);
|
||||
_breakdownService = breakdownService;
|
||||
}
|
||||
@@ -226,7 +231,7 @@ public sealed class RiskSimulationService
|
||||
long l => l,
|
||||
decimal dec => (double)dec,
|
||||
JsonElement je when je.TryGetDouble(out var d) => d,
|
||||
string s when double.TryParse(s, out var d) => d,
|
||||
string s when double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => d,
|
||||
_ => 0.0
|
||||
},
|
||||
RiskSignalType.Categorical => value switch
|
||||
@@ -461,7 +466,7 @@ public sealed class RiskSimulationService
|
||||
|
||||
private string GenerateSimulationId(RiskSimulationRequest request, string profileHash)
|
||||
{
|
||||
var seed = $"{request.ProfileId}|{profileHash}|{request.Findings.Count}|{Guid.NewGuid()}";
|
||||
var seed = $"{request.ProfileId}|{profileHash}|{request.Findings.Count}|{_guidProvider.NewGuid()}";
|
||||
var hash = _cryptoHash.ComputeHashHexForPurpose(Encoding.UTF8.GetBytes(seed), HashPurpose.Content);
|
||||
return $"rsim-{hash[..16]}";
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0440-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Policy.Engine. |
|
||||
| AUDIT-0440-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Policy.Engine. |
|
||||
| AUDIT-0440-A | TODO | Revalidated 2026-01-07 (open findings). |
|
||||
| AUDIT-0440-A | DOING | Revalidated 2026-01-07 (open findings). |
|
||||
| AUDIT-HOTLIST-POLICY-ENGINE-0001 | DOING | Apply approved hotlist fixes and tests from audit tracker. |
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Telemetry;
|
||||
|
||||
@@ -161,7 +161,7 @@ public sealed record RuleHitTrace
|
||||
/// <summary>
|
||||
/// Creates a trace ID from the current activity or generates a new one.
|
||||
/// </summary>
|
||||
public static string GetOrCreateTraceId()
|
||||
public static string GetOrCreateTraceId(IGuidProvider? guidProvider = null)
|
||||
{
|
||||
var activity = Activity.Current;
|
||||
if (activity is not null)
|
||||
@@ -169,15 +169,14 @@ public sealed record RuleHitTrace
|
||||
return activity.TraceId.ToString();
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[16];
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
return Convert.ToHexStringLower(bytes);
|
||||
var provider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
return provider.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a span ID from the current activity or generates a new one.
|
||||
/// </summary>
|
||||
public static string GetOrCreateSpanId()
|
||||
public static string GetOrCreateSpanId(IGuidProvider? guidProvider = null)
|
||||
{
|
||||
var activity = Activity.Current;
|
||||
if (activity is not null)
|
||||
@@ -185,9 +184,9 @@ public sealed record RuleHitTrace
|
||||
return activity.SpanId.ToString();
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[8];
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
return Convert.ToHexStringLower(bytes);
|
||||
var provider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
var bytes = provider.NewGuid().ToByteArray();
|
||||
return Convert.ToHexStringLower(bytes.AsSpan(0, 8));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,11 +303,12 @@ public static class RuleHitTraceFactory
|
||||
bool expressionResult = false,
|
||||
long evaluationMicroseconds = 0,
|
||||
bool isSampled = false,
|
||||
ImmutableDictionary<string, string>? attributes = null)
|
||||
ImmutableDictionary<string, string>? attributes = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
var time = timeProvider ?? TimeProvider.System;
|
||||
var traceId = RuleHitTrace.GetOrCreateTraceId();
|
||||
var spanId = RuleHitTrace.GetOrCreateSpanId();
|
||||
var traceId = RuleHitTrace.GetOrCreateTraceId(guidProvider);
|
||||
var spanId = RuleHitTrace.GetOrCreateSpanId(guidProvider);
|
||||
var parentSpanId = Activity.Current?.ParentSpanId.ToString();
|
||||
|
||||
return new RuleHitTrace
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Telemetry;
|
||||
|
||||
@@ -84,7 +87,7 @@ public interface IRuleHitTraceCollector
|
||||
/// <summary>
|
||||
/// Records a rule hit trace.
|
||||
/// </summary>
|
||||
void Record(RuleHitTrace trace);
|
||||
void Record(RuleHitTrace trace, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all traces for a run.
|
||||
@@ -131,8 +134,6 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IReadOnlyList<IRuleHitTraceExporter> _exporters;
|
||||
private readonly ConcurrentDictionary<string, RunTraceBuffer> _runBuffers = new();
|
||||
private readonly Random _sampler;
|
||||
private readonly object _samplerLock = new();
|
||||
private volatile bool _incidentMode;
|
||||
private bool _disposed;
|
||||
|
||||
@@ -144,7 +145,6 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable
|
||||
_options = options ?? RuleHitSamplingOptions.Default;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_exporters = exporters?.ToList() ?? new List<IRuleHitTraceExporter>();
|
||||
_sampler = new Random();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -159,7 +159,7 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable
|
||||
/// <summary>
|
||||
/// Records a rule hit trace with sampling.
|
||||
/// </summary>
|
||||
public void Record(RuleHitTrace trace)
|
||||
public void Record(RuleHitTrace trace, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(trace);
|
||||
|
||||
@@ -181,7 +181,7 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable
|
||||
if (buffer.Count >= _options.MaxBufferSizePerRun)
|
||||
{
|
||||
// Async flush without blocking
|
||||
_ = FlushAsync(trace.RunId, CancellationToken.None);
|
||||
_ = FlushAsync(trace.RunId, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,26 +316,26 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable
|
||||
// VEX overrides get elevated sampling
|
||||
if (trace.IsVexOverride)
|
||||
{
|
||||
return Sample(_options.VexOverrideSamplingRate);
|
||||
return Sample(_options.VexOverrideSamplingRate, trace);
|
||||
}
|
||||
|
||||
// High-severity outcomes get elevated sampling
|
||||
if (_options.HighSeverityOutcomes.Contains(trace.Outcome))
|
||||
{
|
||||
return Sample(_options.HighSeveritySamplingRate);
|
||||
return Sample(_options.HighSeveritySamplingRate, trace);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(trace.AssignedSeverity) &&
|
||||
_options.HighSeverityOutcomes.Contains(trace.AssignedSeverity))
|
||||
{
|
||||
return Sample(_options.HighSeveritySamplingRate);
|
||||
return Sample(_options.HighSeveritySamplingRate, trace);
|
||||
}
|
||||
|
||||
// Base sampling rate
|
||||
return Sample(_options.BaseSamplingRate);
|
||||
return Sample(_options.BaseSamplingRate, trace);
|
||||
}
|
||||
|
||||
private bool Sample(double rate)
|
||||
private static bool Sample(double rate, RuleHitTrace trace)
|
||||
{
|
||||
if (rate >= 1.0)
|
||||
{
|
||||
@@ -347,10 +347,29 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_samplerLock)
|
||||
var hashValue = ComputeSampleHash(trace);
|
||||
var sample = hashValue / (double)ulong.MaxValue;
|
||||
return sample < rate;
|
||||
}
|
||||
|
||||
private static ulong ComputeSampleHash(RuleHitTrace trace)
|
||||
{
|
||||
var key = string.Join("|", new[]
|
||||
{
|
||||
return _sampler.NextDouble() < rate;
|
||||
}
|
||||
trace.RunId,
|
||||
trace.PolicyId,
|
||||
trace.RuleName,
|
||||
trace.Outcome,
|
||||
trace.AssignedSeverity ?? string.Empty,
|
||||
trace.VulnerabilityId ?? string.Empty,
|
||||
trace.ComponentPurl ?? string.Empty,
|
||||
trace.VexStatus ?? string.Empty,
|
||||
trace.VexVendor ?? string.Empty,
|
||||
trace.IsVexOverride ? "1" : "0"
|
||||
});
|
||||
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
||||
return BinaryPrimitives.ReadUInt64BigEndian(hash.AsSpan(0, 8));
|
||||
}
|
||||
|
||||
private static void RecordMetrics(RuleHitTrace trace)
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed partial class TenantContextMiddleware
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly TenantContextOptions _options;
|
||||
private readonly ILogger<TenantContextMiddleware> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
// Valid tenant/project ID pattern: alphanumeric, dashes, underscores
|
||||
[GeneratedRegex("^[a-zA-Z0-9_-]+$", RegexOptions.Compiled)]
|
||||
@@ -28,11 +29,13 @@ public sealed partial class TenantContextMiddleware
|
||||
public TenantContextMiddleware(
|
||||
RequestDelegate next,
|
||||
IOptions<TenantContextOptions> options,
|
||||
ILogger<TenantContextMiddleware> logger)
|
||||
ILogger<TenantContextMiddleware> logger,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_options = options?.Value ?? new TenantContextOptions();
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, ITenantContextAccessor tenantContextAccessor)
|
||||
@@ -138,7 +141,8 @@ public sealed partial class TenantContextMiddleware
|
||||
tenantHeader,
|
||||
string.IsNullOrWhiteSpace(projectHeader) ? null : projectHeader,
|
||||
canWrite,
|
||||
actorId);
|
||||
actorId,
|
||||
_timeProvider);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Tenant context established: tenant={TenantId}, project={ProjectId}, canWrite={CanWrite}, actor={ActorId}",
|
||||
|
||||
@@ -80,22 +80,29 @@ public sealed record TenantContext
|
||||
/// <summary>
|
||||
/// Timestamp when the context was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a tenant context for a specific tenant.
|
||||
/// </summary>
|
||||
public static TenantContext ForTenant(string tenantId, string? projectId = null, bool canWrite = false, string? actorId = null)
|
||||
public static TenantContext ForTenant(
|
||||
string tenantId,
|
||||
string? projectId = null,
|
||||
bool canWrite = false,
|
||||
string? actorId = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
|
||||
var clock = timeProvider ?? TimeProvider.System;
|
||||
|
||||
return new TenantContext
|
||||
{
|
||||
TenantId = tenantId,
|
||||
ProjectId = projectId,
|
||||
CanWrite = canWrite,
|
||||
ActorId = actorId,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = clock.GetUtcNow()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Engine.ReachabilityFacts;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
@@ -51,6 +52,7 @@ public sealed class VexDecisionEmitter : IVexDecisionEmitter
|
||||
private readonly IPolicyGateEvaluator _gateEvaluator;
|
||||
private readonly IOptionsMonitor<VexDecisionEmitterOptions> _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ILogger<VexDecisionEmitter> _logger;
|
||||
// LIN-BE-012: Optional verdict link service for SBOM linking
|
||||
private readonly IVerdictLinkService? _verdictLinkService;
|
||||
@@ -76,6 +78,7 @@ public sealed class VexDecisionEmitter : IVexDecisionEmitter
|
||||
IPolicyGateEvaluator gateEvaluator,
|
||||
IOptionsMonitor<VexDecisionEmitterOptions> options,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<VexDecisionEmitter> logger,
|
||||
IVerdictLinkService? verdictLinkService = null)
|
||||
{
|
||||
@@ -83,6 +86,7 @@ public sealed class VexDecisionEmitter : IVexDecisionEmitter
|
||||
_gateEvaluator = gateEvaluator ?? throw new ArgumentNullException(nameof(gateEvaluator));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_verdictLinkService = verdictLinkService;
|
||||
}
|
||||
@@ -185,7 +189,7 @@ public sealed class VexDecisionEmitter : IVexDecisionEmitter
|
||||
}
|
||||
|
||||
// Build document
|
||||
var documentId = $"urn:uuid:{Guid.NewGuid()}";
|
||||
var documentId = $"urn:uuid:{_guidProvider.NewGuid()}";
|
||||
var document = new VexDecisionDocument
|
||||
{
|
||||
Id = documentId,
|
||||
|
||||
@@ -454,8 +454,7 @@ public sealed class VexDecisionSigningService : IVexDecisionSigningService
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Verify actual signature if signer client provides public key resolution
|
||||
// For now, we just verify the signature is well-formed base64
|
||||
// Signature verification is limited to base64 validation when key resolution is unavailable.
|
||||
try
|
||||
{
|
||||
_ = Convert.FromBase64String(sig.Sig);
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Engine.Domain;
|
||||
using StellaOps.Policy.Engine.EffectiveDecisionMap;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
@@ -21,19 +22,22 @@ internal sealed class WhatIfSimulationService
|
||||
private readonly PolicyCompilationService _compilationService;
|
||||
private readonly ILogger<WhatIfSimulationService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public WhatIfSimulationService(
|
||||
IEffectiveDecisionMap decisionMap,
|
||||
IPolicyPackRepository policyRepository,
|
||||
PolicyCompilationService compilationService,
|
||||
ILogger<WhatIfSimulationService> logger,
|
||||
TimeProvider timeProvider)
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider)
|
||||
{
|
||||
_decisionMap = decisionMap ?? throw new ArgumentNullException(nameof(decisionMap));
|
||||
_policyRepository = policyRepository ?? throw new ArgumentNullException(nameof(policyRepository));
|
||||
_compilationService = compilationService ?? throw new ArgumentNullException(nameof(compilationService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -533,9 +537,9 @@ internal sealed class WhatIfSimulationService
|
||||
};
|
||||
}
|
||||
|
||||
private static string GenerateSimulationId(WhatIfSimulationRequest request)
|
||||
private string GenerateSimulationId(WhatIfSimulationRequest request)
|
||||
{
|
||||
var seed = $"{request.TenantId}|{request.BaseSnapshotId}|{request.DraftPolicy?.PackId}|{Guid.NewGuid()}";
|
||||
var seed = $"{request.TenantId}|{request.BaseSnapshotId}|{request.DraftPolicy?.PackId}|{_guidProvider.NewGuid()}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(seed));
|
||||
return $"whatif-{Convert.ToHexStringLower(hash)[..16]}";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Engine.AirGap;
|
||||
using StellaOps.Policy.RiskProfile.Models;
|
||||
using Xunit;
|
||||
@@ -10,6 +11,7 @@ public sealed class RiskProfileAirGapExportServiceTests
|
||||
{
|
||||
private readonly FakeCryptoHash _cryptoHash = new();
|
||||
private readonly FakeTimeProvider _timeProvider = new();
|
||||
private readonly IGuidProvider _guidProvider = new SequentialGuidProvider();
|
||||
private readonly NullLogger<RiskProfileAirGapExportService> _logger = new();
|
||||
|
||||
private RiskProfileAirGapExportService CreateService(ISealedModeService? sealedMode = null)
|
||||
@@ -17,6 +19,7 @@ public sealed class RiskProfileAirGapExportServiceTests
|
||||
return new RiskProfileAirGapExportService(
|
||||
_cryptoHash,
|
||||
_timeProvider,
|
||||
_guidProvider,
|
||||
_logger,
|
||||
sealedMode);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Engine.Attestation;
|
||||
using StellaOps.TestKit.Fixtures;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Integration;
|
||||
|
||||
public sealed class PolicyEngineApiHostTests : IClassFixture<PolicyEngineWebServiceFixture>
|
||||
{
|
||||
private readonly PolicyEngineWebServiceFixture _factory;
|
||||
|
||||
public PolicyEngineApiHostTests(PolicyEngineWebServiceFixture factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Healthz_ReturnsOk()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/healthz");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PolicyLintRules_RequireAuth()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/policy/lint/rules");
|
||||
|
||||
Assert.True(response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PolicyLintRules_WithAuth_ReturnsOk()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add(TestAuthHandler.HeaderName, TestAuthHandler.HeaderValue);
|
||||
|
||||
var response = await client.GetAsync("/api/v1/policy/lint/rules");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PolicySnapshotsApi_RequiresAuth()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/policy/snapshots");
|
||||
|
||||
Assert.True(response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictAttestationOptions_BindFromConfiguration()
|
||||
{
|
||||
var options = _factory.Services.GetRequiredService<VerdictAttestationOptions>();
|
||||
|
||||
Assert.True(options.Enabled);
|
||||
Assert.True(options.FailOnError);
|
||||
Assert.True(options.RekorEnabled);
|
||||
Assert.Equal("http://attestor.test", options.AttestorUrl);
|
||||
Assert.Equal(TimeSpan.FromSeconds(15), options.Timeout);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PolicyEngineWebServiceFixture : WebServiceFixture<StellaOps.Policy.Engine.Program>
|
||||
{
|
||||
public PolicyEngineWebServiceFixture()
|
||||
: base(ConfigureServices, ConfigureWebHost)
|
||||
{
|
||||
}
|
||||
|
||||
private static void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.RemoveAll<IHostedService>();
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
|
||||
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
|
||||
})
|
||||
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
|
||||
TestAuthHandler.SchemeName,
|
||||
_ => { });
|
||||
}
|
||||
|
||||
private static void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, config) =>
|
||||
{
|
||||
var settings = new Dictionary<string, string?>
|
||||
{
|
||||
["VerdictAttestation:Enabled"] = "true",
|
||||
["VerdictAttestation:FailOnError"] = "true",
|
||||
["VerdictAttestation:RekorEnabled"] = "true",
|
||||
["VerdictAttestation:AttestorUrl"] = "http://attestor.test",
|
||||
["VerdictAttestation:Timeout"] = "00:00:15"
|
||||
};
|
||||
|
||||
config.AddInMemoryCollection(settings);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
public const string SchemeName = "Test";
|
||||
public const string HeaderName = "X-Test-Auth";
|
||||
public const string HeaderValue = "true";
|
||||
|
||||
public TestAuthHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock)
|
||||
: base(options, logger, encoder, clock)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(HeaderName, out var value) ||
|
||||
!string.Equals(value, HeaderValue, StringComparison.Ordinal))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("Missing test auth header."));
|
||||
}
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("scope", "policy:read"),
|
||||
new Claim("tenant_id", "test-tenant"),
|
||||
new Claim(ClaimTypes.NameIdentifier, "test-user")
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, SchemeName);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, SchemeName);
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public sealed class PolicyBundleServiceTests
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new PolicyEngineOptions());
|
||||
var metadataExtractor = new PolicyMetadataExtractor();
|
||||
var compilationService = new PolicyCompilationService(compiler, complexity, metadataExtractor, new StaticOptionsMonitor(options.Value), TimeProvider.System);
|
||||
var repo = new InMemoryPolicyPackRepository();
|
||||
var repo = new InMemoryPolicyPackRepository(TimeProvider.System);
|
||||
return new ServiceHarness(
|
||||
new PolicyBundleService(compilationService, repo, TimeProvider.System),
|
||||
repo);
|
||||
|
||||
@@ -436,7 +436,7 @@ public sealed class PolicyRuntimeEvaluationServiceTests
|
||||
|
||||
private static TestHarness CreateHarness()
|
||||
{
|
||||
var repository = new InMemoryPolicyPackRepository();
|
||||
var repository = new InMemoryPolicyPackRepository(TimeProvider.System);
|
||||
var cacheLogger = NullLogger<InMemoryPolicyEvaluationCache>.Instance;
|
||||
var serviceLogger = NullLogger<PolicyRuntimeEvaluationService>.Instance;
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new PolicyEngineOptions());
|
||||
|
||||
@@ -12,7 +12,7 @@ public sealed class PolicyRuntimeEvaluatorTests
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_ReturnsDeterministicDecisionAndCaches()
|
||||
{
|
||||
var repo = new InMemoryPolicyPackRepository();
|
||||
var repo = new InMemoryPolicyPackRepository(TimeProvider.System);
|
||||
await repo.StoreBundleAsync(
|
||||
"pack-1",
|
||||
1,
|
||||
@@ -41,7 +41,7 @@ public sealed class PolicyRuntimeEvaluatorTests
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_ThrowsWhenBundleMissing()
|
||||
{
|
||||
var evaluator = new PolicyRuntimeEvaluator(new InMemoryPolicyPackRepository());
|
||||
var evaluator = new PolicyRuntimeEvaluator(new InMemoryPolicyPackRepository(TimeProvider.System));
|
||||
var request = new PolicyEvaluationRequest("pack-x", 1, "subject-a");
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => evaluator.EvaluateAsync(request, TestContext.Current.CancellationToken));
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Policy.Engine.Tenancy;
|
||||
using Xunit;
|
||||
|
||||
@@ -139,11 +140,13 @@ public sealed class TenantContextMiddlewareTests
|
||||
private readonly NullLogger<TenantContextMiddleware> _logger;
|
||||
private readonly TenantContextAccessor _tenantAccessor;
|
||||
private readonly TenantContextOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public TenantContextMiddlewareTests()
|
||||
{
|
||||
_logger = NullLogger<TenantContextMiddleware>.Instance;
|
||||
_tenantAccessor = new TenantContextAccessor();
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
_options = new TenantContextOptions
|
||||
{
|
||||
Enabled = true,
|
||||
@@ -166,7 +169,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", "tenant-123");
|
||||
|
||||
@@ -192,7 +196,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", "tenant-123", "project-456");
|
||||
|
||||
@@ -214,7 +219,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
var middleware = new TenantContextMiddleware(
|
||||
_ => { nextCalled = true; return Task.CompletedTask; },
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", tenantId: null);
|
||||
|
||||
@@ -245,7 +251,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
MsOptions.Options.Create(optionsNotRequired),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", tenantId: null);
|
||||
|
||||
@@ -266,7 +273,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
var middleware = new TenantContextMiddleware(
|
||||
_ => { nextCalled = true; return Task.CompletedTask; },
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/healthz", tenantId: null);
|
||||
|
||||
@@ -287,7 +295,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
var middleware = new TenantContextMiddleware(
|
||||
_ => { nextCalled = true; return Task.CompletedTask; },
|
||||
MsOptions.Options.Create(disabledOptions),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", tenantId: null);
|
||||
|
||||
@@ -313,7 +322,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", tenantId);
|
||||
|
||||
@@ -338,7 +348,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
var middleware = new TenantContextMiddleware(
|
||||
_ => { nextCalled = true; return Task.CompletedTask; },
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", tenantId);
|
||||
|
||||
@@ -358,7 +369,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
var middleware = new TenantContextMiddleware(
|
||||
_ => Task.CompletedTask,
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", longTenantId);
|
||||
|
||||
@@ -384,7 +396,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", "tenant-123", projectId);
|
||||
|
||||
@@ -409,7 +422,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", "tenant-123");
|
||||
var claims = new[]
|
||||
@@ -440,7 +454,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", "tenant-123");
|
||||
var claims = new[]
|
||||
@@ -471,7 +486,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", "tenant-123");
|
||||
var claims = new[] { new Claim("sub", "user-id-123") };
|
||||
@@ -498,7 +514,8 @@ public sealed class TenantContextMiddlewareTests
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
MsOptions.Options.Create(_options),
|
||||
_logger);
|
||||
_logger,
|
||||
_timeProvider);
|
||||
|
||||
var context = CreateHttpContext("/api/risk/profiles", "tenant-123");
|
||||
context.Request.Headers["X-StellaOps-Actor"] = "service-account-123";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Engine.ReachabilityFacts;
|
||||
using StellaOps.Policy.Engine.Vex;
|
||||
@@ -398,6 +399,7 @@ public class VexDecisionEmitterTests
|
||||
gateEvaluator,
|
||||
options,
|
||||
TimeProvider.System,
|
||||
SystemGuidProvider.Instance,
|
||||
NullLogger<VexDecisionEmitter>.Instance);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MsOptions = Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Engine.ReachabilityFacts;
|
||||
using StellaOps.Policy.Engine.Vex;
|
||||
@@ -499,6 +500,7 @@ public sealed class VexDecisionReachabilityIntegrationTests
|
||||
gateEvaluator,
|
||||
new OptionsMonitorWrapper<VexDecisionEmitterOptions>(options.Value),
|
||||
timeProvider ?? TimeProvider.System,
|
||||
SystemGuidProvider.Instance,
|
||||
NullLogger<VexDecisionEmitter>.Instance);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user