mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2026-06-28 05:08:12 +02:00
Compare commits
51 Commits
kernel
..
saltthangs
| Author | SHA1 | Date | |
|---|---|---|---|
| 33c24cd136 | |||
| 12f4447875 | |||
| 576c7bfedd | |||
| b3b7ecdded | |||
| 0af020b6c3 | |||
| da94788255 | |||
| 7952c274c4 | |||
| 435e2b4182 | |||
| d0edfd2131 | |||
| 13ebde61bd | |||
| fa2ae1b87f | |||
| 5bf9751adf | |||
| 3effdbc91e | |||
| 30312b93a6 | |||
| a9c03e39bb | |||
| 8836529496 | |||
| b09c3776b7 | |||
| dfdb1fbaeb | |||
| 4d34470b84 | |||
| 61aa963a2d | |||
| 81c8d54589 | |||
| 4f3b57f495 | |||
| 84228a819b | |||
| 81ebea0451 | |||
| d71e80cf66 | |||
| 33a116357d | |||
| 8c17ae0f66 | |||
| f54939b444 | |||
| d48a22e37e | |||
| 6393d08e86 | |||
| 730c828bec | |||
| b4e5171415 | |||
| 84decc1db6 | |||
| 7d4d6a0756 | |||
| 66c0a662fc | |||
| 778cc055ea | |||
| 932deab751 | |||
| 1281f0ee37 | |||
| f774334b6c | |||
| 7fcace34c4 | |||
| 9541024eb7 | |||
| 0d166ef732 | |||
| f7d2994f8b | |||
| 8f0757606d | |||
| 0a8f2e01a0 | |||
| 4546d7bc52 | |||
| 17849d8758 | |||
| d3d30a587c | |||
| 034711d148 | |||
| a0cf0489d6 | |||
| 613d31c8a6 |
@@ -3,6 +3,8 @@ base:
|
|||||||
- ca
|
- ca
|
||||||
- global.soc_global
|
- global.soc_global
|
||||||
- global.adv_global
|
- global.adv_global
|
||||||
|
- salt.soc_salt
|
||||||
|
- salt.adv_salt
|
||||||
- docker.soc_docker
|
- docker.soc_docker
|
||||||
- docker.adv_docker
|
- docker.adv_docker
|
||||||
- influxdb.token
|
- influxdb.token
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# Custom salt beacon that watches the SOC audit_settings table in postgres for
|
||||||
|
# new settings changes and emits a beacon event per new row. This replaces the
|
||||||
|
# inotify watch on /opt/so/saltstack/local/pillar -- instead of monitoring pillar
|
||||||
|
# files on disk, we monitor the securityonion.audit_settings table that SOC writes to.
|
||||||
|
#
|
||||||
|
# Detection is poll-based with a monotonic `id` watermark persisted to
|
||||||
|
# WATERMARK_FILE: each pass selects rows with id greater than the last id seen,
|
||||||
|
# which makes it self-healing (a missed poll simply catches up on the next one).
|
||||||
|
#
|
||||||
|
# Each emitted event carries setting_id and node_id; the push_pillar reactor maps
|
||||||
|
# setting_id -> app via pillar_push_map.yaml and writes a push intent, after which
|
||||||
|
# the existing so-push-drainer / orch.push_batch pipeline takes over unchanged.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
WATERMARK_FILE = '/opt/so/state/pillar_db_watch.id'
|
||||||
|
CONTAINER = 'so-postgres'
|
||||||
|
DATABASE = 'securityonion'
|
||||||
|
|
||||||
|
# Unaligned, tuples-only psql output with a field separator that cannot appear in
|
||||||
|
# an id/setting_id/node_id, so we can split each row reliably.
|
||||||
|
FIELD_SEP = '\x1f'
|
||||||
|
|
||||||
|
|
||||||
|
def __virtual__():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate(config):
|
||||||
|
return True, 'valid'
|
||||||
|
|
||||||
|
|
||||||
|
def _read_watermark():
|
||||||
|
# Returns the last processed id, or None if the watermark has not been seeded.
|
||||||
|
try:
|
||||||
|
with open(WATERMARK_FILE, 'r') as f:
|
||||||
|
return int((f.read() or '').strip())
|
||||||
|
except (IOError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _write_watermark(value):
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(WATERMARK_FILE), exist_ok=True)
|
||||||
|
tmp = WATERMARK_FILE + '.tmp'
|
||||||
|
with open(tmp, 'w') as f:
|
||||||
|
f.write(str(int(value)))
|
||||||
|
os.rename(tmp, WATERMARK_FILE)
|
||||||
|
except OSError:
|
||||||
|
log.exception('pillar_db beacon: failed to persist watermark to %s', WATERMARK_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def _query(sql):
|
||||||
|
# Run a query against securityonion inside the so-postgres container over the unix
|
||||||
|
# socket (trust auth, no password). Returns stdout on success, or None on any
|
||||||
|
# failure so the caller can no-op and retry on the next interval.
|
||||||
|
cmd = [
|
||||||
|
'docker', 'exec', CONTAINER,
|
||||||
|
'psql', '-U', 'postgres', '-d', DATABASE,
|
||||||
|
'-tA', '-F', FIELD_SEP, '-c', sql,
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
log.warning('pillar_db beacon: psql timed out')
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
log.exception('pillar_db beacon: failed to exec psql')
|
||||||
|
return None
|
||||||
|
if result.returncode != 0:
|
||||||
|
log.warning('pillar_db beacon: psql failed (rc=%s): %s',
|
||||||
|
result.returncode, (result.stderr or '').strip())
|
||||||
|
return None
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def beacon(config):
|
||||||
|
retval = []
|
||||||
|
|
||||||
|
watermark = _read_watermark()
|
||||||
|
|
||||||
|
# First run / missing watermark: seed to the current MAX(id) and emit nothing
|
||||||
|
# so we never replay the entire settings history into a fleetwide push.
|
||||||
|
if watermark is None:
|
||||||
|
seed = _query('SELECT COALESCE(MAX(id), 0) FROM audit_settings;')
|
||||||
|
if seed is None:
|
||||||
|
return retval # postgres not ready yet; retry next interval
|
||||||
|
try:
|
||||||
|
_write_watermark(int((seed or '0').strip() or 0))
|
||||||
|
except ValueError:
|
||||||
|
log.warning('pillar_db beacon: could not parse MAX(id) seed: %r', seed)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
rows = _query(
|
||||||
|
"SELECT id, setting_id, COALESCE(node_id, '') FROM audit_settings "
|
||||||
|
"WHERE id > %d ORDER BY id;" % watermark
|
||||||
|
)
|
||||||
|
if rows is None:
|
||||||
|
return retval
|
||||||
|
|
||||||
|
max_id = watermark
|
||||||
|
for line in rows.splitlines():
|
||||||
|
# Do NOT str.strip() the whole line: Python treats the \x1f field
|
||||||
|
# separator (and \x1c-\x1e) as whitespace, so stripping would eat an
|
||||||
|
# empty trailing node_id field and make the row look malformed.
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
parts = line.split(FIELD_SEP)
|
||||||
|
if len(parts) < 3:
|
||||||
|
log.warning('pillar_db beacon: skipping malformed row: %r', line)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
row_id = int(parts[0])
|
||||||
|
except ValueError:
|
||||||
|
log.warning('pillar_db beacon: skipping row with non-int id: %r', line)
|
||||||
|
continue
|
||||||
|
setting_id = parts[1]
|
||||||
|
node_id = parts[2]
|
||||||
|
retval.append({
|
||||||
|
'tag': 'audit_settings',
|
||||||
|
'id': row_id,
|
||||||
|
'setting_id': setting_id,
|
||||||
|
'node_id': node_id,
|
||||||
|
})
|
||||||
|
if row_id > max_id:
|
||||||
|
max_id = row_id
|
||||||
|
|
||||||
|
if max_id > watermark:
|
||||||
|
_write_watermark(max_id)
|
||||||
|
log.info('pillar_db beacon: emitted %d change(s), watermark %d -> %d',
|
||||||
|
len(retval), watermark, max_id)
|
||||||
|
|
||||||
|
return retval
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# Custom salt beacon that watches the suricata/strelka rule directories for changes
|
||||||
|
# and emits a beacon event per changed directory. This replaces the stock salt
|
||||||
|
# `inotify` beacon, which leaks a kernel inotify instance every time the minion
|
||||||
|
# rebuilds the beacon loader's __context__ (orphaning the old pyinotify.Notifier
|
||||||
|
# without closing it) until fs.inotify.max_user_instances is exhausted and the
|
||||||
|
# beacon dies with EMFILE. Polling holds zero inotify instances, so the leak is
|
||||||
|
# impossible, and it keeps firing during state runs (no blackout).
|
||||||
|
#
|
||||||
|
# Detection is poll-based with a per-directory fingerprint persisted to
|
||||||
|
# WATERMARK_DIR: each pass walks the directory and hashes every file's
|
||||||
|
# (relpath, st_mtime_ns, st_size), which catches content writes, additions,
|
||||||
|
# moves, and deletions. A change in the digest emits one event; an unchanged
|
||||||
|
# digest emits nothing. This makes it self-healing (a missed poll simply catches
|
||||||
|
# up on the next one).
|
||||||
|
#
|
||||||
|
# Each emitted event carries the watched directory path under the configured tag
|
||||||
|
# (e.g. salt/beacon/<minion>/rules_db/suricata); the push_suricata / push_strelka
|
||||||
|
# reactors write a push intent, after which the existing so-push-drainer /
|
||||||
|
# orch.push_batch pipeline takes over unchanged.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
WATERMARK_DIR = '/opt/so/state'
|
||||||
|
|
||||||
|
# Temp/editor files that should not trigger a push. Mirrors the exclude regexes
|
||||||
|
# the inotify beacon used. Matched against the full pathname.
|
||||||
|
EXCLUDES = [
|
||||||
|
re.compile(r'\.sw[a-z]$'),
|
||||||
|
re.compile(r'~$'),
|
||||||
|
re.compile(r'/4913$'),
|
||||||
|
re.compile(r'/\.#'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __virtual__():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate(config):
|
||||||
|
return True, 'valid'
|
||||||
|
|
||||||
|
|
||||||
|
def _paths_from_config(config):
|
||||||
|
# The beacon config arrives as a list of single-key dicts (salt beacon style).
|
||||||
|
# Merge it and return the {dir: tag} mapping under the 'paths' key.
|
||||||
|
merged = {}
|
||||||
|
if isinstance(config, list):
|
||||||
|
for item in config:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
merged.update(item)
|
||||||
|
elif isinstance(config, dict):
|
||||||
|
merged = config
|
||||||
|
paths = merged.get('paths', {})
|
||||||
|
return paths if isinstance(paths, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _excluded(pathname):
|
||||||
|
for pattern in EXCLUDES:
|
||||||
|
if pattern.search(pathname):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _fingerprint(directory):
|
||||||
|
# Stat-only walk; hash each file's (relpath, mtime_ns, size). Returns a hex
|
||||||
|
# digest, or the digest of an empty tree if the directory does not exist.
|
||||||
|
h = hashlib.sha1()
|
||||||
|
if os.path.isdir(directory):
|
||||||
|
entries = []
|
||||||
|
for root, _dirs, files in os.walk(directory):
|
||||||
|
for name in files:
|
||||||
|
full = os.path.join(root, name)
|
||||||
|
if _excluded(full):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
st = os.stat(full)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
rel = os.path.relpath(full, directory)
|
||||||
|
entries.append('%s\0%d\0%d' % (rel, st.st_mtime_ns, st.st_size))
|
||||||
|
for line in sorted(entries):
|
||||||
|
h.update(line.encode('utf-8', 'surrogateescape'))
|
||||||
|
h.update(b'\n')
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _watermark_file(tag):
|
||||||
|
return os.path.join(WATERMARK_DIR, 'rules_db_%s.hash' % tag)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_watermark(tag):
|
||||||
|
try:
|
||||||
|
with open(_watermark_file(tag), 'r') as f:
|
||||||
|
return (f.read() or '').strip() or None
|
||||||
|
except IOError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _write_watermark(tag, digest):
|
||||||
|
path = _watermark_file(tag)
|
||||||
|
try:
|
||||||
|
os.makedirs(WATERMARK_DIR, exist_ok=True)
|
||||||
|
tmp = path + '.tmp'
|
||||||
|
with open(tmp, 'w') as f:
|
||||||
|
f.write(digest)
|
||||||
|
os.rename(tmp, path)
|
||||||
|
except OSError:
|
||||||
|
log.exception('rules_db beacon: failed to persist watermark to %s', path)
|
||||||
|
|
||||||
|
|
||||||
|
def beacon(config):
|
||||||
|
retval = []
|
||||||
|
|
||||||
|
for directory, tag in _paths_from_config(config).items():
|
||||||
|
digest = _fingerprint(directory)
|
||||||
|
previous = _read_watermark(tag)
|
||||||
|
|
||||||
|
# First run / missing watermark: seed the digest and emit nothing so a
|
||||||
|
# fresh host does not fire a spurious fleetwide push.
|
||||||
|
if previous is None:
|
||||||
|
_write_watermark(tag, digest)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if digest != previous:
|
||||||
|
_write_watermark(tag, digest)
|
||||||
|
retval.append({'tag': tag, 'path': directory})
|
||||||
|
log.info('rules_db beacon: change detected in %s, emitting %s', directory, tag)
|
||||||
|
|
||||||
|
return retval
|
||||||
@@ -291,6 +291,20 @@ download_and_verify() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# check if container with name is running and optionally stop it
|
||||||
|
docker_check_running() {
|
||||||
|
# show running containers, only names
|
||||||
|
if docker ps --format '{{.Names}}' | grep -q "^so-${1}$"; then
|
||||||
|
if [[ "$2" == "--stop" ]]; then
|
||||||
|
docker stop "so-${1}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
elastic_license() {
|
elastic_license() {
|
||||||
|
|
||||||
read -r -d '' message <<- EOM
|
read -r -d '' message <<- EOM
|
||||||
|
|||||||
@@ -5,27 +5,41 @@
|
|||||||
# https://securityonion.net/license; you may not use this file except in compliance with the
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
# Elastic License 2.0.
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Usage: so-restart kibana | playbook
|
|
||||||
|
|
||||||
. /usr/sbin/so-common
|
. /usr/sbin/so-common
|
||||||
|
|
||||||
if [ $# -ge 1 ]; then
|
usage() {
|
||||||
|
echo "Usage: $0 <component> [args]"
|
||||||
|
echo ""
|
||||||
|
echo "Supported args:"
|
||||||
|
echo " --force | -f Force stop all Salt jobs before starting component."
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 kibana Restart Kibana"
|
||||||
|
echo " $0 kibana --force Force stop all Salt jobs before restarting Kibana"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
echo $banner
|
if [[ $# -lt 1 ]]; then
|
||||||
printf "Restarting $1...\n\nThis could take a while if another Salt job is running. \nRun this command with --force to stop all Salt jobs before proceeding.\n"
|
usage
|
||||||
echo $banner
|
|
||||||
|
|
||||||
if [ "$2" = "--force" ]; then
|
|
||||||
printf "\nForce-stopping all Salt jobs before proceeding\n\n"
|
|
||||||
salt-call saltutil.kill_all_jobs
|
|
||||||
fi
|
|
||||||
|
|
||||||
case $1 in
|
|
||||||
"elastic-fleet") docker stop so-elastic-fleet && docker rm so-elastic-fleet && salt-call state.apply elasticfleet queue=True;;
|
|
||||||
*) docker stop so-$1 ; docker rm so-$1 ; salt-call state.apply $1 queue=True;;
|
|
||||||
esac
|
|
||||||
else
|
|
||||||
echo -e "\nPlease provide an argument by running like so-restart $component, or by using the component-specific script.\nEx. so-restart logstash, or so-logstash-restart\n"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
#shellcheck disable=SC2154
|
||||||
|
echo "$banner"
|
||||||
|
printf "Restarting %s...\n\nThis could take a while if another Salt job is running. \nRun this command with --force to stop all Salt jobs before proceeding.\n" "$1"
|
||||||
|
echo "$banner"
|
||||||
|
if [[ "$2" = "--force" ]] || [[ "$2" = "-f" ]]; then
|
||||||
|
printf "\nForce-stopping all Salt jobs before proceeding\n\n"
|
||||||
|
salt-call saltutil.kill_all_jobs
|
||||||
|
fi
|
||||||
|
case $1 in
|
||||||
|
"elastic-fleet"|"elasticfleet")
|
||||||
|
docker_check_running "elastic-fleet" "--stop"
|
||||||
|
docker rm "so-elastic-fleet" 2> /dev/null
|
||||||
|
salt-call state.apply elasticfleet queue=True
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
docker_check_running "$1" "--stop"
|
||||||
|
docker rm "so-${1}" 2> /dev/null
|
||||||
|
salt-call state.apply "$1" queue=True
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|||||||
@@ -5,27 +5,54 @@
|
|||||||
# https://securityonion.net/license; you may not use this file except in compliance with the
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
# Elastic License 2.0.
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
|
||||||
# Usage: so-start all | kibana | playbook
|
|
||||||
|
|
||||||
. /usr/sbin/so-common
|
. /usr/sbin/so-common
|
||||||
|
|
||||||
if [ $# -ge 1 ]; then
|
usage() {
|
||||||
echo $banner
|
echo "Usage: $0 <component> [args]"
|
||||||
printf "Starting $1...\n\nThis could take a while if another Salt job is running. \nRun this command with --force to stop all Salt jobs before proceeding.\n"
|
echo ""
|
||||||
echo $banner
|
echo "Supported args:"
|
||||||
|
echo " --force | -f Force stop all Salt jobs before starting component."
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 kibana Start Kibana"
|
||||||
|
echo " $0 kibana --force Force stop all Salt jobs before starting Kibana"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
if [ "$2" = "--force" ]; then
|
if [[ $# -lt 1 ]]; then
|
||||||
printf "\nForce-stopping all Salt jobs before proceeding\n\n"
|
usage
|
||||||
salt-call saltutil.kill_all_jobs
|
|
||||||
fi
|
|
||||||
|
|
||||||
case $1 in
|
|
||||||
"all") salt-call state.highstate queue=True;;
|
|
||||||
"elastic-fleet") if docker ps | grep -q so-$1; then printf "\n$1 is already running!\n\n"; else docker rm so-$1 >/dev/null 2>&1 ; salt-call state.apply elasticfleet queue=True; fi ;;
|
|
||||||
*) if docker ps | grep -E -q '^so-$1$'; then printf "\n$1 is already running\n\n"; else docker rm so-$1 >/dev/null 2>&1 ; salt-call state.apply $1 queue=True; fi ;;
|
|
||||||
esac
|
|
||||||
else
|
|
||||||
echo -e "\nPlease provide an argument by running like so-start $component, or by using the component-specific script.\nEx. so-start logstash, or so-logstash-start\n"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
#shellcheck disable=SC2154
|
||||||
|
echo "$banner"
|
||||||
|
printf "Starting %s...\n\nThis could take a while if another Salt job is running. \nRun this command with --force to stop all Salt jobs before proceeding.\n" "$1"
|
||||||
|
echo "$banner"
|
||||||
|
if [[ "$2" = "--force" ]] || [[ "$2" == "-f" ]]; then
|
||||||
|
printf "\nForce-stopping all Salt jobs before proceeding\n\n"
|
||||||
|
salt-call saltutil.kill_all_jobs
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
"all")
|
||||||
|
salt-call state.highstate queue=True
|
||||||
|
;;
|
||||||
|
"elastic-fleet"|"elasticfleet")
|
||||||
|
if docker_check_running "elastic-fleet"; then
|
||||||
|
printf "\nso-%s is already running!\n\n" "elastic-fleet"
|
||||||
|
/usr/sbin/so-status
|
||||||
|
else
|
||||||
|
docker rm "so-elastic-fleet" 2> /dev/null
|
||||||
|
salt-call state.apply elasticfleet queue=True
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if docker_check_running "$1"; then
|
||||||
|
printf "\nso-%s is already running\n\n" "$1"
|
||||||
|
/usr/sbin/so-status
|
||||||
|
else
|
||||||
|
docker rm "so-${1}" 2> /dev/null
|
||||||
|
salt-call state.apply "$1" queue=True
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|||||||
@@ -5,21 +5,33 @@
|
|||||||
# https://securityonion.net/license; you may not use this file except in compliance with the
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
# Elastic License 2.0.
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
|
||||||
# Usage: so-stop kibana | playbook | thehive
|
|
||||||
|
|
||||||
. /usr/sbin/so-common
|
. /usr/sbin/so-common
|
||||||
|
|
||||||
if [ $# -ge 1 ]; then
|
usage() {
|
||||||
echo $banner
|
echo "Usage: $0 <component>"
|
||||||
printf "Stopping $1...\n"
|
echo ""
|
||||||
echo $banner
|
echo "Examples:"
|
||||||
|
echo " $0 kibana Stop Kibana"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
case $1 in
|
if [[ $# -lt 1 ]]; then
|
||||||
*) docker stop so-$1 ; docker rm so-$1 ;;
|
usage
|
||||||
esac
|
|
||||||
else
|
|
||||||
echo -e "\nPlease provide an argument by running like so-stop $component, or by using the component-specific script.\nEx. so-stop logstash, or so-logstash-stop\n"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
#shellcheck disable=SC2154
|
||||||
|
echo "$banner"
|
||||||
|
printf "Stopping %s...\n" "$1"
|
||||||
|
echo "$banner"
|
||||||
|
case $1 in
|
||||||
|
"elasticfleet"|"elastic-fleet")
|
||||||
|
docker_check_running "elastic-fleet" "--stop"
|
||||||
|
docker rm "so-elastic-fleet" 2> /dev/null
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
docker_check_running "$1" "--stop"
|
||||||
|
docker rm "so-${1}" 2> /dev/null
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ function status {
|
|||||||
function pcapinfo() {
|
function pcapinfo() {
|
||||||
PCAP=$1
|
PCAP=$1
|
||||||
ARGS=$2
|
ARGS=$2
|
||||||
docker run --rm -v "$PCAP:/input.pcap" --entrypoint capinfos {{ MANAGER }}:5000/{{ IMAGEREPO }}/so-pcaptools:{{ VERSION }} /input.pcap -ae $ARGS
|
docker run --rm -v "$PCAP:/input.pcap" --entrypoint capinfos {{ MANAGER }}:5000/{{ IMAGEREPO }}/so-pcaptools:{{ VERSION }} /input.pcap -ae $ARGS |\
|
||||||
|
sed 's/First packet/Earliest packet/g' | sed 's/Last packet/Latest packet/g'
|
||||||
}
|
}
|
||||||
|
|
||||||
function pcapfix() {
|
function pcapfix() {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
{% import_yaml 'salt/minion.defaults.yaml' as SALT_MINION_DEFAULTS -%}
|
|
||||||
|
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
@@ -7,7 +5,7 @@
|
|||||||
# https://securityonion.net/license; you may not use this file except in compliance with the
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
# Elastic License 2.0.
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
{% from 'salt/schedule.map.jinja' import SCHEDULEMERGED %}
|
||||||
|
|
||||||
# this script checks the time the file /opt/so/log/salt/state-apply-test was last modified and restarts the salt-minion service if it is outside a threshold date/time
|
# this script checks the time the file /opt/so/log/salt/state-apply-test was last modified and restarts the salt-minion service if it is outside a threshold date/time
|
||||||
# the file is modified via file.touch using a scheduled job healthcheck.salt-minion.state-apply-test that runs a state.apply.
|
# the file is modified via file.touch using a scheduled job healthcheck.salt-minion.state-apply-test that runs a state.apply.
|
||||||
@@ -25,7 +23,8 @@ SYSTEM_START_TIME=$(date -d "$(</proc/uptime awk '{print $1}') seconds ago" +%s)
|
|||||||
LAST_HIGHSTATE_END=$([ -e "/opt/so/log/salt/lasthighstate" ] && date -r /opt/so/log/salt/lasthighstate +%s || echo 0)
|
LAST_HIGHSTATE_END=$([ -e "/opt/so/log/salt/lasthighstate" ] && date -r /opt/so/log/salt/lasthighstate +%s || echo 0)
|
||||||
LAST_HEALTHCHECK_STATE_APPLY=$([ -e "/opt/so/log/salt/state-apply-test" ] && date -r /opt/so/log/salt/state-apply-test +%s || echo 0)
|
LAST_HEALTHCHECK_STATE_APPLY=$([ -e "/opt/so/log/salt/state-apply-test" ] && date -r /opt/so/log/salt/state-apply-test +%s || echo 0)
|
||||||
# SETTING THRESHOLD TO ANYTHING UNDER 600 seconds may cause a lot of salt-minion restarts since the job to touch the file occurs every 5-8 minutes by default
|
# SETTING THRESHOLD TO ANYTHING UNDER 600 seconds may cause a lot of salt-minion restarts since the job to touch the file occurs every 5-8 minutes by default
|
||||||
THRESHOLD={{SALT_MINION_DEFAULTS.salt.minion.check_threshold}} #within how many seconds the file /opt/so/log/salt/state-apply-test must have been touched/modified before the salt minion is restarted
|
# THRESHOLD is derived from the salt schedule highstate interval + 1 hour, so the minion-check grace period tracks the schedule automatically.
|
||||||
|
THRESHOLD=$(( ({{ SCHEDULEMERGED.highstate_interval_hours }} + 1) * 3600 )) #within how many seconds the file /opt/so/log/salt/state-apply-test must have been touched/modified before the salt minion is restarted
|
||||||
THRESHOLD_DATE=$((LAST_HEALTHCHECK_STATE_APPLY+THRESHOLD))
|
THRESHOLD_DATE=$((LAST_HEALTHCHECK_STATE_APPLY+THRESHOLD))
|
||||||
|
|
||||||
logCmd() {
|
logCmd() {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
prune_images:
|
prune_images:
|
||||||
cmd.run:
|
cmd.run:
|
||||||
- name: so-docker-prune
|
- name: so-docker-prune
|
||||||
- order: last
|
- onlyif: command -v /usr/sbin/so-docker-prune >/dev/null 2>&1
|
||||||
|
- order: 9000
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ wait_for_elasticsearch:
|
|||||||
so-elastalert:
|
so-elastalert:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastalert:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastalert:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- hostname: elastalert
|
- hostname: elastalert
|
||||||
- name: so-elastalert
|
- name: so-elastalert
|
||||||
- user: so-elastalert
|
- user: so-elastalert
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ include:
|
|||||||
so-elastic-fleet-package-registry:
|
so-elastic-fleet-package-registry:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-fleet-package-registry:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-fleet-package-registry:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- name: so-elastic-fleet-package-registry
|
- name: so-elastic-fleet-package-registry
|
||||||
- hostname: Fleet-package-reg-{{ GLOBALS.hostname }}
|
- hostname: Fleet-package-reg-{{ GLOBALS.hostname }}
|
||||||
- detach: True
|
- detach: True
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ include:
|
|||||||
so-elastic-agent:
|
so-elastic-agent:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- name: so-elastic-agent
|
- name: so-elastic-agent
|
||||||
- hostname: {{ GLOBALS.hostname }}
|
- hostname: {{ GLOBALS.hostname }}
|
||||||
- detach: True
|
- detach: True
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ eaoptionalintegrationsdir:
|
|||||||
|
|
||||||
{% for minion in node_data %}
|
{% for minion in node_data %}
|
||||||
{% set role = node_data[minion]["role"] %}
|
{% set role = node_data[minion]["role"] %}
|
||||||
{% if role in [ "eval","fleet","heavynode","import","manager", "managerhype", "managersearch","standalone" ] %}
|
{% if role in [ "eval","fleet","import","manager", "managerhype", "managersearch","standalone" ] %}
|
||||||
{% set optional_integrations = ELASTICFLEETMERGED.optional_integrations %}
|
{% set optional_integrations = ELASTICFLEETMERGED.optional_integrations %}
|
||||||
{% set integration_keys = optional_integrations.keys() %}
|
{% set integration_keys = optional_integrations.keys() %}
|
||||||
fleet_server_integrations_{{ minion }}:
|
fleet_server_integrations_{{ minion }}:
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ elasticagent_syncartifacts:
|
|||||||
so-elastic-fleet:
|
so-elastic-fleet:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- name: so-elastic-fleet
|
- name: so-elastic-fleet
|
||||||
- hostname: FleetServer-{{ GLOBALS.hostname }}
|
- hostname: FleetServer-{{ GLOBALS.hostname }}
|
||||||
- detach: True
|
- detach: True
|
||||||
|
|||||||
@@ -67,8 +67,6 @@ so-elastic-fleet-package-upgrade:
|
|||||||
interval: 30
|
interval: 30
|
||||||
- require:
|
- require:
|
||||||
- http: wait_for_so-kibana
|
- http: wait_for_so-kibana
|
||||||
- onchanges:
|
|
||||||
- file: /opt/so/state/elastic_fleet_packages.txt
|
|
||||||
|
|
||||||
so-elastic-fleet-integrations:
|
so-elastic-fleet-integrations:
|
||||||
cmd.run:
|
cmd.run:
|
||||||
|
|||||||
@@ -9,13 +9,11 @@
|
|||||||
RETURN_CODE=0
|
RETURN_CODE=0
|
||||||
|
|
||||||
if [ ! -f /opt/so/state/eaintegrations.txt ]; then
|
if [ ! -f /opt/so/state/eaintegrations.txt ]; then
|
||||||
# First, check for any package upgrades
|
|
||||||
/usr/sbin/so-elastic-fleet-package-upgrade
|
|
||||||
|
|
||||||
# Second, update Fleet Server policies
|
# update Fleet Server policies
|
||||||
/usr/sbin/so-elastic-fleet-integration-policy-elastic-fleet-server
|
/usr/sbin/so-elastic-fleet-integration-policy-elastic-fleet-server
|
||||||
|
|
||||||
# Third, configure Elastic Defend Integration seperately
|
# configure Elastic Defend Integration separately
|
||||||
/usr/sbin/so-elastic-fleet-integration-policy-elastic-defend
|
/usr/sbin/so-elastic-fleet-integration-policy-elastic-defend
|
||||||
|
|
||||||
# Each group fetches its agent policy once and dispatches create/update writes concurrently.
|
# Each group fetches its agent policy once and dispatches create/update writes concurrently.
|
||||||
@@ -32,9 +30,12 @@ if [ ! -f /opt/so/state/eaintegrations.txt ]; then
|
|||||||
elastic_fleet_load_integrations_dir "so-grid-nodes_heavy" \
|
elastic_fleet_load_integrations_dir "so-grid-nodes_heavy" \
|
||||||
/opt/so/conf/elastic-fleet/integrations/grid-nodes_heavy "Grid Nodes Policy_Heavy" || RETURN_CODE=1
|
/opt/so/conf/elastic-fleet/integrations/grid-nodes_heavy "Grid Nodes Policy_Heavy" || RETURN_CODE=1
|
||||||
|
|
||||||
# Fleet Server - Optional integrations (one agent policy per FleetServer_* directory)
|
# Fleet Server - Optional integrations (adds integration configuration to a given FleetServer_ policy)
|
||||||
for FLEET_DIR in /opt/so/conf/elastic-fleet/integrations-optional/FleetServer*/; do
|
for FLEET_DIR in /opt/so/conf/elastic-fleet/integrations-optional/FleetServer*/; do
|
||||||
[ -d "$FLEET_DIR" ] || continue
|
[ -d "$FLEET_DIR" ] || continue
|
||||||
|
INTEGRATIONS=("${FLEET_DIR%/}"/*.json)
|
||||||
|
[ -e "${INTEGRATIONS[0]}" ] || continue
|
||||||
|
|
||||||
FLEET_POLICY=$(basename "$FLEET_DIR")
|
FLEET_POLICY=$(basename "$FLEET_DIR")
|
||||||
elastic_fleet_load_integrations_dir "$FLEET_POLICY" \
|
elastic_fleet_load_integrations_dir "$FLEET_POLICY" \
|
||||||
"${FLEET_DIR%/}" "Fleet Server Policy" "elasticsearch-logs" || RETURN_CODE=1
|
"${FLEET_DIR%/}" "Fleet Server Policy" "elasticsearch-logs" || RETURN_CODE=1
|
||||||
|
|||||||
@@ -12,17 +12,22 @@ PKG_LOAD_FAILURES=0
|
|||||||
PKG_LOAD_FAILURES_NAMES=()
|
PKG_LOAD_FAILURES_NAMES=()
|
||||||
|
|
||||||
{%- for PACKAGE in SUPPORTED_PACKAGES %}
|
{%- for PACKAGE in SUPPORTED_PACKAGES %}
|
||||||
echo "Upgrading {{ PACKAGE }} package..."
|
if INSTALLED_VERSION=$(elastic_fleet_package_version_check "{{ PACKAGE }}") && LATEST_VERSION=$(elastic_fleet_package_latest_version_check "{{ PACKAGE }}"); then
|
||||||
if VERSION=$(elastic_fleet_package_latest_version_check "{{ PACKAGE }}"); then
|
|
||||||
if ! elastic_fleet_package_install "{{ PACKAGE }}" "$VERSION"; then
|
if [ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]; then
|
||||||
PKG_LOAD_FAILURES=$((PKG_LOAD_FAILURES + 1))
|
echo "{{ PACKAGE }} integration version $INSTALLED_VERSION is already at the reported latest version $LATEST_VERSION, skipping upgrade."
|
||||||
PKG_LOAD_FAILURES_NAMES+=("{{ PACKAGE }}")
|
else
|
||||||
|
echo "Upgrading {{ PACKAGE }} package to version $LATEST_VERSION..."
|
||||||
|
if ! elastic_fleet_package_install "{{ PACKAGE }}" "$LATEST_VERSION"; then
|
||||||
|
PKG_LOAD_FAILURES=$((PKG_LOAD_FAILURES + 1))
|
||||||
|
PKG_LOAD_FAILURES_NAMES+=("{{ PACKAGE }}")
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
echo "ERROR: Failed to get version information for integration {{ PACKAGE }}"
|
||||||
PKG_LOAD_FAILURES=$((PKG_LOAD_FAILURES + 1))
|
PKG_LOAD_FAILURES=$((PKG_LOAD_FAILURES + 1))
|
||||||
PKG_LOAD_FAILURES_NAMES+=("{{ PACKAGE }}")
|
PKG_LOAD_FAILURES_NAMES+=("{{ PACKAGE }}")
|
||||||
fi
|
fi
|
||||||
echo
|
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
if [ $PKG_LOAD_FAILURES -gt 0 ]; then
|
if [ $PKG_LOAD_FAILURES -gt 0 ]; then
|
||||||
@@ -35,6 +40,3 @@ if [ $PKG_LOAD_FAILURES -gt 0 ]; then
|
|||||||
else
|
else
|
||||||
echo "Successfully upgraded all packages."
|
echo "Successfully upgraded all packages."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
|
||||||
/usr/sbin/so-elasticsearch-templates-load
|
|
||||||
|
|||||||
@@ -181,6 +181,9 @@ if ! elastic_fleet_policy_create "so-grid-nodes_heavy" "SO Grid Nodes - Heavy No
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check for package upgrades
|
||||||
|
so-elastic-fleet-package-upgrade
|
||||||
|
|
||||||
# Load Integrations for default policies
|
# Load Integrations for default policies
|
||||||
so-elastic-fleet-integration-policy-load
|
so-elastic-fleet-integration-policy-load
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ include:
|
|||||||
so-elasticsearch:
|
so-elasticsearch:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elasticsearch:{{ ELASTICSEARCHMERGED.version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elasticsearch:{{ ELASTICSEARCHMERGED.version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- hostname: elasticsearch
|
- hostname: elasticsearch
|
||||||
- name: so-elasticsearch
|
- name: so-elasticsearch
|
||||||
- user: elasticsearch
|
- user: elasticsearch
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
{ "remove": { "field": ["host"], "ignore_failure": true } },
|
{ "remove": { "field": ["host"], "ignore_failure": true } },
|
||||||
{ "json": { "field": "message", "target_field": "message2", "ignore_failure": true } },
|
{ "json": { "field": "message", "target_field": "message2", "ignore_failure": true } },
|
||||||
{ "rename": { "field": "message2.version", "target_field": "ssl.version", "ignore_missing": true } },
|
{ "rename": { "field": "message2.version", "target_field": "ssl.version", "ignore_missing": true } },
|
||||||
|
{ "set": { "description": "Set transport for the community_id processor", "if": "ctx.ssl?.version == null || !ctx.ssl.version.startsWith('DTLS')", "field": "network.transport", "value": "tcp", "ignore_failure": true } },
|
||||||
{ "rename": { "field": "message2.cipher", "target_field": "ssl.cipher", "ignore_missing": true } },
|
{ "rename": { "field": "message2.cipher", "target_field": "ssl.cipher", "ignore_missing": true } },
|
||||||
{ "rename": { "field": "message2.curve", "target_field": "ssl.curve", "ignore_missing": true } },
|
{ "rename": { "field": "message2.curve", "target_field": "ssl.curve", "ignore_missing": true } },
|
||||||
{ "rename": { "field": "message2.server_name", "target_field": "ssl.server_name", "ignore_missing": true } },
|
{ "rename": { "field": "message2.server_name", "target_field": "ssl.server_name", "ignore_missing": true } },
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
global:
|
global:
|
||||||
pcapengine: SURICATA
|
pcapengine: SURICATA
|
||||||
pipeline: REDIS
|
pipeline: REDIS
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ so-hydra:
|
|||||||
- {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }}
|
- {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
# Intentionally unless-stopped -- matches the fleet default.
|
||||||
- restart_policy: unless-stopped
|
- restart_policy: unless-stopped
|
||||||
- watch:
|
- watch:
|
||||||
- file: hydraconfig
|
- file: hydraconfig
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ include:
|
|||||||
so-idh:
|
so-idh:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-idh:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-idh:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- name: so-idh
|
- name: so-idh
|
||||||
- detach: True
|
- detach: True
|
||||||
- network_mode: host
|
- network_mode: host
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ include:
|
|||||||
so-influxdb:
|
so-influxdb:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-influxdb:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-influxdb:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- hostname: influxdb
|
- hostname: influxdb
|
||||||
- networks:
|
- networks:
|
||||||
- sobridge:
|
- sobridge:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ include:
|
|||||||
so-kafka:
|
so-kafka:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-kafka:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-kafka:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- hostname: so-kafka
|
- hostname: so-kafka
|
||||||
- name: so-kafka
|
- name: so-kafka
|
||||||
- networks:
|
- networks:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ include:
|
|||||||
so-kibana:
|
so-kibana:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-kibana:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-kibana:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- hostname: kibana
|
- hostname: kibana
|
||||||
- user: "932:0"
|
- user: "932:0"
|
||||||
- networks:
|
- networks:
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ so-kratos:
|
|||||||
- {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }}
|
- {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
# Intentionally unless-stopped -- matches the fleet default.
|
||||||
- restart_policy: unless-stopped
|
- restart_policy: unless-stopped
|
||||||
- watch:
|
- watch:
|
||||||
- file: kratosschema
|
- file: kratosschema
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ include:
|
|||||||
so-logstash:
|
so-logstash:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-logstash:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-logstash:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- hostname: so-logstash
|
- hostname: so-logstash
|
||||||
- name: so-logstash
|
- name: so-logstash
|
||||||
- networks:
|
- networks:
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{% from 'vars/globals.map.jinja' import GLOBALS %}
|
||||||
|
{% from 'salt/auto_apply.map.jinja' import AUTOAPPLY %}
|
||||||
|
|
||||||
|
include:
|
||||||
|
- salt.minion
|
||||||
|
|
||||||
|
{% if GLOBALS.is_manager and AUTOAPPLY.enabled %}
|
||||||
|
salt_beacons_pushstate:
|
||||||
|
file.managed:
|
||||||
|
- name: /etc/salt/minion.d/beacons_pushstate.conf
|
||||||
|
- source: salt://manager/files/beacons_pushstate.conf.jinja
|
||||||
|
- template: jinja
|
||||||
|
- watch_in:
|
||||||
|
- service: salt_minion_service
|
||||||
|
{% else %}
|
||||||
|
salt_beacons_pushstate:
|
||||||
|
file.absent:
|
||||||
|
- name: /etc/salt/minion.d/beacons_pushstate.conf
|
||||||
|
- watch_in:
|
||||||
|
- service: salt_minion_service
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{% from 'salt/auto_apply.map.jinja' import AUTOAPPLY %}
|
||||||
|
beacons:
|
||||||
|
pillar_db:
|
||||||
|
- interval: {{ AUTOAPPLY.drain_interval }}
|
||||||
|
- disable_during_state_run: False
|
||||||
|
rules_db:
|
||||||
|
- interval: {{ AUTOAPPLY.drain_interval }}
|
||||||
|
- disable_during_state_run: False
|
||||||
|
- paths:
|
||||||
|
/opt/so/saltstack/local/salt/suricata/rules: suricata
|
||||||
|
/opt/so/saltstack/local/salt/strelka/rules/compiled: strelka
|
||||||
@@ -15,6 +15,7 @@ include:
|
|||||||
- manager.elasticsearch
|
- manager.elasticsearch
|
||||||
- manager.kibana
|
- manager.kibana
|
||||||
- manager.managed_soc_annotations
|
- manager.managed_soc_annotations
|
||||||
|
- manager.beacons
|
||||||
|
|
||||||
repo_log_dir:
|
repo_log_dir:
|
||||||
file.directory:
|
file.directory:
|
||||||
@@ -260,6 +261,7 @@ surifiltersrules:
|
|||||||
- user: 939
|
- user: 939
|
||||||
- group: 939
|
- group: 939
|
||||||
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
{{sls}}_state_not_allowed:
|
{{sls}}_state_not_allowed:
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
#!/opt/saltstack/salt/bin/python3
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
so-push-drainer
|
||||||
|
===============
|
||||||
|
|
||||||
|
Scheduled drainer for the active-push feature. Runs on the manager every
|
||||||
|
drain_interval seconds (default 15) via a salt schedule in salt/salt/push_drain_schedule.sls.
|
||||||
|
|
||||||
|
For each intent file under /opt/so/state/push_pending/*.json whose last_touch
|
||||||
|
is older than debounce_seconds, this script:
|
||||||
|
* concatenates the actions lists from every ready intent
|
||||||
|
* dedupes by (state or __highstate__, tgt, tgt_type)
|
||||||
|
* dispatches a single `salt-run state.orchestrate orch.push_batch --async`
|
||||||
|
with the deduped actions list passed as pillar kwargs
|
||||||
|
* deletes the contributed intent files on successful dispatch
|
||||||
|
|
||||||
|
Reactor sls files (push_suricata, push_strelka, push_pillar) write intents
|
||||||
|
but never dispatch directly -- see plan
|
||||||
|
/home/mreeves/.claude/plans/goofy-marinating-hummingbird.md for the full design.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import fcntl
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import salt.client
|
||||||
|
|
||||||
|
PENDING_DIR = '/opt/so/state/push_pending'
|
||||||
|
LOCK_FILE = os.path.join(PENDING_DIR, '.lock')
|
||||||
|
LOG_FILE = '/opt/so/log/salt/so-push-drainer.log'
|
||||||
|
|
||||||
|
HIGHSTATE_SENTINEL = '__highstate__'
|
||||||
|
|
||||||
|
|
||||||
|
def _make_logger():
|
||||||
|
logger = logging.getLogger('so-push-drainer')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
if not logger.handlers:
|
||||||
|
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
|
||||||
|
handler = logging.handlers.RotatingFileHandler(
|
||||||
|
LOG_FILE, maxBytes=5 * 1024 * 1024, backupCount=3,
|
||||||
|
)
|
||||||
|
handler.setFormatter(logging.Formatter(
|
||||||
|
'%(asctime)s | %(levelname)s | %(message)s',
|
||||||
|
))
|
||||||
|
logger.addHandler(handler)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
def _load_push_cfg():
|
||||||
|
"""Read the salt:auto_apply pillar subtree via salt-call. Returns a dict."""
|
||||||
|
caller = salt.client.Caller()
|
||||||
|
cfg = caller.cmd('pillar.get', 'salt:auto_apply', {})
|
||||||
|
return cfg if isinstance(cfg, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_intent(path, log):
|
||||||
|
try:
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (IOError, ValueError) as exc:
|
||||||
|
log.warning('cannot read intent %s: %s', path, exc)
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
log.exception('unexpected error reading %s', path)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_actions(actions):
|
||||||
|
seen = set()
|
||||||
|
deduped = []
|
||||||
|
for action in actions:
|
||||||
|
if not isinstance(action, dict):
|
||||||
|
continue
|
||||||
|
state_key = HIGHSTATE_SENTINEL if action.get('highstate') else action.get('state')
|
||||||
|
tgt = action.get('tgt')
|
||||||
|
tgt_type = action.get('tgt_type', 'compound')
|
||||||
|
if not state_key or not tgt:
|
||||||
|
continue
|
||||||
|
key = (state_key, tgt, tgt_type)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
deduped.append(action)
|
||||||
|
return deduped
|
||||||
|
|
||||||
|
|
||||||
|
def _dispatch(actions, log):
|
||||||
|
pillar_arg = json.dumps({'actions': actions})
|
||||||
|
cmd = [
|
||||||
|
'salt-run',
|
||||||
|
'state.orchestrate',
|
||||||
|
'orch.push_batch',
|
||||||
|
'pillar={}'.format(pillar_arg),
|
||||||
|
'--async',
|
||||||
|
]
|
||||||
|
log.info('dispatching: %s', ' '.join(cmd[:3]) + ' pillar=<{} actions>'.format(len(actions)))
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, check=True, capture_output=True, text=True, timeout=60,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
log.error('dispatch failed (rc=%s): stdout=%s stderr=%s',
|
||||||
|
exc.returncode, exc.stdout, exc.stderr)
|
||||||
|
return False
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
log.error('dispatch timed out after 60s')
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
log.exception('dispatch raised')
|
||||||
|
return False
|
||||||
|
log.info('dispatch accepted: %s', (result.stdout or '').strip())
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log = _make_logger()
|
||||||
|
|
||||||
|
if not os.path.isdir(PENDING_DIR):
|
||||||
|
# Nothing to do; reactors create the dir on first use.
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
push = _load_push_cfg()
|
||||||
|
except Exception:
|
||||||
|
log.exception('failed to read salt:auto_apply pillar; aborting drain pass')
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if not push.get('enabled', True):
|
||||||
|
log.debug('push disabled; exiting')
|
||||||
|
return 0
|
||||||
|
|
||||||
|
debounce_seconds = int(push.get('debounce_seconds', 30))
|
||||||
|
|
||||||
|
os.makedirs(PENDING_DIR, exist_ok=True)
|
||||||
|
lock_fd = os.open(LOCK_FILE, os.O_CREAT | os.O_RDWR, 0o644)
|
||||||
|
try:
|
||||||
|
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||||
|
|
||||||
|
intent_files = [
|
||||||
|
p for p in sorted(glob.glob(os.path.join(PENDING_DIR, '*.json')))
|
||||||
|
if os.path.basename(p) != '.lock'
|
||||||
|
]
|
||||||
|
if not intent_files:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
ready = []
|
||||||
|
skipped = 0
|
||||||
|
broken = []
|
||||||
|
for path in intent_files:
|
||||||
|
intent = _read_intent(path, log)
|
||||||
|
if not isinstance(intent, dict):
|
||||||
|
broken.append(path)
|
||||||
|
continue
|
||||||
|
last_touch = intent.get('last_touch', 0)
|
||||||
|
if now - last_touch < debounce_seconds:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
ready.append((path, intent))
|
||||||
|
|
||||||
|
for path in broken:
|
||||||
|
try:
|
||||||
|
os.unlink(path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not ready:
|
||||||
|
if skipped:
|
||||||
|
log.debug('no ready intents (%d still in debounce window)', skipped)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
combined_actions = []
|
||||||
|
oldest_first_touch = now
|
||||||
|
all_paths = []
|
||||||
|
for path, intent in ready:
|
||||||
|
combined_actions.extend(intent.get('actions', []) or [])
|
||||||
|
first = intent.get('first_touch', now)
|
||||||
|
if first < oldest_first_touch:
|
||||||
|
oldest_first_touch = first
|
||||||
|
all_paths.extend(intent.get('paths', []) or [])
|
||||||
|
|
||||||
|
deduped = _dedupe_actions(combined_actions)
|
||||||
|
if not deduped:
|
||||||
|
log.warning('%d intent(s) had no usable actions; clearing', len(ready))
|
||||||
|
for path, _ in ready:
|
||||||
|
try:
|
||||||
|
os.unlink(path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
debounce_duration = now - oldest_first_touch
|
||||||
|
log.info(
|
||||||
|
'draining %d intent(s): %d action(s) after dedupe (raw=%d), '
|
||||||
|
'debounce_duration=%.1fs, paths=%s',
|
||||||
|
len(ready), len(deduped), len(combined_actions),
|
||||||
|
debounce_duration, all_paths[:20],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not _dispatch(deduped, log):
|
||||||
|
log.warning('dispatch failed; leaving intent files in place for retry')
|
||||||
|
return 1
|
||||||
|
|
||||||
|
for path, _ in ready:
|
||||||
|
try:
|
||||||
|
os.unlink(path)
|
||||||
|
except OSError:
|
||||||
|
log.exception('failed to remove drained intent %s', path)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||||
|
finally:
|
||||||
|
os.close(lock_fd)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -690,6 +690,21 @@ ensure_postgres_local_pillar() {
|
|||||||
chown -R socore:socore "$dir"
|
chown -R socore:socore "$dir"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_salt_local_pillar() {
|
||||||
|
# The salt.auto_apply settings (moved from global.push) are a new SOC settings
|
||||||
|
# module, so the new pillar/top.sls references salt.soc_salt / salt.adv_salt
|
||||||
|
# unconditionally. Managers upgrading from before this change have no
|
||||||
|
# /opt/so/saltstack/local/pillar/salt/ (make_some_dirs only runs at install
|
||||||
|
# time), so the stubs must be created here before salt-master restarts against
|
||||||
|
# the new top.sls.
|
||||||
|
echo "Ensuring salt local pillar stubs exist."
|
||||||
|
local dir=/opt/so/saltstack/local/pillar/salt
|
||||||
|
mkdir -p "$dir"
|
||||||
|
[[ -f "$dir/soc_salt.sls" ]] || touch "$dir/soc_salt.sls"
|
||||||
|
[[ -f "$dir/adv_salt.sls" ]] || touch "$dir/adv_salt.sls"
|
||||||
|
chown -R socore:socore "$dir"
|
||||||
|
}
|
||||||
|
|
||||||
ensure_postgres_secret() {
|
ensure_postgres_secret() {
|
||||||
# On a fresh install, generate_passwords + secrets_pillar seed
|
# On a fresh install, generate_passwords + secrets_pillar seed
|
||||||
# secrets:postgres_pass in /opt/so/saltstack/local/pillar/secrets.sls. That
|
# secrets:postgres_pass in /opt/so/saltstack/local/pillar/secrets.sls. That
|
||||||
@@ -851,6 +866,8 @@ kibana_backport_streams_index_template() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
up_to_3.2.0() {
|
up_to_3.2.0() {
|
||||||
|
ensure_salt_local_pillar
|
||||||
|
|
||||||
fix_logstash_0013_lumberjack_pipeline_name
|
fix_logstash_0013_lumberjack_pipeline_name
|
||||||
|
|
||||||
pin_elasticsearch_data_retention_method
|
pin_elasticsearch_data_retention_method
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ make-rule-dir-nginx:
|
|||||||
so-nginx:
|
so-nginx:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-nginx:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-nginx:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- hostname: so-nginx
|
- hostname: so-nginx
|
||||||
- networks:
|
- networks:
|
||||||
- sobridge:
|
- sobridge:
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{% from 'salt/auto_apply.map.jinja' import AUTOAPPLY %}
|
||||||
|
{% set actions = salt['pillar.get']('actions', []) %}
|
||||||
|
{% set BATCH = AUTOAPPLY.batch %}
|
||||||
|
{% set BATCH_WAIT = AUTOAPPLY.batch_wait %}
|
||||||
|
|
||||||
|
{% for action in actions %}
|
||||||
|
{% if action.get('highstate') %}
|
||||||
|
apply_highstate_{{ loop.index }}:
|
||||||
|
salt.state:
|
||||||
|
- tgt: '{{ action.tgt }}'
|
||||||
|
- tgt_type: {{ action.get('tgt_type', 'compound') }}
|
||||||
|
- highstate: True
|
||||||
|
- batch: {{ action.get('batch', BATCH) }}
|
||||||
|
- batch_wait: {{ action.get('batch_wait', BATCH_WAIT) }}
|
||||||
|
- kwarg:
|
||||||
|
queue: 2
|
||||||
|
{% else %}
|
||||||
|
refresh_pillar_{{ loop.index }}:
|
||||||
|
salt.function:
|
||||||
|
- name: saltutil.refresh_pillar
|
||||||
|
- tgt: '{{ action.tgt }}'
|
||||||
|
- tgt_type: {{ action.get('tgt_type', 'compound') }}
|
||||||
|
|
||||||
|
apply_{{ action.state | replace('.', '_') }}_{{ loop.index }}:
|
||||||
|
salt.state:
|
||||||
|
- tgt: '{{ action.tgt }}'
|
||||||
|
- tgt_type: {{ action.get('tgt_type', 'compound') }}
|
||||||
|
- sls:
|
||||||
|
- {{ action.state }}
|
||||||
|
- batch: {{ action.get('batch', BATCH) }}
|
||||||
|
- batch_wait: {{ action.get('batch_wait', BATCH_WAIT) }}
|
||||||
|
- kwarg:
|
||||||
|
queue: 2
|
||||||
|
- require:
|
||||||
|
- salt: refresh_pillar_{{ loop.index }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
# One pillar directory can map to multiple (state, tgt) actions.
|
||||||
|
# tgt is a raw salt compound expression. tgt_type is always "compound".
|
||||||
|
# Per-action `batch` / `batch_wait` override the orch defaults (25% / 15s).
|
||||||
|
# An action with `highstate: True` triggers state.highstate instead of
|
||||||
|
# state.apply -- see salt/orch/push_batch.sls.
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# - `bpf` is a pillar-only dir (no state of its own) consumed by both
|
||||||
|
# zeek and suricata via macros, so a bpf pillar change re-applies both.
|
||||||
|
# - suricata/strelka/zeek/elasticsearch/redis/kafka/logstash etc. have
|
||||||
|
# their own pillar dirs AND their own state, so they map 1:1 (or 1:2
|
||||||
|
# in strelka's case, because of the split init.sls / manager.sls).
|
||||||
|
#
|
||||||
|
# Intentional omissions (these will log a "not in pillar_push_map.yaml"
|
||||||
|
# warning in push_pillar.sls and wait for the next scheduled highstate):
|
||||||
|
# - `data` and `node_data`: pillar-only data consumed by many states;
|
||||||
|
# handling them generically would amount to a fleetwide highstate.
|
||||||
|
# - `host`: soc_host describes mainint/mainip; a change is a re-IP and
|
||||||
|
# needs a coordinated procedure, not an immediate state push.
|
||||||
|
# - `hypervisor`: state changes touch libvirt and are disruptive; leave
|
||||||
|
# to the next scheduled highstate.
|
||||||
|
# - `sensor`: every field in soc_sensor.yaml is `readonly: True` or
|
||||||
|
# per-minion (`node: True`). Per-minion edits are persisted under
|
||||||
|
# pillar/minions/<id>.sls and are handled by Branch A of push_pillar.sls
|
||||||
|
# (per-minion highstate intent), not by this app-pillar map.
|
||||||
|
#
|
||||||
|
# The role sets here were verified line-by-line against salt/top.sls. If
|
||||||
|
# salt/top.sls changes how an app is targeted, update the corresponding
|
||||||
|
# compound here.
|
||||||
|
|
||||||
|
# firewall: the one pillar everyone touches. Applied everywhere intentionally
|
||||||
|
# because every host's iptables needs to know about every other host in the
|
||||||
|
# grid. Salt's firewall state is idempotent (file.managed + iptables-restore
|
||||||
|
# onchanges in salt/firewall/init.sls), so hosts whose rendered firewall is
|
||||||
|
# unchanged do a file comparison and no-op without touching iptables -- actual
|
||||||
|
# reload happens only on the hosts whose rules actually changed. Fleetwide
|
||||||
|
# blast radius is intentional and matches the pre-plan behavior via highstate.
|
||||||
|
# Adding N sensors in a burst coalesces into one dispatch via the drainer.
|
||||||
|
firewall:
|
||||||
|
- state: firewall
|
||||||
|
tgt: '*'
|
||||||
|
|
||||||
|
# backup: backup.config_backup runs on eval, standalone, manager, managerhype,
|
||||||
|
# managersearch (NOT import -- the backup pillar is included on import per
|
||||||
|
# pillar/top.sls but the backup state is not run there per salt/top.sls).
|
||||||
|
backup:
|
||||||
|
- state: backup.config_backup
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# bpf is pillar-only (no state); consumed by both zeek and suricata as macros.
|
||||||
|
# Both states run on sensor_roles + so-import per salt/top.sls.
|
||||||
|
bpf:
|
||||||
|
- state: zeek
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-heavynode or G@role:so-import or G@role:so-sensor or G@role:so-standalone'
|
||||||
|
- state: suricata
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-heavynode or G@role:so-import or G@role:so-sensor or G@role:so-standalone'
|
||||||
|
|
||||||
|
# ca is applied universally.
|
||||||
|
ca:
|
||||||
|
- state: ca
|
||||||
|
tgt: '*'
|
||||||
|
|
||||||
|
# docker: universal. The docker state is in both the all-non-managers and
|
||||||
|
# all-managers branches of salt/top.sls.
|
||||||
|
docker:
|
||||||
|
- state: docker
|
||||||
|
tgt: '*'
|
||||||
|
|
||||||
|
# elastalert: eval, standalone, manager, managerhype, managersearch (NOT import).
|
||||||
|
elastalert:
|
||||||
|
- state: elastalert
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# elastic-fleet-package-registry: manager_roles exactly.
|
||||||
|
elastic-fleet-package-registry:
|
||||||
|
- state: elastic-fleet-package-registry
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# elasticsearch: 8 roles.
|
||||||
|
elasticsearch:
|
||||||
|
- state: elasticsearch
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-heavynode or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-searchnode or G@role:so-standalone'
|
||||||
|
|
||||||
|
# elasticagent: so-heavynode only.
|
||||||
|
elasticagent:
|
||||||
|
- state: elasticagent
|
||||||
|
tgt: 'G@role:so-heavynode'
|
||||||
|
|
||||||
|
# elasticfleet: base state only on pillar change. elasticfleet.install_agent_grid
|
||||||
|
# is a deploy/enrollment step, not a config reload; leave it to the next highstate.
|
||||||
|
elasticfleet:
|
||||||
|
- state: elasticfleet
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-fleet or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# global: fanout to a fleetwide highstate. The global pillar (soc_global.sls)
|
||||||
|
# carries cross-cutting settings (pipeline, url_base, imagerepo, mdengine, ...)
|
||||||
|
# that are consumed by virtually every state, so a targeted re-apply isn't
|
||||||
|
# meaningful. The drainer's batch/batch_wait throttling controls blast radius.
|
||||||
|
global:
|
||||||
|
- highstate: True
|
||||||
|
tgt: '*'
|
||||||
|
|
||||||
|
# healthcheck: eval, sensor, standalone only.
|
||||||
|
healthcheck:
|
||||||
|
- state: healthcheck
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-sensor or G@role:so-standalone'
|
||||||
|
|
||||||
|
# hydra: manager_roles exactly.
|
||||||
|
hydra:
|
||||||
|
- state: hydra
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# idh: so-idh only.
|
||||||
|
idh:
|
||||||
|
- state: idh
|
||||||
|
tgt: 'G@role:so-idh'
|
||||||
|
|
||||||
|
# influxdb: manager_roles exactly.
|
||||||
|
influxdb:
|
||||||
|
- state: influxdb
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# kafka: standalone, manager, managerhype, managersearch, searchnode, receiver.
|
||||||
|
kafka:
|
||||||
|
- state: kafka
|
||||||
|
tgt: 'G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-receiver or G@role:so-searchnode or G@role:so-standalone'
|
||||||
|
|
||||||
|
# kibana: manager_roles exactly.
|
||||||
|
kibana:
|
||||||
|
- state: kibana
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# kratos: manager_roles exactly.
|
||||||
|
kratos:
|
||||||
|
- state: kratos
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# logrotate: universal (top-of-file '*' branch in salt/top.sls).
|
||||||
|
logrotate:
|
||||||
|
- state: logrotate
|
||||||
|
tgt: '*'
|
||||||
|
|
||||||
|
# logstash: 8 roles, no eval/import.
|
||||||
|
logstash:
|
||||||
|
- state: logstash
|
||||||
|
tgt: 'G@role:so-fleet or G@role:so-heavynode or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-receiver or G@role:so-searchnode or G@role:so-standalone'
|
||||||
|
|
||||||
|
# manager: manager_roles exactly. The manager state is also referenced under
|
||||||
|
# *_sensor / *_heavynode top.sls blocks via `sensor`, but the standalone
|
||||||
|
# `manager` state itself runs only on manager_roles.
|
||||||
|
manager:
|
||||||
|
- state: manager
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# nginx: 10 specific roles. NOT receiver, idh, hypervisor, desktop.
|
||||||
|
nginx:
|
||||||
|
- state: nginx
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-fleet or G@role:so-heavynode or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-searchnode or G@role:so-sensor or G@role:so-standalone'
|
||||||
|
|
||||||
|
# ntp: universal (top-of-file '*' branch in salt/top.sls).
|
||||||
|
ntp:
|
||||||
|
- state: ntp
|
||||||
|
tgt: '*'
|
||||||
|
|
||||||
|
# patch: universal. soc_patch carries the OS update schedule, applied via
|
||||||
|
# patch.os.schedule on every node (it's in both the all-non-managers and
|
||||||
|
# all-managers branches of salt/top.sls).
|
||||||
|
patch:
|
||||||
|
- state: patch.os.schedule
|
||||||
|
tgt: '*'
|
||||||
|
|
||||||
|
# postgres: manager_roles exactly.
|
||||||
|
postgres:
|
||||||
|
- state: postgres
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# redis: 6 roles. standalone, manager, managerhype, managersearch, heavynode, receiver.
|
||||||
|
# (NOT eval, NOT import, NOT searchnode.)
|
||||||
|
redis:
|
||||||
|
- state: redis
|
||||||
|
tgt: 'G@role:so-heavynode or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-receiver or G@role:so-standalone'
|
||||||
|
|
||||||
|
# registry: manager_roles exactly.
|
||||||
|
registry:
|
||||||
|
- state: registry
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# salt: fanout to a fleetwide highstate. The salt.auto_apply settings tune the
|
||||||
|
# push pipeline itself (enabled, debounce/drain intervals, batch sizing) and
|
||||||
|
# salt.schedule sets the per-minion highstate interval; they are consumed by the
|
||||||
|
# manager's schedule, beacons, and master reactor config as well as every
|
||||||
|
# minion's highstate schedule, so a targeted re-apply isn't meaningful. A salt
|
||||||
|
# audit row only fires for SOC-driven salt.auto_apply / salt.schedule edits --
|
||||||
|
# salt version bumps go through soup, not SOC, so they never reach this map.
|
||||||
|
salt:
|
||||||
|
- highstate: True
|
||||||
|
tgt: '*'
|
||||||
|
|
||||||
|
# sensoroni: universal.
|
||||||
|
sensoroni:
|
||||||
|
- state: sensoroni
|
||||||
|
tgt: '*'
|
||||||
|
|
||||||
|
# soc: manager_roles exactly.
|
||||||
|
soc:
|
||||||
|
- state: soc
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
|
||||||
|
|
||||||
|
# stig: broad. Runs on standalone, manager, managerhype, managersearch,
|
||||||
|
# searchnode, sensor, receiver, fleet, hypervisor, desktop.
|
||||||
|
# NOT eval, NOT import, NOT heavynode, NOT idh (the *_idh block in
|
||||||
|
# salt/top.sls intentionally omits stig).
|
||||||
|
stig:
|
||||||
|
- state: stig
|
||||||
|
tgt: 'G@role:so-desktop or G@role:so-fleet or G@role:so-hypervisor or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-receiver or G@role:so-searchnode or G@role:so-sensor or G@role:so-standalone'
|
||||||
|
|
||||||
|
# strelka: sensor-side only on pillar change (sensor_roles). strelka.manager is
|
||||||
|
# intentionally NOT fired on pillar changes -- YARA rule and strelka config
|
||||||
|
# pillar changes are consumed by the sensor-side strelka backend, and re-running
|
||||||
|
# strelka.manager on managers is both unnecessary and disruptive. strelka.manager
|
||||||
|
# is left to the 2-hour highstate.
|
||||||
|
strelka:
|
||||||
|
- state: strelka
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-heavynode or G@role:so-sensor or G@role:so-standalone'
|
||||||
|
|
||||||
|
# suricata: sensor_roles + so-import (5 roles).
|
||||||
|
suricata:
|
||||||
|
- state: suricata
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-heavynode or G@role:so-import or G@role:so-sensor or G@role:so-standalone'
|
||||||
|
|
||||||
|
# telegraf: universal.
|
||||||
|
telegraf:
|
||||||
|
- state: telegraf
|
||||||
|
tgt: '*'
|
||||||
|
|
||||||
|
# versionlock: universal (top-of-file '*' branch in salt/top.sls).
|
||||||
|
versionlock:
|
||||||
|
- state: versionlock
|
||||||
|
tgt: '*'
|
||||||
|
|
||||||
|
# vm: libvirt-driver hypervisors only. Matched by the salt-cloud:driver:libvirt
|
||||||
|
# grain (compound supports nested grain matching via G@<key>:<subkey>:<value>).
|
||||||
|
# pillar/vm/soc_vm.sls write path is referenced at salt/_runners/setup_hypervisor.py:856.
|
||||||
|
vm:
|
||||||
|
- state: vm
|
||||||
|
tgt: 'G@salt-cloud:driver:libvirt'
|
||||||
|
|
||||||
|
# zeek: sensor_roles + so-import (5 roles).
|
||||||
|
zeek:
|
||||||
|
- state: zeek
|
||||||
|
tgt: 'G@role:so-eval or G@role:so-heavynode or G@role:so-import or G@role:so-sensor or G@role:so-standalone'
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
#!py
|
||||||
|
|
||||||
|
# Reactor invoked by the pillar_db beacon when SOC records settings changes in
|
||||||
|
# the securityonion.audit_settings table (see salt/_beacons/pillar_db.py). The beacon
|
||||||
|
# emits one event per new row carrying setting_id and node_id.
|
||||||
|
#
|
||||||
|
# Two branches, keyed on node_id:
|
||||||
|
# A) node_id populated -> the change is scoped to that one minion. Look up the
|
||||||
|
# app in pillar_push_map.yaml and write an intent that runs the app's mapped
|
||||||
|
# state(s) targeted to just that node.
|
||||||
|
# B) node_id empty -> grid-wide app change. Look up the app in
|
||||||
|
# pillar_push_map.yaml and write an intent with the entry's actions as-is.
|
||||||
|
#
|
||||||
|
# The app name is the first dotted segment of setting_id (e.g. "telegraf.output"
|
||||||
|
# -> "telegraf"), which matches the pillar_push_map.yaml keys 1:1.
|
||||||
|
#
|
||||||
|
# Reactors never dispatch directly. The so-push-drainer schedule picks up
|
||||||
|
# ready intents, dedupes across pending files, and dispatches orch.push_batch.
|
||||||
|
|
||||||
|
import fcntl
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from salt.client import Caller
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PENDING_DIR = '/opt/so/state/push_pending'
|
||||||
|
LOCK_FILE = os.path.join(PENDING_DIR, '.lock')
|
||||||
|
MAX_PATHS = 20
|
||||||
|
|
||||||
|
# The pillar_push_map.yaml is shipped via salt:// but the reactor runs on the
|
||||||
|
# master, which mounts the default saltstack tree at this path.
|
||||||
|
PUSH_MAP_PATH = '/opt/so/saltstack/default/salt/reactor/pillar_push_map.yaml'
|
||||||
|
|
||||||
|
_PUSH_MAP_CACHE = {'mtime': 0, 'data': None}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_push_map():
|
||||||
|
try:
|
||||||
|
st = os.stat(PUSH_MAP_PATH)
|
||||||
|
except OSError:
|
||||||
|
LOG.warning('push_pillar: %s not found', PUSH_MAP_PATH)
|
||||||
|
return {}
|
||||||
|
if _PUSH_MAP_CACHE['mtime'] != st.st_mtime:
|
||||||
|
try:
|
||||||
|
with open(PUSH_MAP_PATH, 'r') as f:
|
||||||
|
_PUSH_MAP_CACHE['data'] = yaml.safe_load(f) or {}
|
||||||
|
except Exception:
|
||||||
|
LOG.exception('push_pillar: failed to load %s', PUSH_MAP_PATH)
|
||||||
|
_PUSH_MAP_CACHE['data'] = {}
|
||||||
|
_PUSH_MAP_CACHE['mtime'] = st.st_mtime
|
||||||
|
return _PUSH_MAP_CACHE['data'] or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _push_enabled():
|
||||||
|
try:
|
||||||
|
caller = Caller()
|
||||||
|
return bool(caller.cmd('pillar.get', 'salt:auto_apply:enabled', True))
|
||||||
|
except Exception:
|
||||||
|
LOG.exception('push_pillar: pillar.get salt:auto_apply:enabled failed, assuming enabled')
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _write_intent(key, actions, path):
|
||||||
|
now = time.time()
|
||||||
|
try:
|
||||||
|
os.makedirs(PENDING_DIR, exist_ok=True)
|
||||||
|
except OSError:
|
||||||
|
LOG.exception('push_pillar: cannot create %s', PENDING_DIR)
|
||||||
|
return
|
||||||
|
|
||||||
|
intent_path = os.path.join(PENDING_DIR, '{}.json'.format(key))
|
||||||
|
lock_fd = os.open(LOCK_FILE, os.O_CREAT | os.O_RDWR, 0o644)
|
||||||
|
try:
|
||||||
|
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||||
|
|
||||||
|
intent = {}
|
||||||
|
if os.path.exists(intent_path):
|
||||||
|
try:
|
||||||
|
with open(intent_path, 'r') as f:
|
||||||
|
intent = json.load(f)
|
||||||
|
except (IOError, ValueError):
|
||||||
|
intent = {}
|
||||||
|
|
||||||
|
intent.setdefault('first_touch', now)
|
||||||
|
intent['last_touch'] = now
|
||||||
|
intent['actions'] = actions
|
||||||
|
paths = intent.get('paths', [])
|
||||||
|
if path and path not in paths:
|
||||||
|
paths.append(path)
|
||||||
|
paths = paths[-MAX_PATHS:]
|
||||||
|
intent['paths'] = paths
|
||||||
|
|
||||||
|
tmp_path = intent_path + '.tmp'
|
||||||
|
with open(tmp_path, 'w') as f:
|
||||||
|
json.dump(intent, f)
|
||||||
|
os.rename(tmp_path, intent_path)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception('push_pillar: failed to write intent %s', intent_path)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||||
|
finally:
|
||||||
|
os.close(lock_fd)
|
||||||
|
|
||||||
|
|
||||||
|
def _app_from_setting(setting_id):
|
||||||
|
# setting_id is e.g. 'telegraf.output' -> 'telegraf', 'ntp.config.servers' -> 'ntp'
|
||||||
|
if not setting_id:
|
||||||
|
return None
|
||||||
|
return setting_id.split('.', 1)[0] or None
|
||||||
|
|
||||||
|
|
||||||
|
def _node_actions(entry, node_id):
|
||||||
|
# Copy the app's mapped actions but retarget each one to the single node.
|
||||||
|
# Preserves the state/highstate selection and any batch/batch_wait overrides.
|
||||||
|
actions = []
|
||||||
|
for action in entry:
|
||||||
|
if not isinstance(action, dict):
|
||||||
|
continue
|
||||||
|
node_action = dict(action)
|
||||||
|
node_action['tgt'] = node_id
|
||||||
|
node_action['tgt_type'] = 'glob'
|
||||||
|
actions.append(node_action)
|
||||||
|
return actions
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
if not _push_enabled():
|
||||||
|
LOG.info('push_pillar: push disabled, skipping')
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# The pillar_db beacon nests its payload under data['data']; fall back to the
|
||||||
|
# top level so the reactor is robust to either shape.
|
||||||
|
event = data.get('data', data) # noqa: F821 -- data provided by reactor
|
||||||
|
setting_id = event.get('setting_id', '')
|
||||||
|
node_id = (event.get('node_id') or '').strip()
|
||||||
|
|
||||||
|
app = _app_from_setting(setting_id)
|
||||||
|
if not app:
|
||||||
|
LOG.debug('push_pillar: ignoring event with no app segment: setting_id=%s', setting_id)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
push_map = _load_push_map()
|
||||||
|
entry = push_map.get(app)
|
||||||
|
if not entry:
|
||||||
|
LOG.warning(
|
||||||
|
'push_pillar: app "%s" is not in pillar_push_map.yaml; change will be '
|
||||||
|
'picked up at the next scheduled highstate (setting_id=%s)',
|
||||||
|
app, setting_id,
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Branch A: per-node change -> retarget the app's states to just that node.
|
||||||
|
if node_id:
|
||||||
|
actions = _node_actions(entry, node_id)
|
||||||
|
if not actions:
|
||||||
|
LOG.warning('push_pillar: no usable actions for app "%s" (setting_id=%s)', app, setting_id)
|
||||||
|
return {}
|
||||||
|
_write_intent(
|
||||||
|
'node_{}_{}'.format(node_id, app), actions,
|
||||||
|
'audit:{}@{}'.format(setting_id, node_id),
|
||||||
|
)
|
||||||
|
LOG.info('push_pillar: per-node intent updated for %s on %s (setting_id=%s)',
|
||||||
|
app, node_id, setting_id)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Branch B: grid-wide app change -> use the map entry's actions as-is.
|
||||||
|
actions = list(entry) # copy to avoid mutating the cache
|
||||||
|
_write_intent('pillar_{}'.format(app), actions, 'audit:{}'.format(setting_id))
|
||||||
|
LOG.info('push_pillar: app intent updated for %s (setting_id=%s)', app, setting_id)
|
||||||
|
return {}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#!py
|
||||||
|
|
||||||
|
# Reactor invoked by the rules_db poll beacon (salt/_beacons/rules_db.py) on rule
|
||||||
|
# file changes under /opt/so/saltstack/local/salt/strelka/rules/compiled/.
|
||||||
|
#
|
||||||
|
# Writes (or updates) a push intent at /opt/so/state/push_pending/rules_strelka.json
|
||||||
|
# and returns {}. The so-push-drainer schedule picks up ready intents, dedupes
|
||||||
|
# across pending files, and dispatches orch.push_batch. Reactors never dispatch
|
||||||
|
# directly -- see plan /home/mreeves/.claude/plans/goofy-marinating-hummingbird.md.
|
||||||
|
|
||||||
|
import fcntl
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from salt.client import Caller
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PENDING_DIR = '/opt/so/state/push_pending'
|
||||||
|
LOCK_FILE = os.path.join(PENDING_DIR, '.lock')
|
||||||
|
MAX_PATHS = 20
|
||||||
|
|
||||||
|
# Mirrors GLOBALS.sensor_roles in salt/vars/globals.map.jinja. Sensor-side
|
||||||
|
# strelka runs on exactly these four roles; so-import gets strelka.manager
|
||||||
|
# instead, which is not fired on pillar changes.
|
||||||
|
SENSOR_ROLES = ['so-eval', 'so-heavynode', 'so-sensor', 'so-standalone']
|
||||||
|
|
||||||
|
|
||||||
|
def _sensor_compound():
|
||||||
|
return ' or '.join('G@role:{}'.format(r) for r in SENSOR_ROLES)
|
||||||
|
|
||||||
|
|
||||||
|
def _push_enabled():
|
||||||
|
try:
|
||||||
|
caller = Caller()
|
||||||
|
return bool(caller.cmd('pillar.get', 'salt:auto_apply:enabled', True))
|
||||||
|
except Exception:
|
||||||
|
LOG.exception('push_strelka: pillar.get salt:auto_apply:enabled failed, assuming enabled')
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _write_intent(key, actions, path):
|
||||||
|
now = time.time()
|
||||||
|
try:
|
||||||
|
os.makedirs(PENDING_DIR, exist_ok=True)
|
||||||
|
except OSError:
|
||||||
|
LOG.exception('push_strelka: cannot create %s', PENDING_DIR)
|
||||||
|
return
|
||||||
|
|
||||||
|
intent_path = os.path.join(PENDING_DIR, '{}.json'.format(key))
|
||||||
|
lock_fd = os.open(LOCK_FILE, os.O_CREAT | os.O_RDWR, 0o644)
|
||||||
|
try:
|
||||||
|
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||||
|
|
||||||
|
intent = {}
|
||||||
|
if os.path.exists(intent_path):
|
||||||
|
try:
|
||||||
|
with open(intent_path, 'r') as f:
|
||||||
|
intent = json.load(f)
|
||||||
|
except (IOError, ValueError):
|
||||||
|
intent = {}
|
||||||
|
|
||||||
|
intent.setdefault('first_touch', now)
|
||||||
|
intent['last_touch'] = now
|
||||||
|
intent['actions'] = actions
|
||||||
|
paths = intent.get('paths', [])
|
||||||
|
if path and path not in paths:
|
||||||
|
paths.append(path)
|
||||||
|
paths = paths[-MAX_PATHS:]
|
||||||
|
intent['paths'] = paths
|
||||||
|
|
||||||
|
tmp_path = intent_path + '.tmp'
|
||||||
|
with open(tmp_path, 'w') as f:
|
||||||
|
json.dump(intent, f)
|
||||||
|
os.rename(tmp_path, intent_path)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception('push_strelka: failed to write intent %s', intent_path)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||||
|
finally:
|
||||||
|
os.close(lock_fd)
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
if not _push_enabled():
|
||||||
|
LOG.info('push_strelka: push disabled, skipping')
|
||||||
|
return {}
|
||||||
|
|
||||||
|
path = data.get('path', '') # noqa: F821 -- data provided by reactor
|
||||||
|
actions = [{'state': 'strelka', 'tgt': _sensor_compound()}]
|
||||||
|
_write_intent('rules_strelka', actions, path)
|
||||||
|
LOG.info('push_strelka: intent updated for path=%s', path)
|
||||||
|
return {}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!py
|
||||||
|
|
||||||
|
# Reactor invoked by the rules_db poll beacon (salt/_beacons/rules_db.py) on rule
|
||||||
|
# file changes under /opt/so/saltstack/local/salt/suricata/rules/.
|
||||||
|
#
|
||||||
|
# Writes (or updates) a push intent at /opt/so/state/push_pending/rules_suricata.json
|
||||||
|
# and returns {}. The so-push-drainer schedule picks up ready intents, dedupes
|
||||||
|
# across pending files, and dispatches orch.push_batch. Reactors never dispatch
|
||||||
|
# directly -- see plan /home/mreeves/.claude/plans/goofy-marinating-hummingbird.md.
|
||||||
|
|
||||||
|
import fcntl
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from salt.client import Caller
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PENDING_DIR = '/opt/so/state/push_pending'
|
||||||
|
LOCK_FILE = os.path.join(PENDING_DIR, '.lock')
|
||||||
|
MAX_PATHS = 20
|
||||||
|
|
||||||
|
# Mirrors GLOBALS.sensor_roles in salt/vars/globals.map.jinja. Suricata also
|
||||||
|
# runs on so-import per salt/top.sls, so that role is appended below.
|
||||||
|
SENSOR_ROLES = ['so-eval', 'so-heavynode', 'so-sensor', 'so-standalone']
|
||||||
|
|
||||||
|
|
||||||
|
def _sensor_compound_plus_import():
|
||||||
|
return ' or '.join('G@role:{}'.format(r) for r in SENSOR_ROLES) + ' or G@role:so-import'
|
||||||
|
|
||||||
|
|
||||||
|
def _push_enabled():
|
||||||
|
try:
|
||||||
|
caller = Caller()
|
||||||
|
return bool(caller.cmd('pillar.get', 'salt:auto_apply:enabled', True))
|
||||||
|
except Exception:
|
||||||
|
LOG.exception('push_suricata: pillar.get salt:auto_apply:enabled failed, assuming enabled')
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _write_intent(key, actions, path):
|
||||||
|
now = time.time()
|
||||||
|
try:
|
||||||
|
os.makedirs(PENDING_DIR, exist_ok=True)
|
||||||
|
except OSError:
|
||||||
|
LOG.exception('push_suricata: cannot create %s', PENDING_DIR)
|
||||||
|
return
|
||||||
|
|
||||||
|
intent_path = os.path.join(PENDING_DIR, '{}.json'.format(key))
|
||||||
|
lock_fd = os.open(LOCK_FILE, os.O_CREAT | os.O_RDWR, 0o644)
|
||||||
|
try:
|
||||||
|
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||||
|
|
||||||
|
intent = {}
|
||||||
|
if os.path.exists(intent_path):
|
||||||
|
try:
|
||||||
|
with open(intent_path, 'r') as f:
|
||||||
|
intent = json.load(f)
|
||||||
|
except (IOError, ValueError):
|
||||||
|
intent = {}
|
||||||
|
|
||||||
|
intent.setdefault('first_touch', now)
|
||||||
|
intent['last_touch'] = now
|
||||||
|
intent['actions'] = actions
|
||||||
|
paths = intent.get('paths', [])
|
||||||
|
if path and path not in paths:
|
||||||
|
paths.append(path)
|
||||||
|
paths = paths[-MAX_PATHS:]
|
||||||
|
intent['paths'] = paths
|
||||||
|
|
||||||
|
tmp_path = intent_path + '.tmp'
|
||||||
|
with open(tmp_path, 'w') as f:
|
||||||
|
json.dump(intent, f)
|
||||||
|
os.rename(tmp_path, intent_path)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception('push_suricata: failed to write intent %s', intent_path)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||||
|
finally:
|
||||||
|
os.close(lock_fd)
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
if not _push_enabled():
|
||||||
|
LOG.info('push_suricata: push disabled, skipping')
|
||||||
|
return {}
|
||||||
|
|
||||||
|
path = data.get('path', '') # noqa: F821 -- data provided by reactor
|
||||||
|
actions = [{'state': 'suricata', 'tgt': _sensor_compound_plus_import()}]
|
||||||
|
_write_intent('rules_suricata', actions, path)
|
||||||
|
LOG.info('push_suricata: intent updated for path=%s', path)
|
||||||
|
return {}
|
||||||
@@ -17,6 +17,7 @@ include:
|
|||||||
so-redis:
|
so-redis:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-redis:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-redis:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- hostname: so-redis
|
- hostname: so-redis
|
||||||
- user: socore
|
- user: socore
|
||||||
- networks:
|
- networks:
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ so-dockerregistry:
|
|||||||
- networks:
|
- networks:
|
||||||
- sobridge:
|
- sobridge:
|
||||||
- ipv4_address: {{ DOCKERMERGED.containers['so-dockerregistry'].ip }}
|
- ipv4_address: {{ DOCKERMERGED.containers['so-dockerregistry'].ip }}
|
||||||
|
# Intentionally `always` (not unless-stopped) -- registry is critical infra
|
||||||
|
# and must come back up even if it was manually stopped. Do not homogenize
|
||||||
|
# to unless-stopped; see the container auto-restart section of the plan.
|
||||||
- restart_policy: always
|
- restart_policy: always
|
||||||
- port_bindings:
|
- port_bindings:
|
||||||
{% for BINDING in DOCKERMERGED.containers['so-dockerregistry'].port_bindings %}
|
{% for BINDING in DOCKERMERGED.containers['so-dockerregistry'].port_bindings %}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{% import_yaml 'salt/defaults.yaml' as SALT_DEFAULTS %}
|
||||||
|
{% set AUTOAPPLY = salt['pillar.get']('salt:auto_apply', SALT_DEFAULTS.salt.auto_apply, merge=True) %}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
{% set SCHEDULE = salt['pillar.get']('healthcheck:schedule', 30) %}
|
{% set SCHEDULE = salt['pillar.get']('healthcheck:schedule', 30) %}
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- salt
|
- salt.minion
|
||||||
|
|
||||||
{% if CHECKS and ENABLED %}
|
{% if CHECKS and ENABLED %}
|
||||||
salt_beacons:
|
salt_beacons:
|
||||||
@@ -14,12 +14,13 @@ salt_beacons:
|
|||||||
- defaults:
|
- defaults:
|
||||||
CHECKS: {{ CHECKS }}
|
CHECKS: {{ CHECKS }}
|
||||||
SCHEDULE: {{ SCHEDULE }}
|
SCHEDULE: {{ SCHEDULE }}
|
||||||
- watch_in:
|
- watch_in:
|
||||||
- service: salt_minion_service
|
- service: salt_minion_service
|
||||||
{% else %}
|
{% else %}
|
||||||
salt_beacons:
|
salt_beacons:
|
||||||
file.absent:
|
file.absent:
|
||||||
- name: /etc/salt/minion.d/beacons.conf
|
- name: /etc/salt/minion.d/beacons.conf
|
||||||
- watch_in:
|
- watch_in:
|
||||||
- service: salt_minion_service
|
- service: salt_minion_service
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
salt:
|
||||||
|
auto_apply:
|
||||||
|
enabled: true
|
||||||
|
debounce_seconds: 30
|
||||||
|
drain_interval: 15
|
||||||
|
batch: '25%'
|
||||||
|
batch_wait: 15
|
||||||
|
schedule:
|
||||||
|
highstate_interval_hours: 2
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
reactor:
|
||||||
|
- 'salt/beacon/*/rules_db/suricata':
|
||||||
|
- salt://reactor/push_suricata.sls
|
||||||
|
- 'salt/beacon/*/rules_db/strelka':
|
||||||
|
- salt://reactor/push_strelka.sls
|
||||||
|
- 'salt/beacon/*/pillar_db/audit_settings':
|
||||||
|
- salt://reactor/push_pillar.sls
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{% from 'vars/globals.map.jinja' import GLOBALS %}
|
||||||
|
{% from 'salt/schedule.map.jinja' import SCHEDULEMERGED %}
|
||||||
|
|
||||||
|
highstate_schedule:
|
||||||
|
schedule.present:
|
||||||
|
- function: state.highstate
|
||||||
|
- hours: {{ SCHEDULEMERGED.highstate_interval_hours }}
|
||||||
|
- maxrunning: 1
|
||||||
|
{% if not GLOBALS.is_manager %}
|
||||||
|
- splay: 1800
|
||||||
|
{% endif %}
|
||||||
@@ -5,3 +5,11 @@ salt_bootstrap:
|
|||||||
- source: salt://salt/scripts/bootstrap-salt.sh
|
- source: salt://salt/scripts/bootstrap-salt.sh
|
||||||
- mode: 755
|
- mode: 755
|
||||||
- show_changes: False
|
- show_changes: False
|
||||||
|
|
||||||
|
salt_sbin:
|
||||||
|
file.recurse:
|
||||||
|
- name: /usr/sbin
|
||||||
|
- source: salt://salt/tools/sbin
|
||||||
|
- user: 939
|
||||||
|
- group: 939
|
||||||
|
- file_mode: 755
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
lasthighstate:
|
lasthighstate:
|
||||||
file.touch:
|
file.touch:
|
||||||
- name: /opt/so/log/salt/lasthighstate
|
- name: /opt/so/log/salt/lasthighstate
|
||||||
- order: last
|
- order: 9001
|
||||||
|
|||||||
+18
-1
@@ -10,10 +10,12 @@
|
|||||||
# software that is protected by the license key."
|
# software that is protected by the license key."
|
||||||
|
|
||||||
{% from 'allowed_states.map.jinja' import allowed_states %}
|
{% from 'allowed_states.map.jinja' import allowed_states %}
|
||||||
|
{% from 'salt/auto_apply.map.jinja' import AUTOAPPLY %}
|
||||||
{% if sls in allowed_states %}
|
{% if sls in allowed_states %}
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- salt.minion
|
- salt.minion
|
||||||
|
- salt.master.pyinotify
|
||||||
- salt.master.boot_mine_update
|
- salt.master.boot_mine_update
|
||||||
{% if 'vrt' in salt['pillar.get']('features', []) %}
|
{% if 'vrt' in salt['pillar.get']('features', []) %}
|
||||||
- salt.cloud
|
- salt.cloud
|
||||||
@@ -63,6 +65,21 @@ engines_config:
|
|||||||
- name: /etc/salt/master.d/engines.conf
|
- name: /etc/salt/master.d/engines.conf
|
||||||
- source: salt://salt/files/engines.conf
|
- source: salt://salt/files/engines.conf
|
||||||
|
|
||||||
|
{% if AUTOAPPLY.enabled %}
|
||||||
|
reactor_pushstate_config:
|
||||||
|
file.managed:
|
||||||
|
- name: /etc/salt/master.d/reactor_pushstate.conf
|
||||||
|
- source: salt://salt/files/reactor_pushstate.conf
|
||||||
|
- watch_in:
|
||||||
|
- service: salt_master_service
|
||||||
|
{% else %}
|
||||||
|
reactor_pushstate_config:
|
||||||
|
file.absent:
|
||||||
|
- name: /etc/salt/master.d/reactor_pushstate.conf
|
||||||
|
- watch_in:
|
||||||
|
- service: salt_master_service
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
# update the bootstrap script when used for salt-cloud
|
# update the bootstrap script when used for salt-cloud
|
||||||
salt_bootstrap_cloud:
|
salt_bootstrap_cloud:
|
||||||
file.managed:
|
file.managed:
|
||||||
@@ -78,7 +95,7 @@ salt_master_service:
|
|||||||
- file: checkmine_engine
|
- file: checkmine_engine
|
||||||
- file: pillarWatch_engine
|
- file: pillarWatch_engine
|
||||||
- file: engines_config
|
- file: engines_config
|
||||||
- order: last
|
- order: 9002
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
pyinotify_module_package:
|
||||||
|
file.recurse:
|
||||||
|
- name: /opt/so/conf/salt/module_packages/pyinotify
|
||||||
|
- source: salt://salt/module_packages/pyinotify
|
||||||
|
- clean: True
|
||||||
|
- makedirs: True
|
||||||
|
|
||||||
|
pyinotify_python_module_install:
|
||||||
|
cmd.run:
|
||||||
|
- name: /opt/saltstack/salt/bin/python3.10 -m pip install pyinotify --no-index --find-links=/opt/so/conf/salt/module_packages/pyinotify/ --upgrade
|
||||||
|
- onchanges:
|
||||||
|
- file: pyinotify_module_package
|
||||||
|
- failhard: True
|
||||||
|
- watch_in:
|
||||||
|
- service: salt_minion_service
|
||||||
@@ -2,4 +2,3 @@
|
|||||||
salt:
|
salt:
|
||||||
minion:
|
minion:
|
||||||
version: '3006.19'
|
version: '3006.19'
|
||||||
check_threshold: 3600 # in seconds, threshold used for so-salt-minion-check. any value less than 600 seconds may cause a lot of salt-minion restarts since the job to touch the file occurs every 5-8 minutes by default
|
|
||||||
|
|||||||
@@ -111,13 +111,17 @@ mark_setup_complete_for_upgrades:
|
|||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# this has to be outside the if statement above since there are <requisite>_in calls to this state
|
# this has to be outside the if statement above since there are <requisite>_in calls to this state.
|
||||||
|
# uses watch (not listen) so the restart fires in-state and its result lands on this state's
|
||||||
|
# running entry; that is what lets wait_for_salt_minion_ready below detect any restart
|
||||||
|
# uniformly via onchanges, regardless of whether the trigger came from these files or from
|
||||||
|
# external watch_in's (e.g. beacons, master/pyinotify).
|
||||||
salt_minion_service:
|
salt_minion_service:
|
||||||
service.running:
|
service.running:
|
||||||
- name: salt-minion
|
- name: salt-minion
|
||||||
- enable: True
|
- enable: True
|
||||||
- onlyif: test "{{INSTALLEDSALTVERSION}}" == "{{SALTVERSION}}"
|
- onlyif: test "{{INSTALLEDSALTVERSION}}" == "{{SALTVERSION}}"
|
||||||
- listen:
|
- watch:
|
||||||
- file: mine_functions
|
- file: mine_functions
|
||||||
{% if INSTALLEDSALTVERSION|string == SALTVERSION|string %}
|
{% if INSTALLEDSALTVERSION|string == SALTVERSION|string %}
|
||||||
- file: set_log_levels
|
- file: set_log_levels
|
||||||
@@ -126,3 +130,17 @@ salt_minion_service:
|
|||||||
- file: signing_policy
|
- file: signing_policy
|
||||||
{% endif %}
|
{% endif %}
|
||||||
- order: last
|
- order: last
|
||||||
|
|
||||||
|
# block until the just-restarted salt-minion is back and can execute modules locally, so
|
||||||
|
# follow-on jobs and the next highstate iteration do not race the restart. onchanges +
|
||||||
|
# require on salt_minion_service catches every restart trigger uniformly because watch
|
||||||
|
# mod_watch results replace the service state's running entry. wait logic lives in
|
||||||
|
# /usr/sbin/so-salt-minion-wait (deployed by common_sbin from common/tools/sbin/).
|
||||||
|
wait_for_salt_minion_ready:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-salt-minion-wait
|
||||||
|
- onchanges:
|
||||||
|
- service: salt_minion_service
|
||||||
|
- require:
|
||||||
|
- service: salt_minion_service
|
||||||
|
- order: last
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
|||||||
|
{% from 'vars/globals.map.jinja' import GLOBALS %}
|
||||||
|
{% from 'salt/auto_apply.map.jinja' import AUTOAPPLY %}
|
||||||
|
|
||||||
|
{% if GLOBALS.is_manager and AUTOAPPLY.enabled %}
|
||||||
|
push_drain_schedule:
|
||||||
|
schedule.present:
|
||||||
|
- function: cmd.run
|
||||||
|
- job_args:
|
||||||
|
- /usr/sbin/so-push-drainer
|
||||||
|
- seconds: {{ AUTOAPPLY.drain_interval }}
|
||||||
|
- maxrunning: 1
|
||||||
|
- return_job: False
|
||||||
|
{% elif GLOBALS.is_manager %}
|
||||||
|
push_drain_schedule:
|
||||||
|
schedule.absent:
|
||||||
|
- name: push_drain_schedule
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{% import_yaml 'salt/defaults.yaml' as SALT_DEFAULTS %}
|
||||||
|
{% set SCHEDULEMERGED = salt['pillar.get']('salt:schedule', SALT_DEFAULTS.salt.schedule, merge=True) %}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
salt:
|
||||||
|
auto_apply:
|
||||||
|
enabled:
|
||||||
|
description: Master kill-switch for the active push feature. When disabled, rule and pillar changes are picked up at the next scheduled highstate instead of being pushed immediately.
|
||||||
|
forcedType: bool
|
||||||
|
helpLink: push
|
||||||
|
global: True
|
||||||
|
debounce_seconds:
|
||||||
|
description: Trailing-edge debounce window in seconds. A push intent must be quiet for this long before the drainer dispatches. Rapid bursts of edits within this window coalesce into one dispatch.
|
||||||
|
forcedType: int
|
||||||
|
helpLink: push
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
drain_interval:
|
||||||
|
description: How often the push drainer checks for ready intents, in seconds. Small values lower dispatch latency at the cost of more background work on the manager.
|
||||||
|
forcedType: int
|
||||||
|
helpLink: push
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
batch:
|
||||||
|
description: "Host batch size for push orchestrations. A number (e.g. '10') or a percentage (e.g. '25%'). Limits how many minions run the push state at once so large fleets don't thundering-herd."
|
||||||
|
helpLink: push
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
regex: '^([0-9]+%?)$'
|
||||||
|
regexFailureMessage: Enter a whole number or a whole-number percentage (e.g. 10 or 25%).
|
||||||
|
batch_wait:
|
||||||
|
description: Seconds to wait between host batches in a push orchestration. Gives the fleet time to breathe between waves.
|
||||||
|
forcedType: int
|
||||||
|
helpLink: push
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
schedule:
|
||||||
|
highstate_interval_hours:
|
||||||
|
description: How often every minion in the grid runs a scheduled state.highstate, in hours. Lower values keep minions closer in sync at the cost of more load; higher values reduce load but increase worst-case latency for non-pushed changes. The salt-minion health check restarts a minion if its last highstate is older than this value plus one hour.
|
||||||
|
forcedType: int
|
||||||
|
helpLink: push
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
# Block until the local salt-minion service is back up and can execute modules locally.
|
||||||
|
# Invoked from the wait_for_salt_minion_ready state in salt/minion/init.sls after
|
||||||
|
# salt_minion_service fires its watch-driven mod_watch (a non-blocking systemctl restart),
|
||||||
|
# so follow-on jobs and the next highstate iteration do not race the in-flight restart.
|
||||||
|
|
||||||
|
. /usr/sbin/so-common
|
||||||
|
|
||||||
|
# Initial sleep gives the systemctl restart (--no-block by default for salt-minion on
|
||||||
|
# >=3006.15) time to begin tearing down the old process before we probe for readiness.
|
||||||
|
INITIAL_SLEEP=3
|
||||||
|
TIMEOUT=120
|
||||||
|
PING_TIMEOUT=5
|
||||||
|
|
||||||
|
sleep "$INITIAL_SLEEP"
|
||||||
|
|
||||||
|
elapsed="$INITIAL_SLEEP"
|
||||||
|
while [ "$elapsed" -lt "$TIMEOUT" ]; do
|
||||||
|
if systemctl is-active --quiet salt-minion \
|
||||||
|
&& salt-call --local --timeout="$PING_TIMEOUT" --out=quiet test.ping >/dev/null 2>&1; then
|
||||||
|
echo "salt-minion ready after ${elapsed}s"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
elapsed=$((elapsed + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "salt-minion did not become ready within ${TIMEOUT}s" >&2
|
||||||
|
exit 1
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{% from 'vars/globals.map.jinja' import GLOBALS %}
|
|
||||||
|
|
||||||
highstate_schedule:
|
|
||||||
schedule.present:
|
|
||||||
- function: state.highstate
|
|
||||||
- minutes: 15
|
|
||||||
- maxrunning: 1
|
|
||||||
{% if not GLOBALS.is_manager %}
|
|
||||||
- splay: 120
|
|
||||||
{% endif %}
|
|
||||||
@@ -14,6 +14,7 @@ include:
|
|||||||
so-sensoroni:
|
so-sensoroni:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-soc:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-soc:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- network_mode: host
|
- network_mode: host
|
||||||
- binds:
|
- binds:
|
||||||
- /nsm/import:/nsm/import:rw
|
- /nsm/import:/nsm/import:rw
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ include:
|
|||||||
so-soc:
|
so-soc:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-soc:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-soc:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- hostname: soc
|
- hostname: soc
|
||||||
- name: so-soc
|
- name: so-soc
|
||||||
- networks:
|
- networks:
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ strelka_backend:
|
|||||||
- {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }}
|
- {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
# Intentionally `on-failure` (not unless-stopped) -- strelka backend shuts
|
||||||
|
# down cleanly during rule reloads and we do not want those clean exits to
|
||||||
|
# trigger an auto-restart. Do not homogenize; see the container
|
||||||
|
# auto-restart section of the plan.
|
||||||
- restart_policy: on-failure
|
- restart_policy: on-failure
|
||||||
- watch:
|
- watch:
|
||||||
- file: strelkasensorcompiledrules
|
- file: strelkasensorcompiledrules
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ include:
|
|||||||
strelka_coordinator:
|
strelka_coordinator:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-redis:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-redis:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- name: so-strelka-coordinator
|
- name: so-strelka-coordinator
|
||||||
- networks:
|
- networks:
|
||||||
- sobridge:
|
- sobridge:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ include:
|
|||||||
strelka_filestream:
|
strelka_filestream:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-strelka-manager:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-strelka-manager:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- binds:
|
- binds:
|
||||||
- /opt/so/conf/strelka/filestream/:/etc/strelka/:ro
|
- /opt/so/conf/strelka/filestream/:/etc/strelka/:ro
|
||||||
- /nsm/strelka:/nsm/strelka
|
- /nsm/strelka:/nsm/strelka
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ include:
|
|||||||
strelka_frontend:
|
strelka_frontend:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-strelka-manager:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-strelka-manager:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- binds:
|
- binds:
|
||||||
- /opt/so/conf/strelka/frontend/:/etc/strelka/:ro
|
- /opt/so/conf/strelka/frontend/:/etc/strelka/:ro
|
||||||
- /nsm/strelka/log/:/var/log/strelka/:rw
|
- /nsm/strelka/log/:/var/log/strelka/:rw
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ include:
|
|||||||
strelka_gatekeeper:
|
strelka_gatekeeper:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-redis:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-redis:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- name: so-strelka-gatekeeper
|
- name: so-strelka-gatekeeper
|
||||||
- networks:
|
- networks:
|
||||||
- sobridge:
|
- sobridge:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ include:
|
|||||||
strelka_manager:
|
strelka_manager:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-strelka-manager:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-strelka-manager:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- binds:
|
- binds:
|
||||||
- /opt/so/conf/strelka/manager/:/etc/strelka/:ro
|
- /opt/so/conf/strelka/manager/:/etc/strelka/:ro
|
||||||
{% if DOCKERMERGED.containers['so-strelka-manager'].custom_bind_mounts %}
|
{% if DOCKERMERGED.containers['so-strelka-manager'].custom_bind_mounts %}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ so-suricata:
|
|||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-suricata:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-suricata:{{ GLOBALS.so_version }}
|
||||||
- privileged: True
|
- privileged: True
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- environment:
|
- environment:
|
||||||
- INTERFACE={{ GLOBALS.sensor.interface }}
|
- INTERFACE={{ GLOBALS.sensor.interface }}
|
||||||
{% if DOCKERMERGED.containers['so-suricata'].extra_env %}
|
{% if DOCKERMERGED.containers['so-suricata'].extra_env %}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ so-tcpreplay:
|
|||||||
docker_container.running:
|
docker_container.running:
|
||||||
- network_mode: "host"
|
- network_mode: "host"
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-tcpreplay:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-tcpreplay:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- name: so-tcpreplay
|
- name: so-tcpreplay
|
||||||
- user: root
|
- user: root
|
||||||
- interactive: True
|
- interactive: True
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ include:
|
|||||||
so-telegraf:
|
so-telegraf:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-telegraf:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-telegraf:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- user: 939
|
- user: 939
|
||||||
- group_add: 939,920
|
- group_add: 939,920
|
||||||
- environment:
|
- environment:
|
||||||
|
|||||||
+2
-2
@@ -19,7 +19,7 @@ base:
|
|||||||
- repo.client
|
- repo.client
|
||||||
- versionlock
|
- versionlock
|
||||||
- ntp
|
- ntp
|
||||||
- schedule
|
- salt.highstate_schedule
|
||||||
- logrotate
|
- logrotate
|
||||||
|
|
||||||
# manager node on proper salt version with empty node_data pillar
|
# manager node on proper salt version with empty node_data pillar
|
||||||
@@ -55,6 +55,7 @@ base:
|
|||||||
- motd
|
- motd
|
||||||
- salt.minion-check
|
- salt.minion-check
|
||||||
- salt.lasthighstate
|
- salt.lasthighstate
|
||||||
|
- salt.push_drain_schedule
|
||||||
- common
|
- common
|
||||||
- docker
|
- docker
|
||||||
- docker_clean
|
- docker_clean
|
||||||
@@ -300,7 +301,6 @@ base:
|
|||||||
- nginx
|
- nginx
|
||||||
- elasticfleet
|
- elasticfleet
|
||||||
- elasticfleet.install_agent_grid
|
- elasticfleet.install_agent_grid
|
||||||
- schedule
|
|
||||||
- stig
|
- stig
|
||||||
|
|
||||||
'*_hypervisor and I@features:vrt and G@saltversion:{{saltversion}}':
|
'*_hypervisor and I@features:vrt and G@saltversion:{{saltversion}}':
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ so-zeek:
|
|||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-zeek:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-zeek:{{ GLOBALS.so_version }}
|
||||||
- start: True
|
- start: True
|
||||||
- privileged: True
|
- privileged: True
|
||||||
|
- restart_policy: unless-stopped
|
||||||
{% if DOCKERMERGED.containers['so-zeek'].ulimits %}
|
{% if DOCKERMERGED.containers['so-zeek'].ulimits %}
|
||||||
- ulimits:
|
- ulimits:
|
||||||
{% for ULIMIT in DOCKERMERGED.containers['so-zeek'].ulimits %}
|
{% for ULIMIT in DOCKERMERGED.containers['so-zeek'].ulimits %}
|
||||||
|
|||||||
+1
-1
@@ -1433,7 +1433,7 @@ make_some_dirs() {
|
|||||||
mkdir -p $local_salt_dir/salt/firewall/portgroups
|
mkdir -p $local_salt_dir/salt/firewall/portgroups
|
||||||
mkdir -p $local_salt_dir/salt/firewall/ports
|
mkdir -p $local_salt_dir/salt/firewall/ports
|
||||||
|
|
||||||
for THEDIR in bpf elasticsearch ntp firewall redis backup influxdb postgres strelka sensoroni soc docker zeek suricata nginx telegraf logstash soc manager kratos hydra idh elastalert stig global kafka versionlock hypervisor vm; do
|
for THEDIR in bpf elasticsearch ntp firewall redis backup influxdb postgres strelka sensoroni soc docker zeek suricata nginx telegraf logstash soc manager kratos hydra idh elastalert stig global salt kafka versionlock hypervisor vm; do
|
||||||
mkdir -p $local_salt_dir/pillar/$THEDIR
|
mkdir -p $local_salt_dir/pillar/$THEDIR
|
||||||
touch $local_salt_dir/pillar/$THEDIR/adv_$THEDIR.sls
|
touch $local_salt_dir/pillar/$THEDIR/adv_$THEDIR.sls
|
||||||
touch $local_salt_dir/pillar/$THEDIR/soc_$THEDIR.sls
|
touch $local_salt_dir/pillar/$THEDIR/soc_$THEDIR.sls
|
||||||
|
|||||||
+6
-3
@@ -9,14 +9,17 @@
|
|||||||
# Make sure you are root before doing anything
|
# Make sure you are root before doing anything
|
||||||
uid="$(id -u)"
|
uid="$(id -u)"
|
||||||
if [ "$uid" -ne 0 ]; then
|
if [ "$uid" -ne 0 ]; then
|
||||||
echo "This script must be run using sudo!"
|
echo "This script must be run using sudo!" >&2
|
||||||
fail_setup
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Save the original argument array since we modify it
|
# Save the original argument array since we modify it
|
||||||
original_args=("$@")
|
original_args=("$@")
|
||||||
|
|
||||||
cd "$(dirname "$0")" || fail_setup
|
cd "$(dirname "$0")" || {
|
||||||
|
echo "Unable to change to setup directory" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
echo "Getting started..."
|
echo "Getting started..."
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user