mirror of
https://github.com/opnsense/plugins.git
synced 2026-05-28 04:34:15 -04:00
Merge 89083ba015 into cb9a5d6d69
This commit is contained in:
commit
2c48b30b97
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