Compare commits

..
Author SHA1 Message Date
Josh Patterson bf249b7adf Allow tuning multiple Logstash pipelines in SOC
Add logstash:pipeline_settings, a per-pipeline YAML block rendered into
pipelines.yml. Unset settings fall back to logstash.yml. Restart logstash
when pipelines.yml changes, and add the missing managerhype annotation.

Fixes #15090
2026-08-19 15:33:26 -04:00
Josh Patterson 6f3f58bd70 Merge pull request #16170 from Security-Onion-Solutions/fix/boot-highstate-marker
FIX: enable so-boot-highstate.service on non-manager nodes
2026-08-19 11:50:39 -04:00
Josh Patterson d62c53fc92 Merge remote-tracking branch 'origin/3/dev' into fix/boot-highstate-marker 2026-08-19 11:39:48 -04:00
Josh Patterson 2f2187f714 Write setup-complete marker on non-manager nodes
so-boot-highstate.service was never enabled outside managers: only the
manager branch of so-setup called mark_setup_complete, so the marker its
service.enabled gates on never existed on sensors, search nodes, receivers,
etc.

Move the marker state into salt.minion.boot_highstate as the sole owner
within a highstate. Non-managers never apply salt.minion during setup, so
reaching it means setup is done and the marker is unconditional -- this also
heals already-installed nodes. Managers keep the legacy startup_states gate,
since they do highstate mid-setup.

Also add the marker to setup.virt for salt-cloud guests (replacing the
startup_states line removed in fabecb82) and to so-setup's non-manager branch.
2026-08-19 11:39:47 -04:00
Jorge Reyes 6c37bc1f9b Merge pull request #16165 from Security-Onion-Solutions/reyesj2-patch-stg
patch issue with fs.protected_symlinks
2026-08-18 15:04:37 -05:00
reyesj2 4b74e2c320 allow for unavailable minions 2026-08-17 15:26:01 -05:00
reyesj2 3744c0bd6c fix issue with fs.protected_symlinks prior to checking for fleet health 2026-08-17 15:24:05 -05:00
Matthew Wright 563b9d7c3b Merge pull request #16158 from Security-Onion-Solutions/mwright/advanced-agent-studio
Agentic: Agent Studio Salt Annotations
2026-08-17 13:10:34 -04:00
Josh Patterson ec91f9b830 Merge pull request #16162 from Security-Onion-Solutions/fix/zeekctl-cron
Disable the Zeek stats log
2026-08-14 16:13:12 -04:00
Josh Brower 3e7f508620 Merge pull request #16161 from Security-Onion-Solutions/fixtests
Add another pcap job fp
2026-08-14 13:59:19 -04:00
Josh Brower c4555a5514 Add another pcap job fp 2026-08-14 13:54:14 -04:00
Josh Brower d4d63fa60a Merge pull request #16160 from Security-Onion-Solutions/fixtests
Add fp check
2026-08-14 11:46:24 -04:00
Josh Brower dcb931b97c Update excluded errors in so-log-check script 2026-08-14 11:23:25 -04:00
Josh Brower 8e6b16bde0 Add fp check 2026-08-14 11:22:12 -04:00
Josh Patterson 63692aa1a0 Merge pull request #16159 from Security-Onion-Solutions/fix/zeekctl-cron
Run zeekctl cron so LogExpireInterval and the other expire settings take effect
2026-08-14 09:49:28 -04:00
Matthew Wright ea502e29d0 agentic salt annotations 2026-08-13 12:09:59 -04:00
14 changed files with 187 additions and 23 deletions
+2
View File
@@ -154,6 +154,8 @@ if [[ $EXCLUDE_FALSE_POSITIVE_ERRORS == 'Y' ]]; then
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|id.orig_h" # false positive (zeek test data)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|emerging-all.rules" # false positive (error in rulename)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|invalid query input" # false positive (Invalid user input in hunt query)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|no data available for the requested dates" # false positive (pcap cypress test submits a job with an empty time frame)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|no job processor" # false positive (same empty-time-frame job on import nodes, where no pcap processor runs)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|example" # false positive (example test data)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|status 200" # false positive (request successful, contained error string in content)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|app_layer.error" # false positive (suricata 7) in stats.log e.g. app_layer.error.imap.parser | Total | 0
+10 -2
View File
@@ -81,6 +81,14 @@ ls_custom_pipeline_conf_{{assigned_pipeline}}_{{pipeline}}:
{% for assigned_pipeline in ASSIGNED_PIPELINES %}
{# per-pipeline overrides from pipelines.yml win over the global logstash.yml values #}
{% set PARSED_OVERRIDES = (LOGSTASH_MERGED.get('pipeline_settings', {}).get(assigned_pipeline, '') | load_yaml) or {} %}
{% if PARSED_OVERRIDES is not mapping %}
{% do salt.log.warning('logstash: ignoring malformed pipeline_settings for pipeline ' ~ assigned_pipeline ~ '; expected "setting: value" pairs') %}
{% endif %}
{% set PIPELINE_OVERRIDES = PARSED_OVERRIDES if PARSED_OVERRIDES is mapping else {} %}
{% set THREADS = PIPELINE_OVERRIDES.get('pipeline.workers', LOGSTASH_MERGED.config.pipeline_x_workers) %}
{% set BATCH = PIPELINE_OVERRIDES.get('pipeline.batch.size', LOGSTASH_MERGED.config.pipeline_x_batch_x_size) %}
{% for CONFIGFILE in LOGSTASH_MERGED.defined_pipelines[assigned_pipeline] %}
ls_pipeline_{{assigned_pipeline}}_{{CONFIGFILE.split('.')[0] | replace("/","_") }}:
file.managed:
@@ -92,8 +100,8 @@ ls_pipeline_{{assigned_pipeline}}_{{CONFIGFILE.split('.')[0] | replace("/","_")
GLOBALS: {{ GLOBALS }}
ES_USER: "{{ salt['pillar.get']('elasticsearch:auth:users:so_elastic_user:user', '') }}"
ES_PASS: "{{ salt['pillar.get']('elasticsearch:auth:users:so_elastic_user:pass', '') }}"
THREADS: {{ LOGSTASH_MERGED.config.pipeline_x_workers }}
BATCH: {{ LOGSTASH_MERGED.config.pipeline_x_batch_x_size }}
THREADS: {{ THREADS }}
BATCH: {{ BATCH }}
{% else %}
- name: /opt/so/conf/logstash/pipelines/{{assigned_pipeline}}/{{CONFIGFILE.split('/')[1]}}
{% endif %}
+10
View File
@@ -60,6 +60,16 @@ logstash:
custom008: PLACEHOLDER
custom009: PLACEHOLDER
custom010: PLACEHOLDER
pipeline_settings:
fleet: ''
manager: ''
receiver: ''
search: ''
custom0: ''
custom1: ''
custom2: ''
custom3: ''
custom4: ''
settings:
lsheap: 500m
config:
+1
View File
@@ -105,6 +105,7 @@ so-logstash:
{% endif %}
- watch:
- file: lsetcsync
- file: lspipelinesyml
- file: trusttheca
{% if GLOBALS.is_manager %}
- file: elasticsearch_cacerts
+13
View File
@@ -1,4 +1,17 @@
{%- from 'logstash/map.jinja' import LOGSTASH_MERGED %}
{%- set PIPELINE_SETTINGS = LOGSTASH_MERGED.get('pipeline_settings', {}) %}
{%- for assigned_pipeline in ASSIGNED_PIPELINES %}
- pipeline.id: {{ assigned_pipeline }}
path.config: "/usr/share/logstash/pipelines/{{ assigned_pipeline }}/"
{%- set parsed = (PIPELINE_SETTINGS.get(assigned_pipeline, '') | load_yaml) or {} %}
{#- anything that is not a set of key/value pairs is ignored rather than allowed to
break this render; config.sls logs a warning for the same input #}
{%- set extra = parsed if parsed is mapping else {} %}
{#- pipeline.id and path.config are generated above; drop them so a pasted example
cannot repoint the pipeline or trigger a duplicate id error #}
{%- do extra.pop('pipeline.id', None) %}
{%- do extra.pop('path.config', None) %}
{%- if extra %}
{{ extra | yaml(False) | indent(2, True) }}
{%- endif %}
{% endfor -%}
+27
View File
@@ -16,6 +16,7 @@ logstash:
heavynode: *assigned_pipelines
searchnode: *assigned_pipelines
manager: *assigned_pipelines
managerhype: *assigned_pipelines
managersearch: *assigned_pipelines
fleet: *assigned_pipelines
defined_pipelines:
@@ -51,6 +52,32 @@ logstash:
custom008: *pipeline_config
custom009: *pipeline_config
custom010: *pipeline_config
pipeline_settings:
manager: &pipeline_settings
description: >-
Pipeline-scoped Logstash settings for this pipeline, written in YAML, one setting
per line. For example, `pipeline.workers: 8`. These are added to this pipeline's
entry in pipelines.yml, and any setting left out here falls back to the value in
logstash.yml. Only pipeline-scoped settings are accepted; an unrecognized setting
name will stop every pipeline on this node from starting, and the error is written
to /opt/so/log/logstash/logstash.log rather than the container's console output.
pipeline.id and path.config are managed by Security Onion and are ignored if set
here.
advanced: True
global: False
multiline: True
syntax: yaml
forcedType: string
helpLink: logstash
duplicates: True
search: *pipeline_settings
receiver: *pipeline_settings
fleet: *pipeline_settings
custom0: *pipeline_settings
custom1: *pipeline_settings
custom2: *pipeline_settings
custom3: *pipeline_settings
custom4: *pipeline_settings
settings:
lsheap:
description: Heap size to use for logstash
+23 -1
View File
@@ -344,6 +344,16 @@ check_cluster_health() {
check_fleet_server() {
echo "Checking that Elastic Fleet Server is responding."
# Before checking fleet health, check for and fix known issue with elastic-agent container and fs.protected_symlinks
local protected_symlinks=$(sysctl -b fs.protected_symlinks)
if [[ "$protected_symlinks" == "1" ]]; then
# disable fs.protected_symlinks and restart elasticfleet
sysctl -w fs.protected_symlinks=0
docker stop so-elastic-fleet; docker rm -f so-elastic-fleet
printf "\nUpdated sysctl fs.protected_symlinks. Restarting fleet before running health check and continuing with soup.\n"
salt-call state.apply elasticfleet queue=True
fi
# Modeled on the wait_for_so-elastic-fleet state check in elasticfleet/enabled.sls,
# which waits for HTTP 200 from the Fleet Server status API.
if curl -sk --fail --retry 3 --retry-delay 10 --max-time 30 "https://localhost:8220/api/status" > /dev/null 2>&1; then
@@ -1036,8 +1046,20 @@ post_to_3.2.0() {
}
### 3.2.0 End ###
### 3.2.0 Scripts ###
### 3.3.0 Scripts ###
# Sets fs.protected_symlinks=0
#
# Elastic Agent docker image chowns its directory to the running UID
# but does not chown the elastic-agent launcher symlink.
# Preventing non-root users from following that launcher symlink.
disable_sysctl_fs_protected_symlink() {
salt -C 'I@stig:enabled' state.single sysctl.present name=fs.protected_symlinks value=0 config=/etc/sysctl.conf || true
}
up_to_3.3.0() {
disable_sysctl_fs_protected_symlink
INSTALLEDVERSION=3.3.0
}
+20 -2
View File
@@ -3,6 +3,8 @@
# https://securityonion.net/license; you may not use this file except in compliance with the
# Elastic License 2.0.
{% from 'vars/globals.map.jinja' import GLOBALS %}
# Manages /etc/systemd/system/so-boot-highstate.service, a Type=oneshot
# RemainAfterExit=yes unit that runs `salt-call state.highstate` exactly once
# per system boot. Replaces the legacy `startup_states: highstate` minion
@@ -19,9 +21,25 @@ so_boot_highstate_unit_file:
- onchanges_in:
- module: systemd_reload
# Non-managers never apply salt.minion during setup, so reaching this state means
# setup is finished and the marker is safe to write unconditionally. This also
# heals nodes installed before this fix, which have no marker and no legacy
# startup_states line to grep for. Managers do highstate mid-setup, so they only
# get the marker from the legacy upgrade signal; fresh installs get it from
# mark_setup_complete in setup/so-functions.
mark_setup_complete:
file.managed:
- name: /opt/so/state/setup-complete
- replace: false
- makedirs: True
{% if GLOBALS.is_manager %}
- onlyif: "grep -qx 'startup_states: highstate' /etc/salt/minion"
{% endif %}
- require_in:
- service: so_boot_highstate_service
# Only enable once setup is complete. Until then the gate file is missing and
# the unit's own ConditionPathExists would no-op it anyway -- this just keeps
# `systemctl is-enabled` honest for the sync_es_users gate.
# the unit's own ConditionPathExists would no-op it anyway.
so_boot_highstate_service:
service.enabled:
- name: so-boot-highstate.service
+4 -16
View File
@@ -87,27 +87,15 @@ set_log_levels:
# so-boot-highstate.service (managed in salt.minion.boot_highstate), which
# runs once per system boot only. Strip the line from /etc/salt/minion on
# upgrade; both the commented and uncommented forms historically existed.
# Ordered after mark_setup_complete (salt.minion.boot_highstate); the manager
# gate there greps for this line, so it must run before we delete it.
remove_startup_states:
file.line:
- name: /etc/salt/minion
- match: 'startup_states: highstate'
- mode: delete
# Upgrade-path bridge: systems that already passed setup under the old gate
# (`grep -x 'startup_states: highstate' /etc/salt/minion`) get a /opt/so/state/setup-complete
# marker so so-boot-highstate.service can be enabled and the so-user_sync cron
# in sync_es_users.sls keeps installing. Setup-in-progress systems instead get
# the marker from `mark_setup_complete` in setup/so-functions at the right
# moment. `replace: false` means we never overwrite a marker once written.
mark_setup_complete_for_upgrades:
file.managed:
- name: /opt/so/state/setup-complete
- replace: false
- makedirs: True
- onlyif: "grep -qx 'startup_states: highstate' /etc/salt/minion"
- require_in:
- file: remove_startup_states
- service: so_boot_highstate_service
- require:
- file: mark_setup_complete
{% endif %}
+9
View File
@@ -8,6 +8,15 @@ set_role_grain:
- name: role
- value: so-{{ grains.id.split("_") | last }}
# salt-cloud guests never run so-setup, so nothing else marks them setup-complete.
# Replaces the 'startup_states: highstate' line this state used to append. No
# GLOBALS import -- this runs before the guest's pillars exist.
mark_setup_complete_vm_guest:
file.managed:
- name: /opt/so/state/setup-complete
- replace: false
- makedirs: True
enable_salt_minion:
service.enabled:
- name: salt-minion
+56
View File
@@ -732,11 +732,13 @@ soc:
global: True
advanced: True
forcedType: int
readonlyUi: True
maxDelegationDepth:
description: Maximum delegation nesting depth for sub-agents. For example, a value of 2 lets the main agent delegate to a sub-agent that may itself delegate one level deeper. Any deeper delegation is refused and the requesting agent continues without it. Set to 0 to disable the limit.
global: True
advanced: True
forcedType: int
readonlyUi: True
adapters:
description: Configuration for AI adapters used by the Onion AI assistant. Please see documentation for help on which fields are required for which protocols.
global: True
@@ -779,6 +781,60 @@ soc:
description: Indicates if the Assistant Module should operate in agentic mode or not. If true, agents can work together to solve tasks.
global: True
forcedType: bool
agents:
description: Agent definitions for the Onion AI assistant, managed from the Agent Studio. An entry naming a system agent overrides only the fields an admin may change; everything else comes from the built-in definition.
global: True
advanced: False
readonlyUi: True
storage: db
forcedType: "[]{}"
helpLink: onion-ai
syntax: json
uiElements:
- field: name
label: Name
required: True
- field: enabled
label: Enabled
forcedType: bool
- field: isOrchestrator
label: Orchestrator
forcedType: bool
- field: model
label: Model
- field: allowedSkills
label: Skills
forcedType: "[]string"
- field: canDelegateTo
label: Delegates To
forcedType: "[]string"
- field: description
label: Description
- field: persona
label: Persona
multiline: True
skills:
description: Skill definitions for the Onion AI assistant, managed from the Agent Studio. An entry naming a system skill overrides only its enabled state and persona addendum; its tool set comes from the built-in definition.
global: True
advanced: False
readonlyUi: True
storage: db
forcedType: "[]{}"
helpLink: onion-ai
syntax: json
uiElements:
- field: name
label: Name
required: True
- field: enabled
label: Enabled
forcedType: bool
- field: tools
label: Tools
forcedType: "[]string"
- field: persona
label: Persona
multiline: True
agentMapping:
Orchestrator:
description: The initial agent in most agentic conversations. This agent will delegate requests to specialized agents.
+9
View File
@@ -65,6 +65,15 @@ run_remediate:
- success_retcodes:
- 2
# Elastic Agent docker image chowns its directory to the running UID but does not
# chown the elastic-agent launcher symlink. fs.protected_symlinks=1 then prevents
# non-root users from following that launcher symlink.
{# OSCAP rule id: xccdf_org.ssgproject.content_rule_sysctl_fs_protected_symlinks #}
fs.protected_symlinks:
sysctl.present:
- value: 0
- config: /etc/sysctl.conf
{# OSCAP rule id: xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_burstaction #}
disable_ctrl_alt_del_action:
file.replace:
+2 -2
View File
@@ -1601,7 +1601,7 @@ DISA STIG for Oracle Linux 9 V1R3.</xccdf-1.2:description>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sudoers_validate_passwd" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_crypto_fips_enabled" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_fs_protected_hardlinks" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_fs_protected_symlinks" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_fs_protected_symlinks" selected="false"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_kernel_core_pattern" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_kernel_dmesg_restrict" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_kernel_exec_shield" selected="true"/>
@@ -2202,7 +2202,7 @@ standard DISA STIG for Oracle Linux 9 profile.</xccdf-1.2:description>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sudoers_validate_passwd" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_crypto_fips_enabled" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_fs_protected_hardlinks" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_fs_protected_symlinks" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_fs_protected_symlinks" selected="false"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_kernel_core_pattern" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_kernel_dmesg_restrict" selected="true"/>
<xccdf-1.2:select idref="xccdf_org.ssgproject.content_rule_sysctl_kernel_exec_shield" selected="true"/>
+1
View File
@@ -833,6 +833,7 @@ if ! [[ -f $install_opt_file ]]; then
check_sos_appliance
drop_install_options
hypervisor_local_states
mark_setup_complete
verify_setup
fi