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