Merge pull request #15624 from Security-Onion-Solutions/moreja

Add SOC UI toggle for JA4+ fingerprinting
This commit is contained in:
Josh Patterson
2026-03-17 09:44:04 -04:00
committed by GitHub
89 changed files with 81 additions and 20 deletions

View File

@@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.13"]
python-version: ["3.14"]
python-code-path: ["salt/sensoroni/files/analyzers", "salt/manager/tools/sbin"]
steps:

View File

@@ -67,7 +67,7 @@ delete_so-hydra_so-status.disabled:
wait_for_hydra:
http.wait_for_successful_query:
- name: 'http://{{ GLOBALS.manager }}:4444/'
- name: 'http://{{ GLOBALS.manager }}:4444/health/alive'
- ssl: True
- verify_ssl: False
- status:

View File

@@ -134,8 +134,8 @@ function require() {
function verifyEnvironment() {
require "jq"
require "curl"
response=$(curl -Ss -L ${hydraUrl}/)
[[ "$response" != *"Error 404"* ]] && fail "Unable to communicate with Hydra; specify URL via HYDRA_URL environment variable"
response=$(curl -Ss -L ${hydraUrl}/health/alive)
[[ "$response" != '{"status":"ok"}' ]] && fail "Unable to communicate with Hydra; specify URL via HYDRA_URL environment variable"
}
function createFile() {

View File

@@ -22,7 +22,7 @@ def showUsage(args):
print(' removelistitem - Remove a list item from a yaml key, if it exists and is a list. Requires KEY and LISTITEM args.', file=sys.stderr)
print(' replacelistobject - Replace a list object based on a condition. Requires KEY, CONDITION_FIELD, CONDITION_VALUE, and JSON_OBJECT args.', file=sys.stderr)
print(' add - Add a new key and set its value. Fails if key already exists. Requires KEY and VALUE args.', file=sys.stderr)
print(' get - Displays (to stdout) the value stored in the given key. Requires KEY arg.', file=sys.stderr)
print(' get [-r] - Displays (to stdout) the value stored in the given key. Requires KEY arg. Use -r for raw output without YAML formatting.', file=sys.stderr)
print(' remove - Removes a yaml key, if it exists. Requires KEY arg.', file=sys.stderr)
print(' replace - Replaces (or adds) a new key and set its value. Requires KEY and VALUE args.', file=sys.stderr)
print(' help - Prints this usage information.', file=sys.stderr)
@@ -332,6 +332,11 @@ def getKeyValue(content, key):
def get(args):
raw = False
if len(args) > 0 and args[0] == '-r':
raw = True
args = args[1:]
if len(args) != 2:
print('Missing filename or key arg', file=sys.stderr)
showUsage(None)
@@ -346,12 +351,15 @@ def get(args):
print(f"Key '{key}' not found by so-yaml.py", file=sys.stderr)
return 2
if isinstance(output, bool):
print(str(output).lower())
elif isinstance(output, (dict, list)):
print(yaml.safe_dump(output).strip())
if raw:
if isinstance(output, bool):
print(str(output).lower())
elif isinstance(output, (dict, list)):
print(yaml.safe_dump(output).strip())
else:
print(output)
else:
print(output)
print(yaml.safe_dump(output))
return 0

View File

@@ -393,6 +393,17 @@ class TestRemove(unittest.TestCase):
result = soyaml.get([filename, "key1.child2.deep1"])
self.assertEqual(result, 0)
self.assertIn("45\n...", mock_stdout.getvalue())
def test_get_int_raw(self):
with patch('sys.stdout', new=StringIO()) as mock_stdout:
filename = "/tmp/so-yaml_test-get.yaml"
file = open(filename, "w")
file.write("{key1: { child1: 123, child2: { deep1: 45 } }, key2: false, key3: [e,f,g]}")
file.close()
result = soyaml.get(["-r", filename, "key1.child2.deep1"])
self.assertEqual(result, 0)
self.assertEqual("45\n", mock_stdout.getvalue())
def test_get_str(self):
@@ -404,6 +415,17 @@ class TestRemove(unittest.TestCase):
result = soyaml.get([filename, "key1.child2.deep1"])
self.assertEqual(result, 0)
self.assertIn("hello\n...", mock_stdout.getvalue())
def test_get_str_raw(self):
with patch('sys.stdout', new=StringIO()) as mock_stdout:
filename = "/tmp/so-yaml_test-get.yaml"
file = open(filename, "w")
file.write("{key1: { child1: 123, child2: { deep1: \"hello\" } }, key2: false, key3: [e,f,g]}")
file.close()
result = soyaml.get(["-r", filename, "key1.child2.deep1"])
self.assertEqual(result, 0)
self.assertEqual("hello\n", mock_stdout.getvalue())
def test_get_bool(self):
@@ -415,8 +437,31 @@ class TestRemove(unittest.TestCase):
result = soyaml.get([filename, "key2"])
self.assertEqual(result, 0)
self.assertIn("false\n...", mock_stdout.getvalue())
def test_get_bool_raw(self):
with patch('sys.stdout', new=StringIO()) as mock_stdout:
filename = "/tmp/so-yaml_test-get.yaml"
file = open(filename, "w")
file.write("{key1: { child1: 123, child2: { deep1: 45 } }, key2: false, key3: [e,f,g]}")
file.close()
result = soyaml.get(["-r", filename, "key2"])
self.assertEqual(result, 0)
self.assertEqual("false\n", mock_stdout.getvalue())
def test_get_dict_raw(self):
with patch('sys.stdout', new=StringIO()) as mock_stdout:
filename = "/tmp/so-yaml_test-get.yaml"
file = open(filename, "w")
file.write("{key1: { child1: 123, child2: { deep1: 45 } }, key2: false, key3: [e,f,g]}")
file.close()
result = soyaml.get(["-r", filename, "key1"])
self.assertEqual(result, 0)
self.assertIn("child1: 123", mock_stdout.getvalue())
self.assertNotIn("...", mock_stdout.getvalue())
def test_get_list(self):
with patch('sys.stdout', new=StringIO()) as mock_stdout:
filename = "/tmp/so-yaml_test-get.yaml"

View File

@@ -396,7 +396,7 @@ migrate_pcap_to_suricata() {
for pillar_file in "$PCAPFILE" "$MINIONDIR"/*.sls; do
[[ -f "$pillar_file" ]] || continue
pcap_enabled=$(so-yaml.py get "$pillar_file" pcap.enabled 2>/dev/null) || continue
pcap_enabled=$(so-yaml.py get -r "$pillar_file" pcap.enabled 2>/dev/null) || continue
so-yaml.py add "$pillar_file" suricata.pcap.enabled "$pcap_enabled"
so-yaml.py remove "$pillar_file" pcap
done

View File

@@ -156,6 +156,9 @@ zeekja4cfg:
- source: salt://zeek/files/config.zeek.ja4
- user: 937
- group: 939
- template: jinja
- defaults:
JA4PLUS_ENABLED: {{ ZEEKMERGED.ja4plus_enabled }}
# BPF compilation failed
{% if ZEEKBPF and not ZEEK_BPF_STATUS %}

View File

@@ -1,5 +1,6 @@
zeek:
enabled: False
ja4plus_enabled: False
config:
node:
lb_procs: 0

View File

@@ -8,20 +8,20 @@ export {
option JA4_raw: bool = F;
# FoxIO license required for JA4+
option JA4S_enabled: bool = F;
option JA4S_enabled: bool = {{ 'T' if JA4PLUS_ENABLED else 'F' }};
option JA4S_raw: bool = F;
option JA4D_enabled: bool = F;
option JA4D_enabled: bool = {{ 'T' if JA4PLUS_ENABLED else 'F' }};
option JA4H_enabled: bool = F;
option JA4H_enabled: bool = {{ 'T' if JA4PLUS_ENABLED else 'F' }};
option JA4H_raw: bool = F;
option JA4L_enabled: bool = F;
option JA4SSH_enabled: bool = F;
option JA4L_enabled: bool = {{ 'T' if JA4PLUS_ENABLED else 'F' }};
option JA4T_enabled: bool = F;
option JA4TS_enabled: bool = F;
option JA4SSH_enabled: bool = {{ 'T' if JA4PLUS_ENABLED else 'F' }};
option JA4X_enabled: bool = F;
option JA4T_enabled: bool = {{ 'T' if JA4PLUS_ENABLED else 'F' }};
option JA4TS_enabled: bool = {{ 'T' if JA4PLUS_ENABLED else 'F' }};
option JA4X_enabled: bool = {{ 'T' if JA4PLUS_ENABLED else 'F' }};
}

View File

@@ -2,6 +2,10 @@ zeek:
enabled:
description: Controls whether the Zeek (network packet inspection) process runs. Disabling this process could result in loss of network protocol metadata. If Suricata was selected as the protocol metadata engine during setup then this will already be disabled.
helpLink: zeek.html
ja4plus_enabled:
description: "Enables JA4+ fingerprinting (JA4S, JA4D, JA4H, JA4L, JA4SSH, JA4T, JA4TS, JA4X). By enabling this, you agree to the terms of the JA4+ license (https://github.com/FoxIO-LLC/ja4/blob/main/LICENSE-JA4)."
forcedType: bool
helpLink: zeek.html
config:
local:
load: