├── .config ├── templ-def_assignment.md ├── templ-definition.md ├── templ-examples-machine-config.md ├── templ-examples.md ├── templ-exemption.md ├── templ-initiative.md ├── templ-set_assignment.md └── terraform-docs.yml ├── .github ├── issue_template.md ├── pull_request_template.md └── workflows │ ├── cd-guest-config.yml │ ├── cd.yml │ ├── ci.yml │ └── lock.yaml ├── .gitignore ├── LICENSE ├── README.md ├── examples-machine-config ├── README.md ├── assignments_team_a.tf ├── backend.tf ├── build_packages.tf ├── data.tf ├── definitions.tf └── variables.tf ├── examples ├── README.md ├── assignments_org.tf ├── assignments_team_a.tf ├── backend.tf ├── built-in.tf ├── data.tf ├── definitions.tf ├── duplicate_members.tf ├── exemptions.tf ├── initiatives.tf └── variables.tf ├── img ├── effects.svg ├── logo-social.png ├── logo.svg └── scopes.svg ├── modules ├── def_assignment │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── definition │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── exemption │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── initiative │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf └── set_assignment │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── policies ├── Automation │ ├── onboard_to_automation_dsc_linux.json │ └── onboard_to_automation_dsc_windows.json ├── Compute │ ├── README.md │ ├── deploy_linux_lad_vm_agent.json │ ├── deploy_linux_lad_vmss_agent.json │ ├── deploy_linux_log_analytics_vm_agent.json │ ├── deploy_linux_log_analytics_vmss_agent.json │ ├── deploy_windows_log_analytics_vm_agent.json │ ├── deploy_windows_log_analytics_vmss_agent.json │ ├── deploy_windows_wad_vm_agent.json │ ├── deploy_windows_wad_vmss_agent.json │ ├── example-lad-config.json │ ├── preview_deploy_linux_azure_monitor_vm_agent.json │ └── preview_deploy_windows_azure_monitor_vm_agent.json ├── General │ ├── deny_resource_types.json │ ├── whitelist_regions.json │ └── whitelist_resources.json ├── Guest Configuration │ ├── WindowsSecurityBaseline2016_1.1.0.json │ └── nxLAMPServer_2.1.2.json ├── Monitoring │ ├── README.md │ ├── audit_log_analytics_workspace_retention.json │ ├── audit_subscription_diagnostic_setting_should_exist.json │ ├── deploy_application_gateway_diagnostic_setting.json │ ├── deploy_eventhub_diagnostic_setting.json │ ├── deploy_expressroute_connection_diagnostic_setting.json │ ├── deploy_expressroute_diagnostic_setting.json │ ├── deploy_firewall_diagnostic_setting.json │ ├── deploy_keyvault_diagnostic_setting.json │ ├── deploy_loadbalancer_diagnostic_setting.json │ ├── deploy_network_interface_diagnostic_setting.json │ ├── deploy_network_security_group_diagnostic_setting.json │ ├── deploy_public_ip_diagnostic_setting.json │ ├── deploy_storage_account_diagnostic_setting.json │ ├── deploy_subscription_diagnostic_setting.json │ ├── deploy_virtual_machine_diagnostic_setting.json │ ├── deploy_vnet_diagnostic_setting.json │ └── deploy_vnet_gateway_diagnostic_setting.json ├── Network │ └── deny_nic_public_ip.json ├── README.md ├── Security Center │ ├── README.md │ ├── auto_enroll_subscriptions.json │ ├── auto_provision_log_analytics_agent_custom_workspace.json │ ├── auto_set_contact_details.json │ ├── enable_vulnerability_vm_assessments.json │ ├── export_asc_alerts_and_recommendations_to_eventhub.json │ └── export_asc_alerts_and_recommendations_to_log_analytics.json ├── Storage │ ├── storage_enforce_https.json │ └── storage_enforce_minimum_tls1_2.json └── Tags │ ├── add_replace_resource_group_tag_key_modify.json │ ├── inherit_resource_group_tags_append.json │ ├── inherit_resource_group_tags_modify.json │ └── require_resource_group_tags.json ├── project.code-workspace └── scripts ├── build_machine_config_packages.ps1 ├── convert_from_tf_plan.ps1 ├── dsc_examples ├── README.md ├── WindowsSecurityBaseline2016_1.1.0.ps1 └── nxLAMPServer_2.1.2.ps1 ├── markdown_generator.ps1 └── precommit.ps1 /.config/templ-def_assignment.md: -------------------------------------------------------------------------------- 1 | # POLICY DEFINITION ASSIGNMENT MODULE 2 | 3 | Assignments can be scoped from overarching management groups right down to individual resources by settings the `assignment_scope`. 4 | 5 | ## Role Definitions & Assignments 6 | 7 | A role assignment and remediation task will be automatically created if the `policyRule` contains a list of `roleDefinitionIds`. This can be omitted with `skip_role_assignment=true`, or to assign roles at a different scope to that of the policy assignment use: `role_assignment_scope`. 8 | 9 | For a cleaner solution, a list of `aad_group_remediation_object_ids` can be supplied for System Assigned Identity membership in favour of role assignments, assuming the appropriate RBAC controls already exist for that group. More info on role assignments can be found in the [main README](../../README.md#role-assignments) 10 | 11 | ## Examples 12 | 13 | ### Assign a definition with Modify effect to automatically create a role assignment and remediation task 14 | 15 | ```hcl 16 | module team_a_mg_inherit_resource_group_tags_modify { 17 | source = "gettek/policy-as-code/azurerm//modules/def_assignment" 18 | definition = module.inherit_resource_group_tags_modify.definition 19 | assignment_scope = data.azurerm_management_group.team_a.id 20 | assignment_effect = "Modify" 21 | skip_remediation = var.skip_remediation 22 | 23 | assignment_parameters = { 24 | tagName = "environment" 25 | } 26 | } 27 | ``` 28 | 29 | ### Assign a definition with Modify effect to automatically create a role assignment and remediation task with an explicit role 30 | 31 | ```hcl 32 | data azurerm_role_definition contributor { 33 | name = "Contributor" 34 | } 35 | 36 | module team_a_mg_inherit_resource_group_tags_modify { 37 | source = "gettek/policy-as-code/azurerm//modules/def_assignment" 38 | definition = module.inherit_resource_group_tags_modify.definition 39 | assignment_scope = data.azurerm_management_group.team_a.id 40 | assignment_effect = "Modify" 41 | skip_remediation = var.skip_remediation 42 | 43 | # specify a list of role definitions or omit to use those defined in the policies 44 | role_definition_ids = [ 45 | data.azurerm_role_definition.contributor.id 46 | ] 47 | 48 | # specify a different role assignment scope or omit to use the policy assignment scope 49 | role_assignment_scope = data.azurerm_management_group.team_a.id 50 | 51 | assignment_parameters = { 52 | tagName = "environment" 53 | } 54 | } 55 | ``` 56 | 57 | ### Create a Built-In Policy Definition Assignment with Custom Non-Compliance Message 58 | 59 | ```hcl 60 | # Should use name instead of display name, as Microsoft changes the display names. 61 | data azurerm_policy_definition_built_in deploy_law_on_linux_vms { 62 | name = "053d3325-282c-4e5c-b944-24faffd30d77" #"Deploy Log Analytics extension for Linux VMs" 63 | } 64 | 65 | module team_a_mg_deploy_law_on_linux_vms { 66 | source = "gettek/policy-as-code/azurerm//modules/def_assignment" 67 | definition = data.azurerm_policy_definition_built_in.deploy_law_on_linux_vms 68 | assignment_scope = data.azurerm_management_group.team_a.id 69 | skip_remediation = var.skip_remediation 70 | 71 | assignment_parameters = { 72 | logAnalytics = local.dummy_resource_ids.azurerm_log_analytics_workspace 73 | listOfImageIdToInclude = [ 74 | local.dummy_resource_ids.custom_linux_image_id 75 | ] 76 | } 77 | 78 | non_compliance_message = "Example non-compliance message will be used as opposed to default policy error" 79 | } 80 | ``` 81 | 82 | ### Assign a definition with Modify effect but add identity to an AAD Group instead of role-assignment 83 | 84 | ```hcl 85 | data "azuread_group" "policy_remediation" { 86 | display_name = "policy_remediation" 87 | security_enabled = true 88 | } 89 | 90 | module team_a_mg_inherit_resource_group_tags_modify { 91 | source = "gettek/policy-as-code/azurerm//modules/def_assignment" 92 | definition = module.inherit_resource_group_tags_modify.definition 93 | assignment_scope = data.azurerm_management_group.team_a.id 94 | skip_remediation = false 95 | skip_role_assignment = true # <- set this to true to avoid role assignments 96 | 97 | assignment_parameters = { 98 | tagName = "environment" 99 | } 100 | } 101 | 102 | resource "azuread_group_member" "remediate_team_a_mg_inherit_resource_group_tags_modify" { 103 | group_object_id = data.azuread_group.policy_remediation.id 104 | member_object_id = module.team_a_mg_inherit_resource_group_tags_modify.principal_id 105 | } 106 | ``` 107 | 108 | ### Resource selectors (preview) 109 | 110 | The optional `resource_selectors` property facilitates safe deployment practices (SDP) by enabling you to gradually roll out policy assignments based on factors like resource location, resource type, or whether a resource has a location. 111 | 112 | > 📘 [Microsoft Docs: Azure Policy assignment structure (Resource selectors)](https://learn.microsoft.com/en-us/azure/governance/policy/concepts/assignment-structure#resource-selectors-preview) 113 | 114 | The example below demonstrates the acceptable format for this module: 115 | 116 | ```hcl 117 | module "org_mg_whitelist_regions" { 118 | source = "gettek/policy-as-code/azurerm//modules/def_assignment" 119 | definition = module.whitelist_regions.definition 120 | assignment_scope = data.azurerm_management_group.org.id 121 | assignment_effect = "Deny" 122 | 123 | assignment_parameters = { 124 | listOfRegionsAllowed = [ "uk", "uksouth", "ukwest", "europe", "northeurope", "westeurope", "global" ] 125 | } 126 | 127 | # optional resource selectors (preview) 128 | resource_selectors = [ 129 | { 130 | name = "SDPRegions" 131 | selectors = { 132 | kind = "resourceLocation" 133 | in = [ "uk", "uksouth", "ukwest" ] 134 | } 135 | }, 136 | { 137 | name = "SDPResourceTypes" 138 | selectors = { 139 | kind = "resourceType" 140 | in = [ "Microsoft.Storage/storageAccounts", "Microsoft.EventHub/namespaces", "Microsoft.OperationalInsights/workspaces" ] 141 | } 142 | } 143 | ] 144 | } 145 | ``` 146 | -------------------------------------------------------------------------------- /.config/templ-definition.md: -------------------------------------------------------------------------------- 1 | # POLICY DEFINITION MODULE 2 | 3 | This module depends on populating `var.policy_name` and `var.policy_category` to correspond with the respective custom policy definition `json` file found in the [local library](../../policies). You can also parse in other template files and data sources at runtime, see below for examples and acceptable inputs. 4 | 5 | > 💡 **Note:** More information on Policy Definition Structure [can be found here](https://learn.microsoft.com/en-us/azure/governance/policy/concepts/definition-structure) 6 | 7 | > 💡 **Note:** Specify the `policy_mode` variable if you wish to [change the mode](https://learn.microsoft.com/en-us/azure/governance/policy/concepts/definition-structure#mode) which defaults to `All`. Possible values below. 8 | 9 | ## Examples 10 | 11 | ### Create a basic Policy Definition from a file located in the module library 12 | 13 | ```hcl 14 | module whitelist_regions { 15 | source = "gettek/policy-as-code/azurerm//modules/definition" 16 | policy_name = "whitelist_regions" 17 | display_name = "Allow resources only in whitelisted regions" 18 | policy_category = "General" 19 | management_group_id = data.azurerm_management_group.org.id 20 | } 21 | ``` 22 | 23 | ### Loop around a map to quickly create multiple definitions 24 | ```hcl 25 | locals { 26 | security_center_policies = { 27 | auto_enroll_subscriptions = "Enable Azure Security Center on Subcriptions" 28 | auto_provision_log_analytics_agent_custom_workspace = "Enable Security Center's auto provisioning of the Log Analytics agent on your subscriptions with custom workspace" 29 | auto_set_contact_details = "Automatically set the security contact email address and phone number should they be blank on the subscription" 30 | export_asc_alerts_and_recommendations_to_eventhub = "Export to Event Hub for Azure Security Center alerts and recommendations" 31 | export_asc_alerts_and_recommendations_to_log_analytics = "Export to Log Analytics Workspace for Azure Security Center alerts and recommendations" 32 | } 33 | } 34 | 35 | module "configure_asc" { 36 | source = "gettek/policy-as-code/azurerm//modules/definition" 37 | for_each = local.security_center_policies 38 | policy_name = each.key 39 | display_name = title(replace(each.key, "_", " ")) 40 | policy_description = each.value 41 | policy_category = "Security Center" 42 | management_group_id = data.azurerm_management_group.org.id 43 | } 44 | ``` 45 | 46 | ### Use definition files located outside of the module library 47 | 48 | ```hcl 49 | module "file_path_test" { 50 | source = "gettek/policy-as-code/azurerm//modules/definition" 51 | file_path = "../path/to/file/onboard_to_automation_dsc_linux.json" 52 | management_group_id = data.azurerm_management_group.org.id 53 | } 54 | ``` 55 | 56 | Loop around a folders contents to create multiple definitions: 57 | 58 | ```hcl 59 | module "iam_test" { 60 | source = "gettek/policy-as-code/azurerm//modules/definition" 61 | for_each = { 62 | for p in fileset(path.module, "../../azure/governance/policies/Storage/*.json") : 63 | trimsuffix(basename(p), ".json") => pathexpand(p) 64 | } 65 | file_path = each.value 66 | management_group_id = data.azurerm_management_group.org.id 67 | } 68 | ``` 69 | 70 | You will also be able to supply object properties at runtime such as: 71 | 72 | ```hcl 73 | locals { 74 | policy_file = jsondecode(file("onboard_to_automation_dsc_linux.json")) 75 | } 76 | 77 | module "parameterised_test" { 78 | source = "gettek/policy-as-code/azurerm//modules/definition" 79 | policy_name = "Custom Name" 80 | display_name = "Custom Display Name" 81 | policy_description = "Custom Description" 82 | policy_category = "Custom Category" 83 | policy_version = "Custom Version" 84 | management_group_id = data.azurerm_management_group.org.id 85 | 86 | policy_rule = (local.policy_file).properties.policyRule 87 | policy_parameters = (local.policy_file).properties.parameters 88 | policy_metadata = (local.policy_file).properties.metadata 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /.config/templ-examples-machine-config.md: -------------------------------------------------------------------------------- 1 | # Azure Policy Machine Configuration for Virtual Machines 2 | 3 | [![cd-machine-config](https://github.com/gettek/terraform-azurerm-policy-as-code/actions/workflows/cd-guest-config.yml/badge.svg)](https://github.com/gettek/terraform-azurerm-policy-as-code/actions/workflows/cd-guest-config.yml) 4 | 5 | > 💡 **Note:** Azure Policy Guest Configuration is now called Azure Automanage Machine Configuration. [Learn more about the recent renaming of Microsoft configuration management services.](https://techcommunity.microsoft.com/t5/azure-governance-and-management/coming-soon-guest-configuration-renames-to-machine-configuration/ba-p/3474116) 6 | 7 | This folder demonstrates an effective terraform workflow for continuous compliance with [Azure Policy Machine Configuration](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/overview) using PowerShell Desired State Config (DSC). 8 | 9 | ### Automating CGC Package Deployment 10 | 11 | This workflow executes a terraform null resource to run the [build_machine_config_packages.ps1](../scripts/build_machine_config_packages.ps1) PowerShell script which builds and publishes Custom Machine Config (CGC) policy definitions. PS Module dependencies are installed with the `checkDependancies` flag, this list can be updated as required by your custom configs. 12 | 13 | On initial run or to rebuild/update existing configs, first target the null resource with parallelism set to 1 before running a complete apply such as below. This will prevent the [`GuestConfiguration`](https://www.powershellgallery.com/packages/GuestConfiguration/) PowerShell module from encountering conflicts during package creation. If the `checkDependancies` flag is unset each config should take approximately 10-15 seconds to publish packages and generate Definitions. 14 | 15 | ```bash 16 | apply -target null_resource.build_machine_config_packages -parallelism=1 && tf apply 17 | ``` 18 | 19 | Definitions will stored in the local repo library under [Guest Configuration](../policies/Guest%20Configuration/) 20 | 21 | ### Links 22 | 23 | - 📘 [Understand the machine configuration feature of Azure Automanage](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/overview) 24 | - 📘 [Azure Policy guest configuration extension](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/manage/azure-server-management/guest-configuration-policy) 25 | - 📘 [How to set up a machine configuration authoring environment](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/machine-configuration-create-setup) 26 | - 📘 [How to create custom machine configuration package artifacts](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/machine-configuration-create) 27 | - 📘 [How to create custom machine configuration policy definitions](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/machine-configuration-create-definition) 28 | - 📘 [How to test machine configuration package artifacts](https://learn.microsoft.com/en-gb/azure/governance/machine-configuration/machine-configuration-create-test) 29 | - 📘 [Remediation options for machine configuration](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/machine-configuration-policy-effects) 30 | - 📙 [Azure Policy Guest Configuration samples](https://github.com/Azure/azure-policy/tree/master/samples/GuestConfiguration/package-samples) 31 | - 📙 [DSC GitHub Community](https://github.com/dsccommunity) 32 | - 📙 [Terraform Provider: azurerm_policy_virtual_machine_configuration_assignment](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/policy_virtual_machine_configuration_assignment) -------------------------------------------------------------------------------- /.config/templ-examples.md: -------------------------------------------------------------------------------- 1 | # Azure Policy Deployments 2 | 3 | This examples folder demonstrates an effective deployment of Azure Policy Definitions and Assignments. The order of execution is generally from `definitions.tf` -> `initiatives.tf` -> `assignments_.tf` -> `exemptions.tf` 4 | 5 | > 💡 **Note:** `built-in.tf` demonstrates how to assign Built-In definitions. 6 | -------------------------------------------------------------------------------- /.config/templ-exemption.md: -------------------------------------------------------------------------------- 1 | # POLICY EXEMPTION MODULE 2 | 3 | Exemptions can be used where `not_scopes` become time sensitive or require alternative methods of approval for audit trails. Learn more about Azure Policy [exemption structure](https://learn.microsoft.com/en-us/azure/governance/policy/concepts/exemption-structure). 4 | 5 | > 💡**Note:** This module also allows you to exempt multiple scope types at once (e.g. resource group and individual resource) when using a `for_each` loop as in the example below. 6 | 7 | ## Examples 8 | 9 | ### At Management Group Scope with Optional Metadata 10 | 11 | ```hcl 12 | module exemption_team_a_mg_deny_nic_public_ip { 13 | source = "gettek/policy-as-code/azurerm//modules/exemption" 14 | name = "Deny NIC Public IP Exemption" 15 | display_name = "Exempted while testing" 16 | description = "Allows NIC Public IPs for testing" 17 | scope = data.azurerm_management_group.team_a.id 18 | policy_assignment_id = module.team_a_mg_deny_nic_public_ip.id 19 | exemption_category = "Waiver" 20 | expires_on = "2023-05-25" 21 | 22 | # optional metadata, these can consist of any key/value pairs, or omit to have none 23 | metadata = { 24 | requested_by = "Team A" 25 | approved_by = "Mr Smith" 26 | approved_date = "2021-11-30" 27 | ticket_ref = "1923" 28 | } 29 | } 30 | ``` 31 | 32 | ### Exempt multiple scope types in a for_each loop 33 | 34 | ```hcl 35 | module exemption_team_a_mg_deny_nic_public_ip { 36 | source = "gettek/policy-as-code/azurerm//modules/exemption" 37 | for_each = toset([ 38 | data.azurerm_management_group.team_a.id, 39 | data.azurerm_subscription.current.id, 40 | data.azurerm_resource_group.example.id 41 | data.azurerm_network_interface.example.id 42 | ]) 43 | name = "Deny NIC Public IP Exemption" 44 | display_name = "Exempted while testing" 45 | description = "Allows NIC Public IPs for testing" 46 | scope = each.value 47 | policy_assignment_id = module.team_a_mg_deny_nic_public_ip.id 48 | } 49 | ``` 50 | 51 | ### Exempt a subset of definitions within an Initiative at Subscription Scope 52 | 53 | ```hcl 54 | module "exemption_configure_asc_initiative" { 55 | source = "gettek/policy-as-code/azurerm//modules/exemption" 56 | name = "Onboard subscription to ASC Exemption" 57 | display_name = "Exempted while testing" 58 | description = "Excludes subscription from ASC onboarding during development" 59 | scope = data.azurerm_subscription.current.id 60 | policy_assignment_id = module.org_mg_configure_asc_initiative.id 61 | 62 | # use member_definition_names for simplicity when policy_definition_reference_ids are unknown 63 | member_definition_names = [ 64 | "auto_provision_log_analytics_agent_custom_workspace", 65 | "auto_set_contact_details" 66 | ] 67 | } 68 | ``` 69 | 70 | ### Resource Group Exemption 71 | 72 | ```hcl 73 | module exemption_team_a_mg_deny_nic_public_ip { 74 | source = "gettek/policy-as-code/azurerm//modules/exemption" 75 | name = "Deny NIC Public IP Exemption" 76 | display_name = "Exempted while testing" 77 | description = "Allows NIC Public IPs for testing" 78 | scope = data.azurerm_resource_group.example.id 79 | policy_assignment_id = module.team_a_mg_deny_nic_public_ip.id 80 | } 81 | ``` 82 | 83 | ### Multiple Resource exemptions for all KeyVaults in a Resource Group 84 | 85 | Use `azurerm_resources` to retrieve a list of resource types within the same resource group or specify a simpler (more declarative) array of your choosing. 86 | 87 | ```hcl 88 | data azurerm_resources keyvaults { 89 | type = "Microsoft.KeyVault/vaults" 90 | resource_group_name = "rg-dev-uks-vaults" 91 | } 92 | 93 | module exemption_team_a_mg_key_vaults_require_purge_protection { 94 | source = "gettek/policy-as-code/azurerm//modules/exemption" 95 | for_each = toset(data.azurerm_resources.keyvaults.resources.*.id) 96 | name = "Key vaults should have purge protection enabled Exemption" 97 | display_name = "Exempted while testing" 98 | description = "Do not require purge protection on KVs while testing" 99 | scope = each.value 100 | policy_assignment_id = module.team_a_mg_key_vaults_require_purge_protection.id 101 | exemption_category = "Mitigated" 102 | expires_on = "2023-05-25" 103 | } 104 | ``` 105 | -------------------------------------------------------------------------------- /.config/templ-initiative.md: -------------------------------------------------------------------------------- 1 | # POLICY INITIATIVE MODULE 2 | 3 | Dynamically creates a policy set based on multiple custom or built-in policy definitions 4 | 5 | > ⚠️ **Warning:** To simplify assignments, if any `member_definitions` contain the same parameter names they will be [merged](https://www.terraform.io/language/functions/merge) unless you specify `merge_effects = false` or `merge_parameters = false` as described in the third example below. When `false` parameters will be suffixed with their respective reference Ids e.g. `"effect_AutoEnrollSubscriptions"`. 6 | 7 | 8 | ## Examples 9 | 10 | 11 | ### Create an Initiative with duplicate member definitions 12 | 13 | In many cases, some initiatives such as those for tagging, may need to reuse the same definition multiple times but with different parameters to simplify assignments. 14 | 15 | Please see [duplicate_members.tf](../../examples/duplicate_members.tf) as en example use case. 16 | 17 | > 💡 **Note:** you must set `duplicate_members=true` and `merge_parameters=false` when building initiatives with duplicate members.
18 | > 💡 **Note:** Be cautious when changing the position of `member_definitions` as these reflect the index numbers used in `assignment_parameters`. 19 | 20 | 21 | ### Create an Initiative with custom Policy definitions 22 | 23 | ```hcl 24 | module configure_asc_initiative { 25 | source = "gettek/policy-as-code/azurerm//modules/initiative" 26 | initiative_name = "configure_asc_initiative" 27 | initiative_display_name = "[Security]: Configure Azure Security Center" 28 | initiative_description = "Deploys and configures Azure Security Center settings and defines exports" 29 | initiative_category = "Security Center" 30 | management_group_id = data.azurerm_management_group.org.id 31 | 32 | member_definitions = [ 33 | module.configure_asc["auto_enroll_subscriptions"].definition, 34 | module.configure_asc["auto_provision_log_analytics_agent_custom_workspace"].definition, 35 | module.configure_asc["auto_set_contact_details"].definition, 36 | module.configure_asc["export_asc_alerts_and_recommendations_to_eventhub"].definition, 37 | module.configure_asc["export_asc_alerts_and_recommendations_to_log_analytics"].definition, 38 | ] 39 | } 40 | ``` 41 | 42 | ### Create an Initiative with a mix of custom & built-in Policy definitions without merging effects 43 | 44 | ```hcl 45 | data azurerm_policy_definition deploy_law_on_linux_vms { 46 | display_name = "Deploy Log Analytics extension for Linux VMs" 47 | } 48 | 49 | module configure_asc_initiative { 50 | source = "gettek/policy-as-code/azurerm//modules/initiative" 51 | initiative_name = "configure_asc_initiative" 52 | initiative_display_name = "[Security]: Configure Azure Security Center" 53 | initiative_description = "Deploys and configures Azure Security Center settings and defines exports" 54 | initiative_category = "Security Center" 55 | management_group_id = data.azurerm_management_group.org.id 56 | merge_effects = false 57 | 58 | member_definitions = [ 59 | module.configure_asc["auto_enroll_subscriptions"].definition, 60 | module.configure_asc["auto_provision_log_analytics_agent_custom_workspace"].definition, 61 | module.configure_asc["auto_set_contact_details"].definition, 62 | module.configure_asc["export_asc_alerts_and_recommendations_to_eventhub"].definition, 63 | module.configure_asc["export_asc_alerts_and_recommendations_to_log_analytics"].definition, 64 | data.azurerm_policy_definition.deploy_law_on_linux_vms, 65 | ] 66 | } 67 | 68 | # get all the generated parameter names so we know what to use during assignment 69 | output "list_of_initiative_parameters" { 70 | value = keys(module.configure_asc_initiative.parameters) 71 | } 72 | ``` 73 | 74 | ### Populate member_definitions with a for loop 75 | 76 | ```hcl 77 | locals { 78 | guest_config_prereqs = [ 79 | "add_system_identity_when_none_prerequisite", 80 | "add_system_identity_when_user_prerequisite", 81 | "deploy_extension_linux_prerequisite", 82 | "deploy_extension_windows_prerequisite", 83 | ] 84 | } 85 | 86 | module guest_config_prereqs { 87 | source = "..//modules/definition" 88 | for_each = toset(local.guest_config_prereqs) 89 | policy_name = each.value 90 | policy_category = "Guest Configuration" 91 | management_group_id = data.azurerm_management_group.org.id 92 | } 93 | 94 | module guest_config_prereqs_initiative { 95 | source = "..//modules/initiative" 96 | initiative_name = "guest_config_prereqs_initiative" 97 | initiative_display_name = "[GC]: Deploys Guest Config Prerequisites" 98 | initiative_description = "Deploys and configures Windows and Linux VM Guest Config Prerequisites" 99 | initiative_category = "Guest Configuration" 100 | management_group_id = data.azurerm_management_group.org.id 101 | 102 | member_definitions = [ 103 | for gcp in module.guest_config_prereqs : 104 | gcp.definition 105 | ] 106 | } 107 | ``` 108 | -------------------------------------------------------------------------------- /.config/templ-set_assignment.md: -------------------------------------------------------------------------------- 1 | # POLICY INITIATIVE ASSIGNMENT MODULE 2 | 3 | Assignments can be scoped from overarching management groups right down to individual resources by settings the `assignment_scope`. 4 | 5 | ## Role Definitions & Assignments 6 | 7 | A role assignment and remediation task will be automatically created if any member definition contains a list of `roleDefinitionIds`. This can be omitted with `skip_role_assignment=true`, or to assign roles at a different scope to that of the policy assignment use: `role_assignment_scope`. 8 | 9 | For a cleaner solution, a list of `aad_group_remediation_object_ids` can be supplied for System Assigned Identity membership in favour of role assignments, assuming the appropriate RBAC controls already exist for that group. More info on role assignments can be found in the [main README](../../README.md#role-assignments) 10 | 11 | ## Assignment Effects 12 | 13 | The `assignment_effect` parameter is useful when an initiative contains multiple effects of the same type and `merge_effects=true`, ensuring that all `member_definitions` are assigned with the same effect. 14 | 15 | - Omit `assignment_effect` to use each definition's default effect stored in its policy parameters. 16 | - Specify effects individually by setting them in `assignment_parameters` for more granular control. 17 | 18 | ## Examples 19 | 20 | ### Custom Policy Initiative Assignment with Not-Scope and Overrides (preview) 21 | 22 | The optional `overrides` property allows you to change the effect of a member definition without modifying the underlying policy definition or using a parameterized effect in the policy definition. 23 | 24 | > 📘 [Microsoft Docs: Azure Policy assignment structure (Overrides)](https://learn.microsoft.com/en-us/azure/governance/policy/concepts/assignment-structure#overrides-preview) 25 | > 💡 **Note:** This module also supports Resource selectors (preview), see the [`def_assignment`](../def_assignment) module for an example input 26 | 27 | ```hcl 28 | module org_mg_configure_asc_initiative { 29 | source = "gettek/policy-as-code/azurerm//modules/set_assignment" 30 | initiative = module.configure_asc_initiative.initiative 31 | assignment_scope = data.azurerm_management_group.org.id 32 | assignment_effect = "DeployIfNotExists" 33 | 34 | # resource remediation options 35 | skip_role_assignment = false 36 | skip_remediation = false 37 | re_evaluate_compliance = true 38 | 39 | assignment_parameters = { 40 | workspaceId = local.dummy_resource_ids.azurerm_log_analytics_workspace 41 | eventHubDetails = local.dummy_resource_ids.azurerm_eventhub_namespace_authorization_rule 42 | securityContactsEmail = "admin@cloud.com" 43 | securityContactsPhone = "44897654987" 44 | } 45 | 46 | assignment_not_scopes = [ 47 | data.azurerm_management_group.team_a.id 48 | ] 49 | 50 | # use the 'non_compliance_messages' output from the initiative module to use auto generated messages based off policy properties: descriptions/display names/custom ones found in metadata 51 | # override with your own Key/Value pairs map as 'policy_definition_reference_id = content', use null = 'content' to specify the Default non-compliance message for all member definitions. 52 | non_compliance_messages = module.configure_asc_initiative.non_compliance_messages 53 | 54 | # optional overrides (preview) 55 | overrides = [ 56 | { 57 | effect = "AuditIfNotExists" 58 | selectors = { 59 | in = [ "ExportAscAlertsAndRecommendationsToEventhub", "ExportAscAlertsAndRecommendationsToLogAnalytics" ] 60 | } 61 | }, 62 | { 63 | effect = "Disabled" 64 | selectors = { 65 | in = [ "AutoSetContactDetails" ] 66 | } 67 | } 68 | ] 69 | } 70 | ``` 71 | 72 | ### Built-In Policy Initiative Assignment 73 | ```hcl 74 | # Should use name instead of display name, as Microsoft changes the display names. 75 | data "azurerm_policy_set_definition" "cis_1_3_0" { 76 | name = "612b5213-9160-4969-8578-1518bd2a000c" #"CIS Microsoft Azure Foundations Benchmark v1.3.0" 77 | } 78 | 79 | module org_mg_cis_1_3_0_benchmark { 80 | source = "gettek/policy-as-code/azurerm//modules/set_assignment" 81 | initiative = data.azurerm_policy_set_definition.cis_1_3_0 82 | assignment_scope = data.azurerm_management_group.org.id 83 | 84 | assignment_parameters = { 85 | "effect-b954148f-4c11-4c38-8221-be76711e194a-MicrosoftSql-servers-firewallRules-delete" = "Disabled" 86 | } 87 | } 88 | ``` 89 | 90 | ### Built-In Policy Initiative Containing DINE/Modify Assignment 91 | 92 | ```hcl 93 | # Should use name instead of display name, as Microsoft changes the display names. 94 | data "azurerm_policy_set_definition" "configure_az_monitor_linux_vm_initiative" { 95 | name = "118f04da-0375-44d1-84e3-0fd9e1849403" #"Configure Linux machines to run Azure Monitor Agent and associate them to a Data Collection Rule" 96 | } 97 | 98 | data "azurerm_role_definition" "vm_contributor" { 99 | name = "Virtual Machine Contributor" 100 | } 101 | 102 | module org_mg_configure_az_monitor_linux_vm_initiative { 103 | source = "gettek/policy-as-code/azurerm//modules/set_assignment" 104 | initiative = data.azurerm_policy_set_definition.configure_az_monitor_linux_vm_initiative 105 | assignment_scope = data.azurerm_management_group.org.id 106 | skip_remediation = false 107 | 108 | role_definition_ids = [ 109 | data.azurerm_role_definition.vm_contributor.id 110 | ] 111 | 112 | assignment_parameters = { 113 | listOfLinuxImageIdToInclude = [] 114 | dcrResourceId = "/Data/Collection/Rule/Resource/Id" 115 | } 116 | } 117 | ``` 118 | -------------------------------------------------------------------------------- /.config/terraform-docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | formatter: "markdown" 3 | 4 | settings: 5 | anchor: false 6 | lockfile: false 7 | escape: false 8 | hide-empty: true 9 | 10 | output: 11 | file: "README.md" 12 | 13 | sections: 14 | hide: [providers] 15 | 16 | content: |- 17 | {{ .Header }} 18 | 19 | {{ .Requirements }} 20 | 21 | {{ .Modules }} 22 | 23 | {{ .Resources }} 24 | 25 | {{ .Inputs }} 26 | 27 | {{ .Outputs }} 28 | ... -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | # Issue Template 2 | 3 | ## Prerequisites 4 | 5 | 6 | - [ ] I am running the latest version 7 | - [ ] I checked the documentation and found no answer 8 | - [ ] I checked to make sure that this issue has not already been filed 9 | 10 | ## Context 11 | 12 | 13 | * Module Version: 14 | * Terraform Version: 15 | * AzureRM Provider Version: 16 | 17 | 18 | ```hcl 19 | # add code here 20 | ``` 21 | 22 | ## Expected Behavior 23 | 24 | 25 | ## Current Behavior 26 | 27 | 28 | ## Possible Solution 29 | 30 | 31 | ## Failure Information (for bugs) 32 | 33 | 34 | ### Steps to Reproduce 35 | 36 | 37 | 1. 38 | 2. 39 | 3. 40 | 41 | 42 | ### Failure Logs 43 | 44 | 45 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull Request Template 2 | 3 | ## Description 4 | 5 | 6 | Fixes # (issue) 7 | 8 | ## Type of change 9 | 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | ## How Has This Been Tested? 17 | 18 | 19 | - [ ] Test A 20 | - [ ] Test B 21 | 22 | **Test Configuration**: 23 | * Module Version: 24 | * Terraform Version: 25 | * AzureRM Provider Version: 26 | 27 | ## Checklist: 28 | 29 | - [ ] I have performed a self-review of my own code 30 | - [ ] I have commented my code, particularly in hard-to-understand areas 31 | - [ ] I have made corresponding changes to the documentation 32 | - [ ] My changes generate no new warnings 33 | - [ ] I have checked my code and corrected any misspellings 34 | -------------------------------------------------------------------------------- /.github/workflows/cd-guest-config.yml: -------------------------------------------------------------------------------- 1 | name: cd-machine-config 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - "examples-machine-config" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | cd-machine-config: 11 | env: 12 | TF_IN_AUTOMATION: true 13 | TF_INPUT: false 14 | TF_CLI_ARGS_apply: "-auto-approve -parallelism=30" 15 | TF_CLI_ARGS_destroy: "-auto-approve -parallelism=30" 16 | ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} 17 | ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} 18 | ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} 19 | ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} 20 | runs-on: windows-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: hashicorp/setup-terraform@v2 24 | with: 25 | terraform_version: ~1.10.0 26 | 27 | - name: Terraform Init 28 | id: init 29 | run: terraform init -no-color 30 | working-directory: examples-machine-config 31 | 32 | - name: Terraform Build Machine Configs 33 | id: build 34 | if: ${{ success() }} 35 | run: terraform apply -target null_resource.build_machine_config_packages -parallelism=1 36 | working-directory: examples-machine-config 37 | 38 | - name: Terraform Apply 39 | id: apply 40 | if: ${{ success() }} 41 | run: terraform apply 42 | working-directory: examples-machine-config 43 | 44 | - name: Terraform Destroy 45 | id: destroy 46 | if: ${{ success() }} 47 | run: terraform destroy 48 | working-directory: examples-machine-config 49 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: cd 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | paths: 7 | - "examples" 8 | workflow_dispatch: 9 | 10 | jobs: 11 | cd: 12 | env: 13 | TF_IN_AUTOMATION: true 14 | TF_INPUT: false 15 | TF_CLI_ARGS_init: "-backend-config=storage_account_name=${{ secrets.STORAGE_NAME }} -backend-config=resource_group_name=cgc-cd -backend-config=container_name=tfstate -backend-config=key=policy.tfstate" 16 | TF_CLI_ARGS_apply: "-auto-approve -parallelism=30" 17 | ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} 18 | ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} 19 | ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} 20 | ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: hashicorp/setup-terraform@v2 25 | with: 26 | terraform_version: ~1.10.0 27 | 28 | - name: Terraform Init 29 | id: init 30 | run: terraform init -no-color 31 | working-directory: examples 32 | 33 | - name: Terraform Apply 34 | id: apply 35 | if: ${{ success() }} 36 | run: terraform apply 37 | working-directory: examples 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | ci: 10 | env: 11 | TF_IN_AUTOMATION: true 12 | TF_INPUT: false 13 | TF_WORKING_DIR: examples 14 | TF_CLI_ARGS_init: "-backend-config=storage_account_name=${{ secrets.STORAGE_NAME }} -backend-config=resource_group_name=cgc-cd -backend-config=container_name=tfstate -backend-config=key=policy.tfstate" 15 | ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} 16 | ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} 17 | ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} 18 | ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: hashicorp/setup-terraform@v2 23 | with: 24 | terraform_version: ~1.10.0 25 | 26 | - name: Terraform Format 27 | id: fmt 28 | run: terraform fmt -check 29 | 30 | - name: Terraform Init 31 | id: init 32 | run: terraform init -no-color 33 | working-directory: ${{ env.TF_WORKING_DIR }} 34 | 35 | - name: Terraform Validate 36 | id: validate 37 | run: terraform validate -no-color 38 | 39 | - name: Terraform Plan 40 | id: plan 41 | if: github.event_name == 'pull_request' 42 | run: terraform plan -no-color 43 | working-directory: ${{ env.TF_WORKING_DIR }} 44 | continue-on-error: true 45 | 46 | - name: Update Pull Request 47 | uses: actions/github-script@v6 48 | if: github.event_name == 'pull_request' 49 | with: 50 | github-token: ${{ secrets.GITHUB_TOKEN }} 51 | script: | 52 | // 1. Retrieve existing bot comments for the PR 53 | const { data: comments } = await github.rest.issues.listComments({ 54 | owner: context.repo.owner, 55 | repo: context.repo.repo, 56 | issue_number: context.issue.number, 57 | }) 58 | const botComment = comments.find(comment => { 59 | return comment.user.type === 'Bot' && comment.body.includes('Terraform Format and Style') 60 | }) 61 | 62 | // 2. Prepare format of the comment 63 | const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\` 64 | #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` 65 | #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\` 66 |
Validation Output 67 | 68 | \`\`\`\n 69 | ${{ steps.validate.outputs.stdout }} 70 | \`\`\` 71 | 72 |
73 | 74 | #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` 75 | 76 | *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.TF_WORKING_DIR }}\`, Workflow: \`${{ github.workflow }}\`*`; 77 | 78 | // 3. If we have a comment, update it, otherwise create a new one 79 | if (botComment) { 80 | github.rest.issues.updateComment({ 81 | owner: context.repo.owner, 82 | repo: context.repo.repo, 83 | comment_id: botComment.id, 84 | body: output 85 | }) 86 | } else { 87 | github.rest.issues.createComment({ 88 | issue_number: context.issue.number, 89 | owner: context.repo.owner, 90 | repo: context.repo.repo, 91 | body: output 92 | }) 93 | } 94 | 95 | - name: Terraform Plan Status 96 | if: steps.plan.outcome == 'failure' 97 | run: exit 1 98 | -------------------------------------------------------------------------------- /.github/workflows/lock.yaml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * 6" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v6 14 | with: 15 | days-before-issue-stale: 30 16 | days-before-issue-close: 14 17 | stale-issue-label: "stale" 18 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 19 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 20 | days-before-pr-stale: -1 21 | days-before-pr-close: -1 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###Terraform 2 | # Local .terraform directories 3 | **/.terraform/* 4 | *.tfstate 5 | *.tfstate.* 6 | *.tfrc 7 | *.plan 8 | *.auto.tfvars 9 | lock.json 10 | .terraform.lock.hcl 11 | override.tf 12 | override.tf.json 13 | *_override.tf 14 | *_override.tf.json 15 | .terraformrc 16 | terraform.rc 17 | 18 | ### VisualStudioCode ### 19 | .vscode/* 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | 25 | ### VisualStudioCode Patch ### 26 | # Ignore all local history of files 27 | .history 28 | .DS_Store 29 | .tst 30 | .tmp 31 | .temp 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sadik Tekin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples-machine-config/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Azure Policy Machine Configuration for Virtual Machines 3 | 4 | [![cd-machine-config](https://github.com/gettek/terraform-azurerm-policy-as-code/actions/workflows/cd-guest-config.yml/badge.svg)](https://github.com/gettek/terraform-azurerm-policy-as-code/actions/workflows/cd-guest-config.yml) 5 | 6 | > 💡 **Note:** Azure Policy Guest Configuration is now called Azure Automanage Machine Configuration. [Learn more about the recent renaming of Microsoft configuration management services.](https://techcommunity.microsoft.com/t5/azure-governance-and-management/coming-soon-guest-configuration-renames-to-machine-configuration/ba-p/3474116) 7 | 8 | This folder demonstrates an effective terraform workflow for continuous compliance with [Azure Policy Machine Configuration](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/overview) using PowerShell Desired State Config (DSC). 9 | 10 | ### Automating CGC Package Deployment 11 | 12 | This workflow executes a terraform null resource to run the [build_machine_config_packages.ps1](../scripts/build_machine_config_packages.ps1) PowerShell script which builds and publishes Custom Machine Config (CGC) policy definitions. PS Module dependencies are installed with the `checkDependancies` flag, this list can be updated as required by your custom configs. 13 | 14 | On initial run or to rebuild/update existing configs, first target the null resource with parallelism set to 1 before running a complete apply such as below. This will prevent the [`GuestConfiguration`](https://www.powershellgallery.com/packages/GuestConfiguration/) PowerShell module from encountering conflicts during package creation. If the `checkDependancies` flag is unset each config should take approximately 10-15 seconds to publish packages and generate Definitions. 15 | 16 | ```bash 17 | apply -target null_resource.build_machine_config_packages -parallelism=1 && tf apply 18 | ``` 19 | 20 | Definitions will stored in the local repo library under [Guest Configuration](../policies/Guest%20Configuration/) 21 | 22 | ### Links 23 | 24 | - 📘 [Understand the machine configuration feature of Azure Automanage](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/overview) 25 | - 📘 [Azure Policy guest configuration extension](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/manage/azure-server-management/guest-configuration-policy) 26 | - 📘 [How to set up a machine configuration authoring environment](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/machine-configuration-create-setup) 27 | - 📘 [How to create custom machine configuration package artifacts](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/machine-configuration-create) 28 | - 📘 [How to create custom machine configuration policy definitions](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/machine-configuration-create-definition) 29 | - 📘 [How to test machine configuration package artifacts](https://learn.microsoft.com/en-gb/azure/governance/machine-configuration/machine-configuration-create-test) 30 | - 📘 [Remediation options for machine configuration](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/machine-configuration-policy-effects) 31 | - 📙 [Azure Policy Guest Configuration samples](https://github.com/Azure/azure-policy/tree/master/samples/GuestConfiguration/package-samples) 32 | - 📙 [DSC GitHub Community](https://github.com/dsccommunity) 33 | - 📙 [Terraform Provider: azurerm_policy_virtual_machine_configuration_assignment](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/policy_virtual_machine_configuration_assignment) 34 | 35 | ## Requirements 36 | 37 | | Name | Version | 38 | |------|---------| 39 | | terraform | >= 1.4 | 40 | | azurerm | >= 4.12 | 41 | 42 | ## Modules 43 | 44 | | Name | Source | Version | 45 | |------|--------|---------| 46 | | custom_guest_configs | ..//modules/definition | n/a | 47 | | custom_guest_configs_initiative | ..//modules/initiative | n/a | 48 | | team_a_mg_guest_config_prereqs_initiative | ..//modules/set_assignment | n/a | 49 | | team_a_mg_vm_custom_guest_configs | ..//modules/set_assignment | n/a | 50 | 51 | ## Resources 52 | 53 | | Name | Type | 54 | |------|------| 55 | | [null_resource.build_machine_config_packages](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | 56 | | [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | 57 | | [azurerm_management_group.org](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/management_group) | data source | 58 | | [azurerm_management_group.team_a](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/management_group) | data source | 59 | | [azurerm_policy_set_definition.deploy_guest_config_prereqs_initiative](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/policy_set_definition) | data source | 60 | | [azurerm_role_definition.contributor](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/role_definition) | data source | 61 | | [azurerm_storage_container.guest_config_container](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/storage_container) | data source | 62 | 63 | ## Inputs 64 | 65 | | Name | Description | Type | Default | Required | 66 | |------|-------------|------|---------|:--------:| 67 | | re_evaluate_compliance | Should the module re-evaluate compliant resources for policies that DeployIfNotExists and Modify | `bool` | `false` | no | 68 | | skip_remediation | Skip creation of all remediation tasks for policies that DeployIfNotExists and Modify | `bool` | `true` | no | 69 | | skip_role_assignment | Should the module skip creation of role assignment for policies that DeployIfNotExists and Modify | `bool` | `false` | no | 70 | 71 | 72 | -------------------------------------------------------------------------------- /examples-machine-config/assignments_team_a.tf: -------------------------------------------------------------------------------- 1 | ################## 2 | # Guest Configuration 3 | ################## 4 | 5 | # Assign Prerequisits Initiative 6 | module "team_a_mg_guest_config_prereqs_initiative" { 7 | source = "..//modules/set_assignment" 8 | initiative = data.azurerm_policy_set_definition.deploy_guest_config_prereqs_initiative 9 | assignment_scope = data.azurerm_management_group.team_a.id 10 | 11 | # resource remediation options 12 | re_evaluate_compliance = var.re_evaluate_compliance 13 | skip_remediation = var.skip_remediation 14 | skip_role_assignment = var.skip_role_assignment 15 | 16 | # built-ins that deploy/modify require role_definition_ids be present 17 | role_definition_ids = [ 18 | data.azurerm_role_definition.contributor.id 19 | ] 20 | } 21 | 22 | # Assign Custom Machine Configs Initiative 23 | module "team_a_mg_vm_custom_guest_configs" { 24 | source = "..//modules/set_assignment" 25 | initiative = module.custom_guest_configs_initiative.initiative 26 | assignment_scope = data.azurerm_management_group.team_a.id 27 | 28 | # resource remediation options 29 | re_evaluate_compliance = var.re_evaluate_compliance 30 | skip_remediation = var.skip_remediation 31 | skip_role_assignment = var.skip_role_assignment 32 | 33 | assignment_parameters = { 34 | IncludeArcMachines = "false" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples-machine-config/backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = ">= 4.12" 7 | } 8 | null = { 9 | source = "hashicorp/null" 10 | } 11 | } 12 | } 13 | 14 | provider "azurerm" { 15 | features {} 16 | resource_provider_registrations = "core" 17 | resource_providers_to_register = [ 18 | "Microsoft.PolicyInsights", 19 | "Microsoft.SecurityInsights" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples-machine-config/build_packages.tf: -------------------------------------------------------------------------------- 1 | # execute script to build and publish custom guest config packages located in scripts/dsc_examples 2 | # to build/update your configs first target this resource with parallelism set to 1 before running a complete apply: 3 | # tf apply -target null_resource.build_machine_config_packages -parallelism=1 && tf apply 4 | # requires PWSH >= 7 5 | 6 | resource "null_resource" "build_machine_config_packages" { 7 | for_each = { for dsc in fileset(path.module, "../scripts/dsc_examples/*.ps1") : basename(dsc) => filemd5(dsc) } 8 | provisioner "local-exec" { 9 | command = "build_machine_config_packages.ps1" 10 | interpreter = ["pwsh", "-file"] 11 | working_dir = "${path.module}/../scripts" 12 | 13 | environment = { 14 | configFile = each.key 15 | connectAzAccount = "true" 16 | checkDependancies = "true" 17 | createGuestConfigPackage = "true" 18 | createGuestConfigPolicy = "true" 19 | storageResourceGroupName = "cgc-cd" 20 | storageAccountName = data.azurerm_storage_container.guest_config_container.storage_account_name 21 | containerName = data.azurerm_storage_container.guest_config_container.name 22 | 23 | # the script can also publish definitions to managent groups 24 | # we will skip this step so that definition lifecycles are managed by terraform: 25 | #publishGuestConfigPolicyMG = data.azurerm_management_group.org.name 26 | } 27 | } 28 | 29 | triggers = { 30 | # changes to either filename or filecontents will trigger a rebuild 31 | "${each.key}" = each.value 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples-machine-config/data.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_client_config" "current" {} 2 | 3 | # Org Management Group 4 | data "azurerm_management_group" "org" { 5 | name = "policy_dev" 6 | } 7 | 8 | # Child Management Group 9 | data "azurerm_management_group" "team_a" { 10 | name = "team_a" 11 | } 12 | 13 | # Contributor role 14 | data "azurerm_role_definition" "contributor" { 15 | name = "Contributor" 16 | } 17 | 18 | # storage container to hold custom dsc packages 19 | data "azurerm_storage_container" "guest_config_container" { 20 | name = "configs" 21 | storage_account_name = "guestconfig${substr(md5(data.azurerm_client_config.current.subscription_id), 0, 5)}" 22 | } 23 | 24 | # Onboarding Prerequisites Initiative References: 25 | # [GA]: 12794019-7a00-42cf-95c2-882eed337cc8 "Deploy prerequisites to enable Guest Configuration policies on virtual machines" (SystemAssigned) 26 | # [Preview]: 2b0ce52e-301c-4221-ab38-1601e2b4cee3 "[Preview]: Deploy prerequisites to enable Guest Configuration policies on virtual machines using user-assigned managed identity" (UserAssigned) 27 | data "azurerm_policy_set_definition" "deploy_guest_config_prereqs_initiative" { 28 | name = "12794019-7a00-42cf-95c2-882eed337cc8" 29 | } 30 | -------------------------------------------------------------------------------- /examples-machine-config/definitions.tf: -------------------------------------------------------------------------------- 1 | ################## 2 | # Guest Configuration: Custom Machine Configuration Definitions 3 | ################## 4 | 5 | module "custom_guest_configs" { 6 | source = "..//modules/definition" 7 | for_each = toset([for p in fileset(path.module, "../policies/Guest Configuration/*.json") : trimsuffix(basename(p), ".json")]) 8 | policy_name = each.key 9 | policy_category = "Guest Configuration" 10 | management_group_id = data.azurerm_management_group.org.id 11 | 12 | depends_on = [ 13 | null_resource.build_machine_config_packages 14 | ] 15 | } 16 | 17 | module "custom_guest_configs_initiative" { 18 | source = "..//modules/initiative" 19 | initiative_name = "custom_guest_configs_initiative" 20 | initiative_display_name = "[CGC]: Custom Guest Configurations Initiative" 21 | initiative_description = "Collection of policies that deploy custom machine configurations" 22 | initiative_category = "Guest Configuration" 23 | management_group_id = data.azurerm_management_group.org.id 24 | member_definitions = [for cgc in module.custom_guest_configs : cgc.definition] 25 | } 26 | -------------------------------------------------------------------------------- /examples-machine-config/variables.tf: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------------------------------------------------- 2 | # Module variables 3 | # ---------------------------------------------------------------------------------------------------------------------- 4 | 5 | variable "skip_remediation" { 6 | type = bool 7 | description = "Skip creation of all remediation tasks for policies that DeployIfNotExists and Modify" 8 | default = true 9 | } 10 | 11 | variable "skip_role_assignment" { 12 | type = bool 13 | description = "Should the module skip creation of role assignment for policies that DeployIfNotExists and Modify" 14 | default = false 15 | } 16 | 17 | variable "re_evaluate_compliance" { 18 | type = bool 19 | description = "Should the module re-evaluate compliant resources for policies that DeployIfNotExists and Modify" 20 | default = false 21 | } 22 | 23 | locals { 24 | resource_discovery_mode = var.re_evaluate_compliance == true ? "ReEvaluateCompliance" : "ExistingNonCompliant" 25 | } 26 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Azure Policy Deployments 3 | 4 | This examples folder demonstrates an effective deployment of Azure Policy Definitions and Assignments. The order of execution is generally from `definitions.tf` -> `initiatives.tf` -> `assignments_.tf` -> `exemptions.tf` 5 | 6 | > 💡 **Note:** `built-in.tf` demonstrates how to assign Built-In definitions. 7 | 8 | ## Requirements 9 | 10 | | Name | Version | 11 | |------|---------| 12 | | terraform | >= 1.4 | 13 | | azuread | >= 2.45 | 14 | | azurerm | >= 4.12 | 15 | 16 | ## Modules 17 | 18 | | Name | Source | Version | 19 | |------|--------|---------| 20 | | configure_asc | ..//modules/definition | n/a | 21 | | configure_asc_initiative | ..//modules/initiative | n/a | 22 | | deny_nic_public_ip | ..//modules/definition | n/a | 23 | | deny_resource_types | ..//modules/definition | n/a | 24 | | deploy_resource_diagnostic_setting | ..//modules/definition | n/a | 25 | | exemption_subscription_diagnostics_settings | ..//modules/exemption | n/a | 26 | | file_path_test | ..//modules/definition | n/a | 27 | | inherit_resource_group_tags_modify | ..//modules/definition | n/a | 28 | | org_mg_configure_asc_initiative | ..//modules/set_assignment | n/a | 29 | | org_mg_configure_az_monitor_and_security_vm_initiative | ..//modules/set_assignment | n/a | 30 | | org_mg_platform_diagnostics_initiative | ..//modules/set_assignment | n/a | 31 | | org_mg_storage_enforce_https | ..//modules/def_assignment | n/a | 32 | | org_mg_storage_enforce_minimum_tls1_2 | ..//modules/def_assignment | n/a | 33 | | org_mg_whitelist_regions | ..//modules/def_assignment | n/a | 34 | | parameterised_test | ..//modules/definition | n/a | 35 | | platform_diagnostics_initiative | ..//modules/initiative | n/a | 36 | | require_resource_group_tags | ..//modules/definition | n/a | 37 | | resource_group_tags | ..//modules/initiative | n/a | 38 | | storage_enforce_https | ..//modules/definition | n/a | 39 | | storage_enforce_minimum_tls1_2 | ..//modules/definition | n/a | 40 | | team_a_mg_deny_nic_public_ip | ..//modules/def_assignment | n/a | 41 | | team_a_mg_deny_resource_types | ..//modules/def_assignment | n/a | 42 | | team_a_mg_resource_group_tags | ..//modules/set_assignment | n/a | 43 | | whitelist_regions | ..//modules/definition | n/a | 44 | 45 | ## Resources 46 | 47 | | Name | Type | 48 | |------|------| 49 | | [azuread_group.rem](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/data-sources/group) | data source | 50 | | [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | 51 | | [azurerm_management_group.org](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/management_group) | data source | 52 | | [azurerm_management_group.team_a](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/management_group) | data source | 53 | | [azurerm_policy_set_definition.configure_az_monitor_and_security_vm_initiative](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/policy_set_definition) | data source | 54 | | [azurerm_role_definition.contributor](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/role_definition) | data source | 55 | | [azurerm_role_definition.vm_contributor](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/role_definition) | data source | 56 | | [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source | 57 | | [azurerm_user_assigned_identity.policy_rem](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/user_assigned_identity) | data source | 58 | 59 | ## Inputs 60 | 61 | | Name | Description | Type | Default | Required | 62 | |------|-------------|------|---------|:--------:| 63 | | re_evaluate_compliance | Should the module re-evaluate compliant resources for policies that DeployIfNotExists and Modify | `bool` | `false` | no | 64 | | skip_remediation | Skip creation of all remediation tasks for policies that DeployIfNotExists and Modify | `bool` | `false` | no | 65 | | skip_role_assignment | Should the module skip creation of role assignment for policies that DeployIfNotExists and Modify | `bool` | `false` | no | 66 | 67 | 68 | -------------------------------------------------------------------------------- /examples/assignments_org.tf: -------------------------------------------------------------------------------- 1 | ################## 2 | # General 3 | ################## 4 | module "org_mg_whitelist_regions" { 5 | source = "..//modules/def_assignment" 6 | definition = module.whitelist_regions.definition 7 | assignment_scope = data.azurerm_management_group.org.id 8 | assignment_effect = "Deny" 9 | 10 | assignment_parameters = { 11 | listOfRegionsAllowed = ["uk", "uksouth", "ukwest", "europe", "northeurope", "westeurope", "global"] # Global is used in services such as Azure DNS 12 | } 13 | 14 | assignment_metadata = { 15 | version = "1.0.0" 16 | category = "Batch" 17 | cloud_envs = [ 18 | "AzureCloud", 19 | "AzureChinaCloud", 20 | "AzureUSGovernment" 21 | ] 22 | } 23 | 24 | # optional resource selectors (preview) 25 | resource_selectors = [ 26 | { 27 | name = "SDPRegions" 28 | selectors = { 29 | kind = "resourceLocation" 30 | in = ["uk", "uksouth", "ukwest"] 31 | } 32 | } 33 | ] 34 | } 35 | 36 | 37 | ################## 38 | # Security Center 39 | ################## 40 | module "org_mg_configure_asc_initiative" { 41 | source = "..//modules/set_assignment" 42 | initiative = module.configure_asc_initiative.initiative 43 | assignment_scope = data.azurerm_management_group.org.id 44 | assignment_description = "Deploys and configures Defender settings and defines exports" 45 | assignment_effect = "DeployIfNotExists" 46 | assignment_location = "ukwest" 47 | 48 | # resource remediation options 49 | re_evaluate_compliance = var.re_evaluate_compliance 50 | skip_remediation = var.skip_remediation 51 | skip_role_assignment = var.skip_role_assignment 52 | aad_group_remediation_object_ids = [data.azuread_group.rem.object_id] # add assignment identity to aad group(s) 53 | 54 | assignment_parameters = { 55 | workspaceId = local.dummy_resource_ids.azurerm_log_analytics_workspace 56 | eventHubDetails = local.dummy_resource_ids.azurerm_eventhub_namespace_authorization_rule 57 | securityContactsEmail = "admin@cloud.com" 58 | securityContactsPhone = "44897654987" 59 | } 60 | 61 | # use the `non_compliance_messages` output from the initiative module to set auto generated messages based off policy properties: descriptions/display names/custom ones found in metadata 62 | # or override with you own Key/Value pairs map e.g. policy_definition_reference_id = 'message content' 63 | non_compliance_messages = module.configure_asc_initiative.non_compliance_messages 64 | 65 | # optional overrides (preview) 66 | overrides = [ 67 | { 68 | effect = "AuditIfNotExists" 69 | selectors = { 70 | in = ["ExportAscAlertsAndRecommendationsToEventhub", "ExportAscAlertsAndRecommendationsToLogAnalytics"] 71 | } 72 | } 73 | ] 74 | } 75 | 76 | 77 | ################## 78 | # Monitoring 79 | ################## 80 | module "org_mg_platform_diagnostics_initiative" { 81 | source = "..//modules/set_assignment" 82 | initiative = module.platform_diagnostics_initiative.initiative 83 | assignment_scope = data.azurerm_management_group.org.id 84 | 85 | # resource remediation options 86 | re_evaluate_compliance = var.re_evaluate_compliance 87 | skip_remediation = var.skip_remediation 88 | skip_role_assignment = var.skip_role_assignment 89 | role_assignment_scope = data.azurerm_management_group.team_a.id # set explicit scopes, omit to use the assignment scope 90 | role_definition_ids = [data.azurerm_role_definition.contributor.id] # set explicit roles, omit to use roles found in the definitions "policyRule" 91 | 92 | # NOTE: You may omit parameters at assignment to use the definitions 'defaultValue' 93 | assignment_parameters = { 94 | workspaceId = local.dummy_resource_ids.azurerm_log_analytics_workspace 95 | storageAccountId = local.dummy_resource_ids.azurerm_storage_account 96 | eventHubName = local.dummy_resource_ids.azurerm_eventhub_namespace 97 | eventHubAuthorizationRuleId = local.dummy_resource_ids.azurerm_eventhub_namespace_authorization_rule 98 | metricsEnabled = "True" 99 | logsEnabled = "True" 100 | effect_DeployApplicationGatewayDiagnosticSetting = "DeployIfNotExists" 101 | effect_DeployEventhubDiagnosticSetting = "DeployIfNotExists" 102 | effect_DeployFirewallDiagnosticSetting = "DeployIfNotExists" 103 | effect_DeployKeyvaultDiagnosticSetting = "AuditIfNotExists" 104 | effect_DeployLoadbalancerDiagnosticSetting = "AuditIfNotExists" 105 | effect_DeployNetworkInterfaceDiagnosticSetting = "AuditIfNotExists" 106 | effect_DeployNetworkSecurityGroupDiagnosticSetting = "AuditIfNotExists" 107 | effect_DeployPublicIpDiagnosticSetting = "AuditIfNotExists" 108 | effect_DeployStorageAccountDiagnosticSetting = "DeployIfNotExists" 109 | effect_DeploySubscriptionDiagnosticSetting = "DeployIfNotExists" 110 | effect_DeployVnetDiagnosticSetting = "AuditIfNotExists" 111 | effect_DeployVnetGatewayDiagnosticSetting = "AuditIfNotExists" 112 | } 113 | 114 | non_compliance_messages = module.platform_diagnostics_initiative.non_compliance_messages 115 | } 116 | 117 | 118 | ################## 119 | # Storage 120 | ################## 121 | module "org_mg_storage_enforce_https" { 122 | source = "..//modules/def_assignment" 123 | definition = module.storage_enforce_https.definition 124 | assignment_scope = data.azurerm_management_group.org.id 125 | assignment_effect = "Deny" 126 | } 127 | 128 | module "org_mg_storage_enforce_minimum_tls1_2" { 129 | source = "..//modules/def_assignment" 130 | definition = module.storage_enforce_minimum_tls1_2.definition 131 | assignment_scope = data.azurerm_management_group.org.id 132 | assignment_effect = "Deny" 133 | } 134 | -------------------------------------------------------------------------------- /examples/assignments_team_a.tf: -------------------------------------------------------------------------------- 1 | ################## 2 | # General 3 | ################## 4 | module "team_a_mg_deny_resource_types" { 5 | source = "..//modules/def_assignment" 6 | definition = module.deny_resource_types.definition 7 | assignment_scope = data.azurerm_management_group.team_a.id 8 | assignment_effect = "Audit" 9 | 10 | assignment_parameters = { 11 | listOfResourceTypesNotAllowed = [ 12 | "Microsoft.Storage/operations", 13 | "Microsoft.Storage/storageAccounts", 14 | "Microsoft.Storage/storageAccounts/blobServices", 15 | "Microsoft.Storage/storageAccounts/blobServices/containers", 16 | "Microsoft.Storage/storageAccounts/listAccountSas", 17 | "Microsoft.Storage/storageAccounts/listServiceSas", 18 | "Microsoft.Storage/usages", 19 | ] 20 | } 21 | } 22 | 23 | ################## 24 | # Network 25 | ################## 26 | module "team_a_mg_deny_nic_public_ip" { 27 | source = "..//modules/def_assignment" 28 | definition = module.deny_nic_public_ip.definition 29 | assignment_scope = data.azurerm_management_group.team_a.id 30 | assignment_effect = "Deny" 31 | } 32 | -------------------------------------------------------------------------------- /examples/backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = ">= 4.12" 7 | } 8 | azuread = { 9 | source = "hashicorp/azuread" 10 | version = ">= 2.45" 11 | } 12 | } 13 | backend "azurerm" {} 14 | } 15 | 16 | provider "azurerm" { 17 | features {} 18 | resource_provider_registrations = "core" 19 | resource_providers_to_register = [ 20 | "Microsoft.PolicyInsights", 21 | "Microsoft.SecurityInsights" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/built-in.tf: -------------------------------------------------------------------------------- 1 | ################## 2 | # Built-In Initiative 3 | ################## 4 | data "azurerm_policy_set_definition" "configure_az_monitor_and_security_vm_initiative" { 5 | name = "a15f3269-2e10-458c-87a4-d5989e678a73" #"[Preview]: Configure machines to automatically install the Azure Monitor and Azure Security agents on virtual machines" 6 | } 7 | 8 | 9 | ################## 10 | # Built-In Assignment 11 | ################## 12 | module "org_mg_configure_az_monitor_and_security_vm_initiative" { 13 | source = "..//modules/set_assignment" 14 | initiative = data.azurerm_policy_set_definition.configure_az_monitor_and_security_vm_initiative 15 | assignment_name = "configure_az_monitor" 16 | assignment_scope = data.azurerm_management_group.org.id 17 | skip_remediation = var.skip_remediation 18 | skip_role_assignment = var.skip_role_assignment 19 | 20 | # built-ins that deploy/modify require role_definition_ids be present 21 | role_definition_ids = [ 22 | data.azurerm_role_definition.vm_contributor.id 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /examples/data.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_client_config" "current" {} 2 | 3 | data "azurerm_subscription" "current" {} 4 | 5 | # Org Management Group 6 | data "azurerm_management_group" "org" { 7 | name = "policy_dev" 8 | } 9 | 10 | # Child Management Group 11 | data "azurerm_management_group" "team_a" { 12 | name = "team_a" 13 | } 14 | 15 | # AAD Group 16 | data "azuread_group" "rem" { 17 | display_name = "Policy Remediators" 18 | security_enabled = true 19 | } 20 | 21 | # Contributor role 22 | data "azurerm_role_definition" "contributor" { 23 | name = "Contributor" 24 | } 25 | 26 | # Virtual Machine Contributor role 27 | data "azurerm_role_definition" "vm_contributor" { 28 | name = "Virtual Machine Contributor" 29 | } 30 | 31 | # User Assigned Managed Identity 32 | data "azurerm_user_assigned_identity" "policy_rem" { 33 | name = "policy-remediator" 34 | resource_group_name = "cgc-cd" 35 | } 36 | 37 | locals { 38 | # existing resources would typically be referenced with a datasource 39 | dummy_resource_ids = { 40 | azurerm_log_analytics_workspace = "/uri/log-analytics-workspace-diagnostics" 41 | azurerm_storage_account = "/uri/storage-account-archive" 42 | azurerm_eventhub_namespace = "/uri/event-hub-namespace-diagnostics" 43 | azurerm_eventhub_namespace_authorization_rule = "/uri/event-hub-namespace-diagnostics/RootManageSharedAccessKey" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/definitions.tf: -------------------------------------------------------------------------------- 1 | ################## 2 | # General 3 | ################## 4 | module "deny_resource_types" { 5 | source = "..//modules/definition" 6 | policy_name = "deny_resource_types" 7 | display_name = "Deny Azure Resource types" 8 | policy_category = "General" 9 | management_group_id = data.azurerm_management_group.org.id 10 | } 11 | 12 | module "whitelist_regions" { 13 | source = "..//modules/definition" 14 | policy_name = "whitelist_regions" 15 | display_name = "Whitelist Azure Regions" 16 | policy_category = "General" 17 | management_group_id = data.azurerm_management_group.org.id 18 | } 19 | 20 | ################## 21 | # Monitoring 22 | ################## 23 | 24 | # create definitions by looping around all files found under the Monitoring category folder 25 | module "deploy_resource_diagnostic_setting" { 26 | source = "..//modules/definition" 27 | for_each = toset([ 28 | for p in fileset(path.module, "../policies/Monitoring/*.json") : 29 | trimsuffix(basename(p), ".json") 30 | ]) 31 | policy_name = each.key 32 | policy_category = "Monitoring" 33 | management_group_id = data.azurerm_management_group.org.id 34 | } 35 | 36 | ################## 37 | # Network 38 | ################## 39 | module "deny_nic_public_ip" { 40 | source = "..//modules/definition" 41 | policy_name = "deny_nic_public_ip" 42 | display_name = "Network interfaces should not have public IPs" 43 | policy_category = "Network" 44 | management_group_id = data.azurerm_management_group.org.id 45 | } 46 | 47 | ################## 48 | # Security Center 49 | ################## 50 | 51 | # create definitions by listing them explicitly 52 | module "configure_asc" { 53 | source = "..//modules/definition" 54 | for_each = toset([ 55 | "auto_enroll_subscriptions", 56 | "auto_provision_log_analytics_agent_custom_workspace", 57 | "auto_set_contact_details", 58 | "export_asc_alerts_and_recommendations_to_eventhub", 59 | "export_asc_alerts_and_recommendations_to_log_analytics", 60 | ]) 61 | policy_name = each.value 62 | policy_category = "Security Center" 63 | management_group_id = data.azurerm_management_group.org.id 64 | } 65 | 66 | ################## 67 | # Storage 68 | ################## 69 | module "storage_enforce_https" { 70 | source = "..//modules/definition" 71 | policy_name = "storage_enforce_https" 72 | display_name = "Secure transfer to storage accounts should be enabled" 73 | policy_category = "Storage" 74 | policy_mode = "Indexed" 75 | management_group_id = data.azurerm_management_group.org.id 76 | } 77 | 78 | module "storage_enforce_minimum_tls1_2" { 79 | source = "..//modules/definition" 80 | policy_name = "storage_enforce_minimum_tls1_2" 81 | display_name = "Minimum TLS version for data in transit to storage accounts should be set" 82 | policy_category = "Storage" 83 | policy_mode = "Indexed" 84 | management_group_id = data.azurerm_management_group.org.id 85 | } 86 | 87 | ################## 88 | # Point to a specific filepath 89 | ################## 90 | module "file_path_test" { 91 | source = "..//modules/definition" 92 | file_path = "${path.module}/../policies/Automation/onboard_to_automation_dsc_windows.json" 93 | management_group_id = data.azurerm_management_group.org.id 94 | } 95 | 96 | ################## 97 | # Supply some or all policy object properties at runtime 98 | ################## 99 | locals { 100 | policy_file = jsondecode(file("${path.module}/../policies/Automation/onboard_to_automation_dsc_linux.json")) 101 | } 102 | 103 | module "parameterised_test" { 104 | source = "..//modules/definition" 105 | policy_name = "Custom Name" 106 | display_name = "Custom Display Name" 107 | policy_description = "Custom Description" 108 | policy_category = "Custom Category" 109 | policy_version = "Custom Version" 110 | management_group_id = data.azurerm_management_group.org.id 111 | 112 | policy_rule = (local.policy_file).properties.policyRule 113 | policy_parameters = (local.policy_file).properties.parameters 114 | policy_metadata = (local.policy_file).properties.metadata 115 | } 116 | -------------------------------------------------------------------------------- /examples/duplicate_members.tf: -------------------------------------------------------------------------------- 1 | ################## 2 | # This ResourceTags example demonstrates an initiative containing duplicate member definitions 3 | ################## 4 | 5 | ### DEFINITIONS 6 | module "inherit_resource_group_tags_modify" { 7 | source = "..//modules/definition" 8 | policy_name = "inherit_resource_group_tags_modify" 9 | display_name = "Resources should inherit Resource Group Tags and Values with Modify Remediation" 10 | policy_category = "Tags" 11 | policy_mode = "Indexed" 12 | management_group_id = data.azurerm_management_group.org.id 13 | } 14 | 15 | module "require_resource_group_tags" { 16 | source = "..//modules/definition" 17 | policy_name = "require_resource_group_tags" 18 | display_name = "ResourceGroups require specific tags to be present" 19 | policy_category = "Tags" 20 | policy_mode = "Indexed" 21 | management_group_id = data.azurerm_management_group.org.id 22 | } 23 | 24 | ### INITIATIVE 25 | module "resource_group_tags" { 26 | source = "..//modules/initiative" 27 | initiative_name = "resource_group_tags" 28 | initiative_display_name = "[Tags]: Require & Inherit ResourceGroup Tags" 29 | initiative_description = "Ensures ResourceGroup tags are inherited by its resources" 30 | initiative_category = "Tags" 31 | management_group_id = data.azurerm_management_group.team_a.id 32 | duplicate_members = true # this must be 'true' for the module to handle duplicate defs 33 | merge_parameters = false # this must be 'false' for each occurance to have unique params and references 34 | 35 | # include the same policy as many time as needed 36 | # NOTE: be cautious when changing the position of members as these reflect the index numbers used in 'assignment_parameters' below 37 | member_definitions = [ 38 | module.inherit_resource_group_tags_modify.definition, 39 | module.inherit_resource_group_tags_modify.definition, 40 | module.inherit_resource_group_tags_modify.definition, 41 | module.inherit_resource_group_tags_modify.definition, 42 | module.require_resource_group_tags.definition, 43 | module.require_resource_group_tags.definition, 44 | module.require_resource_group_tags.definition, 45 | module.require_resource_group_tags.definition, 46 | ] 47 | } 48 | 49 | ### ASSIGNMENT 50 | module "team_a_mg_resource_group_tags" { 51 | source = "..//modules/set_assignment" 52 | initiative = module.resource_group_tags.initiative 53 | assignment_scope = data.azurerm_management_group.team_a.id 54 | assignment_location = "ukwest" 55 | 56 | # resource remediation options 57 | re_evaluate_compliance = var.re_evaluate_compliance 58 | skip_remediation = var.skip_remediation 59 | skip_role_assignment = var.skip_role_assignment 60 | 61 | assignment_parameters = { 62 | tagName_0_InheritResourceGroupTagsModify = "DepartmentName" 63 | tagName_1_InheritResourceGroupTagsModify = "CostCode" 64 | tagName_2_InheritResourceGroupTagsModify = "ProductCode" 65 | tagName_3_InheritResourceGroupTagsModify = "Environment" 66 | tagName_4_RequireResourceGroupTags = "DepartmentName" 67 | tagName_5_RequireResourceGroupTags = "CostCode" 68 | tagName_6_RequireResourceGroupTags = "ProductCode" 69 | tagName_7_RequireResourceGroupTags = "Environment" 70 | effect_7_RequireResourceGroupTags = "Disabled" 71 | } 72 | 73 | non_compliance_messages = module.resource_group_tags.non_compliance_messages 74 | } 75 | -------------------------------------------------------------------------------- /examples/exemptions.tf: -------------------------------------------------------------------------------- 1 | # Subscription Scope Resource Exemption 2 | module "exemption_subscription_diagnostics_settings" { 3 | source = "..//modules/exemption" 4 | name = "Subscription Diagnostic Settings Exemption" 5 | display_name = "Exempted while testing" 6 | description = "Excludes subscription from configuring diagnostics settings" 7 | scope = data.azurerm_subscription.current.id 8 | policy_assignment_id = module.org_mg_platform_diagnostics_initiative.id 9 | exemption_category = "Waiver" 10 | expires_on = "2025-05-25" 11 | 12 | # use member_definition_names for simplicity when policy_definition_reference_ids are unknown 13 | member_definition_names = [ 14 | "deploy_subscription_diagnostic_setting" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /examples/initiatives.tf: -------------------------------------------------------------------------------- 1 | ################## 2 | # Security Center 3 | ################## 4 | module "configure_asc_initiative" { 5 | source = "..//modules/initiative" 6 | initiative_name = "configure_asc_initiative" 7 | initiative_display_name = "[Security]: Configure Azure Security Center" 8 | initiative_description = "Deploys and configures Azure Security Center settings and defines exports" 9 | initiative_category = "Security Center" 10 | initiative_version = "2.0.0" 11 | management_group_id = data.azurerm_management_group.org.id 12 | 13 | # Populate member_definitions 14 | member_definitions = [ 15 | module.configure_asc["auto_enroll_subscriptions"].definition, 16 | module.configure_asc["auto_provision_log_analytics_agent_custom_workspace"].definition, 17 | module.configure_asc["auto_set_contact_details"].definition, 18 | module.configure_asc["export_asc_alerts_and_recommendations_to_eventhub"].definition, 19 | module.configure_asc["export_asc_alerts_and_recommendations_to_log_analytics"].definition, 20 | ] 21 | } 22 | 23 | ################## 24 | # Monitoring: Resource & Activity Log Forwarders 25 | ################## 26 | module "platform_diagnostics_initiative" { 27 | source = "..//modules/initiative" 28 | initiative_name = "platform_diagnostics_initiative" 29 | initiative_display_name = "[Platform]: Diagnostics Settings Policy Initiative" 30 | initiative_description = "Collection of policies that deploy resource and activity log forwarders to logging core resources" 31 | initiative_category = "Monitoring" 32 | merge_effects = false # will not merge "effect" parameters 33 | management_group_id = data.azurerm_management_group.org.id 34 | 35 | # Populate member_definitions with a for loop 36 | member_definitions = [for mon in module.deploy_resource_diagnostic_setting : mon.definition] 37 | } 38 | -------------------------------------------------------------------------------- /examples/variables.tf: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------------------------------------------------- 2 | # Module variables 3 | # ---------------------------------------------------------------------------------------------------------------------- 4 | 5 | variable "skip_remediation" { 6 | type = bool 7 | description = "Skip creation of all remediation tasks for policies that DeployIfNotExists and Modify" 8 | default = false 9 | } 10 | 11 | variable "skip_role_assignment" { 12 | type = bool 13 | description = "Should the module skip creation of role assignment for policies that DeployIfNotExists and Modify" 14 | default = false 15 | } 16 | 17 | variable "re_evaluate_compliance" { 18 | type = bool 19 | description = "Should the module re-evaluate compliant resources for policies that DeployIfNotExists and Modify" 20 | default = false 21 | } 22 | -------------------------------------------------------------------------------- /img/logo-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gettek/terraform-azurerm-policy-as-code/df7508a3d696c407aa38f087482591295d5ac066/img/logo-social.png -------------------------------------------------------------------------------- /modules/def_assignment/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | description = "The Policy Assignment Id" 3 | value = local.assignment.id 4 | } 5 | 6 | output "identity_id" { 7 | description = "The Managed Identity block containing Principal Id & Tenant Id of this Policy Assignment if type is SystemAssigned" 8 | value = try(local.assignment.identity[0].principal_id, null) 9 | } 10 | 11 | output "remediation_id" { 12 | description = "The Id of the remediation task" 13 | value = local.remediation_id 14 | } 15 | 16 | output "role_definition_ids" { 17 | description = "The List of Role Definition Ids assignable to the managed identity" 18 | value = local.role_definition_ids 19 | } 20 | -------------------------------------------------------------------------------- /modules/def_assignment/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = ">= 4.12" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /modules/definition/README.md: -------------------------------------------------------------------------------- 1 | 2 | # POLICY DEFINITION MODULE 3 | 4 | This module depends on populating `var.policy_name` and `var.policy_category` to correspond with the respective custom policy definition `json` file found in the [local library](../../policies). You can also parse in other template files and data sources at runtime, see below for examples and acceptable inputs. 5 | 6 | > 💡 **Note:** More information on Policy Definition Structure [can be found here](https://learn.microsoft.com/en-us/azure/governance/policy/concepts/definition-structure) 7 | 8 | > 💡 **Note:** Specify the `policy_mode` variable if you wish to [change the mode](https://learn.microsoft.com/en-us/azure/governance/policy/concepts/definition-structure#mode) which defaults to `All`. Possible values below. 9 | 10 | ## Examples 11 | 12 | ### Create a basic Policy Definition from a file located in the module library 13 | 14 | ```hcl 15 | module whitelist_regions { 16 | source = "gettek/policy-as-code/azurerm//modules/definition" 17 | policy_name = "whitelist_regions" 18 | display_name = "Allow resources only in whitelisted regions" 19 | policy_category = "General" 20 | management_group_id = data.azurerm_management_group.org.id 21 | } 22 | ``` 23 | 24 | ### Loop around a map to quickly create multiple definitions 25 | ```hcl 26 | locals { 27 | security_center_policies = { 28 | auto_enroll_subscriptions = "Enable Azure Security Center on Subcriptions" 29 | auto_provision_log_analytics_agent_custom_workspace = "Enable Security Center's auto provisioning of the Log Analytics agent on your subscriptions with custom workspace" 30 | auto_set_contact_details = "Automatically set the security contact email address and phone number should they be blank on the subscription" 31 | export_asc_alerts_and_recommendations_to_eventhub = "Export to Event Hub for Azure Security Center alerts and recommendations" 32 | export_asc_alerts_and_recommendations_to_log_analytics = "Export to Log Analytics Workspace for Azure Security Center alerts and recommendations" 33 | } 34 | } 35 | 36 | module "configure_asc" { 37 | source = "gettek/policy-as-code/azurerm//modules/definition" 38 | for_each = local.security_center_policies 39 | policy_name = each.key 40 | display_name = title(replace(each.key, "_", " ")) 41 | policy_description = each.value 42 | policy_category = "Security Center" 43 | management_group_id = data.azurerm_management_group.org.id 44 | } 45 | ``` 46 | 47 | ### Use definition files located outside of the module library 48 | 49 | ```hcl 50 | module "file_path_test" { 51 | source = "gettek/policy-as-code/azurerm//modules/definition" 52 | file_path = "../path/to/file/onboard_to_automation_dsc_linux.json" 53 | management_group_id = data.azurerm_management_group.org.id 54 | } 55 | ``` 56 | 57 | Loop around a folders contents to create multiple definitions: 58 | 59 | ```hcl 60 | module "iam_test" { 61 | source = "gettek/policy-as-code/azurerm//modules/definition" 62 | for_each = { 63 | for p in fileset(path.module, "../../azure/governance/policies/Storage/*.json") : 64 | trimsuffix(basename(p), ".json") => pathexpand(p) 65 | } 66 | file_path = each.value 67 | management_group_id = data.azurerm_management_group.org.id 68 | } 69 | ``` 70 | 71 | You will also be able to supply object properties at runtime such as: 72 | 73 | ```hcl 74 | locals { 75 | policy_file = jsondecode(file("onboard_to_automation_dsc_linux.json")) 76 | } 77 | 78 | module "parameterised_test" { 79 | source = "gettek/policy-as-code/azurerm//modules/definition" 80 | policy_name = "Custom Name" 81 | display_name = "Custom Display Name" 82 | policy_description = "Custom Description" 83 | policy_category = "Custom Category" 84 | policy_version = "Custom Version" 85 | management_group_id = data.azurerm_management_group.org.id 86 | 87 | policy_rule = (local.policy_file).properties.policyRule 88 | policy_parameters = (local.policy_file).properties.parameters 89 | policy_metadata = (local.policy_file).properties.metadata 90 | } 91 | ``` 92 | 93 | ## Requirements 94 | 95 | | Name | Version | 96 | |------|---------| 97 | | terraform | >= 1.4 | 98 | | azurerm | >= 4.12 | 99 | 100 | 101 | 102 | ## Resources 103 | 104 | | Name | Type | 105 | |------|------| 106 | | [azurerm_policy_definition.def](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/policy_definition) | resource | 107 | | [random_string.set_replace](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | 108 | 109 | ## Inputs 110 | 111 | | Name | Description | Type | Default | Required | 112 | |------|-------------|------|---------|:--------:| 113 | | display_name | Display Name to be used for this policy | `string` | `""` | no | 114 | | file_path | The filepath to the custom policy. Omitting this assumes the policy is located in the module library | `any` | `null` | no | 115 | | management_group_id | The management group scope at which the policy will be defined. Defaults to current Subscription if omitted. Changing this forces a new resource to be created. | `string` | `null` | no | 116 | | policy_category | The category of the policy, when using the module library this should correspond to the correct category folder under /policies/ | `string` | `null` | no | 117 | | policy_description | Policy definition description | `string` | `""` | no | 118 | | policy_metadata | The metadata for the policy definition. This is a JSON object representing additional metadata that should be stored with the policy definition. Omitting this will fallback to meta in the definition or merge var.policy_category and var.policy_version | `any` | `null` | no | 119 | | policy_mode | Specify which Resource Provider modes will be evaluated, defaults to All. Possible values are All, Indexed, Microsoft.Kubernetes.Data, Microsoft.KeyVault.Data or Microsoft.Network.Data | `string` | `null` | no | 120 | | policy_name | Name to be used for this policy, when using the module library this should correspond to the correct category folder under /policies/policy_category/policy_name. Changing this forces a new resource to be created. | `string` | `""` | no | 121 | | policy_parameters | Parameters for the policy definition. This field is a JSON object representing the parameters of your policy definition. Omitting this assumes the parameters are located in the policy file | `any` | `null` | no | 122 | | policy_rule | The policy rule for the policy definition. This is a JSON object representing the rule that contains an if and a then block. Omitting this assumes the rules are located in the policy file | `any` | `null` | no | 123 | | policy_version | The version for this policy, if different from the one stored in the definition metadata, defaults to 1.0.0 | `string` | `null` | no | 124 | 125 | ## Outputs 126 | 127 | | Name | Description | 128 | |------|-------------| 129 | | definition | The combined Policy Definition resource node | 130 | | id | The Id of the Policy Definition | 131 | | metadata | The metadata of the Policy Definition | 132 | | name | The name of the Policy Definition | 133 | | parameters | The parameters of the Policy Definition | 134 | | rules | The rules of the Policy Definition | 135 | -------------------------------------------------------------------------------- /modules/definition/main.tf: -------------------------------------------------------------------------------- 1 | resource "random_string" "set_replace" { 2 | length = 3 3 | special = false 4 | 5 | keepers = { 6 | parameters = length(local.parameters) > 0 ? md5(jsonencode(local.parameters)) : "asdf" 7 | } 8 | } 9 | 10 | 11 | resource "azurerm_policy_definition" "def" { 12 | name = join("_", [substr(local.policy_name, 0, 60), random_string.set_replace.result]) 13 | display_name = local.display_name 14 | description = local.description 15 | policy_type = "Custom" 16 | mode = local.mode 17 | 18 | management_group_id = var.management_group_id 19 | 20 | metadata = jsonencode(local.metadata) 21 | parameters = length(local.parameters) > 0 ? jsonencode(local.parameters) : null 22 | policy_rule = jsonencode(local.policy_rule) 23 | 24 | lifecycle { 25 | create_before_destroy = true 26 | } 27 | 28 | timeouts { 29 | read = "10m" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /modules/definition/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | description = "The Id of the Policy Definition" 3 | value = local.definition_id 4 | } 5 | 6 | output "name" { 7 | description = "The name of the Policy Definition" 8 | value = var.policy_name 9 | } 10 | 11 | output "rules" { 12 | description = "The rules of the Policy Definition" 13 | value = local.policy_rule 14 | } 15 | 16 | output "parameters" { 17 | description = "The parameters of the Policy Definition" 18 | value = local.parameters 19 | } 20 | 21 | output "metadata" { 22 | description = "The metadata of the Policy Definition" 23 | value = local.metadata 24 | } 25 | 26 | output "definition" { 27 | description = "The combined Policy Definition resource node" 28 | value = { 29 | id = local.definition_id 30 | name = local.policy_name 31 | display_name = local.display_name 32 | description = local.description 33 | mode = local.mode 34 | management_group_id = var.management_group_id 35 | metadata = jsonencode(local.metadata) 36 | parameters = jsonencode(local.parameters) 37 | policy_rule = jsonencode(local.policy_rule) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /modules/definition/variables.tf: -------------------------------------------------------------------------------- 1 | variable "management_group_id" { 2 | type = string 3 | description = "The management group scope at which the policy will be defined. Defaults to current Subscription if omitted. Changing this forces a new resource to be created." 4 | default = null 5 | } 6 | 7 | variable "policy_name" { 8 | type = string 9 | description = "Name to be used for this policy, when using the module library this should correspond to the correct category folder under /policies/policy_category/policy_name. Changing this forces a new resource to be created." 10 | default = "" 11 | 12 | validation { 13 | condition = length(var.policy_name) <= 64 14 | error_message = "Definition names have a maximum 64 character limit, ensure this matches the filename within the local policies library." 15 | } 16 | } 17 | 18 | variable "display_name" { 19 | type = string 20 | description = "Display Name to be used for this policy" 21 | default = "" 22 | 23 | validation { 24 | condition = length(var.display_name) <= 128 25 | error_message = "Definition display names have a maximum 128 character limit." 26 | } 27 | } 28 | 29 | variable "policy_description" { 30 | type = string 31 | description = "Policy definition description" 32 | default = "" 33 | 34 | validation { 35 | condition = length(var.policy_description) <= 512 36 | error_message = "Definition descriptions have a maximum 512 character limit." 37 | } 38 | } 39 | 40 | variable "policy_mode" { 41 | type = string 42 | description = "Specify which Resource Provider modes will be evaluated, defaults to All. Possible values are All, Indexed, Microsoft.Kubernetes.Data, Microsoft.KeyVault.Data or Microsoft.Network.Data" 43 | default = null 44 | 45 | validation { 46 | condition = var.policy_mode == null || var.policy_mode == "All" || var.policy_mode == "Indexed" || var.policy_mode == "Microsoft.Kubernetes.Data" || var.policy_mode == "Microsoft.KeyVault.Data" || var.policy_mode == "Microsoft.Network.Data" 47 | error_message = "Policy mode possible values are: All, Indexed, Microsoft.Kubernetes.Data, Microsoft.KeyVault.Data or Microsoft.Network.Data. Unless explicitly stated, Resource Provider modes only support built-in policy definitions, and exemptions are not supported at the component-level." 48 | } 49 | } 50 | 51 | variable "policy_category" { 52 | type = string 53 | description = "The category of the policy, when using the module library this should correspond to the correct category folder under /policies/" 54 | default = null 55 | } 56 | 57 | variable "policy_version" { 58 | type = string 59 | description = "The version for this policy, if different from the one stored in the definition metadata, defaults to 1.0.0" 60 | default = null 61 | } 62 | 63 | variable "policy_rule" { 64 | type = any 65 | description = "The policy rule for the policy definition. This is a JSON object representing the rule that contains an if and a then block. Omitting this assumes the rules are located in the policy file" 66 | default = null 67 | } 68 | 69 | variable "policy_parameters" { 70 | type = any 71 | description = "Parameters for the policy definition. This field is a JSON object representing the parameters of your policy definition. Omitting this assumes the parameters are located in the policy file" 72 | default = null 73 | } 74 | 75 | variable "policy_metadata" { 76 | type = any 77 | description = "The metadata for the policy definition. This is a JSON object representing additional metadata that should be stored with the policy definition. Omitting this will fallback to meta in the definition or merge var.policy_category and var.policy_version" 78 | default = null 79 | } 80 | 81 | variable "file_path" { 82 | type = any 83 | description = "The filepath to the custom policy. Omitting this assumes the policy is located in the module library" 84 | default = null 85 | } 86 | 87 | locals { 88 | # import the custom policy object from a library or specified file path 89 | policy_object = jsondecode(coalesce(try( 90 | file(var.file_path), 91 | file("${path.cwd}/policies/${title(var.policy_category)}/${var.policy_name}.json"), 92 | file("${path.root}/policies/${title(var.policy_category)}/${var.policy_name}.json"), 93 | file("${path.root}/../policies/${title(var.policy_category)}/${var.policy_name}.json"), 94 | file("${path.module}/../../policies/${title(var.policy_category)}/${var.policy_name}.json"), 95 | "{}" # return empty object if no policy is found 96 | ))) 97 | 98 | # fallbacks 99 | title = title(replace(local.policy_name, "/-|_|\\s/", " ")) 100 | category = coalesce(var.policy_category, try((local.policy_object).properties.metadata.category, "General")) 101 | version = coalesce(var.policy_version, try((local.policy_object).properties.metadata.version, "1.0.0")) 102 | mode = coalesce(var.policy_mode, try((local.policy_object).properties.mode, "All")) 103 | 104 | # use local library attributes if runtime inputs are omitted 105 | policy_name = coalesce(var.policy_name, try((local.policy_object).name, null)) 106 | display_name = coalesce(var.display_name, try((local.policy_object).properties.displayName, local.title)) 107 | description = coalesce(var.policy_description, try((local.policy_object).properties.description, local.title)) 108 | metadata = coalesce(null, var.policy_metadata, try((local.policy_object).properties.metadata, merge({ category = local.category }, { version = local.version }))) 109 | parameters = coalesce(null, var.policy_parameters, try((local.policy_object).properties.parameters, {})) 110 | policy_rule = coalesce(var.policy_rule, try((local.policy_object).properties.policyRule, null)) 111 | 112 | # manually generate the definition Id to prevent "Invalid for_each argument" on set_assignment plan/apply 113 | definition_id = var.management_group_id != null ? "${var.management_group_id}/providers/Microsoft.Authorization/policyDefinitions/${azurerm_policy_definition.def.name}" : azurerm_policy_definition.def.id 114 | } 115 | -------------------------------------------------------------------------------- /modules/definition/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = ">= 4.12" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /modules/exemption/README.md: -------------------------------------------------------------------------------- 1 | 2 | # POLICY EXEMPTION MODULE 3 | 4 | Exemptions can be used where `not_scopes` become time sensitive or require alternative methods of approval for audit trails. Learn more about Azure Policy [exemption structure](https://learn.microsoft.com/en-us/azure/governance/policy/concepts/exemption-structure). 5 | 6 | > 💡**Note:** This module also allows you to exempt multiple scope types at once (e.g. resource group and individual resource) when using a `for_each` loop as in the example below. 7 | 8 | ## Examples 9 | 10 | ### At Management Group Scope with Optional Metadata 11 | 12 | ```hcl 13 | module exemption_team_a_mg_deny_nic_public_ip { 14 | source = "gettek/policy-as-code/azurerm//modules/exemption" 15 | name = "Deny NIC Public IP Exemption" 16 | display_name = "Exempted while testing" 17 | description = "Allows NIC Public IPs for testing" 18 | scope = data.azurerm_management_group.team_a.id 19 | policy_assignment_id = module.team_a_mg_deny_nic_public_ip.id 20 | exemption_category = "Waiver" 21 | expires_on = "2023-05-25" 22 | 23 | # optional metadata, these can consist of any key/value pairs, or omit to have none 24 | metadata = { 25 | requested_by = "Team A" 26 | approved_by = "Mr Smith" 27 | approved_date = "2021-11-30" 28 | ticket_ref = "1923" 29 | } 30 | } 31 | ``` 32 | 33 | ### Exempt multiple scope types in a for_each loop 34 | 35 | ```hcl 36 | module exemption_team_a_mg_deny_nic_public_ip { 37 | source = "gettek/policy-as-code/azurerm//modules/exemption" 38 | for_each = toset([ 39 | data.azurerm_management_group.team_a.id, 40 | data.azurerm_subscription.current.id, 41 | data.azurerm_resource_group.example.id 42 | data.azurerm_network_interface.example.id 43 | ]) 44 | name = "Deny NIC Public IP Exemption" 45 | display_name = "Exempted while testing" 46 | description = "Allows NIC Public IPs for testing" 47 | scope = each.value 48 | policy_assignment_id = module.team_a_mg_deny_nic_public_ip.id 49 | } 50 | ``` 51 | 52 | ### Exempt a subset of definitions within an Initiative at Subscription Scope 53 | 54 | ```hcl 55 | module "exemption_configure_asc_initiative" { 56 | source = "gettek/policy-as-code/azurerm//modules/exemption" 57 | name = "Onboard subscription to ASC Exemption" 58 | display_name = "Exempted while testing" 59 | description = "Excludes subscription from ASC onboarding during development" 60 | scope = data.azurerm_subscription.current.id 61 | policy_assignment_id = module.org_mg_configure_asc_initiative.id 62 | 63 | # use member_definition_names for simplicity when policy_definition_reference_ids are unknown 64 | member_definition_names = [ 65 | "auto_provision_log_analytics_agent_custom_workspace", 66 | "auto_set_contact_details" 67 | ] 68 | } 69 | ``` 70 | 71 | ### Resource Group Exemption 72 | 73 | ```hcl 74 | module exemption_team_a_mg_deny_nic_public_ip { 75 | source = "gettek/policy-as-code/azurerm//modules/exemption" 76 | name = "Deny NIC Public IP Exemption" 77 | display_name = "Exempted while testing" 78 | description = "Allows NIC Public IPs for testing" 79 | scope = data.azurerm_resource_group.example.id 80 | policy_assignment_id = module.team_a_mg_deny_nic_public_ip.id 81 | } 82 | ``` 83 | 84 | ### Multiple Resource exemptions for all KeyVaults in a Resource Group 85 | 86 | Use `azurerm_resources` to retrieve a list of resource types within the same resource group or specify a simpler (more declarative) array of your choosing. 87 | 88 | ```hcl 89 | data azurerm_resources keyvaults { 90 | type = "Microsoft.KeyVault/vaults" 91 | resource_group_name = "rg-dev-uks-vaults" 92 | } 93 | 94 | module exemption_team_a_mg_key_vaults_require_purge_protection { 95 | source = "gettek/policy-as-code/azurerm//modules/exemption" 96 | for_each = toset(data.azurerm_resources.keyvaults.resources.*.id) 97 | name = "Key vaults should have purge protection enabled Exemption" 98 | display_name = "Exempted while testing" 99 | description = "Do not require purge protection on KVs while testing" 100 | scope = each.value 101 | policy_assignment_id = module.team_a_mg_key_vaults_require_purge_protection.id 102 | exemption_category = "Mitigated" 103 | expires_on = "2023-05-25" 104 | } 105 | ``` 106 | 107 | ## Requirements 108 | 109 | | Name | Version | 110 | |------|---------| 111 | | terraform | >= 0.13 | 112 | | azurerm | >= 3.23 | 113 | 114 | 115 | 116 | ## Resources 117 | 118 | | Name | Type | 119 | |------|------| 120 | | [azurerm_management_group_policy_exemption.management_group_exemption](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/management_group_policy_exemption) | resource | 121 | | [azurerm_resource_group_policy_exemption.resource_group_exemption](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group_policy_exemption) | resource | 122 | | [azurerm_resource_policy_exemption.resource_exemption](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_policy_exemption) | resource | 123 | | [azurerm_subscription_policy_exemption.subscription_exemption](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subscription_policy_exemption) | resource | 124 | 125 | ## Inputs 126 | 127 | | Name | Description | Type | Default | Required | 128 | |------|-------------|------|---------|:--------:| 129 | | description | Description for the Policy Exemption | `string` | n/a | yes | 130 | | display_name | Display name for the Policy Exemption | `string` | n/a | yes | 131 | | exemption_category | The policy exemption category. Possible values are Waiver or Mitigated. Defaults to Waiver | `string` | `"Waiver"` | no | 132 | | expires_on | Optional expiration date (format yyyy-mm-dd) of the policy exemption. Defaults to no expiry | `string` | `null` | no | 133 | | member_definition_names | Generate the definition reference Ids from the member definition names when 'policy_definition_reference_ids' are unknown. Omit to exempt all member definitions | `list(string)` | `[]` | no | 134 | | metadata | Optional policy exemption metadata. For example but not limited to; requestedBy, approvedBy, approvedOn, ticketRef, etc | `any` | `null` | no | 135 | | name | Name for the Policy Exemption | `string` | n/a | yes | 136 | | policy_assignment_id | The ID of the policy assignment that is being exempted | `string` | n/a | yes | 137 | | policy_definition_reference_ids | The optional policy definition reference ID list when the associated policy assignment is an assignment of a policy set definition. Omit to exempt all member definitions | `list(string)` | `[]` | no | 138 | | scope | Scope for the Policy Exemption | `string` | n/a | yes | 139 | 140 | ## Outputs 141 | 142 | | Name | Description | 143 | |------|-------------| 144 | | exemption | The Policy Exemption Details | 145 | -------------------------------------------------------------------------------- /modules/exemption/main.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_management_group_policy_exemption" "management_group_exemption" { 2 | count = local.exemption_scope.mg 3 | name = var.name 4 | display_name = var.display_name 5 | description = var.description 6 | management_group_id = var.scope 7 | policy_assignment_id = var.policy_assignment_id 8 | exemption_category = var.exemption_category 9 | expires_on = local.expires_on 10 | policy_definition_reference_ids = local.policy_definition_reference_ids 11 | metadata = local.metadata 12 | } 13 | 14 | resource "azurerm_subscription_policy_exemption" "subscription_exemption" { 15 | count = local.exemption_scope.sub 16 | name = var.name 17 | display_name = var.display_name 18 | description = var.description 19 | subscription_id = var.scope 20 | policy_assignment_id = var.policy_assignment_id 21 | exemption_category = var.exemption_category 22 | expires_on = local.expires_on 23 | policy_definition_reference_ids = local.policy_definition_reference_ids 24 | metadata = local.metadata 25 | } 26 | 27 | resource "azurerm_resource_group_policy_exemption" "resource_group_exemption" { 28 | count = local.exemption_scope.rg 29 | name = var.name 30 | display_name = var.display_name 31 | description = var.description 32 | resource_group_id = var.scope 33 | policy_assignment_id = var.policy_assignment_id 34 | exemption_category = var.exemption_category 35 | expires_on = local.expires_on 36 | policy_definition_reference_ids = local.policy_definition_reference_ids 37 | metadata = local.metadata 38 | } 39 | 40 | resource "azurerm_resource_policy_exemption" "resource_exemption" { 41 | count = local.exemption_scope.resource 42 | name = var.name 43 | display_name = var.display_name 44 | description = var.description 45 | resource_id = var.scope 46 | policy_assignment_id = var.policy_assignment_id 47 | exemption_category = var.exemption_category 48 | expires_on = local.expires_on 49 | policy_definition_reference_ids = local.policy_definition_reference_ids 50 | metadata = local.metadata 51 | } 52 | -------------------------------------------------------------------------------- /modules/exemption/outputs.tf: -------------------------------------------------------------------------------- 1 | output "exemption" { 2 | description = "The Policy Exemption Details" 3 | value = { 4 | name = var.name 5 | id = local.exemption_id 6 | category = var.exemption_category 7 | scope = var.scope 8 | metadata = var.metadata 9 | definition_reference_ids = local.policy_definition_reference_ids 10 | expires_on = local.expires_on 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/exemption/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | description = "Name for the Policy Exemption" 4 | 5 | validation { 6 | condition = length(var.name) <= 64 7 | error_message = "Exemption names have a maximum 64 character limit." 8 | } 9 | } 10 | 11 | variable "display_name" { 12 | type = string 13 | description = "Display name for the Policy Exemption" 14 | 15 | validation { 16 | condition = length(var.display_name) <= 128 17 | error_message = "Exemption display names have a maximum 128 character limit." 18 | } 19 | } 20 | 21 | variable "description" { 22 | type = string 23 | description = "Description for the Policy Exemption" 24 | 25 | validation { 26 | condition = length(var.description) <= 512 27 | error_message = "Exemption descriptions have a maximum 512 character limit." 28 | } 29 | } 30 | 31 | variable "scope" { 32 | type = string 33 | description = "Scope for the Policy Exemption" 34 | } 35 | 36 | variable "policy_assignment_id" { 37 | type = string 38 | description = "The ID of the policy assignment that is being exempted" 39 | } 40 | 41 | variable "policy_definition_reference_ids" { 42 | type = list(string) 43 | description = "The optional policy definition reference ID list when the associated policy assignment is an assignment of a policy set definition. Omit to exempt all member definitions" 44 | default = [] 45 | } 46 | 47 | variable "member_definition_names" { 48 | type = list(string) 49 | description = "Generate the definition reference Ids from the member definition names when 'policy_definition_reference_ids' are unknown. Omit to exempt all member definitions" 50 | default = [] 51 | } 52 | 53 | variable "exemption_category" { 54 | type = string 55 | description = "The policy exemption category. Possible values are Waiver or Mitigated. Defaults to Waiver" 56 | default = "Waiver" 57 | 58 | validation { 59 | condition = var.exemption_category == "Waiver" || var.exemption_category == "Mitigated" 60 | error_message = "Exemption category possible values are: Waiver or Mitigated." 61 | } 62 | } 63 | 64 | variable "expires_on" { 65 | type = string 66 | description = "Optional expiration date (format yyyy-mm-dd) of the policy exemption. Defaults to no expiry" 67 | default = null 68 | } 69 | 70 | variable "metadata" { 71 | type = any 72 | description = "Optional policy exemption metadata. For example but not limited to; requestedBy, approvedBy, approvedOn, ticketRef, etc" 73 | default = null 74 | } 75 | 76 | locals { 77 | exemption_scope = try({ 78 | mg = length(regexall("(\\/managementGroups\\/)", var.scope)) > 0 ? 1 : 0, 79 | sub = length(split("/", var.scope)) == 3 ? 1 : 0, 80 | rg = length(regexall("(\\/managementGroups\\/)", var.scope)) < 1 ? length(split("/", var.scope)) == 5 ? 1 : 0 : 0, 81 | resource = length(split("/", var.scope)) >= 6 ? 1 : 0, 82 | }) 83 | 84 | expires_on = var.expires_on != null ? "${var.expires_on}T23:00:00Z" : null 85 | 86 | metadata = var.metadata != null ? jsonencode(var.metadata) : null 87 | 88 | # generate reference Ids when unknown, assumes the set was created with the initiative module 89 | policy_definition_reference_ids = length(var.member_definition_names) > 0 ? [for name in var.member_definition_names : 90 | replace(title(replace(name, "/-|_|\\s/", " ")), "/\\s/", "") 91 | ] : var.policy_definition_reference_ids 92 | 93 | exemption_id = try( 94 | azurerm_management_group_policy_exemption.management_group_exemption[0].id, 95 | azurerm_subscription_policy_exemption.subscription_exemption[0].id, 96 | azurerm_resource_group_policy_exemption.resource_group_exemption[0].id, 97 | azurerm_resource_policy_exemption.resource_exemption[0].id, 98 | "") 99 | } 100 | -------------------------------------------------------------------------------- /modules/exemption/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.13" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = ">= 3.23" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /modules/initiative/README.md: -------------------------------------------------------------------------------- 1 | 2 | # POLICY INITIATIVE MODULE 3 | 4 | Dynamically creates a policy set based on multiple custom or built-in policy definitions 5 | 6 | > ⚠️ **Warning:** To simplify assignments, if any `member_definitions` contain the same parameter names they will be [merged](https://www.terraform.io/language/functions/merge) unless you specify `merge_effects = false` or `merge_parameters = false` as described in the third example below. When `false` parameters will be suffixed with their respective reference Ids e.g. `"effect_AutoEnrollSubscriptions"`. 7 | 8 | ## Examples 9 | 10 | ### Create an Initiative with duplicate member definitions 11 | 12 | In many cases, some initiatives such as those for tagging, may need to reuse the same definition multiple times but with different parameters to simplify assignments. 13 | 14 | Please see [duplicate_members.tf](../../examples/duplicate_members.tf) as en example use case. 15 | 16 | > 💡 **Note:** you must set `duplicate_members=true` and `merge_parameters=false` when building initiatives with duplicate members.
17 | > 💡 **Note:** Be cautious when changing the position of `member_definitions` as these reflect the index numbers used in `assignment_parameters`. 18 | 19 | ### Create an Initiative with custom Policy definitions 20 | 21 | ```hcl 22 | module configure_asc_initiative { 23 | source = "gettek/policy-as-code/azurerm//modules/initiative" 24 | initiative_name = "configure_asc_initiative" 25 | initiative_display_name = "[Security]: Configure Azure Security Center" 26 | initiative_description = "Deploys and configures Azure Security Center settings and defines exports" 27 | initiative_category = "Security Center" 28 | management_group_id = data.azurerm_management_group.org.id 29 | 30 | member_definitions = [ 31 | module.configure_asc["auto_enroll_subscriptions"].definition, 32 | module.configure_asc["auto_provision_log_analytics_agent_custom_workspace"].definition, 33 | module.configure_asc["auto_set_contact_details"].definition, 34 | module.configure_asc["export_asc_alerts_and_recommendations_to_eventhub"].definition, 35 | module.configure_asc["export_asc_alerts_and_recommendations_to_log_analytics"].definition, 36 | ] 37 | } 38 | ``` 39 | 40 | ### Create an Initiative with a mix of custom & built-in Policy definitions without merging effects 41 | 42 | ```hcl 43 | data azurerm_policy_definition deploy_law_on_linux_vms { 44 | display_name = "Deploy Log Analytics extension for Linux VMs" 45 | } 46 | 47 | module configure_asc_initiative { 48 | source = "gettek/policy-as-code/azurerm//modules/initiative" 49 | initiative_name = "configure_asc_initiative" 50 | initiative_display_name = "[Security]: Configure Azure Security Center" 51 | initiative_description = "Deploys and configures Azure Security Center settings and defines exports" 52 | initiative_category = "Security Center" 53 | management_group_id = data.azurerm_management_group.org.id 54 | merge_effects = false 55 | 56 | member_definitions = [ 57 | module.configure_asc["auto_enroll_subscriptions"].definition, 58 | module.configure_asc["auto_provision_log_analytics_agent_custom_workspace"].definition, 59 | module.configure_asc["auto_set_contact_details"].definition, 60 | module.configure_asc["export_asc_alerts_and_recommendations_to_eventhub"].definition, 61 | module.configure_asc["export_asc_alerts_and_recommendations_to_log_analytics"].definition, 62 | data.azurerm_policy_definition.deploy_law_on_linux_vms, 63 | ] 64 | } 65 | 66 | # get all the generated parameter names so we know what to use during assignment 67 | output "list_of_initiative_parameters" { 68 | value = keys(module.configure_asc_initiative.parameters) 69 | } 70 | ``` 71 | 72 | ### Populate member_definitions with a for loop 73 | 74 | ```hcl 75 | locals { 76 | guest_config_prereqs = [ 77 | "add_system_identity_when_none_prerequisite", 78 | "add_system_identity_when_user_prerequisite", 79 | "deploy_extension_linux_prerequisite", 80 | "deploy_extension_windows_prerequisite", 81 | ] 82 | } 83 | 84 | module guest_config_prereqs { 85 | source = "..//modules/definition" 86 | for_each = toset(local.guest_config_prereqs) 87 | policy_name = each.value 88 | policy_category = "Guest Configuration" 89 | management_group_id = data.azurerm_management_group.org.id 90 | } 91 | 92 | module guest_config_prereqs_initiative { 93 | source = "..//modules/initiative" 94 | initiative_name = "guest_config_prereqs_initiative" 95 | initiative_display_name = "[GC]: Deploys Guest Config Prerequisites" 96 | initiative_description = "Deploys and configures Windows and Linux VM Guest Config Prerequisites" 97 | initiative_category = "Guest Configuration" 98 | management_group_id = data.azurerm_management_group.org.id 99 | 100 | member_definitions = [ 101 | for gcp in module.guest_config_prereqs : 102 | gcp.definition 103 | ] 104 | } 105 | ``` 106 | 107 | ## Requirements 108 | 109 | | Name | Version | 110 | |------|---------| 111 | | terraform | >= 1.4 | 112 | | azurerm | >= 4.12 | 113 | 114 | 115 | 116 | ## Resources 117 | 118 | | Name | Type | 119 | |------|------| 120 | | [azurerm_policy_set_definition.set](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/policy_set_definition) | resource | 121 | | [terraform_data.set_replace](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | 122 | | [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source | 123 | 124 | ## Inputs 125 | 126 | | Name | Description | Type | Default | Required | 127 | |------|-------------|------|---------|:--------:| 128 | | duplicate_members | Does the Initiative contain duplicate member definitions? Defaults to false | `bool` | `false` | no | 129 | | initiative_category | The category of the initiative | `string` | `"General"` | no | 130 | | initiative_description | Policy initiative description | `string` | `""` | no | 131 | | initiative_display_name | Policy initiative display name | `string` | n/a | yes | 132 | | initiative_metadata | The metadata for the policy initiative. This is a JSON object representing additional metadata that should be stored with the policy initiative. Omitting this will default to merge var.initiative_category and var.initiative_version | `any` | `null` | no | 133 | | initiative_name | Policy initiative name. Changing this forces a new resource to be created | `string` | n/a | yes | 134 | | initiative_version | The version for this initiative, defaults to 1.0.0 | `string` | `"1.0.0"` | no | 135 | | management_group_id | The management group scope at which the initiative will be defined. Defaults to current Subscription if omitted. Changing this forces a new resource to be created. Note: if you are using azurerm_management_group to assign a value to management_group_id, be sure to use name or group_id attribute, but not id. | `string` | `null` | no | 136 | | member_definitions | Policy Definition resource nodes that will be members of this initiative | `any` | n/a | yes | 137 | | merge_effects | Should the module merge all member definition effects? Defaults to true | `bool` | `true` | no | 138 | | merge_parameters | Should the module merge all member definition parameters? Defaults to true | `bool` | `true` | no | 139 | 140 | ## Outputs 141 | 142 | | Name | Description | 143 | |------|-------------| 144 | | id | The Id of the Policy Set Definition | 145 | | initiative | The combined Policy Initiative resource node | 146 | | metadata | The metadata of the Policy Set Definition | 147 | | name | The name of the Policy Set Definition | 148 | | non_compliance_messages | Generated Key/Value map of non-compliance messages | 149 | | parameters | The combined parameters of the Policy Set Definition | 150 | | role_definition_ids | Role definition IDs for remediation | 151 | -------------------------------------------------------------------------------- /modules/initiative/main.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_subscription" "current" {} 2 | 3 | resource "terraform_data" "set_replace" { 4 | input = local.replace_trigger 5 | } 6 | 7 | resource "azurerm_policy_set_definition" "set" { 8 | name = var.initiative_name 9 | display_name = var.initiative_display_name 10 | description = var.initiative_description 11 | management_group_id = var.management_group_id 12 | policy_type = "Custom" 13 | metadata = jsonencode(local.metadata) 14 | parameters = length(local.parameters) > 0 ? jsonencode(local.parameters) : null 15 | 16 | dynamic "policy_definition_reference" { 17 | for_each = local.policy_definition_reference 18 | 19 | content { 20 | policy_definition_id = policy_definition_reference.value.policy_definition_id 21 | reference_id = policy_definition_reference.value.reference_id 22 | parameter_values = policy_definition_reference.value.parameter_values 23 | policy_group_names = [] 24 | } 25 | } 26 | 27 | lifecycle { 28 | replace_triggered_by = [terraform_data.set_replace] 29 | } 30 | 31 | timeouts { 32 | read = "10m" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/initiative/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | description = "The Id of the Policy Set Definition" 3 | value = local.initiative_id 4 | } 5 | 6 | output "name" { 7 | description = "The name of the Policy Set Definition" 8 | value = var.initiative_name 9 | } 10 | 11 | output "parameters" { 12 | description = "The combined parameters of the Policy Set Definition" 13 | value = local.parameters 14 | } 15 | 16 | output "metadata" { 17 | description = "The metadata of the Policy Set Definition" 18 | value = local.metadata 19 | } 20 | 21 | output "role_definition_ids" { 22 | description = "Role definition IDs for remediation" 23 | value = local.all_role_definition_ids 24 | } 25 | 26 | output "non_compliance_messages" { 27 | description = "Generated Key/Value map of non-compliance messages" 28 | value = local.non_compliance_messages 29 | } 30 | 31 | output "initiative" { 32 | description = "The combined Policy Initiative resource node" 33 | value = { 34 | id = local.initiative_id 35 | name = var.initiative_name 36 | display_name = var.initiative_display_name 37 | description = var.initiative_description 38 | management_group_id = var.management_group_id 39 | parameters = local.parameters 40 | metadata = jsonencode(local.metadata) 41 | reference_ids = flatten([for i in local.policy_definition_reference : values({ reference_id = i.reference_id })]) 42 | role_definition_ids = local.all_role_definition_ids 43 | replace_trigger = local.replace_trigger 44 | 45 | policy_definition_reference = [ 46 | for i in local.policy_definition_reference : { 47 | parameter_values = i.parameter_values 48 | policy_definition_id = i.policy_definition_id 49 | reference_id = i.reference_id 50 | } 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /modules/initiative/variables.tf: -------------------------------------------------------------------------------- 1 | variable "management_group_id" { 2 | type = string 3 | description = "The management group scope at which the initiative will be defined. Defaults to current Subscription if omitted. Changing this forces a new resource to be created. Note: if you are using azurerm_management_group to assign a value to management_group_id, be sure to use name or group_id attribute, but not id." 4 | default = null 5 | } 6 | 7 | variable "initiative_name" { 8 | type = string 9 | description = "Policy initiative name. Changing this forces a new resource to be created" 10 | 11 | validation { 12 | condition = length(var.initiative_name) <= 64 13 | error_message = "Initiative names have a maximum 64 character limit." 14 | } 15 | } 16 | 17 | variable "initiative_display_name" { 18 | type = string 19 | description = "Policy initiative display name" 20 | 21 | validation { 22 | condition = length(var.initiative_display_name) <= 128 23 | error_message = "Initiative display names have a maximum 128 character limit." 24 | } 25 | } 26 | 27 | variable "initiative_description" { 28 | type = string 29 | description = "Policy initiative description" 30 | default = "" 31 | 32 | validation { 33 | condition = length(var.initiative_description) <= 512 34 | error_message = "Initiative descriptions have a maximum 512 character limit." 35 | } 36 | } 37 | 38 | variable "initiative_category" { 39 | type = string 40 | description = "The category of the initiative" 41 | default = "General" 42 | } 43 | 44 | variable "initiative_version" { 45 | type = string 46 | description = "The version for this initiative, defaults to 1.0.0" 47 | default = "1.0.0" 48 | } 49 | 50 | variable "member_definitions" { 51 | type = any 52 | description = "Policy Definition resource nodes that will be members of this initiative" 53 | } 54 | 55 | variable "initiative_metadata" { 56 | type = any 57 | description = "The metadata for the policy initiative. This is a JSON object representing additional metadata that should be stored with the policy initiative. Omitting this will default to merge var.initiative_category and var.initiative_version" 58 | default = null 59 | } 60 | 61 | variable "merge_effects" { 62 | type = bool 63 | description = "Should the module merge all member definition effects? Defaults to true" 64 | default = true 65 | } 66 | 67 | variable "merge_parameters" { 68 | type = bool 69 | description = "Should the module merge all member definition parameters? Defaults to true" 70 | default = true 71 | } 72 | 73 | variable "duplicate_members" { 74 | type = bool 75 | description = "Does the Initiative contain duplicate member definitions? Defaults to false" 76 | default = false 77 | } 78 | 79 | locals { 80 | # collate all definition properties into a single reusable object: 81 | # - definition references take their policy name transformed to upper camel case 82 | # - index numbers (idx) will be prefixed to references when using duplicate member definitions 83 | member_properties = { 84 | for idx, d in var.member_definitions : 85 | var.duplicate_members == false ? d.name : "${idx}_${d.name}" => { 86 | id = d.id 87 | mode = try(d.mode, "") 88 | reference = var.duplicate_members == false ? replace(title(replace(d.name, "/-|_|\\s/", " ")), "/\\s/", "") : "${idx}_${replace(title(replace(d.name, "/-|_|\\s/", " ")), "/\\s/", "")}" 89 | parameters = try(jsondecode(d.parameters), {}) 90 | category = try(jsondecode(d.metadata).category, "") 91 | version = try(jsondecode(d.metadata).version, "1.*.*") 92 | non_compliance_message = try(jsondecode(d.metadata).non_compliance_message, d.description, d.display_name, "Flagged by Policy: ${d.name}") 93 | role_definition_ids = try(jsondecode(d.policy_rule).then.details.roleDefinitionIds, []) 94 | } 95 | } 96 | 97 | # shift the dynamic 'policy_definition_reference' block to locals so that params can be created and exported without waiting for resource to deploy 98 | # useful as a dependency for assignment modules 99 | policy_definition_reference = { 100 | for k, v in local.member_properties : 101 | k => { 102 | policy_definition_id = v.id 103 | reference_id = v.reference 104 | parameter_values = length(v.parameters) > 0 ? jsonencode({ 105 | for i in keys(v.parameters) : 106 | i => { 107 | value = i == "effect" && var.merge_effects == false ? "[parameters('${i}_${v.reference}')]" : var.merge_parameters == false ? "[parameters('${i}_${v.reference}')]" : "[parameters('${i}')]" 108 | } 109 | }) : null 110 | } 111 | } 112 | 113 | # combine all discovered definition parameters using interpolation 114 | parameters = merge(values({ 115 | for definition, properties in local.member_properties : 116 | definition => { 117 | for parameter_name, parameter_value in properties.parameters : 118 | # if do not merge parameters (or only effects) then suffix parameters with definition references 119 | var.merge_parameters == false || parameter_name == "effect" && var.merge_effects == false ? 120 | "${parameter_name}_${properties.reference}" : 121 | 122 | parameter_name => { 123 | for k, v in parameter_value : 124 | k => ( 125 | # if do not merge parameters (or only effects) then suffix displayNames with definition references 126 | k == "metadata" && var.merge_parameters == false || var.merge_effects == false && try(v.displayName, "") == "Effect" ? 127 | merge(v, { displayName = "${v.displayName} For Policy: ${properties.reference}" }) : 128 | v 129 | ) 130 | } 131 | } 132 | })...) 133 | 134 | # generate replacement trigger by hashing parameters, included as an output to prevent regen at assignment 135 | replace_trigger = md5(jsonencode(local.parameters)) 136 | 137 | # combine all role definition IDs present in the policyRule 138 | all_role_definition_ids = try(distinct([for v in flatten(values({ 139 | for k, v in local.member_properties : 140 | k => v.role_definition_ids 141 | })) : lower(v)]), []) 142 | 143 | metadata = coalesce(null, var.initiative_metadata, merge({ category = var.initiative_category }, { version = var.initiative_version })) 144 | 145 | # build non-compliance messages from metadata, or default to description/display_name if not present 146 | non_compliance_messages = merge( 147 | { null = "Flagged by Initiative: ${var.initiative_name}" }, # default non-compliance message 148 | { for k, v in local.member_properties : 149 | v.reference => v.non_compliance_message 150 | if contains(["All", "Indexed"], v.mode) && var.duplicate_members == false # messages fail on other modes 151 | } 152 | ) 153 | 154 | # manually generate the initiative Id to prevent "Invalid for_each argument" on consumer modules 155 | initiative_id = ( 156 | var.management_group_id != null ? 157 | "${var.management_group_id}/providers/Microsoft.Authorization/policySetDefinitions/${var.initiative_name}" : 158 | "${data.azurerm_subscription.current.id}/providers/Microsoft.Authorization/policySetDefinitions/${var.initiative_name}" 159 | ) 160 | } 161 | -------------------------------------------------------------------------------- /modules/initiative/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = ">= 4.12" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /modules/set_assignment/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | description = "The Policy Assignment Id" 3 | value = local.assignment.id 4 | } 5 | 6 | output "principal_id" { 7 | description = "The Principal Id of this Policy Assignment's Managed Identity if type is SystemAssigned" 8 | value = try(local.assignment.identity[0].principal_id, null) 9 | } 10 | 11 | output "remediation_tasks" { 12 | description = "The Remediation Task Ids and related Policy Definition Ids" 13 | value = [ 14 | for rem in local.remediation_tasks : 15 | { 16 | id = rem.id 17 | policy_definition_reference_id = rem.policy_definition_reference_id 18 | } 19 | ] 20 | } 21 | 22 | output "definition_references" { 23 | description = "The Member Definition References" 24 | value = try(var.initiative.policy_definition_reference, []) 25 | } 26 | 27 | output "definition_reference_ids" { 28 | description = "The Member Definition Reference Ids" 29 | value = try(var.initiative.policy_definition_reference.*.reference_id, []) 30 | } 31 | -------------------------------------------------------------------------------- /modules/set_assignment/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = ">= 4.12" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /policies/Compute/README.md: -------------------------------------------------------------------------------- 1 | # Compute (IaaS) Policies 2 | 3 | ## Description 4 | 5 | 6 | -------------------------------------------------------------------------------- /policies/General/deny_resource_types.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "deny_resource_types", 4 | "properties": { 5 | "metadata": { 6 | "category": "General" 7 | }, 8 | "parameters": { 9 | "listOfResourceTypesNotAllowed": { 10 | "type": "Array", 11 | "metadata": { 12 | "description": "The list of resource types that cannot be deployed.", 13 | "displayName": "Disallowed resources", 14 | "strongType": "resourceTypes" 15 | }, 16 | "defaultValue": [ 17 | "" 18 | ] 19 | }, 20 | "effect": { 21 | "type": "String", 22 | "defaultValue": "Audit", 23 | "allowedValues": [ 24 | "Audit", 25 | "Deny", 26 | "Disabled" 27 | ], 28 | "metadata": { 29 | "displayName": "Effect", 30 | "description": "The effect determines what happens when the policy rule is evaluated to match" 31 | } 32 | } 33 | }, 34 | "policyRule": { 35 | "if": { 36 | "field": "type", 37 | "in": "[parameters('listOfResourceTypesNotAllowed')]" 38 | }, 39 | "then": { 40 | "effect": "[parameters('effect')]" 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /policies/General/whitelist_regions.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "whitelist_regions", 4 | "properties": { 5 | "metadata": { 6 | "category": "General" 7 | }, 8 | "parameters": { 9 | "listOfRegionsAllowed": { 10 | "type": "Array", 11 | "metadata": { 12 | "description": "The list of regions where resources can be deployed.", 13 | "displayName": "Allowed regions", 14 | "strongType": "location" 15 | }, 16 | "defaultValue": [ 17 | "UK South", 18 | "UK West" 19 | ] 20 | }, 21 | "effect": { 22 | "type": "String", 23 | "defaultValue": "Audit", 24 | "allowedValues": [ 25 | "Audit", 26 | "Deny", 27 | "Disabled" 28 | ], 29 | "metadata": { 30 | "displayName": "Effect", 31 | "description": "The effect determines what happens when the policy rule is evaluated to match" 32 | } 33 | } 34 | }, 35 | "policyRule": { 36 | "if": { 37 | "not": { 38 | "field": "location", 39 | "in": "[parameters('listOfRegionsAllowed')]" 40 | } 41 | }, 42 | "then": { 43 | "effect": "[parameters('effect')]" 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /policies/General/whitelist_resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "whitelist_resources", 4 | "properties": { 5 | "metadata": { 6 | "category": "General" 7 | }, 8 | "parameters": { 9 | "listOfResourceTypesAllowed": { 10 | "type": "Array", 11 | "metadata": { 12 | "description": "The list of resource types that can be deployed.", 13 | "displayName": "Allowed resources", 14 | "strongType": "resourceTypes" 15 | }, 16 | "defaultValue": [ 17 | "" 18 | ] 19 | }, 20 | "effect": { 21 | "type": "String", 22 | "defaultValue": "Audit", 23 | "allowedValues": [ 24 | "Audit", 25 | "Deny", 26 | "Disabled" 27 | ], 28 | "metadata": { 29 | "displayName": "Effect", 30 | "description": "The effect determines what happens when the policy rule is evaluated to match" 31 | } 32 | } 33 | }, 34 | "policyRule": { 35 | "if": { 36 | "not": { 37 | "field": "type", 38 | "in": "[parameters('listOfResourceTypesAllowed')]" 39 | } 40 | }, 41 | "then": { 42 | "effect": "[parameters('effect')]" 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /policies/Monitoring/README.md: -------------------------------------------------------------------------------- 1 | # Monitoring Policies 2 | 3 | Azure Monitor Policies 4 | 5 | ## References 6 | 7 | [List of Azure Policy built-in definitions for Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/samples/policy-reference) 8 | 9 | [Create diagnostic settings at scale using Azure Policy](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/diagnostic-settings-policy) -------------------------------------------------------------------------------- /policies/Monitoring/audit_log_analytics_workspace_retention.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "audit_log_analytics_workspace_retention", 4 | "properties": { 5 | "metadata": { 6 | "category": "Monitoring" 7 | }, 8 | "parameters": { 9 | "effect": { 10 | "type": "String", 11 | "metadata": { 12 | "displayName": "Effect", 13 | "description": "Enable or disable the execution of the policy" 14 | }, 15 | "allowedValues": [ 16 | "AuditIfNotExists", 17 | "Disabled" 18 | ], 19 | "defaultValue": "AuditIfNotExists" 20 | }, 21 | "workspaceRetentionDays": { 22 | "type": "Integer", 23 | "metadata": { 24 | "description": "Log Analytics Workspace should be retained for the specified amount of days. Defaults to 15 months", 25 | "displayName": "Log Analytics Workspace Retention Days" 26 | }, 27 | "defaultValue": 456 28 | } 29 | }, 30 | "policyRule": { 31 | "if": { 32 | "allOf": [ 33 | { 34 | "field": "type", 35 | "equals": "Microsoft.OperationalInsights/workspaces" 36 | } 37 | ] 38 | }, 39 | "then": { 40 | "effect": "[parameters('effect')]", 41 | "details": { 42 | "type": "Microsoft.OperationalInsights/workspaces", 43 | "existenceCondition": { 44 | "field": "Microsoft.OperationalInsights/workspaces/retentionInDays", 45 | "greaterOrEquals": "[parameters('workspaceRetentionDays')]" 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /policies/Monitoring/audit_subscription_diagnostic_setting_should_exist.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "audit_subscription_diagnostic_setting_should_exist", 4 | "properties": { 5 | "metadata": { 6 | "category": "Monitoring" 7 | }, 8 | "parameters": { 9 | "effect": { 10 | "type": "String", 11 | "metadata": { 12 | "displayName": "Effect", 13 | "description": "Enable or disable the execution of the policy" 14 | }, 15 | "allowedValues": [ 16 | "AuditIfNotExists", 17 | "Disabled" 18 | ], 19 | "defaultValue": "AuditIfNotExists" 20 | } 21 | }, 22 | "policyRule": { 23 | "if": { 24 | "field": "type", 25 | "equals": "Microsoft.Resources/subscriptions" 26 | }, 27 | "then": { 28 | "effect": "[parameters('effect')]", 29 | "details": { 30 | "type": "Microsoft.Insights/diagnosticSettings", 31 | "existenceCondition": { 32 | "allOf": [ 33 | { 34 | "field": "Microsoft.Insights/diagnosticSettings/workspaceId", 35 | "exists": "true" 36 | }, 37 | { 38 | "field": "Microsoft.Insights/diagnosticSettings/eventHubName", 39 | "exists": "true" 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /policies/Monitoring/deploy_network_interface_diagnostic_setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "deploy_network_interface_diagnostic_setting", 4 | "properties": { 5 | "displayName": "Deploy Diagnostic Settings for Network Interfaces to a Log Analytics workspace", 6 | "description": "Deploys the diagnostic settings for Network Interfaces to stream to a regional Log Analytics workspace when any Network Interface which is missing this diagnostic settings is created or updated.", 7 | "metadata": { 8 | "category": "Monitoring" 9 | }, 10 | "parameters": { 11 | "effect": { 12 | "type": "String", 13 | "defaultValue": "DeployIfNotExists", 14 | "allowedValues": [ 15 | "AuditIfNotExists", 16 | "DeployIfNotExists", 17 | "Disabled" 18 | ], 19 | "metadata": { 20 | "displayName": "Effect", 21 | "description": "Enable or disable the execution of the policy" 22 | } 23 | }, 24 | "profileName": { 25 | "type": "String", 26 | "defaultValue": "setbypolicy_Diagnostics", 27 | "metadata": { 28 | "displayName": "Profile name", 29 | "description": "The diagnostic settings profile name" 30 | } 31 | }, 32 | "workspaceId": { 33 | "type": "String", 34 | "metadata": { 35 | "displayName": "Log Analytics workspace Id", 36 | "description": "Select Log Analytics workspace from dropdown list. If this workspace is outside of the scope of the assignment you must manually grant 'Log Analytics Contributor' permissions (or similar) to the policy assignment's principal ID.", 37 | "assignPermissions": true 38 | } 39 | }, 40 | "storageAccountId": { 41 | "type": "String", 42 | "metadata": { 43 | "displayName": "Storage Account Id", 44 | "description": "The Storage Account Resource Id to send activity logs", 45 | "assignPermissions": true 46 | } 47 | }, 48 | "eventHubAuthorizationRuleId": { 49 | "type": "String", 50 | "metadata": { 51 | "displayName": "Event Hub Authorization Rule Id", 52 | "description": "The Event Hub authorization rule Id for Azure Diagnostics. The authorization rule needs to be at Event Hub namespace level. e.g. /subscriptions/{subscription Id}/resourceGroups/{resource group}/providers/Microsoft.EventHub/namespaces/{Event Hub namespace}/authorizationrules/{authorization rule}", 53 | "assignPermissions": true 54 | } 55 | }, 56 | "eventHubName": { 57 | "type": "String", 58 | "metadata": { 59 | "displayName": "EventHub name", 60 | "description": "The EventHub name to stream activity logs to", 61 | "assignPermissions": true 62 | } 63 | }, 64 | "metricsEnabled": { 65 | "type": "String", 66 | "metadata": { 67 | "displayName": "Enable Metrics", 68 | "description": "Enable Metrics - True or False" 69 | }, 70 | "allowedValues": [ 71 | "True", 72 | "False" 73 | ], 74 | "defaultValue": "True" 75 | } 76 | }, 77 | "policyRule": { 78 | "if": { 79 | "allOf": [ 80 | { 81 | "field": "type", 82 | "equals": "Microsoft.Network/networkInterfaces" 83 | } 84 | ] 85 | }, 86 | "then": { 87 | "effect": "[parameters('effect')]", 88 | "details": { 89 | "type": "Microsoft.Insights/diagnosticSettings", 90 | "name": "[parameters('profileName')]", 91 | "existenceCondition": { 92 | "allOf": [ 93 | { 94 | "field": "Microsoft.Insights/diagnosticSettings/metrics.enabled", 95 | "equals": "[parameters('metricsEnabled')]" 96 | }, 97 | { 98 | "field": "Microsoft.Insights/diagnosticSettings/eventHubName", 99 | "matchInsensitively": "[parameters('eventHubName')]" 100 | } 101 | ] 102 | }, 103 | "roleDefinitionIds": [ 104 | "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" 105 | ], 106 | "deployment": { 107 | "properties": { 108 | "mode": "incremental", 109 | "template": { 110 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 111 | "contentVersion": "1.0.0.0", 112 | "parameters": { 113 | "resourceName": { 114 | "type": "String" 115 | }, 116 | "workspaceId": { 117 | "type": "String" 118 | }, 119 | "storageAccountId": { 120 | "type": "String" 121 | }, 122 | "eventHubAuthorizationRuleId": { 123 | "type": "String" 124 | }, 125 | "eventHubName": { 126 | "type": "String" 127 | }, 128 | "metricsEnabled": { 129 | "type": "String" 130 | }, 131 | "profileName": { 132 | "type": "String" 133 | } 134 | }, 135 | "variables": {}, 136 | "resources": [ 137 | { 138 | "type": "Microsoft.Network/networkInterfaces/providers/diagnosticSettings", 139 | "apiVersion": "2017-05-01-preview", 140 | "name": "[concat(parameters('resourceName'), '/', 'Microsoft.Insights/', parameters('profileName'))]", 141 | "location": "uksouth", 142 | "dependsOn": [], 143 | "properties": { 144 | "workspaceId": "[parameters('workspaceId')]", 145 | "storageAccountId": "[parameters('storageAccountId')]", 146 | "eventHubAuthorizationRuleId": "[parameters('eventHubAuthorizationRuleId')]", 147 | "eventHubName": "[parameters('eventHubName')]", 148 | "metrics": [ 149 | { 150 | "category": "AllMetrics", 151 | "enabled": "[parameters('metricsEnabled')]", 152 | "timeGrain": null 153 | } 154 | ] 155 | } 156 | } 157 | ], 158 | "outputs": { 159 | "policy": { 160 | "type": "String", 161 | "value": "[concat(parameters('eventHubName'), 'configured for diagnostic logs for ', ': ', parameters('resourceName'))]" 162 | } 163 | } 164 | }, 165 | "parameters": { 166 | "resourceName": { 167 | "value": "[field('name')]" 168 | }, 169 | "workspaceId": { 170 | "value": "[parameters('workspaceId')]" 171 | }, 172 | "storageAccountId": { 173 | "Value": "[parameters('storageAccountId')]" 174 | }, 175 | "eventHubAuthorizationRuleId": { 176 | "Value": "[parameters('eventHubAuthorizationRuleId')]" 177 | }, 178 | "eventHubName": { 179 | "Value": "[parameters('eventHubName')]" 180 | }, 181 | "metricsEnabled": { 182 | "value": "[parameters('metricsEnabled')]" 183 | }, 184 | "profileName": { 185 | "value": "[parameters('profileName')]" 186 | } 187 | } 188 | } 189 | } 190 | } 191 | } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /policies/Monitoring/deploy_network_security_group_diagnostic_setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "deploy_network_security_group_diagnostic_setting", 4 | "properties": { 5 | "displayName": "Deploy Diagnostic Settings for Network Security Groups to a Log Analytics workspace", 6 | "description": "Deploys the diagnostic settings for Network Security Groups to stream to a regional Log Analytics workspace when any Network Security Group which is missing this diagnostic settings is created or updated.", 7 | "metadata": { 8 | "category": "Monitoring" 9 | }, 10 | "parameters": { 11 | "effect": { 12 | "type": "String", 13 | "defaultValue": "DeployIfNotExists", 14 | "allowedValues": [ 15 | "AuditIfNotExists", 16 | "DeployIfNotExists", 17 | "Disabled" 18 | ], 19 | "metadata": { 20 | "displayName": "Effect", 21 | "description": "Enable or disable the execution of the policy" 22 | } 23 | }, 24 | "profileName": { 25 | "type": "String", 26 | "defaultValue": "setbypolicy_Diagnostics", 27 | "metadata": { 28 | "displayName": "Profile name", 29 | "description": "The diagnostic settings profile name" 30 | } 31 | }, 32 | "workspaceId": { 33 | "type": "String", 34 | "metadata": { 35 | "displayName": "Log Analytics workspace Id", 36 | "description": "Select Log Analytics workspace from dropdown list. If this workspace is outside of the scope of the assignment you must manually grant 'Log Analytics Contributor' permissions (or similar) to the policy assignment's principal ID.", 37 | "assignPermissions": true 38 | } 39 | }, 40 | "storageAccountId": { 41 | "type": "String", 42 | "metadata": { 43 | "displayName": "Storage Account Id", 44 | "description": "The Storage Account Resource Id to send activity logs", 45 | "assignPermissions": true 46 | } 47 | }, 48 | "eventHubAuthorizationRuleId": { 49 | "type": "String", 50 | "metadata": { 51 | "displayName": "Event Hub Authorization Rule Id", 52 | "description": "The Event Hub authorization rule Id for Azure Diagnostics. The authorization rule needs to be at Event Hub namespace level. e.g. /subscriptions/{subscription Id}/resourceGroups/{resource group}/providers/Microsoft.EventHub/namespaces/{Event Hub namespace}/authorizationrules/{authorization rule}", 53 | "assignPermissions": true 54 | } 55 | }, 56 | "eventHubName": { 57 | "type": "String", 58 | "metadata": { 59 | "displayName": "EventHub name", 60 | "description": "The EventHub name to stream activity logs to", 61 | "assignPermissions": true 62 | } 63 | }, 64 | "logsEnabled": { 65 | "type": "String", 66 | "metadata": { 67 | "displayName": "Enable Logs", 68 | "description": "Enable Logs - True or False" 69 | }, 70 | "allowedValues": [ 71 | "True", 72 | "False" 73 | ], 74 | "defaultValue": "True" 75 | } 76 | }, 77 | "policyRule": { 78 | "if": { 79 | "allOf": [ 80 | { 81 | "field": "type", 82 | "equals": "Microsoft.Network/networkSecurityGroups" 83 | } 84 | ] 85 | }, 86 | "then": { 87 | "effect": "[parameters('effect')]", 88 | "details": { 89 | "type": "Microsoft.Insights/diagnosticSettings", 90 | "name": "[parameters('profileName')]", 91 | "existenceCondition": { 92 | "allOf": [ 93 | { 94 | "field": "Microsoft.Insights/diagnosticSettings/logs.enabled", 95 | "equals": "[parameters('logsEnabled')]" 96 | }, 97 | { 98 | "field": "Microsoft.Insights/diagnosticSettings/eventHubName", 99 | "matchInsensitively": "[parameters('eventHubName')]" 100 | } 101 | ] 102 | }, 103 | "roleDefinitionIds": [ 104 | "/providers/microsoft.authorization/roleDefinitions/749f88d5-cbae-40b8-bcfc-e573ddc772fa", 105 | "/providers/microsoft.authorization/roleDefinitions/17d1049b-9a84-46fb-8f53-869881c3d3ab" 106 | ], 107 | "deployment": { 108 | "properties": { 109 | "mode": "incremental", 110 | "template": { 111 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 112 | "contentVersion": "1.0.0.0", 113 | "parameters": { 114 | "resourceName": { 115 | "type": "String" 116 | }, 117 | "workspaceId": { 118 | "type": "String" 119 | }, 120 | "storageAccountId": { 121 | "type": "String" 122 | }, 123 | "eventHubAuthorizationRuleId": { 124 | "type": "String" 125 | }, 126 | "eventHubName": { 127 | "type": "String" 128 | }, 129 | "logsEnabled": { 130 | "type": "String" 131 | }, 132 | "profileName": { 133 | "type": "String" 134 | } 135 | }, 136 | "variables": {}, 137 | "resources": [ 138 | { 139 | "type": "Microsoft.Network/networkSecurityGroups/providers/diagnosticSettings", 140 | "name": "[concat(parameters('resourceName'), '/', 'Microsoft.Insights/', parameters('profileName'))]", 141 | "apiVersion": "2017-05-01-preview", 142 | "location": "uksouth", 143 | "dependsOn": [], 144 | "properties": { 145 | "workspaceId": "[parameters('workspaceId')]", 146 | "storageAccountId": "[parameters('storageAccountId')]", 147 | "eventHubAuthorizationRuleId": "[parameters('eventHubAuthorizationRuleId')]", 148 | "eventHubName": "[parameters('eventHubName')]", 149 | "logs": [ 150 | { 151 | "category": "NetworkSecurityGroupEvent", 152 | "enabled": "[parameters('logsEnabled')]" 153 | }, 154 | { 155 | "category": "NetworkSecurityGroupRuleCounter", 156 | "enabled": "[parameters('logsEnabled')]" 157 | } 158 | ] 159 | } 160 | } 161 | ] 162 | }, 163 | "parameters": { 164 | "resourceName": { 165 | "value": "[field('name')]" 166 | }, 167 | "workspaceId": { 168 | "value": "[parameters('workspaceId')]" 169 | }, 170 | "storageAccountId": { 171 | "Value": "[parameters('storageAccountId')]" 172 | }, 173 | "eventHubAuthorizationRuleId": { 174 | "Value": "[parameters('eventHubAuthorizationRuleId')]" 175 | }, 176 | "eventHubName": { 177 | "Value": "[parameters('eventHubName')]" 178 | }, 179 | "profileName": { 180 | "value": "[parameters('profileName')]" 181 | }, 182 | "logsEnabled": { 183 | "value": "[parameters('logsEnabled')]" 184 | } 185 | } 186 | } 187 | } 188 | } 189 | } 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /policies/Monitoring/deploy_subscription_diagnostic_setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "deploy_subscription_diagnostic_setting", 4 | "properties": { 5 | "displayName": "Deploy Diagnostic Settings for Subscriptions to a Log Analytics workspace", 6 | "description": "Deploys the diagnostic settings for Subscriptions to stream to a regional Log Analytics workspace when any Subscription which is missing this diagnostic settings is created or updated.", 7 | "metadata": { 8 | "category": "Monitoring" 9 | }, 10 | "parameters": { 11 | "effect": { 12 | "type": "String", 13 | "defaultValue": "DeployIfNotExists", 14 | "metadata": { 15 | "displayName": "Effect", 16 | "description": "Enable or disable the execution of the policy" 17 | }, 18 | "allowedValues": [ 19 | "DeployIfNotExists", 20 | "AuditIfNotExists", 21 | "Disabled" 22 | ] 23 | }, 24 | "profileName": { 25 | "type": "String", 26 | "defaultValue": "setbypolicy_Diagnostics", 27 | "metadata": { 28 | "displayName": "Profile name", 29 | "description": "The diagnostic settings profile name" 30 | } 31 | }, 32 | "workspaceId": { 33 | "type": "String", 34 | "metadata": { 35 | "displayName": "Log Analytics workspace Id", 36 | "description": "Select Log Analytics workspace from dropdown list. If this workspace is outside of the scope of the assignment you must manually grant 'Log Analytics Contributor' permissions (or similar) to the policy assignment's principal ID.", 37 | "assignPermissions": true 38 | } 39 | }, 40 | "storageAccountId": { 41 | "type": "String", 42 | "metadata": { 43 | "displayName": "Storage Account Id", 44 | "description": "The Storage Account Resource Id to send activity logs", 45 | "assignPermissions": true 46 | } 47 | }, 48 | "eventHubAuthorizationRuleId": { 49 | "type": "String", 50 | "metadata": { 51 | "displayName": "Event Hub Authorization Rule Id", 52 | "description": "The Event Hub authorization rule Id for Azure Diagnostics. The authorization rule needs to be at Event Hub namespace level. e.g. /subscriptions/{subscription Id}/resourceGroups/{resource group}/providers/Microsoft.EventHub/namespaces/{Event Hub namespace}/authorizationrules/{authorization rule}", 53 | "assignPermissions": true 54 | } 55 | }, 56 | "eventHubName": { 57 | "type": "String", 58 | "metadata": { 59 | "displayName": "EventHub name", 60 | "description": "The EventHub name to stream activity logs to", 61 | "assignPermissions": true 62 | } 63 | }, 64 | "logsEnabled": { 65 | "type": "String", 66 | "metadata": { 67 | "displayName": "Enable Logs", 68 | "description": "Enable Logs - True or False" 69 | }, 70 | "allowedValues": [ 71 | "True", 72 | "False" 73 | ], 74 | "defaultValue": "True" 75 | } 76 | }, 77 | "policyRule": { 78 | "if": { 79 | "field": "type", 80 | "equals": "Microsoft.Resources/subscriptions" 81 | }, 82 | "then": { 83 | "effect": "[parameters('effect')]", 84 | "details": { 85 | "type": "microsoft.insights/diagnosticSettings", 86 | "existenceCondition": { 87 | "allOf": [ 88 | { 89 | "field": "Microsoft.Insights/diagnosticSettings/logs.enabled", 90 | "equals": "[parameters('logsEnabled')]" 91 | }, 92 | { 93 | "field": "Microsoft.Insights/diagnosticSettings/eventHubName", 94 | "matchInsensitively": "[parameters('eventHubName')]" 95 | } 96 | ] 97 | }, 98 | "roleDefinitionIds": [ 99 | "/providers/microsoft.authorization/roleDefinitions/749f88d5-cbae-40b8-bcfc-e573ddc772fa", 100 | "/providers/microsoft.authorization/roleDefinitions/92aaf0da-9dab-42b6-94a3-d43ce8d16293", 101 | "/providers/microsoft.authorization/roleDefinitions/c12c1c16-33a1-487b-954d-41c89c60f349" 102 | ], 103 | "deployment": { 104 | "properties": { 105 | "mode": "incremental", 106 | "template": { 107 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 108 | "contentVersion": "1.0.0.0", 109 | "parameters": { 110 | "profileName": { 111 | "type": "String" 112 | }, 113 | "workspaceId": { 114 | "type": "String" 115 | }, 116 | "storageAccountId": { 117 | "type": "String" 118 | }, 119 | "eventHubAuthorizationRuleId": { 120 | "type": "String" 121 | }, 122 | "eventHubName": { 123 | "type": "String" 124 | }, 125 | "logsEnabled": { 126 | "type": "String" 127 | } 128 | }, 129 | "variables": {}, 130 | "resources": [ 131 | { 132 | "type": "microsoft.insights/diagnosticSettings", 133 | "apiVersion": "2017-05-01-preview", 134 | "name": "[parameters('profileName')]", 135 | "properties": { 136 | "workspaceId": "[parameters('workspaceId')]", 137 | "storageAccountId": "[parameters('storageAccountId')]", 138 | "eventHubAuthorizationRuleId": "[parameters('eventHubAuthorizationRuleId')]", 139 | "eventHubName": "[parameters('eventHubName')]", 140 | "logs": [ 141 | { 142 | "category": "Administrative", 143 | "enabled": "[parameters('logsEnabled')]" 144 | }, 145 | { 146 | "category": "Security", 147 | "enabled": "[parameters('logsEnabled')]" 148 | }, 149 | { 150 | "category": "ServiceHealth", 151 | "enabled": "[parameters('logsEnabled')]" 152 | }, 153 | { 154 | "category": "Alert", 155 | "enabled": "[parameters('logsEnabled')]" 156 | }, 157 | { 158 | "category": "Recommendation", 159 | "enabled": "[parameters('logsEnabled')]" 160 | }, 161 | { 162 | "category": "Policy", 163 | "enabled": "[parameters('logsEnabled')]" 164 | }, 165 | { 166 | "category": "Autoscale", 167 | "enabled": "[parameters('logsEnabled')]" 168 | }, 169 | { 170 | "category": "ResourceHealth", 171 | "enabled": "[parameters('logsEnabled')]" 172 | } 173 | ] 174 | } 175 | } 176 | ], 177 | "outputs": {} 178 | }, 179 | "parameters": { 180 | "profileName": { 181 | "value": "[parameters('profileName')]" 182 | }, 183 | "workspaceId": { 184 | "value": "[parameters('workspaceId')]" 185 | }, 186 | "storageAccountId": { 187 | "Value": "[parameters('storageAccountId')]" 188 | }, 189 | "eventHubAuthorizationRuleId": { 190 | "Value": "[parameters('eventHubAuthorizationRuleId')]" 191 | }, 192 | "eventHubName": { 193 | "Value": "[parameters('eventHubName')]" 194 | }, 195 | "logsEnabled": { 196 | "value": "[parameters('logsEnabled')]" 197 | } 198 | } 199 | }, 200 | "location": "uksouth" 201 | }, 202 | "deploymentScope": "subscription" 203 | } 204 | } 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /policies/Monitoring/deploy_vnet_diagnostic_setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "deploy_vnet_diagnostic_setting", 4 | "properties": { 5 | "displayName": "Deploy Diagnostic Settings for Virtual Networks to a Log Analytics workspace", 6 | "description": "Deploys the diagnostic settings for Virtual Networks to stream to a regional Log Analytics workspace when any Virtual Network which is missing this diagnostic settings is created or updated.", 7 | "metadata": { 8 | "category": "Monitoring" 9 | }, 10 | "parameters": { 11 | "effect": { 12 | "type": "String", 13 | "defaultValue": "DeployIfNotExists", 14 | "allowedValues": [ 15 | "AuditIfNotExists", 16 | "DeployIfNotExists", 17 | "Disabled" 18 | ], 19 | "metadata": { 20 | "displayName": "Effect", 21 | "description": "Enable or disable the execution of the policy" 22 | } 23 | }, 24 | "profileName": { 25 | "type": "String", 26 | "defaultValue": "setbypolicy_Diagnostics", 27 | "metadata": { 28 | "displayName": "Profile name", 29 | "description": "The diagnostic settings profile name" 30 | } 31 | }, 32 | "workspaceId": { 33 | "type": "String", 34 | "metadata": { 35 | "displayName": "Log Analytics workspace Id", 36 | "description": "Select Log Analytics workspace from dropdown list. If this workspace is outside of the scope of the assignment you must manually grant 'Log Analytics Contributor' permissions (or similar) to the policy assignment's principal ID.", 37 | "assignPermissions": true 38 | } 39 | }, 40 | "storageAccountId": { 41 | "type": "String", 42 | "metadata": { 43 | "displayName": "Storage Account Id", 44 | "description": "The Storage Account Resource Id to send activity logs", 45 | "assignPermissions": true 46 | } 47 | }, 48 | "eventHubAuthorizationRuleId": { 49 | "type": "String", 50 | "metadata": { 51 | "displayName": "Event Hub Authorization Rule Id", 52 | "description": "The Event Hub authorization rule Id for Azure Diagnostics. The authorization rule needs to be at Event Hub namespace level. e.g. /subscriptions/{subscription Id}/resourceGroups/{resource group}/providers/Microsoft.EventHub/namespaces/{Event Hub namespace}/authorizationrules/{authorization rule}", 53 | "assignPermissions": true 54 | } 55 | }, 56 | "eventHubName": { 57 | "type": "String", 58 | "metadata": { 59 | "displayName": "EventHub name", 60 | "description": "The EventHub name to stream activity logs to", 61 | "assignPermissions": true 62 | } 63 | }, 64 | "metricsEnabled": { 65 | "type": "String", 66 | "defaultValue": "False", 67 | "allowedValues": [ 68 | "True", 69 | "False" 70 | ], 71 | "metadata": { 72 | "displayName": "Enable metrics", 73 | "description": "Whether to enable metrics stream to the Log Analytics workspace - True or False" 74 | } 75 | }, 76 | "logsEnabled": { 77 | "type": "String", 78 | "defaultValue": "True", 79 | "allowedValues": [ 80 | "True", 81 | "False" 82 | ], 83 | "metadata": { 84 | "displayName": "Enable logs", 85 | "description": "Whether to enable logs stream to the Log Analytics workspace - True or False" 86 | } 87 | } 88 | }, 89 | "policyRule": { 90 | "if": { 91 | "field": "type", 92 | "equals": "Microsoft.Network/virtualNetworks" 93 | }, 94 | "then": { 95 | "effect": "[parameters('effect')]", 96 | "details": { 97 | "type": "Microsoft.Insights/diagnosticSettings", 98 | "name": "[parameters('profileName')]", 99 | "roleDefinitionIds": [ 100 | "/providers/Microsoft.Authorization/roleDefinitions/92aaf0da-9dab-42b6-94a3-d43ce8d16293" 101 | ], 102 | "existenceCondition": { 103 | "allOf": [ 104 | { 105 | "field": "Microsoft.Insights/diagnosticSettings/logs.enabled", 106 | "equals": "[parameters('logsEnabled')]" 107 | }, 108 | { 109 | "field": "Microsoft.Insights/diagnosticSettings/metrics.enabled", 110 | "equals": "[parameters('metricsEnabled')]" 111 | }, 112 | { 113 | "field": "Microsoft.Insights/diagnosticSettings/eventHubName", 114 | "matchInsensitively": "[parameters('eventHubName')]" 115 | } 116 | ] 117 | }, 118 | "deployment": { 119 | "properties": { 120 | "mode": "incremental", 121 | "template": { 122 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 123 | "contentVersion": "1.0.0.0", 124 | "parameters": { 125 | "resourceName": { 126 | "type": "String" 127 | }, 128 | "workspaceId": { 129 | "type": "String" 130 | }, 131 | "storageAccountId": { 132 | "type": "String" 133 | }, 134 | "eventHubAuthorizationRuleId": { 135 | "type": "String" 136 | }, 137 | "eventHubName": { 138 | "type": "String" 139 | }, 140 | "metricsEnabled": { 141 | "type": "String" 142 | }, 143 | "logsEnabled": { 144 | "type": "String" 145 | }, 146 | "profileName": { 147 | "type": "String" 148 | } 149 | }, 150 | "variables": {}, 151 | "resources": [ 152 | { 153 | "type": "Microsoft.Network/virtualNetworks/providers/diagnosticSettings", 154 | "apiVersion": "2017-05-01-preview", 155 | "name": "[concat(parameters('resourceName'), '/', 'Microsoft.Insights/', parameters('profileName'))]", 156 | "location": "uksouth", 157 | "dependsOn": [], 158 | "properties": { 159 | "workspaceId": "[parameters('workspaceId')]", 160 | "storageAccountId": "[parameters('storageAccountId')]", 161 | "eventHubAuthorizationRuleId": "[parameters('eventHubAuthorizationRuleId')]", 162 | "eventHubName": "[parameters('eventHubName')]", 163 | "metrics": [ 164 | { 165 | "category": "AllMetrics", 166 | "enabled": "[parameters('metricsEnabled')]", 167 | "timeGrain": null 168 | } 169 | ], 170 | "logs": [ 171 | { 172 | "category": "VMProtectionAlerts", 173 | "enabled": "[parameters('logsEnabled')]" 174 | } 175 | ] 176 | } 177 | } 178 | ], 179 | "outputs": {} 180 | }, 181 | "parameters": { 182 | "resourceName": { 183 | "value": "[field('name')]" 184 | }, 185 | "workspaceId": { 186 | "value": "[parameters('workspaceId')]" 187 | }, 188 | "storageAccountId": { 189 | "Value": "[parameters('storageAccountId')]" 190 | }, 191 | "eventHubAuthorizationRuleId": { 192 | "Value": "[parameters('eventHubAuthorizationRuleId')]" 193 | }, 194 | "eventHubName": { 195 | "Value": "[parameters('eventHubName')]" 196 | }, 197 | "metricsEnabled": { 198 | "value": "[parameters('metricsEnabled')]" 199 | }, 200 | "logsEnabled": { 201 | "value": "[parameters('logsEnabled')]" 202 | }, 203 | "profileName": { 204 | "value": "[parameters('profileName')]" 205 | } 206 | } 207 | } 208 | } 209 | } 210 | } 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /policies/Network/deny_nic_public_ip.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "deny_nic_public_ip", 4 | "properties": { 5 | "metadata": { 6 | "category": "Network" 7 | }, 8 | "parameters": { 9 | "effect": { 10 | "type": "String", 11 | "defaultValue": "Deny", 12 | "allowedValues": [ 13 | "Audit", 14 | "Deny", 15 | "Disabled" 16 | ], 17 | "metadata": { 18 | "displayName": "Effect", 19 | "description": "The effect determines what happens when the policy rule is evaluated to match" 20 | } 21 | } 22 | }, 23 | "policyRule": { 24 | "if": { 25 | "allOf": [ 26 | { 27 | "field": "type", 28 | "equals": "Microsoft.Network/networkInterfaces" 29 | }, 30 | { 31 | "not": { 32 | "field": "Microsoft.Network/networkInterfaces/ipconfigurations[*].publicIpAddress.id", 33 | "notLike": "*" 34 | } 35 | } 36 | ] 37 | }, 38 | "then": { 39 | "effect": "[parameters('effect')]" 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /policies/Security Center/README.md: -------------------------------------------------------------------------------- 1 | # Security Center Policies 2 | 3 | ## Description 4 | 5 | Automatically enrolls and configures Azure Security Center on every subscription beneath the `assignment_scope`. The assignment managed identity requires the built-in **Contributor** role 6 | 7 | ## Example Deployment 8 | 9 | ```hcl 10 | ################## 11 | # Security Center 12 | ################## 13 | 14 | # Definitions (Name and Display Name) 15 | locals { 16 | security_center_policies = { 17 | auto_enroll_subscriptions = "Enable Azure Security Center on Subcriptions" 18 | auto_provision_log_analytics_agent_custom_workspace = "Enable Security Center's auto provisioning of the Log Analytics agent on your subscriptions with custom workspace" 19 | auto_set_contact_details = "Automatically set the security contact email address and phone number should they be blank on the subscription" 20 | export_asc_alerts_and_recommendations_to_eventhub = "Export to Event Hub for Azure Security Center alerts and recommendations" 21 | export_asc_alerts_and_recommendations_to_log_analytics = "Export to Log Analytics Workspace for Azure Security Center alerts and recommendations" 22 | } 23 | } 24 | 25 | module configure_asc { 26 | source = "..//modules/definition" 27 | for_each = local.security_center_policies 28 | policy_name = each.key 29 | display_name = each.value 30 | policy_description = each.value 31 | policy_category = "Security Center" 32 | management_group_name = data.azurerm_management_group.org.name 33 | } 34 | 35 | # Initiative 36 | module configure_asc_initiative { 37 | source = "..//modules/initiative" 38 | initiative_name = "configure_asc_initiative" 39 | initiative_display_name = "[Security]: Configure Azure Security Center" 40 | initiative_description = "Deploys and configures Azure Security Center settings and defines exports" 41 | initiative_category = "Security Center" 42 | management_group_name = data.azurerm_management_group.org.name 43 | 44 | member_definitions = [ 45 | module.configure_asc["auto_enroll_subscriptions"].definition, 46 | module.configure_asc["auto_provision_log_analytics_agent_custom_workspace"].definition, 47 | module.configure_asc["auto_set_contact_details"].definition, 48 | module.configure_asc["export_asc_alerts_and_recommendations_to_eventhub"].definition, 49 | module.configure_asc["export_asc_alerts_and_recommendations_to_log_analytics"].definition, 50 | ] 51 | } 52 | 53 | # Assignment 54 | module org_mg_configure_asc_initiative { 55 | source = "..//modules/set_assignment" 56 | initiative = module.configure_asc_initiative.initiative 57 | assignment_scope = data.azurerm_management_group.org.id 58 | assignment_effect = "DeployIfNotExists" 59 | skip_remediation = var.skip_remediation 60 | assignment_parameters = { 61 | workspaceId = local.logging_law_id 62 | eventHubDetails = local.logging_eventhub_namespace_authorization_rule_id 63 | securityContactsEmail = "admin@cloud.com" 64 | securityContactsPhone = "44897654987" 65 | } 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /policies/Security Center/auto_provision_log_analytics_agent_custom_workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "auto_provision_log_analytics_agent_custom_workspace", 4 | "properties": { 5 | "displayName": "Auto Provision Subscriptions to Log Analytics Custom to Workspace", 6 | "description": "Enable Security Center's auto provisioning of the Log Analytics agent on your subscriptions with custom workspace", 7 | "metadata": { 8 | "category": "Security Center" 9 | }, 10 | "parameters": { 11 | "effect": { 12 | "type": "String", 13 | "metadata": { 14 | "displayName": "Effect", 15 | "description": "Enable or disable the execution of the policy" 16 | }, 17 | "allowedValues": [ 18 | "DeployIfNotExists", 19 | "AuditIfNotExists", 20 | "Disabled" 21 | ], 22 | "defaultValue": "DeployIfNotExists" 23 | }, 24 | "workspaceId": { 25 | "type": "String", 26 | "metadata": { 27 | "displayName": "Log Analytics workspace", 28 | "description": "Auto provision the Log Analytics agent on your subscriptions to monitor and collect security data using a custom workspace.", 29 | "assignPermissions": true 30 | }, 31 | "defaultValue": "" 32 | } 33 | }, 34 | "policyRule": { 35 | "if": { 36 | "field": "type", 37 | "equals": "Microsoft.Resources/subscriptions" 38 | }, 39 | "then": { 40 | "effect": "[parameters('effect')]", 41 | "details": { 42 | "type": "Microsoft.Security/autoProvisioningSettings", 43 | "deploymentScope": "subscription", 44 | "existenceScope": "subscription", 45 | "roleDefinitionIds": [ 46 | "/providers/microsoft.authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" 47 | ], 48 | "existenceCondition": { 49 | "field": "Microsoft.Security/autoProvisioningSettings/autoProvision", 50 | "equals": "On" 51 | }, 52 | "deployment": { 53 | "location": "uksouth", 54 | "properties": { 55 | "mode": "incremental", 56 | "parameters": { 57 | "workspaceId": { 58 | "value": "[parameters('workspaceId')]" 59 | } 60 | }, 61 | "template": { 62 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 63 | "contentVersion": "1.0.0.0", 64 | "parameters": { 65 | "workspaceId": { 66 | "type": "String" 67 | } 68 | }, 69 | "variables": {}, 70 | "resources": [ 71 | { 72 | "type": "Microsoft.Security/autoProvisioningSettings", 73 | "name": "default", 74 | "apiVersion": "2017-08-01-preview", 75 | "properties": { 76 | "autoProvision": "On" 77 | } 78 | }, 79 | { 80 | "type": "Microsoft.Security/workspaceSettings", 81 | "apiVersion": "2017-08-01-preview", 82 | "name": "default", 83 | "properties": { 84 | "workspaceId": "[parameters('workspaceId')]", 85 | "scope": "[subscription().id]" 86 | } 87 | } 88 | ] 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /policies/Security Center/auto_set_contact_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "auto_set_contact_details", 4 | "properties": { 5 | "displayName": "Set Security Center contact email address and phone number on Subscriptions", 6 | "description": "Automatically set the security contact email address and phone number should they be blank on the subscription", 7 | "metadata": { 8 | "category": "Security Center" 9 | }, 10 | "parameters": { 11 | "effect": { 12 | "type": "String", 13 | "metadata": { 14 | "displayName": "Effect", 15 | "description": "Enable or disable the execution of the policy" 16 | }, 17 | "allowedValues": [ 18 | "DeployIfNotExists", 19 | "AuditIfNotExists", 20 | "Disabled" 21 | ], 22 | "defaultValue": "DeployIfNotExists" 23 | }, 24 | "securityContactsEmail": { 25 | "type": "String", 26 | "defaultValue": "", 27 | "metadata": { 28 | "displayName": "The email of the Security Center Contact.", 29 | "description": "The email of the Security Center Contact." 30 | } 31 | }, 32 | "securityContactsPhone": { 33 | "type": "String", 34 | "defaultValue": "", 35 | "metadata": { 36 | "displayName": "The phone number of the Security Center Contact.", 37 | "description": "The phone number of the Security Center Contact." 38 | } 39 | } 40 | }, 41 | "policyRule": { 42 | "if": { 43 | "field": "type", 44 | "equals": "Microsoft.Resources/subscriptions" 45 | }, 46 | "then": { 47 | "effect": "[parameters('effect')]", 48 | "details": { 49 | "type": "Microsoft.Security/securityContacts", 50 | "deploymentScope": "subscription", 51 | "existenceScope": "subscription", 52 | "roleDefinitionIds": [ 53 | "/providers/microsoft.authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" 54 | ], 55 | "existenceCondition": { 56 | "anyOf": [ 57 | { 58 | "field": "Microsoft.Security/securityContacts/email", 59 | "equals": "[parameters('securityContactsEmail')]" 60 | }, 61 | { 62 | "field": "Microsoft.Security/securityContacts/phone", 63 | "equals": "[parameters('securityContactsPhone')]" 64 | } 65 | ] 66 | }, 67 | "deployment": { 68 | "location": "uksouth", 69 | "properties": { 70 | "mode": "incremental", 71 | "parameters": { 72 | "securityContactsEmail": { 73 | "value": "[parameters('securityContactsEmail')]" 74 | }, 75 | "securityContactsPhone": { 76 | "value": "[parameters('securityContactsPhone')]" 77 | } 78 | }, 79 | "template": { 80 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 81 | "contentVersion": "1.0.0.0", 82 | "parameters": { 83 | "securityContactsEmail": { 84 | "type": "String" 85 | }, 86 | "securityContactsPhone": { 87 | "type": "String" 88 | } 89 | }, 90 | "variables": {}, 91 | "resources": [ 92 | { 93 | "type": "Microsoft.Security/securityContacts", 94 | "name": "default", 95 | "apiVersion": "2017-08-01-preview", 96 | "properties": { 97 | "email": "[parameters('securityContactsEmail')]", 98 | "phone": "[parameters('securityContactsPhone')]", 99 | "alertNotifications": "On", 100 | "alertsToAdmins": "On" 101 | } 102 | } 103 | ] 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /policies/Security Center/enable_vulnerability_vm_assessments.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "enable_vulnerability_vm_assessments", 4 | "properties": { 5 | "displayName": "Enable Security Center VM Vulnerability Assessments", 6 | "description": "Enable Security Center VM Vulnerability Assessments", 7 | "metadata": { 8 | "category": "Security Center" 9 | }, 10 | "parameters": { 11 | "effect": { 12 | "type": "String", 13 | "metadata": { 14 | "displayName": "Effect", 15 | "description": "Enable or disable the execution of the policy" 16 | }, 17 | "allowedValues": [ 18 | "DeployIfNotExists", 19 | "AuditIfNotExists", 20 | "Disabled" 21 | ], 22 | "defaultValue": "DeployIfNotExists" 23 | } 24 | }, 25 | "policyRule": { 26 | "if": { 27 | "field": "type", 28 | "in": [ 29 | "Microsoft.Compute/virtualMachines", 30 | "Microsoft.ClassicCompute/virtualMachines" 31 | ] 32 | }, 33 | "then": { 34 | "effect": "[parameters('effect')]", 35 | "details": { 36 | "type": "Microsoft.Security/assessments", 37 | "name": "ffff0522-1e88-47fc-8382-2a80ba848f5d", 38 | "existenceCondition": { 39 | "field": "Microsoft.Security/assessments/status.code", 40 | "in": [ 41 | "NotApplicable", 42 | "Healthy" 43 | ] 44 | } 45 | }, 46 | "deployment": { 47 | "properties": { 48 | "mode": "incremental", 49 | "parameters": { 50 | "vmName": { 51 | "value": "[field('name')]" 52 | }, 53 | "vmLocation": { 54 | "value": "[field('location')]" 55 | }, 56 | "apiVersionByEnv": { 57 | "value": "2015-06-01-preview" 58 | } 59 | }, 60 | "template": { 61 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 62 | "contentVersion": "1.0.0.0", 63 | "parameters": { 64 | "vmName": { 65 | "type": "string" 66 | }, 67 | "vmLocation": { 68 | "type": "string" 69 | }, 70 | "apiVersionByEnv": { 71 | "type": "string" 72 | } 73 | }, 74 | "resources": [ 75 | { 76 | "type": "resourceType/providers/serverVulnerabilityAssessments", 77 | "name": "[concat(parameters('vmName'), '/Microsoft.Security/default')]", 78 | "location": "[parameters('vmLocation')]", 79 | "apiVersion": "[parameters('apiVersionByEnv')]" 80 | } 81 | ] 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /policies/Storage/storage_enforce_https.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "storage_enforce_https", 4 | "properties": { 5 | "metadata": { 6 | "category": "Storage" 7 | }, 8 | "parameters": { 9 | "effect": { 10 | "type": "String", 11 | "defaultValue": "Deny", 12 | "allowedValues": [ 13 | "Audit", 14 | "Deny", 15 | "Disabled" 16 | ], 17 | "metadata": { 18 | "displayName": "Effect", 19 | "description": "The effect determines what happens when the policy rule is evaluated to match" 20 | } 21 | } 22 | }, 23 | "policyRule": { 24 | "if": { 25 | "allOf": [ 26 | { 27 | "field": "type", 28 | "equals": "Microsoft.Storage/storageAccounts" 29 | }, 30 | { 31 | "anyOf": [ 32 | { 33 | "allOf": [ 34 | { 35 | "value": "[requestContext().apiVersion]", 36 | "less": "2019-04-01" 37 | }, 38 | { 39 | "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", 40 | "exists": "false" 41 | } 42 | ] 43 | }, 44 | { 45 | "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", 46 | "equals": "false" 47 | } 48 | ] 49 | } 50 | ] 51 | }, 52 | "then": { 53 | "effect": "[parameters('effect')]" 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /policies/Storage/storage_enforce_minimum_tls1_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "storage_enforce_minimum_tls1_2", 4 | "properties": { 5 | "metadata": { 6 | "category": "Storage" 7 | }, 8 | "parameters": { 9 | "effect": { 10 | "type": "String", 11 | "defaultValue": "Deny", 12 | "allowedValues": [ 13 | "Audit", 14 | "Deny", 15 | "Disabled" 16 | ], 17 | "metadata": { 18 | "displayName": "Effect", 19 | "description": "The effect determines what happens when the policy rule is evaluated to match" 20 | } 21 | } 22 | }, 23 | "policyRule": { 24 | "if": { 25 | "allOf": [ 26 | { 27 | "field": "type", 28 | "equals": "Microsoft.Storage/storageAccounts" 29 | }, 30 | { 31 | "not": { 32 | "field": "Microsoft.Storage/storageAccounts/minimumTlsVersion", 33 | "equals": "TLS1_2" 34 | } 35 | } 36 | ] 37 | }, 38 | "then": { 39 | "effect": "[parameters('effect')]" 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /policies/Tags/add_replace_resource_group_tag_key_modify.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "add_replace_resource_group_tag_key_modify", 4 | "properties": { 5 | "metadata": { 6 | "category": "Tags" 7 | }, 8 | "parameters": { 9 | "effect": { 10 | "type": "String", 11 | "defaultValue": "Modify", 12 | "allowedValues": [ 13 | "Modify", 14 | "Disabled" 15 | ], 16 | "metadata": { 17 | "displayName": "Effect", 18 | "description": "The effect determines what happens when the policy rule is evaluated to match" 19 | } 20 | } 21 | }, 22 | "policyRule": { 23 | "if": { 24 | "allOf": [ 25 | { 26 | "field": "type", 27 | "equals": "Microsoft.Resources/subscriptions/resourceGroups" 28 | }, 29 | { 30 | "field": "tags['Owner']", 31 | "exists": "false" 32 | }, 33 | { 34 | "field": "tags['Team']", 35 | "exists": "false" 36 | }, 37 | { 38 | "field": "tags['CostCenter']", 39 | "exists": "false" 40 | }, 41 | { 42 | "value": "[less(length(field('tags')), 3)]", 43 | "equals": "true" 44 | } 45 | ] 46 | }, 47 | "then": { 48 | "effect": "[parameters('effect')]", 49 | "details": { 50 | "roleDefinitionIds": [ 51 | "/providers/microsoft.authorization/roleDefinitions/4a9ae827-6dc8-4573-8ac7-8239d42aa03f" 52 | ], 53 | "operations": [ 54 | { 55 | "operation": "addOrReplace", 56 | "field": "tags['Owner']", 57 | "value": "<>" 58 | }, 59 | { 60 | "operation": "addOrReplace", 61 | "field": "tags['Team']", 62 | "value": "<>" 63 | }, 64 | { 65 | "operation": "addOrReplace", 66 | "field": "tags['CostCenter']", 67 | "value": "<>" 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /policies/Tags/inherit_resource_group_tags_append.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "inherit_resource_group_tags_append", 4 | "properties": { 5 | "metadata": { 6 | "category": "Tags" 7 | }, 8 | "parameters": { 9 | "tagName": { 10 | "type": "String", 11 | "metadata": { 12 | "displayName": "Tag Name", 13 | "description": "Name of the tag, such as 'environment'" 14 | } 15 | }, 16 | "effect": { 17 | "type": "String", 18 | "defaultValue": "Append", 19 | "allowedValues": [ 20 | "Append", 21 | "Disabled" 22 | ], 23 | "metadata": { 24 | "displayName": "Effect", 25 | "description": "The effect determines what happens when the policy rule is evaluated to match" 26 | } 27 | } 28 | }, 29 | "policyRule": { 30 | "if": { 31 | "allOf": [ 32 | { 33 | "field": "[concat('tags[', parameters('tagName'), ']')]", 34 | "exists": "false" 35 | }, 36 | { 37 | "value": "[resourceGroup().tags[parameters('tagName')]]", 38 | "notEquals": "" 39 | } 40 | ] 41 | }, 42 | "then": { 43 | "effect": "[parameters('effect')]", 44 | "details": [ 45 | { 46 | "field": "[concat('tags[', parameters('tagName'), ']')]", 47 | "value": "[resourceGroup().tags[parameters('tagName')]]" 48 | } 49 | ] 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /policies/Tags/inherit_resource_group_tags_modify.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "inherit_resource_group_tags_modify", 4 | "properties": { 5 | "metadata": { 6 | "category": "Tags" 7 | }, 8 | "parameters": { 9 | "tagName": { 10 | "type": "String", 11 | "metadata": { 12 | "displayName": "Tag Name", 13 | "description": "Name of the tag, such as 'environment'" 14 | } 15 | }, 16 | "effect": { 17 | "type": "String", 18 | "defaultValue": "Modify", 19 | "allowedValues": [ 20 | "Modify", 21 | "Disabled" 22 | ], 23 | "metadata": { 24 | "displayName": "Effect", 25 | "description": "The effect determines what happens when the policy rule is evaluated to match" 26 | } 27 | } 28 | }, 29 | "policyRule": { 30 | "if": { 31 | "allOf": [ 32 | { 33 | "field": "[concat('tags[', parameters('tagName'), ']')]", 34 | "notEquals": "[resourceGroup().tags[parameters('tagName')]]" 35 | }, 36 | { 37 | "value": "[resourceGroup().tags[parameters('tagName')]]", 38 | "notEquals": "" 39 | } 40 | ] 41 | }, 42 | "then": { 43 | "effect": "[parameters('effect')]", 44 | "details": { 45 | "roleDefinitionIds": [ 46 | "/providers/microsoft.authorization/roleDefinitions/4a9ae827-6dc8-4573-8ac7-8239d42aa03f" 47 | ], 48 | "operations": [ 49 | { 50 | "operation": "addOrReplace", 51 | "field": "[concat('tags[', parameters('tagName'), ']')]", 52 | "value": "[resourceGroup().tags[parameters('tagName')]]" 53 | } 54 | ] 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /policies/Tags/require_resource_group_tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Microsoft.Authorization/policyDefinitions", 3 | "name": "require_resource_group_tags", 4 | "properties": { 5 | "metadata": { 6 | "category": "Tags" 7 | }, 8 | "parameters": { 9 | "tagName": { 10 | "type": "String", 11 | "metadata": { 12 | "displayName": "Tag Name", 13 | "description": "Name of the tag, such as 'environment'" 14 | } 15 | }, 16 | "effect": { 17 | "type": "String", 18 | "defaultValue": "Audit", 19 | "allowedValues": [ 20 | "Audit", 21 | "Deny", 22 | "Disabled" 23 | ], 24 | "metadata": { 25 | "displayName": "Effect", 26 | "description": "The effect determines what happens when the policy rule is evaluated to match" 27 | } 28 | } 29 | }, 30 | "policyRule": { 31 | "if": { 32 | "allOf": [ 33 | { 34 | "field": "type", 35 | "equals": "Microsoft.Resources/subscriptions/resourceGroups" 36 | }, 37 | { 38 | "field": "[concat('tags[', parameters('tagName'), ']')]", 39 | "exists": "false" 40 | } 41 | ] 42 | }, 43 | "then": { 44 | "effect": "[parameters('effect')]" 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /project.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | // list of extensions which should be recommended for users of this workspace. 9 | "recommendations": [ 10 | "azurepolicy.azurepolicyextension", 11 | "hashicorp.terraform", 12 | "oderwat.indent-rainbow" 13 | ], 14 | // azure policy definition schemas 15 | "json.schemas": [ 16 | { 17 | "fileMatch": [ 18 | "policies/*/*.json" 19 | ], 20 | "url": "https://schema.management.azure.com/schemas/2020-10-01/policyDefinition.json" 21 | } 22 | ], 23 | // github workflows 24 | "github-actions.workflows.pinned.refresh.enabled": true, 25 | "github-actions.workflows.pinned.refresh.interval": 30, 26 | "github-actions.workflows.pinned.workflows": [ 27 | ".github/workflows/cd.yml", 28 | ".github/workflows/ci.yml" 29 | ], 30 | // code formatting 31 | "files.trimTrailingWhitespace": true, 32 | "[markdown]": { 33 | "files.trimTrailingWhitespace": false 34 | }, 35 | "cSpell.language": "en-GB", 36 | "cSpell.enabled": true, 37 | "editor.formatOnPaste": true, 38 | "editor.formatOnSave": true, 39 | // pwsh formatting 40 | "powershell.codeFormatting.addWhitespaceAroundPipe": true, 41 | "powershell.codeFormatting.alignPropertyValuePairs": true, 42 | "powershell.codeFormatting.autoCorrectAliases": true, 43 | "powershell.codeFormatting.ignoreOneLineBlock": true, 44 | "powershell.codeFormatting.newLineAfterCloseBrace": true, 45 | "powershell.codeFormatting.newLineAfterOpenBrace": true, 46 | "powershell.codeFormatting.openBraceOnSameLine": true, 47 | "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", 48 | "powershell.codeFormatting.trimWhitespaceAroundPipe": true, 49 | "powershell.codeFormatting.useConstantStrings": true, 50 | "powershell.codeFormatting.useCorrectCasing": true, 51 | "powershell.codeFormatting.whitespaceAfterSeparator": true, 52 | "powershell.codeFormatting.whitespaceAroundOperator": true, 53 | "powershell.codeFormatting.whitespaceBeforeOpenBrace": true, 54 | "powershell.codeFormatting.whitespaceBeforeOpenParen": true, 55 | "powershell.codeFormatting.whitespaceBetweenParameters": true, 56 | // enable bracket pair colorization. 57 | "editor.bracketPairColorization.enabled": true, 58 | "editor.guides.bracketPairs": "active", 59 | } 60 | } -------------------------------------------------------------------------------- /scripts/convert_from_tf_plan.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Converts Policies into JSON files from a terraform plan output 4 | Useful when migrating from an existing deployment 5 | terraform plan -out='tfplan' 6 | .PARAMETER planFile 7 | The plan file name e.g. tfplan 8 | #> 9 | 10 | [CmdletBinding()] 11 | Param( 12 | [string] [Parameter(Mandatory = $true)] $planFile = "tfplan" 13 | ) 14 | 15 | # check dependancies 16 | if ($PSVersionTable.PSVersion.Major -lt 7) { throw "Please use PowerShell >= 7.0" } 17 | 18 | $VerbosePreference = 'Continue' 19 | $ErrorActionPreference = 'SilentlyContinue' 20 | 21 | Push-Location -Path $PSScriptRoot 22 | 23 | # convert plan file to json 24 | terraform show -json tfplan > tfplan.json 25 | 26 | # create output directories 27 | if (!(Get-ChildItem "./policies_output/.initiatives")) { New-Item -ItemType Directory -Name "./policies_output/.initiatives" } 28 | 29 | # enable title case 30 | $TextInfo = (Get-Culture).TextInfo 31 | 32 | # import definitions 33 | $definitions = (Get-Content "$planFile.json" | ConvertFrom-Json).planned_values.root_module.child_modules.resources 34 | 35 | Write-Host "Creating $($definitions.Count) Policy Definitions from TF Plan..." -ForegroundColor Magenta 36 | 37 | $definitions | Foreach-Object { 38 | $name = $_.values.name.Replace("-", "_").ToLower() 39 | $category = $TextInfo.ToTitleCase($name.Split("_")[1]) 40 | 41 | try { $mode = $_.values.mode } catch { $mode = "All" } 42 | try { $parameters = $_.values.parameters } catch { $parameters = @{} } 43 | try { $policyRule = $_.values.policy_rule } catch { $policyRule = @{} } 44 | try { 45 | $metadata = $_.values.metadata | ConvertFrom-Json 46 | if (!($metadata.category)) { 47 | Add-Member -InputObject $metadata -MemberType NoteProperty -Name "category" -Value $category 48 | } 49 | if (!($metadata.version)) { 50 | Add-Member -InputObject $metadata -MemberType NoteProperty -Name "version" -Value "1.0.0" 51 | } 52 | } 53 | catch { 54 | $metadata = @{ 55 | category = $category 56 | version = "1.0.0" 57 | } 58 | } 59 | 60 | if ($_.type -eq "azurerm_policy_definition") { 61 | if (!(Get-ChildItem "./policies_output/$category")) { New-Item -ItemType Directory -Name "./policies_output/$category" } 62 | [ordered]@{ 63 | type = "Microsoft.Authorization/policyDefinitions" 64 | name = $name 65 | properties = [ordered]@{ 66 | displayName = $_.values.display_name 67 | description = $_.values.description 68 | mode = $(if (!($mode)) { "All" } else { $mode }) 69 | policyType = "Custom" 70 | metadata = $metadata 71 | parameters = $(if (!($parameters)) { @{} } else { $parameters | ConvertFrom-Json }) 72 | policyRule = $(if (!($policyRule)) { @{} } else { $policyRule | ConvertFrom-Json }) 73 | } 74 | } | ConvertTo-Json -Depth 100 | Out-File -FilePath "./policies_output/$category/$name.json" -Force 75 | } 76 | elseif ($_.type -eq "azurerm_policy_set_definition") { 77 | [ordered]@{ 78 | type = "Microsoft.Authorization/policySetDefinitions" 79 | name = $name 80 | properties = [ordered]@{ 81 | displayName = $_.values.display_name 82 | description = $_.values.description 83 | policyType = "Custom" 84 | metadata = $metadata 85 | parameters = $(if (!($parameters)) { @{} } else { $parameters | ConvertFrom-Json }) 86 | policy_definition_group = @() 87 | policyDefinitions = @($_.values.policy_definition_reference) 88 | } 89 | } | ConvertTo-Json -Depth 100 | Out-File -FilePath "./policies_output/initiatives/$name.json" -Force 90 | } 91 | } 92 | Pop-Location 93 | -------------------------------------------------------------------------------- /scripts/dsc_examples/README.md: -------------------------------------------------------------------------------- 1 | # PowerShell Desired State Configs 2 | 3 | [![cd-machine-config](https://github.com/gettek/terraform-azurerm-policy-as-code/actions/workflows/cd-guest-config.yml/badge.svg)](https://github.com/gettek/terraform-azurerm-policy-as-code/actions/workflows/cd-guest-config.yml) 4 | 5 | This folder contains example desired state configs for both windows and linux machines, compiled by [build_machine_config_packages.ps1](../build_machine_config_packages.ps1). See the example deployment [ReadMe](../../examples-machine-config) for more information. 6 | 7 | > 💡 **Note:** Linux configs are prefixed with `nx` so that the build script can determine which platform their respective policies belong in. 8 | > 💡 **Note:** Versioning is suffixed to the filename so the build script can extract and include into policy definition metadata 9 | 10 | ## Links 11 | 12 | - 📙 [Azure Policy Guest Configuration samples](https://github.com/Azure/azure-policy/tree/master/samples/GuestConfiguration/package-samples) 13 | - 📙 [DSC GitHub Community](https://github.com/dsccommunity) -------------------------------------------------------------------------------- /scripts/dsc_examples/WindowsSecurityBaseline2016_1.1.0.ps1: -------------------------------------------------------------------------------- 1 | Configuration WindowsSecurityBaseline2016 2 | { 3 | 4 | Import-DSCResource -ModuleName 'PSDscResources' 5 | Import-DSCResource -ModuleName 'AuditPolicyDSC' 6 | Import-DSCResource -ModuleName 'SecurityPolicyDSC' 7 | 8 | Node localhost 9 | { 10 | Registry 'Registry(POL): HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client\DisabledByDefault' { 11 | ValueName = 'DisabledByDefault' 12 | ValueType = 'Dword' 13 | Key = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client' 14 | ValueData = 1 15 | 16 | } 17 | } 18 | } 19 | WindowsSecurityBaseline2016 -------------------------------------------------------------------------------- /scripts/dsc_examples/nxLAMPServer_2.1.2.ps1: -------------------------------------------------------------------------------- 1 | Configuration nxLAMPServer { 2 | Import-DSCResource -module 'nx' 3 | 4 | Node localhost { 5 | 6 | $requiredPackages = @('httpd', 'mod_ssl', 'php', 'php-mysqlnd', 'mariadb', 'mariadb-server') 7 | $enabledServices = @('httpd', 'mariadb') 8 | 9 | #Ensure packages are installed 10 | ForEach ($package in $requiredPackages) { 11 | nxPackage $Package { 12 | Ensure = 'Present' 13 | Name = $Package 14 | PackageManager = 'yum' 15 | } 16 | } 17 | 18 | #Ensure daemons are enabled 19 | ForEach ($service in $enabledServices) { 20 | nxService $service { 21 | Enabled = $true 22 | Name = $service 23 | Controller = 'SystemD' 24 | State = 'running' 25 | } 26 | } 27 | } 28 | } 29 | nxLAMPServer -------------------------------------------------------------------------------- /scripts/markdown_generator.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Generates Markdown based on local library policy definition content 3 | The output is stored in policies/README.md 4 | #> 5 | 6 | $ErrorActionPreference = 'SilentlyContinue' 7 | 8 | Push-Location -Path "$PSScriptRoot/../policies" 9 | 10 | $definitionChildrenDir = Get-ChildItem -Recurse -Directory | Sort-Object -Property "Name" 11 | $definitionChildren = Get-ChildItem -Recurse -File -Include "*.json" -Exclude "example*.json" | Sort-Object -Property "Directory.Name" 12 | 13 | $definitionList = [ordered]@{} 14 | foreach ($category in $definitionChildrenDir.Name) { 15 | $definitionList[$category] = @() 16 | } 17 | 18 | foreach ($child in $definitionChildren) { 19 | $content = Get-Content -Path $child.FullName | ConvertFrom-Json 20 | 21 | $BaseName = try { $child.BaseName } catch { "" } 22 | $Name = try { $content.name } catch { "" } 23 | $Description = try { $content.properties.description.Replace("\n", " ") } catch { "" } 24 | $DisplayName = try { $content.properties.displayName } catch { "" } 25 | $Version = try { $content.properties.metadata.version } catch { @{} } 26 | $Effect = try { $content.properties.policyRule.then.effect } catch { "" } 27 | 28 | if ($child.BaseName -notin $definitionList[$child.Directory.Name].BaseName) { 29 | 30 | [PSObject]$object = [ordered]@{ 31 | "BaseName" = $BaseName 32 | "Name" = $Name 33 | "Description" = $Description 34 | "DisplayName" = $DisplayName 35 | "Version" = $Version 36 | "Effect" = $Effect 37 | } 38 | 39 | $definitionList[$child.Directory.Name] += $object 40 | 41 | if ($content.properties.parameters | Get-Member -MemberType "NoteProperty") { 42 | [array]$paramsList = @() 43 | 44 | foreach ($param in $content.properties.parameters.PSObject.properties) { 45 | [PSObject]$thisParam = [ordered]@{ 46 | "Name" = $param.Name 47 | "Description" = $param.Value.metadata.description 48 | } 49 | 50 | if ($param.Value.defaultValue) { 51 | $thisParam | Add-Member -NotePropertyName "DefaultValue" -NotePropertyValue $param.Value.defaultValue 52 | } 53 | 54 | if ($param.Value.allowedValues) { 55 | $thisParam | Add-Member -NotePropertyName "AllowedValues" -NotePropertyValue $param.Value.allowedValues 56 | } 57 | 58 | $paramsList += $thisParam 59 | } 60 | 61 | $object | Add-Member -NotePropertyName "Parameters" -NotePropertyValue $paramsList 62 | } 63 | } 64 | else { 65 | throw "`nDuplicate Name!" 66 | } 67 | } 68 | 69 | # Output to Mardown 70 | $heading = "`n# Custom Policy Definition Library" 71 | $file = "README.md" 72 | 73 | Write-Output "$heading" | Out-File -FilePath $file -Force 74 | $append = @{ 75 | "Append" = $true 76 | "FilePath" = $file 77 | } 78 | 79 | Write-Output "`nExample custom definitions - Compile time: $(Get-Date)" | Out-File @append 80 | 81 | Write-Output "`n## Categories" | Out-File @append 82 | foreach ($definition in $definitionList.Keys) { 83 | Write-Output "- [$definition](#$($definition.Replace(" ","-")))" | Out-File @append 84 | } 85 | 86 | Write-Output "`n# Definitions" | Out-File @append 87 | foreach ($definition in $definitionList.Keys) { 88 | Write-Output "`n## $definition" | Out-File @append 89 | 90 | foreach ($item in $definitionList[$definition]) { 91 | 92 | Write-Output "`n### 📜 [$($item.BaseName)](./$($definition.Replace(" ","%20"))/$($item.BaseName).json)" | Out-File @append 93 | Write-Output "| Title | Description |" | Out-File @append 94 | Write-Output "| ----- | ----------- |" | Out-File @append 95 | Write-Output "| Name | $($item.Name) |" | Out-File @append 96 | Write-Output "| DisplayName | $($item.DisplayName) |" | Out-File @append 97 | Write-Output "| Description | $($item.Description.Replace("`n", " ")) |" | Out-File @append 98 | Write-Output "| Version | $($item.Version) |" | Out-File @append 99 | Write-Output "| Effect | $($item.Effect) |" | Out-File @append 100 | 101 | 102 | if ($item | Get-Member -MemberType "NoteProperty") { 103 | Write-Output "`n#### 🧮 ~ Parameters" | Out-File @append 104 | Write-Output "| Name | Description | Default Value | Allowed Values |" | Out-File @append 105 | Write-Output "| ---- | ----------- | ------------- | -------------- |" | Out-File @append 106 | 107 | foreach ($param in $item.parameters) { 108 | Write-Output "| $($param.Name) | $($param.Description) | $($param.DefaultValue) | $($param.AllowedValues) |" | Out-File @append 109 | } 110 | } 111 | 112 | Write-Output "`n
" | Out-File @append 113 | Write-Output "`n
" | Out-File @append 114 | } 115 | 116 | Write-Output "`n---" | Out-File @append 117 | } 118 | 119 | Pop-Location 120 | -------------------------------------------------------------------------------- /scripts/precommit.ps1: -------------------------------------------------------------------------------- 1 | # PRECOMMIT TASKS 2 | # Requires 'terraform-docs' (https://terraform-docs.io/) 3 | 4 | [CmdletBinding()] 5 | Param( 6 | [switch] [Parameter(Mandatory = $False)] $tf, 7 | [switch] [Parameter(Mandatory = $False)] $checksums, 8 | [switch] [Parameter(Mandatory = $False)] $library, 9 | [switch] [Parameter(Mandatory = $False)] $lint 10 | ) 11 | 12 | $env:ARM_SKIP_CREDENTIALS_VALIDATION = 'true' 13 | $env:ARM_SKIP_PROVIDER_REGISTRATION = 'true' 14 | 15 | $docConfigs = Resolve-Path -Path "$PSScriptRoot/../.config" 16 | $modules = Resolve-Path -Path "$PSScriptRoot/../modules" 17 | $examples = Resolve-Path -Path "$PSScriptRoot/../" 18 | $policies = Resolve-Path -Path "$PSScriptRoot/../policies" 19 | 20 | # Modules 21 | Push-Location $modules 22 | (Get-ChildItem -Directory).BaseName | Foreach-Object { 23 | try { 24 | Push-Location -Path $_ 25 | if ($tf) { 26 | Write-Host "🟪 Running TF Init on '$_'..." -ForegroundColor Magenta 27 | terraform init -backend=false -upgrade 28 | Write-Host "✅ Terraform fmt & validate '$_'..." -ForegroundColor Magenta 29 | terraform fmt 30 | terraform validate 31 | } 32 | Write-Host "📜 Generating '$_' Docs..." -ForegroundColor Magenta 33 | terraform-docs ` 34 | -c "$docConfigs\terraform-docs.yml" . ` 35 | --header-from "$docConfigs\templ-$_.md" ` 36 | --hide modules ` 37 | --output-mode replace | Out-Null 38 | } 39 | catch { 40 | Write-Host "🥵 Could not complete precommit tasks: $_" -ForegroundColor Red 41 | } 42 | finally { 43 | Pop-Location 44 | } 45 | } 46 | 47 | # Examples 48 | Push-Location $examples 49 | (Get-ChildItem -Directory -Path examples*).BaseName | Foreach-Object { 50 | try { 51 | Push-Location -Path $_ 52 | if ($tf) { 53 | Write-Host "🟪 Running TF Init on '$_'..." -ForegroundColor Magenta 54 | terraform init -backend=false -upgrade 55 | if ($checksums) { 56 | Write-Host "🔢 Calculating Provider Checksums..." -ForegroundColor Magenta 57 | terraform providers lock -platform=windows_amd64 -platform=linux_amd64 58 | } 59 | Write-Host "✅ Terraform fmt & validate '$_'..." -ForegroundColor Magenta 60 | terraform fmt 61 | terraform validate 62 | } 63 | Write-Host "📜 Generating '$_' Docs..." -ForegroundColor Magenta 64 | terraform-docs ` 65 | -c "$docConfigs\terraform-docs.yml" . ` 66 | --header-from "$docConfigs\templ-$_.md" ` 67 | --output-mode replace | Out-Null 68 | } 69 | catch { 70 | Write-Host "🥵 Could not complete precommit tasks: $_" -ForegroundColor Red 71 | } 72 | finally { 73 | Pop-Location 74 | } 75 | } 76 | 77 | # Return to original working directory 78 | Pop-Location; Pop-Location 79 | 80 | 81 | # Update custom definition library readme 82 | if ($library) { 83 | Write-Host "📜 Generating Custom Library Docs..." -ForegroundColor Magenta 84 | & "$PSScriptRoot/markdown_generator.ps1" 85 | } 86 | 87 | if ($lint) { 88 | $categories = (Get-ChildItem -Directory $policies -Exclude 'Deprecated').name 89 | $noDefaultValues = @{} 90 | 91 | $categories.ForEach({ 92 | Write-Host "Processing Policy Category $_..." -ForegroundColor Magenta 93 | 94 | $category = $_ 95 | $fileName = $_.Replace(' ', '-').ToLower() 96 | $defs = (Get-ChildItem -File "$policies/$_" -Exclude 'README.md', '.gitkeep').FullName 97 | $defs.ForEach({ 98 | $policyName = $_.Split('\')[-1].Replace('.json', '') 99 | $policyObject = (Get-Content $_ | ConvertFrom-Json -AsHashtable -Depth 200) 100 | try { 101 | $policyParameters = $policyObject.properties.parameters 102 | $policyParameters.Keys | ForEach-Object { 103 | $parameterName = $_ 104 | $value = $policyParameters.$_ 105 | 106 | if (!$value.Keys.Contains('defaultValue')) { 107 | $noDefaultValues += @{ 108 | "$category > ${policyName} > $parameterName" = $value.type 109 | } 110 | } 111 | } 112 | } 113 | catch { 114 | Write-Host "$policyName does not contain any params" -ForegroundColor Yellow 115 | } 116 | }) 117 | }) 118 | 119 | if ($noDefaultValues.Count -gt 0) { 120 | Write-Host "❌ The definitions below are missing defaultValues:" 121 | $noDefaultValues | Sort-Object | Format-Table -AutoSize 122 | } 123 | } 124 | --------------------------------------------------------------------------------