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 json
import uuid

from typing import Any

import base64

import pytest
import datetime

from Reco import (
    RecoClient,
    fetch_incidents,
    map_reco_score_to_demisto_score,
    get_max_fetch,
    get_risky_users_from_reco,
    add_risky_user_label,
    get_assets_user_has_access,
    get_sensitive_assets_by_name,
    get_sensitive_assets_by_id,
    get_link_to_user_overview_page,
    get_sensitive_assets_shared_with_public_link,
    get_3rd_parties_list,
    get_files_shared_with_3rd_parties,
    map_reco_alert_score_to_demisto_score,
    get_user_context_by_email_address,
    get_assets_shared_externally_command,
    get_files_exposed_to_email_command,
    get_private_email_list_with_access,
    get_apps_command,
    set_app_authorization_status_command,
    parse_alerts_to_incidents,
    parse_minimum_risk_level,
    list_events_command,
    list_posture_issues_command,
    list_accounts_command,
    list_devices_command,
    list_ai_agents_command,
    list_groups_command,
    list_saas_to_saas_command,
    list_ip_addresses_command,
    list_business_units_command,
    list_audit_logs_command,
    list_posture_checks_command,
    list_threat_detection_policies_command,
    list_exclusions_command,
    list_app_instances_command,
    add_legacy_output_aliases,
)

from test_data.structs import (
    TableData,
    RowData,
    KeyValuePair,
    GetTableResponse,
    GetIncidentTableResponse,
)

DUMMY_RECO_API_DNS_NAME = "https://dummy.reco.ai/api"
ALERT_ID = "ee593dc2-a50e-415e-bed0-8403c18b26ca"
INCIDENT_DESCRIPTION = "Sensitive files are accessible to anyone who has their link"
ENCODING = "utf-8"
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"


def build_alert_detail(alert_id: str, description: str, risk_level: int, created_at: str) -> dict[str, Any]:
    """Build an alert-details response (external API `GET /external-api/alert-details/{id}`)."""
    return {
        "alert": {
            "id": alert_id,
            "description": description,
            "riskLevel": risk_level,
            "createdAt": created_at,
            "policyViolations": [
                {
                    "id": "75123c18-5ea2-4511-b9c0-1aad67e8b2ff",
                    "jsonData": json.dumps({"violation": True, "id": "91bef1de"}),
                }
            ],
        }
    }


def build_alerts_list_response(alert_ids: list[str]) -> dict[str, Any]:
    """Build an alerts/list response (external API `GET /external-api/alerts/list`)."""
    return {"alerts": [{"id": alert_id} for alert_id in alert_ids], "totalResults": len(alert_ids)}


def get_random_assets_user_has_access_to_response() -> GetIncidentTableResponse:
    return GetIncidentTableResponse(
        get_table_response=GetTableResponse(
            data=TableData(
                rows=[
                    RowData(
                        cells=[
                            KeyValuePair(
                                key="source",
                                value=base64.b64encode("GDRIVE_ACCESS_LOG_AP".encode(ENCODING)).decode(ENCODING),
                            ),
                            KeyValuePair(
                                key="file_type",
                                value=base64.b64encode("document".encode(ENCODING)).decode(ENCODING),
                            ),
                            KeyValuePair(
                                key="currently_permitted_users",
                                value=base64.b64encode(json.dumps(["a", "b", "c", "d", "e"]).encode(ENCODING)).decode(ENCODING),
                            ),
                            KeyValuePair(
                                key="labels",
                                value=base64.b64encode(json.dumps(["a", "b", "c", "d", "e"]).encode(ENCODING)).decode(ENCODING),
                            ),
                            KeyValuePair(
                                key="delete_state",
                                value=base64.b64encode("active".encode(ENCODING)).decode(ENCODING),
                            ),
                            KeyValuePair(
                                key="file_size",
                                value=base64.b64encode("0".encode(ENCODING)).decode(ENCODING),
                            ),
                            KeyValuePair(
                                key="file_name",
                                value=base64.b64encode("User Activity Report".encode(ENCODING)).decode(ENCODING),
                            ),
                            KeyValuePair(
                                key="visibility",
                                value=base64.b64encode("shared_internally".encode(ENCODING)).decode(ENCODING),
                            ),
                            KeyValuePair(
                                key="asset_id",
                                value=base64.b64encode("1".encode(ENCODING)).decode(ENCODING),
                            ),
                            KeyValuePair(
                                key="file_owner",
                                value=base64.b64encode("a".encode(ENCODING)).decode(ENCODING),
                            ),
                        ],
                    )
                ],
            ),
            total_number_of_results=1,
            table_definition="",
            dynamic_table_definition="",
            token="",
        ),
    )


def get_mock_assets() -> list[dict[str, Any]]:
    return {
        "assets": [
            {
                "entityId": "1Fk-_IB4nAWh5TRkG7bV7LKj15ZQP0DimklY2fr5fKX1",
                "name": "Untitled document",
                "link": "https://drive.google.com/file/d/1Fk-_IB4nAWh5TRkG7bV7LKj15ZQP0DimklY2fr5fKX1",
                "dataSource": "GSUITE_GDRIVE_AUDIT_LOG_API",
                "type": "ASSET_TYPE_FILE",
                "attributes": {},
                "owner": "test@acme.com",
            }
        ]
    }


def test_test_module_success(requests_mock, reco_client: RecoClient) -> None:
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/alerts/list", json={"alerts": [], "totalResults": 0})

    res = reco_client.validate_api_key()
    assert res == "ok"


@pytest.fixture
def reco_client() -> RecoClient:
    api_token = "dummy api key"
    return RecoClient(api_token=api_token, base_url=DUMMY_RECO_API_DNS_NAME, verify=True, proxy=True)


def test_fetch_incidents_should_succeed(requests_mock, reco_client: RecoClient) -> None:
    created_at = datetime.datetime.now().strftime(TIME_FORMAT)
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/alerts/list", json=build_alerts_list_response([ALERT_ID]))
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/alert-details/{ALERT_ID}",
        json=build_alert_detail(ALERT_ID, INCIDENT_DESCRIPTION, 30, created_at),
    )
    last_run, fetched_incidents = fetch_incidents(
        reco_client=reco_client,
        risk_levels=["CRITICAL"],
        source="test",
        before=datetime.datetime.now(),
        last_run={},
        max_fetch=1,
    )

    assert len(fetched_incidents) == 1
    assert fetched_incidents[0].get("name") == INCIDENT_DESCRIPTION
    assert fetched_incidents[0].get("dbotMirrorId") == ALERT_ID
    assert fetched_incidents[0].get("severity") == 3  # risk_level 30 -> Demisto high
    res_json = json.loads(fetched_incidents[0].get("rawJSON"))
    assert "id" in res_json


def test_fetch_same_incidents(requests_mock, reco_client: RecoClient) -> None:
    created_at = datetime.datetime.now().strftime(TIME_FORMAT)
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/alerts/list", json=build_alerts_list_response([ALERT_ID]))
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/alert-details/{ALERT_ID}",
        json=build_alert_detail(ALERT_ID, INCIDENT_DESCRIPTION, 30, created_at),
    )
    last_run, fetched_incidents = fetch_incidents(
        reco_client=reco_client,
        risk_levels=["CRITICAL"],
        before=datetime.datetime.now(),
        last_run={},
        max_fetch=1,
    )

    assert len(fetched_incidents) == 1
    last_run, incidents = fetch_incidents(
        reco_client=reco_client,
        risk_levels=["CRITICAL"],
        before=datetime.datetime.now(),
        last_run=last_run,
        max_fetch=1,
    )
    assert len(incidents) == 0


def test_fetch_incidents_paginates_a_burst_without_skipping(requests_mock, reco_client: RecoClient) -> None:
    """A page as large as max_fetch must not advance lastRun; the next cycle resumes via
    startIndex instead, so a burst larger than max_fetch is drained over multiple cycles
    rather than the excess being permanently skipped."""
    created_at = datetime.datetime.now().strftime(TIME_FORMAT)
    page_1_ids = [str(uuid.uuid4()), str(uuid.uuid4())]
    page_2_ids = [str(uuid.uuid4())]

    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/alerts/list",
        [
            {"json": build_alerts_list_response(page_1_ids)},
            {"json": build_alerts_list_response(page_2_ids)},
        ],
    )
    for alert_id in page_1_ids + page_2_ids:
        requests_mock.get(
            f"{DUMMY_RECO_API_DNS_NAME}/external-api/alert-details/{alert_id}",
            json=build_alert_detail(alert_id, INCIDENT_DESCRIPTION, 30, created_at),
        )

    last_run, incidents = fetch_incidents(reco_client=reco_client, last_run={}, max_fetch=2)

    assert len(incidents) == 2
    assert last_run["startIndex"] == 2
    assert last_run["lastRun"] is None

    last_run, incidents = fetch_incidents(reco_client=reco_client, last_run=last_run, max_fetch=2)

    assert len(incidents) == 1
    assert last_run["startIndex"] == 0
    assert last_run["lastRun"] is not None


def test_get_alerts_sorts_ascending_and_forwards_pagination(requests_mock, reco_client: RecoClient) -> None:
    """get_alerts must request ascending createdAt order and forward start_index/count, so
    resuming a burst mid-window never skips or re-jumps past unfetched alerts."""
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/alerts/list", json=build_alerts_list_response([]))

    reco_client.get_alerts(limit=50, start_index=100)

    sent_qs = requests_mock.last_request.qs
    assert sent_qs["sortby"] == ["createdat"]
    assert sent_qs["sortorder"] == ["ascending"]
    assert sent_qs["startindex"] == ["100"]
    assert sent_qs["count"] == ["50"]


def test_get_alerts_excludes_preview_status(requests_mock, reco_client: RecoClient) -> None:
    """get_alerts must always filter out ALERT_STATUS_PREVIEW alerts - Preview-state
    Policies are meant to stay out of the customer's SOAR, matching the guard already
    enforced on the webhook and share-service paths."""
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/alerts/list", json=build_alerts_list_response([]))

    reco_client.get_alerts(risk_levels=["HIGH"])

    sent_filter = requests_mock.last_request.qs["filters"][0]
    assert 'status ne "alert_status_preview"' in sent_filter
    assert 'severity eq "high"' in sent_filter


def test_fetch_incidents_without_assets_info(requests_mock, reco_client: RecoClient) -> None:
    created_at = datetime.datetime.now().strftime(TIME_FORMAT)
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/alerts/list", json=build_alerts_list_response([ALERT_ID]))
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/alert-details/{ALERT_ID}",
        json=build_alert_detail(ALERT_ID, INCIDENT_DESCRIPTION, 30, created_at),
    )
    last_run, fetched_incidents = fetch_incidents(reco_client=reco_client, last_run={}, source="GOOGLE_DRIVE", max_fetch=1)

    assert len(fetched_incidents) == 1
    assert fetched_incidents[0].get("name") == INCIDENT_DESCRIPTION
    assert fetched_incidents[0].get("dbotMirrorId") == ALERT_ID
    res_json = json.loads(fetched_incidents[0].get("rawJSON"))
    assert "id" in res_json


def test_empty_response(requests_mock, reco_client: RecoClient) -> None:
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/alerts/list", json={"alerts": [], "totalResults": 0})
    last_run, fetched_incidents = fetch_incidents(reco_client=reco_client, last_run={}, max_fetch=1)

    assert len(fetched_incidents) == 0
    assert last_run is not None


def test_empty_valid_response(requests_mock, reco_client: RecoClient) -> None:
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/alerts/list", json={"alerts": [], "totalResults": 0})
    last_run, fetched_incidents = fetch_incidents(reco_client=reco_client, last_run={}, max_fetch=1)

    assert len(fetched_incidents) == 0
    assert last_run is not None


def test_invalid_response(requests_mock, reco_client: RecoClient) -> None:
    """A malformed alerts/list response (missing the 'alerts' key) yields zero incidents, no exception."""
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/alerts/list", json={})
    last_run, fetched_incidents = fetch_incidents(
        reco_client=reco_client,
        last_run={},
        max_fetch=1,
        risk_levels=["HIGH", "CRITICAL"],
        source="GSUITE_GDRIVE_AUDIT_LOG_API",
    )

    assert len(fetched_incidents) == 0
    assert last_run is not None


def test_risk_level_mapper():
    """Map Reco numeric risk score (10-40) to Demisto severity."""
    assert map_reco_score_to_demisto_score(40) == 4
    assert map_reco_score_to_demisto_score(30) == 3
    assert map_reco_score_to_demisto_score(20) == 2
    assert map_reco_score_to_demisto_score(10) == 0.5
    assert map_reco_score_to_demisto_score(0) == 0.5


def test_risk_level_mapper_mid_range():
    """Mid-range scores (10-40) normalize to tier: 10->0.5, 20->2, 30->3, 40->4."""
    assert map_reco_score_to_demisto_score(15) == 0.5
    assert map_reco_score_to_demisto_score(25) == 2
    assert map_reco_score_to_demisto_score(35) == 3
    assert map_reco_score_to_demisto_score(37) == 3


def test_alert_mapper():
    assert map_reco_alert_score_to_demisto_score("CRITICAL") == 4


def test_parse_minimum_risk_level_expands_to_higher_severities():
    """A single risk_level value expands to itself and every severity above it."""
    assert parse_minimum_risk_level("MEDIUM") == ["MEDIUM", "HIGH", "CRITICAL"]
    assert parse_minimum_risk_level("LOW") == ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
    assert parse_minimum_risk_level("CRITICAL") == ["CRITICAL"]


def test_parse_minimum_risk_level_accepts_numeric_and_lowercase():
    assert parse_minimum_risk_level("30") == ["HIGH", "CRITICAL"]
    assert parse_minimum_risk_level("medium") == ["MEDIUM", "HIGH", "CRITICAL"]


def test_parse_minimum_risk_level_empty_returns_none():
    assert parse_minimum_risk_level(None) is None
    assert parse_minimum_risk_level("") is None


def test_add_legacy_output_aliases_copies_new_field_to_old_key():
    items = [{"id": "1", "name": "foo"}, {"id": "2", "name": "bar"}]
    add_legacy_output_aliases(items, {"app_name": "name"})
    assert items[0]["app_name"] == "foo"
    assert items[1]["app_name"] == "bar"


def test_add_legacy_output_aliases_skips_missing_new_key():
    items = [{"id": "1"}]
    add_legacy_output_aliases(items, {"app_name": "name"})
    assert "app_name" not in items[0]


def test_parse_alerts_to_incidents_numeric_risk():
    """parse_alerts_to_incidents maps numeric risk_level (10-40) to severity."""
    alerts = [
        {"riskLevel": 40, "description": "Critical", "id": "1", "createdAt": "2023-01-01T00:00:00Z"},
        {"risk_level": 20, "description": "Medium", "id": "2", "created_at": "2023-01-01T00:00:00Z"},
        {"riskLevel": 15, "description": "Low tier", "id": "3", "createdAt": "2023-01-01T00:00:00Z"},
    ]
    incidents = parse_alerts_to_incidents(alerts)
    assert len(incidents) == 3
    assert incidents[0]["severity"] == 4
    assert incidents[1]["severity"] == 2
    assert incidents[2]["severity"] == 0.5


def test_parse_alerts_to_incidents_missing_risk_defaults_to_low():
    """When risk_level is missing or invalid, severity defaults to 0.5 (LOW)."""
    alerts = [{"description": "No risk", "id": "1", "createdAt": "2023-01-01T00:00:00Z"}]
    incidents = parse_alerts_to_incidents(alerts)
    assert len(incidents) == 1
    assert incidents[0]["severity"] == 0.5


def test_parse_alerts_to_incidents_risk_level_string_and_int():
    """risk_level works as string labels ('HIGH'), string numbers ('10'), and ints (40)."""
    alerts = [
        {"riskLevel": "HIGH", "description": "High", "id": "1", "createdAt": "2023-01-01T00:00:00Z"},
        {"risk_level": "10", "description": "Low str", "id": "2", "created_at": "2023-01-01T00:00:00Z"},
        {"riskLevel": 40, "description": "Critical int", "id": "3", "createdAt": "2023-01-01T00:00:00Z"},
        {"riskLevel": "30", "description": "High str num", "id": "4", "createdAt": "2023-01-01T00:00:00Z"},
        {"risk_level": "MEDIUM", "description": "Medium", "id": "5", "created_at": "2023-01-01T00:00:00Z"},
    ]
    incidents = parse_alerts_to_incidents(alerts)
    assert len(incidents) == 5
    assert incidents[0]["severity"] == 3  # "HIGH" -> high
    assert incidents[1]["severity"] == 0.5  # "10" -> low
    assert incidents[2]["severity"] == 4  # 40 -> critical
    assert incidents[3]["severity"] == 3  # "30" -> high
    assert incidents[4]["severity"] == 2  # "MEDIUM" -> medium


def test_get_max_fetch_bigger():
    big_number_max_fetch = 600
    result = get_max_fetch(big_number_max_fetch)
    assert result == 500


def test_max_fetch():
    max_fetch = 200
    result = get_max_fetch(max_fetch)
    assert result == max_fetch


def test_update_reco_incident_timeline(requests_mock, reco_client: RecoClient) -> None:
    incident_id = uuid.uuid1()
    requests_mock.post(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/comments/create",
        json={},
        status_code=200,
    )
    res = reco_client.update_reco_incident_timeline(incident_id=str(incident_id), comment="test")
    assert res == {}


def test_update_reco_incident_timeline_error(capfd, requests_mock, reco_client: RecoClient) -> None:
    incident_id = uuid.uuid1()
    requests_mock.post(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/comments/create",
        json={},
        status_code=404,
    )
    with capfd.disabled(), pytest.raises(Exception):
        reco_client.update_reco_incident_timeline(incident_id=str(incident_id), comment="test")


def test_resolve_visibility_event(requests_mock, reco_client: RecoClient) -> None:
    entry_id = uuid.uuid1()
    requests_mock.put(f"{DUMMY_RECO_API_DNS_NAME}/set-label-status", json={}, status_code=200)
    res = reco_client.resolve_visibility_event(entity_id=str(entry_id), label_name="Accessible by all")
    assert res == {}


def test_resolve_visibility_event_error(capfd, requests_mock, reco_client: RecoClient) -> None:
    entry_id = uuid.uuid1()
    requests_mock.put(f"{DUMMY_RECO_API_DNS_NAME}/set-label-status", json={}, status_code=404)
    with capfd.disabled(), pytest.raises(Exception):
        reco_client.resolve_visibility_event(entity_id=str(entry_id), label_name="Accessible by all")


def test_get_risky_users(requests_mock, reco_client: RecoClient) -> None:
    accounts = [
        {
            "id": str(uuid.uuid4()),
            "name": "John Doe",
            "accountEmail": f"{uuid.uuid4()}@acme.com",
            "isAdmin": False,
            "isRiskyUser": True,
        }
    ]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/accounts/list",
        json={"accounts": accounts, "totalResults": len(accounts)},
        status_code=200,
    )
    actual_result = get_risky_users_from_reco(reco_client=reco_client)
    assert len(actual_result.outputs) == len(accounts)
    assert "@" in actual_result.outputs[0].get("accountEmail")


def test_get_risky_users_bad_response(capfd, requests_mock, reco_client: RecoClient) -> None:
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/accounts/list",
        json={},
        status_code=500,
    )
    with capfd.disabled(), pytest.raises(Exception):
        get_risky_users_from_reco(reco_client=reco_client)


def test_add_risky_user_label(requests_mock, reco_client: RecoClient) -> None:
    label_id = f"{uuid.uuid1()}@gmail.com"
    identity_id = str(uuid.uuid4())
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/users/list",
        json={"users": [{"id": identity_id, "email": label_id}], "totalResults": 1},
        status_code=200,
    )
    requests_mock.post(f"{DUMMY_RECO_API_DNS_NAME}/external-api/labels/add", json={}, status_code=200)
    res = add_risky_user_label(reco_client=reco_client, email_address=label_id)
    assert "labeled as risky" in res.readable_output


def test_get_assets_user_has_access_to(requests_mock, reco_client: RecoClient) -> None:
    raw_result = get_random_assets_user_has_access_to_response()
    requests_mock.put(f"{DUMMY_RECO_API_DNS_NAME}/asset-management/query", json=raw_result, status_code=200)
    actual_result = get_assets_user_has_access(
        reco_client=reco_client,
        email_address=f"{uuid.uuid1()}@gmail.com",
        only_sensitive=False,
    )
    assert len(actual_result.outputs) == len(raw_result.getTableResponse.data.rows)
    assert actual_result.outputs[0].get("source") is not None


def test_get_assets_user_bad_response(capfd, requests_mock, reco_client: RecoClient) -> None:
    requests_mock.put(f"{DUMMY_RECO_API_DNS_NAME}/asset-management/query", json={}, status_code=200)
    with capfd.disabled(), pytest.raises(Exception):
        get_assets_user_has_access(reco_client=reco_client, email_address="test", only_sensitive=False)


def test_get_sensitive_assets_by_name(requests_mock, reco_client: RecoClient) -> None:
    files = [{"id": "asset-1", "name": "sensitive.txt", "owner": "test@acme.com", "sensitivityLevel": "40"}]
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/files/list", json={"files": files, "totalResults": len(files)})
    actual_result = get_sensitive_assets_by_name(reco_client=reco_client, asset_name="test", regex_search=True)
    assert len(actual_result.outputs) == len(files)
    assert actual_result.outputs[0].get("id") is not None


def test_get_sensitive_assets_by_id(requests_mock, reco_client: RecoClient) -> None:
    files = [
        {
            "id": "asset-id",
            "name": "sensitive.txt",
            "owner": "test@acme.com",
            "url": "https://drive.example.com/sensitive.txt",
            "sensitivityLevel": "40",
            "permissionVisibility": "PUBLIC",
        }
    ]
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/files/list", json={"files": files, "totalResults": len(files)})
    actual_result = get_sensitive_assets_by_id(reco_client=reco_client, asset_id="asset-id")
    assert len(actual_result.outputs) == len(files)
    output = actual_result.outputs[0]
    assert output.get("id") is not None
    # legacy pre-migration field names must still be populated for backward compatibility
    assert output.get("file_name") == "sensitive.txt"
    assert output.get("file_owner") == "test@acme.com"
    assert output.get("file_url") == "https://drive.example.com/sensitive.txt"
    assert output.get("sensitivity_level") == "40"
    assert output.get("visibility") == "PUBLIC"


def test_get_link_to_user_overview_page(requests_mock, reco_client: RecoClient) -> None:
    entity_id = f"{uuid.uuid1()}@gmail.com"
    link_type = "RM_LINK_TYPE_USER"
    link_res = str(uuid.uuid1())
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/risk-management/risk-management/link?link_type={link_type}&param={entity_id}",
        json={"link": link_res},
        status_code=200,
    )
    actual_result = get_link_to_user_overview_page(reco_client=reco_client, entity=entity_id, link_type=link_type)
    assert actual_result.outputs_prefix == "Reco.Link"
    assert actual_result.outputs.get("link") == link_res


def test_get_link_to_user_overview_page_error(capfd, requests_mock, reco_client: RecoClient) -> None:
    entity_id = f"{uuid.uuid1()}@gmail.com"
    link_type = "RM_LINK_TYPE_USER"
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/risk-management/risk-management/link?link_type={link_type}&param={entity_id}",
        json={},
        status_code=404,
    )
    with capfd.disabled(), pytest.raises(Exception):
        get_link_to_user_overview_page(reco_client=reco_client, entity=entity_id, link_type=link_type)


def test_get_exposed_publicly(requests_mock, reco_client: RecoClient) -> None:
    raw_result = get_random_assets_user_has_access_to_response()
    requests_mock.put(
        f"{DUMMY_RECO_API_DNS_NAME}/risk-management/get-data-risk-management-table", json=raw_result, status_code=200
    )
    actual_result = get_sensitive_assets_shared_with_public_link(reco_client=reco_client)
    assert len(actual_result.outputs) == len(raw_result.getTableResponse.data.rows)
    assert actual_result.outputs[0].get("source") is not None


def test_get_private_email_list_with_access(requests_mock, reco_client: RecoClient) -> None:
    requests_mock.put(
        f"{DUMMY_RECO_API_DNS_NAME}/risk-management/get-data-risk-management-table",
        json={"getTableResponse": {}},
        status_code=200,
    )
    actual_result = get_private_email_list_with_access(reco_client=reco_client)
    assert len(actual_result.outputs) == 0


def test_get_assets_shared_externally_command(requests_mock, reco_client: RecoClient) -> None:
    raw_result = get_random_assets_user_has_access_to_response()
    requests_mock.put(f"{DUMMY_RECO_API_DNS_NAME}/asset-management/query", json=raw_result, status_code=200)
    actual_result = get_assets_shared_externally_command(reco_client=reco_client, email_address="g@example.com")
    assert len(actual_result.outputs) == len(raw_result.getTableResponse.data.rows)


def test_get_files_exposed_to_email_command(requests_mock, reco_client: RecoClient) -> None:
    raw_result = get_random_assets_user_has_access_to_response()
    requests_mock.put(
        f"{DUMMY_RECO_API_DNS_NAME}/risk-management/get-data-risk-management-table", json=raw_result, status_code=200
    )
    actual_result = get_files_exposed_to_email_command(reco_client=reco_client, email_account="g@example.com")
    assert len(actual_result.outputs) == len(raw_result.getTableResponse.data.rows)
    assert actual_result.outputs[0].get("source") is not None


def test_get_exposed_publicly_page_error(capfd, requests_mock, reco_client: RecoClient) -> None:
    requests_mock.put(f"{DUMMY_RECO_API_DNS_NAME}/risk-management/get-data-risk-management-table", json={}, status_code=200)
    with capfd.disabled(), pytest.raises(Exception):
        get_sensitive_assets_shared_with_public_link(reco_client=reco_client)


def test_get_3rd_parties_list_error(capfd, requests_mock, reco_client: RecoClient) -> None:
    requests_mock.put(f"{DUMMY_RECO_API_DNS_NAME}/risk-management/get-data-risk-management-table", json={}, status_code=200)
    with capfd.disabled(), pytest.raises(Exception):
        get_3rd_parties_list(
            reco_client=reco_client,
            last_interaction_time_in_days=30,
        )


def test_get_3rd_parties_list(requests_mock, reco_client: RecoClient) -> None:
    raw_result = get_random_assets_user_has_access_to_response()
    requests_mock.put(
        f"{DUMMY_RECO_API_DNS_NAME}/risk-management/get-data-risk-management-table", json=raw_result, status_code=200
    )
    actual_result = get_3rd_parties_list(
        reco_client=reco_client,
        last_interaction_time_in_days=30,
    )
    assert len(actual_result.outputs) == len(raw_result.getTableResponse.data.rows)


def test_get_files_shared_with_3rd_parties(requests_mock, reco_client: RecoClient) -> None:
    raw_result = get_random_assets_user_has_access_to_response()
    requests_mock.put(
        f"{DUMMY_RECO_API_DNS_NAME}/risk-management/get-data-risk-management-table", json=raw_result, status_code=200
    )
    actual_result = get_files_shared_with_3rd_parties(
        reco_client=reco_client,
        domain="data",
        last_interaction_time_before_in_days=30,
    )
    assert len(actual_result.outputs) == len(raw_result.getTableResponse.data.rows)


def test_date_formatting(reco_client: RecoClient) -> None:
    date = reco_client.get_date_time_before_days_formatted(30)
    assert ".999Z" in date


def test_add_exclusion_filter(requests_mock, reco_client: RecoClient) -> None:
    requests_mock.post(f"{DUMMY_RECO_API_DNS_NAME}/algo/add_values_to_data_type_exclude_analyzer", json={}, status_code=200)
    reco_client.add_exclusion_filter("key", ["val1", "val2"])


def test_change_alert_status(requests_mock, reco_client: RecoClient) -> None:
    alert_id = uuid.uuid1()
    status = "ALERT_STATUS_CLOSED"
    requests_mock.put(
        f"{DUMMY_RECO_API_DNS_NAME}/policy-subsystem/alert-inbox/{str(alert_id)}/status/{status}",
        json={},
        status_code=200,
    )
    res = reco_client.change_alert_status(alert_id=str(alert_id), status=status)
    assert res == {}


def test_change_alert_status_posture_status(requests_mock, reco_client: RecoClient) -> None:
    """The status enum also covers posture-check statuses like ALERT_STATUS_RISK_MITIGATED."""
    alert_id = uuid.uuid1()
    status = "ALERT_STATUS_RISK_MITIGATED"
    requests_mock.put(
        f"{DUMMY_RECO_API_DNS_NAME}/policy-subsystem/alert-inbox/{str(alert_id)}/status/{status}",
        json={},
        status_code=200,
    )
    res = reco_client.change_alert_status(alert_id=str(alert_id), status=status)
    assert res == {}


def test_get_alert_summary(requests_mock, reco_client: RecoClient) -> None:
    alert_id = uuid.uuid1()
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/alert/summarize/{alert_id}",
        json={"content": "test"},
        status_code=200,
    )
    res = reco_client.get_alert_ai_summary(alert_id=str(alert_id))
    assert res.get("content") == "test"


def test_get_alert_summary_error(capfd, requests_mock, reco_client: RecoClient) -> None:
    alert_id = uuid.uuid1()
    requests_mock.put(f"{DUMMY_RECO_API_DNS_NAME}/alert/summarize/{alert_id}", json={}, status_code=404)
    with capfd.disabled(), pytest.raises(Exception):
        reco_client.get_alert_ai_summary(str(alert_id))


def test_get_user_context_by_email(requests_mock, reco_client: RecoClient) -> None:
    users = [
        {
            "email": "charles@corp.com",
            "name": "Yossi",
            "departments": ["Pro"],
            "jobTitles": ["VP Product"],
            "isInternal": False,
        }
    ]
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/users/list", json={"users": users, "totalResults": len(users)})
    res = get_user_context_by_email_address(reco_client, "charles@corp.com")
    assert res.outputs_prefix == "Reco.User"
    assert res.outputs.get("email") != ""
    assert res.outputs.get("email") == "charles@corp.com"
    # legacy pre-migration field names must still be populated for backward compatibility
    assert res.outputs.get("email_account") == "charles@corp.com"
    assert res.outputs.get("full_name") == "Yossi"
    assert res.outputs.get("job_titles") == ["VP Product"]
    assert res.outputs.get("category") == "external"


def test_get_app_discovery_with_filters(requests_mock, reco_client: RecoClient) -> None:
    """Test the get_app_discovery method with date filters."""
    apps = [{"id": "slack.com", "name": "Slack", "category": "Communication"}]
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/apps/list", json={"apps": apps, "totalResults": len(apps)})

    # Test with date filters
    from datetime import datetime, timedelta

    before = datetime.now()
    after = datetime.now() - timedelta(days=30)

    result = reco_client.get_app_discovery(before=before, after=after, limit=100)

    # Verify the response
    assert isinstance(result, list)


def test_get_app_discovery_error(capfd, requests_mock, reco_client: RecoClient) -> None:
    """Test error handling in get_app_discovery."""
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/apps/list", json={}, status_code=500)

    with capfd.disabled(), pytest.raises(Exception):
        reco_client.get_app_discovery()


def test_set_app_authorization_status(requests_mock, reco_client: RecoClient) -> None:
    """Test setting app authorization status."""
    app_id = "microsoft.com"
    authorization_status = "AUTH_STATUS_SANCTIONED"

    requests_mock.put(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/apps/{app_id}/auth-status",
        json={"authorizationStatus": authorization_status},
        status_code=200,
    )

    response = reco_client.set_app_authorization_status(app_id, authorization_status)
    assert response == {"authorizationStatus": authorization_status}


def test_set_app_authorization_status_command(requests_mock, reco_client: RecoClient) -> None:
    """Test the set_app_authorization_status_command function."""
    app_id = "slack.com"
    authorization_status = "AUTH_STATUS_UNSANCTIONED"

    requests_mock.put(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/apps/{app_id}/auth-status",
        json={"authorizationStatus": authorization_status},
        status_code=200,
    )

    result = set_app_authorization_status_command(reco_client, app_id, authorization_status)

    assert result.outputs_prefix == "Reco.AppAuthorization"
    assert result.outputs["app_id"] == app_id
    assert result.outputs["authorization_status"] == authorization_status
    assert result.outputs["updated"] is True
    assert "updated to" in result.readable_output


def test_get_apps_command(requests_mock, reco_client: RecoClient) -> None:
    """Test the get_apps_command function."""
    mock_apps = [
        {
            "id": "slack.com",
            "name": "Slack",
            "category": "Communication",
            "usersCount": 10,
            "authorization": "AUTH_STATUS_SANCTIONED",
            "isUsingAi": False,
            "vendorGrade": "A",
            "aiCapability": True,
            "lastSeen": "2024-01-01T00:00:00Z",
        }
    ]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/apps/list",
        json={"apps": mock_apps, "totalResults": len(mock_apps)},
        status_code=200,
    )

    result = get_apps_command(reco_client)

    assert result.outputs_prefix == "Reco.Apps"
    assert result.outputs_key_field == "id"
    assert isinstance(result.outputs, list)
    assert len(result.outputs) == 1
    assert "App Discovery" in result.readable_output
    # legacy pre-migration field names must still be populated for backward compatibility
    output = result.outputs[0]
    assert output.get("app_id") == "slack.com"
    assert output.get("app_name") == "Slack"
    assert output.get("users_count") == 10


def test_set_app_authorization_status_error(capfd, requests_mock, reco_client: RecoClient) -> None:
    """Test error handling in set_app_authorization_status."""
    app_id = "test.com"
    authorization_status = "AUTH_STATUS_SANCTIONED"

    requests_mock.put(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/apps/{app_id}/auth-status",
        json={},
        status_code=500,
    )

    with capfd.disabled(), pytest.raises(Exception):
        reco_client.set_app_authorization_status(app_id, authorization_status)


def test_set_app_authorization_status_error_2(capfd, requests_mock, reco_client: RecoClient) -> None:
    """Test error handling in set_app_authorization_status with a different failure status code."""
    app_id = "test.com"
    authorization_status = "AUTH_STATUS_SANCTIONED"

    requests_mock.put(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/apps/{app_id}/auth-status",
        json={},
        status_code=404,
    )

    with capfd.disabled(), pytest.raises(Exception):
        reco_client.set_app_authorization_status(app_id, authorization_status)


def test_list_events_command(requests_mock, reco_client: RecoClient) -> None:
    events = [
        {
            "id": "event-1",
            "eventType": "login",
            "formattedEventType": "Login",
            "application": "Slack",
            "eventTime": "2024-01-01T00:00:00Z",
            "outcomeString": "success",
            "actor": {"email": "test@acme.com", "name": "User"},
        }
    ]
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/events/list", json={"events": events, "totalResults": len(events)})
    result = list_events_command(reco_client)
    assert result.outputs_prefix == "Reco.Events"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "Events" in result.readable_output


def test_list_posture_issues_command(requests_mock, reco_client: RecoClient) -> None:
    issues = [{"id": "issue-1", "name": "MFA not enforced", "severity": "HIGH", "checkStatus": "ALERT_STATUS_TO_REVIEW"}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/posture-issues/list",
        json={"issues": issues, "totalResults": len(issues)},
    )
    result = list_posture_issues_command(reco_client)
    assert result.outputs_prefix == "Reco.PostureIssues"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "Posture Issues" in result.readable_output


def test_list_accounts_command(requests_mock, reco_client: RecoClient) -> None:
    accounts = [{"id": "acc-1", "name": "John Doe", "accountEmail": "john@acme.com", "isAdmin": False}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/accounts/list",
        json={"accounts": accounts, "totalResults": len(accounts)},
    )
    result = list_accounts_command(reco_client)
    assert result.outputs_prefix == "Reco.Accounts"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "Accounts" in result.readable_output


def test_list_devices_command(requests_mock, reco_client: RecoClient) -> None:
    devices = [{"id": "dev-1", "name": "MacBook Pro", "devicePlatform": "macOS", "isUnmanaged": False}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/devices/list", json={"devices": devices, "totalResults": len(devices)}
    )
    result = list_devices_command(reco_client)
    assert result.outputs_prefix == "Reco.Devices"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "Devices" in result.readable_output


def test_list_ai_agents_command(requests_mock, reco_client: RecoClient) -> None:
    agents = [{"id": "agent-1", "name": "Copilot", "vendor": "Microsoft", "authorization": "AUTH_STATUS_SANCTIONED"}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/ai-agents/list", json={"aiAgents": agents, "totalResults": len(agents)}
    )
    result = list_ai_agents_command(reco_client)
    assert result.outputs_prefix == "Reco.AiAgents"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "AI Agents" in result.readable_output


def test_list_groups_command(requests_mock, reco_client: RecoClient) -> None:
    groups = [{"id": "group-1", "name": "Engineering", "email": "test@acme.com", "membersCount": 10}]
    requests_mock.get(f"{DUMMY_RECO_API_DNS_NAME}/external-api/groups/list", json={"groups": groups, "totalResults": len(groups)})
    result = list_groups_command(reco_client)
    assert result.outputs_prefix == "Reco.Groups"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "Groups" in result.readable_output


def test_list_saas_to_saas_command(requests_mock, reco_client: RecoClient) -> None:
    grants = [{"id": "grant-1", "plugin": "Zoom for Slack", "authorization": "AUTH_STATUS_TO_REVIEW"}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/saas-to-saas/list",
        json={"saasToSaas": grants, "totalResults": len(grants)},
    )
    result = list_saas_to_saas_command(reco_client)
    assert result.outputs_prefix == "Reco.SaasToSaas"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "SaaS-to-SaaS Grants" in result.readable_output


def test_list_ip_addresses_command(requests_mock, reco_client: RecoClient) -> None:
    ips = [{"ipAddress": "1.2.3.4", "country": "US", "hasVpn": False, "hasProxy": False}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/ip-addresses/list", json={"ipAddresses": ips, "totalResults": len(ips)}
    )
    result = list_ip_addresses_command(reco_client)
    assert result.outputs_prefix == "Reco.IpAddresses"
    assert result.outputs_key_field == "ipAddress"
    assert len(result.outputs) == 1
    assert "IP Addresses" in result.readable_output


def test_list_business_units_command(requests_mock, reco_client: RecoClient) -> None:
    bus = [{"id": "bu-1", "name": "R&D", "manager": "jane@acme.com"}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/business-units/list", json={"businessUnits": bus, "totalResults": len(bus)}
    )
    result = list_business_units_command(reco_client)
    assert result.outputs_prefix == "Reco.BusinessUnits"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "Business Units" in result.readable_output


def test_list_audit_logs_command(requests_mock, reco_client: RecoClient) -> None:
    logs = [{"id": "log-1", "userEmail": "admin@acme.com", "module": "auth", "action": "login"}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/audit-logs/list", json={"auditLogs": logs, "totalResults": len(logs)}
    )
    result = list_audit_logs_command(reco_client)
    assert result.outputs_prefix == "Reco.AuditLogs"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "Audit Logs" in result.readable_output


def test_list_posture_checks_command(requests_mock, reco_client: RecoClient) -> None:
    checks = [{"id": "check-1", "name": "MFA Enforced", "severity": "HIGH", "policyType": "SECURITY"}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/posture-checks/list",
        json={"postureChecks": checks, "totalResults": len(checks)},
    )
    result = list_posture_checks_command(reco_client)
    assert result.outputs_prefix == "Reco.PostureChecks"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "Posture Checks" in result.readable_output


def test_list_threat_detection_policies_command(requests_mock, reco_client: RecoClient) -> None:
    policies = [{"id": "policy-1", "name": "Impossible Travel", "severity": "CRITICAL", "status": "ALERT_STATUS_ACTIVE"}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/policies/list", json={"policies": policies, "totalResults": len(policies)}
    )
    result = list_threat_detection_policies_command(reco_client)
    assert result.outputs_prefix == "Reco.ThreatDetectionPolicies"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "Threat Detection Policies" in result.readable_output


def test_list_exclusions_command(requests_mock, reco_client: RecoClient) -> None:
    exclusions = [{"id": "excl-1", "name": "Trusted IP range", "policyName": "Impossible Travel"}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/exclusions/list",
        json={"exclusions": exclusions, "totalResults": len(exclusions)},
    )
    result = list_exclusions_command(reco_client)
    assert result.outputs_prefix == "Reco.Exclusions"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "Exclusions" in result.readable_output


def test_list_app_instances_command(requests_mock, reco_client: RecoClient) -> None:
    instances = [{"id": "instance-1", "name": "Slack (Prod)", "instanceType": "PRODUCTION", "isUsingAi": False}]
    requests_mock.get(
        f"{DUMMY_RECO_API_DNS_NAME}/external-api/app-instances/list",
        json={"appInstances": instances, "totalResults": len(instances)},
    )
    result = list_app_instances_command(reco_client)
    assert result.outputs_prefix == "Reco.AppInstances"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 1
    assert "App Instances" in result.readable_output