Binalyze AIR Extended

Manage Binalyze AIR forensic acquisition, endpoint isolation, triage, cases, tasks, assets, repositories, and evidence artifacts from Cortex XSOAR.

Forensics & Malware Analysis · Binalyze AIR Extended

Details

IDBinalyze AIR Extended
ProviderBinalyze
CategoryForensics & Malware Analysis
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658

README

Manage Binalyze AIR forensic acquisition, endpoint isolation, triage, cases, tasks, assets, repositories, and evidence artifacts from Cortex XSOAR.
This integration was integrated and tested with Binalyze AIR Extended.

Configure Binalyze AIR Extended in Cortex

Parameter Description Required
Binalyze AIR Server URL The Binalyze AIR Server URL, for example https://air.example.com. True
API Key The Binalyze AIR API token, for example api_1234567890abcdef1234567890abcdef. True
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.

binalyze-air-isolate


Isolate an endpoint or release endpoint isolation.

Base Command

binalyze-air-isolate

Input

Argument Name Description Required
hostname The endpoint hostname. Required
organization_id The organization ID of the endpoint. Required
isolation The isolation action to perform. Possible values are: enable, disable. Required

Context Output

Path Type Description
BinalyzeAIR.Isolate.Result.ID string The isolation task ID.
BinalyzeAIR.Isolate.Result.Name string The isolation task name.
BinalyzeAIR.Isolate.Result.OrganizationID number The endpoint organization ID.

binalyze-air-acquire


Start forensic evidence acquisition from an endpoint.

Base Command

binalyze-air-acquire

Input

Argument Name Description Required
hostname The endpoint hostname. Required
profile The acquisition profile name. Possible values are: compromise-assessment, browsing-history, event-logs, memory-ram-pagefile, quick, full. Required
case_id The Binalyze AIR case ID. Required
organization_id The organization ID of the endpoint. Required

Context Output

Path Type Description
BinalyzeAIR.Acquire.Result.ID string The acquisition task ID.
BinalyzeAIR.Acquire.Result.Name string The acquisition task name.
BinalyzeAIR.Acquire.Result.OrganizationID number The endpoint organization ID.

binalyze-air-create-case


Create a Binalyze AIR case.

Base Command

binalyze-air-create-case

Input

Argument Name Description Required
name The case name. Required
organization_id The organization ID. Required
owner_user_id The owner user ID. Required
visibility The case visibility. Possible values are: public-to-organization, private-to-users, Public to Organization, Private to Users. Required
assigned_user_ids A comma-separated list of assigned user IDs. Optional

Context Output

Path Type Description
BinalyzeAIR.Case.Result.ID string The case ID.
BinalyzeAIR.Case.Result.Name string The case name.

binalyze-air-get-case


Get a Binalyze AIR case by ID.

Base Command

binalyze-air-get-case

Input

Argument Name Description Required
case_id The case ID. Required

Context Output

Path Type Description
BinalyzeAIR.Case unknown The case details.

binalyze-air-list-cases


List Binalyze AIR cases.

Base Command

binalyze-air-list-cases

Input

Argument Name Description Required
name The optional case name filter. Optional
organization_id The optional organization ID filter. Optional
organization_ids A comma-separated list of organization IDs. Optional
page The page number from which to start pagination. Default is 1. Optional
limit The maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
BinalyzeAIR.Cases unknown The case list response.

binalyze-air-close-case


Close a Binalyze AIR case.

Base Command

binalyze-air-close-case

Input

Argument Name Description Required
case_id The case ID. Required
reason The closure reason. Optional

Context Output

Path Type Description
BinalyzeAIR.CloseCase unknown The close case response.

binalyze-air-get-case-tasks


Get tasks associated with a Binalyze AIR case.

Base Command

binalyze-air-get-case-tasks

Input

Argument Name Description Required
case_id The case ID. Required
task_id The optional task ID filter. Optional
page The page number from which to start pagination. Default is 1. Optional
limit The maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
BinalyzeAIR.CaseTask unknown The case task response.

binalyze-air-get-case-endpoints


Get endpoints associated with a Binalyze AIR case.

Base Command

binalyze-air-get-case-endpoints

Input

Argument Name Description Required
case_id The case ID. Required
page The page number from which to start pagination. Default is 1. Optional
limit The maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
BinalyzeAIR.CaseEndpoint unknown The case endpoint response.

binalyze-air-get-case-activities


Get activity history associated with a Binalyze AIR case.

Base Command

binalyze-air-get-case-activities

Input

Argument Name Description Required
case_id The case ID. Required
page The page number from which to start pagination. Default is 1. Optional
limit The maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
BinalyzeAIR.CaseActivity unknown The case activity response.

binalyze-air-list-assets


List Binalyze AIR endpoints/assets with optional filters.

Base Command

binalyze-air-list-assets

Input

Argument Name Description Required
hostname The optional endpoint hostname filter. Optional
organization_id The optional organization ID filter. Optional
organization_ids A comma-separated list of organization IDs. Optional
online_status A comma-separated list of values. Optional
isolation_status A comma-separated list of values. Optional
platform A comma-separated list of values. Optional
page The page number from which to start pagination. Default is 1. Optional
limit The maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
BinalyzeAIR.Asset unknown The asset list response.

binalyze-air-get-asset


Get a Binalyze AIR endpoint/asset by asset ID.

Base Command

binalyze-air-get-asset

Input

Argument Name Description Required
asset_id The asset ID. Required

Context Output

Path Type Description
BinalyzeAIR.Asset unknown The asset details.

binalyze-air-get-asset-by-hostname


Find a Binalyze AIR endpoint/asset by hostname and organization ID.

Base Command

binalyze-air-get-asset-by-hostname

Input

Argument Name Description Required
hostname The endpoint hostname. Required
organization_id The organization ID. Required

Context Output

Path Type Description
BinalyzeAIR.Asset.Result unknown The first matching asset.

binalyze-air-get-asset-tasks


Get tasks associated with an endpoint/asset.

Base Command

binalyze-air-get-asset-tasks

Input

Argument Name Description Required
asset_id The asset ID. Required
page The page number from which to start pagination. Default is 1. Optional
limit The maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
BinalyzeAIR.AssetTask unknown The asset task response.

binalyze-air-get-task


Get task details and normalized terminal status flags for polling.

Base Command

binalyze-air-get-task

Input

Argument Name Description Required
task_id The task ID. Required

Context Output

Path Type Description
BinalyzeAIR.Task.Result unknown The task details.
BinalyzeAIR.Task.Status string The normalized task status.
BinalyzeAIR.Task.IsDone boolean Whether the task is in a terminal state.
BinalyzeAIR.Task.IsSuccess boolean Whether the task completed successfully.

binalyze-air-list-tasks


List Binalyze AIR tasks.

Base Command

binalyze-air-list-tasks

Input

Argument Name Description Required
case_id The optional case ID filter. Optional
organization_id The optional organization ID filter. Optional
organization_ids A comma-separated list of organization IDs. Optional
status The optional status filter. Optional
task_type The optional task type filter. Optional
page The page number from which to start pagination. Default is 1. Optional
limit The maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
BinalyzeAIR.Task unknown The task list response.

binalyze-air-get-task-assignments


Get task assignment details for a Binalyze AIR task.

Base Command

binalyze-air-get-task-assignments

Input

Argument Name Description Required
task_id The task ID. Required
page The page number from which to start pagination. Default is 1. Optional
limit The maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
BinalyzeAIR.TaskAssignment unknown The task assignment response.

binalyze-air-create-triage-rule


Create a YARA, Sigma, or osquery triage rule.

Base Command

binalyze-air-create-triage-rule

Input

Argument Name Description Required
description The rule description. Optional
rule The rule content. Required
engine The rule engine. Possible values are: yara, sigma, osquery. Required
search_in The search scope. Possible values are: system, memory, both, event-records. Optional
organization_ids A comma-separated list of organization IDs. Optional

Context Output

Path Type Description
BinalyzeAIR.TriageRule unknown The created triage rule response.

binalyze-air-update-triage-rule


Update an existing triage rule.

Base Command

binalyze-air-update-triage-rule

Input

Argument Name Description Required
rule_id The triage rule ID. Required
description The rule description. Optional
rule The rule content. Optional
search_in The search scope. Possible values are: system, memory, both, event-records. Optional
organization_ids A comma-separated list of organization IDs. Optional

Context Output

Path Type Description
BinalyzeAIR.TriageRule unknown The updated triage rule response.

binalyze-air-validate-triage-rule


Validate a YARA, Sigma, or osquery triage rule before assignment.

Base Command

binalyze-air-validate-triage-rule

Input

Argument Name Description Required
rule The rule content. Required
engine The rule engine. Possible values are: yara, sigma, osquery. Required

Context Output

Path Type Description
BinalyzeAIR.TriageRuleValidation.Result unknown The validation result payload.
BinalyzeAIR.TriageRuleValidation.Success boolean Whether validation succeeded.

binalyze-air-list-triage-rules


List triage rules.

Base Command

binalyze-air-list-triage-rules

Input

Argument Name Description Required
organization_id The optional organization ID filter. Optional
organization_ids A comma-separated list of organization IDs. Optional
engine The optional rule engine filter. Possible values are: yara, sigma, osquery. Optional
search_in The optional search scope filter. Possible values are: system, memory, both, event-records. Optional
description The optional description filter. Optional
page The page number from which to start pagination. Default is 1. Optional
limit The maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
BinalyzeAIR.TriageRule unknown The triage rule list response.

binalyze-air-get-triage-rule


Get a triage rule by ID.

Base Command

binalyze-air-get-triage-rule

Input

Argument Name Description Required
rule_id The triage rule ID. Required

Context Output

Path Type Description
BinalyzeAIR.TriageRule unknown The triage rule details.

binalyze-air-delete-triage-rule


Delete a triage rule by ID.

Base Command

binalyze-air-delete-triage-rule

Input

Argument Name Description Required
rule_id The triage rule ID. Required

Context Output

Path Type Description
BinalyzeAIR.DeleteTriageRule unknown The delete triage rule response.

binalyze-air-assign-triage-task


Assign one or more triage rules to endpoints by filter.

Base Command

binalyze-air-assign-triage-task

Input

Argument Name Description Required
case_id The case ID. Required
triage_rule_ids A comma-separated list of triage rule IDs. Required
organization_id The organization ID. Required
task_config_choice The task configuration mode. Possible values are: use-policy, use-custom-options. Default is use-policy. Optional
task_config_cpu_limit The CPU limit. Minimum 1, maximum 100. Default is 8. Optional
hostname The endpoint hostname filter. Optional
mitre_attack Whether to enable MITRE ATT&CK mapping. Possible values are: True, False. Default is False. Optional
included_endpoint_ids A comma-separated list of included endpoint IDs. Optional
excluded_endpoint_ids A comma-separated list of excluded endpoint IDs. Optional
group_id The optional endpoint group ID. Optional
group_full_path The optional endpoint group full path. Optional
isolation_status A comma-separated list of isolation status values. Optional
platform A comma-separated list of platform values. Optional
issue The optional issue filter. Optional
online_status A comma-separated list of online status values. Optional
tags A comma-separated list of tags. Optional
version The optional agent version filter. Optional
policy The optional policy filter. Optional
when The scheduler value. Default is now. Optional

Context Output

Path Type Description
BinalyzeAIR.TriageTask unknown The assign triage task response.

binalyze-air-list-acquisition-profiles


List acquisition profiles.

Base Command

binalyze-air-list-acquisition-profiles

Input

Argument Name Description Required
name The optional profile name filter. Optional
organization_id The optional organization ID filter. Optional
organization_ids A comma-separated list of organization IDs. Optional
page The page number from which to start pagination. Default is 1. Optional
limit The maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
BinalyzeAIR.AcquisitionProfile unknown The acquisition profile list response.

binalyze-air-get-acquisition-profile


Get acquisition profile details by ID.

Base Command

binalyze-air-get-acquisition-profile

Input

Argument Name Description Required
profile_id The acquisition profile ID. Required

Context Output

Path Type Description
BinalyzeAIR.AcquisitionProfile unknown The acquisition profile details.

binalyze-air-list-repositories


List Binalyze AIR repositories.

Base Command

binalyze-air-list-repositories

Input

Argument Name Description Required
page The page number from which to start pagination. Default is 1. Optional
limit The maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
BinalyzeAIR.Repository unknown The repository list response.

binalyze-air-get-repository


Get repository details by ID.

Base Command

binalyze-air-get-repository

Input

Argument Name Description Required
repository_id The repository ID. Required

Context Output

Path Type Description
BinalyzeAIR.Repository unknown The repository details.

binalyze-air-download-file


Download a file from the Binalyze AIR InterACT library into the Cortex XSOAR War Room.

Base Command

binalyze-air-download-file

Input

Argument Name Description Required
file_name The file name to download from the InterACT library. Required

Context Output

There is no context output for this command

Command Examples

The command examples for all integration commands are maintained in the command_examples.txt file and were used with demisto-sdk generate-docs.

The examples are grouped logically by operation type, including assets, tasks, acquisition, cases, triage rules, acquisition profiles, repositories, file download, and endpoint response.

Configuration parameters

  • server — Binalyze AIR Server URL (required)
  • api_key — (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (28)

  • binalyze-air-acquire

    Start forensic evidence acquisition from an endpoint.

  • binalyze-air-assign-triage-task

    Assign one or more triage rules to endpoints by filter.

  • binalyze-air-close-case

    Close a Binalyze AIR case.

  • binalyze-air-create-case

    Create a Binalyze AIR case.

  • binalyze-air-create-triage-rule

    Create a YARA, Sigma, or osquery triage rule.

  • binalyze-air-delete-triage-rule

    Delete a triage rule by ID.

  • binalyze-air-download-file

    Download a file from the Binalyze AIR InterACT library into the Cortex XSOAR War Room.

  • binalyze-air-get-acquisition-profile

    Get acquisition profile details by ID.

  • binalyze-air-get-asset

    Get a Binalyze AIR endpoint/asset by asset ID.

  • binalyze-air-get-asset-by-hostname

    Find a Binalyze AIR endpoint/asset by hostname and organization ID.

  • binalyze-air-get-asset-tasks

    Get tasks associated with an endpoint/asset.

  • binalyze-air-get-case

    Get a Binalyze AIR case by ID.

  • binalyze-air-get-case-activities

    Get activity history associated with a Binalyze AIR case.

  • binalyze-air-get-case-endpoints

    Get endpoints associated with a Binalyze AIR case.

  • binalyze-air-get-case-tasks

    Get tasks associated with a Binalyze AIR case.

  • binalyze-air-get-repository

    Get repository details by ID.

  • binalyze-air-get-task

    Get task details and normalized terminal status flags for polling.

  • binalyze-air-get-task-assignments

    Get task assignment details for a Binalyze AIR task.

  • binalyze-air-get-triage-rule

    Get a triage rule by ID.

  • binalyze-air-isolate

    Isolate an endpoint or release endpoint isolation.

  • binalyze-air-list-acquisition-profiles

    List acquisition profiles.

  • binalyze-air-list-assets

    List Binalyze AIR endpoints/assets with optional filters.

  • binalyze-air-list-cases

    List Binalyze AIR cases.

  • binalyze-air-list-repositories

    List Binalyze AIR repositories.

  • binalyze-air-list-tasks

    List Binalyze AIR tasks.

  • binalyze-air-list-triage-rules

    List triage rules.

  • binalyze-air-update-triage-rule

    Update an existing triage rule.

  • binalyze-air-validate-triage-rule

    Validate a YARA, Sigma, or osquery triage rule before assignment.

import sys
from pathlib import Path
from unittest.mock import MagicMock

import pytest

CONTENT_ROOT = Path(__file__).resolve().parents[4]
INTEGRATION_DIR = Path(__file__).resolve().parent

sys.path.insert(0, str(CONTENT_ROOT))
sys.path.insert(0, str(INTEGRATION_DIR))
sys.path.insert(0, str(CONTENT_ROOT / "Tests" / "demistomock"))
sys.path.insert(0, str(CONTENT_ROOT / "Tests" / "scripts"))

for scripts_root in (
    CONTENT_ROOT / "Packs" / "Base" / "Scripts",
    CONTENT_ROOT / "Packs" / "ApiModules" / "Scripts",
):
    if scripts_root.exists():
        for script_dir in scripts_root.iterdir():
            if script_dir.is_dir():
                sys.path.insert(0, str(script_dir))

import CommonServerPython as csp  # noqa: E402

if not hasattr(csp, "ContentClient"):
    csp.ContentClient = csp.BaseClient

from CommonServerPython import DemistoException  # noqa: E402
from BinalyzeAIRExtended import (  # noqa: E402
    Client,
    clean_params,
    remove_empty_values,
    required_int_arg,
    status_from_task,
    visibility_value,
)


def test_clean_params_removes_empty_values():
    assert clean_params({"a": "x", "b": "", "c": None, "d": [], "e": [1, 2]}) == {
        "a": "x",
        "e": "1,2",
    }


def test_remove_empty_values_recursive():
    payload = {
        "caseId": "C-1",
        "filter": {
            "name": "HOST1",
            "groupId": "",
            "tags": [],
            "organizationIds": [0],
        },
        "empty": "",
    }

    assert remove_empty_values(payload) == {
        "caseId": "C-1",
        "filter": {
            "name": "HOST1",
            "organizationIds": [0],
        },
    }


def test_required_int_arg_invalid():
    with pytest.raises(DemistoException):
        required_int_arg({"organization_id": "invalid"}, "organization_id")


def test_required_int_arg_valid():
    assert required_int_arg({"organization_id": "0"}, "organization_id") == 0
    assert required_int_arg({"organization_id": 1}, "organization_id") == 1


def test_visibility_value_normalizes_human_readable_values():
    assert visibility_value("Public to Organization") == "public-to-organization"
    assert visibility_value("Private to Users") == "private-to-users"
    assert visibility_value("public-to-organization") == "public-to-organization"
    assert visibility_value("private-to-users") == "private-to-users"


def test_status_from_task_normalizes_terminal_status():
    assert status_from_task({"status": "Completed"}) == "completed"
    assert status_from_task({"state": {"name": "Failed"}}) == "failed"
    assert status_from_task({}) == "unknown"


def test_get_asset_by_hostname_uses_params():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"result": {"entities": []}})

    client.get_asset_by_hostname("HOST123", 0)

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/endpoints",
        params={"filter[name]": "HOST123", "filter[organizationIds]": 0},
    )


def test_get_profile_id_with_preset_profile_does_not_call_api():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock()

    assert client.get_profile_id("quick", 0) == "quick"
    client._http_request.assert_not_called()


def test_get_profile_id_custom_profile_found():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(
        return_value={
            "result": {
                "entities": [
                    {
                        "name": "custom",
                        "_id": "profile-id",
                    }
                ]
            }
        }
    )

    assert client.get_profile_id("custom", 0) == "profile-id"


def test_get_profile_id_custom_profile_uses_params():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(
        return_value={
            "result": {
                "entities": [
                    {
                        "name": "custom",
                        "_id": "profile-id",
                    }
                ]
            }
        }
    )

    client.get_profile_id("custom", 0)

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/acquisitions/profiles",
        params={"filter[name]": "custom", "filter[organizationIds]": 0},
    )


def test_get_profile_id_custom_profile_not_found():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"result": {"entities": []}})

    with pytest.raises(DemistoException):
        client.get_profile_id("missing-profile", 0)


def test_assign_triage_task_payload_is_cleaned():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"success": True})

    client.assign_triage_task(
        {
            "case_id": "CASE-1",
            "triage_rule_ids": "RULE-1",
            "organization_id": "0",
            "hostname": "HOST123",
            "group_id": "",
            "mitre_attack": "False",
        }
    )

    sent_payload = client._http_request.call_args.kwargs["json_data"]

    assert sent_payload["caseId"] == "CASE-1"
    assert sent_payload["triageRuleIds"] == ["RULE-1"]
    assert sent_payload["filter"]["organizationIds"] == [0]
    assert sent_payload["filter"]["name"] == "HOST123"
    assert "groupId" not in sent_payload["filter"]

    client._http_request.assert_called_once_with(
        method="POST",
        url_suffix="/api/public/triages/triage",
        json_data=sent_payload,
    )


def test_list_cases_uses_params():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"result": {"entities": []}})

    client.list_cases(
        {
            "name": "Case",
            "organization_ids": "0,1",
            "page": 1,
            "limit": 50,
        }
    )

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/cases",
        params={
            "filter[name]": "Case",
            "filter[organizationIds]": "0,1",
            "page": 1,
            "limit": 50,
        },
    )


def test_download_file_uses_params():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"content": b"test"})

    client.download_file("evidence.zip")

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/interact/library/download",
        params={"filename": "evidence.zip"},
        resp_type="response",
    )


def test_air_isolate_sets_enabled_payload():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"success": True})

    client.air_isolate("HOST123", 0, "enable")

    client._http_request.assert_called_once_with(
        method="POST",
        url_suffix="/api/public/endpoints/tasks/isolation",
        json_data={"enabled": True, "filter": {"name": "HOST123", "organizationIds": [0]}},
    )


def test_get_case_uses_case_id_path():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"_id": "CASE-1"})

    client.get_case("CASE-1")

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/cases/CASE-1",
    )


def test_close_case_posts_reason():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"success": True})

    client.close_case("CASE-1", "Resolved")

    client._http_request.assert_called_once_with(
        method="POST",
        url_suffix="/api/public/cases/CASE-1/close",
        json_data={"reason": "Resolved"},
    )


@pytest.mark.parametrize(
    "relation",
    [
        "tasks",
        "endpoints",
        "activities",
    ],
)
def test_get_case_related_uses_standard_pagination(relation):
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"result": {"entities": []}})

    client.get_case_related("CASE-1", relation, {"page": "2", "limit": "25"})

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix=f"/api/public/cases/CASE-1/{relation}",
        params={"page": 2, "limit": 25},
    )


def test_get_case_tasks_can_filter_by_task_id():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"result": {"entities": []}})

    client.get_case_related("CASE-1", "tasks", {"task_id": "TASK-1"})

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/cases/CASE-1/tasks",
        params={"taskId": "TASK-1", "page": 1, "limit": 50},
    )


def test_list_assets_uses_standard_pagination_and_filters():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"result": {"entities": []}})

    client.list_assets(
        {
            "hostname": "HOST123",
            "organization_id": "0",
            "online_status": "online",
            "page": "2",
            "limit": "25",
        }
    )

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/endpoints",
        params={
            "filter[name]": "HOST123",
            "filter[organizationIds]": "0",
            "filter[onlineStatus]": "online",
            "page": 2,
            "limit": 25,
        },
    )


def test_get_asset_uses_asset_id_path():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"_id": "ASSET-1"})

    client.get_asset("ASSET-1")

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/endpoints/ASSET-1",
    )


def test_get_asset_tasks_uses_standard_pagination():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"result": {"entities": []}})

    client.get_asset_tasks("ASSET-1", {"page": "2", "limit": "25"})

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/endpoints/ASSET-1/tasks",
        params={"page": 2, "limit": 25},
    )


def test_list_tasks_uses_standard_pagination_and_filters():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"result": {"entities": []}})

    client.list_tasks(
        {
            "case_id": "CASE-1",
            "organization_id": "0",
            "status": "completed",
            "task_type": "acquisition",
            "page": "3",
            "limit": "10",
        }
    )

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/tasks",
        params={
            "filter[caseIds]": "CASE-1",
            "filter[organizationIds]": "0",
            "filter[status]": "completed",
            "filter[type]": "acquisition",
            "page": 3,
            "limit": 10,
        },
    )


def test_get_task_assignments_uses_standard_pagination():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"result": {"entities": []}})

    client.get_task_assignments("TASK-1", {"page": "2", "limit": "25"})

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/tasks/TASK-1/assignments",
        params={"page": 2, "limit": 25},
    )


def test_update_triage_rule_payload_is_cleaned():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"success": True})

    client.update_triage_rule(
        description="Suspicious PowerShell",
        rule="rule content",
        search_in="system",
        rule_id="RULE-1",
        organization_ids=[],
    )

    client._http_request.assert_called_once_with(
        method="PUT",
        url_suffix="/api/public/triages/rules/RULE-1",
        json_data={
            "description": "Suspicious PowerShell",
            "rule": "rule content",
            "searchIn": "system",
        },
    )


def test_delete_triage_rule_uses_delete_method():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"success": True})

    client.delete_triage_rule("RULE-1")

    client._http_request.assert_called_once_with(
        method="DELETE",
        url_suffix="/api/public/triages/rules/RULE-1",
    )


def test_get_acquisition_profile_uses_profile_id_path():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"_id": "PROFILE-1"})

    client.get_acquisition_profile("PROFILE-1")

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/acquisitions/profiles/PROFILE-1",
    )


def test_list_acquisition_profiles_uses_standard_pagination_and_filters():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"result": {"entities": []}})

    client.list_acquisition_profiles({"name": "Quick", "organization_id": "0", "page": "2", "limit": "25"})

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/acquisitions/profiles",
        params={
            "filter[name]": "Quick",
            "filter[organizationIds]": "0",
            "page": 2,
            "limit": 25,
        },
    )


def test_get_repository_uses_repository_id_path():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"_id": "REPO-1"})

    client.get_repository("REPO-1")

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/repositories/REPO-1",
    )


def test_list_repositories_uses_standard_pagination():
    client = Client(base_url="https://air.example.com", verify=False, headers={}, proxy=False)
    client._http_request = MagicMock(return_value={"result": {"entities": []}})

    client.list_repositories({"page": "2", "limit": "25"})

    client._http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/public/repositories",
        params={"page": 2, "limit": 25},
    )