From efd2c5aa7062edffc2bbd10b75e575d221733339 Mon Sep 17 00:00:00 2001 From: doomedraven Date: Tue, 24 Feb 2026 08:15:15 +0100 Subject: [PATCH 1/7] Support lean MCP reports; move server & docs Introduce a lightweight "lean" CAPE report flow and update MCP server location. Added mcp/filters.py (lean_search_filters) and mcp/__init__.py to expose configurable projection filters. Renamed web/mcp_server.py -> mcp/server.py and added get_lean_cape_report/_apply_lean_report plus lean flags to search_task, extended_search and task report handling to return reduced payloads for LLM/agent consumption. Adjusted docs (usage/mcp.rst) to reference the new module path and use Poetry (poetry run python mcp/server.py). Fixed perform_search usage in lib/cuckoo/common/web_utils.py to honor an optional projection argument and to compute ES _source fields from projection when provided. Updated web/apiv2/views.py to import and apply the lean projection in search and report endpoints. --- docs/book/src/usage/mcp.rst | 14 +++---- lib/cuckoo/common/web_utils.py | 4 +- mcp/__init__.py | 0 mcp/filters.py | 31 +++++++++++++++ web/mcp_server.py => mcp/server.py | 62 ++++++++++++++++++++++++++++-- web/apiv2/views.py | 7 +++- 6 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 mcp/__init__.py create mode 100644 mcp/filters.py rename web/mcp_server.py => mcp/server.py (88%) diff --git a/docs/book/src/usage/mcp.rst b/docs/book/src/usage/mcp.rst index 36c0322c7b3..0c1cae6866d 100644 --- a/docs/book/src/usage/mcp.rst +++ b/docs/book/src/usage/mcp.rst @@ -118,7 +118,7 @@ Scenario B: Remote / Shared Server (SSE) In this mode, a single MCP server instance runs continuously and accepts connections from multiple clients over the network. -0. **Execution:** Start the server using ``python3 web/mcp_server.py --transport sse``. +0. **Execution:** Start the server using ``poetry run python mcp/server.py --transport sse``. 1. **Configuration:** Start the server **without** a ``CAPE_API_TOKEN`` environment variable. 2. **Strict Mode:** Ensure ``token_auth_enabled = yes`` is set in ``conf/api.conf``. 3. **Usage:** Users **must** provide their API token in the ``token`` argument for every tool call (e.g., ``submit_file(..., token="MyKey")``). @@ -142,7 +142,7 @@ Standard execution (Stdio) .. code-block:: bash - CAPE_API_URL=http://your-cape-ip:8000/apiv2 CAPE_API_TOKEN=your_token python3 web/mcp_server.py + CAPE_API_URL=http://your-cape-ip:8000/apiv2 CAPE_API_TOKEN=your_token poetry run python mcp/server.py Remote / SSE execution ~~~~~~~~~~~~~~~~~~~~~~ @@ -151,7 +151,7 @@ To run the server as a persistent service accessible over the network: .. code-block:: bash - python3 web/mcp_server.py --transport sse --port 9004 + poetry run python mcp/server.py --transport sse --port 9004 Deployment behind Nginx ~~~~~~~~~~~~~~~~~~~~~~~ @@ -192,7 +192,7 @@ Add the following to your ``claude_desktop_config.json``: "mcpServers": { "cape": { "command": "poetry", - "args": ["run", "python", "/opt/CAPEv2/web/mcp_server.py"], + "args": ["run", "python", "/opt/CAPEv2/mcp/server.py"], "env": { "CAPE_API_URL": "http://127.0.0.1:8000/apiv2", "CAPE_API_TOKEN": "YOUR_API_TOKEN_HERE", @@ -209,7 +209,7 @@ You can add the server using the CLI command: .. code-block:: bash - gemini mcp add cape poetry run python /opt/CAPEv2/web/mcp_server.py \ + gemini mcp add cape poetry run python /opt/CAPEv2/mcp/server.py \ -e CAPE_API_URL=http://127.0.0.1:8000/apiv2 \ -e CAPE_API_TOKEN=YOUR_API_TOKEN_HERE \ -e CAPE_ALLOWED_SUBMISSION_DIR=/home/user/samples @@ -222,7 +222,7 @@ Or manually add it to your ``~/.gemini/settings.json``: "mcpServers": { "cape": { "command": "poetry", - "args": ["run", "python", "/opt/CAPEv2/web/mcp_server.py"], + "args": ["run", "python", "/opt/CAPEv2/mcp/server.py"], "env": { "CAPE_API_URL": "http://127.0.0.1:8000/apiv2", "CAPE_API_TOKEN": "YOUR_API_TOKEN_HERE", @@ -243,7 +243,7 @@ Open **Agent Panel** -> **...** -> **MCP Servers** -> **Manage MCP Servers** -> "mcpServers": { "cape": { "command": "poetry", - "args": ["run", "python", "/opt/CAPEv2/web/mcp_server.py"], + "args": ["run", "python", "/opt/CAPEv2/mcp/server.py"], "env": { "CAPE_API_URL": "http://127.0.0.1:8000/apiv2", "CAPE_API_TOKEN": "YOUR_API_TOKEN_HERE", diff --git a/lib/cuckoo/common/web_utils.py b/lib/cuckoo/common/web_utils.py index 5755283b553..73d506aea21 100644 --- a/lib/cuckoo/common/web_utils.py +++ b/lib/cuckoo/common/web_utils.py @@ -1422,7 +1422,7 @@ def perform_search( # Stage 8: Make the task doc the new root {"$replaceRoot": {"newRoot": "$task_doc"}}, # Stage 9: Add your custom projection - {"$project": perform_search_filters}, + {"$project": projection or perform_search_filters}, ] retval = list(mongo_aggregate(FILES_COLL, pipeline)) if not retval: @@ -1453,7 +1453,7 @@ def perform_search( return retval if es_as_db: - _source_fields = list(perform_search_filters.keys())[:-1] + _source_fields = list((projection or perform_search_filters).keys())[:-1] if isinstance(search_term_map[term], str): q = {"query": {"match": {search_term_map[term]: value}}} return [d["_source"] for d in es.search(index=get_analysis_index(), body=q, _source=_source_fields)["hits"]["hits"]] diff --git a/mcp/__init__.py b/mcp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mcp/filters.py b/mcp/filters.py new file mode 100644 index 00000000000..faa36ef074a --- /dev/null +++ b/mcp/filters.py @@ -0,0 +1,31 @@ +# Configuration for MCP server search filters +# You can modify this dictionary to include or exclude specific fields in the lean report +# Injested by Agents to give a quick overview + +lean_search_filters = { + "info": 1, + "virustotal_summary": 1, + "detections.family": 1, + "malfamily": 1, + "malfamily_tag": 1, + "malscore": 1, + "network.pcap_sha256": 1, + "network.domains.domain": 1, + "network.http.uri": 1, + "signatures.name": 1, + "signatures.description": 1, + "signatures.severity": 1, + "CAPE": 1, + "behavior.summary.mutexes": 1, + "behavior.summary.executed_commands": 1, + "mlist_cnt": 1, + "f_mlist_cnt": 1, + "target.file.clamav": 1, + "target.file.sha256": 1, + "suri_tls_cnt": 1, + "suri_alert_cnt": 1, + "suri_http_cnt": 1, + "suri_file_cnt": 1, + "trid": 1, + "_id": 0, +} diff --git a/web/mcp_server.py b/mcp/server.py similarity index 88% rename from web/mcp_server.py rename to mcp/server.py index 2d084d43dd1..de2a124bee2 100644 --- a/web/mcp_server.py +++ b/mcp/server.py @@ -30,7 +30,7 @@ api_config = Config("api") # Configuration from Environment or Config File -# Run with: CAPE_API_URL=http://127.0.0.1:8000/apiv2 CAPE_API_TOKEN=your_token python3 web/mcp_server.py +# Run with: CAPE_API_URL=http://127.0.0.1:8000/apiv2 CAPE_API_TOKEN=your_token poetry run python mcp/server.py API_URL = os.environ.get("CAPE_API_URL") if not API_URL: # Try to get from api.conf [api] url @@ -344,8 +344,42 @@ async def submit_static( # --- Task Management & Search --- +def get_lean_cape_report(raw_cape_json): + """Filters a 50MB CAPE report down to a 500-token LLM payload.""" + return { + "score": raw_cape_json.get("info", {}).get("score", 0), + "family": raw_cape_json.get("malfamily", "Unknown") if "malfamily" in raw_cape_json else raw_cape_json.get("detections", {}).get("family", "Unknown"), + "extracted_configs": raw_cape_json.get("CAPE", []), + "high_severity_signatures": [ + {"name": sig["name"], "desc": sig["description"]} + for sig in raw_cape_json.get("signatures", []) + if isinstance(sig, dict) and sig.get("severity", 0) >= 3 + ], + "network": { + "domains": [d["domain"] for d in raw_cape_json.get("network", {}).get("domains", [])] if isinstance(raw_cape_json.get("network", {}).get("domains"), list) else [], + "http_uris": [h["uri"] for h in raw_cape_json.get("network", {}).get("http", [])] if isinstance(raw_cape_json.get("network", {}).get("http"), list) else [], + }, + "indicators": { + "mutexes": raw_cape_json.get("behavior", {}).get("summary", {}).get("mutexes", []) if isinstance(raw_cape_json.get("behavior", {}).get("summary"), dict) else [], + "commands": raw_cape_json.get("behavior", {}).get("summary", {}).get("executed_commands", []) if isinstance(raw_cape_json.get("behavior", {}).get("summary"), dict) else [] + } + } + +def _apply_lean_report(result): + if isinstance(result, dict): + if result.get("error") is False and "data" in result: + if isinstance(result["data"], list): + result["data"] = [get_lean_cape_report(item) for item in result["data"]] + elif isinstance(result["data"], dict): + result["data"] = get_lean_cape_report(result["data"]) + elif "info" in result: + return get_lean_cape_report(result) + elif isinstance(result, list): + return [get_lean_cape_report(item) for item in result] + return result + @mcp_tool("tasksearch") -async def search_task(hash_value: str, token: str = "") -> str: +async def search_task(hash_value: str, lean: bool = True, token: str = "") -> str: """Search for tasks by MD5, SHA1, or SHA256.""" algo = "md5" if len(hash_value) == 40: @@ -354,16 +388,22 @@ async def search_task(hash_value: str, token: str = "") -> str: algo = "sha256" result = await _request("GET", f"tasks/search/{algo}/{hash_value}/", token=token) + if lean: + result = _apply_lean_report(result) return json.dumps(result, indent=2) @mcp_tool("extendedtasksearch") -async def extended_search(option: str, argument: str, token: str = "") -> str: +async def extended_search(option: str, argument: str, lean: bool = True, token: str = "") -> str: """ Search tasks using extended options. Options include: id, name, type, string, ssdeep, crc32, file, command, resolvedapi, key, mutex, domain, ip, signature, signame, etc. """ data = {"option": option, "argument": argument} + if lean: + data["lean"] = True result = await _request("POST", "tasks/extendedsearch/", token=token, data=data) + if lean: + result = _apply_lean_report(result) return json.dumps(result, indent=2) @mcp_tool("extendedtasksearch") @@ -430,7 +470,21 @@ async def get_statistics(days: int = 7, token: str = "") -> str: @mcp_tool("taskreport") async def get_task_report(task_id: int, format: str = "json", token: str = "") -> str: - """Get the analysis report for a task (json, lite, maec, metadata).""" + """Get the analysis report for a task (json, lite, maec, metadata, lean).""" + if format == "lean": + data = {"option": "id", "argument": str(task_id), "lean": True} + result = await _request("POST", "tasks/extendedsearch/", token=token, data=data) + + # Extract the single task report from the search results + if isinstance(result, dict) and not result.get("error") and isinstance(result.get("data"), list): + if len(result["data"]) > 0: + result["data"] = result["data"][0] + else: + result = {"error": True, "message": "Task report not found via lean search."} + + result = _apply_lean_report(result) + return json.dumps(result, indent=2) + result = await _request("GET", f"tasks/get/report/{task_id}/{format}/", token=token) return json.dumps(result, indent=2) diff --git a/web/apiv2/views.py b/web/apiv2/views.py index cf8fba6f818..d639487cc91 100644 --- a/web/apiv2/views.py +++ b/web/apiv2/views.py @@ -53,6 +53,10 @@ statistics, validate_task, ) +try: + from mcp_filters import lean_search_filters +except ImportError: + from mcp.filters import lean_search_filters from lib.cuckoo.core.database import Database, _Database from lib.cuckoo.core.data.task import ( TASK_RECOVERED, @@ -756,7 +760,8 @@ def ext_tasks_search(request): value = tmp_value del tmp_value try: - records = perform_search(term, value, user_id=request.user.id, privs=request.user.is_staff, web=False) + projection = lean_search_filters if request.data.get("lean") else None + records = perform_search(term, value, user_id=request.user.id, privs=request.user.is_staff, web=False, projection=projection) except ValueError: if not term: resp = {"error": True, "error_value": "No option provided."} From 76e285179768e5a2e1ba1d0d8d791ea78ea6a977 Mon Sep 17 00:00:00 2001 From: doomedraven Date: Tue, 24 Feb 2026 08:21:13 +0100 Subject: [PATCH 2/7] Update server.py --- mcp/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcp/server.py b/mcp/server.py index de2a124bee2..46563a1fd16 100644 --- a/mcp/server.py +++ b/mcp/server.py @@ -474,14 +474,14 @@ async def get_task_report(task_id: int, format: str = "json", token: str = "") - if format == "lean": data = {"option": "id", "argument": str(task_id), "lean": True} result = await _request("POST", "tasks/extendedsearch/", token=token, data=data) - + # Extract the single task report from the search results if isinstance(result, dict) and not result.get("error") and isinstance(result.get("data"), list): if len(result["data"]) > 0: result["data"] = result["data"][0] else: result = {"error": True, "message": "Task report not found via lean search."} - + result = _apply_lean_report(result) return json.dumps(result, indent=2) From eedea4ee17fb40c6b489829f9c5a8b3613abb55b Mon Sep 17 00:00:00 2001 From: doomedraven Date: Tue, 24 Feb 2026 08:27:19 +0100 Subject: [PATCH 3/7] Update mcp/server.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- mcp/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp/server.py b/mcp/server.py index 46563a1fd16..6fc24996df8 100644 --- a/mcp/server.py +++ b/mcp/server.py @@ -348,7 +348,7 @@ def get_lean_cape_report(raw_cape_json): """Filters a 50MB CAPE report down to a 500-token LLM payload.""" return { "score": raw_cape_json.get("info", {}).get("score", 0), - "family": raw_cape_json.get("malfamily", "Unknown") if "malfamily" in raw_cape_json else raw_cape_json.get("detections", {}).get("family", "Unknown"), + "family": raw_cape_json.get("malfamily") or raw_cape_json.get("detections", {}).get("family") or "Unknown", "extracted_configs": raw_cape_json.get("CAPE", []), "high_severity_signatures": [ {"name": sig["name"], "desc": sig["description"]} From 8ce1c9cd3a2e4fb171f617517c7a090ad0fa8dbc Mon Sep 17 00:00:00 2001 From: doomedraven Date: Tue, 24 Feb 2026 09:11:55 +0100 Subject: [PATCH 4/7] sync --- conf/default/web.conf.default | 8 +++-- lib/cuckoo/common/web_utils.py | 63 ++++++++++++++++++++++++++++++++-- mcp/server.py | 11 ++++++ web/apiv2/views.py | 5 +-- 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/conf/default/web.conf.default b/conf/default/web.conf.default index 001b8f55222..60eb295fb43 100644 --- a/conf/default/web.conf.default +++ b/conf/default/web.conf.default @@ -123,6 +123,8 @@ package = edge # TLP markings on submission and webgui [tlp] enabled = no +# Should TLP: RED tasks be searchable by other users? +public_red = yes #AMSI dump submission checkbox: can be useful to disable if no Win10+ instances #(amsidump is enabled by default in the monitor for Win10+) @@ -197,7 +199,7 @@ vnc_host = localhost # You might need to add your server IP to ALLOWED_HOSTS in web/web/settings.py if it not ["*""] # vnc or rdp guest_protocol = vnc -# TIP: For KVM/QEMU, using 'qxl' or 'virtio' video drivers in your VM XML +# TIP: For KVM/QEMU, using 'qxl' or 'virtio' video drivers in your VM XML # definition provides much better VNC performance than 'vga' or 'cirrus'. guacd_recording_path = /opt/CAPEv2/storage/guacrecordings guest_width = 1280 @@ -215,7 +217,7 @@ rdp_enable_menu_animations = no # VNC Performance Optimizations # Color depth: 8, 16, 24, 32. 16 is a great balance for performance. vnc_color_depth = 16 -# Cursor: 'local' renders the mouse on your browser (feels instant). +# Cursor: 'local' renders the mouse on your browser (feels instant). # 'remote' waits for the server (feels laggy). vnc_cursor = local # Audio (enable only if needed, consumes bandwidth) @@ -248,4 +250,4 @@ enabled = no enabled = no [audit_framework] -enabled = no \ No newline at end of file +enabled = no diff --git a/lib/cuckoo/common/web_utils.py b/lib/cuckoo/common/web_utils.py index 73d506aea21..c1fcecf71e0 100644 --- a/lib/cuckoo/common/web_utils.py +++ b/lib/cuckoo/common/web_utils.py @@ -1328,6 +1328,23 @@ def perform_search( """ if repconf.mongodb.enabled and repconf.elasticsearchdb.enabled and essearch and not term: multi_match_search = {"query": {"multi_match": {"query": value, "fields": ["*"]}}} + if user_id and not privs: + user_filter = None + if force_bool(web_cfg.general.get("public_searches", True)): + if not force_bool(web_cfg.tlp.get("public_red", False)): + user_filter = { + "bool": { + "should": [ + {"bool": {"must_not": [{"terms": {"info.tlp": ["red", "Red", "RED"]}}]}}, + {"term": {"info.user_id": user_id}} + ], + "minimum_should_match": 1 + } + } + else: + user_filter = {"term": {"info.user_id": user_id}} + if user_filter: + multi_match_search = {"query": {"bool": {"must": [{"multi_match": {"query": value, "fields": ["*"]}}], "filter": [user_filter]}}} numhits = es.search(index=get_analysis_index(), body=multi_match_search, size=0)["hits"]["total"] return [ d["_source"] @@ -1421,9 +1438,17 @@ def perform_search( {"$unwind": "$task_doc"}, # Stage 8: Make the task doc the new root {"$replaceRoot": {"newRoot": "$task_doc"}}, - # Stage 9: Add your custom projection - {"$project": projection or perform_search_filters}, ] + + if user_id and not privs: + if force_bool(web_cfg.general.get("public_searches", True)): + if not force_bool(web_cfg.tlp.get("public_red", False)): + pipeline.append({"$match": {"$or": [{"info.tlp": {"$nin": ["red", "Red", "RED"]}}, {"info.user_id": user_id}]}}) + else: + pipeline.append({"$match": {"info.user_id": user_id}}) + + # Stage 9: Add your custom projection pipeline.append({"$project": projection or perform_search_filters}) + retval = list(mongo_aggregate(FILES_COLL, pipeline)) if not retval: return [] @@ -1444,6 +1469,19 @@ def perform_search( projection[f"target.file.{FILE_REF_KEY}"] = 1 if term in search_term_map_repetetive_blocks: mongo_search_query = {"$or": [{path: condition} for path, condition in mongo_search_query.items()]} + + if user_id and not privs: + if force_bool(web_cfg.general.get("public_searches", True)): + if not force_bool(web_cfg.tlp.get("public_red", False)): + mongo_search_query = { + "$and": [ + mongo_search_query, + {"$or": [{"info.tlp": {"$nin": ["red", "Red", "RED"]}}, {"info.user_id": user_id}]} + ] + } + else: + mongo_search_query["info.user_id"] = user_id + retval = list(mongo_find("analysis", mongo_search_query, projection, limit=search_limit)) for doc in retval: @@ -1454,12 +1492,33 @@ def perform_search( if es_as_db: _source_fields = list((projection or perform_search_filters).keys())[:-1] + + user_filter = None + if user_id and not privs: + if force_bool(web_cfg.general.get("public_searches", True)): + if not force_bool(web_cfg.tlp.get("public_red", False)): + user_filter = { + "bool": { + "should": [ + {"bool": {"must_not": [{"terms": {"info.tlp": ["red", "Red", "RED"]}}]}}, + {"term": {"info.user_id": user_id}} + ], + "minimum_should_match": 1 + } + } + else: + user_filter = {"term": {"info.user_id": user_id}} + if isinstance(search_term_map[term], str): q = {"query": {"match": {search_term_map[term]: value}}} + if user_filter: + q = {"query": {"bool": {"must": [q["query"]], "filter": [user_filter]}}} return [d["_source"] for d in es.search(index=get_analysis_index(), body=q, _source=_source_fields)["hits"]["hits"]] else: queries = [{"match": {search_term: value}} for search_term in search_term_map[term]] q = {"query": {"bool": {"should": queries, "minimum_should_match": 1}}} + if user_filter: + q["query"]["bool"]["filter"] = [user_filter] return [d["_source"] for d in es.search(index=get_analysis_index(), body=q, _source=_source_fields)["hits"]["hits"]] diff --git a/mcp/server.py b/mcp/server.py index 6fc24996df8..fd5f7d41699 100644 --- a/mcp/server.py +++ b/mcp/server.py @@ -381,6 +381,9 @@ def _apply_lean_report(result): @mcp_tool("tasksearch") async def search_task(hash_value: str, lean: bool = True, token: str = "") -> str: """Search for tasks by MD5, SHA1, or SHA256.""" + if not re.match(r"^[a-fA-F0-9]+$", hash_value): + return json.dumps({"error": True, "message": "Invalid hash value provided. Only hexadecimal characters are allowed."}, indent=2) + algo = "md5" if len(hash_value) == 40: algo = "sha1" @@ -471,6 +474,10 @@ async def get_statistics(days: int = 7, token: str = "") -> str: @mcp_tool("taskreport") async def get_task_report(task_id: int, format: str = "json", token: str = "") -> str: """Get the analysis report for a task (json, lite, maec, metadata, lean).""" + allowed_formats = {"json", "lite", "maec", "metadata", "lean"} + if format not in allowed_formats: + return json.dumps({"error": True, "message": f"Invalid format provided. Allowed formats: {', '.join(allowed_formats)}"}, indent=2) + if format == "lean": data = {"option": "id", "argument": str(task_id), "lean": True} result = await _request("POST", "tasks/extendedsearch/", token=token, data=data) @@ -570,11 +577,15 @@ async def download_task_fullmemory(task_id: int, destination: str, token: str = @mcp_tool("fileview") async def view_file(hash_value: str, hash_type: str = "sha256", token: str = "") -> str: """View information about a file in the database.""" + if not re.match(r"^[a-fA-F0-9]+$", hash_value): + return json.dumps({"error": True, "message": "Invalid hash value provided. Only hexadecimal characters are allowed."}, indent=2) return await _request("GET", f"files/view/{hash_type}/{hash_value}/", token=token) @mcp_tool("sampledl") async def download_sample(hash_value: str, destination: str, hash_type: str = "sha256", token: str = "") -> str: """Download a sample from the database.""" + if not re.match(r"^[a-fA-F0-9]+$", hash_value): + return json.dumps({"error": True, "message": "Invalid hash value provided. Only hexadecimal characters are allowed."}, indent=2) return await _download_file(f"files/get/{hash_type}/{hash_value}/", destination, f"{hash_value}.bin", token=token) @mcp_tool("machinelist") diff --git a/web/apiv2/views.py b/web/apiv2/views.py index d639487cc91..c8fb73e68cb 100644 --- a/web/apiv2/views.py +++ b/web/apiv2/views.py @@ -53,10 +53,7 @@ statistics, validate_task, ) -try: - from mcp_filters import lean_search_filters -except ImportError: - from mcp.filters import lean_search_filters +from mcp.filters import lean_search_filters from lib.cuckoo.core.database import Database, _Database from lib.cuckoo.core.data.task import ( TASK_RECOVERED, From c9a8c17cfedb78f43ac7f64fff8f7feb0a3f0c60 Mon Sep 17 00:00:00 2001 From: doomedraven Date: Wed, 25 Feb 2026 09:03:36 +0100 Subject: [PATCH 5/7] Update web_utils.py --- lib/cuckoo/common/web_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/cuckoo/common/web_utils.py b/lib/cuckoo/common/web_utils.py index c1fcecf71e0..0fda00c8116 100644 --- a/lib/cuckoo/common/web_utils.py +++ b/lib/cuckoo/common/web_utils.py @@ -1447,7 +1447,8 @@ def perform_search( else: pipeline.append({"$match": {"info.user_id": user_id}}) - # Stage 9: Add your custom projection pipeline.append({"$project": projection or perform_search_filters}) + # Stage 9: Add your custom projection + pipeline.append({"$project": projection or perform_search_filters}) retval = list(mongo_aggregate(FILES_COLL, pipeline)) if not retval: From eb9affca453895853c5d3a2e4328bb7c830f6cb3 Mon Sep 17 00:00:00 2001 From: doomedraven Date: Wed, 25 Feb 2026 09:43:50 +0100 Subject: [PATCH 6/7] Update web_utils.py --- lib/cuckoo/common/web_utils.py | 66 ++-------------------------------- 1 file changed, 3 insertions(+), 63 deletions(-) diff --git a/lib/cuckoo/common/web_utils.py b/lib/cuckoo/common/web_utils.py index 0fda00c8116..077056c46eb 100644 --- a/lib/cuckoo/common/web_utils.py +++ b/lib/cuckoo/common/web_utils.py @@ -1328,23 +1328,6 @@ def perform_search( """ if repconf.mongodb.enabled and repconf.elasticsearchdb.enabled and essearch and not term: multi_match_search = {"query": {"multi_match": {"query": value, "fields": ["*"]}}} - if user_id and not privs: - user_filter = None - if force_bool(web_cfg.general.get("public_searches", True)): - if not force_bool(web_cfg.tlp.get("public_red", False)): - user_filter = { - "bool": { - "should": [ - {"bool": {"must_not": [{"terms": {"info.tlp": ["red", "Red", "RED"]}}]}}, - {"term": {"info.user_id": user_id}} - ], - "minimum_should_match": 1 - } - } - else: - user_filter = {"term": {"info.user_id": user_id}} - if user_filter: - multi_match_search = {"query": {"bool": {"must": [{"multi_match": {"query": value, "fields": ["*"]}}], "filter": [user_filter]}}} numhits = es.search(index=get_analysis_index(), body=multi_match_search, size=0)["hits"]["total"] return [ d["_source"] @@ -1438,18 +1421,9 @@ def perform_search( {"$unwind": "$task_doc"}, # Stage 8: Make the task doc the new root {"$replaceRoot": {"newRoot": "$task_doc"}}, + # Stage 9: Add your custom projection + {"$project": projection or perform_search_filters}, ] - - if user_id and not privs: - if force_bool(web_cfg.general.get("public_searches", True)): - if not force_bool(web_cfg.tlp.get("public_red", False)): - pipeline.append({"$match": {"$or": [{"info.tlp": {"$nin": ["red", "Red", "RED"]}}, {"info.user_id": user_id}]}}) - else: - pipeline.append({"$match": {"info.user_id": user_id}}) - - # Stage 9: Add your custom projection - pipeline.append({"$project": projection or perform_search_filters}) - retval = list(mongo_aggregate(FILES_COLL, pipeline)) if not retval: return [] @@ -1470,19 +1444,6 @@ def perform_search( projection[f"target.file.{FILE_REF_KEY}"] = 1 if term in search_term_map_repetetive_blocks: mongo_search_query = {"$or": [{path: condition} for path, condition in mongo_search_query.items()]} - - if user_id and not privs: - if force_bool(web_cfg.general.get("public_searches", True)): - if not force_bool(web_cfg.tlp.get("public_red", False)): - mongo_search_query = { - "$and": [ - mongo_search_query, - {"$or": [{"info.tlp": {"$nin": ["red", "Red", "RED"]}}, {"info.user_id": user_id}]} - ] - } - else: - mongo_search_query["info.user_id"] = user_id - retval = list(mongo_find("analysis", mongo_search_query, projection, limit=search_limit)) for doc in retval: @@ -1492,34 +1453,13 @@ def perform_search( return retval if es_as_db: - _source_fields = list((projection or perform_search_filters).keys())[:-1] - - user_filter = None - if user_id and not privs: - if force_bool(web_cfg.general.get("public_searches", True)): - if not force_bool(web_cfg.tlp.get("public_red", False)): - user_filter = { - "bool": { - "should": [ - {"bool": {"must_not": [{"terms": {"info.tlp": ["red", "Red", "RED"]}}]}}, - {"term": {"info.user_id": user_id}} - ], - "minimum_should_match": 1 - } - } - else: - user_filter = {"term": {"info.user_id": user_id}} - + _source_fields = list(perform_search_filters.keys())[:-1] if isinstance(search_term_map[term], str): q = {"query": {"match": {search_term_map[term]: value}}} - if user_filter: - q = {"query": {"bool": {"must": [q["query"]], "filter": [user_filter]}}} return [d["_source"] for d in es.search(index=get_analysis_index(), body=q, _source=_source_fields)["hits"]["hits"]] else: queries = [{"match": {search_term: value}} for search_term in search_term_map[term]] q = {"query": {"bool": {"should": queries, "minimum_should_match": 1}}} - if user_filter: - q["query"]["bool"]["filter"] = [user_filter] return [d["_source"] for d in es.search(index=get_analysis_index(), body=q, _source=_source_fields)["hits"]["hits"]] From f16e56484f68742d479b3314a33c90a18979ec26 Mon Sep 17 00:00:00 2001 From: doomedraven Date: Wed, 25 Feb 2026 09:46:06 +0100 Subject: [PATCH 7/7] Update web.conf.default --- conf/default/web.conf.default | 2 -- 1 file changed, 2 deletions(-) diff --git a/conf/default/web.conf.default b/conf/default/web.conf.default index 60eb295fb43..0966b8f943f 100644 --- a/conf/default/web.conf.default +++ b/conf/default/web.conf.default @@ -123,8 +123,6 @@ package = edge # TLP markings on submission and webgui [tlp] enabled = no -# Should TLP: RED tasks be searchable by other users? -public_red = yes #AMSI dump submission checkbox: can be useful to disable if no Win10+ instances #(amsidump is enabled by default in the monitor for Win10+)