up the blokcing tasks
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-11 02:32:18 +02:00
parent 92bc4d3a07
commit 49922dff5a
474 changed files with 76071 additions and 12411 deletions

View File

@@ -67,4 +67,18 @@ internal interface INotifyClient
Task<NotifySendResult> SendAsync(
NotifySendRequest request,
CancellationToken cancellationToken);
/// <summary>
/// Simulate rule evaluation.
/// </summary>
Task<NotifySimulationResult> SimulateAsync(
NotifySimulationRequest request,
CancellationToken cancellationToken);
/// <summary>
/// Acknowledge an incident or signed token.
/// </summary>
Task<NotifyAckResult> AckAsync(
NotifyAckRequest request,
CancellationToken cancellationToken);
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
@@ -610,3 +611,83 @@ internal sealed class NotifySendResult
[JsonPropertyName("idempotencyKey")]
public string? IdempotencyKey { get; init; }
}
internal sealed class NotifySimulationRequest
{
[JsonPropertyName("tenantId")]
public string? TenantId { get; init; }
[JsonPropertyName("events")]
public JsonElement? Events { get; init; }
[JsonPropertyName("rules")]
public JsonElement? Rules { get; init; }
[JsonPropertyName("enabledRulesOnly")]
public bool? EnabledRulesOnly { get; init; }
[JsonPropertyName("historicalLookbackMinutes")]
public int? HistoricalLookbackMinutes { get; init; }
[JsonPropertyName("maxEvents")]
public int? MaxEvents { get; init; }
[JsonPropertyName("eventKindFilter")]
public string? EventKindFilter { get; init; }
[JsonPropertyName("includeNonMatches")]
public bool? IncludeNonMatches { get; init; }
}
internal sealed class NotifySimulationResult
{
[JsonPropertyName("simulationId")]
public string? SimulationId { get; init; }
[JsonPropertyName("totalEvents")]
public int? TotalEvents { get; init; }
[JsonPropertyName("totalRules")]
public int? TotalRules { get; init; }
[JsonPropertyName("matchedEvents")]
public int? MatchedEvents { get; init; }
[JsonPropertyName("totalActionsTriggered")]
public int? TotalActionsTriggered { get; init; }
[JsonPropertyName("durationMs")]
public double? DurationMs { get; init; }
}
internal sealed class NotifyAckRequest
{
[JsonPropertyName("tenantId")]
public string? TenantId { get; init; }
[JsonPropertyName("incidentId")]
public string? IncidentId { get; init; }
[JsonPropertyName("acknowledgedBy")]
public string? AcknowledgedBy { get; init; }
[JsonPropertyName("comment")]
public string? Comment { get; init; }
public string? Token { get; init; }
}
internal sealed class NotifyAckResult
{
[JsonPropertyName("success")]
public bool Success { get; init; }
[JsonPropertyName("incidentId")]
public string? IncidentId { get; init; }
[JsonPropertyName("error")]
public string? Error { get; init; }
[JsonPropertyName("message")]
public string? Message { get; init; }
}

View File

@@ -569,6 +569,131 @@ internal sealed class NotifyClient : INotifyClient
}
}
public async Task<NotifySimulationResult> SimulateAsync(
NotifySimulationRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
EnsureConfigured();
var json = JsonSerializer.Serialize(request, SerializerOptions);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v2/simulate")
{
Content = content
};
if (!string.IsNullOrWhiteSpace(request.TenantId))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", request.TenantId);
}
await AuthorizeRequestAsync(httpRequest, "notify.simulate", cancellationToken).ConfigureAwait(false);
using var response = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError(
"Failed to simulate notify rules (status {StatusCode}). Response: {Payload}",
(int)response.StatusCode,
string.IsNullOrWhiteSpace(payload) ? "<empty>" : payload);
return new NotifySimulationResult { SimulationId = null, TotalEvents = 0, TotalRules = 0 };
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var result = await JsonSerializer
.DeserializeAsync<NotifySimulationResult>(stream, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
return result ?? new NotifySimulationResult { SimulationId = null, TotalEvents = 0, TotalRules = 0 };
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "HTTP error while simulating notify rules");
return new NotifySimulationResult { SimulationId = null, TotalEvents = 0, TotalRules = 0 };
}
catch (TaskCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
logger.LogError(ex, "Request timed out while simulating notify rules");
return new NotifySimulationResult { SimulationId = null, TotalEvents = 0, TotalRules = 0 };
}
}
public async Task<NotifyAckResult> AckAsync(
NotifyAckRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
EnsureConfigured();
var hasToken = !string.IsNullOrWhiteSpace(request.Token);
using var httpRequest = hasToken
? new HttpRequestMessage(HttpMethod.Get, $"/api/v2/ack?token={Uri.EscapeDataString(request.Token!)}")
: new HttpRequestMessage(HttpMethod.Post, "/api/v2/ack")
{
Content = new StringContent(JsonSerializer.Serialize(new AckApiRequestBody
{
TenantId = request.TenantId,
IncidentId = request.IncidentId,
AcknowledgedBy = request.AcknowledgedBy,
Comment = request.Comment
}, SerializerOptions), Encoding.UTF8, "application/json")
};
if (!string.IsNullOrWhiteSpace(request.TenantId))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", request.TenantId);
}
await AuthorizeRequestAsync(httpRequest, "notify.write", cancellationToken).ConfigureAwait(false);
using var response = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError(
"Failed to acknowledge notification (status {StatusCode}). Response: {Payload}",
(int)response.StatusCode,
string.IsNullOrWhiteSpace(payload) ? "<empty>" : payload);
return new NotifyAckResult { Success = false, IncidentId = request.IncidentId, Error = payload };
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var result = await JsonSerializer
.DeserializeAsync<NotifyAckResult>(stream, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
return result ?? new NotifyAckResult { Success = true, IncidentId = request.IncidentId };
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "HTTP error while acknowledging notification");
return new NotifyAckResult { Success = false, IncidentId = request.IncidentId, Error = ex.Message };
}
catch (TaskCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
logger.LogError(ex, "Request timed out while acknowledging notification");
return new NotifyAckResult { Success = false, IncidentId = request.IncidentId, Error = "Request timed out" };
}
}
private sealed record AckApiRequestBody
{
public string? TenantId { get; init; }
public string? IncidentId { get; init; }
public string? AcknowledgedBy { get; init; }
public string? Comment { get; init; }
}
private static string BuildChannelListUri(NotifyChannelListRequest request)
{
var queryParams = new List<string>();