CapeSandbox
CAPE Sandbox is an open-source software for automating the analysis of suspicious files and URLs.
Forensics & Malware Analysis · Cape Sandbox
Details
| ID | CapeSandbox |
|---|---|
| Provider | Open Source |
| Category | Forensics & Malware Analysis |
| From Version | 6.8.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Overview
CAPE Sandbox is an open-source software for automating the analysis of suspicious files and URLs. To learn more about CAPE Sandbox, visit the official CAPE documentation.
This integration was integrated and tested with CAPE V2 API.
Authorization
This integration supports two authentication methods:
- API Token - Recommended. Generate a token in your CAPE Sandbox instance.
- Username and Password - The integration will automatically generate authentication tokens.
Configure Cape Sandbox in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | Base URL of the CAPE Sandbox. | True |
| API token | Token value as generated in CAPE. If provided, Username/Password is not required. | False |
| Username | Required if ‘Username and Password’ is selected above. Provides credentials for token generation. |
False |
| Password | Password for authentication. Required if using Username/Password authentication method. | False |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
cape-file-submit
Submits a file for analysis to CAPE Sandbox. This command supports PCAP files and automatically sets the pcap option to 1. The command polls the CAPE server until the analysis task is complete, and then returns the task results.
Note: Analysis scans may take a long time to complete and could cause a timeout. If a timeout occurs, use the !cape-tasks-list task_id=<task_id> command to check the status of your task.
Base Command
cape-file-submit
Input
| Argument Name | Description | Required |
|---|---|---|
| entry_id | The War Room entry ID of the file to submit for analysis. | Required |
| package | The analysis package to use (e.g., ps1 for PowerShell scripts, exe for executables, dll for DLL files). For available packages, see the CAPE documentation. |
Optional |
| timeout | The maximum time in seconds to allow for the analysis to complete. | Optional |
| priority | The priority level to assign to the task (1-3, where 1 is highest priority). | Optional |
| options | A string of additional options to pass to the analysis (e.g., options:function=DllMain). For available options, see the CAPE documentation. |
Optional |
| machine | The specific ID of the analysis machine to use for the task. | Optional |
| platform | The name of the platform to select the analysis machine from (e.g., windows). |
Optional |
| tags | Comma-separated tags to associate with the analysis task. | Optional |
| custom | A custom string to pass to the analysis. | Optional |
| memory | Set to true to enable a full memory dump during analysis. Possible values are: true, false. |
Optional |
| enforce_timeout | Set to true to strictly enforce the analysis timeout. Possible values are: true, false. |
Optional |
| clock | The VM clock time in the format %m-%d-%Y %H:%M:%S. |
Optional |
Context Output
| Path | Description |
|---|---|
| Cape.Task | Task data returned from CAPE when analysis is ready. This object contains details about the submitted task and its status. |
cape-file-view
View detailed information about a file that has been analyzed by CAPE Sandbox. You must specify at least one identifier: Task ID, MD5 hash, or SHA256 hash.
Base Command
cape-file-view
Input
| Argument Name | Description | Required |
|---|---|---|
| task_id | The unique identifier of the analysis task to view. | Optional |
| md5 | The MD5 hash of the file to view. | Optional |
| sha256 | The SHA256 hash of the file to view. | Optional |
Context Output
| Path | Description |
|---|---|
| Cape.File.id | The ID of the file or task. |
| Cape.File.file_type | The detected type of the file. |
| Cape.File.md5 | The MD5 hash of the file. |
| Cape.File.crc32 | The CRC32 checksum of the file. |
| Cape.File.sha256 | The SHA256 hash of the file. |
| Cape.File.sha512 | The SHA512 hash of the file. |
| Cape.File.parent | The parent process or source of the file. |
| Cape.File.source_url | The URL from which the file was obtained, if applicable. |
cape-sample-download
Download a sample file from a CAPE Sandbox task. You must specify at least one identifier: Task ID, MD5 hash, SHA1 hash, or SHA256 hash. The downloaded file will be added to the War Room.
Base Command
cape-sample-download
Input
| Argument Name | Description | Required |
|---|---|---|
| task_id | The unique identifier of the analysis task to download the sample from. | Optional |
| md5 | The MD5 hash of the sample file to download. | Optional |
| sha1 | The SHA1 hash of the sample file to download. | Optional |
| sha256 | The SHA256 hash of the sample file to download. | Optional |
Context Output
There is no context output for this command. The command directly downloads the file.
cape-url-submit
Submit a URL for analysis to CAPE Sandbox. The command polls the CAPE server until the analysis task is reported as complete, and then returns the task view with the analysis results.
Note: Analysis scans may take a long time to complete and could cause a timeout. If a timeout occurs, use the !cape-tasks-list task_id=<task_id> command to check the status of your task.
Base Command
cape-url-submit
Input
| Argument Name | Description | Required |
|---|---|---|
| url | The URL to submit for analysis. | Required |
| package | The analysis package to use for URL analysis (e.g., ie for Internet Explorer, chrome for Chrome browser). For available packages, see the CAPE documentation. |
Optional |
| timeout | The maximum time in seconds to allow for the analysis to complete. | Optional |
| priority | The priority level to assign to the task (1-3, where 1 is highest priority). | Optional |
| options | A string of additional options to pass to the analysis (e.g., options:function=DllMain). For available options, see the CAPE documentation. |
Optional |
| machine | The specific ID of the analysis machine to use for the task. | Optional |
| platform | The name of the platform to select the analysis machine from (e.g., windows). |
Optional |
| tags | Comma-separated tags to associate with the analysis task. | Optional |
| custom | A custom string to pass to the analysis. | Optional |
| memory | Set to true to enable a full memory dump during analysis. Possible values are: true, false. |
Optional |
| enforce_timeout | Set to true to strictly enforce the analysis timeout. Possible values are: true, false. |
Optional |
| clock | The VM clock time in the format %m-%d-%Y %H:%M:%S. |
Optional |
| polling | Enable or disable generic polling for the CAPE task. Possible values are: true, false. Default is true. |
Optional |
| pollingInterval | The polling interval in seconds. It is strongly recommended to use an interval of 60 seconds or more to prevent API throttling by the CAPE server. | Optional |
| pollingTimeout | The total time allowed in seconds for the XSOAR/XSIAM polling sequence to complete. | Optional |
Context Output
| Path | Description |
|---|---|
| Cape.Task | Task data returned from CAPE when analysis is ready. This object contains details about the submitted task and its status. |
cape-task-delete
Delete a CAPE Sandbox analysis task by its Task ID.
Base Command
cape-task-delete
Input
| Argument Name | Description | Required |
|---|---|---|
| task_id | The unique identifier of the task to delete. | Required |
Context Output
There is no context output for this command.
cape-tasks-list
Returns a list of CAPE Sandbox tasks. If a task_id is provided, the command returns the detailed information for that specific task.
Base Command
cape-tasks-list
Input
| Argument Name | Description | Required |
|---|---|---|
| task_id | The unique identifier of the task to retrieve. If provided, returns details for that specific task only. | Optional |
| page | The page number for pagination (starts at 1). | Optional |
| page_size | The number of tasks to return per page. Maximum is 50. Default is 50. | Optional |
Context Output
| Path | Description |
|---|---|
| Cape.Task | Task data, including details for a single task if task_id was provided, or a list of tasks. |
cape-task-report-get
Retrieve the analysis report associated with a specified CAPE Sandbox task ID. If zip=true, the report will be returned as a downloadable ZIP file.
Base Command
cape-task-report-get
Input
| Argument Name | Description | Required |
|---|---|---|
| task_id | The unique identifier of the task for which to retrieve the report. | Required |
| format | The desired format of the report. Possible values are: json (full JSON report), maec (MAEC 4.1 format), maec5 (MAEC 5.0 format), metadata (metadata only), lite (lightweight report), all (all available formats). Default is json. |
Optional |
| zip | Set to true to download the report as a ZIP file. Possible values are: true, false. Default is false. |
Optional |
Context Output
| Path | Description |
|---|---|
| Cape.Task.Report | Details from the CAPE analysis report info object. This will vary based on the report format requested. |
cape-pcap-file-download
Download the PCAP (Packet Capture) network dump file associated with a specific CAPE Sandbox task ID. The PCAP file contains all network traffic captured during the analysis and can be analyzed with tools like Wireshark.
Base Command
cape-pcap-file-download
Input
| Argument Name | Description | Required |
|---|---|---|
| task_id | The unique identifier of the task for which to download the PCAP file. | Required |
Context Output
There is no context output for this command. The command directly downloads the PCAP file.
cape-task-screenshot-download
Download screenshots captured during the analysis of a CAPE Sandbox task. Screenshots show the visual behavior of the analyzed file or URL. If a specific screenshot number is provided, only that single screenshot will be downloaded; otherwise, all available screenshots for the task will be downloaded as a ZIP file.
Base Command
cape-task-screenshot-download
Input
| Argument Name | Description | Required |
|---|---|---|
| task_id | The unique identifier of the task for which to download screenshots. | Required |
| screenshot | The specific screenshot number to download (e.g., 1, 2). If not provided, all screenshots are downloaded. |
Optional |
Context Output
There is no context output for this command. The command directly downloads the screenshot files.
cape-machines-list
Returns a list of available analysis machines configured in CAPE Sandbox. If a machine_name is provided, the command returns detailed information for that specific machine.
Base Command
cape-machines-list
Input
| Argument Name | Description | Required |
|---|---|---|
| machine_name | The name of the machine to fetch details for. If provided, returns details for that specific machine only. | Optional |
| all_results | Set to true to return all machines, ignoring the limit parameter. Possible values are: true, false. Default is false. |
Optional |
| limit | The maximum number of machines to return when listing. Default is 50. | Optional |
Context Output
| Path | Description |
|---|---|
| Cape.Machine | Machine data, including details for a single machine if machine_name was provided, or a list of machines. |
cape-cuckoo-status-get
Returns the overall status of the CAPE/Cuckoo system in a human-readable format. This command provides a quick overview of the sandbox’s operational status.
Base Command
cape-cuckoo-status-get
Input
There are no input arguments for this command.
Context Output
There is no context output for this command. The output is human-readable text directly in the War Room.
Configuration parameters
url— Server URL (required)token_credentials— API Tokencredentials— Usernameinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (12)
-
cape-cuckoo-status-getReturns the overall status of the CAPE/Cuckoo system in a human-readable format. This command provides a quick overview of the sandbox's operational status.
-
cape-file-submitSubmits a file to the CAPE Sandbox for analysis. This command supports PCAP files and automatically sets the `pcap` parameter to `1`. The command polls the CAPE server until the analysis task is complete, and then returns the task results. Note: Analysis scans may take a long time to complete and could cause a timeout. If a timeout occurs, use the !cape-tasks-list task_id=<task_id> command to check the status of your task.
-
cape-file-viewView detailed information about a file that has been analyzed by CAPE Sandbox. You must specify at least one identifier - Task ID, MD5 hash, or SHA256 hash.
-
cape-machines-listReturns a list of available analysis machines configured in CAPE Sandbox. If a machine_name is provided, the command returns detailed information for that specific machine.
-
cape-pcap-file-downloadDownload the PCAP (Packet Capture) network dump file associated with a specific CAPE Sandbox task ID. The PCAP file contains all network traffic captured during the analysis and can be analyzed with tools like Wireshark.
-
cape-sample-downloadDownload a sample file from a CAPE Sandbox task. You must specify at least one identifier - Task ID, MD5 hash, SHA1 hash, or SHA256 hash. The downloaded file will be added to the War Room.
-
cape-task-deleteDelete a CAPE Sandbox analysis task by its Task ID.
-
cape-task-pollInternal command used by cape-file-submit and cape-url-submit for polling status.
-
cape-task-report-getRetrieve the analysis report associated with a specified CAPE Sandbox task ID. If zip=true, the report will be returned as a downloadable ZIP file.
-
cape-task-screenshot-downloadDownload screenshots captured during the analysis of a CAPE Sandbox task. Screenshots show the visual behavior of the analyzed file or URL. If a specific screenshot number is provided, only that single screenshot will be downloaded; otherwise, all available screenshots for the task will be downloaded.
-
cape-tasks-listReturns a list of CAPE Sandbox tasks. If a task_id is provided, the command returns the detailed information for that specific task.
-
cape-url-submitSubmits a URL to the CAPE Sandbox for analysis. The command polls the CAPE server until the analysis task is complete, and then returns the analysis results. Note: Analysis scans may take a long time to complete and could cause a timeout. If a timeout occurs, use the !cape-tasks-list task_id=<task_id> command to check the status of your task.
import json import os import sys import time import types import pytest from CommonServerPython import * # Mock DemistoClassApiModule before CapeSandbox is imported mock_demisto_class_api_module = types.ModuleType("DemistoClassApiModule") sys.modules["DemistoClassApiModule"] = mock_demisto_class_api_module # Import CapeSandbox after mock setup - this is intentional and required import CapeSandbox # noqa: E402 from CapeSandbox import ( # noqa: E402 FILE_TYPE_FILE, FILE_TYPE_NETWORK_DUMP, FILE_TYPE_REPORT, FILE_TYPE_SCREENSHOT, CapeSandboxClient, build_file_name, build_submit_form, cape_cuckoo_status_get_command, cape_file_submit_command, cape_file_view_command, cape_machines_list_command, cape_pcap_file_download_command, cape_sample_file_download_command, cape_task_delete_command, cape_task_report_get_command, cape_task_screenshot_download_command, cape_tasks_list_command, cape_url_submit_command, extract_entry_file_data, is_valid_md5, is_valid_sha1, is_valid_sha256, parse_integration_params, ) # ======================================== # Constants # ======================================== SERVER_URL = "http://test.example.com" TEST_DATA_PATH_SUFFIX = "test_data" INTEGRATION_DIR_REL = "Packs/CapeSandbox/Integrations/CapeSandbox/" MOCK_MD5 = "00112233445566778899aabbccddeeff" MOCK_SHA1 = "0011223344556677889900112233445566778899" MOCK_SHA256 = "0011223344556677889900112233445566778899001122334455667788990011" # ======================================== # Helper Functions # ======================================== def get_full_path_unified(file_name): """Calculates the full path for a file in the test_data folder.""" path = os.path.join(os.path.dirname(__file__), TEST_DATA_PATH_SUFFIX, file_name) if not os.path.exists(path): fallback_path = os.path.join(os.getcwd(), INTEGRATION_DIR_REL, TEST_DATA_PATH_SUFFIX, file_name) if os.path.exists(fallback_path): path = fallback_path if not os.path.exists(path): raise FileNotFoundError(f"Mock file not found: {file_name} in {TEST_DATA_PATH_SUFFIX}.") return path def util_load_json(file_name): """Loads a JSON file from the test_data directory.""" path = get_full_path_unified(file_name) with open(path, encoding="utf-8") as f: return json.loads(f.read()) def util_load_file(file_name): """Loads a binary file from the test_data directory.""" path = get_full_path_unified(file_name) with open(path, "rb") as f: return f.read() def compare_string_ignore_case(actual, expected): """Compares two strings ignoring case.""" if actual is None or expected is None: return actual == expected return str(actual).lower() == str(expected).lower() # ======================================== # Fixtures # ======================================== @pytest.fixture() def client(): """Returns a CapeSandboxClient instance for testing.""" return CapeSandboxClient( base_url=SERVER_URL, verify=True, proxy=True, api_token="MOCK_API_TOKEN_FOR_TESTING", username=None, password=None, ) @pytest.fixture def mock_context(): """Fixture to ensure integration context is initialized and cleaned up.""" set_integration_context({}) yield set_integration_context({}) # ======================================== # Tests: Helper Functions # ======================================== @pytest.mark.parametrize( "params,expected_error", [ ({"url": ""}, "Server URL.+is required"), ({}, "Server URL.+is required"), ], ) def test_parse_integration_params_missing_url_fail(params, expected_error): """Tests parse_integration_params fails if 'url' is missing or empty.""" with pytest.raises(DemistoException, match=expected_error): parse_integration_params(params) @pytest.mark.parametrize( "params,expected_api_token,expected_username,expected_verify,expected_proxy", [ # API token takes precedence over username/password ( { "url": SERVER_URL, "api_token": "API_TOKEN", "username": "user", "password": "pwd", "insecure": True, "proxy": True, "credentials": {"identifier": "user_cred", "password": "pwd_cred"}, }, "API_TOKEN", "user_cred", False, True, ), # token_credentials takes precedence over api_token ( { "url": SERVER_URL, "token_credentials": {"password": "TOKEN_FROM_CREDS"}, "api_token": "API_TOKEN_DIRECT", "insecure": False, "proxy": False, }, "TOKEN_FROM_CREDS", None, True, False, ), # URL with trailing slash is stripped ( {"url": f"{SERVER_URL}/", "api_token": "TOKEN"}, "TOKEN", None, True, False, ), # credentials identifier takes precedence over username ( { "url": SERVER_URL, "credentials": {"identifier": "cred_user", "password": "cred_pwd"}, "username": "direct_user", "password": "direct_pwd", }, None, "cred_user", True, False, ), ], ) def test_parse_integration_params_various_configs(params, expected_api_token, expected_username, expected_verify, expected_proxy): """Tests parse_integration_params handles various configuration scenarios.""" result = parse_integration_params(params) assert result["api_token"] == expected_api_token assert result["username"] == expected_username assert result["verify_certificate"] == expected_verify assert result["proxy"] == expected_proxy assert result["base_url"] == SERVER_URL def test_build_submit_form_full_and_url_mode(): """Tests build_submit_form correctly assigns all optional args and handles URL mode.""" args = { "url": "http://test.example.com", "package": "win_exe", "timeout": 120, "priority": 2, "memory": "True", "enforce_timeout": "false", "tags": "test_tag", } form_url = build_submit_form(args, url_mode=True) assert form_url["url"] == "http://test.example.com" assert form_url["package"] == "win_exe" assert form_url["timeout"] == 120 assert form_url["priority"] == 2 assert form_url["memory"] == "1" assert "enforce_timeout" not in form_url form_file = build_submit_form(args, url_mode=False) assert "url" not in form_file assert "memory" in form_file @pytest.mark.parametrize( "args,url_mode,check_keys_present,check_keys_absent", [ # All optional parameters with URL mode ( { "url": "http://test.example.com", "package": "win_exe", "timeout": 120, "priority": 2, "memory": "True", "enforce_timeout": "True", "tags": "test_tag", "options": "opt1,opt2", "machine": "win10", "platform": "windows", "custom": "custom_data", "clock": "1970-01-01 12:00:00", }, True, [ "url", "package", "timeout", "priority", "memory", "enforce_timeout", "tags", "options", "machine", "platform", "custom", "clock", ], [], ), # File mode excludes URL ( {"package": "win_exe", "memory": "True", "url": "http://test.example.com"}, False, ["package", "memory"], ["url"], ), # Empty args ({}, False, [], []), # Boolean false values excluded ( {"memory": "false", "enforce_timeout": "False", "package": "exe"}, False, ["package"], ["memory", "enforce_timeout"], ), ], ) def test_build_submit_form_all_parameters(args, url_mode, check_keys_present, check_keys_absent): """Tests build_submit_form with comprehensive parameter combinations.""" form = build_submit_form(args, url_mode=url_mode) for key in check_keys_present: assert key in form, f"Expected key '{key}' not in form" for key in check_keys_absent: assert key not in form, f"Unexpected key '{key}' found in form" @pytest.mark.parametrize( "identifier,file_type_info,file_format,screenshot_number,expected", [ (101, FILE_TYPE_SCREENSHOT, None, 5, "cape_task_101_screenshot_5.png"), (102, FILE_TYPE_REPORT, "csv", None, "cape_task_102_report.csv"), (MOCK_MD5, FILE_TYPE_FILE, None, None, f"cape_task_{MOCK_MD5}_file.json"), (103, FILE_TYPE_NETWORK_DUMP, None, None, "cape_task_103_network_dump.pcap"), ("12345", None, "txt", None, "cape_task_12345.txt"), ("404", None, None, None, "cape_task_404.dat"), ], ) def test_build_file_name_all_types(identifier, file_type_info, file_format, screenshot_number, expected): """Tests build_file_name constructs filenames for all supported types.""" result = build_file_name( identifier, file_type_info=file_type_info, file_format=file_format, screenshot_number=screenshot_number, ) assert result == expected @pytest.mark.parametrize( "identifier,file_type_info,file_format,expected", [ # Report with different formats (101, FILE_TYPE_REPORT, "pdf", "cape_task_101_report.pdf"), (102, FILE_TYPE_REPORT, "html", "cape_task_102_report.html"), (103, FILE_TYPE_REPORT, "zip", "cape_task_103_report.zip"), # File with different formats (201, FILE_TYPE_FILE, "bin", "cape_task_201_file.bin"), (202, FILE_TYPE_FILE, "exe", "cape_task_202_file.exe"), ], ) def test_build_file_name_report_formats(identifier, file_type_info, file_format, expected): """Tests build_file_name with various report and file formats.""" result = build_file_name(identifier, file_type_info=file_type_info, file_format=file_format) assert result == expected @pytest.mark.parametrize( "status,expected", [ ("reported", True), ("running", False), ("pending", False), ("completed", False), ("", False), ("REPORTED", False), ], ) def test_status_is_reported(status, expected): """Tests status_is_reported function correctly identifies reported status.""" assert CapeSandbox.status_is_reported(status) is expected def test_extract_entry_file_data_not_found_fail(mocker): """Tests extract_entry_file_data raises DemistoException on missing entry.""" mocker.patch.object(demisto, "getFilePath", return_value={}) with pytest.raises(DemistoException, match="Could not find file or entry"): extract_entry_file_data("not_an_entry") def test_extract_entry_file_data_general_exception_fail(mocker): """Tests extract_entry_file_data handles generic exceptions.""" mocker.patch.object(demisto, "getFilePath", side_effect=Exception("API error")) with pytest.raises(DemistoException, match="An unexpected error occurred"): extract_entry_file_data("entry_id") def test_extract_entry_file_data_uses_name_field(mocker): """Tests extract_entry_file_data uses 'name' key when available.""" mocker.patch.object( demisto, "getFilePath", return_value={"path": "/tmp/a.txt", "name": "custom.txt"}, ) path, name = extract_entry_file_data("entry_id") assert name == "custom.txt" def test_extract_entry_file_data_uses_basename_fallback(mocker): """Tests extract_entry_file_data uses os.path.basename if 'name' is missing.""" mocker.patch.object(demisto, "getFilePath", return_value={"path": "/tmp/file.exe"}) path, name = extract_entry_file_data("entry_id") assert name == "file.exe" @pytest.mark.parametrize( "validator_func,valid_hash,invalid_hash", [ (is_valid_md5, MOCK_MD5, "BADHASH"), (is_valid_sha1, MOCK_SHA1, MOCK_MD5), (is_valid_sha256, MOCK_SHA256, MOCK_MD5), ], ) def test_hash_validators(validator_func, valid_hash, invalid_hash): """Tests all hash format validation functions.""" assert validator_func(valid_hash) assert not validator_func(invalid_hash) assert not validator_func(None) # ======================================== # Tests: Client Initialization & Authentication # ======================================== @pytest.mark.parametrize( "api_token,username,password", [ (None, None, None), (None, "user", None), ], ) def test_client_init_auth_fail(api_token, username, password): """Tests client initialization fails if auth is missing or incomplete.""" with pytest.raises( DemistoException, match=r"Either API token or Username \+ Password must be provided", ): CapeSandboxClient( base_url=SERVER_URL, verify=True, proxy=True, api_token=api_token, username=username, password=password, ) def test_ensure_token_uses_api_token(client): """Tests ensure_token returns api_token when available.""" assert client.ensure_token() == "MOCK_API_TOKEN_FOR_TESTING" def test_ensure_token_uses_cached_token(mocker, mock_context): """Tests ensure_token uses valid token from cache.""" mock_time = int(time.time()) + 3600 set_integration_context({"auth_info": {"token": "CACHED_TOKEN", "valid_until": str(mock_time)}}) client_auth = CapeSandboxClient( base_url=SERVER_URL, verify=True, proxy=True, api_token=None, username="user", password="pwd", ) mocker.patch.object(CapeSandbox.time, "time", return_value=int(time.time()) + 10) assert client_auth.ensure_token() == "CACHED_TOKEN" def test_ensure_token_invalid_cache_renewal(mocker, mock_context, requests_mock): """Tests token renewal logic when cache is invalid.""" set_integration_context({"auth_info": {"token": "BAD_TOKEN", "valid_until": "NOT_A_NUMBER"}}) requests_mock.post( f"{SERVER_URL}/{CapeSandbox.API_AUTH}", json={"token": "NEW_TOKEN"}, status_code=200, ) client_auth = CapeSandboxClient( base_url=SERVER_URL, verify=True, proxy=True, api_token=None, username="user", password="pwd", ) token = client_auth.ensure_token() assert token == "NEW_TOKEN" assert get_integration_context().get("auth_info", {}).get("token") == "NEW_TOKEN" def test_ensure_token_expired_renewal(mocker, mock_context, requests_mock): """Tests token renewal logic when cache is expired.""" mock_time = int(time.time()) - 3600 set_integration_context({"auth_info": {"token": "EXPIRED_TOKEN", "valid_until": str(mock_time)}}) requests_mock.post( f"{SERVER_URL}/{CapeSandbox.API_AUTH}", json={"token": "NEW_TOKEN_2"}, status_code=200, ) client_auth = CapeSandboxClient( base_url=SERVER_URL, verify=True, proxy=True, api_token=None, username="user", password="pwd", ) token = client_auth.ensure_token() assert token == "NEW_TOKEN_2" def test_ensure_token_renewal_api_fail(mocker, requests_mock): """Tests token renewal fails if API doesn't return a token key.""" requests_mock.post( f"{SERVER_URL}/{CapeSandbox.API_AUTH}", json={"error": "failed"}, status_code=200, ) client_auth = CapeSandboxClient( base_url=SERVER_URL, verify=True, proxy=True, api_token=None, username="user", password="pwd", ) with pytest.raises(DemistoException, match="Failed to obtain API token from CAPE response"): client_auth.ensure_token() # ======================================== # Tests: Client HTTP & Error Handling # ======================================== @pytest.mark.parametrize( "error_response,expected_match", [ ({"error": True, "error_value": "Task not found."}, "Task not found"), ({"error": True, "message": "Internal Server Error."}, "Internal Server Error"), ({"error": True}, "Unknown API error occurred"), ], ) def test_http_request_api_error_check(error_response, expected_match): """Tests _check_for_api_error successfully detects and raises DemistoException.""" client = CapeSandboxClient(base_url=SERVER_URL, verify=True, proxy=True, api_token="TOKEN") with pytest.raises(DemistoException, match=expected_match): client._check_for_api_error(error_response, "test-suffix", "json") @pytest.mark.parametrize( "binary_response,resp_type,expected_match", [ # Binary content that contains JSON error ( b'{"error": true, "error_value": "No screenshots created for task 23"}', "content", "No screenshots created", ), # Binary content that contains JSON error with message field (b'{"error": true, "message": "File not found"}', "content", "File not found"), # Binary content that contains JSON error without specific message (b'{"error": true}', "content", "Unknown API error occurred"), ], ) def test_check_for_api_error_binary_json_error(binary_response, resp_type, expected_match): """Tests _check_for_api_error detects JSON errors in binary content responses.""" client = CapeSandboxClient(base_url=SERVER_URL, verify=True, proxy=True, api_token="TOKEN") with pytest.raises(DemistoException, match=expected_match): client._check_for_api_error(binary_response, "test-suffix", resp_type) @pytest.mark.parametrize( "binary_response,resp_type", [ # Valid binary content (not JSON) (b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR", "content"), # Binary content with invalid JSON (b"{invalid json}", "content"), # Binary content that's valid JSON but no error (b'{"status": "success", "data": "test"}', "content"), # Empty binary content (b"", "content"), ], ) def test_check_for_api_error_binary_valid_content(binary_response, resp_type): """Tests _check_for_api_error doesn't raise for valid binary content.""" client = CapeSandboxClient(base_url=SERVER_URL, verify=True, proxy=True, api_token="TOKEN") # Should not raise any exception client._check_for_api_error(binary_response, "test-suffix", resp_type) def test_check_for_api_error_json_response_type(): """Tests _check_for_api_error handles dict responses for json resp_type.""" client = CapeSandboxClient(base_url=SERVER_URL, verify=True, proxy=True, api_token="TOKEN") # Should raise for error dict with pytest.raises(DemistoException, match="Task not found"): client._check_for_api_error({"error": True, "error_value": "Task not found"}, "test-suffix", "json") # Should not raise for non-error dict client._check_for_api_error({"status": "success", "data": []}, "test-suffix", "json") @pytest.mark.parametrize( "limit,offset,expected_match", [ (0, 0, "limit must be > 0"), (1, -1, "offset must be >= 0"), ], ) def test_list_tasks_invalid_pagination_fail(client, limit, offset, expected_match): """Tests list_tasks fails on invalid limit/offset.""" with pytest.raises(DemistoException, match=expected_match): client.list_tasks(limit=limit, offset=offset) def test_list_tasks_happy_path(mocker, client): """Tests list_tasks calls API correctly.""" mocker.patch.object(client, "_http_request", return_value={"data": []}) client.list_tasks(limit=10, offset=5) client._http_request.assert_called_with( method="GET", headers=mocker.ANY, url_suffix=mocker.ANY, data=None, files=None, resp_type=mocker.ANY, ok_codes=mocker.ANY, ) @pytest.mark.parametrize( "method_name,hash_type", [ ("files_view_by_md5", "MD5"), ("files_get_by_sha1", "SHA1"), ("files_view_by_sha256", "SHA256"), ], ) def test_invalid_hash_format_fail(client, method_name, hash_type): """Tests that client methods fail on invalid hash format.""" method = getattr(client, method_name) with pytest.raises(DemistoException, match=f"Invalid {hash_type} hash format"): method("bad_hash") def test_view_machine_missing_name_fail(client): """Tests view_machine fails if machine_name is missing.""" with pytest.raises(DemistoException, match="machine_name is required"): client.view_machine("") # ======================================== # Tests: test-module Command # ======================================== # Tests: 429 Rate Limit Handling # ======================================== def test_extract_retry_wait_time_from_json_detail(client): """Tests _extract_retry_wait_time extracts seconds from JSON error message.""" error_msg = 'Error in API call [429] - {"detail": "Request was throttled. Expected available in 35 seconds."}' wait_time = client._extract_retry_wait_time(error_msg) assert wait_time == 35 def test_extract_retry_wait_time_with_single_quotes(client): """Tests _extract_retry_wait_time handles single quotes in JSON.""" error_msg = "Error [429] - {'detail': 'Expected available in 120 seconds.'}" wait_time = client._extract_retry_wait_time(error_msg) assert wait_time == 120 def test_extract_retry_wait_time_no_match_returns_default(client): """Tests _extract_retry_wait_time returns default delay when no match found.""" error_msg = "Error [429] - Too Many Requests" wait_time = client._extract_retry_wait_time(error_msg) assert wait_time == CapeSandbox.RETRY_BASE_DELAY def test_extract_retry_wait_time_invalid_json_returns_default(client): """Tests _extract_retry_wait_time returns default on invalid JSON.""" error_msg = "Error [429] - {invalid json" wait_time = client._extract_retry_wait_time(error_msg) assert wait_time == CapeSandbox.RETRY_BASE_DELAY def test_handle_429_error_should_retry(mocker, client): """Tests _handle_429_error returns True for 429 error within retry limit.""" error = DemistoException("Error [429] - Too Many Requests") mocker.patch.object(client, "_extract_retry_wait_time", return_value=5) mocker.patch.object(CapeSandbox.time, "sleep") should_retry = client._handle_429_error(error, attempt=0, method="GET", endpoint="/test") assert should_retry is True CapeSandbox.time.sleep.assert_called_once_with(5) def test_handle_429_error_max_retries_exceeded(mocker, client, capfd): """Tests _handle_429_error returns False when max retries exceeded.""" error = DemistoException("Error [429] - Too Many Requests") mocker.patch.object(client, "_extract_retry_wait_time", return_value=5) # Attempt 2 is the last attempt (0, 1, 2 = 3 attempts total) with capfd.disabled(): should_retry = client._handle_429_error( error, attempt=CapeSandbox.MAX_RETRY_ATTEMPTS - 1, method="GET", endpoint="/test", ) assert should_retry is False def test_handle_429_error_non_429_error_no_retry(client): """Tests _handle_429_error returns False for non-429 errors.""" error = DemistoException("Error [500] - Internal Server Error") should_retry = client._handle_429_error(error, attempt=0, method="GET", endpoint="/test") assert should_retry is False def test_http_request_retries_on_429(mocker, client): """Tests http_request retries on 429 error and succeeds on second attempt.""" # First call raises 429, second call succeeds mocker.patch.object( client, "_http_request", side_effect=[ DemistoException('Error [429] - {"detail": "Expected available in 5 seconds."}'), {"data": "success"}, ], ) mocker.patch.object(CapeSandbox.time, "sleep") result = client.http_request("GET", url_suffix="/test") assert result == {"data": "success"} assert client._http_request.call_count == 2 CapeSandbox.time.sleep.assert_called_once() def test_http_request_fails_after_max_retries(mocker, client, capfd): """Tests http_request raises error after max retry attempts.""" error_429 = DemistoException("Error [429] - Too Many Requests") mocker.patch.object(client, "_http_request", side_effect=error_429) mocker.patch.object(CapeSandbox.time, "sleep") with capfd.disabled(), pytest.raises(DemistoException, match="Too Many Requests"): client.http_request("GET", url_suffix="/test") # Should attempt MAX_RETRY_ATTEMPTS times assert client._http_request.call_count == CapeSandbox.MAX_RETRY_ATTEMPTS def test_http_request_non_429_error_no_retry(mocker, client): """Tests http_request doesn't retry on non-429 errors.""" error_500 = DemistoException("Error [500] - Internal Server Error") mocker.patch.object(client, "_http_request", side_effect=error_500) with pytest.raises(DemistoException, match="Internal Server Error"): client.http_request("GET", url_suffix="/test") # Should only attempt once (no retries) assert client._http_request.call_count == 1 # ======================================== def test_test_module(mocker, client): """Tests test_module command returns 'ok'.""" mocker.patch.object(client, "ensure_token", return_value="TOKEN_OK") result = CapeSandbox.test_module(client) assert result == "ok" def test_test_module_failure(mocker, client): """Tests test_module command fails when connectivity fails.""" client_auth = CapeSandboxClient( base_url=SERVER_URL, verify=True, proxy=True, api_token=None, username="user", password="pwd", ) mocker.patch.object(client_auth, "ensure_token", side_effect=DemistoException("Auth server down.")) with pytest.raises(DemistoException, match="Auth server down"): CapeSandbox.test_module(client_auth) # ======================================== # Tests: File & URL Submission Commands # ======================================== def test_cape_file_submit_command(mocker, client): """Tests cape_file_submit_command returns correct outputs.""" mocker.patch.object( CapeSandbox, "extract_entry_file_data", return_value=("test_data/test_file.txt", "test_file.txt"), ) mocker.patch.object( client, "submit_file", return_value={"data": {"task_ids": [123]}}, ) mocker.patch.object( CapeSandbox, "initiate_polling", return_value=CommandResults( readable_output="Polling initiated for task 123", outputs={"id": 123, "target": "test_file.txt", "status": "pending"}, outputs_prefix="Cape.Task.File", ), ) args = {"entry_id": "test_entry_id"} result = cape_file_submit_command(client, args) assert compare_string_ignore_case(result.readable_output, "Polling initiated for task 123") assert result.outputs_prefix == "Cape.Task.File" assert isinstance(result.outputs, dict) outputs = result.outputs assert isinstance(outputs, dict) assert outputs.get("id") == 123 def test_cape_file_submit_command_no_task_id_fail(mocker, client): """Tests submission fails if API returns no task ID.""" mocker.patch.object( CapeSandbox, "extract_entry_file_data", return_value=("path/to/file", "file.exe"), ) mocker.patch.object(client, "submit_file", return_value={"data": {"task_ids": []}}) with pytest.raises(DemistoException, match="No task id returned from CAPE"): cape_file_submit_command(client, {"entry_id": "e_id"}) def test_cape_file_submit_command_pcap_logic(mocker, client): """Tests submission correctly identifies and sets the pcap flag.""" mocker.patch.object( CapeSandbox, "extract_entry_file_data", return_value=("path/to/file.pcap", "file.pcap"), ) mocker.patch.object(client, "submit_file", return_value={"data": {"task_ids": [100]}}) mocker.patch.object(CapeSandbox, "initiate_polling", return_value=CommandResults()) cape_file_submit_command(client, {"entry_id": "e_id"}) client.submit_file.assert_called_with(form=mocker.ANY, file_path=mocker.ANY, is_pcap=True) def test_cape_file_submit_file_not_found_fail(mocker, client): """Tests file submission fails when file is not found.""" mocker.patch.object( CapeSandbox, "extract_entry_file_data", side_effect=DemistoException("Could not find file with entry ID 'invalid_id'."), ) args = {"entry_id": "invalid_id"} with pytest.raises(DemistoException) as excinfo: cape_file_submit_command(client, args) assert "could not find file" in str(excinfo.value).lower() def test_cape_url_submit_command(mocker, client): """Tests cape_url_submit_command returns correct outputs.""" mocker.patch.object( client, "submit_url", return_value={"data": {"task_ids": [456]}}, ) mocker.patch.object( CapeSandbox, "initiate_polling", return_value=CommandResults( readable_output="Polling initiated for URL task 456", outputs={ "id": 456, "target": "http://test.example.com", "status": "pending", }, outputs_prefix="Cape.Task.Url", ), ) args = {"url": "http://test.example.com"} result = cape_url_submit_command(client, args) assert compare_string_ignore_case(result.readable_output, "Polling initiated for URL task 456") assert result.outputs_prefix == "Cape.Task.Url" assert isinstance(result.outputs, dict) outputs = result.outputs assert isinstance(outputs, dict) assert outputs.get("id") == 456 def test_cape_url_submit_command_no_url_fail(client): """Tests URL submission fails if URL argument is missing.""" with pytest.raises(KeyError): cape_url_submit_command(client, {}) def test_cape_url_submit_command_no_task_id_fail(mocker, client): """Tests URL submission fails if API returns no task ID.""" mocker.patch.object(client, "submit_url", return_value={"data": {"task_ids": None}}) with pytest.raises(DemistoException, match="No task id returned from CAPE"): cape_url_submit_command(client, {"url": "http://test.example.com"}) # ======================================== # Tests: File View & Download Commands # ======================================== @pytest.mark.parametrize( "id_type,id_value,client_method", [ ("task_id", "123", "files_view_by_task"), ("md5", MOCK_MD5, "files_view_by_md5"), ("sha256", MOCK_SHA256, "files_view_by_sha256"), ], ) def test_cape_file_view_command_by_id_type(mocker, client, id_type, id_value, client_method): """Tests cape_file_view_command with different ID types.""" mocker.patch.object( client, client_method, return_value=util_load_json("cape_file_view_response.json"), ) args = {id_type: id_value} result = cape_file_view_command(client, args) assert isinstance(result.outputs, dict) outputs = result.outputs assert isinstance(outputs, dict) assert outputs["id"] == "test_task_id" @pytest.mark.parametrize( "id_type,id_value,client_method,expected_filename", [ ("task_id", "123", "files_get_by_task", "cape_task_123_file.bin"), ("md5", MOCK_MD5, "files_get_by_md5", f"cape_file_{MOCK_MD5}.bin"), ("sha1", MOCK_SHA1, "files_get_by_sha1", f"cape_file_{MOCK_SHA1}.bin"), ("sha256", MOCK_SHA256, "files_get_by_sha256", f"cape_file_{MOCK_SHA256}.bin"), ], ) def test_cape_sample_file_download_command_by_id_type(mocker, client, id_type, id_value, client_method, expected_filename): """Tests cape_sample_file_download_command with different ID types.""" mock_content = util_load_file("cape_sample_file_download_response.bin") mocker.patch.object(client, client_method, return_value=mock_content) mocker.patch.object(CapeSandbox, "build_file_name", return_value=expected_filename) mocker.patch( "CapeSandbox.fileResult", return_value={"File": expected_filename, "Contents": mock_content}, ) args = {id_type: id_value} result = cape_sample_file_download_command(client, args) assert result["File"] == expected_filename @pytest.mark.parametrize( "command_func,args", [ (cape_file_view_command, {"task_id": 1, "md5": MOCK_MD5}), (cape_sample_file_download_command, {"task_id": 1, "sha1": MOCK_SHA1}), ], ) def test_commands_multiple_ids_fail(client, command_func, args): """Tests commands fail if multiple ID types are provided.""" with pytest.raises(DemistoException, match="Provide only one of"): command_func(client, args) @pytest.mark.parametrize( "command_func", [ cape_file_view_command, cape_sample_file_download_command, ], ) def test_commands_no_id_fail(client, command_func): """Tests commands fail when no identifying argument is provided.""" args = {} with pytest.raises(DemistoException) as excinfo: command_func(client, args) assert "provide one of:" in str(excinfo.value).lower() # ======================================== # Tests: Task Management Commands # ======================================== def test_cape_pcap_file_download_command(mocker, client): """Tests cape_pcap_file_download_command returns fileResult.""" mock_pcap_content = util_load_file("cape_pcap_file_download_response.pcap") expected_filename = "cape_task_123_network_dump.pcap" mocker.patch.object(client, "get_task_pcap", return_value=mock_pcap_content) mocker.patch.object(CapeSandbox, "build_file_name", return_value=expected_filename) mocker.patch( "CapeSandbox.fileResult", return_value={"File": expected_filename, "Contents": mock_pcap_content}, ) args = {"task_id": "123"} result = cape_pcap_file_download_command(client, args) assert result["File"] == expected_filename @pytest.mark.parametrize( "command_func", [ cape_pcap_file_download_command, cape_task_delete_command, cape_task_report_get_command, cape_task_screenshot_download_command, ], ) def test_commands_missing_task_id_fail(client, command_func): """Tests commands fail when required task_id is missing.""" args = {} with pytest.raises(KeyError): command_func(client, args) def test_cape_task_delete_command(mocker, client): """Tests cape_task_delete_command returns success message.""" mocker.patch.object( client, "delete_task", return_value={"status": "success", "message": "Task deleted successfully"}, ) args = {"task_id": "123"} result = cape_task_delete_command(client, args) expected_output = "Task ID: 123 was deleted successfully" assert compare_string_ignore_case(result.readable_output, expected_output) def test_cape_task_delete_command_no_task_id_fail(client): """Tests cape_task_delete_command fails when task_id is missing.""" args = {} with pytest.raises(KeyError): cape_task_delete_command(client, args) def test_cape_tasks_list_command_multiple_tasks(mocker, client): """Tests cape_tasks_list_command returns multiple task outputs.""" mocker.patch.object( client, "list_tasks", return_value=util_load_json("cape_tasks_list_response.json"), ) args = {"limit": 2, "page": 1} result = cape_tasks_list_command(client, args) assert result.outputs_prefix == "Cape.Task" assert isinstance(result.outputs, list) assert len(result.outputs) == 2 assert result.outputs[0].get("id") == 1 @pytest.mark.parametrize( "args,expected_limit,expected_offset", [ ({"page": -1}, 50, 0), ({"page_size": 200}, 50, 0), ], ) def test_cape_tasks_list_command_invalid_pagination(mocker, client, args, expected_limit, expected_offset): """Tests task list command handles invalid page size/page number.""" mocker.patch.object(client, "list_tasks", return_value={"data": []}) cape_tasks_list_command(client, args) client.list_tasks.assert_called_with(limit=expected_limit, offset=expected_offset) # ======================================== # Tests: Task Report Commands # ======================================== def test_cape_task_report_get_command_json(mocker, client): """Tests cape_task_report_get_command returns JSON report.""" mocker.patch.object( client, "get_task_report", return_value=util_load_json("cape_task_report_get_response.json"), ) args = {"task_id": "123", "format": "json", "zip": False} result = cape_task_report_get_command(client, args) assert result.outputs_prefix == "Cape.Task.Report" assert isinstance(result.outputs, dict) assert result.outputs["id"] == 123 def test_cape_task_report_get_command_zip_download(mocker, client): """Tests cape_task_report_get_command returns zip fileResult.""" mock_content = b"mock zip content" mocker.patch.object(client, "get_task_report", return_value=mock_content) mocker.patch.object(CapeSandbox, "build_file_name", return_value="report.zip") mocker.patch( "CapeSandbox.fileResult", return_value={"File": "report.zip", "Contents": mock_content}, ) args = {"task_id": "123", "zip": True} result = cape_task_report_get_command(client, args) assert result["File"] == "report.zip" def test_cape_task_report_get_command_no_info_fail(mocker, client): """Tests report retrieval raises error if report is empty/missing 'info' key.""" mocker.patch.object( client, "get_task_report", return_value={"status": True, "message": "Report is empty"}, ) with pytest.raises(DemistoException, match="Report is empty"): cape_task_report_get_command(client, {"task_id": 123}) def test_cape_task_report_get_command_json_no_info_path(mocker, client): """Tests report retrieval fails when 'info' is missing.""" mock_resp = { "status": True, "target": {"file": {"name": "test.txt"}}, "id": 123, } mocker.patch.object(client, "get_task_report", return_value=mock_resp) mocker.patch("CapeSandbox.string_to_table_header", side_effect=lambda x: x.upper()) with pytest.raises(DemistoException, match="No info object found in report for task"): cape_task_report_get_command(client, {"task_id": 123}) def test_cape_task_report_get_command_json_target_file_data(mocker, client): """Tests report retrieval includes target file data in readable output.""" mock_resp = { "info": {"id": 123, "started": "1970-01-01"}, "target": {"file": {"name": "test.exe", "sha256": MOCK_SHA256}}, } mocker.patch.object(client, "get_task_report", return_value=mock_resp) mocker.patch("CapeSandbox.string_to_table_header", side_effect=lambda x: x.upper()) result = cape_task_report_get_command(client, {"task_id": "123"}) assert MOCK_SHA256 in result.readable_output assert "test.exe" in result.readable_output def test_cape_task_report_get_command_not_found_fail(mocker, client): """Tests report retrieval fails when API returns 404.""" mocker.patch.object( client, "get_task_report", side_effect=DemistoException("Task with ID 999 not found on CAPE sandbox.", res={"status": 404}), ) args = {"task_id": 999, "format": "json", "zip": False} with pytest.raises(DemistoException) as excinfo: cape_task_report_get_command(client, args) assert "999 not found" in str(excinfo.value) # ======================================== # Tests: Screenshot Commands # ======================================== def test_cape_task_screenshot_download_single(mocker, client): """Tests downloading a single, specified screenshot number.""" mock_content = util_load_file("mock_screenshot_content_1.png") mocker.patch.object(client, "get_task_screenshot", return_value=mock_content) mocker.patch.object(CapeSandbox, "build_file_name", return_value="screenshot_5.png") mocker.patch("CapeSandbox.fileResult", return_value={"File": "screenshot_5.png"}) args = {"task_id": "123", "screenshot": 5} result = cape_task_screenshot_download_command(client, args) assert isinstance(result.outputs, list) assert len(result.outputs) == 1 assert result.outputs[0]["File"] == "screenshot_5.png" def test_cape_task_screenshot_download_all_zip_success(mocker, client): """ Tests the 'download all' (no 'screenshot' arg) success path. - Mocks the client's 'download_all_screenshots_zip' to return bytes. - Verifies the correct file is created. """ mocker.patch.object(client, "download_all_screenshots_zip", return_value=b"zip_content") mocker.patch("CapeSandbox.fileResult", return_value={"Name": "mocked.zip"}) args = {"task_id": "123"} results = cape_task_screenshot_download_command(client, args) client.download_all_screenshots_zip.assert_called_with(task_id="123") assert results.outputs is not None assert isinstance(results.outputs, list) assert len(results.outputs) == 1 assert results.outputs_prefix == "Cape.Task.Screenshot" assert results.outputs[0]["Name"] == "mocked.zip" assert "All Screenshots (ZIP)" in results.readable_output def test_cape_task_screenshot_download_single_success(mocker, client): """ Tests the 'download single' (with 'screenshot' arg) success path. - Mocks the client's 'get_task_screenshot' to return bytes. - Verifies the correct file is created. """ mocker.patch.object(client, "get_task_screenshot", return_value=b"png_content") mocker.patch( "CapeSandbox.fileResult", return_value={"Name": "mocked.png", "ScreenshotNumber": "5"}, ) args = {"task_id": "123", "screenshot": "5"} results = cape_task_screenshot_download_command(client, args) client.get_task_screenshot.assert_called_with(task_id="123", number="5") assert results.outputs is not None assert isinstance(results.outputs, list) assert len(results.outputs) == 1 assert results.outputs_prefix == "Cape.Task.Screenshot" assert results.outputs[0]["Name"] == "mocked.png" assert results.outputs[0]["ScreenshotNumber"] == "5" assert "Screenshot 5" in results.readable_output def test_cape_task_screenshot_download_failure(mocker, client): """ Tests that a client-side exception is correctly raised. This test works for BOTH 'all' and 'single' paths. """ # Mock the 'single' path to fail mocker.patch.object(client, "get_task_screenshot", side_effect=DemistoException("Image Not Found")) args = {"task_id": "123", "screenshot": "99"} # The command should catch the DemistoException and re-raise it with pytest.raises( DemistoException, match="Failed to fetch screenshots for task 123: Image Not Found", ): cape_task_screenshot_download_command(client, args) # Mock the 'all' path to fail mocker.patch.object( client, "download_all_screenshots_zip", side_effect=DemistoException("ZIP Not Found"), ) args_all = {"task_id": "123"} with pytest.raises( DemistoException, match="Failed to fetch screenshots for task 123: ZIP Not Found", ): cape_task_screenshot_download_command(client, args_all) # ======================================== # Tests: Machine Commands # ======================================== def test_cape_machines_list_command_single_machine_view_by_name(mocker, client): """Tests cape_machines_list_command returns single machine.""" mock_resp = {"machine": {"id": 5, "name": "win7"}} mocker.patch.object(client, "view_machine", return_value=mock_resp) result = cape_machines_list_command(client, {"machine_name": "win7"}) assert result.outputs_prefix == "Cape.Machine" assert result.outputs is not None assert isinstance(result.outputs, dict) assert result.outputs["id"] == 5 def test_cape_machines_list_command_multiple_machines(mocker, client): """Tests cape_machines_list_command returns multiple machines.""" mocker.patch.object( client, "list_machines", return_value=util_load_json("cape_machines_list_response.json"), ) args = {"limit": 2, "all_results": False} result = cape_machines_list_command(client, args) assert result.outputs_prefix == "Cape.Machine" assert result.outputs is not None assert isinstance(result.outputs, list) assert len(result.outputs) == 2 assert result.outputs[0].get("id") == 1 # ======================================== # Tests: Status Commands # ======================================== def test_cape_cuckoo_status_get_command_unexpected_response(mocker, client): """Tests cuckoo status command handles empty/non-dict response gracefully.""" mocker.patch.object(client, "get_cuckoo_status", return_value={"message": "status ok"}) result = cape_cuckoo_status_get_command(client, {}) assert result.readable_output is not None assert "N/A" in result.readable_output # ======================================== # Tests: Polling Functions # ======================================== def test_initiate_polling_basic(mocker): """Tests initiate_polling creates correct CommandResults with ScheduledCommand.""" result = CapeSandbox.initiate_polling( command="test-command", args={"arg1": "value1"}, task_id=123, api_target="test.exe", outputs_prefix="Cape.Task.File", ) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Cape.Task.File" outputs = result.outputs assert isinstance(outputs, dict) assert outputs["id"] == 123 assert outputs["target"] == "test.exe" assert outputs["status"] == "pending" assert "Polling initiated" in result.readable_output assert result.scheduled_command is not None scheduled_cmd = result.scheduled_command assert scheduled_cmd._command == "cape-task-poll" # type: ignore[attr-defined] def test_initiate_polling_custom_intervals(): """Tests initiate_polling creates CommandResults with ScheduledCommand.""" result = CapeSandbox.initiate_polling( command="test-command", args={"pollingInterval": 30, "pollingTimeout": 600}, task_id=456, api_target="malware.dll", outputs_prefix="Cape.Task.File", ) # Just verify the scheduled command exists and has the right command name assert result.scheduled_command is not None scheduled_cmd = result.scheduled_command assert scheduled_cmd._command == "cape-task-poll" # type: ignore[attr-defined] def test_cape_task_poll_report_non_429_error_raises(mocker, client): """Tests cape_task_poll_report raises non-429 errors.""" mocker.patch.object( client, "get_task_status", side_effect=DemistoException("Error [500] - Internal Server Error"), ) args = {"task_id": "888", "outputs_prefix": "Cape.Task.File"} with pytest.raises(DemistoException, match="Internal Server Error"): CapeSandbox.cape_task_poll_report(args, client) # ======================================== # Tests: Main Function # ======================================== def test_main_invalid_command_fail(mocker): """Tests main() raises error for invalid/unimplemented command.""" mocker.patch.object(demisto, "command", return_value="invalid-command") mocker.patch.object(demisto, "params", return_value={"url": SERVER_URL, "api_token": "TOKEN"}) mocker.patch.object(demisto, "args", return_value={}) mock_return_error = mocker.patch("CapeSandbox.return_error") CapeSandbox.main() # Verify return_error was called with appropriate message mock_return_error.assert_called_once() error_call_args = mock_return_error.call_args[0][0] assert "invalid-command" in error_call_args assert "not implemented" in error_call_args.lower() def test_main_test_module_success(mocker): """Tests main() executes test-module command successfully.""" mocker.patch.object(demisto, "command", return_value="test-module") mocker.patch.object(demisto, "params", return_value={"url": SERVER_URL, "api_token": "TOKEN"}) mocker.patch.object(demisto, "args", return_value={}) mock_return_results = mocker.patch("CapeSandbox.return_results") CapeSandbox.main() mock_return_results.assert_called_once_with("ok") def test_main_cape_file_submit_success(mocker): """Tests main() executes cape-file-submit command successfully.""" mocker.patch.object(demisto, "command", return_value="cape-file-submit") mocker.patch.object(demisto, "params", return_value={"url": SERVER_URL, "api_token": "TOKEN"}) mocker.patch.object(demisto, "args", return_value={"entry_id": "test_id"}) mocker.patch.object( CapeSandbox, "extract_entry_file_data", return_value=("/tmp/file.exe", "file.exe"), ) mock_client = mocker.MagicMock() mock_client.submit_file.return_value = {"data": {"task_ids": [123]}} mocker.patch.object(CapeSandbox, "CapeSandboxClient", return_value=mock_client) mock_return_results = mocker.patch("CapeSandbox.return_results") mocker.patch.object( CapeSandbox, "initiate_polling", return_value=CommandResults(readable_output="Polling initiated"), ) CapeSandbox.main() mock_return_results.assert_called_once() def test_main_cape_task_poll_success(mocker): """Tests main() executes cape-task-poll command with correct argument passing.""" mocker.patch.object(demisto, "command", return_value="cape-task-poll") mocker.patch.object(demisto, "params", return_value={"url": SERVER_URL, "api_token": "TOKEN"}) mocker.patch.object( demisto, "args", return_value={"task_id": "123", "outputs_prefix": "Cape.Task.File"}, ) mock_client = mocker.MagicMock() mock_client.get_task_status.return_value = {"data": "running"} mocker.patch.object(CapeSandbox, "CapeSandboxClient", return_value=mock_client) mock_return_results = mocker.patch("CapeSandbox.return_results") CapeSandbox.main() # Verify return_results was called (polling result) mock_return_results.assert_called_once() def test_main_command_execution_error(mocker): """Tests main() handles command execution errors gracefully.""" mocker.patch.object(demisto, "command", return_value="cape-file-view") mocker.patch.object(demisto, "params", return_value={"url": SERVER_URL, "api_token": "TOKEN"}) mocker.patch.object(demisto, "args", return_value={}) mock_return_error = mocker.patch("CapeSandbox.return_error") CapeSandbox.main() # Verify error was handled mock_return_error.assert_called_once() error_message = mock_return_error.call_args[0][0] assert "cape-file-view" in error_message.lower() @pytest.mark.parametrize( "command_name,expected_in_map", [ ("test-module", True), ("cape-file-submit", True), ("cape-url-submit", True), ("cape-file-view", True), ("cape-sample-download", True), ("cape-tasks-list", True), ("cape-task-delete", True), ("cape-task-report-get", True), ("cape-pcap-file-download", True), ("cape-machines-list", True), ("cape-cuckoo-status-get", True), ("cape-task-poll", True), ("cape-task-screenshot-download", True), ("non-existent-command", False), ("", False), ], ) def test_command_map_completeness(command_name, expected_in_map): """Tests that COMMAND_MAP contains all expected commands.""" assert (command_name in CapeSandbox.COMMAND_MAP) == expected_in_map