This commit is contained in:
Gunnar 2026-05-25 09:40:05 +08:00 committed by GitHub
commit 2c48b30b97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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