AzureRiskyUsers

Azure Risky Users provides access to all at-risk users and risk detections in the Azure AD environment.

Vulnerability Management · Azure Risky Users

Details

IDAzureRiskyUsers
ProviderMicrosoft
CategoryVulnerability Management
From Version6.0.0
Docker Imagedemisto/crypto:1.0.0.10120494
Supported ModulesAgentix Cloud Runtime Security Cloud Posture Security XSIAM EDR Cortex Cloud

README

Azure Risky Users provides access to all at-risk users and risk detections in Azure AD environment.
This integration was integrated and tested with version 1.0 of Microsoft Graph Azure Risky Users.

Self-Deployed Application

To use a self-configured Azure application, you need to add a new Azure App Registration in the Azure Portal.

The application must have the following permissions:

  • IdentityRiskEvent.Read.All
  • IdentityRiskEvent.ReadWrite.All
  • IdentityRiskyUser.Read.All
  • IdentityRiskyUser.ReadWrite.All
  • User.Read.All

In case you want to use Device code flow, you must allow public client flows (can be found under the Authentication section of the app).

Authentication Using the Client Credentials Flow (recommended)

Follow these steps for a self-deployed configuration:

  1. To use a self-configured Azure application, you need to add a new Azure App Registration in the Azure Portal. To add the registration, refer to the following Microsoft article steps 1-8.
  2. Select the client-credentials Authentication Type.
  3. Enter your Client/Application ID in the Application ID parameter.
  4. Enter your Client Secret in the Client Secret parameter.
  5. Enter your Tenant ID in the Tenant ID parameter.
  6. Save the instance.
  7. Run the !azure-risky-users-auth-test command - a ‘Success’ message should be printed to the War Room.

Authentication Using the Device Code Flow

Follow these steps for a self-deployed configuration:

  1. Fill in the required parameters.
  2. Run the !azure-risky-users-auth-start command.
  3. Follow the instructions that appear.
  4. Run the !azure-risky-users-auth-complete command.

At end of the process you’ll see a message that you’ve logged in successfully.

Cortex XSOAR Application

In order to use the Cortex XSOAR Azure application,
use the Client ID - (application_id) (ec854987-95fa-4c8f-8056-768dd0f409ac).

Authentication Using the Device Code Flow -

In order to connect to the Azure Risky Users using the Cortex XSOAR Azure App with Device Code flow authentication. See device authorization grant flow.

  1. Fill in the required parameters - use the above mentioned Client ID - (application_id).
  2. Run the !azure-risky-users-auth-start command.
  3. Follow the instructions that appear.
  4. Run the !azure-risky-users-auth-complete command.

At end of the process you’ll see a message that you’ve logged in successfully.

Configure AzureRiskyUsers on Cortex

  1. Navigate to one of the following:
    • Cortex XSOAR 8: Settings & Info > Settings > Integrations > Instances
    • Cortex XSOAR 6: Settings > Integrations
    • Cortex XSIAM: Settings > Configurations > Automation & Feed Integrations
  2. Search for AzureRiskyUsers.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Required
    Client ID True
    Authentication Type True
    Tenant ID (for Client Credentials mode) False
    Client Secret (for Client Credentials mode) False
    Azure Managed Identities Client ID False
    Use system proxy False
    Trust any certificate False
  4. Click Test to validate the URLs, token, and connection.

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

azure-risky-users-auth-test


Tests the connectivity to Azure.

Base Command

azure-risky-users-auth-test

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example


#### Human Readable Output

> Success!

### azure-risky-users-auth-start

***
Run this command to start the authorization process and follow the instructions in the command results.

#### Base Command

`azure-risky-users-auth-start`

#### Input

There are no input arguments for this command.

#### Context Output

There is no context output for this command.

#### Command Example

```!azure-risky-users-auth-start```

#### Human Readable Output

>### Authorization instructions
>
>1. To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code XXXXX to authenticate.
>2. Run the ***!azure-risky-users-auth-complete*** command in the War Room.

### azure-risky-users-auth-complete

***
Run this command to complete the authorization process. Should be used after running the azure-risky-users-auth-start command.

#### Base Command

`azure-risky-users-auth-complete`

#### Input

There are no input arguments for this command.

#### Context Output

There is no context output for this command.

#### Command Example

```!azure-risky-users-auth-complete```

#### Human Readable Output

> Authorization completed successfully.

### azure-risky-users-auth-reset

***
Run this command if for some reason you need to rerun the authentication process.

#### Base Command

`azure-risky-users-auth-reset`

#### Input

There are no input arguments for this command.

#### Context Output

There is no context output for this command.

#### Command Example

```!azure-risky-users-auth-reset```

#### Human Readable Output

>Authorization was reset successfully. Run **!azure-risky-users-auth-start** to start the authentication process.

### azure-risky-users-list

***
Returns a list of all risky users and their properties.

#### Base Command

`azure-risky-users-list`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| risk_state | Sets the Risk State to retrieve. Possible values are: atRisk, confirmedCompromised, remediated, dismissed. | Optional |
| limit | Limit of results to retrieve. Default is 50. | Optional |
| page | Page number. | Optional |
| page_size | Amount of results per request. Value can be between 1 and 500. When only page_size is given, the first page results will be fetched. | Optional |
| next_token | The URL for the next set of items to return during pagination. (This URL can be retrieved from a previous call). | Optional |
| risk_level | Sets the Risk Level to retrieve. Possible values are: low, medium, high. | Optional |
| order_by | The method used to order the retrieved results. Possible values are: riskLastUpdatedDateTime desc, riskLastUpdatedDateTime asc. Default is riskLastUpdatedDateTime desc. | Optional |
| updated_before | Displays all RiskyUsers before a specific datetime. For Example "2024-02-27T04:49:26.257525Z", "10 days", "5 months", "2 hours". | Optional |
| updated_after | Displays all RiskyUsers after a specific datetime. For Example "2024-02-27T04:49:26.257525Z", "10 days", "5 months", "2 hours". | Optional |

#### Context Output

| **Path** | **Type** | **Description** |
| --- | --- | --- |
| AzureRiskyUsers.RiskyUser.id | String | Unique ID of the user at risk. |
| AzureRiskyUsers.RiskyUser.userDisplayName | String | Risky user display name. |
| AzureRiskyUsers.RiskyUser.userPrincipalName | String | Risky user principal name. |
| AzureRiskyUsers.RiskyUser.riskLevel | String | Level of the detected risky user. Possible values are: low, medium, high, hidden, none, unknownFutureValue. |
| AzureRiskyUsers.RiskyUser.riskState | String | State of the user's risk. Possible values are: none, confirmedSafe, remediated, dismissed, atRisk, confirmedCompromised. |
| AzureRiskyUsers.RiskyUser.riskLastUpdatedDateTime | Date | The date and time that the risky user was last updated. The DateTimeOffset type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 would look like this: 2014-01-01T00:00:00Z. |
| AzureRiskyUsers.RiskyUser.isDeleted | Boolean | Indicates whether the user is deleted. |
| AzureRiskyUsers.RiskyUser.isProcessing | Boolean | Indicates whether a user's risky state is being processed by the backend. |
| AzureRiskyUsers.RiskyUser.riskDetail | String | Details of the detected risk. Possible values are: none, adminGeneratedTemporaryPassword, userPerformedSecuredPasswordChange, userPerformedSecuredPasswordReset, adminConfirmedSigninSafe, aiConfirmedSigninSafe, userPassedMFADrivenByRiskBasedPolicy, adminDismissedAllRiskForUser, adminConfirmedSigninCompromised, hidden, adminConfirmedUserCompromised, unknownFutureValue. |
| AzureRiskyUsers.RiskyUserListNextToken | String | A property in the response that contains a URL to the next page of results. |

#### Command example

```!azure-risky-users-list page_size=2```

#### Context Example

```json
{
    "AzureRiskyUsers": {
        "RiskyUser": [
            {
                "id": "ID_1",
                "isDeleted": false,
                "isProcessing": false,
                "riskDetail": "none",
                "riskLastUpdatedDateTime": "2023-06-04T10:12:39.3625926Z",
                "riskLevel": "medium",
                "riskState": "atRisk",
                "userDisplayName": "user Display Name",
                "userPrincipalName": "User Principal Name"
            },
            {
                "id": "ID_2",
                "isDeleted": false,
                "isProcessing": false,
                "riskDetail": "none",
                "riskLastUpdatedDateTime": "2022-02-23T17:50:40.3408199Z",
                "riskLevel": "high",
                "riskState": "atRisk",
                "userDisplayName": "user Display Name",
                "userPrincipalName": "User Principal Name"
            },
        ],
        "RiskyUserListNextToken": "token",
    }
}

Human Readable Output

Risky Users List

Id User Display Name User Principal Name Risk Level Risk State Risk Detail Risk Last Updated Date Time
ID_1 user Display Name User Principal Name medium atRisk none 2023-06-04T10:12:39.3625926Z
ID_2 user Display Name User Principal Name high atRisk none 2022-02-23T17:50:40.3408199Z

Risky Users List Token

next_token
token

azure-risky-user-get


Retrieve properties and relationships of a Risky User.

Base Command

azure-risky-user-get

Input

Argument Name Description Required
id Risky user ID to retrieve. Required

Context Output

Path Type Description
AzureRiskyUsers.RiskyUser.id String Unique ID of the user at risk.
AzureRiskyUsers.RiskyUser.userDisplayName String Risky user display name.
AzureRiskyUsers.RiskyUser.userPrincipalName String Risky user principal name.
AzureRiskyUsers.RiskyUser.riskLevel String Level of the detected risky user. Possible values are: low, medium, high, hidden, none, unknownFutureValue.
AzureRiskyUsers.RiskyUser.riskState String State of the user’s risk. Possible values are: none, confirmedSafe, remediated, dismissed, atRisk, confirmedCompromised.
AzureRiskyUsers.RiskyUser.riskLastUpdatedDateTime Date The date and time that the risky user was last updated. The DateTimeOffset type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 would look like this: 2014-01-01T00:00:00Z
AzureRiskyUsers.RiskyUser.isDeleted Boolean Indicates whether the user is deleted.
AzureRiskyUsers.RiskyUser.isProcessing Boolean Indicates whether a user’s risky state is being processed by the backend.
AzureRiskyUsers.RiskyUser.riskDetail String Details of the detected risk. Possible values are: none, adminGeneratedTemporaryPassword, userPerformedSecuredPasswordChange, userPerformedSecuredPasswordReset, adminConfirmedSigninSafe, aiConfirmedSigninSafe, userPassedMFADrivenByRiskBasedPolicy, adminDismissedAllRiskForUser, adminConfirmedSigninCompromised, hidden, adminConfirmedUserCompromised, unknownFutureValue.

Command Example

!azure-risky-user-get id=333

Context Example

{
    "AzureRiskyUsers": {
        "RiskyUser": {
            "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#identityProtection/riskyUsers/$entity",
            "id": "333",
            "isDeleted": false,
            "isProcessing": false,
            "riskDetail": "userPerformedSecuredPasswordReset",
            "riskLastUpdatedDateTime": "2020-10-05T12:12:17.2115592Z",
            "riskLevel": "none",
            "riskState": "remediated",
            "userDisplayName": "Yossi Israeli",
            "userPrincipalName": "yossi@test.com"
        }
    }
}

Human Readable Output

Found Risky User With ID: 333

Id User Display Name User Principal Name Risk Level Risk State Risk Detail Risk Last Updated Date Time
333 Yossi Israeli yossi@test.com none remediated userPerformedSecuredPasswordReset 2020-10-05T12:12:17.2115592Z

azure-risky-users-risk-detections-list


Get a list of the riskDetection objects and their properties.

Base Command

azure-risky-users-risk-detections-list

Input

Argument Name Description Required
limit Limit of results to retrieve. Default is 50. Optional
page Page number. Default is 1. Optional
risk_state Risk State to retrieve. If not specified, all states will be retrieved. Possible values are: atRisk, confirmedCompromised, remediated, dismissed, confirmedSafe. Optional
risk_level Specify to get only results with the same Risk Level. Possible values are: low, medium, high. Optional
detected_date_time_before Filter events that created before specific time range starting, e.g. 2022-06-09T23:00:44.7420905Z. Optional
detected_date_time_after Filter events that created after specific time range starting, e.g. 2022-06-09T23:00:44.7420905Z. Optional
order_by The method used to order the retrieved results. Optional

Context Output

Path Type Description
AzureRiskyUsers.RiskDetection.id String Unique ID of the risk detection. Inherited from entity.
AzureRiskyUsers.RiskDetection.userId String Unique ID of the user.
AzureRiskyUsers.RiskDetection.userDisplayName String The user display name of the user.
AzureRiskyUsers.RiskDetection.userPrincipalName String The user principal name (UPN) of the user.
AzureRiskyUsers.RiskDetection.riskDetail String Details of the detected risk. Possible values are: none, adminGeneratedTemporaryPassword, userPerformedSecuredPasswordChange, userPerformedSecuredPasswordReset, adminConfirmedSigninSafe, aiConfirmedSigninSafe, userPassedMFADrivenByRiskBasedPolicy, adminDismissedAllRiskForUser, adminConfirmedSigninCompromised, hidden, adminConfirmedUserCompromised, unknownFutureValue.
AzureRiskyUsers.RiskDetection.riskEventType String The type of risk event detected. The possible values are unlikelyTravel, anonymizedIPAddress, maliciousIPAddress, unfamiliarFeatures, malwareInfectedIPAddress, suspiciousIPAddress, leakedCredentials, investigationsThreatIntelligence, generic,adminConfirmedUserCompromised, mcasImpossibleTravel, mcasSuspiciousInboxManipulationRules, investigationsThreatIntelligenceSigninLinked, maliciousIPAddressValidCredentialsBlockedIP, and unknownFutureValue. If the risk detection is a premium detection, will show generic
AzureRiskyUsers.RiskDetection.riskLevel String Level of the detected risk. Possible values are: low, medium, high, hidden, none, unknownFutureValue.
AzureRiskyUsers.RiskDetection.riskState String The state of a detected risky user or sign-in. Possible values are: none, confirmedSafe, remediated, dismissed, atRisk, confirmedCompromised, unknownFutureValue.
AzureRiskyUsers.RiskDetection.ipAddress String Provides the IP address of the client from where the risk occurred.
AzureRiskyUsers.RiskDetection.source String Source of the risk detection. For example, activeDirectory.
AzureRiskyUsers.RiskDetection.detectionTimingType String Timing of the detected risk (real-time/offline). Possible values are: notDefined, realtime, nearRealtime, offline, unknownFutureValue.
AzureRiskyUsers.RiskDetection.lastUpdatedDateTime Date Date and time that the risk detection was last updated. The DateTimeOffset type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is look like this: 2014-01-01T00:00:00Z
AzureRiskyUsers.RiskDetection.location String Location of the sign-in.
AzureRiskyUsers.RiskDetection.activity String Indicates the activity type the detected risk is linked to. . Possible values are: signin, user, unknownFutureValue.
AzureRiskyUsers.RiskDetection.activityDateTime Date Date and time that the risky activity occurred. The DateTimeOffset type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is look like this: 2014-01-01T00:00:00Z
AzureRiskyUsers.RiskDetection.additionalInfo String Additional information associated with the risk detection in JSON format.
AzureRiskyUsers.RiskDetection.correlationId String Correlation ID of the sign-in associated with the risk detection. This property is null if the risk detection is not associated with a sign-in.
AzureRiskyUsers.RiskDetection.detectedDateTime Date Date and time that the risk was detected. The DateTimeOffset type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is look like this: 2014-01-01T00:00:00Z
AzureRiskyUsers.RiskDetection.requestId String Request ID of the sign-in associated with the risk detection. This property is null if the risk detection is not associated with a sign-in.
AzureRiskyUsers.RiskDetection.tokenIssuerType String Indicates the type of token issuer for the detected sign-in risk. Possible values are: AzureAD, ADFederationServices, UnknownFutureValue.

Command Example

!azure-risky-users-risk-detections-list limit=2

Context Example

{
    "AzureRiskyUsers": {
        "RiskDetection": [
            {
                "activity": "signin",
                "activityDateTime": "2021-06-20T03:51:32.9572792Z",
                "additionalInfo": "[{\"Key\":\"userAgent\",\"Value\":\"Dalvik/2.1.0 (Linux; U; Android 9; VKY-L29 Build/HUAWEIVKY-L29) ;VKY-L29\"}]",
                "correlationId": "aaaa1111",
                "detectedDateTime": "2021-06-20T03:51:32.9572792Z",
                "detectionTimingType": "realtime",
                "id": "555",
                "ipAddress": "1.1.1.1",
                "lastUpdatedDateTime": "2021-06-20T03:53:58.853418Z",
                "location": {
                    "city": "Pisgat Ze'ev",
                    "countryOrRegion": "IL",
                    "geoCoordinates": {
                        "latitude": 31,
                        "longitude": 35
                    },
                    "state": "Yerushalayim"
                },
                "requestId": "bbbb1111",
                "riskDetail": "userPassedMFADrivenByRiskBasedPolicy",
                "riskEventType": "unfamiliarFeatures",
                "riskLevel": "low",
                "riskState": "remediated",
                "source": "IdentityProtection",
                "tokenIssuerType": "AzureAD",
                "userDisplayName": "Shalev Israeli",
                "userId": "777",
                "userPrincipalName": "ShalevI@test.com"
            },
            {
                "activity": "signin",
                "activityDateTime": "2021-06-27T19:16:19.9976898Z",
                "additionalInfo": "[{\"Key\":\"userAgent\",\"Value\":\"Dalvik/2.1.0 (Linux; U; Android 9; SM-G950F Build/PPR1.180610.011) ;SM-G950F\"}]",
                "correlationId": "aaaa2222",
                "detectedDateTime": "2021-06-27T19:16:19.9976898Z",
                "detectionTimingType": "realtime",
                "id": "888",
                "ipAddress": "1.1.1.1",
                "lastUpdatedDateTime": "2021-06-27T19:19:44.4975416Z",
                "location": {
                    "city": "Dniprodzerzhyns'k",
                    "countryOrRegion": "UA",
                    "geoCoordinates": {
                        "latitude": 48,
                        "longitude": 34
                    },
                    "state": "Dnipropetrovs'ka Oblast'"
                },
                "requestId": "bbbb2222",
                "riskDetail": "userPassedMFADrivenByRiskBasedPolicy",
                "riskEventType": "unfamiliarFeatures",
                "riskLevel": "low",
                "riskState": "remediated",
                "source": "IdentityProtection",
                "tokenIssuerType": "AzureAD",
                "userDisplayName": "Svetlana Israeli",
                "userId": "999",
                "userPrincipalName": "SvetlanaI@test.com"
            }
        ]
    }
}

Human Readable Output

Risk Detections List

Current page size: 2
Showing page 1 out others that may exist

Id User Id User Display Name User Principal Name Risk Detail Risk Event Type Risk Level Risk State Risk Detail Last Updated Date Time Ip Address
555 777 Shalev Israeli ShalevI@test.com userPassedMFADrivenByRiskBasedPolicy unfamiliarFeatures low remediated userPassedMFADrivenByRiskBasedPolicy 2021-06-20T03:53:58.853418Z 1.1.1.1
888 999 Svetlana Israeli SvetlanaI@test.com userPassedMFADrivenByRiskBasedPolicy unfamiliarFeatures low remediated userPassedMFADrivenByRiskBasedPolicy 2021-06-27T19:19:44.4975416Z 1.1.1.1

azure-risky-users-risk-detection-get


Read the properties and relationships of a riskDetection object.

Base Command

azure-risky-users-risk-detection-get

Input

Argument Name Description Required
id ID of risk detection to retrieve. Required

Context Output

Path Type Description
AzureRiskyUsers.RiskDetection.id String Unique ID of the risk detection. Inherited from entity.
AzureRiskyUsers.RiskDetection.userId String Unique ID of the user.
AzureRiskyUsers.RiskDetection.userDisplayName String The user display name of the user.
AzureRiskyUsers.RiskDetection.userPrincipalName String The user principal name (UPN) of the user.
AzureRiskyUsers.RiskDetection.riskDetail String Details of the detected risk. Possible values are: none, adminGeneratedTemporaryPassword, userPerformedSecuredPasswordChange, userPerformedSecuredPasswordReset, adminConfirmedSigninSafe, aiConfirmedSigninSafe, userPassedMFADrivenByRiskBasedPolicy, adminDismissedAllRiskForUser, adminConfirmedSigninCompromised, hidden, adminConfirmedUserCompromised, unknownFutureValue.
AzureRiskyUsers.RiskDetection.riskEventType String The type of risk event detected. The possible values are unlikelyTravel, anonymizedIPAddress, maliciousIPAddress, unfamiliarFeatures, malwareInfectedIPAddress, suspiciousIPAddress, leakedCredentials, investigationsThreatIntelligence, generic,adminConfirmedUserCompromised, mcasImpossibleTravel, mcasSuspiciousInboxManipulationRules, investigationsThreatIntelligenceSigninLinked, maliciousIPAddressValidCredentialsBlockedIP, and unknownFutureValue. If the risk detection is a premium detection, will show generic
AzureRiskyUsers.RiskDetection.riskLevel String Level of the detected risk. Possible values are: low, medium, high, hidden, none, unknownFutureValue.
AzureRiskyUsers.RiskDetection.riskState String The state of a detected risky user or sign-in. Possible values are: none, confirmedSafe, remediated, dismissed, atRisk, confirmedCompromised, unknownFutureValue.
AzureRiskyUsers.RiskDetection.ipAddress String Provides the IP address of the client from where the risk occurred.
AzureRiskyUsers.RiskDetection.source String Source of the risk detection. For example, activeDirectory.
AzureRiskyUsers.RiskDetection.detectionTimingType String Timing of the detected risk (real-time/offline). Possible values are: notDefined, realtime, nearRealtime, offline, unknownFutureValue.
AzureRiskyUsers.RiskDetection.lastUpdatedDateTime Date Date and time that the risk detection was last updated. The DateTimeOffset type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is look like this: 2014-01-01T00:00:00Z
AzureRiskyUsers.RiskDetection.location String Location of the sign-in.
AzureRiskyUsers.RiskDetection.activity String Indicates the activity type the detected risk is linked to. . Possible values are: signin, user, unknownFutureValue.
AzureRiskyUsers.RiskDetection.activityDateTime Date Date and time that the risky activity occurred. The DateTimeOffset type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is look like this: 2014-01-01T00:00:00Z
AzureRiskyUsers.RiskDetection.additionalInfo String Additional information associated with the risk detection in JSON format.
AzureRiskyUsers.RiskDetection.correlationId String Correlation ID of the sign-in associated with the risk detection. This property is null if the risk detection is not associated with a sign-in.
AzureRiskyUsers.RiskDetection.detectedDateTime Date Date and time that the risk was detected. The DateTimeOffset type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is look like this: 2014-01-01T00:00:00Z
AzureRiskyUsers.RiskDetection.requestId String Request ID of the sign-in associated with the risk detection. This property is null if the risk detection is not associated with a sign-in.
AzureRiskyUsers.RiskDetection.tokenIssuerType String Indicates the type of token issuer for the detected sign-in risk. Possible values are: AzureAD, ADFederationServices, UnknownFutureValue.

Command Example

!azure-risky-users-risk-detection-get id=6565

Context Example

{
    "AzureRiskyUsers": {
        "RiskDetection": {
            "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#identityProtection/riskDetections/$entity",
            "activity": "signin",
            "activityDateTime": "2021-07-03T13:35:38.8773806Z",
            "additionalInfo": "[{\"Key\":\"userAgent\",\"Value\":\"Dalvik/2.1.0 (Linux; U; Android 9; SM-G950F Build/PPR1.180610.011) ;SM-G950F\"}]",
            "correlationId": "aaaa3333",
            "detectedDateTime": "2021-07-03T13:35:38.8773806Z",
            "detectionTimingType": "realtime",
            "id": "6565",
            "ipAddress": "3.3.3.3",
            "lastUpdatedDateTime": "2021-07-03T13:38:04.6531838Z",
            "location": {
                "city": "Lviv",
                "countryOrRegion": "UA",
                "geoCoordinates": {
                    "latitude": 49,
                    "longitude": 24
                },
                "state": "L'vivs'ka Oblast'"
            },
            "requestId": "bbbb33333",
            "riskDetail": "userPassedMFADrivenByRiskBasedPolicy",
            "riskEventType": "unfamiliarFeatures",
            "riskLevel": "low",
            "riskState": "remediated",
            "source": "IdentityProtection",
            "tokenIssuerType": "AzureAD",
            "userDisplayName": "Svetlana Israeli",
            "userId": "999",
            "userPrincipalName": "SvetlanaI@test.com"
        }
    }
}

Human Readable Output

Found Risk Detection with ID: 6565

Id User Id User Display Name User Principal Name Risk Detail Risk Event Type Risk Level Risk State Ip Address Detection Timing Type Last Updated Date Time Location
6565 999 Svetlana Israeli SvetlanaI@test.com userPassedMFADrivenByRiskBasedPolicy unfamiliarFeatures low remediated 3.3.3.3 realtime 2021-07-03T13:38:04.6531838Z city: Lviv
state: L’vivs’ka Oblast’
countryOrRegion: UA
geoCoordinates: {“latitude”: 49, “longitude”: 24}

azure-risky-users-confirm-safe


Confirms user(s) as safe (post-investigation).

Base Command

azure-risky-users-confirm-safe

Input

Argument Name Description Required
user A comma-separated list of User IDs or UPNs of the user(s) to confirm as safe. Results may not be immediately visible due to processing delays in Azure. Required

Context Output

Path Type Description
AzureRiskyUsers.Remediation.UserID String The ID of the user.
AzureRiskyUsers.Remediation.UserPrincipalName String The UPN of the user. This key will be populated only if the user’s UPN is provided.
AzureRiskyUsers.Remediation.Success Boolean Whether the new state was applied successfully.
AzureRiskyUsers.Remediation.RiskState String The new risk state of the user.

Command Example

!azure-risky-users-confirm-safe user=user_id_1,name2@test.com,name3@test.com

Context Example

{
    "AzureRiskyUsers": {
        "Remediation": [
            {
                "UserID": "user_id_1",
                "UserPrincipalName": "",
                "Success": true,
                "RiskState": "confirmedSafe"
            },
            {
                "UserID": "user_id_2",
                "UserPrincipalName": "name2@test.com",
                "Success": true,
                "RiskState": "confirmedSafe"
            },
            {
                "UserID": "user_id_3",
                "UserPrincipalName": "name3@test.com",
                "Success": false,
                "RiskState": ""
            }
        ]
    }
}

Human Readable Output

Successfully confirmed users as safe

UserID UserPrincipalName Success RiskState
user_id_1   true confirmedSafe
user_id_2 name2@test.com true confirmedSafe

Unable to confirm users as safe

User Error
name3@test.com Error: confirmation failed.

azure-risky-users-confirm-compromise


Confirms user(s) as compromised.

Base Command

azure-risky-users-confirm-compromise

Input

Argument Name Description Required
user A comma-separated list of User IDs or UPNs of the user(s) to confirm as compromised. Results may not be immediately visible due to processing delays in Azure. Required

Context Output

Path Type Description
AzureRiskyUsers.Remediation.UserID String The ID of the user.
AzureRiskyUsers.Remediation.UserPrincipalName String The UPN of the user. This key will be populated only if the user’s UPN is provided.
AzureRiskyUsers.Remediation.Success Boolean Whether the new state was applied successfully.
AzureRiskyUsers.Remediation.RiskState String The new risk state of the user.

Command Example

!azure-risky-users-confirm-compromise user=user_id_1,name2@test.com,name3@test.com

Context Example

{
    "AzureRiskyUsers": {
        "Remediation": [
            {
                "UserID": "user_id_1",
                "UserPrincipalName": "",
                "Success": true,
                "RiskState": "confirmedCompromised"
            },
            {
                "UserID": "user_id_2",
                "UserPrincipalName": "name2@test.com",
                "Success": true,
                "RiskState": "confirmedCompromised"
            },
            {
                "UserID": "user_id_3",
                "UserPrincipalName": "name3@test.com",
                "Success": false,
                "RiskState": ""
            }
        ]
    }
}

Human Readable Output

Successfully confirmed users as compromised

UserID UserPrincipalName Success RiskState
user_id_1   true confirmedCompromised
user_id_2 name2@test.com true confirmedCompromised

Unable to confirm users as compromised

User Error
name3@test.com Error: confirmation failed.

Configuration parameters

  • client_id — Client ID
  • authentication_type — Authentication Type (required)
  • tenant_id — Tenant ID (for Client Credentials mode)
  • client_secret — Client Secret (for Client Credentials mode)
  • managed_identities_client_id
  • proxy — Use system proxy settings
  • insecure — Trust any certificate (not secure)

Commands (10)

  • azure-risky-user-get

    Retrieves properties and relationships of a Risky User.

  • azure-risky-users-auth-complete

    Completes the authorization process. Run this command after executing the azure-risky-users-auth-start command.

  • azure-risky-users-auth-reset

    Run this command if for some reason you need to rerun the authentication process.

  • azure-risky-users-auth-start

    Starts the authorization process. Follow the instructions in the command results.

  • azure-risky-users-auth-test

    Tests the connectivity to Azure.

  • azure-risky-users-confirm-compromise

    Confirms user(s) as compromised.

  • azure-risky-users-confirm-safe

    Confirms user(s) as safe (post-investigation).

  • azure-risky-users-list

    Returns a list of all risky users and their properties.

  • azure-risky-users-risk-detection-get

    Reads the properties and relationships of a riskDetection object.

  • azure-risky-users-risk-detections-list

    Returns a comma-separated list of the Risk Detection objects and their properties.

import json

import pytest
from AzureRiskyUsers import CLIENT_CREDENTIALS_FLOW, DEVICE_FLOW, Client

BASE_URL = "https://graph.microsoft.com/v1.0/"
ACCESS_TOKEN_REQUEST_URL = "https://login.microsoftonline.com/organizations/oauth2/v2.0/token"


def load_mock_response(file_name: str) -> dict:
    """
    Load one of the mock responses to be used for assertion.
    Args:
        file_name (str): Name of the mock response JSON file to return.
    """
    with open(f"test_data/{file_name}", encoding="utf-8") as json_file:
        return json.loads(json_file.read())


def mock_client():
    return Client(client_id="client_id", verify=False, proxy=False, authentication_type=DEVICE_FLOW)


def test_risky_users_list_command_without_pagination(requests_mock) -> None:
    """
    Scenario: List Risky Users.
    Given:
     - User has provided valid credentials.
     - Headers and JWT token have been set.
    When:
     - risky_users_list_command is called.
    Then:
     - Ensure number of items is correct.
     - Ensure outputs prefix is correct.
     - Ensure outputs key fields is correct.
    """
    from AzureRiskyUsers import risky_users_list_command

    mock_response = load_mock_response("list_risky_users.json")
    requests_mock.post(ACCESS_TOKEN_REQUEST_URL, json={})
    requests_mock.get(f"{BASE_URL}identityProtection/riskyUsers", json=mock_response)
    result = risky_users_list_command(mock_client(), {"limit": "20"})
    assert result[0].outputs_prefix == "AzureRiskyUsers.RiskyUser"
    assert result[0].outputs_key_field == "id"
    assert len(result[0].raw_response) == 3


def test_risky_users_list_command_with_page_size(requests_mock) -> None:
    """
    Scenario: List Risky Users.
    Given:
     - User has provided valid credentials.
     - Headers and JWT token have been set.
    When:
     - risky_users_list_command is called.
    Then:
     - Ensure number of items is correct.
     - Ensure outputs prefix is correct.
     - Ensure outputs key fields is correct.
    """
    from AzureRiskyUsers import risky_users_list_command

    mock_response = load_mock_response("list_risky_users.json")
    requests_mock.post(ACCESS_TOKEN_REQUEST_URL, json={})
    requests_mock.get(f"{BASE_URL}identityProtection/riskyUsers", json=mock_response)
    result = risky_users_list_command(mock_client(), {"page_size": "3"})
    assert result[0].outputs_prefix == "AzureRiskyUsers.RiskyUser"
    assert result[0].outputs_key_field == "id"
    assert len(result[0].raw_response) == 3
    assert result[1].outputs == {"AzureRiskyUsers(true)": {"RiskyUserListNextToken": mock_response.get("@odata.nextLink")}}


def test_risky_users_list_command_with_token(requests_mock) -> None:
    """
    Scenario: List Risky Users.
    Given:
     - User has provided valid credentials.
     - Headers and JWT token have been set.
    When:
     - risky_users_list_command is called.
    Then:
     - Ensure number of items is correct.
     - Ensure outputs prefix is correct.
     - Ensure outputs key fields is correct.
    """
    from AzureRiskyUsers import risky_users_list_command

    mock_response = load_mock_response("list_risky_users.json")
    requests_mock.post(ACCESS_TOKEN_REQUEST_URL, json={})
    requested_mock_token = requests_mock.get(f"{BASE_URL}/token", json=mock_response)
    requested_mock_without_token = requests_mock.get(f"{BASE_URL}identityProtection/riskyUsers", json=mock_response)
    result = risky_users_list_command(mock_client(), {"next_token": f"{BASE_URL}/token"})
    assert result[0].outputs_prefix == "AzureRiskyUsers.RiskyUser"
    assert result[0].outputs_key_field == "id"
    assert len(result[0].outputs) == 3
    assert len(result[0].raw_response) == 3
    assert result[1].outputs == {"AzureRiskyUsers(true)": {"RiskyUserListNextToken": mock_response.get("@odata.nextLink")}}
    assert requested_mock_token.call_count == 1
    assert requested_mock_without_token.call_count == 0


def test_risky_users_list_command_with_limit(requests_mock) -> None:
    """
    Scenario: List Risky Users.
    Given:
     - User has provided valid credentials.
     - Headers and JWT token have been set.
    When:
     - risky_users_list_command is called.
    Then:
     - Ensure number of items is correct.
     - Ensure outputs prefix is correct.
     - Ensure outputs key fields is correct.
    """
    from AzureRiskyUsers import risky_users_list_command

    mock_response = load_mock_response("list_risky_users.json")
    requests_mock.post(ACCESS_TOKEN_REQUEST_URL, json={})
    requests_mock.get(f"{BASE_URL}identityProtection/riskyUsers", json=mock_response)
    result = risky_users_list_command(mock_client(), {"limit": "6"})
    assert len(result) == 1
    assert result[0].outputs_prefix == "AzureRiskyUsers.RiskyUser"
    assert result[0].outputs_key_field == "id"
    assert len(result[0].outputs) == 6
    assert "RiskyUserListNextToken" not in result[0].outputs


def test_risky_users_list_command_with_order_by(requests_mock) -> None:
    """
    Scenario: List Risky Users.
    Given:
     - User has provided valid credentials.
     - Headers and JWT token have been set.
    When:
     - risky_users_list_command is called.
    Then:
     - Ensure number of items is correct.
     - Ensure outputs prefix is correct.
     - Ensure outputs key fields is correct.
    """
    from AzureRiskyUsers import risky_users_list_command

    mock_response = load_mock_response("list_risky_users.json")
    requests_mock.post(ACCESS_TOKEN_REQUEST_URL, json={})
    requests_mock.get(f"{BASE_URL}identityProtection/riskyUsers", json=mock_response)

    args = {"order_by": "riskLastUpdatedDateTime desc", "limit": "10"}

    result = risky_users_list_command(mock_client(), args)
    assert len(result) == 1
    assert result[0].outputs_prefix == "AzureRiskyUsers.RiskyUser"
    assert result[0].outputs_key_field == "id"

    assert result[0].outputs[0].get("id") == "111"
    assert "RiskyUserListNextToken" not in result[0].outputs


def test_risky_users_list_command_with_updated_after(requests_mock) -> None:
    """
    Scenario: List Risky Users.
    Given:
     - User has provided valid credentials.
     - Headers and JWT token have been set.
    When:
     - risky_users_list_command is called.
    Then:
     - Ensure number of items is correct.
     - Ensure outputs prefix is correct.
     - Ensure outputs key fields is correct.
    """
    from AzureRiskyUsers import risky_users_list_command

    mock_response = load_mock_response("list_risky_users.json")
    requests_mock.post(ACCESS_TOKEN_REQUEST_URL, json={})
    requests_mock.get(f"{BASE_URL}identityProtection/riskyUsers", json=mock_response)

    args = {"updated_after": "500 days ago", "limit": "10"}

    result = risky_users_list_command(mock_client(), args)
    assert len(result) == 1
    assert result[0].outputs_prefix == "AzureRiskyUsers.RiskyUser"
    assert result[0].outputs_key_field == "id"

    assert result[0].outputs[0].get("id") == "111"
    assert "RiskyUserListNextToken" not in result[0].outputs


def test_risky_user_get_command(requests_mock) -> None:
    """
    Scenario: Get Risky User.
    Given:
     - User has provided valid credentials.
     - Headers and JWT token have been set.
    When:
     - risky_user_get_command is called.
    Then:
     - Ensure outputs prefix is correct.
     - Ensure outputs key fields is correct.
     - Ensure user ID is correct.

    """
    from AzureRiskyUsers import risky_user_get_command

    mock_response = load_mock_response("get_risky_user.json")
    requests_mock.post(ACCESS_TOKEN_REQUEST_URL, json={})
    requests_mock.get(f"{BASE_URL}identityProtection/riskyUsers/1", json=mock_response)
    result = risky_user_get_command(mock_client(), args={"id": "1"})
    assert result.outputs_prefix == "AzureRiskyUsers.RiskyUser"
    assert result.outputs_key_field == "id"
    assert result.raw_response.get("id") == "1"


def test_risk_detections_list_command(requests_mock) -> None:
    """
    Scenario: List Risk Detections.
    Given:
     - User has provided valid credentials.
     - Headers and JWT token have been set.
    When:
     - risk_detections_list_command is called.
    Then:
     - Ensure outputs prefix is correct.
     - Ensure outputs key fields is correct.
     - Ensure risk detection ID is correct.

    """
    from AzureRiskyUsers import risk_detections_list_command

    mock_response = load_mock_response("list_risk_detections.json")
    requests_mock.post(ACCESS_TOKEN_REQUEST_URL, json={})
    requests_mock.get(f"{BASE_URL}identityProtection/riskDetections", json=mock_response)
    result = risk_detections_list_command(mock_client(), {"limit": "20", "page": "1"})
    assert result.outputs_prefix == "AzureRiskyUsers.RiskDetection"
    assert result.outputs_key_field == "id"
    assert result.raw_response.get("value")[0].get("id") == "1"


def test_risk_detection_get_command(requests_mock) -> None:
    """
    Scenario: Get Risk Detection.
    Given:
     - User has provided valid credentials.
     - Headers and JWT token have been set.
    When:
     - risk_detection_get_command is called.
    Then:
     - Ensure outputs prefix is correct.
     - Ensure outputs key fields is correct.
     - Ensure risk detection ID is correct.

    """
    from AzureRiskyUsers import risk_detection_get_command

    mock_response = load_mock_response("get_risk_detection.json")
    requests_mock.post(ACCESS_TOKEN_REQUEST_URL, json={})
    requests_mock.get(f"{BASE_URL}identityProtection/riskDetections/1", json=mock_response)
    result = risk_detection_get_command(mock_client(), args={"id": "1"})
    assert result.outputs_prefix == "AzureRiskyUsers.RiskDetection"
    assert result.outputs_key_field == "id"
    assert result.raw_response.get("value")[0].get("id") == "1"


def test_build_query_filter() -> None:
    """
    Scenario: Build query filter for API call.
    Given:
     - Provided valid arguments.
    When:
     - build_query_filter function is called.
    Then:
     - Ensure results are valid.
    """
    from AzureRiskyUsers import build_query_filter

    result = build_query_filter(risk_state="dismissed", risk_level="medium")
    assert result == "riskState eq 'dismissed' and riskLevel eq 'medium'"


def test_get_skip_token() -> None:
    """
    Scenario: Get skip token.
    Given:
     - Provided valid arguments.
    When:
     - get_skip_token function is called.
    Then:
     - Ensure results are valid.
    """
    from AzureRiskyUsers import get_skip_token

    result = get_skip_token(
        next_link=None, outputs_prefix="AzureRiskyUsers.RiskyUser", outputs_key_field="id", readable_output="test"
    )
    assert result.outputs_prefix == "AzureRiskyUsers.RiskyUser"
    assert result.outputs_key_field == "id"
    assert result.readable_output == "test"


@pytest.mark.parametrize(
    "authentication_type, expected_grant, expected_scope, expected_token_retrieval",
    [
        (
            DEVICE_FLOW,
            "urn:ietf:params:oauth:grant-type:device_code",
            "https://graph.microsoft.com/IdentityRiskyUser.Read.All IdentityRiskEvent.ReadWrite.All IdentityRiskyUser.Read.All"
            " IdentityRiskyUser.ReadWrite.All offline_access",
            "https://login.microsoftonline.com/organizations/oauth2/v2.0/token",
        ),
        (
            CLIENT_CREDENTIALS_FLOW,
            "client_credentials",
            "https://graph.microsoft.com/.default",
            "https://login.microsoftonline.com//oauth2/v2.0/token",
        ),
    ],
)
def test_create_client_by_auth_type(authentication_type, expected_grant, expected_scope, expected_token_retrieval):
    """
    Test that the client is created according to the authentication type as expected.

    Given:
     - Authentication type:
        1. Device
        2. Client Credentials
    When:
     - Running the client's instructor.
    Then:
     - Verify that the client's grant type, scope, and token retrieval url are as expected.

    """
    client = Client(client_id="client_id", verify=False, proxy=False, authentication_type=authentication_type)

    assert client.ms_client.grant_type == expected_grant
    assert client.ms_client.scope == expected_scope
    assert client.ms_client.token_retrieval_url == expected_token_retrieval


@pytest.mark.parametrize(argnames="client_id", argvalues=["test_client_id", None])
def test_test_module_command_with_managed_identities(mocker, requests_mock, client_id):
    """
    Scenario: run test module when managed identities client id provided.
    Given:
     - User has provided managed identities client oid.
    When:
     - test-module called.
    Then:
     - Ensure the output are as expected
    """
    import AzureRiskyUsers
    import demistomock as demisto
    from AzureRiskyUsers import MANAGED_IDENTITIES_TOKEN_URL, Resources, main

    mock_token = {"access_token": "test_token", "expires_in": "86400"}
    get_mock = requests_mock.get(MANAGED_IDENTITIES_TOKEN_URL, json=mock_token)
    params = {
        "managed_identities_client_id": {"password": client_id},
        "authentication_type": "Azure Managed Identities",
        "subscription_id": {"password": "test"},
        "resource_group": "test_resource_group",
    }
    mocker.patch.object(demisto, "params", return_value=params)
    mocker.patch.object(demisto, "command", return_value="test-module")
    mocker.patch.object(AzureRiskyUsers, "return_results")
    mocker.patch("MicrosoftApiModule.get_integration_context", return_value={})

    main()

    assert "ok" in AzureRiskyUsers.return_results.call_args[0][0]
    qs = get_mock.last_request.qs
    assert qs["resource"] == [Resources.graph]
    assert (client_id and qs["client_id"] == [client_id]) or "client_id" not in qs


@pytest.mark.parametrize(
    "query,filter_name,filter_value,filter_operator,expected_query",
    [
        ("", "riskState", "dismissed", "eq", "riskState eq 'dismissed'"),
        (
            "riskState eq 'dismissed'",
            "detectedDateTime",
            "2022-06-09T23:00:44.7420905Z",
            "le",
            "riskState eq 'dismissed' and detectedDateTime le 2022-06-09T23:00:44.7420905Z",
        ),
    ],
)
def test_update_query(query, filter_name, filter_value, filter_operator, expected_query):
    """
    Scenario: Build query filter for API call.
    Given:
     - Provided valid arguments.
    When:
     - update_query function is called.
    Then:
     - Ensure results are valid.
    """
    from AzureRiskyUsers import update_query

    query = update_query(query, filter_name, filter_value, filter_operator)
    assert query == expected_query


def test_do_pagination_with_next_token(mocker):
    """
    Given:
      - The function arguments: response, limit:
          - The response has a nextLink URL.
          - The limit is greater than the number of results in the first response.
    When:
      - Calling the 'do_pagination' function.
    Then:
      - Assert the request url is as expected - make an API request using the nextLink URL.
      - Verify the function output is as expected
    """
    from AzureRiskyUsers import do_pagination

    mock_response = load_mock_response("list_risky_users.json")
    requests_mock = mocker.patch.object(Client, "risky_users_list_request", return_value=mock_response)
    limit = 6
    expected_result = mock_response.get("value", []) * 2

    result = do_pagination(mock_client(), mock_response, limit)
    data = result.get("value", [])
    last_next_link = result.get("@odata.nextLink", "")
    assert requests_mock.call_count == 1
    assert data == expected_result
    assert last_next_link == mock_response.get("@odata.nextLink")


def test_do_pagination_without_next_token(mocker):
    """
    Given:
      - The function arguments: response, limit:
          - The response has a nextLink URL.
          - The limit is greater than the number of results in the first response.
    When:
      - Calling the 'do_pagination' function.
    Then:
      - Assert the request url is as expected - make an API request using the nextLink URL.
      - Verify the function output is as expected
    """
    from AzureRiskyUsers import do_pagination

    mock_response = load_mock_response("list_risky_users.json")
    mock_response["@odata.nextLink"] = None
    requests_mock = mocker.patch.object(Client, "risky_users_list_request", return_value=mock_response)
    limit = 6
    expected_result = mock_response.get("value", [])

    result = do_pagination(mock_client(), mock_response, limit)
    data = result.get("value", [])
    last_next_link = result.get("@odata.nextLink", "")
    assert requests_mock.call_count == 0
    assert data == expected_result
    assert last_next_link == mock_response.get("@odata.nextLink")


def test_risky_users_confirm_compromise_command(mocker):
    """
    Scenario: Confirm users as compromised.
    Given:
     - A list of users (UPNs).
    When:
     - risky_users_confirm_compromise_command is called.
    Then:
     - Ensure client.upn_to_user_id is called for each user.
     - Ensure client.confirm_compromised_request is called with the correct user IDs.
     - Ensure the readable output is as expected.
    """
    from AzureRiskyUsers import risky_users_confirm

    mock_client_instance = mock_client()
    mocker.patch.object(mock_client_instance, "upn_to_user_id", side_effect=lambda x: f"id_{x}")
    mocker.patch.object(mock_client_instance, "confirm_compromised_request")

    users_to_compromise = ["user1@example.com", "user2@example.com"]
    args = {"user": users_to_compromise}

    result = risky_users_confirm(mock_client_instance, args, mock_client_instance.confirm_compromised_request, "compromised")

    mock_client_instance.upn_to_user_id.assert_has_calls([mocker.call("user1@example.com"), mocker.call("user2@example.com")])
    mock_client_instance.confirm_compromised_request.assert_has_calls(
        [mocker.call(user_ids=["id_user1@example.com"]), mocker.call(user_ids=["id_user2@example.com"])]
    )
    assert result[0].outputs[0]["Success"] is True
    assert result[0].outputs[1]["Success"] is True


def test_risky_users_confirm_safe_command(mocker):
    """
    Scenario: Confirm users as safe.
    Given:
     - A list of users (UPNs).
    When:
     - risky_users_confirm_safe_command is called.
    Then:
     - Ensure client.upn_to_user_id is called for each user.
     - Ensure client.confirm_safe_request is called with the correct user IDs.
     - Ensure the readable output is as expected.
    """
    from AzureRiskyUsers import risky_users_confirm

    mock_client_instance = mock_client()
    mocker.patch.object(mock_client_instance, "upn_to_user_id", side_effect=lambda x: f"id_{x}")
    mocker.patch.object(mock_client_instance, "confirm_safe_request")

    users_to_safe = ["user3@example.com", "user4@example.com"]
    args = {"user": users_to_safe}

    result = risky_users_confirm(mock_client_instance, args, mock_client_instance.confirm_safe_request, "safe")

    mock_client_instance.upn_to_user_id.assert_has_calls([mocker.call("user3@example.com"), mocker.call("user4@example.com")])
    mock_client_instance.confirm_safe_request.assert_has_calls(
        [mocker.call(user_ids=["id_user3@example.com"]), mocker.call(user_ids=["id_user4@example.com"])]
    )
    assert result[0].outputs[0]["Success"] is True
    assert result[0].outputs[1]["Success"] is True