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

@@ -3,6 +3,7 @@ using System.Collections.Immutable;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
@@ -11,7 +12,9 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using StellaOps.Notifier.WebService.Contracts;
using StellaOps.Notifier.WebService.Services;
using StellaOps.Notifier.WebService.Setup;
using StellaOps.Notifier.WebService.Extensions;
using StellaOps.Notifier.WebService.Storage.Compat;
using StellaOps.Notifier.Worker.Channels;
using StellaOps.Notifier.Worker.Security;
using StellaOps.Notifier.Worker.StormBreaker;
using StellaOps.Notifier.Worker.DeadLetter;
@@ -19,18 +22,16 @@ using StellaOps.Notifier.Worker.Retention;
using StellaOps.Notifier.Worker.Observability;
using StellaOps.Notifier.WebService.Endpoints;
using StellaOps.Notifier.WebService.Setup;
using StellaOps.Notifier.Worker.Dispatch;
using StellaOps.Notifier.Worker.Escalation;
using StellaOps.Notifier.Worker.Observability;
using StellaOps.Notifier.Worker.Security;
using StellaOps.Notifier.Worker.StormBreaker;
using StellaOps.Notifier.Worker.Templates;
using StellaOps.Notifier.Worker.Tenancy;
using StellaOps.Notify.Storage.Mongo;
using StellaOps.Notify.Storage.Mongo.Documents;
using StellaOps.Notify.Storage.Mongo.Repositories;
using StellaOps.Notifier.Worker.Templates;
using DeadLetterStatus = StellaOps.Notifier.Worker.DeadLetter.DeadLetterStatus;
using Contracts = StellaOps.Notifier.WebService.Contracts;
using WorkerTemplateService = StellaOps.Notifier.Worker.Templates.INotifyTemplateService;
using WorkerTemplateRenderer = StellaOps.Notifier.Worker.Dispatch.INotifyTemplateRenderer;
using StellaOps.Notify.Models;
using StellaOps.Notify.Queue;
using StellaOps.Notifier.Worker.Storage;
var builder = WebApplication.CreateBuilder(args);
@@ -42,44 +43,28 @@ builder.Configuration
builder.Services.AddSingleton<TimeProvider>(TimeProvider.System);
if (!isTesting)
{
var mongoSection = builder.Configuration.GetSection("notifier:storage:mongo");
builder.Services.AddNotifyMongoStorage(mongoSection);
builder.Services.AddHostedService<MongoInitializationHostedService>();
builder.Services.AddHostedService<PackApprovalTemplateSeeder>();
builder.Services.AddHostedService<AttestationTemplateSeeder>();
builder.Services.AddHostedService<RiskTemplateSeeder>();
}
// Fallback no-op event queue for environments that do not configure a real backend.
builder.Services.TryAddSingleton<INotifyEventQueue, NullNotifyEventQueue>();
// Template service with advanced renderer
builder.Services.AddSingleton<INotifyTemplateRenderer, AdvancedTemplateRenderer>();
builder.Services.AddScoped<INotifyTemplateService, NotifyTemplateService>();
// In-memory storage (document store removed)
builder.Services.AddSingleton<INotifyChannelRepository, InMemoryNotifyRepositories>();
builder.Services.AddSingleton<INotifyRuleRepository, InMemoryNotifyRepositories>();
builder.Services.AddSingleton<INotifyTemplateRepository, InMemoryNotifyRepositories>();
builder.Services.AddSingleton<INotifyDeliveryRepository, InMemoryNotifyRepositories>();
builder.Services.AddSingleton<INotifyAuditRepository, InMemoryNotifyRepositories>();
builder.Services.AddSingleton<INotifyLockRepository, InMemoryNotifyRepositories>();
builder.Services.AddSingleton<IInAppInboxStore, InMemoryInboxStore>();
builder.Services.AddSingleton<INotifyInboxRepository, InMemoryInboxStore>();
builder.Services.AddSingleton<INotifyLocalizationRepository, InMemoryNotifyRepositories>();
builder.Services.AddSingleton<INotifyPackApprovalRepository, InMemoryPackApprovalRepository>();
builder.Services.AddSingleton<INotifyThrottleConfigRepository, InMemoryThrottleConfigRepository>();
builder.Services.AddSingleton<INotifyOperatorOverrideRepository, InMemoryOperatorOverrideRepository>();
builder.Services.AddSingleton<INotifyQuietHoursRepository, InMemoryQuietHoursRepository>();
builder.Services.AddSingleton<INotifyMaintenanceWindowRepository, InMemoryMaintenanceWindowRepository>();
builder.Services.AddSingleton<INotifyEscalationPolicyRepository, InMemoryEscalationPolicyRepository>();
builder.Services.AddSingleton<INotifyOnCallScheduleRepository, InMemoryOnCallScheduleRepository>();
// Localization resolver with fallback chain
builder.Services.AddSingleton<ILocalizationResolver, DefaultLocalizationResolver>();
// Storm breaker for notification storm detection
builder.Services.Configure<StormBreakerConfig>(builder.Configuration.GetSection("notifier:stormBreaker"));
builder.Services.AddSingleton<IStormBreaker, DefaultStormBreaker>();
// Security services (NOTIFY-SVC-40-003)
builder.Services.Configure<AckTokenOptions>(builder.Configuration.GetSection("notifier:security:ackToken"));
builder.Services.AddSingleton<IAckTokenService, HmacAckTokenService>();
builder.Services.Configure<WebhookSecurityOptions>(builder.Configuration.GetSection("notifier:security:webhook"));
builder.Services.AddSingleton<IWebhookSecurityService, DefaultWebhookSecurityService>();
builder.Services.AddSingleton<IHtmlSanitizer, DefaultHtmlSanitizer>();
builder.Services.Configure<TenantIsolationOptions>(builder.Configuration.GetSection("notifier:security:tenantIsolation"));
builder.Services.AddSingleton<ITenantIsolationValidator, DefaultTenantIsolationValidator>();
// Observability, dead-letter, and retention services (NOTIFY-SVC-40-004)
builder.Services.AddSingleton<INotifyMetrics, DefaultNotifyMetrics>();
builder.Services.AddSingleton<IDeadLetterService, InMemoryDeadLetterService>();
builder.Services.AddSingleton<IRetentionPolicyService, DefaultRetentionPolicyService>();
// Template service for v2 API preview endpoint
// Template service with enhanced renderer (worker contracts)
builder.Services.AddTemplateServices(options =>
{
var provenanceUrl = builder.Configuration["notifier:provenance:baseUrl"];
@@ -89,6 +74,22 @@ builder.Services.AddTemplateServices(options =>
}
});
// Localization resolver with fallback chain
builder.Services.AddSingleton<ILocalizationResolver, DefaultLocalizationResolver>();
// Security services (NOTIFY-SVC-40-003)
builder.Services.Configure<AckTokenOptions>(builder.Configuration.GetSection("notifier:security:ackToken"));
builder.Services.AddSingleton<IAckTokenService, HmacAckTokenService>();
builder.Services.Configure<WebhookSecurityOptions>(builder.Configuration.GetSection("notifier:security:webhook"));
builder.Services.AddSingleton<IWebhookSecurityService, InMemoryWebhookSecurityService>();
builder.Services.AddSingleton<IHtmlSanitizer, DefaultHtmlSanitizer>();
builder.Services.Configure<TenantIsolationOptions>(builder.Configuration.GetSection("notifier:security:tenantIsolation"));
builder.Services.AddSingleton<ITenantIsolationValidator, InMemoryTenantIsolationValidator>();
// Observability, dead-letter, and retention services (NOTIFY-SVC-40-004)
builder.Services.AddSingleton<INotifyMetrics, DefaultNotifyMetrics>();
builder.Services.AddSingleton<IDeadLetterService, InMemoryDeadLetterService>();
builder.Services.AddSingleton<IRetentionPolicyService, DefaultRetentionPolicyService>();
// Escalation and on-call services
builder.Services.AddEscalationServices(builder.Configuration);
@@ -98,9 +99,6 @@ builder.Services.AddStormBreakerServices(builder.Configuration);
// Security services (signing, webhook validation, HTML sanitization, tenant isolation)
builder.Services.AddNotifierSecurityServices(builder.Configuration);
// Observability services (metrics, tracing, dead-letter, chaos testing, retention)
builder.Services.AddNotifierObservabilityServices(builder.Configuration);
// Tenancy services (context accessor, RLS enforcement, channel resolution, notification enrichment)
builder.Services.AddNotifierTenancy(builder.Configuration);
@@ -432,7 +430,7 @@ app.MapPost("/api/v1/notify/pack-approvals/{packId}/ack", async (
app.MapGet("/api/v2/notify/templates", async (
HttpContext context,
INotifyTemplateService templateService,
WorkerTemplateService templateService,
string? keyPrefix,
string? locale,
NotifyChannelType? channelType) =>
@@ -443,8 +441,15 @@ app.MapGet("/api/v2/notify/templates", async (
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
}
var templates = await templateService.ListAsync(tenantId, keyPrefix, locale, channelType, context.RequestAborted)
.ConfigureAwait(false);
var templates = await templateService.ListAsync(
tenantId,
new TemplateListOptions
{
KeyPrefix = keyPrefix,
Locale = locale,
ChannelType = channelType
},
context.RequestAborted).ConfigureAwait(false);
return Results.Ok(new { items = templates, count = templates.Count });
});
@@ -452,7 +457,7 @@ app.MapGet("/api/v2/notify/templates", async (
app.MapGet("/api/v2/notify/templates/{templateId}", async (
HttpContext context,
string templateId,
INotifyTemplateService templateService) =>
WorkerTemplateService templateService) =>
{
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
@@ -472,7 +477,7 @@ app.MapPut("/api/v2/notify/templates/{templateId}", async (
HttpContext context,
string templateId,
TemplateUpsertRequest request,
INotifyTemplateService templateService) =>
WorkerTemplateService templateService) =>
{
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
@@ -512,7 +517,7 @@ app.MapPut("/api/v2/notify/templates/{templateId}", async (
app.MapDelete("/api/v2/notify/templates/{templateId}", async (
HttpContext context,
string templateId,
INotifyTemplateService templateService) =>
WorkerTemplateService templateService) =>
{
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
@@ -520,7 +525,13 @@ app.MapDelete("/api/v2/notify/templates/{templateId}", async (
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
}
await templateService.DeleteAsync(tenantId, templateId, context.RequestAborted)
var actor = context.Request.Headers["X-StellaOps-Actor"].ToString();
if (string.IsNullOrWhiteSpace(actor))
{
actor = "api";
}
await templateService.DeleteAsync(tenantId, templateId, actor, context.RequestAborted)
.ConfigureAwait(false);
return Results.NoContent();
@@ -530,7 +541,9 @@ app.MapPost("/api/v2/notify/templates/{templateId}/preview", async (
HttpContext context,
string templateId,
TemplatePreviewRequest request,
INotifyTemplateService templateService) =>
WorkerTemplateService templateService,
WorkerTemplateRenderer renderer,
TimeProvider timeProvider) =>
{
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
@@ -546,17 +559,26 @@ app.MapPost("/api/v2/notify/templates/{templateId}/preview", async (
return Results.NotFound(Error("not_found", $"Template {templateId} not found.", context));
}
var options = new TemplateRenderOptions
var sampleEvent = NotifyEvent.Create(
eventId: Guid.NewGuid(),
kind: request.EventKind ?? "sample.event",
tenant: tenantId,
ts: timeProvider.GetUtcNow(),
payload: request.SamplePayload ?? new JsonObject(),
attributes: request.SampleAttributes ?? new Dictionary<string, string>(),
actor: "preview",
version: "1");
var rendered = await renderer.RenderAsync(template, sampleEvent, context.RequestAborted).ConfigureAwait(false);
return Results.Ok(new TemplatePreviewResponse
{
IncludeProvenance = request.IncludeProvenance ?? false,
ProvenanceBaseUrl = request.ProvenanceBaseUrl,
FormatOverride = request.FormatOverride
};
var result = await templateService.PreviewAsync(template, request.SamplePayload, options, context.RequestAborted)
.ConfigureAwait(false);
return Results.Ok(result);
RenderedBody = rendered.Body,
RenderedSubject = rendered.Subject,
BodyHash = rendered.BodyHash,
Format = rendered.Format.ToString(),
Warnings = null
});
});
// =============================================
@@ -631,7 +653,7 @@ app.MapPut("/api/v2/notify/rules/{ruleId}", async (
channel: a.Channel ?? string.Empty,
template: a.Template ?? string.Empty,
locale: a.Locale,
enabled: a.Enabled ?? true)).ToArray(),
enabled: a.Enabled)).ToArray(),
enabled: request.Enabled ?? true,
description: request.Description);
@@ -647,8 +669,8 @@ app.MapPut("/api/v2/notify/rules/{ruleId}", async (
EntityId = ruleId,
EntityType = "rule",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { ruleId, name = request.Name, enabled = request.Enabled }))
Payload = JsonSerializer.SerializeToNode(
new { ruleId, name = request.Name, enabled = request.Enabled }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -716,7 +738,7 @@ app.MapGet("/api/v2/notify/channels", async (
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
}
var channels = await channelRepository.ListAsync(tenantId, context.RequestAborted).ConfigureAwait(false);
var channels = await channelRepository.ListAsync(tenantId, cancellationToken: context.RequestAborted).ConfigureAwait(false);
return Results.Ok(new { items = channels, count = channels.Count });
});
@@ -789,8 +811,8 @@ app.MapPut("/api/v2/notify/channels/{channelId}", async (
EntityId = channelId,
EntityType = "channel",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { channelId, name = request.Name, type = request.Type }))
Payload = JsonSerializer.SerializeToNode(
new { channelId, name = request.Name, type = request.Type }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -1045,8 +1067,8 @@ app.MapPut("/api/v2/notify/quiet-hours/{scheduleId}", async (
EntityId = scheduleId,
EntityType = "quiet-hours",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { scheduleId, name = request.Name, enabled = request.Enabled }))
Payload = JsonSerializer.SerializeToNode(
new { scheduleId, name = request.Name, enabled = request.Enabled }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -1176,8 +1198,8 @@ app.MapPut("/api/v2/notify/maintenance-windows/{windowId}", async (
EntityId = windowId,
EntityType = "maintenance-window",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { windowId, name = request.Name, startsAt = request.StartsAt, endsAt = request.EndsAt }))
Payload = JsonSerializer.SerializeToNode(
new { windowId, name = request.Name, startsAt = request.StartsAt, endsAt = request.EndsAt }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -1306,8 +1328,8 @@ app.MapPut("/api/v2/notify/throttle-configs/{configId}", async (
EntityId = configId,
EntityType = "throttle-config",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { configId, name = request.Name, defaultWindow = request.DefaultWindow.TotalSeconds }))
Payload = JsonSerializer.SerializeToNode(
new { configId, name = request.Name, defaultWindow = request.DefaultWindow.TotalSeconds }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -1439,8 +1461,8 @@ app.MapPost("/api/v2/notify/overrides", async (
EntityId = overrideId,
EntityType = "operator-override",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { overrideId, overrideType = request.OverrideType, expiresAt = request.ExpiresAt, reason = request.Reason }))
Payload = JsonSerializer.SerializeToNode(
new { overrideId, overrideType = request.OverrideType, expiresAt = request.ExpiresAt, reason = request.Reason }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -1574,8 +1596,8 @@ app.MapPut("/api/v2/notify/escalation-policies/{policyId}", async (
EntityId = policyId,
EntityType = "escalation-policy",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { policyId, name = request.Name, enabled = request.Enabled }))
Payload = JsonSerializer.SerializeToNode(
new { policyId, name = request.Name, enabled = request.Enabled }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -1728,8 +1750,8 @@ app.MapPut("/api/v2/notify/oncall-schedules/{scheduleId}", async (
EntityId = scheduleId,
EntityType = "oncall-schedule",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { scheduleId, name = request.Name, enabled = request.Enabled }))
Payload = JsonSerializer.SerializeToNode(
new { scheduleId, name = request.Name, enabled = request.Enabled }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -1817,8 +1839,8 @@ app.MapPost("/api/v2/notify/oncall-schedules/{scheduleId}/overrides", async (
EntityId = scheduleId,
EntityType = "oncall-schedule",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { scheduleId, overrideId, userId = request.UserId }))
Payload = JsonSerializer.SerializeToNode(
new { scheduleId, overrideId, userId = request.UserId }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -2066,8 +2088,8 @@ app.MapPut("/api/v2/notify/localization/bundles/{bundleId}", async (
EntityId = bundleId,
EntityType = "localization-bundle",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { bundleId, locale = request.Locale, bundleKey = request.BundleKey }))
Payload = JsonSerializer.SerializeToNode(
new { bundleId, locale = request.Locale, bundleKey = request.BundleKey }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -2207,7 +2229,7 @@ app.MapPost("/api/v2/notify/storms/{stormKey}/summary", async (
var actor = context.Request.Headers["X-StellaOps-Actor"].ToString();
if (string.IsNullOrWhiteSpace(actor)) actor = "api";
var summary = await stormBreaker.TriggerSummaryAsync(tenantId, stormKey, context.RequestAborted).ConfigureAwait(false);
var summary = await stormBreaker.GenerateSummaryAsync(tenantId, stormKey, context.RequestAborted).ConfigureAwait(false);
if (summary is null)
{
@@ -2224,8 +2246,8 @@ app.MapPost("/api/v2/notify/storms/{stormKey}/summary", async (
EntityId = summary.SummaryId,
EntityType = "storm-summary",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { stormKey, eventCount = summary.EventCount }))
Payload = JsonSerializer.SerializeToNode(
new { stormKey, eventCount = summary.TotalEvents }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -2310,8 +2332,8 @@ app.MapPost("/api/v1/ack/{token}", async (
EntityId = verification.Token.DeliveryId,
EntityType = "delivery",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
JsonSerializer.Serialize(new { comment = request?.Comment, metadata = request?.Metadata }))
Payload = JsonSerializer.SerializeToNode(
new { comment = request?.Comment, metadata = request?.Metadata }) as JsonObject
};
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
}
@@ -2385,12 +2407,12 @@ app.MapPost("/api/v2/notify/security/ack-tokens/verify", (
app.MapPost("/api/v2/notify/security/html/validate", (
HttpContext context,
ValidateHtmlRequest request,
Contracts.ValidateHtmlRequest request,
IHtmlSanitizer htmlSanitizer) =>
{
if (string.IsNullOrWhiteSpace(request.Html))
{
return Results.Ok(new ValidateHtmlResponse
return Results.Ok(new Contracts.ValidateHtmlResponse
{
IsSafe = true,
Issues = []
@@ -2399,50 +2421,53 @@ app.MapPost("/api/v2/notify/security/html/validate", (
var result = htmlSanitizer.Validate(request.Html);
return Results.Ok(new ValidateHtmlResponse
return Results.Ok(new Contracts.ValidateHtmlResponse
{
IsSafe = result.IsSafe,
Issues = result.Issues.Select(i => new HtmlIssue
IsSafe = result.IsValid,
Issues = result.Errors.Select(i => new Contracts.HtmlIssue
{
Type = i.Type.ToString(),
Description = i.Description,
Element = i.ElementName,
Attribute = i.AttributeName
}).ToArray(),
Stats = result.Stats is not null ? new HtmlStats
Description = i.Message
}).Concat(result.Warnings.Select(w => new Contracts.HtmlIssue
{
CharacterCount = result.Stats.CharacterCount,
ElementCount = result.Stats.ElementCount,
MaxDepth = result.Stats.MaxDepth,
LinkCount = result.Stats.LinkCount,
ImageCount = result.Stats.ImageCount
} : null
Type = "Warning",
Description = w
})).ToArray(),
Stats = null
});
});
app.MapPost("/api/v2/notify/security/html/sanitize", (
HttpContext context,
SanitizeHtmlRequest request,
Contracts.SanitizeHtmlRequest request,
IHtmlSanitizer htmlSanitizer) =>
{
if (string.IsNullOrWhiteSpace(request.Html))
{
return Results.Ok(new SanitizeHtmlResponse
return Results.Ok(new Contracts.SanitizeHtmlResponse
{
SanitizedHtml = string.Empty,
WasModified = false
});
}
var options = new HtmlSanitizeOptions
var profile = new SanitizationProfile
{
Name = "api-request",
AllowDataUrls = request.AllowDataUrls,
AdditionalAllowedTags = request.AdditionalAllowedTags?.ToHashSet()
AllowedTags = request.AdditionalAllowedTags?.ToHashSet(StringComparer.OrdinalIgnoreCase)
?? SanitizationProfile.Basic.AllowedTags,
AllowedAttributes = SanitizationProfile.Basic.AllowedAttributes,
AllowedUrlSchemes = SanitizationProfile.Basic.AllowedUrlSchemes,
MaxContentLength = SanitizationProfile.Basic.MaxContentLength,
MaxNestingDepth = SanitizationProfile.Basic.MaxNestingDepth,
StripComments = SanitizationProfile.Basic.StripComments,
StripScripts = SanitizationProfile.Basic.StripScripts
};
var sanitized = htmlSanitizer.Sanitize(request.Html, options);
var sanitized = htmlSanitizer.Sanitize(request.Html, profile);
return Results.Ok(new SanitizeHtmlResponse
return Results.Ok(new Contracts.SanitizeHtmlResponse
{
SanitizedHtml = sanitized,
WasModified = !string.Equals(request.Html, sanitized, StringComparison.Ordinal)
@@ -2509,14 +2534,21 @@ app.MapGet("/api/v2/notify/security/webhook/{channelId}/secret", (
return Results.Ok(new { channelId, maskedSecret });
});
app.MapGet("/api/v2/notify/security/isolation/violations", (
app.MapGet("/api/v2/notify/security/isolation/violations", async (
HttpContext context,
ITenantIsolationValidator isolationValidator,
int? limit) =>
{
var violations = isolationValidator.GetRecentViolations(limit ?? 100);
var violations = await isolationValidator.GetViolationsAsync(
tenantId: null,
since: null,
cancellationToken: context.RequestAborted).ConfigureAwait(false);
return Results.Ok(new { items = violations, count = violations.Count });
var items = violations
.Take(limit.GetValueOrDefault(100))
.ToList();
return Results.Ok(new { items, count = items.Count });
});
// =============================================
@@ -2670,7 +2702,7 @@ app.MapGet("/api/v2/notify/dead-letter/{entryId}", async (
app.MapPost("/api/v2/notify/dead-letter/retry", async (
HttpContext context,
RetryDeadLetterRequest request,
Contracts.RetryDeadLetterRequest request,
IDeadLetterService deadLetterService) =>
{
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
@@ -2682,9 +2714,9 @@ app.MapPost("/api/v2/notify/dead-letter/retry", async (
var results = await deadLetterService.RetryBatchAsync(tenantId, request.EntryIds, context.RequestAborted)
.ConfigureAwait(false);
return Results.Ok(new RetryDeadLetterResponse
return Results.Ok(new Contracts.RetryDeadLetterResponse
{
Results = results.Select(r => new DeadLetterRetryResultItem
Results = results.Select(r => new Contracts.DeadLetterRetryResultItem
{
EntryId = r.EntryId,
Success = r.Success,