From 3675e04b095ba82cb652cc69a8fd92e4c0956dca Mon Sep 17 00:00:00 2001 From: Filippo Carletti Date: Wed, 23 Oct 2024 14:40:05 +0200 Subject: [PATCH 01/23] feat: add snort3 --- config/snort3.conf | 10 + packages/snort3/Makefile | 158 +++++++ packages/snort3/files/main.uc | 287 ++++++++++++ packages/snort3/files/nftables.uc | 23 + packages/snort3/files/snort-mgr | 429 ++++++++++++++++++ packages/snort3/files/snort-rules | 171 +++++++ packages/snort3/files/snort.config | 78 ++++ packages/snort3/files/snort.init | 62 +++ packages/snort3/files/snort.uc | 179 ++++++++ ...OMPILE_LIT-to-work-around-upstream-b.patch | 34 ++ ..._capture-Fix-compilation-with-GCC-13.patch | 22 + 11 files changed, 1453 insertions(+) create mode 100644 config/snort3.conf create mode 100644 packages/snort3/Makefile create mode 100644 packages/snort3/files/main.uc create mode 100644 packages/snort3/files/nftables.uc create mode 100644 packages/snort3/files/snort-mgr create mode 100644 packages/snort3/files/snort-rules create mode 100644 packages/snort3/files/snort.config create mode 100644 packages/snort3/files/snort.init create mode 100644 packages/snort3/files/snort.uc create mode 100644 packages/snort3/patches/100-remove-HAVE_HS_COMPILE_LIT-to-work-around-upstream-b.patch create mode 100644 packages/snort3/patches/110-packet_capture-Fix-compilation-with-GCC-13.patch diff --git a/config/snort3.conf b/config/snort3.conf new file mode 100644 index 00000000..4bc2c7e1 --- /dev/null +++ b/config/snort3.conf @@ -0,0 +1,10 @@ +CONFIG_PACKAGE_gperftools-runtime=y +CONFIG_PACKAGE_hyperscan-runtime=y +CONFIG_PACKAGE_libunwind=y +CONFIG_PACKAGE_kmod-nfnetlink-queue=y +CONFIG_PACKAGE_kmod-nft-queue=y +CONFIG_PACKAGE_libdaq3=y +CONFIG_PACKAGE_libdnet=y +CONFIG_PACKAGE_libhwloc=y +CONFIG_PACKAGE_libpciaccess=y +CONFIG_PACKAGE_snort3=y diff --git a/packages/snort3/Makefile b/packages/snort3/Makefile new file mode 100644 index 00000000..184daa0f --- /dev/null +++ b/packages/snort3/Makefile @@ -0,0 +1,158 @@ +# +# This is free software, licensed under the GNU General Public License v2. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=snort3 +PKG_VERSION:=3.1.84.0 +PKG_RELEASE:=4 + +PKG_SOURCE_PROTO:=git +PKG_SOURCE_VERSION:=$(PKG_VERSION) +PKG_SOURCE_URL:=https://github.com/snort3/snort3 +PKG_MIRROR_HASH:=ffa69fdd95c55a943ab4dd782923caf31937dd8ad29e202d7fe781373ed84444 + +PKG_MAINTAINER:=W. Michael Petullo , John Audia +PKG_LICENSE:=GPL-2.0-only +PKG_LICENSE_FILES:=COPYING +PKG_CPE_ID:=cpe:/a:snort:snort + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/cmake.mk + +define Package/snort3 + SUBMENU:=Firewall + SECTION:=net + CATEGORY:=Network + DEPENDS:= \ + +(TARGET_x86||TARGET_x86_64):hyperscan-runtime \ + +gperftools-runtime +libstdcpp +libdaq3 +libdnet +libopenssl +libpcap +libpcre \ + +libpthread +libuuid +zlib +libhwloc +libtirpc +luajit +libatomic \ + +kmod-nft-queue +liblzma +ucode +ucode-mod-fs +ucode-mod-uci + TITLE:=Lightweight Network Intrusion Detection System + URL:=http://www.snort.org/ + MENU:=1 +endef + +define Package/snort3/description + Snort is an open source network intrusion detection and prevention system. + It is capable of performing real-time traffic analysis, alerting, blocking + and packet logging on IP networks. It utilizes a combination of protocol + analysis and pattern matching in order to detect anomalies, misuse and + attacks. +endef + +# Hyperscan only builds for x86 +ifdef CONFIG_TARGET_x86_64 + CMAKE_OPTIONS += -DHS_INCLUDE_DIRS=$(STAGING_DIR)/usr/include/hs +endif + +CMAKE_OPTIONS += \ + -DUSE_TIRPC:BOOL=YES \ + -DENABLE_STATIC_DAQ:BOOL=NO \ + -DDAQ_INCLUDE_DIR=$(STAGING_DIR)/usr/include/daq3 \ + -DDAQ_LIBRARIES_DIR_HINT:PATH=$(STAGING_DIR)/usr/lib/daq3 \ + -DFLEX_INCLUDES:PATH=$(STAGING_DIR_HOST)/include \ + -DENABLE_COREFILES:BOOL=NO \ + -DENABLE_GDB:BOOL=NO \ + -DMAKE_DOC:BOOL=NO \ + -DMAKE_HTML_DOC:BOOL=NO \ + -DMAKE_PDF_DOC:BOOL=NO \ + -DMAKE_TEXT_DOC:BOOL=NO \ + -DHAVE_LIBUNWIND=OFF \ + -DENABLE_TCMALLOC=ON \ + -DTCMALLOC_LIBRARIES=$(STAGING_DIR)/usr/lib/libtcmalloc.so \ + -DHAVE_LZMA=ON + +TARGET_CFLAGS += -I$(STAGING_DIR)/usr/include/daq3 -I$(STAGING_DIR)/usr/include/tirpc +TARGET_LDFLAGS += -L$(STAGING_DIR)/usr/lib/daq3 -ltirpc + +define Package/snort3/conffiles +/etc/config/snort +/etc/snort/ +endef + +define Package/snort3/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) \ + $(PKG_INSTALL_DIR)/usr/bin/snort \ + $(1)/usr/bin/ + + $(INSTALL_BIN) \ + $(PKG_INSTALL_DIR)/usr/bin/snort2lua \ + $(1)/usr/bin/ + + $(INSTALL_BIN) \ + $(PKG_INSTALL_DIR)/usr/bin/u2{boat,spewfoo} \ + $(1)/usr/bin/ + + $(INSTALL_BIN) \ + ./files/snort-{mgr,rules} \ + $(1)/usr/bin/ + + $(INSTALL_DIR) $(1)/usr/lib/snort + $(CP) \ + $(PKG_INSTALL_DIR)/usr/lib/snort/daq/daq_hext.so \ + $(1)/usr/lib/snort/ + + $(CP) \ + $(PKG_INSTALL_DIR)/usr/lib/snort/daq/daq_file.so \ + $(1)/usr/lib/snort/ + + $(INSTALL_DIR) $(1)/usr/share/lua + $(CP) \ + $(PKG_INSTALL_DIR)/usr/include/snort/lua/snort_plugin.lua \ + $(1)/usr/share/lua/ + + $(INSTALL_DIR) $(1)/usr/share/snort + $(INSTALL_CONF) \ + ./files/main.uc \ + $(1)/usr/share/snort/ + + $(INSTALL_DIR) $(1)/usr/share/snort/templates + $(INSTALL_CONF) \ + ./files/nftables.uc \ + $(1)/usr/share/snort/templates/ + $(INSTALL_CONF) \ + ./files/snort.uc \ + $(1)/usr/share/snort/templates/ + + $(INSTALL_DIR) $(1)/etc/snort/{rules,lists,builtin_rules,so_rules} + + $(INSTALL_CONF) \ + $(PKG_INSTALL_DIR)/usr/etc/snort/*.lua \ + $(1)/etc/snort + $(INSTALL_CONF) \ + $(PKG_INSTALL_DIR)/usr/etc/snort/file_magic.rules \ + $(1)/etc/snort + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) \ + ./files/snort.init \ + $(1)/etc/init.d/snort + + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) \ + ./files/snort.config \ + $(1)/etc/config/snort + + sed \ + -i \ + -e "/^-- HOME_NET and EXTERNAL_NET/ i -- The values for the two variables HOME_NET and EXTERNAL_NET have been" \ + -e "/^-- HOME_NET and EXTERNAL_NET/ i -- moved to /etc/config/snort, so do not modify them here without good" \ + -e "/^-- HOME_NET and EXTERNAL_NET/ i -- reason.\n" \ + -e 's/^\(HOME_NET\s\+=\)/--\1/g' \ + -e 's/^\(EXTERNAL_NET\s\+=\)/--\1/g' \ + $(1)/etc/snort/snort.lua + sed \ + -i -e "s/^\\(RULE_PATH\\s\\+=\\).*/\\1 'rules'/g" \ + -e "s/^\\(BUILTIN_RULE_PATH\\s\\+=\\).*/\\1 'builtin_rules'/g" \ + -e "s/^\\(PLUGIN_RULE_PATH\\s\\+=\\).*/\\1 'so_rules'/g" \ + -e "s/^\\(WHITE_LIST_PATH\\s\\+=\\).*/\\1 'lists'/g" \ + -e "s/^\\(BLACK_LIST_PATH\\s\\+=\\).*/\\1 'lists'/g" \ + $(1)/etc/snort/snort_defaults.lua +endef + +$(eval $(call BuildPackage,snort3)) diff --git a/packages/snort3/files/main.uc b/packages/snort3/files/main.uc new file mode 100644 index 00000000..33361f2b --- /dev/null +++ b/packages/snort3/files/main.uc @@ -0,0 +1,287 @@ +{% +//------------------------------------------------------------------------------ +// Copyright (c) 2023-2024 Eric Fahlgren +// SPDX-License-Identifier: GPL-2.0 +// +// The tables defined using 'config_item' are the source of record for the +// configuration file, '/etc/config/snort'. If you wish to add new items, +// do that only in the tables and propagate that use into the templates. +// +//------------------------------------------------------------------------------ + +QUIET; // Reference globals passed from CLI, so we get errors when missing. +TYPE; + +import { cursor } from 'uci'; +let uci = cursor(); + +function wrn(fmt, ...args) { + if (QUIET) + exit(1); + + let msg = "ERROR: " + sprintf(fmt, ...args); + + if (getenv("TTY")) + warn(`\033[33m${msg}\033[m\n`); + else + warn(`[!] ${msg}\n`); + exit(1); +} + +function rpad(str, fill, len) +{ + str = rtrim(str) + ' '; + while (length(str) < len) { + str += fill; + } + return str; +} + +//------------------------------------------------------------------------------ + +const ConfigItem = { + contains: function(value) { + // Check if the value is contained in the listed values, + // depending on the item type. + switch (this.type) { + case "enum": + return value in this.values; + case "range": + return value >= this.values[0] && value <= this.values[1]; + default: + return true; + } + }, + + allowed: function() { + // Show a pretty version of the possible values, for error messages. + switch (this.type) { + case "enum": + return "one of [" + join(", ", this.values) + "]"; + case "range": + return `${this.values[0]} <= x <= ${this.values[1]}`; + case "path": + return "a path string"; + case "str": + return "a string"; + default: + return "???"; + } + }, +}; + +function config_item(type, values, def) { + // If no default value is provided explicity, then values[0] is used as default. + if (! type in [ "enum", "range", "path", "str" ]) { + wrn(`Invalid item type '${type}', must be one of "enum", "range", "path" or "str".`); + return; + } + if (type == "enum") { + // Convert values to strings, so 'in' works in 'contains'. + values = map(values, function(i) { return "" + i; }); + } + if (type == "range" && (length(values) != 2 || values[0] > values[1])) { + wrn(`A 'range' type item must have exactly 2 values in ascending order.`); + return; + } + // Maybe check 'path' values for existence??? + + return proto({ + type: type, + values: values, + default: def ?? values[0], + }, ConfigItem); +}; + +const snort_config = { + enabled: config_item("enum", [ 0, 1 ], 0), // Defaults to off, so that user must configure before first start. + manual: config_item("enum", [ 0, 1 ], 1), // Allow user to manually configure, legacy behavior when enabled. + oinkcode: config_item("str", [ "" ]), // User subscription oinkcode. Much more in 'snort-rules' script. + home_net: config_item("str", [ "" ], "192.168.1.0/24"), + external_net: config_item("str", [ "" ], "any"), + + config_dir: config_item("path", [ "/etc/snort" ]), // Location of the base snort configuration files. + temp_dir: config_item("path", [ "/var/snort.d" ]), // Location of all transient snort config, including downloaded rules. + log_dir: config_item("path", [ "/var/log" ]), // Location of the generated logs, and oh-by-the-way the snort PID file (why?). + logging: config_item("enum", [ 0, 1 ], 1), + openappid: config_item("enum", [ 0, 1 ], 0), + + mode: config_item("enum", [ "ids", "ips" ]), + method: config_item("enum", [ "pcap", "afpacket", "nfq" ]), + action: config_item("enum", [ "default", "alert", "block", "drop", "reject" ]), + interface: config_item("str", [ uci.get("network", "wan", "device") ]), + snaplen: config_item("range", [ 1518, 65535 ]), // int daq.snaplen = 1518: set snap length (same as -s) { 0:65535 } + + include: config_item("path", [ "" ]), // User-defined snort configuration, applied at end of snort.lua. +}; + +const nfq_config = { + queue_count: config_item("range", [ 1, 16 ], 4), // Count of queues to allocate in nft chain when method=nfq, usually 2-8. + queue_start: config_item("range", [ 1, 32768], 4), // Start of queue numbers in nftables. + queue_maxlen: config_item("range", [ 1024, 65536 ], 1024), // --daq-var queue_maxlen=int + fanout_type: config_item("enum", [ "hash", "lb", "cpu", "rollover", "rnd", "qm"], "hash"), // See below. + thread_count: config_item("range", [ 0, 32 ], 0), // 0 = use cpu count + chain_type: config_item("enum", [ "prerouting", "input", "forward", "output", "postrouting" ], "input"), + chain_priority: config_item("enum", [ "raw", "filter", "300"], "filter"), + include: config_item("path", [ "" ]), // User-defined rules to include inside queue chain. +}; + + +let _snort_config_doc = +" +This is not an exhaustive list of configuration items, just those that +require more explanation than is given in the tables that define them, below. + +https://openwrt.org/docs/guide-user/services/snort + +snort + manual - When set to 1, use manual configuration for legacy behavior. + When disabled, then use this config. + interface - Default should usually be 'uci get network.wan.device', + something like 'eth0' + home_net - IP range/ranges to protect. May be 'any', but more likely it's + your lan range, default is '192.168.1.0/24' + external_net - IP range external to home. Usually 'any', but if you only + care about true external hosts (trusting all lan devices), + then '!$HOME_NET' or some specific range + mode - 'ids' or 'ips', for detection-only or prevention, respectively + oinkcode - https://www.snort.org/oinkcodes + config_dir - Location of the base snort configuration files. Default /etc/snort + temp_dir - Location of all transient snort config, including downloaded rules + Default /var/snort.d + logging - Enable external logging of events thus enabling 'snort-mgr report', + otherwise events only go to system log (i.e., 'logread -e snort:') + log_dir - Location of the generated logs, and oh-by-the-way the snort + PID file (why?). Default /var/log + openappid - Enabled inspection using the 'openappid' package + See 'opkg info openappid' + action - Override the specified action of your rules. One of 'default', + 'alert', 'block', 'reject' or 'drop', where 'default' means use + the rule as defined and don't override. + method - 'pcap', 'afpacket' or 'nfq' + snaplen - int daq.snaplen = 1518: set snap length (same as -s) { 0:65535 } + include - User-defined snort configuration, applied at end of generated snort.lua + +nfq - https://github.com/snort3/libdaq/blob/master/modules/nfq/README.nfq.md + queue_maxlen - nfq's '--daq-var queue_maxlen=int' + queue_count - Count of queues to use when method=nfq, usually 2-8 + fanout_type - Sets kernel load balancing algorithm*, one of hash, lb, cpu, + rollover, rnd, qm. + thread_count - int snort.-z: maximum number of packet threads + (same as --max-packet-threads); 0 gets the number of + CPU cores reported by the system; default is 1 { 0:max32 } + chain_type - Chain type when generating nft output + chain_priority - Chain priority when generating nft output + include - Full path to user-defined extra rules to include inside queue chain + + * - for details on fanout_type, see these pages: + https://github.com/florincoras/daq/blob/master/README + https://www.kernel.org/doc/Documentation/networking/packet_mmap.txt +"; + +function snort_config_doc(comment) { + if (comment == null) comment = ""; + if (comment != "") comment += " "; + for (let line in split(_snort_config_doc, "\n")) { + let msg = rtrim(sprintf("%s%s", comment, line)); + print(msg, "\n"); + } +} + +//------------------------------------------------------------------------------ + +function load(section, config) { + let self = { + ".name": section, + ".config": config, + }; + + // Set the defaults from definitions in table. + for (let item in config) { + self[item] = config[item].default; + } + + // Overwrite them with any uci config settings. + let cfg = uci.get_all("snort", section); + for (let item in cfg) { + // If you need to rename, delete or change the meaning of a + // config item, just intercept it and do the work here. + + if (exists(config, item)) { + let val = cfg[item]; + if (config[item].contains(val)) + self[item] = val; + else { + wrn(`In option ${item}='${val}', must be ${config[item].allowed()}`); + // ??? self[item] = config[item][0]; ??? + } + } + } + + return self; +} + +let snort = null; +let nfq = null; +function load_all() { + snort = load("snort", snort_config); + nfq = load("nfq", nfq_config); +} + +function dump_config(settings) { + let section = settings[".name"]; + let config = settings[".config"]; + printf("config %s '%s'\n", section, section); + for (let item in config) { + printf("\toption %-15s %-17s# %s\n", item, `'${settings[item]}'`, config[item].allowed()); + } + print("\n"); +} + +function render_snort() { + include("templates/snort.uc", { snort, nfq, rpad }); +} + +function render_nftables() { + include("templates/nftables.uc", { snort, nfq, rpad }); +} + +function render_config() { + snort_config_doc("#"); + dump_config(snort); + dump_config(nfq); +} + +function render_help() { + snort_config_doc(); +} + +//------------------------------------------------------------------------------ + +load_all(); + +let table_type = TYPE; // Supply on cli with '-D TYPE=snort'... +switch (table_type) { + case "snort": + render_snort(); + return; + + case "nftables": + render_nftables(); + return; + + case "config": + render_config(); + return; + + case "help": + render_help(); + return; + + default: + print(`Invalid table type '${table_type}', should be one of snort, nftables, config, help.\n`); + return; +} + +//------------------------------------------------------------------------------ +-%} diff --git a/packages/snort3/files/nftables.uc b/packages/snort3/files/nftables.uc new file mode 100644 index 00000000..74b1678d --- /dev/null +++ b/packages/snort3/files/nftables.uc @@ -0,0 +1,23 @@ +# Do not edit, automatically generated. See /usr/share/snort/templates. +{% +// Copyright (c) 2023-2024 Eric Fahlgren +// SPDX-License-Identifier: GPL-2.0 + +let queues = `${nfq.queue_start}-${int(nfq.queue_start)+int(nfq.queue_count)-1}`; +let chain_type = nfq.chain_type; +-%} + +table inet snort { + chain {{ chain_type }}_{{ snort.mode }} { + type filter hook {{ chain_type }} priority {{ nfq.chain_priority }} + policy accept + {% if (nfq.include) { + // We use the ucode include here, so that the included file is also + // part of the template and can use values passed in from the config. + printf("\n\t\t" + rpad(`#-- Include from '${nfq.include}'`, ">", 64) + "\n"); + include(nfq.include, { snort, nfq }); + printf("\t\t" + rpad("#-- End of included file.", "<", 64) + "\n\n"); + } %} + counter queue flags bypass to {{ queues }} + } +} diff --git a/packages/snort3/files/snort-mgr b/packages/snort3/files/snort-mgr new file mode 100644 index 00000000..a950802a --- /dev/null +++ b/packages/snort3/files/snort-mgr @@ -0,0 +1,429 @@ +#!/bin/sh +# Copyright (c) 2023-2024 Eric Fahlgren +# SPDX-License-Identifier: GPL-2.0 +# shellcheck disable=SC2039,SC2155 # "local" not defined in POSIX sh + +set -o nounset + +PROG="$(command -v snort)" +MAIN="/usr/share/snort/main.uc" +CONF_DIR=$(uci -q get snort.snort.temp_dir || echo "/var/snort.d") +CONF="${CONF_DIR}/snort_conf.lua" + +ACTION="usage" # Show help by default. +VERBOSE=false +QUIET=false +TESTING= +TABLE= +NLINES=0 +DATE_SPEC= +PATTERN= + +[ ! -e "$CONF_DIR" ] && mkdir -p "$CONF_DIR" +[ -e /dev/stdin ] && STDIN=/dev/stdin || STDIN=/proc/self/fd/0 +[ -e /dev/stdout ] && STDOUT=/dev/stdout || STDOUT=/proc/self/fd/1 +[ -t 2 ] && export TTY=1 + +die() { + $QUIET || echo "$@" >&2 + exit 1 +} + +disable_offload() +{ + # From https://forum.openwrt.org/t/snort-3-nfq-with-ips-mode/161172 + # https://blog.snort.org/2016/08/running-snort-on-commodity-hardware.html + # Not needed when running the nfq daq as defragmentation is done by the kernel. + # What about pcap? + + local filter_method=$(uci -q get snort.snort.method) + if [ "$filter_method" = "afpacket" ]; then + local wan=$(uci get snort.snort.interface) + if [ -n "$wan" ] && ethtool -k "$wan" | grep -q -E '(tcp-segmentation-offload|receive-offload): on' ; then + ethtool -K "$wan" gro off lro off tso off 2> /dev/null + log "Disabled gro, lro and tso on '$wan' using ethtool." + fi + fi +} + +nft_rm_table() { + for table_type in 'inet' 'netdev'; do + nft list tables | grep -q "${table_type} snort" && nft delete table "${table_type}" snort + done +} + +nft_add_table() { + if [ "$(uci -q get snort.snort.method)" = "nfq" ]; then + local options='' + $VERBOSE && options='-e' + print nftables | nft $options -f $STDIN + $VERBOSE && nft list table inet snort + fi +} + +setup() { + # Generates all the configuration, then reports the config file for snort. + # Does NOT generate the rules file, you'll need to do 'update-rules' first. + local log_dir=$(uci get snort.snort.log_dir) + [ ! -e "$log_dir" ] && mkdir -p "$log_dir" + nft_rm_table + print snort > "$CONF" + nft_add_table + echo "$CONF" +} + +teardown() { + # Merely cleans up after. + nft_rm_table + [ -e "$CONF" ] && rm "${CONF:?}" +} + +resetup() { + QUIET=true check || die "The generated snort lua configuration contains errors, not restarting. Run 'snort-mgr check'" + teardown + setup +} + +update_rules() { + /usr/bin/snort-rules $TESTING +} + +print() { + # '$1' is optional file type to generate, one of: + # config, snort, nftables or help + local table="${1:-$TABLE}" + utpl -D TYPE="$table" -D QUIET=$QUIET -S "$MAIN" +} + +check() { + local manual=$(uci get snort.snort.manual) + [ "$manual" = 1 ] && return 0 + + $QUIET && OUT=/dev/null || OUT=$STDOUT + local warn no_rules + if $VERBOSE; then + warn='--warn-all' + no_rules=0 + else + warn='-q' + no_rules=1 + fi + + local test_conf="${CONF_DIR}/test_conf.lua" + _SNORT_WITHOUT_RULES="$no_rules" print snort > "${test_conf}" || die "Errors during generation of snort config" + if $PROG -T $warn -c "${test_conf}" 2> $OUT ; then + rm "${test_conf:?}" + else + die "Errors in snort config tests. Examine ${test_conf} for issues" + fi + + if [ "$(uci -q get snort.snort.method)" = "nfq" ]; then + local options='' + local test_nft="${CONF_DIR}/test_conf.nft" + print nftables > "${test_nft}" || die "Errors during generation of nftables config" + $VERBOSE && options='-e' + if nft $options --check -f "${test_nft}" ; then + rm "${test_nft:?}" + else + die "Errors in nftables config tests. Examine ${test_nft} for issues" + fi + fi + +} + +_date_range='' +_operator='' +_date='' + +_parse_date_range() { + local date_spec="$1" + case "$date_spec" in + ('') _operator='>' ; _date='' ;; + (-*) _operator='<' ; _date="${date_spec:1}" ;; + (=*) _operator='~' ; _date="${date_spec:1}" ;; + (+*) _operator='>' ; _date="${date_spec:1}" ;; + (today) _operator='>' ; _date=$(date +'%y/%m/%d-') ;; + (*) die "Invalid date specification '${date_spec}', did you forget the +/- prefix?" ;; + esac + if [ -z "$_date" ]; then + _date_range='' + else + local op=$_operator + [ "$op" = "~" ] && op='contains' + _date_range=" where date $op '$_date'" + fi +} + +_filter_by_date() { + # Grab all the alert_json files in the log directory, scan them + # for matching timestamps and return those lines that match. + + local log_dir="$1" + local operator="$2" + local date="$3" + cat "${log_dir}"/*alert_json.txt \ + | jsonfilter -a -e '$[@.timestamp '${operator}' "'"${date}"'"]' +} + +report() { + # Reported IPs have random source port stripped, but destination port + # (if any) retained. + + local SORT="$(command -v sort)" + if [ ! -x "${SORT}" ] || ! "${SORT}" --version 2> /dev/null | grep -q "coreutils"; then + die "'snort-mgr report' requires coreutils-sort package" + fi + + local logging=$(uci get snort.snort.logging) + local log_dir=$(uci get snort.snort.log_dir) + + if [ "$logging" = 0 ]; then + die "Logging is not enabled in snort config" + fi + + #-- Collect the inputs -- + local msg src srcP dst dstP dir gid sid + local tmp=$(mktemp -t snort.rep.XXXXXX) + _parse_date_range "$DATE_SPEC" + _filter_by_date "${log_dir}" "${_operator}" "${_date}" | while read -r line; do + src='' && dst='' && srcP='' && dstP='' + eval "$(jsonfilter -s "$line" \ + -e 'msg=$.msg' \ + -e 'src=$.src_addr' \ + -e 'dst=$.dst_addr' \ + -e 'srcP=$.src_port' \ + -e 'dstP=$.dst_port' \ + -e 'dir=$.dir' \ + -e 'gid=$.gid' \ + -e 'sid=$.sid')" + + # Append the port to the IP, but only if it's meaningful. + [ "$dir" = 'C2S' ] && [ -n "$dstP" ] && dst="${dst}(${dstP})" + [ "$dir" = 'S2C' ] && [ -n "$srcP" ] && src="${src}(${srcP})" + + echo "$msg#$src#$dst#$dir#$gid#$sid" + done | grep -iE "$PATTERN" > "$tmp" + + #-- Generate output -- + local output + [ "$NLINES" = 0 ] && output="cat" || output="head -n $NLINES" + + local lines=$($SORT "$tmp" | uniq -c | $SORT -nr | $output) + rm "${tmp:?}" + if [ -z "$lines" ]; then + echo -n "There were no incidents " + [ -z "$PATTERN" ] && echo "reported." || echo "matching pattern '$PATTERN'." + return + fi + + local n_total=$(cat "${log_dir}"/*alert_json.txt | wc -l) + local n_incidents=$(echo "$lines" | awk '{total += $1} END {print total}') + local mlen=$(echo "$lines" | awk -F'#' '{print $1}' | wc -L) + local slen=$(echo "$lines" | awk -F'#' '{print $2}' | wc -L) + + local match='' + [ -n "$PATTERN" ] && match=" involving '${PATTERN}'" + echo "Events${match}${_date_range} (run at $(date -Is))" + printf "%-*s %3s %5s %-3s %-*s %s\n" "$mlen" " Count Message" "gid" "sid" "Dir" "$slen" "Source" "Destination" + echo "$lines" | awk -F'#' '{printf "%-'"$mlen"'s %3d %5d %s %-'"$slen"'s %s\n", $1, $5, $6, $4, $2, $3}' + + local pct=$(awk -v n=$n_incidents -v t=$n_total 'END{printf "%.2f", 100*n/t}' /dev/null) + printf "%7d incidents shown of %d logged (%s%%)\n" "$n_incidents" "$n_total" "$pct" + + #-- Lookup rules and references, if requested. -- + if $VERBOSE; then + local rules_dir="$(uci get snort.snort.config_dir)/rules" + local usids="$(echo "$lines" | awk -F'#' '{print $5 "#" $6}' | $SORT -u | $SORT -t'#' -k1n -k2n)" + local nsids="$(echo "$usids" | wc -w)" + + echo '' + echo "$nsids unique rules triggered:" + local rule + local i=1 + for sid in $usids; do + eval "$(echo "$sid" | awk -F'#' '{printf "export gid=%s;export sid=%s", $1, $2}')" + printf "%3d - gid=%3d sid=%5d " "$i" "$gid" "$sid" + rule=$(grep -Hn "\bsid:${sid};" "$rules_dir"/*.rules) + if [ "$gid" -ne 1 ] && echo "$rule" | grep -qv "\bgid:${gid};"; then + # Many rules have gid implicitly '1', zero any that are not + # explicit when expecting non-'1'. + rule="" + fi + if [ -n "$rule" ]; then + echo "$rule" | cut -c -120 + else + rule=$($PROG --list-builtin | grep "^${gid}:${sid}\b") + if [ -n "$rule" ]; then + echo "BUILTIN: ${rule}" + fi + fi + i=$((i + 1)) + done + echo "" + echo "Per-rule details may be viewed by specifying the appropriate gid and sid, e.g.:" + echo " https://www.snort.org/rule-docs/$gid-$sid" + + # Look up the names of the IPs shown in report. + # Note, on my dev box, nslookup fires rule 1:14777, so you get lots + # of incidents if not suppressed. + echo '' + echo 'Hosts by name:' + local IP + local peerdns=$(ifstatus wan | jsonfilter -e '$["dns-server"][0]') + echo "$lines" | awk -F'#' '{printf "%s\n%s\n", $2, $3}' | sed 's/(.*//' | $SORT -u \ + | while read -r IP; do + [ -z "$IP" ] && continue + n=$(nslookup "$IP" | awk '/name = / {n=$NF} END{print n}') + [ -z "$n" ] && [ -n "$peerdns" ] && n=$(nslookup "$IP" "$peerdns" | awk '/name = / {n=$NF} END{print n}') + [ -z "$n" ] && n='--unknown host--' + printf " %-39s %s\n" "$IP" "$n" + done | $SORT -b -k2 + fi +} + +status() { + echo -n 'snort is ' ; service snort status + local mem_total mem_free + eval "$(ubus call system info | jsonfilter -e 'mem_total=$.memory.total' -e 'mem_free=$.memory.free')" + awk -v mem_total="$mem_total" -v mem_free="$mem_free" 'BEGIN { + mem_used = mem_total - mem_free; + printf "Total system memory=%.3fM Used=%.3fM (%.1f%%) Free=%.3fM (%.1f%%)\n", + mem_total/1024**2, + mem_used/1024**2, 100*mem_used/mem_total, + mem_free/1024**2, 100*mem_free/mem_total; + }' + busybox ps w | grep -E "PID|$PROG " | grep -v grep + + if [ "$(uci -q get snort.snort.method)" = "nfq" ]; then + nft list table inet snort + fi +} + +#------------------------------------------------------------------------------- + +usage() { + local msg="${1:-}" + [ -n "$msg" ] && printf "ERROR: %s\n\n" "$msg" + + cat < snort-mgr report --date-spec +23/12/20-09 + + and to report all of the incidents between 1300-1400 on all dates: + > snort-mgr report --date-spec =-13: + + + $0 update-rules [-t/--testing] + + Download and install the snort ruleset. + -t = Generate a test-only ruleset, don't download anything. + + Testing mode generates a canned rule that matches IPv4 ping requests. + A typical test scenario might look like: + + > snort-mgr -t update-rules + > /etc/init.d/snort start + > ping -c4 8.8.8.8 + > snort-mgr report + + + $0 print config|snort|nftables|help + + Print the rendered file contents. Table types are: + config - Display contents of /etc/config/snort, but with all values and + descriptions. Missing entries rendered with defaults. + snort - The top-level snort configuration lua script, with includes. + nftables - The nftables script used to define the input queues when using + the 'nfq' DAQ, with any included content. + help - Display config file help. + + + $0 check [-q/--quiet] + + Test the rendered config using snort's check mode without + applying it to the running system. + + + $0 status + + Print the service status, system memory use and if nfq is the current daq, + then the nftables with counter values and so on. + +USAGE + exit 1 +} + +while [ "${1:-}" ]; do + case "$1" in + -h|--help) + usage + ;; + -q|--quiet) + QUIET=true + ;; + -v|--verbose) + VERBOSE=true + ;; + -t|--testing) + TESTING=-t + ;; + -n|--n-lines) + [ -z "$2" ] && usage "'--n-lines' requires a value" + NLINES="$2" + shift + ;; + -d|--date-spec) + [ -z "$2" ] && usage "'--date-spec' requires a value" + DATE_SPEC="$2" + shift + ;; + -p|--pattern) + [ -z "$2" ] && usage "'--pattern' requires a value" + PATTERN="$2" + shift + ;; + print) + [ -z "$2" ] && usage "'print' requires a table type" + ACTION="$1" + TABLE="$2" + shift + ;; + setup|teardown|resetup|update-rules|check|report|status) + ACTION="$1" + ;; + *) + usage "'$1' is not a valid command or option" + ;; + esac + shift +done + +[ -n "$ACTION" ] && eval "$ACTION" diff --git a/packages/snort3/files/snort-rules b/packages/snort3/files/snort-rules new file mode 100644 index 00000000..cc838332 --- /dev/null +++ b/packages/snort3/files/snort-rules @@ -0,0 +1,171 @@ +#!/bin/sh +# Copyright (c) 2023-2024 Eric Fahlgren +# SPDX-License-Identifier: GPL-2.0 +# shellcheck disable=SC2039,SC2155 # "local" not defined in POSIX sh + +set -o nounset + +alias log='logger -s -t "snort-rules[$$]" -p "info"' + +download_rules() { + # Further information: + # https://www.snort.org/products#rule_subscriptions + # https://www.snort.org/oinkcodes + # + # Also, what to do about "subscription" vs Talos_LightSPD rules when subbed? + # Add a "use_rules" list or option or something? + local oinkcode=$(uci -q get snort.snort.oinkcode) + + local conf_dir=$(uci -q get snort.snort.config_dir || echo "/etc/snort") + local rules_dir="$conf_dir/rules" + local data_dir=$(uci -q get snort.snort.temp_dir || echo "/var/snort.d") + local data_tar="$data_dir/rules.tar.gz" + + local new_rules + local rules_file + local archive_loc + + # Make sure everything exists. + [ -d "$data_dir" ] || mkdir -p "$data_dir" + + if $testing ; then + log "Generating testing rules..." + archive_loc="testing-rules" + new_rules="$data_dir/$archive_loc" + rm -fr "${new_rules:?}" + mkdir -p "$new_rules" + rules_file="$new_rules/testing.rules" + { + echo 'alert icmp any any <> any any (msg:"TEST ALERT ICMP v4"; icode:0; itype: 8; sid:99010;)' + echo 'alert icmp any any <> any any (msg:"TEST ALERT ICMP v6"; icode:0; itype:33; sid:99011;)' + echo 'alert icmp any any <> any any (msg:"TEST ALERT ICMP v6"; icode:0; itype:34; sid:99012;)' + } >> "$rules_file" + + else + if [ -z "$oinkcode" ]; then + # If you do not have a subscription, then we use the community rules: + log "Downloading community rules..." + url="https://www.snort.org/downloads/community/snort3-community-rules.tar.gz" + archive_loc="snort3-community-rules" + + else + # If you have a subscription and its corresponding oinkcode, use this: + # + # 'snortver' is the version number of the snort executable in use on your + # router. + # + # Ideally, the 'snort --version' output would work, but OpenWrt builds + # are often between (or, more likely, newer than) those listed on the + # snort.org downloads page. + # + # So instead, we define it manually to be the value just before the + # installed version. Look on https://www.snort.org/advisories/ and + # select the most recent date. On that page, find the closest version + # number preceding your installed version and modify the hard-coded + # value below (for example, installed is 31600 then use 31470): + + #snortver=$(snort --version | awk '/Version/ {print gensub("\\.", "", "", $NF)}') + snortver=31470 + + log "Downloading subscription rules..." + url="https://www.snort.org/rules/snortrules-snapshot-$snortver.tar.gz?oinkcode=$oinkcode" + # Non-community tar contains many "*.rules" file, we only care about + # the one directory. + archive_loc="rules" + fi + + wget "$url" -O "$data_tar" 2>&1 | log || exit 1 + + old_rules="$data_dir/old.rules" + if $backup; then + rm -fr "${old_rules:?}" + mkdir -p "$old_rules" + + for rules_file in "$rules_dir"/*; do + # Before we overwrite with the new download. + log "Stashing '$rules_file' to '$old_rules/'..." + mv -f "$rules_file" "$old_rules/" + done + fi + + log "Unpacking '$data_tar'..." + tar xzvof "$data_tar" "$archive_loc" -C "$data_dir" | log || exit 1 + + # Get rid of the non-rule files and aggregator. + new_rules="$data_dir/$archive_loc" + find "$new_rules" \( -iname 'includes.rules' -o ! -iname '*.rules' -type f \) -exec rm '{}' \; + + # Old unfinished experiment with diffing old and new rules. + #for rules_file in "$new_rules"/*; do + #blah blah + #if [ -e "$old_rules" ] && ! cmp -s "$new_rules" "$old_rules" ; then + # diff "$new_rules" "$old_rules" 2>&1 | log + #fi + fi + + + mkdir -p "$conf_dir" + rm -fr "${rules_dir:?}" + if $persist; then + mv -f "$new_rules" "$rules_dir" + else + ln -s "$new_rules" "$rules_dir" + fi + + log "Snort rules loaded, restart snort now." +} + +#------------------------------------------------------------------------------- + +testing=false +persist=false +backup=false + +usage() { + local msg="$1" + [ -n "$msg" ] && printf "ERROR: %s\n\n" "$msg" + + cat < maximum number of packet threads +# (same as --max-packet-threads); 0 gets the number of +# CPU cores reported by the system; default is 1 { 0:max32 } +# chain_type - Chain type when generating nft output +# chain_priority - Chain priority when generating nft output +# include - Full path to user-defined extra rules to include inside queue chain +# +# * - for details on fanout_type, see these pages: +# https://github.com/florincoras/daq/blob/master/README +# https://www.kernel.org/doc/Documentation/networking/packet_mmap.txt +# +config snort 'snort' + option enabled '0' # one of [0, 1] + option manual '1' # one of [0, 1] + option oinkcode '' # a string + option home_net '192.168.1.0/24' # a string + option external_net 'any' # a string + option config_dir '/etc/snort' # a path string + option temp_dir '/var/snort.d' # a path string + option log_dir '/var/log' # a path string + option logging '1' # one of [0, 1] + option openappid '0' # one of [0, 1] + option mode 'ids' # one of [ids, ips] + option method 'pcap' # one of [pcap, afpacket, nfq] + option action 'default' # one of [default, alert, block, drop, reject] + option interface 'eth0' # a string + option snaplen '1518' # 1518 <= x <= 65535 + option include '' # a path string + +config nfq 'nfq' + option queue_count '4' # 1 <= x <= 16 + option queue_start '4' # 1 <= x <= 32768 + option queue_maxlen '1024' # 1024 <= x <= 65536 + option fanout_type 'hash' # one of [hash, lb, cpu, rollover, rnd, qm] + option thread_count '0' # 0 <= x <= 32 + option chain_type 'input' # one of [prerouting, input, forward, output, postrouting] + option chain_priority 'filter' # one of [raw, filter, 300] + option include '' # a path string + diff --git a/packages/snort3/files/snort.init b/packages/snort3/files/snort.init new file mode 100644 index 00000000..f73ebe87 --- /dev/null +++ b/packages/snort3/files/snort.init @@ -0,0 +1,62 @@ +#!/bin/sh /etc/rc.common +# shellcheck disable=SC2039 # "local" not defined in POSIX sh + +START=99 +STOP=10 + +USE_PROCD=1 +PROG=/usr/bin/snort +MGR=/usr/bin/snort-mgr + +validate_snort_section() { + $MGR -q check || return 1 + uci_validate_section snort snort "${1}" \ + 'enabled:bool:0' \ + 'manual:bool:1' \ + 'config_dir:string' \ + 'interface:string' +} + +start_service() { + # If you wish to use application-managed PID file: + # output.logdir, in the snort lua config, determines the PID file location. + # Add '--create-pidfile' to the 'command', below. + + local enabled + local manual + local config_dir + local interface + + validate_snort_section snort || { + echo "Validation failed, try 'snort-mgr check'." + return 1 + } + + [ "$enabled" = 0 ] && return + + procd_open_instance + if [ "$manual" = 0 ]; then + local config_file=$($MGR setup) + procd_set_param command "$PROG" -q -c "${config_file}" + else + procd_set_param command $PROG -q -i "$interface" -c "${config_dir%/}/snort.lua" --tweaks local + procd_set_param env SNORT_LUA_PATH="$config_dir" + procd_set_param file $CONFIGFILE + fi + procd_set_param respawn + procd_set_param stdout 0 + procd_set_param stderr 1 + procd_close_instance +} + +stop_service() +{ + service_stop "$PROG" + $MGR teardown +} + +service_triggers() +{ + procd_add_reload_trigger "snort" + procd_add_validation validate_snort_section +} diff --git a/packages/snort3/files/snort.uc b/packages/snort3/files/snort.uc new file mode 100644 index 00000000..6e14a0ab --- /dev/null +++ b/packages/snort3/files/snort.uc @@ -0,0 +1,179 @@ +{% +// Copyright (c) 2023-2024 Eric Fahlgren +// SPDX-License-Identifier: GPL-2.0 + +import { lsdir } from 'fs'; + +// Create some snort-format-specific items. + +let line_mode = snort.mode == "ids" ? "tap" : "inline"; +let mod_mode = snort.mode == "ids" ? "passive" : "inline"; + +let inputs = null; +let vars = null; +switch (snort.method) { +case "pcap": +case "afpacket": + inputs = `{ '${snort.interface}' }`; + vars = "{}"; + break; + +case "nfq": + inputs = "{ "; + for (let i = int(nfq.queue_start); i < int(nfq.queue_start)+int(nfq.queue_count); i++) { + inputs += `'${i}', ` + } + inputs += "}"; + + vars = `{ 'device=${snort.interface}', 'queue_maxlen=${nfq.queue_maxlen}', 'fanout_type=${nfq.fanout_type}', 'fail_open', }`; + break; +} +-%} +-- Do not edit, automatically generated. See /usr/share/snort/templates. + +-- These must be defined before processing snort.lua +HOME_NET = [[ {{ snort.home_net }} ]] +EXTERNAL_NET = [[ {{ snort.external_net }} ]] + +include('{{ snort.config_dir }}/snort.lua') + +snort = { +{% if (snort.mode == 'ips'): %} + ['-Q'] = true, +{% endif %} + ['--daq'] = '{{ snort.method }}', +{% if (snort.method == 'nfq'): %} + ['--max-packet-threads'] = {{ nfq.thread_count }}, +{% endif %} +} + +ips = { + -- View all options with "snort --help-module ips" + mode = '{{ line_mode }}', + variables = default_variables, +--enable_builtin_rules=true, +{% if (snort.action != 'default'): %} + action_override = '{{ snort.action }}', +{% endif %} +{% if (getenv("_SNORT_WITHOUT_RULES") == "1"): %} + -- WARNING: THIS IS A TEST-ONLY CONFIGURATION WITHOUT ANY RULES. +{% else %} + rules = [[ +{% + let rules_dir = snort.config_dir + '/rules'; + for (let rule in lsdir(rules_dir)) { + if (wildcard(rule, '*includes.rules', true)) continue; + if (wildcard(rule, '*.rules', true)) { + printf(` include ${rules_dir}/${rule}\n`); + } + } +%} + ]], +{% endif -%} +} + +daq = { + -- View all options with "snort --help-module daq" + inputs = {{ inputs }}, + snaplen = {{ snort.snaplen }}, + module_dirs = { '/usr/lib/daq/', }, + modules = { + { + name = '{{ snort.method }}', + mode = '{{ mod_mode }}', + variables = {{ vars }}, + } + } +} + +-- alert_syslog = { level = 'info', } -- Generate output to syslog. +alert_syslog = nil -- Disable output to syslog + +{% if (int(snort.logging)): %} +-- Note that this is also the location of the PID file, if you use it. +output = { + -- View all options with "snort --help-module output" + logdir = '{{ snort.log_dir }}', + + show_year = true, -- Include year in timestamps. + -- See also 'process.utc = true' if you wish to record timestamps + -- in UTC. +} + +--[[ +alert_full = { + -- View all options with "snort --help-config alert_full" + file = true, +} +--]] + +--[[ +alert_fast = { + -- View all options with "snort --help-config alert_fast" + file = true, + packet = false, +} +--]] + +alert_json = { + -- View all options with "snort --help-config alert_json" + file = true, + + -- This is a minimal set of fields that simply supports 'snort-mgr report' + -- and minimizes log size, but loses a lot of information: +--fields = 'timestamp dir src_addr src_port dst_addr dst_port gid sid msg', + + -- This is our preferred smallish set, which also supports the report, but + -- more closely matches 'alert_fast' contents. + fields = [[ + timestamp + pkt_num pkt_gen pkt_len + proto + dir + src_addr src_port + dst_addr dst_port + gid sid rev + action + msg + ]], +} + +{% endif -%} + +normalizer = { + tcp = { + ips = true, + } +} + +file_policy = { + enable_type = true, + enable_signature = true, + rules = { + use = { + verdict = 'log', + enable_file_type = true, + enable_file_signature = true, + } + } +} + +-- To use openappid with snort, 'opkg install openappid' and enable in config. +{% if (int(snort.openappid)): %} +appid = { + -- View all options with "snort --help-module appid" + log_stats = true, + app_detector_dir = '/usr/lib/openappid', + app_stats_period = 60, +} +{% endif %} + +{% +if (snort.include) { + // We use the ucode include here, so that the included file is also + // part of the template and can use values passed in from the config. + printf(rpad(`-- Include from '${snort.include}'`, ">", 80) + "\n"); + include(snort.include, { snort, nfq }); + printf(rpad("-- End of included file.", "<", 80) + "\n"); +} +%} diff --git a/packages/snort3/patches/100-remove-HAVE_HS_COMPILE_LIT-to-work-around-upstream-b.patch b/packages/snort3/patches/100-remove-HAVE_HS_COMPILE_LIT-to-work-around-upstream-b.patch new file mode 100644 index 00000000..629e8d69 --- /dev/null +++ b/packages/snort3/patches/100-remove-HAVE_HS_COMPILE_LIT-to-work-around-upstream-b.patch @@ -0,0 +1,34 @@ +From bf87399e720ec5e5adf9d74a17d86781b1e41428 Mon Sep 17 00:00:00 2001 +From: graysky +Date: Mon, 8 Jan 2024 13:00:28 -0500 +Subject: [PATCH] Hack: fix build with hyperscan + +Workaround to build until upstream bug is fixed[1]. + +1. https://github.com/intel/hyperscan/issues/388 + +--- + cmake/sanity_checks.cmake | 1 - + config.cmake.h.in | 1 - + 2 files changed, 2 deletions(-) + +--- a/cmake/sanity_checks.cmake ++++ b/cmake/sanity_checks.cmake +@@ -136,7 +136,6 @@ if (HS_FOUND) + cmake_push_check_state(RESET) + set(CMAKE_REQUIRED_INCLUDES ${HS_INCLUDE_DIRS}) + set(CMAKE_REQUIRED_LIBRARIES ${HS_LIBRARIES}) +- check_function_exists(hs_compile_lit HAVE_HS_COMPILE_LIT) + cmake_pop_check_state() + endif() + endif() +--- a/config.cmake.h.in ++++ b/config.cmake.h.in +@@ -124,7 +124,6 @@ + + /* hyperscan available */ + #cmakedefine HAVE_HYPERSCAN 1 +-#cmakedefine HAVE_HS_COMPILE_LIT 1 + + /* iconv available */ + #cmakedefine HAVE_ICONV 1 diff --git a/packages/snort3/patches/110-packet_capture-Fix-compilation-with-GCC-13.patch b/packages/snort3/patches/110-packet_capture-Fix-compilation-with-GCC-13.patch new file mode 100644 index 00000000..5d6fb79e --- /dev/null +++ b/packages/snort3/patches/110-packet_capture-Fix-compilation-with-GCC-13.patch @@ -0,0 +1,22 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens +Date: Sat, 23 Mar 2024 19:11:15 +0100 +Subject: packet_capture: Fix compilation with GCC 13 + +Fix the following compile problem with GCC 13: +src/network_inspectors/packet_capture/packet_capture.h:25:54: error: 'int16_t' does not name a type + 25 | void packet_capture_enable(const std::string&, const int16_t g = -1, const std::string& t = ""); +--- + src/network_inspectors/packet_capture/packet_capture.h | 1 + + 1 file changed, 1 insertion(+) + +--- a/src/network_inspectors/packet_capture/packet_capture.h ++++ b/src/network_inspectors/packet_capture/packet_capture.h +@@ -22,6 +22,7 @@ + + #include + #include ++#include + + void packet_capture_enable(const std::string&, const int16_t g = -1, const std::string& t = ""); + void packet_capture_disable(); From fbd9cbe516ec085ddbb7973739f567b4000d03c6 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Thu, 31 Oct 2024 16:23:11 +0100 Subject: [PATCH 02/23] feat(snort3): add ns-snort-rules --- packages/snort3/Makefile | 4 + packages/snort3/files/ns-snort-rules | 266 +++++++++++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 packages/snort3/files/ns-snort-rules diff --git a/packages/snort3/Makefile b/packages/snort3/Makefile index 184daa0f..ed85eaca 100644 --- a/packages/snort3/Makefile +++ b/packages/snort3/Makefile @@ -92,6 +92,10 @@ define Package/snort3/install ./files/snort-{mgr,rules} \ $(1)/usr/bin/ + $(INSTALL_BIN) \ + ./files/ns-snort-rules \ + $(1)/usr/bin/ + $(INSTALL_DIR) $(1)/usr/lib/snort $(CP) \ $(PKG_INSTALL_DIR)/usr/lib/snort/daq/daq_hext.so \ diff --git a/packages/snort3/files/ns-snort-rules b/packages/snort3/files/ns-snort-rules new file mode 100644 index 00000000..96cdcd59 --- /dev/null +++ b/packages/snort3/files/ns-snort-rules @@ -0,0 +1,266 @@ +#!/usr/bin/python3 + +import argparse +import os +import sys +import shutil +import logging +import urllib.request +import tarfile +from nethsec import snort + +# Constants +DATA_DIR = "/var/ns-snort" +RULES_DIR = os.path.join(DATA_DIR, "rules") +BACKUP_DIR = os.path.join(DATA_DIR, "old.rules") +OFFICIAL_RULES_DIR = os.path.join(DATA_DIR, "snort-rules") +ET_RULES_DIR = os.path.join(DATA_DIR, "et-rules") + +COMMUNITY_RULES_URL = "https://www.snort.org/downloads/community/snort3-community-rules.tar.gz" +ET_RULES_URL = "https://rules.emergingthreats.net/open/snort-2.9.7.0/emerging.rules.tar.gz" + +# Setup logging +logging.basicConfig(level=logging.INFO, format='%(message)s') + +def log(message): + logging.info(message) + +def backup_rules(): + # remove old backup + shutil.rmtree(BACKUP_DIR, ignore_errors=True) + if os.path.exists(RULES_DIR): + shutil.copytree(RULES_DIR, BACKUP_DIR) + log(f"Backup created at {BACKUP_DIR}") + +def generate_testing_rules(): + log("Generating testing rules...") + testing_rules_dir = os.path.join(DATA_DIR, "testing-rules") + shutil.rmtree(testing_rules_dir, ignore_errors=True) + os.makedirs(testing_rules_dir, exist_ok=True) + rules_file = os.path.join(testing_rules_dir, "testing.rules") + testing_rules = [ + 'alert icmp any any <> any any (msg:"TEST ALERT ICMP v4"; icode:0; itype:8; sid:99010;)', + 'alert icmp any any <> any any (msg:"TEST ALERT ICMP v6"; icode:0; itype:33; sid:99011;)', + 'alert icmp any any <> any any (msg:"TEST ALERT ICMP v6"; icode:0; itype:34; sid:99012;)' + ] + with open(rules_file, 'w') as f: + for rule in testing_rules: + f.write(rule + "\n") + log(f"Testing rules generated at {rules_file}") + +def download_official_rules(oinkcode): + if oinkcode: + url = f"https://www.snort.org/downloads/snortrules-snapshot-{oinkcode}.tar.gz" + archive_loc = f"snortrules-snapshot-{oinkcode}" + else: + log("Downloading community rules...") + url = COMMUNITY_RULES_URL + archive_loc = "snort3-community-rules" + + os.makedirs(OFFICIAL_RULES_DIR, exist_ok=True) + archive_path = os.path.join(DATA_DIR, f"{archive_loc}.tar.gz") + + try: + with urllib.request.urlopen(url) as response: + if response.status != 200: + log(f"Failed to download rules from {url}") + sys.exit(1) + with open(archive_path, 'wb') as f: + shutil.copyfileobj(response, f) + log(f"Downloaded rules to {archive_path}") + + with tarfile.open(archive_path, 'r:gz') as tar: + tar.extractall(path=OFFICIAL_RULES_DIR) + log(f"Extracted rules to {OFFICIAL_RULES_DIR}") + except Exception as e: + log(f"Failed to download rules from {url}: {e}") + sys.exit(1) + +def download_et_rules(): + url = ET_RULES_URL + archive_loc = "emerging-threats-rules" + archive_path = os.path.join(DATA_DIR, f"{archive_loc}.tar.gz") + os.makedirs(ET_RULES_DIR, exist_ok=True) + log("Downloading Emerging Threats rules...") + try: + with urllib.request.urlopen(url) as response: + if response.status != 200: + log(f"Failed to download rules from {url}") + sys.exit(1) + with open(archive_path, 'wb') as f: + shutil.copyfileobj(response, f) + log(f"Emerging Threats rules downloaded to {archive_path}") + + with tarfile.open(archive_path, 'r:gz') as tar: + tar.extractall(path=ET_RULES_DIR) + log(f"Extracted Emerging Threats rules to {ET_RULES_DIR}") + except Exception as e: + log(f"Failed to download Emerging Threats rules from {url}: {e}") + sys.exit(1) + +def filter_official_rules(policy, disabled_sids): + rules = [] + ret = [] + # official rules are downloaded to /var/snort.d/snort3-community-rules/snort3-community.rules + for rule in snort.parse_file(os.path.join(OFFICIAL_RULES_DIR, "snort3-community-rules/snort3-community.rules")): + # rule.metadata can contain: + # - policy balanced-ips drop + # - policy connectivity-ips drop + # - policy security-ips drop + # - policy max-detect-ips drop + if rule.metadata is None or not rule.enabled or rule.sid in disabled_sids: + log(f"Skipping disabled rule {rule.sid}") + if policy == "connectivity" and 'policy connectivity-ips drop' in rule.metadata: + rules.append(rule) + elif policy == "balanced" and 'policy balanced-ips drop' in rule.metadata: + rules.append(rule) + elif policy == "security" and 'policy security-ips drop' in rule.metadata: + rules.append(rule) + for rule in rules: + if rule.action != 'drop': + rule.raw = rule.raw.replace(rule.action, "drop", 1) + ret.append(rule.raw) + return ret + +def filter_et_rules(alert, block, disabled_sids): + rules = [] + if not alert and not block: + log("No ET categories specified.") + return rules + alert_files = [] + if alert == 'default': + alert_files = [ + 'emerging-current_events', + 'emerging-dos', + 'emerging-ftp', + 'emerging-games', + 'emerging-inappropriate', + 'emerging-info', + 'emerging-misc', + 'emerging-mobile_malware', + 'emerging-p2p', + 'emerging-scan', + 'emerging-shellcode', + 'emerging-sql', + 'emerging-malware', + 'emerging-voip', + 'emerging-web_client', + 'emerging-worm' + ] + elif alert: + alert_files = alert.split(',') + block_files = [] + if block == 'default': + block_files = [ + 'emerging-botcc', + 'emerging-ciarmy', + 'emerging-compromised', + 'emerging-drop', + 'emerging-dshield', + 'emerging-activex', + 'emerging-attack_response', + 'emerging-exploit', + 'emerging-netbios' + ] + elif block: + block_files = block.split(',') + # ET rules are downloaded to /var/snort.d/emerging-threats-rules/rules/ + for file in alert_files: + log(f"Processing alert ET category {file}") + tmp = snort.parse_file(os.path.join(ET_RULES_DIR, "rules", f"{file}.rules")) + for rule in tmp: + if rule.enabled and rule.sid not in disabled_sids: + if rule.action != 'alert': + rule.raw = rule.raw.replace(rule.action, "alert", 1) + rules.append(rule.raw) + for file in block_files: + log(f"Processing block ET category {file}") + tmp = snort.parse_file(os.path.join(ET_RULES_DIR, "rules", f"{file}.rules")) + for rule in tmp: + if rule.enabled and rule.sid not in disabled_sids: + # replace alert with drop + if rule.action != 'drop': + rule.raw = rule.raw.replace(rule.action, "drop", 1) + rules.append(rule.raw) + + return rules + +def prepare_rule_file(): + rule_file = os.path.join(RULES_DIR, "snort.rules") + if os.path.exists(rule_file): + os.remove(rule_file) + return rule_file + +def append_rules_to_file(rules): + # create dir if not exists + os.makedirs(RULES_DIR, exist_ok=True) + rule_file = os.path.join(RULES_DIR, "snort.rules") + # create file if not exists + if not os.path.exists(rule_file): + with open(rule_file, 'w') as f: + f.write("# This file is automatically generated by ns-snort-rules\n") + for rule in rules: + with open(rule_file, 'a') as f: + f.write(str(rule) + "\n") + log(f"Appended {len(rules)} rules to {rule_file}") + +def main(): + parser = argparse.ArgumentParser(description='Process snort rules.') + parser.add_argument('--testing', action='store_true', help="Create synthetic testing rules instead of downloading.") + + # Options for official rules + parser.add_argument('--official-download', action='store_true', help="Download official Snort rules.") + parser.add_argument('--official-oinkcode', type=str, help="Oinkcode for downloading official rules.") + parser.add_argument('--official-policy', choices=['connectivity', 'balanced', 'security'], help="Policy for official rules: connectivity, balanced, or security.") + # Options for ET rules + parser.add_argument('--et-download', action='store_true', help="Download Emerging Threats rules.") + parser.add_argument('--et-alert', type=str, help="Comma separated list of ET categories to alert on, or 'default'.", default='') + parser.add_argument('--et-block', type=str, help="Comma separated list of ET categories to block, or 'default'.", default='') + parser.add_argument('--et-list', action='store_true', help="List available Emerging Threats categories.") + # General options + parser.add_argument('--disable-rules', type=str, help="Comma separated list of SIDs to disable.", default='') + + args = parser.parse_args() + + # if no args, print help + if len(sys.argv) == 1: + parser.print_help(sys.stderr) + sys.exit(1) + + if args.et_list: + log("Available Emerging Threats categories:") + rules_dir = os.path.join(ET_RULES_DIR, "rules") + # Check for /var/ns-snort/et-rules/rules/LICENSE + if not os.path.exists(os.path.join(rules_dir, "LICENSE")): + download_et_rules() + for file in os.listdir(rules_dir): + if file.endswith(".rules"): + log(file[:-6]) + sys.exit(0) + + backup_rules() + + if args.testing: + generate_testing_rules() + sys.exit(0) + + prepare_rule_file() # /etc/snort/rules/snort.rules + + if args.disable_rules: + # make a list of disabled sids as integers + disabled_sids = list(map(int, args.disable_rules.split(','))) + else: + disabled_sids = [] + + rules = [] + if args.official_download: + download_official_rules(args.official_oinkcode) + rules += filter_official_rules(args.official_policy, disabled_sids) + if args.et_download: + download_et_rules() + rules += filter_et_rules(args.et_alert, args.et_block, disabled_sids) + + append_rules_to_file(rules) + +if __name__ == "__main__": + main() From 7533bfcd3fbf9d1b2b11a5311cdc7e21d39b3368 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 4 Nov 2024 11:58:07 +0100 Subject: [PATCH 03/23] feat(snort3): Nethesis custom config Nethesis custom config is under /var/ns-snort. The directory contains both temporary data and configuration. - Make sure there is always at least one rule so snort can correctly start - Setup all basic lua scripts - Move configuration and download data to a custom directory --- packages/snort3/files/snort.init | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/snort3/files/snort.init b/packages/snort3/files/snort.init index f73ebe87..099d68d9 100644 --- a/packages/snort3/files/snort.init +++ b/packages/snort3/files/snort.init @@ -27,6 +27,14 @@ start_service() { local config_dir local interface + # nethesis patch: config_dir sometimes seems empty, read it using external command + if [ "$(uci -q get snort.snort.config_dir)" == "/var/ns-snort" ]; then + cdir="$(uci -q get snort.snort.config_dir)" + mkdir -p "${cdir}/rules" + echo 'pass ip any any -> any any (sid:9999999; rev:1;)' > "${cdir}/rules/default.rules" + find /etc/snort -type f ! -name snort.rules -exec cp '{}' "${cdir}" \; + fi + validate_snort_section snort || { echo "Validation failed, try 'snort-mgr check'." return 1 From 2fb5a567d99ca459379d7b8cb1994395cd3796e0 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 4 Nov 2024 14:32:03 +0100 Subject: [PATCH 04/23] fix(snort3): add README --- packages/snort3/README.md | 120 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 packages/snort3/README.md diff --git a/packages/snort3/README.md b/packages/snort3/README.md new file mode 100644 index 00000000..d2de6010 --- /dev/null +++ b/packages/snort3/README.md @@ -0,0 +1,120 @@ +# snort3 + +Nethesis fork of [Snort](https://www.snort.org/) 3 module from [OpenWrt packages main branch](https://github.com/openwrt/packages/tree/master/net/snort3). + +Changes: + +- ported to OpenWrt 23.05.5 +- added custom script for downloading rules +- small patch to init.d script to make sure that snort3 is started even if no rules are present +- use `/var/ns-snort` as working directory +- rules are not part of backup to avoid large backups and generating a new remote backup every time rules are updated + +## Quick start + +Enable snort3 and configure it to run in IPS mode, use only a limited number of rules from the official Snort ruleset and download them: +```bash +uci set snort.snort.enabled=1 +uci set snort.snort.external_net='!$HOME_NET' +uci set snort.snort.mode=ips +uci set snort.snort.manual=0 +uci set snort.snort.method=nfq +uci set snort.snort.config_dir=/var/ns-snort +uci set snort.nfq.queue_count=$(grep -c ^processor /proc/cpuinfo) +uci set snort.nfq.thread_count=$(grep -c ^processor /proc/cpuinfo) +uci set snort.nfq.chain_type=forward +uci commit snort + +ns-snort-rules --official-download --official-policy connectivity +/etc/init.d/snort restart +``` + +## Download rules + +Before configuring snort3 you need to download the rules. + +To download the rules use the `ns-snort-rules` script. +The script supports the following rulesets: +- Snort [Community Rules](https://www.snort.org/downloads/#rule-downloads) +- Snort Registered Rules using the Oinkcode +- [Emerging Threats rules](https://rules.emergingthreats.net/) + + +### Snort rules + +Snort rules support 3 policies: +- `connectivity` - This policy is specifically designed to favor device performance over the security controls in the policy. It should allow a customer to deploy one of our devices with minimal false positives and full rated performance of the box in most network deployments. In addition, this policy should detect the most common and most prevalent threats our customers will experience. +- `balanced` - This policy is the default policy that is recommended for initial deployments. This policy attempts to balance security needs and performance characteristics. Customers should be able to start with this policy and get a very good block rate with public evaluation tools, and relatively high performance rate with evaluation and testing tools. It is the default shipping state of the Snort Subscriber Ruleset for Open-Source Snort. +- `security` - This policy is designed for the small segment of our customer base that is exceptionally concerned about organizational security. Customers deploy this policy in protected networks, that have a lower bandwidth requirements, but much higher security requirements. Additionally, customers care less about false positives and noisy signatures. Application control, and locked down network usage are also concerns to customers deploying this policy. It should provide maximum protection, and application control, but should not bring the network down.he most critical attacks, such as those that are directed at highly critical systems or that are likely to be used in widespread attacks. + +Above description is extracted from [official Snort FAQ](https://www.snort.org/faq/why-are-rules-commented-out-by-default). + +When downloading the ules you must specify the policy to use. + +Example: download official Snort rules, and setup the policy to `connectivity`: + +```bash +ns-snort-rules --official-download --official-policy connectivity +``` + +### Emerging Threats rules + +Emerging Threats rules come with multiple rulesets. +For more info on available ruleset see the [official documentation](https://tools.emergingthreats.net/docs/ETPro%20Rule%20Categories.pdf). + +Example: download Emerging Threats rules, and setup the alert and block policy to `default`: +```bash +ns-snort-rules --et-download --et-alert=default --et-block=default +``` + +The `et-alert` and `et-block` takes a comma separated list of rulesets. +Both arguments are optional, if none of them is specified no rules will be enabled. +Categories specified in `et-alert` will be enabled as alert rules, and categories specified in `et-block` will be enabled as block rules. +The special value `default` will enable a safe set of rules for the specified policy. + +To list all available rulesets in the Emerging Threats rules use `--et-list` argument. + +More info on these rules are avaialble [here](https://community.emergingthreats.net/t/signature-metadata/96). + +### Disable rules + +To disable some rules use the `--disable-rules` argument. +The argument takes a comma separated list of SIDs to disable. + +Example: disable rules with SID 1 and 2: +```bash +ns-snort-rules --official-download --official-policy connectivity --disable-rules 1,2 +``` + +### Download rules examples + +Download both Snort and Emerging Threats rules. +Set the policy for Snort rules to `connectivity` and enable only the default Emerging Threats rules: +```bash +ns-snort-rules --official-download --official-policy balanced --et-download --et-block=default +``` + +Download only Snort rules with `security` policy: +```bash +ns-snort-rules --official-download --official-policy security +``` + +Download only Emerging Threats rules with all default rules for alert and block: +```bash +ns-snort-rules --et-download --et-alert=default --et-block=default +``` + +Download only Emerging Threats rules, block just `emerging-ciarmy` category, disable all alert rules: +```bash +ns-snort-rules --et-download --et-block=emerging-ciarmy +``` + +At the end of the download, always restart the snort service: +```bash +/etc/init.d/snort restart +``` + +Use only the test rules: +```bash +ns-snort-rules --test +``` \ No newline at end of file From a076a6673cd6e3476143bb9bbcad557f9f3bf2fd Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 4 Nov 2024 15:28:58 +0100 Subject: [PATCH 05/23] fix(snort3): limit blocking ET rules Exclude blocking rules without relevant metadata. At time of writing, the number of blocked changes from 3075 to 2107. --- packages/snort3/README.md | 4 +++- packages/snort3/files/ns-snort-rules | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index d2de6010..f8725ff5 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -74,7 +74,9 @@ The special value `default` will enable a safe set of rules for the specified po To list all available rulesets in the Emerging Threats rules use `--et-list` argument. -More info on these rules are avaialble [here](https://community.emergingthreats.net/t/signature-metadata/96). +Accordingly to ET [metadata guidelines](https://community.emergingthreats.net/t/signature-metadata/96), only rules with +metadata `deployment Perimeter` and `signature_severity Critical` are enabled as block rules. +Other block rules are disabled. ### Disable rules diff --git a/packages/snort3/files/ns-snort-rules b/packages/snort3/files/ns-snort-rules index 96cdcd59..daeed5e0 100644 --- a/packages/snort3/files/ns-snort-rules +++ b/packages/snort3/files/ns-snort-rules @@ -177,6 +177,8 @@ def filter_et_rules(alert, block, disabled_sids): log(f"Processing block ET category {file}") tmp = snort.parse_file(os.path.join(ET_RULES_DIR, "rules", f"{file}.rules")) for rule in tmp: + if not 'deployment Perimeter' in rule.metadata and not 'signature_severity Critical' in rule.metadata: + continue if rule.enabled and rule.sid not in disabled_sids: # replace alert with drop if rule.action != 'drop': From 0850c53397a0ac2d40f01f7d6025446f397cf877 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 4 Nov 2024 15:42:05 +0100 Subject: [PATCH 06/23] fix(snort3): ns-snort-rules, add license header --- packages/snort3/files/ns-snort-rules | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/snort3/files/ns-snort-rules b/packages/snort3/files/ns-snort-rules index daeed5e0..b7a3d164 100644 --- a/packages/snort3/files/ns-snort-rules +++ b/packages/snort3/files/ns-snort-rules @@ -1,5 +1,10 @@ #!/usr/bin/python3 +# +# Copyright (C) 2024 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-2.0-only +# + import argparse import os import sys From 942fa49d2915f7b15d5cc1345ed5fbcd99c79f19 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 4 Nov 2024 16:23:41 +0100 Subject: [PATCH 07/23] fix(snort3): init, remove default rule The default rule is not required: snort can start without rules. Also the default rule was marking all traffic as pass. --- packages/snort3/README.md | 5 +++-- packages/snort3/files/ns-snort-rules | 12 ++++++++---- packages/snort3/files/snort.init | 1 - 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index f8725ff5..0af5b42a 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -20,6 +20,7 @@ uci set snort.snort.mode=ips uci set snort.snort.manual=0 uci set snort.snort.method=nfq uci set snort.snort.config_dir=/var/ns-snort +uci set snort.snort.log_dir=/var/log/snort uci set snort.nfq.queue_count=$(grep -c ^processor /proc/cpuinfo) uci set snort.nfq.thread_count=$(grep -c ^processor /proc/cpuinfo) uci set snort.nfq.chain_type=forward @@ -116,7 +117,7 @@ At the end of the download, always restart the snort service: /etc/init.d/snort restart ``` -Use only the test rules: +Use only the testing rules, just alert for all ICMP traffic: ```bash -ns-snort-rules --test +ns-snort-rules --testing ``` \ No newline at end of file diff --git a/packages/snort3/files/ns-snort-rules b/packages/snort3/files/ns-snort-rules index b7a3d164..83885f70 100644 --- a/packages/snort3/files/ns-snort-rules +++ b/packages/snort3/files/ns-snort-rules @@ -18,6 +18,7 @@ from nethsec import snort DATA_DIR = "/var/ns-snort" RULES_DIR = os.path.join(DATA_DIR, "rules") BACKUP_DIR = os.path.join(DATA_DIR, "old.rules") +TESTING_RULES_FILE = os.path.join(RULES_DIR, "testing.rules") OFFICIAL_RULES_DIR = os.path.join(DATA_DIR, "snort-rules") ET_RULES_DIR = os.path.join(DATA_DIR, "et-rules") @@ -39,10 +40,9 @@ def backup_rules(): def generate_testing_rules(): log("Generating testing rules...") - testing_rules_dir = os.path.join(DATA_DIR, "testing-rules") - shutil.rmtree(testing_rules_dir, ignore_errors=True) - os.makedirs(testing_rules_dir, exist_ok=True) - rules_file = os.path.join(testing_rules_dir, "testing.rules") + shutil.rmtree(RULES_DIR, ignore_errors=True) + os.makedirs(RULES_DIR, exist_ok=True) + rules_file = TESTING_RULES_FILE testing_rules = [ 'alert icmp any any <> any any (msg:"TEST ALERT ICMP v4"; icode:0; itype:8; sid:99010;)', 'alert icmp any any <> any any (msg:"TEST ALERT ICMP v6"; icode:0; itype:33; sid:99011;)', @@ -250,6 +250,10 @@ def main(): if args.testing: generate_testing_rules() sys.exit(0) + else: + # cleanup testing rules + if os.path.exists(TESTING_RULES_FILE): + os.remove(TESTING_RULES_FILE) prepare_rule_file() # /etc/snort/rules/snort.rules diff --git a/packages/snort3/files/snort.init b/packages/snort3/files/snort.init index 099d68d9..91f8c50a 100644 --- a/packages/snort3/files/snort.init +++ b/packages/snort3/files/snort.init @@ -31,7 +31,6 @@ start_service() { if [ "$(uci -q get snort.snort.config_dir)" == "/var/ns-snort" ]; then cdir="$(uci -q get snort.snort.config_dir)" mkdir -p "${cdir}/rules" - echo 'pass ip any any -> any any (sid:9999999; rev:1;)' > "${cdir}/rules/default.rules" find /etc/snort -type f ! -name snort.rules -exec cp '{}' "${cdir}" \; fi From 2930ae0bc351a322121f0df8c3b7f38bddf5518e Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 4 Nov 2024 16:40:30 +0100 Subject: [PATCH 08/23] fix(snort3): improve README --- packages/snort3/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index 0af5b42a..d619ee1a 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -30,6 +30,11 @@ ns-snort-rules --official-download --official-policy connectivity /etc/init.d/snort restart ``` +To see what has been blocked or alerted, use: +```bash +snort-mgr report -v +``` + ## Download rules Before configuring snort3 you need to download the rules. From 89e573237da9eb6d2bec6b52ccf020640d19f891 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 4 Nov 2024 18:12:59 +0100 Subject: [PATCH 09/23] feat(ns-api): add ns.snort API to configure Snort 3 --- packages/ns-api/Makefile | 2 + packages/ns-api/README.md | 15 ++++ packages/ns-api/files/ns.snort | 120 ++++++++++++++++++++++++++++ packages/ns-api/files/ns.snort.json | 13 +++ 4 files changed, 150 insertions(+) create mode 100755 packages/ns-api/files/ns.snort create mode 100644 packages/ns-api/files/ns.snort.json diff --git a/packages/ns-api/Makefile b/packages/ns-api/Makefile index 0dace945..135a4dc4 100644 --- a/packages/ns-api/Makefile +++ b/packages/ns-api/Makefile @@ -154,6 +154,8 @@ define Package/ns-api/install $(INSTALL_DATA) ./files/ns.scan.json $(1)/usr/share/rpcd/acl.d/ $(INSTALL_BIN) ./files/ns.objects $(1)/usr/libexec/rpcd/ $(INSTALL_DATA) ./files/ns.objects.json $(1)/usr/share/rpcd/acl.d/ + $(INSTALL_BIN) ./files/ns.snort $(1)/usr/libexec/rpcd/ + $(INSTALL_DATA) ./files/ns.snort.json $(1)/usr/share/rpcd/acl.d/ $(INSTALL_BIN) ./files/ns.nathelpers $(1)/usr/libexec/rpcd/ $(INSTALL_DATA) ./files/ns.nathelpers.json $(1)/usr/share/rpcd/acl.d/ $(INSTALL_DIR) $(1)/lib/upgrade/keep.d diff --git a/packages/ns-api/README.md b/packages/ns-api/README.md index 6e65c6b7..883076b3 100644 --- a/packages/ns-api/README.md +++ b/packages/ns-api/README.md @@ -7669,3 +7669,18 @@ Output example: } } ``` + +## ns.snort + +Configure Snort IDS. + +### setup + +Setup Snort IDS: +```bash +api-cli ns.snort setup --data '{"enabled": true, "set_home_net": true, "include_vpn": false}' +``` + +If the API has been called for the first time, it will set all required configuration including IDS behavior. +If `set_home_net` is `true`, the API will set the `HOME_NET` variable for the Snort configuration. +If `include_vpn` is `true`, the API will include the VPN networks in the `HOME_NET` variable. diff --git a/packages/ns-api/files/ns.snort b/packages/ns-api/files/ns.snort new file mode 100755 index 00000000..2f1e3684 --- /dev/null +++ b/packages/ns-api/files/ns.snort @@ -0,0 +1,120 @@ +#!/usr/bin/python3 + +# +# Copyright (C) 2024 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-2.0-only +# + +# Read SSH authorized keys + +import os +import json +import subprocess +import json +import sys +import ipaddress +from nethsec import utils +from euci import EUci + +# Retrieve all non-WAN interfaces and their IP addresses +# It also return the IP address of VPN interfaces +def get_snort_homenet(uci, include_vpn=False): + snort_homenet = [] + # load JSON from `ip` command + snort_homenet = set() + try: + ip_output = subprocess.check_output(['ip', '--json', 'addr', 'show']) + ip_data = json.loads(ip_output) + except subprocess.CalledProcessError as e: + print(f"Failed to execute ip command: {e}", file=sys.stderr) + return [] + except json.JSONDecodeError as e: + print(f"Failed to parse JSON: {e}", file=sys.stderr) + return [] + wan_devices = utils.get_all_wan_devices(uci) + device_ip_map = {} + for interface in ip_data: + ifname = interface.get('ifname') + addr_info = interface.get('addr_info', []) + for addr in addr_info: + if addr.get('family') == 'inet': + local_ip = addr.get('local') + prefixlen = addr.get('prefixlen') + network = ipaddress.IPv4Network(f"{local_ip}/{prefixlen}", strict=False).network_address + device_ip_map[ifname] = f"{network}/{prefixlen}" + break + for device in device_ip_map: + if device in wan_devices or device == 'lo': + continue + snort_homenet.add(device_ip_map[device]) + + if include_vpn: + ipsec_tunnels = utils.get_all_by_type(uci, 'ipsec', 'tunnel') + for tunnel in ipsec_tunnels: + try: + remote_subnet = list(uci.get_all('ipsec', tunnel, 'remote_subnet')) + except: + remote_subnet = None + if remote_subnet: + for network in remote_subnet: + snort_homenet.add(network) + + ovpn_tunnels = utils.get_all_by_type(uci, 'openvpn', 'openvpn') + for tunnel in ovpn_tunnels: + # skip custom config + if not tunnel.startswith("ns_"): + continue + # skip road warrior servers + if uci.get('openvpn', tunnel, 'ns_auth_mode', default='') != '': + continue + # skip disabled tunnels + if uci.get('openvpn', tunnel, 'enabled', default='0') == '0': + continue + try: + remote_network = list(uci.get_all('openvpn', tunnel, 'route')) + except: + remote_network = None + if remote_network: + # route has this form: '192.168.6.0 255.255.255.0' + for network in remote_network: + ip, netmask = network.split() + addr = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False) + snort_homenet.add(str(addr)) + + return ' '.join(list(snort_homenet)) + +def setup(enabled, set_home_net = False, include_vpn = False): + uci = EUci() + + # first setup + config_dir = uci.get('snort', 'snort', 'config_dir', default = '') + if config_dir != '/var/ns-snort': + uci.set('snort', 'snort', 'config_dir', '/var/ns-snort') + uci.set('snort', 'snort', 'log_dir', '/var/log/snort') + uci.set('snort', 'snort', 'mode', 'ips') + uci.set('snort', 'snort', 'manual', '0') + uci.set('snort', 'snort', 'method', 'nfq') + uci.set('snort', 'snort', 'external_net', '!$HOME_NET') + uci.set('snort', 'nfq', 'chain_type', 'forward') + uci.set('snort', 'nfq', 'queue_count', str(os.cpu_count())) + uci.set('snort', 'nfq', 'thread_count', str(os.cpu_count())) + + if set_home_net: + uci.set('snort', 'snort', 'home_net', get_snort_homenet(uci, include_vpn)) + + uci.set('snort', 'snort', 'enabled', '1' if enabled else '0') + uci.save('snort') + +cmd = sys.argv[1] + +if cmd == 'list': + print(json.dumps({ + "setup": {"enabled": True, "set_home_net": True, "include_vpn": False}, + })) +else: + action = sys.argv[2] + if action == "setup": + data = json.JSONDecoder().decode(sys.stdin.read()) + setup(data.get('enabled', True), data.get('set_home_net', True), data.get('include_vpn', False)) + print(json.dumps({"status": "success"})) + diff --git a/packages/ns-api/files/ns.snort.json b/packages/ns-api/files/ns.snort.json new file mode 100644 index 00000000..5c9d2478 --- /dev/null +++ b/packages/ns-api/files/ns.snort.json @@ -0,0 +1,13 @@ +{ + "snort-manager": { + "description": "Manage snort configuration", + "write": {}, + "read": { + "ubus": { + "ns.snort": [ + "*" + ] + } + } + } +} From 853980f76888de92a6054eaf1ae341ed83400043 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 4 Nov 2024 18:13:19 +0100 Subject: [PATCH 10/23] fix(snort3): document API usage --- packages/snort3/README.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index d619ee1a..c8bc8570 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -14,19 +14,9 @@ Changes: Enable snort3 and configure it to run in IPS mode, use only a limited number of rules from the official Snort ruleset and download them: ```bash -uci set snort.snort.enabled=1 -uci set snort.snort.external_net='!$HOME_NET' -uci set snort.snort.mode=ips -uci set snort.snort.manual=0 -uci set snort.snort.method=nfq -uci set snort.snort.config_dir=/var/ns-snort -uci set snort.snort.log_dir=/var/log/snort -uci set snort.nfq.queue_count=$(grep -c ^processor /proc/cpuinfo) -uci set snort.nfq.thread_count=$(grep -c ^processor /proc/cpuinfo) -uci set snort.nfq.chain_type=forward -uci commit snort - +echo '{"enabled": true, "set_home_net": true, "include_vpn": false}' | /usr/libexec/rpcd/ns.snort call setup ns-snort-rules --official-download --official-policy connectivity +uci commit snort /etc/init.d/snort restart ``` From 7ee193605c078704ffeaa0532b736471ece604a8 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 4 Nov 2024 18:14:20 +0100 Subject: [PATCH 11/23] fix(snort3): document how to disable --- packages/snort3/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index c8bc8570..07c9bf5e 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -20,6 +20,13 @@ uci commit snort /etc/init.d/snort restart ``` +To disable snort3: +```bash +echo '{"enabled": false}' | /usr/libexec/rpcd/ns.snort call setup +uci commit snort +/etc/init.d/snort stop +``` + To see what has been blocked or alerted, use: ```bash snort-mgr report -v From c6fd4ccebaec780158b554533a22da4f4e574faa Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 5 Nov 2024 10:34:12 +0100 Subject: [PATCH 12/23] feat(snort3): support source and destination bypass --- packages/snort3/README.md | 20 +++++++++++++++++++- packages/snort3/files/nftables.uc | 25 +++++++++++++++++++++++++ packages/snort3/files/snort.init | 19 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index 07c9bf5e..3b2dec3d 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -122,4 +122,22 @@ At the end of the download, always restart the snort service: Use only the testing rules, just alert for all ICMP traffic: ```bash ns-snort-rules --testing -``` \ No newline at end of file +``` + +## Bypass IDS + +The IPS support bypass for destination or source IP addresses. Both IPv4 and IPv6 are supported. + +The following options are supported inside `snort.nfq` section: +- `bypass_dst_v4` - bypass IDS for destination IPv4 addresses +- `bypass_src_v4` - bypass IDS for source IPv4 addresses +- `bypass_dst_v6` - bypass IDS for destination IPv6 addresses +- `bypass_src_v6` - bypass IDS for source IPv6 addresses + +Usage example: +```bash +uci add_list snort.nfq.bypass_src_v4=192.168.100.23 +uci add_list snort.nfq.bypass_src_v4=192.168.100.28 +uci commit snort +/etc/init.d/snort restart +``` diff --git a/packages/snort3/files/nftables.uc b/packages/snort3/files/nftables.uc index 74b1678d..0fa82675 100644 --- a/packages/snort3/files/nftables.uc +++ b/packages/snort3/files/nftables.uc @@ -8,9 +8,34 @@ let chain_type = nfq.chain_type; -%} table inet snort { + set bypass_src_v4 { + type ipv4_addr + flags interval + include "/var/ns-snort/bypass_src_v4.conf" + } + set bypass_dst_v4 { + type ipv4_addr + flags interval + include "/var/ns-snort/bypass_dst_v4.conf" + } + set bypass_src_v6 { + type ipv6_addr + flags interval + include "/var/ns-snort/bypass_src_v6.conf" + } + set bypass_dst_v6 { + type ipv6_addr + flags interval + include "/var/ns-snort/bypass_dst_v6.conf" + } + chain {{ chain_type }}_{{ snort.mode }} { type filter hook {{ chain_type }} priority {{ nfq.chain_priority }} policy accept + ip saddr @bypass_src_v4 counter accept + ip daddr @bypass_dst_v4 counter accept + ip6 saddr @bypass_src_v6 counter accept + ip6 daddr @bypass_dst_v6 counter accept {% if (nfq.include) { // We use the ucode include here, so that the included file is also // part of the template and can use values passed in from the config. diff --git a/packages/snort3/files/snort.init b/packages/snort3/files/snort.init index 91f8c50a..1f45bce4 100644 --- a/packages/snort3/files/snort.init +++ b/packages/snort3/files/snort.init @@ -17,6 +17,21 @@ validate_snort_section() { 'interface:string' } +update_bypass_set() { + set_name=$1 + set_file="/var/ns-snort/${set_name}.conf" + ips=$(uci -q get snort.nfq.${set_name}) + if [ -z "$ips" ]; then + : > $set_file + else + echo -n "elements = {" > $set_file + for ip in $ips; do + echo -n " $ip," >> $set_file + done + echo " }" >> $set_file + fi +} + start_service() { # If you wish to use application-managed PID file: # output.logdir, in the snort lua config, determines the PID file location. @@ -32,6 +47,10 @@ start_service() { cdir="$(uci -q get snort.snort.config_dir)" mkdir -p "${cdir}/rules" find /etc/snort -type f ! -name snort.rules -exec cp '{}' "${cdir}" \; + update_bypass_set "bypass_src_v4" + update_bypass_set "bypass_dst_v4" + update_bypass_set "bypass_src_v6" + update_bypass_set "bypass_dst_v6" fi validate_snort_section snort || { From b0617dcab459d262c47a6d64bd182d14172e20c9 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Thu, 7 Nov 2024 15:08:27 +0100 Subject: [PATCH 13/23] fix(snort3): log start and stop to messages --- packages/snort3/files/snort.init | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/snort3/files/snort.init b/packages/snort3/files/snort.init index 1f45bce4..c6bf5fd7 100644 --- a/packages/snort3/files/snort.init +++ b/packages/snort3/files/snort.init @@ -63,14 +63,14 @@ start_service() { procd_open_instance if [ "$manual" = 0 ]; then local config_file=$($MGR setup) - procd_set_param command "$PROG" -q -c "${config_file}" + procd_set_param command "$PROG" -c "${config_file}" else procd_set_param command $PROG -q -i "$interface" -c "${config_dir%/}/snort.lua" --tweaks local procd_set_param env SNORT_LUA_PATH="$config_dir" procd_set_param file $CONFIGFILE fi procd_set_param respawn - procd_set_param stdout 0 + procd_set_param stdout 1 procd_set_param stderr 1 procd_close_instance } From 56b4b8023067dbeb859987c1562112fb3006eaa8 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 12 Nov 2024 16:39:07 +0100 Subject: [PATCH 14/23] fix(ns-api): improve snort setup Changes: - always setup queue_count and thread_count - add ns_policy option - add ns_disabled_rules option New options are directly used from ns-snort-rules --- packages/ns-api/files/ns.snort | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/ns-api/files/ns.snort b/packages/ns-api/files/ns.snort index 2f1e3684..9f1bd017 100755 --- a/packages/ns-api/files/ns.snort +++ b/packages/ns-api/files/ns.snort @@ -44,7 +44,8 @@ def get_snort_homenet(uci, include_vpn=False): device_ip_map[ifname] = f"{network}/{prefixlen}" break for device in device_ip_map: - if device in wan_devices or device == 'lo': + # exclude WAN interfaces, loopback, PPPoE and VPN interfaces + if device in wan_devices or device == 'lo' or device.startswith('tun') or device.startswith('ipsec') or device.startswith('wg') or device.startswith('tap') or device.startswith("ppp"): continue snort_homenet.add(device_ip_map[device]) @@ -83,7 +84,7 @@ def get_snort_homenet(uci, include_vpn=False): return ' '.join(list(snort_homenet)) -def setup(enabled, set_home_net = False, include_vpn = False): +def setup(enabled, set_home_net = False, include_vpn = False, ns_policy = 'balanced', ns_disabled_rules = []): uci = EUci() # first setup @@ -96,12 +97,17 @@ def setup(enabled, set_home_net = False, include_vpn = False): uci.set('snort', 'snort', 'method', 'nfq') uci.set('snort', 'snort', 'external_net', '!$HOME_NET') uci.set('snort', 'nfq', 'chain_type', 'forward') - uci.set('snort', 'nfq', 'queue_count', str(os.cpu_count())) - uci.set('snort', 'nfq', 'thread_count', str(os.cpu_count())) + + # always set the number of threads to the number of CPUs + # if the hardware changes, a new setup is required + uci.set('snort', 'nfq', 'queue_count', str(os.cpu_count())) + uci.set('snort', 'nfq', 'thread_count', str(os.cpu_count())) if set_home_net: uci.set('snort', 'snort', 'home_net', get_snort_homenet(uci, include_vpn)) + uci.set('snort', 'snort', 'ns_policy', ns_policy) + uci.set('snort', 'snort', 'ns_disabled_rules', ns_disabled_rules) uci.set('snort', 'snort', 'enabled', '1' if enabled else '0') uci.save('snort') @@ -109,12 +115,12 @@ cmd = sys.argv[1] if cmd == 'list': print(json.dumps({ - "setup": {"enabled": True, "set_home_net": True, "include_vpn": False}, + "setup": {"enabled": True, "set_home_net": True, "include_vpn": False, "ns_policy": "balanced", "ns_disabled_rules": []}, })) else: action = sys.argv[2] if action == "setup": data = json.JSONDecoder().decode(sys.stdin.read()) - setup(data.get('enabled', True), data.get('set_home_net', True), data.get('include_vpn', False)) + setup(data.get('enabled', True), data.get('set_home_net', True), data.get('include_vpn', False), data.get('ns_policy', 'connectivity'), data.get('ns_disabled_rules', [])) print(json.dumps({"status": "success"})) From 8272279527c42913905c18dc8d199db618c7e011 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 12 Nov 2024 16:39:21 +0100 Subject: [PATCH 15/23] fix(snort3): ns-snort-rules, refactor Changes: - remove ET rules: these rules do not work correctly on Snort 3 - read configuration from UCI - add --download option to force rules download --- packages/snort3/README.md | 99 +++++++---------- packages/snort3/files/ns-snort-rules | 152 ++++----------------------- 2 files changed, 57 insertions(+), 194 deletions(-) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index 3b2dec3d..04e6a62b 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -5,17 +5,18 @@ Nethesis fork of [Snort](https://www.snort.org/) 3 module from [OpenWrt packages Changes: - ported to OpenWrt 23.05.5 -- added custom script for downloading rules +- added custom script for downloading rules, `ns-snort-rules`: the script reads configuration from UCI, then download and filter rules - small patch to init.d script to make sure that snort3 is started even if no rules are present - use `/var/ns-snort` as working directory - rules are not part of backup to avoid large backups and generating a new remote backup every time rules are updated +- added new options for rules management in UCI ## Quick start Enable snort3 and configure it to run in IPS mode, use only a limited number of rules from the official Snort ruleset and download them: ```bash -echo '{"enabled": true, "set_home_net": true, "include_vpn": false}' | /usr/libexec/rpcd/ns.snort call setup -ns-snort-rules --official-download --official-policy connectivity +echo '{"enabled": true, "set_home_net": true, "include_vpn": false, "ns_policy": "security", "ns_disabled_rules": []}' | /usr/libexec/rpcd/ns.snort call setup +ns-snort-rules --download uci commit snort /etc/init.d/snort restart ``` @@ -32,98 +33,72 @@ To see what has been blocked or alerted, use: snort-mgr report -v ``` +## Configuration + +The configuration is stored in UCI under the `snort` configuration file. +This package add the following extra UCI options: + +- `ns_policy` - the policy to use for the Snort rules. Possible values are `connectivity`, `balanced`, `security`, `max-detect`. +- `ns_disabled_rules` - a list of SIDs to disable. + ## Download rules -Before configuring snort3 you need to download the rules. +Before configuring snort3 you need to select a policy then download the rules. To download the rules use the `ns-snort-rules` script. The script supports the following rulesets: - Snort [Community Rules](https://www.snort.org/downloads/#rule-downloads) -- Snort Registered Rules using the Oinkcode -- [Emerging Threats rules](https://rules.emergingthreats.net/) - +- Snort [Subscription Rules](https://www.snort.org/products#rule_subscriptions) using the Oinkcode ### Snort rules -Snort rules support 3 policies: +Snort rules support 4 policies: - `connectivity` - This policy is specifically designed to favor device performance over the security controls in the policy. It should allow a customer to deploy one of our devices with minimal false positives and full rated performance of the box in most network deployments. In addition, this policy should detect the most common and most prevalent threats our customers will experience. - `balanced` - This policy is the default policy that is recommended for initial deployments. This policy attempts to balance security needs and performance characteristics. Customers should be able to start with this policy and get a very good block rate with public evaluation tools, and relatively high performance rate with evaluation and testing tools. It is the default shipping state of the Snort Subscriber Ruleset for Open-Source Snort. - `security` - This policy is designed for the small segment of our customer base that is exceptionally concerned about organizational security. Customers deploy this policy in protected networks, that have a lower bandwidth requirements, but much higher security requirements. Additionally, customers care less about false positives and noisy signatures. Application control, and locked down network usage are also concerns to customers deploying this policy. It should provide maximum protection, and application control, but should not bring the network down.he most critical attacks, such as those that are directed at highly critical systems or that are likely to be used in widespread attacks. +- `max-detect` - This ruleset is meant to be used in testing environments and as such is not optimized for performance. False Positives for many of the rules in this policy are tolerated and/or expected and FP investigations will normally not be undertaken. Above description is extracted from [official Snort FAQ](https://www.snort.org/faq/why-are-rules-commented-out-by-default). When downloading the ules you must specify the policy to use. -Example: download official Snort rules, and setup the policy to `connectivity`: - -```bash -ns-snort-rules --official-download --official-policy connectivity -``` - -### Emerging Threats rules - -Emerging Threats rules come with multiple rulesets. -For more info on available ruleset see the [official documentation](https://tools.emergingthreats.net/docs/ETPro%20Rule%20Categories.pdf). - -Example: download Emerging Threats rules, and setup the alert and block policy to `default`: +Example: setup the policy to `connectivity` and download the official Community Snort rules: ```bash -ns-snort-rules --et-download --et-alert=default --et-block=default +uci set snort.snort.ns_policy=connectivity +ns-snort-rules --download +uci commit snort +/etc/init.d/snort restart ``` -The `et-alert` and `et-block` takes a comma separated list of rulesets. -Both arguments are optional, if none of them is specified no rules will be enabled. -Categories specified in `et-alert` will be enabled as alert rules, and categories specified in `et-block` will be enabled as block rules. -The special value `default` will enable a safe set of rules for the specified policy. - -To list all available rulesets in the Emerging Threats rules use `--et-list` argument. - -Accordingly to ET [metadata guidelines](https://community.emergingthreats.net/t/signature-metadata/96), only rules with -metadata `deployment Perimeter` and `signature_severity Critical` are enabled as block rules. -Other block rules are disabled. +If rules have been downloaded, the `ns-snort-rules` script will not download them again unless the `--download` argument is used. ### Disable rules -To disable some rules use the `--disable-rules` argument. -The argument takes a comma separated list of SIDs to disable. - -Example: disable rules with SID 1 and 2: -```bash -ns-snort-rules --official-download --official-policy connectivity --disable-rules 1,2 -``` - -### Download rules examples - -Download both Snort and Emerging Threats rules. -Set the policy for Snort rules to `connectivity` and enable only the default Emerging Threats rules: -```bash -ns-snort-rules --official-download --official-policy balanced --et-download --et-block=default -``` +To disable some rules use the `ns_disabled_rules` option inside UCI. +The option is a list of rule SIDS. -Download only Snort rules with `security` policy: +Example: disable rules with SID 24225 and 24227: ```bash -ns-snort-rules --official-download --official-policy security +uci add_list snort.snort.ns_disabled_rules=24225 +uci add_list snort.snort.ns_disabled_rules=24227 +uci commit snort +/etc/init.d/snort restart ``` -Download only Emerging Threats rules with all default rules for alert and block: -```bash -ns-snort-rules --et-download --et-alert=default --et-block=default -``` +### Testing rules -Download only Emerging Threats rules, block just `emerging-ciarmy` category, disable all alert rules: -```bash -ns-snort-rules --et-download --et-block=emerging-ciarmy -``` +Testing rules are a small set of rules that can be used to test the Snort installation. +The rules do not block any traffic but generate alerts for any type of ICMP traffic. +When enabled, testing rules exclude the official Snort rules. +To use them, no download is required. -At the end of the download, always restart the snort service: +Usage example: ```bash +ns-snort-rules --testing +uci commit snort /etc/init.d/snort restart ``` -Use only the testing rules, just alert for all ICMP traffic: -```bash -ns-snort-rules --testing -``` - ## Bypass IDS The IPS support bypass for destination or source IP addresses. Both IPv4 and IPv6 are supported. diff --git a/packages/snort3/files/ns-snort-rules b/packages/snort3/files/ns-snort-rules index 83885f70..772fc9f5 100644 --- a/packages/snort3/files/ns-snort-rules +++ b/packages/snort3/files/ns-snort-rules @@ -13,6 +13,7 @@ import logging import urllib.request import tarfile from nethsec import snort +from euci import EUci # Constants DATA_DIR = "/var/ns-snort" @@ -20,10 +21,7 @@ RULES_DIR = os.path.join(DATA_DIR, "rules") BACKUP_DIR = os.path.join(DATA_DIR, "old.rules") TESTING_RULES_FILE = os.path.join(RULES_DIR, "testing.rules") OFFICIAL_RULES_DIR = os.path.join(DATA_DIR, "snort-rules") -ET_RULES_DIR = os.path.join(DATA_DIR, "et-rules") - COMMUNITY_RULES_URL = "https://www.snort.org/downloads/community/snort3-community-rules.tar.gz" -ET_RULES_URL = "https://rules.emergingthreats.net/open/snort-2.9.7.0/emerging.rules.tar.gz" # Setup logging logging.basicConfig(level=logging.INFO, format='%(message)s') @@ -51,10 +49,11 @@ def generate_testing_rules(): with open(rules_file, 'w') as f: for rule in testing_rules: f.write(rule + "\n") - log(f"Testing rules generated at {rules_file}") + log(f"Testing rules generated at {rules_file}. Restart snort to apply: /etc/init.d/snort restart") def download_official_rules(oinkcode): if oinkcode: + log("Downloading subscriber rules...") url = f"https://www.snort.org/downloads/snortrules-snapshot-{oinkcode}.tar.gz" archive_loc = f"snortrules-snapshot-{oinkcode}" else: @@ -81,28 +80,6 @@ def download_official_rules(oinkcode): log(f"Failed to download rules from {url}: {e}") sys.exit(1) -def download_et_rules(): - url = ET_RULES_URL - archive_loc = "emerging-threats-rules" - archive_path = os.path.join(DATA_DIR, f"{archive_loc}.tar.gz") - os.makedirs(ET_RULES_DIR, exist_ok=True) - log("Downloading Emerging Threats rules...") - try: - with urllib.request.urlopen(url) as response: - if response.status != 200: - log(f"Failed to download rules from {url}") - sys.exit(1) - with open(archive_path, 'wb') as f: - shutil.copyfileobj(response, f) - log(f"Emerging Threats rules downloaded to {archive_path}") - - with tarfile.open(archive_path, 'r:gz') as tar: - tar.extractall(path=ET_RULES_DIR) - log(f"Extracted Emerging Threats rules to {ET_RULES_DIR}") - except Exception as e: - log(f"Failed to download Emerging Threats rules from {url}: {e}") - sys.exit(1) - def filter_official_rules(policy, disabled_sids): rules = [] ret = [] @@ -115,83 +92,21 @@ def filter_official_rules(policy, disabled_sids): # - policy max-detect-ips drop if rule.metadata is None or not rule.enabled or rule.sid in disabled_sids: log(f"Skipping disabled rule {rule.sid}") + continue if policy == "connectivity" and 'policy connectivity-ips drop' in rule.metadata: rules.append(rule) elif policy == "balanced" and 'policy balanced-ips drop' in rule.metadata: rules.append(rule) elif policy == "security" and 'policy security-ips drop' in rule.metadata: rules.append(rule) + elif policy == "max-detect" and 'policy max-detect-ips drop' in rule.metadata: + rules.append(rule) for rule in rules: if rule.action != 'drop': rule.raw = rule.raw.replace(rule.action, "drop", 1) ret.append(rule.raw) return ret -def filter_et_rules(alert, block, disabled_sids): - rules = [] - if not alert and not block: - log("No ET categories specified.") - return rules - alert_files = [] - if alert == 'default': - alert_files = [ - 'emerging-current_events', - 'emerging-dos', - 'emerging-ftp', - 'emerging-games', - 'emerging-inappropriate', - 'emerging-info', - 'emerging-misc', - 'emerging-mobile_malware', - 'emerging-p2p', - 'emerging-scan', - 'emerging-shellcode', - 'emerging-sql', - 'emerging-malware', - 'emerging-voip', - 'emerging-web_client', - 'emerging-worm' - ] - elif alert: - alert_files = alert.split(',') - block_files = [] - if block == 'default': - block_files = [ - 'emerging-botcc', - 'emerging-ciarmy', - 'emerging-compromised', - 'emerging-drop', - 'emerging-dshield', - 'emerging-activex', - 'emerging-attack_response', - 'emerging-exploit', - 'emerging-netbios' - ] - elif block: - block_files = block.split(',') - # ET rules are downloaded to /var/snort.d/emerging-threats-rules/rules/ - for file in alert_files: - log(f"Processing alert ET category {file}") - tmp = snort.parse_file(os.path.join(ET_RULES_DIR, "rules", f"{file}.rules")) - for rule in tmp: - if rule.enabled and rule.sid not in disabled_sids: - if rule.action != 'alert': - rule.raw = rule.raw.replace(rule.action, "alert", 1) - rules.append(rule.raw) - for file in block_files: - log(f"Processing block ET category {file}") - tmp = snort.parse_file(os.path.join(ET_RULES_DIR, "rules", f"{file}.rules")) - for rule in tmp: - if not 'deployment Perimeter' in rule.metadata and not 'signature_severity Critical' in rule.metadata: - continue - if rule.enabled and rule.sid not in disabled_sids: - # replace alert with drop - if rule.action != 'drop': - rule.raw = rule.raw.replace(rule.action, "drop", 1) - rules.append(rule.raw) - - return rules - def prepare_rule_file(): rule_file = os.path.join(RULES_DIR, "snort.rules") if os.path.exists(rule_file): @@ -212,39 +127,14 @@ def append_rules_to_file(rules): log(f"Appended {len(rules)} rules to {rule_file}") def main(): + uci = EUci() parser = argparse.ArgumentParser(description='Process snort rules.') - parser.add_argument('--testing', action='store_true', help="Create synthetic testing rules instead of downloading.") - # Options for official rules - parser.add_argument('--official-download', action='store_true', help="Download official Snort rules.") - parser.add_argument('--official-oinkcode', type=str, help="Oinkcode for downloading official rules.") - parser.add_argument('--official-policy', choices=['connectivity', 'balanced', 'security'], help="Policy for official rules: connectivity, balanced, or security.") - # Options for ET rules - parser.add_argument('--et-download', action='store_true', help="Download Emerging Threats rules.") - parser.add_argument('--et-alert', type=str, help="Comma separated list of ET categories to alert on, or 'default'.", default='') - parser.add_argument('--et-block', type=str, help="Comma separated list of ET categories to block, or 'default'.", default='') - parser.add_argument('--et-list', action='store_true', help="List available Emerging Threats categories.") # General options - parser.add_argument('--disable-rules', type=str, help="Comma separated list of SIDs to disable.", default='') - + parser.add_argument('--testing', action='store_true', help="Create synthetic testing rules instead of downloading.") + parser.add_argument('--download', action='store_true', help="Download Snort rules.") args = parser.parse_args() - # if no args, print help - if len(sys.argv) == 1: - parser.print_help(sys.stderr) - sys.exit(1) - - if args.et_list: - log("Available Emerging Threats categories:") - rules_dir = os.path.join(ET_RULES_DIR, "rules") - # Check for /var/ns-snort/et-rules/rules/LICENSE - if not os.path.exists(os.path.join(rules_dir, "LICENSE")): - download_et_rules() - for file in os.listdir(rules_dir): - if file.endswith(".rules"): - log(file[:-6]) - sys.exit(0) - backup_rules() if args.testing: @@ -257,19 +147,17 @@ def main(): prepare_rule_file() # /etc/snort/rules/snort.rules - if args.disable_rules: - # make a list of disabled sids as integers - disabled_sids = list(map(int, args.disable_rules.split(','))) - else: - disabled_sids = [] - - rules = [] - if args.official_download: - download_official_rules(args.official_oinkcode) - rules += filter_official_rules(args.official_policy, disabled_sids) - if args.et_download: - download_et_rules() - rules += filter_et_rules(args.et_alert, args.et_block, disabled_sids) + try: + disabled_rules = list(map(int, uci.get_all("snort", "snort", "ns_disabled_rules"))) + except: + disabled_rules = [] + + if args.download: + oinkcode = uci.get("snort", "snort", "oinkcode", default=None) + download_official_rules(oinkcode) + + official_policy = uci.get("snort", "snort", "ns_policy", default="security") + rules = filter_official_rules(official_policy, disabled_rules) append_rules_to_file(rules) From 4c195dcff14307f7549d8aaa6d8d75130ca16605 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 12 Nov 2024 17:28:10 +0100 Subject: [PATCH 16/23] feat(ns-api): snort, restart cron when needed --- packages/ns-api/Makefile | 1 + packages/ns-api/README.md | 4 ++- packages/ns-api/files/ns.snort | 30 ++++++++++++++++++- .../ns-api/files/post-commit/restart-cron.py | 22 ++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100755 packages/ns-api/files/post-commit/restart-cron.py diff --git a/packages/ns-api/Makefile b/packages/ns-api/Makefile index 135a4dc4..7ee4d9f2 100644 --- a/packages/ns-api/Makefile +++ b/packages/ns-api/Makefile @@ -171,6 +171,7 @@ define Package/ns-api/install $(INSTALL_BIN) ./files/pre-commit/update-objects.py $(1)/usr/libexec/ns-api/pre-commit $(INSTALL_BIN) ./files/post-commit/configure-netifyd.py $(1)/usr/libexec/ns-api/post-commit $(INSTALL_BIN) ./files/post-commit/reload-ipsets.py $(1)/usr/libexec/ns-api/post-commit + $(INSTALL_BIN) ./files/post-commit/restart-cron.py $(1)/usr/libexec/ns-api/post-commit $(INSTALL_BIN) ./files/remove-pppoe-keepalive $(1)/usr/share/ns-api endef diff --git a/packages/ns-api/README.md b/packages/ns-api/README.md index 883076b3..eab8d0ec 100644 --- a/packages/ns-api/README.md +++ b/packages/ns-api/README.md @@ -7678,9 +7678,11 @@ Configure Snort IDS. Setup Snort IDS: ```bash -api-cli ns.snort setup --data '{"enabled": true, "set_home_net": true, "include_vpn": false}' +api-cli ns.snort setup --data '{"enabled": true, "set_home_net": true, "include_vpn": false, "ns_policy": "balanced", "ns_disabled_rules": []}' ``` If the API has been called for the first time, it will set all required configuration including IDS behavior. If `set_home_net` is `true`, the API will set the `HOME_NET` variable for the Snort configuration. If `include_vpn` is `true`, the API will include the VPN networks in the `HOME_NET` variable. +The `ns_policy` can be `balanced`, `security` or `connectivity` or `max-detect`. +The `ns_disabled_rules` is a list of SIDs (integer) of rules to be disabled. diff --git a/packages/ns-api/files/ns.snort b/packages/ns-api/files/ns.snort index 9f1bd017..d645af91 100755 --- a/packages/ns-api/files/ns.snort +++ b/packages/ns-api/files/ns.snort @@ -84,6 +84,27 @@ def get_snort_homenet(uci, include_vpn=False): return ' '.join(list(snort_homenet)) +def add_download_cron_job(): + # add download rules cron job: every night at 2:30 plus random 30 minutes + cron_job = f"30 2 * * * sleep $((RANDOM % 1800)) && /usr/bin/ns-snort-rules --download" + with open('/etc/crontabs/root', 'r') as f: + lines = f.readlines() + for line in lines: + if 'ns-snort-rules' in line: + return + with open('/etc/crontabs/root', 'w') as f: + for line in lines: + f.write(line) + f.write(f'{cron_job}\n') + +def remove_download_cron_job(): + with open('/etc/crontabs/root', 'r') as f: + lines = f.readlines() + with open('/etc/crontabs/root', 'w') as f: + for line in lines: + if 'ns-snort-rules' not in line: + f.write(line) + def setup(enabled, set_home_net = False, include_vpn = False, ns_policy = 'balanced', ns_disabled_rules = []): uci = EUci() @@ -108,7 +129,14 @@ def setup(enabled, set_home_net = False, include_vpn = False, ns_policy = 'balan uci.set('snort', 'snort', 'ns_policy', ns_policy) uci.set('snort', 'snort', 'ns_disabled_rules', ns_disabled_rules) - uci.set('snort', 'snort', 'enabled', '1' if enabled else '0') + + if enabled: + uci.set('snort', 'snort', 'enabled', '1') + add_download_cron_job() + else: + uci.set('snort', 'snort', 'enabled', '0') + remove_download_cron_job() + uci.save('snort') cmd = sys.argv[1] diff --git a/packages/ns-api/files/post-commit/restart-cron.py b/packages/ns-api/files/post-commit/restart-cron.py new file mode 100755 index 00000000..7cf18727 --- /dev/null +++ b/packages/ns-api/files/post-commit/restart-cron.py @@ -0,0 +1,22 @@ +#!/usr/bin/python + +# +# Copyright (C) 2024 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-2.0-only +# + +# This script forces cron restart + +import subprocess + +force_restart = False + +# snort: added or removed cron job for rules download +if 'snort' in changes: + for change in changes['firewall']: + if 'enabled' in change: + force_restart = True + break + +if force_restart: + subprocess.run(["/etc/init.d", "cron", "restart"]) From 2a9426ddd7610ba8c97d5cef8465ede45d76616a Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 12 Nov 2024 17:28:29 +0100 Subject: [PATCH 17/23] fix(snort3): download rules on startup When a machine is rebooted or updated using an image, Snort rules are not present because stored in RAM. During the service start, if rules are not present, the init script now tries to load the rules multiple times. Maximum wait time is 1 minute. Snort will not fail the start: a nightly cron job can update the rules and restart the service. --- packages/snort3/files/snort.init | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/snort3/files/snort.init b/packages/snort3/files/snort.init index c6bf5fd7..0c1c3d59 100644 --- a/packages/snort3/files/snort.init +++ b/packages/snort3/files/snort.init @@ -32,6 +32,21 @@ update_bypass_set() { fi } +download_rules_if_needed () { + if ! find /var/ns-snort/rules/ -type f -name "*.rules" | grep -q . ; then + # no rules file found, start downloaded loop + attempt=0 + until /usr/bin/ns-snort-rules --download; do + attempt=$((attempt + 1)) + if [ "$attempt" -ge 6 ]; then + echo "Error: failed to download snort rules after 6 attempts (1 minute)." + break + fi + sleep 10 + done + fi +} + start_service() { # If you wish to use application-managed PID file: # output.logdir, in the snort lua config, determines the PID file location. @@ -51,6 +66,7 @@ start_service() { update_bypass_set "bypass_dst_v4" update_bypass_set "bypass_src_v6" update_bypass_set "bypass_dst_v6" + download_rules_if_needed fi validate_snort_section snort || { From d959c6e194b41644dfe3468241d177fc2f35e1b1 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 12 Nov 2024 17:37:12 +0100 Subject: [PATCH 18/23] fix(snort): restart service after rules download The restart is required to load the new rules. --- packages/ns-api/files/ns.snort | 2 +- packages/snort3/files/ns-snort-rules | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/ns-api/files/ns.snort b/packages/ns-api/files/ns.snort index d645af91..d9fe1ba2 100755 --- a/packages/ns-api/files/ns.snort +++ b/packages/ns-api/files/ns.snort @@ -86,7 +86,7 @@ def get_snort_homenet(uci, include_vpn=False): def add_download_cron_job(): # add download rules cron job: every night at 2:30 plus random 30 minutes - cron_job = f"30 2 * * * sleep $((RANDOM % 1800)) && /usr/bin/ns-snort-rules --download" + cron_job = f"30 2 * * * sleep $((RANDOM % 1800)) && /usr/bin/ns-snort-rules --download --restart" with open('/etc/crontabs/root', 'r') as f: lines = f.readlines() for line in lines: diff --git a/packages/snort3/files/ns-snort-rules b/packages/snort3/files/ns-snort-rules index 772fc9f5..9aa3f78a 100644 --- a/packages/snort3/files/ns-snort-rules +++ b/packages/snort3/files/ns-snort-rules @@ -12,6 +12,7 @@ import shutil import logging import urllib.request import tarfile +import subprocess from nethsec import snort from euci import EUci @@ -133,12 +134,16 @@ def main(): # General options parser.add_argument('--testing', action='store_true', help="Create synthetic testing rules instead of downloading.") parser.add_argument('--download', action='store_true', help="Download Snort rules.") + parser.add_argument('--restart', action='store_true', help="Force Snort to restart after updating rules.") args = parser.parse_args() backup_rules() if args.testing: generate_testing_rules() + if args.restart: + log("Restarting snort...") + subprocess.run(["/etc/init.d/snort", "restart"], check=True) sys.exit(0) else: # cleanup testing rules @@ -161,5 +166,9 @@ def main(): append_rules_to_file(rules) + if args.restart: + log("Restarting snort...") + subprocess.run(["/etc/init.d/snort", "restart"], check=True) + if __name__ == "__main__": main() From 43ac1c7f860eb341daeb10ae20d99f9df4e71a76 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Wed, 13 Nov 2024 09:57:10 +0100 Subject: [PATCH 19/23] feat(snort3): add suppression rules --- packages/snort3/README.md | 30 ++++++++++++++++++++++++++---- packages/snort3/files/snort.init | 19 ++++++++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index 04e6a62b..58375b1d 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -15,7 +15,7 @@ Changes: Enable snort3 and configure it to run in IPS mode, use only a limited number of rules from the official Snort ruleset and download them: ```bash -echo '{"enabled": true, "set_home_net": true, "include_vpn": false, "ns_policy": "security", "ns_disabled_rules": []}' | /usr/libexec/rpcd/ns.snort call setup +echo '{"enabled": true, "set_home_net": true, "include_vpn": false, "ns_policy": "connectivity", "ns_disabled_rules": []}' | /usr/libexec/rpcd/ns.snort call setup ns-snort-rules --download uci commit snort /etc/init.d/snort restart @@ -33,13 +33,15 @@ To see what has been blocked or alerted, use: snort-mgr report -v ``` -## Configuration +## New configuration options The configuration is stored in UCI under the `snort` configuration file. -This package add the following extra UCI options: + +This package add the following extra UCI options under the `snort` section: - `ns_policy` - the policy to use for the Snort rules. Possible values are `connectivity`, `balanced`, `security`, `max-detect`. - `ns_disabled_rules` - a list of SIDs to disable. +- `ns_suppress` - a list of suppress rules. See [Rule suppression](#rule-suppression) for more details. ## Download rules @@ -74,6 +76,8 @@ If rules have been downloaded, the `ns-snort-rules` script will not download the ### Disable rules +A disabled rule is a rule that is not include in the Snort ruleset. + To disable some rules use the `ns_disabled_rules` option inside UCI. The option is a list of rule SIDS. @@ -99,7 +103,7 @@ uci commit snort /etc/init.d/snort restart ``` -## Bypass IDS +## Bypass IPS The IPS support bypass for destination or source IP addresses. Both IPv4 and IPv6 are supported. @@ -116,3 +120,21 @@ uci add_list snort.nfq.bypass_src_v4=192.168.100.28 uci commit snort /etc/init.d/snort restart ``` +## Rule suppression + +A suppression rule is a rule that is ignored by Snort for a specific IP address or CIDR. + +To add a suppress rule use the `ns_suppress` option inside UCI `snort.snort` section. +Each suppress rule is a comma separated list of values: `gid,sid,direction,ip,description`: + +- `gid` - the rule GID, it is a number and usually is always `1` +- `sid` - the rule SID, it is a number +- `direction` - the direction of the rule, it can be `by_src` or `by_dst` +- `ip` - the IPv4 address or CIDR to suppress +- `description` - a description of the suppress rule, it is optional and can be omitted; it must contain no commas nor no spaces and newlines + +Example: +```bash +uci add_list snort.snort.ns_suppress='1,1234,by_src,1.2.3.4,very_bad' +uci add_list snort.snort.ns_suppress='1,1234,by_dst,8.8.8.8,noisy_rule' +``` \ No newline at end of file diff --git a/packages/snort3/files/snort.init b/packages/snort3/files/snort.init index 0c1c3d59..3f729882 100644 --- a/packages/snort3/files/snort.init +++ b/packages/snort3/files/snort.init @@ -47,6 +47,22 @@ download_rules_if_needed () { fi } +setup_tweaks() { + tweaks_file="/var/ns-snort/ns_local.lua" + echo -e "suppress = \n{\n" > $tweaks_file + # suppress element format: gid,sid,direction,ip,description + suppress=$(uci -q get snort.snort.ns_suppress) + for s in $suppress; do + s=$(echo $s | sed "s/'//g") + gid=$(echo $s | cut -d, -f1) + sid=$(echo $s | cut -d, -f2) + direction=$(echo $s | cut -d, -f3) + ip=$(echo $s | cut -d, -f4) + echo -e " { gid = $gid, sid = $sid, track = '$direction', ip = '$ip' },\n" >> $tweaks_file + done + echo "}" >> $tweaks_file +} + start_service() { # If you wish to use application-managed PID file: # output.logdir, in the snort lua config, determines the PID file location. @@ -67,6 +83,7 @@ start_service() { update_bypass_set "bypass_src_v6" update_bypass_set "bypass_dst_v6" download_rules_if_needed + setup_tweaks fi validate_snort_section snort || { @@ -79,7 +96,7 @@ start_service() { procd_open_instance if [ "$manual" = 0 ]; then local config_file=$($MGR setup) - procd_set_param command "$PROG" -c "${config_file}" + procd_set_param command "$PROG" -c "${config_file}" --tweaks ns_local else procd_set_param command $PROG -q -i "$interface" -c "${config_dir%/}/snort.lua" --tweaks local procd_set_param env SNORT_LUA_PATH="$config_dir" From 089477991a79b4d9c57c3736979614ace03e5ae0 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Wed, 13 Nov 2024 10:10:04 +0100 Subject: [PATCH 20/23] fix(snort3): improve README --- packages/snort3/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index 58375b1d..dda9c900 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -43,6 +43,13 @@ This package add the following extra UCI options under the `snort` section: - `ns_disabled_rules` - a list of SIDs to disable. - `ns_suppress` - a list of suppress rules. See [Rule suppression](#rule-suppression) for more details. +This package also adds the following extra UCI options under the `nfq` section: + +- `bypass_dst_v4` - bypass IDS for destination IPv4 addresses +- `bypass_src_v4` - bypass IDS for source IPv4 addresses +- `bypass_dst_v6` - bypass IDS for destination IPv6 addresses +- `bypass_src_v6` - bypass IDS for source IPv6 addresses + ## Download rules Before configuring snort3 you need to select a policy then download the rules. From d2a19902541ca63a79615baae98c434c26c195a5 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Fri, 15 Nov 2024 10:17:55 +0100 Subject: [PATCH 21/23] feat(snort3): add option to alert excluded rules Official rules with a policy are rarerly triggered. Add the ns_alert_excluded option to alert all rules not belonging to a policy: this will give the user some insight about the network traffic without blocking rules with low confidence. --- packages/snort3/README.md | 9 +++++++++ packages/snort3/files/ns-snort-rules | 24 ++++++++++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index dda9c900..58306bfb 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -10,6 +10,7 @@ Changes: - use `/var/ns-snort` as working directory - rules are not part of backup to avoid large backups and generating a new remote backup every time rules are updated - added new options for rules management in UCI +- log alerts as JSON files to `/var/log/snort` ## Quick start @@ -28,6 +29,13 @@ uci commit snort /etc/init.d/snort stop ``` +To change the policy to `balanced` and download the rules: +```bash +echo '{"enabled": true, "set_home_net": true, "include_vpn": false, "ns_policy": "balanced", "ns_disabled_rules": []}' | /usr/libexec/rpcd/ns.snort call setup +uci commit snort +ns-snort-rules --restart +``` + To see what has been blocked or alerted, use: ```bash snort-mgr report -v @@ -40,6 +48,7 @@ The configuration is stored in UCI under the `snort` configuration file. This package add the following extra UCI options under the `snort` section: - `ns_policy` - the policy to use for the Snort rules. Possible values are `connectivity`, `balanced`, `security`, `max-detect`. +- `ns_alert_excluded` - if set to `1` the rules that are not part of any policy are added as alert rules, ICMP rules are always excluded because they are used for testing. - `ns_disabled_rules` - a list of SIDs to disable. - `ns_suppress` - a list of suppress rules. See [Rule suppression](#rule-suppression) for more details. diff --git a/packages/snort3/files/ns-snort-rules b/packages/snort3/files/ns-snort-rules index 9aa3f78a..a4a7b17a 100644 --- a/packages/snort3/files/ns-snort-rules +++ b/packages/snort3/files/ns-snort-rules @@ -81,8 +81,9 @@ def download_official_rules(oinkcode): log(f"Failed to download rules from {url}: {e}") sys.exit(1) -def filter_official_rules(policy, disabled_sids): - rules = [] +def filter_official_rules(policy, alert_excluded, disabled_sids): + drop_rules = [] + alert_rules = [] ret = [] # official rules are downloaded to /var/snort.d/snort3-community-rules/snort3-community.rules for rule in snort.parse_file(os.path.join(OFFICIAL_RULES_DIR, "snort3-community-rules/snort3-community.rules")): @@ -95,17 +96,23 @@ def filter_official_rules(policy, disabled_sids): log(f"Skipping disabled rule {rule.sid}") continue if policy == "connectivity" and 'policy connectivity-ips drop' in rule.metadata: - rules.append(rule) + drop_rules.append(rule) elif policy == "balanced" and 'policy balanced-ips drop' in rule.metadata: - rules.append(rule) + drop_rules.append(rule) elif policy == "security" and 'policy security-ips drop' in rule.metadata: - rules.append(rule) + drop_rules.append(rule) elif policy == "max-detect" and 'policy max-detect-ips drop' in rule.metadata: - rules.append(rule) - for rule in rules: + drop_rules.append(rule) + else: + # Add excluded rules as alerts but not if they are ICMP (too noisy) + if alert_excluded and 'PROTOCOL-ICMP' not in rule.msg: + alert_rules.append(rule) + for rule in drop_rules: if rule.action != 'drop': rule.raw = rule.raw.replace(rule.action, "drop", 1) ret.append(rule.raw) + # append alert rules + ret += alert_rules return ret def prepare_rule_file(): @@ -162,7 +169,8 @@ def main(): download_official_rules(oinkcode) official_policy = uci.get("snort", "snort", "ns_policy", default="security") - rules = filter_official_rules(official_policy, disabled_rules) + alert_excluded = uci.get("snort", "snort", "ns_alert_excluded", default="") + rules = filter_official_rules(official_policy, alert_excluded, disabled_rules) append_rules_to_file(rules) From d79dbe41ecd75035d5edfeff5f745f2c3625175b Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Fri, 15 Nov 2024 10:21:39 +0100 Subject: [PATCH 22/23] feat(snort3): logs alerts to syslog Make sure that all logs are stored to /var/log/messages to ease debugging. Also, such log can be sent remotely and stored safefly on the controller. --- packages/snort3/README.md | 1 + packages/snort3/files/snort.uc | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index 58306bfb..d4efacca 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -11,6 +11,7 @@ Changes: - rules are not part of backup to avoid large backups and generating a new remote backup every time rules are updated - added new options for rules management in UCI - log alerts as JSON files to `/var/log/snort` +- log alerts to syslog ## Quick start diff --git a/packages/snort3/files/snort.uc b/packages/snort3/files/snort.uc index 6e14a0ab..2386ab1a 100644 --- a/packages/snort3/files/snort.uc +++ b/packages/snort3/files/snort.uc @@ -86,8 +86,8 @@ daq = { } } --- alert_syslog = { level = 'info', } -- Generate output to syslog. -alert_syslog = nil -- Disable output to syslog +alert_syslog = { level = 'info', } -- Generate output to syslog. +-- alert_syslog = nil -- Disable output to syslog {% if (int(snort.logging)): %} -- Note that this is also the location of the PID file, if you use it. From 7f08002eb9c7f768359a05c683a3922d461e9aa3 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 3 Dec 2024 16:52:29 +0100 Subject: [PATCH 23/23] fix(snort): improve readme --- packages/snort3/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/snort3/README.md b/packages/snort3/README.md index d4efacca..94af11d0 100644 --- a/packages/snort3/README.md +++ b/packages/snort3/README.md @@ -6,7 +6,7 @@ Changes: - ported to OpenWrt 23.05.5 - added custom script for downloading rules, `ns-snort-rules`: the script reads configuration from UCI, then download and filter rules -- small patch to init.d script to make sure that snort3 is started even if no rules are present +- patched init.d script to implement bypass and rule suppression - use `/var/ns-snort` as working directory - rules are not part of backup to avoid large backups and generating a new remote backup every time rules are updated - added new options for rules management in UCI @@ -154,4 +154,4 @@ Example: ```bash uci add_list snort.snort.ns_suppress='1,1234,by_src,1.2.3.4,very_bad' uci add_list snort.snort.ns_suppress='1,1234,by_dst,8.8.8.8,noisy_rule' -``` \ No newline at end of file +```