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
| ID | Reco |
|---|---|
| Provider | Reco AI |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 6.5.0 |
| Docker Image | demisto/python3:3.12.13.10404775 |
| Supported Modules | Agentix 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 settingsincidentType— Incident typeisFetch— Fetch incidentsmax_fetch— Max fetchsource— Sourcebefore— Beforeafter— Afterrisk_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-alertAdd a comment to an alert in Reco.
-
reco-add-exclusion-filterAdd exclusion filter to Reco Classifier.
-
reco-add-leaving-org-user-labelTag a user as leaving org user in Reco.
-
reco-add-risky-user-labelTag a user as risky in Reco.
-
reco-change-alert-statusupdate alert status in Reco.
-
reco-get-3rd-parties-accessible-to-data-listGet 3rd parties accessible to sensitive assets.
-
reco-get-alert-ai-summaryGet alert ai summary from Reco.
-
reco-get-appsGet app discovery data from Reco. Fetches all available apps using pagination.
-
reco-get-assets-by-idGet all assets from Reco by id.
-
reco-get-assets-shared-externallyGet files user has access to from Reco.
-
reco-get-assets-user-has-access-toGet all files user has access to from Reco.
-
reco-get-files-exposed-to-email-addressGet files user has access to from Reco.
-
reco-get-files-shared-with-3rd-partiesGet files shared with 3rd parties.
-
reco-get-link-to-user-overview-pageGenerate a magic link for reco UI (overview page).
-
reco-get-private-email-list-with-accessGet Private emails with access.
-
reco-get-risky-usersGet Risky Users from Reco.
-
reco-get-sensitive-assets-by-idGet all sensitive assets from Reco by id.
-
reco-get-sensitive-assets-by-nameGet all sensitive assets from Reco by name.
-
reco-get-sensitive-assets-with-public-linkGet all sensitive assets with public link from Reco.
-
reco-get-user-context-by-email-addressGet user context by email address from Reco.
-
reco-list-accountsLists accounts (SaaS user accounts) from Reco using the external API.
-
reco-list-ai-agentsLists AI agents detected by Reco using the external API.
-
reco-list-app-instancesLists integrated app instances (app portfolio) from Reco using the external API. Only returns instances with an active integration status.
-
reco-list-audit-logsLists Reco platform audit logs using the external API.
-
reco-list-business-unitsLists external business units from Reco using the external API.
-
reco-list-devicesLists devices from Reco using the external API.
-
reco-list-eventsLists SaaS events from Reco using the external API.
-
reco-list-exclusionsLists alert suppression exclusion rules from Reco using the external API.
-
reco-list-groupsLists SaaS groups from Reco using the external API.
-
reco-list-ip-addressesLists observed IP addresses from Reco using the external API.
-
reco-list-posture-checksLists posture check definitions from Reco using the external API.
-
reco-list-posture-issuesLists posture issues from Reco using the external API.
-
reco-list-saas-to-saasLists SaaS-to-SaaS OAuth grants and integrations from Reco using the external API.
-
reco-list-threat-detection-policiesLists threat detection policies from Reco using the external API.
-
reco-resolve-visibility-eventResolve an event in Reco Finding. Reco Findings contains aggregations of events. This command resolves the event in the Reco Finding.
-
reco-set-app-authorization-statusSet authorization status for an application in Reco.
-
reco-update-incident-timelineAdd 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}¶m={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}¶m={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