added webhooks and templates to alerting, email is next
This commit is contained in:
@@ -123,6 +123,58 @@
|
||||
|
||||
<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 -->
|
||||
<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>
|
||||
|
||||
<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>{
|
||||
"event": "alert.created",
|
||||
"alert": {
|
||||
@@ -181,6 +233,9 @@
|
||||
"rule": {...}
|
||||
}</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>
|
||||
<ul class="small">
|
||||
<li><strong>None:</strong> No authentication</li>
|
||||
@@ -196,6 +251,92 @@
|
||||
</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 %}
|
||||
|
||||
{% 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';
|
||||
|
||||
Reference in New Issue
Block a user