Files

634 lines
29 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ 'Edit' if mode == 'edit' else 'New' }} Webhook - SneakyScanner{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-12 mb-4">
<h1 style="color: #60a5fa;">{{ 'Edit' if mode == 'edit' else 'Create' }} Webhook</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('webhooks.list_webhooks') }}">Webhooks</a></li>
<li class="breadcrumb-item active">{{ 'Edit' if mode == 'edit' else 'New' }}</li>
</ol>
</nav>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-body">
<form id="webhook-form">
<!-- Basic Information -->
<h5 class="card-title mb-3">Basic Information</h5>
<div class="mb-3">
<label for="name" class="form-label">Webhook Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="name" name="name" required
placeholder="e.g., Slack Notifications">
<div class="form-text">A descriptive name for this webhook</div>
</div>
<div class="mb-3">
<label for="url" class="form-label">Webhook URL <span class="text-danger">*</span></label>
<input type="url" class="form-control" id="url" name="url" required
placeholder="https://hooks.example.com/webhook">
<div class="form-text">The endpoint where alerts will be sent</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="enabled" name="enabled" checked>
<label class="form-check-label" for="enabled">Enabled</label>
</div>
<div class="form-text">Disabled webhooks will not receive notifications</div>
</div>
<hr class="my-4">
<!-- Authentication -->
<h5 class="card-title mb-3">Authentication</h5>
<div class="mb-3">
<label for="auth_type" class="form-label">Authentication Type</label>
<select class="form-select" id="auth_type" name="auth_type">
<option value="none">None</option>
<option value="bearer">Bearer Token</option>
<option value="basic">Basic Auth (username:password)</option>
<option value="custom">Custom Headers</option>
</select>
</div>
<div class="mb-3" id="auth_token_field" style="display: none;">
<label for="auth_token" class="form-label">Authentication Token</label>
<input type="password" class="form-control" id="auth_token" name="auth_token"
placeholder="Enter token or username:password">
<div class="form-text" id="auth_token_help">Will be encrypted when stored</div>
</div>
<div class="mb-3" id="custom_headers_field" style="display: none;">
<label for="custom_headers" class="form-label">Custom Headers (JSON)</label>
<textarea class="form-control font-monospace" id="custom_headers" name="custom_headers" rows="4"
placeholder='{"X-API-Key": "your-key", "X-Custom-Header": "value"}'></textarea>
<div class="form-text">JSON object with custom HTTP headers</div>
</div>
<hr class="my-4">
<!-- Filters -->
<h5 class="card-title mb-3">Alert Filters</h5>
<div class="mb-3">
<label class="form-label">Alert Types</label>
<div class="form-text mb-2">Select which alert types trigger this webhook (leave all unchecked for all types)</div>
<div class="form-check">
<input class="form-check-input alert-type-check" type="checkbox" value="unexpected_port" id="type_unexpected">
<label class="form-check-label" for="type_unexpected">Unexpected Port</label>
</div>
<div class="form-check">
<input class="form-check-input alert-type-check" type="checkbox" value="drift_detection" id="type_drift">
<label class="form-check-label" for="type_drift">Drift Detection</label>
</div>
<div class="form-check">
<input class="form-check-input alert-type-check" type="checkbox" value="cert_expiry" id="type_cert">
<label class="form-check-label" for="type_cert">Certificate Expiry</label>
</div>
<div class="form-check">
<input class="form-check-input alert-type-check" type="checkbox" value="weak_tls" id="type_tls">
<label class="form-check-label" for="type_tls">Weak TLS</label>
</div>
<div class="form-check">
<input class="form-check-input alert-type-check" type="checkbox" value="ping_failed" id="type_ping">
<label class="form-check-label" for="type_ping">Ping Failed</label>
</div>
</div>
<div class="mb-3">
<label class="form-label">Severity Filter</label>
<div class="form-text mb-2">Select which severities trigger this webhook (leave all unchecked for all severities)</div>
<div class="form-check">
<input class="form-check-input severity-check" type="checkbox" value="critical" id="severity_critical">
<label class="form-check-label" for="severity_critical">Critical</label>
</div>
<div class="form-check">
<input class="form-check-input severity-check" type="checkbox" value="warning" id="severity_warning">
<label class="form-check-label" for="severity_warning">Warning</label>
</div>
<div class="form-check">
<input class="form-check-input severity-check" type="checkbox" value="info" id="severity_info">
<label class="form-check-label" for="severity_info">Info</label>
</div>
</div>
<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>
<div class="row">
<div class="col-md-6 mb-3">
<label for="timeout" class="form-label">Timeout (seconds)</label>
<input type="number" class="form-control" id="timeout" name="timeout" min="1" max="60" value="10">
<div class="form-text">Maximum time to wait for response</div>
</div>
<div class="col-md-6 mb-3">
<label for="retry_count" class="form-label">Retry Count</label>
<input type="number" class="form-control" id="retry_count" name="retry_count" min="0" max="5" value="3">
<div class="form-text">Number of retry attempts on failure</div>
</div>
</div>
<hr class="my-4">
<!-- Submit Buttons -->
<div class="d-flex justify-content-between">
<a href="{{ url_for('webhooks.list_webhooks') }}" class="btn btn-secondary">Cancel</a>
<div>
<button type="button" class="btn btn-outline-primary me-2" id="test-btn">
<i class="bi bi-send"></i> Test Webhook
</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Save Webhook
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Help Sidebar -->
<div class="col-lg-4">
<div class="card">
<div class="card-body">
<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">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": {
"id": 123,
"type": "cert_expiry",
"severity": "warning",
"message": "...",
"ip_address": "192.168.1.10",
"port": 443
},
"scan": {...},
"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>
<li><strong>Bearer:</strong> Add Authorization header with token</li>
<li><strong>Basic:</strong> Use username:password format</li>
<li><strong>Custom:</strong> Define custom HTTP headers</li>
</ul>
<h6 class="mt-3">Retry Logic</h6>
<p class="small text-muted">Failed webhooks are retried with exponential backoff (2^attempt seconds, max 60s).</p>
</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 %}
{% block scripts %}
<script>
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;
const tokenField = document.getElementById('auth_token_field');
const headersField = document.getElementById('custom_headers_field');
const tokenHelp = document.getElementById('auth_token_help');
tokenField.style.display = 'none';
headersField.style.display = 'none';
if (authType === 'bearer') {
tokenField.style.display = 'block';
document.getElementById('auth_token').placeholder = 'Enter bearer token';
tokenHelp.textContent = 'Bearer token for Authorization header (encrypted when stored)';
} else if (authType === 'basic') {
tokenField.style.display = 'block';
document.getElementById('auth_token').placeholder = 'username:password';
tokenHelp.textContent = 'Format: username:password (encrypted when stored)';
} else if (authType === 'custom') {
headersField.style.display = 'block';
}
});
// Load existing webhook data if editing
if (mode === 'edit' && webhookId) {
loadWebhookData(webhookId);
}
async function loadWebhookData(id) {
try {
const response = await fetch(`/api/webhooks/${id}`);
const data = await response.json();
const webhook = data.webhook;
// Populate form fields
document.getElementById('name').value = webhook.name || '';
document.getElementById('url').value = webhook.url || '';
document.getElementById('enabled').checked = webhook.enabled;
document.getElementById('auth_type').value = webhook.auth_type || 'none';
document.getElementById('timeout').value = webhook.timeout || 10;
document.getElementById('retry_count').value = webhook.retry_count || 3;
// Trigger auth type change to show relevant fields
document.getElementById('auth_type').dispatchEvent(new Event('change'));
// Don't populate auth_token (it's encrypted)
if (webhook.custom_headers) {
document.getElementById('custom_headers').value = JSON.stringify(webhook.custom_headers, null, 2);
}
// Check alert types
if (webhook.alert_types && webhook.alert_types.length > 0) {
webhook.alert_types.forEach(type => {
const checkbox = document.querySelector(`.alert-type-check[value="${type}"]`);
if (checkbox) checkbox.checked = true;
});
}
// Check severities
if (webhook.severity_filter && webhook.severity_filter.length > 0) {
webhook.severity_filter.forEach(sev => {
const checkbox = document.querySelector(`.severity-check[value="${sev}"]`);
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');
}
}
// Form submission
document.getElementById('webhook-form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = {
name: document.getElementById('name').value,
url: document.getElementById('url').value,
enabled: document.getElementById('enabled').checked,
auth_type: document.getElementById('auth_type').value,
timeout: parseInt(document.getElementById('timeout').value),
retry_count: parseInt(document.getElementById('retry_count').value)
};
// Add auth token if provided
const authToken = document.getElementById('auth_token').value;
if (authToken) {
formData.auth_token = authToken;
}
// Add custom headers if provided
const customHeaders = document.getElementById('custom_headers').value;
if (customHeaders.trim()) {
try {
formData.custom_headers = JSON.parse(customHeaders);
} catch (e) {
alert('Invalid JSON in custom headers');
return;
}
}
// Collect selected alert types
const alertTypes = Array.from(document.querySelectorAll('.alert-type-check:checked'))
.map(cb => cb.value);
if (alertTypes.length > 0) {
formData.alert_types = alertTypes;
}
// Collect selected severities
const severities = Array.from(document.querySelectorAll('.severity-check:checked'))
.map(cb => cb.value);
if (severities.length > 0) {
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';
const response = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.status === 'success') {
alert('Webhook saved successfully!');
window.location.href = '{{ url_for("webhooks.list_webhooks") }}';
} else {
alert(`Failed to save webhook: ${result.message}`);
}
} catch (error) {
console.error('Error saving webhook:', error);
alert('Failed to save webhook');
}
});
// Test webhook button
document.getElementById('test-btn').addEventListener('click', async function() {
if (mode !== 'edit' || !webhookId) {
alert('Please save the webhook first before testing');
return;
}
if (!confirm('Send a test payload to this webhook?')) return;
try {
const response = await fetch(`/api/webhooks/${webhookId}/test`, { method: 'POST' });
const result = await response.json();
if (result.status === 'success') {
alert(`Test successful!\nHTTP ${result.status_code}\n${result.message}`);
} else {
alert(`Test failed:\n${result.message}`);
}
} catch (error) {
console.error('Error testing webhook:', error);
alert('Failed to test webhook');
}
});
</script>
{% endblock %}