stabilize tests
This commit is contained in:
24
src/Notifier/AGENTS.md
Normal file
24
src/Notifier/AGENTS.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Notifier Module
|
||||
|
||||
## Working Directory
|
||||
- `src/Notifier/**` (WebService, Worker, tests).
|
||||
|
||||
## Required Reading
|
||||
- `docs/README.md`
|
||||
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
- `docs/modules/notifier/README.md`
|
||||
- `docs/modules/notify/architecture.md`
|
||||
- `docs/modules/notify/security/redaction-catalog.md`
|
||||
|
||||
## Engineering Rules
|
||||
- Deterministic delivery behavior; stable ordering for notifications.
|
||||
- Enforce redaction defaults and avoid secrets in logs.
|
||||
- Offline-first; no external network calls in tests.
|
||||
|
||||
## Testing & Verification
|
||||
- Tests live in `src/Notifier/StellaOps.Notifier/StellaOps.Notifier.Tests` and `src/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService.Tests`.
|
||||
- Cover rule evaluation, delivery pipeline, and audit logging.
|
||||
|
||||
## Sprint Discipline
|
||||
- Record delivery workflow changes in sprint Decisions & Risks.
|
||||
@@ -9,10 +9,8 @@ namespace StellaOps.Notifier.Tests;
|
||||
|
||||
public sealed class AttestationTemplateSeederTests
|
||||
{
|
||||
private const string SkipReason = "Offline bundle files not yet created in offline/notifier/";
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact(Skip = SkipReason)]
|
||||
[Fact]
|
||||
public async Task SeedTemplates_and_routing_load_from_offline_bundle()
|
||||
{
|
||||
var templateRepo = new InMemoryTemplateRepository();
|
||||
@@ -52,8 +50,9 @@ public sealed class AttestationTemplateSeederTests
|
||||
var directory = AppContext.BaseDirectory;
|
||||
while (directory != null)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(directory, "offline", "notifier")) ||
|
||||
File.Exists(Path.Combine(directory, "StellaOps.sln")))
|
||||
var manifest = Path.Combine(directory, "offline", "notifier", "notify-kit.manifest.json");
|
||||
var notifierDocs = Path.Combine(directory, "src", "Notifier", "StellaOps.Notifier", "StellaOps.Notifier.docs");
|
||||
if (File.Exists(manifest) || Directory.Exists(notifierDocs))
|
||||
{
|
||||
return directory;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ public sealed class ArtifactHashesTests
|
||||
{
|
||||
private static string RepoRoot => Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../../../"));
|
||||
|
||||
private const string SkipReason = "Offline kit files not yet created in offline/notifier/";
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
[Fact]
|
||||
public void ArtifactHashesHasNoTbdAndFilesExist()
|
||||
{
|
||||
var hashesPath = Path.Combine(RepoRoot, "offline/notifier/artifact-hashes.json");
|
||||
|
||||
@@ -6,7 +6,15 @@
|
||||
// Note: These tests verify the full notification pipeline for identity alerts.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Templates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notifier.Tests.Contracts;
|
||||
@@ -17,6 +25,11 @@ namespace StellaOps.Notifier.Tests.Contracts;
|
||||
/// </summary>
|
||||
public sealed class IdentityAlertNotificationTests
|
||||
{
|
||||
private static readonly Guid EventId = Guid.Parse("00000000-0000-0000-0000-0000000000a1");
|
||||
private const string OccurredAtUtc = "2026-01-15T12:30:00Z";
|
||||
private const string RekorIntegratedAtUtc = "2026-01-15T12:25:00Z";
|
||||
private const string TenantId = "tenant-01";
|
||||
|
||||
[Fact]
|
||||
public void IdentityMatchedTemplate_ContainsRequiredVariables()
|
||||
{
|
||||
@@ -51,52 +64,136 @@ public sealed class IdentityAlertNotificationTests
|
||||
routingRuleEventKinds.Should().Contain(eventKind);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires full notification pipeline. Run in integration environment.")]
|
||||
public async Task EndToEnd_IdentityAlertEvent_RendersSlackMessage()
|
||||
[Fact]
|
||||
public async Task IdentityAlertEvent_RendersSlackMessage()
|
||||
{
|
||||
// This test verifies the full flow:
|
||||
// 1. Create IdentityAlertEvent
|
||||
// 2. Route through notification rules
|
||||
// 3. Select identity-matched template
|
||||
// 4. Render Slack message
|
||||
// 5. Verify output format
|
||||
var template = LoadTemplate("identity-matched.slack.template.json");
|
||||
var renderer = CreateRenderer();
|
||||
var result = await renderer.RenderAsync(
|
||||
template,
|
||||
CreateIdentityEvent("critical", suppressedCount: 2),
|
||||
CancellationToken.None);
|
||||
|
||||
await Task.CompletedTask;
|
||||
result.Body.Should().Contain(":rotating_light:");
|
||||
result.Body.Should().Contain("Identity Watchlist Alert");
|
||||
result.Body.Should().Contain("Production Signing Key");
|
||||
result.Body.Should().Contain("rekor-uuid-1234");
|
||||
result.Body.Should().Contain(EventId.ToString());
|
||||
result.Body.Should().Contain("2 duplicate alerts suppressed");
|
||||
result.Body.Should().NotContain(":warning:");
|
||||
result.Body.Should().NotContain(":information_source:");
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires full notification pipeline. Run in integration environment.")]
|
||||
public async Task EndToEnd_IdentityAlertEvent_RendersEmailMessage()
|
||||
[Fact]
|
||||
public async Task IdentityAlertEvent_RendersEmailMessage()
|
||||
{
|
||||
// Verify email template rendering
|
||||
await Task.CompletedTask;
|
||||
var template = LoadTemplate("identity-matched.email.template.json");
|
||||
var renderer = CreateRenderer();
|
||||
var result = await renderer.RenderAsync(
|
||||
template,
|
||||
CreateIdentityEvent("warning", suppressedCount: 0),
|
||||
CancellationToken.None);
|
||||
|
||||
result.Subject.Should().NotBeNull();
|
||||
result.Subject.Should().Contain("[WARNING]");
|
||||
result.Body.Should().Contain("Identity Watchlist Alert");
|
||||
result.Body.Should().Contain("Production Signing Key");
|
||||
result.Body.Should().Contain(EventId.ToString());
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires full notification pipeline. Run in integration environment.")]
|
||||
public async Task EndToEnd_IdentityAlertEvent_RendersWebhookPayload()
|
||||
[Fact]
|
||||
public async Task IdentityAlertEvent_RendersWebhookPayload()
|
||||
{
|
||||
// Verify webhook payload rendering
|
||||
await Task.CompletedTask;
|
||||
var template = LoadTemplate("identity-matched.webhook.template.json");
|
||||
var renderer = CreateRenderer();
|
||||
var result = await renderer.RenderAsync(
|
||||
template,
|
||||
CreateIdentityEvent("info", suppressedCount: 1),
|
||||
CancellationToken.None);
|
||||
|
||||
using var document = JsonDocument.Parse(result.Body);
|
||||
var root = document.RootElement;
|
||||
|
||||
root.GetProperty("alertType").GetString().Should().Be("identity-watchlist-match");
|
||||
root.GetProperty("severity").GetString().Should().Be("info");
|
||||
root.GetProperty("watchlist").GetProperty("entryName").GetString()
|
||||
.Should().Be("Production Signing Key");
|
||||
root.GetProperty("rekorEntry").GetProperty("uuid").GetString()
|
||||
.Should().Be("rekor-uuid-1234");
|
||||
root.GetProperty("eventId").GetString().Should().Be(EventId.ToString());
|
||||
root.GetProperty("suppressedCount").GetInt32().Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires full notification pipeline. Run in integration environment.")]
|
||||
public async Task EndToEnd_IdentityAlertEvent_RendersTeamsCard()
|
||||
[Fact]
|
||||
public async Task IdentityAlertEvent_RendersTeamsCard()
|
||||
{
|
||||
// Verify Teams adaptive card rendering
|
||||
await Task.CompletedTask;
|
||||
var template = LoadTemplate("identity-matched.teams.template.json");
|
||||
var renderer = CreateRenderer();
|
||||
var result = await renderer.RenderAsync(
|
||||
template,
|
||||
CreateIdentityEvent("critical", suppressedCount: 1),
|
||||
CancellationToken.None);
|
||||
|
||||
using var document = JsonDocument.Parse(result.Body);
|
||||
var root = document.RootElement;
|
||||
root.TryGetProperty("attachments", out var attachments).Should().BeTrue();
|
||||
attachments.GetArrayLength().Should().BeGreaterThan(0);
|
||||
|
||||
var content = attachments.EnumerateArray().First().GetProperty("content");
|
||||
var header = content.GetProperty("body").EnumerateArray().First();
|
||||
header.GetProperty("color").GetString().Should().Be("attention");
|
||||
result.Body.Should().Contain("Production Signing Key");
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires full notification pipeline. Run in integration environment.")]
|
||||
public async Task EndToEnd_SeverityRouting_CriticalAlertUsesCorrectChannel()
|
||||
[Fact]
|
||||
public async Task SeverityRouting_CriticalAlertUsesCorrectChannel()
|
||||
{
|
||||
// Verify that Critical severity alerts route to high-priority channels
|
||||
await Task.CompletedTask;
|
||||
var template = LoadTemplate("identity-matched.slack.template.json");
|
||||
var renderer = CreateRenderer();
|
||||
var result = await renderer.RenderAsync(
|
||||
template,
|
||||
CreateIdentityEvent("warning", suppressedCount: 0),
|
||||
CancellationToken.None);
|
||||
|
||||
result.Body.Should().Contain(":warning:");
|
||||
result.Body.Should().NotContain(":rotating_light:");
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires full notification pipeline. Run in integration environment.")]
|
||||
public async Task EndToEnd_ChannelOverrides_UsesEntrySpecificChannels()
|
||||
[Fact]
|
||||
public async Task ChannelOverrides_UsesEntrySpecificChannels()
|
||||
{
|
||||
// Verify that channelOverrides from watchlist entry are respected
|
||||
await Task.CompletedTask;
|
||||
var template = NotifyTemplate.Create(
|
||||
templateId: "identity-channel-overrides",
|
||||
tenantId: TenantId,
|
||||
channelType: NotifyChannelType.Slack,
|
||||
key: "identity.channelOverrides",
|
||||
locale: "en-us",
|
||||
body: "Overrides: {{#each event.channelOverrides}}{{@channelType}}={{@channelName}};{{/each}}",
|
||||
renderMode: NotifyTemplateRenderMode.PlainText,
|
||||
format: NotifyDeliveryFormat.PlainText);
|
||||
|
||||
var channelOverrides = new JsonArray
|
||||
{
|
||||
new JsonObject
|
||||
{
|
||||
["channelType"] = "Slack",
|
||||
["channelName"] = "sec-alerts"
|
||||
},
|
||||
new JsonObject
|
||||
{
|
||||
["channelType"] = "Email",
|
||||
["channelName"] = "soc"
|
||||
}
|
||||
};
|
||||
|
||||
var renderer = CreateRenderer();
|
||||
var result = await renderer.RenderAsync(
|
||||
template,
|
||||
CreateIdentityEvent("info", suppressedCount: 0, channelOverrides: channelOverrides),
|
||||
CancellationToken.None);
|
||||
|
||||
result.Body.Should().Contain("Slack=sec-alerts;");
|
||||
result.Body.Should().Contain("Email=soc;");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -118,8 +215,6 @@ public sealed class IdentityAlertNotificationTests
|
||||
[Fact]
|
||||
public void TemplateFilesExist_AllChannelTypes()
|
||||
{
|
||||
// Verify that templates exist for all required channel types
|
||||
// This is a documentation test - actual file existence is verified elsewhere
|
||||
var requiredTemplates = new[]
|
||||
{
|
||||
"identity-matched.slack.template.json",
|
||||
@@ -128,7 +223,12 @@ public sealed class IdentityAlertNotificationTests
|
||||
"identity-matched.teams.template.json"
|
||||
};
|
||||
|
||||
requiredTemplates.Should().HaveCount(4);
|
||||
var directory = LocateTemplateDirectory();
|
||||
foreach (var template in requiredTemplates)
|
||||
{
|
||||
var path = Path.Combine(directory, template);
|
||||
File.Exists(path).Should().BeTrue($"Expected template at {path}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -151,4 +251,149 @@ public sealed class IdentityAlertNotificationTests
|
||||
|
||||
webhookFields.Should().HaveCountGreaterThanOrEqualTo(10);
|
||||
}
|
||||
|
||||
private static EnhancedTemplateRenderer CreateRenderer()
|
||||
{
|
||||
var templateService = new StubTemplateService(new TemplateRedactionConfig
|
||||
{
|
||||
AllowedFields = [],
|
||||
DeniedFields = [],
|
||||
Mode = "none"
|
||||
});
|
||||
|
||||
return new EnhancedTemplateRenderer(
|
||||
templateService,
|
||||
Options.Create(new TemplateRendererOptions()),
|
||||
NullLogger<EnhancedTemplateRenderer>.Instance);
|
||||
}
|
||||
|
||||
private static NotifyEvent CreateIdentityEvent(
|
||||
string severity,
|
||||
int suppressedCount,
|
||||
JsonArray? channelOverrides = null)
|
||||
{
|
||||
var payload = BuildIdentityPayload(severity, suppressedCount, channelOverrides);
|
||||
var timestamp = DateTimeOffset.Parse(
|
||||
OccurredAtUtc,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
|
||||
|
||||
return NotifyEvent.Create(
|
||||
EventId,
|
||||
"attestor.identity.matched",
|
||||
TenantId,
|
||||
timestamp,
|
||||
payload,
|
||||
actor: "attestor");
|
||||
}
|
||||
|
||||
private static JsonObject BuildIdentityPayload(
|
||||
string severity,
|
||||
int suppressedCount,
|
||||
JsonArray? channelOverrides)
|
||||
{
|
||||
var identityEvent = new JsonObject
|
||||
{
|
||||
["eventId"] = EventId.ToString(),
|
||||
["watchlistEntryId"] = "wl-entry-01",
|
||||
["watchlistEntryName"] = "Production Signing Key",
|
||||
["severity"] = severity,
|
||||
["occurredAtUtc"] = OccurredAtUtc,
|
||||
["suppressedCount"] = suppressedCount,
|
||||
["matchedIdentity"] = new JsonObject
|
||||
{
|
||||
["issuer"] = "https://authority.stellaops.local",
|
||||
["subjectAlternativeName"] = "spiffe://prod/app",
|
||||
["keyId"] = "key-123"
|
||||
},
|
||||
["rekorEntry"] = new JsonObject
|
||||
{
|
||||
["uuid"] = "rekor-uuid-1234",
|
||||
["logIndex"] = 42,
|
||||
["artifactSha256"] = "sha256:abc123",
|
||||
["integratedTimeUtc"] = RekorIntegratedAtUtc
|
||||
}
|
||||
};
|
||||
|
||||
if (channelOverrides is not null)
|
||||
{
|
||||
identityEvent["channelOverrides"] = channelOverrides;
|
||||
}
|
||||
|
||||
return new JsonObject
|
||||
{
|
||||
["event"] = identityEvent
|
||||
};
|
||||
}
|
||||
|
||||
private static NotifyTemplate LoadTemplate(string fileName)
|
||||
{
|
||||
var directory = LocateTemplateDirectory();
|
||||
var path = Path.Combine(directory, fileName);
|
||||
var json = File.ReadAllText(path);
|
||||
return NotifyCanonicalJsonSerializer.Deserialize<NotifyTemplate>(json);
|
||||
}
|
||||
|
||||
private static string LocateTemplateDirectory()
|
||||
{
|
||||
var directory = AppContext.BaseDirectory;
|
||||
string? fallback = null;
|
||||
while (directory != null)
|
||||
{
|
||||
var offlineRoot = Path.Combine(directory, "offline", "notifier");
|
||||
var candidate = Path.Combine(offlineRoot, "templates", "attestation");
|
||||
|
||||
if (Directory.Exists(candidate))
|
||||
{
|
||||
fallback ??= candidate;
|
||||
|
||||
// Prefer the repo's offline kit (manifest present) to avoid stale bin copies.
|
||||
var manifest = Path.Combine(offlineRoot, "notify-kit.manifest.json");
|
||||
if (File.Exists(manifest))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
directory = Directory.GetParent(directory)?.FullName;
|
||||
}
|
||||
|
||||
if (fallback is not null)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unable to locate notifier template directory.");
|
||||
}
|
||||
|
||||
private sealed class StubTemplateService : INotifyTemplateService
|
||||
{
|
||||
private readonly TemplateRedactionConfig _config;
|
||||
|
||||
public StubTemplateService(TemplateRedactionConfig config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public Task<NotifyTemplate?> ResolveAsync(string tenantId, string key, NotifyChannelType channelType, string locale, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<NotifyTemplate?>(null);
|
||||
|
||||
public Task<NotifyTemplate?> GetByIdAsync(string tenantId, string templateId, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<NotifyTemplate?>(null);
|
||||
|
||||
public Task<TemplateUpsertResult> UpsertAsync(NotifyTemplate template, string actor, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(TemplateUpsertResult.Created(template.TemplateId));
|
||||
|
||||
public Task<bool> DeleteAsync(string tenantId, string templateId, string actor, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task<IReadOnlyList<NotifyTemplate>> ListAsync(string tenantId, TemplateListOptions? options = null, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<IReadOnlyList<NotifyTemplate>>([]);
|
||||
|
||||
public TemplateValidationResult Validate(string templateBody)
|
||||
=> TemplateValidationResult.Valid();
|
||||
|
||||
public TemplateRedactionConfig GetRedactionConfig(NotifyTemplate template)
|
||||
=> _config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ public sealed class OfflineKitManifestTests
|
||||
{
|
||||
private static string RepoRoot => Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../../../"));
|
||||
|
||||
private const string SkipReason = "Offline kit files not yet created in offline/notifier/";
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
[Fact]
|
||||
public void ManifestDssePayloadMatchesManifest()
|
||||
{
|
||||
var manifestPath = Path.Combine(RepoRoot, "offline/notifier/notify-kit.manifest.json");
|
||||
@@ -25,7 +23,7 @@ public sealed class OfflineKitManifestTests
|
||||
Assert.True(JsonElement.DeepEquals(payload.RootElement, manifest.RootElement));
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
[Fact]
|
||||
public void ManifestArtifactsHaveHashes()
|
||||
{
|
||||
var manifestPath = Path.Combine(RepoRoot, "offline/notifier/notify-kit.manifest.json");
|
||||
|
||||
@@ -8,9 +8,7 @@ public sealed class RenderingDeterminismTests
|
||||
{
|
||||
private static string RepoRoot => Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../../../"));
|
||||
|
||||
private const string SkipReason = "Fixture files not yet created in docs/notifications/fixtures/rendering/";
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
[Fact]
|
||||
public void RenderingIndexMatchesTemplates()
|
||||
{
|
||||
var indexPath = Path.Combine(RepoRoot, "docs/notifications/fixtures/rendering/index.ndjson");
|
||||
|
||||
@@ -8,9 +8,7 @@ public sealed class SchemaCatalogTests
|
||||
{
|
||||
private static string RepoRoot => Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../../../"));
|
||||
|
||||
private const string SkipReason = "Schema catalog files not yet created in docs/notifications/schemas/";
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
[Fact]
|
||||
public void CatalogMatchesDssePayload()
|
||||
{
|
||||
var catalogPath = Path.Combine(RepoRoot, "docs/notifications/schemas/notify-schemas-catalog.json");
|
||||
@@ -37,7 +35,7 @@ public sealed class SchemaCatalogTests
|
||||
Assert.True(text.IndexOf("TBD", StringComparison.OrdinalIgnoreCase) < 0);
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
[Fact]
|
||||
public void InputsLockAlignsWithCatalog()
|
||||
{
|
||||
var catalogPath = Path.Combine(RepoRoot, "docs/notifications/schemas/notify-schemas-catalog.json");
|
||||
|
||||
@@ -132,7 +132,8 @@ public sealed class DeliveryRetryEndpointTests : IClassFixture<NotifierApplicati
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.Equal("delivery-failed", result.GetProperty("deliveryId").GetString());
|
||||
Assert.Equal("Pending", result.GetProperty("status").GetString());
|
||||
Assert.True(result.GetProperty("retried").GetBoolean());
|
||||
Assert.Equal(1, result.GetProperty("newAttemptNumber").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -169,7 +170,7 @@ public sealed class DeliveryRetryEndpointTests : IClassFixture<NotifierApplicati
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.Equal(initialAttempts + 1, result.GetProperty("attemptCount").GetInt32());
|
||||
Assert.Equal(initialAttempts + 1, result.GetProperty("newAttemptNumber").GetInt32());
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -202,10 +203,10 @@ public sealed class DeliveryRetryEndpointTests : IClassFixture<NotifierApplicati
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.Equal(0, result.GetProperty("total").GetInt32());
|
||||
Assert.Equal(0, result.GetProperty("sent").GetInt32());
|
||||
Assert.Equal(0, result.GetProperty("failed").GetInt32());
|
||||
Assert.Equal(0, result.GetProperty("pending").GetInt32());
|
||||
Assert.Equal(0, result.GetProperty("totalSent").GetInt32());
|
||||
Assert.Equal(0, result.GetProperty("totalFailed").GetInt32());
|
||||
Assert.Equal(0, result.GetProperty("totalPending").GetInt32());
|
||||
Assert.Equal(0, result.GetProperty("totalThrottled").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -226,10 +227,10 @@ public sealed class DeliveryRetryEndpointTests : IClassFixture<NotifierApplicati
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.Equal(5, result.GetProperty("total").GetInt32());
|
||||
Assert.Equal(3, result.GetProperty("sent").GetInt32()); // Sent + Delivered
|
||||
Assert.Equal(1, result.GetProperty("failed").GetInt32());
|
||||
Assert.Equal(1, result.GetProperty("pending").GetInt32());
|
||||
Assert.Equal(3, result.GetProperty("totalSent").GetInt32()); // Sent + Delivered
|
||||
Assert.Equal(1, result.GetProperty("totalFailed").GetInt32());
|
||||
Assert.Equal(1, result.GetProperty("totalPending").GetInt32());
|
||||
Assert.Equal(0, result.GetProperty("totalThrottled").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -249,8 +250,8 @@ public sealed class DeliveryRetryEndpointTests : IClassFixture<NotifierApplicati
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
var byChannel = result.GetProperty("byChannel");
|
||||
Assert.Equal(2, byChannel.GetProperty("slack").GetInt32());
|
||||
Assert.Equal(1, byChannel.GetProperty("email").GetInt32());
|
||||
Assert.Equal(2, byChannel.GetProperty("slack").GetProperty("sent").GetInt32());
|
||||
Assert.Equal(1, byChannel.GetProperty("email").GetProperty("sent").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -270,8 +271,8 @@ public sealed class DeliveryRetryEndpointTests : IClassFixture<NotifierApplicati
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
var byEventKind = result.GetProperty("byEventKind");
|
||||
Assert.Equal(2, byEventKind.GetProperty("finding.created").GetInt32());
|
||||
Assert.Equal(1, byEventKind.GetProperty("policy.promoted").GetInt32());
|
||||
Assert.Equal(2, byEventKind.GetProperty("finding.created").GetProperty("sent").GetInt32());
|
||||
Assert.Equal(1, byEventKind.GetProperty("policy.promoted").GetProperty("sent").GetInt32());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using StellaOps.Notifier.Tests.Support;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Notifier.Tests;
|
||||
@@ -19,17 +19,23 @@ public sealed class OpenApiEndpointTests : IClassFixture<NotifierApplicationFact
|
||||
_packRepo = factory.PackRepo;
|
||||
}
|
||||
|
||||
#if false // disabled until test host wiring stabilises
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
[Fact]
|
||||
public async Task OpenApi_endpoint_serves_yaml_with_scope_header()
|
||||
{
|
||||
var response = await _client.GetAsync("/.well-known/openapi", CancellationToken.None);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
#endif
|
||||
|
||||
[Fact(Explicit = true, Skip = "Pending test host wiring")]
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.True(response.Headers.TryGetValues("X-OpenAPI-Scope", out var scopeValues) &&
|
||||
scopeValues.Contains("notify"));
|
||||
Assert.Equal("application/yaml", response.Content.Headers.ContentType?.MediaType);
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync(CancellationToken.None);
|
||||
Assert.Contains("openapi: 3.1.0", body);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Deprecation_headers_emitted_for_api_surface()
|
||||
{
|
||||
var response = await _client.GetAsync("/api/v1/notify/rules", CancellationToken.None);
|
||||
@@ -42,7 +48,8 @@ public sealed class OpenApiEndpointTests : IClassFixture<NotifierApplicationFact
|
||||
linkValues.Any(v => v.Contains("rel=\"deprecation\"")));
|
||||
}
|
||||
|
||||
[Fact(Explicit = true, Skip = "Pending test host wiring")]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PackApprovals_endpoint_validates_missing_headers()
|
||||
{
|
||||
var content = new StringContent("""{"eventId":"00000000-0000-0000-0000-000000000001","issuedAt":"2025-11-17T16:00:00Z","kind":"pack.approval.granted","packId":"offline-kit","decision":"approved","actor":"task-runner"}""", Encoding.UTF8, "application/json");
|
||||
@@ -51,7 +58,8 @@ public sealed class OpenApiEndpointTests : IClassFixture<NotifierApplicationFact
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact(Explicit = true, Skip = "Pending test host wiring")]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PackApprovals_endpoint_accepts_happy_path_and_echoes_resume_token()
|
||||
{
|
||||
var content = new StringContent("""{"eventId":"00000000-0000-0000-0000-000000000002","issuedAt":"2025-11-17T16:00:00Z","kind":"pack.approval.granted","packId":"offline-kit","decision":"approved","actor":"task-runner","resumeToken":"rt-ok"}""", Encoding.UTF8, "application/json");
|
||||
@@ -70,7 +78,8 @@ public sealed class OpenApiEndpointTests : IClassFixture<NotifierApplicationFact
|
||||
Assert.True(_packRepo.Exists("tenant-a", Guid.Parse("00000000-0000-0000-0000-000000000002"), "offline-kit"));
|
||||
}
|
||||
|
||||
[Fact(Explicit = true, Skip = "Pending test host wiring")]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PackApprovals_acknowledgement_requires_tenant_and_token()
|
||||
{
|
||||
var ackContent = new StringContent("""{"ackToken":"token-123"}""", Encoding.UTF8, "application/json");
|
||||
|
||||
@@ -8,10 +8,8 @@ namespace StellaOps.Notifier.Tests;
|
||||
|
||||
public sealed class PackApprovalTemplateSeederTests
|
||||
{
|
||||
private const string SkipReason = "Template seeder files not yet created";
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact(Skip = SkipReason)]
|
||||
[Fact]
|
||||
public async Task SeedAsync_loads_templates_from_docs()
|
||||
{
|
||||
var templateRepo = new InMemoryTemplateRepository();
|
||||
|
||||
@@ -9,10 +9,8 @@ namespace StellaOps.Notifier.Tests;
|
||||
|
||||
public sealed class RiskTemplateSeederTests
|
||||
{
|
||||
private const string SkipReason = "Offline bundle files not yet created in offline/notifier/";
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact(Skip = SkipReason)]
|
||||
[Fact]
|
||||
public async Task SeedTemplates_and_routing_load_from_offline_bundle()
|
||||
{
|
||||
var templateRepo = new InMemoryTemplateRepository();
|
||||
@@ -52,8 +50,9 @@ public sealed class RiskTemplateSeederTests
|
||||
var directory = AppContext.BaseDirectory;
|
||||
while (directory != null)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(directory, "offline", "notifier")) ||
|
||||
File.Exists(Path.Combine(directory, "StellaOps.sln")))
|
||||
var manifest = Path.Combine(directory, "offline", "notifier", "notify-kit.manifest.json");
|
||||
var notifierDocs = Path.Combine(directory, "src", "Notifier", "StellaOps.Notifier", "StellaOps.Notifier.docs");
|
||||
if (File.Exists(manifest) || Directory.Exists(notifierDocs))
|
||||
{
|
||||
return directory;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="OpenApiEndpointTests.cs" />
|
||||
<Content Include="TestContent/**" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
extern alias webservice;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using StellaOps.Notify.Queue;
|
||||
using StellaOps.Notifier.WebService.Storage.Compat;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
@@ -20,12 +20,13 @@ public sealed class NotifierApplicationFactory : WebApplicationFactory<WebProgra
|
||||
internal InMemoryLockRepository LockRepo { get; } = new();
|
||||
internal InMemoryAuditRepository AuditRepo { get; } = new();
|
||||
internal InMemoryPackApprovalRepository PackRepo { get; } = new();
|
||||
internal INotifyEventQueue EventQueue { get; set; } = new NullNotifyEventQueue();
|
||||
|
||||
protected override IHost CreateHost(IHostBuilder builder)
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.UseEnvironment("Testing");
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<INotifyRuleRepository>();
|
||||
services.RemoveAll<INotifyChannelRepository>();
|
||||
@@ -43,9 +44,7 @@ public sealed class NotifierApplicationFactory : WebApplicationFactory<WebProgra
|
||||
services.AddSingleton<INotifyLockRepository>(LockRepo);
|
||||
services.AddSingleton<INotifyAuditRepository>(AuditRepo);
|
||||
services.AddSingleton<INotifyPackApprovalRepository>(PackRepo);
|
||||
services.AddSingleton<INotifyEventQueue, NullNotifyEventQueue>();
|
||||
services.AddSingleton(EventQueue);
|
||||
});
|
||||
|
||||
return base.CreateHost(builder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,3 +8,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0394-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Notifier.Tests. |
|
||||
| AUDIT-0394-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Notifier.Tests. |
|
||||
| AUDIT-0394-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
|
||||
@@ -287,10 +287,49 @@ public sealed class EnhancedTemplateRendererTests
|
||||
Assert.DoesNotContain("CRITICAL:", resultFalse.Body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RenderAsync_IfBlock_AllowsWhitespaceTokens()
|
||||
{
|
||||
// Arrange
|
||||
var template = CreateTemplate("{{ #if critical }}OK{{ /if }}");
|
||||
var payload = new JsonObject { ["critical"] = "true" };
|
||||
var notifyEvent = CreateEvent("test", "user", payload);
|
||||
|
||||
// Act
|
||||
var result = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("OK", result.Body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RenderAsync_NoneRenderMode_DoesNotWrapJson()
|
||||
{
|
||||
// Arrange
|
||||
var template = NotifyTemplate.Create(
|
||||
templateId: "tmpl-json",
|
||||
tenantId: "test-tenant",
|
||||
channelType: NotifyChannelType.Webhook,
|
||||
key: "test.key",
|
||||
locale: "en-us",
|
||||
body: "{\"message\":\"{{message}}\"}",
|
||||
renderMode: NotifyTemplateRenderMode.None,
|
||||
format: NotifyDeliveryFormat.Json);
|
||||
var payload = new JsonObject { ["message"] = "hello" };
|
||||
var notifyEvent = CreateEvent("test.event", "user", payload);
|
||||
|
||||
// Act
|
||||
var result = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("{\"message\":\"hello\"}", result.Body);
|
||||
}
|
||||
|
||||
private static NotifyTemplate CreateTemplate(
|
||||
string body,
|
||||
NotifyDeliveryFormat format = NotifyDeliveryFormat.PlainText,
|
||||
Dictionary<string, string>? metadata = null)
|
||||
Dictionary<string, string>? metadata = null,
|
||||
NotifyTemplateRenderMode renderMode = NotifyTemplateRenderMode.Markdown)
|
||||
{
|
||||
return NotifyTemplate.Create(
|
||||
templateId: "test-template",
|
||||
@@ -299,6 +338,7 @@ public sealed class EnhancedTemplateRendererTests
|
||||
key: "test.key",
|
||||
locale: "en-us",
|
||||
body: body,
|
||||
renderMode: renderMode,
|
||||
format: format,
|
||||
metadata: metadata);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Contracts;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Contracts;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notifier.Worker.Escalation;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notifier.Worker.Escalation;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Fallback;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notifier.Worker.Localization;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notifier.Worker.Localization;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notifier.WebService.Contracts;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notifier.Worker.Dispatch;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.Worker.Templates;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.Linq;
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notifier.Worker.Observability;
|
||||
using StellaOps.Notifier.Worker.Retention;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Linq;
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.WebService.Contracts;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notifier.Worker.Simulation;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Simulation;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notifier.Worker.StormBreaker;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notifier.Worker.StormBreaker;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.WebService.Contracts;
|
||||
using StellaOps.Notifier.Worker.Dispatch;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.Worker.Templates;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -123,10 +123,6 @@ app.UseWebSockets(new WebSocketOptions
|
||||
|
||||
app.MapHealthChecks("/healthz");
|
||||
|
||||
// Tenant context middleware (extracts and validates tenant from headers/query)
|
||||
app.UseTenantContext();
|
||||
app.TryUseStellaRouter(routerOptions);
|
||||
|
||||
// Deprecation headers for retiring v1 APIs (RFC 8594 / IETF Sunset)
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
@@ -141,6 +137,10 @@ app.Use(async (context, next) =>
|
||||
await next().ConfigureAwait(false);
|
||||
});
|
||||
|
||||
// Tenant context middleware (extracts and validates tenant from headers/query)
|
||||
app.UseTenantContext();
|
||||
app.TryUseStellaRouter(routerOptions);
|
||||
|
||||
app.MapPost("/api/v1/notify/pack-approvals", async (
|
||||
HttpContext context,
|
||||
PackApprovalRequest request,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Services;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Services;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Services;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Services;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Services;
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Setup;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
using StellaOps.Notify.Queue;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Notify.Queue;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Setup;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Setup;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.Json;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Setup;
|
||||
|
||||
@@ -172,7 +173,10 @@ public sealed class PackApprovalTemplateSeeder : IHostedService
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(contentRootPath, "StellaOps.Notifier.docs", "pack-approval-templates.json"),
|
||||
Path.Combine(contentRootPath, "..", "StellaOps.Notifier.docs", "pack-approval-templates.json")
|
||||
Path.Combine(contentRootPath, "..", "StellaOps.Notifier.docs", "pack-approval-templates.json"),
|
||||
Path.Combine(contentRootPath, "StellaOps.Notifier", "StellaOps.Notifier.docs", "pack-approval-templates.json"),
|
||||
Path.Combine(contentRootPath, "Notifier", "StellaOps.Notifier", "StellaOps.Notifier.docs", "pack-approval-templates.json"),
|
||||
Path.Combine(contentRootPath, "src", "Notifier", "StellaOps.Notifier", "StellaOps.Notifier.docs", "pack-approval-templates.json")
|
||||
};
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Xml;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Setup;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0395-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Notifier.WebService. |
|
||||
| AUDIT-0395-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Notifier.WebService. |
|
||||
| AUDIT-0395-A | TODO | Revalidated 2026-01-07 (open findings). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"templateId": "identity-matched-email",
|
||||
"tenantId": "bootstrap",
|
||||
"channelType": "email",
|
||||
"channelType": "Email",
|
||||
"key": "identity-matched",
|
||||
"locale": "en-US",
|
||||
"schemaVersion": "1.0.0",
|
||||
"renderMode": "Markdown",
|
||||
"renderMode": "Html",
|
||||
"format": "Html",
|
||||
"description": "Identity watchlist match alert for Email",
|
||||
"description": "Email notification for identity watchlist matches",
|
||||
"metadata": {
|
||||
"category": "attestation",
|
||||
"eventKind": "attestor.identity.matched",
|
||||
"subject": "[{{ event.severity }}] Identity Watchlist Alert: {{ event.watchlistEntryName }}"
|
||||
"category": "attestation",
|
||||
"subject": "[{{ event.severity | upper }}] Identity Watchlist Alert: {{ event.watchlistEntryName }}"
|
||||
},
|
||||
"body": "# Identity Watchlist Alert\n\n**Watchlist Entry:** {{ event.watchlistEntryName }}\n\n**Severity:** {{ event.severity }}\n\n**Occurred:** {{ event.occurredAtUtc }}\n\n---\n\n## Matched Identity\n\n| Field | Value |\n|-------|-------|\n{% if event.matchedIdentity.issuer %}| Issuer | {{ event.matchedIdentity.issuer }} |{% endif %}\n{% if event.matchedIdentity.subjectAlternativeName %}| Subject Alternative Name | {{ event.matchedIdentity.subjectAlternativeName }} |{% endif %}\n{% if event.matchedIdentity.keyId %}| Key ID | {{ event.matchedIdentity.keyId }} |{% endif %}\n\n## Rekor Entry Details\n\n| Field | Value |\n|-------|-------|\n| UUID | {{ event.rekorEntry.uuid }} |\n| Log Index | {{ event.rekorEntry.logIndex }} |\n| Artifact SHA256 | {{ event.rekorEntry.artifactSha256 }} |\n| Integrated Time (UTC) | {{ event.rekorEntry.integratedTimeUtc }} |\n\n{% if event.suppressedCount > 0 %}\n---\n\n*Note: {{ event.suppressedCount }} similar alerts were suppressed within the deduplication window.*\n{% endif %}\n\n---\n\n*This alert was generated by Stella Ops identity watchlist monitoring.*"
|
||||
"body": "<!DOCTYPE html>\n<html>\n<head><style>body{font-family:sans-serif;line-height:1.5;}.severity-critical{color:#dc3545;}.severity-warning{color:#ffc107;}.severity-info{color:#0dcaf0;}.section{margin:1em 0;padding:1em;background:#f8f9fa;border-radius:4px;}.label{font-weight:bold;color:#666;}.mono{font-family:monospace;background:#e9ecef;padding:2px 6px;border-radius:3px;}</style></head>\n<body>\n<h2 class=\"severity-{{ event.severity }}\">Identity Watchlist Alert</h2>\n<div class=\"section\">\n<p><span class=\"label\">Severity:</span> <strong>{{ event.severity }}</strong></p>\n<p><span class=\"label\">Watchlist Entry:</span> {{ event.watchlistEntryName }}</p>\n</div>\n<div class=\"section\">\n<h3>Matched Identity</h3>\n{{ #if event.matchedIdentity.issuer }}<p><span class=\"label\">Issuer:</span> <span class=\"mono\">{{ event.matchedIdentity.issuer }}</span></p>{{ /if }}\n{{ #if event.matchedIdentity.subjectAlternativeName }}<p><span class=\"label\">Subject Alternative Name:</span> <span class=\"mono\">{{ event.matchedIdentity.subjectAlternativeName }}</span></p>{{ /if }}\n{{ #if event.matchedIdentity.keyId }}<p><span class=\"label\">Key ID:</span> <span class=\"mono\">{{ event.matchedIdentity.keyId }}</span></p>{{ /if }}\n</div>\n<div class=\"section\">\n<h3>Rekor Entry</h3>\n<p><span class=\"label\">UUID:</span> <span class=\"mono\">{{ event.rekorEntry.uuid }}</span></p>\n<p><span class=\"label\">Log Index:</span> {{ event.rekorEntry.logIndex }}</p>\n<p><span class=\"label\">Artifact SHA-256:</span> <span class=\"mono\">{{ event.rekorEntry.artifactSha256 }}</span></p>\n<p><span class=\"label\">Integrated Time (UTC):</span> {{ event.rekorEntry.integratedTimeUtc }}</p>\n</div>\n{{ #if (gt event.suppressedCount 0) }}<p><em>{{ event.suppressedCount }} duplicate alerts suppressed</em></p>{{ /if }}\n<hr>\n<p style=\"font-size:0.85em;color:#666;\">Event ID: {{ event.eventId }} | Occurred: {{ event.occurredAtUtc }}</p>\n</body>\n</html>"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"templateId": "identity-matched-slack",
|
||||
"tenantId": "bootstrap",
|
||||
"channelType": "slack",
|
||||
"channelType": "Slack",
|
||||
"key": "identity-matched",
|
||||
"locale": "en-US",
|
||||
"schemaVersion": "1.0.0",
|
||||
"renderMode": "Markdown",
|
||||
"format": "Json",
|
||||
"description": "Identity watchlist match alert for Slack",
|
||||
"description": "Slack notification for identity watchlist matches",
|
||||
"metadata": {
|
||||
"category": "attestation",
|
||||
"eventKind": "attestor.identity.matched"
|
||||
"eventKind": "attestor.identity.matched",
|
||||
"category": "attestation"
|
||||
},
|
||||
"body": ":warning: *Identity Watchlist Alert*\n\n*Entry:* {{ event.watchlistEntryName }}\n*Severity:* {{ event.severity }}\n\n*Matched Identity:*\n{% if event.matchedIdentity.issuer %}• Issuer: `{{ event.matchedIdentity.issuer }}`{% endif %}\n{% if event.matchedIdentity.subjectAlternativeName %}• SAN: `{{ event.matchedIdentity.subjectAlternativeName }}`{% endif %}\n{% if event.matchedIdentity.keyId %}• Key ID: `{{ event.matchedIdentity.keyId }}`{% endif %}\n\n*Rekor Entry:*\n• UUID: `{{ event.rekorEntry.uuid }}`\n• Log Index: `{{ event.rekorEntry.logIndex }}`\n• Artifact: `{{ event.rekorEntry.artifactSha256 }}`\n• Time: {{ event.rekorEntry.integratedTimeUtc }}\n\n{% if event.suppressedCount > 0 %}_({{ event.suppressedCount }} similar alerts suppressed)_{% endif %}"
|
||||
"body": "{{ #if (eq event.severity \"critical\") }}:rotating_light:{{ else if (eq event.severity \"warning\") }}:warning:{{ else }}:information_source:{{ /if }} *Identity Watchlist Alert*\n\n*Severity:* `{{ event.severity }}`\n*Watchlist Entry:* {{ event.watchlistEntryName }}\n\n*Matched Identity:*\n{{ #if event.matchedIdentity.issuer }}> Issuer: `{{ event.matchedIdentity.issuer }}`\n{{ /if }}{{ #if event.matchedIdentity.subjectAlternativeName }}> SAN: `{{ event.matchedIdentity.subjectAlternativeName }}`\n{{ /if }}{{ #if event.matchedIdentity.keyId }}> Key ID: `{{ event.matchedIdentity.keyId }}`\n{{ /if }}\n*Rekor Entry:*\n> UUID: `{{ event.rekorEntry.uuid }}`\n> Log Index: {{ event.rekorEntry.logIndex }}\n> Artifact: `{{ event.rekorEntry.artifactSha256 }}`\n> Integrated: {{ event.rekorEntry.integratedTimeUtc }}\n\n{{ #if (gt event.suppressedCount 0) }}:mute: {{ event.suppressedCount }} duplicate alerts suppressed\n{{ /if }}---\n_Event ID: {{ event.eventId }}_"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"templateId": "identity-matched-teams",
|
||||
"tenantId": "bootstrap",
|
||||
"channelType": "teams",
|
||||
"channelType": "Teams",
|
||||
"key": "identity-matched",
|
||||
"locale": "en-US",
|
||||
"schemaVersion": "1.0.0",
|
||||
"renderMode": "Markdown",
|
||||
"renderMode": "None",
|
||||
"format": "Json",
|
||||
"description": "Identity watchlist match alert for Microsoft Teams",
|
||||
"description": "Microsoft Teams adaptive card for identity watchlist matches",
|
||||
"metadata": {
|
||||
"eventKind": "attestor.identity.matched",
|
||||
"category": "attestation",
|
||||
"eventKind": "attestor.identity.matched"
|
||||
"contentType": "application/json"
|
||||
},
|
||||
"body": "{ \"@type\": \"MessageCard\", \"@context\": \"http://schema.org/extensions\", \"themeColor\": \"{% if event.severity == 'Critical' %}d13438{% elsif event.severity == 'Warning' %}ffb900{% else %}0078d4{% endif %}\", \"summary\": \"Identity Watchlist Alert: {{ event.watchlistEntryName }}\", \"sections\": [{ \"activityTitle\": \"⚠️ Identity Watchlist Alert\", \"activitySubtitle\": \"Entry: {{ event.watchlistEntryName }}\", \"facts\": [{ \"name\": \"Severity\", \"value\": \"{{ event.severity }}\" }, { \"name\": \"Occurred\", \"value\": \"{{ event.occurredAtUtc }}\" }{% if event.matchedIdentity.issuer %}, { \"name\": \"Issuer\", \"value\": \"{{ event.matchedIdentity.issuer }}\" }{% endif %}{% if event.matchedIdentity.subjectAlternativeName %}, { \"name\": \"SAN\", \"value\": \"{{ event.matchedIdentity.subjectAlternativeName }}\" }{% endif %}, { \"name\": \"Rekor UUID\", \"value\": \"{{ event.rekorEntry.uuid }}\" }, { \"name\": \"Log Index\", \"value\": \"{{ event.rekorEntry.logIndex }}\" }{% if event.suppressedCount > 0 %}, { \"name\": \"Suppressed Count\", \"value\": \"{{ event.suppressedCount }}\" }{% endif %}], \"markdown\": true }] }"
|
||||
"body": "{\"type\":\"message\",\"attachments\":[{\"contentType\":\"application/vnd.microsoft.card.adaptive\",\"content\":{\"$schema\":\"http://adaptivecards.io/schemas/adaptive-card.json\",\"type\":\"AdaptiveCard\",\"version\":\"1.4\",\"body\":[{\"type\":\"TextBlock\",\"text\":\"Identity Watchlist Alert\",\"weight\":\"bolder\",\"size\":\"large\",\"color\":\"{{ #if (eq event.severity 'critical') }}attention{{ else if (eq event.severity 'warning') }}warning{{ else }}default{{ /if }}\"},{\"type\":\"FactSet\",\"facts\":[{\"title\":\"Severity\",\"value\":\"{{ event.severity }}\"},{\"title\":\"Watchlist Entry\",\"value\":\"{{ event.watchlistEntryName }}\"}]},{\"type\":\"TextBlock\",\"text\":\"Matched Identity\",\"weight\":\"bolder\",\"spacing\":\"medium\"},{\"type\":\"FactSet\",\"facts\":[{{ #if event.matchedIdentity.issuer }}{\"title\":\"Issuer\",\"value\":\"{{ event.matchedIdentity.issuer }}\"},{{ /if }}{{ #if event.matchedIdentity.subjectAlternativeName }}{\"title\":\"SAN\",\"value\":\"{{ event.matchedIdentity.subjectAlternativeName }}\"},{{ /if }}{{ #if event.matchedIdentity.keyId }}{\"title\":\"Key ID\",\"value\":\"{{ event.matchedIdentity.keyId }}\"},{{ /if }}{\"title\":\"\",\"value\":\"\"}]},{\"type\":\"TextBlock\",\"text\":\"Rekor Entry\",\"weight\":\"bolder\",\"spacing\":\"medium\"},{\"type\":\"FactSet\",\"facts\":[{\"title\":\"UUID\",\"value\":\"{{ event.rekorEntry.uuid }}\"},{\"title\":\"Log Index\",\"value\":\"{{ event.rekorEntry.logIndex }}\"},{\"title\":\"Artifact\",\"value\":\"{{ event.rekorEntry.artifactSha256 }}\"},{\"title\":\"Integrated\",\"value\":\"{{ event.rekorEntry.integratedTimeUtc }}\"}]}{{ #if (gt event.suppressedCount 0) }},{\"type\":\"TextBlock\",\"text\":\"{{ event.suppressedCount }} duplicate alerts suppressed\",\"isSubtle\":true,\"spacing\":\"small\"}{{ /if }}],\"msteams\":{\"width\":\"Full\"}}}]}"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"templateId": "identity-matched-webhook",
|
||||
"tenantId": "bootstrap",
|
||||
"channelType": "webhook",
|
||||
"channelType": "Webhook",
|
||||
"key": "identity-matched",
|
||||
"locale": "en-US",
|
||||
"schemaVersion": "1.0.0",
|
||||
"renderMode": "None",
|
||||
"format": "Json",
|
||||
"description": "Identity watchlist match alert for Webhook (SIEM/SOC integration)",
|
||||
"description": "Webhook payload for identity watchlist matches",
|
||||
"metadata": {
|
||||
"eventKind": "attestor.identity.matched",
|
||||
"category": "attestation",
|
||||
"eventKind": "attestor.identity.matched"
|
||||
"contentType": "application/json"
|
||||
},
|
||||
"body": "{ \"eventType\": \"attestor.identity.matched\", \"eventId\": \"{{ event.eventId }}\", \"tenantId\": \"{{ event.tenantId }}\", \"severity\": \"{{ event.severity }}\", \"occurredAtUtc\": \"{{ event.occurredAtUtc }}\", \"watchlist\": { \"entryId\": \"{{ event.watchlistEntryId }}\", \"entryName\": \"{{ event.watchlistEntryName }}\" }, \"matchedIdentity\": { \"issuer\": \"{{ event.matchedIdentity.issuer }}\", \"subjectAlternativeName\": \"{{ event.matchedIdentity.subjectAlternativeName }}\", \"keyId\": \"{{ event.matchedIdentity.keyId }}\" }, \"rekorEntry\": { \"uuid\": \"{{ event.rekorEntry.uuid }}\", \"logIndex\": {{ event.rekorEntry.logIndex }}, \"artifactSha256\": \"{{ event.rekorEntry.artifactSha256 }}\", \"integratedTimeUtc\": \"{{ event.rekorEntry.integratedTimeUtc }}\" }, \"suppressedCount\": {{ event.suppressedCount }} }"
|
||||
"body": "{\"alertType\":\"identity-watchlist-match\",\"severity\":\"{{ event.severity }}\",\"watchlist\":{\"entryId\":\"{{ event.watchlistEntryId }}\",\"entryName\":\"{{ event.watchlistEntryName }}\"},\"matchedIdentity\":{\"issuer\":{{ #if event.matchedIdentity.issuer }}\"{{ event.matchedIdentity.issuer }}\"{{ else }}null{{ /if }},\"subjectAlternativeName\":{{ #if event.matchedIdentity.subjectAlternativeName }}\"{{ event.matchedIdentity.subjectAlternativeName }}\"{{ else }}null{{ /if }},\"keyId\":{{ #if event.matchedIdentity.keyId }}\"{{ event.matchedIdentity.keyId }}\"{{ else }}null{{ /if }}},\"rekorEntry\":{\"uuid\":\"{{ event.rekorEntry.uuid }}\",\"logIndex\":{{ event.rekorEntry.logIndex }},\"artifactSha256\":\"{{ event.rekorEntry.artifactSha256 }}\",\"integratedTimeUtc\":\"{{ event.rekorEntry.integratedTimeUtc }}\"},\"eventId\":\"{{ event.eventId }}\",\"occurredAtUtc\":\"{{ event.occurredAtUtc }}\",\"suppressedCount\":{{ event.suppressedCount }}}"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"templateId": "tmpl-risk-profile-state-email",
|
||||
"tenantId": "bootstrap",
|
||||
"channelType": "Email",
|
||||
"key": "tmpl-risk-profile-state",
|
||||
"locale": "en-US",
|
||||
"schemaVersion": "1.0.0",
|
||||
"renderMode": "Html",
|
||||
"format": "Html",
|
||||
"description": "Email notification for risk profile state changes",
|
||||
"metadata": {
|
||||
"eventKind": "risk.profile.published",
|
||||
"category": "risk",
|
||||
"subject": "[Notify] Risk profile update: {{ event.profileName }}"
|
||||
},
|
||||
"body": "<!DOCTYPE html>\n<html>\n<head><style>body{font-family:sans-serif;line-height:1.5;}.section{margin:1em 0;padding:1em;background:#f8f9fa;border-radius:4px;}.label{font-weight:bold;color:#666;}.mono{font-family:monospace;background:#e9ecef;padding:2px 6px;border-radius:3px;}</style></head>\n<body>\n<h2>Risk Profile Update</h2>\n<div class=\"section\">\n<p><span class=\"label\">Profile:</span> <span class=\"mono\">{{ event.profileName }}</span></p>\n<p><span class=\"label\">State:</span> {{ event.state }}</p>\n<p><span class=\"label\">Owner:</span> {{ event.owner }}</p>\n<p><span class=\"label\">Summary:</span> {{ event.summary }}</p>\n{{ #if event.policyId }}<p><span class=\"label\">Policy:</span> {{ event.policyId }} (v{{ event.policyVersion }})</p>{{ /if }}\n</div>\n<hr>\n<p style=\"font-size:0.85em;color:#666;\">Event ID: {{ event.eventId }} | Occurred: {{ event.occurredAtUtc }}</p>\n</body>\n</html>"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"templateId": "tmpl-risk-profile-state-slack",
|
||||
"tenantId": "bootstrap",
|
||||
"channelType": "Slack",
|
||||
"key": "tmpl-risk-profile-state",
|
||||
"locale": "en-US",
|
||||
"schemaVersion": "1.0.0",
|
||||
"renderMode": "Markdown",
|
||||
"format": "Json",
|
||||
"description": "Slack notification for risk profile state changes",
|
||||
"metadata": {
|
||||
"eventKind": "risk.profile.published",
|
||||
"category": "risk"
|
||||
},
|
||||
"body": ":information_source: *Risk profile update*\n\n*Profile:* {{ event.profileName }}\n*State:* {{ event.state }}\n*Owner:* {{ event.owner }}\n*Summary:* {{ event.summary }}\n\n{{ #if event.policyId }}*Policy:* {{ event.policyId }} (v{{ event.policyVersion }})\n{{ /if }}---\n_Event ID: {{ event.eventId }} | {{ event.occurredAtUtc }}_"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"templateId": "tmpl-risk-severity-change-email",
|
||||
"tenantId": "bootstrap",
|
||||
"channelType": "Email",
|
||||
"key": "tmpl-risk-severity-change",
|
||||
"locale": "en-US",
|
||||
"schemaVersion": "1.0.0",
|
||||
"renderMode": "Html",
|
||||
"format": "Html",
|
||||
"description": "Email notification for risk severity changes",
|
||||
"metadata": {
|
||||
"eventKind": "risk.profile.severity.changed",
|
||||
"category": "risk",
|
||||
"subject": "[Notify] Risk severity changed: {{ event.profileName }}"
|
||||
},
|
||||
"body": "<!DOCTYPE html>\n<html>\n<head><style>body{font-family:sans-serif;line-height:1.5;}.section{margin:1em 0;padding:1em;background:#f8f9fa;border-radius:4px;}.label{font-weight:bold;color:#666;}.mono{font-family:monospace;background:#e9ecef;padding:2px 6px;border-radius:3px;}</style></head>\n<body>\n<h2>Risk Severity Changed</h2>\n<div class=\"section\">\n<p><span class=\"label\">Profile:</span> <span class=\"mono\">{{ event.profileName }}</span></p>\n<p><span class=\"label\">Previous:</span> {{ event.previousSeverity }}</p>\n<p><span class=\"label\">Current:</span> {{ event.newSeverity }}</p>\n<p><span class=\"label\">Reason:</span> {{ event.reason }}</p>\n{{ #if event.referenceUrl }}<p><span class=\"label\">Reference:</span> <a href=\"{{ event.referenceUrl }}\">{{ event.referenceUrl }}</a></p>{{ /if }}\n</div>\n<hr>\n<p style=\"font-size:0.85em;color:#666;\">Event ID: {{ event.eventId }} | Occurred: {{ event.occurredAtUtc }}</p>\n</body>\n</html>"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"templateId": "tmpl-risk-severity-change-slack",
|
||||
"tenantId": "bootstrap",
|
||||
"channelType": "Slack",
|
||||
"key": "tmpl-risk-severity-change",
|
||||
"locale": "en-US",
|
||||
"schemaVersion": "1.0.0",
|
||||
"renderMode": "Markdown",
|
||||
"format": "Json",
|
||||
"description": "Slack notification for risk severity changes",
|
||||
"metadata": {
|
||||
"eventKind": "risk.profile.severity.changed",
|
||||
"category": "risk"
|
||||
},
|
||||
"body": ":rotating_light: *Risk severity changed*\n\n*Profile:* {{ event.profileName }}\n*Previous:* {{ event.previousSeverity }}\n*Current:* {{ event.newSeverity }}\n*Reason:* {{ event.reason }}\n\n{{ #if event.referenceUrl }}*Reference:* {{ event.referenceUrl }}\n{{ /if }}---\n_Event ID: {{ event.eventId }} | {{ event.occurredAtUtc }}_"
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Channels;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Channels;
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Mail;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.Worker.Options;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Mail;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Channels;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Channels;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Channels;
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Channels;
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Channels;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Channels;
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notifier.Worker.Options;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.Worker.Options;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Channels;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notifier.Worker.Observability;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.DeadLetter;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Digest;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System.Globalization;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Channels;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Digest;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Cronos;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Digest;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.Worker.Options;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Dispatch;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Dispatch;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Dispatch;
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Escalation;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Escalation;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Escalation;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Escalation;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Escalation;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Escalation;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Escalation;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Channels;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Fallback;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Localization;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Observability;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Observability;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Observability;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Observability;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Observability;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using StellaOps.Notify.Engine;
|
||||
using StellaOps.Notify.Models;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Notifier.Worker.Processing;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user