Sidebar 5-group restructure + demo data badges + audit emission infrastructure
Sprint 4 — Sidebar restructure (S4-T01+T02):
5 groups: Release Control, Security, Operations, Audit & Evidence, Setup & Admin
Groups 4+5 collapsed by default for new users
Operations extracted from Release Control into own group
Audit extracted from Security into own group
groupOrder and resolveMenuGroupLabel updated
Approvals badge moved to section-level
Sprint 2 — Demo data badges (S2-T04+T05):
Backend: isDemo=true on all compatibility/seed responses in
PackAdapterEndpoints, QuotaCompatibilityEndpoints, VulnerabilitiesController
Frontend: "(Demo)" badges on Usage & Limits page quotas
Frontend: "(Demo)" badges on triage artifact list when seed data
New PlatformItemResponse/PlatformListResponse with IsDemo field
Sprint 6 — Audit emission infrastructure (S6-T01+T02):
New shared library: src/__Libraries/StellaOps.Audit.Emission/
- AuditActionAttribute: [AuditAction("module", "action")] endpoint tag
- AuditActionFilter: IEndpointFilter that auto-emits UnifiedAuditEvent
- HttpAuditEventEmitter: POSTs to Timeline /api/v1/audit/ingest
- Single-line DI: services.AddAuditEmission(configuration)
Timeline service: POST /api/v1/audit/ingest ingestion endpoint
- IngestAuditEventStore: 10k-event ring buffer
- CompositeUnifiedAuditEventProvider: merges HTTP-polled + ingested
Documentation: docs/modules/audit/AUDIT_EMISSION_GUIDE.md
Angular build: 0 errors. .NET builds: 0 errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Audit.Emission;
|
||||
|
||||
/// <summary>
|
||||
/// Sends audit events to the Timeline service's <c>POST /api/v1/audit/ingest</c> endpoint.
|
||||
/// Failures are logged but never propagated -- audit emission must not block the calling service.
|
||||
/// </summary>
|
||||
public sealed class HttpAuditEventEmitter : IAuditEventEmitter
|
||||
{
|
||||
/// <summary>
|
||||
/// Named HTTP client identifier used for DI registration.
|
||||
/// </summary>
|
||||
public const string HttpClientName = "StellaOps.AuditEmission";
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IOptions<AuditEmissionOptions> _options;
|
||||
private readonly ILogger<HttpAuditEventEmitter> _logger;
|
||||
|
||||
public HttpAuditEventEmitter(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IOptions<AuditEmissionOptions> options,
|
||||
ILogger<HttpAuditEventEmitter> logger)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task EmitAsync(AuditEventPayload auditEvent, CancellationToken cancellationToken)
|
||||
{
|
||||
var options = _options.Value;
|
||||
if (!options.Enabled)
|
||||
{
|
||||
_logger.LogDebug("Audit emission is disabled; skipping event {EventId}", auditEvent.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient(HttpClientName);
|
||||
var uri = new Uri(new Uri(options.TimelineBaseUrl), "/api/v1/audit/ingest");
|
||||
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
uri,
|
||||
auditEvent,
|
||||
SerializerOptions,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Audit ingest returned HTTP {StatusCode} for event {EventId}",
|
||||
(int)response.StatusCode,
|
||||
auditEvent.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Audit event {EventId} emitted successfully ({Module}.{Action})",
|
||||
auditEvent.Id,
|
||||
auditEvent.Module,
|
||||
auditEvent.Action);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogWarning("Audit emission timed out for event {EventId}", auditEvent.Id);
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or JsonException)
|
||||
{
|
||||
_logger.LogWarning(ex, "Audit emission failed for event {EventId}", auditEvent.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user