mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2026-10-05 22:14:51 +02:00
Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd6647e775 | ||
|
|
da2c19188a | ||
|
|
90b3d37be6 | ||
|
|
fcd2f67076 | ||
|
|
a9cdd17694 | ||
|
|
b32aaac290 | ||
|
|
1aee3f28dc | ||
|
|
678cb0d5b2 | ||
|
|
31c5190a1f | ||
|
|
4ce7a06abe | ||
|
|
ba95b9bbc2 | ||
|
|
43475452b3 | ||
|
|
99322cf26a | ||
|
|
117548757f | ||
|
|
22bda63847 | ||
|
|
2a4611df45 | ||
|
|
89f8bcd19f | ||
|
|
563269cbac | ||
|
|
523c39d4f2 | ||
|
|
b4557e973c | ||
|
|
0f53a7e0bc | ||
|
|
8de8ba811a | ||
|
|
d122ee7fea | ||
|
|
9732e1c639 | ||
|
|
53f9ebcd46 | ||
|
|
47d74f1ae1 | ||
|
|
855716846a | ||
|
|
8e35d70595 | ||
|
|
e4625cfcae | ||
|
|
eb803dce0e | ||
|
|
a06f08217a | ||
|
|
29d27cf255 | ||
|
|
235a60e587 | ||
|
|
a8f7c46b0d | ||
|
|
26d895ccb7 | ||
|
|
b43efc458f | ||
|
|
21222ff119 | ||
|
|
88fa7e7fb4 | ||
|
|
efe0581892 |
No files matched your search
@@ -131,6 +131,8 @@ def beacon(config): # noqa: C901
|
|||||||
'setting_id': setting_id,
|
'setting_id': setting_id,
|
||||||
'node_id': node_id,
|
'node_id': node_id,
|
||||||
})
|
})
|
||||||
|
log.info('postgres_pillar_beacon: audit_settings id=%d setting_id=%s node_id=%s',
|
||||||
|
row_id, setting_id, node_id)
|
||||||
if row_id > max_id:
|
if row_id > max_id:
|
||||||
max_id = row_id
|
max_id = row_id
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ if [[ $EXCLUDE_FALSE_POSITIVE_ERRORS == 'Y' ]]; then
|
|||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Unexpected authorization header" # expected WARN log lines indicating invalid auth header
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Unexpected authorization header" # expected WARN log lines indicating invalid auth header
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Missing ory_kratos_session cookie" # expected WARN log lines indicating invalid auth header
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Missing ory_kratos_session cookie" # expected WARN log lines indicating invalid auth header
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Static assets preprocessor only supports GET and HEAD requests" # expected WARN log lines indicating invalid auth header
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Static assets preprocessor only supports GET and HEAD requests" # expected WARN log lines indicating invalid auth header
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|respondError" # respondError is a function name, output via http middleware as standard request logging
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EXCLUDE_KNOWN_ERRORS == 'Y' ]]; then
|
if [[ $EXCLUDE_KNOWN_ERRORS == 'Y' ]]; then
|
||||||
@@ -240,7 +241,7 @@ if [[ $EXCLUDE_KNOWN_ERRORS == 'Y' ]]; then
|
|||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|marked for removal" # docker container getting recycled
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|marked for removal" # docker container getting recycled
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|tcp 127.0.0.1:6791: bind: address already in use" # so-elastic-fleet agent restarting. Seen starting w/ 8.18.8 https://github.com/elastic/kibana/issues/201459
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|tcp 127.0.0.1:6791: bind: address already in use" # so-elastic-fleet agent restarting. Seen starting w/ 8.18.8 https://github.com/elastic/kibana/issues/201459
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|TransformTask\] \[logs-.*user so_kibana lacks the required permissions" # Known issue with integrations starting transform jobs that are explicitly not allowed to start as a system user
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|TransformTask\] \[logs-.*user so_kibana lacks the required permissions" # Known issue with integrations starting transform jobs that are explicitly not allowed to start as a system user
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|manifest unknown" # appears in so-dockerregistry log for so-tcpreplay following docker upgrade to 29.2.1-1
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|manifest unknown" # so-dockerregistry logs a tag lookup miss during image copy; not tied to one docker version
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Could not index event to Elasticsearch.*\"version\" => \"9.0.8\"" # Expected during Elastic upgrade temporarily, as policies referencing older pipelines are updated
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Could not index event to Elasticsearch.*\"version\" => \"9.0.8\"" # Expected during Elastic upgrade temporarily, as policies referencing older pipelines are updated
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ dockergroup:
|
|||||||
dockerheldpackages:
|
dockerheldpackages:
|
||||||
pkg.installed:
|
pkg.installed:
|
||||||
- pkgs:
|
- pkgs:
|
||||||
- containerd.io: 2.2.1-1.el9
|
- containerd.io: 2.3.6-1.el9
|
||||||
- docker-ce: 3:29.2.1-1.el9
|
- docker-ce: 3:29.8.1-1.el9
|
||||||
- docker-ce-cli: 1:29.2.1-1.el9
|
- docker-ce-cli: 1:29.8.1-1.el9
|
||||||
- docker-ce-rootless-extras: 29.2.1-1.el9
|
- docker-ce-rootless-extras: 29.8.1-1.el9
|
||||||
- hold: True
|
- hold: True
|
||||||
- update_holds: True
|
- update_holds: True
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ elastalert:
|
|||||||
- gid: 933
|
- gid: 933
|
||||||
- home: /opt/so/conf/elastalert
|
- home: /opt/so/conf/elastalert
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elastalogdir:
|
elastalogdir:
|
||||||
file.directory:
|
file.directory:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ elastic-agent-pr:
|
|||||||
- gid: 948
|
- gid: 948
|
||||||
- home: /opt/so/conf/elastic-fleet-pr
|
- home: /opt/so/conf/elastic-fleet-pr
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ elastic-agent:
|
|||||||
- gid: 949
|
- gid: 949
|
||||||
- home: /opt/so/conf/elastic-agent
|
- home: /opt/so/conf/elastic-agent
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elasticagentconfdir:
|
elasticagentconfdir:
|
||||||
file.directory:
|
file.directory:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ elastic-fleet:
|
|||||||
- gid: 947
|
- gid: 947
|
||||||
- home: /opt/so/conf/elastic-fleet
|
- home: /opt/so/conf/elastic-fleet
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elasticfleet_sbin:
|
elasticfleet_sbin:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"\\.gz$"
|
"\\.gz$"
|
||||||
],
|
],
|
||||||
"include_files": [],
|
"include_files": [],
|
||||||
"processors": "- dissect:\n tokenizer: \"/nsm/import/%{import.id}/evtx/%{import.file}\"\n field: \"log.file.path\"\n target_prefix: \"\"\n- decode_json_fields:\n fields: [\"message\"]\n target: \"\"\n- drop_fields:\n fields: [\"host\"]\n ignore_missing: true\n- add_fields:\n target: data_stream\n fields:\n type: logs\n dataset: system.security\n- add_fields:\n target: event\n fields:\n dataset: system.security\n module: system\n imported: true\n- add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.security-2.22.3\n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-Sysmon/Operational'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: windows.sysmon_operational\n - add_fields:\n target: event\n fields:\n dataset: windows.sysmon_operational\n module: windows\n imported: true\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-windows.sysmon_operational-3.9.0\n- if:\n equals:\n winlog.channel: 'Application'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: system.application\n - add_fields:\n target: event\n fields:\n dataset: system.application\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.application-2.22.3\n- if:\n equals:\n winlog.channel: 'System'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: system.system\n - add_fields:\n target: event\n fields:\n dataset: system.system\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.system-2.22.3\n \n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-PowerShell/Operational'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: windows.powershell_operational\n - add_fields:\n target: event\n fields:\n dataset: windows.powershell_operational\n module: windows\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-windows.powershell_operational-3.9.0\n- add_fields:\n target: data_stream\n fields:\n dataset: import",
|
"processors": "- dissect:\n tokenizer: \"/nsm/import/%{import.id}/evtx/%{import.file}\"\n field: \"log.file.path\"\n target_prefix: \"\"\n- decode_json_fields:\n fields: [\"message\"]\n target: \"\"\n- add_fields:\n target: event\n fields:\n dataset: windows.forwarded\n module: windows\n imported: true\n- add_fields:\n target: \"@metadata\"\n fields:\n pipeline: import.evtx\n- if:\n equals:\n winlog.channel: 'Security'\n then: \n - add_fields:\n target: event\n fields:\n dataset: system.security\n module: system\n- if:\n equals:\n winlog.channel: 'Windows PowerShell'\n then: \n - add_fields:\n target: event\n fields:\n dataset: windows.powershell\n module: windows\n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-Sysmon/Operational'\n then: \n - add_fields:\n target: event\n fields:\n dataset: windows.sysmon_operational\n module: windows\n imported: true\n- if:\n equals:\n winlog.channel: 'Application'\n then: \n - add_fields:\n target: event\n fields:\n dataset: system.application\n module: system\n- if:\n equals:\n winlog.channel: 'System'\n then: \n - add_fields:\n target: event\n fields:\n dataset: system.system\n module: system\n \n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-PowerShell/Operational'\n then: \n - add_fields:\n target: event\n fields:\n dataset: windows.powershell_operational\n module: windows\n- add_fields:\n target: data_stream\n fields:\n type: logs\n dataset: import",
|
||||||
"tags": [
|
"tags": [
|
||||||
"import"
|
"import"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -30,17 +30,14 @@
|
|||||||
'azure_metrics.monitor': 'azure.monitor',
|
'azure_metrics.monitor': 'azure.monitor',
|
||||||
'azure_metrics.storage_account': 'azure.storage_account',
|
'azure_metrics.storage_account': 'azure.storage_account',
|
||||||
'azure_openai.metrics': 'azure.open_ai',
|
'azure_openai.metrics': 'azure.open_ai',
|
||||||
'beat.state': 'beats.stack_monitoring.state',
|
|
||||||
'beat.stats': 'beats.stack_monitoring.stats',
|
|
||||||
'enterprisesearch.health': 'enterprisesearch.stack_monitoring.health',
|
|
||||||
'enterprisesearch.stats': 'enterprisesearch.stack_monitoring.stats',
|
|
||||||
'kibana.cluster_actions': 'kibana.stack_monitoring.cluster_actions',
|
'kibana.cluster_actions': 'kibana.stack_monitoring.cluster_actions',
|
||||||
'kibana.cluster_rules': 'kibana.stack_monitoring.cluster_rules',
|
'kibana.cluster_rules': 'kibana.stack_monitoring.cluster_rules',
|
||||||
'kibana.node_actions': 'kibana.stack_monitoring.node_actions',
|
'kibana.node_actions': 'kibana.stack_monitoring.node_actions',
|
||||||
'kibana.node_rules': 'kibana.stack_monitoring.node_rules',
|
'kibana.node_rules': 'kibana.stack_monitoring.node_rules',
|
||||||
'kibana.stats': 'kibana.stack_monitoring.stats',
|
'kibana.stats': 'kibana.stack_monitoring.stats',
|
||||||
'kibana.status': 'kibana.stack_monitoring.status',
|
'kibana.status': 'kibana.stack_monitoring.status',
|
||||||
'logstash.node_cel': 'logstash.stack_monitoring.node',
|
'logstash.node': 'logstash.stack_monitoring.node',
|
||||||
|
'logstash.node_cel': 'logstash.node',
|
||||||
'logstash.node_stats': 'logstash.stack_monitoring.node_stats',
|
'logstash.node_stats': 'logstash.stack_monitoring.node_stats',
|
||||||
'synthetics.browser': 'synthetics-browser',
|
'synthetics.browser': 'synthetics-browser',
|
||||||
'synthetics.browser_network': 'synthetics-browser.network',
|
'synthetics.browser_network': 'synthetics-browser.network',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ elasticsearch:
|
|||||||
- gid: 930
|
- gid: 930
|
||||||
- home: /opt/so/conf/elasticsearch
|
- home: /opt/so/conf/elasticsearch
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elasticsearch_sbin:
|
elasticsearch_sbin:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
|
|||||||
@@ -3309,6 +3309,7 @@ elasticsearch:
|
|||||||
composed_of:
|
composed_of:
|
||||||
- event-mappings
|
- event-mappings
|
||||||
- logs-system.security@package
|
- logs-system.security@package
|
||||||
|
- so-fleet_system.security_caseless-1
|
||||||
- logs-system.security@custom
|
- logs-system.security@custom
|
||||||
- so-fleet_integrations.ip_mappings-1
|
- so-fleet_integrations.ip_mappings-1
|
||||||
- so-fleet_globals-1
|
- so-fleet_globals-1
|
||||||
@@ -4175,6 +4176,7 @@ elasticsearch:
|
|||||||
index_template:
|
index_template:
|
||||||
composed_of:
|
composed_of:
|
||||||
- logs-windows.forwarded@package
|
- logs-windows.forwarded@package
|
||||||
|
- so-fleet_process_caseless-1
|
||||||
- logs-windows.forwarded@custom
|
- logs-windows.forwarded@custom
|
||||||
- so-fleet_integrations.ip_mappings-1
|
- so-fleet_integrations.ip_mappings-1
|
||||||
- so-fleet_globals-1
|
- so-fleet_globals-1
|
||||||
@@ -4224,6 +4226,7 @@ elasticsearch:
|
|||||||
index_template:
|
index_template:
|
||||||
composed_of:
|
composed_of:
|
||||||
- logs-windows.powershell@package
|
- logs-windows.powershell@package
|
||||||
|
- so-fleet_process_caseless-1
|
||||||
- logs-windows.powershell@custom
|
- logs-windows.powershell@custom
|
||||||
- so-fleet_integrations.ip_mappings-1
|
- so-fleet_integrations.ip_mappings-1
|
||||||
- so-fleet_globals-1
|
- so-fleet_globals-1
|
||||||
@@ -4273,6 +4276,7 @@ elasticsearch:
|
|||||||
index_template:
|
index_template:
|
||||||
composed_of:
|
composed_of:
|
||||||
- logs-windows.powershell_operational@package
|
- logs-windows.powershell_operational@package
|
||||||
|
- so-fleet_process_caseless-1
|
||||||
- logs-windows.powershell_operational@custom
|
- logs-windows.powershell_operational@custom
|
||||||
- so-fleet_integrations.ip_mappings-1
|
- so-fleet_integrations.ip_mappings-1
|
||||||
- so-fleet_globals-1
|
- so-fleet_globals-1
|
||||||
@@ -4322,6 +4326,7 @@ elasticsearch:
|
|||||||
index_template:
|
index_template:
|
||||||
composed_of:
|
composed_of:
|
||||||
- logs-windows.sysmon_operational@package
|
- logs-windows.sysmon_operational@package
|
||||||
|
- so-fleet_process_caseless-1
|
||||||
- logs-windows.sysmon_operational@custom
|
- logs-windows.sysmon_operational@custom
|
||||||
- so-fleet_integrations.ip_mappings-1
|
- so-fleet_integrations.ip_mappings-1
|
||||||
- so-fleet_globals-1
|
- so-fleet_globals-1
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
"if": "ctx.tags != null && ctx.tags.contains('import')",
|
"if": "ctx.tags != null && ctx.tags.contains('import') && ctx._index != null && ctx._index.startsWith('logs-import-')",
|
||||||
"override": true,
|
"override": true,
|
||||||
"field": "data_stream.dataset",
|
"field": "data_stream.dataset",
|
||||||
"value": "import"
|
"value": "import"
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
"if": "ctx.tags != null && ctx.tags.contains('import')",
|
"if": "ctx.tags != null && ctx.tags.contains('import') && ctx._index != null && ctx._index.startsWith('logs-import-')",
|
||||||
"override": true,
|
"override": true,
|
||||||
"field": "data_stream.namespace",
|
"field": "data_stream.namespace",
|
||||||
"value": "so"
|
"value": "so"
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"description" : "import.evtx: normalize imported EVTX and reroute to logs-<dataset>-import",
|
||||||
|
"processors" : [
|
||||||
|
{ "script": {
|
||||||
|
"description": "Host from the event, not the importing node",
|
||||||
|
"lang": "painless",
|
||||||
|
"source": "Map host = ['os': ['type': 'windows', 'family': 'windows', 'platform': 'windows']]; def cn = ctx.winlog?.computer_name; if (cn != null && cn.toString().length() > 0) { String name = cn.toString(); int dot = name.indexOf('.'); if (dot > 0) { name = name.substring(0, dot); } host.put('hostname', name); host.put('name', name.toLowerCase()); } ctx.host = host;"
|
||||||
|
} },
|
||||||
|
{ "script": {
|
||||||
|
"description": "String event IDs, as Winlogbeat sends",
|
||||||
|
"lang": "painless",
|
||||||
|
"source": "if (ctx.winlog?.event_id != null) { ctx.winlog.event_id = ctx.winlog.event_id.toString(); } if (ctx.event?.code != null) { ctx.event.code = ctx.event.code.toString(); }"
|
||||||
|
} },
|
||||||
|
{ "script": {
|
||||||
|
"description": "Unnamed <Data> to param1..N, as Winlogbeat",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx.winlog?.event_data?.Data instanceof Map && ctx.winlog.event_data.Data['#text'] != null",
|
||||||
|
"source": "def t = ctx.winlog.event_data.Data['#text']; List vals = t instanceof List ? t : [t]; for (int i = 0; i < vals.size(); i++) { ctx.winlog.event_data['param' + (i + 1)] = vals.get(i); } ctx.winlog.event_data.remove('Data');"
|
||||||
|
} },
|
||||||
|
{ "script": {
|
||||||
|
"description": "String values and LF line endings, as Winlogbeat",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx.winlog?.event_data instanceof Map || ctx.winlog?.user_data instanceof Map",
|
||||||
|
"source": "String lf = String.valueOf((char) 10); String crlf = String.valueOf((char) 13) + lf; for (def key : ['event_data', 'user_data']) { def m = ctx.winlog[key]; if (!(m instanceof Map)) { continue; } for (def e : m.entrySet()) { def v = e.getValue(); if (v instanceof String) { e.setValue(v.replace(crlf, lf)); } else if (v instanceof Number || v instanceof Boolean) { e.setValue(v.toString()); } } }"
|
||||||
|
} },
|
||||||
|
{ "set": { "description": "event.kind, as Winlogbeat", "field": "event.kind", "value": "event", "override": false } },
|
||||||
|
{ "set": { "field": "data_stream.dataset", "copy_from": "event.dataset", "override": true, "ignore_empty_value": true } },
|
||||||
|
{ "set": { "field": "data_stream.namespace", "value": "import", "override": true } },
|
||||||
|
{ "reroute": { "dataset": "{{data_stream.dataset}}", "namespace": "{{data_stream.namespace}}" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"managed_by": "security_onion",
|
||||||
|
"managed": true,
|
||||||
|
"description": "Adds .caseless for Lucene queries. Restates each field's package type and .text."
|
||||||
|
},
|
||||||
|
"template": {
|
||||||
|
"mappings": {
|
||||||
|
"properties": {
|
||||||
|
"process": {
|
||||||
|
"properties": {
|
||||||
|
"executable": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "match_only_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "match_only_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"command_line": {
|
||||||
|
"type": "wildcard",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "match_only_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parent": {
|
||||||
|
"properties": {
|
||||||
|
"executable": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "match_only_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "match_only_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"command_line": {
|
||||||
|
"type": "wildcard",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "match_only_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "match_only_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"managed_by": "security_onion",
|
||||||
|
"managed": true,
|
||||||
|
"description": "Adds .caseless for Lucene queries. Keeps each field's existing keyword type."
|
||||||
|
},
|
||||||
|
"template": {
|
||||||
|
"mappings": {
|
||||||
|
"properties": {
|
||||||
|
"process": {
|
||||||
|
"properties": {
|
||||||
|
"command_line": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parent": {
|
||||||
|
"properties": {
|
||||||
|
"executable": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"command_line": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"fields": {
|
||||||
|
"caseless": {
|
||||||
|
"type": "keyword",
|
||||||
|
"ignore_above": 1024,
|
||||||
|
"normalizer": "lowercase"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ kafka_user:
|
|||||||
- gid: 960
|
- gid: 960
|
||||||
- home: /opt/so/conf/kafka
|
- home: /opt/so/conf/kafka
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
kafka_home_dir:
|
kafka_home_dir:
|
||||||
file.absent:
|
file.absent:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ kibana:
|
|||||||
- gid: 932
|
- gid: 932
|
||||||
- home: /opt/so/conf/kibana
|
- home: /opt/so/conf/kibana
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
# Drop the correct nginx config based on role
|
# Drop the correct nginx config based on role
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ kratos:
|
|||||||
- uid: 928
|
- uid: 928
|
||||||
- gid: 928
|
- gid: 928
|
||||||
- home: /opt/so/conf/kratos
|
- home: /opt/so/conf/kratos
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
kratosdir:
|
kratosdir:
|
||||||
file.directory:
|
file.directory:
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ logstash:
|
|||||||
- uid: 931
|
- uid: 931
|
||||||
- gid: 931
|
- gid: 931
|
||||||
- home: /opt/so/conf/logstash
|
- home: /opt/so/conf/logstash
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
logstash_sbin:
|
logstash_sbin:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ is older than debounce_seconds, this script:
|
|||||||
* dispatches a single `salt-run state.orchestrate orch.push_batch --async`
|
* dispatches a single `salt-run state.orchestrate orch.push_batch --async`
|
||||||
with the deduped actions list passed as pillar kwargs
|
with the deduped actions list passed as pillar kwargs
|
||||||
* deletes the contributed intent files on successful dispatch
|
* deletes the contributed intent files on successful dispatch
|
||||||
|
* records the orchestration jid under /opt/so/state/push_dispatched and, on
|
||||||
|
later passes, looks up its result and logs success or per-minion failures
|
||||||
|
|
||||||
Reactor sls files (push_files, push_pillar) write intents
|
Reactor sls files (push_files, push_pillar) write intents
|
||||||
but never dispatch directly
|
but never dispatch directly
|
||||||
@@ -30,6 +32,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -40,8 +43,19 @@ PENDING_DIR = '/opt/so/state/push_pending'
|
|||||||
LOCK_FILE = os.path.join(PENDING_DIR, '.lock')
|
LOCK_FILE = os.path.join(PENDING_DIR, '.lock')
|
||||||
LOG_FILE = '/opt/so/log/salt/so-push-drainer.log'
|
LOG_FILE = '/opt/so/log/salt/so-push-drainer.log'
|
||||||
|
|
||||||
|
DISPATCHED_DIR = '/opt/so/state/push_dispatched'
|
||||||
|
|
||||||
HIGHSTATE_SENTINEL = '__highstate__'
|
HIGHSTATE_SENTINEL = '__highstate__'
|
||||||
|
|
||||||
|
RESULT_CHECK_DELAY = 30
|
||||||
|
RESULT_RECHECK_MAX = 300
|
||||||
|
RESULT_MAX_AGE = 7200
|
||||||
|
RESULT_CHECKS_PER_PASS = 5
|
||||||
|
TEXT_LIMIT = 500
|
||||||
|
|
||||||
|
# salt-run --async reports the jid only in a log line (stderr by default).
|
||||||
|
JID_RE = re.compile(r'salt/run/(\d{20})')
|
||||||
|
|
||||||
|
|
||||||
def _make_logger():
|
def _make_logger():
|
||||||
logger = logging.getLogger('so-push-drainer')
|
logger = logging.getLogger('so-push-drainer')
|
||||||
@@ -113,14 +127,167 @@ def _dispatch(actions, log):
|
|||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
log.error('dispatch failed (rc=%s): stdout=%s stderr=%s',
|
log.error('dispatch failed (rc=%s): stdout=%s stderr=%s',
|
||||||
exc.returncode, exc.stdout, exc.stderr)
|
exc.returncode, exc.stdout, exc.stderr)
|
||||||
return False
|
return None
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
log.error('dispatch timed out after 60s')
|
log.error('dispatch timed out after 60s')
|
||||||
return False
|
return None
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception('dispatch raised')
|
log.exception('dispatch raised')
|
||||||
|
return None
|
||||||
|
output = '{}\n{}'.format(result.stderr or '', result.stdout or '')
|
||||||
|
match = JID_RE.search(output)
|
||||||
|
if not match:
|
||||||
|
log.warning('dispatch accepted but no jid found, result will not be tracked: output=%s',
|
||||||
|
_trim(output))
|
||||||
|
return ''
|
||||||
|
log.info('dispatch accepted: jid=%s', match.group(1))
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _trim(value):
|
||||||
|
text = value if isinstance(value, str) else json.dumps(value, default=str)
|
||||||
|
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||||
|
if 'Traceback (most recent call last):' in text:
|
||||||
|
# Keep the lead-in and the raised exception; the frames are noise in a log line.
|
||||||
|
lines = [text.split('Traceback (most recent call last):', 1)[0].strip(), lines[-1]]
|
||||||
|
text = ' '.join(line for line in lines if line)
|
||||||
|
return text if len(text) <= TEXT_LIMIT else text[:TEXT_LIMIT] + '...'
|
||||||
|
|
||||||
|
|
||||||
|
def _unlink(path, log):
|
||||||
|
try:
|
||||||
|
os.unlink(path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except OSError:
|
||||||
|
log.exception('failed to remove %s', path)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_record(path, record, log):
|
||||||
|
try:
|
||||||
|
os.makedirs(DISPATCHED_DIR, exist_ok=True)
|
||||||
|
tmp_path = path + '.tmp'
|
||||||
|
with open(tmp_path, 'w') as f:
|
||||||
|
json.dump(record, f)
|
||||||
|
os.rename(tmp_path, path)
|
||||||
|
except Exception:
|
||||||
|
log.exception('failed to record dispatch %s', record.get('jid'))
|
||||||
|
|
||||||
|
|
||||||
|
def _record_dispatch(jid, actions, paths, log):
|
||||||
|
record = {'jid': jid, 'dispatched_at': time.time(), 'actions': actions, 'paths': paths}
|
||||||
|
_write_record(os.path.join(DISPATCHED_DIR, '{}.json'.format(jid)), record, log)
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_jid(jid, log):
|
||||||
|
"""Returns the job cache entry for jid, {} while it is still running, or None on error."""
|
||||||
|
cmd = ['salt-run', 'jobs.lookup_jid', jid, '--out=json']
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=60)
|
||||||
|
return json.loads(result.stdout or '{}')
|
||||||
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, ValueError) as exc:
|
||||||
|
log.warning('lookup of jid %s failed: %s', jid, exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _minion_failure(minion_ret):
|
||||||
|
if isinstance(minion_ret, dict):
|
||||||
|
return '; '.join(
|
||||||
|
'{}: {}'.format(state.get('__id__', state_key), _trim(state.get('comment', '')))
|
||||||
|
for state_key, state in minion_ret.items()
|
||||||
|
if isinstance(state, dict) and state.get('result') is False
|
||||||
|
)
|
||||||
|
# A state run rejected before it starts (e.g. another state run is in
|
||||||
|
# progress) returns a list of error strings instead of state results.
|
||||||
|
if isinstance(minion_ret, (list, str)):
|
||||||
|
return _trim(minion_ret)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def _step_failures(step):
|
||||||
|
if not isinstance(step, dict) or step.get('result') is not False:
|
||||||
|
return []
|
||||||
|
failures = ['{}: {}'.format(step.get('__id__', step.get('name')), _trim(step.get('comment', '')))]
|
||||||
|
changes = step.get('changes')
|
||||||
|
minion_rets = changes.get('ret') if isinstance(changes, dict) else None
|
||||||
|
if isinstance(minion_rets, dict):
|
||||||
|
for minion, minion_ret in minion_rets.items():
|
||||||
|
text = _minion_failure(minion_ret)
|
||||||
|
if text:
|
||||||
|
failures.append('{}: {}'.format(minion, text))
|
||||||
|
return failures
|
||||||
|
|
||||||
|
|
||||||
|
def _orch_failures(ret):
|
||||||
|
if not isinstance(ret, dict):
|
||||||
|
return [_trim(ret)]
|
||||||
|
failures = []
|
||||||
|
for job in ret.values():
|
||||||
|
if not isinstance(job, dict):
|
||||||
|
continue
|
||||||
|
job_ret = job.get('return')
|
||||||
|
data = job_ret.get('data') if isinstance(job_ret, dict) else {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
if data:
|
||||||
|
failures.append(_trim(data))
|
||||||
|
data = {}
|
||||||
|
for steps in data.values():
|
||||||
|
if not isinstance(steps, dict):
|
||||||
|
failures.append(_trim(steps))
|
||||||
|
continue
|
||||||
|
for step in steps.values():
|
||||||
|
failures.extend(_step_failures(step))
|
||||||
|
if job.get('success') is False and not failures:
|
||||||
|
failures.append('orchestration reported failure: {}'.format(_trim(job.get('return'))))
|
||||||
|
return failures
|
||||||
|
|
||||||
|
|
||||||
|
def _recheck_delay(age):
|
||||||
|
return min(RESULT_RECHECK_MAX, max(RESULT_CHECK_DELAY, age / 4))
|
||||||
|
|
||||||
|
|
||||||
|
def _check_dispatched(log, now):
|
||||||
|
due = []
|
||||||
|
for path in glob.glob(os.path.join(DISPATCHED_DIR, '*.json')):
|
||||||
|
record = _read_intent(path, log)
|
||||||
|
if not isinstance(record, dict) or not record.get('jid'):
|
||||||
|
_unlink(path, log)
|
||||||
|
continue
|
||||||
|
age = now - record.get('dispatched_at', 0)
|
||||||
|
last_check = record.get('checked_at', record.get('dispatched_at', 0))
|
||||||
|
if now - last_check >= _recheck_delay(age):
|
||||||
|
due.append((last_check, path, record, age))
|
||||||
|
# Least recently checked first, so pushes that are still running can't starve finished ones.
|
||||||
|
for _, path, record, age in sorted(due, key=lambda item: item[:2])[:RESULT_CHECKS_PER_PASS]:
|
||||||
|
jid = record['jid']
|
||||||
|
try:
|
||||||
|
if _report_result(record, age, log):
|
||||||
|
_unlink(path, log)
|
||||||
|
else:
|
||||||
|
record['checked_at'] = now
|
||||||
|
_write_record(path, record, log)
|
||||||
|
except Exception:
|
||||||
|
# Drop the record so one unreadable result can't fail every pass ahead of the drain.
|
||||||
|
log.exception('cannot evaluate result for jid=%s; no longer tracking', jid)
|
||||||
|
_unlink(path, log)
|
||||||
|
|
||||||
|
|
||||||
|
def _report_result(record, age, log):
|
||||||
|
"""Logs the outcome of a dispatched push. Returns True once the record is finished with."""
|
||||||
|
jid = record['jid']
|
||||||
|
paths = record.get('paths', [])
|
||||||
|
ret = _lookup_jid(jid, log)
|
||||||
|
if not ret:
|
||||||
|
if age > RESULT_MAX_AGE:
|
||||||
|
log.warning('no result for jid=%s after %ds, no longer tracking; paths=%s', jid, age, paths)
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
log.info('dispatch accepted: %s', (result.stdout or '').strip())
|
failures = _orch_failures(ret)
|
||||||
|
if failures:
|
||||||
|
log.error('push failed jid=%s paths=%s; change will be applied at the next scheduled highstate: %s',
|
||||||
|
jid, paths, ' | '.join(failures))
|
||||||
|
else:
|
||||||
|
log.info('push succeeded jid=%s paths=%s', jid, paths)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -143,6 +310,9 @@ def main():
|
|||||||
|
|
||||||
debounce_seconds = int(push.get('debounce_seconds', 30))
|
debounce_seconds = int(push.get('debounce_seconds', 30))
|
||||||
|
|
||||||
|
# Outside the lock: lookups are slow and the reactors take the same lock.
|
||||||
|
_check_dispatched(log, time.time())
|
||||||
|
|
||||||
os.makedirs(PENDING_DIR, exist_ok=True)
|
os.makedirs(PENDING_DIR, exist_ok=True)
|
||||||
lock_fd = os.open(LOCK_FILE, os.O_CREAT | os.O_RDWR, 0o644)
|
lock_fd = os.open(LOCK_FILE, os.O_CREAT | os.O_RDWR, 0o644)
|
||||||
try:
|
try:
|
||||||
@@ -208,10 +378,16 @@ def main():
|
|||||||
len(ready), len(deduped), len(combined_actions),
|
len(ready), len(deduped), len(combined_actions),
|
||||||
debounce_duration, all_paths[:20],
|
debounce_duration, all_paths[:20],
|
||||||
)
|
)
|
||||||
|
for action in deduped:
|
||||||
|
log.info('action: %s tgt=%s', 'highstate' if action.get('highstate') else action.get('state'),
|
||||||
|
action.get('tgt'))
|
||||||
|
|
||||||
if not _dispatch(deduped, log):
|
jid = _dispatch(deduped, log)
|
||||||
|
if jid is None:
|
||||||
log.warning('dispatch failed; leaving intent files in place for retry')
|
log.warning('dispatch failed; leaving intent files in place for retry')
|
||||||
return 1
|
return 1
|
||||||
|
if jid:
|
||||||
|
_record_dispatch(jid, deduped, all_paths[:20], log)
|
||||||
|
|
||||||
for path, _ in ready:
|
for path, _ in ready:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,477 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
from importlib.machinery import SourceFileLoader
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
SCRIPT = os.path.join(HERE, 'so-push-drainer')
|
||||||
|
_loader = SourceFileLoader('so_push_drainer', SCRIPT)
|
||||||
|
_spec = importlib.util.spec_from_loader('so_push_drainer', _loader)
|
||||||
|
drainer = importlib.util.module_from_spec(_spec)
|
||||||
|
|
||||||
|
# salt is not installed where these tests run; the drainer only needs salt.client.Caller.
|
||||||
|
# Mocked only while the drainer loads: run from the repo root, 'salt' is this repo's salt/ directory.
|
||||||
|
_salt = MagicMock()
|
||||||
|
with patch.dict(sys.modules, {'salt': _salt, 'salt.client': _salt.client}):
|
||||||
|
_loader.exec_module(drainer)
|
||||||
|
|
||||||
|
MASTER = 'manager.localdomain_master'
|
||||||
|
JID = '20260930171554259426'
|
||||||
|
ASYNC_STDERR = ('[WARNING ] Running in asynchronous mode. Results of this execution may be collected '
|
||||||
|
'by attaching to the master event bus or by examining the master job cache, if '
|
||||||
|
'configured. This execution is running under tag salt/run/{}\n'.format(JID))
|
||||||
|
CONFLICT = ('The function "state.sls" is running as PID 372218 and was started at '
|
||||||
|
'2026, Sep 30 17:15:40.466233 with jid 20260930171540466233')
|
||||||
|
|
||||||
|
|
||||||
|
def _orch_ret(steps, success=True):
|
||||||
|
return {MASTER: {
|
||||||
|
'fun': 'runner.state.orchestrate',
|
||||||
|
'jid': JID,
|
||||||
|
'return': {'data': {MASTER: steps}, 'outputter': 'highstate', 'retcode': 0 if success else 1},
|
||||||
|
'success': success,
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
REFRESH_STEP = {
|
||||||
|
'salt_|-refresh_pillar_1_|-saltutil.refresh_pillar_|-function': {
|
||||||
|
'__id__': 'refresh_pillar_1', 'result': True,
|
||||||
|
'changes': {'ret': {'manager_standalone': True}},
|
||||||
|
'comment': 'Function ran successfully.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CONFLICT_RET = _orch_ret(dict(REFRESH_STEP, **{
|
||||||
|
'salt_|-apply_soc_1_|-apply_soc_1_|-state': {
|
||||||
|
'__id__': 'apply_soc_1', 'result': False,
|
||||||
|
'changes': {'out': 'highstate', 'ret': {'manager_standalone': [CONFLICT]}},
|
||||||
|
'comment': 'Run failed on minions: manager_standalone',
|
||||||
|
},
|
||||||
|
}), success=False)
|
||||||
|
|
||||||
|
STATE_FAIL_RET = _orch_ret({
|
||||||
|
'salt_|-apply_hydra_1_|-apply_hydra_1_|-state': {
|
||||||
|
'__id__': 'apply_hydra_1', 'result': False,
|
||||||
|
'changes': {'out': 'highstate', 'ret': {'manager_standalone': {
|
||||||
|
'test_|-no_license_|-no_license_|-fail_without_changes': {
|
||||||
|
'__id__': 'hydra.enabled_no_license_detected', 'result': False,
|
||||||
|
'comment': 'This is a feature supported only for customers with a valid license.',
|
||||||
|
},
|
||||||
|
'file_|-hydra_conf_|-/opt/so/conf/hydra_|-managed': {'result': True, 'comment': 'ok'},
|
||||||
|
}}},
|
||||||
|
'comment': 'Run failed on minions: manager_standalone',
|
||||||
|
},
|
||||||
|
}, success=False)
|
||||||
|
|
||||||
|
SUCCESS_RET = _orch_ret(dict(REFRESH_STEP, **{
|
||||||
|
'salt_|-apply_telegraf_1_|-apply_telegraf_1_|-state': {
|
||||||
|
'__id__': 'apply_telegraf_1', 'result': True,
|
||||||
|
'changes': {'out': 'highstate', 'ret': {'manager_standalone': {
|
||||||
|
'file_|-tgrafconf_|-/opt/so/conf/telegraf/etc/telegraf.conf_|-managed': {'result': True},
|
||||||
|
}}},
|
||||||
|
'comment': 'States ran successfully.',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
class DrainerTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.tmpdir = tempfile.mkdtemp()
|
||||||
|
self.pending = os.path.join(self.tmpdir, 'push_pending')
|
||||||
|
self.dispatched = os.path.join(self.tmpdir, 'push_dispatched')
|
||||||
|
os.makedirs(self.pending)
|
||||||
|
for name, value in (
|
||||||
|
('PENDING_DIR', self.pending),
|
||||||
|
('LOCK_FILE', os.path.join(self.pending, '.lock')),
|
||||||
|
('DISPATCHED_DIR', self.dispatched),
|
||||||
|
('LOG_FILE', os.path.join(self.tmpdir, 'log', 'so-push-drainer.log')),
|
||||||
|
):
|
||||||
|
patcher = patch.object(drainer, name, value)
|
||||||
|
patcher.start()
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
self.log = MagicMock()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
||||||
|
|
||||||
|
def write_json(self, directory, name, data):
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
path = os.path.join(directory, name)
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
if isinstance(data, str):
|
||||||
|
f.write(data)
|
||||||
|
else:
|
||||||
|
json.dump(data, f)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def logged(self, level):
|
||||||
|
return ' '.join(c.args[0] % c.args[1:] for c in getattr(self.log, level).call_args_list)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHelpers(DrainerTestCase):
|
||||||
|
|
||||||
|
def test_make_logger_adds_handler_once(self):
|
||||||
|
logger = logging.getLogger('so-push-drainer')
|
||||||
|
|
||||||
|
def close_handlers():
|
||||||
|
for handler in logger.handlers:
|
||||||
|
handler.close()
|
||||||
|
logger.handlers.clear()
|
||||||
|
|
||||||
|
self.addCleanup(close_handlers)
|
||||||
|
logger.handlers.clear()
|
||||||
|
self.assertIs(drainer._make_logger(), logger)
|
||||||
|
drainer._make_logger()
|
||||||
|
self.assertEqual(len(logger.handlers), 1)
|
||||||
|
self.assertTrue(os.path.isdir(os.path.dirname(drainer.LOG_FILE)))
|
||||||
|
|
||||||
|
def test_load_push_cfg(self):
|
||||||
|
with patch.object(drainer.salt.client, 'Caller') as caller:
|
||||||
|
caller.return_value.cmd.return_value = {'enabled': False}
|
||||||
|
self.assertEqual(drainer._load_push_cfg(), {'enabled': False})
|
||||||
|
caller.return_value.cmd.return_value = 'garbage'
|
||||||
|
self.assertEqual(drainer._load_push_cfg(), {})
|
||||||
|
|
||||||
|
def test_read_intent(self):
|
||||||
|
good = self.write_json(self.pending, 'good.json', {'a': 1})
|
||||||
|
bad = self.write_json(self.pending, 'bad.json', '{nope')
|
||||||
|
self.assertEqual(drainer._read_intent(good, self.log), {'a': 1})
|
||||||
|
self.assertIsNone(drainer._read_intent(bad, self.log))
|
||||||
|
with patch('builtins.open', side_effect=RuntimeError('boom')):
|
||||||
|
self.assertIsNone(drainer._read_intent(good, self.log))
|
||||||
|
self.log.exception.assert_called_once()
|
||||||
|
|
||||||
|
def test_dedupe_actions(self):
|
||||||
|
actions = [
|
||||||
|
'not a dict',
|
||||||
|
{'state': 'soc'},
|
||||||
|
{'state': 'soc', 'tgt': '*'},
|
||||||
|
{'state': 'soc', 'tgt': '*', 'tgt_type': 'compound'},
|
||||||
|
{'highstate': True, 'tgt': '*'},
|
||||||
|
{'state': 'soc', 'tgt': 'node1', 'tgt_type': 'glob'},
|
||||||
|
]
|
||||||
|
self.assertEqual(drainer._dedupe_actions(actions), [actions[2], actions[4], actions[5]])
|
||||||
|
|
||||||
|
def test_trim(self):
|
||||||
|
self.assertEqual(drainer._trim(' text \n'), 'text')
|
||||||
|
self.assertEqual(drainer._trim(['a']), '["a"]')
|
||||||
|
self.assertEqual(drainer._trim(None), 'null')
|
||||||
|
self.assertEqual(drainer._trim('x' * 600), 'x' * drainer.TEXT_LIMIT + '...')
|
||||||
|
|
||||||
|
def test_trim_traceback(self):
|
||||||
|
comment = ('An exception occurred in this state: Traceback (most recent call last):\n'
|
||||||
|
' File "salt/client/__init__.py", line 1934, in pub\n'
|
||||||
|
' raise AuthenticationError(err_msg)\n'
|
||||||
|
'salt.exceptions.AuthenticationError: Authentication error occurred.\n')
|
||||||
|
self.assertEqual(drainer._trim(comment), 'An exception occurred in this state: '
|
||||||
|
'salt.exceptions.AuthenticationError: Authentication error occurred.')
|
||||||
|
self.assertEqual(drainer._trim('line one\n line two\n'), 'line one line two')
|
||||||
|
|
||||||
|
def test_unlink(self):
|
||||||
|
drainer._unlink(os.path.join(self.tmpdir, 'missing'), self.log)
|
||||||
|
self.log.exception.assert_not_called()
|
||||||
|
drainer._unlink(self.tmpdir, self.log)
|
||||||
|
self.log.exception.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestDispatch(DrainerTestCase):
|
||||||
|
|
||||||
|
def run_dispatch(self, **kwargs):
|
||||||
|
with patch.object(drainer.subprocess, 'run', **kwargs) as run:
|
||||||
|
jid = drainer._dispatch([{'state': 'soc', 'tgt': '*'}], self.log)
|
||||||
|
return jid, run
|
||||||
|
|
||||||
|
def test_jid_parsed_from_stderr(self):
|
||||||
|
jid, run = self.run_dispatch(return_value=MagicMock(stdout='', stderr=ASYNC_STDERR))
|
||||||
|
self.assertEqual(jid, JID)
|
||||||
|
cmd = run.call_args[0][0]
|
||||||
|
self.assertEqual(cmd[:3], ['salt-run', 'state.orchestrate', 'orch.push_batch'])
|
||||||
|
self.assertIn('--async', cmd)
|
||||||
|
|
||||||
|
def test_jid_parsed_from_stdout(self):
|
||||||
|
jid, _ = self.run_dispatch(return_value=MagicMock(stdout=ASYNC_STDERR, stderr=None))
|
||||||
|
self.assertEqual(jid, JID)
|
||||||
|
|
||||||
|
def test_no_jid(self):
|
||||||
|
jid, _ = self.run_dispatch(return_value=MagicMock(stdout='unexpected output', stderr=None))
|
||||||
|
self.assertEqual(jid, '')
|
||||||
|
self.assertIn('output=unexpected output', self.logged('warning'))
|
||||||
|
|
||||||
|
def test_failures_return_none(self):
|
||||||
|
for exc in (subprocess.CalledProcessError(1, 'salt-run', 'out', 'err'),
|
||||||
|
subprocess.TimeoutExpired('salt-run', 60),
|
||||||
|
RuntimeError('boom')):
|
||||||
|
jid, _ = self.run_dispatch(side_effect=exc)
|
||||||
|
self.assertIsNone(jid)
|
||||||
|
|
||||||
|
def test_record_dispatch(self):
|
||||||
|
drainer._record_dispatch(JID, [{'state': 'soc'}], ['audit:soc.config.licenseKey'], self.log)
|
||||||
|
with open(os.path.join(self.dispatched, JID + '.json')) as f:
|
||||||
|
record = json.load(f)
|
||||||
|
self.assertEqual(record['jid'], JID)
|
||||||
|
self.assertEqual(record['paths'], ['audit:soc.config.licenseKey'])
|
||||||
|
self.assertIn('dispatched_at', record)
|
||||||
|
|
||||||
|
def test_record_dispatch_errors(self):
|
||||||
|
with patch.object(drainer.os, 'makedirs', side_effect=OSError('ro')):
|
||||||
|
drainer._record_dispatch(JID, [], [], self.log)
|
||||||
|
drainer._record_dispatch(JID, [object()], [], self.log)
|
||||||
|
self.assertEqual(self.log.exception.call_count, 2)
|
||||||
|
self.assertFalse(os.path.exists(os.path.join(self.dispatched, JID + '.json')))
|
||||||
|
|
||||||
|
|
||||||
|
class TestResults(DrainerTestCase):
|
||||||
|
|
||||||
|
def test_lookup_jid(self):
|
||||||
|
with patch.object(drainer.subprocess, 'run') as run:
|
||||||
|
run.return_value = MagicMock(stdout=json.dumps(SUCCESS_RET))
|
||||||
|
self.assertEqual(drainer._lookup_jid(JID, self.log), SUCCESS_RET)
|
||||||
|
self.assertEqual(run.call_args[0][0], ['salt-run', 'jobs.lookup_jid', JID, '--out=json'])
|
||||||
|
run.return_value = MagicMock(stdout='')
|
||||||
|
self.assertEqual(drainer._lookup_jid(JID, self.log), {})
|
||||||
|
run.return_value = MagicMock(stdout='not json')
|
||||||
|
self.assertIsNone(drainer._lookup_jid(JID, self.log))
|
||||||
|
run.side_effect = subprocess.TimeoutExpired('salt-run', 60)
|
||||||
|
self.assertIsNone(drainer._lookup_jid(JID, self.log))
|
||||||
|
|
||||||
|
def test_minion_failure_shapes(self):
|
||||||
|
self.assertEqual(drainer._minion_failure([CONFLICT]), json.dumps([CONFLICT]))
|
||||||
|
self.assertEqual(drainer._minion_failure('Rendering SLS failed'), 'Rendering SLS failed')
|
||||||
|
self.assertEqual(drainer._minion_failure(True), '')
|
||||||
|
self.assertEqual(drainer._minion_failure({'a': {'result': True}}), '')
|
||||||
|
|
||||||
|
def test_orch_failures_conflict(self):
|
||||||
|
failures = drainer._orch_failures(CONFLICT_RET)
|
||||||
|
self.assertEqual(failures[0], 'apply_soc_1: Run failed on minions: manager_standalone')
|
||||||
|
self.assertIn('manager_standalone', failures[1])
|
||||||
|
self.assertIn('is running as PID 372218', failures[1])
|
||||||
|
self.assertEqual(len(failures), 2)
|
||||||
|
|
||||||
|
def test_orch_failures_failed_state(self):
|
||||||
|
failures = drainer._orch_failures(STATE_FAIL_RET)
|
||||||
|
self.assertEqual(len(failures), 2)
|
||||||
|
self.assertIn('hydra.enabled_no_license_detected: This is a feature', failures[1])
|
||||||
|
self.assertNotIn('hydra_conf', failures[1])
|
||||||
|
|
||||||
|
def test_orch_failures_success(self):
|
||||||
|
self.assertEqual(drainer._orch_failures(SUCCESS_RET), [])
|
||||||
|
|
||||||
|
def test_orch_failures_render_error(self):
|
||||||
|
ret = {MASTER: {'return': {'data': {MASTER: ['Rendering SLS failed']}}, 'success': False}}
|
||||||
|
self.assertEqual(drainer._orch_failures(ret), ['["Rendering SLS failed"]'])
|
||||||
|
|
||||||
|
def test_orch_failures_not_a_dict(self):
|
||||||
|
self.assertEqual(drainer._orch_failures(['No minions matched']), ['["No minions matched"]'])
|
||||||
|
self.assertEqual(drainer._orch_failures('Runner error'), ['Runner error'])
|
||||||
|
|
||||||
|
def test_orch_failures_data_not_a_dict(self):
|
||||||
|
ret = {MASTER: {'return': {'data': ["Rendering SLS 'orch.push_batch' failed"]}, 'success': False}}
|
||||||
|
self.assertEqual(drainer._orch_failures(ret), ['["Rendering SLS \'orch.push_batch\' failed"]'])
|
||||||
|
|
||||||
|
def test_orch_failures_odd_changes(self):
|
||||||
|
for changes in ('Run failed', {'ret': ['manager_standalone']}):
|
||||||
|
ret = _orch_ret({'salt_|-apply_soc_1_|-apply_soc_1_|-state': {
|
||||||
|
'__id__': 'apply_soc_1', 'result': False, 'changes': changes, 'comment': 'Run failed on minions',
|
||||||
|
}}, success=False)
|
||||||
|
self.assertEqual(drainer._orch_failures(ret), ['apply_soc_1: Run failed on minions'])
|
||||||
|
|
||||||
|
def test_orch_failures_unparsed(self):
|
||||||
|
self.assertEqual(drainer._orch_failures({MASTER: 'odd'}), [])
|
||||||
|
ret = {MASTER: {'return': 'Exception occurred', 'success': False}}
|
||||||
|
self.assertEqual(drainer._orch_failures(ret), ['orchestration reported failure: Exception occurred'])
|
||||||
|
|
||||||
|
def record(self, jid, age, now):
|
||||||
|
return self.write_json(self.dispatched, jid + '.json', {
|
||||||
|
'jid': jid, 'dispatched_at': now - age, 'actions': [], 'paths': ['audit:' + jid],
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_check_dispatched(self):
|
||||||
|
now = time.time()
|
||||||
|
results = {
|
||||||
|
'1_failed': CONFLICT_RET,
|
||||||
|
'2_ok': SUCCESS_RET,
|
||||||
|
'3_pending': {},
|
||||||
|
'4_expired': None,
|
||||||
|
}
|
||||||
|
young = self.record('0_young', 5, now)
|
||||||
|
paths = {jid: self.record(jid, 60, now) for jid in results}
|
||||||
|
paths['4_expired'] = self.record('4_expired', drainer.RESULT_MAX_AGE + 1, now)
|
||||||
|
bad = self.write_json(self.dispatched, '5_bad.json', '{nope')
|
||||||
|
with patch.object(drainer, '_lookup_jid', side_effect=lambda jid, log: results[jid]):
|
||||||
|
drainer._check_dispatched(self.log, now)
|
||||||
|
|
||||||
|
self.assertTrue(os.path.exists(young))
|
||||||
|
with open(paths['3_pending']) as f:
|
||||||
|
self.assertEqual(json.load(f)['checked_at'], now)
|
||||||
|
for jid in ('1_failed', '2_ok', '4_expired'):
|
||||||
|
self.assertFalse(os.path.exists(paths[jid]), jid)
|
||||||
|
self.assertFalse(os.path.exists(bad))
|
||||||
|
self.assertIn('push failed jid=1_failed', self.logged('error'))
|
||||||
|
self.assertIn('is running as PID 372218', self.logged('error'))
|
||||||
|
self.assertIn('push succeeded jid=2_ok', self.logged('info'))
|
||||||
|
self.assertIn('no result for jid=4_expired', self.logged('warning'))
|
||||||
|
|
||||||
|
def test_check_dispatched_survives_bad_result(self):
|
||||||
|
now = time.time()
|
||||||
|
bad = self.record('1_bad', 60, now)
|
||||||
|
good = self.record('2_ok', 60, now)
|
||||||
|
|
||||||
|
def orch_failures(ret):
|
||||||
|
if ret == 'boom':
|
||||||
|
raise ValueError('unexpected shape')
|
||||||
|
return []
|
||||||
|
|
||||||
|
with patch.object(drainer, '_lookup_jid', side_effect=lambda jid, log: 'boom' if jid == '1_bad' else SUCCESS_RET), \
|
||||||
|
patch.object(drainer, '_orch_failures', side_effect=orch_failures):
|
||||||
|
drainer._check_dispatched(self.log, now)
|
||||||
|
self.assertFalse(os.path.exists(bad))
|
||||||
|
self.assertFalse(os.path.exists(good))
|
||||||
|
self.log.exception.assert_called_once()
|
||||||
|
self.assertIn('jid=1_bad', self.log.exception.call_args[0][0] % self.log.exception.call_args[0][1:])
|
||||||
|
self.assertIn('push succeeded jid=2_ok', self.logged('info'))
|
||||||
|
|
||||||
|
def test_recheck_delay(self):
|
||||||
|
self.assertEqual(drainer._recheck_delay(10), drainer.RESULT_CHECK_DELAY)
|
||||||
|
self.assertEqual(drainer._recheck_delay(400), 100)
|
||||||
|
self.assertEqual(drainer._recheck_delay(drainer.RESULT_MAX_AGE), drainer.RESULT_RECHECK_MAX)
|
||||||
|
|
||||||
|
def test_check_dispatched_limit_rotates(self):
|
||||||
|
now = time.time()
|
||||||
|
limit = drainer.RESULT_CHECKS_PER_PASS
|
||||||
|
jids = ['{:02d}'.format(i) for i in range(limit + 2)]
|
||||||
|
for jid in jids:
|
||||||
|
self.record(jid, 60, now)
|
||||||
|
with patch.object(drainer, '_lookup_jid', return_value={}) as lookup:
|
||||||
|
drainer._check_dispatched(self.log, now)
|
||||||
|
self.assertEqual([c.args[0] for c in lookup.call_args_list], jids[:limit])
|
||||||
|
lookup.reset_mock()
|
||||||
|
drainer._check_dispatched(self.log, now + 40)
|
||||||
|
self.assertEqual([c.args[0] for c in lookup.call_args_list], jids[limit:] + jids[:limit - 2])
|
||||||
|
|
||||||
|
def test_check_dispatched_not_blocked_by_running(self):
|
||||||
|
now = time.time()
|
||||||
|
for i in range(drainer.RESULT_CHECKS_PER_PASS):
|
||||||
|
self.record('1_running{}'.format(i), 600, now)
|
||||||
|
done = self.record('2_done', 60, now)
|
||||||
|
|
||||||
|
def lookup(jid, log):
|
||||||
|
return SUCCESS_RET if jid == '2_done' else {}
|
||||||
|
|
||||||
|
with patch.object(drainer, '_lookup_jid', side_effect=lookup) as lookup_jid:
|
||||||
|
drainer._check_dispatched(self.log, now)
|
||||||
|
self.assertTrue(os.path.exists(done))
|
||||||
|
lookup_jid.reset_mock()
|
||||||
|
drainer._check_dispatched(self.log, now + 15)
|
||||||
|
self.assertEqual([c.args[0] for c in lookup_jid.call_args_list], ['2_done'])
|
||||||
|
self.assertFalse(os.path.exists(done))
|
||||||
|
self.assertIn('push succeeded jid=2_done', self.logged('info'))
|
||||||
|
|
||||||
|
|
||||||
|
class TestMain(DrainerTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.cfg = {'enabled': True, 'debounce_seconds': 30}
|
||||||
|
for name, kwargs in (
|
||||||
|
('_make_logger', {'return_value': self.log}),
|
||||||
|
('_load_push_cfg', {'side_effect': lambda: self.cfg}),
|
||||||
|
('_check_dispatched', {}),
|
||||||
|
):
|
||||||
|
patcher = patch.object(drainer, name, **kwargs)
|
||||||
|
setattr(self, name, patcher.start())
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
|
||||||
|
def intent(self, name, age=60, actions=None, paths=None):
|
||||||
|
now = time.time()
|
||||||
|
return self.write_json(self.pending, name, {
|
||||||
|
'first_touch': now - age - 5, 'last_touch': now - age,
|
||||||
|
'actions': [{'state': 'soc', 'tgt': '*'}] if actions is None else actions,
|
||||||
|
'paths': paths or ['audit:soc.config.licenseKey'],
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_no_pending_dir(self):
|
||||||
|
shutil.rmtree(self.pending)
|
||||||
|
self.assertEqual(drainer.main(), 0)
|
||||||
|
self._load_push_cfg.assert_not_called()
|
||||||
|
|
||||||
|
def test_cfg_error(self):
|
||||||
|
self._load_push_cfg.side_effect = RuntimeError('no salt')
|
||||||
|
self.assertEqual(drainer.main(), 1)
|
||||||
|
|
||||||
|
def test_disabled(self):
|
||||||
|
self.cfg['enabled'] = False
|
||||||
|
self.assertEqual(drainer.main(), 0)
|
||||||
|
self._check_dispatched.assert_not_called()
|
||||||
|
|
||||||
|
def test_no_intents_still_checks_results(self):
|
||||||
|
self.assertEqual(drainer.main(), 0)
|
||||||
|
self._check_dispatched.assert_called_once()
|
||||||
|
|
||||||
|
def test_debounce_and_broken(self):
|
||||||
|
young = self.intent('young.json', age=1)
|
||||||
|
broken = self.write_json(self.pending, 'broken.json', '{nope')
|
||||||
|
with patch.object(drainer, '_dispatch') as dispatch:
|
||||||
|
self.assertEqual(drainer.main(), 0)
|
||||||
|
dispatch.assert_not_called()
|
||||||
|
self.assertTrue(os.path.exists(young))
|
||||||
|
self.assertFalse(os.path.exists(broken))
|
||||||
|
|
||||||
|
def test_broken_unlink_error_ignored(self):
|
||||||
|
self.write_json(self.pending, 'broken.json', '{nope')
|
||||||
|
with patch.object(drainer.os, 'unlink', side_effect=OSError('busy')):
|
||||||
|
self.assertEqual(drainer.main(), 0)
|
||||||
|
|
||||||
|
def test_no_usable_actions(self):
|
||||||
|
path = self.intent('empty.json', actions=[{'state': 'soc'}])
|
||||||
|
self.assertEqual(drainer.main(), 0)
|
||||||
|
self.assertFalse(os.path.exists(path))
|
||||||
|
self.intent('empty.json', actions=[{'state': 'soc'}])
|
||||||
|
with patch.object(drainer.os, 'unlink', side_effect=OSError('busy')):
|
||||||
|
self.assertEqual(drainer.main(), 0)
|
||||||
|
|
||||||
|
def test_dispatch_failure_keeps_intents(self):
|
||||||
|
path = self.intent('pillar_soc.json')
|
||||||
|
with patch.object(drainer, '_dispatch', return_value=None):
|
||||||
|
self.assertEqual(drainer.main(), 1)
|
||||||
|
self.assertTrue(os.path.exists(path))
|
||||||
|
|
||||||
|
def test_dispatch_records_jid(self):
|
||||||
|
soc = self.intent('pillar_soc.json')
|
||||||
|
hs = self.intent('pillar_global.json', actions=[{'highstate': True, 'tgt': '*'}], paths=['audit:global.x'])
|
||||||
|
with patch.object(drainer, '_dispatch', return_value=JID) as dispatch, \
|
||||||
|
patch.object(drainer, '_record_dispatch') as record:
|
||||||
|
self.assertEqual(drainer.main(), 0)
|
||||||
|
self.assertEqual(len(dispatch.call_args[0][0]), 2)
|
||||||
|
record.assert_called_once()
|
||||||
|
self.assertEqual(record.call_args[0][0], JID)
|
||||||
|
self.assertEqual(sorted(record.call_args[0][2]), ['audit:global.x', 'audit:soc.config.licenseKey'])
|
||||||
|
self.assertFalse(os.path.exists(soc))
|
||||||
|
self.assertFalse(os.path.exists(hs))
|
||||||
|
self.assertIn('action: highstate tgt=*', self.logged('info'))
|
||||||
|
|
||||||
|
def test_dispatch_without_jid_not_recorded(self):
|
||||||
|
self.intent('pillar_soc.json')
|
||||||
|
with patch.object(drainer, '_dispatch', return_value=''), \
|
||||||
|
patch.object(drainer, '_record_dispatch') as record, \
|
||||||
|
patch.object(drainer.os, 'unlink', side_effect=OSError('busy')):
|
||||||
|
self.assertEqual(drainer.main(), 0)
|
||||||
|
record.assert_not_called()
|
||||||
|
self.log.exception.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -42,7 +42,8 @@ def loadYaml(filename):
|
|||||||
try:
|
try:
|
||||||
with open(filename, "r") as file:
|
with open(filename, "r") as file:
|
||||||
content = file.read()
|
content = file.read()
|
||||||
return yaml.safe_load(content)
|
loaded = yaml.safe_load(content)
|
||||||
|
return loaded if loaded is not None else {}
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print(f"File not found: {filename}", file=sys.stderr)
|
print(f"File not found: {filename}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -95,6 +95,20 @@ class TestRemove(unittest.TestCase):
|
|||||||
expected = "key1:\n child1: 123\n child2:\n deep2: ab\nkey2: false\n"
|
expected = "key1:\n child1: 123\n child2:\n deep2: ab\nkey2: false\n"
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
def test_remove_empty_file(self):
|
||||||
|
filename = "/tmp/so-yaml_test-remove-empty.yaml"
|
||||||
|
file = open(filename, "w")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
code = soyaml.remove([filename, "key1"])
|
||||||
|
self.assertEqual(code, 0)
|
||||||
|
|
||||||
|
file = open(filename, "r")
|
||||||
|
actual = file.read()
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
self.assertEqual(actual, "{}\n")
|
||||||
|
|
||||||
def test_remove_missing_args(self):
|
def test_remove_missing_args(self):
|
||||||
with patch('sys.exit', new=MagicMock()) as sysmock:
|
with patch('sys.exit', new=MagicMock()) as sysmock:
|
||||||
with patch('sys.stderr', new=StringIO()) as mock_stderr:
|
with patch('sys.stderr', new=StringIO()) as mock_stderr:
|
||||||
@@ -294,6 +308,36 @@ class TestRemove(unittest.TestCase):
|
|||||||
expected = "key1:\n child1: 123\n child2:\n deep1: 45\n deep2: d\nkey2: false\nkey3:\n- e\n- f\n- g\n"
|
expected = "key1:\n child1: 123\n child2:\n deep1: 45\n deep2: d\nkey2: false\nkey3:\n- e\n- f\n- g\n"
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
def test_add_empty_file(self):
|
||||||
|
filename = "/tmp/so-yaml_test-add-empty.yaml"
|
||||||
|
file = open(filename, "w")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
code = soyaml.add([filename, "telegraf.output", "BOTH"])
|
||||||
|
self.assertEqual(code, 0)
|
||||||
|
|
||||||
|
file = open(filename, "r")
|
||||||
|
actual = file.read()
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
expected = "telegraf:\n output: BOTH\n"
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
def test_add_empty_file_simple(self):
|
||||||
|
filename = "/tmp/so-yaml_test-add-empty-simple.yaml"
|
||||||
|
file = open(filename, "w")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
code = soyaml.add([filename, "telegraf", "BOTH"])
|
||||||
|
self.assertEqual(code, 0)
|
||||||
|
|
||||||
|
file = open(filename, "r")
|
||||||
|
actual = file.read()
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
expected = "telegraf: BOTH\n"
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
def test_replace_missing_arg(self):
|
def test_replace_missing_arg(self):
|
||||||
with patch('sys.exit', new=MagicMock()) as sysmock:
|
with patch('sys.exit', new=MagicMock()) as sysmock:
|
||||||
with patch('sys.stderr', new=StringIO()) as mock_stderr:
|
with patch('sys.stderr', new=StringIO()) as mock_stderr:
|
||||||
@@ -346,6 +390,21 @@ class TestRemove(unittest.TestCase):
|
|||||||
expected = "key1:\n child1: 123\n child2:\n deep1: 46\nkey2: false\nkey3:\n- e\n- f\n- g\n"
|
expected = "key1:\n child1: 123\n child2:\n deep1: 46\nkey2: false\nkey3:\n- e\n- f\n- g\n"
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
def test_replace_empty_file(self):
|
||||||
|
filename = "/tmp/so-yaml_test-replace-empty.yaml"
|
||||||
|
file = open(filename, "w")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
code = soyaml.replace([filename, "telegraf.output", "BOTH"])
|
||||||
|
self.assertEqual(code, 0)
|
||||||
|
|
||||||
|
file = open(filename, "r")
|
||||||
|
actual = file.read()
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
expected = "telegraf:\n output: BOTH\n"
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
def test_convert(self):
|
def test_convert(self):
|
||||||
self.assertEqual(soyaml.convertType("foo"), "foo")
|
self.assertEqual(soyaml.convertType("foo"), "foo")
|
||||||
self.assertEqual(soyaml.convertType("foo.bar"), "foo.bar")
|
self.assertEqual(soyaml.convertType("foo.bar"), "foo.bar")
|
||||||
@@ -506,6 +565,18 @@ class TestRemove(unittest.TestCase):
|
|||||||
self.assertEqual(result, 2)
|
self.assertEqual(result, 2)
|
||||||
self.assertEqual("", mock_stdout.getvalue())
|
self.assertEqual("", mock_stdout.getvalue())
|
||||||
|
|
||||||
|
def test_get_empty_file(self):
|
||||||
|
with patch('sys.stdout', new=StringIO()) as mock_stdout:
|
||||||
|
with patch('sys.stderr', new=StringIO()) as mock_stderr:
|
||||||
|
filename = "/tmp/so-yaml_test-get-empty.yaml"
|
||||||
|
file = open(filename, "w")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
result = soyaml.get([filename, "telegraf.output"])
|
||||||
|
self.assertEqual(result, 2)
|
||||||
|
self.assertEqual("", mock_stdout.getvalue())
|
||||||
|
self.assertIn("Key 'telegraf.output' not found by so-yaml.py", mock_stderr.getvalue())
|
||||||
|
|
||||||
def test_get_usage(self):
|
def test_get_usage(self):
|
||||||
with patch('sys.exit', new=MagicMock()) as sysmock:
|
with patch('sys.exit', new=MagicMock()) as sysmock:
|
||||||
with patch('sys.stderr', new=StringIO()) as mock_stderr:
|
with patch('sys.stderr', new=StringIO()) as mock_stderr:
|
||||||
@@ -991,3 +1062,29 @@ class TestLoadYaml(unittest.TestCase):
|
|||||||
soyaml.loadYaml("/tmp/so-yaml_test-unreadable.yaml")
|
soyaml.loadYaml("/tmp/so-yaml_test-unreadable.yaml")
|
||||||
sysmock.assert_called_with(1)
|
sysmock.assert_called_with(1)
|
||||||
self.assertIn("Error reading file", mock_stderr.getvalue())
|
self.assertIn("Error reading file", mock_stderr.getvalue())
|
||||||
|
|
||||||
|
def test_load_yaml_empty_file(self):
|
||||||
|
filename = "/tmp/so-yaml_test-load-empty.yaml"
|
||||||
|
file = open(filename, "w")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
result = soyaml.loadYaml(filename)
|
||||||
|
self.assertEqual(result, {})
|
||||||
|
|
||||||
|
def test_load_yaml_whitespace_only(self):
|
||||||
|
filename = "/tmp/so-yaml_test-load-whitespace.yaml"
|
||||||
|
file = open(filename, "w")
|
||||||
|
file.write(" \n\n \n")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
result = soyaml.loadYaml(filename)
|
||||||
|
self.assertEqual(result, {})
|
||||||
|
|
||||||
|
def test_load_yaml_comments_only(self):
|
||||||
|
filename = "/tmp/so-yaml_test-load-comments.yaml"
|
||||||
|
file = open(filename, "w")
|
||||||
|
file.write("# Just a comment\n# Another comment\n")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
result = soyaml.loadYaml(filename)
|
||||||
|
self.assertEqual(result, {})
|
||||||
@@ -1183,6 +1183,13 @@ up_to_3.4.0() {
|
|||||||
echo "Removing so-kratos, so-hydra and so-soc so they are recreated on the soauth network."
|
echo "Removing so-kratos, so-hydra and so-soc so they are recreated on the soauth network."
|
||||||
docker rm -f so-kratos so-hydra so-soc >> $SOUP_LOG 2>&1
|
docker rm -f so-kratos so-hydra so-soc >> $SOUP_LOG 2>&1
|
||||||
|
|
||||||
|
for template in so-metrics-logstash.node so-metrics-logstash.stack_monitoring.node; do
|
||||||
|
if ! remove_elasticsearch_index_template "$template" "logstash node and node_cel index patterns reversed"; then
|
||||||
|
FINAL_MESSAGE_QUEUE+=("WARNING: Unable to automatically remove the $template index template. Addon integration templates may fail to load until it is removed:")
|
||||||
|
FINAL_MESSAGE_QUEUE+=(" - sudo so-elasticsearch-query _index_template/$template -XDELETE && so-checkin")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
INSTALLEDVERSION=3.4.0
|
INSTALLEDVERSION=3.4.0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1248,6 +1255,10 @@ valid_soauth_range() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post_to_3.4.0() {
|
post_to_3.4.0() {
|
||||||
|
for idx in "metrics-logstash.node-default" "metrics-logstash.stack_monitoring.node-default"; do
|
||||||
|
rollover_index "$idx"
|
||||||
|
done
|
||||||
|
|
||||||
set_postversion 3.4.0
|
set_postversion 3.4.0
|
||||||
}
|
}
|
||||||
### 3.4.0 End ###
|
### 3.4.0 End ###
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
{% set BATCH = AUTOAPPLY.batch %}
|
{% set BATCH = AUTOAPPLY.batch %}
|
||||||
{% set BATCH_WAIT = AUTOAPPLY.batch_wait %}
|
{% set BATCH_WAIT = AUTOAPPLY.batch_wait %}
|
||||||
|
|
||||||
|
{# queue must be a top-level salt.state arg (kwarg is ignored); an int is max_queue and still fails on conflict #}
|
||||||
|
|
||||||
{% for action in actions %}
|
{% for action in actions %}
|
||||||
{% if action.get('highstate') %}
|
{% if action.get('highstate') %}
|
||||||
apply_highstate_{{ loop.index }}:
|
apply_highstate_{{ loop.index }}:
|
||||||
@@ -12,8 +14,7 @@ apply_highstate_{{ loop.index }}:
|
|||||||
- highstate: True
|
- highstate: True
|
||||||
- batch: {{ action.get('batch', BATCH) }}
|
- batch: {{ action.get('batch', BATCH) }}
|
||||||
- batch_wait: {{ action.get('batch_wait', BATCH_WAIT) }}
|
- batch_wait: {{ action.get('batch_wait', BATCH_WAIT) }}
|
||||||
- kwarg:
|
- queue: True
|
||||||
queue: 2
|
|
||||||
{% else %}
|
{% else %}
|
||||||
refresh_pillar_{{ loop.index }}:
|
refresh_pillar_{{ loop.index }}:
|
||||||
salt.function:
|
salt.function:
|
||||||
@@ -29,8 +30,7 @@ apply_{{ action.state | replace('.', '_') }}_{{ loop.index }}:
|
|||||||
- {{ action.state }}
|
- {{ action.state }}
|
||||||
- batch: {{ action.get('batch', BATCH) }}
|
- batch: {{ action.get('batch', BATCH) }}
|
||||||
- batch_wait: {{ action.get('batch_wait', BATCH_WAIT) }}
|
- batch_wait: {{ action.get('batch_wait', BATCH_WAIT) }}
|
||||||
- kwarg:
|
- queue: True
|
||||||
queue: 2
|
|
||||||
- require:
|
- require:
|
||||||
- salt: refresh_pillar_{{ loop.index }}
|
- salt: refresh_pillar_{{ loop.index }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ def run():
|
|||||||
# top level so the reactor is robust to either shape.
|
# top level so the reactor is robust to either shape.
|
||||||
event = data.get('data', data) # noqa: F821 -- data provided by reactor
|
event = data.get('data', data) # noqa: F821 -- data provided by reactor
|
||||||
setting_id = event.get('setting_id', '')
|
setting_id = event.get('setting_id', '')
|
||||||
|
audit_id = event.get('id')
|
||||||
node_id = (event.get('node_id') or '').strip()
|
node_id = (event.get('node_id') or '').strip()
|
||||||
|
|
||||||
app = _app_from_setting(setting_id)
|
app = _app_from_setting(setting_id)
|
||||||
@@ -150,8 +151,8 @@ def run():
|
|||||||
if not entry:
|
if not entry:
|
||||||
LOG.warning(
|
LOG.warning(
|
||||||
'push_pillar: app "%s" is not in pillar_push_map.yaml; change will be '
|
'push_pillar: app "%s" is not in pillar_push_map.yaml; change will be '
|
||||||
'picked up at the next scheduled highstate (setting_id=%s)',
|
'picked up at the next scheduled highstate (setting_id=%s audit_id=%s)',
|
||||||
app, setting_id,
|
app, setting_id, audit_id,
|
||||||
)
|
)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -165,12 +166,12 @@ def run():
|
|||||||
'node_{}_{}'.format(node_id, app), actions,
|
'node_{}_{}'.format(node_id, app), actions,
|
||||||
'audit:{}@{}'.format(setting_id, node_id),
|
'audit:{}@{}'.format(setting_id, node_id),
|
||||||
)
|
)
|
||||||
LOG.info('push_pillar: per-node intent updated for %s on %s (setting_id=%s)',
|
LOG.info('push_pillar: per-node intent updated for %s on %s (setting_id=%s audit_id=%s)',
|
||||||
app, node_id, setting_id)
|
app, node_id, setting_id, audit_id)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Branch B: grid-wide app change -> use the map entry's actions as-is.
|
# Branch B: grid-wide app change -> use the map entry's actions as-is.
|
||||||
actions = list(entry) # copy to avoid mutating the cache
|
actions = list(entry) # copy to avoid mutating the cache
|
||||||
_write_intent('pillar_{}'.format(app), actions, 'audit:{}'.format(setting_id))
|
_write_intent('pillar_{}'.format(app), actions, 'audit:{}'.format(setting_id))
|
||||||
LOG.info('push_pillar: app intent updated for %s (setting_id=%s)', app, setting_id)
|
LOG.info('push_pillar: app intent updated for %s (setting_id=%s audit_id=%s)', app, setting_id, audit_id)
|
||||||
return {}
|
return {}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
'epel-testing.repo',
|
'epel-testing.repo',
|
||||||
'saltstack.repo',
|
'saltstack.repo',
|
||||||
'salt-latest.repo',
|
'salt-latest.repo',
|
||||||
'wazuh.repo'
|
'wazuh.repo',
|
||||||
'Rocky-Base.repo',
|
'Rocky-Base.repo',
|
||||||
'Rocky-CR.repo',
|
'Rocky-CR.repo',
|
||||||
'Rocky-Debuginfo.repo',
|
'Rocky-Debuginfo.repo',
|
||||||
|
|||||||
+18
-6
@@ -118,21 +118,33 @@ crondetectionsbackup:
|
|||||||
- month: '*'
|
- month: '*'
|
||||||
- dayweek: '*'
|
- dayweek: '*'
|
||||||
|
|
||||||
|
# sigma-cli only loads *.yml from the pipelines dir
|
||||||
socsigmafinalpipeline:
|
socsigmafinalpipeline:
|
||||||
file.managed:
|
file.managed:
|
||||||
- name: /opt/so/conf/soc/sigma_final_pipeline.yaml
|
- name: /opt/so/conf/soc/sigma_pipelines/sigma_final_pipeline.yml
|
||||||
- source: salt://soc/files/soc/sigma_final_pipeline.yaml
|
- source: salt://soc/files/soc/sigma_final_pipeline.yaml
|
||||||
- user: 939
|
- user: 939
|
||||||
- group: 939
|
- group: 939
|
||||||
- mode: 600
|
- mode: 600
|
||||||
|
- makedirs: True
|
||||||
|
|
||||||
socsigmasopipeline:
|
# sigma-cli loads every *.yml here; clean removes anything else
|
||||||
file.managed:
|
socsigmapipelines:
|
||||||
- name: /opt/so/conf/soc/sigma_so_pipeline.yaml
|
file.recurse:
|
||||||
- source: salt://soc/files/soc/sigma_so_pipeline.yaml
|
- name: /opt/so/conf/soc/sigma_pipelines
|
||||||
|
- source: salt://soc/files/soc/sigma_pipelines
|
||||||
- user: 939
|
- user: 939
|
||||||
- group: 939
|
- group: 939
|
||||||
- mode: 600
|
- file_mode: 600
|
||||||
|
- clean: True
|
||||||
|
- require:
|
||||||
|
- file: socsigmafinalpipeline
|
||||||
|
|
||||||
|
socsigmapipelinesold:
|
||||||
|
file.absent:
|
||||||
|
- names:
|
||||||
|
- /opt/so/conf/soc/sigma_final_pipeline.yaml
|
||||||
|
- /opt/so/conf/soc/sigma_so_pipeline.yaml
|
||||||
|
|
||||||
socsigmaplaybookpipeline:
|
socsigmaplaybookpipeline:
|
||||||
file.managed:
|
file.managed:
|
||||||
|
|||||||
+10
-2
@@ -1496,7 +1496,7 @@ soc:
|
|||||||
verifyCert: false
|
verifyCert: false
|
||||||
notification:
|
notification:
|
||||||
dismissedPruneDays: 30
|
dismissedPruneDays: 30
|
||||||
enabled: false
|
enabled: true
|
||||||
playbook:
|
playbook:
|
||||||
autoUpdateEnabled: true
|
autoUpdateEnabled: true
|
||||||
playbookImportFrequencySeconds: 86400
|
playbookImportFrequencySeconds: 86400
|
||||||
@@ -1561,6 +1561,14 @@ soc:
|
|||||||
reconcilePersona: ""
|
reconcilePersona: ""
|
||||||
toolUseTurnAttempts: 12
|
toolUseTurnAttempts: 12
|
||||||
toolUseTurnDelayMs: 175
|
toolUseTurnDelayMs: 175
|
||||||
|
agentSessionMaxTurns: 20
|
||||||
|
agentStreamFlushIntervalMs: 1000
|
||||||
|
agentStreamIdleTimeoutSeconds: 300
|
||||||
|
automationSettings:
|
||||||
|
tickIntervalSeconds: 60
|
||||||
|
maxConcurrentItems: 4
|
||||||
|
maxQueuedItems: 0
|
||||||
|
alertTriageEpoch: "2026-09-24T00:00:00Z"
|
||||||
tools:
|
tools:
|
||||||
filterEventFields:
|
filterEventFields:
|
||||||
- "@timestamp"
|
- "@timestamp"
|
||||||
@@ -2799,7 +2807,7 @@ soc:
|
|||||||
- id: sonnet
|
- id: sonnet
|
||||||
displayName: Claude Sonnet
|
displayName: Claude Sonnet
|
||||||
origin: USA
|
origin: USA
|
||||||
contextLimitSmall: 200000
|
contextLimitSmall: 1000000
|
||||||
contextLimitLarge: 1000000
|
contextLimitLarge: 1000000
|
||||||
lowBalanceColorAlert: 500000
|
lowBalanceColorAlert: 500000
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -47,9 +47,8 @@ so-soc:
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
- /opt/so/conf/soc/motd.md:/opt/sensoroni/html/motd.md:ro
|
- /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/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_pipelines:/opt/sensoroni/sigma_pipelines:ro
|
||||||
- /opt/so/conf/soc/sigma_playbook_pipeline.yaml:/opt/sensoroni/sigma_playbook_pipeline.yaml:ro
|
- /opt/so/conf/soc/sigma_playbook_pipeline.yaml:/opt/sensoroni/sigma_playbook_pipeline.yaml:ro
|
||||||
- /opt/so/conf/soc/sigma_final_pipeline.yaml:/opt/sensoroni/sigma_final_pipeline.yaml:ro
|
|
||||||
- /opt/so/conf/soc/playbook_placeholder_map.yaml:/opt/sensoroni/playbook_placeholder_map.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/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.js:/opt/sensoroni/html/js/custom.js:ro
|
||||||
@@ -107,6 +106,7 @@ so-soc:
|
|||||||
- file: socclientsroles
|
- file: socclientsroles
|
||||||
- file: socplaybookplaceholdermap
|
- file: socplaybookplaceholdermap
|
||||||
- file: socplaybookplaceholdermapcustom
|
- file: socplaybookplaceholdermapcustom
|
||||||
|
- file: socsigmapipelines
|
||||||
|
|
||||||
delete_so-soc_so-status.disabled:
|
delete_so-soc_so-status.disabled:
|
||||||
file.uncomment:
|
file.uncomment:
|
||||||
|
|||||||
@@ -0,0 +1,477 @@
|
|||||||
|
name: Security Onion ES|QL Pipeline
|
||||||
|
# ES|QL query settings
|
||||||
|
priority: 92
|
||||||
|
transformations:
|
||||||
|
- id: esql_default_index
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val: .ds-logs-*
|
||||||
|
- id: esql_source_metadata
|
||||||
|
type: set_state
|
||||||
|
key: metadata
|
||||||
|
val: "_id, _index, _source"
|
||||||
|
- id: esql_source_keep
|
||||||
|
type: set_state
|
||||||
|
key: keep
|
||||||
|
val: "_id, _index, _source"
|
||||||
|
# unmapped fields read as null instead of failing the query
|
||||||
|
- id: esql_unmapped_fields
|
||||||
|
type: set_state
|
||||||
|
key: unmapped_fields
|
||||||
|
val: nullify
|
||||||
|
# FROM targets per logsource, any namespace; later entries win, correlations get the union
|
||||||
|
- id: esql_index_process_creation
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.process-*
|
||||||
|
- .ds-logs-windows.sysmon_operational-*
|
||||||
|
- .ds-logs-system.security-*
|
||||||
|
- .ds-logs-windows.powershell-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-sysmon_linux.log-*
|
||||||
|
- .ds-logs-auditd_manager.auditd-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
category: process_creation
|
||||||
|
- id: esql_index_process_creation_windows
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.process-*
|
||||||
|
- .ds-logs-windows.sysmon_operational-*
|
||||||
|
- .ds-logs-system.security-*
|
||||||
|
- .ds-logs-windows.powershell-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
category: process_creation
|
||||||
|
- id: esql_index_process_creation_linux
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.process-*
|
||||||
|
- .ds-logs-sysmon_linux.log-*
|
||||||
|
- .ds-logs-auditd_manager.auditd-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
category: process_creation
|
||||||
|
- id: esql_index_process_creation_macos
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.process-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: macos
|
||||||
|
category: process_creation
|
||||||
|
- id: esql_index_file
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.file-*
|
||||||
|
- .ds-logs-windows.sysmon_operational-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-sysmon_linux.log-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
category: file_event
|
||||||
|
- type: logsource
|
||||||
|
category: file_delete
|
||||||
|
- type: logsource
|
||||||
|
category: file_rename
|
||||||
|
- type: logsource
|
||||||
|
category: file_change
|
||||||
|
- type: logsource
|
||||||
|
category: file_access
|
||||||
|
- id: esql_index_file_windows
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.file-*
|
||||||
|
- .ds-logs-windows.sysmon_operational-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
category: file_event
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
category: file_delete
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
category: file_rename
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
category: file_change
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
category: file_access
|
||||||
|
- id: esql_index_file_linux
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.file-*
|
||||||
|
- .ds-logs-sysmon_linux.log-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
category: file_event
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
category: file_delete
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
category: file_rename
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
category: file_change
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
category: file_access
|
||||||
|
- id: esql_index_file_macos
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.file-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: macos
|
||||||
|
category: file_event
|
||||||
|
- type: logsource
|
||||||
|
product: macos
|
||||||
|
category: file_delete
|
||||||
|
- type: logsource
|
||||||
|
product: macos
|
||||||
|
category: file_rename
|
||||||
|
- type: logsource
|
||||||
|
product: macos
|
||||||
|
category: file_change
|
||||||
|
- type: logsource
|
||||||
|
product: macos
|
||||||
|
category: file_access
|
||||||
|
- id: esql_index_registry
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.registry-*
|
||||||
|
- .ds-logs-windows.sysmon_operational-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
category: registry_set
|
||||||
|
- type: logsource
|
||||||
|
category: registry_add
|
||||||
|
- type: logsource
|
||||||
|
category: registry_delete
|
||||||
|
- type: logsource
|
||||||
|
category: registry_event
|
||||||
|
- id: esql_index_library
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.library-*
|
||||||
|
- .ds-logs-windows.sysmon_operational-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
category: image_load
|
||||||
|
- type: logsource
|
||||||
|
category: driver_load
|
||||||
|
- id: esql_index_endpoint_network
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.network-*
|
||||||
|
- .ds-logs-windows.sysmon_operational-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-sysmon_linux.log-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
category: network_connection
|
||||||
|
- type: logsource
|
||||||
|
category: dns_query
|
||||||
|
- id: esql_index_endpoint_network_windows
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.network-*
|
||||||
|
- .ds-logs-windows.sysmon_operational-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
category: network_connection
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
category: dns_query
|
||||||
|
- id: esql_index_endpoint_network_linux
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.network-*
|
||||||
|
- .ds-logs-sysmon_linux.log-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
category: network_connection
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
category: dns_query
|
||||||
|
- id: esql_index_endpoint_network_macos
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-endpoint.events.network-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: macos
|
||||||
|
category: network_connection
|
||||||
|
- type: logsource
|
||||||
|
product: macos
|
||||||
|
category: dns_query
|
||||||
|
- id: esql_index_sysmon_only
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-windows.sysmon_operational-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
category: process_access
|
||||||
|
- type: logsource
|
||||||
|
category: create_remote_thread
|
||||||
|
- type: logsource
|
||||||
|
category: pipe_created
|
||||||
|
- type: logsource
|
||||||
|
category: create_stream_hash
|
||||||
|
- type: logsource
|
||||||
|
category: wmi_event
|
||||||
|
- type: logsource
|
||||||
|
category: raw_access_thread
|
||||||
|
- type: logsource
|
||||||
|
category: process_tampering
|
||||||
|
- type: logsource
|
||||||
|
category: sysmon_status
|
||||||
|
- type: logsource
|
||||||
|
category: sysmon_error
|
||||||
|
- type: logsource
|
||||||
|
category: file_executable_detected
|
||||||
|
- type: logsource
|
||||||
|
category: file_block_executable
|
||||||
|
- type: logsource
|
||||||
|
category: file_block_shredding
|
||||||
|
- type: logsource
|
||||||
|
category: clipboard_capture
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
service: sysmon
|
||||||
|
- id: esql_index_ps_operational
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-windows.powershell_operational-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
category: ps_script
|
||||||
|
- type: logsource
|
||||||
|
category: ps_module
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
service: powershell
|
||||||
|
- id: esql_index_ps_classic
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-windows.powershell-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
category: ps_classic_start
|
||||||
|
- type: logsource
|
||||||
|
category: ps_classic_provider_start
|
||||||
|
- type: logsource
|
||||||
|
category: ps_classic_script
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
service: powershell-classic
|
||||||
|
- id: esql_index_win_security
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-system.security-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
service: security
|
||||||
|
- id: esql_index_win_system
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-system.system-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
service: system
|
||||||
|
- id: esql_index_win_application
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-system.application-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: windows
|
||||||
|
service: application
|
||||||
|
- id: esql_index_linux_auth
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-system.auth-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
service: auth
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
service: sshd
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
service: sudo
|
||||||
|
- id: esql_index_linux_syslog
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-system.syslog-*
|
||||||
|
- .ds-logs-syslog-so-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
service: syslog
|
||||||
|
- id: esql_index_linux_auditd
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-auditd_manager.auditd-*
|
||||||
|
- .ds-logs-auditd.log-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: linux
|
||||||
|
service: auditd
|
||||||
|
- id: esql_index_network
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-zeek-so-*
|
||||||
|
- .ds-logs-suricata-so-*
|
||||||
|
- .ds-logs-suricata.alerts-so-*
|
||||||
|
- .ds-logs-endpoint.events.network-*
|
||||||
|
- .ds-logs-windows.sysmon_operational-*
|
||||||
|
- .ds-logs-sysmon_linux.log-*
|
||||||
|
- .ds-logs-windows.forwarded-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
category: network
|
||||||
|
- id: esql_index_so_network
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-zeek-so-*
|
||||||
|
- .ds-logs-suricata-so-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_cond_op: or
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
category: network
|
||||||
|
service: connection
|
||||||
|
- type: logsource
|
||||||
|
category: network
|
||||||
|
service: dns
|
||||||
|
- type: logsource
|
||||||
|
category: network
|
||||||
|
service: http
|
||||||
|
- type: logsource
|
||||||
|
category: network
|
||||||
|
service: file
|
||||||
|
- type: logsource
|
||||||
|
category: network
|
||||||
|
service: x509
|
||||||
|
- type: logsource
|
||||||
|
category: network
|
||||||
|
service: ssl
|
||||||
|
- type: logsource
|
||||||
|
category: network
|
||||||
|
service: ssh
|
||||||
|
- type: logsource
|
||||||
|
category: dns
|
||||||
|
- id: esql_index_zeek
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-zeek-so-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: zeek
|
||||||
|
- id: esql_index_opencanary
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-idh-so-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: opencanary
|
||||||
|
- id: esql_index_kratos
|
||||||
|
type: set_state
|
||||||
|
key: index
|
||||||
|
val:
|
||||||
|
- .ds-logs-kratos-so-*
|
||||||
|
- .ds-logs-import-so-*
|
||||||
|
rule_conditions:
|
||||||
|
- type: logsource
|
||||||
|
product: kratos
|
||||||
|
|
||||||
+10
-12
@@ -14,18 +14,16 @@ transformations:
|
|||||||
- process.args
|
- process.args
|
||||||
- related.ip
|
- related.ip
|
||||||
- dns.resolved_ip
|
- dns.resolved_ip
|
||||||
- id: esql_default_index
|
# Not every source maps .caseless; EQL/ES|QL already match case-insensitively.
|
||||||
type: set_state
|
- id: caseless_to_parent_fields
|
||||||
key: index
|
type: field_name_mapping
|
||||||
val: .ds-logs-*
|
mapping:
|
||||||
- id: esql_source_metadata
|
process.executable.caseless: process.executable
|
||||||
type: set_state
|
process.name.caseless: process.name
|
||||||
key: metadata
|
process.parent.executable.caseless: process.parent.executable
|
||||||
val: "_id, _index, _source"
|
process.parent.name.caseless: process.parent.name
|
||||||
- id: esql_source_keep
|
target.process.executable.caseless: target.process.executable
|
||||||
type: set_state
|
target.process.name.caseless: target.process.name
|
||||||
key: keep
|
|
||||||
val: "_id, _index, _source"
|
|
||||||
- id: baseline_field_name_mapping
|
- id: baseline_field_name_mapping
|
||||||
type: field_name_mapping
|
type: field_name_mapping
|
||||||
mapping:
|
mapping:
|
||||||
@@ -2,13 +2,13 @@ name: Security Onion - Playbook Pipeline
|
|||||||
priority: 97
|
priority: 97
|
||||||
transformations:
|
transformations:
|
||||||
# Route to lowercase-normalized .caseless subfields for case-insensitive matching.
|
# Route to lowercase-normalized .caseless subfields for case-insensitive matching.
|
||||||
# file.path.caseless exists on Defend only (Sysmon file events lack it);
|
|
||||||
# registry.path / dll.path / file.name have no .caseless on any source.
|
# registry.path / dll.path / file.name have no .caseless on any source.
|
||||||
- id: case_insensitive_string_fields
|
- id: case_insensitive_string_fields
|
||||||
type: field_name_mapping
|
type: field_name_mapping
|
||||||
mapping:
|
mapping:
|
||||||
process.executable: process.executable.caseless
|
process.executable: process.executable.caseless
|
||||||
process.parent.executable: process.parent.executable.caseless
|
process.parent.executable: process.parent.executable.caseless
|
||||||
|
process.parent.name: process.parent.name.caseless
|
||||||
process.command_line: process.command_line.caseless
|
process.command_line: process.command_line.caseless
|
||||||
process.parent.command_line: process.parent.command_line.caseless
|
process.parent.command_line: process.parent.command_line.caseless
|
||||||
file.path: file.path.caseless
|
file.path: file.path.caseless
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ soc:
|
|||||||
description: Schedules that are shared across the Security Onion product. Modify via one of the SOC Schedules view.
|
description: Schedules that are shared across the Security Onion product. Modify via one of the SOC Schedules view.
|
||||||
readonlyUi: True
|
readonlyUi: True
|
||||||
global: True
|
global: True
|
||||||
|
advanced: True
|
||||||
forcedType: string
|
forcedType: string
|
||||||
syntax: json
|
syntax: json
|
||||||
storage: db
|
storage: db
|
||||||
@@ -495,6 +496,7 @@ soc:
|
|||||||
description: JSON list of notifications. Modify via the SOC Notifications view.
|
description: JSON list of notifications. Modify via the SOC Notifications view.
|
||||||
readonlyUi: True
|
readonlyUi: True
|
||||||
global: True
|
global: True
|
||||||
|
advanced: True
|
||||||
forcedType: string
|
forcedType: string
|
||||||
syntax: json
|
syntax: json
|
||||||
storage: db
|
storage: db
|
||||||
@@ -503,6 +505,10 @@ soc:
|
|||||||
description: The number of days to retain dismissed notifications. When a notification is dismissed, it will be pruned after this many days. Only one user need dismiss a notification for it to be pruned.
|
description: The number of days to retain dismissed notifications. When a notification is dismissed, it will be pruned after this many days. Only one user need dismiss a notification for it to be pruned.
|
||||||
forcedType: int
|
forcedType: int
|
||||||
global: True
|
global: True
|
||||||
|
maxListLimit:
|
||||||
|
description: Maximum number of notifications to display.
|
||||||
|
forcedType: int
|
||||||
|
global: True
|
||||||
enabled:
|
enabled:
|
||||||
description: Enables or disables the SOC notification module.
|
description: Enables or disables the SOC notification module.
|
||||||
forcedType: bool
|
forcedType: bool
|
||||||
@@ -533,6 +539,48 @@ soc:
|
|||||||
global: True
|
global: True
|
||||||
sensitive: True
|
sensitive: True
|
||||||
advanced: True
|
advanced: True
|
||||||
|
postgresmetrics:
|
||||||
|
host:
|
||||||
|
description: Hostname or IP address of the PostgreSQL server used by Telegraf. Defaults to the manager hostname.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
port:
|
||||||
|
description: Port of the PostgreSQL server used by Telegraf.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
sslMode:
|
||||||
|
description: "Use encrypted connections to the PostgreSQL server used by Telegraf. Must be one of the following values: disable, allow, prefer, require, verify-ca, verify-full."
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
database:
|
||||||
|
description: Database to authenticate to on the PostgreSQL server.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
user:
|
||||||
|
description: Username to authenticate to the PostgreSQL server used by Telegraf.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
password:
|
||||||
|
description: Password used to authenticate to the PostgreSQL server used by Telegraf.
|
||||||
|
global: True
|
||||||
|
sensitive: True
|
||||||
|
advanced: True
|
||||||
|
cacheExpirationMs:
|
||||||
|
description: The interval (in milliseconds) to wait before querying the DB for updated metrics.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
maxMetricAgeSeconds:
|
||||||
|
description: The maximum age (in seconds) of metrics to display in the SOC Grid Metrics view. Metrics older than this value will not be displayed.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
alarms:
|
||||||
|
description: JSON list of metric alarms. Modify via the SOC Grid Alarms view.
|
||||||
|
readonlyUi: True
|
||||||
|
advanced: True
|
||||||
|
global: True
|
||||||
|
forcedType: string
|
||||||
|
syntax: json
|
||||||
|
storage: db
|
||||||
salt:
|
salt:
|
||||||
longRelayTimeoutMs:
|
longRelayTimeoutMs:
|
||||||
description: Duration (in milliseconds) to wait for a response from the Salt API when executing tasks known for being long running before giving up and showing an error on the SOC UI.
|
description: Duration (in milliseconds) to wait for a response from the Salt API when executing tasks known for being long running before giving up and showing an error on the SOC UI.
|
||||||
@@ -791,6 +839,7 @@ soc:
|
|||||||
- gemini
|
- gemini
|
||||||
- openai_responses
|
- openai_responses
|
||||||
- openai_chat
|
- openai_chat
|
||||||
|
- openai_embeddings
|
||||||
- field: apiUrl
|
- field: apiUrl
|
||||||
label: API URL
|
label: API URL
|
||||||
required: False
|
required: False
|
||||||
@@ -812,6 +861,15 @@ soc:
|
|||||||
description: Indicates if the Assistant Module should operate in agentic mode or not. If true, agents can work together to solve tasks.
|
description: Indicates if the Assistant Module should operate in agentic mode or not. If true, agents can work together to solve tasks.
|
||||||
global: True
|
global: True
|
||||||
forcedType: bool
|
forcedType: bool
|
||||||
|
automations:
|
||||||
|
description: Scheduled automations for the Onion AI assistant, managed from the Agent Studio.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
readonlyUi: True
|
||||||
|
storage: db
|
||||||
|
forcedType: string
|
||||||
|
syntax: json
|
||||||
|
helpLink: onion-ai
|
||||||
agents:
|
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.
|
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
|
global: True
|
||||||
@@ -844,6 +902,9 @@ soc:
|
|||||||
- field: persona
|
- field: persona
|
||||||
label: Persona
|
label: Persona
|
||||||
multiline: True
|
multiline: True
|
||||||
|
- field: maxConcurrentInstances
|
||||||
|
label: Max Concurrent Instances
|
||||||
|
forcedType: int
|
||||||
skills:
|
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.
|
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
|
global: True
|
||||||
@@ -947,6 +1008,43 @@ soc:
|
|||||||
description: The number of times to retry extracting memories from a session if errors occur.
|
description: The number of times to retry extracting memories from a session if errors occur.
|
||||||
global: True
|
global: True
|
||||||
advanced: True
|
advanced: True
|
||||||
|
agentSessionMaxTurns:
|
||||||
|
description: Maximum number of model turns a headless agent session, such as one started by an automation, may take before it is stopped. Turns taken by delegated sub-agents count toward this limit. A session that reaches the limit is recorded as failed.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
forcedType: int
|
||||||
|
agentStreamFlushIntervalMs:
|
||||||
|
description: Milliseconds between writes of a streaming headless agent turn to the database. Lower values show progress sooner in the Agent Studio at the cost of more frequent Elasticsearch updates.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
forcedType: int
|
||||||
|
agentStreamIdleTimeoutSeconds:
|
||||||
|
description: Seconds a streaming headless agent turn may go without receiving any output before it is abandoned and the session is recorded as failed. Set to 0 to disable the timeout.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
forcedType: int
|
||||||
|
automationSettings:
|
||||||
|
tickIntervalSeconds:
|
||||||
|
description: How often, in seconds, the automation scheduler checks for automations that are due to run. Must be greater than 0.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
forcedType: int
|
||||||
|
maxConcurrentItems:
|
||||||
|
description: Maximum number of automation work items that may run at the same time. Additional work items wait in the queue until a running item finishes. User chat sessions count toward this limit but are never held back by it. Set to 0 to disable the limit.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
forcedType: int
|
||||||
|
maxQueuedItems:
|
||||||
|
description: Maximum number of automation work items that may wait to start. Once the queue is full, no new work items are created until the backlog drains. Set to 0 to disable the limit.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
forcedType: int
|
||||||
|
alertTriageEpoch:
|
||||||
|
description: The earliest alert time the Alert Triage automation will consider. Alerts before this time are never triaged, which keeps a first run on an existing deployment from working through old history. Must be in UTC format (2026-09-24T00:00:00Z).
|
||||||
|
regex: '^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z)?$'
|
||||||
|
regexFailureMessage: Expecting date in RFC3339 format (2026-09-24T00:00:00Z)
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
tools:
|
tools:
|
||||||
filterEventFields:
|
filterEventFields:
|
||||||
description: A whitelist of fields to return when OnionAI uses the query_events tool. All other fields are removed. One field per line.
|
description: A whitelist of fields to return when OnionAI uses the query_events tool. All other fields are removed. One field per line.
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ suricata:
|
|||||||
- gid: 940
|
- gid: 940
|
||||||
- home: /nsm/suricata
|
- home: /nsm/suricata
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
socoregroupwithsuricata:
|
socoregroupwithsuricata:
|
||||||
group.present:
|
group.present:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ zeek:
|
|||||||
- gid: 937
|
- gid: 937
|
||||||
- home: /opt/so/conf/zeek
|
- home: /opt/so/conf/zeek
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
# Create some directories
|
# Create some directories
|
||||||
zeekpolicydir:
|
zeekpolicydir:
|
||||||
|
|||||||
Reference in new issue
Block a user