notify doctors work, audit work, new product advisory sprints

This commit is contained in:
master
2026-01-13 08:36:29 +02:00
parent b8868a5f13
commit 9ca7cb183e
343 changed files with 24492 additions and 3544 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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());

View File

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

View File

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

View File

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

View File

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