From ce4469f458926917af4bb3cb40c254f1d6425d84 Mon Sep 17 00:00:00 2001 From: mmetc <92726601+mmetc@users.noreply.github.com> Date: Tue, 10 May 2022 11:59:42 +0200 Subject: [PATCH] new plugin: security/crowdsec (#2945) This is a plugin we developed to provide configuration and a basic UI for the crowdsec IDS and IPS. It depends on a couple of binaries recently added to ports.conf Adding machines (servers, other firewalls) and advanced configuration are not managed by the UI but available from the command line. --- security/crowdsec/+POST_DEINSTALL.post | 50 ++ security/crowdsec/+POST_INSTALL.post | 11 + security/crowdsec/+PRE_DEINSTALL.pre | 8 + security/crowdsec/LICENSE | 21 + security/crowdsec/Makefile | 10 + security/crowdsec/pkg-descr | 49 ++ .../crowdsec/src/etc/cron.d/oscrowdsec.cron | 9 + .../src/etc/crowdsec/acquis.d/opnsense.yaml | 15 + .../src/etc/inc/plugins.inc.d/crowdsec.inc | 95 ++++ security/crowdsec/src/etc/rc.d/oscrowdsec | 104 +++++ .../src/etc/rc.syshook.d/start/50-crowdsec | 6 + .../CrowdSec/Api/AlertsController.php | 33 ++ .../CrowdSec/Api/BouncersController.php | 33 ++ .../CrowdSec/Api/CollectionsController.php | 33 ++ .../CrowdSec/Api/DecisionsController.php | 53 +++ .../CrowdSec/Api/GeneralController.php | 17 + .../CrowdSec/Api/MachinesController.php | 33 ++ .../CrowdSec/Api/ParsersController.php | 33 ++ .../CrowdSec/Api/PostoverflowsController.php | 33 ++ .../CrowdSec/Api/ScenariosController.php | 33 ++ .../CrowdSec/Api/ServiceController.php | 78 ++++ .../CrowdSec/Api/VersionController.php | 28 ++ .../OPNsense/CrowdSec/GeneralController.php | 19 + .../OPNsense/CrowdSec/OverviewController.php | 18 + .../OPNsense/CrowdSec/forms/general.xml | 95 ++++ .../app/models/OPNsense/CrowdSec/ACL/ACL.xml | 9 + .../app/models/OPNsense/CrowdSec/General.php | 12 + .../app/models/OPNsense/CrowdSec/General.xml | 56 +++ .../models/OPNsense/CrowdSec/Menu/Menu.xml | 8 + .../app/views/OPNsense/CrowdSec/general.volt | 142 ++++++ .../app/views/OPNsense/CrowdSec/overview.volt | 249 ++++++++++ .../scripts/OPNsense/CrowdSec/hub-upgrade.sh | 18 + .../scripts/OPNsense/CrowdSec/reconfigure.py | 94 ++++ .../scripts/OPNsense/CrowdSec/reconfigure.sh | 33 ++ .../conf/actions.d/actions_crowdsec.conf | 94 ++++ .../templates/OPNsense/CrowdSec/+TARGETS | 4 + .../OPNsense/CrowdSec/crowdsec.rc.conf.d | 11 + .../CrowdSec/crowdsec_firewall.rc.conf.d | 11 + .../OPNsense/CrowdSec/oscrowdsec.rc.conf.d | 1 + .../templates/OPNsense/CrowdSec/settings.json | 5 + .../src/opnsense/www/js/CrowdSec/crowdsec.js | 426 ++++++++++++++++++ 41 files changed, 2090 insertions(+) create mode 100755 security/crowdsec/+POST_DEINSTALL.post create mode 100755 security/crowdsec/+POST_INSTALL.post create mode 100755 security/crowdsec/+PRE_DEINSTALL.pre create mode 100644 security/crowdsec/LICENSE create mode 100644 security/crowdsec/Makefile create mode 100644 security/crowdsec/pkg-descr create mode 100644 security/crowdsec/src/etc/cron.d/oscrowdsec.cron create mode 100644 security/crowdsec/src/etc/crowdsec/acquis.d/opnsense.yaml create mode 100644 security/crowdsec/src/etc/inc/plugins.inc.d/crowdsec.inc create mode 100755 security/crowdsec/src/etc/rc.d/oscrowdsec create mode 100755 security/crowdsec/src/etc/rc.syshook.d/start/50-crowdsec create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AlertsController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/BouncersController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/CollectionsController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/DecisionsController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/GeneralController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/MachinesController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ParsersController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/PostoverflowsController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ScenariosController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ServiceController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/VersionController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/GeneralController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/OverviewController.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/forms/general.xml create mode 100644 security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/ACL/ACL.xml create mode 100644 security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.php create mode 100644 security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.xml create mode 100644 security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/Menu/Menu.xml create mode 100644 security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/general.volt create mode 100644 security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/overview.volt create mode 100755 security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh create mode 100755 security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py create mode 100755 security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh create mode 100644 security/crowdsec/src/opnsense/service/conf/actions.d/actions_crowdsec.conf create mode 100644 security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/+TARGETS create mode 100644 security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec.rc.conf.d create mode 100644 security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec_firewall.rc.conf.d create mode 100644 security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/oscrowdsec.rc.conf.d create mode 100644 security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/settings.json create mode 100644 security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec.js diff --git a/security/crowdsec/+POST_DEINSTALL.post b/security/crowdsec/+POST_DEINSTALL.post new file mode 100755 index 000000000..536a32529 --- /dev/null +++ b/security/crowdsec/+POST_DEINSTALL.post @@ -0,0 +1,50 @@ +#!/bin/sh + +# Removing the plugin from the web interface will autoremove the dependencies +# too, and here we have to delete the files in rc.conf.d (because they are +# generated from templates when the configuration is saved, and the package +# system did not keep track of them). + +# But.. If the plugin is removed from the command line (which does not happen +# outside of testing conditions), the crowdsec and bouncer services will not be +# removed. However, since we deleted the files that enabled these services, +# they will be disabled at the next reboot. + +rm -f /etc/rc.conf.d/crowdsec \ + /etc/rc.conf.d/crowdsec_firewall \ + /etc/rc.conf.d/oscrowdsec + + +# Remove aliases and with them, the rules. We don't have plugin files +# anymore so we do that on the fly. + +/usr/local/bin/php <<'EOT' +aliases->alias->iterateItems() as $index => $alias) { + if (strval($alias->name) == $name) { + if ($model->aliases->alias->del($index)) { + $model->serializeToConfig(); + Config::getInstance()->save(); + } + } + } +} + +removeAlias('crowdsec_blacklists'); +removeAlias('crowdsec6_blacklists'); +EOT + + +# apply the configuration changes to the packet filter +configctl filter reload diff --git a/security/crowdsec/+POST_INSTALL.post b/security/crowdsec/+POST_INSTALL.post new file mode 100755 index 000000000..6a6497449 --- /dev/null +++ b/security/crowdsec/+POST_INSTALL.post @@ -0,0 +1,11 @@ +#!/bin/sh + +# the configuration file used in reconfigure (i.e. settings.json) may eventually +# have credentials, so we create a directory to contain it -- the directory +# permissions will be copied to the file while generating the jinja template. + +# shellcheck disable=SC2174 +mkdir -p -m 0700 /usr/local/etc/crowdsec/opnsense + +configctl crowdsec reconfigure + diff --git a/security/crowdsec/+PRE_DEINSTALL.pre b/security/crowdsec/+PRE_DEINSTALL.pre new file mode 100755 index 000000000..9f7107ef0 --- /dev/null +++ b/security/crowdsec/+PRE_DEINSTALL.pre @@ -0,0 +1,8 @@ +#!/bin/sh + +# need to temporarily stop the bouncer to remove all the rules +service crowdsec_firewall stop >/dev/null 2>&1 | : + +# the rest of the cleanup is done in the post-deinstall script, otherwise +# the plugin recreates the objects during "filter reload". + diff --git a/security/crowdsec/LICENSE b/security/crowdsec/LICENSE new file mode 100644 index 000000000..92d86fc27 --- /dev/null +++ b/security/crowdsec/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2021 Crowdsec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/security/crowdsec/Makefile b/security/crowdsec/Makefile new file mode 100644 index 000000000..fb0653f19 --- /dev/null +++ b/security/crowdsec/Makefile @@ -0,0 +1,10 @@ +PLUGIN_NAME= crowdsec +PLUGIN_VERSION= 0.2 +PLUGIN_DEVEL= yes +#PLUGIN_REVISION= 1 +PLUGIN_DEPENDS= crowdsec +PLUGIN_COMMENT= Lightweight and collaborative security engine +PLUGIN_MAINTAINER= marco@crowdsec.net +PLUGIN_WWW= https://crowdsec.net/ + +.include "../../Mk/plugins.mk" diff --git a/security/crowdsec/pkg-descr b/security/crowdsec/pkg-descr new file mode 100644 index 000000000..2cd4f07c1 --- /dev/null +++ b/security/crowdsec/pkg-descr @@ -0,0 +1,49 @@ +Crowdsec is an open-source, lightweight software, detecting peers with +aggressive behaviors to prevent them from accessing your systems. Its user +friendly design and assistance offers a low technical barrier of entry and +nevertheless a high security gain. + +WWW: https://crowdsec.net/ + +Plugin Changelog +================ + +0.2 + +* first published release +* added options `lapi_enabled`, `crowdsec_firewall_verbose` +* removed options `crowdsec_flags`, `crowdsec_firewall_flags` +* changed default for `agent_enabled`, `firewall_bouncer_enabled` to 1 + +0.1 + +* fixed packet tags with ipv6 +* custom `crowdsec_flags`, `crosdsec_firewall_flags` + +0.0.9 + +* fixed the javascript, 0.0.8 had a syntax error +* new option: rules_tag +* new option: lapi_manual_configuration +* ipv4/ipv6 validation with regexp + +0.0.8 + +* crowdsec update 1.3.2 +* configurable `rules_log` and LAPI address/port + +0.0.7 + +* automated removal of Alias objects when the plugin is uninstalled + +0.0.6 + +* crowdsec update 1.3.1.r1 +* bouncer update to 0.0.23.r1 +* automated creation of Alias and Rule objects + +0.0.5 + +* fixed an issue that prevented the bouncer from banning IPs on opnsense +* fixed support for notification plugins + diff --git a/security/crowdsec/src/etc/cron.d/oscrowdsec.cron b/security/crowdsec/src/etc/cron.d/oscrowdsec.cron new file mode 100644 index 000000000..2ec072c40 --- /dev/null +++ b/security/crowdsec/src/etc/cron.d/oscrowdsec.cron @@ -0,0 +1,9 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +# +# User-defined crontab files can be loaded via /etc/cron.d +# or /usr/local/etc/cron.d and follow the same format as +# /etc/crontab, see the crontab(5) manual page. +SHELL=/bin/sh +PATH=/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin +#minute hour mday month wday who command +0 1,13 * * * root /usr/local/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh diff --git a/security/crowdsec/src/etc/crowdsec/acquis.d/opnsense.yaml b/security/crowdsec/src/etc/crowdsec/acquis.d/opnsense.yaml new file mode 100644 index 000000000..905be2256 --- /dev/null +++ b/security/crowdsec/src/etc/crowdsec/acquis.d/opnsense.yaml @@ -0,0 +1,15 @@ +# +# Before 22.1, OPNsense used circular logs under /var/log/*.log that +# can still be around. They are old, in binary format and are not needed by crowdsec. +# +# For this reason we don't scan /var/log/*.log, but some plugins can write +# their (plaintext) logs in that location, in such case add their pathnames too. +# + +filenames: + # ssh + - /var/log/audit/*.log + # web admin + - /var/log/lighttpd/*.log +labels: + type: syslog diff --git a/security/crowdsec/src/etc/inc/plugins.inc.d/crowdsec.inc b/security/crowdsec/src/etc/inc/plugins.inc.d/crowdsec.inc new file mode 100644 index 000000000..a1e2eb6b0 --- /dev/null +++ b/security/crowdsec/src/etc/inc/plugins.inc.d/crowdsec.inc @@ -0,0 +1,95 @@ + + +use OPNsense\Core\Config; +use OPNsense\Firewall\Alias; +use OPNsense\Firewall\Plugin; + +function add_alias_if_not_exist($name, $description, $proto) { + $model = new Alias(); + + if ($model->getByName($name) != null) { + return; + } + + $new_alias = $model->aliases->alias->Add(); + $new_alias->name = $name; + $new_alias->description = $description; + $new_alias->proto = $proto; + $new_alias->type = 'external'; + $model->serializeToConfig(); + Config::getInstance()->save(); +} + +function crowdsec_firewall(Plugin $fw) +{ + global $config; + + $general = $config['OPNsense']['crowdsec']['general']; + + $bouncer_enabled = isset($general['firewall_bouncer_enabled']) && $general['firewall_bouncer_enabled']; + + if (!$bouncer_enabled) { + return; + } + + $rules_log_enabled = isset($general['rules_log']) && $general['rules_log']; + + $rules_tag = ""; + if (isset($general['rules_tag'])) { + $rules_tag = $general['rules_tag']; + }; + + add_alias_if_not_exist('crowdsec_blacklists', 'CrowdSec (IPv4)', 'IPv4'); + + // 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', # $ to reference an alias + 'type' => 'block', + 'log' => $rules_log_enabled, + 'tag' => $rules_tag, + 'label' => 'blocked by crowdsec', + 'quick' => true + ), + null + ); + + add_alias_if_not_exist('crowdsec6_blacklists', 'CrowdSec (IPv6)', 'IPv6'); + + $fw->registerFilterRule( + 1, /* priority */ + array( + 'ipprotocol' => 'inet6', + 'descr' => 'CrowdSec (IPv6)', + 'from' => '$crowdsec6_blacklists', # $ to reference an alias + 'type' => 'block', + 'log' => $rules_log_enabled, + 'tag' => $rules_tag, + 'label' => 'blocked by crowdsec', + 'quick' => true + ), + null + ); +} + +function crowdsec_services() +{ + $services[] = array( + 'description' => 'CrowdSec', + 'configd' => array( + 'restart' => array('crowdsec restart'), + 'start' => array('crowdsec start'), + 'stop' => array('crowdsec stop'), + ), + 'name' => 'crowdsec' + ); + + return $services; +} diff --git a/security/crowdsec/src/etc/rc.d/oscrowdsec b/security/crowdsec/src/etc/rc.d/oscrowdsec new file mode 100755 index 000000000..a64dc0d38 --- /dev/null +++ b/security/crowdsec/src/etc/rc.d/oscrowdsec @@ -0,0 +1,104 @@ +#!/bin/sh +# +# $FreeBSD$ +# + +# PROVIDE: oscrowdsec +# REQUIRE: NETWORKING syslogd +# BEFORE: DAEMON +# KEYWORD: shutdown + +. /etc/rc.subr + +name="oscrowdsec" +rcvar="oscrowdsec_enable" + +load_rc_config $name + +: ${oscrowdsec_enable="NO"} + + +oscrowdsec_start () { + # + # Start, or stop the services according to the plugin's configuration. + # When starting -> error if the services are already running + # When stopping -> no error + # + + if service crowdsec enabled; then + service crowdsec start + else + service crowdsec stop || : + fi + + if service crowdsec_firewall enabled; then + service crowdsec_firewall start + else + service crowdsec_firewall stop || : + fi + +# XXX should complain if they were not stopped? +# service crowdsec status +# if [ $? -eq 0 ]; then +# debug "oscrowdsec_start: crowdsec is still running" +# return 0 +# fi +} + +oscrowdsec_stop () { + # Always stop the services, enabled or not, running or not. No errors. + + service crowdsec stop || : + service crowdsec_firewall stop || : + + # XXX should complain if they were running and have not been stopped? +} + +oscrowdsec_restart () { + oscrowdsec_stop || : + oscrowdsec_start +} + +oscrowdsec_status () { + # return error if at least one program is not running + ret=0 + + if service crowdsec status; then + ret=$? + fi + + if service crowdsec_firewall status; then + if [ $ret -eq 0 ]; then + ret=$? + fi + fi + return $ret +} + +oscrowdsec_reload () { + # Here we take it easy. the bouncer does not even support reload + oscrowdsec_restart +} + +case $1 in + start) + oscrowdsec_start + exit $? + ;; + stop) + oscrowdsec_stop + exit $? + ;; + restart) + oscrowdsec_restart + exit $? + ;; + status) + oscrowdsec_status + exit $? + ;; + reload) + oscrowdsec_reload + exit $? + ;; +esac diff --git a/security/crowdsec/src/etc/rc.syshook.d/start/50-crowdsec b/security/crowdsec/src/etc/rc.syshook.d/start/50-crowdsec new file mode 100755 index 000000000..58359695f --- /dev/null +++ b/security/crowdsec/src/etc/rc.syshook.d/start/50-crowdsec @@ -0,0 +1,6 @@ +#!/bin/sh + +# https://docs.opnsense.org/development/backend/autorun.html + +/usr/local/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh + diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AlertsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AlertsController.php new file mode 100644 index 000000000..6aff27e81 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AlertsController.php @@ -0,0 +1,33 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\CrowdSec; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class AlertsController extends ApiControllerBase +{ + /** + * 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) { + // only return valid json type responses + return $bckresult; + } + return array("message" => "unable to list alerts"); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/BouncersController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/BouncersController.php new file mode 100644 index 000000000..94de1a877 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/BouncersController.php @@ -0,0 +1,33 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\CrowdSec; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class BouncersController extends ApiControllerBase +{ + /** + * 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) { + // only return valid json type responses + return $bckresult; + } + return array("message" => "unable to list bouncers"); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/CollectionsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/CollectionsController.php new file mode 100644 index 000000000..62c63afa6 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/CollectionsController.php @@ -0,0 +1,33 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\CrowdSec; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class CollectionsController extends ApiControllerBase +{ + /** + * retrieve list of collections + * @return array of collections + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function getAction() + { + $backend = new Backend(); + $bckresult = json_decode(trim($backend->configdRun("crowdsec collections-list")), true); + if ($bckresult !== null) { + // only return valid json type responses + return $bckresult; + } + return array("message" => "unable to list collections"); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/DecisionsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/DecisionsController.php new file mode 100644 index 000000000..7421e74e4 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/DecisionsController.php @@ -0,0 +1,53 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\CrowdSec; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class DecisionsController extends ApiControllerBase +{ + /** + * 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) { + // only return valid json type responses + return $bckresult; + } + return array("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) { + // why does the action return \n\n for empty output? + if (trim($bckresult) === '') { + return array("message" => "OK"); + } + // TODO handle error + return array("message" => $bckresult); + } + return array("message" => "OK"); + } else { + $this->response->setStatusCode(405, "Method Not Allowed"); + $this->response->setHeader("Allow", "DELETE"); + } + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/GeneralController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/GeneralController.php new file mode 100644 index 000000000..038e49181 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/GeneralController.php @@ -0,0 +1,17 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiMutableModelControllerBase; + +/** + * @package OPNsense\CrowdSec + */ +class GeneralController extends ApiMutableModelControllerBase +{ + protected static $internalModelName = 'general'; + protected static $internalModelClass = '\OPNsense\CrowdSec\General'; +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/MachinesController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/MachinesController.php new file mode 100644 index 000000000..617e43bf4 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/MachinesController.php @@ -0,0 +1,33 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\CrowdSec; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class MachinesController extends ApiControllerBase +{ + /** + * 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) { + // only return valid json type responses + return $bckresult; + } + return array("message" => "unable to list machines"); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ParsersController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ParsersController.php new file mode 100644 index 000000000..6dfcfcdb8 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ParsersController.php @@ -0,0 +1,33 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\CrowdSec; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class ParsersController extends ApiControllerBase +{ + /** + * retrieve list of registered parsers + * @return array of parsers + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function getAction() + { + $backend = new Backend(); + $bckresult = json_decode(trim($backend->configdRun("crowdsec parsers-list")), true); + if ($bckresult !== null) { + // only return valid json type responses + return $bckresult; + } + return array("message" => "unable to list parsers"); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/PostoverflowsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/PostoverflowsController.php new file mode 100644 index 000000000..a52fc928c --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/PostoverflowsController.php @@ -0,0 +1,33 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\CrowdSec; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class PostoverflowsController extends ApiControllerBase +{ + /** + * retrieve list of registered postoverflows + * @return array of postoverflows + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function getAction() + { + $backend = new Backend(); + $bckresult = json_decode(trim($backend->configdRun("crowdsec postoverflows-list")), true); + if ($bckresult !== null) { + // only return valid json type responses + return $bckresult; + } + return array("message" => "unable to list postoverflows"); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ScenariosController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ScenariosController.php new file mode 100644 index 000000000..5daa6b82b --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ScenariosController.php @@ -0,0 +1,33 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\CrowdSec; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class ScenariosController extends ApiControllerBase +{ + /** + * retrieve list of registered scenarios + * @return array of scenarios + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function getAction() + { + $backend = new Backend(); + $bckresult = json_decode(trim($backend->configdRun("crowdsec scenarios-list")), true); + if ($bckresult !== null) { + // only return valid json type responses + return $bckresult; + } + return array("message" => "unable to list scenarios"); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ServiceController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ServiceController.php new file mode 100644 index 000000000..dae2711c7 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ServiceController.php @@ -0,0 +1,78 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\Core\Backend; + +/** + * Class ServiceController + * @package OPNsense\CrowdSec + */ +class ServiceController extends ApiControllerBase +{ + /** + * reconfigure CrowdSec + */ + public function reloadAction() + { + $status = "failed"; + if ($this->request->isPost()) { + $backend = new Backend(); + $bckresult = trim($backend->configdRun('template reload OPNsense/CrowdSec')); + if ($bckresult == "OK") { + $bckresult = trim($backend->configdRun('crowdsec reconfigure')); + if ($bckresult == "OK") { + $status = "ok"; + } + } + } + return array("status" => $status); + } + + /** + * retrieve status of crowdsec + * @return array + * @throws \Exception + */ + public function statusAction() + { + $backend = new Backend(); + $response = $backend->configdRun("crowdsec crowdsec-status"); + + $status = "unkown"; + if (strpos($response, "not running") > 0) { + $status = "stopped"; + } elseif (strpos($response, "is running") > 0) { + $status = "running"; + } + + $response = $backend->configdRun("crowdsec crowdsec-firewall-status"); + + $firewall_status = "unknown"; + if (strpos($response, "not running") > 0) { + $firewall_status = "stopped"; + } elseif (strpos($response, "is running") > 0) { + $firewall_status = "running"; + } + + return array( + "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); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/VersionController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/VersionController.php new file mode 100644 index 000000000..d236c26f8 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/VersionController.php @@ -0,0 +1,28 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\CrowdSec; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class VersionController extends ApiControllerBase +{ + /** + * retrieve version description + * @return version description + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function getAction() + { + $backend = new Backend(); + return $backend->configdRun("crowdsec version"); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/GeneralController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/GeneralController.php new file mode 100644 index 000000000..115ca686c --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/GeneralController.php @@ -0,0 +1,19 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class GeneralController + * @package OPNsense\CrowdSec + */ +class GeneralController extends \OPNsense\Base\IndexController +{ + public function indexAction() + { + $this->view->pick('OPNsense/CrowdSec/general'); + $this->view->generalForm = $this->getForm("general"); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/OverviewController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/OverviewController.php new file mode 100644 index 000000000..6dbd461d5 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/OverviewController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class OverviewController + * @package OPNsense\CrowdSec + */ +class OverviewController extends \OPNsense\Base\IndexController +{ + public function indexAction() + { + $this->view->pick('OPNsense/CrowdSec/overview'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/forms/general.xml b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/forms/general.xml new file mode 100644 index 000000000..596c3b3e9 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/forms/general.xml @@ -0,0 +1,95 @@ +
+ + + + general.agent_enabled + + checkbox + Enable/disable the CrowdSec agent. Keep this enabled to detect + attacks and receive alerts from the CrowSec central service. + + + + + general.lapi_enabled + + checkbox + Enable/disable the CrowdSec Local API. Keep this enabled unless you + connect to a LAPI on another machine + + + + + general.firewall_bouncer_enabled + + checkbox + Enable/disable the firewall bouncer. Keep this enabled to block + packets from the attacking IP addresses. + + + + + general.lapi_manual_configuration + + checkbox + Avoid overwriting LAPI settings for config.yaml, + local_api_credentials.yaml, crowdsec-firewall-bouncer.yaml. The next + two configuration options (lapi_listen_address, lapi_listen_port) will + be ignored. Allows unsupported configurations like linking together + multiple opnsense instances or connecting to an existing crowdsec + multi-server setup. + + + + + general.lapi_listen_address + + text + Where to listen for LAPI connections: IP address. The default value + is 127.0.0.1. You can change it to a LAN address to connect from other + agents/machines and bouncers. + + This is written in /usr/local/etc/crowdsec/config.yaml, + local_api_credentials.yaml and bouncers/crowdsec-firewall-bouncer.yaml. + To enable TLS, add the certificate information to config.yaml and change + http to https in the other two files. Comments in YAML will not be + preserved. + + + + + general.lapi_listen_port + + text + Where to listen for LAPI connections: port. The default value is + 8080, but you can change it to avoid conflicts with existing + services. + + + + + general.rules_log + + checkbox + Enable log collection for CrowdSec's block rules. + + + + + general.rules_tag + + text + Add a tag to packets that are dropped by CrowdSec rules for + diagnostic purposes. + + + + + general.crowdsec_firewall_verbose + + checkbox + Verbose /var/log/crowdsec/crowdsec-firewall-bouncer.log. Enable this + for debugging. + + +
diff --git a/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/ACL/ACL.xml b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/ACL/ACL.xml new file mode 100644 index 000000000..54840d23f --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + CrowdSec + + ui/crowdsec/* + api/crowdsec/* + + + diff --git a/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.php b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.php new file mode 100644 index 000000000..3307e8ba6 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.php @@ -0,0 +1,12 @@ + + +namespace OPNsense\CrowdSec; + +use OPNsense\Base\BaseModel; + +class General extends BaseModel +{ +} diff --git a/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.xml b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.xml new file mode 100644 index 000000000..16d3b3632 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.xml @@ -0,0 +1,56 @@ + + //OPNsense/crowdsec/general + CrowdSec general configuration + 0.2 + + + + 1 + Y + + + + 1 + Y + + + + 1 + Y + + + + 0 + Y + + + + 127.0.0.1 + Y + ((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$)) + + + + 8080 + Y + N + N + + + + 0 + Y + + + + + N + + + + 0 + Y + + + + diff --git a/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/Menu/Menu.xml b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/Menu/Menu.xml new file mode 100644 index 000000000..c50b8cd43 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/Menu/Menu.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/general.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/general.volt new file mode 100644 index 000000000..e6753a4bf --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/general.volt @@ -0,0 +1,142 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + +
+
+

This plugin installs a CrowdSec agent/LAPI + node, and a Firewall Bouncer.

+ +

Out of the box, by enabling them in the "Settings" tab, they can protect the OPNsense server + by receiving thousands of IP addresses of active attackers, which are immediately banned at the + firewall level. In addition, the logs of the ssh service and OPNsense administration interface are + analyzed for possible brute-force attacks; any such scenario triggers a ban and is reported to the + CrowdSec Central API + (meaning timestamp, scenario, attacking IP).

+ +

Other attack behaviors can be recognized on the OPNsense server and its plugins, or + any other agent + connected to the same LAPI node. Other types of remediation are possible (ex. captcha test for scraping attempts).

+ +

Please refer to the tutorials to explore + the possibilities.

+ +

A few remarks:

+ +
    +
  • + If your OPNsense is <22.1, you must check "Disable circular logs" in the Settings menu for the + ssh and web-auth parsers to work. If you upgrade to 22.1, it will be done automatically. + See acquis.d/opnsense.yaml +
  • +
  • + At the moment, the CrowdSec package for OPNsense is fully functional on the + command line but its web interface is limited; you can only list the installed objects and revoke + decisions. For anything else + you need the shell. +
  • +
  • + Do not enable/start the agent and bouncer services with sysrc or /etc/rc.conf + like you would on vanilla freebsd, the plugin takes care of that. +
  • +
  • + The parsers, scenarios and all objects from the CrowdSec Hub + are periodically upgraded. The + crowdsecurity/freebsd and + crowdsecurity/opnsense + collections are installed by default. +
  • +
+ +
+ + crowdsec.net + + + Documentation + + + Blog + + + Console + + + CrowdSec Hub + +
+ +
+ + GitHub + + + Discourse + + + Discord + + + Twitter + +
+
+ +
+ +
+ {{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_GeneralSettings'])}} +
+ +
+ +
+
+
+ diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/overview.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/overview.volt new file mode 100644 index 000000000..87987c30a --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/overview.volt @@ -0,0 +1,249 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + +
+ Service status: crowdsec ... - firewall bouncer ... +
+ + + +
+ +
+ + + + + + + + + + + + + + + + +
NameIP AddressLast UpdateValidated?Version
+
+ +
+ + + + + + + + + + + + + + + + + +
NameIP AddressValidLast API PullTypeVersion
+
+ +
+ + + + + + + + + + + + + + + +
NameStatusVersionLocal Path
+
+ +
+ + + + + + + + + + + + + + + + +
NameStatusVersionPathDescription
+
+ +
+ + + + + + + + + + + + + + + + +
NameStatusVersionLocal PathDescription
+
+ +
+ + + + + + + + + + + + + + + + +
NameStatusVersionLocal PathDescription
+
+ +
+ + + + + + + + + + + + + + + + + + +
IDValueReasonCountryASDecisionsCreated At
+
+ +
+ Note: the decisions coming from the CAPI (signals collected by the CrowdSec users) do not appear here. + To show them, use cscli decisions list -a in a shell. + + + + + + + + + + + + + + + + + + + + + + +
IDSourceScope:ValueReasonActionCountryASEventsExpirationAlert ID
+
+ +
+
+        
+
+ + + + +
diff --git a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh new file mode 100755 index 000000000..1c00e7a1b --- /dev/null +++ b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +if [ ! -e "/usr/local/etc/crowdsec/collections/opnsense.yaml" ]; then + /usr/local/bin/cscli --error collections install crowdsecurity/opnsense +fi + +/usr/local/bin/cscli --error hub update \ + && /usr/local/bin/cscli --error hub upgrade + +if service crowdsec enabled; then + # have to check status explicitly because "restart" can set $? = 0 even when failing + if ! service crowdsec status >/dev/null 2>&1; then + service crowdsec start >/dev/null 2>&1 || : + else + service crowdsec restart >/dev/null 2>&1 || : + fi +fi + diff --git a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py new file mode 100755 index 000000000..6f012249b --- /dev/null +++ b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +import logging +import json +import urllib.parse +import yaml + +logging.basicConfig(level=logging.INFO) + + +def load_config(filename): + with open(filename) as fin: + return yaml.safe_load(fin) + + +# only save if some value has changed +def save_config(filename, new_config): + 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): + # 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): + old_tuple = urllib.parse.urlsplit(old_url) + new_tuple = old_tuple._replace(netloc=get_netloc(settings)) + new_url = urllib.parse.urlunsplit(new_tuple) + # client lapi requires a trailing slash for the path part + # and no, query and fragment don't make much sense + if not new_tuple.query and not new_tuple.fragment and not new_url.endswith('/'): + new_url += '/' + return new_url + + +def configure_agent(settings): + config_path = '/usr/local/etc/crowdsec/config.yaml' + config = load_config(config_path) + + config['common']['log_dir'] = '/var/log/crowdsec' + config['crowdsec_service']['acquisition_dir'] = '/usr/local/etc/crowdsec/acquis.d/' + + if not int(settings.get('lapi_manual_configuration', '0')): + config['api']['server']['listen_uri'] = get_netloc(settings) + + save_config(config_path, config) + + +def configure_lapi_credentials(settings): + config_path = '/usr/local/etc/crowdsec/local_api_credentials.yaml' + config = load_config(config_path) + + if not int(settings.get('lapi_manual_configuration', '0')): + config['url'] = get_new_url(config['url'], settings) + + save_config(config_path, config) + + +def configure_bouncer(settings): + 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['pf'] = {'anchor_name': ''} + + if not int(settings.get('lapi_manual_configuration', '0')): + config['api_url'] = get_new_url(config['api_url'], settings) + + save_config(config_path, config) + + +def main(): + try: + with open('/usr/local/etc/crowdsec/opnsense/settings.json') as f: + settings = json.load(f) + except FileNotFoundError: + logging.info("settings.json not found, won't change crowdsec config") + return + + configure_agent(settings) + configure_lapi_credentials(settings) + configure_bouncer(settings) + + +if __name__ == '__main__': + main() diff --git a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh new file mode 100755 index 000000000..c6d0c2c2c --- /dev/null +++ b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# This script is run +# - when the plugin is installed (by +POST_INSTALL.post) +# - when saving the "settings" form (which calls /api/crowdsec/service/reload) +# - by hand, running "configctl crowdsec reconfigure" + +set -e + +# apply configuration options specific to opnsense +/usr/local/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py + +# enable pf anchor here - the tables and rules will be created by the bouncer +/usr/local/sbin/configctl filter reload >/dev/null + +# the hub is upgraded by cron too +/usr/local/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh + +# crowdsec was already restarted by hub-upgrade.sh +if service crowdsec_firewall enabled; then + # have to check status explicitly because "restart" can set $? = 0 even when failing + if ! service crowdsec_firewall status >/dev/null 2>&1; then + service crowdsec_firewall start >/dev/null 2>&1 || : + else + service crowdsec_firewall restart >/dev/null 2>&1 || : + fi +fi + +# left from v0.0.8 +rm -f /usr/local/etc/crowdsec/opnsense-settings.json + +echo "OK" + diff --git a/security/crowdsec/src/opnsense/service/conf/actions.d/actions_crowdsec.conf b/security/crowdsec/src/opnsense/service/conf/actions.d/actions_crowdsec.conf new file mode 100644 index 000000000..e0e0f6293 --- /dev/null +++ b/security/crowdsec/src/opnsense/service/conf/actions.d/actions_crowdsec.conf @@ -0,0 +1,94 @@ + +# https://docs.opnsense.org/development/backend/configd.html + +[start] +command:/usr/local/etc/rc.d/oscrowdsec start +type: script +message: starting crowdsec services + +[stop] +command:/usr/local/etc/rc.d/oscrowdsec stop +type: script +message: stopping crowdsec services + +[status] +command:/usr/local/etc/rc.d/oscrowdsec status; exit 0 +type: script_output +message: oscrowdsec status + +[restart] +command:/usr/local/etc/rc.d/oscrowdsec restart +type: script +message: stopping crowdsec services + +[reload] +command:/usr/local/etc/rc.d/oscrowdsec reload +type: script +message: reload crowdsec configuration + +[crowdsec-status] +command:/usr/local/etc/rc.d/crowdsec status;exit 0 +type:script_output +message: request crowdsec status + +[crowdsec-firewall-status] +command:/usr/local/etc/rc.d/crowdsec_firewall status;exit 0 +type:script_output +message: request crowdsec_firewall status + +[alerts-list] +command:/usr/local/bin/cscli alerts list -l 0 -o json | sed 's/^null$/\[\]/' +type:script_output +message:crowdsec alerts list + +[bouncers-list] +command:/usr/local/bin/cscli bouncers list -o json | sed 's/^null$/\[\]/' +type:script_output +message:crowdsec bouncers list + +[collections-list] +command:/usr/local/bin/cscli collections list -o json +type:script_output +message:crowdsec collections list + +[decisions-list] +command:/usr/local/bin/cscli decisions list -l 0 -o json | sed 's/^null$/\[\]/' +type:script_output +message:crowdsec decisions list + +[decisions-delete] +command:/usr/local/bin/cscli --error decisions delete 2>&1 +parameters:--id %s +type:script_output +message:crowdsec decisions delete + +[machines-list] +command:/usr/local/bin/cscli machines list -o json | sed 's/^null$/\[\]/' +type:script_output +message:crowdsec machines 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 + +[scenarios-list] +command:/usr/local/bin/cscli scenarios list -o json +type:script_output +message:crowdsec scenarios list + +[version] +command:/usr/local/bin/cscli version 2>&1 +type:script_output +message:crowdsec version + +[reconfigure] +command:/usr/local/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh +type:script_output +message:crowdsec reconfigure + diff --git a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/+TARGETS b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/+TARGETS new file mode 100644 index 000000000..c2ebc9224 --- /dev/null +++ b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/+TARGETS @@ -0,0 +1,4 @@ +oscrowdsec.rc.conf.d:/etc/rc.conf.d/oscrowdsec +crowdsec.rc.conf.d:/etc/rc.conf.d/crowdsec +crowdsec_firewall.rc.conf.d:/etc/rc.conf.d/crowdsec_firewall +settings.json:/usr/local/etc/crowdsec/opnsense/settings.json diff --git a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec.rc.conf.d b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec.rc.conf.d new file mode 100644 index 000000000..77eebf92f --- /dev/null +++ b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec.rc.conf.d @@ -0,0 +1,11 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +{% if helpers.exists('OPNsense.crowdsec.general.agent_enabled') and OPNsense.crowdsec.general.agent_enabled|default("1") == "1" %} +crowdsec_enable="YES" +{% else %} +crowdsec_enable="NO" +{% endif %} +{% if helpers.exists('OPNsense.crowdsec.general.lapi_enabled') and OPNsense.crowdsec.general.lapi_enabled|default("1") == "1" %} +crowdsec_flags="" +{% else %} +crowdsec_flags="-no-api" +{% endif %} diff --git a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec_firewall.rc.conf.d b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec_firewall.rc.conf.d new file mode 100644 index 000000000..61bae583b --- /dev/null +++ b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec_firewall.rc.conf.d @@ -0,0 +1,11 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +{% if helpers.exists('OPNsense.crowdsec.general.firewall_bouncer_enabled') and OPNsense.crowdsec.general.firewall_bouncer_enabled|default("1") == "1" %} +crowdsec_firewall_enable="YES" +{% else %} +crowdsec_firewall_enable="NO" +{% endif %} +{% if helpers.exists('OPNsense.crowdsec.general.crowdsec_firewall_verbose') and OPNsense.crowdsec.general.crowdsec_firewall_verbose|default("0") == "1" %} +crowdsec_firewall_flags="-v" +{% else %} +crowdsec_firewall_flags="" +{% endif %} diff --git a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/oscrowdsec.rc.conf.d b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/oscrowdsec.rc.conf.d new file mode 100644 index 000000000..dd3cec08e --- /dev/null +++ b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/oscrowdsec.rc.conf.d @@ -0,0 +1 @@ +oscrowdsec_enable="YES" diff --git a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/settings.json b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/settings.json new file mode 100644 index 000000000..0c3ed00b0 --- /dev/null +++ b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/settings.json @@ -0,0 +1,5 @@ +{% if helpers.exists('OPNsense.crowdsec.general') -%} + {{ OPNsense.crowdsec.general | tojson }} +{%- endif %} + + diff --git a/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec.js b/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec.js new file mode 100644 index 000000000..39eaacd43 --- /dev/null +++ b/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec.js @@ -0,0 +1,426 @@ +/*global moment, $ */ +/*exported CrowdSec */ +/*eslint no-undef: "error"*/ +/*eslint semi: "error"*/ + +var CrowdSec = (function() { + 'use strict'; + + var _refresh_template = ''; + + var _dataFormatters = { + yesno: function(column, row) { + return _yesno2html(row[column.id]); + }, + + delete: function(column, row) { + var val = row.id; + if (isNaN(val)) { + return ''; + } + return ''; + }, + + duration: function(column, row) { + var duration = row[column.id]; + if (!duration) { + return 'n/a'; + } + return $('
').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 $('
').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 ("m" === parseInt(matches[5], 10)) { + // units in milliseconds + seconds *= 0.001; + } + if ("-" === parseInt(matches[1], 10)) { + // negative + seconds = -seconds; + } + return seconds; + } + + function _updateFreshness(selector, timestamp) { + var $freshness = $(selector).find('.actionBar .freshness'); + if (timestamp) { + $freshness.data('refresh_timestamp', timestamp); + } else { + timestamp = $freshness.data('refresh_timestamp'); + } + var howlong_human = '???'; + if (timestamp) { + var howlong_ms = moment() - moment(timestamp); + howlong_human = moment.duration(howlong_ms).humanize(); + } + $freshness.text(howlong_human + ' ago'); + } + + function _addFreshness(selector) { + // this creates one timer per tab + var freshness_template = 'Last refresh: '; + $(selector).find('.actionBar').prepend(freshness_template); + setInterval(function() { + _updateFreshness(selector); + }, 5000); + } + + function _humanizeDate(text) { + return moment(text).fromNow(); + } + + function _humanizeDuration(text) { + return moment.duration(_parseDuration(text), 'seconds').humanize(); + } + + function _yesno2html(val) { + if (val) { + return ''; + } else { + return ''; + } + } + + 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 = ''; + for (var 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 crowdsec_status = data['crowdsec-status']; + if (crowdsec_status === 'unknown') { + crowdsec_status = 'Unknown'; + } else { + crowdsec_status = _yesno2html(crowdsec_status === 'running'); + } + $('#crowdsec-status').html(crowdsec_status); + + var crowdsec_firewall_status = data['crowdsec-firewall-status']; + if (crowdsec_firewall_status === 'unknown') { + crowdsec_firewall_status = 'Unknown'; + } else { + crowdsec_firewall_status = _yesno2html(crowdsec_firewall_status === 'running'); + } + $('#crowdsec-firewall-status').html(crowdsec_firewall_status); + }); + } + + 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() { + $(_refresh_template).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); + _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 _initCollections() { + var url = '/api/crowdsec/collections/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 || ' ' + }); + }); + $('#collections table').bootgrid('clear').bootgrid('append', rows); + }; + _initTab('#collections', url, dataCallback); + } + + function _initScenarios() { + var url = '/api/crowdsec/scenarios/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 || ' ', + description: row.description || ' ' + }); + }); + $('#scenarios table').bootgrid('clear').bootgrid('append', rows); + }; + _initTab('#scenarios', url, dataCallback); + } + + function _initParsers() { + var url = '/api/crowdsec/parsers/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 || ' ', + description: row.description || ' ' + }); + }); + $('#parsers table').bootgrid('clear').bootgrid('append', rows); + }; + _initTab('#parsers ', url, dataCallback); + } + + function _initPostoverflows() { + var url = '/api/crowdsec/postoverflows/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 || ' ', + description: row.description || ' ' + }); + }); + $('#postoverflows table').bootgrid('clear').bootgrid('append', rows); + }; + _initTab('#postoverflows ', url, dataCallback); + } + + 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); + } + + 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); + } + + 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 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'); + } + } + }); + }); + $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 + }; + +}());