411 lines
12 KiB
Bash
411 lines
12 KiB
Bash
#!/bin/sh
|
|
# Copyright (c) 2023-2024 Eric Fahlgren <eric.fahlgren@gmail.com>
|
|
# SPDX-License-Identifier: GPL-2.0
|
|
# shellcheck disable=SC2039,SC2155 # "local" not defined in POSIX sh
|
|
|
|
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
|
|
|
|
}
|
|
|
|
_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 date
|
|
case "$DATE_SPEC" in
|
|
('') operator='>' ; date='' ;;
|
|
(-*) 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
|
|
|
|
# We need to create a single json array because 'jsonfilter -a' is
|
|
# severely broken.
|
|
awk '
|
|
BEGIN { print "[" }
|
|
{ print $0"," }
|
|
END { print "{}]" }
|
|
' "${log_dir}"/*alert_json.txt \
|
|
| jsonfilter -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)
|
|
_filter_by_date "${log_dir}" | while read -r line; do
|
|
unset -v 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)
|
|
|
|
echo "Events involving ${PATTERN:-all IPs} - $(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}'
|
|
|
|
printf "%7d incidents shown of %d logged\n" "$n_incidents" "$n_total"
|
|
|
|
#-- 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 <<USAGE
|
|
Usage:
|
|
|
|
$0 setup|teardown|resetup [-v/--verbose] [-q/--quiet]
|
|
|
|
Normally only used internally by init scripts to manage the generation
|
|
of configuration files and any needed firewall rules. None of these
|
|
modify the snort rules in any way (see 'update-rules').
|
|
setup = generates snort config, sets up firewall.
|
|
teardown = removes any firewall rules.
|
|
resetup = shorthand for teardown and then setup.
|
|
|
|
|
|
$0 report [-v/--verbose] [-n/--n-lines N] [-d/--date-spec D] [-p/--pattern P]
|
|
|
|
Report on incidents. Note this is somewhat experimental, so suggested
|
|
improvements are quite welcome. Reported Source and Destination are of
|
|
the form "ip(port)", with zero and random source ports stripped.
|
|
-v = Show the rules that were triggered, after report table.
|
|
-n N = Show only the N highest frequency incidents.
|
|
-d D = Filter entries by date specification in D.
|
|
-p P = Grep pattern to filter incidents, applied to all outputs.
|
|
pattern = A case-insensitive grep pattern used to filter output.
|
|
|
|
The date specification for '-d' can be either literal 'today'
|
|
or a snort-formatted date prefixed by '-' or '+', meaning 'before'
|
|
and 'after', respectively. Snort date reporting has the format
|
|
'YY/MM/DD-hh:mm:ss.ssssss', and you can use any prefix as a date.
|
|
For example,
|
|
> snort-mgr --date-spec +23/12/20-09 report
|
|
will process all incidents from from 2023-12-20 at 0900 and later.
|
|
|
|
|
|
$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 [ -n "$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"
|