Compare commits

..

4 Commits

Author SHA1 Message Date
Josh Patterson 795aa898a3 suricata: only reload rules once the ruleset file exists
On a fresh install the surirulesync file.recurse creates .gitkeep before
SOC has generated all-rulesets.rules. That change satisfied the
surirulereload onchanges requisite, so the reload ran with no ruleset
present, failed to stat the file, and reported the state (and install)
as failed.

Add an onlyif guard so the reload only runs when all-rulesets.rules
exists. A .gitkeep-only sync now leaves the state a clean success
(onlyif condition false); once SOC writes the ruleset, the reload fires
normally.
2026-07-01 15:12:54 -04:00
Josh Patterson 69d77382f1 suricata: timestamp each line of reload log output
Route the reload/verify output (ours plus so-common's retry/fail lines)
through a synchronous timestamping pipeline so every line in reload.log
is prefixed with a date/time, and preserve the real exit code via
PIPESTATUS.
2026-07-01 15:12:53 -04:00
Josh Patterson ee36f5f84c suricata: verify reloaded ruleset is newer than the rules file
Treating an in-progress reload as instant success could report success
while Suricata was still running a stale ruleset (the in-flight reload
may have started before the new all-rulesets.rules was written).

Make success conditional on Suricata actually having loaded the current
ruleset: capture the rules-file mtime up front, trigger a blocking
reload-rules, then query ruleset-reload-time and only succeed when
last_reload >= mtime. An in-progress reload now retries (waits for it to
clear so our own fresh reload runs) instead of short-circuiting, and a
ruleset that never catches up within the retry window fails via fail().

Also drop the redundant ruleset-reload-nonblocking call (the verified
blocking reload is authoritative and the async call was what left a
reload running) and log human-readable timestamps.
2026-07-01 09:00:36 -04:00
Josh Patterson 52574e21c6 suricata: treat in-progress rule reload as success
so-suricata-reload-rules failed the surirulereload state when a rule
reload was already running: suricatasc returns
{"message":"Reload already in progress","return":"NOK"}, which never
matched the expected output, so retry looped all 60 attempts (~3 min)
and called fail.

Wrap the suricatasc calls so an in-progress reload is treated as
success (the in-flight reload picks up the new rules) while genuine
container-not-ready conditions still retry and ultimately fail.
2026-06-30 09:40:23 -04:00
10 changed files with 62 additions and 170 deletions
-24
View File
@@ -134,30 +134,6 @@ socsigmasopipeline:
- group: 939
- mode: 600
socsigmaplaybookpipeline:
file.managed:
- name: /opt/so/conf/soc/sigma_playbook_pipeline.yaml
- source: salt://soc/files/soc/sigma_playbook_pipeline.yaml
- user: 939
- group: 939
- mode: 600
socplaybookplaceholdermap:
file.managed:
- name: /opt/so/conf/soc/playbook_placeholder_map.yaml
- source: salt://soc/files/soc/playbook_placeholder_map.yaml
- user: 939
- group: 939
- mode: 600
socplaybookplaceholdermapcustom:
file.managed:
- name: /opt/so/conf/soc/playbook_placeholder_map_custom.yaml
- source: salt://soc/files/soc/playbook_placeholder_map_custom.yaml
- user: 939
- group: 939
- mode: 600
socbanner:
file.managed:
- name: /opt/so/conf/soc/banner.md
+2 -2
View File
@@ -1499,9 +1499,9 @@ soc:
playbookRepoPath: /opt/sensoroni/playbooks/
playbookRepos:
default:
- repo: https://github.com/defensivedepth/HCIP-Sigma
- repo: https://github.com/Security-Onion-Solutions/securityonion-resources-playbooks
branch: main
folder: playbooks
folder: securityonion-normalized
airgap:
- repo: file:///nsm/airgap-resources/playbooks/securityonion-resources-playbooks
branch: main
-5
View File
@@ -45,10 +45,7 @@ so-soc:
- /opt/so/conf/soc/motd.md:/opt/sensoroni/html/motd.md:ro
- /opt/so/conf/soc/banner.md:/opt/sensoroni/html/login/banner.md:ro
- /opt/so/conf/soc/sigma_so_pipeline.yaml:/opt/sensoroni/sigma_so_pipeline.yaml:ro
- /opt/so/conf/soc/sigma_playbook_pipeline.yaml:/opt/sensoroni/sigma_playbook_pipeline.yaml:ro
- /opt/so/conf/soc/sigma_final_pipeline.yaml:/opt/sensoroni/sigma_final_pipeline.yaml:rw
- /opt/so/conf/soc/playbook_placeholder_map.yaml:/opt/sensoroni/playbook_placeholder_map.yaml:ro
- /opt/so/conf/soc/playbook_placeholder_map_custom.yaml:/opt/sensoroni/playbook_placeholder_map_custom.yaml:rw
- /opt/so/conf/soc/custom.js:/opt/sensoroni/html/js/custom.js:ro
- /opt/so/conf/soc/custom_roles:/opt/sensoroni/rbac/custom_roles:ro
- /opt/so/conf/soc/soc_users_roles:/opt/sensoroni/rbac/users_roles:rw
@@ -102,8 +99,6 @@ so-soc:
- file: soccustomroles
- file: socusersroles
- file: socclientsroles
- file: socplaybookplaceholdermap
- file: socplaybookplaceholdermapcustom
delete_so-soc_so-status.disabled:
file.uncomment:
@@ -1,49 +0,0 @@
# Global Playbook placeholder map: %token% -> event field path.
#
# Loaded by the SOC Playbook module and used to resolve `field|expand:%placeholder%` values
# from an alert when converting playbook questions to OQL.
# Left: the %token% used in a question
# Right: the event field its value is read from (event_data.-nested or bare; the module
# tries both).
#
# Example: with `src_ip: source.ip` (below), a question that writes
# `source.ip|expand: '%src_ip%'` resolves %src_ip% to the alert's source.ip at convert time.
#
# This is the global base layer. To add or override tokens edit playbook_placeholder_map_custom.yaml.
# those entries overlay this map and win on conflict.
CommandLine: process.command_line
CurrentDirectory: process.working_directory
Image: process.executable
ImageLoaded: dll.name
ParentImage: process.parent.executable
ParentName: process.parent.name
ParentProcessGuid: process.parent.entity_id
ProcessGuid: process.entity_id
TargetFilename: file.name
TargetObject: registry.path
TargetUserName: user.target.name
User: user.name
community_id: network.community_id
dns_resolved_ip: dns.resolved_ip
document_id: soc_id
dst_ip: destination.ip
dst_port: destination.port
event_data_source_ip: source.ip
file_path: file.path
file_dirs: process.file_dirs
file_name: process.name
file_paths: process.file_paths
hostname: host.name
private_ip: network.private_ip
public_ip: network.public_ip
related_hosts: related.hosts
related_ip: related.ip
src_ip: source.ip
dns_query_name: dns.query_name
flow_id: log.id.uid
payload: network.data.decoded
rule_category: rule.category
rule_name: rule.name
rule_uuid: rule.uuid
src_port: source.port
@@ -1,14 +0,0 @@
# Custom Playbook placeholder map: %token% -> event field path.
#
#
# Left: the %token% used in a playbook question.
# Right: the event field its value is read from (event_data.-nested or bare; the module tries
# both). Note: a token that is simply named after a flat event field resolves automatically
# without an entry here - only add a mapping when the token name differs from the field name.
#
# Example:
#
# account_id: cloudflare.account_id
#
# A question that writes
# `account_id|expand: '%account_id%'` resolves %account_id% from the alert at convert time.
@@ -1,12 +0,0 @@
name: Security Onion - Playbook Pipeline
priority: 97
transformations:
# Route string fields to their lowercase-normalized .caseless subfield so wildcard
# matches are case-insensitive.
- id: case_insensitive_string_fields
type: field_name_mapping
mapping:
process.executable: process.executable.caseless
process.parent.executable: process.parent.executable.caseless
process.command_line: process.command_line.caseless
process.parent.command_line: process.parent.command_line.caseless
-51
View File
@@ -63,14 +63,6 @@ transformations:
rule_conditions:
- type: logsource
category: antivirus
# OS-agnostic process_creation scoping for product-less (NIDS/host-pivot) rules.
- id: process_creation_os_agnostic
type: add_condition
conditions:
event.category: process
rule_conditions:
- type: logsource
category: process_creation
# Transforms the `Hashes` field to ECS fields
# ECS fields are used by the hash fields emitted by Elastic Defend
# If shipped with Elastic Agent, sysmon logs will also have hashes mapped to ECS fields
@@ -116,40 +108,6 @@ transformations:
- type: logsource
product: windows
category: driver_load
- id: ecs_fix_process_creation
type: field_name_mapping
mapping:
# bare `Hashes` (the combined-string case is broken out above)
winlog.event_data.Hashes: process.hash.sha256
winlog.event_data.IntegrityLevel: process.Ext.token.integrity_level_name
winlog.event_data.ParentName: process.parent.name
rule_conditions:
- type: logsource
product: windows
category: process_creation
- id: ecs_fix_registry_set
type: field_name_mapping
mapping:
winlog.event_data.Details: registry.data.strings
# field rename only; EventType values (SetValue/CreateKey) still differ from
# event.action values (modification/creation)
winlog.event_data.EventType: event.action
rule_conditions:
- type: logsource
product: windows
category: registry_set
- id: ecs_fix_image_load
type: field_name_mapping
mapping:
file.path: dll.path
file.code_signature.signed: dll.code_signature.exists
winlog.event_data.Signature: dll.code_signature.subject_name
file.code_signature.status: dll.code_signature.status
winlog.event_data.Hashes: dll.hash.sha256
rule_conditions:
- type: logsource
product: windows
category: image_load
- id: linux_security_add-fields
type: add_condition
conditions:
@@ -323,15 +281,6 @@ transformations:
rule_conditions:
- type: logsource
category: file_event
# Scope image_load rules to Elastic Endpoint library events (event.category:library, dll.*
# populated).
- id: endpoint_image_load_add-fields
type: add_condition
conditions:
event.category: 'library'
rule_conditions:
- type: logsource
category: image_load
# Maps network rules to all network logs
# This targets all network logs, all services, generated from endpoints and network
- id: network_add-fields
+1 -9
View File
@@ -46,15 +46,7 @@ soc:
syntax: yaml
file: True
global: True
advanced: False
helpLink: security-onion-console-customization
playbook_placeholder_map_custom__yaml:
title: Playbook Placeholder Map
description: Custom mappings of Playbook %placeholder% tokens to event fields.
syntax: yaml
file: True
global: True
advanced: False
advanced: True
helpLink: security-onion-console-customization
config:
licenseKey:
+3 -2
View File
@@ -65,10 +65,11 @@ so-suricata:
- file: suriclassifications
surirulereload:
cmd.run:
cmd.run:
- name: /usr/sbin/so-suricata-reload-rules >> /opt/so/log/suricata/reload.log 2>&1
- onchanges:
- onchanges:
- file: surirulesync
- onlyif: test -f /opt/so/rules/suricata/all-rulesets.rules
- require:
- docker_container: so-suricata
@@ -7,5 +7,59 @@
. /usr/sbin/so-common
retry 60 3 'docker exec so-suricata /opt/suricata/bin/suricatasc -c reload-rules /var/run/suricata/suricata-command.socket' '{"message":"done","return":"OK"}' || fail "The Suricata container was not ready in time."
retry 60 3 'docker exec so-suricata /opt/suricata/bin/suricatasc -c ruleset-reload-nonblocking /var/run/suricata/suricata-command.socket' '{"message":"done","return":"OK"}' || fail "The Suricata container was not ready in time."
RULES_FILE="/opt/so/rules/suricata/all-rulesets.rules"
SOCKET="/var/run/suricata/suricata-command.socket"
SURICATASC="docker exec so-suricata /opt/suricata/bin/suricatasc"
# Format an epoch as a human-readable local timestamp for log messages.
fmt_time() { date -d "@$1" '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null; }
# Prefix each input line with the current timestamp.
timestamp_lines() { while IFS= read -r line; do printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S %Z')" "$line"; done; }
# Epoch of Suricata's last *completed* ruleset reload; non-zero return on failure.
suricata_reload_epoch() {
local out ts
out=$($SURICATASC -c ruleset-reload-time "$SOCKET" 2>/dev/null)
ts=$(echo "$out" | jq -r '.message[0].last_reload // empty' 2>/dev/null)
[ -n "$ts" ] || return 1
date -d "$ts" +%s 2>/dev/null
}
# Trigger a fresh reload and confirm Suricata is running a ruleset at least as new
# as the rules file. Returns 0 only when both hold, so retry keeps going until an
# in-progress reload clears and our own reload completes.
reload_and_verify() {
local out reload_epoch
out=$($SURICATASC -c reload-rules "$SOCKET")
echo "reload-rules: $out"
if [[ "$out" =~ "Reload already in progress" ]]; then
echo "A reload is already in progress; waiting for it to clear so a fresh reload can load the current ruleset."
return 1
fi
if [[ ! "$out" =~ '{"message":"done","return":"OK"}' ]]; then
echo "Suricata not ready or unexpected reload output; will retry."
return 1
fi
reload_epoch=$(suricata_reload_epoch) || { echo "Could not read ruleset-reload-time; will retry."; return 1; }
if [ "$reload_epoch" -ge "$target_mtime" ]; then
echo "Loaded ruleset is current: last reload ($(fmt_time "$reload_epoch")) is newer than rules file ($(fmt_time "$target_mtime"))."
return 0
fi
echo "Loaded ruleset is stale: last reload ($(fmt_time "$reload_epoch")) is older than rules file ($(fmt_time "$target_mtime")); retrying."
return 1
}
# Run the reload/verify, timestamping every line of output (ours and the
# retry/fail helpers') so reload.log shows when each step ran. The pipeline is
# synchronous, so the log is fully flushed and ordered before we exit; the
# script's real exit code is preserved via PIPESTATUS.
{
# Epoch mtime of the ruleset we need Suricata to have loaded. Captured once so
# a file update mid-reload does not move the goalpost.
target_mtime=$(stat -c %Y "$RULES_FILE") || fail "Could not stat the Suricata rules file: $RULES_FILE"
retry 60 3 'reload_and_verify' || fail "Suricata did not load the current ruleset in time."
} 2>&1 | timestamp_lines
exit "${PIPESTATUS[0]}"