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 demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401,F403
from typing import Any
from urllib.parse import quote
import json
import traceback

PRESET_PROFILES = [
    "browsing-history",
    "compromise-assessment",
    "event-logs",
    "full",
    "memory-ram-pagefile",
    "quick",
]

TERMINAL_TASK_STATES = {"completed", "complete", "failed", "failure", "cancelled", "canceled", "error"}
SUCCESS_TASK_STATES = {"completed", "complete", "success", "succeeded"}
DEFAULT_PAGE = 1
DEFAULT_LIMIT = 50


def remove_empty_values(value: Any) -> Any:
    """Recursively removes empty values from dicts and lists."""
    if isinstance(value, dict):
        cleaned = {key: remove_empty_values(item) for key, item in value.items() if item not in (None, "", [])}
        return {key: item for key, item in cleaned.items() if item not in (None, "", [], {})}

    if isinstance(value, list):
        cleaned_list = [remove_empty_values(item) for item in value]
        return [item for item in cleaned_list if item not in (None, "", [], {})]

    return value


def clean_params(params: dict[str, Any]) -> dict[str, Any]:
    """Returns a params dictionary suitable for BaseClient._http_request."""
    cleaned: dict[str, Any] = {}
    for key, value in params.items():
        if value in (None, "", []):
            continue
        if isinstance(value, list):
            cleaned[key] = ",".join(str(item) for item in value)
        else:
            cleaned[key] = value
    return cleaned


def url_path(*parts: str) -> str:
    """Safely builds a URL suffix with quoted path variables."""
    return "/" + "/".join(quote(str(part).strip("/"), safe="") for part in parts)


def to_bool(value: Any, default: bool = False) -> bool:
    if value in (None, ""):
        return default
    return argToBoolean(value)


def to_list(value: Any) -> list[Any]:
    if value in (None, ""):
        return []
    return argToList(value)


def required_str_arg(args: dict[str, Any], name: str) -> str:
    value = args.get(name)
    if value in (None, ""):
        raise DemistoException(f"The {name} argument is required.")
    return str(value)


def optional_int_arg(args: dict[str, Any], name: str) -> int | None:
    value = args.get(name)
    if value in (None, ""):
        return None
    try:
        return int(str(value))
    except (TypeError, ValueError):
        raise DemistoException(f"The {name} argument must be an integer. Invalid value: {value}")


def pagination_params(args: dict[str, Any]) -> dict[str, int]:
    return {
        "page": optional_int_arg(args, "page") or DEFAULT_PAGE,
        "limit": optional_int_arg(args, "limit") or DEFAULT_LIMIT,
    }


def required_int_arg(args: dict[str, Any], name: str) -> int:
    value = optional_int_arg(args, name)
    if value is None:
        raise DemistoException(f"The {name} argument is required.")
    return value


def int_list_arg(args: dict[str, Any], name: str) -> list[int]:
    value = args.get(name)
    if value in (None, ""):
        return []
    result: list[int] = []
    for item in argToList(value):
        try:
            result.append(int(item))
        except (TypeError, ValueError):
            raise DemistoException(f"The {name} argument must contain only integer values. Invalid value: {item}")
    return result


def first_entity(result: dict[str, Any]) -> dict[str, Any]:
    payload = result.get("result", result)
    if isinstance(payload, dict):
        entities = payload.get("entities") or payload.get("items") or payload.get("data")
        if isinstance(entities, list) and entities:
            return entities[0]
        return payload
    if isinstance(payload, list) and payload:
        return payload[0]
    return {}


def status_from_task(task: dict[str, Any]) -> str:
    if not task:
        return "unknown"
    for key in ("status", "state", "taskStatus", "processingStatus", "assignmentStatus"):
        value = task.get(key)
        if value:
            if isinstance(value, dict):
                nested = value.get("name") or value.get("value") or value.get("status")
                if nested:
                    return str(nested).lower()
            return str(value).lower()
    return "unknown"


def visibility_value(value: str) -> str:
    mapping = {
        "Public to Organization": "public-to-organization",
        "Private to Users": "private-to-users",
        "public-to-organization": "public-to-organization",
        "private-to-users": "private-to-users",
    }
    return mapping.get(value, value)


def markdown(title: str, data: Any, headers: list[str] | None = None) -> str:
    if isinstance(data, list):
        return tableToMarkdown(title, data, headers=headers, removeNull=True)
    if isinstance(data, dict):
        return tableToMarkdown(title, data, headers=headers, removeNull=True, headerTransform=string_to_table_header)
    return f"### {title}\n```json\n{json.dumps(data, indent=2, default=str)}\n```"


class Client(BaseClient):
    def test_api(self) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix="/api/public/endpoints",
            params={"filter[organizationIds]": 0},
        )

    def get_profile_id(self, profile: str, organization_id: int | None) -> str:
        if profile in PRESET_PROFILES:
            return profile
        if organization_id is None:
            raise DemistoException("The organization_id argument is required for custom acquisition profiles.")
        result = self._http_request(
            method="GET",
            url_suffix="/api/public/acquisitions/profiles",
            params={"filter[name]": profile, "filter[organizationIds]": organization_id},
        )
        entities = result.get("result", {}).get("entities", []) if isinstance(result, dict) else []
        for entity in entities:
            if entity.get("name") == profile and entity.get("_id"):
                return entity.get("_id")
        raise DemistoException(f'The acquisition profile "{profile}" was not found.')

    def air_acquire(self, hostname: str, profile: str, case_id: str, organization_id: int) -> dict[str, Any]:
        payload = {
            "caseId": case_id,
            "droneConfig": {"autoPilot": False, "enabled": False},
            "taskConfig": {"choice": "use-policy"},
            "acquisitionProfileId": self.get_profile_id(profile, organization_id),
            "filter": {"name": hostname, "organizationIds": [organization_id]},
        }
        return self._http_request(method="POST", url_suffix="/api/public/acquisitions/acquire", json_data=payload)

    def air_isolate(self, hostname: str, organization_id: int, isolation: str) -> dict[str, Any]:
        payload = {
            "enabled": isolation.lower() == "enable",
            "filter": {"name": hostname, "organizationIds": [organization_id]},
        }
        return self._http_request(method="POST", url_suffix="/api/public/endpoints/tasks/isolation", json_data=payload)

    def create_case(
        self, organization_id: int, name: str, owner_user_id: str, visibility: str, assigned_user_ids: list[str]
    ) -> dict[str, Any]:
        payload = {
            "organizationId": organization_id,
            "name": name,
            "ownerUserId": owner_user_id,
            "visibility": visibility_value(visibility),
            "assignedUserIds": assigned_user_ids,
        }
        return self._http_request(method="POST", url_suffix="/api/public/cases", json_data=remove_empty_values(payload))

    def get_case(self, case_id: str) -> dict[str, Any]:
        return self._http_request(method="GET", url_suffix=url_path("api", "public", "cases", case_id))

    def list_cases(self, args: dict[str, Any]) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix="/api/public/cases",
            params=clean_params(
                {
                    "filter[name]": args.get("name"),
                    "filter[organizationIds]": args.get("organization_ids") or args.get("organization_id"),
                    **pagination_params(args),
                }
            ),
        )

    def get_case_related(self, case_id: str, relation: str, args: dict[str, Any]) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix=url_path("api", "public", "cases", case_id, relation),
            params=clean_params({"taskId": args.get("task_id"), **pagination_params(args)}),
        )

    def close_case(self, case_id: str, reason: str = "") -> dict[str, Any]:
        return self._http_request(
            method="POST",
            url_suffix=url_path("api", "public", "cases", case_id, "close"),
            json_data=remove_empty_values({"reason": reason}),
        )

    def list_assets(self, args: dict[str, Any]) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix="/api/public/endpoints",
            params=clean_params(
                {
                    "filter[name]": args.get("hostname") or args.get("name"),
                    "filter[organizationIds]": args.get("organization_ids") or args.get("organization_id"),
                    "filter[onlineStatus]": args.get("online_status"),
                    "filter[isolationStatus]": args.get("isolation_status"),
                    "filter[platform]": args.get("platform"),
                    **pagination_params(args),
                }
            ),
        )

    def get_asset(self, asset_id: str) -> dict[str, Any]:
        return self._http_request(method="GET", url_suffix=url_path("api", "public", "endpoints", asset_id))

    def get_asset_by_hostname(self, hostname: str, organization_id: int) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix="/api/public/endpoints",
            params={"filter[name]": hostname, "filter[organizationIds]": organization_id},
        )

    def get_asset_tasks(self, asset_id: str, args: dict[str, Any]) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix=url_path("api", "public", "endpoints", asset_id, "tasks"),
            params=clean_params(pagination_params(args)),
        )

    def get_task(self, task_id: str) -> dict[str, Any]:
        return self._http_request(method="GET", url_suffix=url_path("api", "public", "tasks", task_id))

    def list_tasks(self, args: dict[str, Any]) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix="/api/public/tasks",
            params=clean_params(
                {
                    "filter[caseIds]": args.get("case_id"),
                    "filter[organizationIds]": args.get("organization_ids") or args.get("organization_id"),
                    "filter[status]": args.get("status"),
                    "filter[type]": args.get("task_type"),
                    **pagination_params(args),
                }
            ),
        )

    def get_task_assignments(self, task_id: str, args: dict[str, Any]) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix=url_path("api", "public", "tasks", task_id, "assignments"),
            params=clean_params(pagination_params(args)),
        )

    def create_triage_rule(
        self, description: str, rule: str, search_in: str, engine: str, organization_ids: list[int]
    ) -> dict[str, Any]:
        payload = {
            "description": description,
            "rule": rule,
            "searchIn": search_in,
            "engine": engine,
            "organizationIds": organization_ids,
        }
        return self._http_request(method="POST", url_suffix="/api/public/triages/rules", json_data=remove_empty_values(payload))

    def list_triage_rules(self, args: dict[str, Any]) -> dict[str, Any]:
        organization_ids = args.get("organization_ids") or args.get("organization_id")
        return self._http_request(
            method="GET",
            url_suffix="/api/public/triages/rules",
            params=clean_params(
                {
                    "filter[organizationIds]": organization_ids,
                    "filter[engine]": args.get("engine"),
                    "filter[searchIn]": args.get("search_in"),
                    "filter[description]": args.get("description"),
                    **pagination_params(args),
                }
            ),
        )

    def get_triage_rule(self, rule_id: str) -> dict[str, Any]:
        return self._http_request(method="GET", url_suffix=url_path("api", "public", "triages", "rules", rule_id))

    def update_triage_rule(
        self, description: str, rule: str, search_in: str, rule_id: str, organization_ids: list[int]
    ) -> dict[str, Any]:
        payload = {"description": description, "rule": rule, "searchIn": search_in, "organizationIds": organization_ids}
        return self._http_request(
            method="PUT",
            url_suffix=url_path("api", "public", "triages", "rules", rule_id),
            json_data=remove_empty_values(payload),
        )

    def delete_triage_rule(self, rule_id: str) -> dict[str, Any]:
        return self._http_request(method="DELETE", url_suffix=url_path("api", "public", "triages", "rules", rule_id))

    def validate_triage_rule(self, rule: str, engine: str) -> dict[str, Any]:
        return self._http_request(
            method="POST", url_suffix="/api/public/triages/rules/validate", json_data={"rule": rule, "engine": engine}
        )

    def assign_triage_task(self, args: dict[str, Any]) -> dict[str, Any]:
        organization_id = required_int_arg(args, "organization_id")
        body = {
            "caseId": args.get("case_id"),
            "triageRuleIds": to_list(args.get("triage_rule_ids")),
            "taskConfig": {
                "choice": args.get("task_config_choice", "use-policy"),
                "cpu": {"limit": optional_int_arg(args, "task_config_cpu_limit") or 8},
            },
            "mitreAttack": {"enabled": to_bool(args.get("mitre_attack"), False)},
            "filter": {
                "name": args.get("hostname"),
                "groupId": args.get("group_id"),
                "groupFullPath": args.get("group_full_path"),
                "isolationStatus": to_list(args.get("isolation_status")),
                "platform": to_list(args.get("platform")),
                "issue": args.get("issue"),
                "onlineStatus": to_list(args.get("online_status")),
                "tags": to_list(args.get("tags")),
                "version": args.get("version"),
                "policy": args.get("policy"),
                "includedEndpointIds": to_list(args.get("included_endpoint_ids")),
                "excludedEndpointIds": to_list(args.get("excluded_endpoint_ids")),
                "organizationIds": [organization_id],
            },
            "schedulerConfig": {"when": args.get("when", "now")},
        }
        return self._http_request(method="POST", url_suffix="/api/public/triages/triage", json_data=remove_empty_values(body))

    def list_acquisition_profiles(self, args: dict[str, Any]) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix="/api/public/acquisitions/profiles",
            params=clean_params(
                {
                    "filter[name]": args.get("name"),
                    "filter[organizationIds]": args.get("organization_ids") or args.get("organization_id"),
                    **pagination_params(args),
                }
            ),
        )

    def get_acquisition_profile(self, profile_id: str) -> dict[str, Any]:
        return self._http_request(method="GET", url_suffix=url_path("api", "public", "acquisitions", "profiles", profile_id))

    def list_repositories(self, args: dict[str, Any]) -> dict[str, Any]:
        return self._http_request(
            method="GET", url_suffix="/api/public/repositories", params=clean_params(pagination_params(args))
        )

    def get_repository(self, repository_id: str) -> dict[str, Any]:
        return self._http_request(method="GET", url_suffix=url_path("api", "public", "repositories", repository_id))

    def download_file(self, file_name: str) -> Any:
        return self._http_request(
            method="GET", url_suffix="/api/public/interact/library/download", params={"filename": file_name}, resp_type="response"
        )


def test_connection(client: Client) -> str:
    try:
        client.test_api()
    except DemistoException as ex:
        if "Unauthorized" in str(ex) or "401" in str(ex):
            return f"Authorization Error: Make sure the API key is correct. {str(ex)}"
        if "ConnectionError" in str(ex):
            return f"Connection Error: Test connection failed. {str(ex)}"
        raise
    return "ok"


def command_results(
    title: str, prefix: str, result: dict[str, Any], outputs: dict[str, Any] | None = None, key_field: str = "ID"
) -> CommandResults:
    return CommandResults(
        outputs_prefix=prefix,
        outputs_key_field=key_field,
        outputs=outputs if outputs is not None else result,
        readable_output=markdown(title, result),
        raw_response=result,
    )


def air_acquire_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.air_acquire(
        hostname=required_str_arg(args, "hostname"),
        profile=required_str_arg(args, "profile"),
        case_id=required_str_arg(args, "case_id"),
        organization_id=required_int_arg(args, "organization_id"),
    )
    res = result.get("result", {})
    formatted = {"ID": res.get("_id"), "Name": res.get("name"), "OrganizationID": res.get("organizationId")}
    return command_results(
        "Binalyze AIR Acquisition Results", "BinalyzeAIR.Acquire", result, {"Result": formatted, "Success": result.get("success")}
    )


def air_isolate_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.air_isolate(
        required_str_arg(args, "hostname"), required_int_arg(args, "organization_id"), required_str_arg(args, "isolation")
    )
    res = result.get("result", {})
    formatted = {"ID": res.get("_id"), "Name": res.get("name"), "OrganizationID": res.get("organizationId")}
    return command_results(
        "Binalyze AIR Isolation Results", "BinalyzeAIR.Isolate", result, {"Result": formatted, "Success": result.get("success")}
    )


def create_case_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.create_case(
        organization_id=required_int_arg(args, "organization_id"),
        name=required_str_arg(args, "name"),
        owner_user_id=required_str_arg(args, "owner_user_id"),
        visibility=required_str_arg(args, "visibility"),
        assigned_user_ids=to_list(args.get("assigned_user_ids")),
    )
    payload = result.get("result", {}) if isinstance(result, dict) else {}
    formatted = {"ID": payload.get("_id") or payload.get("caseId") or payload.get("id"), "Name": payload.get("name")}
    return command_results(
        "Binalyze AIR Create Case Result", "BinalyzeAIR.Case", result, {"Result": formatted, "Success": result.get("success")}
    )


def get_case_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.get_case(required_str_arg(args, "case_id"))
    return command_results("Binalyze AIR Case", "BinalyzeAIR.Case", result, key_field="_id")


def list_cases_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.list_cases(args)
    return command_results("Binalyze AIR Cases", "BinalyzeAIR.Cases", result, key_field="_id")


def close_case_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.close_case(required_str_arg(args, "case_id"), args.get("reason", ""))
    return command_results("Binalyze AIR Close Case Result", "BinalyzeAIR.CloseCase", result, key_field="_id")


def get_case_related_command(client: Client, args: dict[str, Any], relation: str, prefix: str, title: str) -> CommandResults:
    result = client.get_case_related(required_str_arg(args, "case_id"), relation, args)
    return command_results(title, prefix, result, key_field="_id")


def list_assets_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.list_assets(args)
    return command_results("Binalyze AIR Assets", "BinalyzeAIR.Asset", result, key_field="_id")


def get_asset_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.get_asset(required_str_arg(args, "asset_id"))
    return command_results("Binalyze AIR Asset", "BinalyzeAIR.Asset", result, key_field="_id")


def get_asset_by_hostname_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.get_asset_by_hostname(required_str_arg(args, "hostname"), required_int_arg(args, "organization_id"))
    entity = first_entity(result)
    return command_results("Binalyze AIR Asset Lookup Result", "BinalyzeAIR.Asset", result, {"Result": entity}, key_field="_id")


def get_asset_tasks_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.get_asset_tasks(required_str_arg(args, "asset_id"), args)
    return command_results("Binalyze AIR Asset Tasks", "BinalyzeAIR.AssetTask", result, key_field="_id")


def get_task_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.get_task(required_str_arg(args, "task_id"))
    task = result.get("result", result) if isinstance(result, dict) else {}
    status = status_from_task(task if isinstance(task, dict) else {})
    outputs = {
        "Result": task,
        "Status": status,
        "IsDone": status in TERMINAL_TASK_STATES,
        "IsSuccess": status in SUCCESS_TASK_STATES,
    }
    return command_results("Binalyze AIR Task", "BinalyzeAIR.Task", result, outputs, key_field="_id")


def list_tasks_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.list_tasks(args)
    return command_results("Binalyze AIR Tasks", "BinalyzeAIR.Task", result, key_field="_id")


def get_task_assignments_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.get_task_assignments(required_str_arg(args, "task_id"), args)
    return command_results("Binalyze AIR Task Assignments", "BinalyzeAIR.TaskAssignment", result, key_field="_id")


def create_triage_rule_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.create_triage_rule(
        args.get("description", ""),
        required_str_arg(args, "rule"),
        args.get("search_in", ""),
        required_str_arg(args, "engine"),
        int_list_arg(args, "organization_ids"),
    )
    return command_results("Binalyze AIR Create Triage Rule Result", "BinalyzeAIR.TriageRule", result, key_field="_id")


def update_triage_rule_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.update_triage_rule(
        args.get("description", ""),
        args.get("rule", ""),
        args.get("search_in", ""),
        required_str_arg(args, "rule_id"),
        int_list_arg(args, "organization_ids"),
    )
    return command_results("Binalyze AIR Update Triage Rule Result", "BinalyzeAIR.TriageRule", result, key_field="_id")


def validate_triage_rule_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.validate_triage_rule(required_str_arg(args, "rule"), required_str_arg(args, "engine"))
    outputs = {"Result": result.get("result"), "Success": result.get("success")}
    return command_results(
        "Binalyze AIR Validate Triage Rule Result", "BinalyzeAIR.TriageRuleValidation", result, outputs, key_field="Success"
    )


def list_triage_rules_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.list_triage_rules(args)
    return command_results("Binalyze AIR Triage Rules", "BinalyzeAIR.TriageRule", result, key_field="_id")


def get_triage_rule_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.get_triage_rule(required_str_arg(args, "rule_id"))
    return command_results("Binalyze AIR Triage Rule", "BinalyzeAIR.TriageRule", result, key_field="_id")


def delete_triage_rule_command(client: Client, args: dict[str, Any]) -> CommandResults:
    rule_id = required_str_arg(args, "rule_id")
    result = client.delete_triage_rule(rule_id)
    return command_results(
        "Binalyze AIR Delete Triage Rule Result", "BinalyzeAIR.DeleteTriageRule", result, {"RuleID": rule_id}, key_field="RuleID"
    )


def assign_triage_task_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.assign_triage_task(args)
    return command_results("Binalyze AIR Assign Triage Task Result", "BinalyzeAIR.TriageTask", result, key_field="_id")


def list_acquisition_profiles_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.list_acquisition_profiles(args)
    return command_results("Binalyze AIR Acquisition Profiles", "BinalyzeAIR.AcquisitionProfile", result, key_field="_id")


def get_acquisition_profile_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.get_acquisition_profile(required_str_arg(args, "profile_id"))
    return command_results("Binalyze AIR Acquisition Profile", "BinalyzeAIR.AcquisitionProfile", result, key_field="_id")


def list_repositories_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.list_repositories(args)
    return command_results("Binalyze AIR Repositories", "BinalyzeAIR.Repository", result, key_field="_id")


def get_repository_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.get_repository(required_str_arg(args, "repository_id"))
    return command_results("Binalyze AIR Repository", "BinalyzeAIR.Repository", result, key_field="_id")


def download_file_command(client: Client, args: dict[str, Any]) -> Any:
    file_name = required_str_arg(args, "file_name")
    result = client.download_file(file_name)
    if result.status_code == 200:
        return fileResult(file_name, result.content)
    raise DemistoException(f"Failed to download file. Status code: {result.status_code}")


def main() -> None:
    command = demisto.command()
    params = demisto.params()
    args = demisto.args()
    api_key = params.get("api_key")
    base_url = params.get("server")
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)
    headers = {
        "Authorization": f"Bearer {api_key}",
        "User-Agent": "Binalyze AIR Extended Cortex XSOAR Integration",
        "Content-Type": "application/json",
        "Accept-Charset": "UTF-8",
    }
    if command == "binalyze-air-download-file":
        headers["Accept"] = "application/octet-stream"

    client = Client(base_url=base_url, verify=verify_certificate, headers=headers, proxy=proxy, ok_codes=(200, 201, 202, 204))

    commands = {
        "binalyze-air-acquire": lambda: air_acquire_command(client, args),
        "binalyze-air-isolate": lambda: air_isolate_command(client, args),
        "binalyze-air-create-case": lambda: create_case_command(client, args),
        "binalyze-air-get-case": lambda: get_case_command(client, args),
        "binalyze-air-list-cases": lambda: list_cases_command(client, args),
        "binalyze-air-close-case": lambda: close_case_command(client, args),
        "binalyze-air-get-case-tasks": lambda: get_case_related_command(
            client, args, "tasks", "BinalyzeAIR.CaseTask", "Binalyze AIR Case Tasks"
        ),
        "binalyze-air-get-case-endpoints": lambda: get_case_related_command(
            client, args, "endpoints", "BinalyzeAIR.CaseEndpoint", "Binalyze AIR Case Endpoints"
        ),
        "binalyze-air-get-case-activities": lambda: get_case_related_command(
            client, args, "activities", "BinalyzeAIR.CaseActivity", "Binalyze AIR Case Activities"
        ),
        "binalyze-air-list-assets": lambda: list_assets_command(client, args),
        "binalyze-air-get-asset": lambda: get_asset_command(client, args),
        "binalyze-air-get-asset-by-hostname": lambda: get_asset_by_hostname_command(client, args),
        "binalyze-air-get-asset-tasks": lambda: get_asset_tasks_command(client, args),
        "binalyze-air-get-task": lambda: get_task_command(client, args),
        "binalyze-air-list-tasks": lambda: list_tasks_command(client, args),
        "binalyze-air-get-task-assignments": lambda: get_task_assignments_command(client, args),
        "binalyze-air-create-triage-rule": lambda: create_triage_rule_command(client, args),
        "binalyze-air-update-triage-rule": lambda: update_triage_rule_command(client, args),
        "binalyze-air-validate-triage-rule": lambda: validate_triage_rule_command(client, args),
        "binalyze-air-list-triage-rules": lambda: list_triage_rules_command(client, args),
        "binalyze-air-get-triage-rule": lambda: get_triage_rule_command(client, args),
        "binalyze-air-delete-triage-rule": lambda: delete_triage_rule_command(client, args),
        "binalyze-air-assign-triage-task": lambda: assign_triage_task_command(client, args),
        "binalyze-air-list-acquisition-profiles": lambda: list_acquisition_profiles_command(client, args),
        "binalyze-air-get-acquisition-profile": lambda: get_acquisition_profile_command(client, args),
        "binalyze-air-list-repositories": lambda: list_repositories_command(client, args),
        "binalyze-air-get-repository": lambda: get_repository_command(client, args),
        "binalyze-air-download-file": lambda: download_file_command(client, args),
    }

    try:
        demisto.debug(f"Command being called is {command}")
        if command == "test-module":
            return_results(test_connection(client))
        if command in commands:
            return_results(commands[command]())
            return
        raise NotImplementedError(f"Command {command} is not implemented.")
    except (DemistoException, NotImplementedError, ValueError, TypeError) as ex:
        demisto.error(traceback.format_exc())
        return_error(f'Failed to execute "{command}". Error: {str(ex)}')


if __name__ in ("__main__", "__builtin__", "builtins"):
    main()