From 885b626d80472b16f200f047c059053ca6963aba Mon Sep 17 00:00:00 2001 From: mmetc <92726601+mmetc@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:54:05 +0200 Subject: [PATCH] v0.1.4, crowdsec 1.6.3-1, installer script (#95) --- install-crowdsec.sh | 313 +++ security/pfSense-pkg-crowdsec/Makefile | 5 +- .../files/pkg-deinstall.in | 2 +- .../pfSense-pkg-crowdsec/files/pkg-install.in | 4 +- .../files/usr/local/pkg/crowdsec.inc | 56 +- .../files/usr/local/pkg/crowdsec.xml | 15 +- .../local/share/pfSense-pkg-crowdsec/info.xml | 2 +- .../usr/local/www/crowdsec/endpoint/api.php | 34 +- .../usr/local/www/crowdsec/js/crowdsec.js | 1865 +++++++++-------- .../files/usr/local/www/crowdsec/metrics.html | 292 +-- .../files/usr/local/www/crowdsec/status.php | 4 +- security/pfSense-pkg-crowdsec/pkg-descr | 5 + 12 files changed, 1508 insertions(+), 1089 deletions(-) create mode 100755 install-crowdsec.sh diff --git a/install-crowdsec.sh b/install-crowdsec.sh new file mode 100755 index 0000000..9f9b299 --- /dev/null +++ b/install-crowdsec.sh @@ -0,0 +1,313 @@ +#!/bin/sh + +set -eu + +# Allow downloading on other systems too +download() { + if command -v fetch > /dev/null; then + fetch -q -o - "$1" + elif command -v curl > /dev/null; then + curl -fsSL "$1" + elif command -v wget > /dev/null; then + wget --no-verbose -O- "$1" + else + echo "Error: No suitable download tool found. Please install fetch, wget, or curl." + exit 1 + fi +} + +terminate_services() { + echo "Stopping crowdsec services..." + PID_FILES="/var/run/crowdsec_daemon.pid /var/run/crowdsec.pid /var/run/crowdsec_firewall.pid" + for pidfile in $PID_FILES; do + if [ -f "$pidfile" ]; then + PID=$(cat "$pidfile") + if kill -0 "$PID" > /dev/null 2>&1; then + # don't use TERM, to make sure sbin/daemon doesn't hang if crowdsec is misconfigured + kill -INT "$PID" || true + else + echo "Process $PID (from $pidfile) is not running." + fi + fi + done + + service crowdsec onestop || true + service crowdsec_firewall onestop || true +} + +# Set variables used by get_archive +set_vars() { + REPO_OWNER="crowdsecurity" + REPO_NAME="pfSense-pkg-crowdsec" + + # Fetch the latest stable release, unless a specific tag is requested + RELEASE_TYPE="latest" + + if [ "$(uname -s)" = "FreeBSD" ]; then + DETECTED_ARCH=$(uname -m) + DETECTED_FREEBSD_VERSION=$(uname -r | cut -d- -f1 | cut -d. -f1) + else + DETECTED_ARCH= + DETECTED_FREEBSD_VERSION= + fi + + if [ -z "$ARCH" ]; then + ARCH="$DETECTED_ARCH" + fi + + if [ -z "$FREEBSD_VERSION" ]; then + FREEBSD_VERSION="$DETECTED_FREEBSD_VERSION" + fi + + if [ -z "$ARCH" ] || [ -z "$FREEBSD_VERSION" ]; then + echo "Error: This script is intended for FreeBSD systems." + echo "Please specify both --arch and --freebsd parameters to continue." + echo "Example: $0 --arch amd64 --freebsd 15" + exit 1 + fi + + if [ "$(uname -i)" != "pfSense" ]; then + echo "Warning: This script is intended for pfSense systems." + echo "If this is not the case you will be able to download the packages, but you may not be able to use them." + echo + fi + + if [ -n "$RELEASE_TAG" ]; then + RELEASE_TYPE="tags/$RELEASE_TAG" + fi + +} + + +# download the required archive and set the $TARFILE variable +get_archive() { + URL="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/$RELEASE_TYPE" + echo "Looking up $URL" + + # Fetch the release data from GitHub API + RELEASE_JSON=$(download "$URL") + + TARFILE="freebsd-$FREEBSD_VERSION-$ARCH.tar" + + echo "Selecting archive for FreeBSD $FREEBSD_VERSION/$ARCH" + + # We have jq at home + ASSET_URL=$(echo "$RELEASE_JSON" | tr ',' '\n' | grep '"browser_download_url":' | grep "/$TARFILE" | sed -E 's/.*"browser_download_url": *"([^"]+)".*/\1/') + + if [ -z "$ASSET_URL" ]; then + echo "Error: Can't find file $TARFILE in the release assets." + exit 1 + fi + + echo + echo "The archive to be downloaded is: $ASSET_URL" + + echo + printf "Do you want to proceed with the download? (y/N) " + read -r REPLY + if [ "$REPLY" != "y" ] && [ "$REPLY" != "Y" ]; then + echo "Download canceled." + exit 1 + fi + + echo "Downloading archive: $ASSET_URL" + echo "done." + + download "$ASSET_URL" > "$TARFILE" +} + + +install_packages() { + if ! command -v pkg > /dev/null; then + echo + echo "Error: The 'pkg' command is not available on this system." + echo "Please manually install the packages using 'pkg add -f' on a pfSense system, or run this script there." + exit 1 + fi + + if [ "$(id -u)" -ne 0 ]; then + echo + echo "This script must be run as root. Please run it as root or manually install the packages." + exit 1 + fi + + TMP_DIR=$(mktemp -d) + trap 'rm -rf "$TMP_DIR"' EXIT + + echo "Extracting archive to $TMP_DIR" + tar -xzf "$TARFILE" -C "$TMP_DIR" + + # Install the packages in order to respect the dependencies + PKG_NAMES="abseil re2 crowdsec-firewall-bouncer crowdsec pfSense-pkg-crowdsec" + + echo + echo "The following packages are ready for installation:" + + PKG_PATHS="" + for package in $PKG_NAMES; do + # match a digit from the version to avoid matching other packages too + PKG_PATH=$(find "$TMP_DIR" -name "$package-[0-9]*.pkg") + if [ -n "$PKG_PATH" ]; then + echo " - $(basename "$PKG_PATH")" + PKG_PATHS="$PKG_PATHS $PKG_PATH" + else + echo "Error: Package $package not found in the archive." + exit 1 + fi + done + + echo + printf "Do you want to install them now? (y/N) " + read -r REPLY + if [ "$REPLY" != "y" ] && [ "$REPLY" != "Y" ]; then + echo "Installation canceled." + exit 1 + fi + + terminate_services + + # prevent the services from starting before the plugin configures the filter tables + rm -f /var/run/crowdsec.running /var/run/crowdsec_firewall.running + + for PKG_PATH in $PKG_PATHS; do + echo "Installing $(basename "$PKG_PATH")" + pkg add -qf "$PKG_PATH" + sleep 3 + done + + # Clean up + rm -rf "$TMP_DIR" + + echo "# -------------- #" + echo "Installation complete." + echo "You can configure and activate CrowdSec on you pfSense admin page (Package / Services: CrowdSec)." +} + + +uninstall_packages() { + if [ "$(uname -s)" != "FreeBSD" ]; then + echo "Error: The --uninstall option is intended for FreeBSD systems." + exit 1 + fi + + if [ "$(id -u)" -ne 0 ]; then + echo + echo "This script must be run as root to uninstall the packages." + exit 1 + fi + + # Uninstall the packages in order of dependency + PKG_NAMES="pfSense-pkg-crowdsec crowdsec-firewall-bouncer crowdsec" + INSTALLED_PKGS="" + + echo "Checking for installed CrowdSec-related packages..." + for package in $PKG_NAMES; do + if pkg info "$package" > /dev/null 2>&1; then + PKG_VERSION=$(pkg info "$package" | grep Version | awk '{print $3}') + echo " - $package (version $PKG_VERSION) is installed." + INSTALLED_PKGS="$INSTALLED_PKGS $package" + fi + done + + if [ -z "$INSTALLED_PKGS" ]; then + echo "No CrowdSec-related packages are installed." + exit 0 + fi + + echo + printf "Do you want to uninstall these packages? (y/N) " + read -r REPLY + if [ "$REPLY" != "y" ] && [ "$REPLY" != "Y" ]; then + echo "Uninstallation canceled." + exit 1 + fi + + # In case the service management is not behaving correctly, stop the services manually + terminate_services + + echo "Uninstalling packages..." + for package in $INSTALLED_PKGS; do + echo "Removing $package..." + pkg delete -y "$package" + done + + echo "Uninstallation complete." + echo "Configuration and data are left in /usr/local/etc/crowdsec and /var/db/crowdsec," + echo "in case you want to reinstall or upgrade CrowdSec." + + rm -f /var/run/crowdsec.running /var/run/crowdsec_firewall.running +} + + +# -------------- # + +RELEASE_TAG="" +ARCH="" +FREEBSD_VERSION="" +TARFILE="" + +while [ $# -gt 0 ]; do + case "$1" in + --release) + RELEASE_TAG="$2" + shift + ;; + --arch) + ARCH="$2" + shift + ;; + --freebsd) + FREEBSD_VERSION="$2" + shift + ;; + --from) + TARFILE="$2" + shift + ;; + --uninstall) + uninstall_packages + shift + exit 0 + ;; + *) + echo "Usage: $0 [--release ] [--arch ] [--freebsd ] | [--from ]" + exit 1 + ;; + esac + shift +done + +echo "#----------------------------------------------------------------#" +echo "# This script is intended to be used only if the CrowdSec #" +echo "# package is not available in the official pfSense repositories, #" +echo "# or to test pre-release versions. #" +echo "# Please check the pfSense package manager before proceeding. #" +echo "#----------------------------------------------------------------#" +echo + +# Prompt user for confirmation to proceed +printf "Do you want to continue? (y/N) " +read -r REPLY +if [ "$REPLY" != "y" ] && [ "$REPLY" != "Y" ]; then + echo "Operation canceled." + exit 1 +fi + +echo + +if [ -n "$TARFILE" ]; then + if [ -n "$RELEASE_TAG" ] || [ -n "$ARCH" ] || [ -n "$FREEBSD_VERSION" ]; then + echo "Error: When using --from an existing archive, you can't select release, architecture or freebsd version." + exit 1 + fi + if [ ! -f "$TARFILE" ]; then + echo "Error: Specified tar file $TARFILE does not exist." + exit 1 + fi + echo "Using provided tar file: $TARFILE" +else + set_vars + get_archive +fi + +install_packages diff --git a/security/pfSense-pkg-crowdsec/Makefile b/security/pfSense-pkg-crowdsec/Makefile index 4190983..2fcc6b4 100644 --- a/security/pfSense-pkg-crowdsec/Makefile +++ b/security/pfSense-pkg-crowdsec/Makefile @@ -1,7 +1,7 @@ # $FreeBSD$ PORTNAME= pfSense-pkg-crowdsec -PORTVERSION= 0.1.3 +PORTVERSION= 0.1.4 CATEGORIES= security MASTER_SITES= # empty DISTFILES= # empty @@ -12,7 +12,7 @@ COMMENT= Crowdsec package for pfSense LICENSE= APACHE20 -RUN_DEPENDS= crowdsec>=1.5.5:security/crowdsec \ +RUN_DEPENDS= crowdsec>=1.6.3:security/crowdsec \ crowdsec-firewall-bouncer>=0.0.28_1:security/crowdsec-firewall-bouncer NO_BUILD= yes @@ -29,7 +29,6 @@ do-install: ${MKDIR} ${STAGEDIR}${PREFIX}/www/shortcuts ${MKDIR} ${STAGEDIR}${PREFIX}/www/crowdsec/js ${MKDIR} ${STAGEDIR}${PREFIX}/www/crowdsec/css - ${MKDIR} ${STAGEDIR}${PREFIX}/www/crowdsec/images ${MKDIR} ${STAGEDIR}${PREFIX}/www/crowdsec/endpoint ${MKDIR} ${STAGEDIR}/etc/inc/priv ${MKDIR} ${STAGEDIR}${DATADIR} diff --git a/security/pfSense-pkg-crowdsec/files/pkg-deinstall.in b/security/pfSense-pkg-crowdsec/files/pkg-deinstall.in index 83d1c3d..090f793 100644 --- a/security/pfSense-pkg-crowdsec/files/pkg-deinstall.in +++ b/security/pfSense-pkg-crowdsec/files/pkg-deinstall.in @@ -1,3 +1,3 @@ #!/bin/sh -/usr/local/bin/php -f /etc/rc.packages %%PORTNAME%% ${2} +"${PKG_ROOTDIR}/usr/local/bin/php" -f /etc/rc.packages %%PORTNAME%% "$2" diff --git a/security/pfSense-pkg-crowdsec/files/pkg-install.in b/security/pfSense-pkg-crowdsec/files/pkg-install.in index c192d05..f428bf2 100644 --- a/security/pfSense-pkg-crowdsec/files/pkg-install.in +++ b/security/pfSense-pkg-crowdsec/files/pkg-install.in @@ -1,7 +1,7 @@ #!/bin/sh -if [ "${2}" != "POST-INSTALL" ]; then +if [ "$2" != "POST-INSTALL" ]; then exit 0 fi -${PKG_ROOTDIR}/usr/local/bin/php -f ${PKG_ROOTDIR}/etc/rc.packages %%PORTNAME%% ${2} +"${PKG_ROOTDIR}/usr/local/bin/php" -f "${PKG_ROOTDIR}/etc/rc.packages" %%PORTNAME%% "$2" diff --git a/security/pfSense-pkg-crowdsec/files/usr/local/pkg/crowdsec.inc b/security/pfSense-pkg-crowdsec/files/usr/local/pkg/crowdsec.inc index b10114c..be40bc1 100644 --- a/security/pfSense-pkg-crowdsec/files/usr/local/pkg/crowdsec.inc +++ b/security/pfSense-pkg-crowdsec/files/usr/local/pkg/crowdsec.inc @@ -57,6 +57,32 @@ $crowdsec_aliases = array( ); + +/** + * update the XML configuration: convert old options, drop deprecated, etc. + * + * @return void + */ +function crowdsec_config_migrate() +{ + parse_config(true); + $cf = config_get_path('installedpackages/crowdsec/config/0', []); + + // noop on fresh install + if (!$cf) { + return; + } + + // for backward compatibility, convert remote_lapi_host and remote_lapi_port to remote_lapi_url + if (empty($cf['remote_lapi_url']) && !empty($cf['remote_lapi_host'])) { + $cf['remote_lapi_url'] = 'http://' . $cf['remote_lapi_host'] . ':' . $cf['remote_lapi_port']; + unset($cf['remote_lapi_host']); + unset($cf['remote_lapi_port']); + config_set_path('installedpackages/crowdsec/config/0', $cf); + } + write_config('pfsense_crowdsec: configuration updated.'); +} + /** * custom_php_validation_command hook (setting edition before submit) * @@ -74,17 +100,13 @@ function crowdsec_validate_form($post, &$input_errors) } // Use external LAPI if (empty($post['enable_lapi'])) { - if (empty($post['remote_lapi_port'])) { - $input_errors[] = 'Remote LAPI port is required for remote LAPI.'; + if (empty($post['remote_lapi_url'])) { + $input_errors[] = 'An URL is required for remote LAPI.'; } else { - $remote_lapi_port = $post['remote_lapi_port']; - if (!is_numeric($remote_lapi_port)) { - $input_errors[] = 'Remote LAPI port must be a numeric value.'; + if (filter_var($post['remote_lapi_url'], FILTER_VALIDATE_URL) === FALSE) { + $input_errors[] = 'Remote LAPI URL is not valid.'; } } - if (empty($post['remote_lapi_host'])) { - $input_errors[] = 'Remote LAPI host is required for remote LAPI.'; - } if (empty($post['remote_agent_user'])) { $input_errors[] = 'Agent user is required for external LAPI usage.'; } @@ -127,6 +149,7 @@ function crowdsec_validate_form($post, &$input_errors) */ function crowdsec_install() { + crowdsec_config_migrate(); $candidate_log_dir = '/var/log/crowdsec/'; $candidate_log_max_size = 999999; global $crowdsec_aliases; @@ -271,6 +294,7 @@ function crowdsec_resync_config() if (!$cf) { return; } + // Init some flags to handle services management $should_use_crowdsec = false; $should_use_firewall = false; @@ -302,7 +326,7 @@ function crowdsec_resync_config() if (!empty($fw_conf)) { $fw_conf['api_key'] = '${API_KEY}'; if (!empty($cf['lapi_port']) && !empty($cf['lapi_host'])) { - $fw_conf['api_url'] = 'http://' . $cf['lapi_host'] . ':' . $cf['lapi_port'] . '/'; + $fw_conf['api_url'] = 'http://' . $cf['lapi_host'] . ':' . $cf['lapi_port']; } } @@ -315,8 +339,8 @@ function crowdsec_resync_config() * @see /usr/local/etc/rc.d/crowdsec_firewall */ // there is no --force option, we ignore errors if the machine or bouncer don't exist - mwexec('cscli bouncers delete pfsense-firewall 2>/dev/null'); - mwexec('cscli machines delete pfsense 2>/dev/null'); + mwexec('cscli bouncers delete pfsense-firewall --ignore-missing'); + mwexec('cscli machines delete pfsense --ignore-missing'); } else { // Handle Remote LAPI ON (e.g. LAPI OFF) // Modify Agent yaml config if (!empty($crowdsec_conf)) { @@ -328,8 +352,9 @@ function crowdsec_resync_config() $lapi_credentials['login'] = $cf['remote_agent_user']; $lapi_credentials['password'] = $cf['remote_agent_password']; } - if (!empty($cf['remote_lapi_port']) && !empty($cf['remote_lapi_host'])) { - $lapi_credentials['url'] = 'http://' . $cf['remote_lapi_host'] . ':' . $cf['remote_lapi_port']; + if (!empty($cf['remote_lapi_url'])) { + // TODO: clean up the url?? + $lapi_credentials['url'] = $cf['remote_lapi_url']; } } // Modify Firewall bouncer yaml config @@ -337,8 +362,8 @@ function crowdsec_resync_config() if (!empty($cf['remote_fw_bouncer_api_key'])) { $fw_conf['api_key'] = $cf['remote_fw_bouncer_api_key']; } - if (!empty($cf['remote_lapi_port']) && !empty($cf['remote_lapi_host'])) { - $fw_conf['api_url'] = 'http://' . $cf['remote_lapi_host'] . ':' . $cf['remote_lapi_port'] . '/'; + if (!empty($cf['remote_lapi_url'])) { + $fw_conf['api_url'] = $cf['remote_lapi_url']; } } } @@ -404,6 +429,7 @@ function crowdsec_resync_config() if (!file_exists(CROWDSEC_PFSENSE_COLLECTION)) { if (!empty($crowdsec_conf)) { shell_exec("cscli hub update"); + shell_exec("cscli hub upgrade"); shell_exec("cscli --error collections install crowdsecurity/pfsense"); // We assume reloading won't fail shell_exec("service crowdsec reload"); diff --git a/security/pfSense-pkg-crowdsec/files/usr/local/pkg/crowdsec.xml b/security/pfSense-pkg-crowdsec/files/usr/local/pkg/crowdsec.xml index 49a31e0..e674d1a 100644 --- a/security/pfSense-pkg-crowdsec/files/usr/local/pkg/crowdsec.xml +++ b/security/pfSense-pkg-crowdsec/files/usr/local/pkg/crowdsec.xml @@ -148,20 +148,13 @@ You will need to register the log processor and the bouncer in the LAPI machine, then report here their connection credentials.

Run the following commands on the remote machine:

# cscli machines add pfsense --auto -f -

and

# cscli bouncers add pfsense-firewall
]]>
- Remote LAPI host - remote_lapi_host + Remote LAPI URL + remote_lapi_url input - Host name or IP. + Full URL of the LAPI endpoint, i.e.: http://remote-address:8080/ - Remote LAPI port - remote_lapi_port - input - Port number of the LAPI endpoint. - 8080 - - - User + Login remote_agent_user input Name of the pfSense machine in the remote LAPI diff --git a/security/pfSense-pkg-crowdsec/files/usr/local/share/pfSense-pkg-crowdsec/info.xml b/security/pfSense-pkg-crowdsec/files/usr/local/share/pfSense-pkg-crowdsec/info.xml index f57923e..6654bae 100644 --- a/security/pfSense-pkg-crowdsec/files/usr/local/share/pfSense-pkg-crowdsec/info.xml +++ b/security/pfSense-pkg-crowdsec/files/usr/local/share/pfSense-pkg-crowdsec/info.xml @@ -2,7 +2,7 @@ crowdsec - https://doc.crowdsec.net/docs/next/getting_started/install_crowdsec_pfsense + https://docs.crowdsec.net/docs/next/getting_started/install_crowdsec_pfsense Lightweight and collaborative security engine https://www.crowdsec.net/ %%PKGVERSION%% diff --git a/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/endpoint/api.php b/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/endpoint/api.php index 681bc68..140c7fd 100644 --- a/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/endpoint/api.php +++ b/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/endpoint/api.php @@ -22,8 +22,19 @@ require_once("globals.inc"); -$default = '[]'; +$default = json_encode([]); $method = $_SERVER['REQUEST_METHOD'] ?? ''; + +function getServiceStatus($service) { + $status = trim(shell_exec("service $service onestatus")); + if (strpos($status, "not running") !== false) { + return "stopped"; + } elseif (strpos($status, "is running") !== false) { + return "running"; + } + return "unknown"; +} + if ($method === 'DELETE' && isset($_GET['action']) && isset($_GET['decision_id'])) { $id = (int) strip_tags($_GET['decision_id']); $action = strip_tags($_GET['action']); @@ -35,7 +46,6 @@ else { echo $default; } - } else { echo $default; } @@ -75,27 +85,13 @@ echo shell_exec("/usr/local/bin/cscli metrics -o json"); break; case 'services-status': - $crowdsec = trim(shell_exec("service crowdsec onestatus")); - $crowdsec_status = "unknown"; - if (strpos($crowdsec, "not running") > 0) { - $crowdsec_status = "stopped"; - } elseif (strpos($crowdsec, "is running") > 0) { - $crowdsec_status = "running"; - } - $crowdsec_firewall = trim(shell_exec("service crowdsec_firewall onestatus")); - $crowdsec_firewall_status = "unknown"; - if (strpos($crowdsec_firewall, "not running") > 0) { - $crowdsec_firewall_status = "stopped"; - } elseif (strpos($crowdsec_firewall, "is running") > 0) { - $crowdsec_firewall_status = "running"; - } echo json_encode( [ - 'crowdsec-status' => $crowdsec_status, - 'crowdsec-firewall-status'=> $crowdsec_firewall_status + 'crowdsec-status' => getServiceStatus('crowdsec'), + 'crowdsec-firewall-status'=> getServiceStatus('crowdsec_firewall'), ]); break; - default; + default: echo $default; } } else { diff --git a/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/js/crowdsec.js b/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/js/crowdsec.js index ed1c411..02dce2b 100644 --- a/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/js/crowdsec.js +++ b/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/js/crowdsec.js @@ -4,928 +4,1001 @@ /* eslint semi: "error" */ const CrowdSec = (function () { - 'use strict'; - - const api_url = '/crowdsec/endpoint/api.php'; - const crowdsec_path = '/usr/local/etc/crowdsec/'; - const _refreshTemplate = ''; - - const _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'); + 'use strict'; + + const api_url = '/crowdsec/endpoint/api.php'; + const crowdsec_path = '/usr/local/etc/crowdsec/'; + const _refreshTemplate = + ''; + + const _dataFormatters = { + yesno: function (column, row) { + return _yesno2html(row[column.id]); + }, + + delete: function (column, row) { + const val = row.id; + if (isNaN(val)) { + return ''; + } + return ( + '' + ); + }, + + duration: function (column, row) { + const 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) { + const dt = row[column.id]; + const 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'); + }, + }; + let metricsInterval = null; + + function _decisionsByType(decisions) { + const dectypes = {}; + if (!decisions) { + return ''; + } + decisions.map(function (decision) { + // TODO ignore negative expiration? + dectypes[decision.type] = dectypes[decision.type] + ? dectypes[decision.type] + 1 + : 1; + }); + let ret = ''; + for (const type in dectypes) { + if (ret !== '') { + ret += ' '; + } + ret += type + ':' + dectypes[type]; + } + return ret; + } + + function _updateFreshness(selector, timestamp) { + const $freshness = $(selector).find('.actionBar .freshness'); + if (timestamp) { + $freshness.data('refresh_timestamp', timestamp); + } else { + timestamp = $freshness.data('refresh_timestamp'); + } + const howlongHuman = '???'; + if (timestamp) { + const howlongms = moment() - moment(timestamp); + howlongHuman = moment.duration(howlongms).humanize(); + } + $freshness.text(howlongHuman + ' ago'); + } + + function _addFreshness(selector) { + // this creates one timer per tab + const freshnessTemplate = + 'Last refresh: '; + $(selector).find('.actionBar').prepend(freshnessTemplate); + setInterval(function () { + _updateFreshness(selector); + }, 5000); + } + + function _refreshTab(selector, action, dataCallback) { + $('.loading').show(); + $.ajax({ + url: api_url, + cache: false, + dataType: 'json', + data: { action: action }, + method: 'POST', + success: dataCallback, + complete: function () { + $('.loading').hide(); + _updateFreshness(selector, moment()); + }, + }); + } + + function _parseDuration(duration) { + const re = /(-?)(?:(?:(\d+)h)?(\d+)m)?(\d+).\d+(m?)s/m; + const matches = duration.match(re); + let seconds = 0; + + if (!matches.length) { + throw new Error( + 'Unable to parse the following duration: ' + duration + '.', + ); + } + if (typeof matches[2] !== 'undefined') { + seconds += parseInt(matches[2], 10) * 3600; // hours + } + if (typeof matches[3] !== 'undefined') { + seconds += parseInt(matches[3], 10) * 60; // minutes + } + if (typeof matches[4] !== 'undefined') { + seconds += parseInt(matches[4], 10); // seconds + } + if (parseInt(matches[5], 10) === 'm') { + // units in milliseconds + seconds *= 0.001; + } + if (parseInt(matches[1], 10) === '-') { + // negative + seconds = -seconds; + } + return seconds; + } + + function _humanizeDate(text) { + return moment(text).fromNow(); + } + + function _humanizeDuration(text) { + return moment.duration(_parseDuration(text), 'seconds').humanize(); + } + + function _yesno2html(val) { + if (val) { + return ''; + } else { + return ''; + } + } + + function _initTab(selector, action, dataCallback) { + const tab = $(selector); + const table = tab.find('table.crowdsecTable'); + if (!table.length) { + return; + } + // Navigation + window.location.hash = selector; + history.pushState(null, null, window.location.hash); + table + .on('initialized.rs.jquery.bootgrid', function () { + $(_refreshTemplate) + .on('click', function () { + _refreshTab(selector, action, dataCallback); + }) + .insertBefore(tab.find('.actionBar .actions .dropdown:first')); + _addFreshness(selector); + _refreshTab(selector, action, dataCallback); + if (action.startsWith('metrics')) { + // Refresh periodically + if (metricsInterval) { + clearInterval(metricsInterval); + } + metricsInterval = setInterval(function () { + _refreshTab(selector, action, dataCallback); + }, 60000); } + }) + .bootgrid({ + rowCount: [50, 100, 200], + caseSensitive: false, + formatters: _dataFormatters, + }); + } + + function _initStatusMachines() { + const action = 'status-machines-list'; + const id = '#tab-status-machines'; + const dataCallback = function (data) { + const rows = []; + data.map(function (row) { + rows.push({ + name: row.machineId, + ip_address: row.ipAddress || ' ', + last_update: row.updated_at || ' ', + validated: row.isValidated, + version: row.version || ' ', + }); + }); + $(id + ' table') + .bootgrid('clear') + .bootgrid('append', rows); }; - let metricsInterval = null; - - function _decisionsByType(decisions) { - const dectypes = {}; - if (!decisions) { - return ''; - } - decisions.map(function (decision) { - // TODO ignore negative expiration? - dectypes[decision.type] = dectypes[decision.type] ? (dectypes[decision.type] + 1) : 1; + _initTab(id, action, dataCallback); + } + + function _initStatusCollections() { + const action = 'status-collections-list'; + const id = '#tab-status-collections'; + const dataCallback = function (data) { + const rows = []; + if (data.collections) { + 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 || ' ', + }); }); - let ret = ''; - for (const type in dectypes) { - if (ret !== '') { - ret += ' '; - } - ret += (type + ':' + dectypes[type]); - } - return ret; - } - - 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 = '???'; - if (timestamp) { - var howlongms = moment() - moment(timestamp); - howlongHuman = moment.duration(howlongms).humanize(); - } - $freshness.text(howlongHuman + ' ago'); - } - - function _addFreshness(selector) { - // this creates one timer per tab - var freshnessTemplate = 'Last refresh: '; - $(selector).find('.actionBar').prepend(freshnessTemplate); - setInterval(function () { - _updateFreshness(selector); - }, 5000); - } - - function _refreshTab(selector, action, dataCallback) { - $('.loading').show(); - $.ajax({ - url: api_url, - cache: false, - dataType: 'json', - data: { action: action }, - type: 'POST', - method: 'POST', - success: dataCallback, - complete: function () { - $(".loading").hide(); - _updateFreshness(selector, moment()); - } - }) - } - - 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; - } - - 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 _initTab(selector, action, dataCallback) { - const tab = $(selector); - const table = tab.find('table.crowdsecTable'); - if (!table.length) { + } + $(id + ' table') + .bootgrid('clear') + .bootgrid('append', rows); + }; + _initTab(id, action, dataCallback); + } + + function _initStatusScenarios() { + const action = 'status-scenarios-list'; + const id = '#tab-status-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, action, dataCallback); + } + + function _initStatusAppsecConfigs() { + const action = 'status-appsec-configs-list'; + const id = '#tab-status-appsec-configs'; + const dataCallback = function (data) { + const rows = []; + data['appsec-configs'].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, action, dataCallback); + } + + function _initStatusAppsecRules() { + const action = 'status-appsec-rules-list'; + const id = '#tab-status-appsec-rules'; + const dataCallback = function (data) { + const rows = []; + data['appsec-rules'].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, action, dataCallback); + } + + function _initStatusContexts() { + const action = 'status-contexts-list'; + const id = '#tab-status-contexts'; + const dataCallback = function (data) { + const rows = []; + data.contexts.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, action, dataCallback); + } + + function _initStatusParsers() { + const action = 'status-parsers-list'; + const id = '#tab-status-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, action, dataCallback); + } + + function _initStatusPostoverflows() { + const action = 'status-postoverflows-list'; + const id = '#tab-status-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, action, dataCallback); + } + + function _initStatusBouncers() { + const action = 'status-bouncers-list'; + const id = '#tab-status-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, + last_pull: row.last_pull, + type: row.type || ' ', + version: row.version || ' ', + }); + }); + $(id + ' table') + .bootgrid('clear') + .bootgrid('append', rows); + }; + _initTab(id, action, dataCallback); + } + + function _initStatusAlerts() { + const action = 'status-alerts-list'; + const id = '#tab-status-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, action, dataCallback); + } + + function _initStatusDecisions() { + const action = 'status-decisions-list'; + const id = '#tab-status-decisions'; + const dataCallback = function (data) { + const rows = []; + data.map(function (row) { + row.decisions.map(function (decision) { + // ignore deleted decisions + if (decision.duration.startsWith('-')) { return; - } - // Navigation - window.location.hash = selector; - history.pushState(null, null, window.location.hash); - table.on('initialized.rs.jquery.bootgrid', function () { - $(_refreshTemplate).on('click', function () { - _refreshTab(selector, action, dataCallback); - }).insertBefore(tab.find('.actionBar .actions .dropdown:first')); - _addFreshness(selector); - _refreshTab(selector, action, dataCallback); - if (action.startsWith("metrics")) { - // Refresh periodically - if (metricsInterval) { - clearInterval(metricsInterval); - } - metricsInterval = setInterval(function () { - _refreshTab(selector, action, dataCallback) - }, 60000); - } - }).bootgrid({ - rowCount: [50, 100, 200], - caseSensitive: false, - formatters: _dataFormatters - }) - } - - function _initStatusMachines() { - const action = 'status-machines-list'; - const id = '#tab-status-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 || ' ' - }); + } + 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, action, dataCallback); + } + + function _initMetricsAcquisition() { + const action = 'metrics-acquisition-list'; + const id = '#tab-metrics-acquisition'; + const dataCallback = function (data) { + const rows = []; + if (data.acquisition) { + const acquisition = Object.entries(data.acquisition); + acquisition.map(function (acquisition) { + if (acquisition.length === 2) { + rows.push({ + // search will break on empty values when using .append(). so we use spaces + source: acquisition[0] || ' ', + read: acquisition[1].reads || ' ', + parsed: acquisition[1].parsed || ' ', + unparsed: acquisition[1].unparsed || ' ', + poured: acquisition[1].pour || ' ', }); - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function _initStatusCollections() { - const action = 'status-collections-list'; - const id = "#tab-status-collections"; - const dataCallback = function (data) { - const rows = []; - if (data.collections) { - data.collections.map(function (row) { + } + }); + } + $(id + ' table') + .bootgrid('clear') + .bootgrid('append', rows); + }; + _initTab(id, action, dataCallback); + } + + function _initMetricsDecisions() { + const action = 'metrics-decisions-list'; + const id = '#tab-metrics-decisions'; + const dataCallback = function (data) { + const rows = []; + if (data.decisions) { + const decisions = Object.entries(data.decisions); + decisions.map(function (decision) { + if (decision.length === 2) { + const origins = Object.entries(decision[1]); + origins.map(function (origin) { + if (origin.length === 2) { + const types = Object.entries(origin[1]); + types.map(function (type) { + if (type.length === 2) { 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 || ' ' + // search will break on empty values when using .append(). so we use spaces + reason: decision[0] || ' ', + origin: origin[0], + action: type[0], + count: type[1], }); + } }); - } - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function _initStatusScenarios() { - const action = 'status-scenarios-list'; - const id = "#tab-status-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, action, dataCallback); - } - - function _initStatusAppsecConfigs() { - const action = 'status-appsec-configs-list'; - const id = "#tab-status-appsec-configs"; - const dataCallback = function (data) { - const rows = []; - data["appsec-configs"].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, action, dataCallback); - } - - function _initStatusAppsecRules() { - const action = 'status-appsec-rules-list'; - const id = "#tab-status-appsec-rules"; - const dataCallback = function (data) { - const rows = []; - data["appsec-rules"].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, action, dataCallback); - } - - function _initStatusContexts() { - const action = 'status-contexts-list'; - const id = "#tab-status-contexts"; - const dataCallback = function (data) { - const rows = []; - data.contexts.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, action, dataCallback); - } - - function _initStatusParsers() { - const action = 'status-parsers-list'; - const id = "#tab-status-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, action, dataCallback); + } + + function _initMetricsBucket() { + const action = 'metrics-bucket-list'; + const id = '#tab-metrics-bucket'; + const dataCallback = function (data) { + const rows = []; + if (data.buckets) { + const buckets = Object.entries(data.buckets); + buckets.map(function (bucket) { + if (bucket.length === 2) { + rows.push({ + bucket: bucket[0] || ' ', + current: bucket[1].curr_count || ' ', + overflows: bucket[1].overflow || ' ', + instantiated: bucket[1].instantiation || ' ', + poured: bucket[1].pour || ' ', + underflows: bucket[1].underflow || ' ', }); - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function _initStatusPostoverflows() { - const action = 'status-postoverflows-list'; - const id = "#tab-status-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, action, dataCallback); + } + + function _initMetricsParser() { + const action = 'metrics-parser-list'; + const id = '#tab-metrics-parser'; + const dataCallback = function (data) { + const rows = []; + if (data.parsers) { + const parsers = Object.entries(data.parsers); + parsers.map(function (parser) { + if (parser.length === 2) { + rows.push({ + parsers: parser[0] || ' ', + hits: parser[1].hits || ' ', + parsed: parser[1].parsed || ' ', + unparsed: parser[1].unparsed || ' ', }); - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } + } + }); + } - function _initStatusBouncers() { - const action = 'status-bouncers-list'; - const id = "#tab-status-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, - last_pull: row.last_pull, - type: row.type || ' ', - version: row.version || ' ' - }); + $(id + ' table') + .bootgrid('clear') + .bootgrid('append', rows); + }; + _initTab(id, action, dataCallback); + } + + function _initMetricsAlerts() { + const action = 'metrics-alerts-list'; + const id = '#tab-metrics-alerts'; + const dataCallback = function (data) { + const rows = []; + if (data.alerts) { + const alerts = Object.entries(data.alerts); + alerts.map(function (alert) { + if (alert.length === 2) { + rows.push({ + reason: alert[0] || ' ', + count: alert[1] || ' ', }); - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function _initStatusAlerts() { - const action = 'status-alerts-list'; - const id = "#tab-status-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, action, dataCallback); + } + + function _initMetricsLapiMachines() { + const action = 'metrics-lapi-machines-list'; + const id = '#tab-metrics-lapi-machines'; + const dataCallback = function (data) { + const rows = []; + if (data.lapi_machine) { + const machines = Object.entries(data.lapi_machine); + machines.map(function (machine) { + if (machine.length === 2) { + const routes = Object.entries(machine[1]); + routes.map(function (route) { + const methods = Object.values(route); + if (methods.length === 2) { + const methodTypes = Object.entries(methods[1]); + methodTypes.map(function (type) { + if (type.length === 2) { + rows.push({ + machine: machine[0] || ' ', + route: route[0] || ' ', + method: type[0], + hits: type[1], + }); + } }); + } }); - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } + } + }); + } - function _initStatusDecisions() { - const action = 'status-decisions-list'; - const id = "#tab-status-decisions"; - const dataCallback = function (data) { - const rows = []; - data.map(function (row) { - row.decisions.map(function (decision) { - // ignore deleted decisions - if (decision.duration.startsWith('-')) { - return; - } + $(id + ' table') + .bootgrid('clear') + .bootgrid('append', rows); + }; + _initTab(id, action, dataCallback); + } + + function _initMetricsLapiBouncers() { + const action = 'metrics-lapi-bouncers-list'; + const id = '#tab-metrics-lapi-bouncers'; + const dataCallback = function (data) { + const rows = []; + if (data.lapi_bouncer) { + const bouncers = Object.entries(data.lapi_bouncer); + bouncers.map(function (bouncer) { + if (bouncer.length === 2) { + const routes = Object.entries(bouncer[1]); + routes.map(function (route) { + const methods = Object.values(route); + if (methods.length === 2) { + const methodTypes = Object.entries(methods[1]); + methodTypes.map(function (type) { + if (type.length === 2) { 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 || ' ' + bouncer: bouncer[0] || ' ', + route: route[0] || ' ', + method: type[0], + hits: type[1], }); + } }); + } }); - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function _initMetricsAcquisition() { - const action = 'metrics-acquisition-list'; - const id = "#tab-metrics-acquisition"; - const dataCallback = function (data) { - const rows = []; - if (data.acquisition) { - const acquisition = Object.entries(data.acquisition); - acquisition.map(function (acquisition) { - if (acquisition.length === 2) { - rows.push({ - // search will break on empty values when using .append(). so we use spaces - source: acquisition[0] || ' ', - read: acquisition[1].reads || ' ', - parsed: acquisition[1].parsed || ' ', - unparsed: acquisition[1].unparsed || ' ', - poured: acquisition[1].pour || ' ', - }); - } - }); - } - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function _initMetricsDecisions() { - const action = 'metrics-decisions-list'; - const id = "#tab-metrics-decisions"; - const dataCallback = function (data) { - const rows = []; - if (data.decisions) { - const decisions = Object.entries(data.decisions); - decisions.map(function (decision) { - if (decision.length === 2) { - const origins = Object.entries(decision[1]); - origins.map(function (origin) { - if (origin.length === 2) { - const types = Object.entries(origin[1]); - types.map(function (type) { - if (type.length === 2) { - rows.push({ - // search will break on empty values when using .append(). so we use spaces - reason: decision[0] || ' ', - origin: origin[0], - action: type[0], - count: type[1] - }); - } - }); - } - }); - } - }); - } - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function _initMetricsBucket() { - const action = 'metrics-bucket-list'; - const id = "#tab-metrics-bucket"; - const dataCallback = function (data) { - const rows = []; - if (data.buckets) { - const buckets = Object.entries(data.buckets); - buckets.map(function (bucket) { - if (bucket.length === 2) { - rows.push({ - bucket: bucket[0] || ' ', - current: bucket[1].curr_count || ' ', - overflows: bucket[1].overflow || ' ', - instantiated: bucket[1].instantiation || ' ', - poured: bucket[1].pour || ' ', - underflows: bucket[1].underflow || ' ', - }); - } - }); - } - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function _initMetricsParser() { - const action = 'metrics-parser-list'; - const id = "#tab-metrics-parser"; - const dataCallback = function (data) { - const rows = []; - if (data.parsers) { - const parsers = Object.entries(data.parsers); - parsers.map(function (parser) { - if (parser.length === 2) { - rows.push({ - parsers: parser[0] || ' ', - hits: parser[1].hits || ' ', - parsed: parser[1].parsed || ' ', - unparsed: parser[1].unparsed || ' ' - }); - } - }); - } - - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function _initMetricsAlerts() { - const action = 'metrics-alerts-list'; - const id = "#tab-metrics-alerts"; - const dataCallback = function (data) { - const rows = []; - if (data.alerts) { - const alerts = Object.entries(data.alerts); - alerts.map(function (alert) { - if (alert.length === 2) { - rows.push({ - reason: alert[0] || ' ', - count: alert[1] || ' ' - }); - } - }); - } - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function _initMetricsLapiMachines() { - const action = 'metrics-lapi-machines-list'; - const id = "#tab-metrics-lapi-machines"; - const dataCallback = function (data) { - const rows = []; - if (data.lapi_machine) { - const machines = Object.entries(data.lapi_machine); - machines.map(function (machine) { - if (machine.length === 2) { - const routes = Object.entries(machine[1]); - routes.map(function (route) { - const methods = Object.values(route); - if (methods.length === 2) { - const methodTypes = Object.entries(methods[1]); - methodTypes.map(function (type) { - if (type.length === 2) { - rows.push({ - machine: machine[0] || ' ', - route: route[0] || ' ', - method: type[0], - hits: type[1] - }); - } - }); - - } - }); - } - }); - } - - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function _initMetricsLapiBouncers() { - const action = 'metrics-lapi-bouncers-list'; - const id = "#tab-metrics-lapi-bouncers"; - const dataCallback = function (data) { - const rows = []; - if (data.lapi_bouncer) { - const bouncers = Object.entries(data.lapi_bouncer); - bouncers.map(function (bouncer) { - if (bouncer.length === 2) { - const routes = Object.entries(bouncer[1]); - routes.map(function (route) { - const methods = Object.values(route); - if (methods.length === 2) { - const methodTypes = Object.entries(methods[1]); - methodTypes.map(function (type) { - if (type.length === 2) { - rows.push({ - bouncer: bouncer[0] || ' ', - route: route[0] || ' ', - method: type[0], - hits: type[1] - }); - } - }); - - } - }); - } - }); - } - - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - + } + }); + } - function _initMetricsLapi() { - const action = 'metrics-lapi-list'; - const id = "#tab-metrics-lapi"; - const dataCallback = function (data) { - const rows = []; - if (data.lapi) { - const infos = Object.entries(data.lapi); - infos.map(function (info) { - if (info.length === 2) { - const routes = Object.entries(info[1]); - routes.map(function (route) { - if (route.length === 2) { - rows.push({ - route: info[0] || ' ', - method: route[0], - hits: route[1] - }); - } - }); - } + $(id + ' table') + .bootgrid('clear') + .bootgrid('append', rows); + }; + _initTab(id, action, dataCallback); + } + + function _initMetricsLapi() { + const action = 'metrics-lapi-list'; + const id = '#tab-metrics-lapi'; + const dataCallback = function (data) { + const rows = []; + if (data.lapi) { + const infos = Object.entries(data.lapi); + infos.map(function (info) { + if (info.length === 2) { + const routes = Object.entries(info[1]); + routes.map(function (route) { + if (route.length === 2) { + rows.push({ + route: info[0] || ' ', + method: route[0], + hits: route[1], }); - } - - $(id + ' table').bootgrid('clear').bootgrid('append', rows); - }; - _initTab(id, action, dataCallback); - } - - function initService() { - $.ajax({ - url: api_url, - cache: false, - dataType: 'json', - data: { action: 'services-status' }, - type: 'POST', - method: 'POST', - success: function (data) { - var crowdsecStatus = data['crowdsec-status']; - if (crowdsecStatus === 'unknown') { - crowdsecStatus = 'Unknown'; - } else { - crowdsecStatus = _yesno2html(crowdsecStatus === 'running'); - } - $('#crowdsec-status').html(crowdsecStatus); - - var crowdsecFirewallStatus = data['crowdsec-firewall-status']; - if (crowdsecFirewallStatus === 'unknown') { - crowdsecFirewallStatus = 'Unknown'; - } else { - crowdsecFirewallStatus = _yesno2html(crowdsecFirewallStatus === 'running'); - } - $('#crowdsec-firewall-status').html(crowdsecFirewallStatus); - } - }) - } - - function deleteDecision(decisionId) { - const $modal = $('#remove-decision-modal'); - const action = 'status-decision-delete'; - - $modal.find('.modal-title').text('Delete decision #' + decisionId); - $modal.find('.modal-body').text('Are you sure?'); - $modal.modal('show'); - $modal.find('#remove-decision-confirm').on('click', function () { - $.ajax({ - // XXX handle errors - url: api_url + '?action=' + action + '&decision_id=' + decisionId, - type: 'DELETE', - method: 'DELETE', - dataType: 'json', - success: function (result) { - if (result && result.message === 'OK') { - $('#tab-status-decisions table').bootgrid('remove', [decisionId]); - } - } + } }); + } }); - } + } - function _handleStatusHash(hash) { - $('#hub-dropdown li').each(function () { - if ($(this).data('tab') === hash.replace('#', '')) { - $(this).addClass('active'); - $('#hub-dropdown-parent').addClass('ui-tabs-active ui-state-active'); - } - }); - switch (hash) { - case '#tab-status-alerts': - _initStatusAlerts(); - break; - case '#tab-status-bouncers': - _initStatusBouncers(); - break; - case '#tab-status-collections': - _initStatusCollections(); - break; - case '#tab-status-decisions': - _initStatusDecisions(); - break; - case '#tab-status-machines': - _initStatusMachines(); - break; - case '#tab-status-parsers': - _initStatusParsers(); - break; - case '#tab-status-postoverflows': - _initStatusPostoverflows(); - break; - case '#tab-status-scenarios': - _initStatusScenarios(); - break; - case '#tab-status-appsec-configs': - _initStatusAppsecConfigs(); - break; - case '#tab-status-appsec-rules': - _initStatusAppsecRules(); - break; - case '#tab-status-contexts': - _initStatusContexts(); - break; - default: - // First tab is collection for remote lapi - if ($('#li-status-machines').length === 0) { - _initStatusCollections(); - } else { - _initStatusMachines(); - } + $(id + ' table') + .bootgrid('clear') + .bootgrid('append', rows); + }; + _initTab(id, action, dataCallback); + } + + function initService() { + $.ajax({ + url: api_url, + cache: false, + dataType: 'json', + data: { action: 'services-status' }, + method: 'POST', + success: function (data) { + let crowdsecStatus = data['crowdsec-status']; + if (crowdsecStatus === 'unknown') { + crowdsecStatus = 'Unknown'; + } else { + crowdsecStatus = _yesno2html(crowdsecStatus === 'running'); } - } - - function _handleMetricsHash(hash) { - switch (hash) { - case '#tab-metrics-acquisition': - _initMetricsAcquisition(); - break; - case '#tab-metrics-bucket': - _initMetricsBucket(); - break; - case '#tab-metrics-parser': - _initMetricsParser(); - break; - case '#tab-metrics-lapi': - _initMetricsLapi(); - break; - case '#tab-metrics-lapi-machines': - _initMetricsLapiMachines(); - break; - case '#tab-metrics-lapi-bouncers': - _initMetricsLapiBouncers(); - break; - case '#tab-metrics-decisions': - _initMetricsDecisions(); - break; - case '#tab-metrics-alerts': - _initMetricsAlerts(); - break; - default: - _initMetricsAcquisition(); + $('#crowdsec-status').html(crowdsecStatus); + let crowdsecFirewallStatus = data['crowdsec-firewall-status']; + if (crowdsecFirewallStatus === 'unknown') { + crowdsecFirewallStatus = 'Unknown'; + } else { + crowdsecFirewallStatus = _yesno2html( + crowdsecFirewallStatus === 'running', + ); + } + $('#crowdsec-firewall-status').html(crowdsecFirewallStatus); + }, + }); + } + + function deleteDecision(decisionId) { + const $modal = $('#remove-decision-modal'); + const action = 'status-decision-delete'; + + $modal.find('.modal-title').text('Delete decision #' + decisionId); + $modal.find('.modal-body').text('Are you sure?'); + $modal.modal('show'); + $modal.find('#remove-decision-confirm').on('click', function () { + $.ajax({ + // XXX handle errors + url: api_url + '?action=' + action + '&decision_id=' + decisionId, + method: 'DELETE', + dataType: 'json', + success: function (result) { + if (result && result.message === 'OK') { + $('#tab-status-decisions table').bootgrid('remove', [decisionId]); + } + }, + }); + }); + } + + function _handleStatusHash(hash) { + $('#hub-dropdown li').each(function () { + if ($(this).data('tab') === hash.replace('#', '')) { + $(this).addClass('active'); + $('#hub-dropdown-parent').addClass('ui-tabs-active ui-state-active'); + } + }); + switch (hash) { + case '#tab-status-alerts': + _initStatusAlerts(); + break; + case '#tab-status-bouncers': + _initStatusBouncers(); + break; + case '#tab-status-collections': + _initStatusCollections(); + break; + case '#tab-status-decisions': + _initStatusDecisions(); + break; + case '#tab-status-machines': + _initStatusMachines(); + break; + case '#tab-status-parsers': + _initStatusParsers(); + break; + case '#tab-status-postoverflows': + _initStatusPostoverflows(); + break; + case '#tab-status-scenarios': + _initStatusScenarios(); + break; + case '#tab-status-appsec-configs': + _initStatusAppsecConfigs(); + break; + case '#tab-status-appsec-rules': + _initStatusAppsecRules(); + break; + case '#tab-status-contexts': + _initStatusContexts(); + break; + default: + // First tab is collection for remote lapi + if ($('#li-status-machines').length === 0) { + _initStatusCollections(); + } else { + _initStatusMachines(); } } - - function initStatus() { - // Machines tab is the first to be visible - $("#tabs").tabs({ - beforeActivate: function (event, ui) { - switch (ui.newPanel[0].id) { - case 'hub-tabs': - event.preventDefault(); - break; - default: - break - } - }, - activate: function (event, ui) { - switch (ui.newPanel[0].id) { - case 'tab-status-alerts': - _initStatusAlerts(); - break; - case 'tab-status-bouncers': - _initStatusBouncers(); - break; - case 'tab-status-decisions': - _initStatusDecisions(); - break; - case 'tab-status-machines': - _initStatusMachines(); - break; - case 'tab-status-collections': - _initStatusCollections(); - break; - case 'tab-status-parsers': - _initStatusParsers(); - break; - case 'tab-status-postoverflows': - _initStatusPostoverflows(); - break; - case 'tab-status-scenarios': - _initStatusScenarios(); - break; - case 'tab-status-appsec-configs': - _initStatusAppsecConfigs(); - break; - case 'tab-status-appsec-rules': - _initStatusAppsecRules(); - break; - case 'tab-status-contexts': - _initStatusContexts(); - break; - default: - _initStatusMachines(); - break - } - } - }); - // activate a tab from the hash, if it exists - _handleStatusHash(window.location.hash); - - $(window).on('hashchange', function (e) { - _handleStatusHash(window.location.hash); - }); - - $(window).on('popstate', function (event) { - _handleStatusHash(window.location.hash); - }); - - // Handle Hub tab - $("#hub-dropdown-parent").mouseenter(function () { - $("#hub-dropdown").show(); - }).mouseleave(function () { - $("#hub-dropdown").hide(); - }); - $('#tabs li').on('click', function () { - const parent = $(this).parent('ul'); - if ($(this).hasClass("main-tab")) { - $('#hub-dropdown-parent').removeClass('ui-tabs-active ui-state-active'); - $('#hub-dropdown li').removeClass('active'); - } - }); - $('#hub-dropdown li').on('click', function () { - const dataTabValue = $(this).data('tab'); - const $targetLink = $("li.hub a").filter(function () { - return $(this).attr('href') === `#${dataTabValue}`; - }); - if ($targetLink.length) { - $targetLink.click(); - } - $('#hub-dropdown-parent').mouseleave(); - $('#tabs li').removeClass('ui-tabs-active ui-state-active'); - $('#hub-dropdown-parent').addClass('ui-tabs-active ui-state-active'); - $('#hub-dropdown li').removeClass('active'); - $(this).addClass('active'); - }); - } - - function initMetrics() { - // Acquisition tab is the first to be visible - $("#tabs").tabs({ - activate: function (event, ui) { - switch (ui.newPanel[0].id) { - case 'tab-metrics-acquisition': - _initMetricsAcquisition(); - break; - case 'tab-metrics-bucket': - _initMetricsBucket(); - break; - case 'tab-metrics-parser': - _initMetricsParser(); - break; - case 'tab-metrics-lapi': - _initMetricsLapi(); - break; - case 'tab-metrics-lapi-machines': - _initMetricsLapiMachines(); - break; - case 'tab-metrics-lapi-bouncers': - _initMetricsLapiBouncers(); - break; - case 'tab-metrics-decisions': - _initMetricsDecisions(); - break; - case 'tab-metrics-alerts': - _initMetricsAlerts(); - break; - default: - _initMetricsAcquisition(); - break - } - } - }); - // activate a tab from the hash, if it exists - _handleMetricsHash(window.location.hash); - - $(window).on('hashchange', function (e) { - _handleMetricsHash(window.location.hash); - }); - - $(window).on('popstate', function (event) { - _handleMetricsHash(window.location.hash); - }); - } - - return { - deleteDecision: deleteDecision, - initStatus: initStatus, - initMetrics: initMetrics, - initService: initService - }; -}()); + } + + function _handleMetricsHash(hash) { + switch (hash) { + case '#tab-metrics-acquisition': + _initMetricsAcquisition(); + break; + case '#tab-metrics-bucket': + _initMetricsBucket(); + break; + case '#tab-metrics-parser': + _initMetricsParser(); + break; + case '#tab-metrics-lapi': + _initMetricsLapi(); + break; + case '#tab-metrics-lapi-machines': + _initMetricsLapiMachines(); + break; + case '#tab-metrics-lapi-bouncers': + _initMetricsLapiBouncers(); + break; + case '#tab-metrics-decisions': + _initMetricsDecisions(); + break; + case '#tab-metrics-alerts': + _initMetricsAlerts(); + break; + default: + _initMetricsAcquisition(); + } + } + + function initStatus() { + // Machines tab is the first to be visible + $('#tabs').tabs({ + beforeActivate: function (event, ui) { + switch (ui.newPanel[0].id) { + case 'hub-tabs': + event.preventDefault(); + break; + default: + break; + } + }, + activate: function (event, ui) { + switch (ui.newPanel[0].id) { + case 'tab-status-alerts': + _initStatusAlerts(); + break; + case 'tab-status-bouncers': + _initStatusBouncers(); + break; + case 'tab-status-decisions': + _initStatusDecisions(); + break; + case 'tab-status-machines': + _initStatusMachines(); + break; + case 'tab-status-collections': + _initStatusCollections(); + break; + case 'tab-status-parsers': + _initStatusParsers(); + break; + case 'tab-status-postoverflows': + _initStatusPostoverflows(); + break; + case 'tab-status-scenarios': + _initStatusScenarios(); + break; + case 'tab-status-appsec-configs': + _initStatusAppsecConfigs(); + break; + case 'tab-status-appsec-rules': + _initStatusAppsecRules(); + break; + case 'tab-status-contexts': + _initStatusContexts(); + break; + default: + _initStatusMachines(); + break; + } + }, + }); + // activate a tab from the hash, if it exists + _handleStatusHash(window.location.hash); + + $(window).on('hashchange', function (e) { + _handleStatusHash(window.location.hash); + }); + + $(window).on('popstate', function (event) { + _handleStatusHash(window.location.hash); + }); + + // Handle Hub tab + $('#hub-dropdown-parent') + .mouseenter(function () { + $('#hub-dropdown').show(); + }) + .mouseleave(function () { + $('#hub-dropdown').hide(); + }); + $('#tabs li').on('click', function () { + const parent = $(this).parent('ul'); + if ($(this).hasClass('main-tab')) { + $('#hub-dropdown-parent').removeClass('ui-tabs-active ui-state-active'); + $('#hub-dropdown li').removeClass('active'); + } + }); + $('#hub-dropdown li').on('click', function () { + const dataTabValue = $(this).data('tab'); + const $targetLink = $('li.hub a').filter(function () { + return $(this).attr('href') === `#${dataTabValue}`; + }); + if ($targetLink.length) { + $targetLink.click(); + } + $('#hub-dropdown-parent').mouseleave(); + $('#tabs li').removeClass('ui-tabs-active ui-state-active'); + $('#hub-dropdown-parent').addClass('ui-tabs-active ui-state-active'); + $('#hub-dropdown li').removeClass('active'); + $(this).addClass('active'); + }); + } + + function initMetrics() { + // Acquisition tab is the first to be visible + $('#tabs').tabs({ + activate: function (event, ui) { + switch (ui.newPanel[0].id) { + case 'tab-metrics-acquisition': + _initMetricsAcquisition(); + break; + case 'tab-metrics-bucket': + _initMetricsBucket(); + break; + case 'tab-metrics-parser': + _initMetricsParser(); + break; + case 'tab-metrics-lapi': + _initMetricsLapi(); + break; + case 'tab-metrics-lapi-machines': + _initMetricsLapiMachines(); + break; + case 'tab-metrics-lapi-bouncers': + _initMetricsLapiBouncers(); + break; + case 'tab-metrics-decisions': + _initMetricsDecisions(); + break; + case 'tab-metrics-alerts': + _initMetricsAlerts(); + break; + default: + _initMetricsAcquisition(); + break; + } + }, + }); + // activate a tab from the hash, if it exists + _handleMetricsHash(window.location.hash); + + $(window).on('hashchange', function (e) { + _handleMetricsHash(window.location.hash); + }); + + $(window).on('popstate', function (event) { + _handleMetricsHash(window.location.hash); + }); + } + + return { + deleteDecision: deleteDecision, + initStatus: initStatus, + initMetrics: initMetrics, + initService: initService, + }; +})(); diff --git a/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/metrics.html b/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/metrics.html index 7a6d6b5..4340a7c 100644 --- a/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/metrics.html +++ b/security/pfSense-pkg-crowdsec/files/usr/local/www/crowdsec/metrics.html @@ -1,17 +1,17 @@ - + @@ -19,13 +19,13 @@ -