added webhooks and templates to alerting, email is next

This commit is contained in:
2025-11-18 19:26:12 -06:00
parent 230094d7b2
commit 21254c3522
2 changed files with 477 additions and 5 deletions

View File

@@ -123,6 +123,58 @@
<hr class="my-4"> <hr class="my-4">
<!-- Webhook Template -->
<h5 class="card-title mb-3">Webhook Template</h5>
<div class="alert alert-info small">
<i class="bi bi-info-circle"></i>
Customize the webhook payload using Jinja2 templates. Leave empty to use the default JSON format.
</div>
<div class="mb-3">
<label for="preset_selector" class="form-label">Load Preset Template</label>
<select class="form-select" id="preset_selector">
<option value="">-- Select a preset --</option>
</select>
<div class="form-text">Choose from pre-built templates for popular services</div>
</div>
<div class="mb-3">
<label for="template_format" class="form-label">Template Format</label>
<select class="form-select" id="template_format" name="template_format">
<option value="json">JSON</option>
<option value="text">Plain Text</option>
</select>
<div class="form-text">Output format of the rendered template</div>
</div>
<div class="mb-3">
<label for="template" class="form-label">Template</label>
<textarea class="form-control font-monospace" id="template" name="template" rows="12"
placeholder="Leave empty for default format, or enter custom Jinja2 template..."></textarea>
<div class="form-text">
Available variables: <code>{{ "{{" }} alert.* {{ "}}" }}</code>, <code>{{ "{{" }} scan.* {{ "}}" }}</code>, <code>{{ "{{" }} rule.* {{ "}}" }}</code>
<a href="#" data-bs-toggle="modal" data-bs-target="#variablesModal">View all variables</a>
</div>
</div>
<div class="mb-3">
<label for="content_type_override" class="form-label">Custom Content-Type (optional)</label>
<input type="text" class="form-control font-monospace" id="content_type_override" name="content_type_override"
placeholder="e.g., application/json, text/plain, text/markdown">
<div class="form-text">Override the default Content-Type header (auto-detected from template format if not set)</div>
</div>
<div class="mb-3">
<button type="button" class="btn btn-outline-secondary btn-sm" id="preview-template-btn">
<i class="bi bi-eye"></i> Preview Template
</button>
<button type="button" class="btn btn-outline-secondary btn-sm ms-2" id="clear-template-btn">
<i class="bi bi-x-circle"></i> Clear Template
</button>
</div>
<hr class="my-4">
<!-- Advanced Settings --> <!-- Advanced Settings -->
<h5 class="card-title mb-3">Advanced Settings</h5> <h5 class="card-title mb-3">Advanced Settings</h5>
@@ -166,7 +218,7 @@
<h5 class="card-title"><i class="bi bi-info-circle"></i> Help</h5> <h5 class="card-title"><i class="bi bi-info-circle"></i> Help</h5>
<h6 class="mt-3">Payload Format</h6> <h6 class="mt-3">Payload Format</h6>
<p class="small text-muted">Webhooks receive JSON payloads with alert details:</p> <p class="small text-muted">Default JSON payload format (can be customized with templates):</p>
<pre class="small bg-dark text-light p-2 rounded"><code>{ <pre class="small bg-dark text-light p-2 rounded"><code>{
"event": "alert.created", "event": "alert.created",
"alert": { "alert": {
@@ -181,6 +233,9 @@
"rule": {...} "rule": {...}
}</code></pre> }</code></pre>
<h6 class="mt-3">Custom Templates</h6>
<p class="small text-muted">Use Jinja2 templates to customize payloads for services like Slack, Discord, Gotify, or create your own format. Select a preset or write a custom template.</p>
<h6 class="mt-3">Authentication Types</h6> <h6 class="mt-3">Authentication Types</h6>
<ul class="small"> <ul class="small">
<li><strong>None:</strong> No authentication</li> <li><strong>None:</strong> No authentication</li>
@@ -196,6 +251,92 @@
</div> </div>
</div> </div>
<!-- Template Variables Modal -->
<div class="modal fade" id="variablesModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Available Template Variables</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<h6>Alert Variables</h6>
<ul class="small font-monospace">
<li>{{ "{{" }} alert.id {{ "}}" }} - Alert ID</li>
<li>{{ "{{" }} alert.type {{ "}}" }} - Alert type (unexpected_port, cert_expiry, etc.)</li>
<li>{{ "{{" }} alert.severity {{ "}}" }} - Severity level (critical, warning, info)</li>
<li>{{ "{{" }} alert.message {{ "}}" }} - Human-readable alert message</li>
<li>{{ "{{" }} alert.ip_address {{ "}}" }} - IP address (if applicable)</li>
<li>{{ "{{" }} alert.port {{ "}}" }} - Port number (if applicable)</li>
<li>{{ "{{" }} alert.acknowledged {{ "}}" }} - Boolean: is acknowledged</li>
<li>{{ "{{" }} alert.created_at {{ "}}" }} - Alert creation timestamp</li>
</ul>
<h6 class="mt-3">Scan Variables</h6>
<ul class="small font-monospace">
<li>{{ "{{" }} scan.id {{ "}}" }} - Scan ID</li>
<li>{{ "{{" }} scan.title {{ "}}" }} - Scan title from config</li>
<li>{{ "{{" }} scan.timestamp {{ "}}" }} - Scan start time</li>
<li>{{ "{{" }} scan.duration {{ "}}" }} - Scan duration in seconds</li>
<li>{{ "{{" }} scan.status {{ "}}" }} - Scan status (running, completed, failed)</li>
<li>{{ "{{" }} scan.triggered_by {{ "}}" }} - How scan was triggered (manual, scheduled, api)</li>
</ul>
<h6 class="mt-3">Rule Variables</h6>
<ul class="small font-monospace">
<li>{{ "{{" }} rule.id {{ "}}" }} - Rule ID</li>
<li>{{ "{{" }} rule.name {{ "}}" }} - Rule name</li>
<li>{{ "{{" }} rule.type {{ "}}" }} - Rule type</li>
<li>{{ "{{" }} rule.threshold {{ "}}" }} - Rule threshold value</li>
<li>{{ "{{" }} rule.severity {{ "}}" }} - Rule severity</li>
</ul>
<h6 class="mt-3">App Variables</h6>
<ul class="small font-monospace">
<li>{{ "{{" }} app.name {{ "}}" }} - Application name</li>
<li>{{ "{{" }} app.version {{ "}}" }} - Application version</li>
<li>{{ "{{" }} app.url {{ "}}" }} - Repository URL</li>
</ul>
<h6 class="mt-3">Other Variables</h6>
<ul class="small font-monospace">
<li>{{ "{{" }} timestamp {{ "}}" }} - Current UTC timestamp</li>
</ul>
<h6 class="mt-3">Jinja2 Features</h6>
<p class="small">Templates support Jinja2 syntax including:</p>
<ul class="small">
<li>Conditionals: <code>{{ "{%" }} if alert.severity == 'critical' {{ "%}" }}...{{ "{%" }} endif {{ "%}" }}</code></li>
<li>Filters: <code>{{ "{{" }} alert.type|upper {{ "}}" }}</code>, <code>{{ "{{" }} alert.created_at.isoformat() {{ "}}" }}</code></li>
<li>Default values: <code>{{ "{{" }} alert.port|default('N/A') {{ "}}" }}</code></li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Template Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Template Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="small text-muted">Preview using sample data:</p>
<pre class="bg-dark text-light p-3 rounded" id="preview-output" style="max-height: 500px; overflow-y: auto;"><code></code></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
@@ -203,6 +344,106 @@
const webhookId = {{ webhook.id if webhook else 'null' }}; const webhookId = {{ webhook.id if webhook else 'null' }};
const mode = '{{ mode }}'; const mode = '{{ mode }}';
// Load template presets on page load
async function loadPresets() {
try {
const response = await fetch('/api/webhooks/template-presets');
const data = await response.json();
if (data.status === 'success') {
const selector = document.getElementById('preset_selector');
data.presets.forEach(preset => {
const option = document.createElement('option');
option.value = JSON.stringify({
template: preset.template,
format: preset.format,
content_type: preset.content_type
});
option.textContent = `${preset.name} - ${preset.description}`;
selector.appendChild(option);
});
}
} catch (error) {
console.error('Failed to load presets:', error);
}
}
// Handle preset selection
document.getElementById('preset_selector').addEventListener('change', function() {
if (!this.value) return;
try {
const preset = JSON.parse(this.value);
document.getElementById('template').value = preset.template;
document.getElementById('template_format').value = preset.format;
document.getElementById('content_type_override').value = preset.content_type;
} catch (error) {
console.error('Failed to load preset:', error);
}
});
// Handle preview template button
document.getElementById('preview-template-btn').addEventListener('click', async function() {
const template = document.getElementById('template').value.trim();
if (!template) {
alert('Please enter a template first');
return;
}
const templateFormat = document.getElementById('template_format').value;
try {
const response = await fetch('/api/webhooks/preview-template', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
template: template,
template_format: templateFormat
})
});
const result = await response.json();
if (result.status === 'success') {
// Display preview in modal
const output = document.querySelector('#preview-output code');
if (templateFormat === 'json') {
// Pretty print JSON
try {
const parsed = JSON.parse(result.rendered);
output.textContent = JSON.stringify(parsed, null, 2);
} catch (e) {
output.textContent = result.rendered;
}
} else {
output.textContent = result.rendered;
}
// Show modal
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
modal.show();
} else {
alert(`Preview failed: ${result.message}`);
}
} catch (error) {
console.error('Error previewing template:', error);
alert('Failed to preview template');
}
});
// Handle clear template button
document.getElementById('clear-template-btn').addEventListener('click', function() {
if (confirm('Clear template and reset to default format?')) {
document.getElementById('template').value = '';
document.getElementById('template_format').value = 'json';
document.getElementById('content_type_override').value = '';
document.getElementById('preset_selector').value = '';
}
});
// Load presets on page load
loadPresets();
// Show/hide auth fields based on type // Show/hide auth fields based on type
document.getElementById('auth_type').addEventListener('change', function() { document.getElementById('auth_type').addEventListener('change', function() {
const authType = this.value; const authType = this.value;
@@ -268,6 +509,17 @@ async function loadWebhookData(id) {
if (checkbox) checkbox.checked = true; if (checkbox) checkbox.checked = true;
}); });
} }
// Load template fields
if (webhook.template) {
document.getElementById('template').value = webhook.template;
}
if (webhook.template_format) {
document.getElementById('template_format').value = webhook.template_format;
}
if (webhook.content_type_override) {
document.getElementById('content_type_override').value = webhook.content_type_override;
}
} catch (error) { } catch (error) {
console.error('Error loading webhook:', error); console.error('Error loading webhook:', error);
alert('Failed to load webhook data'); alert('Failed to load webhook data');
@@ -318,6 +570,18 @@ document.getElementById('webhook-form').addEventListener('submit', async functio
formData.severity_filter = severities; formData.severity_filter = severities;
} }
// Add template fields
const template = document.getElementById('template').value.trim();
if (template) {
formData.template = template;
formData.template_format = document.getElementById('template_format').value;
const contentTypeOverride = document.getElementById('content_type_override').value.trim();
if (contentTypeOverride) {
formData.content_type_override = contentTypeOverride;
}
}
try { try {
const url = mode === 'edit' ? `/api/webhooks/${webhookId}` : '/api/webhooks'; const url = mode === 'edit' ? `/api/webhooks/${webhookId}` : '/api/webhooks';
const method = mode === 'edit' ? 'PUT' : 'POST'; const method = mode === 'edit' ? 'PUT' : 'POST';

View File

@@ -2195,6 +2195,9 @@ Create a new webhook configuration.
| `severity_filter` | array | No | Array of severities to filter (empty = all severities) | | `severity_filter` | array | No | Array of severities to filter (empty = all severities) |
| `timeout` | integer | No | Request timeout in seconds (default: 10) | | `timeout` | integer | No | Request timeout in seconds (default: 10) |
| `retry_count` | integer | No | Number of retry attempts (default: 3) | | `retry_count` | integer | No | Number of retry attempts (default: 3) |
| `template` | string | No | Jinja2 template for custom payload (leave empty for default format) |
| `template_format` | string | No | Template output format: json, text (default: json) |
| `content_type_override` | string | No | Custom Content-Type header (auto-detected if not specified) |
**Success Response (201 Created):** **Success Response (201 Created):**
```json ```json
@@ -2258,6 +2261,19 @@ curl -X POST http://localhost:5000/api/webhooks \
"auth_token": "your-secret-token" "auth_token": "your-secret-token"
}' \ }' \
-b cookies.txt -b cookies.txt
# Create webhook with custom template
curl -X POST http://localhost:5000/api/webhooks \
-H "Content-Type: application/json" \
-d '{
"name": "Gotify Notifications",
"url": "https://gotify.example.com/message",
"template": "{\"title\": \"{{ scan.title }}\", \"message\": \"{{ alert.message }}\", \"priority\": {% if alert.severity == \"critical\" %}5{% else %}3{% endif %}}",
"template_format": "json",
"auth_type": "custom",
"custom_headers": {"X-Gotify-Key": "your-app-token"}
}' \
-b cookies.txt
``` ```
### Update Webhook ### Update Webhook
@@ -2421,6 +2437,126 @@ curl -X POST http://localhost:5000/api/webhooks/1/test \
-b cookies.txt -b cookies.txt
``` ```
### Preview Template
Preview a webhook template with sample data before saving.
**Endpoint:** `POST /api/webhooks/preview-template`
**Authentication:** Required
**Request Body:**
```json
{
"template": "{\"title\": \"{{ scan.title }}\", \"message\": \"{{ alert.message }}\"}",
"template_format": "json"
}
```
**Request Body Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `template` | string | Yes | Jinja2 template to preview |
| `template_format` | string | No | Output format: json or text (default: json) |
**Success Response (200 OK):**
```json
{
"status": "success",
"rendered": "{\"title\": \"Production Infrastructure Scan\", \"message\": \"Unexpected port 8080 found open on 192.168.1.100\"}",
"format": "json"
}
```
**Error Responses:**
*400 Bad Request* - Template validation error:
```json
{
"status": "error",
"message": "Template validation error: Unexpected '}}' at line 2"
}
```
*400 Bad Request* - Template rendering error:
```json
{
"status": "error",
"message": "Template rendering error: undefined variable 'invalid_var'"
}
```
**Usage Example:**
```bash
curl -X POST http://localhost:5000/api/webhooks/preview-template \
-H "Content-Type: application/json" \
-d '{
"template": "{\"title\": \"Alert\", \"message\": \"{{ alert.message }}\"}",
"template_format": "json"
}' \
-b cookies.txt
```
### Get Template Presets
Get list of available webhook template presets for popular services.
**Endpoint:** `GET /api/webhooks/template-presets`
**Authentication:** Required
**Success Response (200 OK):**
```json
{
"status": "success",
"presets": [
{
"id": "default_json",
"name": "Default JSON (Current Format)",
"description": "Standard webhook payload format matching the current implementation",
"format": "json",
"content_type": "application/json",
"category": "general",
"template": "{\n \"event\": \"alert.created\",\n \"alert\": {...}\n}"
},
{
"id": "gotify",
"name": "Gotify",
"description": "Optimized for Gotify push notification server with markdown support",
"format": "json",
"content_type": "application/json",
"category": "service",
"template": "{\n \"title\": \"{{ scan.title }}\",\n \"message\": \"{{ alert.message }}\",\n \"priority\": {% if alert.severity == 'critical' %}8{% else %}5{% endif %}\n}"
},
{
"id": "slack",
"name": "Slack",
"description": "Rich Block Kit format for Slack webhooks with visual formatting",
"format": "json",
"content_type": "application/json",
"category": "service",
"template": "..."
}
]
}
```
**Available Presets:**
- `default_json` - Standard JSON format (backward compatible)
- `custom_json` - Flexible custom format with title/message/priority
- `gotify` - Gotify push notification server format
- `ntfy` - Simple text format for Ntfy pub-sub service
- `slack` - Slack Block Kit format with rich formatting
- `discord` - Discord embedded message format
- `plain_text` - Simple plain text format
**Usage Example:**
```bash
curl -X GET http://localhost:5000/api/webhooks/template-presets \
-b cookies.txt
```
### Get Webhook Delivery Logs ### Get Webhook Delivery Logs
Get delivery history for a specific webhook. Get delivery history for a specific webhook.
@@ -2508,7 +2644,9 @@ curl -X GET "http://localhost:5000/api/webhooks/1/logs?page=2" \
### Webhook Payload Format ### Webhook Payload Format
When alerts are triggered, webhooks receive JSON payloads in this format: #### Default Format
When alerts are triggered, webhooks without custom templates receive JSON payloads in this default format:
```json ```json
{ {
@@ -2538,6 +2676,76 @@ When alerts are triggered, webhooks receive JSON payloads in this format:
} }
``` ```
#### Custom Templates
Webhooks support custom Jinja2 templates to customize the payload format for different services. Templates have access to the following variables:
**Available Template Variables:**
| Variable | Description | Example |
|----------|-------------|---------|
| `{{ alert.id }}` | Alert ID | `123` |
| `{{ alert.type }}` | Alert type | `"cert_expiry"`, `"unexpected_port"` |
| `{{ alert.severity }}` | Severity level | `"critical"`, `"warning"`, `"info"` |
| `{{ alert.message }}` | Human-readable message | `"Certificate expires in 15 days"` |
| `{{ alert.ip_address }}` | IP address | `"192.168.1.10"` |
| `{{ alert.port }}` | Port number | `443` |
| `{{ alert.acknowledged }}` | Is acknowledged | `true`, `false` |
| `{{ alert.created_at }}` | Creation timestamp | `datetime` object |
| `{{ scan.id }}` | Scan ID | `42` |
| `{{ scan.title }}` | Scan title | `"Production Network Scan"` |
| `{{ scan.status }}` | Scan status | `"completed"`, `"running"`, `"failed"` |
| `{{ scan.duration }}` | Scan duration (seconds) | `125.5` |
| `{{ scan.triggered_by }}` | How scan was triggered | `"manual"`, `"scheduled"`, `"api"` |
| `{{ rule.id }}` | Rule ID | `3` |
| `{{ rule.name }}` | Rule name | `"Certificate Expiry Warning"` |
| `{{ rule.type }}` | Rule type | `"cert_expiry"` |
| `{{ rule.threshold }}` | Rule threshold | `30` |
| `{{ app.name }}` | Application name | `"SneakyScanner"` |
| `{{ app.version }}` | Application version | `"1.0.0-phase5"` |
| `{{ app.url }}` | Repository URL | `"https://github..."` |
| `{{ timestamp }}` | Current UTC timestamp | `datetime` object |
**Jinja2 Features:**
- Conditionals: `{% if alert.severity == 'critical' %}...{% endif %}`
- Filters: `{{ alert.type|upper }}`, `{{ alert.created_at.isoformat() }}`
- Default values: `{{ alert.port|default('N/A') }}`
**Template Examples:**
*Custom JSON with priority:*
```jinja2
{
"title": "{{ scan.title }}",
"message": "{{ alert.message }}",
"priority": {% if alert.severity == 'critical' %}5{% elif alert.severity == 'warning' %}3{% else %}1{% endif %},
"alert_id": {{ alert.id }}
}
```
*Plain text format:*
```jinja2
ALERT: {{ alert.severity|upper }}
{{ alert.message }}
Scan: {{ scan.title }}
Rule: {{ rule.name }}
Time: {{ timestamp.strftime('%Y-%m-%d %H:%M:%S UTC') }}
```
*Gotify format:*
```jinja2
{
"title": "{{ scan.title }}",
"message": "**{{ alert.severity|upper }}**: {{ alert.message }}",
"priority": {% if alert.severity == 'critical' %}8{% elif alert.severity == 'warning' %}5{% else %}2{% endif %},
"extras": {
"client::display": {"contentType": "text/markdown"},
"alert_id": {{ alert.id }}
}
}
```
### Authentication Types ### Authentication Types
**None:** **None:**
@@ -2840,8 +3048,8 @@ API versioning will be implemented in future phases. For now, the API is conside
- **Webhooks API** - Webhook management, delivery tracking, authentication support, retry logic - **Webhooks API** - Webhook management, delivery tracking, authentication support, retry logic
### Endpoint Count ### Endpoint Count
- Total endpoints: 65+ - Total endpoints: 67+
- Authenticated endpoints: 60+ - Authenticated endpoints: 62+
- Public endpoints: 5 (login, setup, health checks) - Public endpoints: 5 (login, setup, health checks)
--- ---
@@ -2855,5 +3063,5 @@ For issues, questions, or feature requests:
--- ---
**Last Updated:** 2025-11-18 **Last Updated:** 2025-11-18
**Phase:** 5 - Alerts Management **Phase:** 5 - Alerts Management & Custom Webhook Templates
**Next Update:** Phase 6 - Future Enhancements **Next Update:** Phase 6 - Future Enhancements