crowdsec: 1.0.10 (#4706)

* Update crowdsec rule reference ($ -> <>); bump release

* php cleanup

* javascript: reformat

* backport js changes from pfsense: var -> const, let, function order

* backport name change

* backport changes: const -> var; id =

* prettier

* tabs

* backport callback style

* cron: avoid spamming stdout when the hub index is updated

* icon

* add outgoing rules

* blacklists -> blocklists

* some python typing

* enroll to console from the settings

* v1.0.10 with option to disable rule generation
This commit is contained in:
mmetc 2025-05-27 14:34:14 +02:00 committed by GitHub
parent 90fedaaee3
commit 248fa0c978
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 637 additions and 517 deletions

View file

@ -41,8 +41,8 @@ function removeAlias($name)
}
}
removeAlias('crowdsec_blacklists');
removeAlias('crowdsec6_blacklists');
removeAlias('crowdsec_blocklists');
removeAlias('crowdsec6_blocklists');
EOT

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= crowdsec
PLUGIN_VERSION= 1.0.9
PLUGIN_VERSION= 1.0.10
PLUGIN_DEPENDS= crowdsec
PLUGIN_COMMENT= Lightweight and collaborative security engine
PLUGIN_MAINTAINER= marco@crowdsec.net

View file

@ -8,6 +8,13 @@ WWW: https://crowdsec.net/
Plugin Changelog
================
1.0.10
* changed alias names crowdsec*blacklists -> crowdsec*blocklists
* added rules for outgoing connections too
* added enroll_key to settings for automatic enrollment
* option to disable rule generation (bring your own rules!)
* code cleanup, reformat, typing
1.0.9
* Update rule reference ($ -> <>) for opnsense 25.1

View file

@ -43,39 +43,69 @@ function crowdsec_firewall(Plugin $fw)
$rules_tag = $general['rules_tag'];
}
add_alias_if_not_exist('crowdsec_blacklists', 'CrowdSec (IPv4)', 'IPv4');
add_alias_if_not_exist('crowdsec_blocklists', 'CrowdSec (IPv4)', 'IPv4');
add_alias_if_not_exist('crowdsec6_blocklists', 'CrowdSec (IPv6)', 'IPv6');
// https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/library/OPNsense/Firewall/FilterRule.php
$fw->registerFilterRule(
1, /* priority */
array(
'ipprotocol' => 'inet',
'descr' => 'CrowdSec (IPv4)',
'from' => '<crowdsec_blacklists>',
'direction' => 'in',
'type' => 'block',
'log' => $rules_log_enabled,
'tag' => $rules_tag,
'quick' => true
)
);
// if missing, default to true
if (!isset($general['rules_enabled']) || $general['rules_enabled'] != 0) {
$fw->registerFilterRule(
1, /* priority */
array(
'ipprotocol' => 'inet',
'descr' => 'CrowdSec (IPv4) in',
'from' => '<crowdsec_blocklists>',
'direction' => 'in',
'type' => 'block',
'log' => $rules_log_enabled,
'tag' => $rules_tag,
'quick' => true
)
);
add_alias_if_not_exist('crowdsec6_blacklists', 'CrowdSec (IPv6)', 'IPv6');
$fw->registerFilterRule(
1, /* priority */
array(
'ipprotocol' => 'inet',
'descr' => 'CrowdSec (IPv4) out',
'to' => '<crowdsec_blocklists>',
'direction' => 'out',
'type' => 'block',
'log' => $rules_log_enabled,
'tag' => $rules_tag,
'quick' => true
)
);
$fw->registerFilterRule(
1, /* priority */
array(
'ipprotocol' => 'inet6',
'descr' => 'CrowdSec (IPv6)',
'from' => '<crowdsec6_blacklists>',
'direction' => 'in',
'type' => 'block',
'log' => $rules_log_enabled,
'tag' => $rules_tag,
'quick' => true
)
);
$fw->registerFilterRule(
1, /* priority */
array(
'ipprotocol' => 'inet6',
'descr' => 'CrowdSec (IPv6) in',
'from' => '<crowdsec6_blocklists>',
'direction' => 'in',
'type' => 'block',
'log' => $rules_log_enabled,
'tag' => $rules_tag,
'quick' => true
)
);
$fw->registerFilterRule(
1, /* priority */
array(
'ipprotocol' => 'inet6',
'descr' => 'CrowdSec (IPv6) out',
'to' => '<crowdsec6_blocklists>',
'direction' => 'out',
'type' => 'block',
'log' => $rules_log_enabled,
'tag' => $rules_tag,
'quick' => true
)
);
}
}
function crowdsec_services()

View file

@ -15,19 +15,19 @@ use OPNsense\Core\Backend;
class AlertsController extends ApiControllerBase
{
/**
* retrieve list of alerts
* Retrieve list of alerts
*
* @return array of alerts
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
{
$backend = new Backend();
$bckresult = json_decode(trim($backend->configdRun("crowdsec alerts-list")), true);
if ($bckresult !== null) {
$result = json_decode(trim((new Backend())->configdRun("crowdsec alerts-list")), true);
if ($result !== null) {
// only return valid json type responses
return $bckresult;
return $result;
}
return array("message" => "unable to list alerts");
return ["message" => "unable to list alerts"];
}
}

View file

@ -15,19 +15,19 @@ use OPNsense\Core\Backend;
class BouncersController extends ApiControllerBase
{
/**
* retrieve list of bouncers
* Retrieve list of bouncers
*
* @return array of bouncers
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
{
$backend = new Backend();
$bckresult = json_decode(trim($backend->configdRun("crowdsec bouncers-list")), true);
if ($bckresult !== null) {
$result = json_decode(trim((new Backend())->configdRun("crowdsec bouncers-list")), true);
if ($result !== null) {
// only return valid json type responses
return $bckresult;
return $result;
}
return array("message" => "unable to list bouncers");
return ["message" => "unable to list bouncers"];
}
}

View file

@ -15,36 +15,35 @@ use OPNsense\Core\Backend;
class DecisionsController extends ApiControllerBase
{
/**
* retrieve list of decisions
* Retrieve list of decisions
*
* @return array of decisions
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
{
$backend = new Backend();
$bckresult = json_decode(trim($backend->configdRun("crowdsec decisions-list")), true);
if ($bckresult !== null) {
$result = json_decode(trim((new Backend())->configdRun("crowdsec decisions-list")), true);
if ($result !== null) {
// only return valid json type responses
return $bckresult;
return $result;
}
return array("message" => "unable to list decisions");
return ["message" => "unable to list decisions"];
}
public function deleteAction($decision_id)
{
if ($this->request->isDelete()) {
$backend = new Backend();
$bckresult = $backend->configdRun("crowdsec decisions-delete ${decision_id}");
if ($bckresult !== null) {
$result = (new Backend())->configdRun("crowdsec decisions-delete ${decision_id}");
if ($result !== null) {
// why does the action return \n\n for empty output?
if (trim($bckresult) === '') {
return array("message" => "OK");
if (trim($result) === '') {
return ["message" => "OK"];
}
// TODO handle error
return array("message" => $bckresult);
return ["message" => result];
}
return array("message" => "OK");
return ["message" => "OK"];
} else {
$this->response->setStatusCode(405, "Method Not Allowed");
$this->response->setHeader("Allow", "DELETE");

View file

@ -15,19 +15,19 @@ use OPNsense\Core\Backend;
class HubController extends ApiControllerBase
{
/**
* retrieve the registered hub items
* Retrieve the registered hub items
*
* @return dictionary of items, by type
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
{
$backend = new Backend();
$bckresult = json_decode(trim($backend->configdRun("crowdsec hub-items")), true);
if ($bckresult !== null) {
$result = json_decode(trim((new Backend())->configdRun("crowdsec hub-items")), true);
if ($result !== null) {
// only return valid json type responses
return $bckresult;
return $result;
}
return array("message" => "unable to list hub items");
return ["message" => "unable to list hub items"];
}
}

View file

@ -15,19 +15,19 @@ use OPNsense\Core\Backend;
class MachinesController extends ApiControllerBase
{
/**
* retrieve list of registered machines
* Retrieve list of registered machines
*
* @return array of machines
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
{
$backend = new Backend();
$bckresult = json_decode(trim($backend->configdRun("crowdsec machines-list")), true);
if ($bckresult !== null) {
$result = json_decode(trim((new Backend())->configdRun("crowdsec machines-list")), true);
if ($result !== null) {
// only return valid json type responses
return $bckresult;
return $result;
}
return array("message" => "unable to list machines");
return ["message" => "unable to list machines"];
}
}

View file

@ -30,11 +30,12 @@ class ServiceController extends ApiControllerBase
}
}
}
return array("status" => $status);
return ["status" => $status];
}
/**
* retrieve status of crowdsec
* Retrieve status of crowdsec
*
* @return array
* @throws \Exception
*/
@ -59,20 +60,9 @@ class ServiceController extends ApiControllerBase
$firewall_status = "running";
}
return array(
return [
"crowdsec-status" => $status,
"crowdsec-firewall-status" => $firewall_status,
);
}
/**
* return debug information
* @return array
*/
public function debugAction()
{
$backend = new Backend();
$response = $backend->configdRun("crowdsec debug");
return array("message" => $response);
];
}
}

View file

@ -15,14 +15,14 @@ use OPNsense\Core\Backend;
class VersionController extends ApiControllerBase
{
/**
* retrieve version description
* Retrieve version description
*
* @return version description
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function getAction()
{
$backend = new Backend();
return $backend->configdRun("crowdsec version");
return (new Backend())->configdRun("crowdsec version");
}
}

View file

@ -27,6 +27,14 @@
packets from the attacking IP addresses.</help>
</field>
<!-- enroll_key -->
<field>
<id>general.enroll_key</id>
<label>Enrollment key from https://app.crowdsec.net</label>
<type>text</type>
<help>Click "Enroll command" on the the website and copy the key here.</help>
</field>
<!-- lapi_manual_configuration -->
<field>
<id>general.lapi_manual_configuration</id>
@ -66,6 +74,16 @@
services.</help>
</field>
<!-- rules_enabled -->
<field>
<id>general.rules_enabled</id>
<label>Create blocklist rules</label>
<type>checkbox</type>
<help>Generate block rules from the Crowdsec blocklists.
They are applied t all interfaces, ipv4/v6, ingress and egress.
If you disable this, you'll have to write your own rules to block anything.</help>
</field>
<!-- rules_log -->
<field>
<id>general.rules_log</id>

View file

@ -1,7 +1,7 @@
<model>
<mount>//OPNsense/crowdsec/general</mount>
<description>CrowdSec general configuration</description>
<version>1.0.9</version>
<version>1.0.10</version>
<items>
<agent_enabled type="BooleanField">
@ -37,6 +37,11 @@
<EnableRanges>N</EnableRanges>
</lapi_listen_port>
<rules_enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</rules_enabled>
<rules_log type="BooleanField">
<default>0</default>
<Required>Y</Required>
@ -47,6 +52,11 @@
<ValidationMessage>A tag must only contain numbers and letters and must be between 1 and 63 characters.</ValidationMessage>
</rules_tag>
<enroll_key type="TextField">
<Mask>/^([0-9a-zA-Z]{1,63})$/u</Mask>
<ValidationMessage>The enrollment key can only contain numbers and letters and must be between 1 and 63 characters. Did you take it from app.crowdsec.net?</ValidationMessage>
</enroll_key>
<crowdsec_firewall_verbose type="BooleanField">
<default>0</default>
<Required>Y</Required>

View file

@ -3,7 +3,7 @@
<script>
$( document ).ready(function() {
var data_get_map = {'frm_GeneralSettings':"/api/crowdsec/general/get"};
const data_get_map = {'frm_GeneralSettings':"/api/crowdsec/general/get"};
mapDataToFormUI(data_get_map).done(function(data){
// place actions to run after load, for example update form styles.
});
@ -64,8 +64,8 @@
<a href="https://doc.crowdsec.net/docs/next/user_guides/multiserver_setup">any other agent</a>
connected to the same LAPI node. Other types of remediation are possible (ex. captcha test for scraping attempts).</p>
We recommend you to <a href="https://app.crowdsec.net/">register to the Console</a>. This helps you manage your instances,
and us to have better overall metrics.
We recommend you to <a href="https://app.crowdsec.net/">register to the Console</a>. This helps you manage your instances,
and us to have better overall metrics.
<p>Please refer to the <a href="https://crowdsec.net/blog/category/tutorial/">tutorials</a> to explore
the possibilities.</p>
@ -148,16 +148,16 @@
<p>
It might be a good idea to have a secondary IP from which you can
connect, should anything go wrong.
</p>
</p>
<pre><code>[root@OPNsense ~]# cscli decisions add -t ban -d 2m -i &lt;your_ip_address&gt;</code></pre>
<pre><code>[root@OPNsense ~]# cscli decisions add -t ban -d 2m -i &lt;your_ip_address&gt;</code></pre>
<p>
This is a more secure way to test than attempting to brute-force
yourself: the default ban period is 4 hours, and Crowdsec reads the
logs from the beginning, so it could ban you even if you failed ssh
login 10 times in 30 seconds two hours before installing it.
</p>
<p>
This is a more secure way to test than attempting to brute-force
yourself: the default ban period is 4 hours, and Crowdsec reads the
logs from the beginning, so it could ban you even if you failed ssh
login 10 times in 30 seconds two hours before installing it.
</p>
<div>
<a class="btn btn-default btn-info" href="https://github.com/crowdsecurity/crowdsec">

View file

@ -45,7 +45,6 @@ ul.nav>li>a {
<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>
<li class="pull-right"><a data-toggle="tab" id="debug_tab" href="#debug" style="display:none">Debug</a></li>
</ul>
<div class="tab-content content-box">
@ -223,13 +222,8 @@ ul.nav>li>a {
</table>
</div>
<div id="debug" class="tab-pane fade in">
<pre>
</pre>
</div>
<!-- Modal popup to confirm decision deletion -->
<div class="modal fade" id="delete-decision-modal" tabindex="-1" role="dialog" aria-labelledby="modalLabel" aria-hidden="true">
<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">
@ -243,7 +237,7 @@ ul.nav>li>a {
</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="delete-decision-confirm">Yes, delete</button>
<button type="button" class="btn btn-danger" data-dismiss="modal" id="remove-decision-confirm">Yes, delete</button>
</div>
</div>
</div>

View file

@ -2,9 +2,9 @@
test -x /usr/local/bin/cscli || exit 0
/usr/local/bin/cscli --error hub update
/usr/local/bin/cscli --error -o human hub update >/dev/null
upgraded=$(/usr/local/bin/cscli --error hub upgrade)
upgraded=$(/usr/local/bin/cscli --error -o human hub upgrade)
if [ ! -e "/usr/local/etc/crowdsec/collections/opnsense.yaml" ]; then
/usr/local/bin/cscli --error collections install crowdsecurity/opnsense

View file

@ -2,33 +2,35 @@
import logging
import json
import subprocess
import urllib.parse
from typing import cast, Any
import yaml
logging.basicConfig(level=logging.INFO)
def load_config(filename):
def load_config(filename: str) -> dict[str, Any]:
with open(filename) as fin:
return yaml.safe_load(fin)
# only save if some value has changed
def save_config(filename, new_config):
def save_config(filename: str, new_config: dict[str, Any]):
old_config = load_config(filename)
if old_config != new_config:
with open(filename, 'w') as fout:
yaml.dump(new_config, fout)
def get_netloc(settings):
def get_netloc(settings: dict[str, str]):
# defaults if config has not been saved yet
listen_address = settings.get('lapi_listen_address', '127.0.0.1')
listen_port = settings.get('lapi_listen_port', '8080')
return '{}:{}'.format(listen_address, listen_port)
def get_new_url(old_url, settings):
def get_new_url(old_url: str, settings: dict[str, str]):
old_tuple = urllib.parse.urlsplit(old_url)
new_tuple = old_tuple._replace(netloc=get_netloc(settings))
new_url = urllib.parse.urlunsplit(new_tuple)
@ -39,7 +41,7 @@ def get_new_url(old_url, settings):
return new_url
def configure_agent(settings):
def configure_agent(settings: dict[str, str]):
config_path = '/usr/local/etc/crowdsec/config.yaml'
config = load_config(config_path)
@ -53,7 +55,7 @@ def configure_agent(settings):
save_config(config_path, config)
def configure_lapi(settings):
def configure_lapi(settings: dict[str, str]):
config_path = '/usr/local/etc/crowdsec/config.yaml'
config = load_config(config_path)
@ -63,7 +65,7 @@ def configure_lapi(settings):
save_config(config_path, config)
def configure_lapi_credentials(settings):
def configure_lapi_credentials(settings: dict[str, str]):
config_path = '/usr/local/etc/crowdsec/local_api_credentials.yaml'
config = load_config(config_path)
@ -73,13 +75,13 @@ def configure_lapi_credentials(settings):
save_config(config_path, config)
def configure_bouncer(settings):
def configure_bouncer(settings: dict[str, str]):
config_path = '/usr/local/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml'
config = load_config(config_path)
config['log_dir'] = '/var/log/crowdsec'
config['blacklists_ipv4'] = 'crowdsec_blacklists'
config['blacklists_ipv6'] = 'crowdsec6_blacklists'
config['blacklists_ipv4'] = 'crowdsec_blocklists'
config['blacklists_ipv6'] = 'crowdsec6_blocklists'
config['retry_initial_connect'] = True
config['pf'] = {'anchor_name': ''}
@ -89,10 +91,35 @@ def configure_bouncer(settings):
save_config(config_path, config)
def enroll(settings: dict[str, str]):
enroll_key = settings.get('enroll_key')
if enroll_key:
try:
p = subprocess.run(['cscli', 'capi', 'status'], check=True, text=True, stdout=subprocess.PIPE)
if "instance is enrolled" in p.stdout:
logging.info("crowdsec instance is already enrolled")
return
except subprocess.CalledProcessError:
return
except Exception as e:
logging.error("could not run command 'cscli' to perform enrollment: %s", e)
try:
logging.info("enrolling crowdsec instance, please accept the enrollment on https://app.crowdsec.net")
_ = subprocess.run(
['cscli', 'console', 'enroll', '-e', 'context', enroll_key],
check=True, text=True)
except subprocess.CalledProcessError as e:
logging.error("enrollment failed: %s", e)
return
except Exception as e:
logging.error("could not run command 'cscli' to perform enrollment: %s", e)
def main():
try:
with open('/usr/local/etc/crowdsec/opnsense/settings.json') as f:
settings = json.load(f)
settings = cast(dict[str, str], json.load(f))
except FileNotFoundError:
logging.info("settings.json not found, won't change crowdsec config")
return
@ -100,6 +127,7 @@ def main():
configure_agent(settings)
configure_lapi(settings)
configure_lapi_credentials(settings)
enroll(settings)
configure_bouncer(settings)

View file

@ -3,427 +3,471 @@
/* eslint no-undef: "error" */
/* eslint semi: "error" */
var CrowdSec = (function () {
'use strict';
const CrowdSec = (function () {
'use strict';
var crowdsec_path = '/usr/local/etc/crowdsec/';
var _refreshTemplate = '<button class="btn btn-default" type="button" title="Refresh"><span class="icon glyphicon glyphicon-refresh"></span></button>';
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>';
var _dataFormatters = {
yesno: function (column, row) {
return _yesno2html(row[column.id]);
},
const _dataFormatters = {
yesno: function (column, row) {
return _yesno2html(row[column.id]);
},
delete: function (column, row) {
var 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>';
},
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) {
var 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');
},
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) {
var dt = row[column.id];
var 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 _parseDuration (duration) {
var re = /(-?)(?:(?:(\d+)h)?(\d+)m)?(\d+).\d+(m?)s/m;
var matches = duration.match(re);
var 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;
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 '';
}
function _updateFreshness (selector, timestamp) {
var $freshness = $(selector).find('.actionBar .freshness');
if (timestamp) {
$freshness.data('refresh_timestamp', timestamp);
} else {
timestamp = $freshness.data('refresh_timestamp');
}
var howlongHuman = '???';
var howlongms;
if (timestamp) {
howlongms = moment() - moment(timestamp);
howlongHuman = moment.duration(howlongms).humanize();
}
$freshness.text(howlongHuman + ' ago');
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 _addFreshness (selector) {
// this creates one timer per tab
var freshnessTemplate = '<span style="float:left">Last refresh: <span class="freshness"></span></span>';
$(selector).find('.actionBar').prepend(freshnessTemplate);
setInterval(function () {
_updateFreshness(selector);
}, 5000);
function _updateFreshness(selector, timestamp) {
const $freshness = $(selector).find('.actionBar .freshness');
if (timestamp) {
$freshness.data('refresh_timestamp', timestamp);
} else {
timestamp = $freshness.data('refresh_timestamp');
}
function _humanizeDate (text) {
return moment(text).fromNow();
const howlongHuman = '???';
if (timestamp) {
const howlongms = moment() - moment(timestamp);
howlongHuman = moment.duration(howlongms).humanize();
}
$freshness.text(howlongHuman + ' ago');
}
function _humanizeDuration (text) {
return moment.duration(_parseDuration(text), 'seconds').humanize();
}
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 _yesno2html (val) {
if (val) {
return '<i class="fa fa-check text-success"></i>';
} else {
return '<i class="fa fa-times text-danger"></i>';
}
}
function _decisionsByType (decisions) {
var dectypes = {};
if (!decisions) {
return '';
}
decisions.map(function (decision) {
// TODO ignore negative expiration?
dectypes[decision.type] = dectypes[decision.type] ? (dectypes[decision.type] + 1) : 1;
});
var ret = '';
var type;
for (type in dectypes) {
if (ret !== '') {
ret += ' ';
}
ret += (type + ':' + dectypes[type]);
}
return ret;
}
function _initService () {
$.ajax({
url: '/api/crowdsec/service/status',
cache: false
}).done(function (data) {
// TODO handle errors
var crowdsecStatus = data['crowdsec-status'];
if (crowdsecStatus === 'unknown') {
crowdsecStatus = '<span class="text-danger">Unknown</span>';
} else {
crowdsecStatus = _yesno2html(crowdsecStatus === 'running');
}
$('#crowdsec-status').html(crowdsecStatus);
var 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 _initDebug () {
$.ajax({
url: '/api/crowdsec/service/debug',
cache: false
}).done(function (data) {
$('#debug pre').text(data.message);
});
}
function _initTab (selector, url, dataCallback) {
var $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 _refreshTab (selector, url, dataCallback) {
$.ajax({
url: url,
cache: false
}).done(dataCallback);
function _refreshTab(selector, url, dataCallback) {
$.ajax({
url: url,
cache: false,
success: dataCallback,
complete: function () {
_updateFreshness(selector, moment());
}
},
});
}
function _initMachines () {
var url = '/api/crowdsec/machines/get';
var dataCallback = function (data) {
var 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 || ' '
});
});
$('#machines table').bootgrid('clear').bootgrid('append', rows);
};
_initTab('#machines', url, dataCallback);
}
function _parseDuration(duration) {
const re = /(-?)(?:(?:(\d+)h)?(\d+)m)?(\d+).\d+(m?)s/m;
const matches = duration.match(re);
let seconds = 0;
function _initCollections () {
var url = '/api/crowdsec/hub/get';
var dataCallback = function (data) {
var 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 || ' '
});
});
$('#collections table').bootgrid('clear').bootgrid('append', rows);
};
_initTab('#collections', url, dataCallback);
if (!matches.length) {
throw new Error(
'Unable to parse the following duration: ' + duration + '.',
);
}
function _initScenarios () {
var url = '/api/crowdsec/hub/get';
var dataCallback = function (data) {
var 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 || ' '
});
});
$('#scenarios table').bootgrid('clear').bootgrid('append', rows);
};
_initTab('#scenarios', url, dataCallback);
if (typeof matches[2] !== 'undefined') {
seconds += parseInt(matches[2], 10) * 3600; // hours
}
function _initParsers () {
var url = '/api/crowdsec/hub/get';
var dataCallback = function (data) {
var 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 || ' '
});
});
$('#parsers table').bootgrid('clear').bootgrid('append', rows);
};
_initTab('#parsers ', url, dataCallback);
if (typeof matches[3] !== 'undefined') {
seconds += parseInt(matches[3], 10) * 60; // minutes
}
function _initPostoverflows () {
var url = '/api/crowdsec/hub/get';
var dataCallback = function (data) {
var 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 || ' '
});
});
$('#postoverflows table').bootgrid('clear').bootgrid('append', rows);
};
_initTab('#postoverflows ', url, dataCallback);
if (typeof matches[4] !== 'undefined') {
seconds += parseInt(matches[4], 10); // seconds
}
function _initBouncers () {
var url = '/api/crowdsec/bouncers/get';
var dataCallback = function (data) {
var 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 || ' '
});
});
$('#bouncers table').bootgrid('clear').bootgrid('append', rows);
};
_initTab('#bouncers ', url, dataCallback);
if (parseInt(matches[5], 10) === 'm') {
// units in milliseconds
seconds *= 0.001;
}
function _initAlerts () {
var url = '/api/crowdsec/alerts/get';
var dataCallback = function (data) {
var 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
});
});
$('#alerts table').bootgrid('clear').bootgrid('append', rows);
};
_initTab('#alerts ', url, dataCallback);
if (parseInt(matches[1], 10) === '-') {
// negative
seconds = -seconds;
}
return seconds;
}
function _initDecisions () {
var url = '/api/crowdsec/decisions/get';
var dataCallback = function (data) {
var 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 || ' '
});
});
});
$('#decisions table').bootgrid('clear').bootgrid('append', rows);
};
_initTab('#decisions ', url, dataCallback);
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 deleteDecision (decisionId) {
var $modal = $('#delete-decision-modal');
$modal.find('.modal-title').text('Delete decision #' + decisionId);
$modal.find('.modal-body').text('Are you sure?');
$modal.find('#delete-decision-confirm').on('click', function () {
$.ajax({
// XXX handle errors
url: '/api/crowdsec/decisions/delete/' + decisionId,
type: 'DELETE',
success: function (result) {
if (result && result.message === 'OK') {
$('#decisions table').bootgrid('remove', [decisionId]);
$modal.modal('hide');
}
}
});
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 || ' ',
});
$modal.modal('show');
}
function init () {
_initService();
$('#machines_tab').on('click', _initMachines);
$('#collections_tab').on('click', _initCollections);
$('#scenarios_tab').on('click', _initScenarios);
$('#parsers_tab').on('click', _initParsers);
$('#postoverflows_tab').on('click', _initPostoverflows);
$('#bouncers_tab').on('click', _initBouncers);
$('#alerts_tab').on('click', _initAlerts);
$('#decisions_tab').on('click', _initDecisions);
$('[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();
});
if (new URLSearchParams(window.location.search).has('debug')) {
$('#debug_tab').show().on('click', _initDebug);
}
// 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
});
$(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,
};
})();