up
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
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
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
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Engine.BatchContext;
|
||||
|
||||
internal sealed record BatchContextRequest(
|
||||
[property: JsonPropertyName("tenant_id")] string TenantId,
|
||||
[property: JsonPropertyName("policy_profile_hash")] string PolicyProfileHash,
|
||||
[property: JsonPropertyName("knobs_version")] string KnobsVersion,
|
||||
[property: JsonPropertyName("overlay_hash")] string OverlayHash,
|
||||
[property: JsonPropertyName("items")] IReadOnlyList<BatchContextItem> Items,
|
||||
[property: JsonPropertyName("options")] BatchContextOptions Options);
|
||||
|
||||
internal sealed record BatchContextItem(
|
||||
[property: JsonPropertyName("component_purl")] string ComponentPurl,
|
||||
[property: JsonPropertyName("advisory_id")] string AdvisoryId);
|
||||
|
||||
internal sealed record BatchContextOptions(
|
||||
[property: JsonPropertyName("include_reachability")] bool IncludeReachability);
|
||||
|
||||
internal sealed record BatchContextResponse(
|
||||
[property: JsonPropertyName("context_id")] string ContextId,
|
||||
[property: JsonPropertyName("expires_at")] string ExpiresAt,
|
||||
[property: JsonPropertyName("knobs_version")] string KnobsVersion,
|
||||
[property: JsonPropertyName("overlay_hash")] string OverlayHash,
|
||||
[property: JsonPropertyName("items")] IReadOnlyList<BatchContextResolvedItem> Items);
|
||||
|
||||
internal sealed record BatchContextResolvedItem(
|
||||
[property: JsonPropertyName("component_purl")] string ComponentPurl,
|
||||
[property: JsonPropertyName("advisory_id")] string AdvisoryId,
|
||||
[property: JsonPropertyName("status")] string Status,
|
||||
[property: JsonPropertyName("trace_ref")] string TraceRef);
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Policy.Engine.BatchContext;
|
||||
|
||||
/// <summary>
|
||||
/// Creates deterministic batch context responses for advisory AI (POLICY-ENGINE-31-002).
|
||||
/// </summary>
|
||||
internal sealed class BatchContextService
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public BatchContextService(TimeProvider timeProvider)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public BatchContextResponse Create(BatchContextRequest request)
|
||||
{
|
||||
if (request is null) throw new ArgumentNullException(nameof(request));
|
||||
if (request.Items is null || request.Items.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("items are required", nameof(request.Items));
|
||||
}
|
||||
|
||||
var sortedItems = request.Items
|
||||
.OrderBy(i => i.ComponentPurl, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.AdvisoryId, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var status = request.Options?.IncludeReachability == true ? "pending-reachability" : "pending";
|
||||
var resolved = sortedItems
|
||||
.Select(i => new BatchContextResolvedItem(
|
||||
i.ComponentPurl,
|
||||
i.AdvisoryId,
|
||||
status,
|
||||
ComputeTraceRef(request.TenantId, i)))
|
||||
.ToList();
|
||||
|
||||
var expires = _timeProvider.GetUtcNow().AddHours(1).ToString("O");
|
||||
var contextId = ComputeContextId(request, sortedItems);
|
||||
|
||||
return new BatchContextResponse(
|
||||
contextId,
|
||||
ExpiresAt: expires,
|
||||
KnobsVersion: request.KnobsVersion,
|
||||
OverlayHash: request.OverlayHash,
|
||||
Items: resolved);
|
||||
}
|
||||
|
||||
private static string ComputeContextId(BatchContextRequest request, IReadOnlyList<BatchContextItem> sortedItems)
|
||||
{
|
||||
var canonical = new
|
||||
{
|
||||
tenant = request.TenantId,
|
||||
profile = request.PolicyProfileHash,
|
||||
knobs = request.KnobsVersion,
|
||||
overlay = request.OverlayHash,
|
||||
items = sortedItems.Select(i => new { i.ComponentPurl, i.AdvisoryId }).ToArray(),
|
||||
includeReachability = request.Options?.IncludeReachability ?? false
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(canonical, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
});
|
||||
|
||||
Span<byte> hash = stackalloc byte[16];
|
||||
SHA256.HashData(Encoding.UTF8.GetBytes(json), hash);
|
||||
return Convert.ToHexString(hash);
|
||||
}
|
||||
|
||||
private static string ComputeTraceRef(string tenant, BatchContextItem item)
|
||||
{
|
||||
var stable = $"{tenant}|{item.ComponentPurl}|{item.AdvisoryId}";
|
||||
Span<byte> hash = stackalloc byte[12];
|
||||
SHA256.HashData(Encoding.UTF8.GetBytes(stable), hash);
|
||||
return Convert.ToHexString(hash);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user