Skip to content

Commit 410a232

Browse files
committed
Merge branch 'master' into staging
2 parents e7255b8 + 43ae9fd commit 410a232

File tree

10 files changed

+235
-33
lines changed

10 files changed

+235
-33
lines changed

analyzer/linux/lib/core/packages.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,11 @@ def strace_analysis(self):
202202

203203
target_cmd = f"{self.target}"
204204
if "args" in kwargs:
205-
target_cmd += f' {" ".join(kwargs["args"])}'
205+
args = kwargs["args"]
206+
if not isinstance(args, str):
207+
args = " ".join(args)
208+
target_cmd += f" {args}"
209+
206210

207211
# eg: strace_args=-e trace=!recvfrom;epoll_pwait
208212
strace_args = self.options.get("strace_args", "").replace(";", ",")

analyzer/windows/analyzer.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -657,19 +657,25 @@ def analysis_loop(self, aux_modules):
657657

658658
emptytime = None
659659
complete_folder = hashlib.md5(f"cape-{self.config.id}".encode()).hexdigest()
660-
complete_analysis_pattern = os.path.join(os.environ["TMP"], complete_folder)
660+
complete_analysis_patterns = [os.path.join(os.environ["TMP"], complete_folder)]
661+
if "SystemRoot" in os.environ:
662+
complete_analysis_patterns.append(os.path.join(os.environ["SystemRoot"], "Temp", complete_folder))
663+
661664
while self.do_run:
662665
self.time_counter = timeit.default_timer() - time_start
663666
if self.time_counter >= int(self.config.timeout):
664667
log.info("Analysis timeout hit, terminating analysis")
665668
ANALYSIS_TIMED_OUT = True
666669
break
667670

668-
if os.path.isdir(complete_analysis_pattern):
671+
if any(os.path.isdir(p) for p in complete_analysis_patterns):
669672
log.info("Analysis termination requested by user")
670673
ANALYSIS_TIMED_OUT = True
671674
break
672675

676+
if ANALYSIS_TIMED_OUT:
677+
break
678+
673679
# If the process lock is locked, it means that something is
674680
# operating on the list of monitored processes. Therefore we
675681
# cannot proceed with the checks until the lock is released.
Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,148 @@
1+
import os
2+
import zipfile
3+
import logging
4+
15
from lib.common.abstracts import Package
26
from lib.common.common import check_file_extension
37
from lib.common.constants import OPT_ARGUMENTS
48

9+
log = logging.getLogger(__name__)
10+
11+
# CONFIGURATION - allow non installed nodejs
12+
# Best practice: Keep filenames in one place
13+
# Grab a copy of https://nodejs.org/download/release/latest-v25.x/node-v25.2.1-win-x64.zip or another version of your interest
14+
# Store it in extras as nodejs.zip
15+
NODE_ZIP_NAME = "nodejs.zip"
16+
NODE_DIR_NAME = "nodejs"
17+
18+
19+
def setup_node_environment():
20+
"""
21+
Attempts to unzip a portable Node environment.
22+
Returns: (path_to_node_exe, None) on success (None, error_message) on failure
23+
"""
24+
try:
25+
# Determine paths
26+
user_profile = os.environ.get("USERPROFILE", "C:\\Users\\Admin")
27+
install_path = os.path.join(user_profile, "AppData", "Local", "app")
28+
29+
# Look for zip in absolute path relative to current execution or fixed 'extras'
30+
# Assuming 'extras' is in the current working dir of the analyzer
31+
node_zip_path = os.path.abspath(os.path.join("extras", NODE_ZIP_NAME))
32+
node_bin_path = os.path.join(install_path, NODE_DIR_NAME)
33+
34+
if not os.path.exists(node_zip_path):
35+
return None, f"Zip not found at {node_zip_path}"
36+
37+
if not os.path.exists(node_bin_path):
38+
os.makedirs(node_bin_path)
39+
40+
node_exe_path = None
41+
42+
# 1. Open Zip and Find node.exe BEFORE extracting
43+
with zipfile.ZipFile(node_zip_path, 'r') as z:
44+
# list of all files in zip
45+
file_list = z.namelist()
46+
47+
# Find the internal path to node.exe
48+
# This works for both "node.exe" (root) and "node-v25.../node.exe" (subfolder)
49+
node_internal_path = next((f for f in file_list if f.lower().endswith("node.exe")), None)
50+
51+
if not node_internal_path:
52+
return None, "Archive does not contain node.exe"
53+
54+
# 2. Extract
55+
# We extract to a specific folder to avoid cluttering if it's a "root-files" zip
56+
# We use the zip name (minus extension) as a container folder
57+
extract_path = node_bin_path
58+
59+
if not os.path.exists(extract_path):
60+
# Security: Check for path traversal before extraction.
61+
for member in z.infolist():
62+
if member.filename.startswith("/") or ".." in member.filename:
63+
return None, f"Aborting extraction. Zip contains potentially malicious path: {member.filename}"
64+
65+
os.makedirs(extract_path)
66+
log.info("Extracting to %s...", extract_path)
67+
z.extractall(extract_path)
68+
69+
# 3. Construct the full path
70+
# extract_path + internal_path_inside_zip
71+
# e.g. C:\Apps\node-v25\ + node-v25-win-x64/node.exe
72+
node_exe_path = os.path.join(extract_path, node_internal_path)
73+
74+
# Normalizing path separators (fixes mix of / and \)
75+
node_exe_path = os.path.normpath(node_exe_path)
76+
77+
# 4. Final Verification and Env Setup
78+
if node_exe_path and os.path.exists(node_exe_path):
79+
# Add the folder containing node.exe to PATH
80+
node_dir = os.path.dirname(node_exe_path)
81+
current_path = os.environ.get("PATH", "")
82+
os.environ["PATH"] = f"{node_dir};{current_path}"
83+
84+
return node_exe_path, None
85+
else:
86+
return None, "Extraction finished but node.exe not found on disk."
87+
88+
except (zipfile.BadZipFile, OSError) as e:
89+
return None, f"Exception during Node setup: {str(e)}"
90+
591

692
class NodeJS(Package):
793
"""Package for executing JavaScript files using NodeJS."""
894

995
PATHS = [
10-
("ProgramFiles", "NodeJS", "node.exe"),
96+
# Standard 64-bit Install (most common)
97+
# Default folder is usually lowercase "nodejs"
98+
("ProgramFiles", "nodejs", "node.exe"),
99+
100+
# 32-bit Node on 64-bit Windows
101+
("ProgramFiles(x86)", "nodejs", "node.exe"),
102+
103+
# Your specific custom paths (Case insensitive, so NodeJS works too)
11104
("LOCALAPPDATA", "Programs", "NodeJS", "node.exe"),
105+
106+
# Fallback for manual installs at root
107+
("SystemDrive", "nodejs", "node.exe"),
12108
]
109+
13110
summary = "Executes a JS sample using NodeJS."
14-
description = "Uses node.exe instead of wscript.exe to execute JavaScript files."
111+
description = "Uses node.exe to execute JavaScript files."
15112
option_names = (OPT_ARGUMENTS,)
16113

17114
def start(self, path):
18-
node = self.get_path("node.exe")
19115
path = check_file_extension(path, ".js")
20116
args = self.options.get(OPT_ARGUMENTS, "")
21-
return self.execute(node, f'"{path}" {args}', path)
117+
118+
# Prepare the argument list
119+
# CAPE expects a list of arguments for the process
120+
node_args = f'"{path}"'
121+
122+
# Append additional arguments if they exist
123+
if args:
124+
node_args += f" {args}"
125+
126+
# 1. Try to set up Custom Node
127+
binary = None
128+
129+
# Check if the zip exists before trying setup
130+
if os.path.exists(os.path.join("extras", NODE_ZIP_NAME)):
131+
custom_bin, error = setup_node_environment()
132+
if custom_bin:
133+
binary = custom_bin
134+
log.info("Using Custom Node.js: %s", binary)
135+
else:
136+
log.error("Failed to setup Custom Node: %s", error)
137+
# Do NOT return here, fall through to system node
138+
139+
# 2. Fallback to System Node if custom failed or zip missing
140+
if not binary:
141+
log.info("Falling back to system installed Node.js")
142+
binary = self.get_path("node.exe")
143+
144+
# 3. Execution
145+
if not binary:
146+
raise Exception("Node.js executable not found in custom bundle OR system paths.")
147+
148+
return self.execute(binary, node_args, path)

lib/cuckoo/common/cleaners_utils.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
mongo_update_one,
5757
mongo_update_many,
5858
mongo_delete_calls_by_task_id_in_range,
59-
mongo_delete_data_range,
6059
)
6160
elif repconf.elasticsearchdb.enabled:
6261
from dev_utils.elasticsearchdb import all_docs, delete_analysis_and_related_calls, get_analysis_index
@@ -470,9 +469,9 @@ def tmp_clean_before(timerange: str):
470469

471470

472471
def cuckoo_clean_before(args: dict):
473-
"""Clean up failed tasks
472+
"""Clean up old tasks
474473
It deletes all stored data from file system and configured databases (SQL
475-
and MongoDB for tasks completed before now - time range.
474+
and optionally MongoDB) for tasks completed before now - time range.
476475
"""
477476
# Init logging.
478477
# This need to init a console logger handler, because the standard
@@ -498,7 +497,15 @@ def cuckoo_clean_before(args: dict):
498497
log.info("url filter applied")
499498
category = "url"
500499

501-
old_tasks = db.list_tasks(added_before=added_before, category=category, not_status=TASK_PENDING)
500+
tags_tasks_like = args.get("tags_tasks_filter", False)
501+
delete_pending = args.get("delete_pending", False)
502+
503+
old_tasks = db.list_tasks(
504+
added_before=added_before,
505+
category=category,
506+
not_status=False if delete_pending else TASK_PENDING,
507+
tags_tasks_like=tags_tasks_like
508+
)
502509

503510
# We need this to cleanup file system and MongoDB calls collection
504511
id_arr = [e.id for e in old_tasks]
@@ -535,10 +542,11 @@ def cuckoo_clean_before(args: dict):
535542
response = input("You are deleting mongo data in cluster, are you sure you want to continue? y/n")
536543
if response.lower() in ("n", "not"):
537544
sys.exit()
538-
mongo_delete_data_range(range_end=highest_id)
539-
# cleanup_files_collection_by_id(highest_id)
545+
mongo_delete_data(id_arr)
540546

541-
db.delete_tasks(added_before=added_before, category=category)
547+
db.delete_tasks(added_before=added_before,
548+
category=category,
549+
tags_tasks_like=tags_tasks_like)
542550

543551

544552
def cuckoo_clean_sorted_pcap_dump():

modules/processing/network.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -776,8 +776,6 @@ def run(self):
776776
offset = file.tell()
777777
continue
778778

779-
self._add_hosts(connection)
780-
781779
if ip.p == dpkt.ip.IP_PROTO_TCP:
782780
tcp = ip.data
783781
if not isinstance(tcp, dpkt.tcp.TCP):
@@ -843,6 +841,7 @@ def run(self):
843841
self._icmp_dissect(connection, icmp)
844842

845843
offset = file.tell()
844+
self._add_hosts(connection)
846845
except AttributeError:
847846
continue
848847
except dpkt.dpkt.NeedData:

poetry.lock

Lines changed: 11 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ pymongo = ">=4.0.1"
6464
# ImageHash = "4.3.1"
6565
LnkParse3 = "1.5.0"
6666
cachetools = "^5.5.1"
67-
django-allauth = "65.3.1" # https://django-allauth.readthedocs.io/en/latest/configuration.html
67+
django-allauth = "65.13.0" # https://django-allauth.readthedocs.io/en/latest/configuration.html
6868
# socks5man = {git = "https://github.com/CAPESandbox/socks5man.git", rev = "7b335d027297b67abdf28f38cc7d5d42c9d810b5"}
6969
# httpreplay = {git = "https://github.com/CAPESandbox/httpreplay.git", rev = "0d5a5b3144ab15f93189b83ca8188afde43db134"}
7070
# bingraph = {git = "https://github.com/CAPESandbox/binGraph.git", rev = "552d1210ac6770f8b202d0d1fc4610cc14d878ec"}

requirements.txt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,9 @@ cython==3.0.11 ; python_version >= "3.10" and python_version < "4.0" \
361361
daphne==3.0.2 ; python_version >= "3.10" and python_version < "4.0" \
362362
--hash=sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f \
363363
--hash=sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393
364-
django-allauth==65.3.1 ; python_version >= "3.10" and python_version < "4.0" \
365-
--hash=sha256:e02e951b71a2753a746459f2efa114c7c72bf2cef6887dbe8607a577c0350587
364+
django-allauth==65.13.0 ; python_version >= "3.10" and python_version < "4.0" \
365+
--hash=sha256:119c0cf1cc2e0d1a0fe2f13588f30951d64989256084de2d60f13ab9308f9fa0 \
366+
--hash=sha256:7d7b7e7ad603eb3864c142f051e2cce7be2f9a9c6945a51172ec83d48c6c843b
366367
django-crispy-forms==2.3 ; python_version >= "3.10" and python_version < "4.0" \
367368
--hash=sha256:2db17ae08527201be1273f0df789e5f92819e23dd28fec69cffba7f3762e1a38 \
368369
--hash=sha256:efc4c31e5202bbec6af70d383a35e12fc80ea769d464fb0e7fe21768bb138a20
@@ -1509,9 +1510,9 @@ pyopenssl==25.0.0 ; python_version >= "3.10" and python_version < "4.0" \
15091510
pyparsing==3.2.1 ; python_version >= "3.10" and python_version < "4.0" \
15101511
--hash=sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1 \
15111512
--hash=sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a
1512-
pypdf==6.4.0 ; python_version >= "3.10" and python_version < "4.0" \
1513-
--hash=sha256:4769d471f8ddc3341193ecc5d6560fa44cf8cd0abfabf21af4e195cc0c224072 \
1514-
--hash=sha256:55ab9837ed97fd7fcc5c131d52fcc2223bc5c6b8a1488bbf7c0e27f1f0023a79
1513+
pypdf==5.2.0 ; python_version >= "3.10" and python_version < "4.0" \
1514+
--hash=sha256:7c38e68420f038f2c4998fd9d6717b6db4f6cef1642e9cf384d519c9cf094663 \
1515+
--hash=sha256:d107962ec45e65e3bd10c1d9242bdbbedaa38193c9e3a6617bd6d996e5747b19
15151516
pyre2-updated==0.3.8 ; python_version >= "3.10" and python_version < "4.0" \
15161517
--hash=sha256:2bda9bf4d59568152e085450ffc1c08fcf659000d06766861f7ff340ba601c3e \
15171518
--hash=sha256:350be9580700b67af87f5227453d1123bc9f4513f0bcc60450574f1bc46cb24f \

tests/test_analyzer_logic.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import pytest
2+
import os
3+
import hashlib
4+
import tempfile
5+
6+
# Ideally, this function would be imported from your application code
7+
def check_completion_logic(config):
8+
complete_folder = hashlib.md5(f"cape-{config.id}".encode()).hexdigest()
9+
complete_analysis_patterns = [os.path.join(os.environ["TMP"], complete_folder)]
10+
if "SystemRoot" in os.environ:
11+
complete_analysis_patterns.append(os.path.join(os.environ["SystemRoot"], "Temp", complete_folder))
12+
13+
return any(os.path.isdir(path) for path in complete_analysis_patterns)
14+
15+
class MockConfig:
16+
id = 123
17+
18+
@pytest.fixture
19+
def mock_env(monkeypatch):
20+
"""Pytest fixture to mock environment and create temp dirs."""
21+
with tempfile.TemporaryDirectory() as tmp_dir, tempfile.TemporaryDirectory() as sysroot_dir:
22+
monkeypatch.setenv("TMP", tmp_dir)
23+
monkeypatch.setenv("SystemRoot", sysroot_dir)
24+
os.makedirs(os.path.join(sysroot_dir, "Temp"), exist_ok=True)
25+
yield
26+
27+
def test_completion_folder_in_tmp(mock_env):
28+
config = MockConfig()
29+
complete_folder = hashlib.md5(f"cape-{config.id}".encode()).hexdigest()
30+
path = os.path.join(os.environ["TMP"], complete_folder)
31+
os.makedirs(path)
32+
33+
assert check_completion_logic(config) is True
34+
35+
os.rmdir(path)
36+
assert check_completion_logic(config) is False
37+
38+
def test_completion_folder_in_systemroot(mock_env):
39+
config = MockConfig()
40+
complete_folder = hashlib.md5(f"cape-{config.id}".encode()).hexdigest()
41+
path = os.path.join(os.environ["SystemRoot"], "Temp", complete_folder)
42+
os.makedirs(path)
43+
44+
assert check_completion_logic(config) is True
45+
46+
os.rmdir(path)
47+
assert check_completion_logic(config) is False

0 commit comments

Comments
 (0)