finish secrets finding work and audit remarks work save
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SecretFindingAlertTemplates.cs
|
||||
// Sprint: SPRINT_20260104_007_BE (Secret Detection Alert Integration)
|
||||
// Task: SDA-003 - Add secret-finding alert template
|
||||
// Task: SDA-004 - Implement Slack/Teams formatters
|
||||
// Description: Default templates for secret finding alert notifications
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notify.Engine.Templates;
|
||||
|
||||
/// <summary>
|
||||
/// Provides default templates for secret finding alert notifications.
|
||||
/// Templates support scanner.secret.finding event with severity-based styling.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Per SPRINT_20260104_007_BE tasks SDA-003 and SDA-004.
|
||||
/// </remarks>
|
||||
public static class SecretFindingAlertTemplates
|
||||
{
|
||||
/// <summary>
|
||||
/// Template key for secret finding notifications.
|
||||
/// </summary>
|
||||
public const string SecretFindingKey = "notification.scanner.secret.finding";
|
||||
|
||||
/// <summary>
|
||||
/// Get all default secret finding 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>();
|
||||
|
||||
// Channel-specific templates
|
||||
templates.Add(CreateSlackTemplate(tenantId, locale));
|
||||
templates.Add(CreateTeamsTemplate(tenantId, locale));
|
||||
templates.Add(CreateEmailTemplate(tenantId, locale));
|
||||
templates.Add(CreateWebhookTemplate(tenantId, locale));
|
||||
templates.Add(CreatePagerDutyTemplate(tenantId, locale));
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
#region Slack Template
|
||||
|
||||
private static NotifyTemplate CreateSlackTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-secret-finding-slack-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Slack,
|
||||
key: SecretFindingKey,
|
||||
locale: locale,
|
||||
body: SlackBody,
|
||||
renderMode: NotifyTemplateRenderMode.Json,
|
||||
format: NotifyDeliveryFormat.Slack,
|
||||
description: "Slack notification for secret detection findings",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:secret-templates");
|
||||
|
||||
private const string SlackBody = """
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Secret Detected in Container Scan",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Severity:*\n{{#if (eq severity \"Critical\")}}:rotating_light:{{/if}}{{#if (eq severity \"High\")}}:warning:{{/if}}{{#if (eq severity \"Medium\")}}:large_yellow_circle:{{/if}}{{#if (eq severity \"Low\")}}:information_source:{{/if}} {{severity}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Rule:*\n{{ruleName}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Category:*\n{{ruleCategory}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Rule ID:*\n`{{ruleId}}`"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Image:*\n`{{imageRef}}`"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*File:*\n`{{filePath}}:{{lineNumber}}`"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Detected Value (masked):*\n```{{maskedValue}}```"
|
||||
}
|
||||
},
|
||||
{{#if remediationGuidance}}
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Remediation:*\n{{remediationGuidance}}"
|
||||
}
|
||||
},
|
||||
{{/if}}
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "Scan ID: `{{scanId}}` | Triggered by: {{scanTriggeredBy}} | Detected: {{detectedAt}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{{#if findingUrl}}
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "View in StellaOps",
|
||||
"emoji": true
|
||||
},
|
||||
"url": "{{findingUrl}}",
|
||||
"style": "primary"
|
||||
}
|
||||
]
|
||||
}
|
||||
{{/if}}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Teams Template
|
||||
|
||||
private static NotifyTemplate CreateTeamsTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-secret-finding-teams-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Teams,
|
||||
key: SecretFindingKey,
|
||||
locale: locale,
|
||||
body: TeamsBody,
|
||||
renderMode: NotifyTemplateRenderMode.AdaptiveCard,
|
||||
format: NotifyDeliveryFormat.Teams,
|
||||
description: "Teams notification for secret detection findings",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:secret-templates");
|
||||
|
||||
private const string TeamsBody = """
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.4",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "Large",
|
||||
"weight": "Bolder",
|
||||
"text": "Secret Detected in Container Scan",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "FactSet",
|
||||
"facts": [
|
||||
{
|
||||
"title": "Severity",
|
||||
"value": "{{severity}}"
|
||||
},
|
||||
{
|
||||
"title": "Rule",
|
||||
"value": "{{ruleName}}"
|
||||
},
|
||||
{
|
||||
"title": "Category",
|
||||
"value": "{{ruleCategory}}"
|
||||
},
|
||||
{
|
||||
"title": "Image",
|
||||
"value": "{{imageRef}}"
|
||||
},
|
||||
{
|
||||
"title": "File",
|
||||
"value": "{{filePath}}:{{lineNumber}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "**Detected Value (masked):**",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{maskedValue}}",
|
||||
"fontType": "Monospace",
|
||||
"wrap": true
|
||||
},
|
||||
{{#if remediationGuidance}}
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "**Remediation:** {{remediationGuidance}}",
|
||||
"wrap": true
|
||||
},
|
||||
{{/if}}
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "Scan: {{scanId}} | By: {{scanTriggeredBy}} | At: {{detectedAt}}",
|
||||
"size": "Small",
|
||||
"isSubtle": true,
|
||||
"wrap": true
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{{#if findingUrl}}
|
||||
{
|
||||
"type": "Action.OpenUrl",
|
||||
"title": "View in StellaOps",
|
||||
"url": "{{findingUrl}}"
|
||||
}
|
||||
{{/if}}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Email Template
|
||||
|
||||
private static NotifyTemplate CreateEmailTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-secret-finding-email-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Email,
|
||||
key: SecretFindingKey,
|
||||
locale: locale,
|
||||
body: EmailBody,
|
||||
renderMode: NotifyTemplateRenderMode.Html,
|
||||
format: NotifyDeliveryFormat.Html,
|
||||
description: "Email notification for secret detection findings",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:secret-templates");
|
||||
|
||||
private const string EmailBody = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: {{#if (eq severity "Critical")}}#dc3545{{/if}}{{#if (eq severity "High")}}#fd7e14{{/if}}{{#if (eq severity "Medium")}}#ffc107{{/if}}{{#if (eq severity "Low")}}#17a2b8{{/if}}; color: white; padding: 15px; border-radius: 8px 8px 0 0; }
|
||||
.header h1 { margin: 0; font-size: 18px; }
|
||||
.content { background: #f8f9fa; padding: 20px; border-radius: 0 0 8px 8px; }
|
||||
.field { margin-bottom: 15px; }
|
||||
.field-label { font-weight: 600; color: #666; font-size: 12px; text-transform: uppercase; }
|
||||
.field-value { font-size: 14px; margin-top: 4px; }
|
||||
.code { background: #e9ecef; padding: 10px; border-radius: 4px; font-family: monospace; word-break: break-all; }
|
||||
.severity-critical { color: #dc3545; font-weight: bold; }
|
||||
.severity-high { color: #fd7e14; font-weight: bold; }
|
||||
.severity-medium { color: #ffc107; font-weight: bold; }
|
||||
.severity-low { color: #17a2b8; }
|
||||
.footer { font-size: 12px; color: #666; margin-top: 20px; padding-top: 15px; border-top: 1px solid #dee2e6; }
|
||||
.btn { display: inline-block; padding: 10px 20px; background: #0d6efd; color: white; text-decoration: none; border-radius: 4px; margin-top: 15px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Secret Detected in Container Scan</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="field">
|
||||
<div class="field-label">Severity</div>
|
||||
<div class="field-value severity-{{lowercase severity}}">{{severity}}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Rule</div>
|
||||
<div class="field-value">{{ruleName}} ({{ruleId}})</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Category</div>
|
||||
<div class="field-value">{{ruleCategory}}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Image</div>
|
||||
<div class="field-value code">{{imageRef}}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Location</div>
|
||||
<div class="field-value code">{{filePath}}:{{lineNumber}}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Detected Value (masked)</div>
|
||||
<div class="field-value code">{{maskedValue}}</div>
|
||||
</div>
|
||||
{{#if remediationGuidance}}
|
||||
<div class="field">
|
||||
<div class="field-label">Remediation</div>
|
||||
<div class="field-value">{{remediationGuidance}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if findingUrl}}
|
||||
<a href="{{findingUrl}}" class="btn">View in StellaOps</a>
|
||||
{{/if}}
|
||||
<div class="footer">
|
||||
Scan ID: {{scanId}}<br>
|
||||
Triggered by: {{scanTriggeredBy}}<br>
|
||||
Detected: {{detectedAt}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Webhook Template
|
||||
|
||||
private static NotifyTemplate CreateWebhookTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-secret-finding-webhook-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.Webhook,
|
||||
key: SecretFindingKey,
|
||||
locale: locale,
|
||||
body: WebhookBody,
|
||||
renderMode: NotifyTemplateRenderMode.Json,
|
||||
format: NotifyDeliveryFormat.Json,
|
||||
description: "Webhook notification for secret detection findings",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:secret-templates");
|
||||
|
||||
private const string WebhookBody = """
|
||||
{
|
||||
"event": "scanner.secret.finding",
|
||||
"version": "1.0",
|
||||
"timestamp": "{{detectedAt}}",
|
||||
"payload": {
|
||||
"eventId": "{{eventId}}",
|
||||
"tenantId": "{{tenantId}}",
|
||||
"scanId": "{{scanId}}",
|
||||
"imageRef": "{{imageRef}}",
|
||||
"imageDigest": "{{imageDigest}}",
|
||||
"finding": {
|
||||
"severity": "{{severity}}",
|
||||
"ruleId": "{{ruleId}}",
|
||||
"ruleName": "{{ruleName}}",
|
||||
"ruleCategory": "{{ruleCategory}}",
|
||||
"filePath": "{{filePath}}",
|
||||
"lineNumber": {{lineNumber}},
|
||||
"maskedValue": "{{maskedValue}}"
|
||||
},
|
||||
"context": {
|
||||
"triggeredBy": "{{scanTriggeredBy}}",
|
||||
"findingUrl": "{{findingUrl}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
#endregion
|
||||
|
||||
#region PagerDuty Template
|
||||
|
||||
private static NotifyTemplate CreatePagerDutyTemplate(string tenantId, string locale) =>
|
||||
NotifyTemplate.Create(
|
||||
templateId: $"tmpl-secret-finding-pagerduty-{tenantId}",
|
||||
tenantId: tenantId,
|
||||
channelType: NotifyChannelType.PagerDuty,
|
||||
key: SecretFindingKey,
|
||||
locale: locale,
|
||||
body: PagerDutyBody,
|
||||
renderMode: NotifyTemplateRenderMode.Json,
|
||||
format: NotifyDeliveryFormat.Json,
|
||||
description: "PagerDuty incident for secret detection findings",
|
||||
metadata: CreateMetadata("1.0.0"),
|
||||
createdBy: "system:secret-templates");
|
||||
|
||||
private const string PagerDutyBody = """
|
||||
{
|
||||
"routing_key": "{{pagerDutyRoutingKey}}",
|
||||
"event_action": "trigger",
|
||||
"dedup_key": "secret-{{tenantId}}-{{ruleId}}-{{imageRef}}",
|
||||
"payload": {
|
||||
"summary": "[{{severity}}] Secret detected: {{ruleName}} in {{imageRef}}",
|
||||
"severity": "{{#if (eq severity \"Critical\")}}critical{{/if}}{{#if (eq severity \"High\")}}error{{/if}}{{#if (eq severity \"Medium\")}}warning{{/if}}{{#if (eq severity \"Low\")}}info{{/if}}",
|
||||
"source": "stellaops-scanner",
|
||||
"component": "secret-detection",
|
||||
"group": "{{ruleCategory}}",
|
||||
"class": "{{ruleId}}",
|
||||
"custom_details": {
|
||||
"image_ref": "{{imageRef}}",
|
||||
"file_path": "{{filePath}}",
|
||||
"line_number": {{lineNumber}},
|
||||
"masked_value": "{{maskedValue}}",
|
||||
"scan_id": "{{scanId}}",
|
||||
"triggered_by": "{{scanTriggeredBy}}"
|
||||
}
|
||||
},
|
||||
"links": [
|
||||
{{#if findingUrl}}
|
||||
{
|
||||
"href": "{{findingUrl}}",
|
||||
"text": "View in StellaOps"
|
||||
}
|
||||
{{/if}}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
#endregion
|
||||
|
||||
private static IEnumerable<KeyValuePair<string, string>> CreateMetadata(string version) =>
|
||||
ImmutableDictionary<string, string>.Empty
|
||||
.Add("template-version", version)
|
||||
.Add("template-source", "system")
|
||||
.Add("template-category", "secret-detection");
|
||||
}
|
||||
Reference in New Issue
Block a user