Bug #8681 ยป suricata-verify-unix-and-traversal.patch
| run.py | ||
|---|---|---|
|
import traceback
|
||
|
import platform
|
||
|
import signal
|
||
|
import socket
|
||
|
VALIDATE_EVE = False
|
||
|
def unix_socket_v2_shutdown(sock_path, timeout=10):
|
||
|
"""Unix command socket V2: handshake then shutdown (no suricatasc)."""
|
||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||
|
sock.settimeout(timeout)
|
||
|
try:
|
||
|
sock.connect(sock_path)
|
||
|
sock.sendall(b'{"version":"0.2"}\n')
|
||
|
while True:
|
||
|
chunk = sock.recv(1)
|
||
|
if not chunk:
|
||
|
raise OSError("unexpected EOF during unix socket handshake")
|
||
|
if chunk == b"\n":
|
||
|
break
|
||
|
sock.sendall((json.dumps({"command": "shutdown"}) + "\n").encode())
|
||
|
finally:
|
||
|
sock.close()
|
||
|
WIN32 = sys.platform == "win32"
|
||
|
suricata_yaml = "suricata.yaml" if WIN32 else "./suricata.yaml"
|
||
| ... | ... | |
|
output = subprocess.check_output([suricata_bin, "--build-info"])
|
||
|
start_support = False
|
||
|
for line in output.splitlines():
|
||
|
if line.decode().startswith("Features:"):
|
||
|
self.features = set(line.decode().split()[1:])
|
||
|
if "Suricata Configuration" in line.decode():
|
||
|
decoded = line.decode()
|
||
|
if decoded.startswith("Features:"):
|
||
|
self.features = set(decoded.split()[1:])
|
||
|
if "Suricata Configuration" in decoded:
|
||
|
start_support = True
|
||
|
if start_support and "support:" in line.decode():
|
||
|
(fkey, val) = line.decode().split(" support:")
|
||
|
if start_support and " support:" in decoded:
|
||
|
(fkey, val) = decoded.split(" support:")
|
||
|
fkey = fkey.strip()
|
||
|
val = val.strip()
|
||
|
if val.startswith("yes"):
|
||
|
self.features.add(fkey)
|
||
|
# Not covered by the generic "* support: yes" parse (uses "enabled:" instead).
|
||
|
if "Unix socket enabled:" in decoded:
|
||
|
rest = decoded.split("Unix socket enabled:", 1)[1].strip()
|
||
|
if rest.startswith("yes"):
|
||
|
self.features.add("UNIX_SOCKET")
|
||
|
def load_config(self, config_filename):
|
||
|
output = subprocess.check_output([
|
||
| ... | ... | |
|
env=self.build_env())
|
||
|
def check_unix_socket(self):
|
||
|
if "unix-commands" in self.config:
|
||
|
uses_cmds = "unix-commands" in self.config
|
||
|
uses_scripts = "unix-socket-scripts" in self.config
|
||
|
if not uses_cmds and not uses_scripts:
|
||
|
return
|
||
|
if uses_cmds:
|
||
|
if HAS_SURICATA_SC is None:
|
||
|
raise UnsatisfiedRequirementError("skipping unix socket tests")
|
||
|
elif not HAS_SURICATA_SC:
|
||
|
raise UnsatisfiedRequirementError("missing suricatasc")
|
||
|
if not self.suricata_config.has_feature("UNIX_SOCKET"):
|
||
|
raise UnsatisfiedRequirementError("requires feature UNIX_SOCKET")
|
||
|
# 104 on MacOS, 108 on Linux
|
||
|
if len(os.path.join(self.output, "socket")) > 104:
|
||
|
raise UnsatisfiedRequirementError("requires shorter path for unix socket")
|
||
|
if not self.suricata_config.has_feature("UNIX_SOCKET"):
|
||
|
raise UnsatisfiedRequirementError("requires feature UNIX_SOCKET")
|
||
|
# 104 on MacOS, 108 on Linux
|
||
|
if len(os.path.join(self.output, "socket")) > 104:
|
||
|
raise UnsatisfiedRequirementError("requires shorter path for unix socket")
|
||
|
def check_skip(self):
|
||
|
if not "skip" in self.config:
|
||
| ... | ... | |
|
env["TZ"] = "UTC"
|
||
|
env["TEST_DIR"] = self.directory
|
||
|
env["OUTPUT_DIR"] = self.output
|
||
|
env["TEST_SOCKET_SLUG"] = re.sub(r"[^a-zA-Z0-9_.-]", "_", self.name)[:80]
|
||
|
if not "ASAN_OPTIONS" in env:
|
||
|
env["ASAN_OPTIONS"] = "detect_leaks=1"
|
||
|
if self.config.get("env"):
|
||
| ... | ... | |
|
shell = True
|
||
|
else:
|
||
|
args = self.default_args()
|
||
|
if "unix-commands" in self.config:
|
||
|
args.append("--unix-socket=%s" % os.path.join(self.output, "socket"))
|
||
|
if "unix-commands" in self.config or "unix-socket-scripts" in self.config:
|
||
|
has_unix_sock_arg = any(
|
||
|
x == "--unix-socket" or x.startswith("--unix-socket=")
|
||
|
for x in args
|
||
|
)
|
||
|
if not has_unix_sock_arg:
|
||
|
args.append("--unix-socket=%s" % os.path.join(self.output, "socket"))
|
||
|
env = self.build_env()
|
||
| ... | ... | |
|
args, shell=shell, cwd=self.directory, env=env,
|
||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||
|
if "unix-commands" in self.config:
|
||
|
uses_unix_cmds = "unix-commands" in self.config
|
||
|
uses_unix_scripts = "unix-socket-scripts" in self.config
|
||
|
if uses_unix_cmds or uses_unix_scripts:
|
||
|
timeout = 2
|
||
|
if "startup-timeout" in self.config:
|
||
|
timeout = self.config["startup-timeout"]
|
||
|
sock_path = os.path.join(self.output, "socket")
|
||
|
for a in reversed(args):
|
||
|
if a.startswith("--unix-socket="):
|
||
|
cand = os.path.expanduser(a.split("=", 1)[1])
|
||
|
if os.path.isabs(cand):
|
||
|
sock_path = cand
|
||
|
break
|
||
|
start = self.wait_suricata_start(p, timeout)
|
||
|
if start is None:
|
||
|
p.terminate()
|
||
|
raise TestError("Suricata did not start engine before timeout")
|
||
|
else:
|
||
|
stdout.write(start)
|
||
|
f = open(os.path.join(self.output, "sc.json"), "w")
|
||
|
for cmd in self.config["unix-commands"]:
|
||
|
argsl = [os.path.join(self.cwd, suricatasc_bin), os.path.join(self.output, "socket"), "-c", cmd]
|
||
|
try:
|
||
|
script_env = dict(env)
|
||
|
script_env["SOCKET_PATH"] = sock_path
|
||
|
if uses_unix_scripts:
|
||
|
for script in self.config["unix-socket-scripts"]:
|
||
|
spath = os.path.join(self.directory, script)
|
||
|
subprocess.check_call(
|
||
|
argsl, shell=shell, cwd=self.directory, env=env,
|
||
|
stdout=f, stderr=subprocess.PIPE, timeout=timeout)
|
||
|
except:
|
||
|
raise TestError("got non zero exit code for unix-socket command %s" % cmd);
|
||
|
f.close()
|
||
|
argsl = [os.path.join(self.cwd, suricatasc_bin), os.path.join(self.output, "socket"), "-c", "shutdown"]
|
||
|
subprocess.check_call(
|
||
|
argsl, shell=shell, cwd=self.directory, env=env,
|
||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
|
||
|
[sys.executable, "-u", spath],
|
||
|
cwd=self.directory,
|
||
|
env=script_env,
|
||
|
timeout=timeout,
|
||
|
)
|
||
|
if uses_unix_cmds:
|
||
|
f = open(os.path.join(self.output, "sc.json"), "w")
|
||
|
for cmd in self.config["unix-commands"]:
|
||
|
argsl = [
|
||
|
os.path.join(self.cwd, suricatasc_bin),
|
||
|
sock_path,
|
||
|
"-c",
|
||
|
cmd,
|
||
|
]
|
||
|
try:
|
||
|
subprocess.check_call(
|
||
|
argsl,
|
||
|
shell=shell,
|
||
|
cwd=self.directory,
|
||
|
env=env,
|
||
|
stdout=f,
|
||
|
stderr=subprocess.PIPE,
|
||
|
timeout=timeout,
|
||
|
)
|
||
|
except Exception:
|
||
|
raise TestError(
|
||
|
"got non zero exit code for unix-socket command %s"
|
||
|
% cmd
|
||
|
)
|
||
|
f.close()
|
||
|
argsl = [
|
||
|
os.path.join(self.cwd, suricatasc_bin),
|
||
|
sock_path,
|
||
|
"-c",
|
||
|
"shutdown",
|
||
|
]
|
||
|
subprocess.check_call(
|
||
|
argsl,
|
||
|
shell=shell,
|
||
|
cwd=self.directory,
|
||
|
env=env,
|
||
|
stdout=subprocess.PIPE,
|
||
|
stderr=subprocess.PIPE,
|
||
|
timeout=timeout,
|
||
|
)
|
||
|
elif uses_unix_scripts:
|
||
|
unix_socket_v2_shutdown(sock_path, timeout=timeout)
|
||
|
# used to get a return value from the threads
|
||
| tests/datarep-rule-traversal-01/suricata.yaml | ||
|---|---|---|
|
%YAML 1.1
|
||
|
---
|
||
|
engine-analysis:
|
||
|
rules: yes
|
||
|
logging:
|
||
|
outputs:
|
||
|
- file:
|
||
|
enabled: yes
|
||
|
filename: eve.json
|
||
|
type: json
|
||
| tests/datarep-rule-traversal-01/test.rules | ||
|---|---|---|
|
alert dns any any -> any any (dns.query; datarep:dns_reps, >, 200, load ../evil.rep, type string; sid:1; rev:1;)
|
||
| tests/datarep-rule-traversal-01/test.yaml | ||
|---|---|---|
|
requires:
|
||
|
min-version: 7
|
||
|
pcap: false
|
||
|
exit-code: 1
|
||
|
args:
|
||
|
- --engine-analysis
|
||
|
checks:
|
||
|
# Stage 1: the unpatched error ("failed to set up datarep set") does not
|
||
|
# expose a raw "No such file or directory" โ the datarep module wraps the
|
||
|
# error, so count: 0 is satisfied even on unpatched builds. After the fix
|
||
|
# this still holds because the traversal is caught before the open attempt.
|
||
|
- filter:
|
||
|
requires:
|
||
|
lambda: sys.platform != "win32"
|
||
|
count: 0
|
||
|
match:
|
||
|
log_level: Error
|
||
|
engine.message.__find: "No such file or directory"
|
||
|
# Stage 2: traversal must be caught and reported.
|
||
|
# Fails on unpatched (message not emitted), passes after fix.
|
||
|
- filter:
|
||
|
requires:
|
||
|
lambda: sys.platform != "win32"
|
||
|
count: 1
|
||
|
match:
|
||
|
log_level: Error
|
||
|
engine.message.__find: "Directory traversals not allowed"
|
||
| tests/filemd5-rule-traversal-01/suricata.yaml | ||
|---|---|---|
|
%YAML 1.1
|
||
|
---
|
||
|
engine-analysis:
|
||
|
rules: yes
|
||
|
logging:
|
||
|
outputs:
|
||
|
- file:
|
||
|
enabled: yes
|
||
|
filename: eve.json
|
||
|
type: json
|
||
| tests/filemd5-rule-traversal-01/test.rules | ||
|---|---|---|
|
alert http any any -> any any (msg:"TEST FILEMD5 TRAVERSAL"; filemd5:../evil.md5; sid:1; rev:1;)
|
||
| tests/filemd5-rule-traversal-01/test.yaml | ||
|---|---|---|
|
requires:
|
||
|
min-version: 7
|
||
|
pcap: false
|
||
|
exit-code: 1
|
||
|
args:
|
||
|
- --engine-analysis
|
||
|
checks:
|
||
|
# Stage 1: on unpatched builds Suricata resolves ../evil.md5 and logs "No
|
||
|
# such file or directory", so count: 0 fails on unpatched. After the fix
|
||
|
# the traversal is caught before the open attempt and this message is absent,
|
||
|
# so count: 0 is satisfied on patched builds as well.
|
||
|
- filter:
|
||
|
requires:
|
||
|
lambda: sys.platform != "win32"
|
||
|
count: 0
|
||
|
match:
|
||
|
log_level: Error
|
||
|
engine.message.__find: "No such file or directory"
|
||
|
# Stage 2: traversal must be caught and reported.
|
||
|
# Fails on unpatched (message not emitted), passes after fix.
|
||
|
- filter:
|
||
|
requires:
|
||
|
lambda: sys.platform != "win32"
|
||
|
count: 1
|
||
|
match:
|
||
|
log_level: Error
|
||
|
engine.message.__find: "Directory traversals not allowed"
|
||
| tests/lua-rule-traversal-01/suricata.yaml | ||
|---|---|---|
|
%YAML 1.1
|
||
|
---
|
||
|
engine-analysis:
|
||
|
rules: yes
|
||
|
logging:
|
||
|
outputs:
|
||
|
- file:
|
||
|
enabled: yes
|
||
|
filename: eve.json
|
||
|
type: json
|
||
| tests/lua-rule-traversal-01/test.rules | ||
|---|---|---|
|
alert http any any -> any any (msg:"TEST LUA TRAVERSAL"; lua:../evil.lua; sid:1; rev:1;)
|
||
| tests/lua-rule-traversal-01/test.yaml | ||
|---|---|---|
|
requires:
|
||
|
features:
|
||
|
- HAVE_LUA
|
||
|
min-version: 7
|
||
|
pcap: false
|
||
|
exit-code: 1
|
||
|
args:
|
||
|
- --engine-analysis
|
||
|
- --set security.lua.allow-rules=true
|
||
|
checks:
|
||
|
# Stage 1: on unpatched builds Suricata resolves ../evil.lua and logs "No
|
||
|
# such file or directory", so count: 0 fails on unpatched. After the fix
|
||
|
# the traversal is caught before the open attempt and this message is absent,
|
||
|
# so count: 0 is satisfied on patched builds as well.
|
||
|
- filter:
|
||
|
requires:
|
||
|
lambda: sys.platform != "win32"
|
||
|
count: 0
|
||
|
match:
|
||
|
log_level: Error
|
||
|
engine.message.__find: "No such file or directory"
|
||
|
# Stage 2: traversal must be caught and reported.
|
||
|
# Fails on unpatched (message not emitted), passes after fix.
|
||
|
- filter:
|
||
|
requires:
|
||
|
lambda: sys.platform != "win32"
|
||
|
count: 1
|
||
|
match:
|
||
|
log_level: Error
|
||
|
engine.message.__find: "Directory traversals not allowed"
|
||
| tests/unix-socket/marker.rule | ||
|---|---|---|
|
alert tcp any any -> any any (msg:"SV unix-socket tenant marker"; sid:90004201; rev:1;)
|
||
| tests/unix-socket/tenant-outside.yaml | ||
|---|---|---|
|
%YAML 1.1
|
||
|
---
|
||
|
# Loaded via register-tenant with path traversal relative to a nested test dir.
|
||
|
rule-files:
|
||
|
- marker.rule
|
||
| tests/unix-socket/unix-socket-conf-get-01/README.md | ||
|---|---|---|
|
# unix-socket-conf-get-01
|
||
|
Unix command socket: `conf-get` returns configuration values to any client that can
|
||
|
connect to the socket (no authentication). Uses `sensor-name` set via `--set` as a
|
||
|
deterministic marker string.
|
||
|
Related: Unix socket trust-boundary report (information disclosure); see also
|
||
|
`unix-socket-high4-no-auth-01` for the same issue using a raw Python client (no
|
||
|
`suricatasc`).
|
||
|
Requires: Suricata built with Unix socket support; `suricatasc` from the Rust tree.
|
||
|
## PCAP
|
||
|
None (`pcap: false`).
|
||
| tests/unix-socket/unix-socket-conf-get-01/test.yaml | ||
|---|---|---|
|
# Unix command socket: conf-get exposes arbitrary configuration values to any
|
||
|
# client that can connect (no authentication). Documents HIGH-5 / trust-boundary.
|
||
|
requires:
|
||
|
min-version: 7
|
||
|
pcap: false
|
||
|
args:
|
||
|
# Marker value retrievable via unix socket without credentials.
|
||
|
- --set sensor-name=SURICATA_VERIFY_UNIX_SOCKET_CONF_GET_MARKER
|
||
|
unix-commands:
|
||
|
# First argument is the configuration variable name (see suricatasc positional args).
|
||
|
- "conf-get sensor-name"
|
||
|
checks:
|
||
|
- filter:
|
||
|
filename: sc.json
|
||
|
count: 1
|
||
|
match:
|
||
|
return: OK
|
||
|
message: SURICATA_VERIFY_UNIX_SOCKET_CONF_GET_MARKER
|
||
| tests/unix-socket/unix-socket-high4-no-auth-01/README.md | ||
|---|---|---|
|
# unix-socket-high4-no-auth-01
|
||
|
**HIGH-4** documents the missing authentication / trust-boundary on the Unix command
|
||
|
socket: `no_auth_conf_get.py` performs only the normal V2 handshake (`version` string)
|
||
|
and then sends `conf-get` for `sensor-name`. There is no secret, token, or capability
|
||
|
check โ any peer that can `connect(2)` to the socket receives configuration values.
|
||
|
`suricatasc` is not used for that step so the test proves the server accepts raw JSON
|
||
|
commands without an auxiliary auth channel.
|
||
| tests/unix-socket/unix-socket-high4-no-auth-01/no_auth_conf_get.py | ||
|---|---|---|
|
#!/usr/bin/env python3
|
||
|
# HIGH-4: Unix command socket accepts V2 commands with no authentication token or
|
||
|
# handshake secret beyond the fixed protocol version string.
|
||
|
import json
|
||
|
import os
|
||
|
import socket
|
||
|
SOCKET_PATH = os.environ["SOCKET_PATH"]
|
||
|
OUTPUT_DIR = os.environ["OUTPUT_DIR"]
|
||
|
MARKER = "SURICATA_VERIFY_HIGH4_NO_AUTH_MARKER"
|
||
|
OUT = os.path.join(OUTPUT_DIR, "unix-no-auth-result.json")
|
||
|
def read_line(sock):
|
||
|
buf = b""
|
||
|
while True:
|
||
|
chunk = sock.recv(1)
|
||
|
if not chunk:
|
||
|
return None
|
||
|
buf += chunk
|
||
|
if chunk == b"\n":
|
||
|
return buf[:-1]
|
||
|
def main():
|
||
|
ok = False
|
||
|
marker_seen = False
|
||
|
preview = ""
|
||
|
sock = None
|
||
|
try:
|
||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||
|
sock.settimeout(15)
|
||
|
sock.connect(SOCKET_PATH)
|
||
|
sock.sendall(b'{"version":"0.2"}\n')
|
||
|
handshake = read_line(sock)
|
||
|
if handshake is None:
|
||
|
preview = "eof_handshake"
|
||
|
else:
|
||
|
hj = json.loads(handshake.decode("utf-8"))
|
||
|
if hj.get("return") != "OK":
|
||
|
preview = handshake.decode("utf-8", errors="replace")[:500]
|
||
|
else:
|
||
|
cmd = {
|
||
|
"command": "conf-get",
|
||
|
"arguments": {"variable": "sensor-name"},
|
||
|
}
|
||
|
sock.sendall((json.dumps(cmd) + "\n").encode())
|
||
|
resp = read_line(sock)
|
||
|
if resp:
|
||
|
rj = json.loads(resp.decode("utf-8"))
|
||
|
ok = rj.get("return") == "OK"
|
||
|
msg = rj.get("message", "")
|
||
|
marker_seen = msg == MARKER
|
||
|
preview = resp.decode("utf-8", errors="replace")[:500]
|
||
|
else:
|
||
|
preview = "eof_after_conf_get"
|
||
|
except Exception as e:
|
||
|
preview = "%s:%s" % (type(e).__name__, e)
|
||
|
finally:
|
||
|
if sock is not None:
|
||
|
try:
|
||
|
sock.close()
|
||
|
except OSError:
|
||
|
pass
|
||
|
with open(OUT, "w") as f:
|
||
|
json.dump(
|
||
|
{
|
||
|
"unauthenticated_conf_get_ok": ok,
|
||
|
"marker_seen": marker_seen,
|
||
|
"response_preview": preview,
|
||
|
},
|
||
|
f,
|
||
|
)
|
||
|
if __name__ == "__main__":
|
||
|
main()
|
||
| tests/unix-socket/unix-socket-high4-no-auth-01/test.yaml | ||
|---|---|---|
|
# HIGH-4: Any local user who can connect to the command socket may invoke privileged
|
||
|
# commands (here: conf-get) โ there is no credential or auth token beyond the fixed
|
||
|
# protocol version handshake.
|
||
|
requires:
|
||
|
min-version: 7
|
||
|
pcap: false
|
||
|
startup-timeout: 5
|
||
|
args:
|
||
|
- --set sensor-name=SURICATA_VERIFY_HIGH4_NO_AUTH_MARKER
|
||
|
unix-socket-scripts:
|
||
|
- no_auth_conf_get.py
|
||
|
unix-commands:
|
||
|
- version
|
||
|
checks:
|
||
|
- filter:
|
||
|
filename: unix-no-auth-result.json
|
||
|
count: 1
|
||
|
match:
|
||
|
unauthenticated_conf_get_ok: true
|
||
|
marker_seen: true
|
||
| tests/unix-socket/unix-socket-high8-toctou-sequence-01/README.md | ||
|---|---|---|
|
# unix-socket-high8-toctou-sequence-01
|
||
|
**HIGH-8** is the time-of-check/time-of-use gap when ensuring `SOCKET_PATH` exists:
|
||
|
`stat(SOCKET_PATH)` then `SCMkDir(SOCKET_PATH)` inside `UnixNew()`.
|
||
|
A reliable race harness is not practical in `suricata-verify` (narrow window, layout
|
||
|
depends on install paths for relative `unix-command.filename`). Instead,
|
||
|
`toctou_stat_mkdir_order.py` asserts that both calls remain present in the expected
|
||
|
order inside the `check_dir` block. When Suricata replaces this with a safe primitive,
|
||
|
remove or rewrite this check accordingly.
|
||
| tests/unix-socket/unix-socket-high8-toctou-sequence-01/test.yaml | ||
|---|---|---|
|
# HIGH-8: TOCTOU between stat(SOCKET_PATH) and SCMkDir(SOCKET_PATH) in UnixNew().
|
||
|
# Runtime exploitation is non-deterministic in CI; this guards the vulnerable
|
||
|
# sequence in source until the directory creation path is fixed safely.
|
||
|
requires:
|
||
|
min-version: 7
|
||
|
pcap: false
|
||
|
lambda: sys.platform != "win32"
|
||
|
args:
|
||
|
- --dump-config
|
||
|
checks:
|
||
|
- shell:
|
||
|
args: python3 "${TEST_DIR}/toctou_stat_mkdir_order.py"
|
||
| tests/unix-socket/unix-socket-high8-toctou-sequence-01/toctou_stat_mkdir_order.py | ||
|---|---|---|
|
#!/usr/bin/env python3
|
||
|
# HIGH-8: UnixNew() uses stat(SOCKET_PATH) followed by SCMkDir(SOCKET_PATH), which
|
||
|
# is a classic TOCTOU window on the runtime socket directory. This script is a static
|
||
|
# regression check: if the engine is hardened (e.g. atomic mkdir or equivalent),
|
||
|
# update this test accordingly.
|
||
|
import os
|
||
|
import sys
|
||
|
def main():
|
||
|
srcdir = os.environ.get("SRCDIR", "")
|
||
|
path = os.path.join(srcdir, "src", "unix-manager.c")
|
||
|
if not os.path.isfile(path):
|
||
|
print("missing %s (need Suricata source tree as SRCDIR)" % path, file=sys.stderr)
|
||
|
sys.exit(1)
|
||
|
with open(path, encoding="utf-8") as f:
|
||
|
text = f.read()
|
||
|
try:
|
||
|
start = text.index("if (check_dir)")
|
||
|
end = text.index("/* Remove socket file */")
|
||
|
except ValueError:
|
||
|
print("unix-manager.c layout changed; update TOCTOU regression script", file=sys.stderr)
|
||
|
sys.exit(1)
|
||
|
chunk = text[start:end]
|
||
|
stat_tok = "stat(SOCKET_PATH"
|
||
|
mkdir_tok = "SCMkDir(SOCKET_PATH"
|
||
|
if stat_tok not in chunk or mkdir_tok not in chunk:
|
||
|
print("expected %s and %s inside UnixNew check_dir block" % (stat_tok, mkdir_tok), file=sys.stderr)
|
||
|
sys.exit(1)
|
||
|
if chunk.index(stat_tok) >= chunk.index(mkdir_tok):
|
||
|
print("expected stat(SOCKET_PATH before SCMkDir(SOCKET_PATH", file=sys.stderr)
|
||
|
sys.exit(1)
|
||
|
if __name__ == "__main__":
|
||
|
main()
|
||
| tests/unix-socket/unix-socket-tenant-path-01/README.md | ||
|---|---|---|
|
# unix-socket-tenant-path-01
|
||
|
Multi-detect must be initialized (`PostConfLoadedDetectSetup`), so this test ships a
|
||
|
minimal `test.rules` file (Suricata otherwise passes `--disable-detection` and skips
|
||
|
multi-tenant setup).
|
||
|
`register-tenant` loads `../tenant-outside.yaml` relative to the test directory i.e.
|
||
|
YAML stored **outside** the per-test output directory, documenting arbitrary path
|
||
|
acceptance for tenant configuration files via the Unix socket.
|
||
|
Related: Unix socket trust-boundary report (tenant YAML path).
|
||
|
Requires: `multi-detect.enabled`, `selector: direct`, loaders; `default-rule-path`
|
||
|
points at `tests/unix-socket/` so `tenant-outside.yaml` can resolve `marker.rule`.
|
||
|
## PCAP
|
||
|
None (`pcap: false`).
|
||
| tests/unix-socket/unix-socket-tenant-path-01/test.rules | ||
|---|---|---|
|
# Minimal rules so Suricata does not use --disable-detection (required for
|
||
|
# multi-detect / PostConfLoadedDetectSetup).
|
||
|
alert tcp any any -> any any (msg:"unix-socket-tenant-path stub"; sid:90004202; rev:1;)
|
||
| tests/unix-socket/unix-socket-tenant-path-01/test.yaml | ||
|---|---|---|
|
# Unix socket register-tenant accepts a filesystem path with ".." relative to the
|
||
|
# process working directory (test dir). Loads YAML outside the test directory,
|
||
|
# documenting missing path confinement for tenant YAML (HIGH-6). Requires multi-detect.
|
||
|
requires:
|
||
|
min-version: 7
|
||
|
pcap: false
|
||
|
startup-timeout: 15
|
||
|
args:
|
||
|
- --set multi-detect.enabled=yes
|
||
|
- --set multi-detect.selector=direct
|
||
|
- --set multi-detect.loaders=2
|
||
|
# Tenant YAML references marker.rule in tests/unix-socket/; rule path resolves from cwd unless overridden.
|
||
|
- --set default-rule-path=${TEST_DIR}/..
|
||
|
unix-commands:
|
||
|
# Tenant YAML lives one directory up (../tenant-outside.yaml); rules alongside it.
|
||
|
- "register-tenant 42 ../tenant-outside.yaml"
|
||
|
checks:
|
||
|
- filter:
|
||
|
filename: sc.json
|
||
|
count: 1
|
||
|
match:
|
||
|
return: OK
|
||
|
message: adding tenant succeeded
|
||
| tests/unix-socket/unix-socket-v2-partial-read-01/README.md | ||
|---|---|---|
|
# unix-socket-v2-partial-read-01
|
||
|
Regression coverage for **HIGH-3**: after the V2 handshake, the manager reads a command
|
||
|
line with `recv()`. If the first read returns bytes without a newline, it waits with
|
||
|
`select()`. On POSIX, `nfds` must be `max(fd)+1`; passing only `fd` never marks the
|
||
|
descriptor ready, so the rest of the command is never read and the peer sees a
|
||
|
closed connection.
|
||
|
`suricatasc` sends whole JSON lines in one write, so this path is not exercised by
|
||
|
normal CLI use. The bundled script sends `{"command":"version"` then completes the
|
||
|
JSON on a second write after a short delay.
|
||
|
Until the engine `select()` argument is corrected, this check fails (the peer sees a
|
||
|
reset connection or never gets a complete reply). Land together with the Suricata fix.
|
||
| tests/unix-socket/unix-socket-v2-partial-read-01/fragment_v2_command.py | ||
|---|---|---|
|
#!/usr/bin/env python3
|
||
|
# Exercise Unix command socket V2 path where the first recv() does not include the
|
||
|
# terminating newline (HIGH-3 / select nfds off-by-one).
|
||
|
import json
|
||
|
import os
|
||
|
import socket
|
||
|
import time
|
||
|
SOCKET_PATH = os.environ["SOCKET_PATH"]
|
||
|
OUTPUT_DIR = os.environ["OUTPUT_DIR"]
|
||
|
OUT = os.path.join(OUTPUT_DIR, "unix-fragment-result.json")
|
||
|
def read_line(sock):
|
||
|
buf = b""
|
||
|
while True:
|
||
|
chunk = sock.recv(1)
|
||
|
if not chunk:
|
||
|
return None
|
||
|
buf += chunk
|
||
|
if chunk == b"\n":
|
||
|
return buf[:-1]
|
||
|
def main():
|
||
|
ok = False
|
||
|
preview = ""
|
||
|
sock = None
|
||
|
try:
|
||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||
|
sock.settimeout(15)
|
||
|
sock.connect(SOCKET_PATH)
|
||
|
sock.sendall(b'{"version":"0.2"}\n')
|
||
|
handshake = read_line(sock)
|
||
|
if handshake is None:
|
||
|
preview = "eof_handshake"
|
||
|
else:
|
||
|
hj = json.loads(handshake.decode("utf-8"))
|
||
|
if hj.get("return") != "OK":
|
||
|
preview = handshake.decode("utf-8", errors="replace")[:500]
|
||
|
else:
|
||
|
sock.sendall(b'{"command":"version"')
|
||
|
time.sleep(0.05)
|
||
|
sock.sendall(b'}\n')
|
||
|
resp = read_line(sock)
|
||
|
if resp:
|
||
|
rj = json.loads(resp.decode("utf-8"))
|
||
|
ok = rj.get("return") == "OK"
|
||
|
preview = resp.decode("utf-8", errors="replace")[:500]
|
||
|
else:
|
||
|
preview = "eof_after_fragmented_command"
|
||
|
except Exception as e:
|
||
|
preview = "%s:%s" % (type(e).__name__, e)
|
||
|
finally:
|
||
|
if sock is not None:
|
||
|
try:
|
||
|
sock.close()
|
||
|
except OSError:
|
||
|
pass
|
||
|
with open(OUT, "w") as f:
|
||
|
json.dump(
|
||
|
{"fragmented_version_command_ok": ok, "response_preview": preview},
|
||
|
f,
|
||
|
)
|
||
|
if __name__ == "__main__":
|
||
|
main()
|
||
| tests/unix-socket/unix-socket-v2-partial-read-01/test.yaml | ||
|---|---|---|
|
# HIGH-3: V2 command handling uses select(client_fd, ...) instead of fd+1; a command
|
||
|
# split across reads never wakes select and the connection is closed as incomplete.
|
||
|
requires:
|
||
|
min-version: 7
|
||
|
pcap: false
|
||
|
startup-timeout: 5
|
||
|
unix-socket-scripts:
|
||
|
- fragment_v2_command.py
|
||
|
unix-commands:
|
||
|
- version
|
||
|
checks:
|
||
|
- filter:
|
||
|
filename: unix-fragment-result.json
|
||
|
count: 1
|
||
|
match:
|
||
|
fragmented_version_command_ok: true
|
||