Reco

Reco is the leader in SaaS & AI Security, providing full visibility and control across your SaaS ecosystem and AI agents.

Data Enrichment & Threat Intelligence · Reco

Details

IDReco
ProviderReco AI
CategoryData Enrichment & Threat Intelligence
From Version6.5.0
Docker Imagedemisto/python3:3.12.13.10404775
Supported ModulesAgentix XSIAM

README

Reco is the leader in SaaS & AI Security — securing AI sprawl across SaaS apps and agents. This integration connects Reco’s SaaS & AI Security platform to Cortex XSOAR, enabling real-time threat response, posture management, AI governance, and identity risk workflows.

This integration was integrated and tested with Reco External API v1.

Configure Reco in Cortex

Parameter Description Required
Server URL (e.g. https://host.reco.ai/api/v1) Base URL of your Reco instance True
JWT app token API Token (Bearer) True
Trust any certificate (not secure) Skip TLS verification False
Use system proxy settings Route requests through the system proxy False
Incident type Incident type to map Reco alerts to False
Fetch incidents Enable automatic incident fetching False
Max fetch Maximum incidents to fetch per run (up to 500) False
Source Filter fetched incidents by SaaS source False
Before Fetch incidents created before this timestamp False
After Fetch incidents created after this timestamp False
Minimum risk level (e.g. MEDIUM fetches medium and higher) The minimum severity threshold for fetched incidents. Accepts a single value: LOW, MEDIUM, HIGH, or CRITICAL (or numeric equivalents 10, 20, 30, 40). Alerts at or above this severity are fetched. False
First fetch timestamp How far back to fetch on first run (e.g. 7 days, 12 hours) False

SCIM v2 Filters

All reco-list-* commands accept an optional filters argument using SCIM v2 syntax:

Operator Meaning Example
eq Equals severity eq "HIGH"
ne Not equals status ne "CLOSED"
co Contains email co "@example.com"
sw Starts with name sw "John"
gt / ge Greater than / or equal createdAt gt "2024-01-01T00:00:00Z"
lt / le Less than / or equal lastSeen le "2024-12-31T23:59:59Z"
in Matches any listed value severity in ["HIGH","CRITICAL"]
not in Excludes listed values status not in ["CLOSED"]
and / or / not Logical operators isAdmin eq true and hasMfa eq false

Pagination is embedded in the filter string: limit eq 100 and page eq 1.

Commands

reco-add-comment-to-alert


Add a comment to an alert in Reco.

Base Command

reco-add-comment-to-alert

Input

Argument Name Description Required
alert_id Alert ID to add the comment to. Required
comment Comment text. Required

reco-update-incident-timeline


Add a comment to an incident timeline in Reco.

Base Command

reco-update-incident-timeline

Input

Argument Name Description Required
incident_id Incident ID. Required
comment Comment text. Required

reco-resolve-visibility-event


Resolve an event in a Reco Finding.

Base Command

reco-resolve-visibility-event

Input

Argument Name Description Required
entity_id Entity ID of the file to resolve. Required
label_name Label name to resolve (e.g. Accessible to All Org Users). Required

reco-get-risky-users


List all accounts flagged as risky (auto-paginates all results).

Base Command

reco-get-risky-users

Context Output

Path Type Description
Reco.RiskyUsers.id String Account ID
Reco.RiskyUsers.name String Account display name
Reco.RiskyUsers.accountEmail String Account email address
Reco.RiskyUsers.permissions String Account permissions (ADMIN / PRIVILEGED / STANDARD)
Reco.RiskyUsers.hasMfa String MFA status (MFA / NOMFA / NA)
Reco.RiskyUsers.openAlerts Number Number of open alerts for this account
Reco.RiskyUsers.isAdmin Boolean Whether the account has admin privileges
Reco.RiskyUsers.isRiskyUser Boolean Whether the account is flagged as risky
Reco.RiskyUsers.lastSeen Date Last activity timestamp
Reco.RiskyUsers Unknown Risky Users.

reco-add-risky-user-label


Tag a user as risky in Reco.

Base Command

reco-add-risky-user-label

Input

Argument Name Description Required
email_address Email address of the user to tag as risky. Required

reco-add-leaving-org-user-label


Tag a user as a departing employee in Reco.

Base Command

reco-add-leaving-org-user-label

Input

Argument Name Description Required
email_address Email address of the user to tag as departing. Required

reco-get-assets-user-has-access-to


List files a user has access to.

Base Command

reco-get-assets-user-has-access-to

Input

Argument Name Description Required
email_address User email address. Required
only_sensitive Return only sensitive assets. Optional

Context Output

Path Type Description
Reco.Assets Unknown Assets the user has access to

reco-get-sensitive-assets-by-name


Find sensitive assets by name.

Base Command

reco-get-sensitive-assets-by-name

Input

Argument Name Description Required
asset_name Asset name to search for. Required
regex_search Use substring/contains matching instead of exact match. Optional

Context Output

Path Type Description
Reco.SensitiveAssets.id String Asset ID
Reco.SensitiveAssets.name String Asset name
Reco.SensitiveAssets.owner String Asset owner
Reco.SensitiveAssets.url String Asset URL
Reco.SensitiveAssets.sensitivityLevel Number Sensitivity level (30=HIGH, 40=CRITICAL)
Reco.SensitiveAssets.permissionVisibility String Permission visibility (PUBLIC / INTERNAL / RESTRICTED)
Reco.SensitiveAssets.location String File path
Reco.SensitiveAssets.dataCategories Unknown Detected data categories
Reco.SensitiveAssets.file_name String The name of the asset.
Reco.SensitiveAssets.file_owner String The owner of the asset.
Reco.SensitiveAssets.file_url Unknown Json string of the asset’s url and the name.
Reco.SensitiveAssets.currently_permitted_users String List of currently permitted users.
Reco.SensitiveAssets.visibility String Visibility of the asset.
Reco.SensitiveAssets.source String SaaS tool source of the asset.
Reco.SensitiveAssets.sensitivity_level Number The sensitivity level of the asset.

reco-get-sensitive-assets-by-id


Find sensitive assets by ID.

Base Command

reco-get-sensitive-assets-by-id

Input

Argument Name Description Required
asset_id Asset ID. Required

Context Output

Path Type Description
Reco.SensitiveAssets.id String Asset ID
Reco.SensitiveAssets.name String Asset name
Reco.SensitiveAssets.owner String Asset owner
Reco.SensitiveAssets.url String Asset URL
Reco.SensitiveAssets.sensitivityLevel Number Sensitivity level (30=HIGH, 40=CRITICAL)
Reco.SensitiveAssets.permissionVisibility String Permission visibility (PUBLIC / INTERNAL / RESTRICTED)
Reco.SensitiveAssets.location String File path
Reco.SensitiveAssets.dataCategories Unknown Detected data categories
Reco.SensitiveAssets.file_name String The name of the asset.
Reco.SensitiveAssets.file_owner String The owner of the asset.
Reco.SensitiveAssets.file_url Unknown Json string of the asset’s url and the name.
Reco.SensitiveAssets.currently_permitted_users String List of currently permitted users.
Reco.SensitiveAssets.visibility String Visibility of the asset.
Reco.SensitiveAssets.source String SaaS tool source of the asset.
Reco.SensitiveAssets.sensitivity_level Number The sensitivity level of the asset.

reco-get-assets-by-id


Find any asset by ID.

Base Command

reco-get-assets-by-id

Input

Argument Name Description Required
asset_id Asset ID. Required

Context Output

Path Type Description
Reco.SensitiveAssets.id String Asset ID
Reco.SensitiveAssets.name String Asset name
Reco.SensitiveAssets.owner String Asset owner
Reco.SensitiveAssets.url String Asset URL
Reco.SensitiveAssets.sensitivityLevel Number Sensitivity level (30=HIGH, 40=CRITICAL)
Reco.SensitiveAssets.permissionVisibility String Permission visibility (PUBLIC / INTERNAL / RESTRICTED)
Reco.SensitiveAssets.location String File path
Reco.SensitiveAssets.dataCategories Unknown Detected data categories
Reco.SensitiveAssets.file_name String The name of the asset.
Reco.SensitiveAssets.file_owner String The owner of the asset.
Reco.SensitiveAssets.file_url Unknown Json string of the asset’s url and the name.
Reco.SensitiveAssets.currently_permitted_users String List of currently permitted users.
Reco.SensitiveAssets.visibility String Visibility of the asset.
Reco.SensitiveAssets.source String SaaS tool source of the asset.
Reco.SensitiveAssets.sensitivity_level Number The sensitivity level of the asset.

reco-get-link-to-user-overview-page


Generate a deep link to the Reco UI overview page for an entity.

Base Command

reco-get-link-to-user-overview-page

Input

Argument Name Description Required
entity Entity type (e.g. RM_LINK_TYPE_USER). Required
param Entity ID or email. Optional

reco-get-3rd-parties-accessible-to-data-list


List third-party domains that have access to sensitive data.

Base Command

reco-get-3rd-parties-accessible-to-data-list

Input

Argument Name Description Required
last_interaction_time_in_days Include domains with activity within this many days. Required

Context Output

Path Type Description
Reco.Domains.domain String Third-party domain
Reco.Domains.last_activity String Last interaction date
Reco.Domains.files_num Number Number of files accessible
Reco.Domains.users_with_access_num Number Number of users with access

reco-get-sensitive-assets-with-public-link


List sensitive assets exposed via a public link.

Base Command

reco-get-sensitive-assets-with-public-link

Context Output

Path Type Description
Reco.Assets.asset_id String Asset ID
Reco.Assets.asset Unknown Asset metadata
Reco.Assets.data_category String Primary data category
Reco.Assets.data_categories String Data categories of the asset
Reco.SensitiveAssets.location String Path of the asset
Reco.SensitiveAssets.source String SaaS tool source of the asset
Reco.Assets.last_access_date String Last access date

reco-get-files-shared-with-3rd-parties


List files shared with a specific third-party domain.

Base Command

reco-get-files-shared-with-3rd-parties

Input

Argument Name Description Required
domain Third-party domain to query. Required
last_interaction_time_in_days Include files with activity within this many days. Required

Context Output

Path Type Description
Reco.Assets.asset_id String Asset ID
Reco.Assets.location String File location
Reco.Assets.file_owner String File owner
Reco.Assets.asset Unknown Asset metadata
Reco.Assets.data_category String Data category of the asset
Reco.Assets.users String Users the file is shared with
Reco.Assets.domain String Third-party domain
Reco.Assets.last_access_date String Last access date

reco-change-alert-status


Update the status of a Reco alert.

Base Command

reco-change-alert-status

Input

Argument Name Description Required
alert_id Alert ID. Required
status New status. Possible values: ALERT_STATUS_NEW, ALERT_STATUS_IN_PROGRESS, ALERT_STATUS_CLOSED, ALERT_STATUS_PREVIEW, ALERT_STATUS_TO_REVIEW, ALERT_STATUS_PASSED, ALERT_STATUS_RISK_ACCEPTED, ALERT_STATUS_RISK_MITIGATED, ALERT_STATUS_NA, ALERT_STATUS_NA_NEW, ALERT_STATUS_NA_NO_AD, ALERT_STATUS_NOT_APPLICABLE. Required

reco-get-user-context-by-email-address


Get identity context for a user by email address.

Base Command

reco-get-user-context-by-email-address

Input

Argument Name Description Required
email_address User email address. Required

Context Output

Path Type Description
Reco.User.id String Identity ID
Reco.User.email String Primary email address
Reco.User.name String Full name
Reco.User.departments String Departments
Reco.User.jobTitles String Job titles
Reco.User.isFormer Boolean Whether the user is a former employee
Reco.User.isInternal Boolean Whether the user is an internal employee
Reco.User.openAlerts Number Number of open alerts
Reco.User.lastSeen Date Last activity timestamp
Reco.User.email_account String The email of the user.
Reco.User.full_name String The user full name.
Reco.User.job_titles String Job Title.
Reco.User.category String Category.
Reco.User.groups String The groups user is member of.
Reco.User.labels Unknown User Labels.

reco-get-files-exposed-to-email-address


List files accessible to a specific email address.

Base Command

reco-get-files-exposed-to-email-address

Input

Argument Name Description Required
email_address Email address. Required

Context Output

Path Type Description
Reco.Assets.asset_id String Asset ID
Reco.Assets.asset Unknown Asset metadata
Reco.Assets.data_category String Data category of the asset
Reco.Assets.data_categories String Data categories of the asset
Reco.Assets.location String File location
Reco.Assets.source String SaaS tool source of the asset
Reco.Assets.last_access_date String Last access date
Reco.Assets.email_account String Email account with access
Reco.Assets.file_owner String File owner

reco-get-assets-shared-externally


List files an owner has shared outside the organization.

Base Command

reco-get-assets-shared-externally

Input

Argument Name Description Required
email_address File owner email address. Required

Context Output

Path Type Description
Reco.Assets.asset_id String Asset ID
Reco.Assets.asset Unknown Asset metadata
Reco.Assets.data_category String Data category of the asset
Reco.Assets.data_categories String Data categories of the asset
Reco.SensitiveAssets.location String Path of the asset
Reco.SensitiveAssets.source String SaaS tool source of the asset
Reco.Assets.last_access_date String Last access date
Reco.Assets.file_owner String File owner

reco-get-private-email-list-with-access


List private (non-corporate) email addresses with file access.

Base Command

reco-get-private-email-list-with-access

Context Output

Path Type Description
Reco.privateEmails.email_account String Private email account
Reco.privateEmails.primary_email String Associated corporate email
Reco.privateEmails.files_num Number Number of files accessible
Reco.privateEmails.user_category String User category

reco-get-alert-ai-summary


Get an AI-generated summary of an alert.

Base Command

reco-get-alert-ai-summary

Input

Argument Name Description Required
alert_id Alert ID. Required

Context Output

Path Type Description
Reco.AlertSummary.markdown String Markdown-formatted alert summary

reco-get-apps


List all discovered SaaS applications (auto-paginates all results).

Base Command

reco-get-apps

Input

Argument Name Description Required
before Filter apps last seen before this date. Optional
after Filter apps last seen after this date. Optional
limit Page size (omit for all results). Optional

Context Output

Path Type Description
Reco.Apps.id String The unique identifier of the application.
Reco.Apps.name String The name of the application.
Reco.Apps.category String The category of the application.
Reco.Apps.usersCount Number The number of users with access to the application.
Reco.Apps.authorization String The authorization/sanction status of the application.
Reco.Apps.authType String The authentication type used by the application.
Reco.Apps.isUsingAi Boolean Whether the application uses AI.
Reco.Apps.isShadowApp Boolean Whether the application is a shadow/unmanaged app.
Reco.Apps.vendorGrade String The vendor security grade of the application.
Reco.Apps.aiCapability String The AI capability description for the application.
Reco.Apps.lastSeen Date The last activity timestamp for the application.
Reco.Apps.app_name String The name of the application.
Reco.Apps.app_id String The unique identifier of the application.
Reco.Apps.risk_score Number The risk score of the application.
Reco.Apps.users_count Number The number of users with access to the application.
Reco.Apps.data_access String The data access level of the application.
Reco.Apps.updated_at Date The last update timestamp of the application.
Reco.Apps.created_at Date The creation timestamp of the application.
Reco.Apps.status String The status of the application.

reco-set-app-authorization-status


Update the authorization status of an application.

Base Command

reco-set-app-authorization-status

Input

Argument Name Description Required
app_id Application ID. Required
authorization_status Authorization status. Possible values: AUTH_STATUS_SANCTIONED, AUTH_STATUS_UNSANCTIONED, AUTH_STATUS_TO_REVIEW, AUTH_STATUS_ACCEPTED_RISK, AUTH_STATUS_EVALUATING, AUTH_STATUS_UNDER_INVESTIGATION, AUTH_STATUS_INVESTIGATED, AUTH_STATUS_CLOUD_INVENTORY, AUTH_STATUS_SYSTEM_SANCTIONED. Required

Context Output

Path Type Description
Reco.AppAuthorization.app_id String The application ID that was updated.
Reco.AppAuthorization.authorization_status String The authorization status that was set.
Reco.AppAuthorization.updated Boolean Whether the update was successful.
Reco.AppAuthorization.rows_affected Number Number of rows affected by the update operation.

Command example

!reco-set-app-authorization-status app_id="microsoft.com" authorization_status="AUTH_STATUS_SANCTIONED"

reco-add-exclusion-filter


Add values to a Reco classifier exclusion list.

Base Command

reco-add-exclusion-filter

Input

Argument Name Description Required
key_to_add Exclusion key (e.g. CASE_SENSITIVE_TERMS, OWNERS, FILE_IDS, LOCATIONS). Required
values_to_add Comma-separated values to add. Required

List Commands (External API)

All commands below accept filters (SCIM v2 expression) and limit (default 1000).

reco-list-events


List SaaS activity events.

Base Command

reco-list-events

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “actor.email eq “user@example.com” and eventTime gt “2024-01-01T00:00:00Z””). Optional
limit The maximum number of events to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.Events.id String Event ID
Reco.Events.eventType String Event type code
Reco.Events.formattedEventType String Human-readable event type
Reco.Events.application String Source SaaS application
Reco.Events.actorEmail String Actor email address
Reco.Events.actorName String Actor display name
Reco.Events.eventTime Date Event timestamp
Reco.Events.outcomeString String Event outcome description

reco-list-posture-issues


List security posture issues.

Base Command

reco-list-posture-issues

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “severity eq “HIGH””). Optional
limit The maximum number of posture issues to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.PostureIssues.id String Issue ID
Reco.PostureIssues.name String Issue name
Reco.PostureIssues.severity String Severity (LOW/MEDIUM/HIGH/CRITICAL)
Reco.PostureIssues.checkStatus String Check status
Reco.PostureIssues.scorePercentage Number Compliance score percentage
Reco.PostureIssues.checkedInstance Unknown The SaaS instance this issue was checked against
Reco.PostureIssues.url String Link to issue in Reco UI

reco-list-accounts


List SaaS accounts.

Base Command

reco-list-accounts

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “isRiskyUser eq true” or “accountEmail co “@example.com””). Optional
limit The maximum number of accounts to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.Accounts.id String Account ID
Reco.Accounts.name String Account display name
Reco.Accounts.accountEmail String Account email address
Reco.Accounts.permissions String Permission level
Reco.Accounts.hasMfa String MFA status
Reco.Accounts.openAlerts Number Open alerts count
Reco.Accounts.isAdmin Boolean Admin flag
Reco.Accounts.isRiskyUser Boolean Risky user flag
Reco.Accounts.lastSeen Date Last activity

reco-list-devices


List managed and unmanaged devices.

Base Command

reco-list-devices

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “isUnmanaged eq true” or “devicePlatform eq “Windows””). Optional
limit The maximum number of devices to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.Devices.id String Device ID
Reco.Devices.name String Device name
Reco.Devices.devicePlatform String Device platform (Windows, macOS, iOS, Android, etc.)
Reco.Devices.os String Operating system of the device
Reco.Devices.osVersion String Operating system version
Reco.Devices.isUnmanaged Boolean Whether the device is unmanaged (not enrolled in MDM)
Reco.Devices.hasNonCompliant Boolean Whether the device has non-compliant policies
Reco.Devices.lastSeen Date Last activity timestamp

reco-list-ai-agents


List detected AI agents.

Base Command

reco-list-ai-agents

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “authorization eq “AUTH_STATUS_UNSANCTIONED””). Optional
limit The maximum number of AI agents to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.AiAgents.id String AI agent ID
Reco.AiAgents.name String AI agent name
Reco.AiAgents.vendor String Vendor of the AI agent
Reco.AiAgents.type String Type of AI agent
Reco.AiAgents.authorization String Authorization/sanction status of the AI agent
Reco.AiAgents.agentStatus String Current status of the AI agent
Reco.AiAgents.risk Number Risk level of the AI agent (0=NA, 1=LOW, 2=MEDIUM, 3=HIGH, 4=CRITICAL)
Reco.AiAgents.lastUsage Date Last usage timestamp

reco-list-groups


List SaaS groups.

Base Command

reco-list-groups

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “name co “Engineering””). Optional
limit The maximum number of groups to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.Groups.id String Group ID
Reco.Groups.name String Group name
Reco.Groups.email String Group email address
Reco.Groups.membersCount Number Number of members in the group
Reco.Groups.appsCount Number Number of apps the group has access to

reco-list-saas-to-saas


List SaaS-to-SaaS OAuth grants.

Base Command

reco-list-saas-to-saas

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “authorization eq “AUTH_STATUS_UNSANCTIONED” or permissionRisk eq “30””). Optional
limit The maximum number of SaaS-to-SaaS grants to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.SaasToSaas.id String SaaS-to-SaaS grant ID
Reco.SaasToSaas.plugin String The plugin or app name receiving the grant
Reco.SaasToSaas.authorization String Authorization status of the grant
Reco.SaasToSaas.permissionRisk String Permission risk level (10=LOW, 20=MEDIUM, 30=HIGH)
Reco.SaasToSaas.accounts Number Number of accounts with this grant
Reco.SaasToSaas.aiCapability String AI capability of the third-party app
Reco.SaasToSaas.lastSeen Date Last activity timestamp for this grant

reco-list-ip-addresses


List observed IP addresses.

Base Command

reco-list-ip-addresses

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “country eq “CN” or hasVpn eq true”). Optional
limit The maximum number of IP addresses to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.IpAddresses.ipAddress String The IP address or CIDR range
Reco.IpAddresses.country String Country of the IP address
Reco.IpAddresses.asnName String ASN name of the IP address
Reco.IpAddresses.eventsCount Number Number of events from this IP
Reco.IpAddresses.usersCount Number Number of users seen from this IP
Reco.IpAddresses.hasVpn Boolean Whether the IP is associated with a VPN
Reco.IpAddresses.hasProxy Boolean Whether the IP is associated with a proxy
Reco.IpAddresses.lastEventTime Date Last event timestamp from this IP

reco-list-business-units


List external business units.

Base Command

reco-list-business-units

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “name eq “Finance””). Optional
limit The maximum number of business units to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.BusinessUnits.id String Business unit ID
Reco.BusinessUnits.name String Business unit name
Reco.BusinessUnits.manager String Manager of the business unit
Reco.BusinessUnits.createdAt Date Creation timestamp of the business unit

reco-list-audit-logs


List Reco platform audit logs.

Base Command

reco-list-audit-logs

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “userEmail eq “admin@example.com” and action eq “DELETE””). Optional
limit The maximum number of audit log entries to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.AuditLogs.id String Audit log entry ID
Reco.AuditLogs.userEmail String Email of the user who performed the action
Reco.AuditLogs.module String Module where the action was performed
Reco.AuditLogs.action String Action performed
Reco.AuditLogs.objectName String Name of the object affected
Reco.AuditLogs.timestamp Date Timestamp of the audit log entry
Reco.AuditLogs.remoteAddr String Remote IP address of the actor

reco-list-posture-checks


List posture check definitions.

Base Command

reco-list-posture-checks

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “severity eq “HIGH” and apps co “Google””). Optional
limit The maximum number of posture check definitions to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.PostureChecks.id String Posture check ID
Reco.PostureChecks.name String Posture check name
Reco.PostureChecks.severity String Severity of the posture check
Reco.PostureChecks.policyType String Policy type of the posture check
Reco.PostureChecks.apps Unknown Applications this posture check applies to
Reco.PostureChecks.type String Type of posture check (built-in or custom)

reco-list-threat-detection-policies


List threat detection policies.

Base Command

reco-list-threat-detection-policies

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “severity eq “HIGH” and status eq “ON””). Optional
limit The maximum number of policies to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.ThreatDetectionPolicies.id String Policy ID
Reco.ThreatDetectionPolicies.name String Policy name
Reco.ThreatDetectionPolicies.severity String Severity of the policy
Reco.ThreatDetectionPolicies.status String Status of the policy (ON, OFF, or PREVIEW)
Reco.ThreatDetectionPolicies.apps Unknown Applications monitored by the policy
Reco.ThreatDetectionPolicies.openAlerts Number Number of open alerts triggered by this policy
Reco.ThreatDetectionPolicies.type String Type of policy (built-in or custom)

reco-list-exclusions


List alert suppression exclusion rules.

Base Command

reco-list-exclusions

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “policyName co “MFA””). Optional
limit The maximum number of exclusions to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.Exclusions.id String Exclusion rule ID
Reco.Exclusions.name String Exclusion rule name
Reco.Exclusions.policyName String Name of the policy this exclusion applies to
Reco.Exclusions.apps Unknown Applications this exclusion applies to
Reco.Exclusions.createdBy String User who created the exclusion
Reco.Exclusions.createdAt Date Creation timestamp of the exclusion

reco-list-app-instances


List integrated app instances (app portfolio). Only returns instances with an active integration status.

Base Command

reco-list-app-instances

Input

Argument Name Description Required
filters The SCIM v2 filter expression (e.g. “isUsingAi eq true”). Optional
limit The maximum number of app instances to return. Default is 1000. Optional

Context Output

Path Type Description
Reco.AppInstances.id String App instance ID
Reco.AppInstances.name String App instance name
Reco.AppInstances.instanceType String Instance type
Reco.AppInstances.accountsCount Number Number of accounts
Reco.AppInstances.isUsingAi Boolean Whether AI features are used
Reco.AppInstances.saasToSaasCount Number SaaS-to-SaaS grant count
Reco.AppInstances.filesCount Number File count

<~PLATFORM>

License Requirements

The following configuration parameters require one of these licenses: Cortex XSIAM or Agentix:

  • Fetch incidents

</~PLATFORM>

Configuration parameters

  • url — Server URL (e.g. https://host.reco.ai/api/v1) (required)
  • api_token — JWT app token (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • incidentType — Incident type
  • isFetch — Fetch incidents
  • max_fetch — Max fetch
  • source — Source
  • before — Before
  • after — After
  • risk_level — Minimum risk level (e.g. MEDIUM fetches medium and higher)
  • first_fetch — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • incidentFetchInterval — Incidents Fetch Interval

Commands (37)

  • reco-add-comment-to-alert

    Add a comment to an alert in Reco.

  • reco-add-exclusion-filter

    Add exclusion filter to Reco Classifier.

  • reco-add-leaving-org-user-label

    Tag a user as leaving org user in Reco.

  • reco-add-risky-user-label

    Tag a user as risky in Reco.

  • reco-change-alert-status

    update alert status in Reco.

  • reco-get-3rd-parties-accessible-to-data-list

    Get 3rd parties accessible to sensitive assets.

  • reco-get-alert-ai-summary

    Get alert ai summary from Reco.

  • reco-get-apps

    Get app discovery data from Reco. Fetches all available apps using pagination.

  • reco-get-assets-by-id

    Get all assets from Reco by id.

  • reco-get-assets-shared-externally

    Get files user has access to from Reco.

  • reco-get-assets-user-has-access-to

    Get all files user has access to from Reco.

  • reco-get-files-exposed-to-email-address

    Get files user has access to from Reco.

  • reco-get-files-shared-with-3rd-parties

    Get files shared with 3rd parties.

  • reco-get-link-to-user-overview-page

    Generate a magic link for reco UI (overview page).

  • reco-get-private-email-list-with-access

    Get Private emails with access.

  • reco-get-risky-users

    Get Risky Users from Reco.

  • reco-get-sensitive-assets-by-id

    Get all sensitive assets from Reco by id.

  • reco-get-sensitive-assets-by-name

    Get all sensitive assets from Reco by name.

  • reco-get-sensitive-assets-with-public-link

    Get all sensitive assets with public link from Reco.

  • reco-get-user-context-by-email-address

    Get user context by email address from Reco.

  • reco-list-accounts

    Lists accounts (SaaS user accounts) from Reco using the external API.

  • reco-list-ai-agents

    Lists AI agents detected by Reco using the external API.

  • reco-list-app-instances

    Lists integrated app instances (app portfolio) from Reco using the external API. Only returns instances with an active integration status.

  • reco-list-audit-logs

    Lists Reco platform audit logs using the external API.

  • reco-list-business-units

    Lists external business units from Reco using the external API.

  • reco-list-devices

    Lists devices from Reco using the external API.

  • reco-list-events

    Lists SaaS events from Reco using the external API.

  • reco-list-exclusions

    Lists alert suppression exclusion rules from Reco using the external API.

  • reco-list-groups

    Lists SaaS groups from Reco using the external API.

  • reco-list-ip-addresses

    Lists observed IP addresses from Reco using the external API.

  • reco-list-posture-checks

    Lists posture check definitions from Reco using the external API.

  • reco-list-posture-issues

    Lists posture issues from Reco using the external API.

  • reco-list-saas-to-saas

    Lists SaaS-to-SaaS OAuth grants and integrations from Reco using the external API.

  • reco-list-threat-detection-policies

    Lists threat detection policies from Reco using the external API.

  • reco-resolve-visibility-event

    Resolve an event in Reco Finding. Reco Findings contains aggregations of events. This command resolves the event in the Reco Finding.

  • reco-set-app-authorization-status

    Set authorization status for an application in Reco.

  • reco-update-incident-timeline

    Add a comment to an alert in Reco.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401
import base64
import json
import time
from datetime import datetime, timedelta
import dateutil.parser

from typing import Any

ENTRY_TYPE_USER = "ENTRY_TYPE_USER"
ENTRY_TYPE_IDENTITY = "ENTRY_TYPE_IDENTITY"

LABEL_STATUS_ACTIVE = "LABEL_STATUS_ACTIVE"

RISKY_USER = "Risky User"

LEAVING_ORG_USER = "Leaving Org User"

ENTRY_TYPE_EVENT = "ENTRY_TYPE_EVENT"

LABEL_STATUS_RESOLVED = "LABEL_STATUS_RESOLVED"

FILTER_RELATIONSHIP_AND = "FILTER_RELATIONSHIP_AND"
PAGE_SIZE = 1000
MAX_PAGES = 1000  # hard cap on _paginate_all iterations; guards against an unbounded loop

DEMISTO_OCCURRED_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
RECO_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
DEMISTO_INFORMATIONAL = 0.5
RECO_API_TIMEOUT_IN_SECONDS = 180
RECO_ACTIVE_INCIDENTS_VIEW = "active_incidents_view"
RATE_LIMIT_MAX_RETRIES = 3
RATE_LIMIT_RETRY_BASE_DELAY = 2  # seconds; doubles on each attempt
ALERT_VIEW_WITH_SHARED_STATUS = "ALERT_VIEW_WITH_SHARED_STATUS"
RECO_TIMELINE_EVENT_TYPE = "TIMELINE_EVENT_TYPE_USER_COMMENT"
CREATED_AT_FIELD = "created_at"
STEP_FETCH = "fetch"
STEP_INIT = "init"

# Base path for the Reco external API (relative to /api/v1)
EXTERNAL_API_BASE = "/external-api"

# Maps numeric or string risk level values to the severity name expected by the external API.
RISK_LEVEL_TO_SEVERITY_NAME: dict[str, str] = {
    "10": "LOW",
    "LOW": "LOW",
    "low": "LOW",
    "20": "MEDIUM",
    "MEDIUM": "MEDIUM",
    "medium": "MEDIUM",
    "30": "HIGH",
    "HIGH": "HIGH",
    "high": "HIGH",
    "40": "CRITICAL",
    "CRITICAL": "CRITICAL",
    "critical": "CRITICAL",
}

# Ascending severity order, used to expand a minimum risk level into "that level and above".
SEVERITY_ORDER: list[str] = ["LOW", "MEDIUM", "HIGH", "CRITICAL"]


def parse_minimum_risk_level(risk_level_param: str | None) -> list[str] | None:
    """Parse a single risk_level value into the list of severity names at or above it.

    Accepts a numeric value (10/20/30/40) or a name (LOW/MEDIUM/HIGH/CRITICAL).
    Example: "MEDIUM" -> ["MEDIUM", "HIGH", "CRITICAL"] (fetches medium severity and higher).
    """
    if not risk_level_param:
        return None
    value = str(risk_level_param).strip()
    severity_name = RISK_LEVEL_TO_SEVERITY_NAME.get(value) or RISK_LEVEL_TO_SEVERITY_NAME.get(value.upper())
    if not severity_name:
        demisto.debug(f"Unknown risk level value '{value}', using upper-cased as-is")
        return [value.upper()]
    min_index = SEVERITY_ORDER.index(severity_name)
    return SEVERITY_ORDER[min_index:]


def extract_response(response: Any) -> list[dict[str, Any]]:
    if response.get("getTableResponse") is None:
        demisto.error(f"got bad response, {response}")
        raise Exception(f"got bad response, {response}")
    else:
        demisto.info(f"Count of entities: {response.get('getTableResponse').get('totalNumberOfResults')}")
        entities = response.get("getTableResponse", {}).get("data", {}).get("rows", [])
        demisto.info(f"Got {len(entities)} entities")
        return entities


class RecoClient(BaseClient):
    def __init__(self, api_token: str, base_url: str, verify: bool, proxy):
        super().__init__(
            base_url,
            verify=verify,
            proxy=proxy,
            headers={
                "Authorization": f"Bearer {api_token}",
                "User-Agent": f"xsoar/{get_pack_version()}",
            },
        )

    # --- External API helpers ---

    def _rate_limited_request(self, method: str, url_suffix: str, **kwargs) -> dict[str, Any]:
        """Wrap _http_request with automatic 429 retry and exponential backoff.

        Respects a Retry-After header when present; otherwise backs off
        RATE_LIMIT_RETRY_BASE_DELAY * 2^attempt seconds between attempts.
        Raises on the last attempt or on any non-429 error.
        """
        for attempt in range(RATE_LIMIT_MAX_RETRIES):
            try:
                return self._http_request(method=method, url_suffix=url_suffix, **kwargs)
            except DemistoException as exc:
                is_rate_limited = exc.res is not None and getattr(exc.res, "status_code", None) == 429
                if is_rate_limited and attempt < RATE_LIMIT_MAX_RETRIES - 1:
                    retry_after = int(
                        exc.res.headers.get(  # type: ignore[union-attr]
                            "Retry-After",
                            RATE_LIMIT_RETRY_BASE_DELAY * (2**attempt),
                        )
                    )
                    demisto.debug(
                        f"Rate limited (429) on {url_suffix}, "
                        f"retrying in {retry_after}s (attempt {attempt + 1}/{RATE_LIMIT_MAX_RETRIES})"
                    )
                    time.sleep(retry_after)  # pylint: disable=E9003
                else:
                    raise
        raise DemistoException(f"Max retries ({RATE_LIMIT_MAX_RETRIES}) exceeded for {url_suffix}")

    def _external_api_list(
        self,
        endpoint: str,
        filters: str = "",
        count: int = PAGE_SIZE,
        start_index: int = 0,
        sort_by: str = "",
        sort_order: str = "",
    ) -> dict[str, Any]:
        """GET /external-api/{endpoint} with SCIM filter, sort, and pagination params."""
        params: dict[str, Any] = {"count": count, "startIndex": start_index}
        if filters:
            params["filters"] = filters
        if sort_by:
            params["sortBy"] = sort_by
        if sort_order:
            params["sortOrder"] = sort_order
        return self._rate_limited_request(
            method="GET",
            url_suffix=f"{EXTERNAL_API_BASE}/{endpoint}",
            params=params,
            timeout=RECO_API_TIMEOUT_IN_SECONDS,
        )

    def _external_api_post(self, endpoint: str, body: dict[str, Any]) -> dict[str, Any]:
        """POST /external-api/{endpoint}."""
        return self._rate_limited_request(
            method="POST",
            url_suffix=f"{EXTERNAL_API_BASE}/{endpoint}",
            data=json.dumps(body),
            timeout=RECO_API_TIMEOUT_IN_SECONDS,
        )

    def _external_api_put(self, url_suffix: str, body: dict[str, Any]) -> dict[str, Any]:
        """PUT to an arbitrary external-api path suffix."""
        return self._rate_limited_request(
            method="PUT",
            url_suffix=url_suffix,
            data=json.dumps(body),
            timeout=RECO_API_TIMEOUT_IN_SECONDS,
        )

    def _paginate_all(
        self,
        endpoint: str,
        item_key: str,
        filters: str = "",
        page_size: int = 1000,
    ) -> list[dict[str, Any]]:
        """Fetch every page from a list endpoint, returning all items combined.

        Uses the `totalResults` field in the response to know when to stop.
        Stops early if an empty page is returned (defensive guard against
        totalResults being stale or the server having fewer items than reported).
        Capped at MAX_PAGES to guard against an unbounded loop if the server
        never reports totalResults or an empty page.
        """
        all_items: list[dict[str, Any]] = []
        start_index = 0
        for _page in range(MAX_PAGES):
            response = self._external_api_list(
                endpoint,
                filters=filters,
                count=page_size,
                start_index=start_index,
            )
            page_items = response.get(item_key, [])
            if not page_items:
                break
            all_items.extend(page_items)
            total = int(response.get("totalResults", 0) or response.get("total_results", 0) or 0)
            demisto.debug(f"_paginate_all {endpoint}: fetched {len(all_items)}/{total}")
            if total and len(all_items) >= total:
                break
            start_index += len(page_items)
        else:
            demisto.debug(f"_paginate_all {endpoint}: reached MAX_PAGES ({MAX_PAGES}) cap, stopping")
        return all_items

    # --- Alerts (external API) ---

    def get_alerts(
        self,
        risk_levels: list[str] | None = None,
        source: str | None = None,
        before: datetime | None = None,
        after: datetime | None = None,
        limit: int = PAGE_SIZE,
        start_index: int = 0,
    ) -> list[dict[str, Any]]:
        """List threat alerts via the external API, oldest first.

        risk_levels: list of severity names, e.g. ["HIGH", "CRITICAL"].  Multiple
        values are combined with OR so a single call fetches all matching severities.

        Sorted ascending by createdAt (rather than the API's default descending order) so
        that fetch_incidents can page through a burst via `start_index` without ever
        skipping alerts, regardless of how many share the same fetch window.

        Preview-state alerts are always excluded: a Policy in Preview state is explicitly
        meant to stay out of the customer's SOAR/ticketing systems while it's being
        evaluated (the webhook and share-service paths already enforce this upstream).
        """
        filter_parts: list[str] = ['status ne "ALERT_STATUS_PREVIEW"']

        if risk_levels:
            if len(risk_levels) == 1:
                filter_parts.append(f'severity eq "{risk_levels[0]}"')
            else:
                # Use the SCIM `in` operator - cleaner than a chain of OR clauses
                vals = ", ".join(f'"{r}"' for r in risk_levels)
                filter_parts.append(f"severity in [{vals}]")

        if source:
            filter_parts.append(f'apps co "{source}"')
        if after:
            filter_parts.append(f'createdAt gt "{after.strftime(DEMISTO_OCCURRED_FORMAT)}"')
        if before:
            filter_parts.append(f'createdAt lt "{before.strftime(DEMISTO_OCCURRED_FORMAT)}"')

        scim_filter = " and ".join(filter_parts)
        demisto.debug(f"get_alerts SCIM filter: {scim_filter!r}")

        try:
            response = self._external_api_list(
                "alerts/list",
                filters=scim_filter,
                count=limit,
                start_index=start_index,
                sort_by="createdAt",
                sort_order="ascending",
            )
            alerts = response.get("alerts", [])
            demisto.info(f"get_alerts: fetched {len(alerts)} alerts (total={response.get('totalResults', '?')})")
            return alerts
        except Exception as e:
            demisto.error(f"get_alerts error: {str(e)}")
            return []

    def get_single_alert(self, alert_id: str) -> dict[str, Any]:
        """Get full alert detail including policy violations via the external API."""
        try:
            response = self._rate_limited_request(
                method="GET",
                url_suffix=f"{EXTERNAL_API_BASE}/alert-details/{alert_id}",
                timeout=RECO_API_TIMEOUT_IN_SECONDS,
            )
            return response.get("alert", {})
        except Exception as e:
            demisto.error(f"get_single_alert({alert_id}) error: {str(e)}")
            raise

    def get_alert_ai_summary(self, alert_id: str) -> dict[str, Any]:  # pragma: no cover
        """Get alert AI summary (internal API - no external equivalent yet)."""
        try:
            return self._http_request(
                method="GET",
                url_suffix=f"/alert/summarize/{alert_id}",
                timeout=RECO_API_TIMEOUT_IN_SECONDS,
            )
        except Exception as e:
            demisto.error(f"get_alert_ai_summary error: {str(e)}")
            raise

    def change_alert_status(self, alert_id: str, status: str) -> Any:
        """Change alert status (internal API - no external equivalent yet)."""
        try:
            return self._http_request(
                method="PUT",
                url_suffix=f"/policy-subsystem/alert-inbox/{alert_id}/status/{status}",
                timeout=RECO_API_TIMEOUT_IN_SECONDS,
            )
        except Exception as e:
            demisto.error(f"change_alert_status error: {str(e)}")
            raise

    # --- Comments (external API) ---

    def update_reco_incident_timeline(self, incident_id: str, comment: str) -> Any:
        """Add a comment to an alert entity via the external API."""
        body: dict[str, Any] = {
            "entityId": incident_id,
            "entityType": "alert",
            "content": comment,
        }
        try:
            return self._external_api_post("comments/create", body)
        except Exception as e:
            demisto.error(f"update_reco_incident_timeline error: {str(e)}")
            raise

    # --- Labels (external API) ---

    def add_entity_label(
        self,
        entity_id: str,
        entity_type: str,
        label_name: str,
        instance_id: str = "",
    ) -> Any:
        """Add a label to any Reco entity via the external API.

        entity_type: "account", "identity", "app", "posture", "saas-to-saas",
                     "ip-address", "ai-agent", "device"
        """
        body: dict[str, Any] = {
            "entityId": entity_id,
            "entityType": entity_type,
            "labelName": label_name,
        }
        if instance_id:
            body["instanceId"] = instance_id
        try:
            return self._external_api_post("labels/add", body)
        except Exception as e:
            demisto.error(f"add_entity_label error: {str(e)}")
            raise

    def resolve_visibility_event(self, entity_id: str, label_name: str) -> Any:
        """Resolve a visibility event (internal API - no external equivalent)."""
        try:
            return self._http_request(
                method="PUT",
                url_suffix="/set-label-status",
                timeout=RECO_API_TIMEOUT_IN_SECONDS,
                data=json.dumps(
                    {
                        "labelsRelationStatusUpdate": [
                            {
                                "labelName": label_name,
                                "entryId": f"{entity_id}_visibility",
                                "entryType": ENTRY_TYPE_EVENT,
                                "newStatus": LABEL_STATUS_RESOLVED,
                                "oldStatus": LABEL_STATUS_ACTIVE,
                                "comment": "Resolved by XSOAR Automation",
                            }
                        ]
                    }
                ),
            )
        except Exception as e:
            demisto.error(f"resolve_visibility_event error: {str(e)}")
            raise

    # --- Identities / Users (external API) ---

    def get_identities(self, email_address: Optional[str] = None, label: Optional[str] = None) -> list[dict[str, Any]]:
        """List identities from the external API, optionally filtered by email."""
        filter_parts: list[str] = []
        if email_address:
            filter_parts.append(f'email co "{email_address}"')
        # label filter is handled separately via get_risky_users (ListAccounts)
        scim_filter = " and ".join(filter_parts)
        try:
            response = self._external_api_list("users/list", filters=scim_filter, count=PAGE_SIZE)
            return response.get("users", [])
        except Exception as e:
            demisto.error(f"get_identities error: {str(e)}")
            raise

    def get_risky_users(self) -> list[dict[str, Any]]:
        """List ALL accounts flagged as risky via the external API (auto-paginated)."""
        try:
            return self._paginate_all("accounts/list", "accounts", filters="isRiskyUser eq true")
        except Exception as e:
            demisto.error(f"get_risky_users error: {str(e)}")
            raise

    def get_user_context_by_email_address(self, email_address: str) -> list[dict[str, Any]]:
        """Get identity context for an email address via the external API."""
        try:
            response = self._external_api_list(
                "users/list",
                filters=f'email co "{email_address}"',
                count=10,
            )
            return response.get("users", [])
        except Exception as e:
            demisto.error(f"get_user_context_by_email_address error: {str(e)}")
            raise

    # --- Apps (external API) ---

    def get_app_discovery(
        self,
        before: datetime | None = None,
        after: datetime | None = None,
        limit: int | None = None,
    ) -> list[dict[str, Any]]:
        """List discovered apps via the external API. Auto-paginates all results when limit is None."""
        filter_parts: list[str] = []
        if after:
            filter_parts.append(f'lastSeen gt "{after.strftime(DEMISTO_OCCURRED_FORMAT)}"')
        if before:
            filter_parts.append(f'lastSeen lt "{before.strftime(DEMISTO_OCCURRED_FORMAT)}"')
        scim_filter = " and ".join(filter_parts)
        try:
            if limit is None:
                return self._paginate_all("apps/list", "apps", filters=scim_filter)
            response = self._external_api_list("apps/list", filters=scim_filter, count=limit)
            return response.get("apps", [])
        except Exception as e:
            demisto.error(f"get_app_discovery error: {str(e)}")
            raise

    def set_app_authorization_status(self, app_id: str, authorization_status: str) -> Any:
        """Update app authorization status via the external API."""
        try:
            return self._external_api_put(
                f"{EXTERNAL_API_BASE}/apps/{app_id}/auth-status",
                {"authorizationStatus": authorization_status},
            )
        except Exception as e:
            demisto.error(f"set_app_authorization_status error: {str(e)}")
            raise

    # --- Files / Assets (external API) ---

    def get_sensitive_assets_information(
        self, asset_name: str | None, asset_id: str | None, sensitive_only: bool, regex_search: bool
    ) -> list[dict[str, Any]]:
        """List files from the external API, optionally filtered by name or id (auto-paginated)."""
        filter_parts: list[str] = []
        if asset_name:
            op = "co" if regex_search else "eq"
            filter_parts.append(f'name {op} "{asset_name}"')
        elif asset_id:
            filter_parts.append(f'id eq "{asset_id}"')
        if sensitive_only:
            filter_parts.append('sensitivityLevel in ["30","40"]')
        scim_filter = " and ".join(filter_parts)
        try:
            return self._paginate_all("files/list", "files", filters=scim_filter)
        except Exception as e:
            demisto.error(f"get_sensitive_assets_information error: {str(e)}")
            raise

    # --- Internal API methods (data risk management - no external equivalent) ---

    def get_exposed_publicly_files_at_risk(self) -> list[dict[str, Any]]:
        """Get exposed publicly files at risk (internal API)."""
        params: Dict[str, Any] = {
            "getTableRequest": {
                "tableName": "DATA_RISK_MANAGEMENT_VIEW_BREAKDOWN_EXPOSED_PUBLICLY",
                "pageSize": PAGE_SIZE,
                "fieldSorts": {"sorts": [{"sortBy": "last_access_date", "sortDirection": "SORT_DIRECTION_DESC"}]},
                "fieldFilters": {
                    "relationship": "FILTER_RELATIONSHIP_OR",
                    "filters": {"filters": [{"field": "data_category", "stringEquals": {"value": "ALL"}}]},
                },
            }
        }
        try:
            response = self._http_request(
                method="PUT",
                url_suffix="/risk-management/get-data-risk-management-table",
                timeout=RECO_API_TIMEOUT_IN_SECONDS,
                data=json.dumps(params),
            )
            return extract_response(response)
        except Exception as e:
            demisto.error(f"get_exposed_publicly_files_at_risk error: {str(e)}")
            raise

    def get_files_exposed_to_email(self, email_account) -> list[dict[str, Any]]:
        """Get files exposed to an email account (internal API)."""
        params = {
            "getTableRequest": {
                "tableName": "data_posture_view_files_by_emails_slider",
                "pageSize": PAGE_SIZE,
                "fieldSorts": {"sorts": [{"sortBy": "last_access_date", "sortDirection": "SORT_DIRECTION_DESC"}]},
                "fieldFilters": {
                    "relationship": "FILTER_RELATIONSHIP_AND",
                    "fieldFilterGroups": {
                        "fieldFilters": [
                            {
                                "relationship": "FILTER_RELATIONSHIP_AND",
                                "fieldFilterGroups": {
                                    "fieldFilters": [
                                        {
                                            "relationship": "FILTER_RELATIONSHIP_AND",
                                            "filters": {
                                                "filters": [
                                                    {"field": "email_account", "stringEquals": {"value": f"{email_account}"}}
                                                ]
                                            },
                                        }
                                    ]
                                },
                                "forceEstimateSize": True,
                            }
                        ]
                    },
                    "forceEstimateSize": True,
                },
            }
        }
        try:
            response = self._http_request(
                method="PUT",
                url_suffix="/risk-management/get-data-risk-management-table",
                timeout=RECO_API_TIMEOUT_IN_SECONDS,
                data=json.dumps(params),
            )
            return extract_response(response)
        except Exception as e:
            demisto.error(f"get_files_exposed_to_email error: {str(e)}")
            raise

    def get_list_of_private_emails_with_access(self) -> list[dict[str, Any]]:
        """Get private email addresses with file access (internal API)."""
        params = {
            "getTableRequest": {
                "tableName": "data_posture_view_private_email_with_access",
                "pageSize": PAGE_SIZE,
                "fieldSorts": {"sorts": [{"sortBy": "files_num", "sortDirection": "SORT_DIRECTION_DESC"}]},
                "fieldFilters": {
                    "relationship": "FILTER_RELATIONSHIP_AND",
                    "fieldFilterGroups": {"fieldFilters": []},
                    "forceEstimateSize": True,
                },
            }
        }
        try:
            response = self._http_request(
                method="PUT",
                url_suffix="/risk-management/get-data-risk-management-table",
                timeout=RECO_API_TIMEOUT_IN_SECONDS,
                data=json.dumps(params),
            )
            return extract_response(response)
        except Exception as e:
            demisto.error(f"get_list_of_private_emails_with_access error: {str(e)}")
            raise

    @staticmethod
    def get_date_time_before_days_formatted(last_interaction_time_before_in_days: int) -> str:
        thirty_days_ago = datetime.utcnow() - timedelta(days=last_interaction_time_before_in_days)
        return thirty_days_ago.strftime("%Y-%m-%dT%H:%M:%S.999Z")

    def get_3rd_parties_risk_list(self, last_interaction_time_before_in_days: int) -> list[dict[str, Any]]:
        """Get 3rd party domains with file access (internal API)."""
        formatted_date = self.get_date_time_before_days_formatted(last_interaction_time_before_in_days)
        params = {
            "getTableRequest": {
                "tableName": "data_posture_view_3rd_parties_domain",
                "pageSize": PAGE_SIZE,
                "fieldSorts": {"sorts": [{"sortBy": "files_num", "sortDirection": "SORT_DIRECTION_DESC"}]},
                "fieldFilters": {
                    "relationship": "FILTER_RELATIONSHIP_AND",
                    "fieldFilterGroups": {
                        "fieldFilters": [
                            {
                                "relationship": "FILTER_RELATIONSHIP_AND",
                                "fieldFilterGroups": {
                                    "fieldFilters": [
                                        {
                                            "relationship": "FILTER_RELATIONSHIP_AND",
                                            "filters": {
                                                "filters": [{"field": "last_activity", "before": {"value": f"{formatted_date}"}}]
                                            },
                                        }
                                    ]
                                },
                            }
                        ]
                    },
                    "forceEstimateSize": True,
                },
            }
        }
        try:
            response = self._http_request(
                method="PUT",
                url_suffix="/risk-management/get-data-risk-management-table",
                timeout=RECO_API_TIMEOUT_IN_SECONDS,
                data=json.dumps(params),
            )
            return extract_response(response)
        except Exception as e:
            demisto.error(f"get_3rd_parties_risk_list error: {str(e)}")
            raise

    def get_files_shared_with_3rd_parties(self, domain: str, last_interaction_time_before_in_days: int) -> list[dict[str, Any]]:
        """Get files shared with a specific 3rd party domain (internal API)."""
        formatted_date = self.get_date_time_before_days_formatted(last_interaction_time_before_in_days)
        params = {
            "getTableRequest": {
                "tableName": "data_posture_view_files_by_domain_slider",
                "pageSize": PAGE_SIZE,
                "fieldSorts": {"sorts": [{"sortBy": "last_access_date", "sortDirection": "SORT_DIRECTION_ASC"}]},
                "fieldFilters": {
                    "relationship": "FILTER_RELATIONSHIP_AND",
                    "fieldFilterGroups": {
                        "fieldFilters": [
                            {
                                "relationship": "FILTER_RELATIONSHIP_AND",
                                "filters": {
                                    "filters": [
                                        {"field": "domain", "regexCaseInsensitive": {"value": f"{domain}"}},
                                        {"field": "last_access_date", "before": {"value": f"{formatted_date}"}},
                                    ]
                                },
                            }
                        ]
                    },
                    "forceEstimateSize": True,
                },
            }
        }
        try:
            response = self._http_request(
                method="PUT",
                url_suffix="/risk-management/get-data-risk-management-table",
                timeout=RECO_API_TIMEOUT_IN_SECONDS,
                data=json.dumps(params),
            )
            return extract_response(response)
        except Exception as e:
            demisto.error(f"get_files_shared_with_3rd_parties error: {str(e)}")
            raise

    def get_assets_user_has_access(self, email_address: str, only_sensitive: bool) -> list[dict[str, Any]]:
        """Get assets a user has access to (internal API)."""
        params: dict[str, Any] = {
            "getTableRequest": {
                "tableName": "files_view",
                "pageSize": PAGE_SIZE,
                "fieldFilters": {
                    "relationship": "FILTER_RELATIONSHIP_AND",
                    "fieldFilterGroups": {
                        "fieldFilters": [
                            {
                                "relationship": "FILTER_RELATIONSHIP_OR",
                                "filters": {
                                    "filters": [
                                        {
                                            "field": "currently_permitted_users",
                                            "regexCaseInsensitive": {"value": email_address},
                                        }
                                    ]
                                },
                            }
                        ]
                    },
                },
            }
        }
        if only_sensitive is True:
            params["getTableRequest"]["fieldFilters"]["fieldFilterGroups"]["fieldFilters"].append(
                {
                    "relationship": "FILTER_RELATIONSHIP_OR",
                    "filters": {
                        "filters": [
                            {"field": "sensitivity_level", "stringEquals": {"value": "30"}},
                            {"field": "sensitivity_level", "stringEquals": {"value": "40"}},
                        ]
                    },
                }
            )
        try:
            response = self._http_request(
                method="PUT",
                url_suffix="/asset-management/query",
                timeout=RECO_API_TIMEOUT_IN_SECONDS * 2,
                data=json.dumps(params),
            )
            return extract_response(response)
        except Exception as e:
            demisto.error(f"get_assets_user_has_access error: {str(e)}")
            raise

    def get_assets_shared_externally(self, email_address: str) -> list[dict[str, Any]]:
        """Get assets an owner has shared externally (internal API)."""
        params: dict[str, Any] = {
            "getTableRequest": {
                "tableName": "files_view",
                "pageSize": PAGE_SIZE,
                "fieldSorts": {"sorts": [{"sortBy": "last_access_date", "sortDirection": "SORT_DIRECTION_DESC"}]},
                "fieldFilters": {
                    "relationship": "FILTER_RELATIONSHIP_AND",
                    "fieldFilterGroups": {
                        "fieldFilters": [
                            {
                                "relationship": "FILTER_RELATIONSHIP_OR",
                                "filters": {
                                    "filters": [
                                        {
                                            "field": "permission_visibility",
                                            "stringEquals": {"value": "PERMISSION_TYPE_SHARED_EXTERNALLY"},
                                        }
                                    ]
                                },
                            },
                            {
                                "relationship": "FILTER_RELATIONSHIP_OR",
                                "filters": {
                                    "filters": [{"field": "file_owner", "stringContains": {"value": f"{email_address}"}}]
                                },
                            },
                        ]
                    },
                    "forceEstimateSize": True,
                },
            }
        }
        try:
            response = self._http_request(
                method="PUT",
                url_suffix="/asset-management/query",
                timeout=RECO_API_TIMEOUT_IN_SECONDS * 2,
                data=json.dumps(params),
            )
            return extract_response(response)
        except Exception as e:
            demisto.error(f"get_assets_shared_externally error: {str(e)}")
            raise

    def get_link_to_user_overview_page(self, link_type: str, entity_id: str) -> str:
        """Get a magic link to a Reco UI overview page (internal API)."""
        try:
            response = self._http_request(
                method="GET",
                url_suffix=f"/risk-management/risk-management/link?link_type={link_type}&param={entity_id}",
                timeout=RECO_API_TIMEOUT_IN_SECONDS,
            )
            link = response.get("link", None)
        except Exception as e:
            demisto.error(f"get_link_to_user_overview_page error: {str(e)}")
            raise
        return link  # type: ignore[return-value]

    def add_exclusion_filter(self, key_to_add: str, values_to_add: list[str]):
        """Add exclusion filter values (internal API)."""
        body = {"environmentName": "string", "keyToAddTo": key_to_add, "valuesToAdd": values_to_add}
        try:
            return self._http_request(
                method="POST",
                url_suffix="/algo/add_values_to_data_type_exclude_analyzer",
                timeout=RECO_API_TIMEOUT_IN_SECONDS * 2,
                data=json.dumps(body),
            )
        except Exception as e:
            demisto.error(f"add_exclusion_filter error: {str(e)}")
            raise

    # --- New external API list methods ---

    def list_events(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List SaaS events via the external API."""
        return self._external_api_list("events/list", filters=filters, count=count)

    def list_posture_issues(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List posture issues via the external API."""
        return self._external_api_list("posture-issues/list", filters=filters, count=count)

    def list_accounts(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List accounts via the external API."""
        return self._external_api_list("accounts/list", filters=filters, count=count)

    def list_devices(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List devices via the external API."""
        return self._external_api_list("devices/list", filters=filters, count=count)

    def list_ai_agents(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List AI agents via the external API."""
        return self._external_api_list("ai-agents/list", filters=filters, count=count)

    def list_integrations(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List connected integrations via the external API."""
        return self._external_api_list("integrations/list", filters=filters, count=count)

    def list_groups(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List SaaS groups via the external API."""
        return self._external_api_list("groups/list", filters=filters, count=count)

    def list_saas_to_saas(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List SaaS-to-SaaS grants via the external API."""
        return self._external_api_list("saas-to-saas/list", filters=filters, count=count)

    def list_ip_addresses(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List observed IP addresses via the external API."""
        return self._external_api_list("ip-addresses/list", filters=filters, count=count)

    def list_business_units(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List external business units via the external API."""
        return self._external_api_list("business-units/list", filters=filters, count=count)

    def list_audit_logs(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List platform audit logs via the external API."""
        return self._external_api_list("audit-logs/list", filters=filters, count=count)

    def list_posture_checks(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List posture check definitions via the external API."""
        return self._external_api_list("posture-checks/list", filters=filters, count=count)

    def list_threat_detection_policies(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List threat detection policies via the external API."""
        return self._external_api_list("policies/list", filters=filters, count=count)

    def list_exclusions(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List alert suppression exclusion rules via the external API."""
        return self._external_api_list("exclusions/list", filters=filters, count=count)

    def list_app_instances(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List integrated app instances (portfolio) via the external API."""
        return self._external_api_list("app-instances/list", filters=filters, count=count)

    def list_labeled_netapp_files(self, filters: str = "", count: int = PAGE_SIZE) -> dict[str, Any]:
        """List NetApp files carrying an active label via the external API."""
        return self._external_api_list("labeled-netapp-files/list", filters=filters, count=count)

    # --- API key validation ---

    def validate_api_key(self) -> str:
        """Validate the API token by making a minimal external API call."""
        try:
            self._external_api_list("alerts/list", count=1)
            return "ok"
        except Exception as e:
            demisto.error(f"validate_api_key error: {str(e)}")
            raise


# --- Helpers ---


def add_legacy_output_aliases(items: list[dict[str, Any]], aliases: dict[str, str]) -> list[dict[str, Any]]:
    """Copy renamed External API fields back onto their pre-migration key names, in place.

    Restores backward compatibility for playbooks/automations built against the old
    internal-API field names, alongside the new camelCase External API fields.
    """
    for item in items:
        for old_key, new_key in aliases.items():
            if new_key in item:
                item[old_key] = item[new_key]
    return items


def parse_table_row_to_dict(alert: list[dict[str, Any]]) -> dict[str, Any]:
    """Decode a table row returned by Reco's internal Table API (base64-encoded cells)."""
    if alert is None:
        return {}
    alert_as_dict = {}
    for obj in alert:
        key = obj.get("key", None)
        value = obj.get("value", None)
        if key is None or value is None:
            continue
        obj[key] = base64.b64decode(value).decode("utf-8")
        if key != "labels":
            obj[key] = obj[key].replace('"', "")
        if key in ["updated_at", "created_at", "event_time"]:
            try:
                parsed_time = datetime.strptime(obj[key], RECO_TIME_FORMAT)
            except Exception:
                parsed_time = datetime.strptime(obj[key], DEMISTO_OCCURRED_FORMAT)
            if parsed_time:
                obj[key] = parsed_time.strftime(DEMISTO_OCCURRED_FORMAT)
        if key == "risk_level":
            try:
                obj[key] = int(obj[key])
            except Exception:
                pass
        if key in ["group", "job_title", "departments", "labels"]:
            try:
                obj[key] = json.loads(obj[key])
            except Exception:
                pass
        alert_as_dict[key] = obj[key]
    return alert_as_dict


# --- Alert fetching ---


def get_alerts(
    reco_client: RecoClient,
    risk_levels: list[str] | None = None,
    source: str | None = None,
    before: datetime | None = None,
    after: datetime | None = None,
    limit: int = PAGE_SIZE,
    start_index: int = 0,
) -> tuple[list[Any], int]:
    """Fetch alert summaries then enrich each with full detail (policy violations).

    Returns the enriched ThreatAlertDetail dicts alongside the raw page size, so callers
    can tell whether the page was truncated by `limit` (more alerts may remain).
    """
    raw_alerts = reco_client.get_alerts(risk_levels, source, before, after, limit, start_index)
    alerts_data: list[Any] = []

    for raw in raw_alerts:
        alert_id = raw.get("id")
        if not alert_id:
            demisto.debug(f"Alert without id: {raw}")
            continue
        try:
            detail = reco_client.get_single_alert(alert_id)
            if not detail:
                continue
            # policy violations arrive as JSON strings (not base64) from the external API
            for violation in detail.get("policyViolations", []):
                json_data = violation.get("jsonData", "{}")
                if isinstance(json_data, str):
                    try:
                        parsed = json.loads(json_data)
                        parsed.pop("violation", None)
                        violation["jsonData"] = parsed
                    except json.JSONDecodeError:
                        pass
            alerts_data.append(detail)
        except Exception as e:
            demisto.error(f"Failed to enrich alert {alert_id}: {str(e)}")

    demisto.info(f"get_alerts: enriched {len(alerts_data)}/{len(raw_alerts)} alerts")
    return alerts_data, len(raw_alerts)


# --- Score mapping ---


def map_reco_score_to_demisto_score(reco_score: int) -> int | float:
    demisto_informational = 0.5
    demisto_medium = 2
    demisto_high = 3
    demisto_critical = 4
    MAPPING = {40: demisto_critical, 30: demisto_high, 20: demisto_medium, 10: demisto_informational, 0: demisto_informational}
    tier = min(40, max(0, (reco_score // 10) * 10))
    return MAPPING.get(tier, demisto_informational)


def map_reco_alert_score_to_demisto_score(reco_score: str) -> int | float:
    demisto_informational = 0.5
    demisto_medium = 2
    demisto_high = 3
    demisto_critical = 4
    MAPPING = {"CRITICAL": demisto_critical, "HIGH": demisto_high, "MEDIUM": demisto_medium, "LOW": demisto_informational}
    return MAPPING.get(reco_score, demisto_informational)


def _reco_risk_to_demisto_severity(raw_risk: Any) -> int | float:
    """Normalize Reco risk level to XSOAR severity. Handles int, numeric str, or name str."""
    if raw_risk is None:
        return map_reco_score_to_demisto_score(10)
    if isinstance(raw_risk, int):
        return map_reco_score_to_demisto_score(min(40, max(10, raw_risk)))
    if isinstance(raw_risk, str):
        stripped = raw_risk.strip().upper()
        if stripped in ("CRITICAL", "HIGH", "MEDIUM", "LOW"):
            return map_reco_alert_score_to_demisto_score(stripped)
        try:
            return map_reco_score_to_demisto_score(min(40, max(10, int(raw_risk))))
        except (TypeError, ValueError):
            pass
    try:
        return map_reco_score_to_demisto_score(min(40, max(10, int(raw_risk))))
    except (TypeError, ValueError):
        return map_reco_score_to_demisto_score(10)


# --- Incident parsing ---


def parse_alerts_to_incidents(alerts: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Convert ThreatAlertDetail dicts (external API) to XSOAR incident format.

    Also handles the legacy internal-API shape (cells[]) for backward compatibility.
    """
    incidents = []
    for alert in alerts:
        # Support legacy row format (cells[]) and modern external API dicts
        alert_dict = parse_table_row_to_dict(alert.get("cells", {})) if alert.get("cells") else alert
        occurred = alert_dict.get("created_at") or alert_dict.get("createdAt", "")
        raw_risk = alert_dict.get("risk_level") or alert_dict.get("riskLevel") or alert_dict.get("severity")
        incident = {
            "name": alert_dict.get("description", ""),
            "occurred": occurred,
            "dbotMirrorId": alert_dict.get("id", ""),
            "rawJSON": json.dumps(alert),
            "severity": _reco_risk_to_demisto_severity(raw_risk),
        }
        incidents.append(incident)
    return incidents


def get_max_fetch(max_fetch: int) -> int:
    return min(max_fetch, 500)


# --- Fetch incidents ---


def fetch_incidents(
    reco_client: RecoClient,
    last_run: dict[str, Any],
    max_fetch: int,
    risk_levels: list[str] | None = None,
    source: str | None = None,
    before: datetime | None = None,
    after: datetime | None = None,
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
    demisto.info(f"fetch-incidents called {max_fetch=}")
    next_run: dict[str, Any] = {}

    last_run_time = last_run.get("lastRun", None)
    if last_run_time is not None:
        after = dateutil.parser.parse(last_run_time)
    start_index = last_run.get("startIndex", 0)

    alerts, raw_page_size = get_alerts(reco_client, risk_levels, source, before, after, max_fetch, start_index)
    incidents = parse_alerts_to_incidents(alerts)

    existing_incidents = last_run.get("incident_ids", [])
    incidents = [
        incident
        for incident in incidents
        if (incident.get("severity", 0) > DEMISTO_INFORMATIONAL)
        and (incident.get("dbotMirrorId", None) not in existing_incidents)
    ]

    if raw_page_size < max_fetch:
        # The window is fully drained - safe to advance the timestamp cursor.
        incidents_sorted = sorted(incidents, key=lambda k: k["occurred"])
        if incidents_sorted:
            last_occurred_dt = dateutil.parser.parse(incidents_sorted[-1]["occurred"])
            next_run["lastRun"] = (last_occurred_dt + timedelta(seconds=1)).strftime(DEMISTO_OCCURRED_FORMAT)
        else:
            next_run["lastRun"] = last_run_time
        next_run["startIndex"] = 0
    else:
        # The page was full - more alerts may remain in this window. Keep lastRun in place
        # and resume from start_index next cycle, so a burst larger than max_fetch is
        # drained over multiple cycles instead of the excess being permanently skipped.
        next_run["lastRun"] = last_run_time
        next_run["startIndex"] = start_index + raw_page_size

    next_run["incident_ids"] = existing_incidents + [inc["dbotMirrorId"] for inc in incidents]

    return next_run, incidents


# --- Command functions ---


def get_risky_users_from_reco(reco_client: RecoClient) -> CommandResults:
    """Return accounts flagged as risky."""
    accounts = reco_client.get_risky_users()
    return CommandResults(
        readable_output=tableToMarkdown(
            "Risky Users",
            accounts,
            headers=["id", "name", "accountEmail", "permissions", "openAlerts", "roles", "isAdmin", "lastSeen"],
        ),
        outputs_prefix="Reco.RiskyUsers",
        outputs_key_field="accountEmail",
        outputs=accounts,
        raw_response=accounts,
    )


def add_risky_user_label(reco_client: RecoClient, email_address: str) -> CommandResults:
    """Tag an identity as risky via the external API labels endpoint."""
    identities = reco_client.get_identities(email_address=email_address)
    if not identities:
        return CommandResults(readable_output=f"No identity found for {email_address}")
    raw_response = None
    for identity in identities:
        identity_id = identity.get("id")
        if identity_id:
            raw_response = reco_client.add_entity_label(identity_id, "identity", RISKY_USER)
    return CommandResults(
        raw_response=raw_response,
        readable_output=f"User {email_address} labeled as risky",
    )


def add_leaving_org_user(reco_client: RecoClient, email_address: str) -> CommandResults:  # pragma: no cover
    """Tag an identity as a leaving-org user via the external API labels endpoint."""
    identities = reco_client.get_identities(email_address=email_address)
    if not identities:
        return CommandResults(readable_output=f"No identity found for {email_address}")
    raw_response = None
    for identity in identities:
        identity_id = identity.get("id")
        if identity_id:
            raw_response = reco_client.add_entity_label(identity_id, "identity", LEAVING_ORG_USER)
    return CommandResults(
        raw_response=raw_response,
        readable_output=f"User {email_address} labeled as leaving org user",
    )


def get_alert_ai_summary(reco_client: RecoClient, alert_id: str) -> CommandResults:
    response = reco_client.get_alert_ai_summary(alert_id)
    content = str(response.get("markdown")) if response.get("markdown") else json.dumps(response)
    return CommandResults(
        readable_output=content,
        outputs_prefix="Reco.AlertSummary",
        outputs_key_field="alert_id",
        outputs=response,
        raw_response=response,
    )


def get_assets_user_has_access(reco_client: RecoClient, email_address: str, only_sensitive: bool) -> CommandResults:
    assets = reco_client.get_assets_user_has_access(email_address, only_sensitive)
    assets_list = [parse_table_row_to_dict(a.get("cells", {})) for a in assets]
    return CommandResults(
        readable_output=tableToMarkdown(
            "Assets",
            assets_list,
            headers=["file_name", "file_owner", "file_url", "currently_permitted_users", "visibility", "location", "source"],
        ),
        outputs_prefix="Reco.Assets",
        outputs_key_field="asset_value",
        outputs=assets_list,
        raw_response=assets,
    )


def get_sensitive_assets_shared_with_public_link(reco_client: RecoClient) -> CommandResults:
    assets = reco_client.get_exposed_publicly_files_at_risk()
    assets_list = [parse_table_row_to_dict(a.get("cells", {})) for a in assets]
    return CommandResults(
        readable_output=tableToMarkdown(
            "Assets",
            assets_list,
            headers=["asset_id", "asset", "data_category", "data_categories", "last_access_date", "visibility", "location"],
        ),
        outputs_prefix="Reco.Assets",
        outputs_key_field="asset_id",
        outputs=assets_list,
        raw_response=assets,
    )


def get_assets_shared_externally_command(reco_client: RecoClient, email_address: str) -> CommandResults:
    assets = reco_client.get_assets_shared_externally(email_address)
    assets_list = [parse_table_row_to_dict(a.get("cells", {})) for a in assets]
    return CommandResults(
        readable_output=tableToMarkdown(
            "Assets",
            assets_list,
            headers=[
                "asset_id",
                "asset",
                "data_category",
                "data_categories",
                "last_access_date",
                "visibility",
                "location",
                "file_owner",
            ],
        ),
        outputs_prefix="Reco.Assets",
        outputs_key_field="asset_id",
        outputs=assets_list,
        raw_response=assets,
    )


def get_files_exposed_to_email_command(reco_client: RecoClient, email_account: str) -> CommandResults:
    assets = reco_client.get_files_exposed_to_email(email_account)
    assets_list = [parse_table_row_to_dict(a.get("cells", {})) for a in assets]
    return CommandResults(
        readable_output=tableToMarkdown(
            "Assets",
            assets_list,
            headers=[
                "asset_id",
                "asset",
                "data_category",
                "data_categories",
                "last_access_date",
                "visibility",
                "location",
                "email_account",
                "file_owner",
            ],
        ),
        outputs_prefix="Reco.Assets",
        outputs_key_field="asset_id",
        outputs=assets_list,
        raw_response=assets,
    )


def get_3rd_parties_list(reco_client: RecoClient, last_interaction_time_in_days: int) -> CommandResults:
    domains = reco_client.get_3rd_parties_risk_list(last_interaction_time_in_days)
    domains_list = [parse_table_row_to_dict(d.get("cells", {})) for d in domains]
    return CommandResults(
        readable_output=tableToMarkdown(
            "Domains",
            domains_list,
            headers=["domain", "last_activity", "files_num", "users_with_access_num"],
        ),
        outputs_prefix="Reco.Domains",
        outputs_key_field="domain",
        outputs=domains_list,
        raw_response=domains,
    )


def get_files_shared_with_3rd_parties(
    reco_client: RecoClient, domain: str, last_interaction_time_before_in_days: int
) -> CommandResults:
    files = reco_client.get_files_shared_with_3rd_parties(domain, last_interaction_time_before_in_days)
    files_list = [parse_table_row_to_dict(f.get("cells", {})) for f in files]
    return CommandResults(
        readable_output=tableToMarkdown(
            "Files",
            files_list,
            headers=["domain", "location", "users", "file_owner", "data_category", "asset", "last_access_date", "asset_id"],
        ),
        outputs_prefix="Reco.Assets",
        outputs_key_field="asset_id",
        outputs=files_list,
        raw_response=files,
    )


LEGACY_ASSET_FIELD_ALIASES = {
    "file_name": "name",
    "file_owner": "owner",
    "file_url": "url",
    "sensitivity_level": "sensitivityLevel",
    "visibility": "permissionVisibility",
}


def assets_to_command_result(files: list[dict[str, Any]]) -> CommandResults:
    """Convert File objects (external API) to CommandResults for asset commands."""
    add_legacy_output_aliases(files, LEGACY_ASSET_FIELD_ALIASES)
    return CommandResults(
        readable_output=tableToMarkdown(
            "Assets",
            files,
            headers=["id", "name", "owner", "url", "sensitivityLevel", "permissionVisibility", "location", "dataCategories"],
        ),
        outputs_prefix="Reco.SensitiveAssets",
        outputs_key_field="id",
        outputs=files,
        raw_response=files,
    )


def get_sensitive_assets_by_name(reco_client: RecoClient, asset_name: str, regex_search: bool) -> CommandResults:
    files = reco_client.get_sensitive_assets_information(asset_name, None, True, regex_search)
    return assets_to_command_result(files)


def get_assets_by_id(reco_client: RecoClient, asset_id: str) -> CommandResults:
    files = reco_client.get_sensitive_assets_information(None, asset_id, False, False)
    return assets_to_command_result(files)


def get_sensitive_assets_by_id(reco_client: RecoClient, asset_id: str) -> CommandResults:
    files = reco_client.get_sensitive_assets_information(None, asset_id, True, False)
    return assets_to_command_result(files)


LEGACY_USER_FIELD_ALIASES = {
    "email_account": "email",
    "full_name": "name",
    "job_titles": "jobTitles",
}


def get_user_context_by_email_address(reco_client: RecoClient, email_address: str) -> CommandResults:
    """Return identity context for an email address (external API)."""
    users = reco_client.get_user_context_by_email_address(email_address)
    user_data = users[0] if users else None
    if user_data:
        add_legacy_output_aliases([user_data], LEGACY_USER_FIELD_ALIASES)
        # "category" has no direct External API field; approximate from isInternal, matching
        # the old values ("internal"/"external"). "groups" and "labels" have no equivalent
        # data in the External API and are not populated.
        if "isInternal" in user_data:
            user_data["category"] = "internal" if user_data["isInternal"] else "external"
    return CommandResults(
        readable_output=tableToMarkdown("User", user_data, headers=list(user_data.keys()) if user_data else []),
        outputs_prefix="Reco.User",
        outputs_key_field="email",
        outputs=user_data,
        raw_response=users,
    )


def add_exclusion_filter(reco_client: RecoClient, key_to_add: str, values: list[str]) -> CommandResults:  # pragma: no cover
    response = reco_client.add_exclusion_filter(key_to_add, values)
    return CommandResults(raw_response=response, readable_output="Exclusion filter added successfully")


def change_alert_status(reco_client: RecoClient, alert_id: str, status: str) -> CommandResults:  # pragma: no cover
    response = reco_client.change_alert_status(alert_id, status)
    return CommandResults(raw_response=response, readable_output=f"Alert {alert_id} status changed successfully to {status}")


def get_private_email_list_with_access(reco_client: RecoClient) -> CommandResults:
    result = reco_client.get_list_of_private_emails_with_access()
    identities_list = [parse_table_row_to_dict(i.get("cells", {})) for i in result]
    return CommandResults(
        readable_output=tableToMarkdown(
            "PrivateEmails",
            identities_list,
            headers=["email_account", "primary_email", "files_num", "user_category"],
        ),
        outputs_prefix="Reco.privateEmails",
        outputs_key_field="email_account",
        outputs=identities_list,
        raw_response=result,
    )


def get_link_to_user_overview_page(reco_client: RecoClient, entity: str, link_type: str) -> CommandResults:
    link = reco_client.get_link_to_user_overview_page(link_type, entity)
    return CommandResults(outputs_prefix="Reco.Link", outputs={"link": link}, raw_response=link)


LEGACY_APP_FIELD_ALIASES = {
    "app_id": "id",
    "app_name": "name",
    "users_count": "usersCount",
    "created_at": "firstSeen",
}


def get_apps_command(
    reco_client: RecoClient, before: datetime | None = None, after: datetime | None = None, limit: int = PAGE_SIZE
) -> CommandResults:
    """List discovered apps from the external API."""
    apps = reco_client.get_app_discovery(before=before, after=after, limit=limit)
    # "risk_score", "data_access", "status", and "updated_at" have no equivalent field in the
    # External API and are not populated.
    add_legacy_output_aliases(apps, LEGACY_APP_FIELD_ALIASES)
    headers = ["id", "name", "category", "usersCount", "authorization", "isUsingAi", "vendorGrade", "aiCapability", "lastSeen"]
    return CommandResults(
        readable_output=tableToMarkdown("App Discovery", apps, headers=headers),
        outputs_prefix="Reco.Apps",
        outputs_key_field="id",
        outputs=apps,
        raw_response=apps,
    )


def set_app_authorization_status_command(reco_client: RecoClient, app_id: str, authorization_status: str) -> CommandResults:
    """Update app authorization status via the external API."""
    reco_client.set_app_authorization_status(app_id, authorization_status)
    return CommandResults(
        readable_output=f"App {app_id} authorization status updated to {authorization_status}",
        outputs_prefix="Reco.AppAuthorization",
        outputs={"app_id": app_id, "authorization_status": authorization_status, "updated": True},
    )


# --- New command functions (external API) ---


def list_events_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List SaaS events from the external API."""
    response = reco_client.list_events(filters=filters, count=limit)
    events = response.get("events", [])
    flat: list[dict[str, Any]] = []
    for e in events:
        row = dict(e)
        actor = e.get("actor") or {}
        row["actorEmail"] = actor.get("email", "")
        row["actorName"] = actor.get("name", "")
        flat.append(row)
    return CommandResults(
        readable_output=tableToMarkdown(
            "Events",
            flat,
            headers=["id", "eventType", "formattedEventType", "application", "actorEmail", "eventTime", "outcomeString"],
        ),
        outputs_prefix="Reco.Events",
        outputs_key_field="id",
        outputs=events,
        raw_response=response,
    )


def list_posture_issues_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List posture issues from the external API."""
    response = reco_client.list_posture_issues(filters=filters, count=limit)
    issues = response.get("issues", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "Posture Issues",
            issues,
            headers=["id", "name", "severity", "checkStatus", "scorePercentage", "checkedInstance", "url"],
        ),
        outputs_prefix="Reco.PostureIssues",
        outputs_key_field="id",
        outputs=issues,
        raw_response=response,
    )


def list_accounts_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List accounts from the external API."""
    response = reco_client.list_accounts(filters=filters, count=limit)
    accounts = response.get("accounts", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "Accounts",
            accounts,
            headers=["id", "name", "accountEmail", "permissions", "hasMfa", "openAlerts", "isAdmin", "isRiskyUser", "lastSeen"],
        ),
        outputs_prefix="Reco.Accounts",
        outputs_key_field="id",
        outputs=accounts,
        raw_response=response,
    )


def list_devices_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List devices from the external API."""
    response = reco_client.list_devices(filters=filters, count=limit)
    devices = response.get("devices", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "Devices",
            devices,
            headers=["id", "name", "devicePlatform", "os", "osVersion", "isUnmanaged", "hasNonCompliant", "lastSeen"],
        ),
        outputs_prefix="Reco.Devices",
        outputs_key_field="id",
        outputs=devices,
        raw_response=response,
    )


def list_ai_agents_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List AI agents from the external API."""
    response = reco_client.list_ai_agents(filters=filters, count=limit)
    agents = response.get("aiAgents", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "AI Agents",
            agents,
            headers=["id", "name", "vendor", "type", "authorization", "agentStatus", "risk", "lastUsage"],
        ),
        outputs_prefix="Reco.AiAgents",
        outputs_key_field="id",
        outputs=agents,
        raw_response=response,
    )


def list_groups_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List SaaS groups from the external API."""
    response = reco_client.list_groups(filters=filters, count=limit)
    groups = response.get("groups", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "Groups",
            groups,
            headers=["id", "name", "email", "membersCount", "appsCount"],
        ),
        outputs_prefix="Reco.Groups",
        outputs_key_field="id",
        outputs=groups,
        raw_response=response,
    )


def list_saas_to_saas_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List SaaS-to-SaaS grants from the external API."""
    response = reco_client.list_saas_to_saas(filters=filters, count=limit)
    grants = response.get("saasToSaas", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "SaaS-to-SaaS Grants",
            grants,
            headers=["id", "plugin", "authorization", "permissionRisk", "accounts", "aiCapability", "lastSeen"],
        ),
        outputs_prefix="Reco.SaasToSaas",
        outputs_key_field="id",
        outputs=grants,
        raw_response=response,
    )


def list_ip_addresses_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List observed IP addresses from the external API."""
    response = reco_client.list_ip_addresses(filters=filters, count=limit)
    ips = response.get("ipAddresses", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "IP Addresses",
            ips,
            headers=["ipAddress", "country", "asnName", "eventsCount", "usersCount", "hasVpn", "hasProxy", "lastEventTime"],
        ),
        outputs_prefix="Reco.IpAddresses",
        outputs_key_field="ipAddress",
        outputs=ips,
        raw_response=response,
    )


def list_business_units_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List external business units from the external API."""
    response = reco_client.list_business_units(filters=filters, count=limit)
    bus = response.get("businessUnits", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "Business Units",
            bus,
            headers=["id", "name", "manager", "createdAt"],
        ),
        outputs_prefix="Reco.BusinessUnits",
        outputs_key_field="id",
        outputs=bus,
        raw_response=response,
    )


def list_audit_logs_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List platform audit logs from the external API."""
    response = reco_client.list_audit_logs(filters=filters, count=limit)
    logs = response.get("auditLogs", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "Audit Logs",
            logs,
            headers=["id", "userEmail", "module", "action", "objectName", "timestamp", "remoteAddr"],
        ),
        outputs_prefix="Reco.AuditLogs",
        outputs_key_field="id",
        outputs=logs,
        raw_response=response,
    )


def list_posture_checks_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List posture check definitions from the external API."""
    response = reco_client.list_posture_checks(filters=filters, count=limit)
    checks = response.get("postureChecks", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "Posture Checks",
            checks,
            headers=["id", "name", "severity", "policyType", "apps", "type"],
        ),
        outputs_prefix="Reco.PostureChecks",
        outputs_key_field="id",
        outputs=checks,
        raw_response=response,
    )


def list_threat_detection_policies_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List threat detection policies from the external API."""
    response = reco_client.list_threat_detection_policies(filters=filters, count=limit)
    policies = response.get("policies", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "Threat Detection Policies",
            policies,
            headers=["id", "name", "severity", "status", "apps", "openAlerts", "type"],
        ),
        outputs_prefix="Reco.ThreatDetectionPolicies",
        outputs_key_field="id",
        outputs=policies,
        raw_response=response,
    )


def list_exclusions_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List alert suppression exclusion rules from the external API."""
    response = reco_client.list_exclusions(filters=filters, count=limit)
    exclusions = response.get("exclusions", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "Exclusions",
            exclusions,
            headers=["id", "name", "policyName", "apps", "createdBy", "createdAt"],
        ),
        outputs_prefix="Reco.Exclusions",
        outputs_key_field="id",
        outputs=exclusions,
        raw_response=response,
    )


def list_app_instances_command(reco_client: RecoClient, filters: str = "", limit: int = PAGE_SIZE) -> CommandResults:
    """List integrated app instances (portfolio) from the external API."""
    response = reco_client.list_app_instances(filters=filters, count=limit)
    instances = response.get("appInstances", [])
    return CommandResults(
        readable_output=tableToMarkdown(
            "App Instances",
            instances,
            headers=["id", "name", "instanceType", "accountsCount", "isUsingAi", "saasToSaasCount", "filesCount"],
        ),
        outputs_prefix="Reco.AppInstances",
        outputs_key_field="id",
        outputs=instances,
        raw_response=response,
    )


# --- Main ---


def main() -> None:  # pragma: no cover
    try:
        command = demisto.command()
        demisto.debug(f"Reco Command being called is {command}")
        params = demisto.params()
        args = demisto.args()
        api_url = params.get("url")
        api_token = params.get("api_token")
        verify_certificate = not params.get("insecure", False)
        proxy = params.get("proxy", False)

        if not api_url:
            raise ValueError("Server URL is required")
        if not api_token:
            raise ValueError("API Token is required")

        max_fetch = get_max_fetch(int(params.get("max_fetch", "200")))

        reco_client = RecoClient(
            api_token=api_token,
            base_url=api_url,
            verify=verify_certificate,
            proxy=proxy,
        )

        if command == "fetch-incidents":
            # risk_level accepts a single value; alerts at or above it are fetched (PRO-303)
            risk_levels = parse_minimum_risk_level(params.get("risk_level"))
            source = params.get("source")
            before = params.get("before")
            after: datetime | None = None

            if arg := params.get("first_fetch"):
                first_fetch_ts = dateparser.parse(arg)
                if first_fetch_ts:
                    after = first_fetch_ts

            next_run, incidents = fetch_incidents(
                reco_client,
                last_run=demisto.getLastRun(),
                max_fetch=max_fetch,
                risk_levels=risk_levels,
                source=source,
                before=before,
                after=after,
            )
            demisto.setLastRun(next_run)
            demisto.incidents(incidents)

        elif command == "reco-add-comment-to-alert":
            incident_id = args["alert_id"]
            response = reco_client.update_reco_incident_timeline(
                incident_id=incident_id,
                comment=args["comment"],
            )
            return_results(CommandResults(raw_response=response, readable_output=f"Comment added to alert {incident_id}"))

        elif command == "reco-update-incident-timeline":
            incident_id = args["incident_id"]
            response = reco_client.update_reco_incident_timeline(
                incident_id=incident_id,
                comment=args["comment"],
            )
            return_results(CommandResults(raw_response=response, readable_output=f"Timeline updated for incident {incident_id}"))

        elif command == "reco-resolve-visibility-event":
            entity_id = args["entity_id"]
            label_name = args["label_name"]
            response = reco_client.resolve_visibility_event(entity_id=entity_id, label_name=label_name)
            return_results(CommandResults(raw_response=response, readable_output=f"Visibility event {entity_id} resolved"))

        elif command == "test-module":
            return_results(reco_client.validate_api_key())

        elif command == "reco-get-risky-users":
            return_results(get_risky_users_from_reco(reco_client))

        elif command == "reco-add-risky-user-label":
            return_results(add_risky_user_label(reco_client, args["email_address"]))

        elif command == "reco-add-leaving-org-user-label":
            return_results(add_leaving_org_user(reco_client, args["email_address"]))

        elif command == "reco-get-assets-user-has-access-to":
            return_results(
                get_assets_user_has_access(
                    reco_client,
                    args["email_address"],
                    args.get("only_sensitive", False),
                )
            )

        elif command == "reco-get-sensitive-assets-by-name":
            return_results(
                get_sensitive_assets_by_name(
                    reco_client,
                    args["asset_name"],
                    args.get("regex_search", False),
                )
            )

        elif command == "reco-get-sensitive-assets-by-id":
            return_results(get_sensitive_assets_by_id(reco_client, args["asset_id"]))

        elif command == "reco-get-link-to-user-overview-page":
            return_results(get_link_to_user_overview_page(reco_client, args["entity"], args["param"]))

        elif command == "reco-get-sensitive-assets-with-public-link":
            return_results(get_sensitive_assets_shared_with_public_link(reco_client))

        elif command == "reco-get-3rd-parties-accessible-to-data-list":
            return_results(get_3rd_parties_list(reco_client, int(args["last_interaction_time_in_days"])))

        elif command == "reco-get-files-shared-with-3rd-parties":
            return_results(
                get_files_shared_with_3rd_parties(
                    reco_client,
                    args["domain"],
                    int(args["last_interaction_time_in_days"]),
                )
            )

        elif command == "reco-add-exclusion-filter":
            return_results(add_exclusion_filter(reco_client, args["key_to_add"], argToList(args["values_to_add"])))

        elif command == "reco-change-alert-status":
            return_results(change_alert_status(reco_client, args["alert_id"], args["status"]))

        elif command == "reco-get-user-context-by-email-address":
            return_results(get_user_context_by_email_address(reco_client, args["email_address"]))

        elif command == "reco-get-files-exposed-to-email-address":
            return_results(get_files_exposed_to_email_command(reco_client, args["email_address"]))

        elif command == "reco-get-assets-shared-externally":
            return_results(get_assets_shared_externally_command(reco_client, args["email_address"]))

        elif command == "reco-get-private-email-list-with-access":
            return_results(get_private_email_list_with_access(reco_client))

        elif command == "reco-get-assets-by-id":
            return_results(get_assets_by_id(reco_client, args["asset_id"]))

        elif command == "reco-get-alert-ai-summary":
            return_results(get_alert_ai_summary(reco_client, args.get("alert_id", "")))

        elif command == "reco-get-apps":
            before_dt = dateparser.parse(args["before"]) if args.get("before") else None
            after_dt = dateparser.parse(args["after"]) if args.get("after") else None
            return_results(
                get_apps_command(reco_client, before=before_dt, after=after_dt, limit=int(args.get("limit") or PAGE_SIZE))
            )

        elif command == "reco-set-app-authorization-status":
            return_results(set_app_authorization_status_command(reco_client, args["app_id"], args["authorization_status"]))

        elif command == "reco-list-events":
            return_results(
                list_events_command(reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE))
            )

        elif command == "reco-list-posture-issues":
            return_results(
                list_posture_issues_command(
                    reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE)
                )
            )

        elif command == "reco-list-accounts":
            return_results(
                list_accounts_command(reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE))
            )

        elif command == "reco-list-devices":
            return_results(
                list_devices_command(reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE))
            )

        elif command == "reco-list-ai-agents":
            return_results(
                list_ai_agents_command(reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE))
            )

        elif command == "reco-list-groups":
            return_results(
                list_groups_command(reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE))
            )

        elif command == "reco-list-saas-to-saas":
            return_results(
                list_saas_to_saas_command(reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE))
            )

        elif command == "reco-list-ip-addresses":
            return_results(
                list_ip_addresses_command(reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE))
            )

        elif command == "reco-list-business-units":
            return_results(
                list_business_units_command(
                    reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE)
                )
            )

        elif command == "reco-list-audit-logs":
            return_results(
                list_audit_logs_command(reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE))
            )

        elif command == "reco-list-posture-checks":
            return_results(
                list_posture_checks_command(
                    reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE)
                )
            )

        elif command == "reco-list-threat-detection-policies":
            return_results(
                list_threat_detection_policies_command(
                    reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE)
                )
            )

        elif command == "reco-list-exclusions":
            return_results(
                list_exclusions_command(reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE))
            )

        elif command == "reco-list-app-instances":
            return_results(
                list_app_instances_command(
                    reco_client, filters=args.get("filters", ""), limit=int(args.get("limit") or PAGE_SIZE)
                )
            )

        else:
            raise NotImplementedError(f"{command} is not an existing reco command")

    except Exception as e:
        demisto.error(f"Failed to execute {demisto.command()} command. Error: {str(e)}")
        return_error(str(e))


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