Skip to content

Commit

Permalink
[Marketplace Contribution] - EXPANDR-7038 - Azure Resource Graph (#35326
Browse files Browse the repository at this point in the history
)

* [Marketplace Contribution] - EXPANDR-7038 - Azure Resource Graph (#32121)

* Add Pack ReadMe

* Add integration

* Add integration description, image, and secrets ignore file

* Add metadata file and pack ignore

* Add test files and tests first

* Add Integration ReadMe

* Update marketplaces

* Update commands descriptions and output

* Update secrets ignore

* Resize image

* Update integration yml commands

* Update integration readme

* Resize image

* Address doc review and some design review comments

* Update client credential flow section of ReadMe

* Update list_operations_command to support a limit argument

* Update azure-rg-list-operations in ReadMe

* Update azure-rg-list-operations to support paging

* Update azure-rg-query to support paging

* Update tests

* Remove Comments

* Update integration configuration yml settings

* Add management_groups & subscriptions parameters for query command

* Add suggested changes from second review

* Update Readme and Description from code review

* Update integration files with code review suggestions

* Update defaultValue key in YAML and docker version

* Update section titles in YAML

* Remove subscription_id from client and format

- Subscription ID is not used during configuration
- Fixed usage of wrong variable in query command

* Remove DefaultValues

- The default values are not necessary and would make the conditionals for limits and paging more complex

* Update ReadMe

* Formatting

* Remove subscription_id from client in test file

* Update tests and fix mypy errors

* Update address mypy errors

* Update README.md

* Apply suggestions from code review

* Update README.md

---------

Co-authored-by: John <40349459+BigEasyJ@users.noreply.github.com>
Co-authored-by: Jasmine Beilin <71636766+JasBeilin@users.noreply.github.com>
  • Loading branch information
3 people authored Jul 11, 2024
1 parent e20b7df commit 7030e67
Show file tree
Hide file tree
Showing 14 changed files with 946 additions and 0 deletions.
Empty file.
10 changes: 10 additions & 0 deletions Packs/AzureResourceGraph/.secrets-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
https://management.azure.com
https://xsoar.pan.dev*
https://portal.azure.com
https://login.microsoftonline.com'
AzureResourceGraphClient
Microsoft.ResourceGraph/*
"microsoft.network/*
azure-rg
Query
11.22.33.44
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
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):

self.ms_client = MicrosoftClient(
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=Scopes.management_azure,
certificate_thumbprint=certificate_thumbprint, private_key=private_key,
command_prefix="azure-rg",
)

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)

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)
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: {str(e)}')


if __name__ in ['__main__', 'builtin', 'builtins']:
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
category: Cloud Services
sectionorder:
- Connect
- Collect
commonfields:
id: Azure Resource Graph
version: -1
configuration:
- display: Server URL (e.g., https://management.azure.com)
name: host
defaultvalue: https://management.azure.com
type: 0
required: true
section: Connect
- displaypassword: ID / Client ID
name: cred_auth_id
type: 9
required: false
hiddenusername: true
display: ""
section: Connect
- displaypassword: Token / Tenant ID
name: cred_token
type: 9
required: false
hiddenusername: true
display: ""
section: Connect
- displaypassword: Key / Client Secret
name: cred_enc_key
type: 9
required: false
hiddenusername: true
display: ""
section: Connect
- display: ""
displaypassword: Certificate Thumbprint
name: cred_certificate_thumbprint
type: 9
required: false
hiddenusername: true
additionalinfo: Used for certificate authentication. As appears in the "Certificates & secrets" page of the app.
section: Connect
- display: Private Key
name: private_key
type: 14
additionalinfo: Used for certificate authentication. The private key of the registered certificate.
hidden: true
section: Connect
- display: Use system proxy settings
name: proxy
type: 8
required: false
- display: Trust any certificate (not secure)
name: unsecure
type: 8
required: false
description: 'Azure Resource Graph integration is designed to allow for executing Azure Resource Graph commands, like querying resource data.'
display: Azure Resource Graph
name: Azure Resource Graph
script:
commands:
- name: azure-rg-auth-reset
arguments: []
description: Run this command if for some reason you need to rerun the authentication process.
- name: azure-rg-list-operations
arguments:
- description: 'The maximum number of operations to return. NOTE: Do not use with "page" and "page_size".'
name: limit
- description: The maximum number of operations to return for page.
name: page_size
- description: The page number to return.
name: page
outputs:
- contextPath: AzureResourceGraph.Operations
description: A list of available Azure Resource Graph operations permissions and descriptions.
type: String
description: Gets all Azure Resource Graph operations permissions and descriptions.
- name: azure-rg-query
arguments:
- description: 'The maximum number of operations to return (Default is 50). NOTE: Do not use with "page" and "page_size".'
name: limit
- description: The maximum number of operations to return for page.
name: page_size
- description: The page number to return.
name: page
- name: management_groups
description: "Azure management groups against which to execute the query. Example: 'mg1, mg2'."
isArray: true
- name: subscriptions
description: "Azure subscriptions against which to execute the query. Example: 'sub1, sub2'."
isArray: true
- name: query
required: true
description: |-
The query for the Azure Resource Graph to execute.
For example: `resources | where (resourceGroup =~ ('demisto-sentinel2'))`.
The query is based on Kusto Query Language (KQL). Reference: https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/
outputs:
- contextPath: AzureResourceGraph.Query
description: Data returned from query.
type: String
description: 'Executes a given Azure Resource Graph Query. (Ex: query="Resources | project name, type | limit 5 | order by name asc").'
dockerimage: demisto/crypto:1.0.0.100307
runonce: false
script: ''
subtype: python3
type: python
fromversion: 6.10.0
tests:
- No tests (auto formatted)
Loading

0 comments on commit 7030e67

Please sign in to comment.