GoogleCloudSCC

Security Command Center is a security and risk management platform for Google Cloud. Security Command Center enables you to understand your security and data attack surface by providing asset inventory and discovery, identifying vulnerabilities and threats, and helping you mitigate and remediate risks across an organization. This integration helps you to perform tasks related to findings and assets.

Data Enrichment & Threat Intelligence · Google Cloud SCC

Details

IDGoogleCloudSCC
ProviderGoogle
CategoryData Enrichment & Threat Intelligence
From Version6.0.0
Docker Imagedemisto/google-api-py3:1.0.0.10182333
Supported ModulesAgentix XSIAM

README

Security Command Center is a security and risk management platform for Google Cloud. Security Command Center enables you to understand your security and data attack surface by providing asset inventory and discovery, identifying vulnerabilities and threats, and helping you mitigate and remediate risks across an organization. This integration helps you to perform tasks related to findings and assets.
This integration was integrated and tested with version v1 of GoogleCloudSCC.

Detailed Description

This integration uses Pub/Sub to fetch the incidents. This integration supports multiple organizations. In order to fetch data from multiple organizations, configure multiple instances for different organizations. To set up the initial parameters of Google SCC in Cortex XSOAR, please follow the below instructions. For more information, refer to this guide by Google SCC for configuring Cortex XSOAR Integration.

Scope

We need to provide the below mentioned OAuth scope to execute the commands: https://www.googleapis.com/auth/cloud-platform.

Create a Service Account

  1. Go to the Google documentation and follow the procedure mentioned in the Creating a Service Account section. After you create a service account, a Service Account Private Key file is downloaded. You will need this file when configuring an instance of the integration.
  2. Grant the Security Command Center admin permission to the Service Account to enable the Service Account to perform certain Google Cloud API commands.
  3. For additional information on the types of permissions that can be granted to Service Account, see the Permissions section below.
  4. In Cortex XSOAR, configure an instance of the Google Cloud Security Command Center integration. For the Service Account Private Key parameter, add the Service Account Private Key file contents (JSON).

Permissions

To set up Security Command Center or change the configuration of your organization, you need both of the following roles at the organization level:

  • Organization Admin (roles/resourcemanager.organizationAdmin)
  • Security Center Admin (roles/securitycenter.admin)

If a user doesn’t require edit permissions, consider granting them viewer roles. To view all assets and findings in Security Command Center, users need the Security Center Admin Viewer (roles/securitycenter.adminViewer) role at the organization level. Users who need to edit the findings need the Security Center Admin (roles/securitycenter.admin) role at the organization level.

To restrict access to individual folders and projects, don’t grant all roles at the organization level. Instead, grant the following roles at the folder or project level:

  • Security Center Assets Viewer (roles/securitycenter.assetsViewer)
  • Security Center Findings Viewer (roles/securitycenter.findingsViewer)

Refer to Google Documentation for further information on granting roles to persons and applications, as well as specific permissions.

Steps to configure workload identity federation

  1. Follow the steps to construct a workload identity pool and a workload identity pool provider to leverage workload identity federation.
  2. Navigate to the ‘Granting external identities permission to impersonate a service account’ section.
  3. Follow the step-1 mentioned in the Google documentation to create a credential file for external identities. The contents of the downloaded file should be given into the ‘Service Account Configuration’ parameter.

    Prerequisite for accessing Google services from AWS

    1. Create an IAM AWS Role.
    2. Attach the IAM role to EC2 instance.

    Prerequisite for accessing Google services from Azure

    1. Create an Azure AD application and service principal.
    2. Set an Application ID URI for the application.
    3. Create a managed identity. Note the Object ID of the managed identity. You need it later when you configure impersonation.
    4. Assign the managed identity to a virtual machine or another resource that runs your application.

Getting your Organization ID

The Organization ID is a unique identifier for an organization and is automatically created when your organization resource is created.

  1. To get the Organization ID for your organization, follow the steps mentioned in Google documentation provided here.
  2. To get your Organization ID using the Cloud Console, Go to the Cloud Console and at the top of the page, click the project selection drop-down list and from the Select window that appears, click the organization drop-down list and select the organization you want.
  3. On the right side, click More, then click Settings. The Settings page displays your organization’s ID.

Getting your Project ID

When we create a new project or for an existing project, Project ID generates for that project. To get the Project ID and the Project number, you can follow the same instructions provided above for getting Organization ID. For more details, You can follow the instructions provided in Google documentation here.

Getting Subscription ID from Pub/Sub

To fetch incidents using Google Pub/Sub, we need to configure Pub/Sub first. This Google documentation will help setting up Pub/Sub prerequisites for creating a subscription.

  1. To add a subscription, we need to have a topic first. So after you create a topic, go to the menu for the topic and click on Create subscription and it will take you to the Add new subscription page.
  2. Type a name for the subscription and leave the delivery type as Pull.
  3. Set the Message retention duration to retain unacknowledged messages for a specified duration. If the checkbox of Retain acknowledged messages is enabled, acknowledged messages are retained for the same duration. It is recommended to keep maximum possible value for Message retention so messages can be retained inside subscription until they are pulled.
  4. Set the Acknowledgement deadline for pub/sub to wait for the subscriber to acknowledge receipt before resending the message. Minimum recommended value for Acknowledgement deadline is 300 seconds for this integration.
  5. Apply the other settings as required and click on the CREATE button.
  6. Once the subscription is created, it will take you to the Subscriptions page, where you can see the Subscription ID for the subscription you just created.

Setting up finding notifications

  • Enable the Security Command Center API notifications feature. Notifications send information to a Pub/Sub topic to provide findings updates and new findings within minutes. Set up the notifications as per Google Documentation available and get SCC data in Cortex XSOAR.
  • The basic parameters required for setting up pub/sub notifications are ORGANIZATION_ID, PUBSUB_TOPIC, DESCRIPTION and FILTER.
  • Before creating a pub/sub notification, make sure to check the filter parameters using google-cloud-scc-finding-list command provided in this integration. The total size applicable for the filter provided can be checked using Total retrieved findings available inside the command results section. A maximum of 200 findings per minute is recommended.

Configure GoogleCloudSCC on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for GoogleCloudSCC.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Description Required
    Service Account Configuration If the application runs on cloud provider (AWS, Azure) use workload identity federation configuration setup file otherwise use service account credential file. True
    Organization ID Organization ID defines from which organization incidents need to be fetched. True
    Fetch incidents Enables fetch incident. False
    Project ID ID of the project to use for fetching incidents. If ID is not provided it will be taken from the provided service account JSON.
    Only required if the XSOAR instance is running on AWS or Azure cloud solutions.
    False
    Subscription ID ID of subscription from which to fetch incidents. False
    Max Incidents The maximum number of incidents to fetch every time. False
    Incident type Type of incident. False
    Trust any certificate (not secure) Enables to trust on all certificates. False
    Use system proxy settings Enables system proxy settings. False
  4. Click Test to validate configuration parameter.

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.

google-cloud-scc-asset-list


Lists an organization’s assets.

Base Command

google-cloud-scc-asset-list

Input

Argument Name Description Required
filter The filter expression is a list of one or more restrictions combined via logical operators AND and OR.
Parentheses are supported, and OR has higher precedence than AND.Examples include:
1) name
2) securityCenterProperties.resource_name
3) resourceProperties.name
4) securityMarks.marks.marka

The supported operators are:
1) = for all value types.
2) >, <, >=, <= for integer values.
3) :, meaning substring matching, for strings.

The following field and operator combinations are supported:
1) name: =
2) updateTime: =, >, <, >=, <

Example: resourceProperties.displayName=”test.com” OR resourceProperties.projectNumber=”455757558851”
Use a negated partial match on the empty string to filter based on a property not existing: “-resourceProperties.project=45”.
Optional
orderBy This parameter defines what fields and order to use for sorting.
The string value should be a comma-separated list of fields.
The default sorting order is ascending. To specify descending order for a field, a suffix “desc” should be appended to the field name.
For example: “name desc,resourceProperties.owner”.
The following fields are supported for orderBy:
name, updateTime, resourceProperties, securityMarks.marks, securityCenterProperties.resource_name,
securityCenterProperties.resource_display_name, securityCenterProperties.resource_parent, securityCenterProperties.resource_parent_display_name, securityCenterProperties.resource_project,
securityCenterProperties.resource_project_display_name, securityCenterProperties.resource_type.
Optional
readTime Time is used as a reference point when filtering assets. The filter is limited to assets existing at the supplied time and their values are those at that specific time. If not provided, it will take current time. Format: YYYY-MM-ddTHH:mm:ss.sssZ
Example: 2020-07-22T07:10:02.782Z.
Optional
compareDuration When compareDuration is set, the “stateChange” attribute is updated to indicate whether the asset was added, removed, or remained present during the compareDuration period of time that precedes the readTime.
Possible “stateChange” values when compareDuration is specified:
1) ADDED
2) REMOVED
3) ACTIVE

If compareDuration is not specified, then the only possible stateChange is “UNUSED”,
Example value: 3.5s.
Optional
fieldMask A field mask is used to specify the specific response fields to be listed in the response.
An empty field mask will list all fields. Comma-separated values are supported in this parameter.
Example: “asset.resourceProperties.owner,asset.securityCenterProperties.resourceName”.
Optional
pageToken The value returned by the last response of the google-cloud-scc-asset-list command indicates that this is a continuation of prior assets.list call, and that the system should return the next page of data. Optional
pageSize The maximum number of results to return in a single response. The minimum value is 1 and maximum value is 1000. Default is 10. Optional
resourceType This parameter is used to filter assets by resource types by providing a single value or a comma-separated value of resource types. If any resource type is not provided, by default all resource types will be considered for listing assets.The value provided inside resourceType would be applied in the query as a filter parameter for filtering results. Example: cloudfunction, bucket. Optional
project This parameter is used to filter assets by the project by providing a single value or a comma-separated value of projects. If any project is not provided, by default all projects will be considered for listing assets. Value provided inside the project would be applied in the query as a filter parameter for filtering results. Example: Automeet, Backstory. Optional
activeAssetsOnly This parameter is used to filter assets by their lifeCycleState value by selecting an option from the dropdown. If ‘True’ is selected the assets having lifeCycleState as ‘ACTIVE’ will be fetched and if ‘False’ or no option selected from the dropdown, then assets with all states will be considered for listing assets. The value selected in activeAssetsOnly would be applied in the query as a filter parameter for filtering results. Possible values are: True, False. Default is False. Optional

Context Output

Path Type Description
GoogleCloudSCC.Asset.name String The relative resource name of the asset.
GoogleCloudSCC.Asset.securityCenterProperties.resourceName String The full resource name of the Google Cloud resource this asset represents.
GoogleCloudSCC.Asset.securityCenterProperties.resourceType String The type of the Google Cloud resource.
GoogleCloudSCC.Asset.securityCenterProperties.resourceParent String The full resource name of the immediate parent of the resource.
GoogleCloudSCC.Asset.securityCenterProperties.resourceProject String The full resource name of the project the resource belongs to.
GoogleCloudSCC.Asset.securityCenterProperties.resourceOwners String Owners of the Google Cloud resource.
GoogleCloudSCC.Asset.securityCenterProperties.resourceDisplayName String The user defined display name for this resource.
GoogleCloudSCC.Asset.securityCenterProperties.resourceParentDisplayName String The user defined display name for the parent of this resource.
GoogleCloudSCC.Asset.securityCenterProperties.resourceProjectDisplayName String The user defined display name for the project of this resource.
GoogleCloudSCC.Asset.securityCenterProperties.folders.resourceFolder String Full resource name of this folder.
GoogleCloudSCC.Asset.securityCenterProperties.folders.resourceFolderDisplayName String The user defined display name for this folder.
GoogleCloudSCC.Asset.resourceProperties Unknown Resource managed properties. These properties are managed and defined by the Google Cloud resource and cannot be modified by the user. Properties are varying from assets to assets.
GoogleCloudSCC.Asset.securityMarks.name String The relative resource name of the SecurityMarks.
GoogleCloudSCC.Asset.securityMarks.marks String Mutable user specified security marks belonging to the parent resource.
GoogleCloudSCC.Asset.createTime String The time at which the asset was created in the Security Command Center.
GoogleCloudSCC.Asset.updateTime String The time at which the asset was last updated, added, or deleted in Security Command Center.
GoogleCloudSCC.Asset.iamPolicy.policyBlob String Cloud IAM Policy information associated with the Google Cloud resource described by the Security Command Center asset.
GoogleCloudSCC.Asset.stateChange String State change of the asset between the points in time.
GoogleCloudSCC.Asset.readTime String Time used for executing the list request.
GoogleCloudSCC.Token.nextPageToken String Token to retrieve the next page of results, or empty if there are no more results.
GoogleCloudSCC.Token.name String Name of the command.

Command Example

!google-cloud-scc-asset-list pageSize="3"

Context Example

{
    "GoogleCloudSCC": {
        "Asset": [
            {
                "createTime": "2020-07-22T07:10:02.782Z",
                "iamPolicy": {
                    "policyBlob": "{\"bindings\":[{\"role\":\"roles/billing.admin\",\"members\":[\"group:gcp-billing-admins@test.com\",\"user:harsh.shah@test.com\",\"user:malhar@test.com\",\"user:shail.rabdu@test.com\"]},{\"role\":\"roles/billing.creator\",\"members\":[\"domain:test.com\",\"group:gcp-billing-admins@test.com\",\"user:harsh.shah@test.com\",\"user:malhar@test.com\",\"user:shail.rabdu@test.com\"]},{\"role\":\"roles/billing.user\",\"members\":[\"group:gcp-organization-admins@test.com\"]},{\"role\":\"roles/browser\",\"members\":[\"user:jignesh.patel@test.com\"]},{\"role\":\"roles/cloudfunctions.serviceAgent\",\"members\":[\"serviceAccount:service-org-595779152576@security-center-api.iam.gserviceaccount.com\"]},{\"role\":\"roles/cloudsql.admin\",\"members\":[\"serviceAccount:service-org-595779152576@security-center-api.iam.gserviceaccount.com\"]},{\"role\":\"roles/cloudsupport.admin\",\"members\":[\"group:gcp-organization-admins@test.com\"]},{\"role\":\"roles/compute.admin\",\"members\":[\"user:jignesh.patel@test.com\"]},{\"role\":\"roles/iam.organizationRoleAdmin\",\"members\":[\"group:gcp-organization-admins@test.com\"]},{\"role\":\"roles/iam.serviceAccountAdmin\",\"members\":[\"user:jignesh.patel@test.com\"]},{\"role\":\"roles/orgpolicy.policyAdmin\",\"members\":[\"group:gcp-organization-admins@test.com\"]},{\"role\":\"roles/owner\",\"members\":[\"user:harsh.shah@test.com\"]},{\"role\":\"roles/pubsub.editor\",\"members\":[\"serviceAccount:scc-test-sa-0908@gscc-demo-0908.iam.gserviceaccount.com\"]},{\"role\":\"roles/resourcemanager.folderAdmin\",\"members\":[\"group:gcp-organization-admins@test.com\"]},{\"role\":\"roles/resourcemanager.organizationAdmin\",\"members\":[\"group:gcp-organization-admins@test.com\",\"user:harsh.shah@test.com\",\"user:it.systems@test.com\",\"user:malhar@test.com\",\"user:shivang.patel@test.com\"]},{\"role\":\"roles/resourcemanager.organizationViewer\",\"members\":[\"group:gcp-billing-admins@test.com\",\"user:shivang.patel@test.com\"]},{\"role\":\"roles/resourcemanager.projectCreator\",\"members\":[\"group:gcp-organization-admins@test.com\",\"user:it.systems@test.com\"]},{\"role\":\"roles/securitycenter.admin\",\"members\":[\"domain:test.com\",\"group:gcp-organization-admins@test.com\",\"serviceAccount:scc-test-sa-0908@gscc-demo-0908.iam.gserviceaccount.com\",\"deleted:serviceAccount:scc-test-sa@gscc-demo.iam.gserviceaccount.com?uid\\u003d111170257821042589392\",\"serviceAccount:scc-test@calcium-vial-280707.iam.gserviceaccount.com\",\"user:jignesh.patel@test.com\",\"user:namrata.haridwari@test.com\",\"user:shivang.patel@test.com\"]},{\"role\":\"roles/securitycenter.serviceAgent\",\"members\":[\"serviceAccount:service-org-595779152576@security-center-api.iam.gserviceaccount.com\"]},{\"role\":\"roles/serviceusage.serviceUsageAdmin\",\"members\":[\"serviceAccount:service-org-595779152576@security-center-api.iam.gserviceaccount.com\"]},{\"role\":\"roles/viewer\",\"members\":[\"serviceAccount:scc-test-sa-0908@gscc-demo-0908.iam.gserviceaccount.com\",\"deleted:serviceAccount:scc-test-sa@gscc-demo.iam.gserviceaccount.com?uid\\u003d111170257821042589392\"]}]}"
                },
                "name": "organizations/595779152576/assets/7180457033309348544",
                "readTime": "2021-02-11T13:51:59.620Z",
                "resourceProperties": {
                    "creationTime": "2017-01-23T08:50:47.212Z",
                    "displayName": "test.com",
                    "lifecycleState": "ACTIVE",
                    "name": "organizations/595779152576",
                    "organizationId": "595779152576",
                    "owner": "{\"directoryCustomerId\":\"C02umwv6u\"}"
                },
                "securityCenterProperties": {
                    "resourceDisplayName": "test.com",
                    "resourceName": "//cloudresourcemanager.googleapis.com/organizations/595779152576",
                    "resourceType": "google.cloud.resourcemanager.Organization"
                },
                "securityMarks": {
                    "marks": {
                        "compressed": "SSH",
                        "LastSeen": "Yesterday"
                    },
                    "name": "organizations/595779152576/assets/7180457033309348544/securityMarks"
                },
                "updateTime": "2021-02-06T11:01:26.317Z"
            },
            {
                "createTime": "2020-12-16T10:05:58.742Z",
                "iamPolicy": {
                    "policyBlob": "{\"bindings\":[{\"role\":\"roles/owner\",\"members\":[\"user:milankumar.thummar@test.com\"]}]}"
                },
                "name": "organizations/595779152576/assets/2994068353411300094",
                "readTime": "2021-02-11T13:51:59.620Z",
                "resourceProperties": {
                    "createTime": "2020-12-16T10:05:54.696Z",
                    "lifecycleState": "ACTIVE",
                    "name": "Calender",
                    "parent": "{\"id\":\"595779152576\",\"type\":\"organization\"}",
                    "projectId": "calender-1608113154215",
                    "projectNumber": "455757558851"
                },
                "securityCenterProperties": {
                    "resourceDisplayName": "calender-1608113154215",
                    "resourceName": "//cloudresourcemanager.googleapis.com/projects/455757558851",
                    "resourceOwners": [
                        "user:milankumar.thummar@test.com"
                    ],
                    "resourceParent": "//cloudresourcemanager.googleapis.com/organizations/595779152576",
                    "resourceParentDisplayName": "test.com",
                    "resourceProject": "//cloudresourcemanager.googleapis.com/projects/455757558851",
                    "resourceProjectDisplayName": "calender-1608113154215",
                    "resourceType": "google.cloud.resourcemanager.Project"
                },
                "securityMarks": {
                    "marks": {
                        "compressed": "SSH",
                        "LastSeen": "Yesterday"
                    },
                    "name": "organizations/595779152576/assets/2994068353411300094/securityMarks"
                },
                "updateTime": "2020-12-16T10:06:00.134Z"
            },
            {
                "createTime": "2019-09-24T02:10:50.766Z",
                "iamPolicy": {
                    "policyBlob": "{\"bindings\":[{\"role\":\"roles/owner\",\"members\":[\"user:heena.vaghela@test.com\"]}]}"
                },
                "name": "organizations/595779152576/assets/14656821127596596302",
                "readTime": "2021-02-11T13:51:59.620Z",
                "resourceProperties": {
                    "createTime": "2019-08-13T06:58:21.574Z",
                    "lifecycleState": "ACTIVE",
                    "name": "Test Proj",
                    "parent": "{\"id\":\"595779152576\",\"type\":\"organization\"}",
                    "projectId": "test-proj-249706",
                    "projectNumber": "265894444436"
                },
                "securityCenterProperties": {
                    "resourceDisplayName": "test-proj-249706",
                    "resourceName": "//cloudresourcemanager.googleapis.com/projects/265894444436",
                    "resourceOwners": [
                        "user:heena.vaghela@test.com"
                    ],
                    "resourceParent": "//cloudresourcemanager.googleapis.com/organizations/595779152576",
                    "resourceParentDisplayName": "test.com",
                    "resourceProject": "//cloudresourcemanager.googleapis.com/projects/265894444436",
                    "resourceProjectDisplayName": "test-proj-249706",
                    "resourceType": "google.cloud.resourcemanager.Project"
                },
                "securityMarks": {
                    "name": "organizations/595779152576/assets/14656821127596596302/securityMarks"
                },
                "updateTime": "2020-04-16T06:09:38.488Z"
            }
        ],
        "Token": {
            "name": "google-cloud-scc-asset-list",
            "nextPageToken": "next-page-token"
        }
    }
}

Human Readable Output

Total retrieved asset(s): 3

| Organization ID |Name|Project|Resource Name|Resource Type|Resource Owners|Security Marks|
|—|—|—|—|—|—|—|
| 595779152576 | organizations/595779152576/assets/7180457033309348544 | organizations/595779152576 | //cloudresourcemanager.googleapis.com/organizations/595779152576 | google.cloud.resourcemanager.Organization | | compressed: SSH
LastSeen: Yesterday |
| 595779152576 | organizations/595779152576/assets/2994068353411300094 | Calender | //cloudresourcemanager.googleapis.com/projects/455757558851 | google.cloud.resourcemanager.Project | user:milankumar.thummar@test.com | compressed: SSH
LastSeen: Yesterday |
| 595779152576 | organizations/595779152576/assets/14656821127596596302 | Test Proj | //cloudresourcemanager.googleapis.com/projects/265894444436 | google.cloud.resourcemanager.Project | user:heena.vaghela@test.com | |
To fetch the next batch of results, execute the command with the page token as next-page-token

google-cloud-scc-finding-list


Lists an organization or source’s findings.

Base Command

google-cloud-scc-finding-list

Input

Argument Name Description Required
severity Filter findings by their severity (LOW, MEDIUM, HIGH, CRITICAL). Comma-separated values are supported and if any severity value is not provided, by default all the severities will be considered for listing of findings. Value provided inside severity would be applied in the query as a filter parameter for filtering results. Optional
category Filter findings by providing comma-separated values of categories or a single category.
If any category value is not provided, by default all the categories will be considered for listing findings.
Value provided inside the category would be applied in the query as a filter parameter for filtering results.
For Example: anomaly,application.
Optional
sourceTypeId Filter findings by providing the value of a single source type. If any source type Id value is not provided, by default all source types will be considered for list findings. Default is -. Optional
pageSize The maximum number of results to return in a single response. The minimum value is 1 and maximum value is 1000. Default is 10. Optional
state Filter the findings by their state. Can be ‘ACTIVE’, ‘INACTIVE’. Comma-separated values are supported and if any state value is not provided, by default ‘ACTIVE’ state will be considered for listing of findings. Value provided inside the state would be applied in the query as a filter parameter for filtering results. Default is ACTIVE. Optional
filter The filter expression is a list of one or more restrictions combined via logical operators AND and OR.
Parentheses are supported, and OR has higher precedence than AND.Examples include:
1) name
2) sourceProperties.a_property
3) securityMarks.marks.marka

The supported operators are:
1) = for all value types.
2) >, <, >=, <= for integer values.
3) :, meaning substring matching, for strings.

The following field and operator combinations are supported:
1) name: =
2) parent: =, :
3) resourceName: =, :
4) state: =, :
5) category: =, :
6) externalUri: =, :
7) eventTime: =, >, <, >=, <=
8) severity: =, :
9) findingClass: =

Examples: “sourceProperties.browser=”chrome” AND sourceProperties.event_type=”proximity”“
Use a negated partial match on the empty string to filter based on a property not existing: “-severity=LOW”.
Optional
orderBy This parameter defines what fields and order to use for sorting.
The string value should be a comma separated list of fields. The default sorting order is ascending.
To specify descending order for a field, a suffix “ desc” should be appended to the field name.
For example: “name desc,sourceProperties.browser”.
Supported fields: name, parent, state, category, resourceName, eventTime, sourceProperties, securityMarks.marks.
Optional
compareDuration When compareDuration is set, the “stateChange” attribute is updated to indicate whether the finding had its state changed, the finding’s state remained unchanged, or if the finding was added in any state during the compareDuration period of time that precedes the readTime. This is the time between (readTime - compareDuration) and readTime.
The results aren’t affected if the finding is made inactive and then active again.

Possible “stateChange” values when compareDuration is specified:
1) CHANGED
2) UNCHANGED
3) ADDED
4) REMOVED

If compareDuration is not specified, then the only possible stateChange is “UNUSED”.
Example value: “3.5s”.
Optional
readTime Time used as a reference point when filtering findings. The filter is limited to findings existing at the supplied time and their values are those at that specific time. If not provided, it will take current time.
Format: YYYY-MM-ddTHH:mm:ss.sssZ
Example: 2020-07-22T07:10:02.782Z.
Optional
fieldMask A field mask is used to specify the specific response fields to be listed in the response.
An empty field mask will list all fields. Comma-separated values are supported in this parameter.
Example: “user.displayName,sourceProperties.browser”.
Optional
pageToken The value returned by the last response of a google-cloud-scc-finding-list command indicates that this is a continuation of a prior findings.list call, and that the system should return the next page of data. Optional

Context Output

Path Type Description
GoogleCloudSCC.Finding.name String The relative resource name of this finding.
GoogleCloudSCC.Finding.parent String The relative resource name of the source the finding belongs to.
GoogleCloudSCC.Finding.resourceName String For findings on Google Cloud resources, the full resource name of the Google Cloud resource this finding is for.
GoogleCloudSCC.Finding.state String The state of the finding.
GoogleCloudSCC.Finding.category String The additional taxonomy group within findings from a given source.
GoogleCloudSCC.Finding.externalUri String The URI that, if available, points to a web page outside of Security Command Center where additional information about the finding can be found.
GoogleCloudSCC.Finding.createTime String The time at which the finding was created in Security Command Center.
GoogleCloudSCC.Finding.eventTime String The time at which the event took place, or when an update to the finding occurred.
GoogleCloudSCC.Finding.resource.name String The full resource name of the resource.
GoogleCloudSCC.Finding.resource.parentDisplayName String The human readable name of resource’s parent.
GoogleCloudSCC.Finding.resource.parentName String The full resource name of resource’s parent.
GoogleCloudSCC.Finding.resource.projectDisplayName String The human readable name of project that the resource belongs to.
GoogleCloudSCC.Finding.resource.projectName String The full resource name of the project that the resource belongs to.
GoogleCloudSCC.Finding.resource.folders.resourceFolder String Full resource name of this folder.
GoogleCloudSCC.Finding.resource.folders.resourceFolderDisplayName String The user defined display name for this folder.
GoogleCloudSCC.Finding.stateChange String State change of the finding between the points in time.
GoogleCloudSCC.Finding.sourceProperties Unknown Source specific properties. These properties are managed by the source that writes the finding. Properties are varying from finding to finding.
GoogleCloudSCC.Finding.severity String Severity of the finding.
GoogleCloudSCC.Finding.securityMarks.name String The relative resource name of the SecurityMarks.
GoogleCloudSCC.Finding.securityMarks.marks String Mutable user specified security marks belonging to the parent resource.
GoogleCloudSCC.Finding.readTime String Time used for executing the list request.
GoogleCloudSCC.Token.nextPageToken String Token to retrieve the next page of results, or empty if there are no more results.
GoogleCloudSCC.Token.name String Name of the command.

Command Example

!google-cloud-scc-finding-list sourceTypeId="-" pageSize="3" state="ACTIVE"

Context Example

{
    "GoogleCloudSCC": {
        "Finding": [
            {
                "category": "page",
                "createTime": "2020-05-15T05:57:46.641Z",
                "eventTime": "2021-02-11T09:33:30.716Z",
                "externalUri": "http://www.fake-url.com",
                "name": "organizations/595779152576/sources/10134421585261057824/findings/00002906967111ea87141217baf6db4d",
                "parent": "organizations/595779152576/sources/10134421585261057824",
                "readTime": "2021-02-11T13:52:10.594Z",
                "resource": {
                    "name": "//cloudresourcemanager.googleapis.com/projects/339295427573",
                    "parentDisplayName": "test.com",
                    "parentName": "//cloudresourcemanager.googleapis.com/organizations/595779152576",
                    "projectDisplayName": "gscc-demo-0908",
                    "projectName": "//cloudresourcemanager.googleapis.com/projects/339295427573"
                },
                "resourceName": "//cloudresourcemanager.googleapis.com/projects/339295427573",
                "securityMarks": {
                    "name": "organizations/595779152576/sources/10134421585261057824/findings/00002906967111ea87141217baf6db4d/securityMarks"
                },
                "sourceProperties": {
                    "access_method": "IPSec",
                    "appcategory": "Technology",
                    "bypass_traffic": "yes",
                    "category": "Technology",
                    "ccl": "unknown",
                    "count": "1",
                    "domain": "www.fake-url.com",
                    "dst_country": "US",
                    "dst_geoip_src": "2",
                    "dst_latitude": 35.7319,
                    "dst_location": "Morganton",
                    "dst_longitude": -81.7091,
                    "dst_region": "North Carolina",
                    "dst_zipcode": "28655",
                    "dstip": "127.0.0.1",
                    "id": "fda1f2cb566f247dac4c4c77",
                    "insertion_epoch_timestamp": "1584069016",
                    "organization_unit": "None",
                    "page": "www.fake-url.com",
                    "page_id": "0",
                    "policy": "Domains",
                    "site": "apple",
                    "src_country": "US",
                    "src_geoip_src": "2",
                    "src_latitude": 37.4073,
                    "src_location": "San Jose",
                    "src_longitude": -121.939,
                    "src_region": "California",
                    "src_zipcode": "95134",
                    "srcip": "127.0.0.1",
                    "ssl_decrypt_policy": "no",
                    "tenant_name": "partners",
                    "timestamp": "1584069012",
                    "traffic_type": "Web",
                    "transaction_id": "0",
                    "type": "page",
                    "ur_normalized": "127.0.0.1",
                    "url": "www.fake-url.com",
                    "user": "127.0.0.1",
                    "user_generated": "yes",
                    "userip": "127.0.0.1"
                },
                "state": "ACTIVE"
            },
            {
                "category": "page",
                "createTime": "2020-05-30T15:19:49.539Z",
                "eventTime": "2021-02-11T07:21:45.317Z",
                "name": "organizations/595779152576/sources/10134421585261057824/findings/00002ccaa28911ea9d221217baf6db4d",
                "parent": "organizations/595779152576/sources/10134421585261057824",
                "readTime": "2021-02-11T13:52:10.594Z",
                "resource": {
                    "name": "//cloudresourcemanager.googleapis.com/projects/339295427573",
                    "parentDisplayName": "test.com",
                    "parentName": "//cloudresourcemanager.googleapis.com/organizations/595779152576",
                    "projectDisplayName": "gscc-demo-0908",
                    "projectName": "//cloudresourcemanager.googleapis.com/projects/339295427573"
                },
                "resourceName": "//cloudresourcemanager.googleapis.com/projects/339295427573",
                "securityMarks": {
                    "name": "organizations/595779152576/sources/10134421585261057824/findings/00002ccaa28911ea9d221217baf6db4d/securityMarks"
                },
                "state": "ACTIVE"
            },
            {
                "category": "page",
                "createTime": "2020-05-30T02:41:01.848Z",
                "eventTime": "2020-03-16T01:38:52Z",
                "externalUri": "http://www.fake-url.com",
                "name": "organizations/595779152576/sources/10134421585261057824/findings/000031c6a21f11ea9d221217baf6db4d",
                "parent": "organizations/595779152576/sources/10134421585261057824",
                "readTime": "2021-02-11T13:52:10.594Z",
                "resource": {
                    "name": "//cloudresourcemanager.googleapis.com/projects/339295427573",
                    "parentDisplayName": "test.com",
                    "parentName": "//cloudresourcemanager.googleapis.com/organizations/595779152576",
                    "projectDisplayName": "gscc-demo-0908",
                    "projectName": "//cloudresourcemanager.googleapis.com/projects/339295427573"
                },
                "resourceName": "//cloudresourcemanager.googleapis.com/projects/339295427573",
                "securityMarks": {
                    "name": "organizations/595779152576/sources/10134421585261057824/findings/000031c6a21f11ea9d221217baf6db4d/securityMarks"
                },
                "sourceProperties": {
                    "access_method": "IPSec",
                    "app": "LinkedIn",
                    "appcategory": "Social",
                    "bypass_traffic": "yes",
                    "category": "Social",
                    "cci": "65",
                    "ccl": "medium",
                    "count": "1",
                    "domain": "www.fake-url.com",
                    "dst_country": "US",
                    "dst_geoip_src": "1",
                    "dst_latitude": 37.368889,
                    "dst_location": "Sunnyvale",
                    "dst_longitude": -122.035278,
                    "dst_region": "California",
                    "dst_timezone": "N/A",
                    "dst_zipcode": "N/A",
                    "dstip": "127.0.0.1",
                    "id": "567a33f799d411dab82da23e",
                    "insertion_epoch_timestamp": "1584322739",
                    "organization_unit": "None",
                    "page": "www.fake-url.com",
                    "page_id": "0",
                    "policy": "No_Decrypt",
                    "site": "Linkedin",
                    "src_country": "US",
                    "src_geoip_src": "2",
                    "src_latitude": 37.4073,
                    "src_location": "San Jose",
                    "src_longitude": -121.939,
                    "src_region": "California",
                    "src_zipcode": "95134",
                    "srcip": "127.0.0.1",
                    "ssl_decrypt_policy": "yes",
                    "tenant_name": "partners",
                    "timestamp": "1584322732",
                    "traffic_type": "CloudApp",
                    "transaction_id": "0",
                    "type": "page",
                    "ur_normalized": "127.0.0.1",
                    "url": "www.fake-url.com",
                    "user": "127.0.0.1",
                    "user_generated": "yes",
                    "userip": "127.0.0.1"
                },
                "state": "ACTIVE"
            }
        ],
        "Token": {
            "name": "google-cloud-scc-finding-list",
            "nextPageToken": "next-page-token"
        }
    }
}

Human Readable Output

Total retrieved finding(s): 3

| Organization ID |Name|Category|Resource Name|Finding Class|Event Time|Create Time|Security Marks|
|—|—|—|—|—|—|—|—|
| 595779152576 | organizations/595779152576/sources/10134421585261057824/findings/00002906967111ea87141217baf6db4d | page | //cloudresourcemanager.googleapis.com/projects/339295427573 | THREAT | February 11, 2021 at 09:33:30 AM | May 15, 2020 at 05:57:46 AM | { “name”: “wrench”, “count”: “3” } |
| 595779152576 | organizations/595779152576/sources/10134421585261057824/findings/00002ccaa28911ea9d221217baf6db4d | page | //cloudresourcemanager.googleapis.com/projects/339295427573 | THREAT | February 11, 2021 at 07:21:45 AM | May 30, 2020 at 03:19:49 PM | { “name”: “wrench”, “count”: “3” } |
| 595779152576 | organizations/595779152576/sources/10134421585261057824/findings/000031c6a21f11ea9d221217baf6db4d | page | //cloudresourcemanager.googleapis.com/projects/339295427573 | THREAT | March 16, 2020 at 01:38:52 AM | May 30, 2020 at 02:41:01 AM | { “name”: “wrench”, “count”: “3” } |
To fetch the next batch of results, execute the command with the page token as next-page-token

google-cloud-scc-finding-update


Update an organization’s or source’s finding.

Base Command

google-cloud-scc-finding-update

Input

Argument Name Description Required
name The relative resource name of the finding.
Format: organizations/{organization_id}/sources/{source_id}/finding/{findingId}
Example: organizations/595779152576/sources/14801394649435054450/findings/bc5a86da657611ebb979005056a5924e.
Required
eventTime Time at which the event took place. By default UTC current time will be taken if no value is provided in eventTime.
Format: YYYY-MM-ddTHH:mm:ss.sssZ
Example: 2020-07-22T07:10:02.782Z, 2014-10-02T15:01:23.045123456Z.
Optional
severity Related severity of the finding. Possible values are: LOW, MEDIUM, HIGH, CRITICAL. Optional
externalUri URI that points to a web page outside of Cloud SCC (Security Command Center) where additional information about the finding can be found. Optional
sourceProperties Source specific properties. These properties are managed by the source that writes the finding. For example “key1=val1,key2=val2”. Optional
updateMask A updateMask argument supports single or comma-separated fields that need to be updated/deleted. A updateMask is automatically generated in the backend for the specific arguments provided in the command and only those values will be updated. To delete attributes/properties, add those keys in updateMask without specifying those fields individually in the command arguments. Optional

Context Output

Path Type Description
GoogleCloudSCC.Finding.name String The relative resource name of this finding.
GoogleCloudSCC.Finding.parent String The relative resource name of the source the finding belongs to.
GoogleCloudSCC.Finding.resourceName String For findings on Google Cloud resources, the full resource name of the Google Cloud resource this finding is for.
GoogleCloudSCC.Finding.state String The state of the finding.
GoogleCloudSCC.Finding.category String The additional taxonomy group within findings from a given source.
GoogleCloudSCC.Finding.externalUri String The URI that, if available, points to a web page outside of Security Command Center where additional information about the finding can be found.
GoogleCloudSCC.Finding.createTime String The time at which the finding was created in Security Command Center.
GoogleCloudSCC.Finding.eventTime String The time at which the event took place, or when an update to the finding occurred.
GoogleCloudSCC.Finding.sourceProperties Unknown Source specific properties. These properties are managed by the source that writes the finding. Properties are varying from finding to finding.
GoogleCloudSCC.Finding.severity String Severity of the finding.
GoogleCloudSCC.Finding.securityMarks.name String The relative resource name of the SecurityMarks.
GoogleCloudSCC.Finding.securityMarks.marks String Mutable user specified security marks belonging to the parent resource.

Command Example

!google-cloud-scc-finding-update name="organizations/595779152576/sources/10134421585261057824/findings/00002906967111ea87141217baf6db4d"

Context Example

{
    "GoogleCloudSCC": {
        "Finding": {
            "category": "page",
            "createTime": "2020-05-15T05:57:46.641Z",
            "eventTime": "2021-02-11T13:52:25.986162Z",
            "externalUri": "http://www.fake-url.com",
            "name": "organizations/595779152576/sources/10134421585261057824/findings/00002906967111ea87141217baf6db4d",
            "parent": "organizations/595779152576/sources/10134421585261057824",
            "resourceName": "//cloudresourcemanager.googleapis.com/projects/339295427573",
            "securityMarks": {
                "name": "organizations/595779152576/sources/10134421585261057824/findings/00002906967111ea87141217baf6db4d/securityMarks"
            },
            "sourceProperties": {
                "access_method": "IPSec",
                "appcategory": "Technology",
                "bypass_traffic": "yes",
                "category": "Technology",
                "ccl": "unknown",
                "count": "1",
                "domain": "www.fake-url.com",
                "dst_country": "US",
                "dst_geoip_src": "2",
                "dst_latitude": 35.7319,
                "dst_location": "Morganton",
                "dst_longitude": -81.7091,
                "dst_region": "North Carolina",
                "dst_zipcode": "28655",
                "dstip": "127.0.0.1",
                "id": "fda1f2cb566f247dac4c4c77",
                "insertion_epoch_timestamp": "1584069016",
                "organization_unit": "None",
                "page": "www.fake-url.com",
                "page_id": "0",
                "policy": "Domains",
                "site": "apple",
                "src_country": "US",
                "src_geoip_src": "2",
                "src_latitude": 37.4073,
                "src_location": "San Jose",
                "src_longitude": -121.939,
                "src_region": "California",
                "src_zipcode": "95134",
                "srcip": "127.0.0.1",
                "ssl_decrypt_policy": "no",
                "tenant_name": "partners",
                "timestamp": "1584069012",
                "traffic_type": "Web",
                "transaction_id": "0",
                "type": "page",
                "ur_normalized": "127.0.0.1",
                "url": "www.fake-url.com",
                "user": "127.0.0.1",
                "user_generated": "yes",
                "userip": "127.0.0.1"
            },
            "state": "ACTIVE"
        }
    }
}

Human Readable Output

The finding has been updated successfully

Organization ID Name State Category Event Time Create Time External Uri Resource Name
595779152576 organizations/595779152576/sources/10134421585261057824/findings/00002906967111ea87141217baf6db4d ACTIVE page February 11, 2021 at 01:52:25 PM May 15, 2020 at 05:57:46 AM http://www.fake-url.com //cloudresourcemanager.googleapis.com/projects/339295427573

google-cloud-scc-asset-resource-list


Lists cloud asset’s resources.

Base Command

google-cloud-scc-asset-resource-list

Input

Argument Name Description Required
parent Name of the organization or project the assets belong to. Organization Id provided in the Integration Configuration will be taken by default, if no value is provided to the parent.

Format: “organizations/[organization-number]” (such as “organizations/123”), “projects/[project-id]” (such as “projects/my-project-id”), or “projects/[project-number]” (such as “projects/12345”).
Optional
assetTypes This parameter is used to filter assets by asset types by providing a single value or a comma-separated value of asset types.
For example: “compute.googleapis.com/Disk”.

Regular expression is also supported.
For example:
1) “compute.googleapis.com.” resources whose asset type starts with “compute.googleapis.com”.
2) “.
Instance” resources whose asset type ends with “Instance”.
3) “.Instance.” resources whose asset type contains “Instance”.
Optional
pageSize The maximum number of results to return in a single response. The minimum value is 1 and maximum value is 1000. Default is 10. Optional
pageToken The nextPageToken returned from the previous scc-asset-resource-list command response, or unspecified for the first scc-asset-resource-list command. It is a continuation of a prior scc-asset-resource-list call, and the API should return the next page of assets. Optional
readTime Time used as a reference point when filtering assets. This can only be set to a timestamp between the current time and the current time minus 35 days (inclusive). If not provided, it will take current time.

Format:
(<number> <time unit>, e.g., “12 hours ago”, “7 days ago”, “1 week”, “1 month”) or (<date> <time>, e.g. “yyyy-mm-ddTHH-MM-SS”) or ( “YYYY-MM-ddTHH:mm:ss.sssZ”, e.g. 2020-07-22T07:10:02.782Z) or (<date>, e.g. “2020-07-22”).
Optional

Context Output

Path Type Description
GoogleCloudSCC.CloudAsset.Resource.name String The full name of the asset.
GoogleCloudSCC.CloudAsset.Resource.assetType String The type of the asset.
GoogleCloudSCC.CloudAsset.Resource.updateTime String The last update timestamp of an asset. The updateTime is updated when create/update/delete operation is performed.
GoogleCloudSCC.CloudAsset.Resource.readTime String Time used for executing the list request.
GoogleCloudSCC.CloudAsset.Resource.ancestors Unknown The ancestry path of an asset in Google Cloud resource hierarchy, represented as a list of relative resource names. An ancestry path starts with the closest ancestor in the hierarchy and ends at root. If the asset is a project, folder, or organization, the ancestry path starts from the asset itself.
GoogleCloudSCC.CloudAsset.Resource.resource.version String The API version.
GoogleCloudSCC.CloudAsset.Resource.resource.discoveryDocumentUri String The URL of the discovery document containing the resource’s JSON schema. This value is unspecified for resources that do not have an API based on a discovery document, such as Cloud Bigtable.
GoogleCloudSCC.CloudAsset.Resource.resource.discoveryName String The JSON schema name listed in the discovery document. This value is unspecified for resources that do not have an API based on a discovery document, such as Cloud Bigtable.
GoogleCloudSCC.CloudAsset.Resource.resource.resourceUrl String The REST URL for accessing the resource. An HTTP GET request using this URL returns the resource itself.
GoogleCloudSCC.CloudAsset.Resource.resource.parent String The full name of the immediate parent of this resource. For third-party assets, this field may be set differently.
GoogleCloudSCC.CloudAsset.Resource.resource.data String The content of the resource, in which some sensitive fields are removed and may not be present.
GoogleCloudSCC.CloudAsset.Resource.resource.location String The location of the resource in Google Cloud, such as its zone and region.
GoogleCloudSCC.Token.name String Name of the command.
GoogleCloudSCC.Token.nextPageToken String Token to retrieve the next page of results, or empty if there are no more results.

Command Example

!google-cloud-scc-asset-resource-list pageSize=2

Context Example

{
    "GoogleCloudSCC": {
        "CloudAsset": {
            "Resource": [
                {
                    "ancestors": [
                        "organizations/123456789"
                    ],
                    "assetType": "cloudbilling.googleapis.com/BillingAccount",
                    "name": "//cloudbilling.googleapis.com/billingAccounts/12345-6789",
                    "readTime": "2021-06-17T10:19:59.557941456Z",
                    "resource": {
                        "data": {
                            "displayName": "My Billing Account",
                            "name": "billingAccounts/12345-6789"
                        },
                        "discoveryDocumentUri": "https://cloudbilling.googleapis.com/$discovery/rest",
                        "discoveryName": "BillingAccount",
                        "location": "global",
                        "version": "v1"
                    },
                    "updateTime": "2020-08-21T09:05:39.425Z"
                },
                {
                    "ancestors": [
                        "organizations/123456789"
                    ],
                    "assetType": "cloudbilling.googleapis.com/BillingAccount",
                    "name": "//cloudbilling.googleapis.com/billingAccounts/23456-7890",
                    "readTime": "2021-06-17T10:19:59.557941456Z",
                    "resource": {
                        "data": {
                            "displayName": "Our-Account",
                            "name": "billingAccounts/23456-7890"
                        },
                        "discoveryDocumentUri": "https://cloudbilling.googleapis.com/$discovery/rest",
                        "discoveryName": "BillingAccount",
                        "location": "global",
                        "version": "v1"
                    },
                    "updateTime": "2021-04-01T19:38:12.836197Z"
                }
            ]
        },
        "Token": {
            "name": "google-cloud-scc-asset-resource-list",
            "nextPageToken": "next-page-token"
        }
    }
}

Human Readable Output

| Organization ID |Asset Name|Asset Type|Discovery Name|Ancestors|Update Time (In UTC)|
|—|—|—|—|—|—|
| 595779152576 | //cloudbilling.googleapis.com/billingAccounts/12345-6789 | cloudbilling.googleapis.com/BillingAccount | BillingAccount | organizations/123456789 | August 21, 2020 at 09:05:39 AM |
| 595779152576 | //cloudbilling.googleapis.com/billingAccounts/23456-7890 | cloudbilling.googleapis.com/BillingAccount | BillingAccount | organizations/123456789 | April 01, 2021 at 07:38:12 PM |
To fetch the next batch of results, execute the command with the page token as next-page-token

google-cloud-scc-asset-owner-get


Gets the owner information for the provided projects.

Base Command

google-cloud-scc-asset-owner-get

Input

Argument Name Description Required
projectName Name of the project. Supports comma separated values.

Format: “projects/[project-number]” or for multiple projects “projects/[first-project-number], projects/[second-project-number]”.
Required
maxIteration Number of iterations to search the owner information. Each iteration retrieves 1000 records. The minimum value is 1 and maximum value is 10. Default is 2. Optional

Context Output

Path Type Description
GoogleCloudSCC.CloudAsset.IamPolicy.name String The full name of the asset.
GoogleCloudSCC.CloudAsset.IamPolicy.owners Unknown List of owners of the asset.
GoogleCloudSCC.CloudAsset.IamPolicy.assetType String The type of the asset.
GoogleCloudSCC.CloudAsset.IamPolicy.updateTime String The last update timestamp of an asset. The updateTime is updated when create/update/delete operation is performed.
GoogleCloudSCC.CloudAsset.IamPolicy.readTime String Time used for executing the list request.
GoogleCloudSCC.CloudAsset.IamPolicy.ancestors Unknown The ancestry path of an asset in Google Cloud resource hierarchy, represented as a list of relative resource names. An ancestry path starts with the closest ancestor in the hierarchy and ends at root. If the asset is a project, folder, or organization, the ancestry path starts from the asset itself.
GoogleCloudSCC.CloudAsset.IamPolicy.version String Specifies the format of the policy.
GoogleCloudSCC.CloudAsset.IamPolicy.etag String The etag is used for optimistic concurrency control as a way to help prevent simultaneous updates of a policy from overwriting each other. It is strongly suggested that systems make use of the etag in the read-modify-write cycle to perform policy updates in order to avoid race conditions: An etag is returned in the response to getIamPolicy, and systems are expected to put that etag in the request to setIamPolicy to ensure that their change will be applied to the same version of the policy.
GoogleCloudSCC.CloudAsset.IamPolicy.bindings.role String A role is a named collection of permissions that provide the ability to perform actions on Google Cloud resources.
GoogleCloudSCC.CloudAsset.IamPolicy.bindings.members Unknown A member, also known as an identity or principal, which can be a user account, service account, Google group, or domain.
GoogleCloudSCC.CloudAsset.IamPolicy.bindings.condition String A condition, which is an optional logic expression that further constrains the role binding based on attributes about the request, such as its origin, the target resource, and so on. Conditions are typically used to control whether access is granted based on the context for a request.
GoogleCloudSCC.CloudAsset.IamPolicy.auditConfigs.service String Specifies a service that will be enabled for audit logging.
GoogleCloudSCC.CloudAsset.IamPolicy.auditConfigs.auditLogConfigs.logType String The log type that this config enables.
GoogleCloudSCC.CloudAsset.IamPolicy.auditConfigs.auditLogConfigs.exemptedMembers String Specifies the identities that do not cause logging for this type of permission.

Command Example

!google-cloud-scc-asset-owner-get projectName="projects/123456789"

Context Example

{
    "GoogleCloudSCC": {
        "CloudAsset": {
            "IamPolicy": {
                "ancestors": [
                    "projects/123456789",
                    "organizations/123456789"
                ],
                "assetType": "cloudresourcemanager.googleapis.com/Project",
                "iamPolicy": {
                    "bindings": [
                        {
                            "members": [
                                "serviceAccount:dummmyaccount@dummycom",
                                "user:dummmyuser1@dummycom"
                            ],
                            "role": "roles/owner"
                        }
                    ],
                    "etag": "BwV9ONRnkz4=",
                    "version": 1
                },
                "name": "//cloudresourcemanager.googleapis.com/projects/123456789",
                "owners": [
                    "serviceAccount:dummmyaccount@dummycom",
                    "user:dummmyuser1@dummycom"
                ],
                "readTime": "2021-06-17T10:20:43.762746137Z",
                "updateTime": "2018-12-24T10:00:00Z"
            }
        }
    }
}

Human Readable Output

Organization ID Project Name Project Owner Ancestors Update Time (In UTC)
595779152576 //cloudresourcemanager.googleapis.com/projects/123456789 serviceAccount:dummmyaccount@dummycom,
user:dummmyuser1@dummycom
projects/123456789,
organizations/123456789
December 24, 2018 at 10:00:00 AM

google-cloud-scc-finding-state-update


Update the state of organization’s or source’s finding.

Base Command

google-cloud-scc-finding-state-update

Input

Argument Name Description Required
name The relative resource name of the finding.

Format: organizations/{organization_id}/sources/{source_id}/finding/{findingId}

Example: organizations/595779152576/sources/14801394649435054450/findings/bc5a86da657611ebb979005056a5924e.
Required
state The desired state of the finding. Possible values are: ACTIVE, INACTIVE. Required

Context Output

Path Type Description
GoogleCloudSCC.Finding.name String The relative resource name of this finding.
GoogleCloudSCC.Finding.parent String The relative resource name of the source the finding belongs to.
GoogleCloudSCC.Finding.resourceName String For findings on Google Cloud resources, the full resource name of the Google Cloud resource this finding is for.
GoogleCloudSCC.Finding.state String The state of the finding.
GoogleCloudSCC.Finding.category String The additional taxonomy group within findings from a given source.
GoogleCloudSCC.Finding.externalUri String The URI that, if available, points to a web page outside of Security Command Center where additional information about the finding can be found.
GoogleCloudSCC.Finding.createTime String The time at which the finding was created in Security Command Center.
GoogleCloudSCC.Finding.eventTime String The time at which the event took place, or when an update to the finding occurred.
GoogleCloudSCC.Finding.sourceProperties Unknown Source specific properties. These properties are managed by the source that writes the finding. Properties are varying from finding to finding.
GoogleCloudSCC.Finding.severity String Severity of the finding.
GoogleCloudSCC.Finding.securityMarks.name String The relative resource name of the SecurityMarks.
GoogleCloudSCC.Finding.securityMarks.marks String Mutable user specified security marks belonging to the parent resource.

Command Example

!google-cloud-scc-finding-state-update name="organizations/595779152576/sources/10134421585261057824/findings/00002906967111ea87141217baf6db4d" state=ACTIVE"

Context Example

{
    "GoogleCloudSCC": {
        "Finding": {
            "category": "page",
            "createTime": "2020-05-15T05:57:46.641Z",
            "eventTime": "2021-02-11T13:52:25.986162Z",
            "externalUri": "http://www.fake-url.com",
            "name": "organizations/595779152576/sources/10134421585261057824/findings/00002906967111ea87141217baf6db4d",
            "parent": "organizations/595779152576/sources/10134421585261057824",
            "resourceName": "//cloudresourcemanager.googleapis.com/projects/339295427573",
            "securityMarks": {
                "name": "organizations/595779152576/sources/10134421585261057824/findings/00002906967111ea87141217baf6db4d/securityMarks"
            },
            "sourceProperties": {
                "access_method": "IPSec",
                "appcategory": "Technology",
                "bypass_traffic": "yes",
                "category": "Technology",
                "ccl": "unknown",
                "count": "1",
                "domain": "www.fake-url.com",
                "dst_country": "US",
                "dst_geoip_src": "2",
                "dst_latitude": 35.7319,
                "dst_location": "Morganton",
                "dst_longitude": -81.7091,
                "dst_region": "North Carolina",
                "dst_zipcode": "28655",
                "dstip": "127.0.0.1",
                "id": "fda1f2cb566f247dac4c4c77",
                "insertion_epoch_timestamp": "1584069016",
                "organization_unit": "None",
                "page": "www.fake-url.com",
                "page_id": "0",
                "policy": "Domains",
                "site": "apple",
                "src_country": "US",
                "src_geoip_src": "2",
                "src_latitude": 37.4073,
                "src_location": "San Jose",
                "src_longitude": -121.939,
                "src_region": "California",
                "src_zipcode": "95134",
                "srcip": "127.0.0.1",
                "ssl_decrypt_policy": "no",
                "tenant_name": "partners",
                "timestamp": "1584069012",
                "traffic_type": "Web",
                "transaction_id": "0",
                "type": "page",
                "ur_normalized": "127.0.0.1",
                "url": "www.fake-url.com",
                "user": "127.0.0.1",
                "user_generated": "yes",
                "userip": "127.0.0.1"
            },
            "state": "ACTIVE"
        }
    }
}

Human Readable Output

The finding has been updated successfully

Organization ID Name State Severity Category Event Time Create Time External Uri Resource Name
595779152576 organizations/595779152576/sources/10134421585261057824/findings/00002906967111ea87141217baf6db4d ACTIVE High page February 11, 2021 at 01:52:25 PM May 15, 2020 at 05:57:46 AM http://www.fake-url.com //cloudresourcemanager.googleapis.com/projects/339295427573

Known Limitations

This integration supports only secure connection hence disabling SSL(Trust any certificate) support is not provided.

Configuration parameters

  • service_account_json — Service Account Configuration (required)
  • organization_id — Organization ID (required)
  • isFetch — Fetch incidents
  • project_id — Project ID
  • subscription_id — Subscription ID
  • max_fetch — Max Incidents
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (6)

  • google-cloud-scc-asset-list Deprecated

    Lists an organization's assets.

  • google-cloud-scc-asset-owner-get

    Gets the owner information for the provided projects.

  • google-cloud-scc-asset-resource-list

    Lists cloud asset's resources.

  • google-cloud-scc-finding-list

    Lists an organization or source's findings.

  • google-cloud-scc-finding-state-update

    Update the state of organization's or source's finding.

  • google-cloud-scc-finding-update

    Update an organization's or source's finding.

from CommonServerPython import *

""" IMPORTS """
import json
import traceback
import urllib.parse
from collections.abc import Callable
from copy import deepcopy
from typing import Any

import dateparser
import httplib2
from google.auth import aws, exceptions, identity_pool
from google.oauth2 import service_account
from google_auth_httplib2 import AuthorizedHttp
from googleapiclient import discovery
from googleapiclient.errors import HttpError

""" CONSTANTS """
SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
SERVICE_NAME = "securitycenter"
PUBSUB_SERVICE_NAME = "pubsub"
CLOUD_ASSET_SERVICE_NAME = "cloudasset"
SERVICE_VERSION = "v1"
PUBSUB_SERVICE_VERSION = "v1"
CLOUD_ASSET_SERVICE_VERSION = "v1"
DEFAULT_MAX_FETCH_VALUE = "50"
MAX_FETCH_VALUE = "200"
DEFAULT_PAGE_SIZE = 10
MAX_PAGE_SIZE = 1000
INCIDENT_NAME_PREFIX = "GoogleCloudSCC"
STATE_LIST = ["ACTIVE", "INACTIVE"]  # List of state mentioned in API doc
SEVERITY_LIST = ["LOW", "MEDIUM", "HIGH", "CRITICAL"]  # List of severity mentioned in API doc
ALLOWED_DATE_UNIT = ["minute", "minutes", "hour", "hours", "day", "days", "month", "months", "year", "years"]
DATE_FORMAT = "%B %d, %Y at %I:%M:%S %p"
ISO_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
MARKDOWN_LINK = "[{}]({})"
TIMEOUT_TIME = 60  # in second

# The maximum number of results to return in a single response.
# (ref: https://cloud.google.com/security-command-center/docs/reference/rest/v1/organizations.sources.findings/list)
MAX_ITERATION = 10
DEFAULT_MAX_ITERATION = 2

ERROR_MESSAGES: dict[str, str] = {
    "JSON_PARSE_ERROR": "Unable to parse json. Please check the {} parameter.",
    "INVALID_ORGANIZATION_ID": "Invalid Organization ID.",
    "INVALID_ORGANIZATION_OR_PERMISSION_DENIED_ERROR": "Organization Id is not valid or permission denied.",
    "INVALID_SERVICE_ACCOUNT": "Invalid service account credentials.",
    "BAD_REQUEST_ERROR": "An error occurred while fetching/submitting the data. Reason: {}",
    "AUTHENTICATION_ERROR": "Unauthenticated. Check the configured Service Account JSON. Reason: {}",
    "AUTHORIZATION_ERROR": "Request has insufficient privileges. Reason: {}",
    "NOT_FOUND_ERROR": "Not found. Reason: {}",
    "TOO_MANY_REQUESTS_ERROR": "Too many requests please try after sometime. Reason: {}",
    "INTERNAL_SERVER_ERROR": "The server encountered an internal error. Reason: {}",
    "CONFLICT_ERROR": "Conflict. Reason: {}",
    "TIMEOUT_ERROR": "Connection Timeout Error - potential reasons might be that the Server is not accessible "
    "from your host. Reason: {}",
    "PROXY_ERROR": "Proxy Error - if the 'Use system proxy' checkbox in the integration configuration is"
    " selected, try clearing the checkbox.",
    "UNKNOWN_ERROR": "An error occurred. Status: {}. Reason: {}",
    "NO_RECORDS_FOUND": "No {} record(s) found for the given argument(s).",
    "MAX_INCIDENT_ERROR": "The parameter Max Incidents must be a positive integer."
    f" Accepted values can be in the range of 1-{MAX_FETCH_VALUE}.",
    "INVALID_STATE_ERROR": "The state value must be ACTIVE or INACTIVE.",
    "INVALID_SEVERITY_ERROR": "The severity value must be LOW, MEDIUM, HIGH or CRITICAL.",
    "INVALID_PAGE_SIZE_ERROR": "Page size should be an integer between 1 to 1000.",
    "INVALID_SOURCE_PROPERTIES": "Invalid format provided in sourceProperties. Supported format: key1=value1,key2="
    "value2. if the value contains ',' or '=' character then escape with extra '\\'.",
    "REQUIRED_PROJECT_ID": "Project ID is required for fetch incidents.",
    "REQUIRED_SUBSCRIPTION_ID": "Subscription ID is required for fetch incidents.",
    "INVALID_INCIDENT": "Error while parsing pub/sub message. Reason: {}",
    "INVALID_DATE_TIME": '{} should be in following format: (<number> <time unit>, e.g., "12 hours ago",'
    ' "7 days ago", "1 week", "1 month") or (<date> <time>, e.g. '
    '"yyyy-mm-ddTHH-MM-SS") or ( "YYYY-MM-ddTHH:mm:ss.sssZ", '
    'e.g. 2020-07-22T07:10:02.782Z) or (<date>, e.g. "2020-07-22").',
    "INVALID_PROJECT_NAME_ERROR": "projectName should be in following format: "
    '"projects/[project-number]" or "projects/[first-project-number], '
    'projects/[second-project-number]".',
    "INVALID_MAX_ITERATION_ERROR": f"maxIteration should be an integer between 1 to {MAX_ITERATION}.",
}

OUTPUT_PREFIX: dict[str, Any] = {
    "LIST_ASSET": "GoogleCloudSCC.Asset(val.name && val.name == obj.name)",
    "LIST_FINDING": "GoogleCloudSCC.Finding(val.name && val.name == obj.name)",
    "TOKEN": "GoogleCloudSCC.Token(val.name && val.name == obj.name)",
    "FINDING": "GoogleCloudSCC.Finding",
    "LIST_RESOURCE": "GoogleCloudSCC.CloudAsset.Resource(val.name && val.name == obj.name)",
    "GET_OWNER": "GoogleCloudSCC.CloudAsset.IamPolicy",
}

GET_OUTPUT_MESSAGE: dict[str, Any] = {"HEADER_MESSAGE": "Total retrieved {0}: {1}"}

COMMON_STRING: dict[str, str] = {
    "RESOURCE_NAME": "Resource Name",
    "SECURITY_MARKS": "Security Marks",
    "SET_STATE_HR_STR": "The state of the finding has been updated successfully.",
    "EVENT_TIME": "Event Time (In UTC)",
    "CREATE_TIME": "Create Time (In UTC)",
}

AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request"
AZURE_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
NEXT_PAGE_TOKEN_MESSAGE = "To fetch the next batch of results, execute the command with the page token as {}"
""" HELPER CLASSES """


class GoogleNameParser:
    """
    Used to easily transform Google Cloud SCC names
    """

    # Google SCC helpers
    ORGANIZATION_PATH = "organizations/{}"
    SOURCE_PATH = "/sources/{}"
    FINDING_PATH = "/findings/{}"
    SCC_URL = "https://console.cloud.google.com/security/command-center/{}?organizationId={}&resourceId={}"

    # Google Pub/Sub helpers
    PROJECT_PATH = "projects/{}"
    SUBSCRIPTION_PATH = "/subscriptions/{}"

    @staticmethod
    def get_organization_id():
        """
        Return a fully-qualified organizations string.

        :return: fully-qualified organizations string.
        """
        return demisto.params().get("organization_id", "")

    @staticmethod
    def get_organization_path():
        """
        Return a fully-qualified organizations string.

        :return: fully-qualified organizations string.
        """
        return GoogleNameParser.ORGANIZATION_PATH.format(GoogleNameParser.get_organization_id())

    @staticmethod
    def get_source_path(source: str) -> str:
        """
        Return a fully-qualified source string.

        :param source: source id param.
        :return: fully-qualified source string.
        """
        return GoogleNameParser.get_organization_path() + GoogleNameParser.SOURCE_PATH.format(source)

    @staticmethod
    def get_finding_path(source, finding):
        """
        fully-qualified finding string.

        :param source: source name or source id
        :param finding: finding id
        :return: fully-qualified finding string.
        """
        return GoogleNameParser.get_source_path(source) + GoogleNameParser.FINDING_PATH.format(finding)

    @staticmethod
    def get_finding_url(name: str):
        """
        Get a finding url which redirect UI

        :param name: resource name
        :return: finding url
        """
        return GoogleNameParser.SCC_URL.format("findings", GoogleNameParser.get_organization_id(), name)

    @staticmethod
    def get_asset_url(name: str):
        """
        Get a asset url which redirect UI

        :param name: resource name
        :return: asset url
        """
        return GoogleNameParser.SCC_URL.format("assets", GoogleNameParser.get_organization_id(), name)

    @staticmethod
    def get_project_path(project_id):
        """
        Get a fully-qualified project path.

        :param project_id: project id of project
        :return: fully-qualified project path
        """
        return GoogleNameParser.PROJECT_PATH.format(project_id)

    @staticmethod
    def get_subscription_path(project_id, subscription_id):
        """
        Get a fully-qualified subscription path.

        :param project_id: project id of project
        :param subscription_id: subscription id of the topic
        :return:
        """
        return GoogleNameParser.get_project_path(project_id) + GoogleNameParser.SUBSCRIPTION_PATH.format(subscription_id)


class BaseGoogleClient:
    """
    A Client class to wrap the google cloud api library as a service.
    """

    def __init__(
        self,
        service_name: str,
        service_version: str,
        service_account_json: str,
        scopes: list,
        proxy: bool,
        insecure: bool,
        **kwargs,
    ):
        """
        :param service_name: The name of the service. You can find this and the service  here
         https://github.com/googleapis/google-api-python-client/blob/master/docs/dyn/index.md
        :param service_version: The version of the API.
        :param service_account_json: A string of the generated credentials.json
        :param scopes: The scope needed for the project. (i.e. ['https://www.googleapis.com/auth/cloud-platform'])
        :param proxy: Proxy flag
        :param kwargs: Potential arguments dict
        """
        service_account_json = safe_load_non_strict_json(service_account_json)  # type: ignore
        try:
            if service_account_json.get("subject_token_type") == AWS_SUBJECT_TOKEN_TYPE:
                credentials = aws.Credentials.from_info(info=service_account_json, scopes=scopes)
            elif service_account_json.get("subject_token_type") == AZURE_SUBJECT_TOKEN_TYPE:
                credentials = identity_pool.Credentials.from_info(service_account_json, scopes=scopes)
            else:
                credentials = service_account.Credentials.from_service_account_info(info=service_account_json, scopes=scopes)
            http_client = AuthorizedHttp(credentials=credentials, http=self.get_http_client_with_proxy(proxy, insecure))
            self.service = discovery.build(service_name, service_version, http=http_client, cache_discovery=False)
        except httplib2.ServerNotFoundError as e:
            raise ValueError(ERROR_MESSAGES["TIMEOUT_ERROR"].format(str(e)))
        except (httplib2.socks.HTTPError, IndexError) as e:
            # library not able to handle Proxy error and throws Index Error
            demisto.debug(f"Failed to execute {demisto.command()} command. Error: {e!s} , traceback: {traceback.format_exc()}")
            raise ValueError(ERROR_MESSAGES["PROXY_ERROR"])
        except exceptions.RefreshError as error:
            error_message = ERROR_MESSAGES["INVALID_SERVICE_ACCOUNT"]
            if error.args:
                error_message += f" Reason: {error.args[0]}"
            raise ValueError(error_message)

    @staticmethod
    def get_http_client_with_proxy(proxy: bool, insecure: bool) -> httplib2.Http:
        """
        Create an http client with proxy with whom to use when using a proxy.
        :param proxy: Whether to use a proxy.
        :param insecure: Whether to perform a ssl validation.

        :return: ProxyInfo object.
        """
        proxy_info = {}
        if proxy:
            proxies = handle_proxy()
            https_proxy = proxies.get("https")
            http_proxy = proxies.get("http")
            proxy_conf = https_proxy if https_proxy else http_proxy

            if proxy_conf:
                if not proxy_conf.startswith("https") and not proxy_conf.startswith("http"):
                    proxy_conf = "https://" + proxy_conf
                parsed_proxy = urllib.parse.urlparse(proxy_conf)
                proxy_info = httplib2.ProxyInfo(
                    proxy_type=httplib2.socks.PROXY_TYPE_HTTP,
                    proxy_host=parsed_proxy.hostname,
                    proxy_port=parsed_proxy.port,
                    proxy_user=parsed_proxy.username,
                    proxy_pass=parsed_proxy.password,
                )
        return httplib2.Http(
            proxy_info=proxy_info,
            timeout=TIMEOUT_TIME,
            disable_ssl_certificate_validation=insecure,
            ca_certs=os.getenv("REQUESTS_CA_BUNDLE") or os.getenv("SSL_CERT_FILE"),
        )

    @staticmethod
    def execute_request(request) -> dict[str, Any]:
        """
        Execute the request and handle error scenario.

        :param request: request object
        :return: dictionary of json response
        """
        try:
            return request.execute()
        except HttpError as e:
            status = e.resp.status
            reason = e._get_reason()

            status_code_message_map = {
                400: ERROR_MESSAGES["BAD_REQUEST_ERROR"],
                401: ERROR_MESSAGES["AUTHENTICATION_ERROR"],
                403: ERROR_MESSAGES["AUTHORIZATION_ERROR"],
                404: ERROR_MESSAGES["NOT_FOUND_ERROR"],
                409: ERROR_MESSAGES["CONFLICT_ERROR"],
                429: ERROR_MESSAGES["TOO_MANY_REQUESTS_ERROR"],
                500: ERROR_MESSAGES["INTERNAL_SERVER_ERROR"],
            }

            if status in status_code_message_map:
                raise ValueError(status_code_message_map[status].format(reason))
            else:
                raise ValueError(ERROR_MESSAGES["UNKNOWN_ERROR"].format(status, reason))
        except httplib2.socks.HTTPError as e:
            demisto.debug(f"Failed to execute {demisto.command()} command. Error: {e!s} , traceback: {traceback.format_exc()}")
            raise ValueError(ERROR_MESSAGES["PROXY_ERROR"])
        except httplib2.ServerNotFoundError as e:
            raise ValueError(ERROR_MESSAGES["TIMEOUT_ERROR"].format(str(e)))


class GoogleSccClient(BaseGoogleClient):
    """
    A Client class to wrap the google cloud security center api library as a service.
    """

    def __init__(self, organization_id: str, **kwargs):
        """Constructor for GoogleSccClient class."""
        super().__init__(**kwargs)
        self.organization_id = organization_id

    def get_findings(
        self,
        parent: str,
        compare_duration: str | None = None,
        field_mask: str | None = None,
        filter_string: str | None = None,
        order_by: str | None = None,
        page_size: str | int | None = DEFAULT_PAGE_SIZE,
        page_token: str | None = None,
        read_time: str | None = None,
    ) -> dict[str, Any]:
        """
        Get an organization or source's findings.

        :param parent: Name of the source the findings belong to.
        :param compare_duration: A duration in seconds that is used to derived stateChange of finding.
        :param field_mask: A field mask to specify the Finding fields to be listed in the response.
        :param filter_string: Expression that defines the filter to apply across findings.
        :param order_by: Expression that defines what fields and order to use for sorting.
        :param page_size: The maximum number of results to return in a single response.
        :param page_token: The value returned by the last call; indicates that this is a continuation of a prior call.
        :param read_time: The Time used as a reference point when filtering findings.

        :return: list of findings
        """
        request = (
            self.service.organizations()  # pylint: disable=E1101
            .sources()
            .findings()
            .list(  # pylint: disable=E1101
                parent=parent,
                compareDuration=compare_duration,
                fieldMask=field_mask,
                filter=filter_string,
                orderBy=order_by,
                pageSize=page_size,
                pageToken=page_token,
                readTime=read_time,
            )
        )
        result = self.execute_request(request)
        return result

    def get_assets(
        self,
        parent: str,
        compare_duration: str,
        field_mask: str,
        filter_string: str,
        order_by: str,
        page_size: str | int,
        page_token: str,
        read_time: str,
    ) -> dict[str, Any]:
        """
        Get an organization's assets.

        :param parent: Name of the organization assets should belong to.
        :param compare_duration: A duration in seconds that is used to derived stateChange of finding.
        :param field_mask: A field mask to specify the Finding fields to be listed in the response.
        :param filter_string: Expression that defines the filter to apply across findings.
        :param order_by: Expression that defines what fields and order to use for sorting.
        :param page_size: The maximum number of results to return in a single response.
        :param page_token: The value returned by the last call; indicates that this is a continuation of a prior call.
        :param read_time: The Time used as a reference point when filtering findings.
        :return: list of assets
        """
        request = (
            self.service.organizations()  # pylint: disable=E1101
            .assets()
            .list(  # pylint: disable=E1101
                parent=parent,
                compareDuration=compare_duration,
                fieldMask=field_mask,
                filter=filter_string,
                orderBy=order_by,
                pageSize=page_size,
                pageToken=page_token,
                readTime=read_time,
            )
        )

        result = self.execute_request(request)
        return result

    def get_source(self, name: str) -> dict[str, Any]:
        """
        Gets a source.

        :param name: A Relative resource name of the source.
        :return:
        """
        request = self.service.organizations().sources().get(name=name)  # pylint: disable=E1101
        result = self.execute_request(request)
        return result

    def update_finding(
        self,
        name: str,
        event_time: str | None,
        severity: str | None,
        external_uri: str | None,
        source_properties: str | None,
        update_mask: list,
    ) -> dict[str, Any]:
        """
        Updates a finding. The corresponding source must exist for a finding update to succeed.

        :param name: The resource name of this finding.
        :param event_time: event time of finding
        :param severity: severity of finding
        :param external_uri: external_uri of finding
        :param source_properties: source_properties of finding
        :param update_mask: which field you want to update

        :return: updated finding response
        """
        body = assign_params(
            eventTime=event_time, severity=severity, externalUri=external_uri, sourceProperties=source_properties
        )
        update_mask = get_update_mask_for_update_finding(body, update_mask)  # type: ignore
        request = (
            self.service.organizations()  # pylint: disable=E1101
            .sources()
            .findings()
            .patch(  # pylint: disable=E1101
                name=name, updateMask=update_mask, body=body
            )
        )
        result = self.execute_request(request)
        return result

    def update_state(
        self,
        name: str,
        event_time: str | None,
        state: str,
    ) -> dict[str, Any]:
        """
        Updates a state.

        :param name: The resource name of this finding.
        :param event_time: event time of finding
        :param state: state of finding


        :return: updated response
        """
        body = assign_params(startTime=event_time, state=state)

        request = (
            self.service.organizations()  # pylint: disable=E1101
            .sources()
            .findings()
            .setState(  # pylint: disable=E1101
                name=name, body=body
            )
        )
        result = self.execute_request(request)
        return result


class GooglePubSubClient(BaseGoogleClient):
    """
    A Client class to wrap the google cloud pub/sub api library as a service.
    """

    def __init__(self, project_id, subscription_id, service_account_json, **kwargs):
        """Constructor for GooglePubSubClient class."""
        super().__init__(service_account_json=service_account_json, **kwargs)
        self.project_id = project_id or extract_project_id_from_service_account(service_account_json)
        self.subscription_id = subscription_id

    def pull_messages(self, max_messages, ret_immediately=True) -> dict[str, Any]:
        """
        Pull messages for the subscription

        :param max_messages: The maximum number of messages to return for this request. Must be a positive integer
        :param ret_immediately: when set to true will return immediately, otherwise will be async
        :return: Messages
        """
        subscription = GoogleNameParser.get_subscription_path(self.project_id, self.subscription_id)
        body = assign_params(returnImmediately=ret_immediately, maxMessages=max_messages)
        request = (
            self.service.projects()  # pylint: disable=E1101
            .subscriptions()
            .pull(  # pylint: disable=E1101
                subscription=subscription, body=body
            )
        )
        result = self.execute_request(request)
        return result

    def acknowledge_messages(self, acks_list: list) -> dict[str, Any]:
        """
        Pull messages for the subscription

        :param acks_list: The maximum number of messages to return for this request. Must be a positive integer
        :return:
        """
        subscription = GoogleNameParser.get_subscription_path(self.project_id, self.subscription_id)
        body = assign_params(ackIds=acks_list)
        request = (
            self.service.projects()  # pylint: disable=E1101
            .subscriptions()
            .acknowledge(  # pylint: disable=E1101
                subscription=subscription, body=body
            )
        )
        result = self.execute_request(request)
        return result


class GoogleCloudAssetClient(BaseGoogleClient):
    """
    A Client class to wrap the google cloud assets api library as a service.
    """

    def __init__(self, organization_id: str, **kwargs):
        """Constructor for GoogleCloudAssetClient class."""
        super().__init__(**kwargs)
        self.organization_id = organization_id

    def get_assets(
        self, parent: str, asset_types: list, content_type: str, page_size: str | int, page_token: str, read_time: str | None
    ) -> dict[str, Any]:
        """
        Get a assets based on asset type and content type.

        :param parent: Name of the organization or project the assets belong to.
        :param asset_types: A list of asset types to take a snapshot for.
        :param content_type: A field mask to specify the Finding fields to be listed in the response.
        :param page_size: The maximum number of results to return in a single response.
        :param page_token: The value returned by the last call; indicates that this is a continuation of a prior call.
        :param read_time: Timestamp to take an asset snapshot.
        :return: list of assets
        """
        request = self.service.assets().list(  # pylint: disable=E1101
            parent=parent,
            assetTypes=asset_types,
            contentType=content_type,
            pageSize=int(page_size),
            pageToken=page_token,
            readTime=read_time,
        )

        result = self.execute_request(request)
        return result


""" HELPER FUNCTIONS """


def init_google_scc_client(**kwargs) -> GoogleSccClient:
    """
    Initializes google scc client
    :param kwargs: keyword arguments
    :return: SCC Client object
    """
    client = GoogleSccClient(service_name=SERVICE_NAME, service_version=SERVICE_VERSION, scopes=SCOPES, **kwargs)
    return client


def init_google_pubsub_client(**kwargs) -> GooglePubSubClient:
    """
    Initializes google pubsub client
    :param kwargs: keyword arguments
    :return: Pubsub Client object
    """
    client = GooglePubSubClient(service_name=PUBSUB_SERVICE_NAME, service_version=PUBSUB_SERVICE_VERSION, scopes=SCOPES, **kwargs)
    return client


def init_google_cloud_assets_client(**kwargs) -> GoogleCloudAssetClient:
    """
    Initializes google cloud assets client
    :param kwargs: keyword arguments
    :return: Google cloud assets client object
    """
    client = GoogleCloudAssetClient(
        service_name=CLOUD_ASSET_SERVICE_NAME, service_version=CLOUD_ASSET_SERVICE_VERSION, scopes=SCOPES, **kwargs
    )
    return client


def safe_load_non_strict_json(json_string: str) -> dict[str, Any]:
    """
    Loads the JSON with non-strict mode.

    :param json_string: json string to parse.

    :return: Parsed dictionary.
    :raises ValueError: If there is any other issues while parsing json.
    """
    try:
        if json_string:
            return json.loads(json_string, strict=False)
        return {}
    except ValueError:
        raise ValueError(ERROR_MESSAGES["JSON_PARSE_ERROR"].format("Service Account JSON"))


def validate_get_int(max_results: str | None, message: str, limit: int | str = 0) -> int | None:
    """
    Validate and convert string max_results to integer.

    :param max_results: max results in string.
    :param message: Message to display when exception raised.
    :param limit: If max_results > limit raise the exception.

    :return: int max_results
    :raises ValueError: if max_results is not a integer and < 0.
    """
    if max_results:
        try:
            max_results_int = int(max_results)
            if max_results_int <= 0:
                raise ValueError(message)
            if limit and max_results_int > int(limit):
                raise ValueError(message)
            return max_results_int
        except ValueError:
            raise ValueError(message)
    return None


def validate_project_and_subscription_id(params: dict[str, Any]) -> None:
    """
    Validates parameters for fetch-incidents command.

    :param params: parameters dictionary.

    :return: None
    """
    pubsub_client = init_google_pubsub_client(**params)
    pubsub_client.pull_messages(1)


def validate_service_account_and_organization_name(params: dict[str, str]) -> None:
    """
    Validate Service Account JSON and Organization ID

    :param params: configuration parameter
    :return:
    """
    service_account_json = safe_load_non_strict_json(params.get("service_account_json", ""))

    if not isinstance(service_account_json, dict) or not service_account_json:
        raise ValueError(ERROR_MESSAGES["INVALID_SERVICE_ACCOUNT"])

    client = init_google_scc_client(**params)
    parent = GoogleNameParser.get_source_path("-")
    client.get_findings(parent, page_size=1)


def validate_state_and_severity_list(state_list: list, severity_list: list) -> None:
    """
    Validate severity and state list values

    :param state_list: state list
    :param severity_list: severity list

    :return:
    """
    for state in state_list:
        if state and state.strip().upper() not in STATE_LIST:
            raise ValueError(ERROR_MESSAGES["INVALID_STATE_ERROR"])

    # Validate Severity param
    for severity in severity_list:
        if severity and severity.strip().upper() not in SEVERITY_LIST:
            raise ValueError(ERROR_MESSAGES["INVALID_SEVERITY_ERROR"])


def validate_configuration_param(params: dict[str, Any]) -> None:
    """
    validate configuration parameter through API call.

    :param params: parameter dictionary
    :return: None
    """
    # Validate parameter by hitting finding endpoint
    organization_id: str = params.get("organization_id", "")
    if not organization_id:
        raise ValueError(ERROR_MESSAGES["INVALID_ORGANIZATION_ID"])

    max_fetch = params.get("max_fetch") or DEFAULT_MAX_FETCH_VALUE
    validate_get_int(max_fetch, ERROR_MESSAGES["MAX_INCIDENT_ERROR"], MAX_FETCH_VALUE)
    service_account_json = params.get("service_account_json", "")
    project_id = params.get("project_id", "") or extract_project_id_from_service_account(service_account_json)
    subscription_id = params.get("subscription_id", "")
    is_fetch = params.get("isFetch", False)
    if is_fetch and not project_id:
        raise ValueError(ERROR_MESSAGES["REQUIRED_PROJECT_ID"])

    if is_fetch and not subscription_id:
        raise ValueError(ERROR_MESSAGES["REQUIRED_SUBSCRIPTION_ID"])


def create_filter_list_findings(category: str, filter_string: str, severity: list, state: list) -> str:
    """
    creating common filter query string for "list findings" API based on various filter parameter.

    :param category: category filter
    :param filter_string: filter string
    :param severity: severity filter
    :param state: state filter
    :return: filter query string
    """
    if severity:
        filter_string = add_filter("Severity", filter_string, severity)
    if state:
        filter_string = add_filter("State", filter_string, state)
    if category:
        category_list: list = category.split(",")
        filter_string = add_filter("Category", filter_string, category_list)

    return filter_string


def add_filter(label, filter_string, values) -> str:
    """
    adding filter in filter parameter string.

    :param label: label of the filter. i.e Severity, Category etc
    :param filter_string: filter param string
    :param values: list of values
    :return: filter params string
    """
    if filter_string:
        filter_string = filter_string + " AND "

    filter_string += "({})".format(" OR ".join([f'{label}="{value.strip()}"' for value in values]))
    return filter_string


def prepare_markdown_fields_for_fetch_incidents(fields: dict[str, Any]) -> dict[str, str]:
    """
    Prepares markdown fields for incident.

    :param fields: fields received in response of incident.
    :returns: None
    """
    security_marks = dict_safe_get(fields, ["finding", "securityMarks", "marks"], {})
    mfa_details = dict_safe_get(fields, ["finding", "sourceProperties", "MfaDetails"], {})
    security_marks_hr = tableToMarkdown("", security_marks)
    mfa_details_hr = tableToMarkdown("", mfa_details)
    return {"securityMarks": security_marks_hr, "MfaDetails": mfa_details_hr}


def strip_dict(args: dict[str, str]) -> dict[str, str]:
    """
    Remove leading and trailing white spaces from dictionary values and remove empty entries.
    :param args: Arguments dict.
    :return: Dictionary with whitespaces and empty entries removed.
    """
    return {key: value.strip() for (key, value) in args.items() if value and value.strip()}


def create_filter_list_assets(asset_type: str, project: str, filter_string: str, active_assets_only: str) -> str:
    """
    creating common filter query string for "list findings" API based on various filter parameter.
    :param asset_type: type filter
    :param filter_string: filter dict
    :param project: project filter
    :param  active_assets_only: lifeCycleState filter
    :return: filter query string
    """
    if filter_string is None:
        filter_string = ""
    if project:
        project_list: list = project.split(",")
        filter_string = add_filter("resourceProperties.name", filter_string, project_list)
    if asset_type:
        type_list: list = asset_type.split(",")
        filter_string = add_filter("securityCenterProperties.resourceType", filter_string, type_list)
    if active_assets_only.lower() == "true":
        filter_string = add_filter("resourceProperties.lifecycleState", filter_string, ["ACTIVE"])
    return filter_string


def prepare_human_readable_dict_for_list_asset(asset: dict[str, Any]) -> dict[str, Any]:
    """
    Prepare human-readable dictionary for list asset command.
    :param asset: asset information
    :return: human-readable dict
    """
    asset_url = GoogleNameParser.get_asset_url(asset.get("name", ""))

    return {
        "Organization ID": GoogleNameParser.get_organization_id(),
        COMMON_STRING["RESOURCE_NAME"]: asset.get("securityCenterProperties", {}).get("resourceName", ""),
        "Resource Type": asset.get("securityCenterProperties", {}).get("resourceType", ""),
        "Resource Owners": asset.get("securityCenterProperties", {}).get("resourceOwners", {}),
        "Project": asset.get("resourceProperties", {}).get("name", ""),
        "Name": get_markdown_link(asset.get("name", ""), asset_url),
        COMMON_STRING["SECURITY_MARKS"]: asset.get("securityMarks", {}).get("marks", {}),
    }


def prepare_outputs_for_list_assets(result) -> tuple[dict[str, Any], str]:
    """
    Preparing context output and human-readable for list-assets command.

    :param result: result dictionary
    :return:
    """
    hr_asset_list = []
    ec_asset_list = []

    list_assets = result.get("listAssetsResults", [])
    if len(list_assets) <= 0:
        return {}, ERROR_MESSAGES["NO_RECORDS_FOUND"].format("asset")

    read_time = result.get("readTime", "")
    total_size = result.get("totalSize")

    for asset in list_assets:
        flatten_keys_to_root(asset, ["asset"], {"readTime": read_time, "stateChange": asset.get("stateChange", None)})
        ec_asset_list.append(asset)
        hr_asset_dict = prepare_human_readable_dict_for_list_asset(asset)
        hr_asset_list.append(hr_asset_dict)

    # Preparing headers
    headers = [
        "Organization ID",
        "Name",
        "Project",
        COMMON_STRING["RESOURCE_NAME"],
        "Resource Type",
        "Resource Owners",
        COMMON_STRING["SECURITY_MARKS"],
    ]
    readable_output = tableToMarkdown(
        GET_OUTPUT_MESSAGE["HEADER_MESSAGE"].format("asset(s)", total_size), t=hr_asset_list, headers=headers, removeNull=True
    )

    # preparing context
    ec_asset_dict: dict[str, Any] = {OUTPUT_PREFIX["LIST_ASSET"]: ec_asset_list}

    next_page_token = result.get("nextPageToken", "")
    if next_page_token:
        token_ec = {"name": "google-cloud-scc-asset-list", "nextPageToken": next_page_token}
        ec_asset_dict.update({OUTPUT_PREFIX["TOKEN"]: token_ec})
        readable_output += NEXT_PAGE_TOKEN_MESSAGE.format(next_page_token)

    return remove_empty_elements(ec_asset_dict), readable_output


def flatten_keys_to_root(data_dict: dict[str, Any], keys: list, update_dict: dict[str, Any]):
    """
    Add list of keys to root level in dict

    :param data_dict: dictionary
    :param keys: list of keys
    :param update_dict: dictionary that will be add in data dict
    :return: flatten dict for provided keys
    """
    for key in keys:
        value = data_dict.pop(key, None)
        if value and isinstance(value, dict):
            data_dict.update(value)
        else:
            data_dict[key] = value
    data_dict.update(update_dict)


def convert_string_to_date_format(date: str, date_format: str = DATE_FORMAT) -> str | None:
    """
    Convert date into given format

    :param date: date string
    :param date_format: output date format
    :return: human readable date
    """
    date_obj = dateparser.parse(date)

    if date_obj:
        return date_obj.strftime(date_format)
    return None


def prepare_hr_and_ec_for_list_findings(result: dict[str, Any]) -> tuple[str, dict[str, Any]]:
    """
    Prepare human readable output

    :param result: List findings API response
    :return: markdown string and context data  of list findings
    """
    # Preparing list of entry context and human readable
    hr_finding_list = []
    ec_finding_list = []

    findings = result.get("listFindingsResults", [])
    if not len(findings):
        return ERROR_MESSAGES["NO_RECORDS_FOUND"].format("finding"), {}

    read_time = result.get("readTime", "")
    total_size = result.get("totalSize")

    for finding in findings:
        flatten_keys_to_root(finding, ["finding"], {"readTime": read_time, "stateChange": finding.get("stateChange", None)})
        ec_finding_list.append(finding)
        finding_url = GoogleNameParser.get_finding_url(finding.get("name", ""))
        hr_finding_list.append(
            {
                "Organization ID": GoogleNameParser.get_organization_id(),
                "Name": get_markdown_link(finding.get("name", ""), finding_url),
                "Category": finding.get("category", ""),
                COMMON_STRING["RESOURCE_NAME"]: finding.get("resourceName", ""),
                "Finding Class": finding.get("findingClass", ""),
                COMMON_STRING["EVENT_TIME"]: convert_string_to_date_format(finding.get("eventTime", "")),
                COMMON_STRING["CREATE_TIME"]: convert_string_to_date_format(finding.get("createTime", "")),
                COMMON_STRING["SECURITY_MARKS"]: finding.get("securityMarks", {}).get("marks", {}),
            }
        )

    headers = [
        "Organization ID",
        "Name",
        "Category",
        COMMON_STRING["RESOURCE_NAME"],
        "Finding Class",
        COMMON_STRING["EVENT_TIME"],
        COMMON_STRING["CREATE_TIME"],
        COMMON_STRING["SECURITY_MARKS"],
    ]
    readable_output = tableToMarkdown(
        GET_OUTPUT_MESSAGE["HEADER_MESSAGE"].format("finding(s)", total_size), t=hr_finding_list, headers=headers, removeNull=True
    )

    # preparing context
    ec_dict: dict[str, Any] = {OUTPUT_PREFIX["LIST_FINDING"]: ec_finding_list}
    next_page_token = result.get("nextPageToken", "")
    if next_page_token:
        token_ec = {"name": "google-cloud-scc-finding-list", "nextPageToken": next_page_token}
        ec_dict[OUTPUT_PREFIX["TOKEN"]] = token_ec
        readable_output += NEXT_PAGE_TOKEN_MESSAGE.format(next_page_token)

    return readable_output, remove_empty_elements(ec_dict)


def get_and_validate_args_finding_update(args: dict[str, Any]) -> tuple:
    """
    Get and validate arguments of finding update command.

    :param args: arguments of finding update command.
    :return: name, event_time, severity, external_uri, source_properties, update_mask
    """
    # Get command args
    name = args.get("name", None)
    event_time = args.get("eventTime") or datetime.now().strftime(ISO_DATE_FORMAT)
    severity = args.get("severity", "").upper()
    external_uri = args.get("externalUri", None)
    source_properties = args.get("sourceProperties", None)
    update_mask = [value for value in args.get("updateMask", "").split(",") if value.strip()]

    if severity and severity.strip().upper() not in SEVERITY_LIST:
        raise ValueError(ERROR_MESSAGES["INVALID_SEVERITY_ERROR"])

    try:
        if source_properties:
            source_properties = dict(
                split_and_escape(line, "=")  # type: ignore
                for line in split_and_escape(source_properties, ",")
            )
    except ValueError:
        raise ValueError(ERROR_MESSAGES["INVALID_SOURCE_PROPERTIES"])

    return name, event_time, severity, external_uri, source_properties, update_mask


def get_and_validate_args_finding_state_update(args: dict[str, Any]) -> tuple:
    """
    Get and validate arguments of finding state update command.

    :param args: arguments of finding state update command.
    :return: name, event_time, state
    """
    # Get command args
    name = args.get("name", None)
    event_time = datetime.now().strftime(ISO_DATE_FORMAT)
    state = args.get("state", "").upper()

    if state and state.strip().upper() not in STATE_LIST:
        raise ValueError(ERROR_MESSAGES["INVALID_STATE_ERROR"])

    return name, event_time, state


def prepare_hr_and_ec_for_update_finding(result: dict[str, Any]) -> tuple[str, dict[str, Any]]:
    """
    Prepare human readable output

    :param result: List findings API response
    :return: markdown string and context data  of list findings
    """
    # Preparing list of entry context and human readable
    finding_url = GoogleNameParser.get_finding_url(result.get("name", ""))

    hr_data = {
        "Organization ID": GoogleNameParser.get_organization_id(),
        "Name": get_markdown_link(result.get("name", ""), finding_url),
        "State": result.get("state", ""),
        "Severity": result.get("severity", ""),
        "Category": result.get("category", ""),
        COMMON_STRING["EVENT_TIME"]: convert_string_to_date_format(result.get("eventTime", "")),
        COMMON_STRING["CREATE_TIME"]: convert_string_to_date_format(result.get("createTime", "")),
        "External Uri": get_markdown_link(result.get("externalUri", ""), result.get("externalUri", "")),
        COMMON_STRING["RESOURCE_NAME"]: result.get("resourceName", ""),
    }

    headers = [
        "Organization ID",
        "Name",
        "State",
        "Severity",
        "Category",
        COMMON_STRING["EVENT_TIME"],
        COMMON_STRING["CREATE_TIME"],
        "External Uri",
        COMMON_STRING["RESOURCE_NAME"],
    ]
    readable_output = tableToMarkdown("The finding has been updated successfully.", t=hr_data, headers=headers, removeNull=True)

    return readable_output, remove_empty_elements(result)


def validate_with_regex(validation_message: str, pattern: str, string: str, flags=0) -> None:
    """
    Match the string with regex pattern, if match is not found then raises ValueError with supplied validation message
    :param pattern: pattern to match with
    :param string: string to match on
    :param flags: flags used in re library
    :param validation_message: message to raise ValueError with
    """
    match = re.match(pattern, string, flags)
    if match is None:
        raise ValueError(validation_message)


def prepare_hr_and_ec_for_cloud_asset_list(result: dict[str, Any]) -> tuple[str, dict[str, Any]]:
    """
    Prepare human readable output

    :param result: List Cloud Asset API response
    :return: markdown string and context data of cloud assets
    """
    # Preparing list of entry context and human readable
    hr_asset_list = []
    ec_asset_list = []

    list_assets = result.get("assets", [])
    if len(list_assets) == 0:
        return ERROR_MESSAGES["NO_RECORDS_FOUND"].format("resource"), {}

    read_time = result.get("readTime", "")

    for asset in list_assets:
        asset["readTime"] = read_time
        ec_asset_list.append(asset)
        resource = asset.get("resource", {})
        hr_asset_dict = {
            "Organization ID": GoogleNameParser.get_organization_id(),
            "Asset Name": asset.get("name", ""),
            "Asset Type": asset.get("assetType", ""),
            "Parent": resource.get("parent", ""),
            "Discovery Name": resource.get("discoveryName", ""),
            "Ancestors": asset.get("ancestors", ""),
            "Update Time (In UTC)": convert_string_to_date_format(asset.get("updateTime", "")),
        }
        hr_asset_list.append(hr_asset_dict)

    headers = ["Organization ID", "Asset Name", "Asset Type", "Parent", "Discovery Name", "Ancestors", "Update Time (In UTC)"]
    readable_output = tableToMarkdown("", t=hr_asset_list, headers=headers, removeNull=True)

    # preparing context
    ec_dict: dict[str, Any] = {OUTPUT_PREFIX["LIST_RESOURCE"]: ec_asset_list}
    next_page_token = result.get("nextPageToken", "")
    if next_page_token:
        token_ec = {"name": "google-cloud-scc-asset-resource-list", "nextPageToken": next_page_token}
        ec_dict[OUTPUT_PREFIX["TOKEN"]] = token_ec
        readable_output += NEXT_PAGE_TOKEN_MESSAGE.format(next_page_token)

    return readable_output, remove_empty_elements(ec_dict)


def prepare_hr_and_ec_for_cloud_asset_owners_get(assets: list, read_time: str) -> tuple[str, list]:
    """
    Prepare human readable output

    :param assets: Cloud Assets
    :param read_time: readTime returned in API response
    :return: markdown string and context data of cloud assets
    """

    if len(assets) == 0:
        return ERROR_MESSAGES["NO_RECORDS_FOUND"].format("project"), []

    hr_asset_list = []

    for asset in assets:
        asset["owners"] = find_asset_owners(asset)
        asset["readTime"] = read_time

        hr_asset_dict = {
            "Organization ID": GoogleNameParser.get_organization_id(),
            "Project Name": asset.get("name", ""),
            "Project Owner": asset["owners"],
            "Ancestors": asset.get("ancestors", ""),
            "Update Time (In UTC)": convert_string_to_date_format(asset.get("updateTime", "")),
        }
        hr_asset_list.append(hr_asset_dict)

    headers = ["Organization ID", "Project Name", "Project Owner", "Ancestors", "Update Time (In UTC)"]
    readable_output = tableToMarkdown("", t=hr_asset_list, headers=headers, removeNull=True)

    return readable_output, remove_empty_elements(assets)


def find_asset_owners(asset: dict) -> list:
    """
    Retrieve owners from a cloud asset
    :param asset: asset from which to extract owners
    :return: a list of owners of provided asset
    """
    iam_policies = asset.get("iamPolicy", {})

    for binding in iam_policies.get("bindings", []):
        if binding["role"] == "roles/owner":
            return binding["members"]
    return []


def get_update_mask_for_update_finding(body: dict[str, Any], update_mask: list) -> str:
    """
    Get updateMask for finding update API call.

    :param body: json body
    :param update_mask: list which contains which field should update
    :return: updateMask
    """
    for key, value in body.items():
        if key == "sourceProperties" and key not in update_mask:
            update_mask.extend(["sourceProperties." + inner_key for inner_key in value])
            continue
        if key not in update_mask:
            update_mask.append(key)
    return ",".join(update_mask)


def split_and_escape(key: str, delimiter) -> list[str]:
    """
    Split key by delimiter with escape support.

    :param key: string which will be split
    :param delimiter: delimiter
    :return: a list of the extract keys
    """
    regex = r"(?<!\\)" + re.escape(delimiter)
    split_keys = (x.replace(rf"\{delimiter}", delimiter) for x in re.split(regex, key))
    keys = [split_key.strip() for split_key in list(split_keys)]
    return keys


def get_markdown_link(name: str, link: str) -> str | None:
    """
    Prepare markdown supported link.

    :param name: display name of link
    :param link: link address
    :return: link in markdown format
    """
    if name and link:
        return MARKDOWN_LINK.format(name, link)
    return None


def extract_project_id_from_service_account(service_account_json: str) -> str:
    """
    Extracts project name from a client secret json

    :param service_account_json: service account json string
    :return:
    """
    service_account_json = safe_load_non_strict_json(service_account_json)  # type: ignore
    project_id = ""
    if isinstance(service_account_json, dict):
        project_id = service_account_json.get("project_id")  # type: ignore
    if isinstance(project_id, list):
        project_id = project_id[0]
    return project_id


def get_finding_id_from_path(finding_path: str) -> str:
    """
    Get finding ID from finding path

    :param finding_path: fully qualified path of finding
    :return: finding id
    """
    id_list = finding_path.split("findings/", 1)[-1:]
    if id_list:
        return id_list[0]
    return ""


def convert_messages_to_incidents(messages: dict[str, Any]) -> tuple[list, list]:
    """
    convert pub/sub messages to incidents

    :param messages: pub/sdub message
    :return: list of incidents and list of ack ids
    """
    incidents = []
    acknowledges = []

    data_list = messages.get("receivedMessages", [])

    for data in data_list:
        acknowledges.append(data.get("ackId"))
        encoded_data = data.get("message", {}).get("data", "")
        try:
            data = base64.b64decode(encoded_data).decode()
            json_data = json.loads(data)

            finding = json_data.get("finding", {})
            finding_name = finding.get("name", "")
            # Support for only finding ingestion for Google Cloud SCC
            if not finding_name:
                continue
            incident_name = get_finding_id_from_path(finding_name)
            create_time = finding.get("createTime")

            json_data["custom"] = prepare_markdown_fields_for_fetch_incidents(json_data)
            json_data["finding_url"] = GoogleNameParser.get_finding_url(finding_name)

            incidents.append(
                {
                    "name": f"{INCIDENT_NAME_PREFIX} - {incident_name}",
                    "occurred": create_time,
                    "rawJSON": json.dumps(json_data),
                    "details": json.dumps(json_data),
                }
            )
        except Exception as e:
            demisto.error(ERROR_MESSAGES["INVALID_INCIDENT"].format(str(e)))

    acknowledges = [ack_id for ack_id in acknowledges if ack_id]
    return incidents, acknowledges


""" COMMANDS """


def test_module(params: dict[str, Any]) -> None:
    """
    Test authentication using service json
    """
    # Basic validation on configuration parameter
    validate_configuration_param(params)

    # Validate Service Account JSON and Organization ID
    validate_service_account_and_organization_name(params)
    if params.get("isFetch", False):
        # Validate Project ID and Subscription ID.
        validate_project_and_subscription_id(params)
    demisto.results("ok")


def fetch_incidents(client: GooglePubSubClient, params: dict[str, Any]) -> list | None:
    """
    Prepares incidents from past activity in Google Drive.

    :param client: GooglePubSubClient object
    :param params: arguments for fetch-incident.

    :return: incidents (``List[dict]``): List of incidents that will be created in XSOAR.
    """
    validate_configuration_param(params)
    max_messages = params.get("max_fetch") or DEFAULT_MAX_FETCH_VALUE

    messages = client.pull_messages(max_messages=max_messages)
    incidents, acks_id = convert_messages_to_incidents(messages)
    if acks_id:
        client.acknowledge_messages(acks_id)
    return incidents


@logger
def asset_list_command(client: GoogleSccClient, args: dict) -> CommandResults:
    """
    Lists an organization's assets.
    :param client: SccClient Object.
    :param args: Command argument(s).
    :return: CommandResults object with context and human-readable.
    """
    # To validate arguments.
    page_size = (
        validate_get_int(args.get("pageSize"), ERROR_MESSAGES["INVALID_PAGE_SIZE_ERROR"], MAX_PAGE_SIZE) or DEFAULT_PAGE_SIZE
    )

    resource_type = args.get("resourceType", "")
    project = args.get("project", "")
    field_mask = args.get("fieldMask", None)
    order_by = args.get("orderBy", None)
    active_assets_only = args.get("activeAssetsOnly", "false")
    filter_string = args.get("filter", "")
    read_time = args.get("readTime", None)
    compare_duration = args.get("compareDuration", None)
    page_token = args.get("pageToken", None)

    # Creating filter
    filter_string = create_filter_list_assets(resource_type, project, filter_string, active_assets_only)
    demisto.debug(f"running command using the following filter: {filter_string}")

    # Build a request
    parent = GoogleNameParser.get_organization_path()
    raw_response = client.get_assets(
        parent, compare_duration, field_mask, filter_string, order_by, page_size, page_token, read_time
    )
    result = deepcopy(raw_response)  # To preserve original API response

    # Preparing list of entry context and human readable
    ec_asset_dict, readable_output = prepare_outputs_for_list_assets(result)

    return CommandResults(readable_output=readable_output, outputs=ec_asset_dict, raw_response=raw_response)


@logger
def finding_list_command(client: GoogleSccClient, args: dict) -> CommandResults:
    """
    Lists an organization or source's findings.

    :param client: SccClient Object.
    :param args: Command argument(s).
    :return: CommandResults object with context and human-readable.
    """

    # Get command args
    severity = [value for value in args.get("severity", "").split(",") if value.strip()]
    category = args.get("category", "")
    source_type = args.get("sourceTypeId", "-")
    page_size = args.get("pageSize")
    state = [value for value in args.get("state", "").split(",") if value.strip()]
    filter_string = args.get("filter", "")
    order_by = args.get("orderBy", None)
    compare_duration = args.get("compareDuration", None)
    field_mask = args.get("fieldMask", None)
    read_time = args.get("readTime", None)
    page_token = args.get("pageToken", None)

    # Validates command args
    validate_state_and_severity_list(state, severity)
    page_size = validate_get_int(page_size, ERROR_MESSAGES["INVALID_PAGE_SIZE_ERROR"], MAX_PAGE_SIZE) or DEFAULT_PAGE_SIZE

    # Creating filter
    filter_string = create_filter_list_findings(category, filter_string, severity, state)
    demisto.debug(f"running command using the following filter: {filter_string}")

    parent = GoogleNameParser.get_source_path(source_type)
    raw_response = client.get_findings(
        parent, compare_duration, field_mask, filter_string, order_by, page_size, page_token, read_time
    )
    result = deepcopy(raw_response)  # To preserve original API response
    readable_output, context = prepare_hr_and_ec_for_list_findings(result)

    return CommandResults(readable_output=readable_output, outputs=context, raw_response=raw_response)


@logger
def finding_update_command(client: GoogleSccClient, args: dict) -> CommandResults:
    """
    Lists an organization or source's findings.

    :param client: SccClient Object.
    :param args: Command argument(s).
    :return: CommandResults object with context and human-readable.
    """

    # Get validated command args
    arguments = get_and_validate_args_finding_update(args)

    # Get response
    result = client.update_finding(*arguments)

    readable_output, context = prepare_hr_and_ec_for_update_finding(result)

    return CommandResults(
        readable_output=readable_output,
        outputs_key_field="name",
        outputs_prefix=OUTPUT_PREFIX["FINDING"],
        outputs=context,
        raw_response=result,
    )


@logger
def finding_state_update_command(client: GoogleSccClient, args: dict) -> CommandResults:
    """
    Update the state of organization's or source's finding.

    :param client: SccClient Object.
    :param args: Command argument(s).
    :return: The updated state.
    """

    # Get validated command args
    arguments = get_and_validate_args_finding_state_update(args)

    # Get response
    result = client.update_state(*arguments)

    readable_output, context = prepare_hr_and_ec_for_update_finding(result)

    return CommandResults(
        readable_output=readable_output,
        outputs_key_field="name",
        outputs_prefix=OUTPUT_PREFIX["FINDING"],
        outputs=context,
        raw_response=result,
    )


@logger
def cloud_asset_list_command(client: GoogleCloudAssetClient, args: dict) -> CommandResults:
    """
    Lists assets with time and resource types.

    :param client: GoogleCloudAssetClient Object.
    :param args: Command argument(s).
    :return: CommandResults object with context and human-readable.
    """
    parent = args.get("parent", GoogleNameParser.get_organization_path())
    asset_types = argToList(args.get("assetTypes"))
    content_type = "RESOURCE"
    page_size = args.get("pageSize")
    page_token = args.get("pageToken")
    read_time = args.get("readTime")  # type: ignore

    # Validates command args
    page_size = validate_get_int(page_size, ERROR_MESSAGES["INVALID_PAGE_SIZE_ERROR"], MAX_PAGE_SIZE) or DEFAULT_PAGE_SIZE
    if read_time:
        read_time = convert_string_to_date_format(read_time, ISO_DATE_FORMAT)  # type: ignore

    if args.get("readTime") and not read_time:
        raise ValueError(ERROR_MESSAGES["INVALID_DATE_TIME"].format("readTime"))

    raw_response = client.get_assets(
        parent,
        asset_types,
        content_type,
        int(page_size),
        page_token,  # type:ignore
        read_time,
    )  # type:ignore
    result = deepcopy(raw_response)  # To preserve original API response
    readable_output, context = prepare_hr_and_ec_for_cloud_asset_list(result)

    return CommandResults(readable_output=readable_output, outputs=context, raw_response=raw_response)


@logger
def cloud_asset_owner_get_command(client: GoogleCloudAssetClient, args: dict) -> CommandResults:
    """
    Gets the owner information for the provided projects
    Lists assets with time and resource types.

    :param client: GoogleCloudAssetClient Object.
    :param args: Command argument(s).
    :return: CommandResults object with context and human-readable.
    """
    project_names = argToList(args.get("projectName"))
    max_iterations = args.get("maxIteration", "2")

    parent = GoogleNameParser.get_organization_path()
    asset_types = ["cloudresourcemanager.googleapis.com/Project"]
    content_type = "IAM_POLICY"

    # Validate command args.
    for project_name in project_names:
        validate_with_regex(ERROR_MESSAGES["INVALID_PROJECT_NAME_ERROR"], r"^projects\/\d{1,}$", project_name)
    max_iterations = (
        validate_get_int(max_iterations, ERROR_MESSAGES["INVALID_MAX_ITERATION_ERROR"], MAX_ITERATION) or DEFAULT_MAX_ITERATION
    )

    # Remove duplicate project names and extract id's.
    project_ids = list(map(lambda name: name.split("/")[-1], set(project_names)))  # noqa: C417
    page_token = ""
    matching_assets = []
    iteration = 0
    response = {}

    # Call API multiple times till we find all the projects or reach max iterations or run out of projects.
    while project_ids:
        response = client.get_assets(parent, asset_types, content_type, MAX_PAGE_SIZE, page_token, None)

        # search assets on project ids and append them to matching assets
        for asset in response["assets"]:
            asset_id = asset["name"].split("/")[-1]
            if asset_id in project_ids:
                matching_assets.append(asset)
                project_ids.remove(asset_id)

        iteration += 1
        if iteration >= max_iterations or not response.get("nextPageToken"):
            break

        page_token = response["nextPageToken"]

    readable_output, context = prepare_hr_and_ec_for_cloud_asset_owners_get(matching_assets, response.get("readTime", ""))
    return CommandResults(
        readable_output=readable_output,
        outputs=context,
        raw_response=context,
        outputs_key_field="name",
        outputs_prefix=OUTPUT_PREFIX["GET_OWNER"],
    )


def main() -> None:
    """
    PARSE AND VALIDATE INTEGRATION PARAMS
    """
    # Commands dictionary
    commands: dict[str, Callable] = {
        "google-cloud-scc-asset-list": asset_list_command,
        "google-cloud-scc-finding-list": finding_list_command,
        "google-cloud-scc-finding-update": finding_update_command,
        "google-cloud-scc-finding-state-update": finding_state_update_command,
    }
    params = demisto.params()
    command = demisto.command()
    demisto.info(f"Command being called is {command}")
    try:
        # Trim the arguments
        args = strip_dict(demisto.args())
        client: GoogleSccClient | GooglePubSubClient | GoogleCloudAssetClient | None = None
        if command == "test-module":
            # This is the call made when pressing the integration test button.
            test_module(params)
        elif command == "google-cloud-scc-asset-resource-list":
            client = init_google_cloud_assets_client(**params)
            return_results(cloud_asset_list_command(client, args))
        elif command == "google-cloud-scc-asset-owner-get":
            client = init_google_cloud_assets_client(**params)
            return_results(cloud_asset_owner_get_command(client, args))
        elif command == "fetch-incidents":
            client = init_google_pubsub_client(**params)
            incidents = fetch_incidents(client, params)
            demisto.incidents(incidents)
        elif command in commands:
            client = init_google_scc_client(**params)
            return_results(commands[command](client, args))
    # Log exceptions
    except Exception as e:
        return_error(f"Failed to execute {demisto.command()} command. Error: {e!s}")


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