98 lines
3.1 KiB
C#
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}";
|
|
}
|
|
}
|