CrowdSec: Add alert inspect detail modal showing full alert info, decisions, and events via cscli alerts inspect. Validate

numeric IDs on configdRun parameters in both alerts and decisions controllers.
This commit is contained in:
Gunnar Lieb 2026-03-10 16:19:58 +01:00
parent 78e3906a3e
commit 89083ba015
4 changed files with 190 additions and 21 deletions

View file

@ -55,6 +55,73 @@ class AlertsController extends ApiControllerBase
return implode(' ', $parts);
}
/**
* Retrieve detailed information for a single alert
*
* @param string $alert_id Alert ID to inspect
* @return array Alert details
*/
public function getAction($alert_id): array
{
if (!ctype_digit(strval($alert_id))) {
return ["message" => "invalid alert id"];
}
$backend = new Backend();
$result = json_decode(trim($backend->configdRun("crowdsec alerts-inspect {$alert_id}")), true);
if ($result === null) {
return ["message" => "unable to retrieve alert details"];
}
$source = $result['source'] ?? [];
$decisions = [];
foreach ($result['decisions'] ?? [] as $dec) {
$decisions[] = [
'id' => $dec['id'] ?? '',
'scope' => ($dec['scope'] ?? '') . ':' . ($dec['value'] ?? ''),
'type' => $dec['type'] ?? '',
'duration' => $dec['duration'] ?? '',
'origin' => $dec['origin'] ?? '',
];
}
$events = [];
foreach ($result['events'] ?? [] as $evt) {
$meta = [];
foreach ($evt['meta'] ?? [] as $m) {
$meta[$m['key'] ?? ''] = $m['value'] ?? '';
}
$events[] = [
'timestamp' => $evt['timestamp'] ?? '',
'meta' => $meta,
];
}
return [
'alert' => [
'id' => $result['id'] ?? '',
'created_at' => $result['created_at'] ?? '',
'machine_id' => $result['machine_id'] ?? '',
'simulated' => $result['simulated'] ?? false,
'remediation' => $result['remediation'] ?? false,
'scenario' => $result['scenario'] ?? '',
'message' => $result['message'] ?? '',
'events_count' => $result['events_count'] ?? 0,
'scope_value' => $this->formatScopeValue($source),
'country' => $source['cn'] ?? '',
'as_name' => $source['as_name'] ?? '',
'as_number' => $source['as_number'] ?? '',
'ip_range' => $source['range'] ?? '',
'start_at' => $result['start_at'] ?? '',
'stop_at' => $result['stop_at'] ?? '',
'uuid' => $result['uuid'] ?? '',
'decisions' => $decisions,
'events' => $events,
],
];
}
/**
* Retrieve list of alerts
*

View file

@ -100,6 +100,10 @@ class DecisionsController extends ApiControllerBase
public function delAction($decision_id): array
{
if (!ctype_digit(strval($decision_id))) {
return ["result" => "invalid decision id"];
}
if ($this->request->isPost()) {
$result = (new Backend())->configdRun("crowdsec decisions-delete {$decision_id}");
if ($result === null) {

View file

@ -1,33 +1,19 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<style>
#alertDetailBody .detail-label { width: 150px; }
#alertDetailBody .detail-value-wrap { word-break: break-all; }
#alertDetailBody h4 { margin-top: 20px; padding-left: 8px; }
#alertDetailBody h5 { padding-left: 8px; }
</style>
<script src="/ui/js/moment-with-locales.min.js"></script>
<script src="/ui/js/CrowdSec/crowdsec-misc.js"></script>
<script>
"use strict";
$(function() {
const decisionsByType = function(decisions) {
const dectypes = {};
if (!decisions) {
return '';
}
decisions.map(function (decision) {
// TODO ignore negative expiration?
dectypes[decision.type] = dectypes[decision.type]
? dectypes[decision.type] + 1
: 1;
});
let ret = '';
for (const type in dectypes) {
if (ret !== '') {
ret += ' ';
}
ret += type + ':' + dectypes[type];
}
return ret;
};
$("#cscli_alerts").UIBootgrid({
search: '/api/crowdsec/alerts/search/',
options: {
@ -35,10 +21,99 @@
multiSelect: false,
formatters: {
"created": CrowdSec.formatters.datetime,
"commands": function(column, row) {
return '<button type="button" class="btn btn-xs btn-default alert-inspect" ' +
'data-alert-id="' + row.id + '" title="Inspect">' +
'<span class="fa fa-fw fa-info-circle"></span></button>';
},
},
}
});
$(document).on('click', '.alert-inspect', function() {
var alertId = $(this).data('alert-id');
var $body = $('#alertDetailBody');
$body.html('<div class="text-center"><i class="fa fa-spinner fa-spin fa-2x"></i></div>');
$('#alertDetailModal').modal('show');
$.getJSON('/api/crowdsec/alerts/get/' + alertId, function(data) {
if (data.message) {
$body.html('<div class="alert alert-danger">' + data.message + '</div>');
return;
}
var a = data.alert;
var html = '<table class="table table-condensed table-striped">' +
'<tbody>' +
'<tr><td class="detail-label"><strong>ID</strong></td><td>' + a.id + '</td></tr>' +
'<tr><td><strong>Date</strong></td><td>' + a.created_at + '</td></tr>' +
'<tr><td><strong>Machine</strong></td><td>' + a.machine_id + '</td></tr>' +
'<tr><td><strong>Simulation</strong></td><td>' + (a.simulated ? 'true' : 'false') + '</td></tr>' +
'<tr><td><strong>Remediation</strong></td><td>' + (a.remediation ? 'true' : 'false') + '</td></tr>' +
'<tr><td><strong>Reason</strong></td><td>' + a.scenario + '</td></tr>' +
'<tr><td><strong>Events Count</strong></td><td>' + a.events_count + '</td></tr>' +
'<tr><td><strong>Scope:Value</strong></td><td>' + a.scope_value + '</td></tr>' +
'<tr><td><strong>Country</strong></td><td>' + a.country + '</td></tr>' +
'<tr><td><strong>AS</strong></td><td>' + a.as_name + (a.as_number ? ' (AS' + a.as_number + ')' : '') + '</td></tr>' +
'<tr><td><strong>IP Range</strong></td><td>' + a.ip_range + '</td></tr>' +
'<tr><td><strong>Begin</strong></td><td>' + a.start_at + '</td></tr>' +
'<tr><td><strong>End</strong></td><td>' + a.stop_at + '</td></tr>' +
'<tr><td><strong>UUID</strong></td><td>' + a.uuid + '</td></tr>' +
'</tbody></table>';
if (a.decisions && a.decisions.length > 0) {
html += '<h4>Active Decisions</h4>' +
'<table class="table table-condensed table-striped">' +
'<thead><tr>' +
'<th>ID</th><th>Scope:Value</th><th>Action</th><th>Expiration</th><th>Origin</th>' +
'</tr></thead><tbody>';
for (var i = 0; i < a.decisions.length; i++) {
var d = a.decisions[i];
html += '<tr>' +
'<td>' + d.id + '</td>' +
'<td>' + d.scope + '</td>' +
'<td>' + d.type + '</td>' +
'<td>' + d.duration + '</td>' +
'<td>' + d.origin + '</td>' +
'</tr>';
}
html += '</tbody></table>';
}
if (a.events && a.events.length > 0) {
html += '<h4>Events</h4>';
for (var i = 0; i < a.events.length; i++) {
var evt = a.events[i];
var meta = evt.meta || {};
if (a.events.length > 1) {
html += '<h5>Event ' + (i + 1) + ' — ' + $('<span>').text(evt.timestamp).html() + '</h5>';
}
html += '<table class="table table-condensed table-striped"><tbody>';
var keys = Object.keys(meta).sort();
for (var j = 0; j < keys.length; j++) {
var escaped = $('<span>').text(meta[keys[j]]).html();
html += '<tr><td class="detail-label"><strong>' + keys[j] + '</strong></td>' +
'<td class="detail-value-wrap">' + escaped + '</td></tr>';
}
html += '</tbody></table>';
}
}
$body.html(html);
}).fail(function() {
$body.html('<div class="alert alert-danger">Failed to retrieve alert details.</div>');
});
});
updateServiceControlUI('crowdsec');
});
</script>
@ -53,6 +128,7 @@
<th data-column-id="as">AS</th>
<th data-column-id="decisions">Decisions</th>
<th data-column-id="created" data-formatter="created">Created</th>
<th data-column-id="commands" data-formatter="commands" data-sortable="false">Details</th>
</tr>
</thead>
<tbody>
@ -60,3 +136,19 @@
<tfoot>
</tfoot>
</table>
<div class="modal fade" id="alertDetailModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
<h4 class="modal-title">Alert Details</h4>
</div>
<div class="modal-body" id="alertDetailBody">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View file

@ -41,6 +41,12 @@ command:/usr/local/bin/cscli alerts list -l 0 -o json
type:script_output
message:crowdsec alerts list
[alerts-inspect]
command:/usr/local/bin/cscli alerts inspect -o json
parameters:%s
type:script_output
message:crowdsec alerts inspect
[bouncers-list]
command:/usr/local/bin/cscli bouncers list -o json
type:script_output