Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d6fe487
Re-enable attribute-defined-outside-init
bitterpanda63 Nov 13, 2025
ff068d9
Create a static new() function on the service config class
bitterpanda63 Nov 13, 2025
0e9e975
thread cache: define all attribtues in init
bitterpanda63 Nov 13, 2025
bae79cb
service_config.py cleanup
bitterpanda63 Nov 13, 2025
bd9a33f
Update thread_cache & it's tests to use new ServiceCOnfig init
bitterpanda63 Nov 13, 2025
141e479
Update request_handler test cases
bitterpanda63 Nov 13, 2025
9913bc8
update rate-limiting test cases with new service config
bitterpanda63 Nov 13, 2025
48b2bfd
Update service config test cases themselves
bitterpanda63 Nov 13, 2025
0c2e7fb
cloud connection manager, fix service config update
bitterpanda63 Nov 13, 2025
7020ed2
lint
bitterpanda63 Nov 13, 2025
9b94bb6
Fix the set_body_internal mess
bitterpanda63 Nov 13, 2025
38e3945
Merge branch 'main' into enable-attribute-defined-outside-init-lint
bitterpanda63 Nov 13, 2025
95a11ee
thread cache reset - also reset middleware_installed
bitterpanda63 Nov 13, 2025
3167477
Merge branch 'main' into enable-attribute-defined-outside-init-lint
bitterpanda63 Dec 30, 2025
64fc8bc
Fix linting conflict with merge
bitterpanda63 Dec 30, 2025
47e26bd
Fix test cases in service_config_test.py after merge conflict
bitterpanda63 Dec 30, 2025
87365b7
Fix socket_test.py test cases for ServiceConfig init after merge conf…
bitterpanda63 Dec 30, 2025
e297833
Fix issues with update_service_config and its test cases after merge
bitterpanda63 Dec 30, 2025
33539b7
Update aikido_zen/background_process/service_config.py
bitterpanda63 Dec 30, 2025
9930ecc
Update aikido_zen/context/__init__.py
bitterpanda63 Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,4 @@ disable =
no-value-for-parameter,
unexpected-keyword-arg,
inconsistent-return-statements,
duplicate-code,
attribute-defined-outside-init,
duplicate-code
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,7 @@ def __init__(self, block, api, token, serverless):
self.token = token # Should be instance of the Token class!
self.routes = Routes(200)
self.hostnames = Hostnames(200)
self.conf = ServiceConfig(
endpoints=[],
last_updated_at=-1, # Has not been updated yet
blocked_uids=[],
bypassed_ips=[],
received_any_stats=True,
)
self.conf = ServiceConfig()
self.firewall_lists = FirewallLists()
self.rate_limiter = RateLimiter(
max_items=5000, time_to_live_in_ms=120 * 60 * 1000 # 120 minutes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ def update_service_config(connection_manager, res):
logger.debug("Updating blocking, setting blocking to : %s", res["block"])
connection_manager.block = bool(res["block"])

connection_manager.conf.update(
endpoints=res.get("endpoints", []),
last_updated_at=res.get("configUpdatedAt", get_unixtime_ms()),
blocked_uids=res.get("blockedUserIds", []),
bypassed_ips=res.get("allowedIPAddresses", []),
received_any_stats=res.get("receivedAnyStats", True),
connection_manager.conf.set_endpoints(res.get("endpoints", []))
connection_manager.conf.set_last_updated_at(
res.get("configUpdatedAt", get_unixtime_ms())
)
connection_manager.conf.set_blocked_user_ids(res.get("blockedUserIds", []))
connection_manager.conf.set_bypassed_ips(res.get("allowedIPAddresses", []))
if res.get("receivedAnyStats", True):
connection_manager.conf.enable_received_stats()

# Handle outbound request blocking configuration
if "blockNewOutgoingRequests" in res:
Expand Down
49 changes: 17 additions & 32 deletions aikido_zen/background_process/service_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,18 @@
from aikido_zen.helpers.match_endpoints import match_endpoints


# noinspection PyAttributeOutsideInit
class ServiceConfig:
"""Class holding the config of the connection_manager"""

def __init__(
self,
endpoints,
last_updated_at: int,
blocked_uids,
bypassed_ips,
received_any_stats: bool,
):
# Init the class using update function :
self.update(
endpoints, last_updated_at, blocked_uids, bypassed_ips, received_any_stats
)

def __init__(self):
self.endpoints = []
self.bypassed_ips = IPMatcher()
self.blocked_uids = set()
self.last_updated_at = -1
self.received_any_stats = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was previously true fyi

self.block_new_outgoing_requests = False
self.outbound_domains = {}

def update(
self,
endpoints,
last_updated_at: int,
blocked_uids,
bypassed_ips,
received_any_stats: bool,
):
self.last_updated_at = last_updated_at
self.received_any_stats = bool(received_any_stats)
self.blocked_uids = set(blocked_uids)
self.set_endpoints(endpoints)
self.set_bypassed_ips(bypassed_ips)

def set_endpoints(self, endpoints):
"""Sets non-graphql endpoints"""

self.endpoints = [
endpoint for endpoint in endpoints if not endpoint.get("graphql")
]
Expand All @@ -68,7 +44,7 @@ def get_endpoints(self, route_metadata):
return match_endpoints(route_metadata, self.endpoints)

def set_bypassed_ips(self, bypassed_ips):
"""Creates an IPMatcher from the given bypassed ip set"""
"""Creates a new IPMatcher from the given bypassed ip set"""
self.bypassed_ips = IPMatcher()
for ip in bypassed_ips:
self.bypassed_ips.add(ip)
Expand All @@ -77,6 +53,15 @@ def is_bypassed_ip(self, ip):
"""Checks if the IP is on the bypass list"""
return self.bypassed_ips.has(ip)

def set_blocked_user_ids(self, blocked_user_ids):
self.blocked_uids = set(blocked_user_ids)

def enable_received_any_stats(self):
self.received_any_stats = True

def set_last_updated_at(self, last_updated_at: int):
self.last_updated_at = last_updated_at

def update_outbound_domains(self, domains):
self.outbound_domains = {
domain["hostname"]: domain["mode"] for domain in domains
Expand Down
123 changes: 50 additions & 73 deletions aikido_zen/background_process/service_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def test_service_config_should_block_outgoing_request():


def test_service_config_initialization():
service_config = ServiceConfig()
endpoints = [
{
"graphql": False,
Expand Down Expand Up @@ -185,26 +186,33 @@ def test_service_config_initialization():
"force_protection_off": False,
},
]
last_updated_at = "2023-10-01"
service_config = ServiceConfig(
endpoints,
last_updated_at,
["0", "0", "1", "5"],
["127.0.0.1", "123.1.2.0/24", "132.1.0.0/16"],
True,
)

# Check that non-GraphQL endpoints are correctly filtered
assert len(service_config.endpoints) == 3
assert len(service_config.endpoints) == 0
service_config.set_endpoints(endpoints)
assert (
len(service_config.endpoints) == 3
) # Check that non-GraphQL endpoints are correctly filtered
assert service_config.endpoints[0]["route"] == "/v1"
assert service_config.endpoints[1]["route"] == "/v3"
assert service_config.endpoints[2]["route"] == "/admin"
assert service_config.last_updated_at == last_updated_at

service_config.set_last_updated_at(37982562953)
assert service_config.last_updated_at == 37982562953

assert isinstance(service_config.bypassed_ips, IPMatcher)
service_config.set_bypassed_ips(["127.0.0.1", "123.1.2.0/24", "132.1.0.0/16"])
assert isinstance(service_config.bypassed_ips, IPMatcher)
assert service_config.bypassed_ips.has("127.0.0.1")
assert service_config.bypassed_ips.has("123.1.2.2")
assert not service_config.bypassed_ips.has("1.1.1.1")
assert service_config.blocked_uids == set(["1", "0", "5"])

assert len(service_config.blocked_uids) == 0
service_config.set_blocked_user_ids({"0", "0", "1", "5"})
assert service_config.blocked_uids == {"1", "0", "5"}

assert not service_config.received_any_stats
service_config.enable_received_any_stats()
assert service_config.received_any_stats == True

v1_endpoint = service_config.get_endpoints(
{
Expand All @@ -230,41 +238,9 @@ def test_service_config_initialization():
assert not admin_endpoint["allowedIPAddresses"].has("192.168.0.1")


# Sample data for testing
sample_endpoints = [
{"url": "http://example.com/api/v1", "graphql": False, "context": "user"},
{"url": "http://example.com/api/v2", "graphql": True, "context": "admin"},
{"url": "http://example.com/api/v3", "graphql": False, "context": "guest"},
]


@pytest.fixture
def service_config():
return ServiceConfig(
endpoints=sample_endpoints,
last_updated_at="2023-10-01T00:00:00Z",
blocked_uids=["user1", "user2"],
bypassed_ips=["192.168.1.1", "10.0.0.1"],
received_any_stats=True,
)


def test_initialization(service_config):
assert len(service_config.endpoints) == 2 # Only non-graphql endpoints
assert service_config.last_updated_at == "2023-10-01T00:00:00Z"
assert isinstance(service_config.bypassed_ips, IPMatcher)
assert service_config.blocked_uids == {"user1", "user2"}


def test_ip_blocking():
config = ServiceConfig(
endpoints=sample_endpoints,
last_updated_at="2023-10-01T00:00:00Z",
blocked_uids=["user1", "user2"],
bypassed_ips=["192.168.1.1", "10.0.0.0/16", "::1/128"],
received_any_stats=True,
)

config = ServiceConfig()
config.set_bypassed_ips(["192.168.1.1", "10.0.0.0/16", "::1/128"])
assert config.is_bypassed_ip("192.168.1.1")
assert config.is_bypassed_ip("10.0.0.1")
assert config.is_bypassed_ip("10.0.1.2")
Expand All @@ -276,38 +252,39 @@ def test_ip_blocking():


def test_service_config_with_empty_allowlist():
endpoints = [
{
"graphql": False,
"method": "GET",
"route": "/admin",
"rate_limiting": {
"enabled": False,
"max_requests": 10,
"window_size_in_ms": 1000,
},
"allowedIPAddresses": [],
"force_protection_off": False,
},
]
last_updated_at = "2023-10-01"
service_config = ServiceConfig(
endpoints,
last_updated_at,
["0", "0", "1", "5"],
["127.0.0.1", "123.1.2.0/24", "132.1.0.0/16"],
True,
)
service_config = ServiceConfig()

# Check that non-GraphQL endpoints are correctly filtered
service_config.set_endpoints(
[
{
"graphql": False,
"method": "GET",
"route": "/admin",
"rate_limiting": {
"enabled": False,
"max_requests": 10,
"window_size_in_ms": 1000,
},
"allowedIPAddresses": [],
"force_protection_off": False,
},
]
)
assert len(service_config.endpoints) == 1
assert service_config.endpoints[0]["route"] == "/admin"
assert service_config.last_updated_at == last_updated_at

service_config.set_last_updated_at(29839537)
assert service_config.last_updated_at == 29839537

service_config.set_blocked_user_ids({"0", "0", "1", "5"})
assert service_config.blocked_uids == {"1", "0", "5"}

service_config.set_bypassed_ips(["127.0.0.1", "123.1.2.0/24", "132.1.0.0/16"])
assert isinstance(service_config.bypassed_ips, IPMatcher)
assert service_config.bypassed_ips.has("127.0.0.1")
assert service_config.bypassed_ips.has("123.1.2.2")
assert not service_config.bypassed_ips.has("1.1.1.1")
assert service_config.blocked_uids == set(["1", "0", "5"])
assert service_config.is_bypassed_ip("127.0.0.1")
assert service_config.is_bypassed_ip("123.1.2.2")
assert not service_config.is_bypassed_ip("1.1.1.1")

admin_endpoint = service_config.get_endpoints(
{
Expand Down
40 changes: 21 additions & 19 deletions aikido_zen/context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def __init__(self, context_obj=None, body=None, req=None, source=None):
self.parsed_userinput = {}
self.xml = {}
self.outgoing_req_redirects = []
self.set_body(body)
self.body = Context.parse_body_object(body)
self.headers: Headers = Headers()
self.cookies = {}
self.query = {}
Expand Down Expand Up @@ -107,26 +107,28 @@ def set_cookies(self, cookies):
self.cookies = cookies

def set_body(self, body):
try:
self.set_body_internal(body)
except Exception as e:
logger.debug("Exception occurred whilst setting body: %s", e)
self.body = Context.parse_body_object(body)

def set_body_internal(self, body):
@staticmethod
def parse_body_object(body):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method docstring restates what parse_body_object does; explain why these parsing rules exist (e.g., server quirks, expected input shapes) instead of repeating the mechanics.

Details

✨ AI Reasoning
​The new staticmethod parse_body_object has a docstring that repeats the mechanics implemented in the method rather than providing rationale or design decisions. It doesn't explain why empty byte bodies should become None, why JSON is heuristically detected, or compatibility constraints that motivated this behavior.

πŸ”§ How do I fix it?
Write comments that explain the purpose, reasoning, or business logic behind the code using words like 'because', 'so that', or 'in order to'.

More info - Comment @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.

"""Sets the body and checks if it's possibly JSON"""
self.body = body
if isinstance(self.body, (str, bytes)) and len(body) == 0:
# Make sure that empty bodies like b"" don't get sent.
self.body = None
if isinstance(self.body, bytes):
self.body = self.body.decode("utf-8") # Decode byte input to string.
if not isinstance(self.body, str):
return
if self.body.strip()[0] in ["{", "[", '"']:
# Might be JSON, but might not have been parsed correctly by server because of wrong headers
parsed_body = json.loads(self.body)
if parsed_body:
self.body = parsed_body
try:
if isinstance(body, (str, bytes)) and len(body) == 0:
# Make sure that empty bodies like b"" don't get sent.
return None
if isinstance(body, bytes):
body = body.decode("utf-8") # Decode byte input to string.
Copy link

@aikido-pr-checks aikido-pr-checks bot Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_body_object reassigns parameter 'body' (e.g., body = body.decode(...)). Assign decode result to a new local variable to preserve the original argument.

Details

✨ AI Reasoning
​A newly added static method transforms its input parameter by decoding bytes into a string and reassigning the same local parameter name. Reassigning a function parameter can obscure the original value and its type; here the method both decodes and later parses the same parameter, which may confuse readers and complicate debugging. The pattern is normalization (decoding) before use, but reassigning the parameter rather than assigning to a new local variable reduces clarity.

πŸ”§ How do I fix it?
Create new local variables instead of reassigning parameters. Use different variable names to clearly distinguish between input and modified values.

More info - Comment @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.

if not isinstance(body, str):
return body
if body.strip()[0] in ["{", "[", '"']:
# Might be JSON, but might not have been parsed correctly by server because of wrong headers
parsed_body = json.loads(body)
if parsed_body:
return parsed_body
return body
except Exception as e:
logger.debug("Exception occurred whilst parsing body: %s", e)
return body

def get_route_metadata(self):
"""Returns a route_metadata object"""
Expand Down
11 changes: 4 additions & 7 deletions aikido_zen/ratelimiting/init_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,10 @@ def user():

def create_connection_manager(endpoints=[], bypassed_ips=[]):
cm = MagicMock()
cm.conf = ServiceConfig(
endpoints=endpoints,
last_updated_at=1,
blocked_uids=[],
bypassed_ips=bypassed_ips,
received_any_stats=True,
)
cm.conf = ServiceConfig()
cm.conf.set_endpoints(endpoints)
cm.conf.enable_received_any_stats()
cm.conf.set_bypassed_ips(bypassed_ips)
cm.rate_limiter = RateLimiter(
max_items=5000, time_to_live_in_ms=120 * 60 * 1000 # 120 minutes
)
Expand Down
11 changes: 4 additions & 7 deletions aikido_zen/sources/functions/request_handler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,19 +154,16 @@ def set_context(remote_address, user_agent="", route="/posts/:number"):


def create_service_config():
config = ServiceConfig(
endpoints=[
config = ServiceConfig()
config.set_endpoints(
[
{
"method": "POST",
"route": "/posts/:number",
"graphql": False,
"allowedIPAddresses": ["1.1.1.1", "2.2.2.2", "3.3.3.3"],
}
],
last_updated_at=None,
blocked_uids=set(),
bypassed_ips=[],
received_any_stats=False,
]
)
get_cache().config = config
return config
Expand Down
Loading
Loading