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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1195 additions and 808 deletions

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

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

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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");

View File

@ -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"];
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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,
];
}

View File

@ -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");
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -0,0 +1,18 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec;
/**
* Class DecisionsController
* @package OPNsense\CrowdSec
*/
class DecisionsController extends \OPNsense\Base\IndexController
{
public function indexAction(): void
{
$this->view->pick('OPNsense/CrowdSec/decisions');
}
}

View File

@ -11,7 +11,7 @@ namespace OPNsense\CrowdSec;
*/
class GeneralController extends \OPNsense\Base\IndexController
{
public function indexAction()
public function indexAction(): void
{
$this->view->pick('OPNsense/CrowdSec/general');
$this->view->generalForm = $this->getForm("general");

View File

@ -0,0 +1,18 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec;
/**
* Class MachinesController
* @package OPNsense\CrowdSec
*/
class MachinesController extends \OPNsense\Base\IndexController
{
public function indexAction(): void
{
$this->view->pick('OPNsense/CrowdSec/machines');
}
}

View File

@ -11,7 +11,7 @@ namespace OPNsense\CrowdSec;
*/
class OverviewController extends \OPNsense\Base\IndexController
{
public function indexAction()
public function indexAction(): void
{
$this->view->pick('OPNsense/CrowdSec/overview');
}

View File

@ -0,0 +1,18 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec;
/**
* Class ParsersController
* @package OPNsense\CrowdSec
*/
class ParsersController extends \OPNsense\Base\IndexController
{
public function indexAction(): void
{
$this->view->pick('OPNsense/CrowdSec/parsers');
}
}

View File

@ -0,0 +1,18 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec;
/**
* Class PostoverflowsController
* @package OPNsense\CrowdSec
*/
class PostoverflowsController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/CrowdSec/postoverflows');
}
}

View File

@ -0,0 +1,18 @@
<?php
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net>
namespace OPNsense\CrowdSec;
/**
* Class ScenariosController
* @package OPNsense\CrowdSec
*/
class ScenariosController extends \OPNsense\Base\IndexController
{
public function indexAction(): void
{
$this->view->pick('OPNsense/CrowdSec/scenarios');
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace OPNsense\CrowdSec;
class Util
{
public static function trimLocalPath($local_path): string
{
$prefix = '/usr/local/etc/crowdsec/';
if (str_starts_with($local_path, $prefix)) {
return substr($local_path, strlen($prefix));
}
return $local_path;
}
}

View File

@ -1,7 +1,7 @@
<model>
<mount>//OPNsense/crowdsec/general</mount>
<description>CrowdSec general configuration</description>
<version>1.0.10</version>
<version>1.0.11</version>
<items>
<agent_enabled type="BooleanField">

View File

@ -2,7 +2,16 @@
<Services>
<CrowdSec cssClass="fa fa-globe fa-fw">
<Settings order="1" url="/ui/crowdsec/general/index"/>
<Overview order="2" url="/ui/crowdsec/overview"/>
<Machines order="2" url="/ui/crowdsec/machines/index"/>
<Bouncers order="3" url="/ui/crowdsec/bouncers/index"/>
<Alerts order="5" url="/ui/crowdsec/alerts/index"/>
<Decisions order="6" url="/ui/crowdsec/decisions/index"/>
<Collections order="7" url="/ui/crowdsec/collections/index"/>
<Scenarios order="8" url="/ui/crowdsec/scenarios/index"/>
<Parsers order="9" url="/ui/crowdsec/parsers/index"/>
<Postoverflows order="10" url="/ui/crowdsec/postoverflows/index"/>
<Appsecrules order="11" url="/ui/crowdsec/appsecrules/index"/>
<Appsecconfigs order="12" url="/ui/crowdsec/appsecconfigs/index"/>
</CrowdSec>
</Services>
</menu>

View File

@ -0,0 +1,62 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<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: {
selection: false,
multiSelect: false,
formatters: {
"created": CrowdSec.formatters.datetime,
},
}
});
updateServiceControlUI('crowdsec');
});
</script>
<table id="cscli_alerts" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="id" data-type="numeric" data-order="asc">ID</th>
<th data-column-id="value">Value</th>
<th data-column-id="reason">Reason</th>
<th data-column-id="country">Country</th>
<th data-column-id="as">AS</th>
<th data-column-id="decisions">Decisions</th>
<th data-column-id="created_at" data-formatter="created">Created</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</table>

View File

@ -0,0 +1,36 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<script src="/ui/js/moment-with-locales.min.js"></script>
<script src="/ui/js/CrowdSec/crowdsec-misc.js"></script>
<script>
"use strict";
$(function() {
$("#cscli_appsecconfigs").UIBootgrid({
search: '/api/crowdsec/appsecconfigs/search/',
options: {
selection: false,
multiSelect: false,
}
});
updateServiceControlUI('crowdsec');
});
</script>
<table id="cscli_appsecconfigs" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name">Name</th>
<th data-column-id="status">Status</th>
<th data-column-id="local_version">Version</th>
<th data-column-id="local_path" data-formatter="localpath" data-visible="false">Path</th>
<th data-column-id="description">Description</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</table>

View File

@ -0,0 +1,36 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<script src="/ui/js/moment-with-locales.min.js"></script>
<script src="/ui/js/CrowdSec/crowdsec-misc.js"></script>
<script>
"use strict";
$(function() {
$("#cscli_appsecrules").UIBootgrid({
search: '/api/crowdsec/appsecrules/search/',
options: {
selection: false,
multiSelect: false,
}
});
updateServiceControlUI('crowdsec');
});
</script>
<table id="cscli_appsecrules" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name">Name</th>
<th data-column-id="status">Status</th>
<th data-column-id="local_version">Version</th>
<th data-column-id="local_path" data-formatter="localpath" data-visible="false">Path</th>
<th data-column-id="description">Description</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</table>

View File

@ -0,0 +1,44 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<script src="/ui/js/moment-with-locales.min.js"></script>
<script src="/ui/js/CrowdSec/crowdsec-misc.js"></script>
<script>
"use strict";
$(function() {
$("#cscli_bouncers").UIBootgrid({
search: '/api/crowdsec/bouncers/search/',
options: {
selection: false,
multiSelect: false,
formatters: {
"created": CrowdSec.formatters.datetime,
"last_seen": CrowdSec.formatters.datetime,
"valid": CrowdSec.formatters.yesno,
},
}
});
updateServiceControlUI('crowdsec');
});
</script>
<table id="cscli_bouncers" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name">Name</th>
<th data-column-id="type">Type</th>
<th data-column-id="version">Version</th>
<th data-column-id="created" data-formatter="created" data-visible="false">Created</th>
<th data-column-id="valid" data-formatter="valid">Valid</th>
<th data-column-id="ip_address">IP Address</th>
<th data-column-id="last_seen" data-formatter="last_seen">Last Seen</th>
<th data-column-id="os" data-visible="false">OS</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</table>

View File

@ -0,0 +1,36 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<script src="/ui/js/moment-with-locales.min.js"></script>
<script src="/ui/js/CrowdSec/crowdsec-misc.js"></script>
<script>
"use strict";
$(function() {
$("#cscli_collections").UIBootgrid({
search: '/api/crowdsec/collections/search/',
options: {
selection: false,
multiSelect: false,
}
});
updateServiceControlUI('crowdsec');
});
</script>
<table id="cscli_collections" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name">Name</th>
<th data-column-id="status">Status</th>
<th data-column-id="local_version">Version</th>
<th data-column-id="local_path" data-formatter="localpath" data-visible="false">Path</th>
<th data-column-id="description">Description</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</table>

View File

@ -0,0 +1,51 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<script src="/ui/js/moment-with-locales.min.js"></script>
<script src="/ui/js/CrowdSec/crowdsec-misc.js"></script>
<script>
"use strict";
$(function() {
$("#cscli_decisions").UIBootgrid({
search: '/api/crowdsec/decisions/search/',
del: '/api/crowdsec/decisions/del/',
datakey: "id",
});
updateServiceControlUI('crowdsec');
});
</script>
Note: the decisions coming from the CAPI (signals collected by the CrowdSec users) do not appear here.
To show them, use <code>cscli decisions list -a</code> in a shell.
<table id="cscli_decisions" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="id" data-type="numeric" data-visible="false" data-order="asc">ID</th>
<th data-column-id="source" data-visible="false">Source</th>
<th data-column-id="scope_value">Scope:Value</th>
<th data-column-id="reason">Reason</th>
<th data-column-id="action" data-visible="false">Action</th>
<th data-column-id="country">Country</th>
<th data-column-id="as">AS</th>
<th data-column-id="events_count" data-type="numeric">Events</th>
<th data-column-id="expiration">Expiration</th>
<th data-column-id="alert_id" data-type="numeric" data-visible="false">Alert&nbsp;ID</th>
<th data-column-id="commands" data-formatter="commands" data-sortable="false">Commands</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td/>
<td>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default">
<span class="fa fa-trash-o fa-fw"></span>
</button>
</td>
</tr>
</tfoot>
</table>

View File

@ -0,0 +1,43 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<script src="/ui/js/moment-with-locales.min.js"></script>
<script src="/ui/js/CrowdSec/crowdsec-misc.js"></script>
<script>
"use strict";
$(function() {
$("#cscli_machines").UIBootgrid({
search: '/api/crowdsec/machines/search/',
options: {
selection: false,
multiSelect: false,
formatters: {
"created": CrowdSec.formatters.datetime,
"last_seen": CrowdSec.formatters.datetime,
"validated": CrowdSec.formatters.yesno,
},
}
});
updateServiceControlUI('crowdsec');
});
</script>
<table id="cscli_machines" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name">Name</th>
<th data-column-id="version">Version</th>
<th data-column-id="validated" data-formatter="validated">Validated?</th>
<th data-column-id="ip_address">IP Address</th>
<th data-column-id="created" data-formatter="created" data-visible="false">Created</th>
<th data-column-id="last_seen" data-formatter="last_seen">Last Seen</th>
<th data-column-id="os" data-visible="false">OS</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</table>

View File

@ -1,246 +0,0 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<script src="/ui/js/moment-with-locales.min.js"></script>
<script src="/ui/js/CrowdSec/crowdsec.js"></script>
<script>
$(function() {
CrowdSec.init();
});
</script>
<style type="text/css">
.content-box table {
table-layout: auto;
}
table.bootgrid-table tr .btn-sm {
padding: 2px 6px;
}
table.bootgrid-table tr > td {
padding: 3px;
}
li.spaced {
margin-left: 15px;
}
ul.nav>li>a {
padding: 6px;
}
</style>
<div>
Service status: crowdsec <span id="crowdsec-status">...</span> - firewall bouncer <span id="crowdsec-firewall-status">...</span>
</div>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li><a data-toggle="tab" id="machines_tab" href="#machines">Machines</a></li>
<li><a data-toggle="tab" id="bouncers_tab" href="#bouncers">Bouncers</a></li>
<li class="spaced"><a data-toggle="tab" id="collections_tab" href="#collections">Collections</a></li>
<li><a data-toggle="tab" id="scenarios_tab" href="#scenarios">Scenarios</a></li>
<li><a data-toggle="tab" id="parsers_tab" href="#parsers">Parsers</a></li>
<li><a data-toggle="tab" id="postoverflows_tab" href="#postoverflows">Postoverflows</a></li>
<li class="spaced"><a data-toggle="tab" id="alerts_tab" href="#alerts">Alerts</a></li>
<li><a data-toggle="tab" id="decisions_tab" href="#decisions">Decisions</a></li>
</ul>
<div class="tab-content content-box">
<div id="machines" class="tab-pane fade in">
<table class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name" data-order="asc">Name</th>
<th data-column-id="ip_address">IP Address</th>
<th data-column-id="last_update" data-formatter="datetime">Last Update</th>
<th data-column-id="validated" data-formatter="yesno" data-searchable="false">Validated?</th>
<th data-column-id="version">Version</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
</tr>
</tfoot>
</table>
</div>
<div id="bouncers" class="tab-pane fade in">
<table class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name" data-order="asc">Name</th>
<th data-column-id="ip_address">IP Address</th>
<th data-column-id="valid" data-formatter="yesno" data-searchable="false">Valid</th>
<th data-column-id="last_pull" data-formatter="datetime">Last API Pull</th>
<th data-column-id="type">Type</th>
<th data-column-id="version">Version</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
</tr>
</tfoot>
</table>
</div>
<div id="collections" class="tab-pane fade in">
<table class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name" data-order="asc">Collection</th>
<th data-column-id="status">Status</th>
<th data-column-id="local_version">Version</th>
<th data-visible="false" data-column-id="local_path">Path</th>
<th data-column-id="description">Description</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
</tr>
</tfoot>
</table>
</div>
<div id="scenarios" class="tab-pane fade in">
<table class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name" data-order="asc">Scenario</th>
<th data-column-id="status">Status</th>
<th data-column-id="local_version">Version</th>
<th data-visible="false" data-column-id="local_path">Path</th>
<th data-column-id="description">Description</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
</tr>
</tfoot>
</table>
</div>
<div id="parsers" class="tab-pane fade in">
<table class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name" data-order="asc">Parser</th>
<th data-column-id="status">Status</th>
<th data-column-id="local_version">Version</th>
<th data-visible="false" data-column-id="local_path">Path</th>
<th data-column-id="description">Description</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
</tr>
</tfoot>
</table>
</div>
<div id="postoverflows" class="tab-pane fade in">
<table class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name" data-order="asc">Postoverflow</th>
<th data-column-id="status">Status</th>
<th data-column-id="local_version">Version</th>
<th data-visible="false" data-column-id="local_path">Path</th>
<th data-column-id="description">Description</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
</tr>
</tfoot>
</table>
</div>
<div id="alerts" class="tab-pane fade in">
<table class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="id" data-type="numeric" data-order="asc">ID</th>
<th data-column-id="value">Value</th>
<th data-column-id="reason">Reason</th>
<th data-column-id="country">Country</th>
<th data-column-id="as">AS</th>
<th data-column-id="decisions">Decisions</th>
<th data-column-id="created_at" data-formatter="datetime">Created At</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
</tr>
</tfoot>
</table>
</div>
<div id="decisions" class="tab-pane fade in">
Note: the decisions coming from the CAPI (signals collected by the CrowdSec users) do not appear here.
To show them, use <code>cscli decisions list -a</code> in a shell.
<table class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="delete" data-formatter="delete"
data-visible-in-selection="false"></th>
<th data-column-id="id" data-visible="false" data-identifier="true" data-type="numeric"
data-order="asc">ID</th>
<th data-visible="false" data-column-id="source">Source</th>
<th data-column-id="scope_value">Scope:Value</th>
<th data-column-id="reason">Reason</th>
<th data-visible="false" data-column-id="action">Action</th>
<th data-column-id="country">Country</th>
<th data-column-id="as">AS</th>
<th data-column-id="events_count" data-type="numeric">Events</th>
<th data-column-id="expiration" data-formatter="duration">Expiration</th>
<th data-visible="false" data-column-id="alert_id" data-type="numeric">Alert&nbsp;ID</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
</tr>
</tfoot>
</table>
</div>
<!-- Modal popup to confirm decision deletion -->
<div class="modal fade" id="remove-decision-modal" tabindex="-1" role="dialog" aria-labelledby="modalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="modalLabel">Modal Title</h4>
</div>
<div class="modal-body">
Modal content...
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">No, cancel</button>
<button type="button" class="btn btn-danger" data-dismiss="modal" id="remove-decision-confirm">Yes, delete</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,36 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<script src="/ui/js/moment-with-locales.min.js"></script>
<script src="/ui/js/CrowdSec/crowdsec-misc.js"></script>
<script>
"use strict";
$(function() {
$("#cscli_parsers").UIBootgrid({
search: '/api/crowdsec/parsers/search/',
options: {
selection: false,
multiSelect: false,
}
});
updateServiceControlUI('crowdsec');
});
</script>
<table id="cscli_parsers" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name">Name</th>
<th data-column-id="status">Status</th>
<th data-column-id="local_version">Version</th>
<th data-column-id="local_path" data-formatter="localpath" data-visible="false">Path</th>
<th data-column-id="description">Description</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</table>

View File

@ -0,0 +1,36 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<script src="/ui/js/moment-with-locales.min.js"></script>
<script src="/ui/js/CrowdSec/crowdsec-misc.js"></script>
<script>
"use strict";
$(function() {
$("#cscli_postoverflows").UIBootgrid({
search: '/api/crowdsec/postoverflows/search/',
options: {
selection: false,
multiSelect: false,
}
});
updateServiceControlUI('crowdsec');
});
</script>
<table id="cscli_postoverflows" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name">Name</th>
<th data-column-id="status">Status</th>
<th data-column-id="local_version">Version</th>
<th data-column-id="local_path" data-formatter="localpath" data-visible="false">Path</th>
<th data-column-id="description">Description</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</table>

View File

@ -0,0 +1,36 @@
{# SPDX-License-Identifier: MIT #}
{# SPDX-FileCopyrightText: © 2021 CrowdSec <info@crowdsec.net> #}
<script src="/ui/js/moment-with-locales.min.js"></script>
<script src="/ui/js/CrowdSec/crowdsec-misc.js"></script>
<script>
"use strict";
$(function() {
$("#cscli_scenarios").UIBootgrid({
search: '/api/crowdsec/scenarios/search/',
options: {
selection: false,
multiSelect: false,
}
});
updateServiceControlUI('crowdsec');
});
</script>
<table id="cscli_scenarios" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="name">Name</th>
<th data-column-id="status">Status</th>
<th data-column-id="local_version">Version</th>
<th data-column-id="local_path" data-formatter="localpath" data-visible="false">Path</th>
<th data-column-id="description">Description</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</table>

View File

@ -57,10 +57,35 @@ parameters:--id %s
type:script_output
message:crowdsec decisions delete
[hub-items]
command:/usr/local/bin/cscli hub list -o json
[collections-list]
command:/usr/local/bin/cscli collections list -o json
type:script_output
message:crowdsec hub list
message:crowdsec collections list
[scenarios-list]
command:/usr/local/bin/cscli scenarios list -o json
type:script_output
message:crowdsec scenarios list
[parsers-list]
command:/usr/local/bin/cscli parsers list -o json
type:script_output
message:crowdsec parsers list
[postoverflows-list]
command:/usr/local/bin/cscli postoverflows list -o json
type:script_output
message:crowdsec postoverflows list
[appsec-rules-list]
command:/usr/local/bin/cscli appsec-rules list -o json
type:script_output
message:crowdsec appsec-rules list
[appsec-configs-list]
command:/usr/local/bin/cscli appsec-configs list -o json
type:script_output
message:crowdsec appsec-configs list
[machines-list]
command:/usr/local/bin/cscli machines list -o json

View File

@ -0,0 +1,47 @@
/* global moment, $ */
/* exported CrowdSec */
/* eslint no-undef: "error" */
/* eslint semi: "error" */
const CrowdSec = (function () {
'use strict';
function _humanizeDate(text) {
return moment(text).fromNow();
}
const formatters = {
yesno: function(column, row) {
const val = row[column.id];
if (val) {
return '<i class="fa fa-check text-success"></i>';
} else {
return '<i class="fa fa-times text-danger"></i>';
}
},
datetime: function (column, row) {
const val = row[column.id];
const parsed = moment(val);
if (!val) {
return '';
}
if (!parsed.isValid()) {
console.error('Cannot parse timestamp: %s', val);
return '???';
}
return $('<div>')
.attr({
'data-toggle': 'tooltip',
'data-placement': 'left',
title: parsed.format(),
})
.text(_humanizeDate(val))
.prop('outerHTML');
},
};
return {
formatters: formatters,
};
})();

View File

@ -1,473 +0,0 @@
/* global moment, $ */
/* exported CrowdSec */
/* eslint no-undef: "error" */
/* eslint semi: "error" */
const CrowdSec = (function () {
'use strict';
const crowdsec_path = '/usr/local/etc/crowdsec/';
const _refreshTemplate =
'<button class="btn btn-default" type="button" title="Refresh"><span class="icon fa fa-refresh"></span></button>';
const _dataFormatters = {
yesno: function (column, row) {
return _yesno2html(row[column.id]);
},
delete: function (column, row) {
const val = row.id;
if (isNaN(val)) {
return '';
}
return (
'<button type="button" class="btn btn-secondary btn-sm" value="' +
val +
'" onclick="CrowdSec.deleteDecision(' +
val +
')"><i class="fa fa-trash" /></button>'
);
},
duration: function (column, row) {
const duration = row[column.id];
if (!duration) {
return 'n/a';
}
return $('<div>')
.attr({
'data-toggle': 'tooltip',
'data-placement': 'left',
title: duration,
})
.text(_humanizeDuration(duration))
.prop('outerHTML');
},
datetime: function (column, row) {
const dt = row[column.id];
const parsed = moment(dt);
if (!dt) {
return '';
}
if (!parsed.isValid()) {
console.error('Cannot parse timestamp: %s', dt);
return '???';
}
return $('<div>')
.attr({
'data-toggle': 'tooltip',
'data-placement': 'left',
title: parsed.format(),
})
.text(_humanizeDate(dt))
.prop('outerHTML');
},
};
function _decisionsByType(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;
}
function _updateFreshness(selector, timestamp) {
const $freshness = $(selector).find('.actionBar .freshness');
if (timestamp) {
$freshness.data('refresh_timestamp', timestamp);
} else {
timestamp = $freshness.data('refresh_timestamp');
}
const howlongHuman = '???';
if (timestamp) {
const howlongms = moment() - moment(timestamp);
howlongHuman = moment.duration(howlongms).humanize();
}
$freshness.text(howlongHuman + ' ago');
}
function _addFreshness(selector) {
// this creates one timer per tab
const freshnessTemplate =
'<span style="float:left">Last refresh: <span class="freshness"></span></span>';
$(selector).find('.actionBar').prepend(freshnessTemplate);
setInterval(function () {
_updateFreshness(selector);
}, 5000);
}
function _refreshTab(selector, url, dataCallback) {
$.ajax({
url: url,
cache: false,
success: dataCallback,
complete: function () {
_updateFreshness(selector, moment());
},
});
}
function _parseDuration(duration) {
const re = /(-?)(?:(?:(\d+)h)?(\d+)m)?(\d+).\d+(m?)s/m;
const matches = duration.match(re);
let seconds = 0;
if (!matches.length) {
throw new Error(
'Unable to parse the following duration: ' + duration + '.',
);
}
if (typeof matches[2] !== 'undefined') {
seconds += parseInt(matches[2], 10) * 3600; // hours
}
if (typeof matches[3] !== 'undefined') {
seconds += parseInt(matches[3], 10) * 60; // minutes
}
if (typeof matches[4] !== 'undefined') {
seconds += parseInt(matches[4], 10); // seconds
}
if (parseInt(matches[5], 10) === 'm') {
// units in milliseconds
seconds *= 0.001;
}
if (parseInt(matches[1], 10) === '-') {
// negative
seconds = -seconds;
}
return seconds;
}
function _humanizeDate(text) {
return moment(text).fromNow();
}
function _humanizeDuration(text) {
return moment.duration(_parseDuration(text), 'seconds').humanize();
}
function _yesno2html(val) {
if (val) {
return '<i class="fa fa-check text-success"></i>';
} else {
return '<i class="fa fa-times text-danger"></i>';
}
}
function _initTab(selector, url, dataCallback) {
const $tab = $(selector);
if ($tab.find('table.bootgrid-table').length) {
return;
}
$tab
.find('table')
.on('initialized.rs.jquery.bootgrid', function () {
$(_refreshTemplate)
.on('click', function () {
_refreshTab(selector, url, dataCallback);
})
.insertBefore($tab.find('.actionBar .actions .dropdown:first'));
_addFreshness(selector);
_refreshTab(selector, url, dataCallback);
})
.bootgrid({
caseSensitive: false,
formatters: _dataFormatters,
});
}
function _initStatusMachines() {
const url = '/api/crowdsec/machines/get';
const id = '#machines';
const dataCallback = function (data) {
const rows = [];
data.map(function (row) {
rows.push({
name: row.machineId,
ip_address: row.ipAddress || ' ',
last_update: row.updated_at || ' ',
validated: row.isValidated,
version: row.version || ' ',
});
});
$(id + ' table')
.bootgrid('clear')
.bootgrid('append', rows);
};
_initTab(id, url, dataCallback);
}
function _initStatusCollections() {
const url = '/api/crowdsec/hub/get';
const id = '#collections';
const dataCallback = function (data) {
const rows = [];
data.collections.map(function (row) {
rows.push({
name: row.name,
status: row.status,
local_version: row.local_version || ' ',
local_path: row.local_path
? row.local_path.replace(crowdsec_path, '')
: ' ',
description: row.description || ' ',
});
});
$(id + ' table')
.bootgrid('clear')
.bootgrid('append', rows);
};
_initTab(id, url, dataCallback);
}
function _initStatusScenarios() {
const url = '/api/crowdsec/hub/get';
const id = '#scenarios';
const dataCallback = function (data) {
const rows = [];
data.scenarios.map(function (row) {
rows.push({
name: row.name,
status: row.status,
local_version: row.local_version || ' ',
local_path: row.local_path
? row.local_path.replace(crowdsec_path, '')
: ' ',
description: row.description || ' ',
});
});
$(id + ' table')
.bootgrid('clear')
.bootgrid('append', rows);
};
_initTab(id, url, dataCallback);
}
function _initStatusParsers() {
const url = '/api/crowdsec/hub/get';
const id = '#parsers';
const dataCallback = function (data) {
const rows = [];
data.parsers.map(function (row) {
rows.push({
name: row.name,
status: row.status,
local_version: row.local_version || ' ',
local_path: row.local_path
? row.local_path.replace(crowdsec_path, '')
: ' ',
description: row.description || ' ',
});
});
$(id + ' table')
.bootgrid('clear')
.bootgrid('append', rows);
};
_initTab(id, url, dataCallback);
}
function _initStatusPostoverflows() {
const url = '/api/crowdsec/hub/get';
const id = '#postoverflows';
const dataCallback = function (data) {
const rows = [];
data.postoverflows.map(function (row) {
rows.push({
name: row.name,
status: row.status,
local_version: row.local_version || ' ',
local_path: row.local_path
? row.local_path.replace(crowdsec_path, '')
: ' ',
description: row.description || ' ',
});
});
$(id + ' table')
.bootgrid('clear')
.bootgrid('append', rows);
};
_initTab(id, url, dataCallback);
}
function _initStatusBouncers() {
const url = '/api/crowdsec/bouncers/get';
const id = '#bouncers';
const dataCallback = function (data) {
const rows = [];
data.map(function (row) {
// TODO - remove || ' ' later, it was fixed for 1.3.3
rows.push({
name: row.name,
ip_address: row.ip_address || ' ',
valid: row.revoked ? false : true,
last_pull: row.last_pull,
type: row.type || ' ',
version: row.version || ' ',
});
});
$(id + ' table')
.bootgrid('clear')
.bootgrid('append', rows);
};
_initTab(id, url, dataCallback);
}
function _initStatusAlerts() {
const url = '/api/crowdsec/alerts/get';
const id = '#alerts';
const dataCallback = function (data) {
const rows = [];
data.map(function (row) {
rows.push({
id: row.id,
value:
row.source.scope + (row.source.value ? ':' + row.source.value : ''),
reason: row.scenario || ' ',
country: row.source.cn || ' ',
as: row.source.as_name || ' ',
decisions: _decisionsByType(row.decisions) || ' ',
created_at: row.created_at,
});
});
$(id + ' table')
.bootgrid('clear')
.bootgrid('append', rows);
};
_initTab(id, url, dataCallback);
}
function _initStatusDecisions() {
const url = '/api/crowdsec/decisions/get';
const id = '#decisions';
const dataCallback = function (data) {
const rows = [];
data.map(function (row) {
row.decisions.map(function (decision) {
// ignore deleted decisions
if (decision.duration.startsWith('-')) {
return;
}
rows.push({
// search will break on empty values when using .append(). so we use spaces
delete: '',
id: decision.id,
source: decision.origin || ' ',
scope_value:
decision.scope + (decision.value ? ':' + decision.value : ''),
reason: decision.scenario || ' ',
action: decision.type || ' ',
country: row.source.cn || ' ',
as: row.source.as_name || ' ',
events_count: row.events_count,
// XXX pre-parse duration to seconds, and integer type, for sorting
expiration: decision.duration || ' ',
alert_id: row.id || ' ',
});
});
});
$(id + ' table')
.bootgrid('clear')
.bootgrid('append', rows);
};
_initTab(id, url, dataCallback);
}
function initService() {
$.ajax({
url: '/api/crowdsec/service/status',
cache: false,
success: function (data) {
let crowdsecStatus = data['crowdsec-status'];
if (crowdsecStatus === 'unknown') {
crowdsecStatus = '<span class="text-danger">Unknown</span>';
} else {
crowdsecStatus = _yesno2html(crowdsecStatus === 'running');
}
$('#crowdsec-status').html(crowdsecStatus);
let crowdsecFirewallStatus = data['crowdsec-firewall-status'];
if (crowdsecFirewallStatus === 'unknown') {
crowdsecFirewallStatus = '<span class="text-danger">Unknown</span>';
} else {
crowdsecFirewallStatus = _yesno2html(
crowdsecFirewallStatus === 'running',
);
}
$('#crowdsec-firewall-status').html(crowdsecFirewallStatus);
},
});
}
function deleteDecision(decisionId) {
const $modal = $('#remove-decision-modal');
$modal.find('.modal-title').text('Delete decision #' + decisionId);
$modal.find('.modal-body').text('Are you sure?');
$modal.find('#remove-decision-confirm').on('click', function () {
$.ajax({
// XXX handle errors
url: '/api/crowdsec/decisions/delete/' + decisionId,
method: 'DELETE',
success: function (result) {
if (result && result.message === 'OK') {
$('#decisions table').bootgrid('remove', [decisionId]);
$modal.modal('hide');
}
},
});
});
$modal.modal('show');
}
function init() {
initService();
$('#machines_tab').on('click', _initStatusMachines);
$('#collections_tab').on('click', _initStatusCollections);
$('#scenarios_tab').on('click', _initStatusScenarios);
$('#parsers_tab').on('click', _initStatusParsers);
$('#postoverflows_tab').on('click', _initStatusPostoverflows);
$('#bouncers_tab').on('click', _initStatusBouncers);
$('#alerts_tab').on('click', _initStatusAlerts);
$('#decisions_tab').on('click', _initStatusDecisions);
$('[data-toggle="tooltip"]').tooltip();
if (window.location.hash) {
// activate a tab from the hash, if it exists
$(window.location.hash + '_tab').click();
} else {
// otherwise, machines
$('#machines_tab').click();
}
$(window).on('hashchange', function (e) {
$(window.location.hash + '_tab').click();
});
// navigation
if (window.location.hash !== '') {
$('a[href="' + window.location.hash + '"]').click();
}
$('.nav-tabs a').on('shown.bs.tab', function (e) {
history.pushState(null, null, e.target.hash);
});
}
return {
deleteDecision: deleteDecision,
init: init,
};
})();