diff --git a/app/web/templates/webhooks/form.html b/app/web/templates/webhooks/form.html
index 5595055..df350c9 100644
--- a/app/web/templates/webhooks/form.html
+++ b/app/web/templates/webhooks/form.html
@@ -123,6 +123,58 @@
+
+ Webhook Template
+
+
+ Customize the webhook payload using Jinja2 templates. Leave empty to use the default JSON format.
+
+
+
+
Load Preset Template
+
+ -- Select a preset --
+
+
Choose from pre-built templates for popular services
+
+
+
+
Template Format
+
+ JSON
+ Plain Text
+
+
Output format of the rendered template
+
+
+
+
Template
+
+
+ Available variables:
{{ "{{" }} alert.* {{ "}}" }},
{{ "{{" }} scan.* {{ "}}" }},
{{ "{{" }} rule.* {{ "}}" }}
+
View all variables
+
+
+
+
+
Custom Content-Type (optional)
+
+
Override the default Content-Type header (auto-detected from template format if not set)
+
+
+
+
+ Preview Template
+
+
+ Clear Template
+
+
+
+
+
Advanced Settings
@@ -166,7 +218,7 @@
Help
Payload Format
- Webhooks receive JSON payloads with alert details:
+ Default JSON payload format (can be customized with templates):
{
"event": "alert.created",
"alert": {
@@ -181,6 +233,9 @@
"rule": {...}
}
+ Custom Templates
+ 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.
+
Authentication Types
None: No authentication
@@ -196,6 +251,92 @@
+
+
+
+
+
+
+
Alert Variables
+
+ {{ "{{" }} alert.id {{ "}}" }} - Alert ID
+ {{ "{{" }} alert.type {{ "}}" }} - Alert type (unexpected_port, cert_expiry, etc.)
+ {{ "{{" }} alert.severity {{ "}}" }} - Severity level (critical, warning, info)
+ {{ "{{" }} alert.message {{ "}}" }} - Human-readable alert message
+ {{ "{{" }} alert.ip_address {{ "}}" }} - IP address (if applicable)
+ {{ "{{" }} alert.port {{ "}}" }} - Port number (if applicable)
+ {{ "{{" }} alert.acknowledged {{ "}}" }} - Boolean: is acknowledged
+ {{ "{{" }} alert.created_at {{ "}}" }} - Alert creation timestamp
+
+
+
Scan Variables
+
+ {{ "{{" }} scan.id {{ "}}" }} - Scan ID
+ {{ "{{" }} scan.title {{ "}}" }} - Scan title from config
+ {{ "{{" }} scan.timestamp {{ "}}" }} - Scan start time
+ {{ "{{" }} scan.duration {{ "}}" }} - Scan duration in seconds
+ {{ "{{" }} scan.status {{ "}}" }} - Scan status (running, completed, failed)
+ {{ "{{" }} scan.triggered_by {{ "}}" }} - How scan was triggered (manual, scheduled, api)
+
+
+
Rule Variables
+
+ {{ "{{" }} rule.id {{ "}}" }} - Rule ID
+ {{ "{{" }} rule.name {{ "}}" }} - Rule name
+ {{ "{{" }} rule.type {{ "}}" }} - Rule type
+ {{ "{{" }} rule.threshold {{ "}}" }} - Rule threshold value
+ {{ "{{" }} rule.severity {{ "}}" }} - Rule severity
+
+
+
App Variables
+
+ {{ "{{" }} app.name {{ "}}" }} - Application name
+ {{ "{{" }} app.version {{ "}}" }} - Application version
+ {{ "{{" }} app.url {{ "}}" }} - Repository URL
+
+
+
Other Variables
+
+ {{ "{{" }} timestamp {{ "}}" }} - Current UTC timestamp
+
+
+
Jinja2 Features
+
Templates support Jinja2 syntax including:
+
+ Conditionals: {{ "{%" }} if alert.severity == 'critical' {{ "%}" }}...{{ "{%" }} endif {{ "%}" }}
+ Filters: {{ "{{" }} alert.type|upper {{ "}}" }}, {{ "{{" }} alert.created_at.isoformat() {{ "}}" }}
+ Default values: {{ "{{" }} alert.port|default('N/A') {{ "}}" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Preview using sample data:
+
+
+
+
+
+
+
{% endblock %}
{% block scripts %}
@@ -203,6 +344,106 @@
const webhookId = {{ webhook.id if webhook else 'null' }};
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
document.getElementById('auth_type').addEventListener('change', function() {
const authType = this.value;
@@ -268,6 +509,17 @@ async function loadWebhookData(id) {
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) {
console.error('Error loading webhook:', error);
alert('Failed to load webhook data');
@@ -318,6 +570,18 @@ document.getElementById('webhook-form').addEventListener('submit', async functio
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 {
const url = mode === 'edit' ? `/api/webhooks/${webhookId}` : '/api/webhooks';
const method = mode === 'edit' ? 'PUT' : 'POST';
diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md
index e8c519b..47e73e9 100644
--- a/docs/API_REFERENCE.md
+++ b/docs/API_REFERENCE.md
@@ -2195,6 +2195,9 @@ Create a new webhook configuration.
| `severity_filter` | array | No | Array of severities to filter (empty = all severities) |
| `timeout` | integer | No | Request timeout in seconds (default: 10) |
| `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):**
```json
@@ -2258,6 +2261,19 @@ curl -X POST http://localhost:5000/api/webhooks \
"auth_token": "your-secret-token"
}' \
-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
@@ -2421,6 +2437,126 @@ curl -X POST http://localhost:5000/api/webhooks/1/test \
-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 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
-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
{
@@ -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
**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
### Endpoint Count
-- Total endpoints: 65+
-- Authenticated endpoints: 60+
+- Total endpoints: 67+
+- Authenticated endpoints: 62+
- Public endpoints: 5 (login, setup, health checks)
---
@@ -2855,5 +3063,5 @@ For issues, questions, or feature requests:
---
**Last Updated:** 2025-11-18
-**Phase:** 5 - Alerts Management
+**Phase:** 5 - Alerts Management & Custom Webhook Templates
**Next Update:** Phase 6 - Future Enhancements