Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -0,0 +1,531 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BudgetAlertTemplates.cs
|
||||
// Sprint: SPRINT_20251226_002_BE_budget_enforcement
|
||||
// Task: BUDGET-07 - Notification templates for budget alerts
|
||||
// Description: Default templates for risk budget warning and exceeded alerts
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notify.Engine.Templates;
|
||||
|
||||
/// <summary>
|
||||
/// Provides default templates for risk budget alert notifications.
|
||||
/// Templates support policy.budget.warning and policy.budget.exceeded events.
|
||||
/// </summary>
|
||||
public static class BudgetAlertTemplates
|
||||
{
|
||||
/// <summary>
|
||||
/// Template key for budget warning notifications.
|
||||
/// </summary>
|
||||
public const string BudgetWarningKey = "notification.policy.budget.warning";
|
||||
|
||||
/// <summary>
|
||||
/// Template key for budget exceeded notifications.
|
||||
/// </summary>
|
||||
public const string BudgetExceededKey = "notification.policy.budget.exceeded";
|
||||
|
||||
/// <summary>
|
||||
/// Get all default budget alert templates for a tenant.
|
||||
/// </summary>
|
||||
/// <param name="tenantId">Tenant identifier.</param>
|
||||
/// <param name="locale">Locale code (default: en-us).</param>
|
||||
/// <returns>Collection of default templates.</returns>
|
||||
public static IReadOnlyList<NotifyTemplate> GetDefaultTemplates(
|
||||
string tenantId,
|
||||
string locale = "en-us")
|
||||
{
|
||||
var templates = new List<NotifyTemplate>();
|
||||
|
||||
// Add warning templates
|
||||
templates.Add(CreateSlackWarningTemplate(tenantId, locale));
|
||||
templates.Add(CreateTeamsWarningTemplate(tenantId, locale));
|
||||
templates.Add(CreateEmailWarningTemplate(tenantId, locale));
|
||||
templates.Add(CreateWebhookWarningTemplate(tenantId, locale));
|
||||
|
||||
// Add exceeded templates
|
||||
templates.Add(CreateSlackExceededTemplate(tenantId, locale));
|
||||
templates.Add(CreateTeamsExceededTemplate(tenantId, locale));
|
||||
templates.Add(CreateEmailExceededTemplate(tenantId, locale));
|
||||
templates.Add(CreateWebhookExceededTemplate(tenantId, locale));
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
#region Warning Templates
|
||||
|
||||
private static NotifyTemplate CreateSlackWarningTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-budget-warning-slack-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Slack,
|
||||
key: BudgetWarningKey,
|
||||
locale: locale,
|
||||
body: SlackWarningBody,
|
||||
renderMode: NotifyTemplateRenderMode.Markdown,
|
||||
format: NotifyDeliveryFormat.Slack,
|
||||
description: "Slack notification for risk budget warning threshold crossed",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:budget-templates");
|
||||
|
||||
private static NotifyTemplate CreateTeamsWarningTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-budget-warning-teams-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Teams,
|
||||
key: BudgetWarningKey,
|
||||
locale: locale,
|
||||
body: TeamsWarningBody,
|
||||
renderMode: NotifyTemplateRenderMode.Markdown,
|
||||
format: NotifyDeliveryFormat.Teams,
|
||||
description: "Teams notification for risk budget warning threshold crossed",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:budget-templates");
|
||||
|
||||
private static NotifyTemplate CreateEmailWarningTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-budget-warning-email-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Email,
|
||||
key: BudgetWarningKey,
|
||||
locale: locale,
|
||||
body: EmailWarningBody,
|
||||
renderMode: NotifyTemplateRenderMode.Html,
|
||||
format: NotifyDeliveryFormat.Html,
|
||||
description: "Email notification for risk budget warning threshold crossed",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:budget-templates");
|
||||
|
||||
private static NotifyTemplate CreateWebhookWarningTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-budget-warning-webhook-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Webhook,
|
||||
key: BudgetWarningKey,
|
||||
locale: locale,
|
||||
body: WebhookWarningBody,
|
||||
renderMode: NotifyTemplateRenderMode.None,
|
||||
format: NotifyDeliveryFormat.Json,
|
||||
description: "Webhook notification for risk budget warning threshold crossed",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:budget-templates");
|
||||
|
||||
#endregion
|
||||
|
||||
#region Exceeded Templates
|
||||
|
||||
private static NotifyTemplate CreateSlackExceededTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-budget-exceeded-slack-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Slack,
|
||||
key: BudgetExceededKey,
|
||||
locale: locale,
|
||||
body: SlackExceededBody,
|
||||
renderMode: NotifyTemplateRenderMode.Markdown,
|
||||
format: NotifyDeliveryFormat.Slack,
|
||||
description: "Slack notification for risk budget exceeded",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:budget-templates");
|
||||
|
||||
private static NotifyTemplate CreateTeamsExceededTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-budget-exceeded-teams-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Teams,
|
||||
key: BudgetExceededKey,
|
||||
locale: locale,
|
||||
body: TeamsExceededBody,
|
||||
renderMode: NotifyTemplateRenderMode.Markdown,
|
||||
format: NotifyDeliveryFormat.Teams,
|
||||
description: "Teams notification for risk budget exceeded",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:budget-templates");
|
||||
|
||||
private static NotifyTemplate CreateEmailExceededTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-budget-exceeded-email-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Email,
|
||||
key: BudgetExceededKey,
|
||||
locale: locale,
|
||||
body: EmailExceededBody,
|
||||
renderMode: NotifyTemplateRenderMode.Html,
|
||||
format: NotifyDeliveryFormat.Html,
|
||||
description: "Email notification for risk budget exceeded",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:budget-templates");
|
||||
|
||||
private static NotifyTemplate CreateWebhookExceededTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-budget-exceeded-webhook-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Webhook,
|
||||
key: BudgetExceededKey,
|
||||
locale: locale,
|
||||
body: WebhookExceededBody,
|
||||
renderMode: NotifyTemplateRenderMode.None,
|
||||
format: NotifyDeliveryFormat.Json,
|
||||
description: "Webhook notification for risk budget exceeded",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:budget-templates");
|
||||
|
||||
#endregion
|
||||
|
||||
#region Template Bodies
|
||||
|
||||
private const string SlackWarningBody = """
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": ":warning: Risk Budget Warning",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Service:*\n{{payload.serviceId}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Status:*\n{{payload.status | uppercase}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Consumed:*\n{{payload.consumed}} / {{payload.allocated}} points"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Usage:*\n{{payload.percentageUsed}}%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": ":chart_with_upwards_trend: Budget window: *{{payload.window}}* | Tier: *{{payload.tier}}*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "Remaining: {{payload.remaining}} points | {{payload.timestamp}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SlackExceededBody = """
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": ":rotating_light: Risk Budget EXHAUSTED",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Service {{payload.serviceId}} has exhausted its risk budget!*\nNew high-risk releases may be blocked until budget resets."
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Consumed:*\n{{payload.consumed}} / {{payload.allocated}} points"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Overage:*\n{{#if payload.remaining}}{{payload.remaining | abs}} points over{{else}}At limit{{/if}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Window:*\n{{payload.window}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Tier:*\n{{payload.tier}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "divider"
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": ":bulb: *Actions:*\n• Review pending releases for risk reduction\n• Request an exception if critical\n• Wait for next budget window"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "Alert generated at {{payload.timestamp}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
private const string TeamsWarningBody = """
|
||||
{
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
"themeColor": "FFA500",
|
||||
"summary": "Risk Budget Warning - {{payload.serviceId}}",
|
||||
"sections": [
|
||||
{
|
||||
"activityTitle": "⚠️ Risk Budget Warning",
|
||||
"activitySubtitle": "Service: {{payload.serviceId}}",
|
||||
"facts": [
|
||||
{ "name": "Status", "value": "{{payload.status | uppercase}}" },
|
||||
{ "name": "Consumed", "value": "{{payload.consumed}} / {{payload.allocated}} points" },
|
||||
{ "name": "Usage", "value": "{{payload.percentageUsed}}%" },
|
||||
{ "name": "Remaining", "value": "{{payload.remaining}} points" },
|
||||
{ "name": "Window", "value": "{{payload.window}}" },
|
||||
{ "name": "Tier", "value": "{{payload.tier}}" }
|
||||
],
|
||||
"markdown": true
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
private const string TeamsExceededBody = """
|
||||
{
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
"themeColor": "FF0000",
|
||||
"summary": "Risk Budget EXHAUSTED - {{payload.serviceId}}",
|
||||
"sections": [
|
||||
{
|
||||
"activityTitle": "🚨 Risk Budget EXHAUSTED",
|
||||
"activitySubtitle": "Service: {{payload.serviceId}}",
|
||||
"activityText": "This service has exhausted its risk budget. New high-risk releases may be blocked until the budget resets.",
|
||||
"facts": [
|
||||
{ "name": "Consumed", "value": "{{payload.consumed}} / {{payload.allocated}} points" },
|
||||
{ "name": "Window", "value": "{{payload.window}}" },
|
||||
{ "name": "Tier", "value": "{{payload.tier}}" }
|
||||
],
|
||||
"markdown": true
|
||||
},
|
||||
{
|
||||
"text": "**Recommended Actions:**\n- Review pending releases for risk reduction\n- Request an exception if release is critical\n- Wait for next budget window reset"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
private const string EmailWarningBody = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
||||
.container { max-width: 600px; margin: 0 auto; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.header { background: #FFA500; color: white; padding: 20px; }
|
||||
.header h1 { margin: 0; font-size: 24px; }
|
||||
.content { padding: 20px; }
|
||||
.stats { display: flex; flex-wrap: wrap; gap: 20px; margin: 20px 0; }
|
||||
.stat { flex: 1; min-width: 120px; background: #f8f9fa; padding: 15px; border-radius: 4px; }
|
||||
.stat-label { font-size: 12px; color: #666; text-transform: uppercase; }
|
||||
.stat-value { font-size: 24px; font-weight: bold; color: #333; }
|
||||
.progress { background: #e9ecef; border-radius: 4px; height: 8px; margin: 20px 0; overflow: hidden; }
|
||||
.progress-bar { background: #FFA500; height: 100%; transition: width 0.3s; }
|
||||
.footer { padding: 15px 20px; background: #f8f9fa; font-size: 12px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>⚠️ Risk Budget Warning</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>The risk budget for <strong>{{payload.serviceId}}</strong> has crossed the <strong>{{payload.status}}</strong> threshold.</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-label">Consumed</div>
|
||||
<div class="stat-value">{{payload.consumed}}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Allocated</div>
|
||||
<div class="stat-value">{{payload.allocated}}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Remaining</div>
|
||||
<div class="stat-value">{{payload.remaining}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: {{payload.percentageUsed}}%"></div>
|
||||
</div>
|
||||
<p style="text-align: center; color: #666;">{{payload.percentageUsed}}% of budget consumed</p>
|
||||
|
||||
<p><strong>Window:</strong> {{payload.window}} | <strong>Tier:</strong> {{payload.tier}}</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
Alert generated at {{payload.timestamp}} by StellaOps Policy Engine
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
|
||||
private const string EmailExceededBody = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
||||
.container { max-width: 600px; margin: 0 auto; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.header { background: #DC3545; color: white; padding: 20px; }
|
||||
.header h1 { margin: 0; font-size: 24px; }
|
||||
.content { padding: 20px; }
|
||||
.alert-box { background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 15px; margin: 15px 0; }
|
||||
.stats { display: flex; flex-wrap: wrap; gap: 20px; margin: 20px 0; }
|
||||
.stat { flex: 1; min-width: 120px; background: #f8f9fa; padding: 15px; border-radius: 4px; }
|
||||
.stat-label { font-size: 12px; color: #666; text-transform: uppercase; }
|
||||
.stat-value { font-size: 24px; font-weight: bold; color: #333; }
|
||||
.actions { background: #e7f3ff; border-radius: 4px; padding: 15px; margin: 15px 0; }
|
||||
.actions h3 { margin: 0 0 10px 0; color: #0066cc; }
|
||||
.actions ul { margin: 0; padding-left: 20px; }
|
||||
.footer { padding: 15px 20px; background: #f8f9fa; font-size: 12px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚨 Risk Budget EXHAUSTED</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="alert-box">
|
||||
<strong>Service {{payload.serviceId}} has exhausted its risk budget!</strong><br>
|
||||
New high-risk releases (G3+) will be blocked until the budget resets.
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-label">Consumed</div>
|
||||
<div class="stat-value">{{payload.consumed}}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Allocated</div>
|
||||
<div class="stat-value">{{payload.allocated}}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Status</div>
|
||||
<div class="stat-value" style="color: #DC3545;">EXHAUSTED</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><strong>Window:</strong> {{payload.window}} | <strong>Tier:</strong> {{payload.tier}}</p>
|
||||
|
||||
<div class="actions">
|
||||
<h3>Recommended Actions</h3>
|
||||
<ul>
|
||||
<li>Review pending releases for risk reduction opportunities</li>
|
||||
<li>Request an exception if the release is business-critical</li>
|
||||
<li>Wait for the next budget window to reset</li>
|
||||
<li>Contact your security team for guidance</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
Alert generated at {{payload.timestamp}} by StellaOps Policy Engine
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
|
||||
private const string WebhookWarningBody = """
|
||||
{
|
||||
"event": "policy.budget.warning",
|
||||
"severity": "{{payload.severity}}",
|
||||
"service": {
|
||||
"id": "{{payload.serviceId}}",
|
||||
"tier": {{payload.tier}}
|
||||
},
|
||||
"budget": {
|
||||
"id": "{{payload.budgetId}}",
|
||||
"window": "{{payload.window}}",
|
||||
"allocated": {{payload.allocated}},
|
||||
"consumed": {{payload.consumed}},
|
||||
"remaining": {{payload.remaining}},
|
||||
"percentageUsed": {{payload.percentageUsed}},
|
||||
"status": "{{payload.status}}",
|
||||
"previousStatus": "{{payload.previousStatus}}"
|
||||
},
|
||||
"timestamp": "{{payload.timestamp}}"
|
||||
}
|
||||
""";
|
||||
|
||||
private const string WebhookExceededBody = """
|
||||
{
|
||||
"event": "policy.budget.exceeded",
|
||||
"severity": "critical",
|
||||
"service": {
|
||||
"id": "{{payload.serviceId}}",
|
||||
"tier": {{payload.tier}}
|
||||
},
|
||||
"budget": {
|
||||
"id": "{{payload.budgetId}}",
|
||||
"window": "{{payload.window}}",
|
||||
"allocated": {{payload.allocated}},
|
||||
"consumed": {{payload.consumed}},
|
||||
"remaining": {{payload.remaining}},
|
||||
"percentageUsed": {{payload.percentageUsed}},
|
||||
"status": "exhausted"
|
||||
},
|
||||
"impact": {
|
||||
"blockingEnabled": true,
|
||||
"affectedRiskLevels": ["G3", "G4", "G5"]
|
||||
},
|
||||
"timestamp": "{{payload.timestamp}}"
|
||||
}
|
||||
""";
|
||||
|
||||
#endregion
|
||||
|
||||
private static IEnumerable<KeyValuePair<string, string>> CreateMetadata(string version) =>
|
||||
new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("version", version),
|
||||
new KeyValuePair<string, string>("source", "budget-alert-templates"),
|
||||
new KeyValuePair<string, string>("sprint", "SPRINT_20251226_002_BE_budget_enforcement")
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user