mirror of
https://github.com/opnsense/plugins.git
synced 2026-05-28 04:34:15 -04:00
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:
parent
78e3906a3e
commit
89083ba015
4 changed files with 190 additions and 21 deletions
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>×</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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue