MetaDefender Aether

Next-generation unified Zero-Day detection solution, combining Threat Reputation, Dynamic Analysis, Threat Scoring, and Threat Hunting into a single adaptive detection pipeline (previously known as MetaDefender Sandbox).

Forensics & Malware Analysis · MetaDefender Aether

Details

IDMetaDefender Aether
CategoryForensics & Malware Analysis
From Version6.10.0
Docker Imagedemisto/fastapi:0.125.0.10158186

README

Next-generation unified Zero-Day detection solution, combining the proven capabilities of MetaDefender Sandbox with built-in Threat Intelligence, Threat Scoring and Threat Hunting - all delivered as a single adaptive detection pipeline. (previously known as MetaDefender Sandbox)

Configure MetaDefender Aether on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for MetaDefender Aether.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Required
    Server URL (e.g. https://www.filescan.io/api) True
    API Key True
    Trust any certificate (not secure) False
    Use system proxy settings False
  4. Click Test to validate the URLs, the API Key and connection.

Commands

You can execute these commands from the Cortex XSOAR 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.

metadefender-aether-scan-url


Scan URL with MetaDefender Aether

Note: MetaDefender Aether handles URL scanning as a file scan.

Notice: Submitting indicators using this command might make the indicator data publicly available. See the vendor’s documentation for more details.

Base Command

metadefender-aether-scan-url

Input

Argument Name Description Required
url The URL to submit Required
timeout The timeout for the polling in seconds Optional
hide_polling_output Hide polling output Optional
description Uploaded file/url description Optional
tags Tags array to propagate Optional
password Custom password, in case uploaded archive is protected Optional
is_private If file should not be available for download by other users Optional

Context Output

Path Type Description
DBotScore.Indicator String The indicator that was tested.
DBotScore.Type String The indicator type.
DBotScore.Vendor String The vendor used to calculate the score.
DBotScore.Score Number The actual score.
File.Name String The full file name.
File.SHA256 String The SHA256 hash of the file.
File.Malicious.Vendor String The vendor that reported the file as malicious.
MetaDefender.Aether.Analysis.finalVerdict.verdict String The final verdict.
MetaDefender.Aether.Analysis.allTags Unknown All tags.
MetaDefender.Aether.Analysis.overallState String Overall state of the scan.
MetaDefender.Aether.Analysis.taskReference.name String Name of the main scan task.
MetaDefender.Aether.Analysis.taskReference.additionalInfo Unknown Additional information about the main scan task.
MetaDefender.Aether.Analysis.taskReference.ID String ID of the main scan task.
MetaDefender.Aether.Analysis.taskReference.state String State of the main scan task.
MetaDefender.Aether.Analysis.taskReference.resourceReference Unknown Resource reference of the main scan task.
MetaDefender.Aether.Analysis.taskReference.opcount Number Counter.
MetaDefender.Aether.Analysis.taskReference.processTime Number processTime.
MetaDefender.Aether.Analysis.subtaskReferences Unknown Status of scan subtasks.
MetaDefender.Aether.Analysis.allSignalGroups Unknown All signal groups.
MetaDefender.Aether.Analysis.resources Unknown Resources.
MetaDefender.Aether.Analysis.file.name String The name of the file.
MetaDefender.Aether.Analysis.file.hash String The SHA256 of the file.
MetaDefender.Aether.Analysis.file.type String The type of the submission.

Command example

!metadefender-aether-scan-url url=https://www.test.com

Context Example

{
    "DBotScore":
    [
        {
            "Indicator": "1111111111111111111111111111111111111111111111111111111111111111",
            "Score": 1,
            "Type": "file",
            "Vendor": "MetaDefender Aether"
        }
    ],
    "File":
    [
        {
            "Name": "https://www.test.com",
            "SHA256": "1111111111111111111111111111111111111111111111111111111111111111"
        }
    ],
    "MetaDefender":
    {
        "Aether":
        {
            "Analysis":
            [
                {
                    "finalVerdict":
                    {
                        "verdict": "BENIGN"
                    },
                    "allTags":
                    [
                        {
                            "source": "MEDIA_TYPE",
                            "sourceIdentifier": "12345678",
                            "isRootTag": true,
                            "tag":
                            {
                                "name": "html",
                                "synonyms":
                                [],
                                "descriptions":
                                [],
                                "verdict":
                                {
                                    "verdict": "INFORMATIONAL",
                                    "threatLevel": 0.1,
                                    "confidence": 1
                                }
                            }
                        }
                    ],
                    "overallState": "success_partial",
                    "taskReference":
                    {
                        "name": "transform-file",
                        "additionalInfo":
                        {
                            "submitName": "https://www.test.com",
                            "submitTime": 1679014774270,
                            "digests":
                            {
                                "SHA-256": "1111111111111111111111111111111111111111111111111111111111111111"
                            }
                        },
                        "ID": "abcd-1234",
                        "state": "SUCCESS",
                        "resourceReference":
                        {
                            "type": "TRANSFORM_FILE",
                            "name": "file",
                            "ID": "abcd-5678"
                        },
                        "opcount": 1,
                        "processTime": 20350
                    },
                    "subtaskReferences":
                    [
                        {
                            "name": "domain-resolve",
                            "additionalInfo": 72,
                            "ID": "12345678",
                            "state": "SUCCESS",
                            "resourceReference":
                            {
                                "type": "DOMAIN_RESOLVE",
                                "name": "domain-resolve",
                                "ID": "123456789"
                            },
                            "opcount": 20,
                            "processTime": 11309
                        }
                    ],
                    "allSignalGroups":
                    [
                        {
                            "identifier": "I000",
                            "description": "OSINT source detected malicious resource",
                            "averageSignalStrength": 0.75,
                            "peakSignalStrength": 0.75,
                            "finalSignalStrength": 0.75,
                            "verdict":
                            {
                                "verdict": "LIKELY_MALICIOUS",
                                "threatLevel": 0.75,
                                "confidence": 1
                            },
                            "allTags":
                            [],
                            "signals":
                            [
                                {
                                    "strength": 0.75,
                                    "isStrictlyBasedOnInputData": false,
                                    "signalReadable": "OSINT provider TEST provider (2/93)",
                                    "additionalInfo": "https://www.google.com",
                                    "originPath": "osint.results.verdict",
                                    "originType": "INPUT_FILE",
                                    "originIdentifier": "1234"
                                }
                            ]
                        }
                    ],
                    "resources":
                    {
                        "00f1e4d6-27fb-45e8-8a02-dc53818044ec":
                        {
                            "resourceReference":
                            {
                                "name": "osint"
                            },
                            "results":
                            []
                        }
                    },
                    "file":
                    {
                        "name": "https://www.test.com",
                        "hash": "1111111111111111111111111111111111111111111111111111111111111111",
                        "type": "other"
                    }
                }
            ]
        }
    }
}

Human Readable Output

Scan Result (digest)

FileHash FileName FileType FinalVerdict SubtaskReferences Tags
1111111111111111111111111111111111111111111111111111111111111111 https://www.test.com other BENIGN osint, url-render, domain-resolve html, png

metadefender-aether-scan-file


Scan File with MetaDefender Aether

Base Command

metadefender-aether-scan-file

Input

Argument Name Description Required
entry_id The War Room entry ID of the file to submit Required
timeout The timeout for the polling in seconds Optional
hide_polling_output Hide polling output Optional
description Uploaded file/url description Optional
tags Tags array to propagate Optional
password Custom password, in case uploaded archive is protected Optional
is_private If file should not be available for download by other users Optional

Context Output

Path Type Description
DBotScore.Indicator String The indicator that was tested.
DBotScore.Type String The indicator type.
DBotScore.Vendor String The vendor used to calculate the score.
DBotScore.Score Number The actual score.
File.Name String The full file name.
File.SHA256 String The SHA256 hash of the file.
File.Malicious.Vendor String The vendor that reported the file as malicious.
MetaDefender.Aether.Analysis.finalVerdict.verdict String The final verdict.
MetaDefender.Aether.Analysis.allTags Unknown All tags.
MetaDefender.Aether.Analysis.overallState String Overall state of the scan.
MetaDefender.Aether.Analysis.taskReference.name String Name of the main scan task.
MetaDefender.Aether.Analysis.taskReference.additionalInfo Unknown Additional information about the main scan task.
MetaDefender.Aether.Analysis.taskReference.ID String ID of the main scan task.
MetaDefender.Aether.Analysis.taskReference.state String State of the main scan task.
MetaDefender.Aether.Analysis.taskReference.resourceReference Unknown Resource reference of the main scan task.
MetaDefender.Aether.Analysis.taskReference.opcount Number Counter.
MetaDefender.Aether.Analysis.taskReference.processTime Number processTime.
MetaDefender.Aether.Analysis.subtaskReferences Unknown Status of scan subtasks.
MetaDefender.Aether.Analysis.allSignalGroups Unknown All signal groups.
MetaDefender.Aether.Analysis.resources Unknown Resources.
MetaDefender.Aether.Analysis.file.name String The name of the file.
MetaDefender.Aether.Analysis.file.hash String The SHA256 of the file.
MetaDefender.Aether.Analysis.file.type String The type of the submission.

Command example

!metadefender-aether-scan-file entry_id=1234@abcd-efgh-ijkl-mnop-xyz

Context Example

{
    "DBotScore":
    [
        {
            "Indicator": "1111111111111111111111111111111111111111111111111111111111111111",
            "Score": 1,
            "Type": "file",
            "Vendor": "MetaDefender Aether"
        }
    ],
    "File":
    [
        {
            "Name": "1234@abcd-efgh-ijkl-mnop-xyz",
            "SHA256": "1111111111111111111111111111111111111111111111111111111111111111"
        }
    ],
    "MetaDefender":
    {
        "Aether":
        {
            "Analysis":
            [
                {
                    "finalVerdict":
                    {
                        "verdict": "MALICIOUS"
                    },
                    "allTags":
                    [
                        {
                            "source": "SIGNAL",
                            "sourceIdentifier": "1234",
                            "isRootTag": false,
                            "tag": 
                            {
                                "name": "packed",
                                "synonyms": [],
                                "descriptions": [],
                                "verdict": {
                                    "verdict": "SUSPICIOUS",
                                    "threatLevel": 0.5,
                                    "confidence": 1
                                }
                            }
                        }
                    ],
                    "overallState": "success_partial",
                    "taskReference":
                    {
                        "name": "transform-file",
                        "additionalInfo": {
                            "submitName": "bad_file.exe",
                            "submitTime": 1679011634945,
                            "digests": {
                                "SHA-256": "1111111111111111111111111111111111111111111111111111111111111111"
                            }
                        },
                        "ID": "1234",
                        "state": "SUCCESS",
                        "resourceReference": {
                            "type": "TRANSFORM_FILE",
                            "name": "file",
                            "ID": "0101010101"
                        },
                        "opcount": 1,
                        "processTime": 7180
                    },
                    "subtaskReferences":
                    [
                        {
                            "name": "domain-resolve",
                            "additionalInfo": 72,
                            "ID": "12345678",
                            "state": "SUCCESS",
                            "resourceReference":
                            {
                                "type": "DOMAIN_RESOLVE",
                                "name": "domain-resolve",
                                "ID": "123456789"
                            },
                            "opcount": 20,
                            "processTime": 11309
                        }
                    ],
                    "allSignalGroups":
                    [
                        {
                            "identifier": "Y002",
                            "description": "Matched a malicious YARA rule",
                            "averageSignalStrength": 1,
                            "peakSignalStrength": 1,
                            "finalSignalStrength": 1,
                            "verdict": {
                                "verdict": "MALICIOUS",
                                "threatLevel": 1,
                                "confidence": 1
                            },
                            "allTags": [],
                            "signals": [
                                {
                                    "strength": 1,
                                    "isStrictlyBasedOnInputData": true,
                                    "signalReadable": "Matched YARA with strength \"0.75\"",
                                    "additionalInfo": "PUP_InstallRex_AntiFWb",
                                    "originPath": "file.yaraMatches",
                                    "originType": "INPUT_FILE",
                                    "originIdentifier": "111111111111111111111111111"
                                }
                            ]
                        }
                    ],
                    "resources":
                    {
                        "00f1e4d6-27fb-45e8-8a02-dc53818044ec":
                        {
                            "resourceReference":
                            {
                                "name": "osint"
                            },
                            "results":
                            []
                        }
                    },
                    "file":
                    {
                        "name": "1234@abcd-efgh-ijkl-mnop-xyz",
                        "hash": "1111111111111111111111111111111111111111111111111111111111111111",
                        "type": "other"
                    }
                }
            ]
        }
    }
}

Human Readable Output

Scan Result (digest)

FileHash FileName FileType FinalVerdict SubtaskReferences Tags
1111111111111111111111111111111111111111111111111111111111111111 1234@abcd-efgh-ijkl-mnop-xyz pe MALICIOUS visualization, osint, domain-resolve html, peexe

metadefender-aether-search-query


Search for reports. Finds reports and uploaded files by various tokens.

Base Command

metadefender-aether-search-query

Input

Argument Name Description Required
query The query string Required
limit Number of total results. Maximum 50 Optional
page Page number, starting from 1 Optional
page_size The page size. Can be 5, 10 or 20 Optional

Context Output

Path Type Description
MetaDefender.Aether.Analysis.id String The analysis id.
MetaDefender.Aether.Analysis.file.name String The name of the file.
MetaDefender.Aether.Analysis.file.sha256 String The SHA256 of the file.
MetaDefender.Aether.Analysis.verdict String The final verdict.
MetaDefender.Aether.Analysis.state String Overall state of the scan.
MetaDefender.Aether.Analysis.date Date The scan date.
MetaDefender.Aether.Analysis.file.mime_type String The file MimeType.
MetaDefender.Aether.Analysis.file.short_type String The type of the submission.
MetaDefender.Aether.Analysis.tags Unknown All tags.

Command example

!metadefender-aether-search-query query="834d1dbfab8330ea5f1844f6e905ed0ac19d1033ee9a9f1122ad2051c56783dc"

Context Example

{
    "MetaDefender":
    {
        "Aether":
        {
            "Analysis":
            [
                {
                    "id": "b4f92c03-0fc2-4a40-9d34-8f2b05dd240c",
                    "file": {
                        "name": "bad_file.exe",
                        "mime_type": "application/x-msdownload",
                        "short_type": "peexe",
                        "sha256": "834d1dbfab8330ea5f1844f6e905ed0ac19d1033ee9a9f1122ad2051c56783dc"
                    },
                    "state": "success",
                    "verdict": "malicious",
                    "tags": [
                        {
                            "source": "MEDIA_TYPE",
                            "sourceIdentifier": "834d1dbfab8330ea5f1844f6e905ed0ac19d1033ee9a9f1122ad2051c56783dc",
                            "isRootTag": true,
                            "tag": {
                                "name": "peexe",
                                "synonyms": [],
                                "descriptions": [],
                                "verdict": {
                                    "verdict": "INFORMATIONAL",
                                    "threatLevel": 0.1,
                                    "confidence": 1
                                }
                            }
                        }
                    ],
                    "date": "03/20/2023, 14:28:09"
                }
            ]
        }
    }
}

Human Readable Output

Analysis Result

Id SampleName SHA256 Verdict State Date
8c38be8c-7cfd-4d64-be41-c98a795c9ce0 bad_file.exe 834d1dbfab8330ea5f1844f6e905ed0ac19d1033ee9a9f1122ad2051c56783dc MALICIOUS success_partial 03/14/2023, 15:07:07
e334d27f-e2b1-46c9-9936-7d3155eb3706 bad_file.exe 834d1dbfab8330ea5f1844f6e905ed0ac19d1033ee9a9f1122ad2051c56783dc UNKNOWN success 03/14/2020, 15:03:48

Configuration parameters

  • url — Server URL (e.g. https://www.filescan.io/api) (required)
  • api_key — (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (3)

  • metadefender-aether-scan-file

    Scan file resource.

  • metadefender-aether-scan-url

    Scan URL resource.

  • metadefender-aether-search-query

    Search for reports. Finds reports and uploaded files by various tokens.

from typing import Any
from CommonServerPython import CommandResults, DemistoException
import json
import pytest
import importlib

MD_Aether = importlib.import_module("MetaDefenderAether")


def util_load_json(path: str) -> Any:
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


@pytest.fixture()
def client():
    return MD_Aether.Client(base_url="https://test.com", api_key="mockkey", proxy=False, verify=False)


APIKEY_VALIDATION_SUCCESS = {
    "accountId": "1234",
    "username": "Aniko",
    "email": "aniko@o.com",
}


@pytest.mark.parametrize("result, expected", [(APIKEY_VALIDATION_SUCCESS, "ok")])
def test_test_module_positive(mocker, client, result, expected):
    mocker.patch.object(client, "test_module", return_value=result)
    response = MD_Aether.test_module_command(client)
    assert response == expected


APIKEY_VALIDATION_FAILURE = {"detail": "Could not validate credentials"}


@pytest.mark.parametrize("result, expected", [(APIKEY_VALIDATION_FAILURE, DemistoException)])
def test_test_module_negative(mocker, client, result, expected):
    mocker.patch.object(client, "test_module", return_value=result)
    with pytest.raises(Exception) as e:
        MD_Aether.test_module_command(client)
    assert isinstance(e.value, expected)


def test_search_query_command_hash_badfile(mocker, client):
    raw_response = util_load_json("test_data/query_hash_badfile.json")

    mocker.patch.object(client, "get_search_query", return_value=raw_response)
    response = MD_Aether.search_query_command(client, {})
    assert response[0].outputs["file"]["sha256"] == "834d1dbfab8330ea5f1844f6e905ed0ac19d1033ee9a9f1122ad2051c56783dc"
    assert response[0].outputs["verdict"] == "malicious"


def test_search_query_command_hash_cleanfile(mocker, client):
    raw_response = util_load_json("test_data/query_hash_cleanfile.json")

    mocker.patch.object(client, "get_search_query", return_value=raw_response)
    response = MD_Aether.search_query_command(client, {})

    assert response[0].outputs["file"]["sha256"] == "b280719e9f2dd010260e6a023e0d69c64fbee8b6cbb8669c722a1da8142d3325"
    assert response[0].outputs["verdict"] == "no_threat"


def test_search_query_command_url(mocker, client):
    raw_response = util_load_json("test_data/query_url_github.json")

    mocker.patch.object(client, "get_search_query", return_value=raw_response)
    response = MD_Aether.search_query_command(client, {})

    assert len(response) == 10

    assert response[0].outputs["file"]["sha256"] == "09a8b930c8b79e7c313e5e741e1d59c39ae91bc1f10cdefa68b47bf77519be57"
    assert response[0].outputs["verdict"] == "no_threat"
    assert response[1].outputs["file"]["sha256"] == "5adca05a86dbcaaa1049b14b364a9ddf305e2476064e2c0590e4ebb49696fa3b"
    assert response[4].outputs["verdict"] == "suspicious"


@pytest.mark.parametrize(
    "args, outputs",
    [
        ({"limit": "-1"}, DemistoException),
        ({"limit": "100"}, DemistoException),
        ({"limit": "a"}, Exception),
        ({"page": "-1"}, DemistoException),
        ({"page": "a"}, Exception),
        ({"page_size": "1"}, DemistoException),
        ({"page_size": "a"}, Exception),
    ],
)
def test_search_query_command_argument_check(mocker, client, args, outputs):
    mocker.patch.object(client, "get_search_query", return_value={})
    with pytest.raises(Exception) as e:
        MD_Aether.search_query_command(client, args)
    assert isinstance(e.value, outputs)


def test_scan_command_url_polling_waiting(requests_mock, mocker, client):
    from CommonServerPython import ScheduledCommand

    mocker.patch.object(client, "_http_request", return_value={"flow_id": "1234"})
    mocker.patch.object(ScheduledCommand, "raise_error_if_not_supported", return_value=None)
    mocker.patch("builtins.open", create=True)

    args = {
        "entry_id": "test_entry_id",
        "description": "test_file",
        "tags": "tag1",
        "password": "pass1234",
        "is_private": True,
    }

    response = MD_Aether.scan_command(client, args)
    assert response.readable_output == 'Waiting for submission "1234" to finish...'


def test_scan_command_url_polling(mocker, client):
    from CommonServerPython import ScheduledCommand

    args = {"url": "https://github.com/"}
    raw_response = util_load_json("test_data/scan_command_url_response.json")
    mocker.patch.object(client, "_http_request", return_value={"flow_id": "1234"})
    mocker.patch.object(ScheduledCommand, "raise_error_if_not_supported", return_value=None)

    response = MD_Aether.scan_command(client, args)
    assert response.readable_output == 'Waiting for submission "1234" to finish...'

    polling_args = {
        "flow_id": "1234",
        "hide_polling_output": True,
        "continue_to_poll": True,
        "url": "test.com",
    }
    mocker.patch.object(client, "get_scan_result", return_value=raw_response)
    response = MD_Aether.scan_command(client, polling_args)

    assert response[0].indicator.dbot_score.indicator == "09a8b930c8b79e7c313e5e741e1d59c39ae91bc1f10cdefa68b47bf77519be57"
    assert response[0].indicator.dbot_score.score == 1
    assert response[0].indicator.dbot_score.integration_name == "MetaDefender Aether"
    assert response[0].indicator.name == "https://github.com/"
    assert response[0].indicator.sha256 == "09a8b930c8b79e7c313e5e741e1d59c39ae91bc1f10cdefa68b47bf77519be57"
    assert response[0].outputs["finalVerdict"]["threatLevel"] == 0.25
    assert len(response[0].outputs["allTags"]) == 4
    assert response[0].outputs["overallState"] == "success_partial"
    assert response[0].outputs["taskReference"]["name"] == "transform-file"
    assert response[0].outputs["file"]["name"] == "https://github.com/"
    assert response[0].outputs["file"]["hash"] == "09a8b930c8b79e7c313e5e741e1d59c39ae91bc1f10cdefa68b47bf77519be57"
    assert response[0].outputs["file"]["type"] == "other"


def test_scan_command_file_polling(mocker, client):
    from CommonServerPython import ScheduledCommand

    args = {"entry_id": "test_entry_id"}
    raw_response = util_load_json("test_data/scan_command_zip_response.json")
    mocker.patch.object(client, "post_sample", return_value={"flow_id": "1234"})
    mocker.patch.object(ScheduledCommand, "raise_error_if_not_supported", return_value=None)

    response = MD_Aether.scan_command(client, args)
    assert response.readable_output == 'Waiting for submission "1234" to finish...'

    polling_args = {
        "flow_id": "1234",
        "hide_polling_output": True,
        "continue_to_poll": True,
        "url": "test.com",
    }
    mocker.patch.object(client, "_http_request", return_value=raw_response)
    response = MD_Aether.scan_command(client, polling_args)

    assert len(response) == 3

    assert response[0].indicator.dbot_score.indicator == "834d1dbfab8330ea5f1844f6e905ed0ac19d1033ee9a9f1122ad2051c56783dc"
    assert response[0].indicator.dbot_score.score == 3
    assert response[0].indicator.dbot_score.integration_name == "MetaDefender Aether"
    assert response[0].indicator.name == "bad_file.exe"
    assert response[0].indicator.sha256 == "834d1dbfab8330ea5f1844f6e905ed0ac19d1033ee9a9f1122ad2051c56783dc"
    assert response[0].outputs["finalVerdict"]["threatLevel"] == 1
    assert len(response[0].outputs["allTags"]) == 9
    assert response[0].outputs["overallState"] == "success_partial"
    assert response[0].outputs["taskReference"]["name"] == "transform-file"
    assert response[0].outputs["file"]["name"] == "bad_file.exe"
    assert response[0].outputs["file"]["hash"] == "834d1dbfab8330ea5f1844f6e905ed0ac19d1033ee9a9f1122ad2051c56783dc"
    assert response[0].outputs["file"]["type"] == "pe"

    assert response[1].indicator.dbot_score.indicator == "ede5221225a03b12d11df11f4babf24e9c4a55e05aff27612813dd44876a71c2"
    assert response[1].indicator.dbot_score.score == 1
    assert response[1].indicator.dbot_score.integration_name == "MetaDefender Aether"
    assert response[1].indicator.name == "munkaltatoi.docx"
    assert response[1].indicator.sha256 == "ede5221225a03b12d11df11f4babf24e9c4a55e05aff27612813dd44876a71c2"
    assert response[1].outputs["finalVerdict"]["threatLevel"] == 0.25
    assert len(response[1].outputs["allTags"]) == 3
    assert response[1].outputs["overallState"] == "success"
    assert response[1].outputs["taskReference"]["name"] == "transform-file"
    assert response[1].outputs["file"]["name"] == "munkaltatoi.docx"
    assert response[1].outputs["file"]["hash"] == "ede5221225a03b12d11df11f4babf24e9c4a55e05aff27612813dd44876a71c2"
    assert response[1].outputs["file"]["type"] == "ms-office"

    assert response[2].indicator.dbot_score.indicator == "2ee79f9a52e660f2322985c72c9dffefdfb5a3c302576d61b4e629d049098cb5"
    assert response[2].indicator.dbot_score.score == 1
    assert response[2].indicator.dbot_score.integration_name == "MetaDefender Aether"
    assert response[2].indicator.name == "poorguy.png"
    assert response[2].indicator.sha256 == "2ee79f9a52e660f2322985c72c9dffefdfb5a3c302576d61b4e629d049098cb5"
    assert response[2].outputs["finalVerdict"]["threatLevel"] == 0.25
    assert len(response[2].outputs["allTags"]) == 2
    assert response[2].outputs["overallState"] == "success"
    assert response[2].outputs["taskReference"]["name"] == "transform-file"
    assert response[2].outputs["file"]["name"] == "poorguy.png"
    assert response[2].outputs["file"]["hash"] == "2ee79f9a52e660f2322985c72c9dffefdfb5a3c302576d61b4e629d049098cb5"
    assert response[2].outputs["file"]["type"] == "other"


def test_scan_command_file_invalid_password(mocker, client):
    from CommonServerPython import ScheduledCommand

    args = {"entry_id": "test_entry_id"}
    raw_response = util_load_json("test_data/scan_command_zip_invalid_pass.json")
    mocker.patch.object(client, "post_sample", return_value={"flow_id": "1234"})
    mocker.patch.object(ScheduledCommand, "raise_error_if_not_supported", return_value=None)

    response = MD_Aether.scan_command(client, args)
    assert response.readable_output == 'Waiting for submission "1234" to finish...'

    polling_args = {
        "flow_id": "1234",
        "hide_polling_output": True,
        "continue_to_poll": True,
        "url": "test.com",
    }
    mocker.patch.object(client, "get_scan_result", return_value=raw_response)

    with pytest.raises(Exception) as e:
        MD_Aether.scan_command(client, polling_args)
    assert isinstance(e.value, DemistoException)


def test_password_validator():
    raw_response = util_load_json("test_data/scan_command_zip_valid_pass.json")
    is_valid = MD_Aether.is_valid_pass(raw_response)
    assert is_valid


@pytest.mark.parametrize(
    "report, DBotScore",
    [
        (
            {
                "finalVerdict": {
                    "verdict": "UNDETERMINED",
                    "threatLevel": 0,
                }
            },
            0,
        ),
        (
            {
                "finalVerdict": {
                    "verdict": "TRUSTED",
                    "threatLevel": -1,
                }
            },
            1,
        ),
        (
            {
                "finalVerdict": {
                    "verdict": "NO_THREAT_DETECTED",
                    "threatLevel": 0.25,
                }
            },
            1,
        ),
        (
            {
                "finalVerdict": {
                    "verdict": "CONFIRMED_THREAT",
                    "threatLevel": 1.0,
                }
            },
            3,
        ),
        (
            {
                "finalVerdict": {
                    "verdict": "HIGH_RISK",
                    "threatLevel": 0.75,
                }
            },
            3,
        ),
        (
            {
                "finalVerdict": {
                    "verdict": "LOW_RISK",
                    "threatLevel": 0.5,
                }
            },
            2,
        ),
        (
            {
                "finalVerdict": {
                    "verdict": "SOME_FANCY",
                    "threatLevel": "not_a_number",
                }
            },
            0,
        ),
    ],
)
def test_build_one_reputation_result(report, DBotScore):
    reputation_result = MD_Aether.build_one_reputation_result(report)
    score = reputation_result.indicator.dbot_score.score
    assert score == DBotScore


def test_post_sample_url(mocker, client):
    """Test post_sample submits a URL and returns the API response."""
    mocker.patch.object(client, "_http_request", return_value={"flow_id": "url-1234"})
    result = client.post_sample({"url": "https://example.com", "description": "test", "tags": "t1"})
    assert result == {"flow_id": "url-1234"}
    client._http_request.assert_called_once_with(
        method="POST",
        url_suffix="/scan/url",
        ok_codes=(200,),
        data={"url": "https://example.com", "description": "test", "tags": "t1"},
    )


def test_post_sample_no_url_no_entry_raises(client):
    """Test post_sample raises when neither url nor entry_id is provided."""
    with pytest.raises(DemistoException, match="No file or URL was provided."):
        client.post_sample({})


def test_post_sample_file(mocker, client):
    """Test post_sample submits a file via entry_id."""
    import demistomock as demisto

    mocker.patch.object(demisto, "getFilePath", return_value={"path": "/tmp/test.txt", "name": "test.txt"})
    mock_open = mocker.mock_open(read_data=b"file-content")
    mocker.patch("builtins.open", mock_open)
    mocker.patch.object(client, "_http_request", return_value={"flow_id": "file-5678"})

    result = client.post_sample({"entry_id": "entry123", "password": "secret", "is_private": True})
    assert result == {"flow_id": "file-5678"}


def test_post_sample_file_not_found(mocker, client):
    """Test post_sample raises when entry_id file is not found."""
    import demistomock as demisto

    mocker.patch.object(demisto, "getFilePath", side_effect=Exception("File not found"))
    with pytest.raises(DemistoException, match="Failed to find file entry"):
        client.post_sample({"entry_id": "bad-entry"})


def test_search_query_command_empty_results(mocker, client):
    """Test search_query_command returns 'No Results' when no items are found."""
    mocker.patch.object(client, "get_search_query", return_value={"items": []})
    response = MD_Aether.search_query_command(client, {"query": "nonexistent"})
    assert response.readable_output == "No Results were found."


def test_search_query_command_with_page_and_page_size(mocker, client):
    """Test search_query_command with explicit page and page_size."""
    raw_response = util_load_json("test_data/query_hash_badfile.json")
    mocker.patch.object(client, "get_search_query", return_value=raw_response)

    response = MD_Aether.search_query_command(client, {"query": "test", "page": "1", "page_size": "10"})
    assert len(response) == 2
    client.get_search_query.assert_called_once_with("test", 1, 10)


def test_search_query_command_with_page_only(mocker, client):
    """Test search_query_command defaults page_size to 10 when only page is given."""
    raw_response = util_load_json("test_data/query_hash_cleanfile.json")
    mocker.patch.object(client, "get_search_query", return_value=raw_response)

    response = MD_Aether.search_query_command(client, {"query": "test", "page": "2"})
    assert len(response) == 1
    client.get_search_query.assert_called_once_with("test", 2, 10)


def test_search_query_command_with_page_size_only(mocker, client):
    """Test search_query_command defaults page to 1 when only page_size is given."""
    raw_response = util_load_json("test_data/query_hash_cleanfile.json")
    mocker.patch.object(client, "get_search_query", return_value=raw_response)

    response = MD_Aether.search_query_command(client, {"query": "test", "page_size": "5"})
    assert len(response) == 1
    client.get_search_query.assert_called_once_with("test", 1, 5)


def test_search_query_command_auto_pagination(mocker, client):
    """Test search_query_command paginates automatically and respects limit."""

    def _make_item(i: int) -> dict:
        sha = f"{i:064x}"
        return {"id": f"item-{i}", "file": {"name": f"file{i}.txt", "sha256": sha}, "verdict": "no_threat"}

    page1 = {"items": [_make_item(i) for i in range(5)]}
    page2 = {"items": [_make_item(i) for i in range(5, 8)]}

    mocker.patch.object(client, "get_search_query", side_effect=[page1, page2])
    response = MD_Aether.search_query_command(client, {"query": "test", "limit": "8"})
    assert len(response) == 8
    assert client.get_search_query.call_count == 2


def test_scan_command_timeout_argument(mocker, client):
    """Test scan_command passes custom timeout to the ScheduledCommand."""
    from CommonServerPython import ScheduledCommand
    import demistomock as demisto

    mocker.patch.object(demisto, "command", return_value="metadefender-aether-scan-url")
    mocker.patch.object(demisto, "args", return_value={"url": "https://example.com", "timeout": "120"})
    mocker.patch.object(client, "_http_request", return_value={"flow_id": "t-1234"})
    mocker.patch.object(ScheduledCommand, "raise_error_if_not_supported", return_value=None)

    # Re-apply the decorator with the patched demisto.args()
    reimported = importlib.reload(MD_Aether)
    new_client = reimported.Client(base_url="https://test.com", api_key="mockkey", proxy=False, verify=False)
    mocker.patch.object(new_client, "_http_request", return_value={"flow_id": "t-1234"})

    response = reimported.scan_command(new_client, {"url": "https://example.com", "timeout": "120"})
    assert response.scheduled_command._timeout == "120"


def test_scan_command_timeout_none_keeps_default(mocker, client):
    """Test scan_command uses default timeout (600) when not provided."""
    from CommonServerPython import ScheduledCommand
    import demistomock as demisto

    mocker.patch.object(demisto, "command", return_value="metadefender-aether-scan-url")
    mocker.patch.object(demisto, "args", return_value={"url": "https://example.com"})
    mocker.patch.object(ScheduledCommand, "raise_error_if_not_supported", return_value=None)

    reimported = importlib.reload(MD_Aether)
    new_client = reimported.Client(base_url="https://test.com", api_key="mockkey", proxy=False, verify=False)
    mocker.patch.object(new_client, "_http_request", return_value={"flow_id": "t-5678"})

    response = reimported.scan_command(new_client, {"url": "https://example.com"})
    assert response.scheduled_command._timeout == "600"


def test_polling_still_in_progress(mocker, client):
    """Test polling returns a scheduled result when scan is not finished."""
    from CommonServerPython import ScheduledCommand

    mocker.patch.object(ScheduledCommand, "raise_error_if_not_supported", return_value=None)
    in_progress_response = {
        "flow_id": "prog-1234",
        "allFinished": False,
        "reports": {},
    }
    mocker.patch.object(client, "get_scan_result", return_value=in_progress_response)

    response = MD_Aether.scan_command(client, {"flow_id": "prog-1234", "continue_to_poll": True})
    # The @polling_function decorator returns the partial_result (or default poll_message)
    # as a CommandResults, not a PollResult, when continue_to_poll=True.
    assert isinstance(response, CommandResults)
    assert response.readable_output is not None


def test_build_reputation_result_multiple_reports():
    """Test build_reputation_result handles multiple reports."""
    api_response = {
        "reports": {
            "report-1": {
                "finalVerdict": {"verdict": "NO_THREAT", "threatLevel": 0.25},
                "file": {"name": "file1.txt", "hash": "hash1", "type": "text"},
                "allTags": [],
                "subtaskReferences": [],
            },
            "report-2": {
                "finalVerdict": {"verdict": "CONFIRMED_THREAT", "threatLevel": 1.0},
                "file": {"name": "file2.exe", "hash": "hash2", "type": "pe"},
                "allTags": [],
                "subtaskReferences": [],
            },
        }
    }
    results = MD_Aether.build_reputation_result(api_response)
    assert len(results) == 2
    scores = {r.indicator.dbot_score.score for r in results}
    assert 1 in scores  # GOOD
    assert 3 in scores  # BAD


def test_build_search_query_result_empty():
    """Test build_search_query_result returns empty list for empty input."""
    results = MD_Aether.build_search_query_result([])
    assert results == []


def test_sample_submission(mocker, client):
    """Test sample_submission returns a PollResult with correct flow_id."""
    from CommonServerPython import ScheduledCommand

    mocker.patch.object(ScheduledCommand, "raise_error_if_not_supported", return_value=None)
    mocker.patch.object(client, "_http_request", return_value={"flow_id": "sub-9999"})

    result = MD_Aether.sample_submission(client, {"url": "https://example.com"})
    assert result.continue_to_poll is True
    assert result.args_for_next_run["flow_id"] == "sub-9999"
    assert 'Waiting for submission "sub-9999"' in result.partial_result.readable_output


def test_is_valid_pass_no_rejected_key():
    """Test is_valid_pass returns True when rejected_files key is absent."""
    assert MD_Aether.is_valid_pass({"flow_id": "123", "reports": {}}) is True


def test_is_valid_pass_invalid():
    """Test is_valid_pass returns False for INVALID_PASSWORD."""
    response = {"rejected_files": [{"rejected_reason": "INVALID_PASSWORD"}]}
    assert MD_Aether.is_valid_pass(response) is False


def test_is_valid_pass_other_reason():
    """Test is_valid_pass returns True for non-password rejection reasons."""
    response = {"rejected_files": [{"rejected_reason": "FILE_TOO_LARGE"}]}
    assert MD_Aether.is_valid_pass(response) is True


def test_main_test_module(mocker):
    """Test main() dispatches test-module command correctly."""
    import demistomock as demisto

    mocker.patch.object(demisto, "params", return_value={"url": "https://test.com", "api_key": {"password": "key123"}})
    mocker.patch.object(demisto, "command", return_value="test-module")
    mocker.patch.object(demisto, "args", return_value={})
    mocker.patch.object(MD_Aether, "test_module_command", return_value="ok")
    MD_Aether.main()
    MD_Aether.test_module_command.assert_called_once()


def test_main_unknown_command(mocker):
    """Test main() raises NotImplementedError for unknown commands."""
    import demistomock as demisto

    mocker.patch.object(demisto, "params", return_value={"url": "https://test.com", "api_key": {"password": "key123"}})
    mocker.patch.object(demisto, "command", return_value="unknown-command")
    mocker.patch.object(demisto, "args", return_value={})
    mock_return_error = mocker.patch("MetaDefenderAether.return_error")

    MD_Aether.main()
    assert mock_return_error.called
    assert "not implemented" in mock_return_error.call_args[0][0].lower()


def test_main_scan_url_command(mocker):
    """Test main() dispatches scan-url command correctly."""
    import demistomock as demisto

    mocker.patch.object(demisto, "params", return_value={"url": "https://test.com", "api_key": {"password": "key123"}})
    mocker.patch.object(demisto, "command", return_value="metadefender-aether-scan-url")
    mocker.patch.object(demisto, "args", return_value={"url": "https://example.com"})
    mock_scan = mocker.patch.object(MD_Aether, "scan_command", return_value="mock-result")
    mocker.patch.object(demisto, "results")

    MD_Aether.main()
    mock_scan.assert_called_once()