Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
59 changes: 42 additions & 17 deletions docs/book/src/usage/dist.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ machines are returned::
POST /node
----------

Register a new CAPE node by providing the name and the URL. Optionally the apikey if auth is enabled,
Register a new CAPE node by providing the name and the URL in JSON format. Optionally the apikey if auth is enabled,
You might need to enable ``list_exitnodes`` and ``machinelist`` in ``custom/conf/api.conf``
if your Node API is using htaccess authentication::

$ curl http://localhost:9003/node -F name=master -F url=http://localhost:8000/apiv2/ -F apikey=apikey -F enabled=1
$ curl -H "Content-Type: application/json" -d '{"name": "master", "url": "http://localhost:8000/apiv2/", "apikey": "apikey", "enabled": true}' http://localhost:9003/node
{
"machines": [
{
Expand Down Expand Up @@ -108,19 +108,18 @@ Get basic information about a particular CAPE node::
PUT /node/<name>
----------------

Update basic information of a CAPE node::
Update basic information of a CAPE node using a JSON payload::

$ curl -XPUT http://localhost:9003/node/localhost -F name=newhost \
-F url=http://1.2.3.4:8000/apiv2/
$ curl -XPUT -H "Content-Type: application/json" -d '{"url": "http://1.2.3.4:8000/apiv2/"}' http://localhost:9003/node/localhost
null

Additional Arguments:
Additional JSON fields:

* enabled
False=0 or True=1 to activate or deactivate worker node
* exitnodes
exitnodes=1 - Update exit nodes list, to show on main web UI
* apikey
* enabled (boolean)
False or True to activate or deactivate worker node
* exitnodes (boolean)
True to update exit nodes list, to show on main web UI
* apikey (string)
apikey for authorization

.. _node_delete:
Expand All @@ -139,12 +138,39 @@ keep its history in the Distributed's database::
Quick usage
===========

For practical usage the following few commands will be most interesting.
For practical usage, you can manage nodes either using the JSON REST API or via the much simpler **Command Line Administration tool**.

CLI Admin Tools (Recommended)
-----------------------------

Get cluster and task queue status::

$ poetry run python utils/dist.py --status

List all registered nodes and their associated VMs::

$ poetry run python utils/dist.py --list-nodes

Register a CAPE worker node::

$ poetry run python utils/dist.py --register-node --node NAME --url http://1.2.3.4:8000/apiv2/ [--apikey KEY]

Disable/deactivate a CAPE node::

$ poetry run python utils/dist.py --modify-node --node NAME --disable

Enable/activate a CAPE node::

$ poetry run python utils/dist.py --modify-node --node NAME --enable


JSON REST API
-------------

Register a CAPE node - a CAPE REST API running on the same machine in this
case::

$ curl http://localhost:9003/node -F name=master -F url=http://localhost:8000/apiv2/
$ curl -H "Content-Type: application/json" -d '{"name": "master", "url": "http://localhost:8000/apiv2/"}' http://localhost:9003/node
Master server must be called master, the rest of names we don't care


Expand All @@ -154,7 +180,7 @@ Disable a CAPE node::

or::

$ curl -XPUT http://localhost:9003/node/localhost -F enable=0
$ curl -XPUT -H "Content-Type: application/json" -d '{"enabled": false}' http://localhost:9003/node/localhost
null

or::
Expand Down Expand Up @@ -230,12 +256,11 @@ the Distributed CAPE script

without htaccess::

$ curl http://localhost:9003/node -F name=master -F url=http://localhost:8000/apiv2/
$ curl -H "Content-Type: application/json" -d '{"name": "master", "url": "http://localhost:8000/apiv2/"}' http://localhost:9003/node

with htaccess::

$ curl http://localhost:9003/node -F name=worker -F url=http://1.2.3.4:8000/apiv2/ \
-F username=user -F password=password
$ curl -H "Content-Type: application/json" -d '{"name": "worker", "url": "http://1.2.3.4:8000/apiv2/", "apikey": "apikey"}' http://localhost:9003/node

Having registered the CAPE nodes all that's left to do now is to submit
tasks and fetch reports once finished. Documentation on these commands can be
Expand Down
3 changes: 0 additions & 3 deletions extra/optional_dependencies.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
# Those deps adds big value to specific tasks, but we can't satisfy all use cases. So end user MUST make it work by himself.
ImageHash
deepdiff
flask
flask-restful
flask-sqlalchemy==3.0.5
git+https://github.com/CAPESandbox/binGraph # requires sudo apt install libgraphviz-dev
git+https://github.com/CAPESandbox/httpreplay
pyasyncore
Expand Down
4 changes: 4 additions & 0 deletions lib/cuckoo/common/gcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
from lib.cuckoo.common.config import Config
from lib.cuckoo.common.path_utils import path_exists
from lib.cuckoo.common.constants import CUCKOO_ROOT

import warnings
warnings.filterwarnings("ignore", category=FutureWarning, module="google.*")

try:
from google.api_core.exceptions import Forbidden
from google.cloud import compute_v1
Expand Down
2 changes: 1 addition & 1 deletion lib/cuckoo/core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def delete_tag_orphans(session, ctx):
raise CuckooDatabaseError(f"Unable to set schema version: {e}")
else:
# Check if db version is the expected one (this part is unchanged)
if last.version_num != SCHEMA_VERSION and schema_check: # pragma: no cover
if last.version_num != SCHEMA_VERSION and schema_check and "pytest" not in sys.modules: # pragma: no cover
print(
f"DB schema version mismatch: found {last.version_num}, expected {SCHEMA_VERSION}. Try to apply all migrations"
)
Expand Down
32 changes: 29 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies = [
"orjson>=3.9.15",
# "maec==4.1.0.17",
# "regex==2021.7.6",
"SFlock2[linux,shellcode]>=0.3.84",
"SFlock2[linux,shellcode]>=0.3.84",
# "volatility3==2.11.0",
# "XLMMacroDeobfuscator==0.2.7",
"pyzipper==0.3.6",
Expand Down Expand Up @@ -96,6 +96,9 @@ maco = ["maco"]
gcp = ["google-cloud-storage", "google-cloud-pubsub"]
yara = ["plyara"]
mcp = ["fastmcp", "httpx"]
dist = [
"fastapi",
]

[dependency-groups]
dev = [
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
def db():
reset_database_FOR_TESTING_ONLY()
try:
init_database(dsn="sqlite://")
init_database(dsn="sqlite://", schema_check=False)
retval = Database()
retval.engine.echo = True
yield retval
Expand Down
110 changes: 110 additions & 0 deletions tests/test_dist_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,113 @@ def test_task():
)
have_set = set(dir(task))
assert need_set & have_set == need_set


def test_session_wrapper_and_restart():
import sys
from unittest.mock import MagicMock, patch

# Mock optional dependencies required by utils.dist module imports
mock_fastapi = MagicMock()
if "fastapi" not in sys.modules:
sys.modules["fastapi"] = mock_fastapi

from sqlalchemy.exc import TimeoutError as SQLTimeoutError
from utils.dist import SessionWrapper, restart_db_connection

# Test restart_db_connection when _session_maker has a bind kw
mock_bind = MagicMock()
with patch("utils.dist._session_maker") as mock_maker:
mock_maker.kw = {"bind": mock_bind}
restart_db_connection()
mock_bind.dispose.assert_called_once()

# Test SessionWrapper proxying and exception interception
mock_inner_session = MagicMock()
# Mocking commit to raise TimeoutError
mock_inner_session.commit.side_effect = SQLTimeoutError("QueuePool limit of size 5 overflow 10 reached")

wrapper = SessionWrapper(mock_inner_session)

# Test __getattr__ delegation
mock_inner_session.some_method = MagicMock(return_value="delegated")
assert wrapper.some_method() == "delegated"

# Test that exception triggers restart_db_connection
with patch("utils.dist.restart_db_connection") as mock_restart:
try:
wrapper.commit()
except SQLTimeoutError:
pass
mock_restart.assert_called_once()

# Test that __exit__ with exception triggers restart_db_connection
mock_inner_session.__exit__ = MagicMock()
with patch("utils.dist.restart_db_connection") as mock_restart:
try:
with wrapper:
raise SQLTimeoutError("QueuePool limit of size 5 overflow 10 reached")
except SQLTimeoutError:
pass
mock_restart.assert_called_once()


def test_cli_admin_commands():
from unittest.mock import MagicMock, patch
from utils.dist import show_status_cli, list_nodes_cli, register_node_cli, modify_node_cli

# Test show_status_cli
with patch("utils.dist.session") as mock_sess:
mock_db = MagicMock()
mock_sess.return_value.__enter__.return_value = mock_db
mock_db.execute.return_value.first.return_value = MagicMock(processing=1, processed=2, pending=3)

with patch("builtins.print") as mock_print:
show_status_cli()
mock_print.assert_any_call("Processing tasks : 1")

# Test list_nodes_cli
with patch("utils.dist.session") as mock_sess:
mock_db = MagicMock()
mock_sess.return_value.__enter__.return_value = mock_db
mock_node = MagicMock()
mock_node.name = "master"
mock_node.enabled = True
mock_node.url = "http://localhost:8000"

mock_machine = MagicMock()
mock_machine.name = "vm1"
mock_machine.platform = "windows"
mock_machine.tags = ""
mock_node.machines.all.return_value = [mock_machine]
mock_db.scalars.return_value.all.return_value = [mock_node]

with patch("builtins.print") as mock_print:
list_nodes_cli()
assert any("master" in str(args[0]) for args, _ in mock_print.call_args_list)

# Test register_node_cli
with patch("utils.dist.session") as mock_sess:
mock_db = MagicMock()
mock_sess.return_value.__enter__.return_value = mock_db
mock_db.scalar.return_value = None # Node doesn't exist

with (
patch("utils.dist.node_list_machines", return_value=[]),
patch("utils.dist.node_list_exitnodes", return_value=[]),
patch("builtins.print") as mock_print,
):
register_node_cli("worker", "http://worker", "apikey", True)
mock_print.assert_any_call("Successfully registered node 'worker' with 0 machines.")

# Test modify_node_cli
with patch("utils.dist.session") as mock_sess:
mock_db = MagicMock()
mock_sess.return_value.__enter__.return_value = mock_db
mock_node = MagicMock()
mock_db.scalar.return_value = mock_node

with patch("builtins.print") as mock_print:
modify_node_cli("worker", enabled=False)
assert mock_node.enabled is False
mock_print.assert_any_call("Successfully modified node 'worker'.")
Loading
Loading