Azure Resource Graph

Azure Resource Graph integration is designed to allow for executing Azure Resource Graph commands, like querying resource data.

Cloud Services · Azure Resource Graph

Details

IDAzure Resource Graph
ProviderMicrosoft
CategoryCloud Services
From Version6.10.0
Docker Imagedemisto/crypto:1.0.0.10120494
Supported ModulesAgentix XSIAM Cloud Posture Security

README

Azure Resource Graph is an Azure service designed to extend Azure Resource Management by providing efficient and performant resource exploration with the ability to query at scale across a given set of resources.

Authorize Cortex XSOAR for Azure Resource Graph (Self-Deployed Configuration)

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

Authentication

To enable and configure authentication using self deployed app, follow the Self-Deployed Application Authentication

For more details about the authentication used in this integration, see Microsoft Integrations - Authentication.

  • After authorizing the Self-Deployed Application, you will get an ID, Token, and Key, which should be inserted in the integration instance configuration’s corresponding fields. After giving consent, the application has to have a role assigned so it can access the relevant resources per subscription.
  • In order to assign a role to the application after consent was given:
    • Go to the Azure Portal UI.
    • Go to Subscriptions, and then Access Control (IAM).
    • Click “Add role assignment”.
    • Create a new role or select a role that includes permissions for the queries you plan to run.
    • Select the Azure Resource Graph application. By default, Azure Applications aren’t displayed in the available options. To find your application, search for the name and select it.

Client Credentials Flow


Follow these steps for client-credentials configuration:.

  1. In the instance configuration, select the Use a self-deployed Azure application - Client Credentials Authorization Flow checkbox.
  2. Enter your Client ID in the ID (Client ID) parameter.
  3. Enter your Client Secret in the Key (Client Secret) parameter.
  4. Enter your Tenant ID in the Token (Tenant ID) parameter.
  5. Click Test to validate the URLs, token, and connection.

To use The Azure Resource Graph, you must have appropriate rights in Azure role-based access control (Azure RBAC) with at least read access to the resources you want to query. No results are returned if you don’t have at least read permissions to the Azure object or object group.

Configure Azure Resource Graph in Cortex

Parameter Description Required
Token / Tenant ID Received from the authorization process or from the self-deployed configuration process (find the tenant ID in your app overview page in the Azure portal) False
Token / Tenant ID   False
ID / Client ID Received from the authorization process or from the self-deployed configuration process. False
Key / Client Secret   False
Certificate Thumbprint Used for certificate authentication. As appears in the “Certificates & secrets” page of the app. False
Private Key Used for certificate authentication. The private key of the registered certificate. False
Trust any certificate (not secure)   False
Use system proxy settings   False
Gov Account Select this checkbox if you are using a US Government cloud account (GCC-High). When enabled, the integration uses the US Government management endpoint and Azure US GCC-High cloud configuration. False

Commands

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

azure-rg-list-operations


Base Command

azure-rg-list-operations

Input

Argument Name Description Required
limit The maximum number of operations to return (Default is 50). Optional

Context Output

Path Type Description
AzureResourceGraph.Operations String A list of available Azure Resource Graph operations permissions and descriptions.

Command Example

!azure-rg-list-operations limit=50

Context Example

{
  "value": [
      {
          "name": "Microsoft.ResourceGraph/operations/read",
          "display": {
              "provider": "Microsoft Resource Graph",
              "resource": "Operation",
              "operation": "Get Operations",
              "description": "Gets the list of supported operations"
          }
      },
      {
          "name": "Microsoft.ResourceGraph/resources/read",
          "display": {
              "provider": "Microsoft Resource Graph",
              "resource": "Resources",
              "operation": "Query resources",
              "description": "Submits a query on resources within specified subscriptions, management groups or tenant scope"
          }
      }
  ]
}

Human Readable Output

Display Name
provider: Microsoft Resource Graph
resource: Operation
operation: Get Operations
description: Gets the list of supported operations
Microsoft.ResourceGraph/operations/read
provider: Microsoft Resource Graph
resource: Resources
operation: Query resources
description: Submits a query on resources within specified subscriptions, management groups or tenant scope
Microsoft.ResourceGraph/resources/read

azure-rg-query


Base Command

azure-rg-query

Input

Argument Name Description Required
query The query to execute. Required

Context Output

Path Type Description
AzureResourceGraph.Query String Data returned from query.

Command Example

!azure-rg-query query="resources | where type == 'microsoft.network/publicipaddresses'| where properties['ipAddress'] == '11.22.33.44' | project name, id, tenantId, subscriptionId, resourceGroup | join kind=inner (resourcecontainers | where type == 'microsoft.resources/subscriptions' | project subscriptionId, properties.managementGroupAncestorsChain) on subscriptionId"

Context Example

{
    "count": 1,
    "data": [
        {
            "id": "/subscriptions/1abc234d-12a3-12a3-12a3-1234abcde123/resourceGroups/test-vm-resource-group/providers/Microsoft.Network/publicIPAddresses/test-vm-1-ip",
            "name": "test-vm-1-ip",
            "properties_managementGroupAncestorsChain": [
                {
                    "displayName": "grand-child-managment-group",
                    "name": "grand-child-managment-group"
                },
                {
                    "displayName": "child-management-group",
                    "name": "child-management-group"
                },
                {
                    "displayName": "test-new-managment-group",
                    "name": "test-new-managment-group"
                },
                {
                    "displayName": "Tenant Root Group",
                    "name": "a11111111-222-3333-12a3-1234abcde123"
                }
            ],
            "resourceGroup": "test-vm-resource-group",
            "subscriptionId": "1abc234d-12a3-12a3-12a3-1234abcde123",
            "tenantId": "a11111111-222-3333-12a3-1234abcde123"
        }
    ],
    "facets": [],
    "resultTruncated": "false",
    "totalRecords": 1
}

Human Readable Output

id name properties_managementGroupAncestorsChain resourceGroup subscriptionId tenantId              
/subscriptions/1abc234d-12a3-12a3-12a3-1234abcde123/resourceGroups/test-vm-resource-group/providers/Microsoft.Network/publicIPAddresses/test-vm-1-ip test-vm-1-ip {‘displayName’: ‘grand-child-managment-group’, ‘name’: ‘grand-child-managment-group’},
{‘displayName’: ‘child-management-group’, ‘name’: ‘child-management-group’},
{‘displayName’: ‘test-new-managment-group’, ‘name’: ‘test-new-managment-group’},
{‘displayName’: ‘Tenant Root Group’, ‘name’: ‘a11111111-222-3333-12a3-1234abcde123’}
test-vm-resource-group 1abc234d-12a3-12a3-12a3-1234abcde123 a11111111-222-3333-12a3-1234abcde123              

azure-rg-auth-reset

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

Base Command

azure-rg-auth-reset

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Configuration parameters

  • host — Server URL (e.g., https://management.azure.com) (required)
  • cred_auth_id
  • cred_token
  • cred_enc_key
  • cred_certificate_thumbprint
  • private_key — Private Key
  • gov_account — Use Gov Account Settings
  • proxy — Use system proxy settings
  • unsecure — Trust any certificate (not secure)

Commands (3)

  • azure-rg-auth-reset

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

  • azure-rg-list-operations

    Gets all Azure Resource Graph operations permissions and descriptions.

  • azure-rg-query

    Executes a given Azure Resource Graph Query. (Ex: query="Resources | project name, type | limit 5 | order by name asc").

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401
from MicrosoftApiModule import *  # noqa: E402

"""GLOBAL VARS"""
API_VERSION = "2022-10-01"
APP_NAME = "azure-resource-graph"
MAX_PAGE_SIZE = 50


class AzureResourceGraphClient:
    """
    Azure Resource Graph Client enables authorized access to query for resource information.
    """

    def __init__(
        self,
        tenant_id,
        auth_id,
        enc_key,
        app_name,
        base_url,
        verify,
        proxy,
        self_deployed,
        ok_codes,
        server,
        certificate_thumbprint,
        private_key,
        is_gov: bool = False,
    ):
        if is_gov:
            scope = "https://management.usgovcloudapi.net/.default"
            azure_cloud = AZURE_US_GCC_HIGH_CLOUD
            server = "https://management.usgovcloudapi.net"
            base_url = f"{server}/providers/Microsoft.ResourceGraph"
        else:
            scope = Scopes.management_azure
            azure_cloud = None

        client_kwargs: dict[str, Any] = {
            "tenant_id": tenant_id,
            "auth_id": auth_id,
            "enc_key": enc_key,
            "app_name": app_name,
            "base_url": base_url,
            "verify": verify,
            "proxy": proxy,
            "self_deployed": self_deployed,
            "ok_codes": ok_codes,
            "scope": scope,
            "certificate_thumbprint": certificate_thumbprint,
            "private_key": private_key,
            "command_prefix": "azure-rg",
        }
        if azure_cloud:
            client_kwargs["azure_cloud"] = azure_cloud

        self.ms_client = MicrosoftClient(**client_kwargs)

        self.server = server
        self.default_params = {"api-version": API_VERSION}

    def list_operations(self):
        return self.ms_client.http_request(
            method="GET",
            full_url=f"{self.server}/providers/Microsoft.ResourceGraph/operations",
            params=self.default_params,
        )

    def query_resources(self, query, paging_options: dict[str, Any], subscriptions: list, management_groups: list):
        request_data = {"query": query, "options": paging_options}

        if subscriptions:
            request_data["subscriptions"] = subscriptions

        if management_groups:
            request_data["managementGroups"] = management_groups

        return self.ms_client.http_request(
            method="POST",
            full_url=f"{self.server}/providers/Microsoft.ResourceGraph/resources",
            params=self.default_params,
            json_data=request_data,
        )


def query_resources_command(client: AzureResourceGraphClient, args: dict[str, Any]) -> CommandResults:
    limit = arg_to_number(args.get("limit"))
    page_size = arg_to_number(args.get("page_size"))
    page_number = arg_to_number(args.get("page"))
    management_groups = argToList(args.get("management_groups", None))
    subscriptions = argToList(args.get("subscriptions", None))

    query = args.get("query")

    list_of_query_results = []
    total_records = 0

    if page_number and not page_size:
        raise DemistoException('Please enter a value for "page_size" when using "page".')
    if page_size and not page_number:
        raise DemistoException('Please enter a value for "page" when using "page_size".')

    if page_number and page_size:
        skip = (page_number - 1) * page_size + 1
        params = {"$skip": skip, "$top": page_size}
        response = client.query_resources(
            query=query, paging_options=params, management_groups=management_groups, subscriptions=subscriptions
        )
        total_records = response.get("totalRecords")
        list_of_query_results = response.get("data")
    elif page_number:
        params = {"$top": page_size}  # type: ignore
        response = client.query_resources(
            query=query, paging_options=params, management_groups=management_groups, subscriptions=subscriptions
        )
        total_records = response.get("totalRecords")
        list_of_query_results = response.get("data")
    else:
        query_results = []
        skip_token = ""
        counter = 0

        while True:
            if skip_token:
                params = {"$skipToken": skip_token}  # type: ignore
            else:
                params = {}

            response = client.query_resources(
                query=query, paging_options=params, management_groups=management_groups, subscriptions=subscriptions
            )

            list_of_query_results = response.get("data")
            query_results.extend(list_of_query_results)
            counter += len(list_of_query_results)
            if limit and counter >= limit:
                break
            if "$skipToken" in response and (not limit or counter < limit):
                skip_token = response.get("$skipToken")
            else:
                break

        total_records = response.get("totalRecords")
        list_of_query_results = query_results

    if limit:
        list_of_query_results = list_of_query_results[:limit]

    title = f"Results of query:\n```{query}```\n\n Total Number of Possible Records:{total_records} \n"
    human_readable = tableToMarkdown(title, list_of_query_results, removeNull=True)

    return CommandResults(
        readable_output=human_readable,
        outputs_prefix="AzureResourceGraph.Query",
        outputs_key_field="Query",
        outputs=list_of_query_results,
        raw_response=response,
    )


def list_operations_command(client: AzureResourceGraphClient, args: dict[str, Any]) -> CommandResults:
    limit = arg_to_number(args.get("limit"))
    page_size = arg_to_number(args.get("page_size"))
    page = arg_to_number(args.get("page"))

    response = client.list_operations()
    operations_list = response.get("value")
    md_output_notes = ""

    if page and not page_size:
        raise DemistoException('Please enter a value for "page_size" when using "page".')
    if page_size and not page:
        raise DemistoException('Please enter a value for "page" when using "page_size".')
    if page and page_size:
        if limit:
            md_output_notes = '"limit" was ignored for paging parameters.'
            demisto.debug('"limit" was ignored for paging parameters.')
        operations_list = pagination(operations_list, page_size, page)

    if page_size:
        limit = page_size

    operations = []
    for operation in operations_list[:limit]:
        operation_context = {"Name": operation.get("name"), "Display": operation.get("display")}
        operations.append(operation_context)

    title = "List of Azure Resource Graph Operations\n\n" + md_output_notes
    human_readable = tableToMarkdown(title, operations, removeNull=True)

    return CommandResults(
        readable_output=human_readable,
        outputs_prefix="AzureResourceGraph.Operations",
        outputs_key_field="Operations",
        outputs=operations,
        raw_response=response,
    )


def test_module(client: AzureResourceGraphClient):
    # Implicitly will test tenant, enc_token and subscription_id
    try:
        result = client.list_operations()
        if result:
            return "ok"
    except DemistoException as e:
        return_error(f"Test connection failed with message {e}")


# Helper Methods


def pagination(response, page_size, page_number):
    """Method to generate a page (slice) of data.
    Args:
        response: The response from the API.
        limit: Maximum number of objects to retrieve.
        page: Page number
    Returns:
        Return a list of objects from the response according to the page and limit per page.
    """
    if page_size > MAX_PAGE_SIZE:
        page_size = MAX_PAGE_SIZE

    starting_index = (page_number - 1) * page_size
    ending_index = starting_index + page_size
    return response[starting_index:ending_index]


def validate_connection_params(
    tenant: str = None,
    auth_and_token_url: str = None,
    enc_key: str = None,
    certificate_thumbprint: str = None,
    private_key: str = None,
) -> None:
    if not tenant or not auth_and_token_url:
        raise DemistoException("Token and ID must be provided.")

    elif not enc_key and not (certificate_thumbprint and private_key):
        raise DemistoException(
            "Key or Certificate Thumbprint and Private Key must be providedFor further information see "
            "https://xsoar.pan.dev/docs/reference/articles/microsoft-integrations---authentication"
        )


def main():
    params: dict = demisto.params()
    args = demisto.args()
    server = params.get("host", "https://management.azure.com").rstrip("/")
    tenant = params.get("cred_token", {}).get("password") or params.get("tenant_id")
    auth_and_token_url = params.get("cred_auth_id", {}).get("password") or params.get("auth_id")
    enc_key = params.get("cred_enc_key", {}).get("password") or params.get("enc_key")
    certificate_thumbprint = params.get("cred_certificate_thumbprint", {}).get("password") or params.get("certificate_thumbprint")
    private_key = params.get("private_key")
    verify = not params.get("unsecure", False)
    proxy: bool = params.get("proxy", False)
    is_gov: bool = argToBoolean(params.get("gov_account", False))

    validate_connection_params(tenant, auth_and_token_url, enc_key, certificate_thumbprint, private_key)

    ok_codes = (200, 201, 202, 204)

    commands_without_args: Dict[Any, Any] = {"test-module": test_module}

    commands_with_args: Dict[Any, Any] = {
        "azure-rg-query": query_resources_command,
        "azure-rg-list-operations": list_operations_command,
    }

    """EXECUTION"""
    command = demisto.command()
    LOG(f"Command being called is {command}")

    try:
        # Initial setup
        base_url = f"{server}/providers/Microsoft.ResourceGraph"
        client = AzureResourceGraphClient(
            base_url=base_url,
            tenant_id=tenant,
            auth_id=auth_and_token_url,
            enc_key=enc_key,
            app_name=APP_NAME,
            verify=verify,
            proxy=proxy,
            self_deployed=True,
            ok_codes=ok_codes,
            server=server,
            certificate_thumbprint=certificate_thumbprint,
            private_key=private_key,
            is_gov=is_gov,
        )
        if command == "azure-rg-auth-reset":
            return_results(reset_auth())
        elif command in commands_without_args:
            return_results(commands_without_args[command](client))
        elif command in commands_with_args:
            return_results(commands_with_args[command](client, args))
        else:
            raise NotImplementedError(f'Command "{command}" is not implemented.')
    except Exception as e:
        return_error(f"Failed to execute {command} command. Error: {e!s}")


if __name__ in ["__main__", "builtin", "builtins"]:
    main()