-
Notifications
You must be signed in to change notification settings - Fork 3.3k
SQL database audit and threat detection commands #2536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
36b820f
20adafd
5a474f5
b389db1
4fccee1
8e48a89
2078105
a8518a7
04748e2
369b5c2
335234c
0b05fad
a244010
411f26b
0fb8ef2
47a0463
85cdc83
1633bbe
5cf8fa3
055210f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,14 +8,23 @@ | |
| get_sql_elasticpools_operations | ||
| ) | ||
|
|
||
| from azure.cli.core.commands.client_factory import get_subscription_id | ||
| from azure.cli.core.commands.client_factory import ( | ||
| get_mgmt_service_client, | ||
| get_subscription_id) | ||
| from azure.cli.core._util import CLIError | ||
| from azure.mgmt.sql.models.sql_management_client_enums import ( | ||
| BlobAuditingPolicyState, | ||
| CreateMode, | ||
| DatabaseEditions, | ||
| ReplicationRole, | ||
| ServiceObjectiveName, | ||
| SecurityAlertPolicyState, | ||
| ServiceObjectiveName | ||
| ) | ||
| from azure.mgmt.resource.resources import ResourceManagementClient | ||
| from azure.mgmt.storage import StorageManagementClient | ||
|
|
||
| # url parse package has different names in Python 2 and 3. 'six' package works cross-version. | ||
| from six.moves.urllib.parse import (quote, urlparse) # pylint: disable=import-error | ||
|
|
||
| ############################################### | ||
| # Common funcs # | ||
|
|
@@ -98,8 +107,6 @@ def _db_create_special( | |
| resource_group_name=dest_db.resource_group_name) | ||
|
|
||
| # Set create mode properties | ||
| # url parse package has different names in Python 2 and 3. 'six' package works cross-version. | ||
| from six.moves.urllib.parse import quote # pylint: disable=import-error | ||
| subscription_id = get_subscription_id() | ||
| kwargs['source_database_id'] = ( | ||
| '/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Sql/servers/{}/databases/{}' | ||
|
|
@@ -336,6 +343,196 @@ def db_update( | |
| return instance | ||
|
|
||
|
|
||
| ##### | ||
| # sql server audit-policy & threat-policy | ||
| ##### | ||
|
|
||
|
|
||
| # Finds a storage account's resource group by querying ARM resource cache. | ||
| # Why do we have to do this: so we know the resource group in order to later query the storage API | ||
| # to determine the account's keys and endpoint. Why isn't this just a command line parameter: | ||
| # because if it was a command line parameter then the customer would need to specify storage | ||
| # resource group just to update some unrelated property, which is annoying and makes no sense to | ||
| # the customer. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
| def _find_storage_account(name): | ||
| resource_type = 'Microsoft.Storage/storageAccounts' | ||
|
||
|
|
||
| client = get_mgmt_service_client(ResourceManagementClient) | ||
| resources = list(client.resources.list( | ||
| filter="name eq '{}' and resourceType eq '{}'" | ||
| .format(name, resource_type))) | ||
|
|
||
| if len(resources) == 0: | ||
| raise CLIError('No resource with name {} and type {} was found.' | ||
| .format(name, resource_type)) | ||
|
|
||
| if len(resources) > 1: | ||
| raise CLIError('Multiple resources with name {} and type {} were found.' | ||
| .format(name, resource_type)) | ||
|
|
||
| # Split the uri and return just the resource group | ||
| return resources[0].id.split('/')[4] | ||
|
|
||
|
|
||
| # Determines storage account name from endpoint url string. | ||
| # e.g. 'https://mystorage.blob.core.windows.net' -> 'mystorage' | ||
| def _get_storage_account_name(storage_endpoint): | ||
| return urlparse(storage_endpoint).netloc.split('.')[0] | ||
|
|
||
|
|
||
| # Gets storage account key by querying storage ARM API. | ||
| def _get_storage_endpoint( | ||
| storage_account, | ||
| resource_group_name): | ||
|
|
||
| # Get storage account | ||
| client = get_mgmt_service_client(StorageManagementClient) | ||
| account = client.storage_accounts.get_properties( | ||
| resource_group_name=resource_group_name, | ||
| account_name=storage_account) | ||
|
|
||
| # Get endpoint | ||
| return account.primary_endpoints.blob # pylint: disable=no-member | ||
|
||
|
|
||
|
|
||
| # Gets storage account key by querying storage ARM API. | ||
| def _get_storage_key( | ||
| storage_account, | ||
| resource_group_name, | ||
| use_secondary_key): | ||
|
|
||
| # Get storage keys | ||
| client = get_mgmt_service_client(StorageManagementClient) | ||
| keys = client.storage_accounts.list_keys( | ||
| resource_group_name=resource_group_name, | ||
| account_name=storage_account) | ||
|
|
||
| # Choose storage key | ||
| index = 1 if use_secondary_key else 0 | ||
| return keys.keys[index].value # pylint: disable=no-member | ||
|
|
||
|
|
||
| # Common code for updating audit and threat detection policy | ||
| def _db_security_policy_update( # pylint: disable=too-many-arguments | ||
| instance, | ||
| enabled, | ||
| storage_account, | ||
| storage_endpoint, | ||
| storage_account_access_key, | ||
| use_secondary_key): | ||
|
|
||
| # Validate storage endpoint arguments | ||
| if storage_endpoint is not None and storage_account is not None: | ||
| raise CLIError('--storage-endpoint and --storage-account cannot both be specified.') | ||
|
|
||
| # Set storage endpoint | ||
| if storage_endpoint is not None: | ||
| instance.storage_endpoint = storage_endpoint | ||
| if storage_account is not None: | ||
| storage_resource_group = _find_storage_account(storage_account) | ||
|
||
| instance.storage_endpoint = _get_storage_endpoint(storage_account, storage_resource_group) | ||
|
|
||
| # Set storage access key | ||
| if storage_account_access_key is not None: | ||
| # Access key is specified | ||
| instance.storage_account_access_key = storage_account_access_key | ||
| elif enabled: | ||
| # Access key is not specified, but state is Enabled. | ||
| # If state is Enabled, then access key property is required in PUT. However access key is | ||
| # readonly (GET returns empty string for access key), so we need to determine the value | ||
| # and then PUT it back. (We don't want the user to be force to specify this, because that | ||
| # would be very annoying when updating non-storage-related properties). | ||
| # This doesn't work if the user used generic update args, i.e. `--set state=Enabled` | ||
| # instead of `--state Enabled`, since the generic update args are applied after this custom | ||
| # function, but at least we tried. | ||
| if storage_account is None: | ||
| storage_account = _get_storage_account_name(instance.storage_endpoint) | ||
| storage_resource_group = _find_storage_account(storage_account) | ||
|
|
||
| instance.storage_account_access_key = _get_storage_key( | ||
| storage_account, | ||
| storage_resource_group, | ||
| use_secondary_key) | ||
|
|
||
|
|
||
| # Update audit policy. Custom update function to apply parameters to instance. | ||
| def db_audit_policy_update( # pylint: disable=too-many-arguments | ||
| instance, | ||
| state=None, | ||
| storage_account=None, | ||
| storage_endpoint=None, | ||
| storage_account_access_key=None, | ||
| audit_actions_and_groups=None, | ||
| retention_days=None): | ||
|
|
||
| # Apply state | ||
| if state is not None: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we have parameters value validation? (e.g. in the "state" case the valid set is "Enabled" and "Disabled")
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it will be validated due to (env) D:\git\azure-cli [datasecurity ≡]> az sql db audit-policy update --state adf |
||
| # pylint: disable=unsubscriptable-object | ||
| instance.state = BlobAuditingPolicyState[state.lower()] | ||
| enabled = instance.state.value.lower() == BlobAuditingPolicyState.enabled.value.lower() | ||
|
|
||
| # Set storage-related properties | ||
| _db_security_policy_update( | ||
| instance, | ||
| enabled, | ||
| storage_account, | ||
| storage_endpoint, | ||
| storage_account_access_key, | ||
| instance.is_storage_secondary_key_in_use) | ||
|
|
||
| # Set other properties | ||
| if audit_actions_and_groups is not None: | ||
| instance.audit_actions_and_groups = audit_actions_and_groups | ||
|
|
||
| if retention_days is not None: | ||
| instance.retention_days = retention_days | ||
|
|
||
| return instance | ||
|
|
||
|
|
||
| # Update threat detection policy. Custom update function to apply parameters to instance. | ||
| def db_threat_detection_policy_update( # pylint: disable=too-many-arguments | ||
| instance, | ||
| state=None, | ||
| storage_account=None, | ||
| storage_endpoint=None, | ||
| storage_account_access_key=None, | ||
| retention_days=None, | ||
| email_addresses=None, | ||
| disabled_alerts=None, | ||
| email_account_admins=None): | ||
|
|
||
| # Apply state | ||
| if state is not None: | ||
| # pylint: disable=unsubscriptable-object | ||
| instance.state = SecurityAlertPolicyState[state.lower()] | ||
| enabled = instance.state.value.lower() == SecurityAlertPolicyState.enabled.value.lower() | ||
|
|
||
| # Set storage-related properties | ||
| _db_security_policy_update( | ||
| instance, | ||
| enabled, | ||
| storage_account, | ||
| storage_endpoint, | ||
| storage_account_access_key, | ||
| False) | ||
|
|
||
| # Set other properties | ||
| if retention_days is not None: | ||
| instance.retention_days = retention_days | ||
|
|
||
| if email_addresses is not None: | ||
| instance.email_addresses = ";".join(email_addresses) | ||
|
|
||
| if disabled_alerts is not None: | ||
| instance.disabled_alerts = ";".join(disabled_alerts) | ||
|
|
||
| if email_account_admins is not None: | ||
| instance.email_account_admins = email_account_admins | ||
|
|
||
| return instance | ||
|
|
||
|
|
||
| ############################################### | ||
| # sql dw # | ||
| ############################################### | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are those related to the change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes these functions are used below