Skip to content

Python SCA: Report packages #402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
2 changes: 2 additions & 0 deletions aikido_zen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def protect(mode="daemon"):
if mode == "daemon_disabled":
logger.debug("Not starting the background process, daemon disabled.")

import aikido_zen.sinks.builtins_import

# Import sources
import aikido_zen.sources.django
import aikido_zen.sources.flask
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Exports the send_heartbeat function"""

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

Expand All @@ -16,12 +17,15 @@ def send_heartbeat(connection_manager):
routes = list(connection_manager.routes)
outgoing_domains = connection_manager.hostnames.as_array()
ai_stats = connection_manager.ai_stats.get_stats()
packages = PackagesStore.export()

connection_manager.statistics.clear()
connection_manager.users.clear()
connection_manager.routes.clear()
connection_manager.hostnames.clear()
connection_manager.ai_stats.clear()
PackagesStore.clear()

res = connection_manager.api.report(
connection_manager.token,
{
Expand All @@ -31,6 +35,7 @@ def send_heartbeat(connection_manager):
"stats": stats,
"ai": ai_stats,
"hostnames": outgoing_domains,
"packages": packages,
"routes": routes,
"users": users,
"middlewareInstalled": connection_manager.middleware_installed,
Expand Down
4 changes: 4 additions & 0 deletions aikido_zen/background_process/commands/sync_data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Exports process_renew_config"""

from aikido_zen.api_discovery.update_route_info import update_route_info
from aikido_zen.background_process.packages import PackagesStore
from aikido_zen.helpers.logging import logger


Expand Down Expand Up @@ -49,6 +50,9 @@ def process_sync_data(connection_manager, data, conn, queue=None):
# Sync ai stats
connection_manager.ai_stats.import_list(data.get("ai_stats", []))

# Sync packages
PackagesStore.import_list(data.get("packages", []))

if connection_manager.conf.last_updated_at > 0:
# Only report data if the config has been fetched.
return {
Expand Down
35 changes: 31 additions & 4 deletions aikido_zen/background_process/packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import importlib.metadata as importlib_metadata

from packaging.version import Version

from aikido_zen.helpers.get_current_unixtime_ms import get_unixtime_ms
from aikido_zen.helpers.logging import logger

# If any version is supported, this constant can be used
Expand Down Expand Up @@ -57,16 +59,24 @@ def is_version_supported(version, required_version):

class PackagesStore:
@staticmethod
def get_packages():
def export():
global packages
return packages
result = []
for package in packages.values():
if package.get("cleared", False):
continue
result.append(dict(package))
return result

@staticmethod
def add_package(package, version, supported):
def add_package(package, version, supported=None):
global packages
packages[package] = {
"name": package,
"version": version,
"supported": bool(supported),
"requiredAt": get_unixtime_ms(),
"supported": supported,
"cleared": False,
}

@staticmethod
Expand All @@ -75,3 +85,20 @@ def get_package(package_name):
if package_name in packages:
return packages[package_name]
return None

@staticmethod
def clear():
# To clear we set the `cleared` attribute to True
# This is to ensure you can still get the packages
# But that they will not show up during an export
global packages
for package in packages:
packages[package]["cleared"] = True

@staticmethod
def import_list(imported_packages):
for package in imported_packages:
if PackagesStore.get_package(package["name"]):
continue
global packages
packages[package["name"]] = package
42 changes: 42 additions & 0 deletions aikido_zen/sinks/builtins_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import importlib.metadata
from importlib.metadata import PackageNotFoundError

from aikido_zen.background_process.packages import PackagesStore
from aikido_zen.sinks import on_import, patch_function, after


@after
def _import(func, instance, args, kwargs, return_value):
if not hasattr(return_value, "__file__"):
return # Would be built-in into the interpreter (system package)

if not hasattr(return_value, "__package__"):
return
name = getattr(return_value, "__package__")

if not name or "." in name:
# Make sure the name exists and that it's not a submodule
return
if name == "importlib_metadata":
# Avoid circular dependencies
return

if PackagesStore.get_package(name):
return

version = None
try:
version = importlib.metadata.version(name)
except PackageNotFoundError:
pass
if version:
PackagesStore.add_package(name, version)


@on_import("builtins")
def patch(m):
"""
patching module builtins
- patches builtins.__import__
"""
patch_function(m, "__import__", _import)
3 changes: 3 additions & 0 deletions aikido_zen/thread/thread_cache.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Exports class ThreadConfig"""

import aikido_zen.background_process.comms as comms
from aikido_zen.background_process.packages import PackagesStore
from aikido_zen.background_process.routes import Routes
from aikido_zen.background_process.service_config import ServiceConfig
from aikido_zen.storage.ai_statistics import AIStatistics
Expand Down Expand Up @@ -48,6 +49,7 @@ def reset(self):
self.users.clear()
self.stats.clear()
self.ai_stats.clear()
PackagesStore.clear()

def renew(self):
if not comms.get_comms():
Expand All @@ -63,6 +65,7 @@ def renew(self):
"users": self.users.as_array(),
"stats": self.stats.get_record(),
"ai_stats": self.ai_stats.get_stats(),
"packages": PackagesStore.export(),
},
receive=True,
)
Expand Down
4 changes: 4 additions & 0 deletions aikido_zen/thread/thread_cache_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ def test_renew_called_with_correct_args(mock_get_comms, thread_cache: ThreadCach
"middleware_installed": False,
"hostnames": [],
"users": [],
"packages": [],
},
receive=True,
)
Expand Down Expand Up @@ -358,6 +359,7 @@ def test_sync_data_for_users(mock_get_comms, thread_cache: ThreadCache):
"middleware_installed": False,
"hostnames": [],
"ai_stats": [],
"packages": [],
"users": [
{
"id": "123",
Expand Down Expand Up @@ -410,6 +412,7 @@ def test_renew_called_with_empty_routes(mock_get_comms, thread_cache: ThreadCach
"hostnames": [],
"users": [],
"ai_stats": [],
"packages": [],
},
receive=True,
)
Expand Down Expand Up @@ -449,6 +452,7 @@ def test_renew_called_with_no_requests(mock_get_comms, thread_cache: ThreadCache
"hostnames": [],
"users": [],
"ai_stats": [],
"packages": [],
},
receive=True,
)
Loading