audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -264,7 +265,7 @@ internal sealed class ExceptionAdapter : IExceptionAdapter
|
||||
builder["exception.owner"] = exception.OwnerId;
|
||||
builder["exception.requester"] = exception.RequesterId;
|
||||
builder["exception.rationale"] = exception.Rationale;
|
||||
builder["exception.expiresAt"] = exception.ExpiresAt.ToString("O");
|
||||
builder["exception.expiresAt"] = exception.ExpiresAt.ToString("O", CultureInfo.InvariantCulture);
|
||||
|
||||
if (exception.ApproverIds.Length > 0)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Policy.Engine.AirGap;
|
||||
@@ -393,7 +394,7 @@ internal sealed class WebhookNotificationChannel : IAirGapNotificationChannel
|
||||
severity = notification.Severity.ToString(),
|
||||
title = notification.Title,
|
||||
message = notification.Message,
|
||||
occurred_at = notification.OccurredAt.ToString("O"),
|
||||
occurred_at = notification.OccurredAt.ToString("O", CultureInfo.InvariantCulture),
|
||||
metadata = notification.Metadata
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -68,7 +69,7 @@ internal sealed class PolicyPackBundleImportService
|
||||
TenantId: tenantId,
|
||||
Status: BundleImportStatus.Validating,
|
||||
ExportCount: 0,
|
||||
ImportedAt: now.ToString("O"),
|
||||
ImportedAt: now.ToString("O", CultureInfo.InvariantCulture),
|
||||
Error: null,
|
||||
Bundle: null);
|
||||
|
||||
@@ -163,7 +164,7 @@ internal sealed class PolicyPackBundleImportService
|
||||
TenantId: tenantId,
|
||||
Status: BundleImportStatus.Imported,
|
||||
ExportCount: bundle.Exports.Count,
|
||||
ImportedAt: now.ToString("O"),
|
||||
ImportedAt: now.ToString("O", CultureInfo.InvariantCulture),
|
||||
Error: null,
|
||||
Bundle: bundle);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -76,7 +77,7 @@ public sealed class RiskProfileAirGapExportService
|
||||
ExportId: Guid.NewGuid().ToString("N")[..16],
|
||||
ProfileId: profile.Id,
|
||||
ProfileVersion: profile.Version,
|
||||
CreatedAt: now.ToString("O"),
|
||||
CreatedAt: now.ToString("O", CultureInfo.InvariantCulture),
|
||||
ArtifactSizeBytes: Encoding.UTF8.GetByteCount(profileJson),
|
||||
ArtifactDigest: artifactDigest,
|
||||
ContentHash: contentHash,
|
||||
@@ -99,7 +100,7 @@ public sealed class RiskProfileAirGapExportService
|
||||
|
||||
return new RiskProfileAirGapBundle(
|
||||
SchemaVersion: 1,
|
||||
GeneratedAt: now.ToString("O"),
|
||||
GeneratedAt: now.ToString("O", CultureInfo.InvariantCulture),
|
||||
TargetRepository: request.TargetRepository,
|
||||
DomainId: DomainId,
|
||||
DisplayName: request.DisplayName ?? "Risk Profiles Export",
|
||||
@@ -337,7 +338,7 @@ public sealed class RiskProfileAirGapExportService
|
||||
Algorithm: "HMAC-SHA256",
|
||||
KeyId: keyId ?? "default",
|
||||
Provider: "stellaops",
|
||||
SignedAt: signedAt.ToString("O"));
|
||||
SignedAt: signedAt.ToString("O", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
private static string ComputeSignatureData(List<RiskProfileAirGapExport> exports, string merkleRoot)
|
||||
@@ -422,7 +423,7 @@ public sealed class RiskProfileAirGapExportService
|
||||
PredicateType: PredicateType,
|
||||
RekorLocation: null,
|
||||
EnvelopeDigest: null,
|
||||
SignedAt: signedAt.ToString("O"));
|
||||
SignedAt: signedAt.ToString("O", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
private static string GenerateBundleId(DateTimeOffset timestamp)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -129,7 +130,7 @@ public sealed record ScoreProvenanceChain
|
||||
rule_name = Verdict.MatchedRuleName,
|
||||
verdict_digest = Verdict.VerdictDigest
|
||||
},
|
||||
created_at = CreatedAt.ToUniversalTime().ToString("O")
|
||||
created_at = CreatedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(canonical, ProvenanceJsonOptions.Default);
|
||||
@@ -659,7 +660,7 @@ public sealed record ProvenanceVerdictRef
|
||||
status = predicate.Verdict.Status,
|
||||
severity = predicate.Verdict.Severity,
|
||||
score = predicate.Verdict.Score,
|
||||
evaluated_at = predicate.EvaluatedAt.ToUniversalTime().ToString("O")
|
||||
evaluated_at = predicate.EvaluatedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(canonical, ProvenanceJsonOptions.Default);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -38,7 +39,7 @@ internal sealed class BatchContextService
|
||||
ComputeTraceRef(request.TenantId, i)))
|
||||
.ToList();
|
||||
|
||||
var expires = _timeProvider.GetUtcNow().AddHours(1).ToString("O");
|
||||
var expires = _timeProvider.GetUtcNow().AddHours(1).ToString("O", CultureInfo.InvariantCulture);
|
||||
var contextId = ComputeContextId(request, sortedItems);
|
||||
|
||||
return new BatchContextResponse(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -58,7 +59,7 @@ internal sealed partial class ConsoleExportJobService
|
||||
Destination: request.Destination,
|
||||
Signing: request.Signing,
|
||||
Enabled: true,
|
||||
CreatedAt: now.ToString("O"),
|
||||
CreatedAt: now.ToString("O", CultureInfo.InvariantCulture),
|
||||
LastRunAt: null,
|
||||
NextRunAt: CalculateNextRun(request.Schedule, now));
|
||||
|
||||
@@ -128,7 +129,7 @@ internal sealed partial class ConsoleExportJobService
|
||||
JobId: jobId,
|
||||
Status: "running",
|
||||
BundleId: null,
|
||||
StartedAt: now.ToString("O"),
|
||||
StartedAt: now.ToString("O", CultureInfo.InvariantCulture),
|
||||
CompletedAt: null,
|
||||
Error: null);
|
||||
|
||||
@@ -177,7 +178,7 @@ internal sealed partial class ConsoleExportJobService
|
||||
BundleId: bundleId,
|
||||
JobId: job.JobId,
|
||||
TenantId: job.TenantId,
|
||||
CreatedAt: now.ToString("O"),
|
||||
CreatedAt: now.ToString("O", CultureInfo.InvariantCulture),
|
||||
Format: job.Format,
|
||||
ArtifactDigest: artifactDigest,
|
||||
ArtifactSizeBytes: contentBytes.Length,
|
||||
@@ -195,14 +196,14 @@ internal sealed partial class ConsoleExportJobService
|
||||
{
|
||||
Status = "completed",
|
||||
BundleId = bundleId,
|
||||
CompletedAt = now.ToString("O")
|
||||
CompletedAt = now.ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
await _executionStore.SaveAsync(completedExecution, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Update job with last run
|
||||
var updatedJob = job with
|
||||
{
|
||||
LastRunAt = now.ToString("O"),
|
||||
LastRunAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
NextRunAt = CalculateNextRun(job.Schedule, now)
|
||||
};
|
||||
await _jobStore.SaveAsync(updatedJob, cancellationToken).ConfigureAwait(false);
|
||||
@@ -212,7 +213,7 @@ internal sealed partial class ConsoleExportJobService
|
||||
var failedExecution = execution with
|
||||
{
|
||||
Status = "failed",
|
||||
CompletedAt = _timeProvider.GetUtcNow().ToString("O"),
|
||||
CompletedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
|
||||
Error = ex.Message
|
||||
};
|
||||
await _executionStore.SaveAsync(failedExecution, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -269,7 +270,7 @@ internal sealed partial class ConsoleExportJobService
|
||||
// In production, this would use a proper cron parser like Cronos
|
||||
if (schedule.StartsWith("0 0 ", StringComparison.Ordinal))
|
||||
{
|
||||
return from.AddDays(1).ToString("O");
|
||||
return from.AddDays(1).ToString("O", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (schedule.StartsWith("0 */", StringComparison.Ordinal))
|
||||
@@ -277,11 +278,11 @@ internal sealed partial class ConsoleExportJobService
|
||||
var hourMatch = Regex.Match(schedule, @"\*/(\d+)");
|
||||
if (hourMatch.Success && int.TryParse(hourMatch.Groups[1].Value, out var hours))
|
||||
{
|
||||
return from.AddHours(hours).ToString("O");
|
||||
return from.AddHours(hours).ToString("O", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
return from.AddDays(1).ToString("O");
|
||||
return from.AddDays(1).ToString("O", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string GenerateId(string prefix)
|
||||
|
||||
@@ -21,6 +21,9 @@ public static class DeterminizationEngineExtensions
|
||||
// Add determinization library services
|
||||
services.AddDeterminization();
|
||||
|
||||
// Add metrics
|
||||
services.TryAddSingleton<DeterminizationGateMetrics>();
|
||||
|
||||
// Add gate
|
||||
services.TryAddSingleton<IDeterminizationGate, DeterminizationGate>();
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ public static class PolicyEngineServiceCollectionExtensions
|
||||
/// Adds all Policy Engine services with default configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Includes core services, event pipeline, worker, explainer, and Evidence-Weighted Score services.
|
||||
/// Includes core services, event pipeline, worker, explainer, determinization gate, and Evidence-Weighted Score services.
|
||||
/// EWS services are registered but only activate when <see cref="PolicyEvidenceWeightedScoreOptions.Enabled"/> is true.
|
||||
/// </remarks>
|
||||
public static IServiceCollection AddPolicyEngine(this IServiceCollection services)
|
||||
@@ -304,6 +304,9 @@ public static class PolicyEngineServiceCollectionExtensions
|
||||
services.AddPolicyEngineWorker();
|
||||
services.AddPolicyEngineExplainer();
|
||||
|
||||
// Determinization gate and policy services (Sprint 20260106_001_003)
|
||||
services.AddDeterminizationEngine();
|
||||
|
||||
// Evidence-Weighted Score services (Sprint 8200.0012.0003)
|
||||
// Always registered; activation controlled by PolicyEvidenceWeightedScoreOptions.Enabled
|
||||
services.AddEvidenceWeightedScore();
|
||||
@@ -342,6 +345,9 @@ public static class PolicyEngineServiceCollectionExtensions
|
||||
services.AddPolicyEngineWorker();
|
||||
services.AddPolicyEngineExplainer();
|
||||
|
||||
// Determinization gate and policy services (Sprint 20260106_001_003)
|
||||
services.AddDeterminizationEngine();
|
||||
|
||||
// Conditional EWS registration based on configuration
|
||||
services.AddEvidenceWeightedScoreIfEnabled(configuration);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -544,7 +545,7 @@ internal sealed class MessagingExceptionEffectiveCache : IExceptionEffectiveCach
|
||||
var statsKey = GetStatsKey(tenantId);
|
||||
var stats = new Dictionary<string, string>
|
||||
{
|
||||
["lastWarmAt"] = warmAt.ToString("O"),
|
||||
["lastWarmAt"] = warmAt.ToString("O", CultureInfo.InvariantCulture),
|
||||
["lastWarmCount"] = count.ToString(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -642,7 +643,7 @@ internal sealed class RedisExceptionEffectiveCache : IExceptionEffectiveCache
|
||||
|
||||
var stats = new Dictionary<string, string>
|
||||
{
|
||||
["lastWarmAt"] = warmAt.ToString("O"),
|
||||
["lastWarmAt"] = warmAt.ToString("O", CultureInfo.InvariantCulture),
|
||||
["lastWarmCount"] = count.ToString(),
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Determinization;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
using StellaOps.Policy.Determinization.Scoring;
|
||||
using StellaOps.Policy.Engine.Gates.Determinization;
|
||||
using StellaOps.Policy.Engine.Policies;
|
||||
using StellaOps.Policy.Gates;
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Gates.Determinization;
|
||||
|
||||
/// <summary>
|
||||
/// OpenTelemetry metrics for determinization gate and observation state tracking.
|
||||
/// </summary>
|
||||
public sealed class DeterminizationGateMetrics : IDisposable
|
||||
{
|
||||
private readonly Meter _meter;
|
||||
private readonly Counter<long> _evaluationsTotal;
|
||||
private readonly Counter<long> _ruleMatchesTotal;
|
||||
private readonly Counter<long> _stateTransitionsTotal;
|
||||
private readonly Histogram<double> _entropyDistribution;
|
||||
private readonly Histogram<double> _trustScoreDistribution;
|
||||
private readonly Histogram<double> _evaluationDurationMs;
|
||||
|
||||
public const string MeterName = "StellaOps.Policy.Engine.DeterminizationGate";
|
||||
|
||||
public DeterminizationGateMetrics()
|
||||
{
|
||||
_meter = new Meter(MeterName, "1.0.0");
|
||||
|
||||
_evaluationsTotal = _meter.CreateCounter<long>(
|
||||
"stellaops_policy_determinization_evaluations_total",
|
||||
unit: "{evaluations}",
|
||||
description: "Total number of determinization gate evaluations");
|
||||
|
||||
_ruleMatchesTotal = _meter.CreateCounter<long>(
|
||||
"stellaops_policy_determinization_rule_matches_total",
|
||||
unit: "{matches}",
|
||||
description: "Total number of determinization rule matches by rule name");
|
||||
|
||||
_stateTransitionsTotal = _meter.CreateCounter<long>(
|
||||
"stellaops_policy_observation_state_transitions_total",
|
||||
unit: "{transitions}",
|
||||
description: "Total number of observation state transitions");
|
||||
|
||||
_entropyDistribution = _meter.CreateHistogram<double>(
|
||||
"stellaops_policy_determinization_entropy",
|
||||
unit: "1",
|
||||
description: "Distribution of entropy scores evaluated");
|
||||
|
||||
_trustScoreDistribution = _meter.CreateHistogram<double>(
|
||||
"stellaops_policy_determinization_trust_score",
|
||||
unit: "1",
|
||||
description: "Distribution of trust scores evaluated");
|
||||
|
||||
_evaluationDurationMs = _meter.CreateHistogram<double>(
|
||||
"stellaops_policy_determinization_evaluation_duration_ms",
|
||||
unit: "ms",
|
||||
description: "Duration of determinization gate evaluations");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record a gate evaluation.
|
||||
/// </summary>
|
||||
public void RecordEvaluation(
|
||||
PolicyVerdictStatus status,
|
||||
string environment,
|
||||
string? matchedRule)
|
||||
{
|
||||
_evaluationsTotal.Add(1,
|
||||
new KeyValuePair<string, object?>("status", status.ToString().ToLowerInvariant()),
|
||||
new KeyValuePair<string, object?>("environment", environment),
|
||||
new KeyValuePair<string, object?>("rule", matchedRule ?? "none"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record a rule match.
|
||||
/// </summary>
|
||||
public void RecordRuleMatch(
|
||||
string ruleName,
|
||||
PolicyVerdictStatus status,
|
||||
string environment)
|
||||
{
|
||||
_ruleMatchesTotal.Add(1,
|
||||
new KeyValuePair<string, object?>("rule", ruleName),
|
||||
new KeyValuePair<string, object?>("status", status.ToString().ToLowerInvariant()),
|
||||
new KeyValuePair<string, object?>("environment", environment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record an observation state transition.
|
||||
/// </summary>
|
||||
public void RecordStateTransition(
|
||||
ObservationState fromState,
|
||||
ObservationState toState,
|
||||
string trigger,
|
||||
string environment)
|
||||
{
|
||||
_stateTransitionsTotal.Add(1,
|
||||
new KeyValuePair<string, object?>("from_state", fromState.ToString().ToLowerInvariant()),
|
||||
new KeyValuePair<string, object?>("to_state", toState.ToString().ToLowerInvariant()),
|
||||
new KeyValuePair<string, object?>("trigger", trigger),
|
||||
new KeyValuePair<string, object?>("environment", environment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record entropy value from evaluation.
|
||||
/// </summary>
|
||||
public void RecordEntropy(double entropy, string environment)
|
||||
{
|
||||
_entropyDistribution.Record(
|
||||
entropy,
|
||||
new KeyValuePair<string, object?>("environment", environment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record trust score from evaluation.
|
||||
/// </summary>
|
||||
public void RecordTrustScore(double trustScore, string environment)
|
||||
{
|
||||
_trustScoreDistribution.Record(
|
||||
trustScore,
|
||||
new KeyValuePair<string, object?>("environment", environment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record evaluation duration.
|
||||
/// </summary>
|
||||
public void RecordDuration(TimeSpan duration, string environment)
|
||||
{
|
||||
_evaluationDurationMs.Record(
|
||||
duration.TotalMilliseconds,
|
||||
new KeyValuePair<string, object?>("environment", environment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a timer scope for measuring evaluation duration.
|
||||
/// </summary>
|
||||
public IDisposable StartEvaluationTimer(string environment)
|
||||
{
|
||||
return new DurationScope(this, environment);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_meter.Dispose();
|
||||
}
|
||||
|
||||
private sealed class DurationScope : IDisposable
|
||||
{
|
||||
private readonly DeterminizationGateMetrics _metrics;
|
||||
private readonly string _environment;
|
||||
private readonly Stopwatch _stopwatch;
|
||||
|
||||
public DurationScope(DeterminizationGateMetrics metrics, string environment)
|
||||
{
|
||||
_metrics = metrics;
|
||||
_environment = environment;
|
||||
_stopwatch = Stopwatch.StartNew();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_stopwatch.Stop();
|
||||
_metrics.RecordDuration(_stopwatch.Elapsed, _environment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
@@ -396,7 +397,7 @@ public sealed class IncrementalPolicyOrchestrator
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(tenantId).Append('|');
|
||||
builder.Append(createdAt.ToString("O")).Append('|');
|
||||
builder.Append(createdAt.ToString("O", CultureInfo.InvariantCulture)).Append('|');
|
||||
|
||||
foreach (var evt in events.OrderBy(e => e.EventId, StringComparer.Ordinal))
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Policy.Engine.Orchestration;
|
||||
|
||||
@@ -55,7 +56,7 @@ internal sealed class LedgerExportService
|
||||
AdvisoryId: item.AdvisoryId,
|
||||
Status: item.Status,
|
||||
TraceRef: item.TraceRef,
|
||||
OccurredAt: result.CompletedAt.ToString("O")));
|
||||
OccurredAt: result.CompletedAt.ToString("O", CultureInfo.InvariantCulture)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ internal sealed class LedgerExportService
|
||||
.ThenBy(r => r.AdvisoryId, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var generatedAt = _timeProvider.GetUtcNow().ToString("O");
|
||||
var generatedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
|
||||
var exportId = StableIdGenerator.CreateUlid($"{request.TenantId}|{generatedAt}|{ordered.Count}");
|
||||
|
||||
var recordLines = ordered.Select(r => JsonSerializer.Serialize(r, SerializerOptions)).ToList();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Orchestration;
|
||||
@@ -95,7 +96,7 @@ internal sealed class OrchestratorJobService
|
||||
.Append(request.ContextId).Append('|')
|
||||
.Append(request.PolicyProfileHash).Append('|')
|
||||
.Append(priority).Append('|')
|
||||
.Append(requestedAt.ToString("O"));
|
||||
.Append(requestedAt.ToString("O", CultureInfo.InvariantCulture));
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Overlay;
|
||||
@@ -36,7 +37,7 @@ internal sealed class OverlayChangeEventPublisher
|
||||
string correlationId,
|
||||
PathDecisionDelta? delta = null)
|
||||
{
|
||||
var emittedAt = _timeProvider.GetUtcNow().ToString("O");
|
||||
var emittedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
|
||||
return new OverlayChangeEvent(
|
||||
Tenant: tenant,
|
||||
RuleId: projection.RuleId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
using StellaOps.Policy.Engine.Streaming;
|
||||
@@ -40,7 +41,7 @@ internal sealed class OverlayProjectionService
|
||||
|
||||
var projections = new List<OverlayProjection>(orderedTargets.Count);
|
||||
var version = 1;
|
||||
var effectiveAt = _timeProvider.GetUtcNow().ToString("O");
|
||||
var effectiveAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
|
||||
|
||||
foreach (var target in orderedTargets)
|
||||
{
|
||||
|
||||
@@ -197,7 +197,7 @@ public sealed class DeterminizationRuleSet
|
||||
ReevalAfter = TimeSpan.FromDays(3),
|
||||
Notes = $"Strict guardrails: entropy={ctx.UncertaintyScore.Entropy:F2}, trust={ctx.TrustScore:F2}, env={ctx.Environment}"
|
||||
},
|
||||
_ => GuardRails.Default()
|
||||
_ => GuardRails.Default
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Policy.RiskProfile.Scope;
|
||||
|
||||
@@ -155,7 +156,7 @@ internal sealed class EffectivePolicyAuditor : IEffectivePolicyAuditor
|
||||
var scope = new Dictionary<string, object?>
|
||||
{
|
||||
["event"] = eventType,
|
||||
["timestamp"] = _timeProvider.GetUtcNow().ToString("O")
|
||||
["timestamp"] = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(actorId))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using StellaOps.Policy.Engine.Ledger;
|
||||
using StellaOps.Policy.Engine.Orchestration;
|
||||
|
||||
@@ -40,7 +41,7 @@ internal sealed class SnapshotService
|
||||
.GroupBy(r => r.Status)
|
||||
.ToDictionary(g => g.Key, g => g.Count(), StringComparer.Ordinal);
|
||||
|
||||
var generatedAt = _timeProvider.GetUtcNow().ToString("O");
|
||||
var generatedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
|
||||
var snapshotId = StableIdGenerator.CreateUlid($"{export.Manifest.ExportId}|{request.OverlayHash}");
|
||||
|
||||
var snapshot = new SnapshotDetail(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenTelemetry.Trace;
|
||||
@@ -62,7 +63,7 @@ public sealed class IncidentModeService
|
||||
_logger.LogWarning(
|
||||
"Incident mode ENABLED. Reason: {Reason}, ExpiresAt: {ExpiresAt}",
|
||||
reason,
|
||||
expiresAt?.ToString("O") ?? "never");
|
||||
expiresAt?.ToString("O", CultureInfo.InvariantCulture) ?? "never");
|
||||
|
||||
PolicyEngineTelemetry.RecordError("incident_mode_enabled", null);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -48,7 +49,7 @@ internal sealed class TrustWeightingService
|
||||
|
||||
private IReadOnlyList<TrustWeightingEntry> Normalize(IReadOnlyList<TrustWeightingEntry> entries)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow().ToString("O");
|
||||
var now = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
|
||||
|
||||
var normalized = entries
|
||||
.Where(e => !string.IsNullOrWhiteSpace(e.Source))
|
||||
@@ -65,7 +66,7 @@ internal sealed class TrustWeightingService
|
||||
|
||||
private static IReadOnlyList<TrustWeightingEntry> DefaultWeights()
|
||||
{
|
||||
var now = TimeProvider.System.GetUtcNow().ToString("O");
|
||||
var now = TimeProvider.System.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
|
||||
return new[]
|
||||
{
|
||||
new TrustWeightingEntry("cartographer", 1.000m, null, now),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Task: GOV-018 - Sealed mode overrides and risk profile events endpoints
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -119,7 +120,7 @@ public static class GovernanceEndpoints
|
||||
.Select(MapOverrideToResponse)
|
||||
.ToList(),
|
||||
VerificationStatus = "verified",
|
||||
LastVerifiedAt = timeProvider.GetUtcNow().ToString("O")
|
||||
LastVerifiedAt = timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
@@ -155,7 +156,7 @@ public static class GovernanceEndpoints
|
||||
state = new SealedModeState
|
||||
{
|
||||
IsSealed = true,
|
||||
SealedAt = now.ToString("O"),
|
||||
SealedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
SealedBy = actor,
|
||||
Reason = request.Reason,
|
||||
TrustRoots = request.TrustRoots ?? [],
|
||||
@@ -167,7 +168,7 @@ public static class GovernanceEndpoints
|
||||
state = new SealedModeState
|
||||
{
|
||||
IsSealed = false,
|
||||
LastUnsealedAt = now.ToString("O")
|
||||
LastUnsealedAt = now.ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -187,7 +188,7 @@ public static class GovernanceEndpoints
|
||||
AllowedSources = state.AllowedSources,
|
||||
Overrides = [],
|
||||
VerificationStatus = "verified",
|
||||
LastVerifiedAt = now.ToString("O")
|
||||
LastVerifiedAt = now.ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
@@ -212,8 +213,8 @@ public static class GovernanceEndpoints
|
||||
Reason = request.Reason,
|
||||
ApprovalId = $"approval-{Guid.NewGuid():N}",
|
||||
ApprovedBy = [actor],
|
||||
ExpiresAt = now.AddHours(request.DurationHours).ToString("O"),
|
||||
CreatedAt = now.ToString("O"),
|
||||
ExpiresAt = now.AddHours(request.DurationHours).ToString("O", CultureInfo.InvariantCulture),
|
||||
CreatedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
Active = true
|
||||
};
|
||||
|
||||
@@ -313,8 +314,8 @@ public static class GovernanceEndpoints
|
||||
Signals = request.Signals ?? [],
|
||||
SeverityOverrides = request.SeverityOverrides ?? [],
|
||||
ActionOverrides = request.ActionOverrides ?? [],
|
||||
CreatedAt = now.ToString("O"),
|
||||
ModifiedAt = now.ToString("O"),
|
||||
CreatedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
CreatedBy = actor,
|
||||
ModifiedBy = actor
|
||||
};
|
||||
@@ -353,7 +354,7 @@ public static class GovernanceEndpoints
|
||||
Signals = request.Signals ?? existing.Signals,
|
||||
SeverityOverrides = request.SeverityOverrides ?? existing.SeverityOverrides,
|
||||
ActionOverrides = request.ActionOverrides ?? existing.ActionOverrides,
|
||||
ModifiedAt = now.ToString("O"),
|
||||
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
ModifiedBy = actor
|
||||
};
|
||||
|
||||
@@ -409,7 +410,7 @@ public static class GovernanceEndpoints
|
||||
var entity = existing with
|
||||
{
|
||||
Status = "active",
|
||||
ModifiedAt = now.ToString("O"),
|
||||
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
ModifiedBy = actor
|
||||
};
|
||||
|
||||
@@ -443,7 +444,7 @@ public static class GovernanceEndpoints
|
||||
var entity = existing with
|
||||
{
|
||||
Status = "deprecated",
|
||||
ModifiedAt = now.ToString("O"),
|
||||
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
ModifiedBy = actor,
|
||||
DeprecationReason = request.Reason
|
||||
};
|
||||
@@ -551,7 +552,7 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
if (RiskProfiles.IsEmpty)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow.ToString("O");
|
||||
var now = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture);
|
||||
RiskProfiles["profile-default"] = new RiskProfileEntity
|
||||
{
|
||||
Id = "profile-default",
|
||||
@@ -599,7 +600,7 @@ public static class GovernanceEndpoints
|
||||
Id = id,
|
||||
TenantId = tenantId,
|
||||
Type = eventType,
|
||||
Timestamp = timeProvider.GetUtcNow().ToString("O"),
|
||||
Timestamp = timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
|
||||
Actor = actor,
|
||||
ActorType = "user",
|
||||
TargetResource = targetId,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Sprint: SPRINT_5200_0001_0001 - Starter Policy Template
|
||||
// Task: T7 - Policy Pack Distribution
|
||||
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
@@ -344,7 +345,7 @@ public sealed class PolicyPackOciPublisher : IPolicyPackOciPublisher
|
||||
{
|
||||
var annotations = new SortedDictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["org.opencontainers.image.created"] = _timeProvider.GetUtcNow().ToString("O"),
|
||||
["org.opencontainers.image.created"] = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
|
||||
["org.opencontainers.image.title"] = request.PackName,
|
||||
["org.opencontainers.image.version"] = request.PackVersion,
|
||||
["stellaops.policy.pack.name"] = request.PackName,
|
||||
|
||||
@@ -50,7 +50,7 @@ internal static class ReceiptCanonicalizer
|
||||
// If the value looks like a timestamp, normalize to ISO 8601 round-trip
|
||||
if (DateTimeOffset.TryParse(element.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dto))
|
||||
{
|
||||
writer.WriteStringValue(dto.ToUniversalTime().ToString("O"));
|
||||
writer.WriteStringValue(dto.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -37,4 +37,11 @@ public sealed record EpssEvidence
|
||||
/// </summary>
|
||||
[JsonPropertyName("model_version")]
|
||||
public string? ModelVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property returning the EPSS score.
|
||||
/// Alias for <see cref="Epss"/> for API compatibility.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double Score => Epss;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,18 @@ public sealed record ReachabilityEvidence
|
||||
/// </summary>
|
||||
[JsonPropertyName("witness_digest")]
|
||||
public string? WitnessDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Analysis confidence [0.0, 1.0].
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public double Confidence { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property indicating if code is reachable.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsReachable => Status == ReachabilityStatus.Reachable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -42,4 +42,11 @@ public sealed record RuntimeEvidence
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property indicating if vulnerable code was observed loaded.
|
||||
/// Alias for <see cref="Detected"/> for API compatibility.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool ObservedLoaded => Detected;
|
||||
}
|
||||
|
||||
@@ -37,4 +37,17 @@ public sealed record VexClaimSummary
|
||||
/// </summary>
|
||||
[JsonPropertyName("justification")]
|
||||
public string? Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property indicating if the VEX status is "not_affected".
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsNotAffected => string.Equals(Status, "not_affected", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Issuer trust level [0.0, 1.0].
|
||||
/// Alias for <see cref="Confidence"/> for API compatibility.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double IssuerTrust => Confidence;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Context for determinization evaluation.
|
||||
/// Contains environment, criticality, and policy settings.
|
||||
/// Contains environment, criticality, policy settings, and computed evidence data.
|
||||
/// </summary>
|
||||
public sealed record DeterminizationContext
|
||||
{
|
||||
@@ -18,56 +18,116 @@ public sealed record DeterminizationContext
|
||||
/// Asset criticality level.
|
||||
/// </summary>
|
||||
[JsonPropertyName("criticality")]
|
||||
public required AssetCriticality Criticality { get; init; }
|
||||
public AssetCriticality Criticality { get; init; } = AssetCriticality.Medium;
|
||||
|
||||
/// <summary>
|
||||
/// Entropy threshold for this context.
|
||||
/// Observations above this trigger guardrails.
|
||||
/// </summary>
|
||||
[JsonPropertyName("entropy_threshold")]
|
||||
public required double EntropyThreshold { get; init; }
|
||||
public double EntropyThreshold { get; init; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Decay threshold for this context.
|
||||
/// Observations below this are considered stale.
|
||||
/// </summary>
|
||||
[JsonPropertyName("decay_threshold")]
|
||||
public required double DecayThreshold { get; init; }
|
||||
public double DecayThreshold { get; init; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Creates context with default production settings.
|
||||
/// Signal snapshot containing evidence from various sources.
|
||||
/// </summary>
|
||||
public static DeterminizationContext Production() => new()
|
||||
{
|
||||
Environment = DeploymentEnvironment.Production,
|
||||
Criticality = AssetCriticality.High,
|
||||
EntropyThreshold = 0.4,
|
||||
DecayThreshold = 0.50
|
||||
};
|
||||
[JsonPropertyName("signal_snapshot")]
|
||||
public required SignalSnapshot SignalSnapshot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates context with relaxed development settings.
|
||||
/// Calculated uncertainty score for this context.
|
||||
/// </summary>
|
||||
public static DeterminizationContext Development() => new()
|
||||
{
|
||||
Environment = DeploymentEnvironment.Development,
|
||||
Criticality = AssetCriticality.Low,
|
||||
EntropyThreshold = 0.6,
|
||||
DecayThreshold = 0.35
|
||||
};
|
||||
[JsonPropertyName("uncertainty_score")]
|
||||
public required UncertaintyScore UncertaintyScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates context with custom thresholds.
|
||||
/// Observation decay information.
|
||||
/// </summary>
|
||||
[JsonPropertyName("decay")]
|
||||
public required ObservationDecay Decay { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Aggregated trust score (0.0-1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("trust_score")]
|
||||
public required double TrustScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates context with default production settings and placeholder signal data.
|
||||
/// </summary>
|
||||
public static DeterminizationContext Production(
|
||||
SignalSnapshot? snapshot = null,
|
||||
UncertaintyScore? uncertaintyScore = null,
|
||||
ObservationDecay? decay = null,
|
||||
double trustScore = 0.5)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return new()
|
||||
{
|
||||
Environment = DeploymentEnvironment.Production,
|
||||
Criticality = AssetCriticality.High,
|
||||
EntropyThreshold = 0.4,
|
||||
DecayThreshold = 0.50,
|
||||
SignalSnapshot = snapshot ?? SignalSnapshot.Empty("CVE-0000-0000", "pkg:unknown/unknown@0.0.0", now),
|
||||
UncertaintyScore = uncertaintyScore ?? UncertaintyScore.Zero(1.0, now),
|
||||
Decay = decay ?? ObservationDecay.Fresh(now),
|
||||
TrustScore = trustScore
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates context with relaxed development settings and placeholder signal data.
|
||||
/// </summary>
|
||||
public static DeterminizationContext Development(
|
||||
SignalSnapshot? snapshot = null,
|
||||
UncertaintyScore? uncertaintyScore = null,
|
||||
ObservationDecay? decay = null,
|
||||
double trustScore = 0.5)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return new()
|
||||
{
|
||||
Environment = DeploymentEnvironment.Development,
|
||||
Criticality = AssetCriticality.Low,
|
||||
EntropyThreshold = 0.6,
|
||||
DecayThreshold = 0.35,
|
||||
SignalSnapshot = snapshot ?? SignalSnapshot.Empty("CVE-0000-0000", "pkg:unknown/unknown@0.0.0", now),
|
||||
UncertaintyScore = uncertaintyScore ?? UncertaintyScore.Zero(1.0, now),
|
||||
Decay = decay ?? ObservationDecay.Fresh(now),
|
||||
TrustScore = trustScore
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates context with custom thresholds and placeholder signal data.
|
||||
/// </summary>
|
||||
public static DeterminizationContext Create(
|
||||
DeploymentEnvironment environment,
|
||||
AssetCriticality criticality,
|
||||
double entropyThreshold,
|
||||
double decayThreshold) => new()
|
||||
double decayThreshold,
|
||||
SignalSnapshot? snapshot = null,
|
||||
UncertaintyScore? uncertaintyScore = null,
|
||||
ObservationDecay? decay = null,
|
||||
double trustScore = 0.5)
|
||||
{
|
||||
Environment = environment,
|
||||
Criticality = criticality,
|
||||
EntropyThreshold = entropyThreshold,
|
||||
DecayThreshold = decayThreshold
|
||||
};
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return new()
|
||||
{
|
||||
Environment = environment,
|
||||
Criticality = criticality,
|
||||
EntropyThreshold = entropyThreshold,
|
||||
DecayThreshold = decayThreshold,
|
||||
SignalSnapshot = snapshot ?? SignalSnapshot.Empty("CVE-0000-0000", "pkg:unknown/unknown@0.0.0", now),
|
||||
UncertaintyScore = uncertaintyScore ?? UncertaintyScore.Zero(1.0, now),
|
||||
Decay = decay ?? ObservationDecay.Fresh(now),
|
||||
TrustScore = trustScore
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,22 @@ public sealed record GuardRails
|
||||
[JsonPropertyName("notes")]
|
||||
public string? Notes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Default guardrails instance with safe settings.
|
||||
/// </summary>
|
||||
public static GuardRails Default { get; } = new()
|
||||
{
|
||||
EnableMonitoring = true,
|
||||
RestrictToNonProd = false,
|
||||
RequireApproval = false,
|
||||
ReevalAfter = TimeSpan.FromDays(7),
|
||||
Notes = null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates GuardRails with default safe settings.
|
||||
/// </summary>
|
||||
public static GuardRails Default() => new()
|
||||
public static GuardRails CreateDefault() => new()
|
||||
{
|
||||
EnableMonitoring = true,
|
||||
RestrictToNonProd = false,
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text.Json.Serialization;
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Per-observation decay configuration.
|
||||
/// Per-observation decay configuration and computed state.
|
||||
/// Tracks evidence staleness with configurable half-life.
|
||||
/// Formula: decayed = max(floor, exp(-ln(2) * age_days / half_life_days))
|
||||
/// </summary>
|
||||
@@ -13,27 +13,27 @@ public sealed record ObservationDecay
|
||||
/// When the observation was first recorded (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("observed_at")]
|
||||
public required DateTimeOffset ObservedAt { get; init; }
|
||||
public DateTimeOffset ObservedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// When the observation was last refreshed (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("refreshed_at")]
|
||||
public required DateTimeOffset RefreshedAt { get; init; }
|
||||
public DateTimeOffset RefreshedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Half-life in days.
|
||||
/// Default: 14 days.
|
||||
/// </summary>
|
||||
[JsonPropertyName("half_life_days")]
|
||||
public required double HalfLifeDays { get; init; }
|
||||
public double HalfLifeDays { get; init; } = 14.0;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum confidence floor.
|
||||
/// Default: 0.35 (consistent with FreshnessCalculator).
|
||||
/// </summary>
|
||||
[JsonPropertyName("floor")]
|
||||
public required double Floor { get; init; }
|
||||
public double Floor { get; init; } = 0.35;
|
||||
|
||||
/// <summary>
|
||||
/// Staleness threshold (0.0-1.0).
|
||||
@@ -41,7 +41,31 @@ public sealed record ObservationDecay
|
||||
/// Default: 0.50
|
||||
/// </summary>
|
||||
[JsonPropertyName("staleness_threshold")]
|
||||
public required double StalenessThreshold { get; init; }
|
||||
public double StalenessThreshold { get; init; } = 0.50;
|
||||
|
||||
/// <summary>
|
||||
/// Last signal update time (alias for RefreshedAt for convenience).
|
||||
/// </summary>
|
||||
[JsonPropertyName("last_signal_update")]
|
||||
public DateTimeOffset LastSignalUpdate { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Age in days since last refresh.
|
||||
/// </summary>
|
||||
[JsonPropertyName("age_days")]
|
||||
public double AgeDays { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pre-computed decay multiplier (0.0-1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("decayed_multiplier")]
|
||||
public double DecayedMultiplier { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the observation is considered stale.
|
||||
/// </summary>
|
||||
[JsonPropertyName("is_stale")]
|
||||
public bool IsStale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the current decay multiplier.
|
||||
@@ -59,7 +83,7 @@ public sealed record ObservationDecay
|
||||
/// <summary>
|
||||
/// Returns true if the observation is stale (decay below threshold).
|
||||
/// </summary>
|
||||
public bool IsStale(DateTimeOffset now) =>
|
||||
public bool CheckIsStale(DateTimeOffset now) =>
|
||||
CalculateDecay(now) < StalenessThreshold;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -82,6 +82,12 @@ public sealed record UncertaintyScore
|
||||
[JsonPropertyName("calculated_at")]
|
||||
public required DateTimeOffset CalculatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Completeness ratio (present_weight / max_weight).
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double Completeness => MaxWeight > 0 ? PresentWeight / MaxWeight : 0.0;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an UncertaintyScore with calculated tier.
|
||||
/// </summary>
|
||||
|
||||
@@ -11,6 +11,16 @@ namespace StellaOps.Policy.Determinization;
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers determinization services with default options.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection</param>
|
||||
/// <returns>Service collection for chaining</returns>
|
||||
public static IServiceCollection AddDeterminization(this IServiceCollection services)
|
||||
{
|
||||
return services.AddDeterminization(_ => { });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers determinization services with the DI container.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.Policy.Exceptions.Models;
|
||||
|
||||
@@ -255,8 +256,8 @@ public sealed record ExceptionEvent
|
||||
NewVersion = newVersion,
|
||||
Description = reason ?? $"Exception extended from {previousExpiry:O} to {newExpiry:O}",
|
||||
Details = ImmutableDictionary<string, string>.Empty
|
||||
.Add("previous_expiry", previousExpiry.ToString("O"))
|
||||
.Add("new_expiry", newExpiry.ToString("O")),
|
||||
.Add("previous_expiry", previousExpiry.ToString("O", CultureInfo.InvariantCulture))
|
||||
.Add("new_expiry", newExpiry.ToString("O", CultureInfo.InvariantCulture)),
|
||||
ClientInfo = clientInfo
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
|
||||
@@ -81,7 +82,7 @@ public static class BudgetExceededEventFactory
|
||||
["action"] = payload.Action,
|
||||
["totalUnknowns"] = payload.TotalUnknowns,
|
||||
["violationCount"] = payload.ViolationCount,
|
||||
["timestamp"] = payload.Timestamp.ToString("O")
|
||||
["timestamp"] = payload.Timestamp.ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
if (payload.TotalLimit.HasValue)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -147,7 +148,7 @@ public sealed class BudgetThresholdNotifier
|
||||
["percentageUsed"] = budget.PercentageUsed,
|
||||
["status"] = budget.Status.ToString().ToLowerInvariant(),
|
||||
["severity"] = severity,
|
||||
["timestamp"] = timeProvider.GetUtcNow().ToString("O")
|
||||
["timestamp"] = timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
@@ -133,7 +134,7 @@ public static class PolicyDigest
|
||||
|
||||
if (rule.Expires is DateTimeOffset expires)
|
||||
{
|
||||
writer.WriteString("expires", expires.ToUniversalTime().ToString("O"));
|
||||
writer.WriteString("expires", expires.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(rule.Justification))
|
||||
@@ -159,7 +160,7 @@ public static class PolicyDigest
|
||||
{
|
||||
if (ignore.Until is DateTimeOffset until)
|
||||
{
|
||||
writer.WriteString("until", until.ToUniversalTime().ToString("O"));
|
||||
writer.WriteString("until", until.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ignore.Justification))
|
||||
|
||||
@@ -4,16 +4,8 @@ using StellaOps.Policy.Determinization.Models;
|
||||
|
||||
namespace StellaOps.Policy;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime monitoring requirements for GuardedPass verdicts.
|
||||
/// </summary>
|
||||
/// <param name="MonitoringIntervalDays">Days between re-evaluation checks.</param>
|
||||
/// <param name="RequireProof">Whether runtime proof is required before production deployment.</param>
|
||||
/// <param name="AlertOnChange">Whether to send alerts if verdict changes on re-evaluation.</param>
|
||||
public sealed record GuardRails(
|
||||
int MonitoringIntervalDays,
|
||||
bool RequireProof,
|
||||
bool AlertOnChange);
|
||||
// NOTE: GuardRails type has been consolidated into StellaOps.Policy.Determinization.Models.GuardRails.
|
||||
// Use that type for runtime monitoring requirements in GuardedPass verdicts.
|
||||
|
||||
/// <summary>
|
||||
/// Status outcomes for policy verdicts.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Description: Deterministic hashing for proof nodes and root hash computation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -110,7 +111,7 @@ public static class ProofHashing
|
||||
["ruleId"] = node.RuleId,
|
||||
["seed"] = Convert.ToBase64String(node.Seed),
|
||||
["total"] = node.Total,
|
||||
["tsUtc"] = node.TsUtc.ToUniversalTime().ToString("O")
|
||||
["tsUtc"] = node.TsUtc.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
return SerializeCanonical(obj);
|
||||
|
||||
@@ -67,8 +67,8 @@ public class ObservationDecayTests
|
||||
var after = observedAt.AddDays(20);
|
||||
|
||||
// Act & Assert
|
||||
decay.IsStale(before).Should().BeFalse();
|
||||
decay.IsStale(after).Should().BeTrue();
|
||||
decay.CheckIsStale(before).Should().BeFalse();
|
||||
decay.CheckIsStale(after).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -12,10 +12,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Confidence.Models;
|
||||
using StellaOps.Policy.Determinization;
|
||||
using StellaOps.Policy.Determinization.Evidence;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
using StellaOps.Policy.Determinization.Scoring;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Engine.Gates.Determinization;
|
||||
using StellaOps.Policy.Engine.Policies;
|
||||
@@ -15,6 +19,7 @@ namespace StellaOps.Policy.Engine.Tests.Gates.Determinization;
|
||||
|
||||
public class DeterminizationGateTests
|
||||
{
|
||||
private static readonly DateTimeOffset Now = DateTimeOffset.UtcNow;
|
||||
private readonly Mock<ISignalSnapshotBuilder> _snapshotBuilderMock;
|
||||
private readonly Mock<IUncertaintyScoreCalculator> _uncertaintyCalculatorMock;
|
||||
private readonly Mock<IDecayedConfidenceCalculator> _decayCalculatorMock;
|
||||
@@ -28,7 +33,7 @@ public class DeterminizationGateTests
|
||||
_decayCalculatorMock = new Mock<IDecayedConfidenceCalculator>();
|
||||
_trustAggregatorMock = new Mock<TrustScoreAggregator>();
|
||||
|
||||
var options = Options.Create(new DeterminizationOptions());
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new DeterminizationOptions());
|
||||
var policy = new DeterminizationPolicy(options, NullLogger<DeterminizationPolicy>.Instance);
|
||||
|
||||
_gate = new DeterminizationGate(
|
||||
@@ -45,13 +50,12 @@ public class DeterminizationGateTests
|
||||
{
|
||||
// Arrange
|
||||
var snapshot = CreateSnapshot();
|
||||
var uncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = 0.45,
|
||||
Tier = UncertaintyTier.Moderate,
|
||||
Completeness = 0.55,
|
||||
MissingSignals = []
|
||||
};
|
||||
var uncertaintyScore = UncertaintyScore.Create(
|
||||
entropy: 0.45,
|
||||
gaps: Array.Empty<SignalGap>(),
|
||||
presentWeight: 55,
|
||||
maxWeight: 100,
|
||||
calculatedAt: Now);
|
||||
|
||||
_snapshotBuilderMock
|
||||
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
@@ -76,12 +80,7 @@ public class DeterminizationGateTests
|
||||
Environment = "development"
|
||||
};
|
||||
|
||||
var mergeResult = new MergeResult
|
||||
{
|
||||
FinalScore = 0.5,
|
||||
FinalTrustLevel = TrustLevel.Medium,
|
||||
Claims = []
|
||||
};
|
||||
var mergeResult = CreateMergeResult(VexStatus.UnderInvestigation, 0.5);
|
||||
|
||||
// Act
|
||||
var result = await _gate.EvaluateAsync(mergeResult, context);
|
||||
@@ -109,13 +108,12 @@ public class DeterminizationGateTests
|
||||
{
|
||||
// Arrange
|
||||
var snapshot = CreateSnapshot();
|
||||
var uncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = 0.5,
|
||||
Tier = UncertaintyTier.Moderate,
|
||||
Completeness = 0.5,
|
||||
MissingSignals = []
|
||||
};
|
||||
var uncertaintyScore = UncertaintyScore.Create(
|
||||
entropy: 0.5,
|
||||
gaps: Array.Empty<SignalGap>(),
|
||||
presentWeight: 50,
|
||||
maxWeight: 100,
|
||||
calculatedAt: Now);
|
||||
|
||||
_snapshotBuilderMock
|
||||
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
@@ -140,12 +138,7 @@ public class DeterminizationGateTests
|
||||
Environment = "development"
|
||||
};
|
||||
|
||||
var mergeResult = new MergeResult
|
||||
{
|
||||
FinalScore = 0.5,
|
||||
FinalTrustLevel = TrustLevel.Medium,
|
||||
Claims = []
|
||||
};
|
||||
var mergeResult = CreateMergeResult(VexStatus.UnderInvestigation, 0.5);
|
||||
|
||||
// Act
|
||||
var result = await _gate.EvaluateAsync(mergeResult, context);
|
||||
@@ -160,13 +153,12 @@ public class DeterminizationGateTests
|
||||
{
|
||||
// Arrange
|
||||
var snapshot = CreateSnapshot();
|
||||
var uncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = 0.2,
|
||||
Tier = UncertaintyTier.Low,
|
||||
Completeness = 0.8,
|
||||
MissingSignals = []
|
||||
};
|
||||
var uncertaintyScore = UncertaintyScore.Create(
|
||||
entropy: 0.2,
|
||||
gaps: Array.Empty<SignalGap>(),
|
||||
presentWeight: 80,
|
||||
maxWeight: 100,
|
||||
calculatedAt: Now);
|
||||
|
||||
_snapshotBuilderMock
|
||||
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
@@ -191,12 +183,7 @@ public class DeterminizationGateTests
|
||||
Environment = "production"
|
||||
};
|
||||
|
||||
var mergeResult = new MergeResult
|
||||
{
|
||||
FinalScore = 0.8,
|
||||
FinalTrustLevel = TrustLevel.High,
|
||||
Claims = []
|
||||
};
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected, 0.8);
|
||||
|
||||
// Act
|
||||
var result = await _gate.EvaluateAsync(mergeResult, context);
|
||||
@@ -212,11 +199,35 @@ public class DeterminizationGateTests
|
||||
Purl = "pkg:npm/test@1.0.0",
|
||||
Epss = SignalState<EpssEvidence>.NotQueried(),
|
||||
Vex = SignalState<VexClaimSummary>.NotQueried(),
|
||||
Reachability = SignalState<ReachabilityEvidence>.NotQueried(),
|
||||
Runtime = SignalState<RuntimeEvidence>.NotQueried(),
|
||||
Reachability = SignalState<StellaOps.Policy.Determinization.Evidence.ReachabilityEvidence>.NotQueried(),
|
||||
Runtime = SignalState<StellaOps.Policy.Determinization.Evidence.RuntimeEvidence>.NotQueried(),
|
||||
Backport = SignalState<BackportEvidence>.NotQueried(),
|
||||
Sbom = SignalState<SbomLineageEvidence>.NotQueried(),
|
||||
Cvss = SignalState<CvssEvidence>.NotQueried(),
|
||||
SnapshotAt = DateTimeOffset.UtcNow
|
||||
SnapshotAt = Now
|
||||
};
|
||||
|
||||
private static MergeResult CreateMergeResult(VexStatus status, double confidence)
|
||||
{
|
||||
var winningClaim = new ScoredClaim
|
||||
{
|
||||
SourceId = "test-source",
|
||||
Status = status,
|
||||
OriginalScore = confidence,
|
||||
AdjustedScore = confidence,
|
||||
ScopeSpecificity = 1,
|
||||
Accepted = true,
|
||||
Reason = "test"
|
||||
};
|
||||
|
||||
return new MergeResult
|
||||
{
|
||||
Status = status,
|
||||
Confidence = confidence,
|
||||
HasConflicts = false,
|
||||
AllClaims = ImmutableArray.Create(winningClaim),
|
||||
WinningClaim = winningClaim,
|
||||
Conflicts = ImmutableArray<ConflictRecord>.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ public sealed class FacetQuotaGateIntegrationTests
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultMaxChurnPercent = 10.0m
|
||||
DefaultQuota = new FacetQuota { MaxChurnPercent = 10.0m }
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
@@ -131,8 +131,7 @@ public sealed class FacetQuotaGateIntegrationTests
|
||||
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultAction = QuotaExceededAction.Block
|
||||
Enabled = true
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
@@ -160,8 +159,7 @@ public sealed class FacetQuotaGateIntegrationTests
|
||||
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultAction = QuotaExceededAction.RequireVex
|
||||
Enabled = true
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
@@ -343,15 +341,9 @@ public sealed class FacetQuotaGateIntegrationTests
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultMaxChurnPercent = 10.0m,
|
||||
FacetOverrides = new Dictionary<string, FacetQuotaOverride>
|
||||
{
|
||||
["os-packages"] = new FacetQuotaOverride
|
||||
{
|
||||
MaxChurnPercent = 30m, // Higher threshold for OS packages
|
||||
Action = QuotaExceededAction.Warn
|
||||
}
|
||||
}
|
||||
DefaultQuota = new FacetQuota { MaxChurnPercent = 10.0m },
|
||||
FacetQuotas = ImmutableDictionary<string, FacetQuota>.Empty
|
||||
.Add("os-packages", new FacetQuota { MaxChurnPercent = 30m })
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
@@ -443,14 +435,16 @@ public sealed class FacetQuotaGateIntegrationTests
|
||||
|
||||
private FacetSeal CreateSealWithTimestamp(string imageDigest, int fileCount, DateTimeOffset createdAt)
|
||||
{
|
||||
var files = Enumerable.Range(0, fileCount)
|
||||
.Select(i => new FacetFileEntry($"/file{i}.txt", $"sha256:{i:x8}", 100, null))
|
||||
.ToImmutableArray();
|
||||
|
||||
var facetEntry = new FacetEntry(
|
||||
FacetId: "test-facet",
|
||||
Files: files,
|
||||
MerkleRoot: $"sha256:facet{fileCount:x8}");
|
||||
var facetEntry = new FacetEntry
|
||||
{
|
||||
FacetId = "test-facet",
|
||||
Name = "Test Facet",
|
||||
Category = FacetCategory.OsPackages,
|
||||
Selectors = ["/file*.txt"],
|
||||
MerkleRoot = $"sha256:facet{fileCount:x8}",
|
||||
FileCount = fileCount,
|
||||
TotalBytes = fileCount * 100L
|
||||
};
|
||||
|
||||
return new FacetSeal
|
||||
{
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) 2026 StellaOps
|
||||
// Sprint: SPRINT_20260106_001_003_POLICY_determinization_gates
|
||||
// Task: DPE-023, DPE-024 - Integration tests for determinization gate in policy pipeline
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Determinization;
|
||||
using StellaOps.Policy.Engine.DependencyInjection;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Engine.Gates.Determinization;
|
||||
using StellaOps.Policy.Engine.Policies;
|
||||
using StellaOps.Policy.Engine.Subscriptions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for determinization gate within the policy pipeline.
|
||||
/// Tests DI wiring, gate registration, and signal update handling.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "20260106.001.003")]
|
||||
[Trait("Task", "DPE-023")]
|
||||
public sealed class DeterminizationGateIntegrationTests
|
||||
{
|
||||
private static ServiceCollection CreateServicesWithConfiguration()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection()
|
||||
.Build();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
return services;
|
||||
}
|
||||
|
||||
#region DI Wiring Tests
|
||||
|
||||
[Fact(DisplayName = "AddDeterminizationEngine registers all required services")]
|
||||
public void AddDeterminizationEngine_RegistersAllServices()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
|
||||
// Act
|
||||
services.AddLogging();
|
||||
services.AddDeterminizationEngine();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// Assert: All services should be resolvable
|
||||
provider.GetService<IDeterminizationGate>().Should().NotBeNull();
|
||||
provider.GetService<IDeterminizationPolicy>().Should().NotBeNull();
|
||||
provider.GetService<ISignalUpdateSubscription>().Should().NotBeNull();
|
||||
provider.GetService<DeterminizationGateMetrics>().Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AddPolicyEngine includes determinization services")]
|
||||
public void AddPolicyEngine_IncludesDeterminizationServices()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
|
||||
// Act
|
||||
services.AddLogging();
|
||||
services.AddMemoryCache();
|
||||
services.AddPolicyEngine();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// Assert: Determinization services should be available
|
||||
provider.GetService<IDeterminizationGate>().Should().NotBeNull();
|
||||
provider.GetService<IDeterminizationPolicy>().Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Determinization services are registered as singletons")]
|
||||
public void DeterminizationServices_AreRegisteredAsSingletons()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
services.AddLogging();
|
||||
services.AddDeterminizationEngine();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var gate1 = provider.GetRequiredService<IDeterminizationGate>();
|
||||
var gate2 = provider.GetRequiredService<IDeterminizationGate>();
|
||||
|
||||
// Assert: Same instance (singleton)
|
||||
gate1.Should().BeSameAs(gate2);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "DeterminizationGateMetrics is resolvable")]
|
||||
public void DeterminizationGateMetrics_IsResolvable()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
services.AddLogging();
|
||||
services.AddDeterminizationEngine();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var metrics = provider.GetService<DeterminizationGateMetrics>();
|
||||
|
||||
// Assert
|
||||
metrics.Should().NotBeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Options Tests
|
||||
|
||||
[Fact(DisplayName = "DeterminizationOptions are bound from configuration")]
|
||||
public void DeterminizationOptions_AreBoundFromConfiguration()
|
||||
{
|
||||
// Arrange
|
||||
var configData = new Dictionary<string, string?>
|
||||
{
|
||||
["Determinization:ManualReviewEntropyThreshold"] = "0.65",
|
||||
["Determinization:RefreshEntropyThreshold"] = "0.45",
|
||||
["Determinization:ConfidenceHalfLifeDays"] = "21"
|
||||
};
|
||||
|
||||
var services = new ServiceCollection();
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configData)
|
||||
.Build();
|
||||
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddOptions<DeterminizationOptions>()
|
||||
.Bind(configuration.GetSection("Determinization"));
|
||||
|
||||
// Act
|
||||
var provider = services.BuildServiceProvider();
|
||||
var options = provider.GetRequiredService<IOptions<DeterminizationOptions>>();
|
||||
|
||||
// Assert
|
||||
options.Value.ManualReviewEntropyThreshold.Should().Be(0.65);
|
||||
options.Value.RefreshEntropyThreshold.Should().Be(0.45);
|
||||
options.Value.ConfidenceHalfLifeDays.Should().Be(21);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for signal update re-evaluation.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "20260106.001.003")]
|
||||
[Trait("Task", "DPE-024")]
|
||||
public sealed class SignalUpdateIntegrationTests
|
||||
{
|
||||
private static ServiceCollection CreateServicesWithConfiguration()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection()
|
||||
.Build();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
return services;
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "SignalUpdateHandler is registered via AddDeterminizationEngine")]
|
||||
public void SignalUpdateHandler_IsRegisteredViaDeterminizationEngine()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
services.AddLogging();
|
||||
services.AddDeterminizationEngine();
|
||||
|
||||
// Act
|
||||
var provider = services.BuildServiceProvider();
|
||||
var handler = provider.GetService<ISignalUpdateSubscription>();
|
||||
|
||||
// Assert
|
||||
handler.Should().NotBeNull();
|
||||
handler.Should().BeOfType<SignalUpdateHandler>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "SignalUpdateHandler receives all dependencies")]
|
||||
public void SignalUpdateHandler_ReceivesAllDependencies()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
services.AddLogging();
|
||||
services.AddDeterminizationEngine();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var handler = provider.GetRequiredService<ISignalUpdateSubscription>();
|
||||
|
||||
// Assert: If dependencies were missing, this would fail to resolve
|
||||
handler.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Determinization;
|
||||
using StellaOps.Policy.Determinization.Evidence;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
using StellaOps.Policy.Engine.Policies;
|
||||
|
||||
@@ -10,11 +11,12 @@ namespace StellaOps.Policy.Engine.Tests.Policies;
|
||||
|
||||
public class DeterminizationPolicyTests
|
||||
{
|
||||
private static readonly DateTimeOffset Now = DateTimeOffset.UtcNow;
|
||||
private readonly DeterminizationPolicy _policy;
|
||||
|
||||
public DeterminizationPolicyTests()
|
||||
{
|
||||
var options = Options.Create(new DeterminizationOptions());
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new DeterminizationOptions());
|
||||
_policy = new DeterminizationPolicy(options, NullLogger<DeterminizationPolicy>.Instance);
|
||||
}
|
||||
|
||||
@@ -22,12 +24,16 @@ public class DeterminizationPolicyTests
|
||||
public void Evaluate_RuntimeEvidenceLoaded_ReturnsEscalated()
|
||||
{
|
||||
// Arrange
|
||||
var runtimeEvidence = new RuntimeEvidence
|
||||
{
|
||||
Detected = true,
|
||||
Source = "tracer",
|
||||
ObservationStart = Now.AddHours(-1),
|
||||
ObservationEnd = Now,
|
||||
Confidence = 0.95
|
||||
};
|
||||
var context = CreateContext(
|
||||
runtime: new SignalState<RuntimeEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new RuntimeEvidence { ObservedLoaded = true }
|
||||
});
|
||||
runtime: SignalState<RuntimeEvidence>.Queried(runtimeEvidence, Now));
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
@@ -42,12 +48,15 @@ public class DeterminizationPolicyTests
|
||||
public void Evaluate_HighEpss_ReturnsQuarantined()
|
||||
{
|
||||
// Arrange
|
||||
var epssEvidence = new EpssEvidence
|
||||
{
|
||||
Cve = "CVE-2024-0001",
|
||||
Epss = 0.8,
|
||||
Percentile = 0.95,
|
||||
PublishedAt = Now.AddDays(-1)
|
||||
};
|
||||
var context = CreateContext(
|
||||
epss: new SignalState<EpssEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new EpssEvidence { Score = 0.8 }
|
||||
},
|
||||
epss: SignalState<EpssEvidence>.Queried(epssEvidence, Now),
|
||||
environment: DeploymentEnvironment.Production);
|
||||
|
||||
// Act
|
||||
@@ -63,12 +72,14 @@ public class DeterminizationPolicyTests
|
||||
public void Evaluate_ReachableCode_ReturnsQuarantined()
|
||||
{
|
||||
// Arrange
|
||||
var reachabilityEvidence = new ReachabilityEvidence
|
||||
{
|
||||
Status = ReachabilityStatus.Reachable,
|
||||
AnalyzedAt = Now,
|
||||
Confidence = 0.9
|
||||
};
|
||||
var context = CreateContext(
|
||||
reachability: new SignalState<ReachabilityEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new ReachabilityEvidence { IsReachable = true, Confidence = 0.9 }
|
||||
});
|
||||
reachability: SignalState<ReachabilityEvidence>.Queried(reachabilityEvidence, Now));
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
@@ -135,12 +146,14 @@ public class DeterminizationPolicyTests
|
||||
public void Evaluate_UnreachableWithHighConfidence_ReturnsAllowed()
|
||||
{
|
||||
// Arrange
|
||||
var reachabilityEvidence = new ReachabilityEvidence
|
||||
{
|
||||
Status = ReachabilityStatus.Unreachable,
|
||||
AnalyzedAt = Now,
|
||||
Confidence = 0.9
|
||||
};
|
||||
var context = CreateContext(
|
||||
reachability: new SignalState<ReachabilityEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new ReachabilityEvidence { IsReachable = false, Confidence = 0.9 }
|
||||
},
|
||||
reachability: SignalState<ReachabilityEvidence>.Queried(reachabilityEvidence, Now),
|
||||
trustScore: 0.8);
|
||||
|
||||
// Act
|
||||
@@ -156,12 +169,15 @@ public class DeterminizationPolicyTests
|
||||
public void Evaluate_VexNotAffected_ReturnsAllowed()
|
||||
{
|
||||
// Arrange
|
||||
var vexSummary = new VexClaimSummary
|
||||
{
|
||||
Status = "not_affected",
|
||||
Confidence = 0.9,
|
||||
StatementCount = 2,
|
||||
ComputedAt = Now
|
||||
};
|
||||
var context = CreateContext(
|
||||
vex: new SignalState<VexClaimSummary>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new VexClaimSummary { IsNotAffected = true, IssuerTrust = 0.9 }
|
||||
},
|
||||
vex: SignalState<VexClaimSummary>.Queried(vexSummary, Now),
|
||||
trustScore: 0.8);
|
||||
|
||||
// Act
|
||||
@@ -249,22 +265,21 @@ public class DeterminizationPolicyTests
|
||||
Backport = SignalState<BackportEvidence>.NotQueried(),
|
||||
Sbom = SignalState<SbomLineageEvidence>.NotQueried(),
|
||||
Cvss = SignalState<CvssEvidence>.NotQueried(),
|
||||
SnapshotAt = DateTimeOffset.UtcNow
|
||||
SnapshotAt = Now
|
||||
};
|
||||
|
||||
return new DeterminizationContext
|
||||
{
|
||||
SignalSnapshot = snapshot,
|
||||
UncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = entropy,
|
||||
Tier = tier,
|
||||
Completeness = 1.0 - entropy,
|
||||
MissingSignals = []
|
||||
},
|
||||
UncertaintyScore = UncertaintyScore.Create(
|
||||
entropy,
|
||||
Array.Empty<SignalGap>(),
|
||||
presentWeight: (1.0 - entropy) * 100,
|
||||
maxWeight: 100,
|
||||
calculatedAt: Now),
|
||||
Decay = new ObservationDecay
|
||||
{
|
||||
LastSignalUpdate = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
LastSignalUpdate = Now.AddDays(-1),
|
||||
AgeDays = 1,
|
||||
DecayedMultiplier = isStale ? 0.3 : 0.9,
|
||||
IsStale = isStale
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Engine.Subscriptions;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Subscriptions;
|
||||
|
||||
public class SignalUpdateHandlerTests
|
||||
{
|
||||
private readonly Mock<IObservationRepository> _observationRepositoryMock;
|
||||
private readonly Mock<IDeterminizationGate> _gateMock;
|
||||
private readonly Mock<IEventPublisher> _eventPublisherMock;
|
||||
private readonly SignalUpdateHandler _handler;
|
||||
|
||||
public SignalUpdateHandlerTests()
|
||||
{
|
||||
_observationRepositoryMock = new Mock<IObservationRepository>();
|
||||
_gateMock = new Mock<IDeterminizationGate>();
|
||||
_eventPublisherMock = new Mock<IEventPublisher>();
|
||||
_handler = new SignalUpdateHandler(
|
||||
_observationRepositoryMock.Object,
|
||||
_gateMock.Object,
|
||||
_eventPublisherMock.Object,
|
||||
NullLogger<SignalUpdateHandler>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WithNoAffectedObservations_CompletesWithoutError()
|
||||
{
|
||||
// Arrange
|
||||
var evt = CreateSignalUpdatedEvent(DeterminizationEventTypes.EpssUpdated);
|
||||
_observationRepositoryMock
|
||||
.Setup(r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<CveObservation>());
|
||||
|
||||
// Act
|
||||
var act = async () => await _handler.HandleAsync(evt);
|
||||
|
||||
// Assert
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WithAffectedObservations_ProcessesEachObservation()
|
||||
{
|
||||
// Arrange
|
||||
var evt = CreateSignalUpdatedEvent(DeterminizationEventTypes.VexUpdated);
|
||||
var observations = new[]
|
||||
{
|
||||
CreateObservation(ObservationState.PendingDeterminization),
|
||||
CreateObservation(ObservationState.PendingDeterminization)
|
||||
};
|
||||
|
||||
_observationRepositoryMock
|
||||
.Setup(r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(observations);
|
||||
|
||||
// Act
|
||||
await _handler.HandleAsync(evt);
|
||||
|
||||
// Assert
|
||||
_observationRepositoryMock.Verify(
|
||||
r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WithException_ContinuesProcessingOtherObservations()
|
||||
{
|
||||
// Arrange
|
||||
var evt = CreateSignalUpdatedEvent(DeterminizationEventTypes.ReachabilityUpdated);
|
||||
var observations = new[]
|
||||
{
|
||||
CreateObservation(ObservationState.PendingDeterminization),
|
||||
CreateObservation(ObservationState.PendingDeterminization)
|
||||
};
|
||||
|
||||
_observationRepositoryMock
|
||||
.Setup(r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(observations);
|
||||
|
||||
// Act - should not throw even if internal processing has issues
|
||||
var act = async () => await _handler.HandleAsync(evt);
|
||||
|
||||
// Assert
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(DeterminizationEventTypes.EpssUpdated)]
|
||||
[InlineData(DeterminizationEventTypes.VexUpdated)]
|
||||
[InlineData(DeterminizationEventTypes.ReachabilityUpdated)]
|
||||
[InlineData(DeterminizationEventTypes.RuntimeUpdated)]
|
||||
[InlineData(DeterminizationEventTypes.BackportUpdated)]
|
||||
public async Task HandleAsync_SupportsAllEventTypes(string eventType)
|
||||
{
|
||||
// Arrange
|
||||
var evt = CreateSignalUpdatedEvent(eventType);
|
||||
_observationRepositoryMock
|
||||
.Setup(r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<CveObservation>());
|
||||
|
||||
// Act
|
||||
var act = async () => await _handler.HandleAsync(evt);
|
||||
|
||||
// Assert
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_RespectsCancellationToken()
|
||||
{
|
||||
// Arrange
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
var evt = CreateSignalUpdatedEvent(DeterminizationEventTypes.EpssUpdated);
|
||||
|
||||
_observationRepositoryMock
|
||||
.Setup(r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, cts.Token))
|
||||
.ThrowsAsync(new OperationCanceledException());
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(
|
||||
() => _handler.HandleAsync(evt, cts.Token));
|
||||
}
|
||||
|
||||
private static SignalUpdatedEvent CreateSignalUpdatedEvent(string eventType) =>
|
||||
new()
|
||||
{
|
||||
EventType = eventType,
|
||||
CveId = "CVE-2024-0001",
|
||||
Purl = "pkg:npm/test@1.0.0",
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Source = "TestSource"
|
||||
};
|
||||
|
||||
private static CveObservation CreateObservation(ObservationState state) =>
|
||||
new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CveId = "CVE-2024-0001",
|
||||
SubjectPurl = "pkg:npm/test@1.0.0",
|
||||
State = state,
|
||||
ObservedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user