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
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### [22.08.2026]
* Performance & Database Infrastructure:
* **psycopg3 Support**: Upgraded the PostgreSQL database connection driver to `psycopg` (v3) for modern async capability and massive performance gains.
* **In-Memory Connection Upgrader**: Added a seamless backward-compatibility layer in `lib/cuckoo/core/database.py`. If `postgresql://` is used with psycopg v3 installed, CAPEv2 automatically and transparently upgrades it in-memory to use the `postgresql+psycopg://` driver, preventing any startup `ImportError` or configuration crashes!

### [31.07.2026]
* Remus detection & dynamic config extraction

Expand Down
2 changes: 1 addition & 1 deletion conf/default/cuckoo.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ sort_pcap = on
# Specify the database connection string.
# Examples, see documentation for more:
# sqlite:///foo.db
# postgresql://foo:bar@localhost:5432/mydatabase
# postgresql+psycopg://foo:bar@localhost:5432/mydatabase
# mysql://foo:bar@localhost/mydatabase
# If empty, default is a SQLite in db/cuckoo.db.
# SQLite doens't support database upgrades!
Expand Down
51 changes: 50 additions & 1 deletion lib/cuckoo/common/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

import contextlib
from typing import Dict

from lib.cuckoo.common.config import Config

repconf = Config("reporting")

mongo_find_one = None
if repconf.mongodb.enabled:
from dev_utils.mongodb import mongo_find_one


if repconf.elasticsearchdb.enabled:
from dev_utils.elasticsearchdb import get_analysis_index, get_calls_index, get_query_by_info_id

Expand Down Expand Up @@ -84,6 +85,8 @@ def helper_percentages_mongo(tid1, tid2, ignore_categories: set = None, filter1=


def helper_summary_mongo(tid1, tid2, filter1=None, filter2=None):
if not mongo_find_one:
return {}
# filter1/filter2: caller-supplied central-mode-scoped filters (see helper_percentages_mongo); None
# -> bare {info.id}. Prevents a colliding tenant doc leaking its behavior.summary into compare/both.
left_sum, right_sum = None, None
Expand Down Expand Up @@ -145,3 +148,49 @@ def get_similar_summary(left_sum, right_sum):
ret[summary].append(item)

return ret


def helper_different_summary_mongo(tid1, tid2, filter1=None, filter2=None):
if not mongo_find_one:
return {}
left_sum, right_sum = None, None
left_sum = mongo_find_one("analysis", filter1 or {"info.id": int(tid1)}, {"behavior.summary": 1})
right_sum = mongo_find_one("analysis", filter2 or {"info.id": int(tid2)}, {"behavior.summary": 1})
return get_different_summary(left_sum, right_sum) if left_sum and right_sum else {}


def helper_different_summary_elastic(es_obj, tid1, tid2):
left_sum, right_sum = None, None
buf = es_obj.search(index=get_analysis_index(), query=get_query_by_info_id(tid1))["hits"]["hits"]
if buf:
left_sum = buf[-1]["_source"]

buf = es_obj.search(index=get_analysis_index(), query=get_query_by_info_id(tid2))["hits"]["hits"]
if buf:
right_sum = buf[-1]["_source"]

return get_different_summary(left_sum, right_sum) if left_sum and right_sum else {}


def get_different_summary(left_sum, right_sum):
ret = {}

left_behavior = left_sum.get("behavior", {}) or {}
left_summary_dict = left_behavior.get("summary", {}) or {}

right_behavior = right_sum.get("behavior", {}) or {}
right_summary_dict = right_behavior.get("summary", {}) or {}

for summary, left_items in left_summary_dict.items():
right_items = set(right_summary_dict.get(summary, []))
for item in left_items:
if item not in right_items:
ret.setdefault(summary, {}).setdefault("left_only", []).append(item)

for summary, right_items in right_summary_dict.items():
left_items = set(left_summary_dict.get(summary, []))
for item in right_items:
if item not in left_items:
ret.setdefault(summary, {}).setdefault("right_only", []).append(item)

return ret
2 changes: 2 additions & 0 deletions lib/cuckoo/common/web_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,8 @@ def perform_search(
# Join with the analysis collection
{"$lookup": {"from": "analysis", "localField": "_id", "foreignField": "info.id", "as": "task_doc"}},
{"$unwind": "$task_doc"},
# Stage 8: Make the task doc the new root (type check to prevent $replaceRoot crashes)
{"$match": {"task_doc": {"$type": "object"}}},
{"$replaceRoot": {"newRoot": "$task_doc"}},
]

Expand Down
8 changes: 8 additions & 0 deletions lib/cuckoo/core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ def _connect_database(self, connection_string):
"""Connect to a Database.
@param connection_string: Connection string specifying the database
"""
# Auto-upgrade connection string to postgresql+psycopg if postgresql:// is used with psycopg v3 installed
if connection_string.startswith("postgresql://"):
try:
import psycopg # noqa: F401
connection_string = connection_string.replace("postgresql://", "postgresql+psycopg://", 1)
except ImportError:
pass

url = make_url(connection_string)
engine_args = {}

Expand Down
Loading
Loading