OPSWAT-Metadefender V2
multi-scanning engine uses 30+ anti-malware engines to scan files for threats, significantly increasing malware detection.
Data Enrichment & Threat Intelligence · OPSWAT-Metadefender
Details
| ID | OPSWAT-Metadefender V2 |
|---|---|
| Provider | OPSWAT Inc |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 5.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
multi-scanning engine uses 30+ anti-malware engines to scan files for threats, significantly increasing malware detection.
This integration was integrated and tested with version 5.0.0 of OPSWAT-Metadefender V2.
Configure OPSWAT-Metadefender v2 in Cortex
| Parameter | Required |
|---|---|
| Server URL (e.g. http://localhost:8008/metascan_rest/) | True |
| API Key - Needed in cloud based solutions | False |
| API Key - Needed in cloud based solutions | False |
| Cloud based | False |
| Trust any certificate (not secure) | False |
| Use system proxy settings | False |
| The high threshold | False |
| The low threshold | 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.
opswat-scan-file
Scan file in OPSWAT
Base Command
opswat-scan-file fileId=1191@302
Input
| Argument Name | Description | Required |
|---|---|---|
| fileId | Entry id of a file in XSOAR. | Required |
| scanRule | Name of the Rule to use for scanning (Optional). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| OPSWAT.FileName | string | OPSWAT file name to scan |
| OPSWAT.ScanId | string | OPSWAT scan id of the scan |
opswat-hash
Check file hash on OPSWAT
Base Command
opswat-hash hash=cc273fe9d442850fa18c31c88c823e07
Input
| Argument Name | Description | Required |
|---|---|---|
| hash | File hash (Can be any hash type). | Required |
Context Output
There is no context output for this command.
opswat-scan-result
Get OPSWAT result
Base Command
opswat-scan-result id=123
Input
| Argument Name | Description | Required |
|---|---|---|
| id | OPSWAT scan id. | Required |
Context Output
There is no context output for this command.
opswat-sanitization-result
Get OPSWAT sanitization result (Requires CDR feature).
In order to have sanitized versions of the file, the DeepCDR feature needs to be enabled in the Workflow rule used for scanning the file.
Base Command
opswat-sanitization-result
Input
| Argument Name | Description | Required |
|---|---|---|
| id | OPSWAT scan id. | Required |
Context Output
There is no context output for this command.
Configuration parameters
url— Server URL (e.g. http://localhost:8008/metascan_rest/) (required)api_key— API Key - Needed in cloud based solutionsapi_key_creds—cloud— Cloud basedinsecure— Trust any certificate (not secure)proxy— Use system proxy settingshighPercnt— The high thresholdlowPercnt— The low threshold
Commands (4)
-
opswat-hashCheck file hash on OPSWAT.
-
opswat-sanitization-resultGet OPSWAT sanitization result (Requires CDR feature).
-
opswat-scan-fileScan file in OPSWAT.
-
opswat-scan-resultGet OPSWAT result.
from CommonServerPython import * import pytest import tempfile import requests BASE_URL = "http://localhost:8008/metascan_rest/" SCAN_RESULTS_RES_1: dict = {} SCAN_RESULTS_RES_2: dict = { "file_info": {"file_type_description": "type_desc", "display_name": "display_name", "md5": "some_md5_hash"}, "process_info": {"progress_percentage": 50}, "scan_results": { "total_avs": 100, "scan_all_result_a": "scan_all_result_a", "scan_all_result_i": 100, "scan_details": {"key": {"def_time": "1/1/2023", "threat_found": "threat"}}, }, } HASH_INFO_RES_1: dict = {} HASH_INFO_RES_2: dict = { "file_info": {"display_name": "display_name", "file_type_description": "type_desc"}, "scan_results": { "scan_all_result_a": "scan_all_result_a", "total_detected_avs": 100, "total_avs": 100, "scan_details": {"key": {"def_time": "1/1/2023", "threat_found": "threat"}}, }, } class MockResponse: def __init__(self, json_data, status_code, content=None): self.json_data = json_data self.status_code = status_code self.content = content def json(self): return self.json_data def content(self): return self.content # This method will be used by the mock to replace requests.post def mocked_requests_post(*args, **kwargs): file_name = kwargs.get("headers", {}).get("filename", "") try: if type(file_name) is str: file_name.encode("latin-1") else: file_name.decode("latin-1") return MockResponse({"data_id": "mock_id"}, 200) except Exception as e: return MockResponse(str(e), 404) @pytest.mark.parametrize( "file_name, data, expected_md_results", [ ( "2022年年年年年.docx", "年年年年年", "# OPSWAT-Metadefender\nThe file has been successfully submitted to scan.\nScan id: mock_id\n", ) ], ) def test_scan_file_command(mocker, file_name, data, expected_md_results): """ Given: - File_name and content to mock file_entry. - case 1: a docx file with chinese letters in the name and content When: - Calling scan_file_command. Then: - Ensures the String type was parsed correctly and that the entry was generated correctly. - case 1: Ensures the request didn't fail due to letters parsing issue and that the entry was generated correctly. """ with tempfile.TemporaryDirectory() as temp_dir: file_path = f"{temp_dir}/{file_name}" with open(file_path, "w") as f: f.write(data) mocker.patch.object(demisto, "getFilePath", return_value={"path": file_path, "name": file_name}) mocker.patch.object(demisto, "params", return_value={"url": BASE_URL}) mocker.patch.object(requests, "post", side_effect=mocked_requests_post) mocker.patch.object(demisto, "args", return_value={"fileId": "1191@302", "scanRule": "Test"}) mocker.patch.object(demisto, "results") from OPSWATMetadefenderV2 import scan_file_command scan_file_command() entry = demisto.results.call_args[0][0] ec_results = entry.get("EntryContext", {}).get("OPSWAT", {}) assert entry.get("HumanReadable") == expected_md_results assert ec_results.get("ScanId") == "mock_id" assert ec_results.get("FileName") == file_name # This method will be used by the mock to replace requests.get def mocked_requests_get(*args, **kwargs): url = args[0] data = {} if "file" in url: id = url[-1] if id == "1": data = SCAN_RESULTS_RES_1 elif id == "2": data = SCAN_RESULTS_RES_2 elif "hash" in url: hash = url[-32:] if hash == "9d59494ca97bac09a2fb22188b03961f": data = HASH_INFO_RES_1 elif hash == "9d59494ca97bac09a2fb22188b03961s": data = HASH_INFO_RES_2 return MockResponse(data, 200) @pytest.mark.parametrize( "id, expected_md_results", [ ("1", "# OPSWAT-Metadefender\n### Results for scan id 1\nNo results for this id\n"), ( "2", "# OPSWAT-Metadefender\n### Results for scan id 2\n### The scan proccess is in progrees (done: 50%) \nFile " "name: display_name\nScan result:scan_all_result_a\nDetected AV: 100/100\nAV Name|Def Time|Threat Name Found\n" "---|---|---\nkey|1/1/2023|threat\n", ), ], ) def test_get_scan_result_command(mocker, id, expected_md_results): """ Given: - a file id. - case 1: an id that return empty response - case 2: an id that return a non empty response When: - Running get_scan_result_command. Then: - Ensures the HumanReadable in the result entry was generated correctly. - case 1: Ensures the human readable contains a no results message. - case 2: Ensures the table in the human readable was generated correctly. """ mocker.patch.object(demisto, "params", return_value={"url": BASE_URL}) mocker.patch.object(requests, "get", side_effect=mocked_requests_get) mocker.patch.object(demisto, "args", return_value={"id": id}) mocker.patch.object(demisto, "results") from OPSWATMetadefenderV2 import get_scan_result_command get_scan_result_command() entry = demisto.results.call_args[0][0] assert entry.get("HumanReadable") == expected_md_results @pytest.mark.parametrize( "hash, expected_md_results", [ ("9d59494ca97bac09a2fb22188b03961f", "# OPSWAT-Metadefender\nNo results for hash 9d59494ca97bac09a2fb22188b03961f\n"), ( "9d59494ca97bac09a2fb22188b03961s", "# OPSWAT-Metadefender\nFile name: display_name\nFile description: type_desc\n" "Scan result: scan_all_result_a\nDetected AV: 100/100\nAV Name|Def Time|Threat Name Found\n" "---|---|---\nkey|1/1/2023|threat\n", ), ], ) def test_get_hash_info_command(mocker, hash, expected_md_results): """ Given: - a file hash. - case 1: a hash that return empty response - case 2: a hash that return a non empty response When: - Running get_hash_info_command. Then: - Ensures the HumanReadable in the result entry was generated correctly. - case 1: Ensures the human readable contains a no results message. - case 2: Ensures the table in the human readable was generated correctly. """ mocker.patch.object(demisto, "params", return_value={"url": BASE_URL}) mocker.patch.object(requests, "get", side_effect=mocked_requests_get) mocker.patch.object(demisto, "args", return_value={"hash": hash}) mocker.patch.object(demisto, "results") from OPSWATMetadefenderV2 import get_hash_info_command get_hash_info_command() entry = demisto.results.call_args[0][0] assert entry.get("HumanReadable") == expected_md_results def test_get_sanitized_file_command(mocker): """ Given: - a file id. When: - Running get_sanitized_file_command. Then: - Ensures that sanitized file was created. """ mocker.patch.object(demisto, "args", return_value={"id": "1"}) mocker.patch.object(demisto, "params", return_value={"url": BASE_URL}) mocker.patch( "OPSWATMetadefenderV2.get_scan_result", return_value={ "process_info": {"post_processing": {"converted_destination": "sanitized.pdf", "actions_ran": "Sanitized"}} }, ) mocker.patch("OPSWATMetadefenderV2.get_sanitized_file", return_value=b"sanitized file content") mocker.patch.object(demisto, "results") from OPSWATMetadefenderV2 import get_sanitized_file_command get_sanitized_file_command() entry = demisto.results.call_args[0][0] assert entry.get("File") == "sanitized.pdf" def test_get_sanitized_file_fail_command(mocker): """ Given: - a file id. When: - Running get_sanitized_file_command. Then: - Ensures that sanitized file wasn't created and warning was created. """ mocker.patch.object(demisto, "args", return_value={"id": "1"}) mocker.patch.object(demisto, "params", return_value={"url": BASE_URL}) mocker.patch("OPSWATMetadefenderV2.get_scan_result", return_value={"process_info": {}}) mocker.patch("OPSWATMetadefenderV2.get_sanitized_file", return_value=b"sanitized file content") mocker.patch.object(demisto, "results") from OPSWATMetadefenderV2 import get_sanitized_file_command get_sanitized_file_command() entry = demisto.results.call_args[0][0] assert entry == {"Type": 11, "ContentsFormat": "text", "Contents": "No sanitized file."} @pytest.mark.parametrize( "method, url_suffix, file_name, parse_json, scan_rule, expected_result, expected_status_code", [ # # Test case 1: Basic GET request ("GET", "test", None, True, None, {"success": True}, 200), # # Test case 2: GET request with scan rule ("GET", "hash/123", None, True, "custom_rule", {"file_info": {"md5": "123"}}, 200), # # Test case 3: POST request ("POST", "file", None, True, None, {"data_id": "scan123"}, 200), # # Test case 5: File upload ("POST", "file", "test_file.txt", True, None, {"data_id": "file123"}, 200), # # Test case 6: Non-JSON response ("GET", "file/converted/123", None, False, None, b"test file content", 200), ], ) def test_http_req(mocker, method, url_suffix, file_name, parse_json, scan_rule, expected_result, expected_status_code): """ Given: - Different HTTP request scenarios for the http_req function - Case 1: Basic GET request - Case 2: GET request with custom scan rule - Case 3: POST request - Case 5: File upload - Case 6: Non-JSON response When: - Calling http_req with various parameters Then: - Ensures the function handles different HTTP methods correctly - Ensures proper headers are set based on parameters - Ensures file uploads are handled correctly - Ensures response parsing works as expected """ # Setup mocks mocker.patch.object(demisto, "params", return_value={"url": BASE_URL, "api_key_creds": {"password": "test_api_key"}}) # Mock file operations mock_open = mocker.mock_open(read_data=b"test file content") mocker.patch("builtins.open", mock_open) # Create mock response mock_response = MockResponse(expected_result, expected_status_code, b"test file content") # Mock requests methods if method.upper() == "GET": mocker.patch.object(requests, "get", return_value=mock_response) elif method.upper() == "POST": mocker.patch.object(requests, "post", return_value=mock_response) # Import the function after mocking from OPSWATMetadefenderV2 import http_req # Call the function result = http_req(method=method, url_suffix=url_suffix, file_name=file_name, parse_json=parse_json, scan_rule=scan_rule) # Verify the result assert result == expected_result # Verify the correct request method was called with proper parameters if method.upper() == "GET": requests_mock = requests.get elif method.upper() == "POST": requests_mock = requests.post # Verify URL expected_url = BASE_URL + url_suffix assert requests_mock.call_args[0][0] == expected_url # Verify headers headers = requests_mock.call_args[1]["headers"] assert headers["Accept"] == "application/json" # Verify scan rule if provided if scan_rule: assert headers["rule"] == scan_rule.encode("utf-8") # Verify file upload headers if file_name is provided if file_name: assert headers["content-type"] == "application/octet-stream" assert headers["filename"] == file_name.encode("utf-8") assert mock_open.call_args[0][0] == file_name assert requests_mock.call_args.kwargs["data"].read() == b"test file content" @pytest.mark.parametrize( "status_code, expected_error", [ (400, "Request failed, got status 400: bad request. Check command parameters"), (401, "Request failed, got status 401: unauthorized. Check your API Key"), (404, "Request failed, got status 404: not found. Check integration URL Address"), (500, "Request failed got status 500"), ], ) def test_http_req_error_handling(mocker, status_code, expected_error): """ Given: - Different HTTP error status codes - Case 1: 400 Bad Request - Case 2: 401 Unauthorized - Case 3: 404 Not Found - Case 4: 500 Server Error (not in predefined error codes) When: - Calling http_req and receiving an error status code Then: - Ensures the function handles error codes correctly - Ensures appropriate error messages are returned """ # Setup mocks mocker.patch.object(demisto, "params", return_value={"url": BASE_URL}) # Create mock response with error status mock_response = MockResponse({}, status_code) mocker.patch.object(requests, "get", return_value=mock_response) # Mock return_error function mock_return_error = mocker.patch("OPSWATMetadefenderV2.return_error") # Import the function after mocking from OPSWATMetadefenderV2 import http_req # Call the function http_req(method="GET", url_suffix="test") # Verify return_error was called with the expected error message mock_return_error.assert_called_once_with(expected_error) def test_http_req_unsupported_method(mocker): """ Given: - An unsupported HTTP method When: - Calling http_req with an unsupported method Then: - Ensures the function handles unsupported methods correctly - Ensures appropriate error message is returned """ # Setup mocks mocker.patch.object(demisto, "params", return_value={"url": BASE_URL}) # Mock return_error function mock_return_error = mocker.patch("OPSWATMetadefenderV2.return_error") # Import the function after mocking from OPSWATMetadefenderV2 import http_req # Call the function with unsupported method result = http_req(method="PUT", url_suffix="test") # Verify return_error was called with the expected error message mock_return_error.assert_called_once_with("Got unsupporthed http method: PUT") # Verify the function returns an empty string assert result == "" @pytest.mark.parametrize( "input_name, expected_basename", [ ("/tmp/evil/../../../etc/passwd", "passwd"), ("simple_file.txt", "simple_file.txt"), ("/absolute/path/to/report.pdf", "report.pdf"), ("../relative/path/data.csv", "data.csv"), ("no_extension", "no_extension"), ], ) def test_scan_file_uses_basename(mocker, input_name, expected_basename): """ Given: - A file entry with various name formats including directory path components. When: - Calling scan_file. Then: - Verify that only the basename of the file name is used. """ from pathlib import Path import demistomock as demisto mocker.patch.object( demisto, "getFilePath", return_value={"path": "README.md", "name": input_name}, ) mocker.patch.object(demisto, "params", return_value={"url": BASE_URL}) mock_copy = mocker.patch("shutil.copy") mocker.patch("os.remove") mocker.patch( "OPSWATMetadefenderV2.http_req", return_value={"data_id": "test123"}, ) from OPSWATMetadefenderV2 import scan_file _, file_name = scan_file("entry1") # Verify the file name returned is the basename only assert file_name == expected_basename assert Path(file_name).name == file_name # Verify shutil.copy was called with the sanitized basename copy_call_args = mock_copy.call_args[0] assert copy_call_args[1] == expected_basename def test_scan_file_cleanup_handles_os_error(mocker): """ Given: - A file entry where os.remove raises an OSError during cleanup. When: - Calling scan_file. Then: - Verify that the OSError is suppressed and the result is returned. """ import demistomock as demisto mocker.patch.object( demisto, "getFilePath", return_value={"path": "README.md", "name": "test_file.txt"}, ) mocker.patch.object(demisto, "params", return_value={"url": BASE_URL}) mocker.patch("shutil.copy") mocker.patch("os.remove", side_effect=OSError("File not found")) mock_debug = mocker.patch.object(demisto, "debug") mocker.patch( "OPSWATMetadefenderV2.http_req", return_value={"data_id": "test123"}, ) from OPSWATMetadefenderV2 import scan_file result, file_name = scan_file("entry1") # Verify the function completed successfully despite the OSError assert result == {"data_id": "test123"} assert file_name == "test_file.txt" # Verify debug was called with the cleanup failure message mock_debug.assert_called_once_with("Could not remove temporary file: test_file.txt")