-
Notifications
You must be signed in to change notification settings - Fork 574
apiv2: add download endpoints for pcap variants, TLS keys, ETW logs, bulk archives #2994
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import socket | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import subprocess | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import tempfile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import zipfile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from datetime import datetime, timedelta | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from io import BytesIO | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1653,34 +1654,260 @@ def tasks_pcap(request, task_id): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response(resp) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @csrf_exempt | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @api_view(["GET"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def tasks_tlspcap(request, task_id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not apiconf.tasktlspcap.get("enabled"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resp = {"error": True, "error_value": "TLS PCAP download API is disabled"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response(resp) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _resolve_task_id(task_id, enabled_key, check_tlp=True): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Shared preamble for artifact-download endpoints. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns ((task_id, None)) on success or ((None, Response(error))) on failure. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `enabled_key` names the apiconf section that gates the endpoint; callers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| that want to share a gate (e.g. all pcap variants under [taskpcap]) reuse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| the same key. TLP:RED checks are skipped only for endpoints that need | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| to serve regardless (none at present).""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1663
to
+1664
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| the same key. TLP:RED checks are skipped only for endpoints that need | |
| to serve regardless (none at present).""" | |
| the same key. By default, TLP:RED tasks are blocked; callers may pass | |
| `check_tlp=False` for endpoints that intentionally need to bypass that | |
| restriction.""" |
Copilot
AI
Apr 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The path validation uses normpath(...).startswith(ANALYSIS_BASE_PATH), which does not account for symlinks. If an analysis artifact path is a symlink to a file outside the analysis directory, this check would still pass and could expose host files. Consider validating with path_safe()/Path.resolve() and/or rejecting symlinks before opening the file.
Copilot
AI
Apr 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_serve_folder_zip() assumes encrypted=True yields an AES-encrypted archive, but create_zip() only encrypts when pyzipper is available; otherwise it silently produces an unencrypted zip. If these endpoints must always be encrypted for safety, consider explicitly checking HAVE_PYZIPPER and returning a clear error when it's missing (or adjust the messaging/docs accordingly).
| Uses `create_zip` (password = ZIP_PWD) for parity with tasks_dropped / | |
| tasks_payloadfiles. Returns a Response with a JSON error if the folder | |
| doesn't exist or is empty.""" | |
| srcdir = os.path.join(CUCKOO_ROOT, "storage", "analyses", "%s" % task_id, rel_folder) | |
| if not os.path.normpath(srcdir).startswith(ANALYSIS_BASE_PATH): | |
| return Response({"error": True, "error_value": "Invalid path"}) | |
| if not path_exists(srcdir) or not os.listdir(srcdir): | |
| return Response({"error": True, "error_value": empty_msg or f"No {rel_folder} artifacts for task {task_id}"}) | |
| mem_zip = create_zip(folder=srcdir, encrypted=True, temp_file=True) | |
| if mem_zip is False: | |
| return Response({"error": True, "error_value": "Can't create zip archive"}) | |
| Uses AES encryption with `pyzipper` (password = ZIP_PWD). Returns a | |
| Response with a JSON error if the folder doesn't exist, is empty, or the | |
| archive cannot be created.""" | |
| srcdir = os.path.join(CUCKOO_ROOT, "storage", "analyses", "%s" % task_id, rel_folder) | |
| if not os.path.normpath(srcdir).startswith(ANALYSIS_BASE_PATH): | |
| return Response({"error": True, "error_value": "Invalid path"}) | |
| if not path_exists(srcdir) or not os.listdir(srcdir): | |
| return Response({"error": True, "error_value": empty_msg or f"No {rel_folder} artifacts for task {task_id}"}) | |
| mem_zip = tempfile.NamedTemporaryFile(delete=True) | |
| written = 0 | |
| try: | |
| with pyzipper.AESZipFile(mem_zip, "w", compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES) as zf: | |
| zf.setpassword(ZIP_PWD.encode()) | |
| for root, _, files in os.walk(srcdir): | |
| for filename in files: | |
| filepath = os.path.join(root, filename) | |
| if path_exists(filepath) and os.path.getsize(filepath) > 0: | |
| arcname = os.path.relpath(filepath, srcdir) | |
| zf.write(filepath, arcname) | |
| written += 1 | |
| except Exception: | |
| mem_zip.close() | |
| return Response({"error": True, "error_value": "Can't create zip archive"}) | |
| if not written: | |
| mem_zip.close() | |
| return Response({"error": True, "error_value": empty_msg or f"No {rel_folder} artifacts for task {task_id}"}) |
Copilot
AI
Apr 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tasks_tlspcap is described as a back-compat endpoint, but it changes the downloaded filename from the previous *_tls.pcap to *_{dump_decrypted.pcap|tls.pcap}. Clients that key off the attachment name may break; consider keeping the historical filename (e.g., always <task>_tls.pcap) while serving the preferred content.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
zf.write(file, path)will follow symlinks by default. If any archived entry is a symlink (e.g., introduced via extracted artifacts), this can zip and expose the link target outside the intended folder. Consider skipping symlinks and/or usingfollow_symlinks=Falsepluspath_safe()/Path.resolve()validation for each file before archiving.