crowdsec: migrate bootgrid -> UIBootGrid (#4816)

* bootgrid -> UIBootGrid

* server-side filtering, pagination etc.

* version bump

* add some field defaults; lint
This commit is contained in:
mmetc
2025-07-20 11:42:28 +02:00
committed by GitHub
parent 259fb1ebb8
commit 3b95aa598f
44 changed files with 1195 additions and 808 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
PLUGIN_NAME= crowdsec
PLUGIN_VERSION= 1.0.10
PLUGIN_VERSION= 1.0.11
PLUGIN_DEPENDS= crowdsec
PLUGIN_COMMENT= Lightweight and collaborative security engine
PLUGIN_MAINTAINER= marco@crowdsec.net
+5
View File
@@ -8,6 +8,11 @@ WWW: https://crowdsec.net/
Plugin Changelog
================
1.0.11
* convert tables to UIBootGrid (required for opnsense 25.7)
* separate page for each table
1.0.10
* changed alias names crowdsec*blacklists -> crowdsec*blocklists
@@ -0,0 +1,18 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec;
/**
* Class AlertsController
* @package OPNsense\CrowdSec
*/
class AlertsController extends \OPNsense\Base\IndexController
{
public function indexAction(): void
{
$this->view->pick('OPNsense/CrowdSec/alerts');
}
}
@@ -6,7 +6,6 @@
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\CrowdSec;
use OPNsense\Core\Backend;
/**
@@ -14,6 +13,48 @@ use OPNsense\Core\Backend;
*/
class AlertsController extends ApiControllerBase
{
/**
* Format scope and value as "scope:value"
*
* @param array $source Array with 'scope' and 'value' keys (can be a decision)
* @return string Formatted string
*/
private function formatScopeValue(array $source): string
{
$scope = $source['scope'] ?? '';
if ($source['value'] !== '') {
$scope = $scope . ':' . $source['value'];
}
return $scope;
}
/**
* Summarize decision types as "type1:count1 type2:count2 ..."
*
* @param array $decisions List of decision arrays
* @return string Summary string
*/
private function formatDecisions(array $decisions): string
{
$counts = [];
foreach ($decisions as $decision) {
if (!isset($decision['type'])) {
continue;
}
$type = $decision['type'];
$counts[$type] = ($counts[$type] ?? 0) + 1;
}
$parts = [];
foreach ($counts as $type => $count) {
$parts[] = "{$type}:{$count}";
}
return implode(' ', $parts);
}
/**
* Retrieve list of alerts
*
@@ -21,13 +62,27 @@ class AlertsController extends ApiControllerBase
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
public function searchAction(): array
{
$result = json_decode(trim((new Backend())->configdRun("crowdsec alerts-list")), true);
if ($result !== null) {
// only return valid json type responses
return $result;
if ($result === null) {
return ["message" => "unable to retrieve data"];
}
return ["message" => "unable to list alerts"];
$rows = [];
foreach ($result as $alert) {
$source = $alert['source'] ?? [];
$rows[] = [
'id' => $alert['id'],
'value' => $this->formatScopeValue($source ?? []),
'reason' => $alert['scenario'] ?? '',
'country' => $source['cn'] ?? '',
'as' => $source['as_name'] ?? '',
'decisions' => $this->formatDecisions($alert['decisions'] ?? []),
'created' => $alert['created_at'] ?? '',
];
}
return $this->searchRecordsetBase($rows);
}
}
@@ -0,0 +1,47 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\Util;
use OPNsense\Core\Backend;
/**
* @package OPNsense\CrowdSec
*/
class AppsecconfigsController extends ApiControllerBase
{
/**
* Retrieve the installed appsec-configs
*
* @return dictionary of items, by type
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function searchAction(): array
{
$result = json_decode(trim((new Backend())->configdRun("crowdsec appsec-configs-list")), true);
if ($result === null) {
return ["message" => "unable to retrieve data"];
}
$items = $result["appsec-configs"];
$rows = [];
foreach ($items as $item) {
$rows[] = [
'name' => $item['name'],
'status' => $item['status'] ?? '',
'local_version' => $item['local_version'] ?? '',
'local_path' => Util::trimLocalPath($item['local_path'] ?? ''),
'description' => $item['description'] ?? '',
];
}
return $this->searchRecordsetBase($rows);
}
}
@@ -0,0 +1,47 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\Util;
use OPNsense\Core\Backend;
/**
* @package OPNsense\CrowdSec
*/
class AppsecrulesController extends ApiControllerBase
{
/**
* Retrieve the installed appsec-rules
*
* @return dictionary of items, by type
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function searchAction(): array
{
$result = json_decode(trim((new Backend())->configdRun("crowdsec appsec-rules-list")), true);
if ($result === null) {
return ["message" => "unable to retrieve data"];
}
$items = $result["appsec-rules"];
$rows = [];
foreach ($items as $item) {
$rows[] = [
'name' => $item['name'],
'status' => $item['status'] ?? '',
'local_version' => $item['local_version'] ?? '',
'local_path' => Util::trimLocalPath($item['local_path'] ?? ''),
'description' => $item['description'] ?? '',
];
}
return $this->searchRecordsetBase($rows);
}
}
@@ -6,7 +6,6 @@
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\CrowdSec;
use OPNsense\Core\Backend;
/**
@@ -21,13 +20,27 @@ class BouncersController extends ApiControllerBase
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
public function searchAction(): array
{
$result = json_decode(trim((new Backend())->configdRun("crowdsec bouncers-list")), true);
if ($result !== null) {
// only return valid json type responses
return $result;
if ($result === null) {
return ["message" => "unable to retrieve data"];
}
return ["message" => "unable to list bouncers"];
$rows = [];
foreach ($result as $bouncer) {
$rows[] = [
'name' => $bouncer['name'],
'type' => $bouncer['type'] ?? '',
'version' => $bouncer['version'] ?? '',
'created' => $bouncer['created_at'] ?? '',
'valid' => ($bouncer['revoked'] ?? false) !== true,
'ip_address' => $bouncer['ip_address'] ?? '',
'last_seen' => $bouncer['last_pull'] ?? '',
'os' => $bouncer['os'] ?? '',
];
}
return $this->searchRecordsetBase($rows);
}
}
@@ -0,0 +1,47 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\Util;
use OPNsense\Core\Backend;
/**
* @package OPNsense\CrowdSec
*/
class CollectionsController extends ApiControllerBase
{
/**
* Retrieve the installed collections
*
* @return dictionary of items, by type
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function searchAction(): array
{
$result = json_decode(trim((new Backend())->configdRun("crowdsec collections-list")), true);
if ($result === null) {
return ["message" => "unable to retrieve data"];
}
$items = $result["collections"];
$rows = [];
foreach ($items as $item) {
$rows[] = [
'name' => $item['name'],
'status' => $item['status'] ?? '',
'local_version' => $item['local_version'] ?? '',
'local_path' => Util::trimLocalPath($item['local_path'] ?? ''),
'description' => $item['description'] ?? '',
];
}
return $this->searchRecordsetBase($rows);
}
}
@@ -6,14 +6,62 @@
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\CrowdSec;
use OPNsense\Core\Backend;
function unrollDecisions(array $alerts): array
{
$result = [];
foreach ($alerts as $alert) {
if (!isset($alert['decisions']) || !is_array($alert['decisions'])) {
continue;
}
foreach ($alert['decisions'] as $decision) {
// ignore deleted decisions
if (isset($decision['duration']) && str_starts_with($decision['duration'], '-')) {
continue;
}
$row = $decision;
// Add parent alert fields with prefix
foreach ($alert as $key => $value) {
if ($key === 'decisions') {
continue; // skip nested array
}
$row["alert_" . $key] = $value;
}
$result[] = $row;
}
}
return $result;
}
/**
* @package OPNsense\CrowdSec
*/
class DecisionsController extends ApiControllerBase
{
/**
* Format scope and value as "scope:value"
*
* @param array $source Array with 'scope' and 'value' keys
* @return string Formatted string
*/
private function formatScopeValue(array $source): string
{
$scope = $source['scope'] ?? '';
if ($source['value'] !== '') {
$scope = $scope . ':' . $source['value'];
}
return $scope;
}
/**
* Retrieve list of decisions
*
@@ -21,29 +69,50 @@ class DecisionsController extends ApiControllerBase
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
public function searchAction(): array
{
$result = json_decode(trim((new Backend())->configdRun("crowdsec decisions-list")), true);
if ($result !== null) {
// only return valid json type responses
return $result;
if ($result === null) {
return ["message" => "unable to retrieve data"];
}
return ["message" => "unable to list decisions"];
$decisions = unrollDecisions($result);
$rows = [];
foreach ($decisions as $dec) {
$alert_source = $dec['alert_source'] ?? [];
$rows[] = [
'id' => $dec['id'],
'source' => $dec['origin'] ?? '',
'scope_value' => $this->formatScopeValue($dec),
'reason' => $dec['scenario'] ?? '',
'action' => $dec['type'] ?? '',
'country' => $alert_source['cn'] ?? '',
'as' => $alert_source['as_name'] ?? '',
'events_count' => $dec['alert_events_count'] ?? '',
'expiration' => $dec['duration'] ?? '',
'alert_id' => $dec['alert_id'],
];
}
return $this->searchRecordsetBase($rows);
}
public function deleteAction($decision_id)
public function delAction($decision_id): array
{
if ($this->request->isDelete()) {
$result = (new Backend())->configdRun("crowdsec decisions-delete ${decision_id}");
if ($result !== null) {
// why does the action return \n\n for empty output?
if (trim($result) === '') {
return ["message" => "OK"];
}
// TODO handle error
return ["message" => result];
if ($this->request->isPost()) {
$result = (new Backend())->configdRun("crowdsec decisions-delete {$decision_id}");
if ($result === null) {
return ["result" => "deleted"];
}
return ["message" => "OK"];
// why does the action return \n\n for empty output?
if (trim($result) === '') {
return ["result" => "deleted"];
}
// TODO assume not found, should handle other errors
return ["result" => "not found"];
} else {
$this->response->setStatusCode(405, "Method Not Allowed");
$this->response->setHeader("Allow", "DELETE");
@@ -1,33 +0,0 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\CrowdSec;
use OPNsense\Core\Backend;
/**
* @package OPNsense\CrowdSec
*/
class HubController extends ApiControllerBase
{
/**
* Retrieve the registered hub items
*
* @return dictionary of items, by type
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
{
$result = json_decode(trim((new Backend())->configdRun("crowdsec hub-items")), true);
if ($result !== null) {
// only return valid json type responses
return $result;
}
return ["message" => "unable to list hub items"];
}
}
@@ -6,7 +6,6 @@
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\CrowdSec;
use OPNsense\Core\Backend;
/**
@@ -15,19 +14,32 @@ use OPNsense\Core\Backend;
class MachinesController extends ApiControllerBase
{
/**
* Retrieve list of registered machines
* Retrieve list of machines
*
* @return array of machines
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
public function searchAction(): array
{
$result = json_decode(trim((new Backend())->configdRun("crowdsec machines-list")), true);
if ($result !== null) {
// only return valid json type responses
return $result;
if ($result === null) {
return ["message" => "unable to retrieve data"];
}
return ["message" => "unable to list machines"];
$rows = [];
foreach ($result as $machine) {
$rows[] = [
'name' => $machine['machineId'],
'ip_address' => $machine['ipAddress'] ?? '',
'version' => $machine['version'] ?? '',
'validated' => $machine['isValidated'] ?? false,
'created' => $machine['created_at'] ?? '',
'last_seen' => $machine['last_heartbeat'] ?? '',
'os' => $machine['os'] ?? '',
];
}
return $this->searchRecordsetBase($rows);
}
}
@@ -0,0 +1,47 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\Util;
use OPNsense\Core\Backend;
/**
* @package OPNsense\CrowdSec
*/
class ParsersController extends ApiControllerBase
{
/**
* Retrieve the installed parsers
*
* @return dictionary of items, by type
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function searchAction(): array
{
$result = json_decode(trim((new Backend())->configdRun("crowdsec parsers-list")), true);
if ($result === null) {
return ["message" => "unable to retrieve data"];
}
$items = $result["parsers"];
$rows = [];
foreach ($items as $item) {
$rows[] = [
'name' => $item['name'],
'status' => $item['status'] ?? '',
'local_version' => $item['local_version'] ?? '',
'local_path' => Util::trimLocalPath($item['local_path'] ?? ''),
'description' => $item['description'] ?? '',
];
}
return $this->searchRecordsetBase($rows);
}
}
@@ -0,0 +1,47 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\Util;
use OPNsense\Core\Backend;
/**
* @package OPNsense\CrowdSec
*/
class PostoverflowsController extends ApiControllerBase
{
/**
* Retrieve the installed postoverflows
*
* @return dictionary of items, by type
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function searchAction(): array
{
$result = json_decode(trim((new Backend())->configdRun("crowdsec postoverflows-list")), true);
if ($result === null) {
return ["message" => "unable to retrieve data"];
}
$items = $result["postoverflows"];
$rows = [];
foreach ($items as $item) {
$rows[] = [
'name' => $item['name'],
'status' => $item['status'] ?? '',
'local_version' => $item['local_version'] ?? '',
'local_path' => Util::trimLocalPath($item['local_path'] ?? ''),
'description' => $item['description'] ?? '',
];
}
return $this->searchRecordsetBase($rows);
}
}
@@ -0,0 +1,47 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\Util;
use OPNsense\Core\Backend;
/**
* @package OPNsense\CrowdSec
*/
class ScenariosController extends ApiControllerBase
{
/**
* Retrieve the installed scenarios
*
* @return dictionary of items, by type
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function searchAction(): array
{
$result = json_decode(trim((new Backend())->configdRun("crowdsec scenarios-list")), true);
if ($result === null) {
return ["message" => "unable to retrieve data"];
}
$items = $result["scenarios"];
$rows = [];
foreach ($items as $item) {
$rows[] = [
'name' => $item['name'],
'status' => $item['status'] ?? '',
'local_version' => $item['local_version'] ?? '',
'local_path' => Util::trimLocalPath($item['local_path'] ?? ''),
'description' => $item['description'] ?? '',
];
}
return $this->searchRecordsetBase($rows);
}
}
@@ -16,8 +16,10 @@ class ServiceController extends ApiControllerBase
{
/**
* reconfigure CrowdSec
*
* @return array Status result
*/
public function reloadAction()
public function reloadAction(): array
{
$status = "failed";
if ($this->request->isPost()) {
@@ -36,7 +38,11 @@ class ServiceController extends ApiControllerBase
/**
* Retrieve status of crowdsec
*
* @return array
* @return array{
* status: string,
* crowdsec-status: string,
* crowdsec-firewall-status: string
* }
* @throws \Exception
*/
public function statusAction()
@@ -44,24 +50,30 @@ class ServiceController extends ApiControllerBase
$backend = new Backend();
$response = $backend->configdRun("crowdsec crowdsec-status");
$status = "unknown";
if (strpos($response, "not running") > 0) {
$status = "stopped";
} elseif (strpos($response, "is running") > 0) {
$status = "running";
$crowdsec_status = "unknown";
if (strpos($response, "not running") !== false) {
$crowdsec_status = "stopped";
} elseif (strpos($response, "is running") !== false) {
$crowdsec_status = "running";
}
$response = $backend->configdRun("crowdsec crowdsec-firewall-status");
$firewall_status = "unknown";
if (strpos($response, "not running") > 0) {
if (strpos($response, "not running") !== false) {
$firewall_status = "stopped";
} elseif (strpos($response, "is running") > 0) {
} elseif (strpos($response, "is running") !== false) {
$firewall_status = "running";
}
$status = "unknown";
if ($crowdsec_status == $firewall_status) {
$status = $crowdsec_status;
}
return [
"crowdsec-status" => $status,
"status" => $status,
"crowdsec-status" => $crowdsec_status,
"crowdsec-firewall-status" => $firewall_status,
];
}
@@ -6,7 +6,6 @@
namespace OPNsense\CrowdSec\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\CrowdSec\CrowdSec;
use OPNsense\Core\Backend;
/**
@@ -21,7 +20,7 @@ class VersionController extends ApiControllerBase
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
public function getAction(): string
{
return (new Backend())->configdRun("crowdsec version");
}
@@ -0,0 +1,18 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec;
/**
* Class AppsecconfigsController
* @package OPNsense\CrowdSec
*/
class AppsecconfigsController extends \OPNsense\Base\IndexController
{
public function indexAction(): void
{
$this->view->pick('OPNsense/CrowdSec/appsecconfigs');
}
}
@@ -0,0 +1,18 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec;
/**
* Class AppsecrulesController
* @package OPNsense\CrowdSec
*/
class AppsecrulesController extends \OPNsense\Base\IndexController
{
public function indexAction(): void
{
$this->view->pick('OPNsense/CrowdSec/appsecrules');
}
}
@@ -0,0 +1,18 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec;
/**
* Class BouncersController
* @package OPNsense\CrowdSec
*/
class BouncersController extends \OPNsense\Base\IndexController
{
public function indexAction(): void
{
$this->view->pick('OPNsense/CrowdSec/bouncers');
}
}
@@ -0,0 +1,18 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec;
/**
* Class CollectionsController
* @package OPNsense\CrowdSec
*/
class CollectionsController extends \OPNsense\Base\IndexController
{
public function indexAction(): void
{
$this->view->pick('OPNsense/CrowdSec/collections');
}
}

Some files were not shown because too many files have changed in this diff Show More