Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized function calls.
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled

This commit is contained in:
master
2025-11-20 07:50:52 +02:00
parent 616ec73133
commit 10212d67c0
473 changed files with 316758 additions and 388 deletions

View File

@@ -0,0 +1,89 @@
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Notifier.Worker.Options;
using StellaOps.Notifier.Worker.Processing;
using Xunit;
namespace StellaOps.Notifier.Tests;
public class HttpEgressSloSinkTests
{
[Fact]
public async Task PublishAsync_NoWebhook_DoesNothing()
{
var handler = new StubHandler();
var sink = CreateSink(handler, new EgressSloOptions { Webhook = null });
await sink.PublishAsync(BuildContext(), CancellationToken.None);
Assert.Equal(0, handler.SendCount);
}
[Fact]
public async Task PublishAsync_SendsWebhookWithPayload()
{
var handler = new StubHandler();
var sink = CreateSink(handler, new EgressSloOptions { Webhook = "https://example.test/slo", TimeoutSeconds = 5 });
await sink.PublishAsync(BuildContext(), CancellationToken.None);
Assert.Equal(1, handler.SendCount);
var request = handler.LastRequest!;
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal("https://example.test/slo", request.RequestUri!.ToString());
}
private static HttpEgressSloSink CreateSink(HttpMessageHandler handler, EgressSloOptions options)
{
var factory = new StubHttpClientFactory(handler);
return new HttpEgressSloSink(factory, Options.Create(options), NullLogger<HttpEgressSloSink>.Instance);
}
private static EgressSloContext BuildContext()
{
var evt = Notify.Models.NotifyEvent.Create(
Guid.NewGuid(),
kind: "policy.violation",
tenant: "tenant-a",
ts: DateTimeOffset.UtcNow,
payload: new System.Text.Json.Nodes.JsonObject(),
actor: "tester",
version: "1");
var ctx = EgressSloContext.FromNotifyEvent(evt);
ctx.AddDelivery("Slack", "tmpl", evt.Kind);
return ctx;
}
private sealed class StubHandler : HttpMessageHandler
{
public int SendCount { get; private set; }
public HttpRequestMessage? LastRequest { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
SendCount++;
LastRequest = request;
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
}
}
private sealed class StubHttpClientFactory : IHttpClientFactory
{
private readonly HttpMessageHandler _handler;
public StubHttpClientFactory(HttpMessageHandler handler)
{
_handler = handler;
}
public HttpClient CreateClient(string name)
{
return new HttpClient(_handler, disposeHandler: false);
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Notifier.Worker.Processing;
namespace StellaOps.Notifier.Tests.Support;
public sealed class TestEgressSloSink : IEgressSloSink
{
private readonly ConcurrentBag<EgressSloContext> _contexts = new();
public IReadOnlyCollection<EgressSloContext> Contexts => _contexts;
public Task PublishAsync(EgressSloContext context, CancellationToken cancellationToken)
{
_contexts.Add(context);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,16 @@
namespace StellaOps.Notifier.Worker.Options;
public sealed class EgressSloOptions
{
/// <summary>
/// Webhook endpoint to receive SLO delivery signals. When null/empty, publishing is disabled.
/// </summary>
public string? Webhook { get; set; }
/// <summary>
/// Request timeout in seconds for the webhook call.
/// </summary>
public int TimeoutSeconds { get; set; } = 5;
public bool Enabled => !string.IsNullOrWhiteSpace(Webhook);
}

View File

@@ -0,0 +1,45 @@
using System.Collections.Generic;
using StellaOps.Notify.Models;
namespace StellaOps.Notifier.Worker.Processing;
/// <summary>
/// Tracks per-event delivery intents for SLO evaluation and webhook emission.
/// </summary>
internal sealed class EgressSloContext
{
private readonly List<EgressSloSignal> _signals = new();
public IReadOnlyList<EgressSloSignal> Signals => _signals;
public static EgressSloContext FromNotifyEvent(NotifyEvent notifyEvent)
{
ArgumentNullException.ThrowIfNull(notifyEvent);
return new EgressSloContext
{
EventId = notifyEvent.EventId,
TenantId = notifyEvent.Tenant,
EventKind = notifyEvent.Kind,
OccurredAt = notifyEvent.Ts
};
}
public Guid EventId { get; private set; }
public string TenantId { get; private set; } = string.Empty;
public string EventKind { get; private set; } = string.Empty;
public DateTimeOffset OccurredAt { get; private set; }
public void AddDelivery(string channelType, string template, string kind)
{
_signals.Add(new EgressSloSignal(channelType, template, kind, OccurredAt));
}
}
internal sealed record EgressSloSignal(
string Channel,
string Template,
string Kind,
DateTimeOffset OccurredAt);

View File

@@ -0,0 +1,93 @@
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Notifier.Worker.Options;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Notifier.Worker.Processing;
internal sealed class HttpEgressSloSink : IEgressSloSink
{
private readonly IHttpClientFactory _clientFactory;
private readonly EgressSloOptions _options;
private readonly ILogger<HttpEgressSloSink> _logger;
public HttpEgressSloSink(
IHttpClientFactory clientFactory,
IOptions<EgressSloOptions> options,
ILogger<HttpEgressSloSink> logger)
{
_clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task PublishAsync(EgressSloContext context, CancellationToken cancellationToken)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (!_options.Enabled)
{
return;
}
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
if (_options.TimeoutSeconds > 0)
{
linkedCts.CancelAfter(TimeSpan.FromSeconds(_options.TimeoutSeconds));
}
try
{
var client = _clientFactory.CreateClient("notifier-slo-webhook");
var payload = Map(context);
var response = await client.PostAsJsonAsync(_options.Webhook, payload, linkedCts.Token).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"SLO webhook returned non-success status {StatusCode} for event {EventId} (tenant {TenantId}).",
(int)response.StatusCode,
context.EventId,
context.TenantId);
}
}
catch (OperationCanceledException) when (linkedCts.IsCancellationRequested)
{
_logger.LogWarning(
"SLO webhook timed out after {TimeoutSeconds}s for event {EventId} (tenant {TenantId}).",
_options.TimeoutSeconds,
context.EventId,
context.TenantId);
}
catch (Exception ex)
{
_logger.LogWarning(
ex,
"Failed to publish SLO webhook for event {EventId} (tenant {TenantId}).",
context.EventId,
context.TenantId);
}
}
private static object Map(EgressSloContext context)
=> new
{
context.EventId,
context.TenantId,
context.EventKind,
context.OccurredAt,
deliveries = context.Signals.Select(signal => new
{
signal.Channel,
signal.Template,
signal.Kind,
signal.OccurredAt
})
};
}

View File

@@ -0,0 +1,17 @@
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Notifier.Worker.Processing;
internal interface IEgressSloSink
{
Task PublishAsync(EgressSloContext context, CancellationToken cancellationToken);
}
internal sealed class NullEgressSloSink : IEgressSloSink
{
public Task PublishAsync(EgressSloContext context, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,39 @@
# QA Playbook — Attestation Routing (NOTIFY-ATTEST-74-002)
## Goal
Verify attestation-related notification flows using the sample rules shipped in `docs/attestation-rules.sample.json`.
## Prereqs
- Notifier WebService + Worker running against a QA tenant.
- Channels configured for:
- `email-kms` (SMTP bridge)
- `webhook-kms` (internal hook)
- `slack-soc` (Slack webhook)
- `webhook-siem`
- Templates pre-seeded from `offline/notifier/templates/attestation/*.json`.
## Steps
1. Import rules and channels
- `POST /api/v1/notify/rules:batch` with `docs/attestation-rules.sample.json` (replace `<tenant-id>`).
- Verify rules are enabled.
2. Emit events
- Rotation: emit `authority.keys.rotated` with signer metadata and impacted tenants.
- Revocation: `authority.keys.revoked`.
- Transparency anomaly: `attestor.transparency.anomaly` and `attestor.transparency.witness.failed`.
3. Validate deliveries
- Confirm email + webhook for rotation/revocation (template `tmpl-attest-key-rotation`).
- Confirm slack + webhook for transparency anomaly (template `tmpl-attest-transparency-anomaly`).
- Check ledger/DB for rendered payloads with template keys and tenant id.
4. Negative checks
- Disabled channel should suppress delivery.
- Missing template should surface as rule error.
## Evidence to capture
- API responses for rule import and event POST.
- Delivery records (IDs, channel, template key) per event.
- Slack/email/webhook payload excerpts (hash or screenshot acceptable).
## Completion criteria
- All four event kinds produce expected channels per sample rules without errors.
- Ledger shows template IDs `tmpl-attest-key-rotation` and `tmpl-attest-transparency-anomaly`.
- Failures (if any) documented with event payload and channel.

View File

@@ -0,0 +1,86 @@
{
"rules": [
{
"ruleId": "attest-key-rotation",
"name": "Attestation key rotation/revocation",
"enabled": true,
"tenantId": "<tenant-id>",
"match": {
"eventKinds": [
"authority.keys.rotated",
"authority.keys.revoked"
]
},
"actions": [
{
"actionId": "email-kms",
"enabled": true,
"channel": "email-kms",
"template": "tmpl-attest-key-rotation"
},
{
"actionId": "webhook-kms",
"enabled": true,
"channel": "webhook-kms",
"template": "tmpl-attest-key-rotation"
}
]
},
{
"ruleId": "attest-transparency-anomaly",
"name": "Transparency witness anomaly",
"enabled": true,
"tenantId": "<tenant-id>",
"match": {
"eventKinds": [
"attestor.transparency.anomaly",
"attestor.transparency.witness.failed"
]
},
"actions": [
{
"actionId": "slack-soc",
"enabled": true,
"channel": "slack-soc",
"template": "tmpl-attest-transparency-anomaly"
},
{
"actionId": "webhook-siem",
"enabled": true,
"channel": "webhook-siem",
"template": "tmpl-attest-transparency-anomaly"
}
]
}
],
"channels": [
{
"channelId": "email-kms",
"type": "email",
"name": "KMS security",
"target": "kms-security@example.com",
"secretRef": "ref://notify/channels/email/kms-security"
},
{
"channelId": "webhook-kms",
"type": "webhook",
"name": "KMS webhook",
"endpoint": "https://hooks.internal/kms",
"secretRef": "ref://notify/channels/webhook/kms"
},
{
"channelId": "slack-soc",
"type": "slack",
"name": "SOC high-priority",
"endpoint": "https://hooks.slack.com/services/T000/B000/XYZ",
"secretRef": "ref://notify/channels/slack/soc"
},
{
"channelId": "webhook-siem",
"type": "webhook",
"name": "SIEM ingest",
"endpoint": "https://siem.example.internal/hooks/notifier",
"secretRef": "ref://notify/channels/webhook/siem"
}
]
}