Skip to content

Commit 0910069

Browse files
authored
Staging (#2733)
1 parent ca6dcdb commit 0910069

File tree

6 files changed

+164
-9
lines changed

6 files changed

+164
-9
lines changed

changelog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
* Monitor update: WMI hooks: add handling for VT_NULL and enable WMI_Get logging
6868

6969
### [06.06.2025]
70-
* Monitor updates:
70+
* Monitor updates:
7171
* WMI hooks
7272
* Fix format string vulnerability in debugger StringsOutput() function
7373

lib/cuckoo/common/abstracts.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from lib.cuckoo.common.path_utils import path_exists, path_mkdir
4141
from lib.cuckoo.common.url_validate import url as url_validator
4242
from lib.cuckoo.common.utils import create_folder, get_memdump_path, load_categories
43-
from lib.cuckoo.core.database import Database, Machine, _Database
43+
from lib.cuckoo.core.database import Database, Machine, _Database, Task
4444

4545
try:
4646
import re2 as re
@@ -270,6 +270,48 @@ def find_machine_to_service_task(self, task):
270270
"""
271271
return self.db.find_machine_to_service_task(task)
272272

273+
def _machine_can_service_task(self, machine: Machine, task: Task) -> bool:
274+
"""Check if a machine can service a task based on platform, arch, and tags."""
275+
# 1. Platform check
276+
if task.platform and machine.platform != task.platform:
277+
return False
278+
279+
task_tags = {tag.name for tag in task.tags}
280+
machine_tags = {tag.name for tag in machine.tags}
281+
282+
# Define architecture tags.
283+
arch_tags = {"x86", "x64"} # Add other relevant archs if needed
284+
task_arch = next((tag for tag in task_tags if tag in arch_tags), None)
285+
286+
# 2. Architecture compatibility check
287+
if task_arch:
288+
if machine.platform == "windows":
289+
# 32-bit Windows can't run 64-bit tasks.
290+
if machine.arch == "x86" and task_arch == "x64":
291+
return False
292+
else: # Strict matching for Linux/other platforms
293+
# The machine's arch must equal the task's arch.
294+
if machine.arch != task_arch:
295+
return False
296+
297+
# 3. Check remaining tags
298+
# All tags that are NOT architecture tags must be present on the machine.
299+
other_tags = task_tags - arch_tags
300+
if not other_tags.issubset(machine_tags):
301+
return False
302+
303+
# For strict platforms (not Windows), the machine must explicitly have the arch tag.
304+
if task_arch and machine.platform != "windows":
305+
if task_arch not in machine_tags:
306+
return False
307+
308+
# For a Windows machine to run an x64 task, it must have the x64 tag.
309+
if task_arch == "x64" and machine.platform == "windows":
310+
if "x64" not in machine_tags:
311+
return False
312+
313+
return True
314+
273315
def scale_pool(self, machine: Machine) -> None:
274316
"""This can be overridden in sub-classes to scale the pool of machines once one has been acquired."""
275317
return
@@ -450,7 +492,7 @@ def start(self, label):
450492
try:
451493
self.vms[label].revertToSnapshot(snapshot, flags=0)
452494
except libvirt.libvirtError as e:
453-
raise CuckooMachineError(f"Unable to restore snapshot on virtual machine {label}") from e
495+
raise CuckooMachineError(f"Unable to restore snapshot on virtual machine {label}. Your snapshot MUST BE in running state!") from e
454496
finally:
455497
self._disconnect(conn)
456498
else:

lib/cuckoo/core/scheduler.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ def find_pending_task_to_service(self) -> Tuple[Optional[Task], Optional[Machine
177177

178178
task: Optional[Task] = None
179179
machine: Optional[Machine] = None
180+
# Cache available machine stats to avoid repeated DB queries within the loop.
181+
available_tags_stats = self.get_available_machine_stats()
182+
180183
# Get the list of all pending tasks in the order that they should be processed.
181184
for task_candidate in self.db.list_tasks(
182185
status=TASK_PENDING,
@@ -192,11 +195,32 @@ def find_pending_task_to_service(self) -> Tuple[Optional[Task], Optional[Machine
192195
try:
193196
machine = self.machinery_manager.find_machine_to_service_task(task_candidate)
194197
except CuckooUnserviceableTaskError:
198+
requested_tags = ", ".join(tag.name for tag in task_candidate.tags)
199+
log_message = (
200+
"Task #{task_id}: {status} unserviceable task because no matching machine could be found. "
201+
"Requested tags: '{tags}'. Available machine tags: {available}. "
202+
"Please check your machinery configuration."
203+
)
204+
195205
if self.cfg.cuckoo.fail_unserviceable:
196-
log.info("Task #%s: Failing unserviceable task", task_candidate.id)
206+
log.info(
207+
log_message.format(
208+
task_id=task_candidate.id,
209+
status="Failing",
210+
tags=requested_tags,
211+
available=available_tags_stats,
212+
)
213+
)
197214
self.db.set_status(task_candidate.id, TASK_FAILED_ANALYSIS)
198215
else:
199-
log.info("Task #%s: Unserviceable task", task_candidate.id)
216+
log.info(
217+
log_message.format(
218+
task_id=task_candidate.id,
219+
status="Unserviceable",
220+
tags=requested_tags,
221+
available=available_tags_stats,
222+
)
223+
)
200224
continue
201225

202226
if machine:

lib/cuckoo/core/startup.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ def init_modules():
305305

306306
# Import machine manager.
307307
import_plugin(f"modules.machinery.{cuckoo.cuckoo.machinery}")
308+
check_snapshot_state()
308309

309310
for category, entries in list_plugins().items():
310311
log.debug('Imported "%s" modules:', category)
@@ -316,6 +317,94 @@ def init_modules():
316317
log.debug("\t |-- %s", entry.__name__)
317318

318319

320+
def check_snapshot_state():
321+
"""Checks the state of snapshots and machine architecture for KVM/QEMU machinery."""
322+
if cuckoo.cuckoo.machinery not in ("kvm", "qemu"):
323+
return
324+
325+
try:
326+
import libvirt
327+
from xml.etree import ElementTree
328+
except ImportError:
329+
raise CuckooStartupError(
330+
"The 'libvirt-python' library is required for KVM/QEMU machinery but is not installed. "
331+
"Please install it (e.g., 'cd /opt/CAPEv2/ ; sudo -u cape /etc/poetry/bin/poetry run extra/libvirt_installer.sh')."
332+
)
333+
334+
machinery_config = Config(cuckoo.cuckoo.machinery)
335+
dsn = machinery_config.get(cuckoo.cuckoo.machinery).get("dsn")
336+
conn = None
337+
338+
try:
339+
conn = libvirt.open(dsn)
340+
except libvirt.libvirtError as e:
341+
raise CuckooStartupError(f"Failed to connect to libvirt with DSN '{dsn}'. Error: {e}")
342+
343+
if conn is None:
344+
raise CuckooStartupError(f"Failed to connect to libvirt with DSN '{dsn}'. Please check your configuration and libvirt service.")
345+
346+
try:
347+
for machine_name in machinery_config.get(cuckoo.cuckoo.machinery).machines.split(","):
348+
machine_name = machine_name.strip()
349+
if not machine_name:
350+
continue
351+
352+
try:
353+
domain = conn.lookupByName(machine_name)
354+
machine_config = machinery_config.get(machine_name)
355+
356+
# Check for valid architecture configuration.
357+
arch = machine_config.get("arch")
358+
if not arch:
359+
raise CuckooStartupError(f"Missing 'arch' configuration for VM '{machine_name}'. Please specify a valid architecture (e.g., x86, x64).")
360+
361+
if arch == "x86_64":
362+
raise CuckooStartupError(
363+
f"Invalid architecture '{arch}' for VM '{machine_name}'. Please use 'x64' instead of 'x86_64'."
364+
)
365+
366+
if arch != arch.lower():
367+
raise CuckooStartupError(
368+
f"Invalid architecture '{arch}' for VM '{machine_name}'. Architecture must be all lowercase."
369+
)
370+
371+
# Check snapshot state.
372+
snapshot_name = machine_config.get("snapshot")
373+
snapshot = None
374+
375+
if snapshot_name:
376+
snapshot = domain.snapshotLookupByName(snapshot_name)
377+
else:
378+
if domain.hasCurrentSnapshot(0):
379+
snapshot = domain.snapshotCurrent(0)
380+
snapshot_name = snapshot.getName()
381+
log.info("No snapshot name configured for VM '%s', checking latest: '%s'", machine_name, snapshot_name)
382+
else:
383+
log.warning("No snapshot configured or found for VM '%s'. Skipping check.", machine_name)
384+
continue
385+
386+
xml_desc = snapshot.getXMLDesc(0)
387+
root = ElementTree.fromstring(xml_desc)
388+
state_element = root.find("state")
389+
390+
if state_element is None or state_element.text != "running":
391+
state = state_element.text if state_element is not None else "unknown"
392+
raise CuckooStartupError(
393+
f"Snapshot '{snapshot_name}' for VM '{machine_name}' is not in a 'running' state (current state: '{state}'). "
394+
"Please ensure you take snapshots of running VMs."
395+
)
396+
397+
except libvirt.libvirtError as e:
398+
# It's possible a snapshot name is provided but doesn't exist, which is a config error.
399+
snapshot_identifier = f"with snapshot '{snapshot_name}'" if snapshot_name else ""
400+
raise CuckooStartupError(
401+
f"Error checking snapshot state for VM '{machine_name}' {snapshot_identifier}. Libvirt error: {e}"
402+
)
403+
finally:
404+
if conn:
405+
conn.close()
406+
407+
319408
def init_rooter():
320409
"""If required, check whether the rooter is running and whether we can
321410
connect to it."""

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -523,9 +523,9 @@ greenlet==3.0.3 ; python_version >= "3.10" and python_version < "4.0" \
523523
gunicorn==23.0.0 ; python_version >= "3.10" and python_version < "4.0" \
524524
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
525525
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
526-
h11==0.16.0 ; python_version >= "3.10" and python_version < "4.0" \
527-
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
528-
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
526+
h11==0.14.0 ; python_version >= "3.10" and python_version < "4.0" \
527+
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
528+
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
529529
httptools==0.6.4 ; python_version >= "3.10" and python_version < "4.0" \
530530
--hash=sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a \
531531
--hash=sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd \

tests/data

Submodule data updated 23 files

0 commit comments

Comments
 (0)