From d897644f178f204e1bc258342978d57263d4fb42 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 13 Nov 2025 15:53:42 +0100 Subject: [PATCH 01/26] Create a new clean Command & CommandContext interface --- .../background_process/commands/command.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 aikido_zen/background_process/commands/command.py diff --git a/aikido_zen/background_process/commands/command.py b/aikido_zen/background_process/commands/command.py new file mode 100644 index 00000000..d8a1e11d --- /dev/null +++ b/aikido_zen/background_process/commands/command.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractmethod + + +class CommandContext: + def __init__(self, connection_manager, queue, connection): + self.connection_manager = connection_manager + self.queue = queue + self.connection = connection + + def send(self, response): + self.connection.send(response) + + +class Command(ABC): + @classmethod + @abstractmethod + def identifier(cls) -> str: + pass + + @classmethod + @abstractmethod + def run(cls, context: CommandContext, request): + pass From 1e005c0c66cc550f37204dfc8b4daf16d1149f70 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 13 Nov 2025 15:53:56 +0100 Subject: [PATCH 02/26] Remove ATTACK event --- .../background_process/commands/attack.py | 9 --- .../commands/attack_test.py | 69 ------------------- 2 files changed, 78 deletions(-) delete mode 100644 aikido_zen/background_process/commands/attack.py delete mode 100644 aikido_zen/background_process/commands/attack_test.py diff --git a/aikido_zen/background_process/commands/attack.py b/aikido_zen/background_process/commands/attack.py deleted file mode 100644 index 92308e98..00000000 --- a/aikido_zen/background_process/commands/attack.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Main export is process_attack""" - - -def process_attack(connection_manager, data, queue): - """ - Adds ATTACK data object to queue - Expected data object : [injection_results, context, blocked_or_not, stacktrace] - """ - queue.put(data) diff --git a/aikido_zen/background_process/commands/attack_test.py b/aikido_zen/background_process/commands/attack_test.py deleted file mode 100644 index b5cb8e81..00000000 --- a/aikido_zen/background_process/commands/attack_test.py +++ /dev/null @@ -1,69 +0,0 @@ -import pytest -from queue import Queue -from unittest.mock import MagicMock -from .attack import process_attack - - -class MockCloudConnectionManager: - def __init__(self): - self.statistics = MagicMock() - - -def test_process_attack_adds_data_to_queue(): - queue = Queue() - connection_manager = MockCloudConnectionManager() - data = ("injection_results", "context", True, "stacktrace") # Example data - process_attack(connection_manager, data, queue) - - # Check if the data is added to the queue - assert not queue.empty() - assert queue.get() == data - - -def test_process_attack_statistics_not_called_when_disabled(): - queue = Queue() - connection_manager = MockCloudConnectionManager() - connection_manager.statistics = None # Disable statistics - data = ("injection_results", "context", True, "stacktrace") # Example data - process_attack(connection_manager, data, queue) - - # Check if on_detected_attack was not called - assert ( - connection_manager.statistics is None - or not connection_manager.statistics.on_detected_attack.called - ) - - -def test_process_attack_multiple_calls(): - queue = Queue() - connection_manager = MockCloudConnectionManager() - data1 = ("injection_results_1", "context_1", True, "stacktrace_1") - data2 = ("injection_results_2", "context_2", False, "stacktrace_2") - - process_attack(connection_manager, data1, queue) - process_attack(connection_manager, data2, queue) - - # Check if both data items are added to the queue - assert queue.qsize() == 2 - assert queue.get() == data1 - assert queue.get() == data2 - - -def test_process_attack_with_different_data_formats(): - queue = Queue() - connection_manager = MockCloudConnectionManager() - - # Test with different types of data - data1 = ("injection_results", "context", True, "stacktrace") - data2 = ("injection_results", "context", False, "stacktrace") - data3 = ("injection_results", "context", None, "stacktrace") - - process_attack(connection_manager, data1, queue) - process_attack(connection_manager, data2, queue) - process_attack(connection_manager, data3, queue) - - # Check if all data items are added to the queue - assert queue.qsize() == 3 - assert queue.get() == data1 - assert queue.get() == data2 - assert queue.get() == data3 From dc5777f99d4baa4f6480bd3c79d97795202b522f Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 13 Nov 2025 15:55:40 +0100 Subject: [PATCH 03/26] Create a PutEventCommand - with the new modern command standard --- .../background_process/commands/put_event.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 aikido_zen/background_process/commands/put_event.py diff --git a/aikido_zen/background_process/commands/put_event.py b/aikido_zen/background_process/commands/put_event.py new file mode 100644 index 00000000..864d66c1 --- /dev/null +++ b/aikido_zen/background_process/commands/put_event.py @@ -0,0 +1,17 @@ +from .command import Command, CommandContext + + +class PutEventReq: + def __init__(self, event): + # Event is a JSON object ready to be sent to core. + self.event = event + + +class PutEventCommand(Command): + @classmethod + def identifier(cls) -> str: + return "put_event" + + @classmethod + def run(cls, context: CommandContext, request: PutEventReq): + context.queue.put(request.event) From c5f4251779815e0cec7fd01dd5ee17e0db0985f4 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 13 Nov 2025 15:55:51 +0100 Subject: [PATCH 04/26] Cleanup the process_incoming_command --- .../background_process/commands/__init__.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/aikido_zen/background_process/commands/__init__.py b/aikido_zen/background_process/commands/__init__.py index 151d2562..891644d7 100644 --- a/aikido_zen/background_process/commands/__init__.py +++ b/aikido_zen/background_process/commands/__init__.py @@ -1,7 +1,6 @@ -"""Commands __init__.py file""" - from aikido_zen.helpers.logging import logger -from .attack import process_attack +from .command import CommandContext +from .put_event import PutEventCommand from .check_firewall_lists import process_check_firewall_lists from .read_property import process_read_property from .should_ratelimit import process_should_ratelimit @@ -9,26 +8,27 @@ from .sync_data import process_sync_data commands_map = { - # This maps to a tuple : (function, returns_data?) - # Commands that don't return data : - "ATTACK": (process_attack, False), - # Commands that return data : - "SYNC_DATA": (process_sync_data, True), - "READ_PROPERTY": (process_read_property, True), - "SHOULD_RATELIMIT": (process_should_ratelimit, True), - "PING": (process_ping, True), - "CHECK_FIREWALL_LISTS": (process_check_firewall_lists, True), + "SYNC_DATA": process_sync_data, + "READ_PROPERTY": process_read_property, + "SHOULD_RATELIMIT": process_should_ratelimit, + "PING": process_ping, + "CHECK_FIREWALL_LISTS": process_check_firewall_lists, } +modern_commands = [PutEventCommand] + def process_incoming_command(connection_manager, obj, conn, queue): - """Processes an incoming command""" - action = obj[0] - data = obj[1] - if action in commands_map: - func, returns_data = commands_map[action] - if returns_data: - return conn.send(func(connection_manager, data, queue)) - func(connection_manager, data, queue) - else: - logger.debug("Command : `%s` not found, aborting", action) + inbound_identifier = obj[0] + inbound_request = obj[1] + if inbound_identifier in commands_map: + func = commands_map[inbound_identifier] + return conn.send(func(connection_manager, inbound_identifier, queue)) + + for cmd in modern_commands: + if cmd.identifier() == inbound_identifier: + cmd.run(CommandContext(connection_manager, queue, conn), inbound_request) + return None + + logger.debug("Command : `%s` not found - did not execute", inbound_identifier) + return None From 0b35c50e9ad784083fc8efb8329199d3cc7dce9d Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 18:38:55 +0100 Subject: [PATCH 05/26] Create a report_event on the cloud_connection_manager --- .../cloud_connection_manager/__init__.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/aikido_zen/background_process/cloud_connection_manager/__init__.py b/aikido_zen/background_process/cloud_connection_manager/__init__.py index 579fa0ef..828c11d8 100644 --- a/aikido_zen/background_process/cloud_connection_manager/__init__.py +++ b/aikido_zen/background_process/cloud_connection_manager/__init__.py @@ -105,3 +105,26 @@ def update_service_config(self, res): def update_firewall_lists(self): """Will update service config with blocklist of IP addresses""" return update_firewall_lists(self) + + def report_event(self, event): + if not self.token: + return {"success": False, "error": "invalid_token"} + try: + payload = { + "time": get_unixtime_ms(), + "agent": get_manager_info(self), + } + payload.update(event) # Merge default fields with event fields + + result = self.api.report(self.token, payload, self.timeout_in_sec) + if not result.get("success", True): + logger.error( + "CloudConnectionManager: Reporting to api failed, error=%s", + result.get("error", "unknown"), + ) + return result + except Exception as e: + logger.debug(e) + logger.error( + "CloudConnectionManager: Reporting to api failed, unexpected error (see debug logs)" + ) From 14b34edebe11f6bede9edd855c5986e7094bcaee Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 18:39:14 +0100 Subject: [PATCH 06/26] use this report_event --- .../cloud_connection_manager/on_start.py | 24 ++++++------------- .../send_heartbeat.py | 9 ++----- .../update_service_config.py | 2 +- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/aikido_zen/background_process/cloud_connection_manager/on_start.py b/aikido_zen/background_process/cloud_connection_manager/on_start.py index 8414821c..72cbb8a7 100644 --- a/aikido_zen/background_process/cloud_connection_manager/on_start.py +++ b/aikido_zen/background_process/cloud_connection_manager/on_start.py @@ -5,26 +5,16 @@ def on_start(connection_manager): - """ - This will send out an Event signalling the start to the server - """ - if not connection_manager.token: - return - res = connection_manager.api.report( - connection_manager.token, - { - "type": "started", - "time": get_unixtime_ms(), - "agent": connection_manager.get_manager_info(), - }, - connection_manager.timeout_in_sec, - ) + event = {"type": "started"} + res = connection_manager.report_event(event) + if not res.get("success", True): - # Update config time even in failure : - connection_manager.conf.last_updated_at = get_unixtime_ms() - logger.error("Failed to communicate with Aikido Server : %s", res["error"]) + connection_manager.conf.last_updated_at = ( + get_unixtime_ms() + ) # Update config time even in failure else: connection_manager.update_service_config(res) connection_manager.update_firewall_lists() logger.info("Established connection with Aikido Server") + return res diff --git a/aikido_zen/background_process/cloud_connection_manager/send_heartbeat.py b/aikido_zen/background_process/cloud_connection_manager/send_heartbeat.py index 9a1ff988..06f290c0 100644 --- a/aikido_zen/background_process/cloud_connection_manager/send_heartbeat.py +++ b/aikido_zen/background_process/cloud_connection_manager/send_heartbeat.py @@ -2,7 +2,6 @@ from aikido_zen.background_process.packages import PackagesStore from aikido_zen.helpers.logging import logger -from aikido_zen.helpers.get_current_unixtime_ms import get_unixtime_ms def send_heartbeat(connection_manager): @@ -26,12 +25,9 @@ def send_heartbeat(connection_manager): connection_manager.ai_stats.clear() PackagesStore.clear() - res = connection_manager.api.report( - connection_manager.token, + res = connection_manager.report_event( { "type": "heartbeat", - "time": get_unixtime_ms(), - "agent": connection_manager.get_manager_info(), "stats": stats, "ai": ai_stats, "hostnames": outgoing_domains, @@ -39,7 +35,6 @@ def send_heartbeat(connection_manager): "routes": routes, "users": users, "middlewareInstalled": connection_manager.middleware_installed, - }, - connection_manager.timeout_in_sec, + } ) connection_manager.update_service_config(res) diff --git a/aikido_zen/background_process/cloud_connection_manager/update_service_config.py b/aikido_zen/background_process/cloud_connection_manager/update_service_config.py index 7d7f729e..42ce97ca 100644 --- a/aikido_zen/background_process/cloud_connection_manager/update_service_config.py +++ b/aikido_zen/background_process/cloud_connection_manager/update_service_config.py @@ -9,8 +9,8 @@ def update_service_config(connection_manager, res): Update configuration based on the server's response """ if res.get("success", False) is False: - logger.debug(res) return + if "block" in res.keys() and res["block"] != connection_manager.block: logger.debug("Updating blocking, setting blocking to : %s", res["block"]) connection_manager.block = bool(res["block"]) From cf3bf1c8b99652eb58db2237a1e4e5e2c3525154 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 18:39:35 +0100 Subject: [PATCH 07/26] cleanup useless functions on cloud_connection_manager --- .../cloud_connection_manager/__init__.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/aikido_zen/background_process/cloud_connection_manager/__init__.py b/aikido_zen/background_process/cloud_connection_manager/__init__.py index 828c11d8..575e4a19 100644 --- a/aikido_zen/background_process/cloud_connection_manager/__init__.py +++ b/aikido_zen/background_process/cloud_connection_manager/__init__.py @@ -10,6 +10,7 @@ from aikido_zen.storage.users import Users from aikido_zen.storage.hostnames import Hostnames from ..realtime.start_polling_for_changes import start_polling_for_changes +from ...helpers.get_current_unixtime_ms import get_unixtime_ms from ...storage.ai_statistics import AIStatistics from ...storage.firewall_lists import FirewallLists from ...storage.statistics import Statistics @@ -58,7 +59,7 @@ def __init__(self, block, api, token, serverless): def start(self, event_scheduler): """Send out start event and add heartbeats""" - res = self.on_start() + res = on_start(self) if res.get("error", None) == "invalid_token": logger.info( "Token was invalid, not starting heartbeats and realtime polling." @@ -86,18 +87,10 @@ def on_detected_attack(self, attack, context, blocked, stack): """This will send something to the API when an attack is detected""" return on_detected_attack(self, attack, context, blocked, stack) - def on_start(self): - """This will send out an Event signalling the start to the server""" - return on_start(self) - def send_heartbeat(self): """This will send a heartbeat to the server""" return send_heartbeat(self) - def get_manager_info(self): - """This returns info about the connection_manager""" - return get_manager_info(self) - def update_service_config(self, res): """Update configuration based on the server's response""" return update_service_config(self, res) From a81cfa5e9cfd2274f751762206c4e199976b661a Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 18:41:01 +0100 Subject: [PATCH 08/26] rename to report_api_event --- .../background_process/cloud_connection_manager/__init__.py | 2 +- .../background_process/cloud_connection_manager/on_start.py | 2 +- .../cloud_connection_manager/send_heartbeat.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aikido_zen/background_process/cloud_connection_manager/__init__.py b/aikido_zen/background_process/cloud_connection_manager/__init__.py index 575e4a19..933df11a 100644 --- a/aikido_zen/background_process/cloud_connection_manager/__init__.py +++ b/aikido_zen/background_process/cloud_connection_manager/__init__.py @@ -99,7 +99,7 @@ def update_firewall_lists(self): """Will update service config with blocklist of IP addresses""" return update_firewall_lists(self) - def report_event(self, event): + def report_api_event(self, event): if not self.token: return {"success": False, "error": "invalid_token"} try: diff --git a/aikido_zen/background_process/cloud_connection_manager/on_start.py b/aikido_zen/background_process/cloud_connection_manager/on_start.py index 72cbb8a7..6407ee7c 100644 --- a/aikido_zen/background_process/cloud_connection_manager/on_start.py +++ b/aikido_zen/background_process/cloud_connection_manager/on_start.py @@ -6,7 +6,7 @@ def on_start(connection_manager): event = {"type": "started"} - res = connection_manager.report_event(event) + res = connection_manager.report_api_event(event) if not res.get("success", True): connection_manager.conf.last_updated_at = ( diff --git a/aikido_zen/background_process/cloud_connection_manager/send_heartbeat.py b/aikido_zen/background_process/cloud_connection_manager/send_heartbeat.py index 06f290c0..18fa732f 100644 --- a/aikido_zen/background_process/cloud_connection_manager/send_heartbeat.py +++ b/aikido_zen/background_process/cloud_connection_manager/send_heartbeat.py @@ -25,7 +25,7 @@ def send_heartbeat(connection_manager): connection_manager.ai_stats.clear() PackagesStore.clear() - res = connection_manager.report_event( + res = connection_manager.report_api_event( { "type": "heartbeat", "stats": stats, From d179445fa24daccd8734e487c6871d9600dc6eab Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 18:42:16 +0100 Subject: [PATCH 09/26] update the queue empty-ing --- .../background_process/aikido_background_process.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/aikido_zen/background_process/aikido_background_process.py b/aikido_zen/background_process/aikido_background_process.py index 881e03b9..aa37cd02 100644 --- a/aikido_zen/background_process/aikido_background_process.py +++ b/aikido_zen/background_process/aikido_background_process.py @@ -101,13 +101,7 @@ def send_to_connection_manager(self, event_scheduler): EMPTY_QUEUE_INTERVAL, 1, self.send_to_connection_manager, (event_scheduler,) ) while not self.queue.empty(): - queue_attack_item = self.queue.get() - self.connection_manager.on_detected_attack( - attack=queue_attack_item[0], - context=queue_attack_item[1], - blocked=queue_attack_item[2], - stack=queue_attack_item[3], - ) + self.connection_manager.report_api_event(self.queue.get()) def add_exit_handlers(): From 964ad49e0ffce3c8fbd55c15287598fa52555686 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 18:42:22 +0100 Subject: [PATCH 10/26] Update comment in put_event --- aikido_zen/background_process/commands/put_event.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aikido_zen/background_process/commands/put_event.py b/aikido_zen/background_process/commands/put_event.py index 864d66c1..36523a77 100644 --- a/aikido_zen/background_process/commands/put_event.py +++ b/aikido_zen/background_process/commands/put_event.py @@ -3,7 +3,8 @@ class PutEventReq: def __init__(self, event): - # Event is a JSON object ready to be sent to core. + # Event is a dictionary containing data that is going to be reported to core + # "time" and "agent" fields are added by default from the CloudConnectionManager self.event = event From 48d6df96071c04f60f7c1df1bbc09b99932d21e3 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 18:49:37 +0100 Subject: [PATCH 11/26] remove on_detected_attack in favour of create_detected_attack_api_event --- .../cloud_connection_manager/__init__.py | 5 -- .../on_detected_attack.py | 54 ------------------- .../create_detected_attack_api_event.py | 31 +++++++++++ 3 files changed, 31 insertions(+), 59 deletions(-) delete mode 100644 aikido_zen/background_process/cloud_connection_manager/on_detected_attack.py create mode 100644 aikido_zen/helpers/create_detected_attack_api_event.py diff --git a/aikido_zen/background_process/cloud_connection_manager/__init__.py b/aikido_zen/background_process/cloud_connection_manager/__init__.py index 933df11a..ba78c928 100644 --- a/aikido_zen/background_process/cloud_connection_manager/__init__.py +++ b/aikido_zen/background_process/cloud_connection_manager/__init__.py @@ -16,7 +16,6 @@ from ...storage.statistics import Statistics # Import functions : -from .on_detected_attack import on_detected_attack from .get_manager_info import get_manager_info from .update_service_config import update_service_config from .on_start import on_start @@ -83,10 +82,6 @@ def report_initial_stats(self): if should_report_initial_stats: self.send_heartbeat() - def on_detected_attack(self, attack, context, blocked, stack): - """This will send something to the API when an attack is detected""" - return on_detected_attack(self, attack, context, blocked, stack) - def send_heartbeat(self): """This will send a heartbeat to the server""" return send_heartbeat(self) diff --git a/aikido_zen/background_process/cloud_connection_manager/on_detected_attack.py b/aikido_zen/background_process/cloud_connection_manager/on_detected_attack.py deleted file mode 100644 index 3d04f2bf..00000000 --- a/aikido_zen/background_process/cloud_connection_manager/on_detected_attack.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Mainly exports on_detected_attack""" - -import json - -from aikido_zen.helpers.get_current_unixtime_ms import get_unixtime_ms -from aikido_zen.helpers.logging import logger -from aikido_zen.helpers.limit_length_metadata import limit_length_metadata -from aikido_zen.helpers.serialize_to_json import serialize_to_json - - -def on_detected_attack(connection_manager, attack, context, blocked, stack): - """ - This will send something to the API when an attack is detected - """ - if not connection_manager.token: - return - - try: - attack["user"] = getattr(context, "user", None) - attack["payload"] = json.dumps(attack["payload"])[:4096] - attack["metadata"] = limit_length_metadata(attack["metadata"], 4096) - attack["blocked"] = blocked - attack["stack"] = stack - - payload = { - "type": "detected_attack", - "time": get_unixtime_ms(), - "agent": connection_manager.get_manager_info(), - "attack": attack, - "request": extract_request_if_possible(context), - } - logger.debug(serialize_to_json(payload)) - result = connection_manager.api.report( - connection_manager.token, - payload, - connection_manager.timeout_in_sec, - ) - logger.debug("Result : %s", result) - except Exception as e: - logger.debug(e) - logger.info("Failed to report an attack") - - -def extract_request_if_possible(context): - if not context: - return None - return { - "method": getattr(context, "method", None), - "url": getattr(context, "url", None), - "ipAddress": getattr(context, "remote_address", None), - "source": getattr(context, "source", None), - "route": getattr(context, "route", None), - "userAgent": context.get_user_agent(), - } diff --git a/aikido_zen/helpers/create_detected_attack_api_event.py b/aikido_zen/helpers/create_detected_attack_api_event.py new file mode 100644 index 00000000..69153a40 --- /dev/null +++ b/aikido_zen/helpers/create_detected_attack_api_event.py @@ -0,0 +1,31 @@ +import json + +from aikido_zen.helpers.limit_length_metadata import limit_length_metadata + + +def create_detected_attack_api_event(attack, context, blocked, stack): + return { + "type": "detected_attack", + "attack": { + **attack, + "user": getattr(context, "user", None), + "payload": json.dumps(attack["payload"])[:4096], + "metadata": limit_length_metadata(attack["metadata"], 4096), + "blocked": blocked, + "stack": stack, + }, + "request": extract_request_if_possible(context), + } + + +def extract_request_if_possible(context): + if not context: + return None + return { + "method": getattr(context, "method", None), + "url": getattr(context, "url", None), + "ipAddress": getattr(context, "remote_address", None), + "source": getattr(context, "source", None), + "route": getattr(context, "route", None), + "userAgent": context.get_user_agent(), + } From fba6c2c29c0853750814eef7f720253c013fbb27 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 18:51:20 +0100 Subject: [PATCH 12/26] use the new create_detected_attack_api_event --- aikido_zen/vulnerabilities/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aikido_zen/vulnerabilities/__init__.py b/aikido_zen/vulnerabilities/__init__.py index bf37a49f..650d2cfa 100644 --- a/aikido_zen/vulnerabilities/__init__.py +++ b/aikido_zen/vulnerabilities/__init__.py @@ -29,6 +29,7 @@ from .path_traversal.check_context_for_path_traversal import ( check_context_for_path_traversal, ) +from ..helpers.create_detected_attack_api_event import create_detected_attack_api_event def run_vulnerability_scan(kind, op, args): @@ -106,6 +107,9 @@ def run_vulnerability_scan(kind, op, args): stack = get_clean_stacktrace() if comms: + event = create_detected_attack_api_event( + injection_results, context, blocked, stack + ) comms.send_data_to_bg_process( "ATTACK", (injection_results, context, blocked, stack) ) From 7d4c053aaa9a862bda7d0b2ccb454ea1708819f6 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 19:04:50 +0100 Subject: [PATCH 13/26] move test cases for create_detected_attack_api_event --- .../create_detected_attack_api_event.py | 29 +++-- .../create_detected_attack_api_event_test.py} | 122 ++++++------------ 2 files changed, 54 insertions(+), 97 deletions(-) rename aikido_zen/{background_process/cloud_connection_manager/on_detected_attack_test.py => helpers/create_detected_attack_api_event_test.py} (51%) diff --git a/aikido_zen/helpers/create_detected_attack_api_event.py b/aikido_zen/helpers/create_detected_attack_api_event.py index 69153a40..c236092f 100644 --- a/aikido_zen/helpers/create_detected_attack_api_event.py +++ b/aikido_zen/helpers/create_detected_attack_api_event.py @@ -1,21 +1,26 @@ import json from aikido_zen.helpers.limit_length_metadata import limit_length_metadata +from aikido_zen.helpers.logging import logger def create_detected_attack_api_event(attack, context, blocked, stack): - return { - "type": "detected_attack", - "attack": { - **attack, - "user": getattr(context, "user", None), - "payload": json.dumps(attack["payload"])[:4096], - "metadata": limit_length_metadata(attack["metadata"], 4096), - "blocked": blocked, - "stack": stack, - }, - "request": extract_request_if_possible(context), - } + try: + return { + "type": "detected_attack", + "attack": { + **attack, + "user": getattr(context, "user", None), + "payload": json.dumps(attack["payload"])[:4096], + "metadata": limit_length_metadata(attack["metadata"], 4096), + "blocked": blocked, + "stack": stack, + }, + "request": extract_request_if_possible(context), + } + except Exception as e: + logger.error("Failed to create detected_attack API event: %s", str(e)) + return None def extract_request_if_possible(context): diff --git a/aikido_zen/background_process/cloud_connection_manager/on_detected_attack_test.py b/aikido_zen/helpers/create_detected_attack_api_event_test.py similarity index 51% rename from aikido_zen/background_process/cloud_connection_manager/on_detected_attack_test.py rename to aikido_zen/helpers/create_detected_attack_api_event_test.py index ae90f0ea..9918dabd 100644 --- a/aikido_zen/background_process/cloud_connection_manager/on_detected_attack_test.py +++ b/aikido_zen/helpers/create_detected_attack_api_event_test.py @@ -1,111 +1,77 @@ import pytest from unittest.mock import MagicMock -from .on_detected_attack import on_detected_attack -from ...context import Context +from .create_detected_attack_api_event import create_detected_attack_api_event +from aikido_zen.context import Context import aikido_zen.test_utils as test_utils -@pytest.fixture -def mock_connection_manager(): - connection_manager = MagicMock() - connection_manager.token = "test_token" - connection_manager.block = True - connection_manager.timeout_in_sec = 5 - connection_manager.api.report = MagicMock(return_value={"status": "success"}) - connection_manager.get_manager_info = lambda: {} - return connection_manager - - -def test_on_detected_attack_no_token(): - connection_manager = MagicMock() - connection_manager.token = None - - on_detected_attack( - connection_manager, - attack={}, - context=test_utils.generate_context(), - blocked=False, - stack=None, - ) - - connection_manager.api.report.assert_not_called() - - -def test_on_detected_attack_with_long_payload(mock_connection_manager): +def test_create_attack_event_with_long_payload(): long_payload = "x" * 5000 # Create a payload longer than 4096 characters attack = { "payload": long_payload, "metadata": {"test": "1"}, } - on_detected_attack( - mock_connection_manager, + event = create_detected_attack_api_event( attack=attack, context=test_utils.generate_context(), blocked=False, stack=None, ) - assert len(attack["payload"]) == 4096 # Ensure payload is truncated - mock_connection_manager.api.report.assert_called_once() + assert len(event["attack"]["payload"]) == 4096 # Ensure payload is truncated -def test_on_detected_attack_with_long_metadata(mock_connection_manager): +def test_create_attack_event_with_long_metadata(): long_metadata = "x" * 5000 # Create metadata longer than 4096 characters attack = { "payload": {}, "metadata": {"test": long_metadata}, } - on_detected_attack( - mock_connection_manager, + event = create_detected_attack_api_event( attack=attack, context=test_utils.generate_context(), blocked=False, stack=None, ) - assert attack["metadata"]["test"] == long_metadata[:4096] - mock_connection_manager.api.report.assert_called_once() + assert event["attack"]["metadata"]["test"] == long_metadata[:4096] -def test_on_detected_attack_success(mock_connection_manager): +def test_create_attack_event_success(): attack = { "payload": {"key": "value"}, "metadata": {}, } - on_detected_attack( - mock_connection_manager, + event = create_detected_attack_api_event( attack=attack, context=test_utils.generate_context(), blocked=False, stack=None, ) - assert mock_connection_manager.api.report.call_count == 1 - - -def test_on_detected_attack_exception_handling(mock_connection_manager, caplog): - attack = { - "payload": {"key": "value"}, - "metadata": {"key": "value"}, + assert event == { + "attack": { + "blocked": False, + "metadata": {}, + "payload": '{"key": "value"}', + "stack": None, + "user": None, + }, + "request": { + "ipAddress": "1.1.1.1", + "method": "POST", + "route": "/", + "source": "flask", + "url": "http://localhost:8080/", + "userAgent": None, + }, + "type": "detected_attack", } - # Simulate an exception during the API call - mock_connection_manager.api.report.side_effect = Exception("API error") - - on_detected_attack( - mock_connection_manager, - attack=attack, - context=test_utils.generate_context(), - blocked=False, - stack=None, - ) - - assert "Failed to report an attack" in caplog.text - -def test_on_detected_attack_with_blocked_and_stack(mock_connection_manager): +def test_create_attack_event_with_blocked_and_stack(): attack = { "payload": {"key": "value"}, "metadata": {}, @@ -113,8 +79,7 @@ def test_on_detected_attack_with_blocked_and_stack(mock_connection_manager): blocked = True stack = "sample stack trace" - on_detected_attack( - mock_connection_manager, + event = create_detected_attack_api_event( attack=attack, context=test_utils.generate_context(), blocked=blocked, @@ -122,19 +87,17 @@ def test_on_detected_attack_with_blocked_and_stack(mock_connection_manager): ) # Check that the attack dictionary has the blocked and stack fields set - assert attack["blocked"] is True - assert attack["stack"] == stack - assert mock_connection_manager.api.report.call_count == 1 + assert event["attack"]["blocked"] is True + assert event["attack"]["stack"] == stack -def test_on_detected_attack_request_data_and_attack_data(mock_connection_manager): +def test_create_attack_event_request_data_and_attack_data(): attack = { "payload": {"key": "value"}, "metadata": {"test": "true"}, } - on_detected_attack( - mock_connection_manager, + event = create_detected_attack_api_event( attack=attack, context=test_utils.generate_context( method="GET", @@ -147,9 +110,6 @@ def test_on_detected_attack_request_data_and_attack_data(mock_connection_manager stack=None, ) - # Extract the call arguments for the report method - _, event, _ = mock_connection_manager.api.report.call_args[0] - # Verify the request attribute in the payload request_data = event["request"] @@ -170,44 +130,36 @@ def test_on_detected_attack_request_data_and_attack_data(mock_connection_manager assert attack_data["user"] is None -def test_on_detected_attack_with_user(mock_connection_manager): +def test_create_attack_event_with_user(): attack = { "payload": {"key": "value"}, "metadata": {}, } - on_detected_attack( - mock_connection_manager, + event = create_detected_attack_api_event( attack=attack, context=test_utils.generate_context(user="test_user"), blocked=False, stack=None, ) - # Extract the call arguments for the report method - _, event, _ = mock_connection_manager.api.report.call_args[0] - # Verify the user is included in the attack data assert event["attack"]["user"] == "test_user" -def test_on_detected_attack_no_context_and_attack_data(mock_connection_manager): +def test_create_attack_event_no_context_and_attack_data(): attack = { "payload": {"key": "value"}, "metadata": {"test": "true"}, } - on_detected_attack( - mock_connection_manager, + event = create_detected_attack_api_event( attack=attack, context=None, blocked=False, stack=None, ) - # Extract the call arguments for the report method - _, event, _ = mock_connection_manager.api.report.call_args[0] - # Verify the request attribute in the payload request_data = event["request"] attack_data = event["attack"] From 346d4e28e366d215fae27d01a8ffc7f03dff9d51 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 19:04:58 +0100 Subject: [PATCH 14/26] update how event is generated in vulnerabilities --- aikido_zen/vulnerabilities/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/aikido_zen/vulnerabilities/__init__.py b/aikido_zen/vulnerabilities/__init__.py index 650d2cfa..551cb4a9 100644 --- a/aikido_zen/vulnerabilities/__init__.py +++ b/aikido_zen/vulnerabilities/__init__.py @@ -106,12 +106,10 @@ def run_vulnerability_scan(kind, op, args): stack = get_clean_stacktrace() - if comms: - event = create_detected_attack_api_event( - injection_results, context, blocked, stack - ) - comms.send_data_to_bg_process( - "ATTACK", (injection_results, context, blocked, stack) - ) + event = create_detected_attack_api_event( + injection_results, context, blocked, stack + ) + if comms and event: + comms.send_data_to_bg_process("ATTACK", (event)) if blocked: raise error_type(*error_args) From f811a61689937da6934467927ab749ffd6ed847b Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 19:15:55 +0100 Subject: [PATCH 15/26] create a send_payload --- aikido_zen/background_process/comms.py | 7 +++++++ aikido_zen/vulnerabilities/__init__.py | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/aikido_zen/background_process/comms.py b/aikido_zen/background_process/comms.py index 48b7e402..db4b9f57 100644 --- a/aikido_zen/background_process/comms.py +++ b/aikido_zen/background_process/comms.py @@ -5,6 +5,8 @@ import multiprocessing.connection as con from threading import Thread + +from aikido_zen.background_process.commands.command import Payload from aikido_zen.helpers.logging import logger # pylint: disable=invalid-name # This variable does change @@ -98,3 +100,8 @@ def target(address, key, receive, data, result_obj): if receive: return {"success": True, "data": result_obj[1]} return {"success": True, "data": None} + + def send_payload(self, payload: Payload): + return self.send_data_to_bg_process( + payload.identifier, payload.request, payload.returns_data + ) diff --git a/aikido_zen/vulnerabilities/__init__.py b/aikido_zen/vulnerabilities/__init__.py index 551cb4a9..f928e682 100644 --- a/aikido_zen/vulnerabilities/__init__.py +++ b/aikido_zen/vulnerabilities/__init__.py @@ -29,6 +29,7 @@ from .path_traversal.check_context_for_path_traversal import ( check_context_for_path_traversal, ) +from ..background_process.commands import PutEventCommand from ..helpers.create_detected_attack_api_event import create_detected_attack_api_event @@ -110,6 +111,6 @@ def run_vulnerability_scan(kind, op, args): injection_results, context, blocked, stack ) if comms and event: - comms.send_data_to_bg_process("ATTACK", (event)) + comms.send_payload(PutEventCommand.generate(event)) if blocked: raise error_type(*error_args) From fa941949f153604aba5a5c2892afa23f75699ec0 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 19:16:06 +0100 Subject: [PATCH 16/26] update command types to add Payload class --- .../background_process/commands/command.py | 17 +++++++++++++++++ .../background_process/commands/put_event.py | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/aikido_zen/background_process/commands/command.py b/aikido_zen/background_process/commands/command.py index d8a1e11d..36b2886f 100644 --- a/aikido_zen/background_process/commands/command.py +++ b/aikido_zen/background_process/commands/command.py @@ -17,7 +17,24 @@ class Command(ABC): def identifier(cls) -> str: pass + @classmethod + @abstractmethod + def returns_data(cls) -> bool: + pass + @classmethod @abstractmethod def run(cls, context: CommandContext, request): pass + + @classmethod + @abstractmethod + def generate(cls, *args, **kwargs): + pass + + +class Payload: + def __init__(self, command: type[Command], request): + self.identifier = command.identifier() + self.returns_data = command.returns_data() + self.request = request diff --git a/aikido_zen/background_process/commands/put_event.py b/aikido_zen/background_process/commands/put_event.py index 36523a77..f0db262d 100644 --- a/aikido_zen/background_process/commands/put_event.py +++ b/aikido_zen/background_process/commands/put_event.py @@ -1,4 +1,4 @@ -from .command import Command, CommandContext +from .command import Command, CommandContext, Payload class PutEventReq: @@ -13,6 +13,14 @@ class PutEventCommand(Command): def identifier(cls) -> str: return "put_event" + @classmethod + def returns_data(cls) -> bool: + return False + @classmethod def run(cls, context: CommandContext, request: PutEventReq): context.queue.put(request.event) + + @classmethod + def generate(cls, event) -> Payload: + return Payload(cls, PutEventReq(event)) From b7a25acd6f85807645b61477b77f544a07261377 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 19:29:54 +0100 Subject: [PATCH 17/26] create send_payload helper function --- aikido_zen/background_process/comms.py | 6 ------ aikido_zen/helpers/ipc/__init__.py | 0 aikido_zen/helpers/ipc/send_payload.py | 8 ++++++++ aikido_zen/vulnerabilities/__init__.py | 3 ++- 4 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 aikido_zen/helpers/ipc/__init__.py create mode 100644 aikido_zen/helpers/ipc/send_payload.py diff --git a/aikido_zen/background_process/comms.py b/aikido_zen/background_process/comms.py index db4b9f57..ffe42dab 100644 --- a/aikido_zen/background_process/comms.py +++ b/aikido_zen/background_process/comms.py @@ -6,7 +6,6 @@ import multiprocessing.connection as con from threading import Thread -from aikido_zen.background_process.commands.command import Payload from aikido_zen.helpers.logging import logger # pylint: disable=invalid-name # This variable does change @@ -100,8 +99,3 @@ def target(address, key, receive, data, result_obj): if receive: return {"success": True, "data": result_obj[1]} return {"success": True, "data": None} - - def send_payload(self, payload: Payload): - return self.send_data_to_bg_process( - payload.identifier, payload.request, payload.returns_data - ) diff --git a/aikido_zen/helpers/ipc/__init__.py b/aikido_zen/helpers/ipc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aikido_zen/helpers/ipc/send_payload.py b/aikido_zen/helpers/ipc/send_payload.py new file mode 100644 index 00000000..5e06ecbd --- /dev/null +++ b/aikido_zen/helpers/ipc/send_payload.py @@ -0,0 +1,8 @@ +from aikido_zen.background_process import AikidoIPCCommunications +from aikido_zen.background_process.commands.command import Payload + + +def send_payload(comms: AikidoIPCCommunications, payload: Payload): + return comms.send_data_to_bg_process( + payload.identifier, payload.request, payload.returns_data + ) diff --git a/aikido_zen/vulnerabilities/__init__.py b/aikido_zen/vulnerabilities/__init__.py index f928e682..d62d0e3a 100644 --- a/aikido_zen/vulnerabilities/__init__.py +++ b/aikido_zen/vulnerabilities/__init__.py @@ -31,6 +31,7 @@ ) from ..background_process.commands import PutEventCommand from ..helpers.create_detected_attack_api_event import create_detected_attack_api_event +from ..helpers.ipc.send_payload import send_payload def run_vulnerability_scan(kind, op, args): @@ -111,6 +112,6 @@ def run_vulnerability_scan(kind, op, args): injection_results, context, blocked, stack ) if comms and event: - comms.send_payload(PutEventCommand.generate(event)) + send_payload(comms, PutEventCommand.generate(event)) if blocked: raise error_type(*error_args) From dcfe12db12c95e9163795cd057f88792cfacaa29 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 19:33:08 +0100 Subject: [PATCH 18/26] move command.py to command_types in helper --- aikido_zen/background_process/commands/__init__.py | 2 +- aikido_zen/background_process/commands/put_event.py | 2 +- .../commands/command.py => helpers/ipc/command_types.py} | 0 aikido_zen/helpers/ipc/send_payload.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename aikido_zen/{background_process/commands/command.py => helpers/ipc/command_types.py} (100%) diff --git a/aikido_zen/background_process/commands/__init__.py b/aikido_zen/background_process/commands/__init__.py index 891644d7..624b953f 100644 --- a/aikido_zen/background_process/commands/__init__.py +++ b/aikido_zen/background_process/commands/__init__.py @@ -1,5 +1,5 @@ from aikido_zen.helpers.logging import logger -from .command import CommandContext +from aikido_zen.helpers.ipc.command_types import CommandContext from .put_event import PutEventCommand from .check_firewall_lists import process_check_firewall_lists from .read_property import process_read_property diff --git a/aikido_zen/background_process/commands/put_event.py b/aikido_zen/background_process/commands/put_event.py index f0db262d..87ef65e1 100644 --- a/aikido_zen/background_process/commands/put_event.py +++ b/aikido_zen/background_process/commands/put_event.py @@ -1,4 +1,4 @@ -from .command import Command, CommandContext, Payload +from aikido_zen.helpers.ipc.command_types import Command, CommandContext, Payload class PutEventReq: diff --git a/aikido_zen/background_process/commands/command.py b/aikido_zen/helpers/ipc/command_types.py similarity index 100% rename from aikido_zen/background_process/commands/command.py rename to aikido_zen/helpers/ipc/command_types.py diff --git a/aikido_zen/helpers/ipc/send_payload.py b/aikido_zen/helpers/ipc/send_payload.py index 5e06ecbd..563ff337 100644 --- a/aikido_zen/helpers/ipc/send_payload.py +++ b/aikido_zen/helpers/ipc/send_payload.py @@ -1,5 +1,5 @@ from aikido_zen.background_process import AikidoIPCCommunications -from aikido_zen.background_process.commands.command import Payload +from aikido_zen.helpers.ipc.command_types import Payload def send_payload(comms: AikidoIPCCommunications, payload: Payload): From 88676cd65f91147c4a9bea91a706b244d52196fd Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 19:35:14 +0100 Subject: [PATCH 19/26] move is_private_ip to aikido_zen/helpers/net --- aikido_zen/helpers/net/__init__.py | 0 .../{vulnerabilities/ssrf => helpers/net}/is_private_ip.py | 0 .../ssrf => helpers/net}/is_private_ip_test.py | 3 +-- aikido_zen/storage/firewall_lists.py | 2 +- aikido_zen/vulnerabilities/ssrf/inspect_getaddrinfo_result.py | 2 +- 5 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 aikido_zen/helpers/net/__init__.py rename aikido_zen/{vulnerabilities/ssrf => helpers/net}/is_private_ip.py (100%) rename aikido_zen/{vulnerabilities/ssrf => helpers/net}/is_private_ip_test.py (96%) diff --git a/aikido_zen/helpers/net/__init__.py b/aikido_zen/helpers/net/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aikido_zen/vulnerabilities/ssrf/is_private_ip.py b/aikido_zen/helpers/net/is_private_ip.py similarity index 100% rename from aikido_zen/vulnerabilities/ssrf/is_private_ip.py rename to aikido_zen/helpers/net/is_private_ip.py diff --git a/aikido_zen/vulnerabilities/ssrf/is_private_ip_test.py b/aikido_zen/helpers/net/is_private_ip_test.py similarity index 96% rename from aikido_zen/vulnerabilities/ssrf/is_private_ip_test.py rename to aikido_zen/helpers/net/is_private_ip_test.py index db1467d9..504cc025 100644 --- a/aikido_zen/vulnerabilities/ssrf/is_private_ip_test.py +++ b/aikido_zen/helpers/net/is_private_ip_test.py @@ -1,5 +1,4 @@ -import pytest -from .is_private_ip import is_private_ip +from aikido_zen.helpers.net.is_private_ip import is_private_ip # Test cases for is_private_ip diff --git a/aikido_zen/storage/firewall_lists.py b/aikido_zen/storage/firewall_lists.py index 68293a6b..9381e82c 100644 --- a/aikido_zen/storage/firewall_lists.py +++ b/aikido_zen/storage/firewall_lists.py @@ -1,7 +1,7 @@ from aikido_zen.helpers.ip_matcher import IPMatcher import regex as re -from aikido_zen.vulnerabilities.ssrf.is_private_ip import is_private_ip +from aikido_zen.helpers.net.is_private_ip import is_private_ip class FirewallLists: diff --git a/aikido_zen/vulnerabilities/ssrf/inspect_getaddrinfo_result.py b/aikido_zen/vulnerabilities/ssrf/inspect_getaddrinfo_result.py index d20e52d9..67e52a5b 100644 --- a/aikido_zen/vulnerabilities/ssrf/inspect_getaddrinfo_result.py +++ b/aikido_zen/vulnerabilities/ssrf/inspect_getaddrinfo_result.py @@ -7,7 +7,7 @@ from aikido_zen.helpers.logging import logger from aikido_zen.thread.thread_cache import get_cache from .imds import resolves_to_imds_ip -from .is_private_ip import is_private_ip +from aikido_zen.helpers.net.is_private_ip import is_private_ip from .find_hostname_in_context import find_hostname_in_context from .extract_ip_array_from_results import extract_ip_array_from_results from .is_redirect_to_private_ip import is_redirect_to_private_ip From 5d481a41ecb4ef30d8ba658d453ea3b3e2a92727 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 19:39:59 +0100 Subject: [PATCH 20/26] Fix regular old commands --- aikido_zen/background_process/commands/__init__.py | 2 +- .../background_process/commands/check_firewall_lists.py | 8 +------- aikido_zen/background_process/commands/ping.py | 2 +- aikido_zen/background_process/commands/read_property.py | 2 +- .../background_process/commands/should_ratelimit.py | 2 +- aikido_zen/background_process/commands/sync_data.py | 2 +- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/aikido_zen/background_process/commands/__init__.py b/aikido_zen/background_process/commands/__init__.py index 624b953f..5b36b758 100644 --- a/aikido_zen/background_process/commands/__init__.py +++ b/aikido_zen/background_process/commands/__init__.py @@ -23,7 +23,7 @@ def process_incoming_command(connection_manager, obj, conn, queue): inbound_request = obj[1] if inbound_identifier in commands_map: func = commands_map[inbound_identifier] - return conn.send(func(connection_manager, inbound_identifier, queue)) + return conn.send(func(connection_manager, inbound_request)) for cmd in modern_commands: if cmd.identifier() == inbound_identifier: diff --git a/aikido_zen/background_process/commands/check_firewall_lists.py b/aikido_zen/background_process/commands/check_firewall_lists.py index 390544a8..acf32304 100644 --- a/aikido_zen/background_process/commands/check_firewall_lists.py +++ b/aikido_zen/background_process/commands/check_firewall_lists.py @@ -1,13 +1,7 @@ """Exports process_check_firewall_lists""" -from aikido_zen.background_process.cloud_connection_manager import ( - CloudConnectionManager, -) - -def process_check_firewall_lists( - connection_manager: CloudConnectionManager, data, conn, queue=None -): +def process_check_firewall_lists(connection_manager, data): """ Checks whether an IP is blocked data: {"ip": string, "user-agent": string} diff --git a/aikido_zen/background_process/commands/ping.py b/aikido_zen/background_process/commands/ping.py index 9245e5fd..9363eab0 100644 --- a/aikido_zen/background_process/commands/ping.py +++ b/aikido_zen/background_process/commands/ping.py @@ -1,6 +1,6 @@ """exports `process_ping`""" -def process_ping(connection_manager, data, queue=None): +def process_ping(connection_manager, data): """when main process quits , or during testing etc""" return "Received" diff --git a/aikido_zen/background_process/commands/read_property.py b/aikido_zen/background_process/commands/read_property.py index b70e6f31..1bff5e39 100644 --- a/aikido_zen/background_process/commands/read_property.py +++ b/aikido_zen/background_process/commands/read_property.py @@ -3,7 +3,7 @@ from aikido_zen.helpers.logging import logger -def process_read_property(connection_manager, data, queue=None): +def process_read_property(connection_manager, data): """ Takes in one arg : name of property on connection_manager, tries to read it. Meant to get config props diff --git a/aikido_zen/background_process/commands/should_ratelimit.py b/aikido_zen/background_process/commands/should_ratelimit.py index 40ba0297..d51f99ce 100644 --- a/aikido_zen/background_process/commands/should_ratelimit.py +++ b/aikido_zen/background_process/commands/should_ratelimit.py @@ -3,7 +3,7 @@ import aikido_zen.ratelimiting as ratelimiting -def process_should_ratelimit(connection_manager, data, queue=None): +def process_should_ratelimit(connection_manager, data): """ Called to check if the context passed along as data should be rate limited data object should be a dict including route_metadata, remote_address and user diff --git a/aikido_zen/background_process/commands/sync_data.py b/aikido_zen/background_process/commands/sync_data.py index 5fedcc8e..4c5a9501 100644 --- a/aikido_zen/background_process/commands/sync_data.py +++ b/aikido_zen/background_process/commands/sync_data.py @@ -4,7 +4,7 @@ from aikido_zen.background_process.packages import PackagesStore -def process_sync_data(connection_manager, data, conn, queue=None): +def process_sync_data(connection_manager, data): """ Synchronizes data between the thread-local cache (with a TTL of usually 1 minute) and the background thread. Which data gets synced? From e240d038bfdd4f2b1bc6893f5ab88a977624dd15 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 19:41:48 +0100 Subject: [PATCH 21/26] Add limit to the serialize_to_json --- aikido_zen/helpers/serialize_to_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aikido_zen/helpers/serialize_to_json.py b/aikido_zen/helpers/serialize_to_json.py index 0905d108..61022434 100644 --- a/aikido_zen/helpers/serialize_to_json.py +++ b/aikido_zen/helpers/serialize_to_json.py @@ -10,7 +10,7 @@ def serialize_to_json(obj): if that fails it returns an empty JSON string "{}" """ try: - return json.dumps(obj) + return json.dumps(obj)[:5000] except Exception: try: return json.dumps(dict(obj)) From 5a7e6d1005b112f6acdcc71964ea5da6fd9447c0 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 19:42:05 +0100 Subject: [PATCH 22/26] revert: add limit --- aikido_zen/helpers/serialize_to_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aikido_zen/helpers/serialize_to_json.py b/aikido_zen/helpers/serialize_to_json.py index 61022434..0905d108 100644 --- a/aikido_zen/helpers/serialize_to_json.py +++ b/aikido_zen/helpers/serialize_to_json.py @@ -10,7 +10,7 @@ def serialize_to_json(obj): if that fails it returns an empty JSON string "{}" """ try: - return json.dumps(obj)[:5000] + return json.dumps(obj) except Exception: try: return json.dumps(dict(obj)) From 4901d347af27a4230fe967fafc4435c5b65023fa Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 19:47:22 +0100 Subject: [PATCH 23/26] improve log in vulnerabilities/__init__.py --- aikido_zen/vulnerabilities/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aikido_zen/vulnerabilities/__init__.py b/aikido_zen/vulnerabilities/__init__.py index d62d0e3a..5f428c7e 100644 --- a/aikido_zen/vulnerabilities/__init__.py +++ b/aikido_zen/vulnerabilities/__init__.py @@ -100,8 +100,6 @@ def run_vulnerability_scan(kind, op, args): logger.debug("Exception occurred in run_vulnerability_scan : %s", e) if injection_results: - logger.debug("Injection results : %s", serialize_to_json(injection_results)) - blocked = is_blocking_enabled() operation = injection_results["operation"] thread_cache.stats.on_detected_attack(blocked, operation) @@ -111,6 +109,7 @@ def run_vulnerability_scan(kind, op, args): event = create_detected_attack_api_event( injection_results, context, blocked, stack ) + logger.debug("Attack: %s", serialize_to_json(event)[:5000]) if comms and event: send_payload(comms, PutEventCommand.generate(event)) if blocked: From 9884fee795d46ddfbe437c404ec6f2b9a09770c2 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 20:29:17 +0100 Subject: [PATCH 24/26] linting fix --- .../background_process/commands/put_event.py | 4 ++-- aikido_zen/helpers/ipc/command_types.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/aikido_zen/background_process/commands/put_event.py b/aikido_zen/background_process/commands/put_event.py index 87ef65e1..707d732a 100644 --- a/aikido_zen/background_process/commands/put_event.py +++ b/aikido_zen/background_process/commands/put_event.py @@ -22,5 +22,5 @@ def run(cls, context: CommandContext, request: PutEventReq): context.queue.put(request.event) @classmethod - def generate(cls, event) -> Payload: - return Payload(cls, PutEventReq(event)) + def generate(cls, request) -> Payload: + return Payload(cls, PutEventReq(request)) diff --git a/aikido_zen/helpers/ipc/command_types.py b/aikido_zen/helpers/ipc/command_types.py index 36b2886f..dbbace9f 100644 --- a/aikido_zen/helpers/ipc/command_types.py +++ b/aikido_zen/helpers/ipc/command_types.py @@ -11,6 +11,13 @@ def send(self, response): self.connection.send(response) +class Payload: + def __init__(self, command, request): + self.identifier = command.identifier() + self.returns_data = command.returns_data() + self.request = request + + class Command(ABC): @classmethod @abstractmethod @@ -29,12 +36,5 @@ def run(cls, context: CommandContext, request): @classmethod @abstractmethod - def generate(cls, *args, **kwargs): + def generate(cls, request) -> Payload: pass - - -class Payload: - def __init__(self, command: type[Command], request): - self.identifier = command.identifier() - self.returns_data = command.returns_data() - self.request = request From 34d1a325ea6f43b71004156306a3a5cb867ef88f Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 20:31:18 +0100 Subject: [PATCH 25/26] Update e2e test to include regex package --- end2end/django_mysql_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end2end/django_mysql_test.py b/end2end/django_mysql_test.py index 2ffed17e..ed791ddd 100644 --- a/end2end/django_mysql_test.py +++ b/end2end/django_mysql_test.py @@ -104,7 +104,7 @@ def test_initial_heartbeat(): "method": "POST", "path": "/app/create" }], - packages={'wrapt', 'asgiref', 'aikido_zen', 'django', 'sqlparse', 'mysqlclient'} + packages={'wrapt', 'asgiref', 'aikido_zen', 'django', 'sqlparse', 'mysqlclient', 'regex'} ) req_stats = heartbeat_events[0]["stats"]["requests"] assert req_stats["aborted"] == 0 From 2ac1c58d4eb187360f43d8434042ec64c18e1186 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 24 Nov 2025 20:57:23 +0100 Subject: [PATCH 26/26] Fix test cases for new put_event data structure changes --- .../cloud_connection_manager/on_start_test.py | 13 ++++---- .../commands/sync_data_test.py | 10 +++---- .../sources/functions/request_handler_test.py | 2 +- aikido_zen/vulnerabilities/init_test.py | 30 ++++++++++++++----- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/aikido_zen/background_process/cloud_connection_manager/on_start_test.py b/aikido_zen/background_process/cloud_connection_manager/on_start_test.py index 4dffb92c..fd9580d4 100644 --- a/aikido_zen/background_process/cloud_connection_manager/on_start_test.py +++ b/aikido_zen/background_process/cloud_connection_manager/on_start_test.py @@ -8,7 +8,7 @@ def mock_connection_manager(): connection_manager = MagicMock() connection_manager.token = "test_token" connection_manager.timeout_in_sec = 5 - connection_manager.api.report = MagicMock(return_value={"success": True}) + connection_manager.report_api_event = MagicMock(return_value={"success": True}) connection_manager.get_manager_info = lambda: {} connection_manager.update_service_config = MagicMock() return connection_manager @@ -19,7 +19,7 @@ def test_on_start_no_token(): connection_manager = MagicMock() connection_manager.token = None on_start(connection_manager) - connection_manager.api.report.assert_not_called() + connection_manager.report_api_event.assert_called() def test_on_start_success(mock_connection_manager, caplog): @@ -27,7 +27,7 @@ def test_on_start_success(mock_connection_manager, caplog): on_start(mock_connection_manager) # Check that the API report method was called - mock_connection_manager.api.report.assert_called_once() + mock_connection_manager.report_api_event.assert_called_once() # Check that the service config was updated mock_connection_manager.update_service_config.assert_called_once() @@ -38,7 +38,7 @@ def test_on_start_success(mock_connection_manager, caplog): def test_on_start_failure(mock_connection_manager, caplog): """Test that an error is logged when the API call fails.""" - mock_connection_manager.api.report.return_value = { + mock_connection_manager.report_api_event.return_value = { "success": False, "error": "Some error", } @@ -46,10 +46,7 @@ def test_on_start_failure(mock_connection_manager, caplog): on_start(mock_connection_manager) # Check that the API report method was called - mock_connection_manager.api.report.assert_called_once() + mock_connection_manager.report_api_event.assert_called_once() # Check that the service config was not updated mock_connection_manager.update_service_config.assert_not_called() - - # Check that the error log was called - assert "Failed to communicate with Aikido Server : Some error" in caplog.text diff --git a/aikido_zen/background_process/commands/sync_data_test.py b/aikido_zen/background_process/commands/sync_data_test.py index 286386cb..32dad72d 100644 --- a/aikido_zen/background_process/commands/sync_data_test.py +++ b/aikido_zen/background_process/commands/sync_data_test.py @@ -72,7 +72,7 @@ def test_process_sync_data_initialization(setup_connection_manager): ], } - result = process_sync_data(connection_manager, data, None) + result = process_sync_data(connection_manager, data) # Check that routes were initialized correctly assert len(connection_manager.routes) == 2 @@ -147,7 +147,7 @@ def test_process_sync_data_with_last_updated_at_below_zero(setup_connection_mana "middleware_installed": True, } - result = process_sync_data(connection_manager, data, None) + result = process_sync_data(connection_manager, data) # Check that routes were initialized correctly assert len(connection_manager.routes) == 2 @@ -214,7 +214,7 @@ def test_process_sync_data_existing_route_and_hostnames(setup_connection_manager } # First call to initialize the route - process_sync_data(connection_manager, data, None) + process_sync_data(connection_manager, data) # Second call to update the existing route data_update = { @@ -241,7 +241,7 @@ def test_process_sync_data_existing_route_and_hostnames(setup_connection_manager }, } - result = process_sync_data(connection_manager, data_update, None) + result = process_sync_data(connection_manager, data_update) # Check that the hit count was updated correctly assert ( @@ -275,7 +275,7 @@ def test_process_sync_data_no_routes(setup_connection_manager): connection_manager = setup_connection_manager data = {"current_routes": {}, "reqs": 0} # No requests to add - result = process_sync_data(connection_manager, data, None) + result = process_sync_data(connection_manager, data) # Check that no routes were initialized assert len(connection_manager.routes) == 0 diff --git a/aikido_zen/sources/functions/request_handler_test.py b/aikido_zen/sources/functions/request_handler_test.py index db76717f..9af37f31 100644 --- a/aikido_zen/sources/functions/request_handler_test.py +++ b/aikido_zen/sources/functions/request_handler_test.py @@ -42,7 +42,7 @@ def __init__(self): def send_data_to_bg_process(self, action, obj, receive=False, timeout_in_sec=0.1): if action != "CHECK_FIREWALL_LISTS": return {"success": False} - res = process_check_firewall_lists(self.conn_manager, obj, None, None) + res = process_check_firewall_lists(self.conn_manager, obj) return { "success": True, "data": res, diff --git a/aikido_zen/vulnerabilities/init_test.py b/aikido_zen/vulnerabilities/init_test.py index da7b13e1..5b7a811a 100644 --- a/aikido_zen/vulnerabilities/init_test.py +++ b/aikido_zen/vulnerabilities/init_test.py @@ -144,13 +144,29 @@ def test_sql_injection_with_comms(caplog, get_context, monkeypatch): ) mock_comms.send_data_to_bg_process.assert_called_once() call_args = mock_comms.send_data_to_bg_process.call_args[0] - assert call_args[0] == "ATTACK" - assert call_args[1][0]["kind"] == "sql_injection" - assert ( - call_args[1][0]["metadata"]["sql"] - == "INSERT * INTO VALUES ('doggoss2', TRUE);" - ) - assert call_args[1][0]["metadata"]["dialect"] == "mysql" + assert call_args[0] == "put_event" + assert call_args[1].event["request"] == { + "ipAddress": "198.51.100.23", + "method": "GET", + "route": "/hello", + "source": "flask", + "url": "http://localhost:8080/hello", + "userAgent": None, + } + del call_args[1].event["attack"]["stack"] # Hard to test + assert call_args[1].event["attack"] == { + "blocked": True, + "kind": "sql_injection", + "metadata": { + "dialect": "mysql", + "sql": "INSERT * INTO VALUES ('doggoss2', TRUE);", + }, + "operation": "test_op", + "pathToPayload": ".test_input_sql", + "payload": '"doggoss2\', TRUE"', + "source": "body", + "user": None, + } def test_ssrf_vulnerability_scan_adds_hostname(get_context):