From aca8f07050ac1d79bc29d3d2465ca982284fcfbc Mon Sep 17 00:00:00 2001 From: Johan Dahlbom Date: Fri, 17 Feb 2023 07:58:00 +0100 Subject: [PATCH] Subperf staging (#2) * BaseUpdate * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update Policy Logic * Update * Update * Update * Add Jq sorting for parameter files * Update * UpdateSubscriptionOfferState * Update * Update * Update * Update Strings * Update Pester String Test * Update * BumpVersion * Update * Update * Update * Update * Update Case Sensitivity * Update Roles * Update Strings * Update Verbosity * Updates * Update * Update * SwitchTo1kPlusSubLogic * Update * Update Save-AzOpsManagementGroupChild Parallel * Update Deployment Removal * Update * Update * Update with code description * Update msgCommon * Change Switch to If * Cleanup Class * Update * Update with additional casing * Update Remove-AzOpsDeployment Graph Query * Update Remove-AzOpsDeployment Array * Remove Invoke-AzOpsPull.Validating.UserRole * Update with Stack * Update Subscription Discovery * Update FileNames * Update FileName2 * Update Synopsis --------- Co-authored-by: Jesper Fajers --- .github/workflows/wiki-sync.yml | 3 +- README.md | 3 +- docs/wiki/Settings.md | 39 +- src/AzOps.psd1 | 1 - ...initions.template.jq => locks.template.jq} | 0 .../policyAssignments.jq | 2 +- .../policyAssignments.template.jq | 24 - .../policyDefinitions.jq | 2 +- .../policyExemptions.jq | 2 +- .../policySetDefinitions.jq | 2 +- .../policySetDefinitions.template.jq | 20 - .../roleAssignments.jq | 3 +- .../roleAssignments.template.jq | 20 - .../roleDefinitions.jq | 2 +- .../roleDefinitions.template.jq | 21 - .../template/Microsoft.KeyVault/vaults.jq | 1 - .../Microsoft.KeyVault/vaults.template.jq | 23 - .../template/Microsoft.Keyvault/vaults.jq | 1 + .../Microsoft.Network/virtualNetworks.jq | 1 - .../template/Microsoft.Network/virtualWans.jq | 1 - .../Microsoft.Network/virtualnetworks.jq | 1 + .../template/Microsoft.Network/virtualwans.jq | 1 + ...template.jq => resourcegroups.template.jq} | 8 +- .../Microsoft.Storage/storageAccounts.jq | 9 - .../Microsoft.Storage/storageaccounts.jq | 9 + src/data/template/generic.jq | 2 +- src/data/template/template.jq | 21 +- src/data/template/template.parameters.jq | 4 +- src/functions/Initialize-AzOpsEnvironment.ps1 | 4 +- src/functions/Invoke-AzOpsPull.ps1 | 61 +- src/functions/Invoke-AzOpsPush.ps1 | 19 +- src/internal/classes/AzOpsRoleAssignment.ps1 | 19 - src/internal/classes/AzOpsRoleDefinition.ps1 | 26 - src/internal/classes/AzOpsScope.ps1 | 55 +- src/internal/configurations/Core.ps1 | 1 - .../functions/ConvertTo-AzOpsState.ps1 | 18 +- ...roups.ps1 => Get-AzOpsManagementGroup.ps1} | 6 +- .../functions/Get-AzOpsNestedSubscription.ps1 | 40 + src/internal/functions/Get-AzOpsPolicy.ps1 | 46 +- .../functions/Get-AzOpsPolicyAssignment.ps1 | 51 +- .../functions/Get-AzOpsPolicyDefinition.ps1 | 35 +- .../functions/Get-AzOpsPolicyExemption.ps1 | 10 +- .../Get-AzOpsPolicySetDefinition.ps1 | 35 +- .../functions/Get-AzOpsResourceDefinition.ps1 | 755 +++++++----------- .../functions/Get-AzOpsResourceLock.ps1 | 6 +- src/internal/functions/Get-AzOpsRole.ps1 | 8 +- .../functions/Get-AzOpsRoleAssignment.ps1 | 25 +- .../functions/Get-AzOpsRoleDefinition.ps1 | 28 +- ...et-AzOpsRoleEligibilityScheduleRequest.ps1 | 6 +- .../functions/Invoke-AzOpsRestMethod.ps1 | 48 ++ .../functions/New-AzOpsDeployment.ps1 | 2 +- .../functions/Remove-AzOpsDeployment.ps1 | 42 +- ...s.ps1 => Remove-AzOpsInvalidCharacter.ps1} | 11 +- ...ps1 => Save-AzOpsManagementGroupChild.ps1} | 79 +- .../functions/Search-AzOpsAzGraph.ps1 | 93 +++ .../functions/Set-AzOpsStringLength.ps1 | 1 + src/internal/scripts/Initialize.ps1 | 8 +- src/localized/en-us/Strings.psd1 | 118 ++- src/tests/functional/Functional.Tests.ps1 | 3 + .../policyAssignments/scenario.ps1 | 2 +- .../roleAssignments/scenario.ps1 | 5 +- .../virtualMachines/deploy/deploy.json | 6 +- .../deploy/deploy.parameters.json | 3 + .../virtualMachines/scenario.ps1 | 3 + .../azureFirewalls/deploy/deploy.json | 5 + .../deploy/deploy.parameters.json | 7 + .../azureFirewalls/scenario.ps1 | 3 + .../connections/deploy/deploy.json | 13 +- .../privateDnsZones/deploy/deploy.json | 13 +- src/tests/general/PSScriptAnalyzer.Tests.ps1 | 4 +- src/tests/general/Strings.Tests.ps1 | 10 + src/tests/integration/Repository.Tests.ps1 | 21 +- src/tests/templates/azuredeploy.jsonc | 3 + src/tests/templates/biceptest.bicep | 3 +- src/tests/templates/biceptest.parameters.json | 3 + src/tests/templates/rbactest.bicep | 10 +- src/tests/templates/rbactest.parameters.json | 18 + 77 files changed, 977 insertions(+), 1040 deletions(-) rename src/data/template/Microsoft.Authorization/{policyDefinitions.template.jq => locks.template.jq} (100%) delete mode 100644 src/data/template/Microsoft.Authorization/policyAssignments.template.jq delete mode 100644 src/data/template/Microsoft.Authorization/policySetDefinitions.template.jq delete mode 100644 src/data/template/Microsoft.Authorization/roleAssignments.template.jq delete mode 100644 src/data/template/Microsoft.Authorization/roleDefinitions.template.jq delete mode 100644 src/data/template/Microsoft.KeyVault/vaults.jq delete mode 100644 src/data/template/Microsoft.KeyVault/vaults.template.jq create mode 100644 src/data/template/Microsoft.Keyvault/vaults.jq delete mode 100644 src/data/template/Microsoft.Network/virtualNetworks.jq delete mode 100644 src/data/template/Microsoft.Network/virtualWans.jq create mode 100644 src/data/template/Microsoft.Network/virtualnetworks.jq create mode 100644 src/data/template/Microsoft.Network/virtualwans.jq rename src/data/template/Microsoft.Resources/{resourceGroups.template.jq => resourcegroups.template.jq} (77%) delete mode 100644 src/data/template/Microsoft.Storage/storageAccounts.jq create mode 100644 src/data/template/Microsoft.Storage/storageaccounts.jq delete mode 100644 src/internal/classes/AzOpsRoleAssignment.ps1 delete mode 100644 src/internal/classes/AzOpsRoleDefinition.ps1 rename src/internal/functions/{Get-AzOpsManagementGroups.ps1 => Get-AzOpsManagementGroup.ps1} (91%) create mode 100644 src/internal/functions/Get-AzOpsNestedSubscription.ps1 create mode 100644 src/internal/functions/Invoke-AzOpsRestMethod.ps1 rename src/internal/functions/{Remove-AzOpsInvalidCharacters.ps1 => Remove-AzOpsInvalidCharacter.ps1} (78%) rename src/internal/functions/{Save-AzOpsManagementGroupChildren.ps1 => Save-AzOpsManagementGroupChild.ps1} (51%) create mode 100644 src/internal/functions/Search-AzOpsAzGraph.ps1 create mode 100644 src/tests/templates/rbactest.parameters.json diff --git a/.github/workflows/wiki-sync.yml b/.github/workflows/wiki-sync.yml index 4575d23a..7863f7e5 100644 --- a/.github/workflows/wiki-sync.yml +++ b/.github/workflows/wiki-sync.yml @@ -49,5 +49,4 @@ jobs: git add . git commit -m "$github_commit_message [$GITHUB_ACTOR/${GITHUB_SHA::8}]" git push --set-upstream https://$GITHUB_TOKEN@github.com/$wiki_target_repo.git master - working-directory: ${{ env.wiki_target_repo }} - + working-directory: ${{ env.wiki_target_repo }} \ No newline at end of file diff --git a/README.md b/README.md index 853be4f0..c2667373 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,15 @@ For tutorials, samples and quick starts, visit the [AzOps Accelerator](https://g - [Az.Accounts](https://github.com/azure/azure-powershell) - [Az.Billing](https://github.com/azure/azure-powershell) +- [Az.ResourceGraph](https://github.com/azure/azure-powershell) - [Az.Resources](https://github.com/azure/azure-powershell) - [PSFramework](https://github.com/PowershellFrameworkCollective/psframework) ## Need help? For introduction guidance, visit the [GitHub Wiki](https://github.com/azure/azops/wiki) -For reference documentation, visit the [Enterprise-Scale](https://github.com/azure/enterprise-scale) For tutorials, samples and quick starts, go to [AzOps Accelerator](https://github.com/azure/azops-accelerator) For information on contributing to the module, visit the [Contributing Guide](https://github.com/Azure/azops/wiki/debug) -For information on migrating to the new version, visit the [Migration Guide](https://github.com/azure/azops/wiki/migration) File an issue via [GitHub Issues](https://github.com/azure/azops/issues/new/choose) ## Output diff --git a/docs/wiki/Settings.md b/docs/wiki/Settings.md index 641e85d5..3b910592 100644 --- a/docs/wiki/Settings.md +++ b/docs/wiki/Settings.md @@ -16,26 +16,25 @@ The following configuration values can be modified within the `settings.json` fi | 04 | EnrollmentAccountPrincipalName | Default enrollment account for Subscription creation | `"Core.EnrollmentAccountPrincipalName": ""` | | 05 | ExcludedSubOffer | Exclude specific Subscription offer types from pull | `"Core.ExcludedSubOffer": ["AzurePass_2014-09-01","FreeTrial_2014-09-01","AAD_2015-09-01"]` | | 06 | ExcludedSubState | Exclude specific states of Subscription from pull | `"Core.ExcludedSubState": ["Disabled","Deleted","Warned","Expired"]` | -| 07 | ExportRawTemplate | Export generic templates without embedding them in the parameter block | `"Core.ExportRawTemplate": true` | -| 08 | IgnoreContextCheck | Skip Azure PowerShell context validation. *Not recommended to change* | `"Core.IgnoreContextCheck": false` | -| 09 | IncludeResourcesInResourceGroup | Discover only resources in these resource groups | `"Core.IncludeResourcesInResourceGroup": ["rg1","rg2"]` | -| 10 | IncludeResourceType | Discover only specific resource types [Resource Types](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-providers-and-types) (only targets Resource Group scoped resources) | `"Core.IncludeResourceType": ["Microsoft.Network/privateDnsZones","Microsoft.Network/firewallPolicies"]` | -| 11 | InvalidateCache | Invalidate cached Subscriptions and Management Groups and do a full discovery. *Not recommended to change* | `"Core.InvalidateCache": false` | -| 12 | OfferType | Default offer type for Subscription creation | `"Core.OfferType": "MS-AZR-0017P"` | -| 13 | PartialMgDiscoveryRoot | Generate folder hierachy for specific Management Groups | `"Core.PartialMgDiscoveryRoot": []` | -| 14 | SkipPim | Do not include Privileged Identity Management resources in pull | `"Core.SkipPim": true` | -| 15 | SkipLock | Do not include ResourceLock resources in pull | `"Core.SkipLock": true` | -| 16 | SkipPolicy | Do not include Azure Policy state in pull | `"Core.SkipPolicy": false` | -| 17 | SkipResource | Do not include Resources within Resource Groups | `"Core.SkipResource": false` | -| 18 | SkipChildResource | Do not include Azure child resources | `"Core.SkipChildResource": false` | -| 19 | SkipResourceGroup | Do not include Resource Groups in pull | `"Core.SkipResourceGroup": false` | -| 20 | SkipResourceType | Skip specific [Resource Types](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-providers-and-types) (only targets Resource Group scoped resources) | `"Core.SkipResourceType": ["Microsoft.VSOnline/plans"]` | -| 21 | SkipRole | Do not include Role types in pull | `"Core.SkipRole": false` | -| 22 | State | Folder to store AzOpsState artefact, defaults to `root` | `"Core.State: "/root"` | -| 23 | SubscriptionsToIncludeResourceGroups | Filter which Subscriptions should include Resource Groups in pull | `"Core.SubscriptionsToIncludeResourceGroups": ["*"]` | -| 24 | TemplateParameterFileSuffix | Default template file suffix. *Not recommended to change* | `"Core.TemplateParameterFileSuffix": ".json"` | -| 25 | ThrottleLimit | Default template file suffix. *Not recommended to change* | `"Core.ThrottleLimit": 10` | -| 26 | WhatifExcludedChangeTypes | Exclude specific change types from WhatIf operations | `"Core.WhatifExcludedChangeTypes": ["NoChange","Ignore"]` | +| 07 | IgnoreContextCheck | Skip Azure PowerShell context validation. *Not recommended to change* | `"Core.IgnoreContextCheck": false` | +| 08 | IncludeResourcesInResourceGroup | Discover only resources in these resource groups | `"Core.IncludeResourcesInResourceGroup": ["rg1","rg2"]` | +| 09 | IncludeResourceType | Discover only specific resource types [Resource Types](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-providers-and-types) (only targets Resource Group scoped resources) | `"Core.IncludeResourceType": ["Microsoft.Network/privateDnsZones","Microsoft.Network/firewallPolicies"]` | +| 10 | InvalidateCache | Invalidate cached Subscriptions and Management Groups and do a full discovery. *Not recommended to change* | `"Core.InvalidateCache": false` | +| 11 | OfferType | Default offer type for Subscription creation | `"Core.OfferType": "MS-AZR-0017P"` | +| 12 | PartialMgDiscoveryRoot | Generate folder hierachy for specific Management Groups | `"Core.PartialMgDiscoveryRoot": []` | +| 13 | SkipPim | Do not include Privileged Identity Management resources in pull | `"Core.SkipPim": true` | +| 14 | SkipLock | Do not include ResourceLock resources in pull | `"Core.SkipLock": true` | +| 15 | SkipPolicy | Do not include Azure Policy state in pull | `"Core.SkipPolicy": false` | +| 16 | SkipResource | Do not include Resources within Resource Groups | `"Core.SkipResource": false` | +| 17 | SkipChildResource | Do not include Azure child resources | `"Core.SkipChildResource": false` | +| 18 | SkipResourceGroup | Do not include Resource Groups in pull | `"Core.SkipResourceGroup": false` | +| 19 | SkipResourceType | Skip specific [Resource Types](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-providers-and-types) (only targets Resource Group scoped resources) | `"Core.SkipResourceType": ["Microsoft.VSOnline/plans"]` | +| 20 | SkipRole | Do not include Role types in pull | `"Core.SkipRole": false` | +| 21 | State | Folder to store AzOpsState artefact, defaults to `root` | `"Core.State: "/root"` | +| 22 | SubscriptionsToIncludeResourceGroups | Filter which Subscriptions should include Resource Groups in pull | `"Core.SubscriptionsToIncludeResourceGroups": ["*"]` | +| 23 | TemplateParameterFileSuffix | Default template file suffix. *Not recommended to change* | `"Core.TemplateParameterFileSuffix": ".json"` | +| 24 | ThrottleLimit | Default template file suffix. *Not recommended to change* | `"Core.ThrottleLimit": 10` | +| 25 | WhatifExcludedChangeTypes | Exclude specific change types from WhatIf operations | `"Core.WhatifExcludedChangeTypes": ["NoChange","Ignore"]` | ## Workflow / Pipeline Settings diff --git a/src/AzOps.psd1 b/src/AzOps.psd1 index dfdf5f61..cb3d668b 100644 --- a/src/AzOps.psd1 +++ b/src/AzOps.psd1 @@ -133,4 +133,3 @@ PrivateData = @{ # DefaultCommandPrefix = '' } - diff --git a/src/data/template/Microsoft.Authorization/policyDefinitions.template.jq b/src/data/template/Microsoft.Authorization/locks.template.jq similarity index 100% rename from src/data/template/Microsoft.Authorization/policyDefinitions.template.jq rename to src/data/template/Microsoft.Authorization/locks.template.jq diff --git a/src/data/template/Microsoft.Authorization/policyAssignments.jq b/src/data/template/Microsoft.Authorization/policyAssignments.jq index 6bc07dec..61349ed8 100644 --- a/src/data/template/Microsoft.Authorization/policyAssignments.jq +++ b/src/data/template/Microsoft.Authorization/policyAssignments.jq @@ -1 +1 @@ -del(.ResourceId, .Properties.Metadata, .ResourceGroupName, .SubscriptionId) \ No newline at end of file +del(.ResourceId, .resourceGroup, .subscriptionId, .properties.metadata.createdOn, .properties.metadata.updatedOn, .properties.metadata.createdBy, .properties.metadata.createdBy, .properties.metadata.updatedBy, .properties.metadata.assignedBy) \ No newline at end of file diff --git a/src/data/template/Microsoft.Authorization/policyAssignments.template.jq b/src/data/template/Microsoft.Authorization/policyAssignments.template.jq deleted file mode 100644 index edfea7ae..00000000 --- a/src/data/template/Microsoft.Authorization/policyAssignments.template.jq +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "AzOps" - } - }, - "parameters": {}, - "variables": {}, - "resources": [ - { - "type": .ResourceType, - "name": .Name, - "apiVersion": "0000-00-00", - "location": .Location, - "identity": .Identity, - "properties": .Properties - } - ], - "outputs": {} -} | .resources[] |= if .identity==null then del(.identity) elif .identity.UserAssignedIdentities==null then del(.identity.UserAssignedIdentities) else . end | -.resources[] |= if .identity.IdentityType !=null then .identity["Type"] = .identity.IdentityType | del(.identity.IdentityType) else . end | -.resources[] |= if .identity.UserAssignedIdentities != null then del(.identity.UserAssignedIdentities[].PrincipalId, .identity.UserAssignedIdentities[].ClientId, .identity.TenantId, .identity.PrincipalId) else . end diff --git a/src/data/template/Microsoft.Authorization/policyDefinitions.jq b/src/data/template/Microsoft.Authorization/policyDefinitions.jq index ae24cb4c..a9f183f3 100644 --- a/src/data/template/Microsoft.Authorization/policyDefinitions.jq +++ b/src/data/template/Microsoft.Authorization/policyDefinitions.jq @@ -1 +1 @@ -del(.ResourceId, .ResourceName, .PolicyDefinitionId, .SubscriptionId, .Properties.PolicyType, .Properties.Metadata.createdBy, .Properties.Metadata.createdOn, .Properties.Metadata.updatedBy, .Properties.Metadata.updatedOn) \ No newline at end of file +del(.ResourceId, .id, .tenantId, .subscriptionId, .properties.policyType, .properties.metadata.createdOn, .properties.metadata.updatedOn, .properties.metadata.createdBy, .properties.metadata.createdBy, .properties.metadata.updatedBy) \ No newline at end of file diff --git a/src/data/template/Microsoft.Authorization/policyExemptions.jq b/src/data/template/Microsoft.Authorization/policyExemptions.jq index b4433ec6..548df53f 100644 --- a/src/data/template/Microsoft.Authorization/policyExemptions.jq +++ b/src/data/template/Microsoft.Authorization/policyExemptions.jq @@ -1 +1 @@ -del(.ResourceId, .Properties.Metadata, .ResourceGroupName, .SubscriptionId, .SystemData) \ No newline at end of file +del(.ResourceId, .ResourceGroupName, .SubscriptionId, .SystemData, .Properties.CreatedOn, .Properties.UpdatedOn, .Properties.CreatedBy, .Properties.CreatedBy, .Properties.UpdatedBy) \ No newline at end of file diff --git a/src/data/template/Microsoft.Authorization/policySetDefinitions.jq b/src/data/template/Microsoft.Authorization/policySetDefinitions.jq index ae24cb4c..a9f183f3 100644 --- a/src/data/template/Microsoft.Authorization/policySetDefinitions.jq +++ b/src/data/template/Microsoft.Authorization/policySetDefinitions.jq @@ -1 +1 @@ -del(.ResourceId, .ResourceName, .PolicyDefinitionId, .SubscriptionId, .Properties.PolicyType, .Properties.Metadata.createdBy, .Properties.Metadata.createdOn, .Properties.Metadata.updatedBy, .Properties.Metadata.updatedOn) \ No newline at end of file +del(.ResourceId, .id, .tenantId, .subscriptionId, .properties.policyType, .properties.metadata.createdOn, .properties.metadata.updatedOn, .properties.metadata.createdBy, .properties.metadata.createdBy, .properties.metadata.updatedBy) \ No newline at end of file diff --git a/src/data/template/Microsoft.Authorization/policySetDefinitions.template.jq b/src/data/template/Microsoft.Authorization/policySetDefinitions.template.jq deleted file mode 100644 index c447ef21..00000000 --- a/src/data/template/Microsoft.Authorization/policySetDefinitions.template.jq +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "AzOps" - } - }, - "parameters": {}, - "variables": {}, - "resources": [ - { - "type": .ResourceType, - "name": .Name, - "apiVersion": "0000-00-00", - "properties": .Properties - } - ], - "outputs": {} -} diff --git a/src/data/template/Microsoft.Authorization/roleAssignments.jq b/src/data/template/Microsoft.Authorization/roleAssignments.jq index b2af5dff..11f8f66d 100644 --- a/src/data/template/Microsoft.Authorization/roleAssignments.jq +++ b/src/data/template/Microsoft.Authorization/roleAssignments.jq @@ -1,2 +1 @@ -del(.Id)| .Properties |= -{DisplayName, ObjectType, PrincipalId, RoleDefinitionId, RoleDefinitionName} \ No newline at end of file +del(.properties.createdOn, .properties.updatedOn, .properties.createdBy, .properties.createdBy, .properties.updatedBy) \ No newline at end of file diff --git a/src/data/template/Microsoft.Authorization/roleAssignments.template.jq b/src/data/template/Microsoft.Authorization/roleAssignments.template.jq deleted file mode 100644 index c447ef21..00000000 --- a/src/data/template/Microsoft.Authorization/roleAssignments.template.jq +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "AzOps" - } - }, - "parameters": {}, - "variables": {}, - "resources": [ - { - "type": .ResourceType, - "name": .Name, - "apiVersion": "0000-00-00", - "properties": .Properties - } - ], - "outputs": {} -} diff --git a/src/data/template/Microsoft.Authorization/roleDefinitions.jq b/src/data/template/Microsoft.Authorization/roleDefinitions.jq index 94b42539..11f8f66d 100644 --- a/src/data/template/Microsoft.Authorization/roleDefinitions.jq +++ b/src/data/template/Microsoft.Authorization/roleDefinitions.jq @@ -1 +1 @@ -del(.Id) \ No newline at end of file +del(.properties.createdOn, .properties.updatedOn, .properties.createdBy, .properties.createdBy, .properties.updatedBy) \ No newline at end of file diff --git a/src/data/template/Microsoft.Authorization/roleDefinitions.template.jq b/src/data/template/Microsoft.Authorization/roleDefinitions.template.jq deleted file mode 100644 index 42a3351d..00000000 --- a/src/data/template/Microsoft.Authorization/roleDefinitions.template.jq +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "AzOps" - } - }, - "parameters": {}, - "variables": {}, - "resources": [ - { - "type": .ResourceType, - "name": .Name, - "apiVersion": "0000-00-00", - "properties": .Properties - } - ], - "outputs": {} -} | -.resources[].properties |= . as $in | {RoleName, Description, AssignableScopes, Permissions} + $in \ No newline at end of file diff --git a/src/data/template/Microsoft.KeyVault/vaults.jq b/src/data/template/Microsoft.KeyVault/vaults.jq deleted file mode 100644 index 754ad6e5..00000000 --- a/src/data/template/Microsoft.KeyVault/vaults.jq +++ /dev/null @@ -1 +0,0 @@ -del(.Id, .ResourceId, .Identity, .Kind, .ResourceName, .ExtensionResourceName, .ParentResource, .Plan, .Properties.provisioningState, .Properties.vaultUri) \ No newline at end of file diff --git a/src/data/template/Microsoft.KeyVault/vaults.template.jq b/src/data/template/Microsoft.KeyVault/vaults.template.jq deleted file mode 100644 index c53f0567..00000000 --- a/src/data/template/Microsoft.KeyVault/vaults.template.jq +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "AzOps" - } - }, - "parameters": {}, - "variables": {}, - "resources": [ - { - "type": "Microsoft.KeyVault/vaults", - "name": .Name, - "apiVersion": "2019-09-01", - "location": .Location, - "tags": .Tags, - "properties": .Properties - } - ], - "outputs": {} -} | .resources [].properties.tenantId="[subscription().tenantId]" | -.resources[].tags |= if . != null then to_entries | sort_by(.key) | from_entries else . end \ No newline at end of file diff --git a/src/data/template/Microsoft.Keyvault/vaults.jq b/src/data/template/Microsoft.Keyvault/vaults.jq new file mode 100644 index 00000000..26d4d77e --- /dev/null +++ b/src/data/template/Microsoft.Keyvault/vaults.jq @@ -0,0 +1 @@ +del(.id, .resourceId, .identity, .kind, .resourceName, .extensionResourceName, .parentResource, .plan, .properties.provisioningState, .properties.vaultUri) \ No newline at end of file diff --git a/src/data/template/Microsoft.Network/virtualNetworks.jq b/src/data/template/Microsoft.Network/virtualNetworks.jq deleted file mode 100644 index bbf840ad..00000000 --- a/src/data/template/Microsoft.Network/virtualNetworks.jq +++ /dev/null @@ -1 +0,0 @@ -del(.. | .Id?, .resourceGuid?, .ResourceId?, .Identity?, .Kind?, .ResourceName?, .ExtensionResourceName?, .ParentResource?, .Plan?, .etag? , .provisioningState?) \ No newline at end of file diff --git a/src/data/template/Microsoft.Network/virtualWans.jq b/src/data/template/Microsoft.Network/virtualWans.jq deleted file mode 100644 index b28b4383..00000000 --- a/src/data/template/Microsoft.Network/virtualWans.jq +++ /dev/null @@ -1 +0,0 @@ -del(.Properties.provisioningState) diff --git a/src/data/template/Microsoft.Network/virtualnetworks.jq b/src/data/template/Microsoft.Network/virtualnetworks.jq new file mode 100644 index 00000000..ca4ed9df --- /dev/null +++ b/src/data/template/Microsoft.Network/virtualnetworks.jq @@ -0,0 +1 @@ +del(.. | .resourceGuid?, .resourceId?, .identity?, .kind?, .resourceName?, .extensionResourceName?, .parentResource?, .plan?, .etag? , .provisioningState?) \ No newline at end of file diff --git a/src/data/template/Microsoft.Network/virtualwans.jq b/src/data/template/Microsoft.Network/virtualwans.jq new file mode 100644 index 00000000..0e9a804f --- /dev/null +++ b/src/data/template/Microsoft.Network/virtualwans.jq @@ -0,0 +1 @@ +del(.properties.provisioningState) diff --git a/src/data/template/Microsoft.Resources/resourceGroups.template.jq b/src/data/template/Microsoft.Resources/resourcegroups.template.jq similarity index 77% rename from src/data/template/Microsoft.Resources/resourceGroups.template.jq rename to src/data/template/Microsoft.Resources/resourcegroups.template.jq index d5da2420..b5ccec4d 100644 --- a/src/data/template/Microsoft.Resources/resourceGroups.template.jq +++ b/src/data/template/Microsoft.Resources/resourcegroups.template.jq @@ -10,11 +10,11 @@ "variables": {}, "resources": [ { - "type": "Microsoft.Resources/resourceGroups", - "name": .ResourceGroupName, + "type": "microsoft.resources/resourcegroups", + "name": .name, "apiVersion": "0000-00-00", - "location": .Location, - "tags": .Tags, + "location": .location, + "tags": .tags, "properties": {} } ], diff --git a/src/data/template/Microsoft.Storage/storageAccounts.jq b/src/data/template/Microsoft.Storage/storageAccounts.jq deleted file mode 100644 index 3afcf30c..00000000 --- a/src/data/template/Microsoft.Storage/storageAccounts.jq +++ /dev/null @@ -1,9 +0,0 @@ -del(.Id, - .Properties.creationTime, - .Properties.primaryEndpoints, - .Properties.encryption.services.file.lastEnabledTime, - .Properties.encryption.services.blob.lastEnabledTime, - .Properties.provisioningState, - .Properties.primaryLocation, - .Properties.statusOfPrimary - ) \ No newline at end of file diff --git a/src/data/template/Microsoft.Storage/storageaccounts.jq b/src/data/template/Microsoft.Storage/storageaccounts.jq new file mode 100644 index 00000000..9593a45b --- /dev/null +++ b/src/data/template/Microsoft.Storage/storageaccounts.jq @@ -0,0 +1,9 @@ +del(.id, + .properties.creationTime, + .properties.primaryEndpoints, + .properties.encryption.services.file.lastEnabledTime, + .properties.encryption.services.blob.lastEnabledTime, + .properties.provisioningState, + .properties.primaryLocation, + .properties.statusOfPrimary + ) \ No newline at end of file diff --git a/src/data/template/generic.jq b/src/data/template/generic.jq index 08375f9b..25103a44 100644 --- a/src/data/template/generic.jq +++ b/src/data/template/generic.jq @@ -7,7 +7,7 @@ def matches: ) )"); -del(.Id, .Properties.provisioningState, .Properties.state, .Properties.resourceGuid) | +del(.Id, .id, .Properties.provisioningState, .properties.provisioningState, .Properties.state, .properties.state, .Properties.resourceGuid, .properties.resourceGuid) | walk(if type=="object" then with_entries(if .value|matches then empty else . end) else . end) \ No newline at end of file diff --git a/src/data/template/template.jq b/src/data/template/template.jq index dc89c8a4..50dc1e6d 100644 --- a/src/data/template/template.jq +++ b/src/data/template/template.jq @@ -10,18 +10,21 @@ "variables": {}, "resources": [ { - "type": .ResourceType, - "name": .Name, - "sku": .Sku, - "kind": .Kind, + "type": .type, + "name": .name, + "sku": .sku, + "kind": .kind, "apiVersion": "0000-00-00", - "location": .Location, - "tags": .Tags, - "properties": .Properties + "location": .location, + "identity": .identity, + "tags": .tags, + "properties": .properties, + "zones": .zones } ], "outputs": {} } | .resources[].tags |= if . != null then to_entries | sort_by(.key) | from_entries else . end -| del(.resources[].sku | nulls) -| del(.resources[].kind | nulls) +| del(.. | select(. == null)) +| del(.. | select(. == "")) +| del (.resources[].properties.extended) \ No newline at end of file diff --git a/src/data/template/template.parameters.jq b/src/data/template/template.parameters.jq index ccc6c39c..d8459097 100644 --- a/src/data/template/template.parameters.jq +++ b/src/data/template/template.parameters.jq @@ -6,4 +6,6 @@ "value": . } } -} \ No newline at end of file +} +| del(.. | select(. == null)) +| del(.. | select(. == "")) \ No newline at end of file diff --git a/src/functions/Initialize-AzOpsEnvironment.ps1 b/src/functions/Initialize-AzOpsEnvironment.ps1 index bdfd60d4..3cee6b05 100644 --- a/src/functions/Initialize-AzOpsEnvironment.ps1 +++ b/src/functions/Initialize-AzOpsEnvironment.ps1 @@ -122,8 +122,8 @@ #region Management Group Resolution Write-PSFMessage -Level Important -String 'Initialize-AzOpsEnvironment.ManagementGroup.Resolution' -StringValues $managementGroups.Count $tempResolved = foreach ($mgmtGroup in $managementGroups) { - Write-PSFMessage -Level Important -String 'Initialize-AzOpsEnvironment.ManagementGroup.Expanding' -StringValues $mgmtGroup.Name - Get-AzOpsManagementGroups -ManagementGroup $mgmtGroup.Name -PartialDiscovery:$PartialMgDiscovery + Write-PSFMessage -Level Verbose -String 'Initialize-AzOpsEnvironment.ManagementGroup.Expanding' -StringValues $mgmtGroup.Name + Get-AzOpsManagementGroup -ManagementGroup $mgmtGroup.Name -PartialDiscovery:$PartialMgDiscovery } $script:AzOpsAzManagementGroup = $tempResolved | Sort-Object -Property Id -Unique #endregion Management Group Resolution diff --git a/src/functions/Invoke-AzOpsPull.ps1 b/src/functions/Invoke-AzOpsPull.ps1 index 10564897..d06517f1 100644 --- a/src/functions/Invoke-AzOpsPull.ps1 +++ b/src/functions/Invoke-AzOpsPull.ps1 @@ -23,17 +23,10 @@ Skip discovery of resource groups .PARAMETER SkipResource Skip discovery of resources inside resource groups. - .PARAMETER InvalidateCache - Invalidate cached subscriptions and Management Groups and do a full discovery. - .PARAMETER ExportRawTemplate - Export generic templates without embedding them in the parameter block. .PARAMETER Rebuild Delete all AutoGeneratedTemplateFolderPath folders inside AzOpsState directory. .PARAMETER Force Delete $script:AzOpsState directory. - .PARAMETER PartialMgDiscoveryRoot - The subset of management groups in the entire hierarchy with which to work. - Needed when lacking root access. .PARAMETER StatePath The root folder under which to write the resource json. .EXAMPLE @@ -74,21 +67,12 @@ [switch] $SkipRole = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipRole'), - [switch] - $InvalidateCache = (Get-PSFConfigValue -FullName 'AzOps.Core.InvalidateCache'), - - [switch] - $ExportRawTemplate = (Get-PSFConfigValue -FullName 'AzOps.Core.ExportRawTemplate'), - [switch] $Rebuild, [switch] $Force, - [string[]] - $PartialMgDiscoveryRoot = (Get-PSFConfigValue -FullName 'AzOps.Core.PartialMgDiscoveryRoot'), - [string] $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State'), @@ -97,25 +81,22 @@ ) begin { - #region Initialize & Prepare - Write-PSFMessage -Level Important -String 'Invoke-AzOpsPull.Initialization.Starting' - if ((-not $SkipRole) -or (-not $SkipPim)) { + #region Prepare + if (-not $SkipPim) { try { Write-PSFMessage -Level Verbose -String 'Invoke-AzOpsPull.Validating.UserRole' $null = Get-AzADUser -First 1 -ErrorAction Stop - Write-PSFMessage -Level Important -String 'Invoke-AzOpsPull.Validating.UserRole.Success' - if (-not $SkipPim) { - Write-PSFMessage -Level Verbose -String 'Invoke-AzOpsPull.Validating.AADP2' - $servicePlanName = "AAD_PREMIUM_P2" - $subscribedSkus = Invoke-AzRestMethod -Uri https://graph.microsoft.com/v1.0/subscribedSkus -ErrorAction Stop - $subscribedSkusValue = $subscribedSkus.Content | ConvertFrom-Json -Depth 100 | Select-Object value - if ($servicePlanName -in $subscribedSkusValue.value.servicePlans.servicePlanName) { - Write-PSFMessage -Level Important -String 'Invoke-AzOpsPull.Validating.AADP2.Success' - } - else { - Write-PSFMessage -Level Warning -String 'Invoke-AzOpsPull.Validating.AADP2.Failed' - $SkipPim = $true - } + Write-PSFMessage -Level Verbose -String 'Invoke-AzOpsPull.Validating.UserRole.Success' + Write-PSFMessage -Level Verbose -String 'Invoke-AzOpsPull.Validating.AADP2' + $servicePlanName = "AAD_PREMIUM_P2" + $subscribedSkus = Invoke-AzRestMethod -Uri https://graph.microsoft.com/v1.0/subscribedSkus -ErrorAction Stop + $subscribedSkusValue = $subscribedSkus.Content | ConvertFrom-Json -Depth 100 | Select-Object value + if ($servicePlanName -in $subscribedSkusValue.value.servicePlans.servicePlanName) { + Write-PSFMessage -Level Verbose -String 'Invoke-AzOpsPull.Validating.AADP2.Success' + } + else { + Write-PSFMessage -Level Warning -String 'Invoke-AzOpsPull.Validating.AADP2.Failed' + $SkipPim = $true } } catch { @@ -134,9 +115,6 @@ $IncludeResourceType = $IncludeResourceType | Where-Object { $_ -notin $resourceTypeDiff.InputObject } } - $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Inherit -Include InvalidateCache, PartialMgDiscovery, PartialMgDiscoveryRoot - Initialize-AzOpsEnvironment @parameters - Assert-AzOpsInitialization -Cmdlet $PSCmdlet -StatePath $StatePath $tenantId = (Get-AzContext).Tenant.Id @@ -177,23 +155,22 @@ $rootScope = $script:AzOpsPartialRoot.id | Sort-Object -Unique } + # Parameters + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Inherit -Include IncludeResourcesInResourceGroup, IncludeResourceType, SkipPim, SkipLock, SkipPolicy, SkipRole, SkipResourceGroup, SkipChildResource, SkipResource, SkipResourceType, StatePath + + Write-PSFMessage -Level Important -String 'Invoke-AzOpsPull.Building.State' -StringValues $StatePath if ($rootScope -and $script:AzOpsAzManagementGroup) { foreach ($root in $rootScope) { - # Create AzOpsState Structure recursively - Save-AzOpsManagementGroupChildren -Scope $root -StatePath $StatePath - - # Discover Resource at scope recursively - $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Inherit -Include IncludeResourcesInResourceGroup, IncludeResourceType, SkipPim, SkipLock, SkipPolicy, SkipRole, SkipResourceGroup, SkipChildResource, SkipResource, SkipResourceType, ExportRawTemplate, StatePath + # Create AzOpsState structure recursively + Save-AzOpsManagementGroupChild -Scope $root -StatePath $StatePath Get-AzOpsResourceDefinition -Scope $root @parameters } } else { # If no management groups are found, iterate through each subscription foreach ($subscription in $script:AzOpsSubscriptions) { - $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Inherit -Include IncludeResourcesInResourceGroup, IncludeResourceType, SkipPim, SkipLock, SkipPolicy, SkipRole, SkipResourceGroup, SkipChildResource, SkipResource, SkipResourceType, ExportRawTemplate, StatePath Get-AzOpsResourceDefinition -Scope $subscription.id @parameters } - } #endregion Root Scopes } diff --git a/src/functions/Invoke-AzOpsPush.ps1 b/src/functions/Invoke-AzOpsPush.ps1 index e310966d..1c1808f3 100644 --- a/src/functions/Invoke-AzOpsPush.ps1 +++ b/src/functions/Invoke-AzOpsPush.ps1 @@ -117,10 +117,14 @@ $templateParameterFileHashtable = Get-Content -Path $fileItem.FullName | ConvertFrom-Json -AsHashtable $effectiveResourceType = $null if ($templateParameterFileHashtable.Keys -contains "`$schema") { - if ($templateParameterFileHashtable.parameters.input.value.Keys -contains "Type") { + if ($templateParameterFileHashtable.parameters.input.value.Keys -ccontains "Type") { # ManagementGroup and Subscription $effectiveResourceType = $templateParameterFileHashtable.parameters.input.value.Type } + elseif ($templateParameterFileHashtable.parameters.input.value.Keys -ccontains "type") { + # ManagementGroup and Subscription + $effectiveResourceType = $templateParameterFileHashtable.parameters.input.value.type + } elseif ($templateParameterFileHashtable.parameters.input.value.Keys -contains "ResourceType") { # Resource $effectiveResourceType = $templateParameterFileHashtable.parameters.input.value.ResourceType @@ -171,6 +175,7 @@ process { if (-not $ChangeSet) { return } + Assert-AzOpsInitialization -Cmdlet $PSCmdlet -StatePath $StatePath #Supported resource types for deletion $DeletionSupportedResourceType = (Get-PSFConfigValue -FullName 'AzOps.Core.DeletionSupportedResourceType') #region Categorize Input @@ -194,12 +199,14 @@ if ($deleteSet -and -not $CustomSortOrder) { $deleteSet = $deleteSet | Sort-Object } if ($addModifySet -and -not $CustomSortOrder) { $addModifySet = $addModifySet | Sort-Object } - Write-PSFMessage -Level Important @common -String 'Invoke-AzOpsPush.Change.AddModify' - foreach ($item in $addModifySet) { - Write-PSFMessage -Level Important @common -String 'Invoke-AzOpsPush.Change.AddModify.File' -StringValues $item + if ($addModifySet) { + Write-PSFMessage -Level Important @common -String 'Invoke-AzOpsPush.Change.AddModify' + foreach ($item in $addModifySet) { + Write-PSFMessage -Level Important @common -String 'Invoke-AzOpsPush.Change.AddModify.File' -StringValues $item + } } - Write-PSFMessage -Level Important @common -String 'Invoke-AzOpsPush.Change.Delete' if ($DeleteSetContents -and $deleteSet) { + Write-PSFMessage -Level Important @common -String 'Invoke-AzOpsPush.Change.Delete' $DeleteSetContents = $DeleteSetContents -join "" -split "-- " | Where-Object { $_ } foreach ($item in $deleteSet) { Write-PSFMessage -Level Important @common -String 'Invoke-AzOpsPush.Change.Delete.File' -StringValues $item @@ -273,7 +280,7 @@ $templateContent = Get-Content $deletion | ConvertFrom-Json -AsHashtable $schemavalue = '$schema' - if ($templateContent.$schemavalue -like "*deploymentParameters.json#" -and (-not($templateContent.parameters.input.value.ResourceType -in $DeletionSupportedResourceType))) { + if ($templateContent.$schemavalue -like "*deploymentParameters.json#" -and (-not($templateContent.parameters.input.value.type -in $DeletionSupportedResourceType))) { Write-PSFMessage -Level Warning -String 'Remove-AzOpsDeployment.SkipUnsupportedResource' -StringValues $deletion -Target $scopeObject continue } diff --git a/src/internal/classes/AzOpsRoleAssignment.ps1 b/src/internal/classes/AzOpsRoleAssignment.ps1 deleted file mode 100644 index 915b5edf..00000000 --- a/src/internal/classes/AzOpsRoleAssignment.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -class AzOpsRoleAssignment { - [string]$ResourceType - [string]$Name - [string]$Id - [hashtable]$Properties - - AzOpsRoleAssignment($Properties) { - $this.Properties = [ordered]@{ - DisplayName = $Properties.DisplayName - PrincipalId = $Properties.ObjectId - RoleDefinitionName = $Properties.RoleDefinitionName - ObjectType = $Properties.ObjectType - RoleDefinitionId = '/providers/Microsoft.Authorization/RoleDefinitions/{0}' -f $Properties.RoleDefinitionId - } - $this.Id = $Properties.RoleAssignmentId - $this.Name = ($Properties.RoleAssignmentId -split "/")[-1] - $this.ResourceType = "Microsoft.Authorization/roleAssignments" - } -} \ No newline at end of file diff --git a/src/internal/classes/AzOpsRoleDefinition.ps1 b/src/internal/classes/AzOpsRoleDefinition.ps1 deleted file mode 100644 index d6a64b4c..00000000 --- a/src/internal/classes/AzOpsRoleDefinition.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -class AzOpsRoleDefinition { - [string]$ResourceType - [string]$Name - [string]$Id - [hashtable]$Properties - AzOpsRoleDefinition($Properties) { - # Removing the Trailing slash to ensure that '/' is not appended twice when adding '/providers/xxx'. - # Example: '/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/' is a valid assignment scope. - $this.Id = '/' + $Properties.AssignableScopes[0].Trim('/') + '/providers/Microsoft.Authorization/roleDefinitions/' + $Properties.Id - $this.Name = $Properties.Id - $this.Properties = [ordered]@{ - AssignableScopes = @($Properties.AssignableScopes) - Description = $Properties.Description - Permissions = @( - [ordered]@{ - Actions = @($Properties.Actions) - DataActions = @($Properties.DataActions) - NotActions = @($Properties.NotActions) - NotDataActions = @($Properties.NotDataActions) - } - ) - RoleName = $Properties.Name - } - $this.ResourceType = "Microsoft.Authorization/roleDefinitions" - } -} \ No newline at end of file diff --git a/src/internal/classes/AzOpsScope.ps1 b/src/internal/classes/AzOpsScope.ps1 index aa1b166c..b5c99586 100644 --- a/src/internal/classes/AzOpsScope.ps1 +++ b/src/internal/classes/AzOpsScope.ps1 @@ -95,7 +95,7 @@ $this.StateRoot = $StateRoot $this.ChildResource = $ChildResource.resourceProvider + '-' + $ChildResource.resourceName # Check and update generated name for invalid filesystem characters and exceeding maximum length - $this.ChildResource = $this.ChildResource | Remove-AzOpsInvalidCharacters | Set-AzOpsStringLength + $this.ChildResource = $this.ChildResource | Remove-AzOpsInvalidCharacter | Set-AzOpsStringLength Write-PSFMessage -Level Verbose -String 'AzOpsScope.ChildResource.InitializeMemberVariables' -StringValues $ChildResource.ResourceProvider, $ChildResource.ResourceName, $scope -FunctionName AzOpsScope -ModuleName AzOps $this.InitializeMemberVariables($Scope) } @@ -186,7 +186,7 @@ $this.InitializeMemberVariables($resourcePath.parameters.input.value.Id) break } - { $_.parameters.input.value.Keys -contains "Type" } { + { $_.parameters.input.value.Keys -ccontains "Type" } { # Parameter Files - Determine Resource Type and Name (Management group) # Management group resource id do contain '/provider' Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.Type' -StringValues ("$($resourcePath.parameters.input.value.Type)/$($resourcePath.parameters.input.value.Name)") -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps @@ -202,6 +202,15 @@ $this.InitializeMemberVariables("$($currentScope.scope)/providers/$($resourcePath.parameters.input.value.ResourceType)/$($resourcePath.parameters.input.value.Name)") break } + { $_.parameters.input.value.Keys -ccontains "type" } { + # Parameter Files - Determine resource type and name (Any ResourceType except management group) + Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.ResourceType' -StringValues ($resourcePath.parameters.input.value.type) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps + $currentScope = New-AzOpsScope -Path ($Path.Directory) + + # Creating Resource Id based on current scope, resource type and name of the resource + $this.InitializeMemberVariables("$($currentScope.scope)/providers/$($resourcePath.parameters.input.value.type)/$($resourcePath.parameters.input.value.name)") + break + } { $_.resources -and $_.resources[0].type -eq 'Microsoft.Management/managementGroups' } { # Template - Management Group @@ -274,18 +283,13 @@ $this.ResourceGroup = $this.GetResourceGroup() $this.ResourceProvider = $this.IsResourceProvider() $this.Resource = $this.GetResource() - if (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) { - $this.StatePath = $this.GetAzOpsResourcePath() + ".json" + if ( (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix') -notcontains 'parameters.json' -and + ("$($this.ResourceProvider)/$($this.Resource)" -in 'Microsoft.Authorization/policyDefinitions', 'Microsoft.Authorization/policySetDefinitions') + ) { + $this.StatePath = ($this.GetAzOpsResourcePath() + '.parameters' + (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')) } else { - if ( (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix') -notcontains 'parameters.json' -and - ("$($this.ResourceProvider)/$($this.Resource)" -in 'Microsoft.Authorization/policyDefinitions', 'Microsoft.Authorization/policySetDefinitions') - ) { - $this.StatePath = ($this.GetAzOpsResourcePath() + '.parameters' + (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')) - } - else { - $this.StatePath = ($this.GetAzOpsResourcePath() + (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')) - } + $this.StatePath = ($this.GetAzOpsResourcePath() + (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')) } } elseif ($this.IsResourceGroup()) { @@ -301,9 +305,6 @@ if ($this.ChildResource -and (-not(Get-PSFConfigValue -FullName AzOps.Core.SkipChildResource))) { $this.StatePath = (Join-Path $this.GetAzOpsResourceGroupPath() -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\$($this.ChildResource).json").ToLower()) } - elseif (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) { - $this.StatePath = (Join-Path $this.GetAzOpsResourceGroupPath() -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.resources_resourcegroups-$($this.ResourceGroup).json").ToLower() ) - } else { $this.StatePath = (Join-Path $this.GetAzOpsResourceGroupPath() -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.resources_resourcegroups-$($this.ResourceGroup)" + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')).ToLower()) } @@ -319,12 +320,7 @@ $this.ManagementGroup = $this.GetManagementGroup() $this.ManagementGroupDisplayName = $this.GetManagementGroupName() } - if (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) { - $this.StatePath = (Join-Path $this.GetAzOpsSubscriptionPath() -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.subscription_subscriptions-$($this.Subscription).json").ToLower()) - } - else { - $this.StatePath = (Join-Path $this.GetAzOpsSubscriptionPath() -ChildPath (("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.subscription_subscriptions-$($this.Subscription)" + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'))).ToLower()) - } + $this.StatePath = (Join-Path $this.GetAzOpsSubscriptionPath() -ChildPath (("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.subscription_subscriptions-$($this.Subscription)" + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'))).ToLower()) } elseif ($this.IsManagementGroup()) { @@ -334,12 +330,7 @@ $this.Name = $this.GetManagementGroup() $this.ManagementGroup = ($this.GetManagementGroup()).Trim() $this.ManagementGroupDisplayName = if($this.Name) { ($script:AzOpsAzManagementGroup | Where-Object Name -eq $this.Name).DisplayName } - if (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) { - $this.StatePath = (Join-Path $this.GetAzOpsManagementGroupPath($this.ManagementGroup) -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.management_managementgroups-$($this.ManagementGroup).json").ToLower()) - } - else { - $this.StatePath = (Join-Path $this.GetAzOpsManagementGroupPath($this.ManagementGroup) -ChildPath (("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.management_managementgroups-$($this.ManagementGroup)" + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'))).ToLower()) - } + $this.StatePath = (Join-Path $this.GetAzOpsManagementGroupPath($this.ManagementGroup) -ChildPath (("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.management_managementgroups-$($this.ManagementGroup)" + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'))).ToLower()) } elseif ($this.IsRoot()) { $this.Type = "root" @@ -443,11 +434,11 @@ $parentObject = $script:AzOpsAzManagementGroup | Where-Object Name -eq $parentMgName if ($groupObject.parentId -and $parentObject) { $parentPath = $this.GetAzOpsManagementGroupPath($parentMgName) - $childPath = "{0} ({1})" -f $groupObject.DisplayName, $groupObject.Name | Remove-AzOpsInvalidCharacters + $childPath = "{0} ({1})" -f $groupObject.DisplayName, $groupObject.Name | Remove-AzOpsInvalidCharacter return Join-Path $parentPath -ChildPath ($childPath.ToLower()) } else { - $childPath = "{0} ({1})" -f $groupObject.DisplayName, $groupObject.Name | Remove-AzOpsInvalidCharacters + $childPath = "{0} ({1})" -f $groupObject.DisplayName, $groupObject.Name | Remove-AzOpsInvalidCharacter return Join-Path $this.StateRoot -ChildPath ($childPath.ToLower()) } } @@ -485,7 +476,7 @@ return $null } [string] GetAzOpsSubscriptionPath() { - $childpath = "{0} ({1})" -f $this.SubscriptionDisplayName, $this.Subscription | Remove-AzOpsInvalidCharacters + $childpath = "{0} ({1})" -f $this.SubscriptionDisplayName, $this.Subscription | Remove-AzOpsInvalidCharacter if ($script:AzOpsAzManagementGroup) { return (Join-Path $this.GetAzOpsManagementGroupPath($this.ManagementGroup) -ChildPath ($childpath).ToLower()) } @@ -494,7 +485,7 @@ } } [string] GetAzOpsResourceGroupPath() { - $childpath = $this.ResourceGroup | Remove-AzOpsInvalidCharacters + $childpath = $this.ResourceGroup | Remove-AzOpsInvalidCharacter return (Join-Path $this.GetAzOpsSubscriptionPath() -ChildPath ($childpath).ToLower()) } [string] GetSubscription() { @@ -548,7 +539,7 @@ [string] GetAzOpsResourcePath() { Write-PSFMessage -Level Debug -String 'AzOpsScope.GetAzOpsResourcePath.Retrieving' -StringValues $this.Scope -FunctionName AzOpsScope -ModuleName AzOps - $childpath = $this.Name | Remove-AzOpsInvalidCharacters + $childpath = $this.Name | Remove-AzOpsInvalidCharacter if ($this.Scope -match $this.regex_resourceGroupResource) { $rgpath = $this.GetAzOpsResourceGroupPath() return (Join-Path (Join-Path $rgpath -ChildPath "$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')".ToLower()) -ChildPath ($this.ResourceProvider + "_" + $this.Resource + "-" + $childpath).ToLower()) diff --git a/src/internal/configurations/Core.ps1 b/src/internal/configurations/Core.ps1 index 40263c35..87f0dac3 100644 --- a/src/internal/configurations/Core.ps1 +++ b/src/internal/configurations/Core.ps1 @@ -5,7 +5,6 @@ Set-PSFConfig -Module AzOps -Name Core.DefaultDeploymentRegion -Value northeurop Set-PSFConfig -Module AzOps -Name Core.EnrollmentAccountPrincipalName -Value '' -Initialize -Validation stringorempty -Description '-' Set-PSFConfig -Module AzOps -Name Core.ExcludedSubOffer -Value 'AzurePass_2014-09-01', 'FreeTrial_2014-09-01', 'AAD_2015-09-01' -Initialize -Validation stringarray -Description 'Excluded QuotaID' Set-PSFConfig -Module AzOps -Name Core.ExcludedSubState -Value 'Disabled', 'Deleted', 'Warned', 'Expired' -Initialize -Validation stringarray -Description 'Excluded subscription states' -Set-PSFConfig -Module AzOps -Name Core.ExportRawTemplate -Value $false -Initialize -Validation bool -Description '-' Set-PSFConfig -Module AzOps -Name Core.IgnoreContextCheck -Value $false -Initialize -Validation bool -Description 'If set to $true, skip AAD tenant validation == 1' Set-PSFConfig -Module AzOps -Name Core.InvalidateCache -Value $true -Initialize -Validation bool -Description 'Invalidates cache and ensures that Management Groups and Subscriptions are re-discovered' Set-PSFConfig -Module AzOps -Name Core.JqTemplatePath -Value "$script:ModuleRoot\data\template" -Initialize -Validation string -Description 'default path to search for jq template' diff --git a/src/internal/functions/ConvertTo-AzOpsState.ps1 b/src/internal/functions/ConvertTo-AzOpsState.ps1 index aa61d62b..675b34aa 100644 --- a/src/internal/functions/ConvertTo-AzOpsState.ps1 +++ b/src/internal/functions/ConvertTo-AzOpsState.ps1 @@ -15,8 +15,6 @@ Used if to return object in pipeline instead of exporting file .PARAMETER ChildResource The ChildResource contains details of the child resource - .PARAMETER ExportRawTemplate - Used in cases you want to return the template without the custom parameters json schema .PARAMETER StatePath The root path to where the entire state is being built in. .EXAMPLE @@ -42,7 +40,6 @@ [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias('MG', 'Role', 'Assignment', 'CustomObject', 'ResourceGroup')] $Resource, [string] @@ -54,9 +51,6 @@ [hashtable] $ChildResource, - [switch] - $ExportRawTemplate, - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string] $StatePath, @@ -158,6 +152,16 @@ Write-PSFMessage -Level Verbose -String 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -StringValues "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState' break } + { $_.type } { + if ( $_.type -eq 'Microsoft.Resources/subscriptions/resourceGroups') { + $resourceType = 'Microsoft.Resources/resourceGroups' + } + else { + $resourceType = $_.type + } + Write-PSFMessage -Level Verbose -String 'ConvertTo-AzOpsState.ObjectType.Resolved.ResourceType' -StringValues $resourceType -FunctionName 'ConvertTo-AzOpsState' + break + } Default { Write-PSFMessage -Level Warning -String 'ConvertTo-AzOpsState.ObjectType.Resolved.Generic' -StringValues "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState' break @@ -204,7 +208,7 @@ (Join-Path $JqTemplatePath -ChildPath "template.parameters.jq") Write-PSFMessage -Level Verbose -String 'ConvertTo-AzOpsState.Jq.Template' -StringValues $jqJsonTemplate -FunctionName 'ConvertTo-AzOpsState' - $object = ($object | ConvertTo-Json -Depth 100 -EnumsAsStrings | jq -r -f $jqJsonTemplate | ConvertFrom-Json) + $object = ($object | ConvertTo-Json -Depth 100 -EnumsAsStrings | jq -r '--sort-keys' | jq -r -f $jqJsonTemplate | ConvertFrom-Json) #endregion } else { diff --git a/src/internal/functions/Get-AzOpsManagementGroups.ps1 b/src/internal/functions/Get-AzOpsManagementGroup.ps1 similarity index 91% rename from src/internal/functions/Get-AzOpsManagementGroups.ps1 rename to src/internal/functions/Get-AzOpsManagementGroup.ps1 index bd77eb12..b8eea91e 100644 --- a/src/internal/functions/Get-AzOpsManagementGroups.ps1 +++ b/src/internal/functions/Get-AzOpsManagementGroup.ps1 @@ -1,4 +1,4 @@ -function Get-AzOpsManagementGroups { +function Get-AzOpsManagementGroup { <# .SYNOPSIS @@ -11,7 +11,7 @@ .PARAMETER PartialDiscovery Whether to recursively grab all Management Groups and add them to the partial root cache .EXAMPLE - Get-AzOpsManagementGroups -ManagementGroup Tailspin + Get-AzOpsManagementGroup -ManagementGroup Tailspin Id : /providers/Microsoft.Management/managementGroups/Tailspin Type : /providers/Microsoft.Management/managementGroups Name : Tailspin @@ -46,7 +46,7 @@ } if ($groupObject.Children) { $groupObject.Children | Where-Object Type -eq "Microsoft.Management/managementGroups" | Foreach-Object -Process { - Get-AzOpsManagementGroups -ManagementGroup $_.Name -PartialDiscovery:$PartialDiscovery + Get-AzOpsManagementGroup -ManagementGroup $_.Name -PartialDiscovery:$PartialDiscovery } } } diff --git a/src/internal/functions/Get-AzOpsNestedSubscription.ps1 b/src/internal/functions/Get-AzOpsNestedSubscription.ps1 new file mode 100644 index 00000000..76b31133 --- /dev/null +++ b/src/internal/functions/Get-AzOpsNestedSubscription.ps1 @@ -0,0 +1,40 @@ +function Get-AzOpsNestedSubscription { + <# + .SYNOPSIS + Create a list of subscriptionId's nested at ManagementGroup Scope + .PARAMETER Scope + ManagementGroup Name + .EXAMPLE + > Get-AzOpsNestedSubscription -Scope 5663f39e-feb1-4303-a1f9-cf20b702de61 + Discover subscriptions at Management Group scope and below + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string] + $Scope + ) + + process { + $children = ($script:AzOpsAzManagementGroup | Where-Object {$_.Name -eq $Scope}).Children + if ($children) { + $subscriptionIds = @() + foreach ($child in $children) { + if (($child.Type -eq '/subscriptions') -and ($script:AzOpsSubscriptions.id -contains $child.Id)) { + $subscriptionIds += [PSCustomObject] @{ + Name = $child.DisplayName + Id = $child.Name + Type = $child.Type + } + } + else { + $subscriptionIds += Get-AzOpsNestedSubscription -Scope $child.Name + } + } + if ($subscriptionIds) { + return $subscriptionIds + } + } + } +} \ No newline at end of file diff --git a/src/internal/functions/Get-AzOpsPolicy.ps1 b/src/internal/functions/Get-AzOpsPolicy.ps1 index e9ae7a55..8c8f6bef 100644 --- a/src/internal/functions/Get-AzOpsPolicy.ps1 +++ b/src/internal/functions/Get-AzOpsPolicy.ps1 @@ -1,4 +1,5 @@ function Get-AzOpsPolicy { + <# .SYNOPSIS Get policy objects from provided scope @@ -6,35 +7,48 @@ ScopeObject .PARAMETER StatePath StatePath + .PARAMETER Subscription + Complete Subscription list + .PARAMETER SubscriptionsToIncludeResourceGroups + Scoped Subscription list + .PARAMETER ResourceGroup + ResourceGroup switch indicating desired scope condition #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Object] + [object] $ScopeObject, [Parameter(Mandatory = $true)] - $StatePath + $StatePath, + [Parameter(Mandatory = $false)] + [object] + $Subscription, + [Parameter(Mandatory = $false)] + [object] + $SubscriptionsToIncludeResourceGroups, + [Parameter(Mandatory = $false)] + [switch] + $ResourceGroup ) process { - Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Policy Definitions', $scopeObject.Scope - $policyDefinitions = Get-AzOpsPolicyDefinition -ScopeObject $ScopeObject - $policyDefinitions | ConvertTo-AzOpsState -StatePath $StatePath - - # Process policyset definitions (initiatives) - Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'PolicySet Definitions', $ScopeObject.Scope - $policySetDefinitions = Get-AzOpsPolicySetDefinition -ScopeObject $ScopeObject - $policySetDefinitions | ConvertTo-AzOpsState -StatePath $StatePath + if (-not $ResourceGroup) { + # Process policy definitions + Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Policy Definitions', $scopeObject.Scope + $policyDefinitions = Get-AzOpsPolicyDefinition -ScopeObject $ScopeObject -Subscription $Subscription + $policyDefinitions | ConvertTo-AzOpsState -StatePath $StatePath + # Process policy set definitions (initiatives) + Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Policy Set Definitions', $ScopeObject.Scope + $policySetDefinitions = Get-AzOpsPolicySetDefinition -ScopeObject $ScopeObject -Subscription $Subscription + $policySetDefinitions | ConvertTo-AzOpsState -StatePath $StatePath + } # Process policy assignments Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Policy Assignments', $ScopeObject.Scope - $policyAssignments = Get-AzOpsPolicyAssignment -ScopeObject $ScopeObject + $policyAssignments = Get-AzOpsPolicyAssignment -ScopeObject $ScopeObject -Subscription $Subscription -SubscriptionsToIncludeResourceGroups $SubscriptionsToIncludeResourceGroups -ResourceGroup $ResourceGroup $policyAssignments | ConvertTo-AzOpsState -StatePath $StatePath - - # Process policy exemptions - Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Policy Exemptions', $ScopeObject.Scope - $policyExemptions = Get-AzOpsPolicyExemption -ScopeObject $ScopeObject - $policyExemptions | ConvertTo-AzOpsState -StatePath $StatePath } + } \ No newline at end of file diff --git a/src/internal/functions/Get-AzOpsPolicyAssignment.ps1 b/src/internal/functions/Get-AzOpsPolicyAssignment.ps1 index 035b1c81..7317ed08 100644 --- a/src/internal/functions/Get-AzOpsPolicyAssignment.ps1 +++ b/src/internal/functions/Get-AzOpsPolicyAssignment.ps1 @@ -7,6 +7,12 @@ Discover all custom policy assignments at the provided scope (Management Groups, subscriptions or resource groups) .PARAMETER ScopeObject The scope object representing the azure entity to retrieve policyset definitions for. + .PARAMETER Subscription + Complete Subscription list + .PARAMETER SubscriptionsToIncludeResourceGroups + Scoped Subscription list + .PARAMETER ResourceGroup + ResourceGroup switch indicating desired scope condition .EXAMPLE > Get-AzOpsPolicyAssignment -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath) Discover all custom policy assignments deployed at Management Group scope @@ -16,28 +22,47 @@ [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Object] - $ScopeObject + [object] + $ScopeObject, + [Parameter(Mandatory = $false)] + [object] + $Subscription, + [Parameter(Mandatory = $false)] + [object] + $SubscriptionsToIncludeResourceGroups, + [Parameter(Mandatory = $false)] + [bool] + $ResourceGroup ) process { - #TODO: Discuss dropping resourcegroups, as no action is taken ever - if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') { + if ($ScopeObject.Type -notin 'resourceGroups', 'subscriptions', 'managementGroups') { return } - - switch ($ScopeObject.Type) { - managementGroups { - Write-PSFMessage -Level Important -String 'Get-AzOpsPolicyAssignment.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject + if ($ScopeObject.Type -eq 'managementGroups') { + Write-PSFMessage -Level Debug -String 'Get-AzOpsPolicyAssignment.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject + if ((-not $SubscriptionsToIncludeResourceGroups) -or (-not $ResourceGroups)) { + $query = "policyresources | where type == 'microsoft.authorization/policyassignments' and resourceGroup == '' and subscriptionId == '' | order by ['id'] asc" + Search-AzOpsAzGraph -ManagementGroupName $ScopeObject.Name -Query $query -ErrorAction Stop + } + } + if ($Subscription) { + if ($SubscriptionsToIncludeResourceGroups -and $ResourceGroup) { + Write-PSFMessage -Level Debug -String 'Get-AzOpsPolicyAssignment.Subscription' -StringValues $SubscriptionsToIncludeResourceGroups.count -Target $ScopeObject + $query = "policyresources | where type == 'microsoft.authorization/policyassignments' and resourceGroup != '' | order by ['id'] asc" + Search-AzOpsAzGraph -Subscription $SubscriptionsToIncludeResourceGroups -Query $query -ErrorAction Stop } - subscriptions { - Write-PSFMessage -Level Important -String 'Get-AzOpsPolicyAssignment.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject + elseif ($ResourceGroup) { + Write-PSFMessage -Level Debug -String 'Get-AzOpsPolicyAssignment.ResourceGroup' -StringValues $Subscription.count -Target $ScopeObject + $query = "policyresources | where type == 'microsoft.authorization/policyassignments' and resourceGroup != '' | order by ['id'] asc" + Search-AzOpsAzGraph -Subscription $Subscription -Query $query -ErrorAction Stop } - resourcegroups { - Write-PSFMessage -Level Important -String 'Get-AzOpsPolicyAssignment.ResourceGroup' -StringValues $ScopeObject.ResourceGroup -Target $ScopeObject + else { + Write-PSFMessage -Level Debug -String 'Get-AzOpsPolicyAssignment.Subscription' -StringValues $Subscription.count -Target $ScopeObject + $query = "policyresources | where type == 'microsoft.authorization/policyassignments' and resourceGroup == '' | order by ['id'] asc" + Search-AzOpsAzGraph -Subscription $Subscription -Query $query -ErrorAction Stop } } - Get-AzPolicyAssignment -Scope $ScopeObject.Scope -WarningAction SilentlyContinue | Where-Object PolicyAssignmentId -match $ScopeObject.scope } } \ No newline at end of file diff --git a/src/internal/functions/Get-AzOpsPolicyDefinition.ps1 b/src/internal/functions/Get-AzOpsPolicyDefinition.ps1 index 2fb568bd..95fd62bf 100644 --- a/src/internal/functions/Get-AzOpsPolicyDefinition.ps1 +++ b/src/internal/functions/Get-AzOpsPolicyDefinition.ps1 @@ -2,11 +2,13 @@ <# .SYNOPSIS - Discover all custom policy definitions at the provided scope (Management Groups, subscriptions or resource groups) + Discover all custom policy definitions at the provided scope (Management Groups or subscriptions) .DESCRIPTION - Discover all custom policy definitions at the provided scope (Management Groups, subscriptions or resource groups) + Discover all custom policy definitions at the provided scope (Management Groups or subscriptions) .PARAMETER ScopeObject The scope object representing the azure entity to retrieve policy definitions for. + .PARAMETER Subscription + Complete Subscription list .EXAMPLE > Get-AzOpsPolicyDefinition -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath) Discover all custom policy definitions deployed at Management Group scope @@ -16,25 +18,26 @@ [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Object] - $ScopeObject + [object] + $ScopeObject, + [Parameter(Mandatory = $false)] + [object] + $Subscription ) process { - #TODO: Discuss dropping resourcegroups, as no action is taken ever - if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') { + if ($ScopeObject.Type -notin 'subscriptions', 'managementGroups') { return } - - switch ($ScopeObject.Type) { - managementGroups { - Write-PSFMessage -Level Important -String 'Get-AzOpsPolicyDefinition.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject - Get-AzPolicyDefinition -Custom -ManagementGroupName $ScopeObject.Name | Where-Object ResourceId -match $ScopeObject.Scope - } - subscriptions { - Write-PSFMessage -Level Important -String 'Get-AzOpsPolicyDefinition.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject - Get-AzPolicyDefinition -Custom -SubscriptionId $ScopeObject.Scope.Split('/')[2] | Where-Object SubscriptionId -eq $ScopeObject.Name - } + if ($ScopeObject.Type -eq 'managementGroups') { + Write-PSFMessage -Level Debug -String 'Get-AzOpsPolicyDefinition.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject + $query = "policyresources | where type == 'microsoft.authorization/policydefinitions' and properties.policyType == 'Custom' and subscriptionId == '' | order by ['id'] asc" + Search-AzOpsAzGraph -ManagementGroupName $ScopeObject.Name -Query $query -ErrorAction Stop + } + if ($Subscription) { + Write-PSFMessage -Level Debug -String 'Get-AzOpsPolicyDefinition.Subscription' -StringValues $Subscription.count -Target $ScopeObject + $query = "policyresources | where type == 'microsoft.authorization/policydefinitions' and properties.policyType == 'Custom' | order by ['id'] asc" + Search-AzOpsAzGraph -Subscription $Subscription -Query $query -ErrorAction Stop } } diff --git a/src/internal/functions/Get-AzOpsPolicyExemption.ps1 b/src/internal/functions/Get-AzOpsPolicyExemption.ps1 index a71d83a9..9007702a 100644 --- a/src/internal/functions/Get-AzOpsPolicyExemption.ps1 +++ b/src/internal/functions/Get-AzOpsPolicyExemption.ps1 @@ -21,22 +21,22 @@ ) process { - if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') { + if ($ScopeObject.Type -notin 'resourceGroups', 'subscriptions', 'managementGroups') { return } switch ($ScopeObject.Type) { managementGroups { - Write-PSFMessage -Level Important -String 'Get-AzOpsPolicyExemption.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject + Write-PSFMessage -Level Debug -String 'Get-AzOpsPolicyExemption.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject } subscriptions { - Write-PSFMessage -Level Important -String 'Get-AzOpsPolicyExemption.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject + Write-PSFMessage -Level Debug -String 'Get-AzOpsPolicyExemption.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject } resourcegroups { - Write-PSFMessage -Level Important -String 'Get-AzOpsPolicyExemption.ResourceGroup' -StringValues $ScopeObject.ResourceGroup -Target $ScopeObject + Write-PSFMessage -Level Debug -String 'Get-AzOpsPolicyExemption.ResourceGroup' -StringValues $ScopeObject.ResourceGroup -Target $ScopeObject } } - Get-AzPolicyExemption -Scope $ScopeObject.Scope -WarningAction SilentlyContinue | Where-Object ResourceId -match $ScopeObject.scope + Get-AzPolicyExemption -Scope $ScopeObject.Scope -WarningAction SilentlyContinue -ErrorAction Continue | Where-Object ResourceId -match $ScopeObject.scope -ErrorAction Continue } } \ No newline at end of file diff --git a/src/internal/functions/Get-AzOpsPolicySetDefinition.ps1 b/src/internal/functions/Get-AzOpsPolicySetDefinition.ps1 index 0cbba070..2e141ccc 100644 --- a/src/internal/functions/Get-AzOpsPolicySetDefinition.ps1 +++ b/src/internal/functions/Get-AzOpsPolicySetDefinition.ps1 @@ -2,11 +2,13 @@ <# .SYNOPSIS - Discover all custom policyset definitions at the provided scope (Management Groups, subscriptions or resource groups) + Discover all custom policyset definitions at the provided scope (Management Groups or subscriptions) .DESCRIPTION - Discover all custom policyset definitions at the provided scope (Management Groups, subscriptions or resource groups) + Discover all custom policyset definitions at the provided scope (Management Groups or subscriptions) .PARAMETER ScopeObject The scope object representing the azure entity to retrieve policyset definitions for. + .PARAMETER Subscription + Complete Subscription list .EXAMPLE > Get-AzOpsPolicySetDefinition -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath) Discover all custom policyset definitions deployed at Management Group scope @@ -16,25 +18,26 @@ [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Object] - $ScopeObject + [object] + $ScopeObject, + [Parameter(Mandatory = $false)] + [object] + $Subscription ) process { - #TODO: Discuss dropping resourcegroups, as no action is taken ever - if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') { + if ($ScopeObject.Type -notin 'subscriptions', 'managementGroups') { return } - - switch ($ScopeObject.Type) { - managementGroups { - Write-PSFMessage -Level Important -String 'Get-AzOpsPolicySetDefinition.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject - Get-AzPolicySetDefinition -Custom -ManagementGroupName $ScopeObject.Name | Where-Object ResourceId -match $ScopeObject.Scope - } - subscriptions { - Write-PSFMessage -Level Important -String 'Get-AzOpsPolicySetDefinition.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject - Get-AzPolicySetDefinition -Custom -SubscriptionId $ScopeObject.Scope.Split('/')[2] | Where-Object SubscriptionId -eq $ScopeObject.Name - } + if ($ScopeObject.Type -eq 'managementGroups') { + Write-PSFMessage -Level Debug -String 'Get-AzOpsPolicySetDefinition.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject + $query = "policyresources | where type == 'microsoft.authorization/policysetdefinitions' and properties.policyType == 'Custom' and subscriptionId == '' | order by ['id'] asc" + Search-AzOpsAzGraph -ManagementGroupName $ScopeObject.Name -Query $query -ErrorAction Stop + } + if ($Subscription) { + Write-PSFMessage -Level Debug -String 'Get-AzOpsPolicySetDefinition.Subscription' -StringValues $Subscription.count -Target $ScopeObject + $query = "policyresources | where type == 'microsoft.authorization/policysetdefinitions' and properties.policyType == 'Custom' | order by ['id'] asc" + Search-AzOpsAzGraph -Subscription $Subscription -Query $query -ErrorAction Stop } } diff --git a/src/internal/functions/Get-AzOpsResourceDefinition.ps1 b/src/internal/functions/Get-AzOpsResourceDefinition.ps1 index df23515a..ef5d73ee 100644 --- a/src/internal/functions/Get-AzOpsResourceDefinition.ps1 +++ b/src/internal/functions/Get-AzOpsResourceDefinition.ps1 @@ -2,9 +2,9 @@ <# .SYNOPSIS - This cmdlet recursively discovers resources (Management Groups, Subscriptions, Resource Groups, Resources, Privileged Identity Management resources, Policies, Role Assignments) from the provided input scope. + This cmdlet discovers resources (Management Groups, Subscriptions, Resource Groups, Resources, Privileged Identity Management resources, Policies, Role Assignments) from the provided input scope. .DESCRIPTION - This cmdlet recursively discovers resources (Management Groups, Subscriptions, Resource Groups, Resources, Privileged Identity Management resources, Policies, Role Assignments) from the provided input scope. + This cmdlet discovers resources (Management Groups, Subscriptions, Resource Groups, Resources, Privileged Identity Management resources, Policies, Role Assignments) from the provided input scope. .PARAMETER Scope Discovery Scope .PARAMETER IncludeResourcesInResourceGroup @@ -27,8 +27,6 @@ Skip discovery of specific resource types. .PARAMETER SkipRole Skip discovery of roles for better performance. - .PARAMETER ExportRawTemplate - Export generic templates without embedding them in the parameter block. .PARAMETER StatePath The root folder under which to write the resource json. .EXAMPLE @@ -84,523 +82,312 @@ [switch] $SkipRole = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipRole'), - [switch] - $ExportRawTemplate = (Get-PSFConfigValue -FullName 'AzOps.Core.ExportRawTemplate'), - [Parameter(Mandatory = $false)] [string] $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State') ) begin { - #region Utility Functions - function ConvertFrom-TypeResource { - [CmdletBinding()] - param ( - [Parameter(ValueFromPipeline = $true)] - [AzOpsScope] - $ScopeObject, - - [string] - $StatePath, - - [switch] - $ExportRawTemplate - ) - - process { - $common = @{ - FunctionName = 'Get-AzOpsResourceDefinition' - Target = $ScopeObject - } + # Set variables for retry with exponential backoff + $backoffMultiplier = 2 + $maxRetryCount = 3 + # Prepare Input Data for parallel processing + $runspaceData = @{ + AzOpsPath = "$($script:ModuleRoot)\AzOps.psd1" + StatePath = $StatePath + runspace_AzOpsAzManagementGroup = $script:AzOpsAzManagementGroup + runspace_AzOpsSubscriptions = $script:AzOpsSubscriptions + runspace_AzOpsPartialRoot = $script:AzOpsPartialRoot + runspace_AzOpsResourceProvider = $script:AzOpsResourceProvider + BackoffMultiplier = $backoffMultiplier + MaxRetryCount = $maxRetryCount + } + } - Write-PSFMessage -Level Verbose @common -String 'Get-AzOpsResourceDefinition.Resource.Processing' -StringValues $ScopeObject.Resource, $ScopeObject.ResourceGroup - try { - $resource = Get-AzResource -ResourceId $ScopeObject.scope -ErrorAction Stop - ConvertTo-AzOpsState -Resource $resource -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate - } - catch { - Write-PSFMessage -Level Warning @common -String 'Get-AzOpsResourceDefinition.Resource.Processing.Failed' -StringValues $ScopeObject.Resource, $ScopeObject.ResourceGroup -ErrorRecord $_ - } + process { + Write-PSFMessage -Level Important -String 'Get-AzOpsResourceDefinition.Processing' -StringValues $Scope + try { + $scopeObject = New-AzOpsScope -Scope $Scope -StatePath $StatePath -ErrorAction Stop + # Logging output metadata + $msgCommon = @{ + FunctionName = 'Get-AzOpsResourceDefinition' + Target = $ScopeObject } } - - function ConvertFrom-TypeResourceGroup { - [CmdletBinding()] - param ( - [Parameter(ValueFromPipeline = $true)] - [AzOpsScope] - $ScopeObject, - - [string[]] - $IncludeResourcesInResourceGroup, - - [string[]] - $IncludeResourceType, - - [switch] - $SkipResource, - - [string[]] - $SkipResourceType, - - [string] - $StatePath, - - [switch] - $ExportRawTemplate, - - $Context, - - [string] - $OdataFilter - ) - - process { - $common = @{ - FunctionName = 'Get-AzOpsResourceDefinition' - Target = $ScopeObject - } - - Write-PSFMessage -Level Verbose @common -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing' -StringValues $ScopeObject.Resourcegroup, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription - - try { - if (($IncludeResourcesInResourceGroup | Foreach-Object { $ScopeObject.ResourceGroup -like $_ }) -contains $true) { - Write-PSFMessage -Level Verbose @common -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing' -StringValues $resourceGroup.ResourceGroupName, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription - $resourceGroup = Get-AzResourceGroup -Name $ScopeObject.ResourceGroup -DefaultProfile $Context -ErrorAction Stop + catch { + Write-PSFMessage -Level Warning -String 'Get-AzOpsResourceDefinition.Processing.NotFound' -StringValues $Scope + return + } + if ($scopeObject.Type -notin 'subscriptions', 'managementGroups') { + Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Finished' -StringValues $scopeObject.Scope + return + } + switch ($scopeObject.Type) { + subscriptions { + Write-PSFMessage -Level Important @msgCommon -String 'Get-AzOpsResourceDefinition.Subscription.Processing' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription + $subscriptions = Get-AzSubscription -SubscriptionId $scopeObject.Name | Where-Object { "/subscriptions/$($_.Id)" -in $script:AzOpsSubscriptions.id } + } + managementGroups { + Write-PSFMessage -Level Important @msgCommon -String 'Get-AzOpsResourceDefinition.ManagementGroup.Processing' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup + $query = "resourcecontainers | where type == 'microsoft.management/managementgroups' | order by ['id'] asc" + $managementgroups = Search-AzOpsAzGraph -ManagementGroupName $scopeObject.Name -Query $query -ErrorAction Stop | Where-Object { $_.id -in $script:AzOpsAzManagementGroup.Id } + $subscriptions = Get-AzOpsNestedSubscription -Scope $scopeObject.Name + if ($managementgroups) { + # Process managementGroup scope in parallel + $managementgroups | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel { + $managementgroup = $_ + $runspaceData = $using:runspaceData + + Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1" + $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru + + & $azOps { + $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup + $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions + $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot + $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider + } + # Process Privileged Identity Management resources, Policies and Roles at managementGroup scope + if ((-not $using:SkipPim) -or (-not $using:SkipPolicy) -or (-not $using:SkipRole)) { + & $azOps { + $ScopeObject = New-AzOpsScope -Scope $managementgroup.id -StatePath $runspaceData.Statepath -ErrorAction Stop + if (-not $using:SkipPim) { + Get-AzOpsPim -ScopeObject $ScopeObject -StatePath $runspaceData.Statepath + } + if (-not $using:SkipPolicy) { + $policyExemptions = Get-AzOpsPolicyExemption -ScopeObject $ScopeObject + $policyExemptions | ConvertTo-AzOpsState -StatePath $runspaceData.Statepath + } + if (-not $using:SkipRole) { + Get-AzOpsRole -ScopeObject $ScopeObject -StatePath $runspaceData.Statepath + } + } + } } } - catch { - Write-PSFMessage -Level Warning @common -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing.Error' -StringValues $ScopeObject.Resourcegroup, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -ErrorRecord $_ - return - } - if ($resourceGroup.ManagedBy) { - Write-PSFMessage -Level Verbose @common -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing.Owned' -StringValues $resourceGroup.ResourceGroupName, $resourceGroup.ManagedBy - return - } - ConvertTo-AzOpsState -Resource $resourceGroup -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath - - # Get all resources in resource groups - $paramGetAzResource = @{ - DefaultProfile = $Context - ResourceGroupName = $resourceGroup.ResourceGroupName - ODataQuery = $OdataFilter - ExpandProperties = $true - } - if ($IncludeResourceType -eq "*") { - Write-PSFMessage -Level Verbose @common -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing.Resources' -StringValues $resourceGroup.ResourceGroupName, $ScopeObject.SubscriptionDisplayName - Get-AzResource @paramGetAzResource | Where-Object { $_.Type -notin $SkipResourceType } | ForEach-Object { - New-AzOpsScope -Scope $_.ResourceId - } | ConvertFrom-TypeResource -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate + } + } + #region Process Policies at $scopeObject + if (-not $SkipPolicy) { + Get-AzOpsPolicy -ScopeObject $scopeObject -Subscription $subscriptions -StatePath $StatePath + } + #endregion Process Policies at $scopeObject + + #region Process subscription scope in parallel + if ($subscriptions) { + $subscriptions | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel { + $subscription = $_ + $runspaceData = $using:runspaceData + + Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1" + $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru + + & $azOps { + $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup + $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions + $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot + $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider } - else { - foreach ($ResourceType in $IncludeResourceType) { - Write-PSFMessage -Level Verbose @common -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing.Resources' -StringValues $resourceGroup.ResourceGroupName, $ScopeObject.SubscriptionDisplayName - Get-AzResource -ResourceType $ResourceType @paramGetAzResource | Where-Object { $_.Type -notin $SkipResourceType } | ForEach-Object { - New-AzOpsScope -Scope $_.ResourceId - } | ConvertFrom-TypeResource -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate + # Process Privileged Identity Management resources, Policies, Locks and Roles at subscription scope + if ((-not $using:SkipPim) -or (-not $using:SkipPolicy) -or (-not $using:SkipLock) -or (-not $using:SkipRole)) { + & $azOps { + $scopeObject = New-AzOpsScope -Scope ($subscription.Type + '/' + $subscription.Id) -StatePath $runspaceData.Statepath -ErrorAction Stop + if (-not $using:SkipPim) { + Get-AzOpsPim -ScopeObject $scopeObject -StatePath $runspaceData.Statepath + } + if (-not $using:SkipPolicy) { + $policyExemptions = Get-AzOpsPolicyExemption -ScopeObject $scopeObject + $policyExemptions | ConvertTo-AzOpsState -StatePath $runspaceData.Statepath + } + if (-not $using:SkipLock) { + Get-AzOpsResourceLock -ScopeObject $scopeObject -StatePath $runspaceData.Statepath + } + if (-not $using:SkipRole) { + Get-AzOpsRole -ScopeObject $scopeObject -StatePath $runspaceData.Statepath + } } } } } + #endregion Process subscription scope in parallel - function ConvertFrom-TypeSubscription { - [CmdletBinding()] - param ( - [Parameter(ValueFromPipeline = $true)] - [AzOpsScope] - $ScopeObject, - - [string[]] - $IncludeResourcesInResourceGroup, - - [string[]] - $IncludeResourceType, - - [switch] - $SkipPim, - - [switch] - $SkipLock, - - [switch] - $SkipPolicy, - - [switch] - $SkipResource, - - [switch] - $SkipResourceGroup, - - [string[]] - $SkipResourceType, - - [switch] - $SkipRole, - - [string] - $StatePath, - - [switch] - $ExportRawTemplate, - - $Context, - - [string] - $ODataFilter - ) - - begin { - # Set variables for retry with exponential backoff - $backoffMultiplier = 2 - $maxRetryCount = 6 + #region Process Resource Groups + if ($SkipResourceGroup -or (-not $subscriptions)) { + if ($SkipResourceGroup) { + Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.SkippingResourceGroup' } - - process { - $common = @{ - FunctionName = 'Get-AzOpsResourceDefinition' - Target = $ScopeObject - } - - Write-PSFMessage -Level Important @common -String 'Get-AzOpsResourceDefinition.Subscription.Processing' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription - - # Skip discovery of resource groups if SkipResourceGroup switch have been used - # Separate discovery of resource groups in subscriptions to support parallel discovery - if ($SkipResourceGroup) { - Write-PSFMessage -Level Verbose @common -String 'Get-AzOpsResourceDefinition.Subscription.SkippingResourceGroup' - } - else { - # Get all Resource Groups in Subscription - # Retry loop with exponential back off implemented to catch errors - # Introduced due to error "Your Azure Credentials have not been set up or expired" - # https://github.com/Azure/azure-powershell/issues/9448 - # Define variables used by script - - - if ( - (((Get-PSFConfigValue -FullName 'AzOps.Core.SubscriptionsToIncludeResourceGroups') | Foreach-Object { $scopeObject.Subscription -like $_ }) -contains $true) -or - (((Get-PSFConfigValue -FullName 'AzOps.Core.SubscriptionsToIncludeResourceGroups') | Foreach-Object { $scopeObject.SubscriptionDisplayName -like $_ }) -contains $true) - ) { - $resourceGroups = Invoke-AzOpsScriptBlock -ArgumentList $Context -ScriptBlock { - param ($Context) - Get-AzResourceGroup -DefaultProfile ($Context | Write-Output) -ErrorAction Stop | Where-Object { -not $_.ManagedBy } - } -RetryCount $maxRetryCount -RetryWait $backoffMultiplier -RetryType Exponential - if (-not $resourceGroups) { - Write-PSFMessage -Level Verbose @common -String 'Get-AzOpsResourceDefinition.Subscription.NoResourceGroup' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription - } - - #region Prepare Input Data for parallel processing - $runspaceData = @{ - AzOpsPath = "$($script:ModuleRoot)\AzOps.psd1" - StatePath = $StatePath - ScopeObject = $ScopeObject - ODataFilter = $ODataFilter - SkipPim = $SkipPim - SkipLock = $SkipLock - SkipPolicy = $SkipPolicy - SkipRole = $SkipRole - SkipResource = $SkipResource - SkipChildResource = $SkipChildResource - SkipResourceType = $SkipResourceType - IncludeResourcesInResourceGroup = $IncludeResourcesInResourceGroup - IncludeResourceType = $IncludeResourceType - MaxRetryCount = $maxRetryCount - BackoffMultiplier = $backoffMultiplier - ExportRawTemplate = $ExportRawTemplate - runspace_AzOpsAzManagementGroup = $script:AzOpsAzManagementGroup - runspace_AzOpsSubscriptions = $script:AzOpsSubscriptions - runspace_AzOpsPartialRoot = $script:AzOpsPartialRoot - runspace_AzOpsResourceProvider = $script:AzOpsResourceProvider - } - #endregion Prepare Input Data for parallel processing - - #region Discover all resource groups in parallel - $resourceGroups | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel { - $resourceGroup = $_ - $runspaceData = $using:runspaceData - - $msgCommon = @{ - FunctionName = 'Get-AzOpsResourceDefinition' - ModuleName = 'AzOps' - } - - # region Importing module - # We need to import all required modules and declare variables again because of the parallel runspaces - # https://devblogs.microsoft.com/powershell/powershell-foreach-object-parallel-feature/ - Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1" - $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru - # endregion Importing module - - & $azOps { - $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup - $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions - $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot - $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider + else { + Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.Subscription.NotFound' + } + } + else { + if ((Get-PSFConfigValue -FullName 'AzOps.Core.SubscriptionsToIncludeResourceGroups') -ne '*') { + $subscriptionsToIncludeResourceGroups = $subscriptions | Where-Object { $_.Id -in (Get-PSFConfigValue -FullName 'AzOps.Core.SubscriptionsToIncludeResourceGroups') } + } + $query = "resourcecontainers | where type == 'microsoft.resources/subscriptions/resourcegroups' | where managedBy == '' | order by ['id'] asc" + if ($subscriptionsToIncludeResourceGroups) { + $resourceGroups = Search-AzOpsAzGraph -Subscription $subscriptionsToIncludeResourceGroups -Query $query -ErrorAction Stop + } + else { + $resourceGroups = Search-AzOpsAzGraph -Subscription $subscriptions -Query $query -ErrorAction Stop + } + if ($resourceGroups) { + # Process Resource Groups in parallel + $resourceGroups | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel { + $resourceGroup = $_ + $runspaceData = $using:runspaceData + + Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1" + $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru + + & $azOps { + $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup + $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions + $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot + $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider + } + # Create Resource Group in file system + & $azOps { + ConvertTo-AzOpsState -Resource $resourceGroup -StatePath $runspaceData.Statepath + } + # Process Privileged Identity Management resources, Policies, Locks and Roles at resource group scope + if ((-not $using:SkipPim) -or (-not $using:SkipPolicy) -or (-not $using:SkipRole) -or (-not $using:SkipLock)) { + & $azOps { + $rgScopeObject = New-AzOpsScope -Scope $resourceGroup.id -StatePath $runspaceData.Statepath -ErrorAction Stop + if (-not $using:SkipLock) { + Get-AzOpsResourceLock -ScopeObject $rgScopeObject -StatePath $runspaceData.Statepath } - - $context = Get-AzContext - $context.Subscription.Id = $runspaceData.ScopeObject.Subscription - - Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.ResourceGroup' -StringValues $resourceGroup.ResourceGroupName -Target $resourceGroup - & $azOps { ConvertTo-AzOpsState -Resource $resourceGroup -ExportRawTemplate:$runspaceData.ExportRawTemplate -StatePath $runspaceData.Statepath } - - #region Process Privileged Identity Management resources, Policies, Locks and Roles at RG scope - if ((-not $using:SkipPim) -or (-not $using:SkipPolicy) -or (-not $using:SkipRole) -or (-not $using:SkipLock)) { - & $azOps { - $rgScopeObject = New-AzOpsScope -Scope $resourceGroup.ResourceId -StatePath $runspaceData.Statepath -ErrorAction Stop - if (-not $using:SkipLock) { - Get-AzOpsResourceLock -ScopeObject $rgScopeObject -StatePath $runspaceData.Statepath - } - if (-not $using:SkipPim) { - Get-AzOpsPim -ScopeObject $rgScopeObject -StatePath $runspaceData.Statepath - } - if (-not $using:SkipPolicy) { - Get-AzOpsPolicy -ScopeObject $rgScopeObject -StatePath $runspaceData.Statepath - } - if (-not $using:SkipRole) { - Get-AzOpsRole -ScopeObject $rgScopeObject -StatePath $runspaceData.Statepath - } - } + if (-not $using:SkipPim) { + Get-AzOpsPim -ScopeObject $rgScopeObject -StatePath $runspaceData.Statepath } - #endregion Process Privileged Identity Management resources, Policies and Roles at RG scope - - if (-not $using:SkipResource) { - Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.ResourceGroup.Resources' -StringValues $resourceGroup.ResourceGroupName -Target $resourceGroup - if (($runspaceData.IncludeResourcesInResourceGroup | Foreach-Object { $resourceGroup.ResourceGroupName -like $_ }) -notcontains $true) { - Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.Subscription.Processing.IncludeResourcesInRG' -StringValues $resourceGroup.ResourceGroupName -Target $resourceGroup - continue - } - try { - $resources = & $azOps { - $parameters = @{ - DefaultProfile = $Context | Select-Object -First 1 - ODataQuery = $runspaceData.ODataFilter - } - if ($resourceGroup.ResourceGroupName) { - $parameters.ResourceGroupName = $resourceGroup.ResourceGroupName - } - Invoke-AzOpsScriptBlock -ArgumentList $parameters -ScriptBlock { - param ( - $Parameters - ) - $param = $Parameters | Write-Output - Get-AzResource @param -ExpandProperties -ErrorAction Stop - } -RetryCount $runspaceData.MaxRetryCount -RetryWait $runspaceData.BackoffMultiplier -RetryType Exponential - } - } - catch { - Write-PSFMessage -Level Warning -String 'Get-AzOpsResourceDefinition.Resource.Processing.Warning' -StringValues $resourceGroup.ResourceGroupName, $_ - } - - if ($runspaceData.IncludeResourceType -eq "*") { - $resources = $resources | Where-Object { $_.Type -notin $runspaceData.SkipResourceType } - } - else { - $resources = $resources | Where-Object { $_.Type -notin $runspaceData.SkipResourceType -and $_.Type -in $runspaceData.IncludeResourceType } - } - if (-not $resources) { - Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.ResourceGroup.NoResources' -StringValues $resourceGroup.ResourceGroupName -Target $resourceGroup - } - $tempExportPath = [System.IO.Path]::GetTempPath() + $resourceGroup.ResourceGroupName + '.json' - # Loop through resources and convert them to AzOpsState - foreach ($resource in $resources) { - # Convert resources to AzOpsState - Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.Resource' -StringValues $resource.Name, $resourceGroup.ResourceGroupName -Target $resource - & $azOps { ConvertTo-AzOpsState -Resource $resource -ExportRawTemplate:$runspaceData.ExportRawTemplate -StatePath $runspaceData.Statepath } - if (-not $using:SkipChildResource) { - try { - $exportParameters = @{ - Resource = $resource.ResourceId - ResourceGroupName = $resourceGroup.ResourceGroupName - SkipAllParameterization = $true - Path = $tempExportPath - DefaultProfile = $Context | Select-Object -First 1 - } - Export-AzResourceGroup @exportParameters -Confirm:$false -Force -ErrorAction Stop | Out-Null - $exportResources = (Get-Content -Path $tempExportPath | ConvertFrom-Json).resources - foreach ($exportResource in $exportResources) { - if (-not(($resource.Name -eq $exportResource.name) -and ($resource.ResourceType -eq $exportResource.type))) { - Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.Subscription.Processing.ChildResource' -StringValues $exportResource.Name, $resourceGroup.ResourceGroupName -Target $exportResource - $ChildResource = @{ - resourceProvider = $exportResource.type -replace '/', '_' - resourceName = $exportResource.name -replace '/', '_' - parentResourceId = $resourceGroup.ResourceId - } - if (Get-Member -InputObject $exportResource -name 'dependsOn') { - $exportResource.PsObject.Members.Remove('dependsOn') - } - $resourceHash = @{resources = @($exportResource) } - & $azOps { - ConvertTo-AzOpsState -Resource $resourceHash -ChildResource $ChildResource -StatePath $runspaceData.Statepath - } - } - } - } - catch { - Write-PSFMessage -Level Warning -String 'Get-AzOpsResourceDefinition.ChildResource.Warning' -StringValues $resourceGroup.ResourceGroupName, $_ - } - } - else { - Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.Subscription.SkippingChildResource' -StringValues $resourceGroup.ResourceGroupName - } - } - if (Test-Path -Path $tempExportPath) { - Remove-Item -Path $tempExportPath - } + if (-not $using:SkipPolicy) { + $policyExemptions = Get-AzOpsPolicyExemption -ScopeObject $rgScopeObject + $policyExemptions | ConvertTo-AzOpsState -StatePath $runspaceData.Statepath } - else { - Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.Subscription.SkippingResources' + if (-not $using:SkipRole) { + Get-AzOpsRole -ScopeObject $rgScopeObject -StatePath $runspaceData.Statepath } } - #endregion Discover all resource groups in parallel - } - else { - Write-PSFMessage -Level Verbose @common -String 'Get-AzOpsResourceDefinition.Subscription.ExcludeResourceGroup' } } - - if ($Script:AzOpsAzManagementGroup.Children) { - $subscriptionItem = $script:AzOpsAzManagementGroup.children | Where-Object Name -eq $ScopeObject.name + } + else { + Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.NoResourceGroup' -StringValues $scopeObject.Name + } + # Process Policies at Resource Group scope + if (-not $SkipPolicy) { + if ($subscriptionsToIncludeResourceGroups) { + Get-AzOpsPolicy -ScopeObject $scopeObject -Subscription $subscriptions -SubscriptionsToIncludeResourceGroups $subscriptionsToIncludeResourceGroups -ResourceGroup -StatePath $StatePath } else { - # Handle subscription-only scenarios without permissions to managementGroups - $subscriptionItem = Get-AzSubscription -SubscriptionId $scopeObject.Subscription + Get-AzOpsPolicy -ScopeObject $scopeObject -Subscription $subscriptions -ResourceGroup -StatePath $StatePath } - - if ($subscriptionItem) { - ConvertTo-AzOpsState -Resource $subscriptionItem -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath + } + # Process Resources at Resource Group scope + if (-not $SkipResource) { + Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.Processing.Resource.Discovery' -StringValues $scopeObject.Name + try { + $SkipResourceType | ForEach-Object { $skipResourceTypes += ($(if($skipResourceTypes){","}) + "'" + $_ + "'") } + $query = "resources | where type !in~ ($skipResourceTypes)" + if ($IncludeResourceType -ne "*") { + $IncludeResourceType | ForEach-Object { $includeResourceTypes += ($(if($includeResourceTypes){","}) + "'" + $_ + "'") } + $query = $query + " and type in~ ($includeResourceTypes)" + } + if ($IncludeResourcesInResourceGroup -ne "*") { + $IncludeResourcesInResourceGroup | ForEach-Object { $includeResourcesInResourceGroups += ($(if($includeResourcesInResourceGroups){","}) + "'" + $_ + "'") } + $query = $query + " and resourceGroup in~ ($includeResourcesInResourceGroups)" + } + $query = $query + " | order by ['id'] asc" + $resourcesBase = Search-AzOpsAzGraph -Subscription $subscriptions -Query $query -ErrorAction Stop + } + catch { + Write-PSFMessage -Level Warning @msgCommon -String 'Get-AzOpsResourceDefinition.Processing.Resource.Warning' -StringValues $scopeObject.Name + } + if ($resourcesBase) { + $resources = @() + foreach ($resource in $resourcesBase) { + if ($resourceGroups | Where-Object { $_.name -eq $resource.resourceGroup -and $_.subscriptionId -eq $resource.subscriptionId }) { + Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.Processing.Resource' -StringValues $resource.name, $resource.resourcegroup -Target $resource + $resources += $resource + ConvertTo-AzOpsState -Resource $resource -StatePath $Statepath + } + } + } + else { + Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.Processing.Resource.Discovery.NotFound' -StringValues $scopeObject.Name } } - } - - function ConvertFrom-TypeManagementGroup { - [CmdletBinding()] - param ( - [Parameter(ValueFromPipeline = $true)] - [AzOpsScope] - $ScopeObject, - - [switch] - $SkipPim, - - [switch] - $SkipPolicy, - - [switch] - $SkipRole, - - [switch] - $SkipResourceGroup, - - [switch] - $SkipResource, - - [switch] - $ExportRawTemplate, - - [string] - $StatePath - ) - begin { - $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Exclude ScopeObject + else { + Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.SkippingResources' } - process { - $common = @{ - FunctionName = 'Get-AzOpsResourceDefinition' - Target = $ScopeObject - } - - Write-PSFMessage -Level Important -String 'Get-AzOpsResourceDefinition.ManagementGroup.Processing' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup - - $childOfManagementGroups = ($script:AzOpsAzManagementGroup | Where-Object Name -eq $ScopeObject.ManagementGroup).Children - - foreach ($child in $childOfManagementGroups) { - - if ($child.Type -eq '/subscriptions') { - if ($script:AzOpsSubscriptions.id -contains $child.Id) { - Get-AzOpsResourceDefinition -Scope $child.Id @parameters + # Process resources as scope in parallel, look for childResource + if (-not $SkipResource -and -not $SkipChildResource) { + $resources | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel { + $resource = $_ + $runspaceData = $using:runspaceData + + Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1" + $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru + + & $azOps { + $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup + $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions + $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot + $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider + } + $context = Get-AzContext + $context.Subscription.Id = $resource.subscriptionId + $tempExportPath = [System.IO.Path]::GetTempPath() + (New-Guid).ToString() + '.json' + try { + & $azOps { + $exportParameters = @{ + Resource = $resource.id + ResourceGroupName = $resource.resourceGroup + SkipAllParameterization = $true + Path = $tempExportPath + DefaultProfile = $context | Select-Object -First 1 + } + Invoke-AzOpsScriptBlock -ArgumentList $exportParameters -ScriptBlock { + param ( + $ExportParameters + ) + $param = $ExportParameters | Write-Output + Export-AzResourceGroup @param -Confirm:$false -Force -ErrorAction Stop | Out-Null + } -RetryCount $runspaceData.MaxRetryCount -RetryWait $runspaceData.BackoffMultiplier -RetryType Exponential } - else { - Write-PSFMessage -Level Warning -String 'Get-AzOpsResourceDefinition.ManagementGroup.Subscription.NotFound' -StringValues $child.Name + $exportResources = (Get-Content -Path $tempExportPath | ConvertFrom-Json).resources + $resourceGroup = $using:resourceGroups | Where-Object {$_.subscriptionId -eq $resource.subscriptionId -and $_.name -eq $resource.resourceGroup} + foreach ($exportResource in $exportResources) { + if (-not(($resource.name -eq $exportResource.name) -and ($resource.type -eq $exportResource.type))) { + Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Processing.ChildResource' -StringValues $exportResource.name, $resource.resourceGroup -Target $exportResource + $ChildResource = @{ + resourceProvider = $exportResource.type -replace '/', '_' + resourceName = $exportResource.name -replace '/', '_' + parentResourceId = $resourceGroup.id + } + if (Get-Member -InputObject $exportResource -name 'dependsOn') { + $exportResource.PsObject.Members.Remove('dependsOn') + } + $resourceHash = @{resources = @($exportResource) } + & $azOps { + ConvertTo-AzOpsState -Resource $resourceHash -ChildResource $ChildResource -StatePath $runspaceData.Statepath + } + } } } - else { - Get-AzOpsResourceDefinition -Scope $child.Id @parameters + catch { + Write-PSFMessage -Level Warning -String 'Get-AzOpsResourceDefinition.ChildResource.Warning' -StringValues $resource.resourceGroup, $_ + } + if (Test-Path -Path $tempExportPath) { + Remove-Item -Path $tempExportPath } } - ConvertTo-AzOpsState -Resource ($script:AzOpsAzManagementGroup | Where-Object Name -eq $ScopeObject.ManagementGroup) -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath - } - } - #endregion Utility Functions - } - - process { - Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Processing' -StringValues $Scope - - try { - $scopeObject = New-AzOpsScope -Scope $Scope -StatePath $StatePath -ErrorAction Stop - } - catch { - Write-PSFMessage -Level Warning -String 'Get-AzOpsResourceDefinition.Processing.NotFound' -StringValues $Scope - return - } - - if ($scopeObject.Subscription) { - Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Subscription.Found' -StringValues $scopeObject.subscriptionDisplayName, $scopeObject.subscription - $context = Get-AzContext - $context.Subscription.Id = $ScopeObject.Subscription - $odataFilter = "`$filter=subscriptionId eq '$($scopeObject.subscription)'" - # Exclude resources in SkipResourceType - $SkipResourceType | Foreach-Object -Process { - $odataFilter = $odataFilter + " AND resourceType ne '$_'" } - # Include resources from if changed from '*' - $IncludeResourceType | Where-Object { $_ -ne '*' } | Foreach-Object -Process { - $odataFilter = $odataFilter + " AND resourceType eq '$_'" + else { + Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.SkippingChildResources' } - Write-PSFMessage -Level Debug -String 'Get-AzOpsResourceDefinition.Subscription.OdataFilter' -StringValues $odataFilter - } - - switch ($scopeObject.Type) { - resource { ConvertFrom-TypeResource -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate } - resourcegroups { ConvertFrom-TypeResourceGroup -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate -Context $context -SkipResource:$SkipResource -SkipResourceType:$SkipResourceType -OdataFilter $odataFilter -IncludeResourceType $IncludeResourceType -IncludeResourcesInResourceGroup $IncludeResourcesInResourceGroup } - subscriptions { ConvertFrom-TypeSubscription -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate -Context $context -SkipResourceGroup:$SkipResourceGroup -SkipResource:$SkipResource -SkipResourceType:$SkipResourceType -SkipPim:$SkipPim -SkipLock:$SkipLock -SkipPolicy:$SkipPolicy -SkipRole:$SkipRole -ODataFilter $odataFilter -IncludeResourceType $IncludeResourceType -IncludeResourcesInResourceGroup $IncludeResourcesInResourceGroup } - managementGroups { ConvertFrom-TypeManagementGroup -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate -SkipPim:$SkipPim -SkipPolicy:$SkipPolicy -SkipRole:$SkipRole -SkipResourceGroup:$SkipResourceGroup -SkipResource:$SkipResource } - } - - if ($scopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') { - Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Finished' -StringValues $scopeObject.Scope - return - } - - #region Process Privileged Identity Management resources - if (-not $SkipPim) { - Get-AzOpsPim -ScopeObject $scopeObject -StatePath $StatePath - } - #endregion Process Privileged Identity Management resources - - #region Process ResourceLock - if (-not $SkipLock) { - Get-AzOpsResourceLock -ScopeObject $scopeObject -StatePath $StatePath - } - #endregion Process ResourceLock - - #region Process Policies - if (-not $SkipPolicy) { - Get-AzOpsPolicy -ScopeObject $scopeObject -StatePath $StatePath - } - #endregion Process Policies - - #region Process Roles - if (-not $SkipRole) { - Get-AzOpsRole -ScopeObject $scopeObject -StatePath $StatePath - } - #endregion Process Roles - - if ($scopeObject.Type -notin 'subscriptions', 'managementGroups') { - Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Finished' -StringValues $scopeObject.Scope - return } - Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Finished' -StringValues $scopeObject.Scope + #endregion Process Resource Groups + Write-PSFMessage -Level Verbose @msgCommon -String 'Get-AzOpsResourceDefinition.Finished' -StringValues $scopeObject.Scope } -} +} \ No newline at end of file diff --git a/src/internal/functions/Get-AzOpsResourceLock.ps1 b/src/internal/functions/Get-AzOpsResourceLock.ps1 index cbd6a82f..dd4482e2 100644 --- a/src/internal/functions/Get-AzOpsResourceLock.ps1 +++ b/src/internal/functions/Get-AzOpsResourceLock.ps1 @@ -24,17 +24,17 @@ ) process { - if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions') { + if ($ScopeObject.Type -notin 'resourceGroups', 'subscriptions') { return } switch ($ScopeObject.Type) { subscriptions { # ScopeObject is a subscription - Write-PSFMessage -Level Important -String 'Get-AzOpsResourceLock.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject + Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceLock.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject } resourcegroups { # ScopeObject is a resourcegroup - Write-PSFMessage -Level Important -String 'Get-AzOpsResourceLock.ResourceGroup' -StringValues $ScopeObject.ResourceGroup -Target $ScopeObject + Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceLock.ResourceGroup' -StringValues $ScopeObject.ResourceGroup -Target $ScopeObject } } # Gather resource locks at scopeObject diff --git a/src/internal/functions/Get-AzOpsRole.ps1 b/src/internal/functions/Get-AzOpsRole.ps1 index 31bd84f1..f05edca5 100644 --- a/src/internal/functions/Get-AzOpsRole.ps1 +++ b/src/internal/functions/Get-AzOpsRole.ps1 @@ -21,11 +21,15 @@ # Process role definitions Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Role Definitions', $ScopeObject.Scope $roleDefinitions = Get-AzOpsRoleDefinition -ScopeObject $ScopeObject - $roleDefinitions | ConvertTo-AzOpsState -StatePath $StatePath + if ($roleDefinitions) { + $roleDefinitions | ConvertTo-AzOpsState -StatePath $StatePath + } # Process role assignments Write-PSFMessage -Level Verbose -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Role Assignments', $ScopeObject.Scope $roleAssignments = Get-AzOpsRoleAssignment -ScopeObject $ScopeObject - $roleAssignments | ConvertTo-AzOpsState -StatePath $StatePath + if ($roleAssignments) { + $roleAssignments | ConvertTo-AzOpsState -StatePath $StatePath + } } } \ No newline at end of file diff --git a/src/internal/functions/Get-AzOpsRoleAssignment.ps1 b/src/internal/functions/Get-AzOpsRoleAssignment.ps1 index 229e99cd..7ccd0330 100644 --- a/src/internal/functions/Get-AzOpsRoleAssignment.ps1 +++ b/src/internal/functions/Get-AzOpsRoleAssignment.ps1 @@ -12,7 +12,6 @@ Discover all custom role assignments deployed at Management Group scope #> - [OutputType([AzOpsRoleAssignment])] [CmdletBinding()] param ( [parameter(Mandatory = $true, ValueFromPipeline = $true)] @@ -21,10 +20,26 @@ ) process { - Write-PSFMessage -Level Important -String 'Get-AzOpsRoleAssignment.Processing' -StringValues $ScopeObject -Target $ScopeObject - foreach ($roleAssignment in Get-AzRoleAssignment -Scope $ScopeObject.Scope -WarningAction SilentlyContinue | Where-Object Scope -eq $ScopeObject.Scope) { - Write-PSFMessage -Level Verbose -String 'Get-AzOpsRoleAssignment.Assignment' -StringValues $roleAssignment.DisplayName, $roleAssignment.RoleDefinitionName -Target $ScopeObject - [AzOpsRoleAssignment]::new($roleAssignment) + Write-PSFMessage -Level Debug -String 'Get-AzOpsRoleAssignment.Processing' -StringValues $ScopeObject -Target $ScopeObject + $apiVersion = (($script:AzOpsResourceProvider | Where-Object {$_.ProviderNamespace -eq 'Microsoft.Authorization'}).ResourceTypes | Where-Object {$_.ResourceTypeName -eq 'roleAssignments'}).ApiVersions | Select-Object -First 1 + $path = "$($scopeObject.Scope)/providers/Microsoft.Authorization/roleAssignments?api-version=$apiVersion&`$filter=atScope()" + $roleAssignments = Invoke-AzOpsRestMethod -Path $path -Method GET + if ($roleAssignments) { + $roleAssignmentMatch = @() + foreach ($roleAssignment in $roleAssignments) { + if ($roleAssignment.properties.scope -eq $ScopeObject.Scope) { + Write-PSFMessage -Level Debug -String 'Get-AzOpsRoleAssignment.Assignment' -StringValues $roleAssignment.id, $roleAssignment.properties.roleDefinitionId -Target $ScopeObject + $roleAssignmentMatch += [PSCustomObject]@{ + id = $roleAssignment.id + name = $roleAssignment.name + properties = $roleAssignment.properties + type = $roleAssignment.type + } + } + } + if ($roleAssignmentMatch) { + return $roleAssignmentMatch + } } } diff --git a/src/internal/functions/Get-AzOpsRoleDefinition.ps1 b/src/internal/functions/Get-AzOpsRoleDefinition.ps1 index 2de1628c..8daae1be 100644 --- a/src/internal/functions/Get-AzOpsRoleDefinition.ps1 +++ b/src/internal/functions/Get-AzOpsRoleDefinition.ps1 @@ -12,7 +12,6 @@ Discover all custom role definitions deployed at Management Group scope #> - [OutputType([AzOpsRoleDefinition])] [CmdletBinding()] param ( [parameter(Mandatory = $true, ValueFromPipeline = $true)] @@ -21,14 +20,27 @@ ) process { - Write-PSFMessage -Level Important -String 'Get-AzOpsRoleDefinition.Processing' -StringValues $ScopeObject -Target $ScopeObject - foreach ($roleDefinition in Get-AzRoleDefinition -Custom -Scope $ScopeObject.Scope -WarningAction SilentlyContinue) { - #removing trailing '/' if it exists in assignable scopes - if (($roledefinition.AssignableScopes[0] -replace "[/]$" -replace '') -eq $ScopeObject.Scope) { - [AzOpsRoleDefinition]::new($roleDefinition) + Write-PSFMessage -Level Debug -String 'Get-AzOpsRoleDefinition.Processing' -StringValues $ScopeObject -Target $ScopeObject + $apiVersion = (($script:AzOpsResourceProvider | Where-Object {$_.ProviderNamespace -eq 'Microsoft.Authorization'}).ResourceTypes | Where-Object {$_.ResourceTypeName -eq 'roleDefinitions'}).ApiVersions | Select-Object -First 1 + $path = "$($scopeObject.Scope)/providers/Microsoft.Authorization/roleDefinitions?api-version=$apiVersion&`$filter=type+eq+'CustomRole'" + $roleDefinitions = Invoke-AzOpsRestMethod -Path $path -Method GET + if ($roleDefinitions) { + $roleDefinitionsMatch = @() + foreach ($roleDefinition in $roleDefinitions) { + if ($roleDefinition.properties.assignableScopes -eq $ScopeObject.Scope) { + Write-PSFMessage -Level Debug -String 'Get-AzOpsRoleDefinition.Definition' -StringValues $roleDefinition.id -Target $ScopeObject + $roleDefinitionsMatch += [PSCustomObject]@{ + # Removing the Trailing slash to ensure that '/' is not appended twice when adding '/providers/xxx'. + # Example: '/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/' is a valid assignment scope. + id = '/' + $roleDefinition.properties.assignableScopes[0].Trim('/') + '/providers/Microsoft.Authorization/roleDefinitions/' + $roleDefinition.id + name = $roleDefinition.Name + properties = $roleDefinition.properties + type = $roleDefinition.type + } + } } - else { - Write-PSFMessage -Level Verbose -String 'Get-AzOpsRoleDefinition.NonAuthorative' -StringValues $roledefinition.Id, Id, $ScopeObject.Scope, $roledefinition.AssignableScopes[0] -Target $ScopeObject + if ($roleDefinitionsMatch) { + return $roleDefinitionsMatch } } } diff --git a/src/internal/functions/Get-AzOpsRoleEligibilityScheduleRequest.ps1 b/src/internal/functions/Get-AzOpsRoleEligibilityScheduleRequest.ps1 index 08dec3a8..df475ec7 100644 --- a/src/internal/functions/Get-AzOpsRoleEligibilityScheduleRequest.ps1 +++ b/src/internal/functions/Get-AzOpsRoleEligibilityScheduleRequest.ps1 @@ -20,19 +20,19 @@ ) process { - if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') { + if ($ScopeObject.Type -notin 'resourceGroups', 'subscriptions', 'managementGroups') { return } # Process RoleEligibilitySchedule which is used to construct AzOpsRoleEligibilityScheduleRequest - Write-PSFMessage -Level Important -String 'Get-AzOpsRoleEligibilityScheduleRequest.Processing' -StringValues $ScopeObject.Scope -Target $ScopeObject + Write-PSFMessage -Level Debug -String 'Get-AzOpsRoleEligibilityScheduleRequest.Processing' -StringValues $ScopeObject.Scope -Target $ScopeObject $roleEligibilitySchedules = Get-AzRoleEligibilitySchedule -Scope $ScopeObject.Scope -WarningAction SilentlyContinue | Where-Object {$_.Scope -eq $ScopeObject.Scope} if ($roleEligibilitySchedules) { foreach ($roleEligibilitySchedule in $roleEligibilitySchedules) { # Process roleEligibilitySchedule together with RoleEligibilityScheduleRequest $roleEligibilityScheduleRequest = Get-AzRoleEligibilityScheduleRequest -Scope $ScopeObject.Scope -Name $roleEligibilitySchedule.Name -ErrorAction SilentlyContinue if ($roleEligibilityScheduleRequest) { - Write-PSFMessage -Level Verbose -String 'Get-AzOpsRoleEligibilityScheduleRequest.Assignment' -StringValues $roleEligibilitySchedule.Name -Target $ScopeObject + Write-PSFMessage -Level Debug -String 'Get-AzOpsRoleEligibilityScheduleRequest.Assignment' -StringValues $roleEligibilitySchedule.Name -Target $ScopeObject # Construct AzOpsRoleEligibilityScheduleRequest by combining information from roleEligibilitySchedule and roleEligibilityScheduleRequest [AzOpsRoleEligibilityScheduleRequest]::new($roleEligibilitySchedule, $roleEligibilityScheduleRequest) } diff --git a/src/internal/functions/Invoke-AzOpsRestMethod.ps1 b/src/internal/functions/Invoke-AzOpsRestMethod.ps1 new file mode 100644 index 00000000..e5b3662f --- /dev/null +++ b/src/internal/functions/Invoke-AzOpsRestMethod.ps1 @@ -0,0 +1,48 @@ +function Invoke-AzOpsRestMethod { + <# + .SYNOPSIS + Process Path with given Method and manage paging of results and returns value's + .PARAMETER Path + Path + .PARAMETER Method + Method + .EXAMPLE + > Invoke-AzOpsRestMethod -Path "/subscriptions/{subscription}/resourcegroups/{resourcegroup}/providers/microsoft.operationalinsights/workspaces/{workspace}?api-version={API}" -Method GET + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Object] + $Path, + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + $Method + ) + + process { + # Process Path with given Method + Write-PSFMessage -Level Debug -String 'Invoke-AzOpsRestMethod.Processing' -StringValues $Path + $allresults = do { + try { + $results = ((Invoke-AzRestMethod -Path $Path -Method $Method -ErrorAction Stop).Content | ConvertFrom-Json -Depth 100) + $results.value + $path = $results.nextLink -replace 'https://management\.azure\.com' + if ($results.StatusCode -eq '429' -or $results.StatusCode -like '5*') { + $results.Headers.GetEnumerator() | ForEach-Object { + if ($_.key -eq 'Retry-After') { + Write-PSFMessage -Level Warning -String 'Invoke-AzOpsRestMethod.Processing.RateLimit' -StringValues $Path, $_.value + Start-Sleep -Seconds $_.value + } + } + } + } + catch { + Write-PSFMessage -Level Warning -String 'Invoke-AzOpsRestMethod.Processing.Warning' -StringValues $_, $Path + } + } + while ($path) + if ($allresults) { + return $allresults + } + } +} \ No newline at end of file diff --git a/src/internal/functions/New-AzOpsDeployment.ps1 b/src/internal/functions/New-AzOpsDeployment.ps1 index 82549464..ddc25680 100644 --- a/src/internal/functions/New-AzOpsDeployment.ps1 +++ b/src/internal/functions/New-AzOpsDeployment.ps1 @@ -122,7 +122,7 @@ $deploymentCommand = 'New-AzTenantDeployment' } # If Management Group resource was not found, validate and prepare for first time deployment of resource - elseif (($scopeObject.StatePath).StartsWith('azopsscope-assume-new-resource_')) { + elseif ($scopeObject.managementGroup -and (($scopeObject.StatePath).StartsWith('azopsscope-assume-new-resource_'))) { $resourceScopeFileContent = Get-Content -Path $addition | ConvertFrom-Json -Depth 100 $resource = ($resourceScopeFileContent.resources | Where-Object {$_.type -eq 'Microsoft.Management/managementGroups'} | Select-Object -First 1) $pathDir = (Get-Item -Path $addition).Directory | Resolve-Path -Relative diff --git a/src/internal/functions/Remove-AzOpsDeployment.ps1 b/src/internal/functions/Remove-AzOpsDeployment.ps1 index 7a9935a5..67bc892b 100644 --- a/src/internal/functions/Remove-AzOpsDeployment.ps1 +++ b/src/internal/functions/Remove-AzOpsDeployment.ps1 @@ -131,7 +131,7 @@ ) if ($resourceToDelete.ResourceType -eq 'Microsoft.Authorization/policyDefinitions') { $dependency = @() - $query = "PolicyResources | where type == 'microsoft.authorization/policysetdefinitions' and properties.policyType == 'Custom' | project id, policyDefinitions = (properties.policyDefinitions) | mv-expand policyDefinitions | project id, policyDefinitionId = tostring(policyDefinitions.policyDefinitionId) | where policyDefinitionId == '$($resourceToDelete.PolicyDefinitionId)' | order by policyDefinitionId asc | order by id asc" + $query = "PolicyResources | where type == 'microsoft.authorization/policysetdefinitions' and properties.policyType == 'Custom' | project id, type, policyDefinitions = (properties.policyDefinitions) | mv-expand policyDefinitions | project id, type, policyDefinitionId = tostring(policyDefinitions.policyDefinitionId) | where policyDefinitionId == '$($resourceToDelete.PolicyDefinitionId)' | order by policyDefinitionId asc | order by id asc" $depPolicySetDefinition = Search-AzGraphDeletionDependency -query $query if ($depPolicySetDefinition) { $depPolicySetDefinition = foreach ($policySetDefinition in $depPolicySetDefinition) { @@ -161,35 +161,15 @@ $results = @() if ($PartialMgDiscoveryRoot) { foreach ($managementRoot in $PartialMgDiscoveryRoot) { - $processing = Search-AzGraph -Query $query -ManagementGroup $managementRoot - if ($processing) { - $results += $processing - do { - if ($processing.SkipToken) { - $processing = Search-AzGraph -Query $query -ManagementGroup $managementRoot -SkipToken $processing.SkipToken - $results += $processing - } - else { - $done = $true - } - } while ($done -ne $true) + $subscriptions = Get-AzOpsNestedSubscription -Scope $managementRoot + $results += Search-AzOpsAzGraph -ManagementGroupName $managementRoot -Query $query -ErrorAction Stop + if ($subscriptions) { + $results += Search-AzOpsAzGraph -Subscription $subscriptions -Query $query -ErrorAction Stop } } } else { - $processing = Search-AzGraph -Query $query -UseTenantScope - if ($processing) { - $results += $processing - do { - if ($processing.SkipToken) { - $processing = Search-AzGraph -Query $query -UseTenantScope -SkipToken $processing.SkipToken - $results += $processing - } - else { - $done = $true - } - } while ($done -ne $true) - } + $results = Search-AzOpsAzGraph -Query $query -UseTenantScope -ErrorAction Stop } if ($results) { $results = $results | Sort-Object Id -Unique @@ -239,6 +219,7 @@ #region remove supported resources if ($scopeObject.Resource -in $DeletionSupportedResourceType) { + $dependency = @() switch ($scopeObject.Resource) { # Check resource existance through optimal path 'locks' { @@ -247,14 +228,13 @@ 'policyAssignments' { $resourceToDelete = Get-AzPolicyAssignment -Id $scopeObject.scope -ErrorAction SilentlyContinue if ($resourceToDelete) { - $dependency = Get-AzPolicyAssignmentDeletionDependency -resourceToDelete $resourceToDelete + $dependency += Get-AzPolicyAssignmentDeletionDependency -resourceToDelete $resourceToDelete $dependency += Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete } } 'policyDefinitions' { $resourceToDelete = Get-AzPolicyDefinition -Id $scopeObject.scope -ErrorAction SilentlyContinue if ($resourceToDelete) { - $dependency = @() $dependency += Get-AzPolicyAssignmentDeletionDependency -resourceToDelete $resourceToDelete $dependency += Get-AzPolicyDefinitionDeletionDependency -resourceToDelete $resourceToDelete $dependency += Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete @@ -263,20 +243,20 @@ 'policyExemptions' { $resourceToDelete = Get-AzPolicyExemption -Id $scopeObject.scope -ErrorAction SilentlyContinue if ($resourceToDelete) { - $dependency = Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete + $dependency += Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete } } 'policySetDefinitions' { $resourceToDelete = Get-AzPolicySetDefinition -Id $scopeObject.scope -ErrorAction SilentlyContinue if ($resourceToDelete) { - $dependency = Get-AzPolicyAssignmentDeletionDependency -resourceToDelete $resourceToDelete + $dependency += Get-AzPolicyAssignmentDeletionDependency -resourceToDelete $resourceToDelete $dependency += Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete } } 'roleAssignments' { $resourceToDelete = Invoke-AzRestMethod -Path "$($scopeObject.scope)?api-version=2022-01-01-preview" | Where-Object { $_.StatusCode -eq 200 } if ($resourceToDelete) { - $dependency = Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete + $dependency += Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete } } } diff --git a/src/internal/functions/Remove-AzOpsInvalidCharacters.ps1 b/src/internal/functions/Remove-AzOpsInvalidCharacter.ps1 similarity index 78% rename from src/internal/functions/Remove-AzOpsInvalidCharacters.ps1 rename to src/internal/functions/Remove-AzOpsInvalidCharacter.ps1 index b050d497..64f91d4f 100644 --- a/src/internal/functions/Remove-AzOpsInvalidCharacters.ps1 +++ b/src/internal/functions/Remove-AzOpsInvalidCharacter.ps1 @@ -1,4 +1,4 @@ -function Remove-AzOpsInvalidCharacters { +function Remove-AzOpsInvalidCharacter { <# .SYNOPSIS @@ -10,12 +10,13 @@ .PARAMETER Override Accepts input to skip selected invalid characters. .EXAMPLE - > Remove-AzOpsInvalidCharacters -String "microsoft.operationalinsights_workspaces_savedsearches-fgh341_logmanagement(fgh343)_logmanagement|countofiislogentriesbyhostrequestedbyclient.json" + > Remove-AzOpsInvalidCharacter -String "microsoft.operationalinsights_workspaces_savedsearches-fgh341_logmanagement(fgh343)_logmanagement|countofiislogentriesbyhostrequestedbyclient.json" Function returns with the '|' invalid character removed: microsoft.operationalinsights_workspaces_savedsearches-fgh341_logmanagement(fgh343)_logmanagementcountofiislogentriesbyhostrequestedbyclient.json #> [CmdletBinding()] + [OutputType([System.String])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string] @@ -34,20 +35,20 @@ # Check if string contains invalid characters $pattern = $InvalidChars | Out-String -NoNewline if ($String -match "[$pattern]") { - Write-PSFMessage -Level Verbose -String 'Remove-AzOpsInvalidCharacters.Invalid' -StringValues $String -FunctionName 'Remove-AzOpsInvalidCharacters' + Write-PSFMessage -Level Verbose -String 'Remove-AzOpsInvalidCharacter.Invalid' -StringValues $String -FunctionName 'Remove-AzOpsInvalidCharacter' # Arrange string into character array $fileNameChar = $String.ToCharArray() # Iterate over each character in string foreach ($character in $fileNameChar) { # If character exists in invalid array then replace character if ($character -in $InvalidChars) { - Write-PSFMessage -Level Verbose -String 'Remove-AzOpsInvalidCharacters.Removal' -StringValues $character, $String -FunctionName 'Remove-AzOpsInvalidCharacters' + Write-PSFMessage -Level Verbose -String 'Remove-AzOpsInvalidCharacter.Removal' -StringValues $character, $String -FunctionName 'Remove-AzOpsInvalidCharacter' # Remove invalid character $String = $String.Replace($character.ToString(),'') } } } - Write-PSFMessage -Level Verbose -String 'Remove-AzOpsInvalidCharacters.Completed' -StringValues $String -FunctionName 'Remove-AzOpsInvalidCharacters' + Write-PSFMessage -Level Verbose -String 'Remove-AzOpsInvalidCharacter.Completed' -StringValues $String -FunctionName 'Remove-AzOpsInvalidCharacter' # Return processed string return $String } diff --git a/src/internal/functions/Save-AzOpsManagementGroupChildren.ps1 b/src/internal/functions/Save-AzOpsManagementGroupChild.ps1 similarity index 51% rename from src/internal/functions/Save-AzOpsManagementGroupChildren.ps1 rename to src/internal/functions/Save-AzOpsManagementGroupChild.ps1 index bb3726b2..7c7d3e63 100644 --- a/src/internal/functions/Save-AzOpsManagementGroupChildren.ps1 +++ b/src/internal/functions/Save-AzOpsManagementGroupChild.ps1 @@ -1,4 +1,4 @@ -function Save-AzOpsManagementGroupChildren { +function Save-AzOpsManagementGroupChild { <# .SYNOPSIS @@ -14,7 +14,7 @@ .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE - > Save-AzOpsManagementGroupChildren -Scope (New-AzOpsScope -scope /providers/Microsoft.Management/managementGroups/contoso) + > Save-AzOpsManagementGroupChild -Scope (New-AzOpsScope -scope /providers/Microsoft.Management/managementGroups/contoso) Discover Management Group hierarchy from scope .INPUTS AzOpsScope @@ -33,13 +33,13 @@ ) process { - Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Starting' - Invoke-PSFProtectedCommand -ActionString 'Save-AzOpsManagementGroupChildren.Creating.Scope' -Target $Scope -ScriptBlock { + Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChild.Starting' + Invoke-PSFProtectedCommand -ActionString 'Save-AzOpsManagementGroupChild.Creating.Scope' -Target $Scope -ScriptBlock { $scopeObject = New-AzOpsScope -Scope $Scope -StatePath $StatePath -ErrorAction SilentlyContinue -Confirm:$false } -EnableException $true -PSCmdlet $PSCmdlet if (-not $scopeObject) { return } # In case -WhatIf is used - Write-PSFMessage -Level Important -String 'Save-AzOpsManagementGroupChildren.Processing' -StringValues $scopeObject.Scope + Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChild.Processing' -StringValues $scopeObject.Scope # Construct all file paths for scope $scopeStatepath = $scopeObject.StatePath @@ -48,11 +48,11 @@ $statepathScopeDirectory = [IO.Directory]::GetParent($statepathDirectory).ToString() $statepathScopeDirectoryParent = [IO.Directory]::GetParent($statepathScopeDirectory).ToString() - Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.StatePath' -StringValues $scopeStatepath - Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.FileName' -StringValues $statepathFileName - Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.Directory' -StringValues $statepathDirectory - Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.ScopeDirectory' -StringValues $statepathScopeDirectory - Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.ScopeDirectoryParent' -StringValues $statepathScopeDirectoryParent + Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChild.Data.StatePath' -StringValues $scopeStatepath + Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChild.Data.FileName' -StringValues $statepathFileName + Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChild.Data.Directory' -StringValues $statepathDirectory + Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChild.Data.ScopeDirectory' -StringValues $statepathScopeDirectory + Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChild.Data.ScopeDirectoryParent' -StringValues $statepathScopeDirectoryParent # If file is found anywhere in "AzOps.Core.State", ensure that it is at the right scope or else it doesn't matter if (Get-ChildItem -Path $Statepath -File -Recurse -Force | Where-Object Name -eq $statepathFileName) { @@ -63,9 +63,9 @@ if ( ((Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath') -notin '.') -and $exisitingScopePath.Parent.Parent.FullName -ne $statepathScopeDirectoryParent) { if ($exisitingScopePath.Parent.FullName -ne $statepathScopeDirectoryParent) { - Write-PSFMessage -Level Verbose -String 'Save-AzOpsManagementGroupChildren.Moving.Source' -StringValues $exisitingScopePath + Write-PSFMessage -Level Important -String 'Save-AzOpsManagementGroupChild.Moving.Source' -StringValues $exisitingScopePath Move-Item -Path $exisitingScopePath.Parent -Destination $statepathScopeDirectoryParent -Force - Write-PSFMessage -Level Important -String 'Save-AzOpsManagementGroupChildren.Moving.Destination' -StringValues $statepathScopeDirectoryParent + Write-PSFMessage -Level Important -String 'Save-AzOpsManagementGroupChild.Moving.Destination' -StringValues $statepathScopeDirectoryParent } } @@ -77,25 +77,66 @@ Get-ChildItem -Path (Get-ChildItem -Path $Statepath -File -Recurse -Force | Where-Object Name -eq $statepathFileName).Directory -File -Filter 'Microsoft.*' | Move-Item -Destination $statepathDirectory -Force } + # Create empty object for any discovered subscriptions below + $subscriptions = @() + # Based on $scopeObject perform unique logic switch ($scopeObject.Type) { managementGroups { - ConvertTo-AzOpsState -Resource ($script:AzOpsAzManagementGroup | Where-Object { $_.Name -eq $scopeObject.managementgroup }) -ExportPath $scopeObject.statepath -StatePath $StatePath - foreach ($child in $script:AzOpsAzManagementGroup.Where{ $_.Name -eq $scopeObject.managementgroup }.Children) { + ConvertTo-AzOpsState -Resource ($script:AzOpsAzManagementGroup | Where-Object { $_.Name -eq $scopeObject.ManagementGroup }) -ExportPath $scopeObject.StatePath -StatePath $StatePath + foreach ($child in $script:AzOpsAzManagementGroup.Where{ $_.Name -eq $scopeObject.ManagementGroup }.Children) { if ($child.Type -eq '/subscriptions') { - if ($script:AzOpsSubscriptions.id -contains $child.Id) { - Save-AzOpsManagementGroupChildren -Scope $child.Id -StatePath $StatePath + if ($script:AzOpsSubscriptions.Id -contains $child.Id) { + # Subscription discovered at ManagementGroup scope, collect subscription information based on $child object and store information in $subscriptions for later + $subscriptions += Save-AzOpsManagementGroupChild -Scope $child.Id -StatePath $StatePath } else { - Write-PSFMessage -Level Warning -String 'Save-AzOpsManagementGroupChildren.Subscription.NotFound' -StringValues $child.Name + Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChild.Subscription.NotFound' -StringValues $child.Name } } else { - Save-AzOpsManagementGroupChildren -Scope $child.Id -StatePath $StatePath + Save-AzOpsManagementGroupChild -Scope $child.Id -StatePath $StatePath } } } subscriptions { - ConvertTo-AzOpsState -Resource ($script:AzOpsAzManagementGroup.children | Where-Object Name -eq $scopeObject.name) -ExportPath $scopeObject.statepath -StatePath $StatePath + if (($script:AzOpsSubscriptions.Id -contains $scopeObject.Scope) -and ($script:AzOpsAzManagementGroup.Children | Where-Object Name -eq $scopeObject.Name)) { + # Subscription matching conditions found, construct subscription and object statepath into $return and output information to caller + $return = [PSCustomObject]@{ + Subscription = ($script:AzOpsAzManagementGroup.Children | Where-Object Name -eq $scopeObject.Name) + Path = $scopeObject.StatePath + } + return $return + } + } + } + # If $subscriptions exists process all subscriptions with parallel to increase performance during folder/file creation + if ($subscriptions) { + # Prepare Input Data for parallel processing + $runspaceData = @{ + AzOpsPath = "$($script:ModuleRoot)\AzOps.psd1" + StatePath = $StatePath + runspace_AzOpsAzManagementGroup = $script:AzOpsAzManagementGroup + runspace_AzOpsSubscriptions = $script:AzOpsSubscriptions + runspace_AzOpsPartialRoot = $script:AzOpsPartialRoot + runspace_AzOpsResourceProvider = $script:AzOpsResourceProvider + } + $subscriptions | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel { + $subscription = $_ + $runspaceData = $using:runspaceData + + Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1" + $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru + + & $azOps { + $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup + $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions + $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot + $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider + } + + & $azOps { + ConvertTo-AzOpsState -Resource $subscription.Subscription -ExportPath $subscription.Path -StatePath $runspaceData.StatePath + } } } } diff --git a/src/internal/functions/Search-AzOpsAzGraph.ps1 b/src/internal/functions/Search-AzOpsAzGraph.ps1 new file mode 100644 index 00000000..c257ae84 --- /dev/null +++ b/src/internal/functions/Search-AzOpsAzGraph.ps1 @@ -0,0 +1,93 @@ +function Search-AzOpsAzGraph { + + <# + .SYNOPSIS + Search Graph based on input query combined with scope ManagementGroupName or Subscription Id. + Manages paging of results, ensuring completeness of results. + .PARAMETER UseTenantScope + Use Tenant as Scope true or false + .PARAMETER ManagementGroupName + ManagementGroup Name + .PARAMETER Subscription + Subscription Id's + .PARAMETER Query + AzureResourceGraph-Query + .EXAMPLE + > Search-AzOpsAzGraph -ManagementGroupName "5663f39e-feb1-4303-a1f9-cf20b702de61" -Query "policyresources | where type == 'microsoft.authorization/policyassignments'" + Discover all policy assignments deployed at Management Group scope and below + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [switch] + $UseTenantScope, + [Parameter(Mandatory = $false)] + [string] + $ManagementGroupName, + [Parameter(Mandatory = $false)] + [object] + $Subscription, + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object] + $Query + ) + + process { + Write-PSFMessage -Level Verbose -String 'Search-AzOpsAzGraph.Processing' -StringValues $Query + $results = @() + if ($UseTenantScope) { + do { + $processing = Search-AzGraph -UseTenantScope -Query $Query -AllowPartialScope -SkipToken $processing.SkipToken -ErrorAction Stop + $results += $processing + } + while ($processing.SkipToken) + } + if ($ManagementGroupName) { + do { + $processing = Search-AzGraph -ManagementGroup $ManagementGroupName -Query $Query -AllowPartialScope -SkipToken $processing.SkipToken -ErrorAction Stop + $results += $processing + } + while ($processing.SkipToken) + } + if ($Subscription) { + # Create a counter, set the batch size, and prepare a variable for the results + $counter = [PSCustomObject] @{ Value = 0 } + $batchSize = 1000 + # Group subscriptions into batches to conform with graph limits + $subscriptionBatch = $Subscription | Group-Object -Property { [math]::Floor($counter.Value++ / $batchSize) } + foreach ($group in $subscriptionBatch) { + do { + $processing = Search-AzGraph -Subscription ($group.Group).Id -Query $Query -SkipToken $processing.SkipToken -ErrorAction Stop + $results += $processing + } + while ($processing.SkipToken) + } + } + if ($results) { + $resultsType = @() + foreach ($result in $results) { + # Process each graph result and normalize ProviderNamespace casing + foreach ($ResourceProvider in $script:AzOpsResourceProvider) { + if ($ResourceProvider.ProviderNamespace -eq $result.type.Split('/')[0]) { + foreach ($ResourceTypeName in $ResourceProvider.ResourceTypes.ResourceTypeName) { + if ($ResourceTypeName -eq $result.type.Split('/')[1]) { + $result.type = ($result.type).replace($result.type.Split('/')[0],$ResourceProvider.ProviderNamespace) + $result.type = ($result.type).replace($result.type.Split('/')[1],$ResourceTypeName) + $resultsType += $result + break + } + } + break + } + } + } + Write-PSFMessage -Level Verbose -String 'Search-AzOpsAzGraph.Processing.Done' -StringValues $Query + return $resultsType + } + else { + Write-PSFMessage -Level Verbose -String 'Search-AzOpsAzGraph.Processing.NoResult' -StringValues $Query + } + } + +} \ No newline at end of file diff --git a/src/internal/functions/Set-AzOpsStringLength.ps1 b/src/internal/functions/Set-AzOpsStringLength.ps1 index ced5b939..1d0b7e83 100644 --- a/src/internal/functions/Set-AzOpsStringLength.ps1 +++ b/src/internal/functions/Set-AzOpsStringLength.ps1 @@ -17,6 +17,7 @@ #> [CmdletBinding()] + [OutputType([System.String])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string] diff --git a/src/internal/scripts/Initialize.ps1 b/src/internal/scripts/Initialize.ps1 index a0fb9e1d..5715503b 100644 --- a/src/internal/scripts/Initialize.ps1 +++ b/src/internal/scripts/Initialize.ps1 @@ -1,7 +1,5 @@ Set-PSFFeature -Name PSFramework.Stop-PSFFunction.ShowWarning -Value $true -ModuleName AzOps -if (Get-PSFConfigValue -FullName AzOps.Core.AutoInitialize) { - if ([runspace]::DefaultRunspace.Id -eq 1) { - Initialize-AzOpsEnvironment - } -} +if ([runspace]::DefaultRunspace.Id -eq 1) { + Initialize-AzOpsEnvironment +} \ No newline at end of file diff --git a/src/localized/en-us/Strings.psd1 b/src/localized/en-us/Strings.psd1 index 3af89af4..09df5ae0 100644 --- a/src/localized/en-us/Strings.psd1 +++ b/src/localized/en-us/Strings.psd1 @@ -18,8 +18,8 @@ 'Assert-AzOpsBicepDependency.NotFound' = 'Unable to locate bicep binary. Will not be able to deploy bicep templates.' # 'AzOpsScope.GetAzOpsManagementGroupPath.NotFound' = 'Management Group path not found: {0}' # $managementgroupName - 'AzOpsScope.GetAzOpsResourcePath.NotFound' = 'Unable to determine Resource Scope for: {0}' # $this.Scope - 'AzOpsScope.GetAzOpsResourcePath.Retrieving' = 'Getting Resource path for: {0}' # $this.Scope + 'AzOpsScope.GetAzOpsResourcePath.NotFound' = 'Unable to determine resource Scope for: {0}' # $this.Scope + 'AzOpsScope.GetAzOpsResourcePath.Retrieving' = 'Getting resource path for: {0}' # $this.Scope 'AzOpsScope.GetManagementGroupName.Found.Azure' = 'Management Group found in Azure: {0}' # $mgName 'AzOpsScope.GetManagementGroup.NotFound' = 'Management Group does not match any existing in Azure. Assume new resource, using directory name: {0}' # $mgId 'AzOpsScope.GetManagementGroupName.NotFound' = 'Management Group not found in Azure. Trying with directory name instead: {0}' # $mgId @@ -29,10 +29,6 @@ 'AzOpsScope.GetSubscriptionDisplayName.NotFound' = 'Subscription DisplayName not found in Azure. Using directory name instead: {0}' # $subId 'AzOpsScope.Input.FromFileName.ManagementGroup' = 'Determining management group name from file name {0}' # ($children.FullName -join ', ') 'AzOpsScope.Input.FromFileName.Subscription' = 'Determining subscription name from file name {0}' # ($children.FullName -join ', ') - 'AzOpsScope.Input.FromFileName.ResourceGroup' = 'Determining resource group name from file name {0}' # ($children.FullName -join ', ') - 'AzOpsScope.Input.BadData.ManagementGroup' = '{0} does not contain .parameters.input.value.Id' # ($children.FullName -join ', ') - 'AzOpsScope.Input.BadData.ResourceGroup' = 'Invalid Resource Group Data! Validate integrity of {0}' # ($children.FullName -join ', ') - 'AzOpsScope.Input.BadData.Subscription' = 'Invalid Subscription Data! Validate integrity of {0}' # ($children.FullName -join ', ') 'AzOpsScope.Input.BadData.UnknownType' = 'Invalid File Structure! Cannot find Management Group / Subscription / Resource Group files in {0}!' # $Path 'AzOpsScope.Input.BadData.TemplateParameterFile' = 'Unable to determine type from Template or Template Parameter file: {0}' # filename 'AzOpsScope.Constructor' = 'Calling Constructor with value {0}' # scope @@ -52,8 +48,8 @@ 'AzOpsScope.InitializeMemberVariablesFromFile.managementgroups' = 'Determine scope based on ResourceType managementgroups {0}' # ResourceType 'AzOpsScope.InitializeMemberVariablesFromFile.subscriptions' = 'Determine scope based on ResourceType subscriptions {0}' # ResourceType 'AzOpsScope.InitializeMemberVariablesFromFile.resourceGroups' = 'Determine scope based on ResourceType resourceGroups {0}' # ResourceType - 'AzOpsScope.InitializeMemberVariablesFromFile.resource' = 'Determine scope based on Resource Type {0} and Resource Name {1}' # ResourceType and #Resource Name - 'AzOpsScope.ChildResource.InitializeMemberVariables' = 'Determine scope of Child Resource based on Resource Type {0}, Resource Name {1} and Parent ResourceID {2}' # ResourceType, Resource Name, Parent ResourceId + 'AzOpsScope.InitializeMemberVariablesFromFile.resource' = 'Determine scope based on ResourceType {0} and Resource Name {1}' # ResourceType and #Resource Name + 'AzOpsScope.ChildResource.InitializeMemberVariables' = 'Determine scope of Child Resource based on ResourceType {0}, Resource Name {1} and Parent ResourceID {2}' # ResourceType, Resource Name, Parent ResourceId 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepTemplate' = 'Converting Bicep template ({0}) to standard ARM Template JSON ({1})' # $BicepTemplatePath, $transpiledTemplatePath @@ -61,14 +57,10 @@ 'ConvertTo-AzOpsState.Exporting.Default' = 'Exporting input resource to AzOpsState to {0}' # $resourceData.ObjectFilePath 'ConvertTo-AzOpsState.File.Create' = 'AzOpsState file not found. Creating new: {0}' # $ObjectFilePath 'ConvertTo-AzOpsState.File.InvalidCharacter' = 'The specified AzOpsState file contains invalid characters (remove any "[" or "]" characters)! Skipping {0}' # $ObjectFilePath - 'ConvertTo-AzOpsState.File.JQError' = 'Jq filter error {0}' # $Resource.ObjectFilePath 'ConvertTo-AzOpsState.File.UseExisting' = 'AzOpsState file is found. Using existing file: {0}' # $ObjectFilePath 'ConvertTo-AzOpsState.NoExportPath' = 'No export path found for {0}. Ensure the original data type remains intact or specify an -ExportPath' # $Resource 'ConvertTo-AzOpsState.Processing' = 'Processing input: {0}' # $Resource - 'ConvertTo-AzOpsState.ResourceError' = 'Error processing resource: {0}' # $Resource 'ConvertTo-AzOpsState.Starting' = 'Starting conversion to AzOps State object' # - 'ConvertTo-AzOpsState.StateConfig.Error' = 'Cannot load {0}, is the json schema valid and does the file exist?' # (Get-PSFConfigValue -FullName 'AzOps.General.StateConfig') - 'ConvertTo-AzOpsState.StatePath' = 'Resolve path to resource state {0}' # $resourceData.ObjectFilePath 'ConvertTo-AzOpsState.GenerateTemplateParameter' = 'Generating template parameter: {0}' # $generateTemplateParameter 'ConvertTo-AzOpsState.GenerateTemplate' = 'Generating template: {0}' # $generateTemplateParameter 'ConvertTo-AzOpsState.GenerateTemplate.ProviderNamespace' = 'Provider namespace: {0}' # $providerNamespace @@ -89,11 +81,11 @@ 'Get-AzOpsCurrentPrincipal.PrincipalId' = 'Current PrincipalId is {0}' #$principalObject.id 'Get-AzOpsPolicyAssignment.ManagementGroup' = 'Retrieving Policy Assignment for Management Group {0} ({1})' # $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup - 'Get-AzOpsPolicyAssignment.ResourceGroup' = 'Retrieving Policy Assignment for Resource Group {0}' # $ScopeObject.ResourceGroup - 'Get-AzOpsPolicyAssignment.Subscription' = 'Retrieving Policy Assignment for Subscription {0} ({1})' # $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription + 'Get-AzOpsPolicyAssignment.ResourceGroup' = 'Retrieving Policy Assignment for Resource Group in {0} Subscription objects' # $Subscription.count + 'Get-AzOpsPolicyAssignment.Subscription' = 'Retrieving Policy Assignment for {0} Subscription objects' # $Subscription.count 'Get-AzOpsPolicyDefinition.ManagementGroup' = 'Retrieving custom policy definitions for Management Group [{0}] ({1})' # $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup - 'Get-AzOpsPolicyDefinition.Subscription' = 'Retrieving custom policy definitions for Subscription [{0}] ({1})' # $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription + 'Get-AzOpsPolicyDefinition.Subscription' = 'Retrieving custom policy definitions for {0} Subscription objects' # $Subscription.count 'Get-AzOpsPolicyExemption.ManagementGroup' = 'Retrieving Policy Exemption for Management Group {0} ({1})' # $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup 'Get-AzOpsPolicyExemption.ResourceGroup' = 'Retrieving Policy Exemption for Resource Group {0}' # $ScopeObject.ResourceGroup @@ -103,40 +95,31 @@ 'Get-AzOpsResourceLock.Subscription' = 'Retrieving Resource Locks for Subscription {0} ({1})' # $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription 'Get-AzOpsPolicySetDefinition.ManagementGroup' = 'Retrieving PolicySet Definition for ManagementGroup {0} ({1})' # $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup - 'Get-AzOpsPolicySetDefinition.Subscription' = 'Retrieving PolicySet Definition for Subscription {0} ({1})' # $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription + 'Get-AzOpsPolicySetDefinition.Subscription' = 'Retrieving PolicySet Definition for {0} Subscription objects' # $Subscription.count 'Get-AzOpsResourceDefinition.ChildResource.Warning' = 'Failed to export childResources in [{0}]. Warning: [{1}]' # $resourceGroup.ResourceGroupName, $_ 'Get-AzOpsResourceDefinition.Finished' = 'Finished processing scope [{0}]' # $scopeObject.Scope 'Get-AzOpsResourceDefinition.ManagementGroup.Processing' = 'Processing Management Group [{0}] ({1})' # $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup - 'Get-AzOpsResourceDefinition.ManagementGroup.Subscription.NotFound' = 'Unable to locate subscription: {0} within AzOpsSubscriptions object' #child.Name - 'Get-AzOpsResourceDefinition.Processing' = 'Processing scope: [{0}]' # $Scope + 'Get-AzOpsResourceDefinition.Processing' = 'Processing resources at [{0}]' # $Scope 'Get-AzOpsResourceDefinition.Processing.Detail' = 'Processing detail: {0} for [{1}]' # 'Policy Definitions', $scopeObject.Scope - 'Get-AzOpsResourceDefinition.Processing.NotFound' = 'Scope [{0}] not found in Azure or it is excluded' # $Scope - 'Get-AzOpsResourceDefinition.Resource.Processing' = 'Processing Resource [{0}] in Resource Group [{1}]' # $ScopeObject.Resource, $ScopeObject.ResourceGroup - 'Get-AzOpsResourceDefinition.Resource.Processing.Warning' = 'Failed to get resources in Resource Group [{0}]. Consider excluding the resource causing the failure with [Core.SkipResourceType] setting [{1}]' # $resourceGroup.ResourceGroupName, $_ - 'Get-AzOpsResourceDefinition.Resource.Processing.Failed' = 'Unable to process Resource [{0}] in Resource Group [{1]' # $ScopeObject.Resource, $ScopeObject.ResourceGroup - 'Get-AzOpsResourceDefinition.ResourceGroup.Processing' = 'Processing Resource Group [{0}] in Subscription [{1}] ({2})' # $ScopeObject.Resourcegroup, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription - 'Get-AzOpsResourceDefinition.ResourceGroup.Processing.Error' = 'Failed to access Resource Group [{0}] in Subscription [{1}] ({2})' # $ScopeObject.Resourcegroup, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription - 'Get-AzOpsResourceDefinition.ResourceGroup.Processing.Owned' = 'Skipping {0} as it is managed by {1}' # $resourceGroup.ResourceGroupName, $resourceGroup.ManagedBy - 'Get-AzOpsResourceDefinition.Subscription.Found' = 'Found Subscription: {0} ({1})' # $scopeObject.subscriptionDisplayName, $scopeObject.subscription - 'Get-AzOpsResourceDefinition.Subscription.NoResourceGroup' = 'No non-managed Resource Group found in Subscription [{0}] ({1})' # $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription - 'Get-AzOpsResourceDefinition.Subscription.OdataFilter' = 'Setting Odatafilter: {0}' # $odataFilter + 'Get-AzOpsResourceDefinition.Processing.NotFound' = 'Scope [{0}] not found in Azure or is excluded' # $Scope + 'Get-AzOpsResourceDefinition.NoResourceGroup' = 'No non-managed Resource Group found in [{0}])' # $scopeObject.Name 'Get-AzOpsResourceDefinition.Subscription.Processing' = 'Processing Subscription [{0}] ({1})' # $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription - 'Get-AzOpsResourceDefinition.Subscription.Processing.IncludeResourcesInRG' = 'Resources in Resource Group [{0}] is skipped due to IncludeResourcesInResourceGroup' # $resourceGroup.ResourceGroupName - 'Get-AzOpsResourceDefinition.Subscription.Processing.Resource' = 'Processing Resource [{0}] in Resource Group [{1}]' # $resource.Name, $resourceGroup.ResourceGroupName - 'Get-AzOpsResourceDefinition.Subscription.Processing.ResourceGroup' = 'Processing Resource Group [{0}]' # $resourceGroup.ResourceGroupName - 'Get-AzOpsResourceDefinition.Subscription.Processing.ResourceGroup.NoResources' = 'No resources found in Resource Group [{0}]' # $resourceGroup.ResourceGroupName - 'Get-AzOpsResourceDefinition.Subscription.Processing.ResourceGroup.Resources' = 'Searching for resources in Resource Group [{0}]' # $resourceGroup.ResourceGroupName - 'Get-AzOpsResourceDefinition.Subscription.SkippingResourceGroup' = 'SkipResourceGroup switch used, skipping Resource Group discovery' # - 'Get-AzOpsResourceDefinition.Subscription.ExcludeResourceGroup' = 'Subscription is skipped due to SubscriptionsToIncludeResourceGroups' # - 'Get-AzOpsResourceDefinition.Subscription.SkippingResources' = 'Resources are skipped in resource group due to SkipResource.' # - 'Get-AzOpsResourceDefinition.Subscription.Processing.ChildResource' = 'Processing Resource [{0}] in Resource Group [{1}]' # $resource.Name, $resourceGroup.ResourceGroupName - 'Get-AzOpsResourceDefinition.Subscription.SkippingChildResource' = 'Child Resources are skipped in resource group {0} due to SkipChildResource' # $resourceGroup.ResourceGroupName - 'Get-AzOpsRoleAssignment.Assignment' = 'Found assignment {0} for role {1}' # $roleAssignment.DisplayName, $roleAssignment.RoleDefinitionName + 'Get-AzOpsResourceDefinition.Subscription.NotFound' = 'No Subscription found to process Resource Groups in' # + 'Get-AzOpsResourceDefinition.Processing.Resource' = 'Processing resource [{0}] in resource Group [{1}]' # $resource.Name, $resourceGroup.ResourceGroupName + 'Get-AzOpsResourceDefinition.Processing.Resource.Discovery' = 'Searching for resources in [{0}]' # $scopeObject.Name + 'Get-AzOpsResourceDefinition.Processing.Resource.Discovery.NotFound' = 'No resources found in [{0}]' # $scopeObject.Name + 'Get-AzOpsResourceDefinition.Processing.Resource.Warning' = 'Failed to get resources in {0}]. Consider excluding the resource causing the failure with [Core.SkipResourceType] setting' # $scopeObject.Name + 'Get-AzOpsResourceDefinition.SkippingResourceGroup' = 'SkipResourceGroup switch used, skipping resource Group discovery' # + 'Get-AzOpsResourceDefinition.SkippingResources' = 'Resources are skipped in due to SkipResource.' # + 'Get-AzOpsResourceDefinition.Processing.ChildResource' = 'Processing resource [{0}] in resource Group [{1}]' # $resource.Name, $resourceGroup.ResourceGroupName + 'Get-AzOpsResourceDefinition.SkippingChildResources' = 'Child resources are skipped, due to SkipChildResource' # + + 'Get-AzOpsRoleAssignment.Assignment' = 'Found assignment {0} for role {1}' # $roleAssignment.id, $roleAssignment.properties.roleDefinitionId 'Get-AzOpsRoleAssignment.Processing' = 'Retrieving Role Assignments at scope {0}' # $ScopeObject - 'Get-AzOpsRoleDefinition.NonAuthorative' = 'Role Definition {0} exists at {1} however it is not authoritative. Current authoritative scope is {2}' # $roledefinition.Id, Id, $ScopeObject.Scope, $roledefinition.AssignableScopes[0] - 'Get-AzOpsRoleDefinition.Processing' = 'Processing {0}' # $ScopeObject + 'Get-AzOpsRoleDefinition.Processing' = 'Processing scope {0}' # $ScopeObject + 'Get-AzOpsRoleDefinition.Definition' = 'Processing object {0}' # $roleDefinition.id 'Get-AzOpsRoleEligibilityScheduleRequest.Processing' = 'Retrieving Privileged Identity Management RoleEligibilitySchedule at [{0}]' # $ScopeObject.Scope 'Get-AzOpsRoleEligibilityScheduleRequest.Assignment' = 'Found Privileged Identity Management RoleEligibilityScheduleRequest assignment [{0}]' # $roleEligibilitySchedule.Name @@ -164,8 +147,8 @@ 'Invoke-AzOpsPull.Deleting.State' = 'Removing state in {0}' # $StatePath 'Invoke-AzOpsPull.Duration' = 'AzOps repository setup completed in {0}' # $stopWatch.Elapsed 'Invoke-AzOpsPull.Initialization.Completed' = 'Completed preparations for the AzOps repository setup' # - 'Invoke-AzOpsPull.Initialization.Starting' = 'Starting preparations for the AzOps repository setup' # 'Invoke-AzOpsPull.Migration.Required' = 'Migration from previous repository state IS required' # + 'Invoke-AzOpsPull.Building.State' = 'Building AzOpsState structure recursively at {0}' # $StatePath 'Invoke-AzOpsPull.Rebuilding.State' = 'Rebuilding state in {0}' # $StatePath 'Invoke-AzOpsPull.Tenant' = 'Connected to tenant {0}' # $tenantId 'Invoke-AzOpsPull.TemplateParameterFileSuffix' = 'Template parameter file suffix {0}' # $TemplateParameterFileSuffix @@ -173,10 +156,14 @@ 'Invoke-AzOpsPull.Validating.AADP2.Success' = 'Azure AD P2 licensing validated' # 'Invoke-AzOpsPull.Validating.AADP2.Failed' = 'Azure AD P2 licensing not found' # 'Invoke-AzOpsPull.Validating.UserRole' = 'Asserting fundamental Azure access' # - 'Invoke-AzOpsPull.Validating.UserRole.Failed' = 'Insufficient access to Azure AD. Role Assignments will be pulled, but not enriched with additional data such as DisplayName and ObjectType' # + 'Invoke-AzOpsPull.Validating.UserRole.Failed' = 'Insufficient access to Azure AD. Privileged Identity Management information will not be pulled' # 'Invoke-AzOpsPull.Validating.UserRole.Success' = 'Azure access validated' # 'Invoke-AzOpsPull.Validating.ResourceGroupDiscovery.Failed' = 'SkipResource set to false or SkipChildResource set to false requires SkipResourceGroup to be set to false. Change value for SkipResourceGroup and retry operation. {0} https://github.com/azure/azops/wiki/settings' # + 'Invoke-AzOpsRestMethod.Processing' = 'Invoke-AzRestMethod processing path: [{0}]' # $Path + 'Invoke-AzOpsRestMethod.Processing.Warning' = 'Invoke-AzRestMethod received [{0}] while processing: [{1}]' # $_, $Path + 'Invoke-AzOpsRestMethod.Processing.RateLimit' = 'Invoke-AzRestMethod is throttled while processing: [{0}], going to sleep for {1} seconds' # $Path, $_.value + 'Invoke-AzOpsPush.Change.AddModify' = 'Adding or modifying:' # 'Invoke-AzOpsPush.Change.AddModify.File' = ' {0}' # $item 'Invoke-AzOpsPush.Change.Delete' = 'Deleting:' # @@ -213,7 +200,7 @@ 'New-AzOpsDeployment.ManagementGroup.Processing' = 'Attempting [Management Group] deployment in [{0}] for {1}' # $defaultDeploymentRegion, $scopeObject 'New-AzOpsDeployment.Processing' = 'Processing deployment {0} for template {1} with parameter "{2}" in mode {3}' # $DeploymentName, $TemplateFilePath, $TemplateParameterFilePath, $Mode - 'New-AzOpsDeployment.ResourceGroup.Processing' = 'Attempting [Resource Group] deployment for {0}' # $scopeObject + 'New-AzOpsDeployment.ResourceGroup.Processing' = 'Attempting [resource Group] deployment for {0}' # $scopeObject 'New-AzOpsDeployment.Root.Processing' = 'Attempting [Tenant Scope] deployment in [{0}] for {1}' # $defaultDeploymentRegion, $scopeObject 'New-AzOpsDeployment.Scope.Empty' = 'Unable to determine the scope of template {0} and parameters {1}' # $TemplateFilePath, $TemplateParameterFilePath 'New-AzOpsDeployment.Scope.Failed' = 'Failed to resolve the scope for template {0} and parameters {1}' # $TemplateFilePath, $TemplateParameterFilePath @@ -251,38 +238,39 @@ 'Register-AzOpsResourceProvider.Provider.Register' = 'Registering provider {0}' # $resourceprovider.ProviderNamespace 'Remove-AzOpsDeployment.Processing' = 'Processing removal {0} for template {1}' # $removeJobName, $TemplateFilePath - 'Remove-AzOpsDeployment.Metadata.Failed' = 'Detected custom template: {0} . Resource Deletion is currently only supported for AzOps Generated templates' #$TemplateFilePath + 'Remove-AzOpsDeployment.Metadata.Failed' = 'Detected custom template: {0} . resource Deletion is currently only supported for AzOps Generated templates' #$TemplateFilePath 'Remove-AzOpsDeployment.Metadata.Success' = 'Processing AzOps Generated Template File {0}' # $TemplateFilePath 'Remove-AzOpsDeployment.Scope.Failed' = 'Failed to resolve the scope for template {0}' # $TemplateFilePath 'Remove-AzOpsDeployment.Scope.Empty' = 'Unable to determine the scope of template {0}' # $TemplateFilePath 'Remove-AzOpsDeployment.SkipDueToWhatIf' = 'Skipping removal of resource due to WhatIf' # - 'Remove-AzOpsDeployment.ResourceDependencyNested' = 'Resource dependency {0} for complete deletion of {1} is outside of supported AzOps scope. Please remove this dependency in Azure without AzOps.'# $roleAssignmentId, $policyAssignment.ResourceId + 'Remove-AzOpsDeployment.ResourceDependencyNested' = 'resource dependency {0} for complete deletion of {1} is outside of supported AzOps scope. Please remove this dependency in Azure without AzOps.'# $roleAssignmentId, $policyAssignment.ResourceId 'Remove-AzOpsDeployment.ResourceDependencyNotFound' = 'Missing resource dependency {0} for successfull deletion of {1}. Please add missing resource and retry.'# $resource.ResourceId, $scopeObject.Scope - 'Remove-AzOpsDeployment.ResourceNotFound' = 'Unable to find resource of type {0} with id {1}.'# $scopeObject.Resource, $scopeObject.scope, $resultsError + 'Remove-AzOpsDeployment.ResourceNotFound' = 'Unable to find resource of type {0} with id {1}.'# $scopeObject.resource, $scopeObject.scope, $resultsError 'Remove-AzOpsDeployment.SkipUnsupportedResource' = 'Deletion is currently only supported for policyAssignments, policyDefinitions, policyExemptions, policySetDefinitions and roleAssignments. Will NOT proceed with deletion of file {0}'# $templateFilePath - 'Remove-AzOpsInvalidCharacters.Completed' = 'Valid string: {0}'# $String - 'Remove-AzOpsInvalidCharacters.Invalid' = 'Invalid character detected in string: {0}, further processing initiated'# $String - 'Remove-AzOpsInvalidCharacters.Removal' = 'Removed invalid character: {0} from string: {1}'# $character, $String - - 'Save-AzOpsManagementGroupChildren.Creating.Scope' = 'Creating scope object' # - 'Save-AzOpsManagementGroupChildren.Data.Directory' = 'Resolved state path directory: {0}' # $statepathDirectory - 'Save-AzOpsManagementGroupChildren.Data.FileName' = 'Resolved state path filename: {0}' # $statepathFileName - 'Save-AzOpsManagementGroupChildren.Data.ScopeDirectory' = 'Resolved state path scope directory: {0}' # $statepathScopeDirectory - 'Save-AzOpsManagementGroupChildren.Data.ScopeDirectoryParent' = 'Resolved state path scope directory parent: {0}' # $statepathScopeDirectoryParent - 'Save-AzOpsManagementGroupChildren.Data.StatePath' = 'Resolved state path: {0}' # $scopeStatepath - 'Save-AzOpsManagementGroupChildren.Moving.Destination' = 'Moved existing state file to: {0}' # $statepathScopeDirectoryParent - 'Save-AzOpsManagementGroupChildren.Moving.Source' = 'Found existing state file in directory: {0}' # $exisitingScopePath - 'Save-AzOpsManagementGroupChildren.New.File' = 'Creating new state file: {0}' # $statepathFileName - 'Save-AzOpsManagementGroupChildren.Processing' = 'Processing Scope: {0}' # $scopeObject.Scope - 'Save-AzOpsManagementGroupChildren.Starting' = 'Starting execution' # - 'Save-AzOpsManagementGroupChildren.Subscription.NotFound' = 'Unable to locate subscription: {0} within AzOpsSubscriptions object' #child.Name + 'Remove-AzOpsInvalidCharacter.Completed' = 'Valid string: {0}'# $String + 'Remove-AzOpsInvalidCharacter.Invalid' = 'Invalid character detected in string: {0}, further processing initiated'# $String + 'Remove-AzOpsInvalidCharacter.Removal' = 'Removed invalid character: {0} from string: {1}'# $character, $String + + 'Save-AzOpsManagementGroupChild.Creating.Scope' = 'Creating scope object' # + 'Save-AzOpsManagementGroupChild.Data.Directory' = 'Resolved state path directory: {0}' # $statepathDirectory + 'Save-AzOpsManagementGroupChild.Data.FileName' = 'Resolved state path filename: {0}' # $statepathFileName + 'Save-AzOpsManagementGroupChild.Data.ScopeDirectory' = 'Resolved state path scope directory: {0}' # $statepathScopeDirectory + 'Save-AzOpsManagementGroupChild.Data.ScopeDirectoryParent' = 'Resolved state path scope directory parent: {0}' # $statepathScopeDirectoryParent + 'Save-AzOpsManagementGroupChild.Data.StatePath' = 'Resolved state path: {0}' # $scopeStatepath + 'Save-AzOpsManagementGroupChild.Moving.Destination' = 'Moved existing state file to: {0}' # $statepathScopeDirectoryParent + 'Save-AzOpsManagementGroupChild.Moving.Source' = 'Found existing state file in directory: {0}' # $exisitingScopePath + 'Save-AzOpsManagementGroupChild.Processing' = 'Processing Scope: {0}' # $scopeObject.Scope + 'Save-AzOpsManagementGroupChild.Starting' = 'Starting execution' # + 'Save-AzOpsManagementGroupChild.Subscription.NotFound' = 'Unable to locate subscription: {0} within AzOpsSubscriptions object' #child.Name + + 'Search-AzOpsAzGraph.Processing' = 'AzGraph processing query: [{0}]' # $Query + 'Search-AzOpsAzGraph.Processing.Done' = 'AzGraph completed processing of query: [{0}]' # $Query + 'Search-AzOpsAzGraph.Processing.NoResult' = 'AzGraph found nothing with query: [{0}]' # $Query 'Set-AzOpsContext.Change' = 'Changing active subscription from {0} to {1} ({2})' # $context.Subscription.Name, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription - 'Set-AzOpsStringLength.IsString' = 'Is string {0}' # $String 'Set-AzOpsStringLength.Shortened' = 'New shortened string {0} in-line with limit of {1}' # $String, $MaxStringLength - 'Set-AzOpsStringLength.StringIsPath' = 'String contains state path {0}' # $String 'Set-AzOpsStringLength.ToLong' = 'String {0} exceeding limit of {1} by {2} characters' # $String, $MaxStringLength, $overSize 'Set-AzOpsStringLength.WithInLimit' = 'String {0} within limit of {1}' # $String diff --git a/src/tests/functional/Functional.Tests.ps1 b/src/tests/functional/Functional.Tests.ps1 index 4425e9db..a6e72e53 100644 --- a/src/tests/functional/Functional.Tests.ps1 +++ b/src/tests/functional/Functional.Tests.ps1 @@ -50,6 +50,8 @@ try { Write-PSFMessage -Level Verbose -Message "Executing deploy of functional test object: $_" -FunctionName "BeforeAll" & $_ } + # Pause for resource consistency + Start-Sleep -Seconds 120 } catch { Write-PSFMessage -Level Warning -String "Executing functional test object failed" @@ -73,6 +75,7 @@ $deploymentLocationId = (Get-FileHash -Algorithm SHA256 -InputStream ([IO.Memory Write-PSFMessage -Level Verbose -Message "Generating folder structure" -FunctionName "BeforeAll" try { + Initialize-AzOpsEnvironment Invoke-AzOpsPull -SkipRole:$false -SkipPolicy:$false -SkipResource:$false -SkipResourceGroup:$false } catch { diff --git a/src/tests/functional/Microsoft.Authorization/policyAssignments/scenario.ps1 b/src/tests/functional/Microsoft.Authorization/policyAssignments/scenario.ps1 index be368053..0b438c66 100644 --- a/src/tests/functional/Microsoft.Authorization/policyAssignments/scenario.ps1 +++ b/src/tests/functional/Microsoft.Authorization/policyAssignments/scenario.ps1 @@ -79,7 +79,7 @@ Describe "Scenario - policyAssignments" { $script:functionalTestDeploy.ProvisioningState | Should -Be "Succeeded" } It "Resource properties PolicyDefinitionId should exist" { - $script:fileContents.resources[0].properties.PolicyDefinitionId | Should -BeTrue + $script:fileContents.resources[0].properties.policyDefinitionId | Should -BeTrue } #endregion Pull Test diff --git a/src/tests/functional/Microsoft.Authorization/roleAssignments/scenario.ps1 b/src/tests/functional/Microsoft.Authorization/roleAssignments/scenario.ps1 index 23d774e9..28b1f9be 100644 --- a/src/tests/functional/Microsoft.Authorization/roleAssignments/scenario.ps1 +++ b/src/tests/functional/Microsoft.Authorization/roleAssignments/scenario.ps1 @@ -79,10 +79,7 @@ Describe "Scenario - roleAssignments" { $script:functionalTestDeploy.ProvisioningState | Should -Be "Succeeded" } It "Resource properties RoleDefinitionId should exist" { - $script:fileContents.resources[0].properties.RoleDefinitionId | Should -BeTrue - } - It "Resource properties RoleDefinitionName should exist" { - $script:fileContents.resources[0].properties.RoleDefinitionName | Should -BeTrue + $script:fileContents.resources[0].properties.roleDefinitionId | Should -BeTrue } #endregion Pull Test diff --git a/src/tests/functional/Microsoft.Compute/virtualMachines/deploy/deploy.json b/src/tests/functional/Microsoft.Compute/virtualMachines/deploy/deploy.json index b03d2d12..1572e690 100644 --- a/src/tests/functional/Microsoft.Compute/virtualMachines/deploy/deploy.json +++ b/src/tests/functional/Microsoft.Compute/virtualMachines/deploy/deploy.json @@ -1,5 +1,5 @@ { - "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "location": { @@ -56,6 +56,9 @@ }, "enableHotpatching": { "type": "bool" + }, + "zone": { + "type": "array" } }, "variables": { @@ -116,6 +119,7 @@ "type": "Microsoft.Compute/virtualMachines", "apiVersion": "2021-07-01", "location": "[parameters('location')]", + "zones": "[parameters('zone')]", "dependsOn": [ "[concat('Microsoft.Network/networkInterfaces/', parameters('networkInterfaceName'))]" ], diff --git a/src/tests/functional/Microsoft.Compute/virtualMachines/deploy/deploy.parameters.json b/src/tests/functional/Microsoft.Compute/virtualMachines/deploy/deploy.parameters.json index 250c29d2..8bbc9486 100644 --- a/src/tests/functional/Microsoft.Compute/virtualMachines/deploy/deploy.parameters.json +++ b/src/tests/functional/Microsoft.Compute/virtualMachines/deploy/deploy.parameters.json @@ -61,6 +61,9 @@ }, "enableHotpatching": { "value": false + }, + "zone": { + "value": ["2"] } } } \ No newline at end of file diff --git a/src/tests/functional/Microsoft.Compute/virtualMachines/scenario.ps1 b/src/tests/functional/Microsoft.Compute/virtualMachines/scenario.ps1 index 6026c983..99b83c90 100644 --- a/src/tests/functional/Microsoft.Compute/virtualMachines/scenario.ps1 +++ b/src/tests/functional/Microsoft.Compute/virtualMachines/scenario.ps1 @@ -57,6 +57,9 @@ Describe "Scenario - virtualMachines" { It "Resource apiVersion should exist" { $script:fileContents.resources[0].apiVersion | Should -BeTrue } + It "Resource zones should exist" { + $script:fileContents.resources[0].zones | Should -BeTrue + } It "Resource properties should exist" { $script:fileContents.resources[0].properties | Should -BeTrue } diff --git a/src/tests/functional/Microsoft.Network/azureFirewalls/deploy/deploy.json b/src/tests/functional/Microsoft.Network/azureFirewalls/deploy/deploy.json index 9f95fc06..84ec1d99 100644 --- a/src/tests/functional/Microsoft.Network/azureFirewalls/deploy/deploy.json +++ b/src/tests/functional/Microsoft.Network/azureFirewalls/deploy/deploy.json @@ -22,6 +22,9 @@ }, "publicIpAddressName": { "type": "string" + }, + "zones": { + "type": "array" } }, "resources": [ @@ -30,6 +33,7 @@ "type": "Microsoft.Network/publicIpAddresses", "name": "[parameters('publicIpAddressName')]", "location": "[parameters('location')]", + "zones": "[parameters('zones')]", "sku": { "name": "Standard" }, @@ -81,6 +85,7 @@ "type": "Microsoft.Network/azureFirewalls", "name": "[parameters('azureFirewallName')]", "location": "[parameters('location')]", + "zones": "[parameters('zones')]", "dependsOn": [ "[resourceId('Microsoft.Network/publicIpAddresses', parameters('publicIpAddressName'))]", "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]", diff --git a/src/tests/functional/Microsoft.Network/azureFirewalls/deploy/deploy.parameters.json b/src/tests/functional/Microsoft.Network/azureFirewalls/deploy/deploy.parameters.json index a34b5345..5d309e5b 100644 --- a/src/tests/functional/Microsoft.Network/azureFirewalls/deploy/deploy.parameters.json +++ b/src/tests/functional/Microsoft.Network/azureFirewalls/deploy/deploy.parameters.json @@ -22,6 +22,13 @@ }, "publicIpAddressName": { "value": "AzOpsFw-pip1" + }, + "zones": { + "value": [ + "1", + "2", + "3" + ] } } } \ No newline at end of file diff --git a/src/tests/functional/Microsoft.Network/azureFirewalls/scenario.ps1 b/src/tests/functional/Microsoft.Network/azureFirewalls/scenario.ps1 index acba0333..c84aa053 100644 --- a/src/tests/functional/Microsoft.Network/azureFirewalls/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/azureFirewalls/scenario.ps1 @@ -54,6 +54,9 @@ Describe "Scenario - azureFirewalls" { It "Resource apiVersion should exist" { $script:fileContents.resources[0].apiVersion | Should -BeTrue } + It "Resource zones should exist" { + $script:fileContents.resources[0].zones | Should -BeTrue + } It "Resource properties should exist" { $script:fileContents.resources[0].properties | Should -BeTrue } diff --git a/src/tests/functional/Microsoft.Network/connections/deploy/deploy.json b/src/tests/functional/Microsoft.Network/connections/deploy/deploy.json index 30dcc496..4e7d3310 100644 --- a/src/tests/functional/Microsoft.Network/connections/deploy/deploy.json +++ b/src/tests/functional/Microsoft.Network/connections/deploy/deploy.json @@ -24,7 +24,7 @@ "type": "Microsoft.Network/localNetworkGateways", "apiVersion": "2020-11-01", "name": "[parameters('localNetworkGatewayName')]", - "location": "centralindia", + "location": "northeurope", "properties": { "localNetworkAddressSpace": { "addressPrefixes": [ @@ -38,7 +38,7 @@ "type": "Microsoft.Network/publicIPAddresses", "apiVersion": "2020-11-01", "name": "[parameters('publicIPAddressesName')]", - "location": "centralindia", + "location": "northeurope", "sku": { "name": "Basic", "tier": "Regional" @@ -50,7 +50,7 @@ "idleTimeoutInMinutes": 4, "dnsSettings": { "domainNameLabel": "[parameters('publicIPAddressesName')]", - "fqdn": "[concat(parameters('publicIPAddressesName'), '.centralindia.cloudapp.azure.com')]" + "fqdn": "[concat(parameters('publicIPAddressesName'), '.northeurope.cloudapp.azure.com')]" }, "ipTags": [] } @@ -59,7 +59,7 @@ "type": "Microsoft.Network/virtualNetworks", "apiVersion": "2020-11-01", "name": "[parameters('virtualNetworkName')]", - "location": "centralindia", + "location": "northeurope", "properties": { "addressSpace": { "addressPrefixes": [ @@ -95,12 +95,11 @@ "privateLinkServiceNetworkPolicies": "Enabled" } }, - { "type": "Microsoft.Network/connections", "apiVersion": "2020-11-01", "name": "[parameters('connectionName')]", - "location": "centralindia", + "location": "northeurope", "dependsOn": [ "[resourceId('Microsoft.Network/virtualNetworkGateways', parameters('virtualNetworkGatewayName'))]", "[resourceId('Microsoft.Network/localNetworkGateways', parameters('localNetworkGatewayName'))]" @@ -130,7 +129,7 @@ "type": "Microsoft.Network/virtualNetworkGateways", "apiVersion": "2020-11-01", "name": "[parameters('virtualNetworkGatewayName')]", - "location": "centralindia", + "location": "northeurope", "dependsOn": [ "[resourceId('Microsoft.Network/publicIPAddresses', parameters('publicIPAddressesName'))]", "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), 'GatewaySubnet')]" diff --git a/src/tests/functional/Microsoft.Network/privateDnsZones/deploy/deploy.json b/src/tests/functional/Microsoft.Network/privateDnsZones/deploy/deploy.json index 24b746d9..d7a65ba8 100644 --- a/src/tests/functional/Microsoft.Network/privateDnsZones/deploy/deploy.json +++ b/src/tests/functional/Microsoft.Network/privateDnsZones/deploy/deploy.json @@ -14,15 +14,7 @@ "metadata": { "description": "Optional. The location of the PrivateDNSZone. Should be global." } - }, - "tags": { - "type": "object", - "defaultValue": { - }, - "metadata": { - "description": "Optional. Tags of the resource." - } - } + } }, "variables": { }, "resources": [ @@ -30,8 +22,7 @@ "type": "Microsoft.Network/privateDnsZones", "apiVersion": "2018-09-01", "name": "[parameters('privateDnsZoneName')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]" + "location": "[parameters('location')]" } ], "outputs": { diff --git a/src/tests/general/PSScriptAnalyzer.Tests.ps1 b/src/tests/general/PSScriptAnalyzer.Tests.ps1 index 10e0c737..1d55d983 100644 --- a/src/tests/general/PSScriptAnalyzer.Tests.ps1 +++ b/src/tests/general/PSScriptAnalyzer.Tests.ps1 @@ -4,7 +4,7 @@ Param ( $SkipTest, [string[]] - $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions") + $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions", "$global:testroot\..\..\scripts") ) if ($SkipTest) { return } @@ -18,7 +18,7 @@ Describe 'Invoking PSScriptAnalyzer against commandbase' { foreach ($file in $commandFiles) { Context "Analyzing $($file.BaseName)" { - $analysis = Invoke-ScriptAnalyzer -Path $file.FullName -ExcludeRule PSAvoidTrailingWhitespace, PSShouldProcess + $analysis = Invoke-ScriptAnalyzer -Path $file.FullName -ExcludeRule PSShouldProcess forEach ($rule in $scriptAnalyzerRules) { diff --git a/src/tests/general/Strings.Tests.ps1 b/src/tests/general/Strings.Tests.ps1 index 6e552173..74625074 100644 --- a/src/tests/general/Strings.Tests.ps1 +++ b/src/tests/general/Strings.Tests.ps1 @@ -8,8 +8,12 @@ Describe "Testing localization strings" { $moduleRoot = (Get-Module AzOps).ModuleBase + $scripts = "$moduleRoot\..\scripts" + $strings = Get-PSFLocalizedString -Module AzOps $stringsResults = Export-PSMDString -ModuleRoot $moduleRoot $exceptions = & "$global:testroot\general\Strings.Exceptions.ps1" + $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.ps1" + $allFiles += Get-ChildItem -Path $scripts -Recurse | Where-Object Name -like "*.ps1" foreach ($stringEntry in $stringsResults) { if ($stringEntry.String -eq "key") { continue } # Skipping the template default entry @@ -20,4 +24,10 @@ Describe "Testing localization strings" { $stringEntry.Text | Should -Not -BeNullOrEmpty } } + + foreach ($string in $($strings.Keys)) { + It "Should be used: $string" -TestCases @{ allFiles = $allFiles; string = $string } { + Select-String -Path $allFiles -Pattern $string | Should -Not -BeNullOrEmpty + } + } } \ No newline at end of file diff --git a/src/tests/integration/Repository.Tests.ps1 b/src/tests/integration/Repository.Tests.ps1 index 89ade4dc..0fb27661 100644 --- a/src/tests/integration/Repository.Tests.ps1 +++ b/src/tests/integration/Repository.Tests.ps1 @@ -1,5 +1,4 @@ - -<# +<# Repository.Tests.ps1 The tests within this file validate that the `Invoke-AzOpsPull` function is invoking as expected with the correct output data. This file must be invoked by the Pester.ps1 file as the Global variable testroot is required for invocation. @@ -66,8 +65,10 @@ Describe "Repository" { Location = "northeurope" } try { - New-AzSubscriptionDeployment -Name 'AzOps-Tests-rbacdep' -Location northeurope -TemplateFile "$($global:testRoot)/templates/rbactest.bicep" + New-AzSubscriptionDeployment -Name 'AzOps-Tests-rbacdep' -Location northeurope -TemplateFile "$($global:testRoot)/templates/rbactest.bicep" -TemplateParameterFile "$($global:testRoot)/templates/rbactest.parameters.json" New-AzManagementGroupDeployment @params + # Pause for resource consistency + Start-Sleep -Seconds 120 } catch { Write-PSFMessage -Level Critical -Message "Deployment of repository test failed" -Exception $_.Exception @@ -146,6 +147,7 @@ Describe "Repository" { Set-PSFConfig -FullName AzOps.Core.State -Value $partialMgDiscoveryRootgeneratedRoot Write-PSFMessage -Level Verbose -Message "Generating folder structure for PartialMgDiscoveryRoot" -FunctionName "BeforeAll" try { + Initialize-AzOpsEnvironment Invoke-AzOpsPull -SkipLock:$true -SkipPim:$true -SkipResourceGroup:$true -SkipPolicy:$true -SkipRole:$true -SkipChildResource:$true -SkipResource:$true } catch { @@ -165,6 +167,7 @@ Describe "Repository" { Write-PSFMessage -Level Verbose -Message "Generating folder structure" -FunctionName "BeforeAll" try { + Initialize-AzOpsEnvironment Invoke-AzOpsPull -SkipLock:$false -SkipRole:$false -SkipPolicy:$false -SkipResource:$false } catch { @@ -600,6 +603,10 @@ Describe "Repository" { $fileContents = Get-Content -Path $script:policyAssignmentsFile -Raw | ConvertFrom-Json -Depth 25 $fileContents.resources[0].properties.scope | Should -Be "$($script:managementManagementGroup.Id)" } + It "Policy Assignments custom metadata property should exist" { + $fileContents = Get-Content -Path $script:policyAssignmentsFile -Raw | ConvertFrom-Json -Depth 25 + $fileContents.resources[0].properties.metadata.customkey | Should -BeTrue + } It "Policy Assignments deployment should be successful" { $script:policyAssignmentDeployment = Get-AzManagementGroupDeployment -ManagementGroupId $script:managementManagementGroup.Name -Name $script:policyAssignmentsDeploymentName $policyAssignmentDeployment.ProvisioningState | Should -Be "Succeeded" @@ -619,7 +626,7 @@ Describe "Repository" { } It "Policy Definitions resource type should exist" { $fileContents = Get-Content -Path $script:policyDefinitionsFile -Raw | ConvertFrom-Json -Depth 25 - $fileContents.parameters.input.value.ResourceType | Should -BeTrue + $fileContents.parameters.input.value.type | Should -BeTrue } It "Policy Definitions resource name should exist" { $fileContents = Get-Content -Path $script:policyDefinitionsFile -Raw | ConvertFrom-Json -Depth 25 @@ -631,7 +638,7 @@ Describe "Repository" { } It "Policy Definitions resource type should match" { $fileContents = Get-Content -Path $script:policyDefinitionsFile -Raw | ConvertFrom-Json -Depth 25 - $fileContents.parameters.input.value.ResourceType | Should -Be "Microsoft.Authorization/policyDefinitions" + $fileContents.parameters.input.value.type | Should -Be "Microsoft.Authorization/policyDefinitions" } It "Policy Definitions deployment should be successful" { $script:policyDefinitionDeployment = Get-AzManagementGroupDeployment -ManagementGroupId $script:testManagementGroup.Name -Name $script:policyDefinitionsDeploymentName @@ -657,7 +664,7 @@ Describe "Repository" { } It "PolicySetDefinitions resource type should exist" { $fileContents = Get-Content -Path $script:policySetDefinitionsFile -Raw | ConvertFrom-Json -Depth 25 - $fileContents.parameters.input.value.ResourceType | Should -BeTrue + $fileContents.parameters.input.value.type | Should -BeTrue } It "PolicySetDefinitions resource name should exist" { $fileContents = Get-Content -Path $script:policySetDefinitionsFile -Raw | ConvertFrom-Json -Depth 25 @@ -669,7 +676,7 @@ Describe "Repository" { } It "PolicySetDefinitions resource type should match" { $fileContents = Get-Content -Path $script:policySetDefinitionsFile -Raw | ConvertFrom-Json -Depth 25 - $fileContents.parameters.input.value.ResourceType | Should -Be "Microsoft.Authorization/policySetDefinitions" + $fileContents.parameters.input.value.type | Should -Be "Microsoft.Authorization/policySetDefinitions" } It "PolicySetDefinitions deployment should be successful" { $script:policySetDefinitionDeployment = Get-AzManagementGroupDeployment -ManagementGroupId $script:testManagementGroup.Name -Name $script:policySetDefinitionsDeploymentName diff --git a/src/tests/templates/azuredeploy.jsonc b/src/tests/templates/azuredeploy.jsonc index b8afc0f0..6aadd429 100644 --- a/src/tests/templates/azuredeploy.jsonc +++ b/src/tests/templates/azuredeploy.jsonc @@ -417,6 +417,9 @@ "apiVersion": "2021-06-01", "name": "TestPolicyAssignment", "properties": { + "metadata": { + "customkey": "customvalue", + }, "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/0a914e76-4921-4c19-b460-a2d36003525a" } } diff --git a/src/tests/templates/biceptest.bicep b/src/tests/templates/biceptest.bicep index ea66f709..a16f3fce 100644 --- a/src/tests/templates/biceptest.bicep +++ b/src/tests/templates/biceptest.bicep @@ -1,10 +1,11 @@ targetScope = 'subscription' @description('Name of the resource group') param resourceGroupName string +param location string resource myRg 'Microsoft.Resources/resourceGroups@2021-04-01' = { name: resourceGroupName - location: 'northeurope' + location: location tags: {} properties: {} } diff --git a/src/tests/templates/biceptest.parameters.json b/src/tests/templates/biceptest.parameters.json index 1d83b99d..63dd3b9d 100644 --- a/src/tests/templates/biceptest.parameters.json +++ b/src/tests/templates/biceptest.parameters.json @@ -4,6 +4,9 @@ "parameters": { "resourceGroupName": { "value": "bicepTest-azopsrg" + }, + "location": { + "value": "northeurope" } } } \ No newline at end of file diff --git a/src/tests/templates/rbactest.bicep b/src/tests/templates/rbactest.bicep index 9d9a123f..d0f4de3e 100644 --- a/src/tests/templates/rbactest.bicep +++ b/src/tests/templates/rbactest.bicep @@ -1,10 +1,12 @@ -param policyAssignmentName string = 'AzOpsDep2 - audit-vm-manageddisks' -param policyDefinitionID string = '/providers/Microsoft.Authorization/policyDefinitions/06a78e20-9358-41c9-923c-fb736d382a4d' +param policyAssignmentName string +param policyDefinitionID string +param location string +param roleDefinitionId string targetScope = 'subscription' resource assignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = { name: policyAssignmentName - location: 'northeurope' + location: location identity: { type: 'SystemAssigned' } @@ -18,6 +20,6 @@ resource roleassignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { properties: { principalId: assignment.identity.principalId principalType: 'ServicePrincipal' - roleDefinitionId: '/providers/microsoft.authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7' + roleDefinitionId: roleDefinitionId } } diff --git a/src/tests/templates/rbactest.parameters.json b/src/tests/templates/rbactest.parameters.json new file mode 100644 index 00000000..18c9f6fc --- /dev/null +++ b/src/tests/templates/rbactest.parameters.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "policyAssignmentName": { + "value": "AzOpsDep2 - audit-vm-manageddisks" + }, + "policyDefinitionID": { + "value": "/providers/Microsoft.Authorization/policyDefinitions/06a78e20-9358-41c9-923c-fb736d382a4d" + }, + "location": { + "value": "northeurope" + }, + "roleDefinitionId": { + "value": "/providers/microsoft.authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" + } + } +} \ No newline at end of file