From 9304513ce82d7afb85ca5b156ef35f3b338bbf47 Mon Sep 17 00:00:00 2001 From: DefensiveDepth Date: Thu, 4 Dec 2025 12:26:13 -0500 Subject: [PATCH] Add support for suricata rules load status --- salt/suricata/config.sls | 8 +++++++ salt/suricata/cron/so-suricata-rulestats | 30 ++++++++++++++++++++++++ salt/suricata/enabled.sls | 12 ++++++++++ salt/telegraf/defaults.yaml | 4 ++++ salt/telegraf/scripts/surirules.sh | 30 ++++++++++++++++++++++++ 5 files changed, 84 insertions(+) create mode 100644 salt/suricata/cron/so-suricata-rulestats create mode 100644 salt/telegraf/scripts/surirules.sh diff --git a/salt/suricata/config.sls b/salt/suricata/config.sls index 59ae376dc..2a4a051cf 100644 --- a/salt/suricata/config.sls +++ b/salt/suricata/config.sls @@ -178,6 +178,14 @@ so-suricata-eve-clean: - template: jinja - source: salt://suricata/cron/so-suricata-eve-clean +so-suricata-rulestats: + file.managed: + - name: /usr/sbin/so-suricata-rulestats + - user: root + - group: root + - mode: 755 + - source: salt://suricata/cron/so-suricata-rulestats + {% else %} {{sls}}_state_not_allowed: diff --git a/salt/suricata/cron/so-suricata-rulestats b/salt/suricata/cron/so-suricata-rulestats new file mode 100644 index 000000000..95b51c58a --- /dev/null +++ b/salt/suricata/cron/so-suricata-rulestats @@ -0,0 +1,30 @@ +#!/bin/bash +# +# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one +# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at +# https://securityonion.net/license; you may not use this file except in compliance with the +# Elastic License 2.0. + +# Query Suricata for ruleset stats and reload time, write to JSON file for Telegraf to consume + +OUTFILE="/opt/so/log/suricata/rulestats.json" +SURICATASC="docker exec so-suricata /opt/suricata/bin/suricatasc" +SOCKET="/var/run/suricata/suricata-command.socket" + +query() { + timeout 10 $SURICATASC -c "$1" "$SOCKET" 2>/dev/null +} + +STATS=$(query "ruleset-stats") +RELOAD=$(query "ruleset-reload-time") + +if echo "$STATS" | jq -e '.return == "OK"' > /dev/null 2>&1; then + LOADED=$(echo "$STATS" | jq -r '.message[0].rules_loaded') + FAILED=$(echo "$STATS" | jq -r '.message[0].rules_failed') + LAST_RELOAD=$(echo "$RELOAD" | jq -r '.message[0].last_reload') + + jq -n --argjson loaded "$LOADED" --argjson failed "$FAILED" --arg reload "$LAST_RELOAD" \ + '{rules_loaded: $loaded, rules_failed: $failed, last_reload: $reload, return: "OK"}' > "$OUTFILE" +else + echo '{"return":"FAIL"}' > "$OUTFILE" +fi diff --git a/salt/suricata/enabled.sls b/salt/suricata/enabled.sls index 1576a0629..ec521abb3 100644 --- a/salt/suricata/enabled.sls +++ b/salt/suricata/enabled.sls @@ -90,6 +90,18 @@ clean_suricata_eve_files: - month: '*' - dayweek: '*' +# Add rulestats cron - runs every minute to query Suricata for rule load status +suricata_rulestats: + cron.present: + - name: /usr/sbin/so-suricata-rulestats > /dev/null 2>&1 + - identifier: suricata_rulestats + - user: root + - minute: '*' + - hour: '*' + - daymonth: '*' + - month: '*' + - dayweek: '*' + {% else %} {{sls}}_state_not_allowed: diff --git a/salt/telegraf/defaults.yaml b/salt/telegraf/defaults.yaml index 79ad9008d..c0a67b0ca 100644 --- a/salt/telegraf/defaults.yaml +++ b/salt/telegraf/defaults.yaml @@ -21,6 +21,7 @@ telegraf: - sostatus.sh - stenoloss.sh - suriloss.sh + - surirules.sh - zeekcaptureloss.sh - zeekloss.sh standalone: @@ -36,6 +37,7 @@ telegraf: - sostatus.sh - stenoloss.sh - suriloss.sh + - surirules.sh - zeekcaptureloss.sh - zeekloss.sh - features.sh @@ -81,6 +83,7 @@ telegraf: - sostatus.sh - stenoloss.sh - suriloss.sh + - surirules.sh - zeekcaptureloss.sh - zeekloss.sh - features.sh @@ -95,6 +98,7 @@ telegraf: - sostatus.sh - stenoloss.sh - suriloss.sh + - surirules.sh - zeekcaptureloss.sh - zeekloss.sh idh: diff --git a/salt/telegraf/scripts/surirules.sh b/salt/telegraf/scripts/surirules.sh new file mode 100644 index 000000000..b38d5df26 --- /dev/null +++ b/salt/telegraf/scripts/surirules.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# +# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one +# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at +# https://securityonion.net/license; you may not use this file except in compliance with the +# Elastic License 2.0. + +# Read Suricata ruleset stats from JSON file written by so-suricata-rulestats cron job +# JSON format: {"rules_loaded":45879,"rules_failed":1,"last_reload":"2025-12-04T14:10:57+0000","return":"OK"} +# or on failure: {"return":"FAIL"} + +# if this script isn't already running +if [[ ! "`pidof -x $(basename $0) -o %PPID`" ]]; then + + STATSFILE="/var/log/suricata/rulestats.json" + + # Check file exists, is less than 90 seconds old, and has valid data + if [ -f "$STATSFILE" ] && [ $(($(date +%s) - $(stat -c %Y "$STATSFILE"))) -lt 90 ] && jq -e '.return == "OK" and .rules_loaded != null and .rules_failed != null' "$STATSFILE" > /dev/null 2>&1; then + LOADED=$(jq -r '.rules_loaded' "$STATSFILE") + FAILED=$(jq -r '.rules_failed' "$STATSFILE") + RELOAD_TIME=$(jq -r '.last_reload // ""' "$STATSFILE") + + echo "surirules loaded=${LOADED}i,failed=${FAILED}i,reload_time=\"${RELOAD_TIME}\",status=\"ok\"" + else + echo "surirules loaded=0i,failed=0i,reload_time=\"\",status=\"unknown\"" + fi + +fi + +exit 0