From 0b9536f172dbb3613d2eadf50303a730ebdef8a0 Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Tue, 20 Oct 2026 10:58:54 +0200 Subject: [PATCH 01/10] Update zen-internals to v0.1.74 and load the right build per platform --- Makefile | 10 ++- aikido_zen/helpers/get_lib_path.py | 36 -------- aikido_zen/helpers/zen_internals.py | 68 +++++++++++---- aikido_zen/helpers/zen_internals_test.py | 83 ++++++++++++++++--- .../sql_injection/init_test.py | 35 +++++++- 5 files changed, 166 insertions(+), 66 deletions(-) delete mode 100644 aikido_zen/helpers/get_lib_path.py diff --git a/Makefile b/Makefile index 0dfd05eeb..195242220 100644 --- a/Makefile +++ b/Makefile @@ -46,18 +46,24 @@ benchmark: # Binaries cache : -BASE_URL = https://github.com/AikidoSec/zen-internals/releases/download/v0.1.60 +BASE_URL = https://github.com/AikidoSec/zen-internals/releases/download/v0.1.74 FILES = \ libzen_internals_aarch64-apple-darwin.dylib \ libzen_internals_aarch64-apple-darwin.dylib.sha256sum \ + libzen_internals_aarch64-pc-windows-msvc.dll \ + libzen_internals_aarch64-pc-windows-msvc.dll.sha256sum \ libzen_internals_aarch64-unknown-linux-gnu.so \ libzen_internals_aarch64-unknown-linux-gnu.so.sha256sum \ + libzen_internals_aarch64-unknown-linux-musl.so \ + libzen_internals_aarch64-unknown-linux-musl.so.sha256sum \ libzen_internals_x86_64-apple-darwin.dylib \ libzen_internals_x86_64-apple-darwin.dylib.sha256sum \ libzen_internals_x86_64-pc-windows-gnu.dll \ libzen_internals_x86_64-pc-windows-gnu.dll.sha256sum \ libzen_internals_x86_64-unknown-linux-gnu.so \ - libzen_internals_x86_64-unknown-linux-gnu.so.sha256sum + libzen_internals_x86_64-unknown-linux-gnu.so.sha256sum \ + libzen_internals_x86_64-unknown-linux-musl.so \ + libzen_internals_x86_64-unknown-linux-musl.so.sha256sum binaries: binaries_make_dir $(addprefix .cache/binaries/, $(FILES)) binaries_make_dir: diff --git a/aikido_zen/helpers/get_lib_path.py b/aikido_zen/helpers/get_lib_path.py deleted file mode 100644 index 72536409c..000000000 --- a/aikido_zen/helpers/get_lib_path.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Exports get_binary_path""" - -import platform -import os - - -def get_binary_path(): - """Returns an absolute path for Rust binary file""" - current_dir = os.path.dirname(os.path.abspath(__file__)) - lib_path = os.path.join(current_dir, "../libs", get_file_name()) - return lib_path - - -def get_file_name(): - """Gives you the file name for the binary based on platform info""" - os_name = platform.system().lower() - machine = platform.machine().lower() - file_name = "libzen_internals_" - - # On macOS, platform.machine() returns "arm64" for Apple Silicon - # On Linux, platform.machine() returns "aarch64" for ARM64 - if "arm64" in machine or "aarch64" in machine: - file_name += "aarch64-" - # On macOS, platform.machine() returns "x86_64" for Intel - # On Linux, platform.machine() returns "x86_64" for AMD64 - elif "x86_64" in machine or "amd64" in machine: - file_name += "x86_64-" # x86_64 or AMD64 - - if os_name == "windows": - file_name += "pc-windows-gnu.dll" # Windows - elif os_name == "darwin": - file_name += "apple-darwin.dylib" # macOS - elif os_name in ["linux", "linux2"]: - file_name += "unknown-linux-gnu.so" # Linux - - return file_name diff --git a/aikido_zen/helpers/zen_internals.py b/aikido_zen/helpers/zen_internals.py index a706a6c51..ec79f3792 100644 --- a/aikido_zen/helpers/zen_internals.py +++ b/aikido_zen/helpers/zen_internals.py @@ -7,29 +7,61 @@ import os import platform -from aikido_zen.helpers.get_lib_path import get_binary_path from aikido_zen.helpers.logging import logger +_TARGET = f"{platform.system().lower()}-{platform.machine().lower()}" + + +def _binary_path(): + os_name = platform.system().lower() + machine = platform.machine().lower() + is_arm = "arm64" in machine or "aarch64" in machine + file_name = "libzen_internals_" + + # On macOS, platform.machine() returns "arm64" for Apple Silicon + # On Linux, platform.machine() returns "aarch64" for ARM64 + if is_arm: + file_name += "aarch64-" + # On macOS, platform.machine() returns "x86_64" for Intel + # On Linux, platform.machine() returns "x86_64" for AMD64 + elif "x86_64" in machine or "amd64" in machine: + file_name += "x86_64-" + + if os_name == "windows": + file_name += "pc-windows-msvc.dll" if is_arm else "pc-windows-gnu.dll" + elif os_name == "darwin": + file_name += "apple-darwin.dylib" + elif os_name in ["linux", "linux2"]: + # platform.libc_ver() names glibc and is empty on musl (Alpine), which cannot + # load the glibc build. + libc = "gnu" if platform.libc_ver()[0] == "glibc" else "musl" + file_name += f"unknown-linux-{libc}.so" + + return os.path.join( + os.path.dirname(os.path.abspath(__file__)), "../libs", file_name + ) + + def _load(): - binary_path = get_binary_path() - target = f"{platform.system().lower()}-{platform.machine().lower()}" + """Returns the loaded library and None, or None and the reason it did not load.""" + binary_path = _binary_path() if not os.path.exists(binary_path): - logger.warning( - "Zen will NOT block SQL injection attacks. Cannot find the zen-internals library for %s. " - "Request support: https://github.com/AikidoSec/firewall-python/issues", - target, + return None, ( + f"Cannot find the zen-internals library for {_TARGET}. " + "Request support: https://github.com/AikidoSec/firewall-python/issues" ) - return None try: - return ctypes.CDLL(binary_path) + return ctypes.CDLL(binary_path), None except OSError as e: - logger.warning( - "Zen will NOT block SQL injection attacks. Failed to load the zen-internals library for %s: %s", - target, - e, - ) - return None + return None, f"Failed to load the zen-internals library for {_TARGET}: {e}" + + +def _warn_unavailable(consequence, function_name): + reason = _load_error or ( + f"The zen-internals library for {_TARGET} has no {function_name} function." + ) + logger.warning("%s. %s", consequence, reason) def _bind(internals_lib, name, argtypes, restype): @@ -46,7 +78,7 @@ def _bind(internals_lib, name, argtypes, restype): return function -_internals_lib = _load() +_internals_lib, _load_error = _load() _detect_sql_injection = _bind( _internals_lib, @@ -60,6 +92,10 @@ def _bind(internals_lib, name, argtypes, restype): ], ctypes.c_int, ) +if _detect_sql_injection is None: + _warn_unavailable( + "Zen will NOT block SQL injection attacks", "detect_sql_injection" + ) def detect_sql_injection(query: bytes, user_input: bytes, dialect: int) -> int: diff --git a/aikido_zen/helpers/zen_internals_test.py b/aikido_zen/helpers/zen_internals_test.py index 483e532b2..e24f5f0d6 100644 --- a/aikido_zen/helpers/zen_internals_test.py +++ b/aikido_zen/helpers/zen_internals_test.py @@ -1,4 +1,7 @@ import ctypes +import os + +import pytest from aikido_zen.helpers import zen_internals @@ -35,25 +38,26 @@ def test_detect_sql_injection_returns_error_when_function_is_not_loaded(monkeypa assert zen_internals.detect_sql_injection(query, b"1 or 1=1", MYSQL) == 2 -def test_load_returns_none_when_library_file_is_missing(monkeypatch, caplog): +def test_load_returns_none_when_library_file_is_missing(monkeypatch): monkeypatch.setattr( - "aikido_zen.helpers.zen_internals.get_binary_path", + "aikido_zen.helpers.zen_internals._binary_path", lambda: "/nonexistent/libzen_internals.so", ) - assert zen_internals._load() is None - assert "Zen will NOT block SQL injection attacks" in caplog.text - assert "Cannot find the zen-internals library" in caplog.text + internals_lib, error = zen_internals._load() + assert internals_lib is None + assert "Cannot find the zen-internals library" in error + assert "Request support" in error -def test_load_returns_none_when_library_fails_to_load(monkeypatch, caplog): +def test_load_returns_none_when_library_fails_to_load(monkeypatch): monkeypatch.setattr( - "aikido_zen.helpers.zen_internals.get_binary_path", lambda: __file__ + "aikido_zen.helpers.zen_internals._binary_path", lambda: __file__ ) - assert zen_internals._load() is None - assert "Zen will NOT block SQL injection attacks" in caplog.text - assert "Failed to load the zen-internals library" in caplog.text + internals_lib, error = zen_internals._load() + assert internals_lib is None + assert "Failed to load the zen-internals library" in error def test_bind_returns_none_when_library_is_not_loaded(): @@ -70,3 +74,62 @@ class LibraryWithoutFunctions: ) is None ) + + +def test_warning_gives_the_load_error_when_library_is_missing(monkeypatch, caplog): + monkeypatch.setattr( + "aikido_zen.helpers.zen_internals._load_error", + "Cannot find the zen-internals library for test-target.", + ) + + zen_internals._warn_unavailable( + "Zen will NOT block SQL injection attacks", "detect_sql_injection" + ) + + assert ( + "Zen will NOT block SQL injection attacks. Cannot find the zen-internals " + "library for test-target." in caplog.text + ) + + +def test_warning_names_the_missing_function_when_library_is_loaded(monkeypatch, caplog): + monkeypatch.setattr("aikido_zen.helpers.zen_internals._load_error", None) + + zen_internals._warn_unavailable( + "Zen will NOT block SQL injection attacks", "detect_sql_injection" + ) + + assert "Zen will NOT block SQL injection attacks." in caplog.text + assert "has no detect_sql_injection function" in caplog.text + + +@pytest.mark.parametrize( + "system, machine, libc, file_name", + [ + ("Darwin", "arm64", "", "libzen_internals_aarch64-apple-darwin.dylib"), + ("Darwin", "x86_64", "", "libzen_internals_x86_64-apple-darwin.dylib"), + ("Linux", "aarch64", "glibc", "libzen_internals_aarch64-unknown-linux-gnu.so"), + ("Linux", "x86_64", "glibc", "libzen_internals_x86_64-unknown-linux-gnu.so"), + ("Linux", "aarch64", "", "libzen_internals_aarch64-unknown-linux-musl.so"), + ("Linux", "x86_64", "", "libzen_internals_x86_64-unknown-linux-musl.so"), + ("Windows", "AMD64", "", "libzen_internals_x86_64-pc-windows-gnu.dll"), + ("Windows", "ARM64", "", "libzen_internals_aarch64-pc-windows-msvc.dll"), + ], +) +def test_binary_path_picks_the_build_for_the_platform( + monkeypatch, system, machine, libc, file_name +): + monkeypatch.setattr("platform.system", lambda: system) + monkeypatch.setattr("platform.machine", lambda: machine) + monkeypatch.setattr("platform.libc_ver", lambda: (libc, "")) + + path = zen_internals._binary_path() + + assert os.path.basename(path) == file_name + assert os.path.dirname(os.path.normpath(path)).endswith( + os.path.join("aikido_zen", "libs") + ) + + +def test_binary_path_exists_for_this_machine(): + assert os.path.exists(zen_internals._binary_path()) diff --git a/aikido_zen/vulnerabilities/sql_injection/init_test.py b/aikido_zen/vulnerabilities/sql_injection/init_test.py index d017bc442..e13ec4b3a 100644 --- a/aikido_zen/vulnerabilities/sql_injection/init_test.py +++ b/aikido_zen/vulnerabilities/sql_injection/init_test.py @@ -1,9 +1,11 @@ import os import pytest +from aikido_zen.helpers import zen_internals from aikido_zen.vulnerabilities.sql_injection import ( detect_sql_injection, should_return_early, ) +from .map_dialect_to_rust_int import map_dialect_to_rust_int """Removed tests : -> `I'm writting you` : Invalid SQL @@ -86,6 +88,17 @@ def is_not_sql_injection(sql, input, dialect="all"): ), f"Expected no SQL injection for SQL: {sql} and input: {input} in {current} dialect" +def is_tokenize_error(sql, input, dialect): + result = zen_internals.detect_sql_injection( + sql.lower().encode(), + input.lower().strip().encode(), + map_dialect_to_rust_int(dialect), + ) + assert ( + result == 3 + ), f"Expected tokenize error for SQL: {sql} and input: {input} in {dialect} dialect" + + def test_should_return_early(): # Test cases where the function should return True @@ -218,7 +231,7 @@ def test_check_string_safely_escaped(): ) # Positive example of same query : is_sql_injection( - "SELECT * FROM comments WHERE comment = 'I'm writting you--'", + "SELECT * FROM comments WHERE comment = 'I'm writting you-- '", "I'm writting you--", ) is_sql_injection( @@ -415,11 +428,29 @@ def test_function_calls_as_sql_injections(): def test_trimmed_user_input_bypass(): is_sql_injection( - "INSERT INTO pets (name, owner) VALUES ('x', 'dummy'), ('injected', 'hacker'); --', 'owner')", + "INSERT INTO pets (name, owner) VALUES ('x', 'dummy'), ('injected', 'hacker'); -- ', 'owner')", "x', 'dummy'), ('injected', 'hacker'); -- ", ) +def test_mysql_does_not_detect_dash_dash_without_space(): + for sql, input in [ + ( + "SELECT * FROM comments WHERE comment = 'I'm writting you--'", + "I'm writting you--", + ), + ( + "INSERT INTO pets (name, owner) VALUES ('x', 'dummy'), ('injected', 'hacker'); --', 'owner')", + "x', 'dummy'), ('injected', 'hacker'); -- ", + ), + ]: + is_tokenize_error(sql, input, "mysql") + is_not_sql_injection(sql, input, "mysql") + is_sql_injection(sql, input, "generic") + is_sql_injection(sql, input, "postgres") + is_sql_injection(sql, input, "sqlite") + + def test_returns_false_when_library_cannot_be_loaded(monkeypatch): monkeypatch.setattr("aikido_zen.helpers.zen_internals._detect_sql_injection", None) From d39dea147cea291bacce80e23a3d50a14ead109f Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Tue, 20 Oct 2026 12:03:13 +0200 Subject: [PATCH 02/10] Pick the musl build from the libc Python was built for --- aikido_zen/helpers/zen_internals.py | 9 +++-- aikido_zen/helpers/zen_internals_test.py | 51 +++++++++++++++++++----- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/aikido_zen/helpers/zen_internals.py b/aikido_zen/helpers/zen_internals.py index ec79f3792..f4fe2b275 100644 --- a/aikido_zen/helpers/zen_internals.py +++ b/aikido_zen/helpers/zen_internals.py @@ -6,6 +6,7 @@ import ctypes import os import platform +import sysconfig from aikido_zen.helpers.logging import logger @@ -33,9 +34,11 @@ def _binary_path(): elif os_name == "darwin": file_name += "apple-darwin.dylib" elif os_name in ["linux", "linux2"]: - # platform.libc_ver() names glibc and is empty on musl (Alpine), which cannot - # load the glibc build. - libc = "gnu" if platform.libc_ver()[0] == "glibc" else "musl" + # The library must use the same libc as this Python, which records it at build + # time (for example "x86_64-pc-linux-musl"). platform.libc_ver() and files under + # /lib describe the system, which can have both glibc and musl installed. + host = sysconfig.get_config_var("HOST_GNU_TYPE") or "" + libc = "musl" if "musl" in host else "gnu" file_name += f"unknown-linux-{libc}.so" return os.path.join( diff --git a/aikido_zen/helpers/zen_internals_test.py b/aikido_zen/helpers/zen_internals_test.py index e24f5f0d6..39177139d 100644 --- a/aikido_zen/helpers/zen_internals_test.py +++ b/aikido_zen/helpers/zen_internals_test.py @@ -104,24 +104,45 @@ def test_warning_names_the_missing_function_when_library_is_loaded(monkeypatch, @pytest.mark.parametrize( - "system, machine, libc, file_name", + "system, machine, host, file_name", [ ("Darwin", "arm64", "", "libzen_internals_aarch64-apple-darwin.dylib"), ("Darwin", "x86_64", "", "libzen_internals_x86_64-apple-darwin.dylib"), - ("Linux", "aarch64", "glibc", "libzen_internals_aarch64-unknown-linux-gnu.so"), - ("Linux", "x86_64", "glibc", "libzen_internals_x86_64-unknown-linux-gnu.so"), - ("Linux", "aarch64", "", "libzen_internals_aarch64-unknown-linux-musl.so"), - ("Linux", "x86_64", "", "libzen_internals_x86_64-unknown-linux-musl.so"), - ("Windows", "AMD64", "", "libzen_internals_x86_64-pc-windows-gnu.dll"), - ("Windows", "ARM64", "", "libzen_internals_aarch64-pc-windows-msvc.dll"), + ( + "Linux", + "aarch64", + "aarch64-unknown-linux-gnu", + "libzen_internals_aarch64-unknown-linux-gnu.so", + ), + ( + "Linux", + "x86_64", + "x86_64-pc-linux-gnu", + "libzen_internals_x86_64-unknown-linux-gnu.so", + ), + ( + "Linux", + "aarch64", + "aarch64-alpine-linux-musl", + "libzen_internals_aarch64-unknown-linux-musl.so", + ), + ( + "Linux", + "x86_64", + "x86_64-pc-linux-musl", + "libzen_internals_x86_64-unknown-linux-musl.so", + ), + ("Linux", "x86_64", None, "libzen_internals_x86_64-unknown-linux-gnu.so"), + ("Windows", "AMD64", None, "libzen_internals_x86_64-pc-windows-gnu.dll"), + ("Windows", "ARM64", None, "libzen_internals_aarch64-pc-windows-msvc.dll"), ], ) def test_binary_path_picks_the_build_for_the_platform( - monkeypatch, system, machine, libc, file_name + monkeypatch, system, machine, host, file_name ): monkeypatch.setattr("platform.system", lambda: system) monkeypatch.setattr("platform.machine", lambda: machine) - monkeypatch.setattr("platform.libc_ver", lambda: (libc, "")) + monkeypatch.setattr("sysconfig.get_config_var", lambda name: host) path = zen_internals._binary_path() @@ -131,5 +152,17 @@ def test_binary_path_picks_the_build_for_the_platform( ) +def test_binary_path_ignores_what_the_system_reports_about_libc(monkeypatch): + monkeypatch.setattr("platform.system", lambda: "Linux") + monkeypatch.setattr("platform.machine", lambda: "x86_64") + monkeypatch.setattr("sysconfig.get_config_var", lambda name: "x86_64-pc-linux-gnu") + monkeypatch.setattr("platform.libc_ver", lambda: ("", "")) + monkeypatch.setattr("glob.glob", lambda pattern: ["/lib/ld-musl-x86_64.so.1"]) + + assert os.path.basename(zen_internals._binary_path()) == ( + "libzen_internals_x86_64-unknown-linux-gnu.so" + ) + + def test_binary_path_exists_for_this_machine(): assert os.path.exists(zen_internals._binary_path()) From bd6f257eda726b14cd625192a4ee8b99ff73cac3 Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Tue, 20 Oct 2026 17:29:23 +0200 Subject: [PATCH 03/10] Retry the musl build when the glibc build does not load --- aikido_zen/helpers/zen_internals.py | 11 ++++++++++- aikido_zen/helpers/zen_internals_test.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/aikido_zen/helpers/zen_internals.py b/aikido_zen/helpers/zen_internals.py index f4fe2b275..2286eb094 100644 --- a/aikido_zen/helpers/zen_internals.py +++ b/aikido_zen/helpers/zen_internals.py @@ -57,7 +57,16 @@ def _load(): try: return ctypes.CDLL(binary_path), None except OSError as e: - return None, f"Failed to load the zen-internals library for {_TARGET}: {e}" + error = e + # A musl Python built without HOST_GNU_TYPE picks the glibc build, which musl + # cannot load. + musl_path = binary_path.replace("-gnu.so", "-musl.so") + if musl_path != binary_path and os.path.exists(musl_path): + try: + return ctypes.CDLL(musl_path), None + except OSError: + pass + return None, f"Failed to load the zen-internals library for {_TARGET}: {error}" def _warn_unavailable(consequence, function_name): diff --git a/aikido_zen/helpers/zen_internals_test.py b/aikido_zen/helpers/zen_internals_test.py index 39177139d..06489fb24 100644 --- a/aikido_zen/helpers/zen_internals_test.py +++ b/aikido_zen/helpers/zen_internals_test.py @@ -1,5 +1,6 @@ import ctypes import os +import shutil import pytest @@ -60,6 +61,23 @@ def test_load_returns_none_when_library_fails_to_load(monkeypatch): assert "Failed to load the zen-internals library" in error +def test_load_falls_back_to_the_musl_build_when_the_glibc_build_fails( + monkeypatch, tmp_path +): + gnu_path = tmp_path / "libzen_internals_x86_64-unknown-linux-gnu.so" + gnu_path.write_text("not a library") + shutil.copy( + zen_internals._binary_path(), tmp_path / gnu_path.name.replace("-gnu", "-musl") + ) + monkeypatch.setattr( + "aikido_zen.helpers.zen_internals._binary_path", lambda: str(gnu_path) + ) + + internals_lib, error = zen_internals._load() + assert internals_lib is not None + assert error is None + + def test_bind_returns_none_when_library_is_not_loaded(): assert zen_internals._bind(None, "detect_sql_injection", [], ctypes.c_int) is None From 7100ebfc04934f904e3e50e3968c765cfe41038a Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Tue, 20 Oct 2026 19:14:30 +0200 Subject: [PATCH 04/10] Log zen-internals load failures where they happen --- aikido_zen/helpers/zen_internals.py | 33 +++++------ aikido_zen/helpers/zen_internals_test.py | 74 ++++++++++++++---------- 2 files changed, 58 insertions(+), 49 deletions(-) diff --git a/aikido_zen/helpers/zen_internals.py b/aikido_zen/helpers/zen_internals.py index 2286eb094..a0f3c9c77 100644 --- a/aikido_zen/helpers/zen_internals.py +++ b/aikido_zen/helpers/zen_internals.py @@ -47,15 +47,16 @@ def _binary_path(): def _load(): - """Returns the loaded library and None, or None and the reason it did not load.""" binary_path = _binary_path() if not os.path.exists(binary_path): - return None, ( - f"Cannot find the zen-internals library for {_TARGET}. " - "Request support: https://github.com/AikidoSec/firewall-python/issues" + logger.warning( + "Cannot find the zen-internals library for %s. " + "Request support: https://github.com/AikidoSec/firewall-python/issues", + _TARGET, ) + return None try: - return ctypes.CDLL(binary_path), None + return ctypes.CDLL(binary_path) except OSError as e: error = e # A musl Python built without HOST_GNU_TYPE picks the glibc build, which musl @@ -63,17 +64,13 @@ def _load(): musl_path = binary_path.replace("-gnu.so", "-musl.so") if musl_path != binary_path and os.path.exists(musl_path): try: - return ctypes.CDLL(musl_path), None + return ctypes.CDLL(musl_path) except OSError: pass - return None, f"Failed to load the zen-internals library for {_TARGET}: {error}" - - -def _warn_unavailable(consequence, function_name): - reason = _load_error or ( - f"The zen-internals library for {_TARGET} has no {function_name} function." + logger.warning( + "Failed to load the zen-internals library for %s: %s", _TARGET, error ) - logger.warning("%s. %s", consequence, reason) + return None def _bind(internals_lib, name, argtypes, restype): @@ -83,14 +80,16 @@ def _bind(internals_lib, name, argtypes, restype): try: function = getattr(internals_lib, name) except AttributeError: - logger.debug("The zen-internals library has no function %s", name) + logger.warning( + "The zen-internals library for %s has no %s function.", _TARGET, name + ) return None function.argtypes = argtypes function.restype = restype return function -_internals_lib, _load_error = _load() +_internals_lib = _load() _detect_sql_injection = _bind( _internals_lib, @@ -105,9 +104,7 @@ def _bind(internals_lib, name, argtypes, restype): ctypes.c_int, ) if _detect_sql_injection is None: - _warn_unavailable( - "Zen will NOT block SQL injection attacks", "detect_sql_injection" - ) + logger.warning("Zen will NOT block SQL injection attacks.") def detect_sql_injection(query: bytes, user_input: bytes, dialect: int) -> int: diff --git a/aikido_zen/helpers/zen_internals_test.py b/aikido_zen/helpers/zen_internals_test.py index 06489fb24..8f9597968 100644 --- a/aikido_zen/helpers/zen_internals_test.py +++ b/aikido_zen/helpers/zen_internals_test.py @@ -1,6 +1,8 @@ import ctypes import os import shutil +import subprocess +import sys import pytest @@ -39,26 +41,24 @@ def test_detect_sql_injection_returns_error_when_function_is_not_loaded(monkeypa assert zen_internals.detect_sql_injection(query, b"1 or 1=1", MYSQL) == 2 -def test_load_returns_none_when_library_file_is_missing(monkeypatch): +def test_load_returns_none_when_library_file_is_missing(monkeypatch, caplog): monkeypatch.setattr( "aikido_zen.helpers.zen_internals._binary_path", lambda: "/nonexistent/libzen_internals.so", ) - internals_lib, error = zen_internals._load() - assert internals_lib is None - assert "Cannot find the zen-internals library" in error - assert "Request support" in error + assert zen_internals._load() is None + assert "Cannot find the zen-internals library" in caplog.text + assert "Request support" in caplog.text -def test_load_returns_none_when_library_fails_to_load(monkeypatch): +def test_load_returns_none_when_library_fails_to_load(monkeypatch, caplog): monkeypatch.setattr( "aikido_zen.helpers.zen_internals._binary_path", lambda: __file__ ) - internals_lib, error = zen_internals._load() - assert internals_lib is None - assert "Failed to load the zen-internals library" in error + assert zen_internals._load() is None + assert "Failed to load the zen-internals library" in caplog.text def test_load_falls_back_to_the_musl_build_when_the_glibc_build_fails( @@ -73,16 +73,14 @@ def test_load_falls_back_to_the_musl_build_when_the_glibc_build_fails( "aikido_zen.helpers.zen_internals._binary_path", lambda: str(gnu_path) ) - internals_lib, error = zen_internals._load() - assert internals_lib is not None - assert error is None + assert zen_internals._load() is not None def test_bind_returns_none_when_library_is_not_loaded(): assert zen_internals._bind(None, "detect_sql_injection", [], ctypes.c_int) is None -def test_bind_returns_none_when_function_is_missing(): +def test_bind_returns_none_when_function_is_missing(caplog): class LibraryWithoutFunctions: pass @@ -92,33 +90,47 @@ class LibraryWithoutFunctions: ) is None ) + assert "has no detect_sql_injection function" in caplog.text -def test_warning_gives_the_load_error_when_library_is_missing(monkeypatch, caplog): - monkeypatch.setattr( - "aikido_zen.helpers.zen_internals._load_error", - "Cannot find the zen-internals library for test-target.", - ) - - zen_internals._warn_unavailable( - "Zen will NOT block SQL injection attacks", "detect_sql_injection" +def _warnings_at_import(cdll_patch): + script = ( + "import ctypes\n" f"{cdll_patch}\n" "import aikido_zen.helpers.zen_internals\n" ) - - assert ( - "Zen will NOT block SQL injection attacks. Cannot find the zen-internals " - "library for test-target." in caplog.text + result = subprocess.run( + [sys.executable, "-c", script], capture_output=True, text=True, check=True ) + return result.stdout + result.stderr -def test_warning_names_the_missing_function_when_library_is_loaded(monkeypatch, caplog): - monkeypatch.setattr("aikido_zen.helpers.zen_internals._load_error", None) +def test_warns_once_per_disabled_feature_when_library_fails_to_load(): + output = _warnings_at_import( + "def fail(*args, **kwargs):\n" + " raise OSError('cannot open shared object file')\n" + "ctypes.CDLL = fail" + ) - zen_internals._warn_unavailable( - "Zen will NOT block SQL injection attacks", "detect_sql_injection" + assert "Failed to load the zen-internals library" in output + assert "cannot open shared object file" in output + assert "Zen will NOT block SQL injection attacks." in output + + +def test_warns_about_the_missing_function_when_library_is_loaded(): + output = _warnings_at_import( + "real_cdll = ctypes.CDLL\n" + "class WithoutSql:\n" + " def __init__(self, lib):\n" + " self._lib = lib\n" + " def __getattr__(self, name):\n" + " if name == 'detect_sql_injection':\n" + " raise AttributeError(name)\n" + " return getattr(self._lib, name)\n" + "ctypes.CDLL = lambda path, *args, **kwargs: WithoutSql(real_cdll(path))" ) - assert "Zen will NOT block SQL injection attacks." in caplog.text - assert "has no detect_sql_injection function" in caplog.text + assert "has no detect_sql_injection function" in output + assert "Zen will NOT block SQL injection attacks." in output + assert "Failed to load the zen-internals library" not in output @pytest.mark.parametrize( From 293dfb64bba92dd836656b0181cdf4288d994b2c Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Tue, 20 Oct 2026 19:23:32 +0200 Subject: [PATCH 05/10] Only rename the file when retrying the musl build --- aikido_zen/helpers/zen_internals.py | 5 ++++- aikido_zen/helpers/zen_internals_test.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/aikido_zen/helpers/zen_internals.py b/aikido_zen/helpers/zen_internals.py index a0f3c9c77..b2027b44d 100644 --- a/aikido_zen/helpers/zen_internals.py +++ b/aikido_zen/helpers/zen_internals.py @@ -61,7 +61,10 @@ def _load(): error = e # A musl Python built without HOST_GNU_TYPE picks the glibc build, which musl # cannot load. - musl_path = binary_path.replace("-gnu.so", "-musl.so") + musl_path = os.path.join( + os.path.dirname(binary_path), + os.path.basename(binary_path).replace("-gnu.so", "-musl.so"), + ) if musl_path != binary_path and os.path.exists(musl_path): try: return ctypes.CDLL(musl_path) diff --git a/aikido_zen/helpers/zen_internals_test.py b/aikido_zen/helpers/zen_internals_test.py index 8f9597968..cdc59ac5f 100644 --- a/aikido_zen/helpers/zen_internals_test.py +++ b/aikido_zen/helpers/zen_internals_test.py @@ -64,10 +64,12 @@ def test_load_returns_none_when_library_fails_to_load(monkeypatch, caplog): def test_load_falls_back_to_the_musl_build_when_the_glibc_build_fails( monkeypatch, tmp_path ): - gnu_path = tmp_path / "libzen_internals_x86_64-unknown-linux-gnu.so" + libs = tmp_path / "app-gnu.so" / "libs" + libs.mkdir(parents=True) + gnu_path = libs / "libzen_internals_x86_64-unknown-linux-gnu.so" gnu_path.write_text("not a library") shutil.copy( - zen_internals._binary_path(), tmp_path / gnu_path.name.replace("-gnu", "-musl") + zen_internals._binary_path(), libs / gnu_path.name.replace("-gnu", "-musl") ) monkeypatch.setattr( "aikido_zen.helpers.zen_internals._binary_path", lambda: str(gnu_path) From 391f3eb56580e98ed073b656b262413a03ea4424 Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Wed, 21 Oct 2026 11:43:16 +0200 Subject: [PATCH 06/10] Test the musl build on Alpine in CI --- .github/workflows/smoke-test-ffi.yml | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/smoke-test-ffi.yml b/.github/workflows/smoke-test-ffi.yml index a6705062b..2ba4e9bf9 100644 --- a/.github/workflows/smoke-test-ffi.yml +++ b/.github/workflows/smoke-test-ffi.yml @@ -52,3 +52,32 @@ jobs: - name: Run tests run: poetry run pytest aikido_zen/vulnerabilities/sql_injection/init_test.py + + smoke-test-musl: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, ubuntu-22.04-arm] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Download binaries + run: make binaries && cp -r .cache/binaries/* aikido_zen/libs + + # pip runs inside the container, so safe-chain is set up there and not on the runner. + # build-base compiles pytricia, which has no prebuilt wheel. + - name: Run tests on python:${{ matrix.python-version }}-alpine + run: | + docker run --rm -v "$(pwd):/app" -w /app python:${{ matrix.python-version }}-alpine sh -eo pipefail -c " + apk add --no-cache build-base curl + curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci + export PATH=\"\$HOME/.safe-chain/shims:\$HOME/.safe-chain/bin:\$PATH\" + pip install --root-user-action=ignore . pytest + python -m pytest aikido_zen/helpers/zen_internals_test.py + " From f79332fda55d7074dd0fb0bc10d9a31fe343c90f Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Thu, 22 Oct 2026 16:08:44 +0200 Subject: [PATCH 07/10] Download the zen-internals binaries again when the version changes --- Makefile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 195242220..c95184808 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,7 @@ FILES = \ libzen_internals_x86_64-unknown-linux-musl.so.sha256sum binaries: binaries_make_dir $(addprefix .cache/binaries/, $(FILES)) + echo "$(BASE_URL)" > $(CACHE_DIR)/.version binaries_make_dir: rm -rf .cache/binaries mkdir -p .cache/binaries/ @@ -73,12 +74,13 @@ binaries_make_dir: @echo "Downloading $*..." curl -L -o $@ $(BASE_URL)/$* +# Binaries from another release may not match the Python bindings, so download them again. .PHONY: check_binaries check_binaries: - @if [ -d "$(CACHE_DIR)" ]; then \ - echo "Directory $(CACHE_DIR) exists."; \ + @if [ "$$(cat $(CACHE_DIR)/.version 2>/dev/null)" = "$(BASE_URL)" ]; then \ + echo "Directory $(CACHE_DIR) has the binaries from $(BASE_URL)."; \ else \ - echo "Directory $(CACHE_DIR) is empty. Running 'make binaries'..."; \ + echo "Directory $(CACHE_DIR) is missing or outdated. Running 'make binaries'..."; \ $(MAKE) binaries; \ fi From 21855671ab0bc5693b990fdd73f7fa789303f7e9 Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Thu, 22 Oct 2026 16:15:41 +0200 Subject: [PATCH 08/10] Copy the zen-internals binaries into aikido_zen/libs on make install too --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index c95184808..19a0702e7 100644 --- a/Makefile +++ b/Makefile @@ -83,6 +83,7 @@ check_binaries: echo "Directory $(CACHE_DIR) is missing or outdated. Running 'make binaries'..."; \ $(MAKE) binaries; \ fi + cp -r $(CACHE_DIR)/* aikido_zen/libs # Replace version number automatically on publish : From 067873226fcf54b8c16106ab434d94c35b2d82fd Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Fri, 23 Oct 2026 16:30:15 +0200 Subject: [PATCH 09/10] Remove the duplicate copy of the zen-internals binaries --- .github/workflows/smoke-test-ffi.yml | 2 +- Makefile | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/smoke-test-ffi.yml b/.github/workflows/smoke-test-ffi.yml index 2ba4e9bf9..a430af0f5 100644 --- a/.github/workflows/smoke-test-ffi.yml +++ b/.github/workflows/smoke-test-ffi.yml @@ -48,7 +48,7 @@ jobs: run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1' -UseBasicParsing) } -ci" - name: Install dependencies - run: make install && cp -r .cache/binaries/* aikido_zen/libs + run: make install - name: Run tests run: poetry run pytest aikido_zen/vulnerabilities/sql_injection/init_test.py diff --git a/Makefile b/Makefile index 19a0702e7..eed0f48be 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,6 @@ BUILD_DIR := dist/ .PHONY: build build: check_binaries @echo "Building using poetry, creates $(BUILD_DIR) folder." - @echo "Copying binaries from $(CACHE_DIR) folder : " - cp -r .cache/binaries/* aikido_zen/libs poetry build .PHONY: clean clean: From 4b71dd6decd014ae17b96d015d4f1e86b1a86cf8 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Fri, 23 Oct 2026 18:13:17 +0200 Subject: [PATCH 10/10] Keep Alpine smoke tests on Ubuntu 24.04 --- .github/workflows/smoke-test-ffi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smoke-test-ffi.yml b/.github/workflows/smoke-test-ffi.yml index a430af0f5..7d2dff2ee 100644 --- a/.github/workflows/smoke-test-ffi.yml +++ b/.github/workflows/smoke-test-ffi.yml @@ -58,7 +58,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, ubuntu-22.04-arm] + os: [ubuntu-24.04, ubuntu-22.04-arm] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: