Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Engine/Services/EvidenceSummaryService.cs
StellaOps Bot 909d9b6220
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
up
2025-12-01 21:16:22 +02:00

98 lines
3.1 KiB
C#

using System.Buffers.Binary;
using System.Security.Cryptography;
using System.Text;
using StellaOps.Policy.Engine.Domain;
namespace StellaOps.Policy.Engine.Services;
/// <summary>
/// Builds deterministic evidence summaries for API/SDK consumers.
/// </summary>
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<string> BuildSignals(EvidenceSummaryRequest request, string severity)
{
var signals = new List<string>(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}";
}
}