SaasSecurity
SaaS Security API is a cloud-based service that you can connect directly to your sanctioned SaaS applications using the cloud app’s API to provide data classification, sharing and permission visibility, and threat detection. This Content Pack provides insights into risks posed by data exposure and policy violations and enables you to use Cortex XSOAR to effectively manage the incidents discovered by SaaS Security API.
Network Security · SaaS Security by Palo Alto Networks
Details
| ID | SaasSecurity |
|---|---|
| Provider | Palo Alto Networks |
| Category | Network Security |
| From Version | 6.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM EDR Cloud Runtime Security |
README
Use the SaaS Security integration to protect against cloud‑based threats by:
- Scanning and analyzing all your assets.
- Applying Security policy to identify exposures, external collaborators, risky user behavior, and sensitive documents.
- Identifying the potential risks associated with each asset.
Configure SaaS Security on Cortex XSOAR
- Navigate to Settings > Integrations > Servers & Services.
- Search for SaaS Security.
-
Click Add instance to create and configure a new integration instance.
Parameter Description Required Server URL The instance configuration URL based on the server location: https://api.aperture.paloaltonetworks.com (US)
https://api.aperture-eu.paloaltonetworks.com (EU)
https://api.aperture-apac.paloaltonetworks.com (APAC)True Client ID The SaaS Security Client ID. See instructions below. True Client Secret The SaaS Security Client Secret. See instructions below. True Fetch incidents If selected, fetches incidents from SaaS Security. False Incidents Fetch Interval Frequency (in hours and minutes) by which Cortex XSOAR fetches incidents from SaaS Security when Fetch Incidents is selected. False Incident type Incident type is set by this field if a classifier does not exist. If a classifier is selected, it takes precedence. False Incident Mirroring Direction Selects which direction you want the incidents mirrored. You can mirror Incoming only (from SaaS Security to Cortex XSOAR), Outgoing only (from Cortex XSOAR to SaaS Security), or both Incoming And Outgoing. False Number of incidents per fetch Important: The limit value can range from 10 to 200 and must be in multiples of 10. True First fetch timestamp (<number> <time unit>. For example, 12 hours, 7 days) False Fetch only incidents with matching state Fetches only incidents with matching All, Closed, or Open state. If nothing is selected, All states will be used. False Fetch only incidents with matching severity If nothing is selected, All severities will be used. False Fetch only incidents with matching status If nothing is selected, All statuses will be used. False Fetch only incidents with matching Application IDs A comma-separated list of Application IDs. Run the saas-security-get-apps command to return the Application ID, Name, and Type for all applications. False Close Mirrored XSOAR Incident If selected, when the incident closes on SaaS Security, the incident closes in Cortex XSOAR. False Trust any certificate (not secure) By default, SSL verification is enabled. If selected, the connection isn’t secure and all requests return an SSL error because the certificate cannot be verified. False Use system proxy settings Uses the system proxy server to communicate with the integration. If not selected, the integration will not use the system proxy server. False - Click Test to validate the URLs, token, and connection.
Configure SaaS Security Incident Mirroring
You can enable incident mirroring between Cortex XSOAR incidents and SaaS Security notables (available from Cortex XSOAR version 6.0.0).
To set up mirroring.
To configure mirroring:
- Navigate to Settings > Integrations > Servers & Services.
- Search for SaaS Security and select your integration instance.
- Enable Fetches incidents.
- In the Incident Mirroring Direction integration parameter, select which direction you want the incidents to be mirrored:
- Incoming — Any changes in the following SaaS Security incidents fields (state, category, status, assigned_to, resolved_by, asset_sha256) will be reflected in Cortex XSOAR incidents.
- Outgoing — Any changes in the following Cortex XSOAR incidents fields (state, category) will be reflected in SaaS Security incidents.
- Incoming And Outgoing (Recommended) — Changes in Cortex XSOAR incidents and SaaS Security incidents will be reflected in both directions.
- None — Turns off incident mirroring.
- (Recommended) Select the Close Mirrored XSOAR Incident integration parameter to close the Cortex XSOAR incident when the corresponding incident is closed on SaaS Security.
- There is no closing parameter for the opposite direction (to close incidents in SaaS Security when they are closed in XSOAR). Close Mirrored XSOAR Incident is the only use case available for mirrored out, when the state and category are updated.
Newly fetched incidents will be mirrored in the direction you select. However, this selection does not affect existing incidents.
- There is no closing parameter for the opposite direction (to close incidents in SaaS Security when they are closed in XSOAR). Close Mirrored XSOAR Incident is the only use case available for mirrored out, when the state and category are updated.
Important Notes
- For mirroring to work, the Incident Mirroring Direction parameter needs to be set before the incident is fetched.
- To ensure mirroring works as expected, mappers are required for both Incoming and Outgoing to map the expected fields in Cortex XSOAR and SaaS Security.
- The only fields that can be mirrored in from SaaS Security to Cortex XSOAR are:
- state
- category
- status
- assigned_to
- resolved_by
- asset_sha256
- The only fields that can be mirrored out from XSOAR to SaaS Security are:
- state
- category The supported categories for closing incidents are: “misidentified”, “no_reason”, and “business_justified”.
Note: Mirroring out works only for closed incidents due to an API limitation.
Create the Client ID and Client Secret on SaaS Security
In the SaaS Security UI, do the following:
- Navigate to Settings > External Service.
- Click Add API Client.
- Specify a unique name for the API client.
- Authorize the API client for the required scopes. You use these scopes in the POST request to the /oauth/token endpoint. The Required Scopes are:
- Log access — Access log files. You can either provide the client log access API or add a syslog receiver.
- Incident management — Retrieve and change the incident status.
- Quarantine management — Quarantine assets and restore quarantined assets.
- Copy the client ID and client secret.
Tip: Record your API client secret somewhere safe. For security purposes, it’s only shown when you create or reset the API client. If you lose your secret you must reset it, which removes access for any integrations that still use the previous secret. - Add the Client ID and Client Secret to Cortex XSOAR.
Note: For more information see the SaaS Security Administrator’s Guide
Commands
You can execute these commands from the Cortex XSOAR 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.
saas-security-incidents-get
Retrieves incidents from the SaaS Security platform.
Base Command
saas-security-incidents-get
Input
| Argument Name | Description | Required |
|---|---|---|
| limit | The number of incidents to pull. Important: The limit value can range from 10 to 200 and must be in multiples of 10. Default is 50. | Optional |
| from | The start time of the query, filtered by the date the incident was updated,\ \ For example, 2021-08-23T09:26:25.872Z. |
Optional |
| to | The end time of the query, filtered by the date the incident was updated. For example, 2021-08-23T09:26:25.872Z. |
Optional |
| app_ids | Comma-separated list of application IDs. Run the ‘saas-security-get-apps’ command to return the Application ID, Name, and Type for all applications. | Optional |
| state | The state of the incidents. If empty, retrieves all states. Possible values: “All”, “Open”, and “Closed”. Possible values are: All, Open, Closed. Default is open. | Optional |
| severity | The severity of the incidents. If none is selected, all severities will be pulled. Possible values: “1.0”, “2.0”, “3.0”, “4.0”, and “5.0”. | Optional |
| status | The status of the incidents. Possible values: “New”, “Assigned”, “In Progress”, “Pending”, “No Reason”, “Business Justified”, “Misidentified”, “In The Cloud”, and “Dismiss”. Possible values are: New, Assigned, In Progress, Pending, No Reason, Business Justified, Misidentified, In The Cloud, Dismiss. | Optional |
| next_page | Get the next batch of incidents. No other argument is needed when providing this. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| SaasSecurity.Incident.incident_id | Number | The incident ID. |
| SaasSecurity.Incident.tenant | String | The tenant associated with the incident. |
| SaasSecurity.Incident.app_id | String | The application ID. |
| SaasSecurity.Incident.app_name | String | The application name. |
| SaasSecurity.Incident.app_type | String | The application type. |
| SaasSecurity.Incident.cloud_id | String | The cloud ID. |
| SaasSecurity.Incident.asset_name | String | The asset name. |
| SaasSecurity.Incident.asset_sha256 | String | The SHA256 hash value of the asset. |
| SaasSecurity.Incident.asset_id | String | The asset ID. |
| SaasSecurity.Incident.asset_page_uri | String | The asset page URI. |
| SaasSecurity.Incident.asset_cloud_uri | String | The asset cloud URI. |
| SaasSecurity.Incident.exposure_type | Number | The exposure type (Internal/External). |
| SaasSecurity.Incident.exposure_level | String | The exposure level. |
| SaasSecurity.Incident.policy_id | String | The policy ID. |
| SaasSecurity.Incident.policy_name | String | The policy name. |
| SaasSecurity.Incident.policy_version | Number | The policy version. |
| SaasSecurity.Incident.policy_page_uri | String | The policy page URI. |
| SaasSecurity.Incident.severity | String | The severity of the incident. |
| SaasSecurity.Incident.status | String | The incident status. |
| SaasSecurity.Incident.state | String | The incident state. |
| SaasSecurity.Incident.category | String | The incident category. |
| SaasSecurity.Incident.resolved_by | String | The name of the user who resolved the incident. |
| SaasSecurity.Incident.resolution_date | Date | The date the incident was resolved. |
| SaasSecurity.Incident.created_at | Date | The date the incident was created, e.g., `2021-08-23T09:26:25.872Z`. |
| SaasSecurity.Incident.updated_at | Date | The Date the incident was last updated. e.g., `2021-08-24T09:26:25.872Z`. |
| SaasSecurity.Incident.asset_owner_id | String | The ID of the asset owner. |
| SaasSecurity.Incident.asset_owner_name | String | The name of the asset owner. |
| SaasSecurity.Incident.asset_owner_email | String | The email address of the asset owner. |
| SaasSecurity.NextResultsPage | String | The URI for the next batch of incidents. |
Command Example
!saas-security-incidents-get limit=11 app_ids=acf49b2389c09f26ad0ccd2b1a603328 from=2021-08-23T20:25:17.495Z state=open
Context Example
{
"SaasSecurity": {
"Incident": [
{
"app_id": "acf49b2389c09f26ad0ccd2b1a603328",
"app_name": "Box 1",
"app_type": "box",
"asset_cloud_uri": "https://www.box.com/files/0/f/114948778953/1/f_675197457403",
"asset_id": "61099dc26b544e38fa3ce06d",
"asset_name": "SP0605 copy 6.java",
"asset_owner_email": "xsoartest@cirrotester.com",
"asset_owner_id": "22FD054D362DC548A9C22F25782E1DAEED03C12F3898CD0F2E2A1B4CF728D04BD644B3CC010FDAC3D10EC0D408F4F79AC147E3D56415D1052BCFCD899A8E249F",
"asset_owner_name": "Xsoar test",
"asset_page_uri": "https://xsoartest.staging.cirrotester.com/cloud_assets/61099dc26b544e38fa3ce06d",
"asset_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"category": "business_justified",
"cloud_id": "675197457403",
"collaborators": [],
"created_at": "2021-08-03T20:25:15.417Z",
"data_patterns": [],
"exposure_level": "internal",
"exposure_type": 8,
"group_ids": [],
"incident_id": 4,
"policy_id": "6109a5d0e64152534b240f48",
"policy_page_uri": "https://xsoartest.staging.cirrotester.com/data_policies/6109a5d0e64152534b240f48",
"policy_version": 1,
"policy_name": "policy name",
"resolution_date": "2021-08-24T07:44:21.608Z",
"resolved_by": "api",
"severity": "Low",
"state": "closed",
"status": "Closed-Business Justified",
"tenant": "xsoartest",
"updated_at": "2021-08-24T07:44:21.608Z"
},
{
"app_id": "acf49b2389c09f26ad0ccd2b1a603328",
"app_name": "Box 1",
"app_type": "box",
"asset_cloud_uri": "https://www.box.com/files/0/f/114948778953/1/f_675197556380",
"asset_id": "61099dbe6b544e38fa3cc9b8",
"asset_name": "SP0605 copy 2.java",
"asset_owner_email": "xsoartest@cirrotester.com",
"asset_owner_id": "22FD054D362DC548A9C22F25782E1DAEED03C12F3898CD0F2E2A1B4CF728D04BD644B3CC010FDAC3D10EC0D408F4F79AC147E3D56415D1052BCFCD899A8E249F",
"asset_owner_name": "Xsoar test",
"asset_page_uri": "https://xsoartest.staging.cirrotester.com/cloud_assets/61099dbe6b544e38fa3cc9b8",
"asset_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"category": "business_justified",
"cloud_id": "675197556380",
"collaborators": [],
"created_at": "2021-08-03T20:25:12.000Z",
"data_patterns": [],
"exposure_level": "internal",
"exposure_type": 8,
"group_ids": [],
"incident_id": 1,
"policy_id": "6109a5d0e64152534b240f48",
"policy_page_uri": "https://xsoartest.staging.cirrotester.com/data_policies/6109a5d0e64152534b240f48",
"policy_version": 1,
"resolution_date": "2021-08-24T08:19:57.429Z",
"resolved_by": "api",
"severity": "Low",
"status": "Closed-Business Justified",
"tenant": "xsoartest",
"updated_at": "2021-08-24T08:19:57.429Z"
}
]
}
}
Human Readable Output
Incidents
Incident Id App Id App Name Asset Name Exposure Level Severity Category Created At Updated At 4 acf49b2389c09f26ad0ccd2b1a603328 Box 1 SP0605 copy 6.java internal Low business_justified 2021-08-03T20:25:15.417Z 2021-08-24T07:44:21.608Z 1 acf49b2389c09f26ad0ccd2b1a603328 Box 1 SP0605 copy 2.java internal Low business_justified 2021-08-03T20:25:12.000Z 2021-08-24T08:19:57.429Z 5 acf49b2389c09f26ad0ccd2b1a603328 Box 1 SP0605 copy 7.java internal Low aperture 2021-08-03T20:25:16.842Z 2021-08-24T17:08:51.022Z 8 acf49b2389c09f26ad0ccd2b1a603328 Box 1 ml_file.java internal Low aperture 2021-08-03T20:25:17.043Z 2021-08-24T17:10:37.433Z 3 acf49b2389c09f26ad0ccd2b1a603328 Box 1 SP0605 copy 5.java internal Low misidentified 2021-08-03T20:25:13.770Z 2021-08-25T14:29:42.288Z
saas-security-incident-get-by-id
Gets an incident by its ID.
Base Command
saas-security-incident-get-by-id
Input
| Argument Name | Description | Required |
|---|---|---|
| id | The incident ID. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| SaasSecurity.Incident.incident_id | Number | The Incident ID. |
| SaasSecurity.Incident.tenant | String | The tenant associated with the incident. |
| SaasSecurity.Incident.app_id | String | The application ID. |
| SaasSecurity.Incident.app_name | String | The application name. |
| SaasSecurity.Incident.app_type | String | The application type. |
| SaasSecurity.Incident.cloud_id | String | The cloud ID. |
| SaasSecurity.Incident.asset_name | String | The asset name. |
| SaasSecurity.Incident.asset_sha256 | String | The SHA256 hash value of the asset. |
| SaasSecurity.Incident.asset_id | String | The asset ID. |
| SaasSecurity.Incident.asset_page_uri | String | The asset page URI. |
| SaasSecurity.Incident.asset_cloud_uri | String | The asset cloud URI. |
| SaasSecurity.Incident.exposure_type | Number | The exposure type (Internal/External). |
| SaasSecurity.Incident.exposure_level | String | The exposure level. |
| SaasSecurity.Incident.policy_id | String | The policy ID. |
| SaasSecurity.Incident.policy_name | String | The policy name. |
| SaasSecurity.Incident.policy_version | Number | The policy version. |
| SaasSecurity.Incident.policy_page_uri | String | The policy page URI. |
| SaasSecurity.Incident.severity | String | The severity of the incident. |
| SaasSecurity.Incident.status | String | The incident status. |
| SaasSecurity.Incident.state | String | The incident state. |
| SaasSecurity.Incident.category | String | The incident category. |
| SaasSecurity.Incident.resolved_by | String | The name of the user who resolved the incident. |
| SaasSecurity.Incident.resolution_date | Date | The date the incident was resolved. |
| SaasSecurity.Incident.created_at | Date | The date the incident was created, e.g., `2021-08-23T09:26:25.872Z`. |
| SaasSecurity.Incident.updated_at | Date | The date the incident was last updated, e.g., `2021-08-24T09:26:25.872Z`. |
| SaasSecurity.Incident.asset_owner_id | String | The ID of the asset owner. |
| SaasSecurity.Incident.asset_owner_name | String | The name of the asset owner. |
| SaasSecurity.Incident.asset_owner_email | String | The email address of the asset owner. |
Command Example
!saas-security-incident-get-by-id id=4
Context Example
{
"SaasSecurity": {
"Incident": {
"app_id": "acf49b2389c09f26ad0ccd2b1a603328",
"app_name": "Box 1",
"app_type": "box",
"asset_cloud_uri": "https://www.box.com/files/0/f/114948778953/1/f_675197457403",
"asset_id": "61099dc26b544e38fa3ce06d",
"asset_name": "SP0605 copy 6.java",
"asset_owner_email": "xsoartest@cirrotester.com",
"asset_owner_id": "22FD054D362DC548A9C22F25782E1DAEED03C12F3898CD0F2E2A1B4CF728D04BD644B3CC010FDAC3D10EC0D408F4F79AC147E3D56415D1052BCFCD899A8E249F",
"asset_owner_name": "Xsoar test",
"asset_page_uri": "https://xsoartest.staging.cirrotester.com/cloud_assets/61099dc26b544e38fa3ce06d",
"asset_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"category": "business_justified",
"cloud_id": "675197457403",
"collaborators": [],
"created_at": "2021-08-03T20:25:15.417Z",
"data_patterns": [],
"exposure_level": "internal",
"exposure_type": 8,
"group_ids": [],
"incident_id": 4,
"policy_id": "6109a5d0e64152534b240f48",
"policy_page_uri": "https://xsoartest.staging.cirrotester.com/data_policies/6109a5d0e64152534b240f48",
"policy_version": 1,
"resolution_date": "2021-08-26T07:04:14.598Z",
"resolved_by": "api",
"severity": "Low",
"state": "closed",
"tenant": "xsoartest",
"updated_at": "2021-08-26T07:04:14.598Z"
}
}
}
Human Readable Output
Incident 4 details
Incident Id App Id App Name Asset Name Exposure Level Severity State Category Created At Updated At 4 acf49b2389c09f26ad0ccd2b1a603328 Box 1 SP0605 copy 6.java internal 1.0 closed business_justified 2021-08-03T20:25:15.417Z 2021-08-26T07:04:14.598Z
saas-security-incident-state-update
Closes an incident and updates its category.
Base Command
saas-security-incident-state-update
Input
| Argument Name | Description | Required |
|---|---|---|
| id | The incident ID. | Required |
| category | The reason for closing the incident. Possible values: “Misidentified”, “No Reason”, and “Business Justified”. Possible values are: Misidentified, No Reason, Business Justified. Default is Reason for state update.. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| SaasSecurity.IncidentState.incident_id | String | The incident ID. |
| SaasSecurity.IncidentState.state | String | The incident state (open/closed). |
| SaasSecurity.IncidentState.category | String | The incident category. |
| SaasSecurity.IncidentState.resolved_by | String | The name of the user who resolved the incident. |
| SaasSecurity.IncidentState.resolution_date | Date | The date when the incident was resolved. |
Command Example
!saas-security-incident-state-update category="Business Justified" id=4
Context Example
{
"SaasSecurity": {
"IncidentState": {
"category": "business_justified",
"incident_id": "4",
"resolution_date": "2021-08-26T07:04:14.598Z",
"resolved_by": "api",
"state": "closed"
}
}
}
Human Readable Output
Incident 4 status details
Category Incident Id Resolution Date Resolved By State business_justified 4 2021-08-26T07:04:14.598Z api closed
saas-security-get-apps
Returns the Application ID, Name, and Type for all applications.
Base Command
saas-security-get-apps
Input
No inputs.
Context Output
| Path | Type | Description |
|---|---|---|
| SaasSecurity.App.app_name | String | The application name. |
| SaasSecurity.App.app_id | String | The application ID. |
| SaasSecurity.App.app_type | String | The application type. |
Command Example
#### Context Example
```json
{
"SaasSecurity": {
"App": [
{
"app_id": "acf49b2389c09f26ad0ccd2b1a603328",
"app_name": "Box 1",
"app_type": "box"
},
{
"app_id": "2642aaa03dc6fc44496bdfffe5e1bc74",
"app_name": "Office 365 1",
"app_type": "office365"
}
]
}
}
Human Readable Output
Apps Info
App Id App Name App Type acf49b2389c09f26ad0ccd2b1a603328 Box 1 box 2642aaa03dc6fc44496bdfffe5e1bc74 Office 365 1 office365
saas-security-asset-remediate
Remediates an asset.
Base Command
saas-security-asset-remediate
Input
| Argument Name | Description | Required |
|---|---|---|
| asset_id | The ID of the asset to remediate. | Required |
| remediation_type | The remediation action to take. Possible values: “Remove public sharing”(only for Office365, Dropbox, Box, Google Drive apps), “Quarantine”, and “Restore”. Possible values are: Remove public sharing, Quarantine, Restore. | Required |
| remove_inherited_sharing | Used when the remediation type is “Remove public sharing”. When set to true, all the parent folders with a shared URL will be removed. Possible values are: true, false. Default is false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| SaasSecurity.Remediation.asset_id | String | The asset ID. |
| SaasSecurity.Remediation.remediation_type | String | The remediation type. |
| SaasSecurity.Remediation.status | String | The remediation action status. |
Command Example
!saas-security-asset-remediate asset_id=61099dc46b544e38fa3ce89a remediation_type=Quarantine
Context Example
{
"SaasSecurity": {
"Remediation": {
"asset_id": "61099dc46b544e38fa3ce89a",
"remediation_type": "system_quarantine",
"status": "pending"
}
}
}
Human Readable Output
Remediation details for asset: 61099dc46b544e38fa3ce89a
Asset Id Remediation Type Status 61099dc46b544e38fa3ce89a system_quarantine pending
saas-security-remediation-status-get
Gets the remediation status for a given asset ID.
Base Command
saas-security-remediation-status-get
Input
| Argument Name | Description | Required |
|---|---|---|
| asset_id | The asset ID. | Required |
| remediation_type | The remediation action that was taken. Possible values: “Remove public sharing”(only for Office365, Dropbox, Box, Google Drive apps), “Quarantine”, and “Restore”. Possible values are: Remove public sharing, Quarantine, Restore. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| SaasSecurity.Remediation.asset_id | String | The asset ID. |
| SaasSecurity.Remediation.asset_name | String | The asset name. |
| SaasSecurity.Remediation.remediation_type | String | The remediation type. |
| SaasSecurity.Remediation.action_taker | String | The source of the remediation action. For example, ‘api’. |
| SaasSecurity.Remediation.action_date | Date | The date when the remediation action was taken. |
| SaasSecurity.Remediation.status | String | The remediation action status. |
Command Example
!saas-security-remediation-status-get asset_id=61099dc46b544e38fa3ce89a remediation_type=Quarantine
Context Example
{
"SaasSecurity": {
"Remediation": {
"action_date": "2021-08-25T21:18:37.148+0000",
"action_taker": "api",
"asset_id": "61099dc46b544e38fa3ce89a",
"asset_name": "SP0605 copy.java",
"remediation_type": "system_quarantine",
"status": "success"
}
}
}
Human Readable Output
Asset 61099dc46b544e38fa3ce89a remediation details
Action Date Action Taker Asset Id Asset Name Remediation Type Status 2021-08-25T21:18:37.148+0000 api 61099dc46b544e38fa3ce89a SP0605 copy.java system_quarantine success
Configuration parameters
url— Server URL (required)credentials— Client ID (required)isFetch— Fetch incidentsincidentFetchInterval— Incidents Fetch IntervalincidentType— Incident typemirror_direction— Incident Mirroring Directionmax_fetch— Number of incidents per fetch. (required)first_fetch— First fetch timestamp (<number> <time unit>. For example, 12 hours, 7 days)state— Fetch only incidents with matching stateseverity— Fetch only incidents with matching severitystatus— Fetch only incidents with matching statusapp_ids— Fetch only incidents with matching Application IDsclose_incident— Close Mirrored XSOAR Incidentinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (10)
-
get-mapping-fieldsReturns the list of fields for an incident type.
-
get-modified-remote-dataGet the list of incidents that were modified since the last update. Note that this method is used for debugging purposes. get-modified-remote-data is used as part of a Mirroring feature, which is available since Cortex XSOAR version 6.1.
-
get-remote-dataGet remote data from a remote incident. Note that this method will not update the current incident. It's used for debugging purposes.
-
saas-security-asset-remediateRemediates an asset.
-
saas-security-get-appsReturns the Application ID, Name, and Type for all applications.
-
saas-security-incident-get-by-idGets an incident by its ID.
-
saas-security-incident-state-updateCloses an incident and updates its category.
-
saas-security-incidents-getRetrieves incidents from the SaaS Security platform.
-
saas-security-remediation-status-getGets the remediation status for a given asset ID.
-
update-remote-systemUpdates local incident changes in the remote incident. This method is only used for debugging purposes and will not update the current incident.
import json from datetime import datetime from requests import Response import demistomock as demisto import pytest from CommonServerPython import DemistoException, EntryFormat, EntryType, BaseClient from freezegun import freeze_time from SaasSecurity import LIMIT_DEFAULT, LIMIT_MAX, LIMIT_MIN, Client, validate_limit def util_load_json(path): with open(path, encoding="utf-8") as f: return json.loads(f.read()) @pytest.fixture() def client(mocker): client = Client(base_url="http://base_url", verify=False, client_id="client_id", client_secret="client_secret", proxy=False) mocker.patch.object(Client, "get_access_token", return_value="access_token") return client @pytest.fixture() def demisto_mocker(mocker): mocker.patch.object( demisto, "params", return_value={ "url": "http://base_url/", "credentials": {"identifier": "client_id", "password": "client_secret"}, "first_fetch": "3 days", "max_fetch": "50", "state": "Open", "severity": "High,Low", "status": "Assigned", "mirror_direction": "Incoming And Outgoing", }, ) mocker.patch.object(demisto, "getLastRun", return_value={"last_run_time": "2021-08-25T13:51:03.247Z"}) mocker.patch.object(demisto, "incidents") mocker.patch.object(demisto, "results") @freeze_time("2021-08-24 18:04:00") def test_get_passed_mins(): """ Tests get_passed_mins helper function. Using @freeze_time decorator in order to make the datetime.now() method a permanent value. """ from SaasSecurity import get_passed_mins start_time = datetime.now() end_time_str = start_time.replace(hour=16).timestamp() expected_time_delta = 120 result = get_passed_mins(start_time, end_time_str) assert expected_time_delta == result @pytest.mark.parametrize( "integration_context, expected_token, force_generate", [ ({"access_token": "valid_access_token", "time_issued": 1629827440.0}, "valid_access_token", False), ({"access_token": "expired_access_token", "time_issued": 1629901436.0}, "new_access_token", False), ({}, "new_access_token", False), ({"access_token": "valid_access_token", "time_issued": 1629827440.0}, "new_access_token", True), ], ) @freeze_time("2020-08-24 18:04:21.446809") def test_get_access_token(mocker, integration_context, expected_token, force_generate): """ Configures mocker instance and patches the client's _http_request to generate access token. Use-cases: 1. There is a valid access token in the integration context. 2. The access token saved in the integration context is no longer valid. 3. There is no access token in the integration context. 4. There is a valid access token in the integration context, but the argument force_generate was given. """ client = Client(base_url="http://base_url", verify=False, client_id="client_id", client_secret="client_secret", proxy=False) mocker.patch.object(demisto, "getIntegrationContext", return_value=integration_context) mocker.patch.object(client, "_http_request", return_value={"access_token": "new_access_token"}) access_token = client.get_access_token(force_generate) assert access_token == expected_token def test_convert_to_xsoar_incident(): """ Given: - A full incident from the Saas Security platform When: - Fetching incidents Then: - Returns xsoar incident """ from SaasSecurity import convert_to_xsoar_incident incident = util_load_json("test_data/get-incident-by-id.json") expected = {"name": "Saas Security: SP0605 copy 6.java", "occurred": "2021-08-03T20:25:15Z", "rawJSON": json.dumps(incident)} xsoar_incident = convert_to_xsoar_incident(incident) assert xsoar_incident == expected def test_convert_to_xsoar_incident_without_occurred(): """ Given: - An incident without the created_at field from the Saas Security platform When: - Fetching incidents Then: - Returns xsoar incident """ from SaasSecurity import convert_to_xsoar_incident incident = util_load_json("test_data/get-incident-by-id.json") incident["created_at"] = None expected = {"name": "Saas Security: SP0605 copy 6.java", "occurred": None, "rawJSON": json.dumps(incident)} xsoar_incident = convert_to_xsoar_incident(incident) assert xsoar_incident == expected @pytest.mark.parametrize("last_run", ("2018-10-01T20:22:35.000Z", None)) @freeze_time("2021-08-24 18:04:00") def test_fetch_incidents(mocker, client, requests_mock, demisto_mocker, last_run): """ Configures mocker instance, requests_mock, uses the demisto_mocker fixture. Given: - Fetch params When: - Fetching incidents Then: - Returns list of xsoar incidents and filter by created time """ from SaasSecurity import main get_incidents = util_load_json("test_data/get-incidents.json") incidents_for_fetch = util_load_json("test_data/fech_incident_data.json") mocker.patch.object(demisto, "command", return_value="fetch-incidents") mocker.patch.object(demisto, "getLastRun", return_value={"last_run_time": last_run}) requests_mock.get("http://base_url/incident/api/incidents/delta", json=get_incidents) main() assert demisto.incidents.call_count == 1 incidents = demisto.incidents.call_args[0][0] if last_run: assert len(incidents) == 8 assert incidents[0]["occurred"] == "2021-08-03T20:25:13Z" assert incidents[1]["occurred"] == "2021-08-03T20:25:15Z" assert incidents_for_fetch == incidents else: assert not incidents def test_fetch_incidents_input(mocker, client): """ Test the inputs to the fetch incidents flow, and verify that they are of the correct format. Given: - Fetch params When: - Fetching incidents Then: - The input params are being sent in the correct format. """ from SaasSecurity import fetch_incidents first_fetch_time = "1 day" fetch_limit = "1" fetch_severity = ["4.0", "5.0"] last_run = "2018-10-01T20:22:35.000Z" get_incidents = util_load_json("test_data/get-incidents.json") expected_params = {"from": "2018-10-01T20:22:35.000Z", "limit": "1", "severities": "4.0,5.0"} mocker.patch.object(demisto, "getLastRun", return_value={"last_run_time": last_run}) http_request = mocker.patch.object(client, "http_request", return_value=get_incidents) fetch_incidents(client, first_fetch_time, fetch_limit, "", fetch_severity, "", "") http_request.assert_called_with("GET", url_suffix="/incident/api/incidents/delta", params=expected_params) def test_get_incidents_input(mocker, client): """ Test the inputs to the get_incidents_command, and verify that they are being sent in the correct format. Given: - The command arguments. When: - Running the command saas-security-incidents-get. Then: - The input params are being sent in the correct format. """ from SaasSecurity import get_incidents_command limit = 1 severity_old_format = ["4", "5"] get_incidents = util_load_json("test_data/get-incidents.json") args = {"limit": limit, "severity": severity_old_format} expected_params = {"limit": 10, "severities": "4.0,5.0", "state": "open"} http_request = mocker.patch.object(client, "http_request", return_value=get_incidents) get_incidents_command(client, args) http_request.assert_called_with("GET", url_suffix="/incident/api/incidents/delta", params=expected_params) def test_get_incident_command_401(mocker, client): """Check that in a case of 401 error, a second call to get a forced access token is made. Given: - The command arguments. When: - Running the command saas-security-incidents-get. Then: - The function for generating an access token is called once. """ from SaasSecurity import get_incidents_command class MockException: def __init__(self, status_code) -> None: self.status_code = status_code limit = 1 severity_old_format = ["4", "5"] get_incidents = util_load_json("test_data/get-incidents.json") args = {"limit": limit, "severity": severity_old_format} res_401 = Response() res_401.status_code = 401 res_401.reason = "Unauthorized" http_request = mocker.patch.object( BaseClient, "_http_request", side_effect=[DemistoException("Error in API call [401] - Unauthorized", res=MockException(401)), get_incidents], ) get_incidents_command(client, args) assert http_request.call_count == 2 def test_get_incidents_command(client, requests_mock): """ Using the client mocker, requests_mock. Given: - Command arguments. When: - Getting incidents with state and status set to 'All Then: - Sends request with the expected query params and created a CommandResult object. """ from SaasSecurity import get_incidents_command incidents = util_load_json("test_data/get-incidents.json") req_mocker = requests_mock.get("http://base_url/incident/api/incidents/delta", json=incidents) result = get_incidents_command(client, {"limit": "5", "state": "All", "severity": "1.0,4.0", "status": "All"}) assert all(param not in req_mocker.last_request.query for param in ("status", "state")) assert len(result.outputs.get("SaasSecurity.Incident(val.incident_id && val.incident_id == obj.incident_id)")) == 8 def test_get_incident_by_id_command(client, requests_mock): """ Using the client mocker, requests_mock. Given: - Incident Id. When: - Getting incidents with state and status set to 'All' Then: - Sends request with the expected query params and creates a CommandResult object. """ from SaasSecurity import get_incident_by_id_command incident = util_load_json("test_data/get-incident-by-id.json") requests_mock.get("http://base_url/incident/api/incidents/4", json=incident) res = get_incident_by_id_command(client, {"id": "4"}) assert res.outputs == incident assert res.outputs_prefix == "SaasSecurity.Incident" def test_update_incident_state_command(client, requests_mock): """ Using the client mocker, requests_mock. Given: - Command arguments: Incident ID and category. When: - Updating an incident status. Then: - Sends request with the expected body and creates a CommandResult object. """ from SaasSecurity import update_incident_state_command updated_status = util_load_json("test_data/update-incident-status.json") req_mocker = requests_mock.post("http://base_url/incident/api/incidents/4/state", json=updated_status) result = update_incident_state_command(client, {"id": "4", "category": "Business Justified"}) assert all(param in req_mocker.last_request.text for param in ("business_justified", "state")) assert result.outputs.get("incident_id") == "4" def test_remediate_asset_command(client, requests_mock): """ Using the client mocker, requests_mock. Given: - Command arguments: Asset ID and remediation action. When: - Remediating an asset. Then: - Sends request with the expected query params and creates a CommandResult object. """ from SaasSecurity import remediate_asset_command req_mocker = requests_mock.post("http://base_url/remediation/api/assets") result = remediate_asset_command(client, {"asset_id": "4", "remediation_type": "Remove public sharing"}) assert "remove_public_sharing" in req_mocker.last_request.text assert result.outputs.get("asset_id") == "4" assert result.outputs.get("remediation_type") == "remove_public_sharing" assert result.outputs.get("status") == "pending" @pytest.mark.xfail( raises=DemistoException, reason="Invalid remediation type: Invalid remediation.\n" "Must be one of the following: Remove public sharing, Quarantine, Restore", ) def test_remediate_asset_command__invalid_action(client, requests_mock): """ Configures mocker instance, requests_mock. Given: - Command arguments: Asset ID and invalid remediation action. When: - Remediating an asset. Then: - Raises a DemistoException. """ from SaasSecurity import remediate_asset_command requests_mock.post("http://base_url/remediation/api/assets") remediate_asset_command(client, {"asset_id": "4", "remediation_type": "Invalid remediation"}) def test_get_remediation_status_command(client, requests_mock): """ Using the client mocker, requests_mock. Given: - Command arguments: Asset ID and remediation action. When: - Remediating an asset. Then: - Sends request with the expected query params and creates a CommandResult object. """ from SaasSecurity import get_remediation_status_command remediation_status = util_load_json("test_data/get-asset-remediation-status.json") req_mocker = requests_mock.get("http://base_url/remediation/api/assets", json=remediation_status) result = get_remediation_status_command(client, {"asset_id": "61099dd36b544e38fa3d22b9", "remediation_type": "Quarantine"}) assert "system_quarantine" in req_mocker.last_request.query assert remediation_status == result.outputs @pytest.mark.parametrize( "close_incident,expected_mirrored_object,expected_entries", [ ( False, { "category": "business_justified", "status": "Closed-Business Justified", "resolved_by": "api", "state": "closed", "asset_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", }, [], ), ( True, { "category": "business_justified", "status": "Closed-Business Justified", "resolved_by": "api", "state": "closed", "asset_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", }, [ { "Type": EntryType.NOTE, "ContentsFormat": EntryFormat.JSON, "Contents": {"dbotIncidentClose": True, "closeReason": "From SaasSecurity: business_justified"}, } ], ), ], ) def test_get_remote_data_command(client, requests_mock, mocker, close_incident, expected_mirrored_object, expected_entries): from SaasSecurity import get_remote_data_command args = {"id": 1, "lastUpdate": "2021-08-24T07:44:21.608Z"} incident = util_load_json("test_data/get-incident-by-id.json") requests_mock.get("http://base_url/incident/api/incidents/1", json=incident) mocker.patch.object(demisto, "params", return_value={"close_incident": close_incident}) result = get_remote_data_command(client, args) assert result.mirrored_object == expected_mirrored_object assert result.entries == expected_entries @pytest.mark.parametrize( "close_incident,expected_mirrored_object,expected_entries", [ ( True, { "category": "business_justified", "status": "Closed-Business Justified", "resolved_by": "api", "state": "Closed", "asset_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", }, [ { "Type": EntryType.NOTE, "ContentsFormat": EntryFormat.JSON, "Contents": {"dbotIncidentClose": True, "closeReason": "From SaasSecurity: business_justified"}, } ], ) ], ) def test_get_remote_data_closed_status_uppercase( client, requests_mock, mocker, close_incident, expected_mirrored_object, expected_entries ): from SaasSecurity import get_remote_data_command args = {"id": 1, "lastUpdate": "2021-08-24T07:44:21.608Z"} incident = util_load_json("test_data/get-incident-by-id.json") incident["state"] = "Closed" requests_mock.get("http://base_url/incident/api/incidents/1", json=incident) mocker.patch.object(demisto, "params", return_value={"close_incident": close_incident}) result = get_remote_data_command(client, args) assert result.mirrored_object == expected_mirrored_object assert result.entries == expected_entries def test_get_modified_remote_data_command(client, requests_mock): from SaasSecurity import get_modified_remote_data_command args = {"lastUpdate": "2020-11-18T13:16:52.005381+02:00"} incidents = util_load_json("test_data/get-incidents.json") requests_mock.get("http://base_url/incident/api/incidents/delta", json=incidents) result = get_modified_remote_data_command(client, args) assert result.modified_incident_ids == ["3", "4", "5", "6", "7", "8", "9", "10"] def test_get_mapping_fields_command(): from SaasSecurity import get_mapping_fields_command result = get_mapping_fields_command().extract_mapping() assert result == {"Saas Security Incident": {"state": "", "category": ""}} @pytest.mark.parametrize( "args,expected_debug_message", [ ( { "incidentChanged": True, "remoteId": "1", "data": {"state": "closed", "category": "No Reason"}, "status": 1, "delta": {"category": "No Reason"}, "entries": [], }, "Incident updated successfully. Result: {'state': 'closed', 'category': 'No Reason'}", ), ( { "incidentChanged": False, "remoteId": "2", "data": {"state": "closed", "category": "No Reason"}, "status": 1, "delta": {"category": "No Reason"}, "entries": [], }, "Skipping updating remote incident fields [2] as it is not new nor changed.", ), ( { "incidentChanged": True, "remoteId": "2", "data": {"state": "closed"}, "status": 1, "delta": {"category": "No Reason"}, "entries": [], }, "Skipping updating the remote incident since the incident is not closed. " "Could not update the category for open incident due to an API limitation.", ), ( { "incidentChanged": True, "remoteId": "2", "data": {"state": "open", "category": "No Reason"}, "status": 1, "delta": {"category": "No Reason"}, "entries": [], }, "Skipping updating the remote incident since the incident is not closed. " "Could not update the category for open incident due to an API limitation.", ), ( { "incidentChanged": True, "remoteId": "2", "data": {"state": "closed", "category": "Invalid Category"}, "status": 1, "delta": {"category": "No Reason"}, "entries": [], }, "The value of category Invalid Category is invalid. " "The category can be one of the following ['no_reason', 'business_justified', 'misidentified'].", ), ( { "incidentChanged": True, "remoteId": "2", "data": {"state": "closed", "category": "No Reason"}, "status": 1, "delta": {"category": "No Reason"}, "entries": [], }, "Incident updated successfully. Result: {'state': 'closed', 'category': 'No Reason'}", ), ( { "incidentChanged": True, "remoteId": "2", "data": {"state": "open", "category": "No Reason"}, "status": 2, "delta": {"category": "No Reason"}, "entries": [], }, "Incident updated successfully. Result: {'state': 'open', 'category': 'No Reason'}", ), ], ) def test_update_remote_system_command(requests_mock, mocker, client, args, expected_debug_message): from SaasSecurity import update_remote_system_command requests_mock.post(f'http://base_url/incident/api/incidents/{args.get("remoteId")}/state', json=args.get("data")) debug_result = mocker.patch.object(demisto, "debug") result = update_remote_system_command(client, args) assert result == args.get("remoteId") assert expected_debug_message in debug_result.call_args[0][0] def test_validate_limit(): """ Test the validate_limit function behavior under various input conditions. Given: Different input values for the limit parameter. When: The validate_limit function is called with these inputs. Then: The function should return the expected validated limit values. Test cases: 1. Negative limit: Should raise a DemistoException 2. Limit less than 10: Should return LIMIT_MIN (10) 3. Limit not divisible by 10: Should round down to nearest multiple of 10 4. Limit greater than MAX_LIMIT: Should return LIMIT_MAX (200) 5. No limit provided: Should return LIMIT_DEFAULT (50) 6. Valid limit within range: Should return the input value 7. Limits at boundaries: Should return LIMIT_MIN for 10 and LIMIT_MAX for 200 """ # Test with negative limit with pytest.raises(DemistoException, match="The limit parameter cannot be negative number or zero"): validate_limit(-1) # Test with limit less than 10 assert validate_limit(5) == LIMIT_MIN # Test with limit not dividable by 10 assert validate_limit(55) == 50 # Test with limit greater than MAX_LIMIT assert validate_limit(250) == LIMIT_MAX # Test with no limit provided assert validate_limit(None) == LIMIT_DEFAULT # Test with valid limit assert validate_limit(100) == 100 # Test with limit at boundaries assert validate_limit(10) == LIMIT_MIN assert validate_limit(200) == LIMIT_MAX