-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Marketplace Contribution] - EXPANDR-7038 - Azure Resource Graph (#35326
) * [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
1 parent
e20b7df
commit 7030e67
Showing
14 changed files
with
946 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
267 changes: 267 additions & 0 deletions
267
Packs/AzureResourceGraph/Integrations/AzureResourceGraph/AzureResourceGraph.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
111 changes: 111 additions & 0 deletions
111
Packs/AzureResourceGraph/Integrations/AzureResourceGraph/AzureResourceGraph.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.