Compare commits

...

71 Commits

Author SHA1 Message Date
Mike Reeves 18212cad0d Add cluster_health option to telegraf.conf
Enable cluster health monitoring in Telegraf configuration.
2026-07-06 17:27:37 -04:00
Josh Brower 9975d36b4f Merge pull request #16019 from Security-Onion-Solutions/feature/sigma-playbooks
support sigma playbooks
2026-07-06 13:17:54 +02:00
Jorge Reyes 8e9e221196 Merge pull request #16035 from Security-Onion-Solutions/reyesj2/huntquery
update default hunt query
2026-07-02 14:50:59 -05:00
Josh Brower 1fe7726aff Changes from feedback 2026-07-02 14:58:48 -04:00
Jason Ertel 07d6b2cfdd Merge pull request #16033 from Security-Onion-Solutions/jertel/wip
avoid setup failure reason ambiguity
2026-07-02 09:20:48 -04:00
Jason Ertel 89afea876a Merge branch '3/dev' into jertel/wip 2026-07-02 09:04:57 -04:00
Jason Ertel 1243a25bd3 avoid setup failure reason ambiguity 2026-07-02 08:59:52 -04:00
Josh Patterson 76f6947f36 Merge pull request #16029 from Security-Onion-Solutions/surirulereload
only reload suricata rules if all-rulesets.rules exists
2026-07-01 16:54:02 -04:00
Jorge Reyes 92a55386c6 Merge pull request #16028 from Security-Onion-Solutions/reyesj2-patch-6
duplicate repo name in so-repo-sync
2026-07-01 15:50:54 -05:00
reyesj2 e7352eb841 duplicate repo name in so-repo-sync 2026-07-01 15:17:55 -05:00
Josh Patterson 795aa898a3 suricata: only reload rules once the ruleset file exists
On a fresh install the surirulesync file.recurse creates .gitkeep before
SOC has generated all-rulesets.rules. That change satisfied the
surirulereload onchanges requisite, so the reload ran with no ruleset
present, failed to stat the file, and reported the state (and install)
as failed.

Add an onlyif guard so the reload only runs when all-rulesets.rules
exists. A .gitkeep-only sync now leaves the state a clean success
(onlyif condition false); once SOC writes the ruleset, the reload fires
normally.
2026-07-01 15:12:54 -04:00
Josh Patterson 69d77382f1 suricata: timestamp each line of reload log output
Route the reload/verify output (ours plus so-common's retry/fail lines)
through a synchronous timestamping pipeline so every line in reload.log
is prefixed with a date/time, and preserve the real exit code via
PIPESTATUS.
2026-07-01 15:12:53 -04:00
Jorge Reyes dc9b4f3ce5 Merge pull request #16027 from Security-Onion-Solutions/reyesj2-patch-6
increase wait_for_so-kibana timeout to 10m
2026-07-01 13:48:10 -05:00
reyesj2 87b9276c79 increase wait_for_so-kibana timeout to 10m 2026-07-01 13:19:47 -05:00
Jorge Reyes 99118f9bed Merge pull request #16023 from Security-Onion-Solutions/reyesj2/uekairgap
update airgap soup to sync uek repo from iso and retain latest packag…
2026-07-01 13:14:55 -05:00
reyesj2 24b75b4a2b typo 2026-07-01 12:50:23 -05:00
Jorge Reyes 395bd627f1 Merge pull request #16024 from Security-Onion-Solutions/reyesj2/fixsearch
remove outdated eval script and associated salt utility state
2026-07-01 11:59:00 -05:00
reyesj2 868b217549 update default hunt query 2026-07-01 11:37:46 -05:00
reyesj2 c33db9d00f remove outdated eval script and associated salt utility state 2026-07-01 11:12:39 -05:00
reyesj2 e88eb65a44 keep old packages for rollback ability 2026-07-01 10:29:05 -05:00
reyesj2 dc8c80633b update airgap soup to sync uek repo from iso and retain latest packages only 2026-07-01 10:23:04 -05:00
Josh Patterson 895aa18486 Merge pull request #16021 from Security-Onion-Solutions/surirulereload
suricata: verify reloaded ruleset is newer than the rules file
2026-07-01 10:33:14 -04:00
Josh Brower 2a6cc58306 Simplify mappings 2026-07-01 09:07:02 -04:00
Josh Patterson ee36f5f84c suricata: verify reloaded ruleset is newer than the rules file
Treating an in-progress reload as instant success could report success
while Suricata was still running a stale ruleset (the in-flight reload
may have started before the new all-rulesets.rules was written).

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

Also drop the redundant ruleset-reload-nonblocking call (the verified
blocking reload is authoritative and the async call was what left a
reload running) and log human-readable timestamps.
2026-07-01 09:00:36 -04:00
Josh Brower 9217670bab support sigma playbooks 2026-06-30 16:21:01 -04:00
Jorge Reyes a3f586cf88 Merge pull request #16018 from Security-Onion-Solutions/reyesj2/kf 2026-06-30 14:46:22 -05:00
reyesj2 670d2b2757 casing 2026-06-30 12:57:56 -05:00
reyesj2 3b8459c6ec soup upgrade kafka cluster metadata v4 2026-06-30 12:43:42 -05:00
Josh Patterson 52574e21c6 suricata: treat in-progress rule reload as success
so-suricata-reload-rules failed the surirulereload state when a rule
reload was already running: suricatasc returns
{"message":"Reload already in progress","return":"NOK"}, which never
matched the expected output, so retry looped all 60 attempts (~3 min)
and called fail.

Wrap the suricatasc calls so an in-progress reload is treated as
success (the in-flight reload picks up the new rules) while genuine
container-not-ready conditions still retry and ultimately fail.
2026-06-30 09:40:23 -04:00
Jorge Reyes 576c7bfedd Merge pull request #16013 from Security-Onion-Solutions/reyesj2/so-start
update so-stop | so-start | so-restart scripts
2026-06-26 13:47:09 -05:00
reyesj2 b3b7ecdded update so-stop | so-start | so-restart scripts 2026-06-26 13:19:18 -05:00
Mike Reeves 0af020b6c3 Merge pull request #16012 from Security-Onion-Solutions/kernel
Serve /kernelrepo through nginx so minions can reach the kernel repo
2026-06-26 12:04:33 -04:00
Mike Reeves 339a5af4a3 Serve /kernelrepo through nginx so minions can reach the kernel repo
The /nsm/kernelrepo bind mount exposed the files, but without a matching
location block external requests to /kernelrepo/ fell through to the SOC
app and returned HTML, so minions hit 'repomd.xml parser error'. Add a
/kernelrepo/ location mirroring /repo/.
2026-06-26 12:02:49 -04:00
Mike Reeves 7952c274c4 Merge pull request #16011 from Security-Onion-Solutions/kernel
Switch boot default to UEK8 kernel and gate kernel repo on salt version
2026-06-26 09:27:01 -04:00
Mike Reeves 67a9abadf2 Gate so_kernel_repo on running salt matching the shipped version
During soup the grid is mid-salt-upgrade. Only assign the UEK8 kernel
repo once the node's grains.saltversion matches salt.minion.version from
minion.defaults.yaml, so the kernel repo and the update it enables don't
activate until the node is fully on the target salt.
2026-06-26 09:21:11 -04:00
Mike Reeves 94f31e1356 Add so-kernel-upgrade to switch the boot default to the UEK8 kernel
Installing kernel-uek-core adds a UEK8 (6.x) boot entry but doesn't make
it the default, because grubby only auto-promotes within the running
kernel's flavor lineage and we cross from a 5.x kernel to the new UEK8
flavor. so-kernel-upgrade finds the newest installed 6.x UEK kernel and
grubby --set-default's it (idempotent, verifies the change, no reboot).
2026-06-26 09:21:11 -04:00
Josh Brower 435e2b4182 Merge pull request #16009 from Security-Onion-Solutions/zeek-communityid
Set transport for ssl.established:false logs
2026-06-25 21:04:27 +02:00
Josh Brower d0edfd2131 set transport for ssl.established:false logs 2026-06-25 14:18:43 -04:00
Mike Reeves 13ebde61bd Merge pull request #16000 from Security-Onion-Solutions/kernel
Add UEK8 kernel repo support across install and grid
2026-06-25 14:15:58 -04:00
Jason Ertel 30312b93a6 Merge pull request #16008 from Security-Onion-Solutions/jertel/wip
support multiple capinfos versions
2026-06-25 10:19:56 -04:00
Jason Ertel a9c03e39bb support multiple capinfos versions 2026-06-25 09:32:08 -04:00
Dan Marr 4d34470b84 Merge pull request #16005 from triggerman86/triggerman-fix-root_check-so-soup
Fix premature fail_setup function call in so-setup
2026-06-24 13:41:53 -04:00
Mike Reeves b0b022c3ad Seed an empty /nsm/kernelrepo so the manager repo is always valid
so-repo-sync only populates /nsm/kernelrepo after the highstate, so on a
manager the file:///nsm/kernelrepo repo could be assigned before any
repodata exists, failing every dnf op. Run createrepo on the dir when
repodata/repomd.xml is missing, leaving a synced repo untouched.
2026-06-24 13:23:25 -04:00
Mike Reeves 27c1c35e62 Mark kernel repo skip_if_unavailable so an empty repo can't brick dnf
When the kernel repo is assigned but /nsm/kernelrepo isn't populated
yet, its missing repomd.xml makes every dnf/pkg operation fail (e.g.
pkg.held for salt during highstate). The kernel repo is supplementary,
so set skip_if_unavailable=1 in both the salt-managed client repo and
the four install-time bootstrap repo files; dnf ignores it until it is
populated instead of aborting. The main repo stays strict.
2026-06-24 13:20:10 -04:00
Mike Reeves f45631af3a Guard kernel reposync on its config section existing
During soup, so-repo-sync runs before the highstate deploys the new
repodownload.conf. On the first upgrade to a kernel-aware version the
on-disk config lacks the [securityonionkernel] section, so dnf aborts
with "Unknown repo: 'securityonionkernel'" (set -e kills soup). Guard
the kernel reposync on the section being present; the next sync after
the highstate deploys it picks it up.
2026-06-24 12:15:10 -04:00
Jorge Reyes 81c8d54589 Merge pull request #16006 from Security-Onion-Solutions/reyesj2-patch-5
remove heayvnode FleetServer_* directory creation, and skip empty dir…
2026-06-23 15:53:34 -05:00
reyesj2 4f3b57f495 remove duplicate package-upgrade attempts, upgrade only when reported latest version differs from installed version 2026-06-23 15:52:10 -05:00
reyesj2 84228a819b remove heayvnode FleetServer_* directory creation, and skip empty directories during FleetServer policy management 2026-06-23 15:30:49 -05:00
Dan Marr 81ebea0451 Fix non-root exit checks at start of so-setup 2026-06-23 16:07:30 -04:00
Mike Reeves 8e2753aeb8 Fix duplicate securityonionkernel repo definition
The install bootstrap appended the [securityonionkernel] section to the
shared /etc/yum.repos.d/securityonion.repo, but the salt state so_kernel_repo
(name securityonionkernel) manages its own canonical file
/etc/yum.repos.d/securityonionkernel.repo. At highstate both files defined the
same repo id, so dnf failed with "repository securityonionkernel is listed
more than 1 time".

Write the bootstrap kernel repo to /etc/yum.repos.d/securityonionkernel.repo
in all four securityonion_repo() branches so the id lives in exactly one file
and salt edits it in place. Mirrors how the main repo's runtime id matches its
file name.
2026-06-23 13:53:14 -04:00
Mike Reeves 698a746d6d Add UEK8 kernel repo support across install and grid
Mirror the kernel repo to full parity with the main package repo so the
grid can pull the Oracle UEK8 kernel:

- setup/so-functions: securityonion_repo() emits a [securityonionkernel]
  section in every branch (mirrorlist on non-airgap, https://$MSRV/kernelrepo
  for airgap/minion, file:///nsm/kernelrepo/ for manager); repo_sync_local()
  and create_repo() sync and build /nsm/kernelrepo.
- manager/init.sls: create /nsm/kernelrepo and deploy mirror-kernel.txt.
- nginx/enabled.sls: serve /nsm/kernelrepo at https://<repo_host>/kernelrepo.
- repo/client/oracle.sls: add so_kernel_repo, gated by
  onlyif test -e /opt/so/state/nic_names_pinned so the kernel repo is only
  assigned once NICs are pinned by MAC.
- update_packages(): run so-nic-pin before the dnf update that pulls the
  kernel, freezing interface names and dropping the pin marker so the kernel
  isn't downgraded then re-upgraded on the first highstate.
2026-06-23 13:19:56 -04:00
Josh Patterson a9f9d8bd0d Merge pull request #15985 from Security-Onion-Solutions/soupmod2
allow manager two full highstates during soup, improve elastic script runtime
2026-06-22 17:02:02 -04:00
Jason Ertel 953fdee3af Merge pull request #15984 from Security-Onion-Solutions/jertel/wip
Upgrade registry
2026-06-22 16:56:18 -04:00
Jason Ertel e2e3e690ca reset version 2026-06-22 16:52:29 -04:00
Josh Patterson 323491f58e Merge pull request #15983 from Security-Onion-Solutions/reyesj2-jpp
wip
2026-06-22 16:52:10 -04:00
reyesj2 96fcc0ec38 wip 2026-06-22 14:25:46 -05:00
Jason Ertel bcc60a4ae0 kilo version 2026-06-22 13:07:49 -04:00
Jason Ertel b77103aa9f upgrade registry 2026-06-22 13:01:02 -04:00
Josh Patterson d0bea2ebcb Restore grouped per-integration logging and retry 409s in fleet integration loader
elastic_fleet_load_integrations_dir now buffers each concurrent job's
output (header + API response) to a per-job file and prints them in
submission order after wait, restoring the readable serial-style output
while keeping concurrent writes.

Add --retry-all-errors to the integration create/update curl calls so
transient 409 conflicts from concurrent writes to the same agent policy
are retried (curl --retry alone does not retry 409).
2026-06-18 11:19:36 -04:00
Josh Patterson 62c01a9756 Merge remote-tracking branch 'origin/3/dev' into soupmod2 2026-06-18 09:53:44 -04:00
Josh Patterson 8e33d0e1e9 Merge remote-tracking branch 'origin/3/dev' into soupmod2 2026-06-16 12:54:18 -04:00
Josh Patterson 1ee555957a Speed up so-elastic-fleet-integration-upgrade
Fetch each agent policy once and extract integration name/package/version/id
locally via a single jq pass instead of re-fetching the identical policy JSON
1+3N times. Memoize epm/packages latest-version lookups so each package is
queried once instead of per (policy, integration). Dispatch the per-integration
dry-run+upgrade as throttled background jobs (MAX_FLEET_JOBS) with
flock-serialized output and a FAIL_FILE marker, mirroring
elastic_fleet_load_integrations_dir.

Behavior preserved: same elastic-defend-endpoints/fleet_server skips, same
AUTO_UPGRADE_INTEGRATIONS default-package gating (moved into jq, using $defaults
to avoid the jq $def keyword collision), and exit 1 on any failure so salt
retries.
2026-06-12 15:23:43 -04:00
Josh Patterson 43f72c1f9f Parallelize so-elasticsearch-templates-load template PUTs
Load component and index templates as throttled background jobs (max 10
concurrent) instead of sequential curl PUTs, matching the bounded-concurrency
+ flock-serialized-output pattern used by the fleet/ILM load scripts. Keeps a
wait barrier between the component phase and the index phase so index
templates never load before their referenced component templates. Failures are
tracked via per-job marker files since counter increments can't escape
background subshells.
2026-06-12 15:11:34 -04:00
Josh Patterson ae6a705ce1 Speed up so-elastic-fleet-integration-policy-load
Fetch each agent policy once per group instead of refetching the full
policy (plus a fresh Kibana session cookie) for every integration file,
and dispatch the create/update writes as throttled background jobs.

Adds elastic_fleet_load_integrations_dir and elastic_fleet_throttle to
so-elastic-fleet-common, reusing the bounded-concurrency pattern from
so-elasticsearch-ilm-policy-load. Replaces the four serial loops in the
loader with one call per agent policy.
2026-06-12 09:38:41 -04:00
Josh Patterson b1273573ed Fix jq $def keyword collision in optional-integrations-load
The agent-policy enumeration passed --argjson def, creating a jq
variable $def. 'def' is a reserved keyword in jq and the deployed jq
version rejects it, so the program failed to compile and
in_use_integrations was left empty (silently disabling the in-use
upgrade guard). Rename the arg to $defaults.
2026-06-11 15:50:53 -04:00
Josh Patterson 6c42c419e2 Serialize ILM policy-load output with flock to stop interleaving
A single printf per block was not actually one write() call, so
concurrent jobs still occasionally interleaved their label and response
lines. Hold an flock around just the printf (curl still runs in
parallel) so each policy's block prints intact, keeping live
completion-order streaming.
2026-06-11 15:42:41 -04:00
Josh Patterson f23652397c Speed up so-elastic-fleet-optional-integrations-load decision logic
Replace the per-package decision loop (which forked ~10 processes per
package and rebuilt a growing JSON file on every add -> O(n^2)) with two
jq passes: one prints the status messages, one builds the bulk install
list. A vnum/needs() jq definition reproduces the previous
version_conversion/compare_versions and excluded/subscription/installed/
upgrade/in-use logic exactly. Also fetch each agent policy once and
extract non-default package names locally instead of re-fetching the
policy per integration (1+K -> 1 GET per policy). Install behavior is
unchanged.
2026-06-11 13:57:56 -04:00
Josh Patterson 07d3b148b5 fix output 2026-06-11 13:37:26 -04:00
Josh Patterson 780d9faf0d Parallelize so-elasticsearch-ilm-policy-load PUTs
Run the ~300 ILM policy PUTs concurrently (bounded to 10 in flight via a
throttle gate) instead of one serial curl per policy. Adds a put_policy
helper and waits for all background jobs before exiting. Preserves policy
parity; only the scheduling changes. Drops the dead empty sid cookie arg
(falls back to basic auth from curl.config as before).
2026-06-11 12:08:32 -04:00
Josh Patterson d2fe51d5fe Merge remote-tracking branch 'origin/3/dev' into soupmod2 2026-06-11 09:26:14 -04:00
Josh Patterson 83aaa76f98 allow full highstate on manager when locked 2026-06-10 16:34:10 -04:00
46 changed files with 1053 additions and 536 deletions
-59
View File
@@ -1,59 +0,0 @@
#!/usr/bin/env bash
# This script adds sensors/nodes/etc to the nodes tab
default_salt_dir=/opt/so/saltstack/default
local_salt_dir=/opt/so/saltstack/local
TYPE=$1
NAME=$2
IPADDRESS=$3
CPUS=$4
GUID=$5
MANINT=$6
ROOTFS=$7
NSM=$8
MONINT=$9
#NODETYPE=$10
#HOTNAME=$11
echo "Seeing if this host is already in here. If so delete it"
if grep -q $NAME "$local_salt_dir/pillar/data/$TYPE.sls"; then
echo "Node Already Present - Let's re-add it"
awk -v blah=" $NAME:" 'BEGIN{ print_flag=1 }
{
if( $0 ~ blah )
{
print_flag=0;
next
}
if( $0 ~ /^ [a-zA-Z0-9]+:$/ )
{
print_flag=1;
}
if ( print_flag == 1 )
print $0
} ' $local_salt_dir/pillar/data/$TYPE.sls > $local_salt_dir/pillar/data/tmp.$TYPE.sls
mv $local_salt_dir/pillar/data/tmp.$TYPE.sls $local_salt_dir/pillar/data/$TYPE.sls
echo "Deleted $NAME from the tab. Now adding it in again with updated info"
fi
echo " $NAME:" >> $local_salt_dir/pillar/data/$TYPE.sls
echo " ip: $IPADDRESS" >> $local_salt_dir/pillar/data/$TYPE.sls
echo " manint: $MANINT" >> $local_salt_dir/pillar/data/$TYPE.sls
echo " totalcpus: $CPUS" >> $local_salt_dir/pillar/data/$TYPE.sls
echo " guid: $GUID" >> $local_salt_dir/pillar/data/$TYPE.sls
echo " rootfs: $ROOTFS" >> $local_salt_dir/pillar/data/$TYPE.sls
echo " nsmfs: $NSM" >> $local_salt_dir/pillar/data/$TYPE.sls
if [ $TYPE == 'sensorstab' ]; then
echo " monint: bond0" >> $local_salt_dir/pillar/data/$TYPE.sls
fi
if [ $TYPE == 'evaltab' ] || [ $TYPE == 'standalonetab' ]; then
echo " monint: bond0" >> $local_salt_dir/pillar/data/$TYPE.sls
if [ ! $10 ]; then
salt-call state.apply utility queue=True
fi
fi
if [ $TYPE == 'nodestab' ]; then
salt-call state.apply elasticsearch queue=True
# echo " nodetype: $NODETYPE" >> $local_salt_dir/pillar/data/$TYPE.sls
# echo " hotname: $HOTNAME" >> $local_salt_dir/pillar/data/$TYPE.sls
fi
+1 -2
View File
@@ -37,8 +37,7 @@
'elasticfleet',
'elasticfleet.manager',
'elasticsearch.cluster',
'elastic-fleet-package-registry',
'utility'
'elastic-fleet-package-registry'
] %}
{% set sensor_states = [
+14
View File
@@ -291,6 +291,20 @@ download_and_verify() {
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() {
read -r -d '' message <<- EOM
+57
View File
@@ -0,0 +1,57 @@
#!/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.
#
# so-kernel-upgrade — switch the boot default to the installed UEK8 (6.x) kernel.
#
# Security Onion is moving off the EL9 stock kernel / UEK7 (5.x) onto UEK8 (6.x).
# Installing the kernel-uek-core package adds a UEK8 boot entry but does NOT make it the
# default: kernel-install/grubby only auto-promote a new kernel within the running
# kernel's flavor lineage, and we're crossing from a 5.x kernel to the new 6.x UEK flavor.
# So even with UPDATEDEFAULT=yes and DEFAULTKERNEL=kernel-uek-core the box keeps booting
# the old kernel. This tool finds the newest installed 6.x UEK kernel and makes it the
# GRUB default via grubby so the next boot comes up on UEK8.
#
# Idempotent: if the UEK8 kernel is already the default it does nothing. It only sets the
# boot default; it does NOT reboot — the admin reboots the node on their own schedule.
log() { echo "[so-kernel-upgrade] $*"; }
[ "$(id -u)" -eq 0 ] || { log "must run as root"; exit 1; }
command -v grubby >/dev/null 2>&1 || { log "grubby not found"; exit 1; }
# Newest installed UEK8 (6.x) kernel known to the bootloader. UEK8 vmlinuz paths look like
# /boot/vmlinuz-6.12.0-203.76.7.5.el9uek.x86_64; the 5.x UEK7 and 5.14 RHCK won't match.
target="$(grubby --info=ALL 2>/dev/null \
| sed -n 's/^kernel="\(.*\)"$/\1/p' \
| grep -E '/vmlinuz-6\.[0-9]+.*uek' \
| sort -V | tail -1)"
if [ -z "$target" ]; then
log "no installed 6.x UEK (UEK8) kernel found — confirm the kernel repo is assigned and"
log "'dnf update' has installed kernel-uek-core. Nothing to do."
exit 0
fi
current="$(grubby --default-kernel 2>/dev/null)"
if [ "$current" = "$target" ]; then
log "UEK8 kernel is already the boot default: $target"
exit 0
fi
log "current default kernel: ${current:-unknown}"
log "switching boot default to UEK8 kernel: $target"
grubby --set-default="$target" || { log "ERROR: grubby --set-default failed for $target"; exit 1; }
# Verify the change actually took before claiming success.
now="$(grubby --default-kernel 2>/dev/null)"
if [ "$now" != "$target" ]; then
log "ERROR: default kernel is still '${now:-unknown}' after set-default"
exit 1
fi
log "boot default is now $target"
log "REBOOT REQUIRED to start using the UEK8 kernel (currently running $(uname -r))."
+34 -20
View File
@@ -5,27 +5,41 @@
# https://securityonion.net/license; you may not use this file except in compliance with the
# Elastic License 2.0.
# Usage: so-restart kibana | playbook
. /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
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"
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"
if [[ $# -lt 1 ]]; then
usage
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
+47 -20
View File
@@ -5,27 +5,54 @@
# https://securityonion.net/license; you may not use this file except in compliance with the
# Elastic License 2.0.
# Usage: so-start all | kibana | playbook
# shellcheck disable=SC1091
. /usr/sbin/so-common
if [ $# -ge 1 ]; then
echo $banner
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 $banner
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 Start Kibana"
echo " $0 kibana --force Force stop all Salt jobs before starting Kibana"
exit 1
}
if [ "$2" = "--force" ]; 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") 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"
if [[ $# -lt 1 ]]; then
usage
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
+25 -13
View File
@@ -5,21 +5,33 @@
# https://securityonion.net/license; you may not use this file except in compliance with the
# Elastic License 2.0.
# Usage: so-stop kibana | playbook | thehive
# shellcheck disable=SC1091
. /usr/sbin/so-common
if [ $# -ge 1 ]; then
echo $banner
printf "Stopping $1...\n"
echo $banner
usage() {
echo "Usage: $0 <component>"
echo ""
echo "Examples:"
echo " $0 kibana Stop Kibana"
exit 1
}
case $1 in
*) docker stop so-$1 ; docker rm so-$1 ;;
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"
if [[ $# -lt 1 ]]; then
usage
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
+2 -1
View File
@@ -63,7 +63,8 @@ function status {
function pcapinfo() {
PCAP=$1
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() {
+1 -1
View File
@@ -173,7 +173,7 @@ eaoptionalintegrationsdir:
{% for minion in node_data %}
{% 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 integration_keys = optional_integrations.keys() %}
fleet_server_integrations_{{ minion }}:
-2
View File
@@ -67,8 +67,6 @@ so-elastic-fleet-package-upgrade:
interval: 30
- require:
- http: wait_for_so-kibana
- onchanges:
- file: /opt/so/state/elastic_fleet_packages.txt
so-elastic-fleet-integrations:
cmd.run:
@@ -30,6 +30,94 @@ fleet_api() {
curl -sK /opt/so/conf/elasticsearch/curl.config -L "localhost:5601/api/fleet/${QUERYPATH}" "$@" --retry 3 --retry-delay 10 --fail 2>/dev/null
}
# Max number of concurrent Fleet write jobs (create/update). Override via env if needed.
MAX_FLEET_JOBS=${MAX_FLEET_JOBS:-10}
# Block until fewer than MAX_FLEET_JOBS background jobs are running.
elastic_fleet_throttle() {
while (( $(jobs -rp | wc -l) >= MAX_FLEET_JOBS )); do
wait -n || true
done
}
# Load every integration JSON in a directory into a single agent policy.
# The agent policy is fetched ONCE (not per file), and the create/update writes
# are dispatched as throttled background jobs.
# $1 AGENT_POLICY - the agent policy id/name to load integrations into
# $2 DIR - directory of integration *.json files
# $3 LABEL - human-readable label for log output
# $4 SKIP_CREATE_NAME - (optional) integration name to skip when creating (still updated if present)
# Returns 1 if the policy cannot be fetched or if any integration failed to create/update.
elastic_fleet_load_integrations_dir() {
local AGENT_POLICY=$1
local DIR=$2
local LABEL=$3
local SKIP_CREATE_NAME=$4
local POLICY_JSON FAIL_FILE OUT_DIR INTEGRATION NAME ID i
FAIL_FILE=$(mktemp)
# Each job buffers its full output (header + API response) into its own file so the
# parent can print them grouped and in submission order after concurrent writes finish.
OUT_DIR=$(mktemp -d)
i=0
# Fetch the agent policy a single time; we look up integration ids locally below.
if ! POLICY_JSON=$(fleet_api "agent_policies/$AGENT_POLICY"); then
echo "Error: Failed to retrieve agent policy '$AGENT_POLICY'."
rm -f "$FAIL_FILE"
rm -rf "$OUT_DIR"
return 1
fi
if ! jq -e '.item.package_policies' <<<"$POLICY_JSON" >/dev/null 2>&1; then
echo "Error: Invalid agent policy response for '$AGENT_POLICY'."
rm -f "$FAIL_FILE"
rm -rf "$OUT_DIR"
return 1
fi
for INTEGRATION in "$DIR"/*.json; do
[ -e "$INTEGRATION" ] || continue
NAME=$(jq -r .name "$INTEGRATION")
ID=$(jq -r --arg n "$NAME" '.item.package_policies[]? | select(.name==$n) | .id' <<<"$POLICY_JSON")
elastic_fleet_throttle
{
local RESP
if [ -n "$ID" ]; then
printf "\n\n%s - Updating integration %s\n" "$LABEL" "$NAME"
if ! RESP=$(elastic_fleet_integration_update "$ID" "@$INTEGRATION"); then
flock 9; echo "update ${INTEGRATION##*/}" >&9
fi
printf '%s\n' "$RESP"
elif [ -n "$SKIP_CREATE_NAME" ] && [ "$NAME" == "$SKIP_CREATE_NAME" ]; then
printf "\n\n%s - Skipping creation of %s\n" "$LABEL" "$NAME"
else
printf "\n\n%s - Creating integration %s\n" "$LABEL" "$NAME"
if ! RESP=$(elastic_fleet_integration_create "@$INTEGRATION"); then
flock 9; echo "create ${INTEGRATION##*/}" >&9
fi
printf '%s\n' "$RESP"
fi
} >"$OUT_DIR/$(printf '%03d' "$i")" 9>>"$FAIL_FILE" &
i=$((i+1))
done
wait || true
# Emit per-integration output grouped and in submission order (glob sorts numerically).
cat "$OUT_DIR"/* 2>/dev/null
rm -rf "$OUT_DIR"
local rc=0
if [ -s "$FAIL_FILE" ]; then
printf "\n%s: failed integrations:\n" "$LABEL"
cat "$FAIL_FILE"
rc=1
fi
rm -f "$FAIL_FILE"
return $rc
}
elastic_fleet_integration_check() {
AGENT_POLICY=$1
@@ -46,7 +134,9 @@ elastic_fleet_integration_create() {
JSON_STRING=$1
if ! fleet_api "package_policies" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -XPOST -d "$JSON_STRING"; then
# --retry-all-errors so transient 409 conflicts (concurrent writes to the same agent
# policy) are retried; curl --retry alone does not retry 409.
if ! fleet_api "package_policies" --retry-all-errors -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -XPOST -d "$JSON_STRING"; then
return 1
fi
}
@@ -77,7 +167,9 @@ elastic_fleet_integration_update() {
JSON_STRING=$2
if ! fleet_api "package_policies/$UPDATE_ID" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -XPUT -d "$JSON_STRING"; then
# --retry-all-errors so transient 409 conflicts (concurrent writes to the same agent
# policy) are retried; curl --retry alone does not retry 409.
if ! fleet_api "package_policies/$UPDATE_ID" --retry-all-errors -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -XPUT -d "$JSON_STRING"; then
return 1
fi
}
@@ -9,102 +9,36 @@
RETURN_CODE=0
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
# Third, configure Elastic Defend Integration seperately
# configure Elastic Defend Integration separately
/usr/sbin/so-elastic-fleet-integration-policy-elastic-defend
# Each group fetches its agent policy once and dispatches create/update writes concurrently.
# Initial Endpoints
for INTEGRATION in /opt/so/conf/elastic-fleet/integrations/endpoints-initial/*.json; do
printf "\n\nInitial Endpoints Policy - Loading $INTEGRATION\n"
elastic_fleet_integration_check "endpoints-initial" "$INTEGRATION"
if [ -n "$INTEGRATION_ID" ]; then
printf "\n\nIntegration $NAME exists - Updating integration\n"
if ! elastic_fleet_integration_update "$INTEGRATION_ID" "@$INTEGRATION"; then
echo -e "\nFailed to update integration for ${INTEGRATION##*/}"
RETURN_CODE=1
continue
fi
else
printf "\n\nIntegration does not exist - Creating integration\n"
if ! elastic_fleet_integration_create "@$INTEGRATION"; then
echo -e "\nFailed to create integration for ${INTEGRATION##*/}"
RETURN_CODE=1
continue
fi
fi
done
elastic_fleet_load_integrations_dir "endpoints-initial" \
/opt/so/conf/elastic-fleet/integrations/endpoints-initial "Initial Endpoints Policy" || RETURN_CODE=1
# Grid Nodes - General
for INTEGRATION in /opt/so/conf/elastic-fleet/integrations/grid-nodes_general/*.json; do
printf "\n\nGrid Nodes Policy_General - Loading $INTEGRATION\n"
elastic_fleet_integration_check "so-grid-nodes_general" "$INTEGRATION"
if [ -n "$INTEGRATION_ID" ]; then
printf "\n\nIntegration $NAME exists - Updating integration\n"
if ! elastic_fleet_integration_update "$INTEGRATION_ID" "@$INTEGRATION"; then
echo -e "\nFailed to update integration for ${INTEGRATION##*/}"
RETURN_CODE=1
continue
fi
else
printf "\n\nIntegration does not exist - Creating integration\n"
if ! elastic_fleet_integration_create "@$INTEGRATION"; then
echo -e "\nFailed to create integration for ${INTEGRATION##*/}"
RETURN_CODE=1
continue
fi
fi
done
elastic_fleet_load_integrations_dir "so-grid-nodes_general" \
/opt/so/conf/elastic-fleet/integrations/grid-nodes_general "Grid Nodes Policy_General" || RETURN_CODE=1
# Grid Nodes - Heavy
for INTEGRATION in /opt/so/conf/elastic-fleet/integrations/grid-nodes_heavy/*.json; do
printf "\n\nGrid Nodes Policy_Heavy - Loading $INTEGRATION\n"
elastic_fleet_integration_check "so-grid-nodes_heavy" "$INTEGRATION"
if [ -n "$INTEGRATION_ID" ]; then
printf "\n\nIntegration $NAME exists - Updating integration\n"
if ! elastic_fleet_integration_update "$INTEGRATION_ID" "@$INTEGRATION"; then
echo -e "\nFailed to update integration for ${INTEGRATION##*/}"
RETURN_CODE=1
continue
fi
else
printf "\n\nIntegration does not exist - Creating integration\n"
if ! elastic_fleet_integration_create "@$INTEGRATION"; then
echo -e "\nFailed to create integration for ${INTEGRATION##*/}"
RETURN_CODE=1
continue
fi
fi
done
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
# Fleet Server - Optional integrations
for INTEGRATION in /opt/so/conf/elastic-fleet/integrations-optional/FleetServer*/*.json; do
if ! [ "$INTEGRATION" == "/opt/so/conf/elastic-fleet/integrations-optional/FleetServer*/*.json" ]; then
FLEET_POLICY=`echo "$INTEGRATION"| cut -d'/' -f7`
printf "\n\nFleet Server Policy - Loading $INTEGRATION\n"
elastic_fleet_integration_check "$FLEET_POLICY" "$INTEGRATION"
if [ -n "$INTEGRATION_ID" ]; then
printf "\n\nIntegration $NAME exists - Updating integration\n"
if ! elastic_fleet_integration_update "$INTEGRATION_ID" "@$INTEGRATION"; then
echo -e "\nFailed to update integration for ${INTEGRATION##*/}"
RETURN_CODE=1
continue
fi
else
printf "\n\nIntegration does not exist - Creating integration\n"
if [ "$NAME" != "elasticsearch-logs" ]; then
if ! elastic_fleet_integration_create "@$INTEGRATION"; then
echo -e "\nFailed to create integration for ${INTEGRATION##*/}"
RETURN_CODE=1
continue
fi
fi
fi
fi
# 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
[ -d "$FLEET_DIR" ] || continue
INTEGRATIONS=("${FLEET_DIR%/}"/*.json)
[ -e "${INTEGRATIONS[0]}" ] || continue
FLEET_POLICY=$(basename "$FLEET_DIR")
elastic_fleet_load_integrations_dir "$FLEET_POLICY" \
"${FLEET_DIR%/}" "Fleet Server Policy" "elasticsearch-logs" || RETURN_CODE=1
done
# Only create the state file if all policies were created/updated successfully
@@ -23,73 +23,90 @@ if [ $? -ne 0 ]; then
fi
default_packages=({% for pkg in SUPPORTED_PACKAGES %}"{{ pkg }}"{% if not loop.last %} {% endif %}{% endfor %})
# JSON array of the default packages, used by the jq filter below.
default_packages_json=$(printf '%s\n' "${default_packages[@]}" | jq -R . | jq -s '.')
# Output lock (serializes concurrent job output) and failure file (one marker line per
# failed integration). Mirrors the pattern used by elastic_fleet_load_integrations_dir.
OUTPUT_LOCK=$(mktemp)
FAIL_FILE=$(mktemp)
trap 'rm -f "$OUTPUT_LOCK" "$FAIL_FILE"' EXIT
# Cache of package name -> latest available version, so the same package is only looked up
# once instead of once per (policy, integration).
declare -A LATEST_VERSION_CACHE
ERROR=false
for AGENT_POLICY in $agent_policies; do
if ! integrations=$(elastic_fleet_integration_policy_names "$AGENT_POLICY"); then
# Fetch the agent policy a single time; package name/version and integration id are all
# extracted locally below instead of re-fetching the same policy per integration.
if ! POLICY_JSON=$(fleet_api "agent_policies/$AGENT_POLICY"); then
# this script upgrades default integration packages, exit 1 and let salt handle retrying
exit 1
fi
for INTEGRATION in $integrations; do
if ! [[ "$INTEGRATION" == "elastic-defend-endpoints" ]] && ! [[ "$INTEGRATION" == "fleet_server-"* ]]; then
# Get package name so we know what package to look for when checking the current and latest available version
if ! PACKAGE_NAME=$(elastic_fleet_integration_policy_package_name "$AGENT_POLICY" "$INTEGRATION"); then
# One jq pass emits name/package.name/package.version/id for every eligible integration.
# The endpoint/fleet_server skips and the default-package gate are applied here in jq.
# $defaults (not $def, a jq reserved keyword) holds the default package list.
while IFS=$'\t' read -r INTEGRATION PACKAGE_NAME PACKAGE_VERSION INTEGRATION_ID; do
[ -n "$INTEGRATION" ] || continue
# Look up the latest available version once per package, then memoize it.
if [[ -z "${LATEST_VERSION_CACHE[$PACKAGE_NAME]+set}" ]]; then
if ! AVAILABLE_VERSION=$(elastic_fleet_package_latest_version_check "$PACKAGE_NAME"); then
echo "Error: Failed getting latest version for $PACKAGE_NAME"
exit 1
fi
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
if [[ " ${default_packages[@]} " =~ " $PACKAGE_NAME " ]]; then
{%- endif %}
# Get currently installed version of package
attempt=0
max_attempts=3
while [ $attempt -lt $max_attempts ]; do
if PACKAGE_VERSION=$(elastic_fleet_integration_policy_package_version "$AGENT_POLICY" "$INTEGRATION") && AVAILABLE_VERSION=$(elastic_fleet_package_latest_version_check "$PACKAGE_NAME"); then
break
fi
attempt=$((attempt + 1))
done
if [ $attempt -eq $max_attempts ]; then
echo "Error: Failed getting $PACKAGE_VERSION or $AVAILABLE_VERSION"
exit 1
fi
# Get integration ID
if ! INTEGRATION_ID=$(elastic_fleet_integration_id "$AGENT_POLICY" "$INTEGRATION"); then
exit 1
fi
if [[ "$PACKAGE_VERSION" != "$AVAILABLE_VERSION" ]]; then
# Dry run of the upgrade
echo ""
echo "Current $PACKAGE_NAME package version ($PACKAGE_VERSION) is not the same as the latest available package ($AVAILABLE_VERSION)..."
echo "Upgrading $INTEGRATION..."
echo "Starting dry run..."
if ! DRYRUN_OUTPUT=$(elastic_fleet_integration_policy_dryrun_upgrade "$INTEGRATION_ID"); then
exit 1
fi
DRYRUN_ERRORS=$(echo "$DRYRUN_OUTPUT" | jq .[].hasErrors)
# If no errors with dry run, proceed with actual upgrade
if [[ "$DRYRUN_ERRORS" == "false" ]]; then
echo "No errors detected. Proceeding with upgrade..."
if ! elastic_fleet_integration_policy_upgrade "$INTEGRATION_ID"; then
echo "Error: Upgrade failed for $PACKAGE_NAME with integration ID '$INTEGRATION_ID'."
ERROR=true
continue
fi
else
echo "Errors detected during dry run for $PACKAGE_NAME policy upgrade..."
ERROR=true
continue
fi
fi
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
fi
{%- endif %}
LATEST_VERSION_CACHE[$PACKAGE_NAME]=$AVAILABLE_VERSION
fi
done
AVAILABLE_VERSION=${LATEST_VERSION_CACHE[$PACKAGE_NAME]}
if [[ "$PACKAGE_VERSION" != "$AVAILABLE_VERSION" ]]; then
# Dry run, then (if clean) the actual upgrade, dispatched as a throttled background
# job. Each job builds its full log into one block, then flushes it under a single
# shared lock (OUTPUT_LOCK) so concurrent jobs never interleave on stdout; a failed
# job also appends a marker line to FAIL_FILE while holding that same lock.
elastic_fleet_throttle
{
block=$'\n'"Current $PACKAGE_NAME package version ($PACKAGE_VERSION) is not the same as the latest available package ($AVAILABLE_VERSION)..."$'\n'
block+="Upgrading $INTEGRATION..."$'\n'"Starting dry run..."$'\n'
fail=""
if ! DRYRUN_OUTPUT=$(elastic_fleet_integration_policy_dryrun_upgrade "$INTEGRATION_ID"); then
block+="Error: Failed to complete dry run for '$INTEGRATION_ID'."$'\n'
fail="dryrun $INTEGRATION"
elif [[ "$(jq .[].hasErrors <<<"$DRYRUN_OUTPUT")" == "false" ]]; then
block+="No errors detected. Proceeding with upgrade..."$'\n'
if ! elastic_fleet_integration_policy_upgrade "$INTEGRATION_ID"; then
block+="Error: Upgrade failed for $PACKAGE_NAME with integration ID '$INTEGRATION_ID'."$'\n'
fail="upgrade $INTEGRATION"
fi
else
block+="Errors detected during dry run for $PACKAGE_NAME policy upgrade..."$'\n'
fail="dryrun-errors $INTEGRATION"
fi
{
flock 9
printf '%s' "$block"
[ -n "$fail" ] && printf '%s\n' "$fail" >>"$FAIL_FILE"
} 9>>"$OUTPUT_LOCK"
} &
fi
done < <(jq -r --argjson defaults "$default_packages_json" '
.item.package_policies[]
| select(.name != "elastic-defend-endpoints")
| select(.name | startswith("fleet_server-") | not)
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
| select(.package.name | IN($defaults[]))
{%- endif %}
| [.name, .package.name, .package.version, .id] | @tsv
' <<<"$POLICY_JSON")
done
if [[ "$ERROR" == "true" ]]; then
# Barrier: wait for every dispatched dry-run/upgrade job to finish.
wait
if [ -s "$FAIL_FILE" ]; then
printf '\nFailed integration upgrades:\n'
cat "$FAIL_FILE"
exit 1
fi
echo
@@ -16,7 +16,6 @@
STATE_FILE_SUCCESS=/opt/so/state/estemplates.txt
INSTALLED_PACKAGE_LIST=/tmp/esfleet_installed_packages.json
BULK_INSTALL_PACKAGE_LIST=/tmp/esfleet_bulk_install.json
BULK_INSTALL_PACKAGE_TMP=/tmp/esfleet_bulk_install_tmp.json
BULK_INSTALL_OUTPUT=/opt/so/state/esfleet_bulk_install_results.json
INTEGRATION_PACKAGE_COMPONENTS=/opt/so/state/esfleet_package_components.json
INPUT_PACKAGE_COMPONENTS=/opt/so/state/esfleet_input_package_components.json
@@ -29,29 +28,6 @@ PENDING_UPDATE=false
# Requiring some level of manual Elastic Stack configuration before installation
EXCLUDED_INTEGRATIONS=('apm')
version_conversion(){
version=$1
echo "$version" | awk -F '.' '{ printf("%d%03d%03d\n", $1, $2, $3); }'
}
compare_versions() {
version1=$1
version2=$2
# Convert versions to numbers
num1=$(version_conversion "$version1")
num2=$(version_conversion "$version2")
# Compare using bc
if (( $(echo "$num1 < $num2" | bc -l) )); then
echo "less"
elif (( $(echo "$num1 > $num2" | bc -l) )); then
echo "greater"
else
echo "equal"
fi
}
IFS=$'\n'
agent_policies=$(elastic_fleet_agent_policy_ids)
if [ $? -ne 0 ]; then
@@ -63,23 +39,23 @@ default_packages=({% for pkg in SUPPORTED_PACKAGES %}"{{ pkg }}"{% if not loop.l
in_use_integrations=()
# Fetch each agent policy once; its package_policies[] already contain both the integration name
# and the .package.name, so extract all non-default package names locally in a single jq instead
# of re-fetching the same policy per integration.
default_packages_json=$(printf '%s\n' "${default_packages[@]}" | jq -R . | jq -s '.')
for AGENT_POLICY in $agent_policies; do
if ! integrations=$(elastic_fleet_integration_policy_names "$AGENT_POLICY"); then
if ! policy_json=$(fleet_api "agent_policies/$AGENT_POLICY"); then
# skip the agent policy if we can't get required info, let salt retry. Integrations loaded by this script are non-default integrations.
echo "Skipping $AGENT_POLICY.. "
continue
fi
for INTEGRATION in $integrations; do
if ! PACKAGE_NAME=$(elastic_fleet_integration_policy_package_name "$AGENT_POLICY" "$INTEGRATION"); then
echo "Not adding $INTEGRATION, couldn't get package name"
continue
fi
# non-default integrations that are in-use in any policy
if ! [[ " ${default_packages[@]} " =~ " $PACKAGE_NAME " ]]; then
in_use_integrations+=("$PACKAGE_NAME")
fi
done
# non-default integrations that are in-use in any policy
while IFS= read -r PACKAGE_NAME; do
[ -n "$PACKAGE_NAME" ] && in_use_integrations+=("$PACKAGE_NAME")
done < <(jq -r --argjson defaults "$default_packages_json" \
'.item.package_policies[].package.name | select(. as $n | ($defaults | index($n)) | not)' \
<<<"$policy_json")
done
if [[ -f $STATE_FILE_SUCCESS ]]; then
@@ -90,72 +66,55 @@ if [[ -f $STATE_FILE_SUCCESS ]]; then
rm -f $INSTALLED_PACKAGE_LIST
echo $latest_package_list | jq '{packages: [.items[] | {name: .name, latest_version: .version, installed_version: .installationInfo.version, subscription: .conditions.elastic.subscription }]}' >> $INSTALLED_PACKAGE_LIST
while read -r package; do
# get package details
package_name=$(echo "$package" | jq -r '.name')
latest_version=$(echo "$package" | jq -r '.latest_version')
installed_version=$(echo "$package" | jq -r '.installed_version')
subscription=$(echo "$package" | jq -r '.subscription')
bulk_package=$(echo "$package" | jq '{name: .name, version: .latest_version}' )
# Build the bulk install list and the per-package status messages with two jq passes
# instead of a per-package bash loop. The old loop forked ~10 processes per package
# (5 jq + awk/bc for the version compare) and re-parsed/rewrote a growing JSON file on
# every add (O(n^2)). Selection and messages below are identical to that logic.
SUB={% if SUB %}true{% else %}false{% endif %}
AUTOUP={% if AUTO_UPGRADE_INTEGRATIONS %}true{% else %}false{% endif %}
EXCLUDED_JSON=$(printf '%s\n' "${EXCLUDED_INTEGRATIONS[@]}" | jq -R 'select(length>0)' | jq -s '.')
INUSE_JSON=$(printf '%s\n' "${in_use_integrations[@]}" | jq -R 'select(length>0)' | jq -s 'unique')
if [[ ! "${EXCLUDED_INTEGRATIONS[@]}" =~ "$package_name" ]]; then
{% if not SUB %}
if [[ "$subscription" != "basic" && "$subscription" != "null" && -n "$subscription" ]]; then
# pass over integrations that require non-basic elastic license
echo "$package_name integration requires an Elastic license of $subscription or greater... skipping"
continue
else
if [[ "$installed_version" == "null" || -z "$installed_version" ]]; then
echo "$package_name is not installed... Adding to next update."
jq --argjson package "$bulk_package" '.packages += [$package]' $BULK_INSTALL_PACKAGE_LIST > $BULK_INSTALL_PACKAGE_TMP && mv $BULK_INSTALL_PACKAGE_TMP $BULK_INSTALL_PACKAGE_LIST
# vnum replicates the previous version_conversion (%d%03d%03d of the first three dotted
# fields); needs() replicates the excluded/subscription/installed/upgrade/in-use logic.
JQ_DECISION='
def vnum:
[ (split(".")|.[0:3][] | gsub("[^0-9].*";"") | (if .=="" then "0" else . end) | tonumber) ]
| (.[0]//0)*1000000 + (.[1]//0)*1000 + (.[2]//0);
def needs($sub;$autoup;$excluded;$inuse):
.name as $n
| ($n | IN($excluded[]) | not)
and ( $sub or (.subscription==null or .subscription=="basic" or .subscription=="") )
and ( (.installed_version==null or .installed_version=="")
or ( ((.latest_version|vnum) > (.installed_version|vnum))
and ( $autoup or ($n | IN($inuse[]) | not) ) ) );'
PENDING_UPDATE=true
else
results=$(compare_versions "$latest_version" "$installed_version")
if [ $results == "greater" ]; then
{#- When auto_upgrade_integrations is false, skip upgrading in_use_integrations #}
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
if ! [[ " ${in_use_integrations[@]} " =~ " $package_name " ]]; then
{%- endif %}
echo "$package_name is at version $installed_version latest version is $latest_version... Adding to next update."
jq --argjson package "$bulk_package" '.packages += [$package]' $BULK_INSTALL_PACKAGE_LIST > $BULK_INSTALL_PACKAGE_TMP && mv $BULK_INSTALL_PACKAGE_TMP $BULK_INSTALL_PACKAGE_LIST
JQ_ARGS=(--argjson sub "$SUB" --argjson autoup "$AUTOUP" --argjson excluded "$EXCLUDED_JSON" --argjson inuse "$INUSE_JSON")
PENDING_UPDATE=true
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
else
echo "skipping available upgrade for in use integration - $package_name."
fi
{%- endif %}
fi
fi
fi
{% else %}
if [[ "$installed_version" == "null" || -z "$installed_version" ]]; then
echo "$package_name is not installed... Adding to next update."
jq --argjson package "$bulk_package" '.packages += [$package]' $BULK_INSTALL_PACKAGE_LIST > $BULK_INSTALL_PACKAGE_TMP && mv $BULK_INSTALL_PACKAGE_TMP $BULK_INSTALL_PACKAGE_LIST
PENDING_UPDATE=true
else
results=$(compare_versions "$latest_version" "$installed_version")
if [ $results == "greater" ]; then
{#- When auto_upgrade_integrations is false, skip upgrading in_use_integrations #}
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
if ! [[ " ${in_use_integrations[@]} " =~ " $package_name " ]]; then
{%- endif %}
echo "$package_name is at version $installed_version latest version is $latest_version... Adding to next update."
jq --argjson package "$bulk_package" '.packages += [$package]' $BULK_INSTALL_PACKAGE_LIST > $BULK_INSTALL_PACKAGE_TMP && mv $BULK_INSTALL_PACKAGE_TMP $BULK_INSTALL_PACKAGE_LIST
PENDING_UPDATE=true
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
else
echo "skipping available upgrade for in use integration - $package_name."
fi
{%- endif %}
fi
fi
{% endif %}
else
echo "Skipping $package_name..."
fi
done <<< "$(jq -c '.packages[]' "$INSTALLED_PACKAGE_LIST")"
# (a) Per-package status messages (parity with the previous echo output).
jq -r "${JQ_ARGS[@]}" "$JQ_DECISION"'
.packages[]
| .name as $n
| if ($n|IN($excluded[])) then "Skipping \($n)..."
elif (($sub|not) and (.subscription!=null and .subscription!="basic" and .subscription!="")) then
"\($n) integration requires an Elastic license of \(.subscription) or greater... skipping"
elif (.installed_version==null or .installed_version=="") then
"\($n) is not installed... Adding to next update."
elif ((.latest_version|vnum) > (.installed_version|vnum)) then
(if ($autoup or ($n|IN($inuse[])|not))
then "\($n) is at version \(.installed_version) latest version is \(.latest_version)... Adding to next update."
else "skipping available upgrade for in use integration - \($n)." end)
else empty end
' "$INSTALLED_PACKAGE_LIST"
# (b) The bulk install list, built in a single pass.
jq "${JQ_ARGS[@]}" "$JQ_DECISION"'
{packages: [ .packages[] | select(needs($sub;$autoup;$excluded;$inuse)) | {name, version: .latest_version} ]}
' "$INSTALLED_PACKAGE_LIST" > "$BULK_INSTALL_PACKAGE_LIST"
if jq -e '.packages | length > 0' "$BULK_INSTALL_PACKAGE_LIST" >/dev/null; then
PENDING_UPDATE=true
fi
if [ "$PENDING_UPDATE" = true ]; then
# Run chunked install of packages
@@ -12,17 +12,22 @@ PKG_LOAD_FAILURES=0
PKG_LOAD_FAILURES_NAMES=()
{%- for PACKAGE in SUPPORTED_PACKAGES %}
echo "Upgrading {{ PACKAGE }} package..."
if VERSION=$(elastic_fleet_package_latest_version_check "{{ PACKAGE }}"); then
if ! elastic_fleet_package_install "{{ PACKAGE }}" "$VERSION"; then
PKG_LOAD_FAILURES=$((PKG_LOAD_FAILURES + 1))
PKG_LOAD_FAILURES_NAMES+=("{{ PACKAGE }}")
if INSTALLED_VERSION=$(elastic_fleet_package_version_check "{{ PACKAGE }}") && LATEST_VERSION=$(elastic_fleet_package_latest_version_check "{{ PACKAGE }}"); then
if [ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]; then
echo "{{ PACKAGE }} integration version $INSTALLED_VERSION is already at the reported latest version $LATEST_VERSION, skipping upgrade."
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
else
echo "ERROR: Failed to get version information for integration {{ PACKAGE }}"
PKG_LOAD_FAILURES=$((PKG_LOAD_FAILURES + 1))
PKG_LOAD_FAILURES_NAMES+=("{{ PACKAGE }}")
fi
echo
{%- endfor %}
if [ $PKG_LOAD_FAILURES -gt 0 ]; then
@@ -35,6 +40,3 @@ if [ $PKG_LOAD_FAILURES -gt 0 ]; then
else
echo "Successfully upgraded all packages."
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
fi
# Check for package upgrades
so-elastic-fleet-package-upgrade
# Load Integrations for default policies
so-elastic-fleet-integration-policy-load
+1
View File
@@ -5,6 +5,7 @@
{ "remove": { "field": ["host"], "ignore_failure": true } },
{ "json": { "field": "message", "target_field": "message2", "ignore_failure": 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.curve", "target_field": "ssl.curve", "ignore_missing": true } },
{ "rename": { "field": "message2.server_name", "target_field": "ssl.server_name", "ignore_missing": true } },
@@ -11,10 +11,8 @@ ADDON_STATEFILE_SUCCESS=/opt/so/state/addon_estemplates.txt
ELASTICSEARCH_TEMPLATES_DIR="/opt/so/conf/elasticsearch/templates"
SO_TEMPLATES_DIR="${ELASTICSEARCH_TEMPLATES_DIR}/index"
ADDON_TEMPLATES_DIR="${ELASTICSEARCH_TEMPLATES_DIR}/addon-index"
SO_LOAD_FAILURES=0
ADDON_LOAD_FAILURES=0
SO_LOAD_FAILURES_NAMES=()
ADDON_LOAD_FAILURES_NAMES=()
FAILED_NAMES=()
FAILED_COUNT=0
IS_HEAVYNODE="false"
FORCE="false"
VERBOSE="false"
@@ -46,20 +44,86 @@ while [[ $# -gt 0 ]]; do
shift
done
# Max number of concurrent template PUT jobs. Override via env if needed.
MAX_TEMPLATE_JOBS=${MAX_TEMPLATE_JOBS:-10}
# Block until fewer than MAX_TEMPLATE_JOBS background jobs are running.
template_throttle() {
while (( $(jobs -rp | wc -l) >= MAX_TEMPLATE_JOBS )); do
wait -n
done
}
# Per-job failure markers and an output lock for serializing parallel job output.
# Each failed load drops one file (named after the template) into FAIL_DIR; the
# output of each job is flushed as a single block under flock so concurrent jobs
# never interleave their (chatty) retry output.
FAIL_DIR=$(mktemp -d)
OUTPUT_LOCK="${FAIL_DIR}/.output.lock"
: > "$OUTPUT_LOCK"
trap 'rm -rf "$FAIL_DIR"' EXIT
# Record a failure: $1 = the template name/path to report later. Slashes are
# encoded so the path becomes a safe single filename.
record_failure() {
local marker="${1//\//__}"
: > "${FAIL_DIR}/fail.${marker}"
}
# Populate FAILED_NAMES and FAILED_COUNT from the current phase's markers.
# Must run in the current shell (not a command substitution) so the array sticks.
collect_failures() {
FAILED_NAMES=()
FAILED_COUNT=0
local f name
shopt -s nullglob
for f in "${FAIL_DIR}"/fail.*; do
name="${f##*/fail.}"
name="${name//__//}"
FAILED_NAMES+=("$name")
FAILED_COUNT=$((FAILED_COUNT + 1))
done
shopt -u nullglob
}
# Clear markers and names between phases so SO and addon counts stay independent.
reset_failures() {
shopt -s nullglob
rm -f "${FAIL_DIR}"/fail.*
shopt -u nullglob
FAILED_NAMES=()
FAILED_COUNT=0
}
# Print a block of text atomically (under the shared output lock) so the output
# of concurrent background jobs is not interleaved.
locked_echo() {
{ flock 9; printf '%s\n' "$1"; } 9>>"$OUTPUT_LOCK"
}
# Loads one template file via PUT. Intended to be dispatched as a background job.
# $1 uri - e.g. _component_template/foo or _index_template/foo
# $2 file - path to the template JSON
# $3 report_name - name/path to record if this load fails
load_template() {
local uri="$1"
local file="$2"
local report_name="$3"
local out rc=0 block
echo "Loading template file $file"
if ! output=$(retry 3 3 "so-elasticsearch-query $uri -d@$file -XPUT" "{\"acknowledged\":true}"); then
echo "$output"
return 1
# Capture everything (including retry's diagnostic chatter) into one block so
# concurrent jobs never interleave; the whole block is flushed under one flock.
block="Loading template file $file"$'\n'
if ! out=$(retry 3 3 "so-elasticsearch-query $uri -d@$file -XPUT" "{\"acknowledged\":true}" 2>&1); then
block+="$out"$'\n'
rc=1
elif [[ "$VERBOSE" == "true" ]]; then
echo "$output"
block+="$out"$'\n'
fi
{ flock 9; printf '%s' "$block"; } 9>>"$OUTPUT_LOCK"
(( rc != 0 )) && record_failure "$report_name"
}
check_required_component_template_exists() {
@@ -110,6 +174,9 @@ load_component_templates() {
return
fi
# Dispatch loads as throttled background jobs. The barrier (wait) happens in
# the caller after all component groups have been dispatched, since index
# templates must not load until every component template is in place.
for component in "$pattern"/*.json; do
tmpl_name=$(basename "${component%.json}")
@@ -118,10 +185,8 @@ load_component_templates() {
tmpl_name="${tmpl_name%-mappings}-mappings"
fi
if ! load_template "_component_template/${tmpl_name}" "$component"; then
SO_LOAD_FAILURES=$((SO_LOAD_FAILURES + 1))
SO_LOAD_FAILURES_NAMES+=("$component")
fi
template_throttle
load_template "_component_template/${tmpl_name}" "$component" "$component" &
done
}
@@ -172,6 +237,9 @@ if [[ "$FORCE" == "true" || ! -f "$SO_STATEFILE_SUCCESS" ]] && index_templates_e
load_component_templates "Elastic Agent" "elastic-agent"
load_component_templates "Security Onion" "so"
# Barrier: every component template PUT must complete before we snapshot the
# component template list and start loading index templates that depend on them.
wait
component_templates=$(so-elasticsearch-component-templates-list)
echo -e "Loading Security Onion index templates...\n"
for so_idx_tmpl in "${SO_TEMPLATES_DIR}"/*.json; do
@@ -181,7 +249,7 @@ if [[ "$FORCE" == "true" || ! -f "$SO_STATEFILE_SUCCESS" ]] && index_templates_e
# TODO: Better way to load only heavynode specific templates
if ! check_heavynode_compatiable_index_template "$tmpl_name"; then
if [[ "$VERBOSE" == "true" ]]; then
echo "Skipping over $so_idx_tmpl, template is not a heavynode specific index template."
locked_echo "Skipping over $so_idx_tmpl, template is not a heavynode specific index template."
fi
continue
@@ -189,32 +257,34 @@ if [[ "$FORCE" == "true" || ! -f "$SO_STATEFILE_SUCCESS" ]] && index_templates_e
fi
if check_required_component_template_exists "$so_idx_tmpl"; then
if ! load_template "_index_template/$tmpl_name" "$so_idx_tmpl"; then
SO_LOAD_FAILURES=$((SO_LOAD_FAILURES + 1))
SO_LOAD_FAILURES_NAMES+=("$so_idx_tmpl")
fi
template_throttle
load_template "_index_template/$tmpl_name" "$so_idx_tmpl" "$so_idx_tmpl" &
else
echo "Skipping over $so_idx_tmpl due to missing required component template(s)."
SO_LOAD_FAILURES=$((SO_LOAD_FAILURES + 1))
SO_LOAD_FAILURES_NAMES+=("$so_idx_tmpl")
locked_echo "Skipping over $so_idx_tmpl due to missing required component template(s)."
record_failure "$so_idx_tmpl"
continue
fi
done
if [[ $SO_LOAD_FAILURES -eq 0 ]]; then
# Barrier: all SO index template PUTs must finish before tallying failures.
wait
collect_failures
if [[ $FAILED_COUNT -eq 0 ]]; then
echo "All Security Onion core templates loaded successfully."
touch "$SO_STATEFILE_SUCCESS"
else
echo "Encountered $SO_LOAD_FAILURES failure(s) loading templates:"
for failed_template in "${SO_LOAD_FAILURES_NAMES[@]}"; do
echo "Encountered $FAILED_COUNT failure(s) loading templates:"
for failed_template in "${FAILED_NAMES[@]}"; do
echo " - $failed_template"
done
if [[ "$SHOULD_EXIT_ON_FAILURE" == "true" ]]; then
fail "Failed to load all Security Onion core templates successfully."
fi
fi
reset_failures
elif ! index_templates_exist "$SO_TEMPLATES_DIR"; then
echo "No Security Onion core index templates found in ${SO_TEMPLATES_DIR}, skipping."
elif [[ -f "$SO_STATEFILE_SUCCESS" ]]; then
@@ -233,26 +303,27 @@ if should_load_addon_templates; then
tmpl_name=$(basename "${addon_idx_tmpl%-template.json}")
if check_required_component_template_exists "$addon_idx_tmpl"; then
if ! load_template "_index_template/${tmpl_name}" "$addon_idx_tmpl"; then
ADDON_LOAD_FAILURES=$((ADDON_LOAD_FAILURES + 1))
ADDON_LOAD_FAILURES_NAMES+=("$addon_idx_tmpl")
fi
template_throttle
load_template "_index_template/${tmpl_name}" "$addon_idx_tmpl" "$addon_idx_tmpl" &
else
echo "Skipping over $addon_idx_tmpl due to missing required component template(s)."
ADDON_LOAD_FAILURES=$((ADDON_LOAD_FAILURES + 1))
ADDON_LOAD_FAILURES_NAMES+=("$addon_idx_tmpl")
locked_echo "Skipping over $addon_idx_tmpl due to missing required component template(s)."
record_failure "$addon_idx_tmpl"
continue
fi
done
if [[ $ADDON_LOAD_FAILURES -eq 0 ]]; then
# Barrier: all addon index template PUTs must finish before tallying failures.
wait
collect_failures
if [[ $FAILED_COUNT -eq 0 ]]; then
echo "All addon integration templates loaded successfully."
touch "$ADDON_STATEFILE_SUCCESS"
else
echo "Encountered $ADDON_LOAD_FAILURES failure(s) loading addon integration templates:"
for failed_template in "${ADDON_LOAD_FAILURES_NAMES[@]}"; do
echo "Encountered $FAILED_COUNT failure(s) loading addon integration templates:"
for failed_template in "${FAILED_NAMES[@]}"; do
echo " - $failed_template"
done
if [[ "$SHOULD_EXIT_ON_FAILURE" == "true" ]]; then
@@ -6,6 +6,48 @@
. /usr/sbin/so-common
MAX_JOBS=${MAX_ILM_JOBS:-10}
# Lock used to serialize block writes so concurrent jobs never interleave their output.
ILM_OUTPUT_LOCK=$(mktemp)
ILM_FAIL_FILE=$(mktemp)
trap 'rm -f "$ILM_OUTPUT_LOCK" "$ILM_FAIL_FILE"' EXIT
# Policies are loaded concurrently (up to MAX_JOBS at a time) for speed. Each policy's block is
# printed the moment its curl returns, so output appears in COMPLETION ORDER, not the order
# policies are defined in configuration.
echo "Loading ILM policies concurrently; output below appears in completion order, not configuration order."
echo
put_policy() {
local desc="$1" policyname="$2" data="$3" result rc=0
if ! result=$(curl -K /opt/so/conf/elasticsearch/curl.config -s -k -L --fail \
-X PUT "https://localhost:9200/_ilm/policy/${policyname}" \
-H 'Content-Type: application/json' -d"${data}" 2>&1); then
rc=1
elif ! jq -e '.acknowledged == true' <<<"$result" >/dev/null 2>&1; then
rc=1
fi
# curl above ran in parallel; serialize just this block write so concurrent jobs never interleave.
{
flock 200
printf 'Setting up %s policy...\n%s\n\n' "${desc}" "${result}"
if (( rc != 0 )); then
printf '%s\n' "${policyname}" >>"$ILM_FAIL_FILE"
fi
} 200>>"${ILM_OUTPUT_LOCK}"
return "$rc"
}
# Block until fewer than MAX_JOBS background curls are running.
throttle() {
while (( $(jobs -rp | wc -l) >= MAX_JOBS )); do
wait -n || true
done
}
{%- from 'elasticsearch/template.map.jinja' import ES_INDEX_SETTINGS %}
{%- if GLOBALS.role != "so-heavynode" %}
{%- from 'elasticsearch/template.map.jinja' import ALL_ADDON_SETTINGS %}
@@ -14,35 +56,36 @@
{%- for index, settings in ES_INDEX_SETTINGS.items() %}
{%- if settings.policy is defined %}
{%- if index == 'so-logs-detections.alerts' %}
echo
echo "Setting up so-logs-detections.alerts-so policy..."
curl -K /opt/so/conf/elasticsearch/curl.config -b "sid=$SESSIONCOOKIE" -s -k -L -X PUT "https://localhost:9200/_ilm/policy/{{ index }}-so" -H 'Content-Type: application/json' -d'{ "policy": {{ settings.policy | tojson(true) }} }'
echo
throttle
put_policy "so-logs-detections.alerts-so" "{{ index }}-so" '{ "policy": {{ settings.policy | tojson(true) }} }' &
{%- elif index == 'so-logs-soc' %}
echo
echo "Setting up so-soc-logs policy..."
curl -K /opt/so/conf/elasticsearch/curl.config -b "sid=$SESSIONCOOKIE" -s -k -L -X PUT "https://localhost:9200/_ilm/policy/so-soc-logs" -H 'Content-Type: application/json' -d'{ "policy": {{ settings.policy | tojson(true) }} }'
echo
echo
echo "Setting up {{ index }}-logs policy..."
curl -K /opt/so/conf/elasticsearch/curl.config -b "sid=$SESSIONCOOKIE" -s -k -L -X PUT "https://localhost:9200/_ilm/policy/{{ index }}-logs" -H 'Content-Type: application/json' -d'{ "policy": {{ settings.policy | tojson(true) }} }'
echo
throttle
put_policy "so-soc-logs" "so-soc-logs" '{ "policy": {{ settings.policy | tojson(true) }} }' &
throttle
put_policy "{{ index }}-logs" "{{ index }}-logs" '{ "policy": {{ settings.policy | tojson(true) }} }' &
{%- else %}
echo
echo "Setting up {{ index }}-logs policy..."
curl -K /opt/so/conf/elasticsearch/curl.config -b "sid=$SESSIONCOOKIE" -s -k -L -X PUT "https://localhost:9200/_ilm/policy/{{ index }}-logs" -H 'Content-Type: application/json' -d'{ "policy": {{ settings.policy | tojson(true) }} }'
echo
throttle
put_policy "{{ index }}-logs" "{{ index }}-logs" '{ "policy": {{ settings.policy | tojson(true) }} }' &
{%- endif %}
{%- endif %}
{%- endfor %}
echo
{%- if GLOBALS.role != "so-heavynode" %}
{%- for index, settings in ALL_ADDON_SETTINGS.items() %}
{%- if settings.policy is defined %}
echo
echo "Setting up {{ index }}-logs policy..."
curl -K /opt/so/conf/elasticsearch/curl.config -b "sid=$SESSIONCOOKIE" -s -k -L -X PUT "https://localhost:9200/_ilm/policy/{{ index }}-logs" -H 'Content-Type: application/json' -d'{ "policy": {{ settings.policy | tojson(true) }} }'
echo
throttle
put_policy "{{ index }}-logs" "{{ index }}-logs" '{ "policy": {{ settings.policy | tojson(true) }} }' &
{%- endif %}
{%- endfor %}
{%- endif %}
wait || true
if [[ -s "$ILM_FAIL_FILE" ]]; then
echo "ERROR: Failed to load ILM policy(s):"
while read -r POLICY; do
echo " - $POLICY"
done < "$ILM_FAIL_FILE"
exit 1
else
echo "Successfully loaded all ILM policies."
fi
+1 -1
View File
@@ -69,7 +69,7 @@ wait_for_so-kibana:
- ssl: True
- verify_ssl: False
- status: 200
- wait_for: 300
- wait_for: 600
- request_interval: 15
- require:
- docker_container: so-kibana
+2
View File
@@ -0,0 +1,2 @@
https://repo.securityonion.net/file/so-repo/prod/3/oracle/9-uek8
https://repo-alt.securityonion.net/prod/3/oracle/9-uek8
+6 -1
View File
@@ -10,4 +10,9 @@ keepcache=0
name=Security Onion Repo repo
mirrorlist=file:///opt/so/conf/reposync/mirror.txt
enabled=1
gpgcheck=1
gpgcheck=1
[securityonionkernelsync]
name=Security Onion Kernel Repo repo
mirrorlist=file:///opt/so/conf/reposync/mirror-kernel.txt
enabled=1
gpgcheck=1
+29
View File
@@ -86,6 +86,28 @@ repo_dir:
- group
- show_changes: False
kernelrepo_dir:
file.directory:
- name: /nsm/kernelrepo
- user: socore
- group: socore
- recurse:
- user
- group
- show_changes: False
# Ensure /nsm/kernelrepo is always a valid (if empty) repo before it is ever assigned to
# a client. Without repodata/repomd.xml an enabled file:///nsm/kernelrepo repo makes every
# dnf operation fail; so-repo-sync only populates it after the highstate, so seed an empty
# repo here. Only runs when repodata is missing, so it won't clobber a synced repo.
kernelrepo_init_empty:
cmd.run:
- name: createrepo /nsm/kernelrepo
- unless: 'test -e /nsm/kernelrepo/repodata/repomd.xml'
- require:
- file: kernelrepo_dir
- pkg: install_createrepo
manager_sbin:
file.recurse:
- name: /usr/sbin
@@ -122,6 +144,13 @@ so-repo-mirrorlist:
- user: socore
- group: socore
so-repo-kernel-mirrorlist:
file.managed:
- name: /opt/so/conf/reposync/mirror-kernel.txt
- source: salt://manager/files/mirror-kernel.txt
- user: socore
- group: socore
so-repo-sync:
{% if MANAGERMERGED.reposync.enabled %}
cron.present:
+11
View File
@@ -10,5 +10,16 @@ NOROOT=1
set -e
curl --retry 5 --retry-delay 60 -A "reposync/$(sync_options)" https://sigs.securityonion.net/checkup --output /tmp/checkup
dnf reposync --norepopath -g --delete -m -c /opt/so/conf/reposync/repodownload.conf --repoid=securityonionsync --download-metadata -p /nsm/repo/
createrepo /nsm/repo
# The kernel repo section is deployed to repodownload.conf by the manager highstate, which
# runs AFTER this script during soup. On the first upgrade to a kernel-aware version the
# on-disk config still predates the section, so guard on its presence to avoid dnf's
# "Unknown repo: 'securityonionkernelsync'" aborting the sync (set -e). The next sync after the
# highstate deploys the section will pick it up.
if grep -q '^\[securityonionkernelsync\]' /opt/so/conf/reposync/repodownload.conf; then
dnf reposync --norepopath -g --delete -m -c /opt/so/conf/reposync/repodownload.conf --repoid=securityonionkernelsync --download-metadata -p /nsm/kernelrepo/
createrepo /nsm/kernelrepo
fi
+41 -9
View File
@@ -245,6 +245,7 @@ check_airgap() {
UPDATE_DIR=/tmp/soagupdate/SecurityOnion
AGDOCKER=/tmp/soagupdate/docker
AGREPO=/tmp/soagupdate/minimal/Packages
AGUEKREPO=/tmp/soagupdate/uek/Packages
else
is_airgap=1
fi
@@ -386,10 +387,11 @@ highstate() {
masterlock() {
echo "Locking Salt Master"
mv -v $TOPFILE $BACKUPTOPFILE
echo "base:" > $TOPFILE
echo " $MINIONID:" >> $TOPFILE
echo " - ca" >> $TOPFILE
echo " - elasticsearch" >> $TOPFILE
# Render the real top file only for the host running soup; every other
# minion gets an empty top (no states) while the master is upgrading.
echo "{% if grains['id'] == '$MINIONID' %}" > $TOPFILE
cat $BACKUPTOPFILE >> $TOPFILE
echo "{% endif %}" >> $TOPFILE
}
masterunlock() {
@@ -849,6 +851,28 @@ kibana_backport_streams_index_template() {
}
# Runs kafka-features.sh upgrade --release-version $1
# Upgrades Kafka KRaft cluster metadata
update_kafka_metadata() {
metadata_version="$1"
global_pillar="/opt/so/saltstack/local/pillar/global/soc_global.sls"
if PIPELINE=$(so-yaml.py get -r "$global_pillar" global.pipeline 2> /dev/null) && [[ "$PIPELINE" == "KAFKA" ]]; then
kafka_nodes_raw=$(salt-call pillar.get kafka:nodes --out=json)
if kafka_nodes=$(jq -er '.local | select(type == "object" and length > 0)' <<< "$kafka_nodes_raw"); then
bootstrap_servers=$(jq -r '[to_entries[] | select(.value.role | contains("broker")) | "\(.value.ip):9092"] | join(",")' <<< "$kafka_nodes")
echo "Upgrading Kafka KRaft cluster version"
so-kafka-cli kafka-features.sh --bootstrap-server "$bootstrap_servers" --command-config /opt/kafka/config/kraft/client.properties upgrade --release-version "$metadata_version" 2>/dev/null || true
return 0
else
FINAL_MESSAGE_QUEUE+=("WARNING: Unable to automatically perform Kafka KRaft cluster metadata update. This step can be performed manually using the following command (replacing \$BROKER_IP with the ip of atleast 1 available Kafka broker):")
FINAL_MESSAGE_QUEUE+=(" - so-kafka-cli kafka-features.sh --bootstrap-server \$BROKER_IP:9092 --command-config /opt/kafka/config/kraft/client.properties upgrade --release-version $metadata_version")
fi
else
echo "Nothing to do!"
fi
}
up_to_3.2.0() {
fix_logstash_0013_lumberjack_pipeline_name
@@ -866,6 +890,8 @@ post_to_3.2.0() {
kibana_backport_streams_index_template
update_kafka_metadata "4.3"
POSTVERSION=3.2.0
}
@@ -979,13 +1005,19 @@ update_airgap_rules() {
rsync -a $UPDATE_DIR/agrules/securityonion-resources/* /nsm/securityonion-resources/
}
update_airgap_repo() {
update_airgap_repos() {
# Update the files in the repo
echo "Syncing new updates to /nsm/repo"
rsync -a $AGREPO/* /nsm/repo/
echo "Creating repo"
echo "Syncing new updates to /nsm/repo & /nsm/kernelrepo"
# Airgap soup copies new files into the local repo, but doesn't remove old packages. Retaining the ability to rollback package updates
rsync -a "$AGREPO"/ /nsm/repo/
rsync -a "$AGUEKREPO"/ /nsm/kernelrepo/
dnf -y install yum-utils createrepo_c
echo "Running createrepo for /nsm/repo"
createrepo /nsm/repo
echo "Running createrepo for /nsm/kernelrepo"
createrepo /nsm/kernelrepo
}
update_salt_mine() {
@@ -1741,7 +1773,7 @@ main() {
set -e
if [[ $is_airgap -eq 0 ]]; then
update_airgap_repo
update_airgap_repos
dnf clean all
check_os_updates
elif [[ $OS == 'oracle' ]]; then
+1
View File
@@ -59,6 +59,7 @@ so-nginx:
- /opt/so/conf/navigator/layers/:/opt/socore/html/navigator/assets/so:ro
- /opt/so/conf/navigator/config.json:/opt/socore/html/navigator/assets/config.json:ro
- /nsm/repo:/opt/socore/html/repo:ro
- /nsm/kernelrepo:/opt/socore/html/kernelrepo:ro
- /nsm/rules:/nsm/rules:ro
{% if NGINXMERGED.external_suricata %}
- /opt/so/rules/nids/suri:/surirules:ro
+10
View File
@@ -323,6 +323,16 @@ http {
autoindex_localtime on;
}
location /kernelrepo/ {
allow all;
sendfile on;
sendfile_max_chunk 1m;
autoindex on;
autoindex_exact_size off;
autoindex_format html;
autoindex_localtime on;
}
location /influxdb/ {
auth_request /auth/sessions/whoami;
rewrite /influxdb/api/(.*) /api/$1 break;
+1 -1
View File
@@ -16,7 +16,7 @@ include:
# Install the registry container
so-dockerregistry:
docker_container.running:
- image: ghcr.io/security-onion-solutions/registry:3.0.0
- image: ghcr.io/security-onion-solutions/registry:3.1.1
- hostname: so-registry
- networks:
- sobridge:
+30
View File
@@ -6,6 +6,10 @@
{% from 'repo/client/map.jinja' import REPOPATH with context %}
{% from 'vars/globals.map.jinja' import GLOBALS %}
{% import_yaml 'salt/minion.defaults.yaml' as saltversion %}
{% set saltversion = saltversion.salt.minion.version %}
{% set INSTALLEDSALTVERSION = grains.saltversion %}
{% set role = grains.id.split('_') | last %}
{% set MANAGER = salt['grains.get']('master') %}
{% if grains['os'] == 'OEL' %}
@@ -57,6 +61,32 @@ so_repo:
- enabled: 1
- gpgcheck: 1
# Only assign the kernel repo once this node's running salt matches the version this
# SO release ships. During a soup the grid is mid-salt-upgrade; gating here keeps the
# UEK8 kernel repo (and the kernel update it enables) from activating until the node is
# fully on the target salt, the same way other states defer across the upgrade window.
{% if saltversion | string == INSTALLEDSALTVERSION | string %}
so_kernel_repo:
pkgrepo.managed:
- name: securityonionkernel
- humanname: Security Onion Kernel Repo
{% if GLOBALS.is_manager %}
- baseurl: file:///nsm/kernelrepo/
{% else %}
- baseurl: https://{{ GLOBALS.repo_host }}/kernelrepo
{% endif %}
- enabled: 1
- gpgcheck: 1
# Supplementary kernel repo: tolerate it being empty/unreachable (e.g. before the
# manager has populated /nsm/kernelrepo) so a missing repomd.xml can't make every
# dnf/pkg operation on the grid fail.
- skip_if_unavailable: 1
# Only assign the kernel repo once physical NIC names are pinned by MAC, so the
# UEK8 kernel update can't renumber interfaces SO binds by name (see pin_nic_names
# in salt/common/init.sls, which drops this marker via /usr/sbin/so-nic-pin).
- onlyif: 'test -e /opt/so/state/nic_names_pinned'
{% endif %}
{% endif %}
# TODO: Add a pillar entry for custom repos
+24
View File
@@ -134,6 +134,30 @@ socsigmasopipeline:
- group: 939
- mode: 600
socsigmaplaybookpipeline:
file.managed:
- name: /opt/so/conf/soc/sigma_playbook_pipeline.yaml
- source: salt://soc/files/soc/sigma_playbook_pipeline.yaml
- user: 939
- group: 939
- mode: 600
socplaybookplaceholdermap:
file.managed:
- name: /opt/so/conf/soc/playbook_placeholder_map.yaml
- source: salt://soc/files/soc/playbook_placeholder_map.yaml
- user: 939
- group: 939
- mode: 600
socplaybookplaceholdermapcustom:
file.managed:
- name: /opt/so/conf/soc/playbook_placeholder_map_custom.yaml
- source: salt://soc/files/soc/playbook_placeholder_map_custom.yaml
- user: 939
- group: 939
- mode: 600
socbanner:
file.managed:
- name: /opt/so/conf/soc/banner.md
+7 -4
View File
@@ -1502,6 +1502,9 @@ soc:
- repo: https://github.com/Security-Onion-Solutions/securityonion-resources-playbooks
branch: main
folder: securityonion-normalized
- repo: https://github.com/Security-Onion-Solutions/securityonion-resources-playbooks
branch: published
folder: sigma
airgap:
- repo: file:///nsm/airgap-resources/playbooks/securityonion-resources-playbooks
branch: main
@@ -1771,13 +1774,13 @@ soc:
enabled: true
queries:
- name: Default Query
description: Show all events grouped by the observer host
query: '* | groupby observer.name'
showSubtitle: true
- name: Log Type
description: Show all events grouped by module and dataset
query: '* | groupby event.module* event.dataset'
showSubtitle: true
- name: Observer
description: Show all events grouped by the observer host
query: '* | groupby observer.name'
showSubtitle: true
- name: SOC - Auth
description: Users authenticated to SOC grouped by IP address and identity
query: 'event.dataset:kratos.audit AND msg:*authenticated* | groupby http.request.headers.x-real-ip user.name'
+6 -1
View File
@@ -45,7 +45,10 @@ so-soc:
- /opt/so/conf/soc/motd.md:/opt/sensoroni/html/motd.md:ro
- /opt/so/conf/soc/banner.md:/opt/sensoroni/html/login/banner.md:ro
- /opt/so/conf/soc/sigma_so_pipeline.yaml:/opt/sensoroni/sigma_so_pipeline.yaml:ro
- /opt/so/conf/soc/sigma_final_pipeline.yaml:/opt/sensoroni/sigma_final_pipeline.yaml:rw
- /opt/so/conf/soc/sigma_playbook_pipeline.yaml:/opt/sensoroni/sigma_playbook_pipeline.yaml:ro
- /opt/so/conf/soc/sigma_final_pipeline.yaml:/opt/sensoroni/sigma_final_pipeline.yaml:ro
- /opt/so/conf/soc/playbook_placeholder_map.yaml:/opt/sensoroni/playbook_placeholder_map.yaml:ro
- /opt/so/conf/soc/playbook_placeholder_map_custom.yaml:/opt/sensoroni/playbook_placeholder_map_custom.yaml:ro
- /opt/so/conf/soc/custom.js:/opt/sensoroni/html/js/custom.js:ro
- /opt/so/conf/soc/custom_roles:/opt/sensoroni/rbac/custom_roles:ro
- /opt/so/conf/soc/soc_users_roles:/opt/sensoroni/rbac/users_roles:rw
@@ -99,6 +102,8 @@ so-soc:
- file: soccustomroles
- file: socusersroles
- file: socclientsroles
- file: socplaybookplaceholdermap
- file: socplaybookplaceholdermapcustom
delete_so-soc_so-status.disabled:
file.uncomment:
@@ -0,0 +1,49 @@
# Global Playbook placeholder map: %token% -> event field path.
#
# Loaded by the SOC Playbook module and used to resolve `field|expand:%placeholder%` values
# from an alert when converting playbook questions to OQL.
# Left: the %token% used in a question
# Right: the event field its value is read from (event_data.-nested or bare; the module
# tries both).
#
# Example: with `src_ip: source.ip` (below), a question that writes
# `source.ip|expand: '%src_ip%'` resolves %src_ip% to the alert's source.ip at convert time.
#
# This is the global base layer. To add or override tokens edit playbook_placeholder_map_custom.yaml.
# those entries overlay this map and win on conflict.
CommandLine: process.command_line
CurrentDirectory: process.working_directory
Image: process.executable
ImageLoaded: dll.name
ParentImage: process.parent.executable
ParentName: process.parent.name
ParentProcessGuid: process.parent.entity_id
ProcessGuid: process.entity_id
TargetFilename: file.name
TargetObject: registry.path
TargetUserName: user.target.name
User: user.name
community_id: network.community_id
dns_resolved_ip: dns.resolved_ip
document_id: soc_id
dst_ip: destination.ip
dst_port: destination.port
event_data_source_ip: source.ip
file_path: file.path
file_dirs: process.file_dirs
file_name: process.name
file_paths: process.file_paths
hostname: host.name
private_ip: network.private_ip
public_ip: network.public_ip
related_hosts: related.hosts
related_ip: related.ip
src_ip: source.ip
dns_query_name: dns.query_name
flow_id: log.id.uid
payload: network.data.decoded
rule_category: rule.category
rule_name: rule.name
rule_uuid: rule.uuid
src_port: source.port
@@ -0,0 +1,14 @@
# Custom Playbook placeholder map: %token% -> event field path.
#
#
# Left: the %token% used in a playbook question.
# Right: the event field its value is read from (event_data.-nested or bare; the module tries
# both). Note: a token that is simply named after a flat event field resolves automatically
# without an entry here - only add a mapping when the token name differs from the field name.
#
# Example:
#
# account_id: cloudflare.account_id
#
# A question that writes
# `account_id|expand: '%account_id%'` resolves %account_id% from the alert at convert time.
@@ -0,0 +1,12 @@
name: Security Onion - Playbook Pipeline
priority: 97
transformations:
# Route string fields to their lowercase-normalized .caseless subfield so wildcard
# matches are case-insensitive.
- id: case_insensitive_string_fields
type: field_name_mapping
mapping:
process.executable: process.executable.caseless
process.parent.executable: process.parent.executable.caseless
process.command_line: process.command_line.caseless
process.parent.command_line: process.parent.command_line.caseless
+51
View File
@@ -63,6 +63,14 @@ transformations:
rule_conditions:
- type: logsource
category: antivirus
# OS-agnostic process_creation scoping for product-less (NIDS/host-pivot) rules.
- id: process_creation_os_agnostic
type: add_condition
conditions:
event.category: process
rule_conditions:
- type: logsource
category: process_creation
# Transforms the `Hashes` field to ECS fields
# ECS fields are used by the hash fields emitted by Elastic Defend
# If shipped with Elastic Agent, sysmon logs will also have hashes mapped to ECS fields
@@ -108,6 +116,40 @@ transformations:
- type: logsource
product: windows
category: driver_load
- id: ecs_fix_process_creation
type: field_name_mapping
mapping:
# bare `Hashes` (the combined-string case is broken out above)
winlog.event_data.Hashes: process.hash.sha256
winlog.event_data.IntegrityLevel: process.Ext.token.integrity_level_name
winlog.event_data.ParentName: process.parent.name
rule_conditions:
- type: logsource
product: windows
category: process_creation
- id: ecs_fix_registry_set
type: field_name_mapping
mapping:
winlog.event_data.Details: registry.data.strings
# field rename only; EventType values (SetValue/CreateKey) still differ from
# event.action values (modification/creation)
winlog.event_data.EventType: event.action
rule_conditions:
- type: logsource
product: windows
category: registry_set
- id: ecs_fix_image_load
type: field_name_mapping
mapping:
file.path: dll.path
file.code_signature.signed: dll.code_signature.exists
winlog.event_data.Signature: dll.code_signature.subject_name
file.code_signature.status: dll.code_signature.status
winlog.event_data.Hashes: dll.hash.sha256
rule_conditions:
- type: logsource
product: windows
category: image_load
- id: linux_security_add-fields
type: add_condition
conditions:
@@ -281,6 +323,15 @@ transformations:
rule_conditions:
- type: logsource
category: file_event
# Scope image_load rules to Elastic Endpoint library events (event.category:library, dll.*
# populated).
- id: endpoint_image_load_add-fields
type: add_condition
conditions:
event.category: 'library'
rule_conditions:
- type: logsource
category: image_load
# Maps network rules to all network logs
# This targets all network logs, all services, generated from endpoints and network
- id: network_add-fields
+9 -1
View File
@@ -46,7 +46,15 @@ soc:
syntax: yaml
file: True
global: True
advanced: True
advanced: False
helpLink: security-onion-console-customization
playbook_placeholder_map_custom__yaml:
title: Playbook Placeholder Map
description: Custom mappings of Playbook %placeholder% tokens to event fields.
syntax: yaml
file: True
global: True
advanced: False
helpLink: security-onion-console-customization
config:
licenseKey:
+3 -2
View File
@@ -65,10 +65,11 @@ so-suricata:
- file: suriclassifications
surirulereload:
cmd.run:
cmd.run:
- name: /usr/sbin/so-suricata-reload-rules >> /opt/so/log/suricata/reload.log 2>&1
- onchanges:
- onchanges:
- file: surirulesync
- onlyif: test -f /opt/so/rules/suricata/all-rulesets.rules
- require:
- docker_container: so-suricata
@@ -7,5 +7,59 @@
. /usr/sbin/so-common
retry 60 3 'docker exec so-suricata /opt/suricata/bin/suricatasc -c reload-rules /var/run/suricata/suricata-command.socket' '{"message":"done","return":"OK"}' || fail "The Suricata container was not ready in time."
retry 60 3 'docker exec so-suricata /opt/suricata/bin/suricatasc -c ruleset-reload-nonblocking /var/run/suricata/suricata-command.socket' '{"message":"done","return":"OK"}' || fail "The Suricata container was not ready in time."
RULES_FILE="/opt/so/rules/suricata/all-rulesets.rules"
SOCKET="/var/run/suricata/suricata-command.socket"
SURICATASC="docker exec so-suricata /opt/suricata/bin/suricatasc"
# Format an epoch as a human-readable local timestamp for log messages.
fmt_time() { date -d "@$1" '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null; }
# Prefix each input line with the current timestamp.
timestamp_lines() { while IFS= read -r line; do printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S %Z')" "$line"; done; }
# Epoch of Suricata's last *completed* ruleset reload; non-zero return on failure.
suricata_reload_epoch() {
local out ts
out=$($SURICATASC -c ruleset-reload-time "$SOCKET" 2>/dev/null)
ts=$(echo "$out" | jq -r '.message[0].last_reload // empty' 2>/dev/null)
[ -n "$ts" ] || return 1
date -d "$ts" +%s 2>/dev/null
}
# Trigger a fresh reload and confirm Suricata is running a ruleset at least as new
# as the rules file. Returns 0 only when both hold, so retry keeps going until an
# in-progress reload clears and our own reload completes.
reload_and_verify() {
local out reload_epoch
out=$($SURICATASC -c reload-rules "$SOCKET")
echo "reload-rules: $out"
if [[ "$out" =~ "Reload already in progress" ]]; then
echo "A reload is already in progress; waiting for it to clear so a fresh reload can load the current ruleset."
return 1
fi
if [[ ! "$out" =~ '{"message":"done","return":"OK"}' ]]; then
echo "Suricata not ready or unexpected reload output; will retry."
return 1
fi
reload_epoch=$(suricata_reload_epoch) || { echo "Could not read ruleset-reload-time; will retry."; return 1; }
if [ "$reload_epoch" -ge "$target_mtime" ]; then
echo "Loaded ruleset is current: last reload ($(fmt_time "$reload_epoch")) is newer than rules file ($(fmt_time "$target_mtime"))."
return 0
fi
echo "Loaded ruleset is stale: last reload ($(fmt_time "$reload_epoch")) is older than rules file ($(fmt_time "$target_mtime")); retrying."
return 1
}
# Run the reload/verify, timestamping every line of output (ours and the
# retry/fail helpers') so reload.log shows when each step ran. The pipeline is
# synchronous, so the log is fully flushed and ordered before we exit; the
# script's real exit code is preserved via PIPESTATUS.
{
# Epoch mtime of the ruleset we need Suricata to have loaded. Captured once so
# a file update mid-reload does not move the goalpost.
target_mtime=$(stat -c %Y "$RULES_FILE") || fail "Could not stat the Suricata rules file: $RULES_FILE"
retry 60 3 'reload_and_verify' || fail "Suricata did not load the current ruleset in time."
} 2>&1 | timestamp_lines
exit "${PIPESTATUS[0]}"
+1
View File
@@ -244,6 +244,7 @@
username = "{{ ES_USER }}"
password = "{{ ES_PASS }}"
insecure_skip_verify = true
cluster_health = true
{%- elif grains['role'] in ['so-searchnode'] %}
[[inputs.elasticsearch]]
servers = ["https://{{ NODEIP }}:9200"]
-6
View File
@@ -83,7 +83,6 @@ base:
- zeek
- strelka
- elastalert
- utility
- elasticfleet
- pcap.cleanup
@@ -113,7 +112,6 @@ base:
- zeek
- strelka
- elastalert
- utility
- elasticfleet
- stig
- kafka
@@ -141,7 +139,6 @@ base:
- elastic-fleet-package-registry
- kibana
- elastalert
- utility
- elasticfleet
- stig
- kafka
@@ -168,7 +165,6 @@ base:
- elastic-fleet-package-registry
- kibana
- elastalert
- utility
- elasticfleet
- kafka
@@ -198,7 +194,6 @@ base:
- elastic-fleet-package-registry
- kibana
- elastalert
- utility
- elasticfleet
- stig
- kafka
@@ -222,7 +217,6 @@ base:
- elasticsearch
- elastic-fleet-package-registry
- kibana
- utility
- suricata
- zeek
- elasticfleet
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
# Wait for ElasticSearch to come up, so that we can query for version infromation
echo -n "Waiting for ElasticSearch..."
COUNT=0
ELASTICSEARCH_CONNECTED="no"
while [[ "$COUNT" -le 30 ]]; do
curl -K /opt/so/conf/elasticsearch/curl.config -k --output /dev/null --silent --head --fail -L https://{{ GLOBALS.manager_ip }}:9200
if [ $? -eq 0 ]; then
ELASTICSEARCH_CONNECTED="yes"
echo "connected!"
break
else
((COUNT+=1))
sleep 1
echo -n "."
fi
done
if [ "$ELASTICSEARCH_CONNECTED" == "no" ]; then
echo
echo -e "Connection attempt timed out. Unable to connect to ElasticSearch. \nPlease try: \n -checking log(s) in /var/log/elasticsearch/\n -running 'docker ps' \n -running 'sudo so-elastic-restart'"
echo
exit
fi
echo "Applying cross cluster search config..."
curl -K /opt/so/conf/elasticsearch/curl.config -s -k -XPUT -L https://{{ GLOBALS.manager_ip }}:9200/_cluster/settings \
-H 'Content-Type: application/json' \
-d "{\"persistent\": {\"search\": {\"remote\": {\"{{ grains.host }}\": {\"seeds\": [\"127.0.0.1:9300\"]}}}}}"
-22
View File
@@ -1,22 +0,0 @@
{% from 'allowed_states.map.jinja' import allowed_states %}
{% from 'vars/globals.map.jinja' import GLOBALS %}
{% if sls in allowed_states %}
{% if grains['role'] in ['so-eval', 'so-import'] %}
fixsearch:
cmd.script:
- shell: /bin/bash
- cwd: /opt/so
- source: salt://utility/bin/eval
- template: jinja
- defaults:
GLOBALS: {{ GLOBALS }}
{% endif %}
{% else %}
{{sls}}_state_not_allowed:
test.fail_without_changes:
- name: {{sls}}_state_not_allowed
{% endif %}
+63 -15
View File
@@ -29,8 +29,12 @@ title() {
}
fail_setup() {
local err_msg=$1
if [[ -n "$err_msg" ]]; then
error "$err_msg"
fi
error "Setup encountered an unrecoverable failure, exiting"
touch /root/failure
echo "setup incomplete: $err_msg" > /root/failure
exit 1
}
@@ -697,7 +701,7 @@ compare_main_nic_ip() {
EOM
[[ -n $TESTING ]] || whiptail --title "$whiptail_title" --msgbox "$message" 11 75
kill -SIGINT "$(ps --pid $$ -oppid=)"; fail_setup
kill -SIGINT "$(ps --pid $$ -oppid=)"; fail_setup "Main IP mismatch"
fi
else
# Setup uses MAINIP, but since we ignore the equality condition when using a VPN
@@ -755,8 +759,7 @@ configure_management_bond() {
info "Setting up $bond_name management interface with mode $bond_mode"
if [[ ${#MBNICS[@]} -eq 0 ]]; then
error "[ERROR] No management bond NICs were selected."
fail_setup
fail_setup "No management bond NICs selected"
fi
nmcli -t -f NAME con show | grep -Fxq "$bond_name"
@@ -886,6 +889,7 @@ create_repo() {
title "Create the repo directory"
logCmd "dnf -y install yum-utils createrepo_c"
logCmd "createrepo /nsm/repo"
logCmd "createrepo /nsm/kernelrepo"
}
@@ -913,8 +917,7 @@ detect_os() {
is_rpm=true
is_supported=true
else
info "This OS is not supported. Security Onion requires Oracle Linux 9."
fail_setup
fail_setup "This OS is not supported. Security Onion requires Oracle Linux 9."
fi
info "Found OS: $OS $OSVER"
@@ -922,7 +925,7 @@ detect_os() {
download_elastic_agent_artifacts() {
if ! update_elastic_agent 2>&1 | tee -a "$setup_log"; then
fail_setup
fail_setup "Failed to update Elastic Agent"
fi
}
@@ -1566,7 +1569,7 @@ proxy_validate() {
error "Received error: $proxy_test_err"
if [[ -n $TESTING ]]; then
error "Exiting setup"
kill -SIGINT "$(ps --pid $$ -oppid=)"; fail_setup
kill -SIGINT "$(ps --pid $$ -oppid=)"; fail_setup "Proxy validation failed"
fi
fi
return $ret
@@ -1773,8 +1776,7 @@ ensure_pyyaml() {
local result=$?
set +o pipefail
if [[ $result -ne 0 ]] || ! rpm -q python3-pyyaml >/dev/null 2>&1; then
error "Failed to install python3-pyyaml (exit=$result)"
fail_setup
fail_setup "Failed to install python3-pyyaml (exit=$result)"
fi
info "python3-pyyaml installed successfully"
}
@@ -1812,6 +1814,16 @@ securityonion_repo() {
echo "mirrorlist=file:///etc/yum/mirror.txt" >> /etc/yum.repos.d/securityonion.repo
echo "enabled=1" >> /etc/yum.repos.d/securityonion.repo
echo "gpgcheck=1" >> /etc/yum.repos.d/securityonion.repo
echo "https://repo.securityonion.net/file/so-repo/prod/3/oracle/9-uek8" > /etc/yum/mirror-kernel.txt
echo "https://so-repo-east.s3.us-east-005.backblazeb2.com/prod/3/oracle/9-uek8" >> /etc/yum/mirror-kernel.txt
echo "[securityonionkernel]" > /etc/yum.repos.d/securityonionkernel.repo
echo "name=Security Onion Kernel Repo repo" >> /etc/yum.repos.d/securityonionkernel.repo
echo "mirrorlist=file:///etc/yum/mirror-kernel.txt" >> /etc/yum.repos.d/securityonionkernel.repo
echo "enabled=1" >> /etc/yum.repos.d/securityonionkernel.repo
echo "gpgcheck=1" >> /etc/yum.repos.d/securityonionkernel.repo
# Supplementary kernel repo: tolerate it being empty/unreachable so a missing
# repomd.xml can't make every dnf operation fail before the repo is populated.
echo "skip_if_unavailable=1" >> /etc/yum.repos.d/securityonionkernel.repo
logCmd "dnf repolist"
else
echo "[securityonion]" > /etc/yum.repos.d/securityonion.repo
@@ -1820,6 +1832,13 @@ securityonion_repo() {
echo "enabled=1" >> /etc/yum.repos.d/securityonion.repo
echo "gpgcheck=1" >> /etc/yum.repos.d/securityonion.repo
echo "sslverify=0" >> /etc/yum.repos.d/securityonion.repo
echo "[securityonionkernel]" > /etc/yum.repos.d/securityonionkernel.repo
echo "name=Security Onion Kernel Repo" >> /etc/yum.repos.d/securityonionkernel.repo
echo "baseurl=https://$MSRV/kernelrepo" >> /etc/yum.repos.d/securityonionkernel.repo
echo "enabled=1" >> /etc/yum.repos.d/securityonionkernel.repo
echo "gpgcheck=1" >> /etc/yum.repos.d/securityonionkernel.repo
echo "sslverify=0" >> /etc/yum.repos.d/securityonionkernel.repo
echo "skip_if_unavailable=1" >> /etc/yum.repos.d/securityonionkernel.repo
logCmd "dnf repolist"
fi
elif [[ ! $waitforstate ]]; then
@@ -1829,12 +1848,25 @@ securityonion_repo() {
echo "enabled=1" >> /etc/yum.repos.d/securityonion.repo
echo "gpgcheck=1" >> /etc/yum.repos.d/securityonion.repo
echo "sslverify=0" >> /etc/yum.repos.d/securityonion.repo
echo "[securityonionkernel]" > /etc/yum.repos.d/securityonionkernel.repo
echo "name=Security Onion Kernel Repo" >> /etc/yum.repos.d/securityonionkernel.repo
echo "baseurl=https://$MSRV/kernelrepo" >> /etc/yum.repos.d/securityonionkernel.repo
echo "enabled=1" >> /etc/yum.repos.d/securityonionkernel.repo
echo "gpgcheck=1" >> /etc/yum.repos.d/securityonionkernel.repo
echo "sslverify=0" >> /etc/yum.repos.d/securityonionkernel.repo
echo "skip_if_unavailable=1" >> /etc/yum.repos.d/securityonionkernel.repo
elif [[ $waitforstate ]]; then
echo "[securityonion]" > /etc/yum.repos.d/securityonion.repo
echo "name=Security Onion Repo" >> /etc/yum.repos.d/securityonion.repo
echo "baseurl=file:///nsm/repo/" >> /etc/yum.repos.d/securityonion.repo
echo "enabled=1" >> /etc/yum.repos.d/securityonion.repo
echo "gpgcheck=1" >> /etc/yum.repos.d/securityonion.repo
echo "[securityonionkernel]" > /etc/yum.repos.d/securityonionkernel.repo
echo "name=Security Onion Kernel Repo" >> /etc/yum.repos.d/securityonionkernel.repo
echo "baseurl=file:///nsm/kernelrepo/" >> /etc/yum.repos.d/securityonionkernel.repo
echo "enabled=1" >> /etc/yum.repos.d/securityonionkernel.repo
echo "gpgcheck=1" >> /etc/yum.repos.d/securityonionkernel.repo
echo "skip_if_unavailable=1" >> /etc/yum.repos.d/securityonionkernel.repo
fi
logCmd "dnf repolist all"
if [[ $waitforstate ]]; then
@@ -1850,9 +1882,12 @@ repo_sync_local() {
# Sync the repo from the SO repo locally.
info "Adding Repo Download Configuration"
mkdir -p /nsm/repo
mkdir -p /nsm/kernelrepo
mkdir -p /opt/so/conf/reposync/cache
echo "https://repo.securityonion.net/file/so-repo/prod/3/oracle/9" > /opt/so/conf/reposync/mirror.txt
echo "https://repo-alt.securityonion.net/prod/3/oracle/9" >> /opt/so/conf/reposync/mirror.txt
echo "https://repo.securityonion.net/file/so-repo/prod/3/oracle/9-uek8" > /opt/so/conf/reposync/mirror-kernel.txt
echo "https://repo-alt.securityonion.net/prod/3/oracle/9-uek8" >> /opt/so/conf/reposync/mirror-kernel.txt
echo "[main]" > /opt/so/conf/reposync/repodownload.conf
echo "gpgcheck=1" >> /opt/so/conf/reposync/repodownload.conf
echo "installonly_limit=3" >> /opt/so/conf/reposync/repodownload.conf
@@ -1866,12 +1901,18 @@ repo_sync_local() {
echo "mirrorlist=file:///opt/so/conf/reposync/mirror.txt" >> /opt/so/conf/reposync/repodownload.conf
echo "enabled=1" >> /opt/so/conf/reposync/repodownload.conf
echo "gpgcheck=1" >> /opt/so/conf/reposync/repodownload.conf
echo "[securityonionkernel]" >> /opt/so/conf/reposync/repodownload.conf
echo "name=Security Onion Kernel Repo repo" >> /opt/so/conf/reposync/repodownload.conf
echo "mirrorlist=file:///opt/so/conf/reposync/mirror-kernel.txt" >> /opt/so/conf/reposync/repodownload.conf
echo "enabled=1" >> /opt/so/conf/reposync/repodownload.conf
echo "gpgcheck=1" >> /opt/so/conf/reposync/repodownload.conf
logCmd "dnf repolist"
if [[ ! $is_airgap ]]; then
curl --retry 5 --retry-delay 60 -A "netinstall/$SOVERSION/$OS/$(uname -r)/1" https://sigs.securityonion.net/checkup --output /tmp/install
retry 5 60 "dnf reposync --norepopath -g --delete -m -c /opt/so/conf/reposync/repodownload.conf --repoid=securityonionsync --download-metadata -p /nsm/repo/" >> "$setup_log" 2>&1 || fail_setup
retry 5 60 "dnf reposync --norepopath -g --delete -m -c /opt/so/conf/reposync/repodownload.conf --repoid=securityonionsync --download-metadata -p /nsm/repo/" >> "$setup_log" 2>&1 || fail_setup "Failed to sync repos"
retry 5 60 "dnf reposync --norepopath -g --delete -m -c /opt/so/conf/reposync/repodownload.conf --repoid=securityonionkernel --download-metadata -p /nsm/kernelrepo/" >> "$setup_log" 2>&1 || fail_setup "Failed to sync kernel repos"
# After the download is complete run createrepo
create_repo
fi
@@ -1884,10 +1925,10 @@ saltify() {
if [[ $waitforstate ]]; then
# install all for a manager
retry 30 10 "bash ../salt/salt/scripts/bootstrap-salt.sh -r -M -X stable $SALTVERSION" || fail_setup
retry 30 10 "bash ../salt/salt/scripts/bootstrap-salt.sh -r -M -X stable $SALTVERSION" || fail_setup "Failed to install salt master"
else
# just a minion
retry 30 10 "bash ../salt/salt/scripts/bootstrap-salt.sh -r -X stable $SALTVERSION" || fail_setup
retry 30 10 "bash ../salt/salt/scripts/bootstrap-salt.sh -r -X stable $SALTVERSION" || fail_setup "Failed to install salt minion"
fi
salt_install_module_deps
@@ -1959,7 +2000,7 @@ set_main_ip() {
info "MAINIP=$MAINIP"
info "MNIC_IP=$MNIC_IP"
whiptail_error_message "The management IP could not be determined. Please check the log at /root/sosetup.log and verify the network configuration. Select OK to exit."
fail_setup
fail_setup "Could not determine MAINIP or MNIC_IP"
fi
sleep 1
done
@@ -2163,7 +2204,7 @@ set_initial_firewall_access() {
set_management_interface() {
title "Setting up the main interface"
if [[ $MNIC == "bond1" ]]; then
configure_management_bond || fail_setup
configure_management_bond || fail_setup "Failed to configure management bond"
fi
if [ "$address_type" = 'DHCP' ]; then
@@ -2228,6 +2269,13 @@ update_sudoers_for_testing() {
}
update_packages() {
# Pin physical NIC names by MAC BEFORE pulling packages, so the UEK8 kernel that
# the update below installs can't renumber the interfaces SO binds by name. Doing
# it here (instead of waiting for the common highstate) also drops the
# /opt/so/state/nic_names_pinned marker that gates the kernel repo, so the kernel
# repo is assigned on the very first highstate and the kernel isn't downgraded and
# then re-upgraded. Run-once: so-nic-pin no-ops if the marker already exists.
logCmd "bash ../salt/common/tools/sbin/so-nic-pin"
logCmd "dnf repolist"
logCmd "dnf -y update --allowerasing --exclude=salt*,docker*,containerd*"
RMREPOFILES=("oracle-linux-ol9.repo" "uek-ol9.repo" "virt-ol9.repo")
+11 -12
View File
@@ -9,14 +9,17 @@
# Make sure you are root before doing anything
uid="$(id -u)"
if [ "$uid" -ne 0 ]; then
echo "This script must be run using sudo!"
fail_setup
echo "This script must be run using sudo!" >&2
exit 1
fi
# Save the original argument array since we modify it
original_args=("$@")
cd "$(dirname "$0")" || fail_setup
cd "$(dirname "$0")" || {
echo "Unable to change to setup directory" >&2
exit 1
}
echo "Getting started..."
@@ -87,8 +90,7 @@ if [[ "$setup_type" == 'iso' ]]; then
if [[ $is_rpm ]]; then
is_iso=true
else
echo "Only use 'so-setup iso' for an ISO install on Security Onion ISO images. Please run 'so-setup network' instead."
fail_setup
fail_setup "Only use 'so-setup iso' for an ISO install on Security Onion ISO images. Please run 'so-setup network' instead."
fi
fi
@@ -127,7 +129,7 @@ catch() {
info "Fatal error occurred at $1 in so-setup, failing setup."
grep --color=never "ERROR" "$setup_log" > "$error_log"
whiptail_setup_failed
fail_setup
fail_setup "Fatal error occurred at $1 in so-setup"
}
# Add the progress function for manager node type installs
@@ -235,8 +237,7 @@ case "$setup_type" in
info "Beginning Security Onion $setup_type install"
;;
*)
error "Invalid install type, must be 'iso', 'network' or 'desktop'."
fail_setup
fail_setup "Invalid install type, must be 'iso', 'network' or 'desktop'."
;;
esac
@@ -770,8 +771,7 @@ if ! [[ -f $install_opt_file ]]; then
logCmd "salt-call state.apply -l info registry"
title "Seeding the docker registry"
if ! docker_seed_registry; then
error "Failed to seed the docker registry"
fail_setup
fail_setup "Failed to seed the docker registry"
fi
title "Applying the manager state"
logCmd "salt-call state.apply -l info manager"
@@ -794,8 +794,7 @@ if ! [[ -f $install_opt_file ]]; then
title "Setting up Elastic Fleet"
logCmd "salt-call state.apply elasticfleet.config"
if ! logCmd so-elastic-fleet-setup; then
error "Failed to run so-elastic-fleet-setup"
fail_setup
fail_setup "Failed to run so-elastic-fleet-setup"
fi
mark_setup_complete
set_initial_firewall_access
+3 -3
View File
@@ -143,15 +143,15 @@ main() {
cat $error_log
echo "--------------------------"
exit_code=1
touch /root/failure
echo "Found setup errors. Check $error_log for details" > /root/failure
elif using_iso && cron_error_in_mail_spool; then
echo "WARNING: Unexpected cron job output in mail spool"
exit_code=1
touch /root/failure
echo "Unexpected cron job output found in /var/spool/mail/" > /root/failure
elif is_manager_node && status_failed; then
echo "WARNING: Containers are not in a healthy state"
exit_code=1
touch /root/failure
echo "Containers are not in a healthy state. Check so-status for details" > /root/failure
else
echo "Successfully completed setup!"
touch /root/success