Add POST /api/alerts/acknowledge-all endpoint to bulk acknowledge all unacknowledged alerts. Add "Ack All" button to alerts page header with confirmation dialog for quick dismissal of all pending alerts.
303 lines
13 KiB
HTML
303 lines
13 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Alerts - SneakyScanner{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mt-4">
|
|
<div class="col-12 d-flex justify-content-between align-items-center mb-4">
|
|
<h1>Alert History</h1>
|
|
<div>
|
|
<button class="btn btn-success me-2" onclick="acknowledgeAllAlerts()">
|
|
<i class="bi bi-check-all"></i> Ack All
|
|
</button>
|
|
<a href="{{ url_for('main.alert_rules') }}" class="btn btn-primary">
|
|
<i class="bi bi-gear"></i> Manage Alert Rules
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alert Statistics -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Total Alerts</h6>
|
|
<h3 class="mb-0">{{ pagination.total }}</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Critical</h6>
|
|
<h3 class="mb-0 text-danger">
|
|
{{ alerts | selectattr('severity', 'equalto', 'critical') | list | length }}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Warnings</h6>
|
|
<h3 class="mb-0 text-warning">
|
|
{{ alerts | selectattr('severity', 'equalto', 'warning') | list | length }}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Unacknowledged</h6>
|
|
<h3 class="mb-0 text-warning">
|
|
{{ alerts | rejectattr('acknowledged') | list | length }}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<form method="get" action="{{ url_for('main.alerts') }}" class="row g-3">
|
|
<div class="col-md-3">
|
|
<label for="severity-filter" class="form-label">Severity</label>
|
|
<select class="form-select" id="severity-filter" name="severity">
|
|
<option value="">All Severities</option>
|
|
<option value="critical" {% if current_severity == 'critical' %}selected{% endif %}>Critical</option>
|
|
<option value="warning" {% if current_severity == 'warning' %}selected{% endif %}>Warning</option>
|
|
<option value="info" {% if current_severity == 'info' %}selected{% endif %}>Info</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="type-filter" class="form-label">Alert Type</label>
|
|
<select class="form-select" id="type-filter" name="alert_type">
|
|
<option value="">All Types</option>
|
|
{% for at in alert_types %}
|
|
<option value="{{ at }}" {% if current_alert_type == at %}selected{% endif %}>
|
|
{{ at.replace('_', ' ').title() }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="ack-filter" class="form-label">Acknowledgment</label>
|
|
<select class="form-select" id="ack-filter" name="acknowledged">
|
|
<option value="">All</option>
|
|
<option value="false" {% if current_acknowledged == 'false' %}selected{% endif %}>Unacknowledged</option>
|
|
<option value="true" {% if current_acknowledged == 'true' %}selected{% endif %}>Acknowledged</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3 d-flex align-items-end">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="bi bi-funnel"></i> Apply Filters
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alerts Table -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Alerts</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if alerts %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 100px;">Severity</th>
|
|
<th>Type</th>
|
|
<th>Message</th>
|
|
<th style="width: 120px;">Target</th>
|
|
<th style="width: 150px;">Scan</th>
|
|
<th style="width: 150px;">Created</th>
|
|
<th style="width: 100px;">Status</th>
|
|
<th style="width: 100px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for alert in alerts %}
|
|
<tr>
|
|
<td>
|
|
{% if alert.severity == 'critical' %}
|
|
<span class="badge bg-danger">Critical</span>
|
|
{% elif alert.severity == 'warning' %}
|
|
<span class="badge bg-warning">Warning</span>
|
|
{% else %}
|
|
<span class="badge bg-info">Info</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="text-muted">{{ alert.alert_type.replace('_', ' ').title() }}</span>
|
|
</td>
|
|
<td>
|
|
{{ alert.message }}
|
|
</td>
|
|
<td>
|
|
{% if alert.ip_address %}
|
|
<small class="text-muted">
|
|
{{ alert.ip_address }}{% if alert.port %}:{{ alert.port }}{% endif %}
|
|
</small>
|
|
{% else %}
|
|
<small class="text-muted">-</small>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<a href="{{ url_for('main.scan_detail', scan_id=alert.scan_id) }}" class="text-decoration-none">
|
|
Scan #{{ alert.scan_id }}
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<small class="text-muted">{{ alert.created_at.strftime('%Y-%m-%d %H:%M') }}</small>
|
|
</td>
|
|
<td>
|
|
{% if alert.acknowledged %}
|
|
<span class="badge bg-success">
|
|
<i class="bi bi-check-circle"></i> Ack'd
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">New</span>
|
|
{% endif %}
|
|
{% if alert.email_sent %}
|
|
<i class="bi bi-envelope-fill text-muted" title="Email sent"></i>
|
|
{% endif %}
|
|
{% if alert.webhook_sent %}
|
|
<i class="bi bi-send-fill text-muted" title="Webhook sent"></i>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if not alert.acknowledged %}
|
|
<button class="btn btn-sm btn-outline-success" onclick="acknowledgeAlert({{ alert.id }})">
|
|
<i class="bi bi-check"></i> Ack
|
|
</button>
|
|
{% else %}
|
|
<small class="text-muted" title="Acknowledged by {{ alert.acknowledged_by }} at {{ alert.acknowledged_at.strftime('%Y-%m-%d %H:%M') }}">
|
|
By: {{ alert.acknowledged_by }}
|
|
</small>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if pagination.pages > 1 %}
|
|
<nav aria-label="Alert pagination" class="mt-4">
|
|
<ul class="pagination justify-content-center">
|
|
<li class="page-item {% if not pagination.has_prev %}disabled{% endif %}">
|
|
<a class="page-link" href="{{ url_for('main.alerts', page=pagination.prev_num, severity=current_severity, alert_type=current_alert_type, acknowledged=current_acknowledged) }}">
|
|
Previous
|
|
</a>
|
|
</li>
|
|
|
|
{% for page_num in pagination.iter_pages(left_edge=1, left_current=1, right_current=2, right_edge=1) %}
|
|
{% if page_num %}
|
|
<li class="page-item {% if page_num == pagination.page %}active{% endif %}">
|
|
<a class="page-link" href="{{ url_for('main.alerts', page=page_num, severity=current_severity, alert_type=current_alert_type, acknowledged=current_acknowledged) }}">
|
|
{{ page_num }}
|
|
</a>
|
|
</li>
|
|
{% else %}
|
|
<li class="page-item disabled">
|
|
<span class="page-link">...</span>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
<li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
|
|
<a class="page-link" href="{{ url_for('main.alerts', page=pagination.next_num, severity=current_severity, alert_type=current_alert_type, acknowledged=current_acknowledged) }}">
|
|
Next
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="text-center py-5 text-muted">
|
|
<i class="bi bi-bell-slash" style="font-size: 3rem;"></i>
|
|
<h5 class="mt-3">No alerts found</h5>
|
|
<p>Alerts will appear here when scan results trigger alert rules.</p>
|
|
<a href="{{ url_for('main.alert_rules') }}" class="btn btn-primary mt-3">
|
|
<i class="bi bi-plus-circle"></i> Configure Alert Rules
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function acknowledgeAlert(alertId) {
|
|
if (!confirm('Acknowledge this alert?')) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/alerts/${alertId}/acknowledge`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': localStorage.getItem('api_key') || ''
|
|
},
|
|
body: JSON.stringify({
|
|
acknowledged_by: 'web_user'
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
location.reload();
|
|
} else {
|
|
alert('Failed to acknowledge alert: ' + (data.message || 'Unknown error'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Failed to acknowledge alert');
|
|
});
|
|
}
|
|
|
|
function acknowledgeAllAlerts() {
|
|
if (!confirm('Acknowledge all unacknowledged alerts?')) {
|
|
return;
|
|
}
|
|
|
|
fetch('/api/alerts/acknowledge-all', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': localStorage.getItem('api_key') || ''
|
|
},
|
|
body: JSON.stringify({
|
|
acknowledged_by: 'web_user'
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
location.reload();
|
|
} else {
|
|
alert('Failed to acknowledge alerts: ' + (data.message || 'Unknown error'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Failed to acknowledge alerts');
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %} |