Skip to content

Instantly share code, notes, and snippets.

@hectorddmx
Last active July 2, 2026 15:10
Show Gist options
  • Select an option

  • Save hectorddmx/e6b2fdc6374626b4344cbf20c5739b81 to your computer and use it in GitHub Desktop.

Select an option

Save hectorddmx/e6b2fdc6374626b4344cbf20c5739b81 to your computer and use it in GitHub Desktop.
Get-AzurePresalesDiscovery.ps1
#requires -Version 7.0
[CmdletBinding()]
param(
[string[]]$SubscriptionId,
[string]$OutputPath = (Join-Path (Get-Location) ("azure-presales-discovery-{0}" -f (Get-Date -Format "yyyyMMdd-HHmmss"))),
[switch]$Connect,
[switch]$IncludeAppConfigNames,
[switch]$IncludeDiagnosticSettings,
[switch]$IncludeEntra,
[switch]$NoArchive,
[switch]$ValidateOnly
)
$ErrorActionPreference = "Stop"
function Write-Section {
param([string]$Message)
Write-Host ""
Write-Host "=== $Message ==="
}
function Get-SafeName {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) {
return "unknown"
}
return ($Value -replace '[^a-zA-Z0-9._-]', '_')
}
function Get-ObjectProperty {
param(
[AllowNull()]$InputObject,
[string]$Name
)
if ($null -eq $InputObject) {
return $null
}
$Property = $InputObject.PSObject.Properties[$Name]
if ($null -eq $Property) {
return $null
}
return $Property.Value
}
function Get-ObjectProperties {
param([AllowNull()]$InputObject)
if ($null -eq $InputObject) {
return @()
}
if ($InputObject -is [System.Collections.IDictionary]) {
return @($InputObject.GetEnumerator() | ForEach-Object {
[pscustomobject]@{
Name = [string]$_.Key
Value = $_.Value
}
})
}
return @($InputObject.PSObject.Properties | ForEach-Object {
[pscustomobject]@{
Name = $_.Name
Value = $_.Value
}
})
}
function Get-ObjectPropertyNames {
param([AllowNull()]$InputObject)
return @(Get-ObjectProperties -InputObject $InputObject | Select-Object -ExpandProperty Name | Sort-Object)
}
function ConvertTo-Array {
param([AllowNull()]$Value)
if ($null -eq $Value) {
return @()
}
return @($Value)
}
function Get-UriHost {
param([AllowNull()][string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) {
return $null
}
try {
return ([System.Uri]$Value).Host
}
catch {
return $null
}
}
function Get-ResourceIdLeaf {
param([AllowNull()][string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) {
return $null
}
return Split-Path -Leaf $Value
}
function New-SafeResourceSummary {
param([object]$Resource)
$Sku = Get-ObjectProperty $Resource "Sku"
[pscustomobject]@{
Name = Get-ObjectProperty $Resource "Name"
ResourceType = Get-ObjectProperty $Resource "ResourceType"
ResourceGroupName = Get-ObjectProperty $Resource "ResourceGroupName"
Location = Get-ObjectProperty $Resource "Location"
Kind = Get-ObjectProperty $Resource "Kind"
Sku = Get-ObjectProperty $Sku "Name"
Tags = Get-ObjectProperty $Resource "Tags"
ResourceId = Get-ObjectProperty $Resource "ResourceId"
}
}
function Export-Json {
param(
[string]$Path,
[AllowNull()]$InputObject
)
$Parent = Split-Path -Parent $Path
New-Item -ItemType Directory -Force -Path $Parent | Out-Null
$Json = $InputObject | ConvertTo-Json -Depth 40
if ($null -eq $Json) {
$Json = "null"
}
Set-Content -Path $Path -Value $Json -Encoding utf8
}
function Export-StepError {
param(
[string]$Name,
[string]$Directory,
[System.Management.Automation.ErrorRecord]$ErrorRecord
)
$ErrorPath = Join-Path $Directory ("{0}.error.json" -f $Name)
Export-Json -Path $ErrorPath -InputObject ([pscustomobject]@{
Name = $Name
Message = $ErrorRecord.Exception.Message
Category = $ErrorRecord.CategoryInfo.Category
TargetName = $ErrorRecord.CategoryInfo.TargetName
})
}
function Invoke-DiscoveryStep {
param(
[string]$Name,
[string]$Directory,
[scriptblock]$ScriptBlock
)
Write-Host "Collecting $Name"
try {
$Result = & $ScriptBlock
Export-Json -Path (Join-Path $Directory ("{0}.json" -f $Name)) -InputObject $Result
return $Result
}
catch {
Write-Warning ("Failed to collect {0}: {1}" -f $Name, $_.Exception.Message)
Export-StepError -Name $Name -Directory $Directory -ErrorRecord $_
return $null
}
}
function Test-ModuleAvailable {
param([string]$Name)
$Module = Get-Module -ListAvailable -Name $Name | Sort-Object Version -Descending | Select-Object -First 1
if ($null -eq $Module) {
throw "Required PowerShell module '$Name' is not installed."
}
[pscustomobject]@{
Name = $Module.Name
Version = $Module.Version.ToString()
Path = $Module.Path
}
}
function Test-Prerequisites {
$MinimumVersion = [version]"7.0"
if ($PSVersionTable.PSVersion -lt $MinimumVersion) {
throw "PowerShell $MinimumVersion or later is required. Current version is $($PSVersionTable.PSVersion)."
}
$RequiredModules = @("Az.Accounts", "Az.Resources")
$Modules = foreach ($ModuleName in $RequiredModules) {
Test-ModuleAvailable -Name $ModuleName
}
[pscustomobject]@{
PowerShellVersion = $PSVersionTable.PSVersion.ToString()
PSEdition = $PSVersionTable.PSEdition
Platform = $PSVersionTable.Platform
RequiredModules = $Modules
}
}
function Import-RequiredModules {
Import-Module Az.Accounts -ErrorAction Stop
Import-Module Az.Resources -ErrorAction Stop
}
function Select-ResourcesByType {
param(
[AllowNull()]$Resources,
[string]$ResourceType
)
return @($Resources | Where-Object { $_.ResourceType -eq $ResourceType })
}
function Convert-VirtualNetwork {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$AddressSpace = Get-ObjectProperty $Properties "addressSpace"
$Subnets = foreach ($Subnet in (ConvertTo-Array (Get-ObjectProperty $Properties "subnets"))) {
$SubnetProperties = Get-ObjectProperty $Subnet "properties"
[pscustomobject]@{
Name = Get-ObjectProperty $Subnet "name"
AddressPrefix = Get-ObjectProperty $SubnetProperties "addressPrefix"
AddressPrefixes = Get-ObjectProperty $SubnetProperties "addressPrefixes"
PrivateEndpointNetworkPolicies = Get-ObjectProperty $SubnetProperties "privateEndpointNetworkPolicies"
Delegations = foreach ($Delegation in (ConvertTo-Array (Get-ObjectProperty $SubnetProperties "delegations"))) {
$DelegationProperties = Get-ObjectProperty $Delegation "properties"
Get-ObjectProperty $DelegationProperties "serviceName"
}
}
}
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
AddressPrefixes = Get-ObjectProperty $AddressSpace "addressPrefixes"
Subnets = $Subnets
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-NetworkSecurityGroup {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$Rules = foreach ($Rule in (ConvertTo-Array (Get-ObjectProperty $Properties "securityRules"))) {
$RuleProperties = Get-ObjectProperty $Rule "properties"
[pscustomobject]@{
Name = Get-ObjectProperty $Rule "name"
Priority = Get-ObjectProperty $RuleProperties "priority"
Direction = Get-ObjectProperty $RuleProperties "direction"
Access = Get-ObjectProperty $RuleProperties "access"
Protocol = Get-ObjectProperty $RuleProperties "protocol"
SourceAddressPrefix = Get-ObjectProperty $RuleProperties "sourceAddressPrefix"
SourceAddressPrefixes = Get-ObjectProperty $RuleProperties "sourceAddressPrefixes"
DestinationAddressPrefix = Get-ObjectProperty $RuleProperties "destinationAddressPrefix"
DestinationAddressPrefixes = Get-ObjectProperty $RuleProperties "destinationAddressPrefixes"
DestinationPortRange = Get-ObjectProperty $RuleProperties "destinationPortRange"
DestinationPortRanges = Get-ObjectProperty $RuleProperties "destinationPortRanges"
}
}
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
SecurityRules = $Rules
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-PublicIpAddress {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$DnsSettings = Get-ObjectProperty $Properties "dnsSettings"
$Sku = Get-ObjectProperty $Resource "Sku"
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
Sku = Get-ObjectProperty $Sku "Name"
IpAddress = Get-ObjectProperty $Properties "ipAddress"
PublicIPAllocationMethod = Get-ObjectProperty $Properties "publicIPAllocationMethod"
Fqdn = Get-ObjectProperty $DnsSettings "fqdn"
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-PrivateEndpoint {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$Subnet = Get-ObjectProperty $Properties "subnet"
$Connections = foreach ($Connection in (ConvertTo-Array (Get-ObjectProperty $Properties "privateLinkServiceConnections"))) {
$ConnectionProperties = Get-ObjectProperty $Connection "properties"
[pscustomobject]@{
Name = Get-ObjectProperty $Connection "name"
PrivateLinkServiceId = Get-ObjectProperty $ConnectionProperties "privateLinkServiceId"
GroupIds = Get-ObjectProperty $ConnectionProperties "groupIds"
}
}
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
SubnetId = Get-ObjectProperty $Subnet "id"
PrivateLinkServiceConnections = $Connections
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-WebSite {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$Identity = Get-ObjectProperty $Resource "Identity"
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
Kind = $Resource.Kind
State = Get-ObjectProperty $Properties "state"
DefaultHostName = Get-ObjectProperty $Properties "defaultHostName"
HttpsOnly = Get-ObjectProperty $Properties "httpsOnly"
ServerFarmId = Get-ObjectProperty $Properties "serverFarmId"
IdentityType = Get-ObjectProperty $Identity "Type"
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-AppServicePlan {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$Sku = Get-ObjectProperty $Resource "Sku"
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
Kind = $Resource.Kind
SkuName = Get-ObjectProperty $Sku "Name"
SkuTier = Get-ObjectProperty $Sku "Tier"
Reserved = Get-ObjectProperty $Properties "reserved"
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-ContainerApp {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$Configuration = Get-ObjectProperty $Properties "configuration"
$Ingress = Get-ObjectProperty $Configuration "ingress"
$Identity = Get-ObjectProperty $Resource "Identity"
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
EnvironmentId = Get-ObjectProperty $Properties "managedEnvironmentId"
LatestRevisionName = Get-ObjectProperty $Properties "latestRevisionName"
ActiveRevisionsMode = Get-ObjectProperty $Configuration "activeRevisionsMode"
IngressExternal = Get-ObjectProperty $Ingress "external"
IdentityType = Get-ObjectProperty $Identity "Type"
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-AksCluster {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$ApiServerAccessProfile = Get-ObjectProperty $Properties "apiServerAccessProfile"
$NetworkProfile = Get-ObjectProperty $Properties "networkProfile"
$Identity = Get-ObjectProperty $Resource "Identity"
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
KubernetesVersion = Get-ObjectProperty $Properties "kubernetesVersion"
DnsPrefix = Get-ObjectProperty $Properties "dnsPrefix"
PrivateCluster = Get-ObjectProperty $ApiServerAccessProfile "enablePrivateCluster"
NodeResourceGroup = Get-ObjectProperty $Properties "nodeResourceGroup"
NetworkPlugin = Get-ObjectProperty $NetworkProfile "networkPlugin"
NetworkPolicy = Get-ObjectProperty $NetworkProfile "networkPolicy"
IdentityType = Get-ObjectProperty $Identity "Type"
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-VirtualMachine {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$HardwareProfile = Get-ObjectProperty $Properties "hardwareProfile"
$StorageProfile = Get-ObjectProperty $Properties "storageProfile"
$OsDisk = Get-ObjectProperty $StorageProfile "osDisk"
$ImageReference = Get-ObjectProperty $StorageProfile "imageReference"
$NetworkProfile = Get-ObjectProperty $Properties "networkProfile"
$NetworkInterfaces = foreach ($NetworkInterface in (ConvertTo-Array (Get-ObjectProperty $NetworkProfile "networkInterfaces"))) {
Get-ObjectProperty $NetworkInterface "id"
}
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
VmSize = Get-ObjectProperty $HardwareProfile "vmSize"
OsType = Get-ObjectProperty $OsDisk "osType"
ImagePublisher = Get-ObjectProperty $ImageReference "publisher"
ImageOffer = Get-ObjectProperty $ImageReference "offer"
ImageSku = Get-ObjectProperty $ImageReference "sku"
NetworkInterfaceIds = $NetworkInterfaces
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-VirtualMachineScaleSet {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$Sku = Get-ObjectProperty $Resource "Sku"
$UpgradePolicy = Get-ObjectProperty $Properties "upgradePolicy"
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
SkuName = Get-ObjectProperty $Sku "Name"
SkuTier = Get-ObjectProperty $Sku "Tier"
Capacity = Get-ObjectProperty $Sku "Capacity"
UpgradePolicyMode = Get-ObjectProperty $UpgradePolicy "mode"
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-StorageAccount {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$Sku = Get-ObjectProperty $Resource "Sku"
$NetworkAcls = Get-ObjectProperty $Properties "networkAcls"
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
Kind = $Resource.Kind
Sku = Get-ObjectProperty $Sku "Name"
AccessTier = Get-ObjectProperty $Properties "accessTier"
AllowBlobPublicAccess = Get-ObjectProperty $Properties "allowBlobPublicAccess"
MinimumTlsVersion = Get-ObjectProperty $Properties "minimumTlsVersion"
PublicNetworkAccess = Get-ObjectProperty $Properties "publicNetworkAccess"
NetworkDefaultAction = Get-ObjectProperty $NetworkAcls "defaultAction"
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-SqlServer {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
FullyQualifiedDomainName = Get-ObjectProperty $Properties "fullyQualifiedDomainName"
PublicNetworkAccess = Get-ObjectProperty $Properties "publicNetworkAccess"
MinimalTlsVersion = Get-ObjectProperty $Properties "minimalTlsVersion"
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-SqlDatabase {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$Sku = Get-ObjectProperty $Resource "Sku"
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
ServerName = ($Resource.Name -split "/")[0]
SkuName = Get-ObjectProperty $Sku "Name"
SkuTier = Get-ObjectProperty $Sku "Tier"
Collation = Get-ObjectProperty $Properties "collation"
MaxSizeBytes = Get-ObjectProperty $Properties "maxSizeBytes"
Status = Get-ObjectProperty $Properties "status"
ZoneRedundant = Get-ObjectProperty $Properties "zoneRedundant"
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-KeyVault {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$NetworkAcls = Get-ObjectProperty $Properties "networkAcls"
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
TenantId = Get-ObjectProperty $Properties "tenantId"
EnableRbacAuthorization = Get-ObjectProperty $Properties "enableRbacAuthorization"
EnabledForDeployment = Get-ObjectProperty $Properties "enabledForDeployment"
EnabledForTemplateDeployment = Get-ObjectProperty $Properties "enabledForTemplateDeployment"
PublicNetworkAccess = Get-ObjectProperty $Properties "publicNetworkAccess"
NetworkDefaultAction = Get-ObjectProperty $NetworkAcls "defaultAction"
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-MonitorActionGroup {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$EmailReceivers = foreach ($Receiver in (ConvertTo-Array (Get-ObjectProperty $Properties "emailReceivers"))) {
[pscustomobject]@{
Name = Get-ObjectProperty $Receiver "name"
Status = Get-ObjectProperty $Receiver "status"
}
}
$WebhookReceivers = foreach ($Receiver in (ConvertTo-Array (Get-ObjectProperty $Properties "webhookReceivers"))) {
[pscustomobject]@{
Name = Get-ObjectProperty $Receiver "name"
ServiceUriSet = $null -ne (Get-ObjectProperty $Receiver "serviceUri")
}
}
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
Enabled = Get-ObjectProperty $Properties "enabled"
GroupShortName = Get-ObjectProperty $Properties "groupShortName"
EmailReceivers = $EmailReceivers
WebhookReceivers = $WebhookReceivers
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-WorkflowOperationMetadata {
param([AllowNull()]$Operations)
foreach ($OperationProperty in (Get-ObjectProperties -InputObject $Operations)) {
$Operation = $OperationProperty.Value
[pscustomobject]@{
Name = $OperationProperty.Name
Type = Get-ObjectProperty $Operation "type"
Kind = Get-ObjectProperty $Operation "kind"
RunAfterNames = Get-ObjectPropertyNames -InputObject (Get-ObjectProperty $Operation "runAfter")
}
}
}
function Convert-LogicAppWorkflow {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$Definition = Get-ObjectProperty $Properties "definition"
$Identity = Get-ObjectProperty $Resource "Identity"
$Sku = Get-ObjectProperty $Resource "Sku"
$IntegrationAccount = Get-ObjectProperty $Properties "integrationAccount"
$EndpointConfiguration = Get-ObjectProperty $Properties "endpointsConfiguration"
$NameParts = @($Resource.Name -split "/")
$Triggers = @(Convert-WorkflowOperationMetadata -Operations (Get-ObjectProperty $Definition "triggers"))
$Actions = @(Convert-WorkflowOperationMetadata -Operations (Get-ObjectProperty $Definition "actions"))
[pscustomobject]@{
Name = $Resource.Name
SiteName = if ($NameParts.Count -gt 1) { $NameParts[0] } else { $null }
WorkflowName = if ($NameParts.Count -gt 1) { $NameParts[-1] } else { $Resource.Name }
ResourceType = $Resource.ResourceType
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
Kind = $Resource.Kind
State = Get-ObjectProperty $Properties "state"
FlowState = Get-ObjectProperty $Properties "flowState"
ProvisioningState = Get-ObjectProperty $Properties "provisioningState"
CreatedTime = Get-ObjectProperty $Properties "createdTime"
ChangedTime = Get-ObjectProperty $Properties "changedTime"
Version = Get-ObjectProperty $Properties "version"
Sku = Get-ObjectProperty $Sku "Name"
IntegrationAccountId = Get-ObjectProperty $IntegrationAccount "id"
AccessEndpointHost = Get-UriHost -Value (Get-ObjectProperty $Properties "accessEndpoint")
EndpointConfigurationKeys = Get-ObjectPropertyNames -InputObject $EndpointConfiguration
TriggerCount = $Triggers.Count
Triggers = $Triggers
ActionCount = $Actions.Count
Actions = $Actions
DefinitionParameterNames = Get-ObjectPropertyNames -InputObject (Get-ObjectProperty $Definition "parameters")
ConfiguredParameterNames = Get-ObjectPropertyNames -InputObject (Get-ObjectProperty $Properties "parameters")
IdentityType = Get-ObjectProperty $Identity "Type"
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-ApiConnection {
param([object]$Resource)
$Properties = Get-ObjectProperty $Resource "Properties"
$Api = Get-ObjectProperty $Properties "api"
$Statuses = foreach ($Status in (ConvertTo-Array (Get-ObjectProperty $Properties "statuses"))) {
[pscustomobject]@{
Status = Get-ObjectProperty $Status "status"
}
}
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
ApiId = Get-ObjectProperty $Api "id"
ApiName = Get-ResourceIdLeaf -Value (Get-ObjectProperty $Api "id")
DisplayName = Get-ObjectProperty $Properties "displayName"
CreatedTime = Get-ObjectProperty $Properties "createdTime"
ChangedTime = Get-ObjectProperty $Properties "changedTime"
OverallStatus = Get-ObjectProperty $Properties "overallStatus"
Statuses = $Statuses
ParameterValueNames = Get-ObjectPropertyNames -InputObject (Get-ObjectProperty $Properties "parameterValues")
CustomParameterValueNames = Get-ObjectPropertyNames -InputObject (Get-ObjectProperty $Properties "customParameterValues")
TestLinksPresent = (ConvertTo-Array (Get-ObjectProperty $Properties "testLinks")).Count -gt 0
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Convert-GenericResourceWithSafeProperties {
param(
[object]$Resource,
[string[]]$PropertyNames
)
$Properties = Get-ObjectProperty $Resource "Properties"
$SelectedProperties = [ordered]@{}
foreach ($PropertyName in $PropertyNames) {
$SelectedProperties[$PropertyName] = Get-ObjectProperty $Properties $PropertyName
}
[pscustomobject]@{
Name = $Resource.Name
ResourceGroupName = $Resource.ResourceGroupName
Location = $Resource.Location
Kind = $Resource.Kind
Sku = Get-ObjectProperty (Get-ObjectProperty $Resource "Sku") "Name"
Properties = [pscustomobject]$SelectedProperties
Tags = $Resource.Tags
ResourceId = $Resource.ResourceId
}
}
function Get-AppConfigNames {
param([object[]]$SiteResources)
$Results = foreach ($Site in $SiteResources) {
$BaseId = $Site.ResourceId
foreach ($ConfigName in @("appsettings", "connectionstrings")) {
try {
$Response = Invoke-AzRestMethod -Method POST -Path ("{0}/config/{1}/list?api-version=2023-12-01" -f $BaseId, $ConfigName)
$Content = $Response.Content | ConvertFrom-Json
$Properties = Get-ObjectProperty $Content "properties"
$Names = foreach ($Property in $Properties.PSObject.Properties) {
$PropertyValue = $Property.Value
if ($ConfigName -eq "connectionstrings") {
[pscustomobject]@{
Name = $Property.Name
Type = Get-ObjectProperty $PropertyValue "type"
HasValue = $null -ne (Get-ObjectProperty $PropertyValue "value")
LooksLikeKeyVaultReference = $false
}
}
else {
$Value = [string]$PropertyValue
[pscustomobject]@{
Name = $Property.Name
Type = $null
HasValue = -not [string]::IsNullOrEmpty($Value)
LooksLikeKeyVaultReference = $Value.Contains("@Microsoft.KeyVault")
}
}
}
[pscustomobject]@{
SiteName = $Site.Name
ResourceGroupName = $Site.ResourceGroupName
ResourceId = $Site.ResourceId
ConfigType = $ConfigName
Names = $Names
}
}
catch {
[pscustomobject]@{
SiteName = $Site.Name
ResourceGroupName = $Site.ResourceGroupName
ResourceId = $Site.ResourceId
ConfigType = $ConfigName
Error = $_.Exception.Message
}
}
}
}
return $Results
}
function Get-DiagnosticSettings {
param([object[]]$Resources)
if ($null -eq (Get-Command Get-AzDiagnosticSetting -ErrorAction SilentlyContinue)) {
return [pscustomobject]@{
Error = "Get-AzDiagnosticSetting is unavailable. Install or import Az.Monitor to collect diagnostic settings."
}
}
$Results = foreach ($Resource in $Resources) {
try {
$Settings = Get-AzDiagnosticSetting -ResourceId $Resource.ResourceId -ErrorAction Stop
foreach ($Setting in (ConvertTo-Array $Settings)) {
[pscustomobject]@{
ResourceName = $Resource.Name
ResourceType = $Resource.ResourceType
ResourceGroupName = $Resource.ResourceGroupName
ResourceId = $Resource.ResourceId
Name = $Setting.Name
WorkspaceId = $Setting.WorkspaceId
StorageAccountId = $Setting.StorageAccountId
EventHubAuthorizationRuleId = $Setting.EventHubAuthorizationRuleId
Logs = $Setting.Log
Metrics = $Setting.Metric
}
}
}
catch {
[pscustomobject]@{
ResourceName = $Resource.Name
ResourceType = $Resource.ResourceType
ResourceGroupName = $Resource.ResourceGroupName
ResourceId = $Resource.ResourceId
Error = $_.Exception.Message
}
}
}
return $Results
}
function Get-EntraSummary {
$Summary = [ordered]@{}
if ($null -ne (Get-Command Get-AzADUser -ErrorAction SilentlyContinue)) {
try {
$Summary["SignedInUsers"] = @(Get-AzADUser -SignedIn -ErrorAction Stop | Select-Object DisplayName, UserPrincipalName, Id)
}
catch {
$Summary["SignedInUsersError"] = $_.Exception.Message
}
}
if ($null -ne (Get-Command Get-AzADServicePrincipal -ErrorAction SilentlyContinue)) {
try {
$Summary["ServicePrincipals"] = @(Get-AzADServicePrincipal -First 1000 -ErrorAction Stop | Select-Object DisplayName, AppId, Id, ServicePrincipalType, AccountEnabled)
}
catch {
$Summary["ServicePrincipalsError"] = $_.Exception.Message
}
}
if ($null -ne (Get-Command Get-AzADApplication -ErrorAction SilentlyContinue)) {
try {
$Summary["Applications"] = @(Get-AzADApplication -First 1000 -ErrorAction Stop | Select-Object DisplayName, AppId, Id, SignInAudience, IdentifierUris)
}
catch {
$Summary["ApplicationsError"] = $_.Exception.Message
}
}
return [pscustomobject]$Summary
}
Write-Section "Prerequisites"
$Prerequisites = Test-Prerequisites
$Prerequisites | ConvertTo-Json -Depth 10
if ($ValidateOnly) {
Write-Host "ValidateOnly completed. No Azure tenant calls were made."
return
}
Import-RequiredModules
if ($Connect) {
Connect-AzAccount -ErrorAction Stop | Out-Null
}
$Context = Get-AzContext -ErrorAction SilentlyContinue
if ($null -eq $Context) {
throw "No Azure context is active. Run Connect-AzAccount first, or rerun this script with -Connect."
}
New-Item -ItemType Directory -Force -Path $OutputPath | Out-Null
Export-Json -Path (Join-Path $OutputPath "prerequisites.json") -InputObject $Prerequisites
Export-Json -Path (Join-Path $OutputPath "current-context.json") -InputObject ([pscustomobject]@{
Account = $Context.Account.Id
Tenant = $Context.Tenant.Id
Subscription = $Context.Subscription.Id
Environment = $Context.Environment.Name
})
$Subscriptions = if ($SubscriptionId -and $SubscriptionId.Count -gt 0) {
foreach ($Id in $SubscriptionId) {
Get-AzSubscription -SubscriptionId $Id -ErrorAction Stop
}
}
else {
Get-AzSubscription -ErrorAction Stop | Where-Object { $_.State -eq "Enabled" }
}
Export-Json -Path (Join-Path $OutputPath "subscriptions.json") -InputObject (
$Subscriptions | Select-Object Name, Id, TenantId, State
)
foreach ($Subscription in $Subscriptions) {
Write-Section ("Subscription {0} ({1})" -f $Subscription.Name, $Subscription.Id)
Set-AzContext -SubscriptionId $Subscription.Id -ErrorAction Stop | Out-Null
$SubscriptionRoot = Join-Path $OutputPath (Get-SafeName $Subscription.Id)
New-Item -ItemType Directory -Force -Path $SubscriptionRoot | Out-Null
Export-Json -Path (Join-Path $SubscriptionRoot "subscription.json") -InputObject (
$Subscription | Select-Object Name, Id, TenantId, State
)
Invoke-DiscoveryStep -Name "locations" -Directory $SubscriptionRoot -ScriptBlock {
Get-AzLocation | Select-Object Location, DisplayName, RegionType, GeographyGroup, PhysicalLocation
} | Out-Null
Invoke-DiscoveryStep -Name "resource-groups" -Directory $SubscriptionRoot -ScriptBlock {
Get-AzResourceGroup | Select-Object ResourceGroupName, Location, Tags, ResourceId, ManagedBy, ProvisioningState
} | Out-Null
Write-Host "Collecting resources-expanded-in-memory"
try {
$ExpandedResources = @(Get-AzResource -ExpandProperties)
}
catch {
Write-Warning ("Failed to collect expanded resources: {0}" -f $_.Exception.Message)
Export-StepError -Name "resources-expanded-in-memory" -Directory $SubscriptionRoot -ErrorRecord $_
$ExpandedResources = @()
}
$Resources = Invoke-DiscoveryStep -Name "resources-summary" -Directory $SubscriptionRoot -ScriptBlock {
$ExpandedResources | ForEach-Object { New-SafeResourceSummary -Resource $_ }
}
Invoke-DiscoveryStep -Name "resource-count-by-type" -Directory $SubscriptionRoot -ScriptBlock {
$Resources | Group-Object ResourceType | Sort-Object Count -Descending | ForEach-Object {
[pscustomobject]@{
ResourceType = $_.Name
Count = $_.Count
}
}
} | Out-Null
Invoke-DiscoveryStep -Name "role-assignments" -Directory $SubscriptionRoot -ScriptBlock {
Get-AzRoleAssignment -Scope ("/subscriptions/{0}" -f $Subscription.Id) | Select-Object DisplayName, SignInName, ObjectType, RoleDefinitionName, Scope
} | Out-Null
Invoke-DiscoveryStep -Name "policy-assignments" -Directory $SubscriptionRoot -ScriptBlock {
Get-AzPolicyAssignment | Select-Object Name, DisplayName, Scope, PolicyDefinitionId, EnforcementMode
} | Out-Null
$NetworkRoot = Join-Path $SubscriptionRoot "network"
Invoke-DiscoveryStep -Name "virtual-networks" -Directory $NetworkRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Network/virtualNetworks" | ForEach-Object { Convert-VirtualNetwork -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "network-security-groups" -Directory $NetworkRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Network/networkSecurityGroups" | ForEach-Object { Convert-NetworkSecurityGroup -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "public-ip-addresses" -Directory $NetworkRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Network/publicIPAddresses" | ForEach-Object { Convert-PublicIpAddress -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "private-endpoints" -Directory $NetworkRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Network/privateEndpoints" | ForEach-Object { Convert-PrivateEndpoint -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "private-dns-zones" -Directory $NetworkRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Network/privateDnsZones" | ForEach-Object {
Convert-GenericResourceWithSafeProperties -Resource $_ -PropertyNames @("numberOfRecordSets")
}
} | Out-Null
$ComputeRoot = Join-Path $SubscriptionRoot "compute"
Invoke-DiscoveryStep -Name "virtual-machines" -Directory $ComputeRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Compute/virtualMachines" | ForEach-Object { Convert-VirtualMachine -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "virtual-machine-scale-sets" -Directory $ComputeRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Compute/virtualMachineScaleSets" | ForEach-Object { Convert-VirtualMachineScaleSet -Resource $_ }
} | Out-Null
$AppRoot = Join-Path $SubscriptionRoot "app-hosting"
$Sites = Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Web/sites"
Invoke-DiscoveryStep -Name "app-service-plans" -Directory $AppRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Web/serverFarms" | ForEach-Object { Convert-AppServicePlan -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "web-and-function-apps" -Directory $AppRoot -ScriptBlock {
$Sites | ForEach-Object { Convert-WebSite -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "container-apps" -Directory $AppRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.App/containerApps" | ForEach-Object { Convert-ContainerApp -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "container-app-environments" -Directory $AppRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.App/managedEnvironments" | ForEach-Object {
Convert-GenericResourceWithSafeProperties -Resource $_ -PropertyNames @("vnetConfiguration", "appLogsConfiguration", "staticIp")
}
} | Out-Null
Invoke-DiscoveryStep -Name "aks-clusters" -Directory $AppRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.ContainerService/managedClusters" | ForEach-Object { Convert-AksCluster -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "container-registries" -Directory $AppRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.ContainerRegistry/registries" | ForEach-Object {
Convert-GenericResourceWithSafeProperties -Resource $_ -PropertyNames @("loginServer", "adminUserEnabled", "publicNetworkAccess")
}
} | Out-Null
if ($IncludeAppConfigNames) {
Invoke-DiscoveryStep -Name "web-and-function-app-config-names-only" -Directory $AppRoot -ScriptBlock {
Get-AppConfigNames -SiteResources $Sites
} | Out-Null
}
$IntegrationRoot = Join-Path $SubscriptionRoot "integration"
Invoke-DiscoveryStep -Name "logic-app-consumption-workflows" -Directory $IntegrationRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Logic/workflows" | ForEach-Object { Convert-LogicAppWorkflow -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "logic-app-standard-sites" -Directory $IntegrationRoot -ScriptBlock {
$Sites | Where-Object { $_.Kind -like "*workflowapp*" } | ForEach-Object { Convert-WebSite -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "logic-app-standard-workflows" -Directory $IntegrationRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Web/sites/workflows" | ForEach-Object { Convert-LogicAppWorkflow -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "api-connections" -Directory $IntegrationRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Web/connections" | ForEach-Object { Convert-ApiConnection -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "logic-app-integration-accounts" -Directory $IntegrationRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Logic/integrationAccounts" | ForEach-Object {
Convert-GenericResourceWithSafeProperties -Resource $_ -PropertyNames @("state", "integrationServiceEnvironment")
}
} | Out-Null
$DataRoot = Join-Path $SubscriptionRoot "data"
Invoke-DiscoveryStep -Name "storage-accounts" -Directory $DataRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Storage/storageAccounts" | ForEach-Object { Convert-StorageAccount -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "sql-servers" -Directory $DataRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Sql/servers" | ForEach-Object { Convert-SqlServer -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "sql-databases" -Directory $DataRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Sql/servers/databases" | ForEach-Object { Convert-SqlDatabase -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "cosmos-db-accounts" -Directory $DataRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.DocumentDB/databaseAccounts" | ForEach-Object {
Convert-GenericResourceWithSafeProperties -Resource $_ -PropertyNames @("databaseAccountOfferType", "locations", "consistencyPolicy", "publicNetworkAccess", "enableFreeTier")
}
} | Out-Null
Invoke-DiscoveryStep -Name "redis-caches" -Directory $DataRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Cache/Redis" | ForEach-Object {
Convert-GenericResourceWithSafeProperties -Resource $_ -PropertyNames @("sku", "enableNonSslPort", "minimumTlsVersion", "publicNetworkAccess")
}
} | Out-Null
$SecurityRoot = Join-Path $SubscriptionRoot "security"
Invoke-DiscoveryStep -Name "key-vaults" -Directory $SecurityRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.KeyVault/vaults" | ForEach-Object { Convert-KeyVault -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "managed-identities" -Directory $SecurityRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.ManagedIdentity/userAssignedIdentities" | ForEach-Object {
Convert-GenericResourceWithSafeProperties -Resource $_ -PropertyNames @("clientId", "principalId")
}
} | Out-Null
$ObservabilityRoot = Join-Path $SubscriptionRoot "observability"
Invoke-DiscoveryStep -Name "log-analytics-workspaces" -Directory $ObservabilityRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.OperationalInsights/workspaces" | ForEach-Object {
Convert-GenericResourceWithSafeProperties -Resource $_ -PropertyNames @("sku", "retentionInDays", "customerId")
}
} | Out-Null
Invoke-DiscoveryStep -Name "application-insights" -Directory $ObservabilityRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Insights/components" | ForEach-Object {
Convert-GenericResourceWithSafeProperties -Resource $_ -PropertyNames @("Application_Type", "WorkspaceResourceId", "RetentionInDays", "DisableIpMasking")
}
} | Out-Null
Invoke-DiscoveryStep -Name "action-groups" -Directory $ObservabilityRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Insights/actionGroups" | ForEach-Object { Convert-MonitorActionGroup -Resource $_ }
} | Out-Null
Invoke-DiscoveryStep -Name "metric-alerts" -Directory $ObservabilityRoot -ScriptBlock {
Select-ResourcesByType -Resources $ExpandedResources -ResourceType "Microsoft.Insights/metricAlerts" | ForEach-Object {
Convert-GenericResourceWithSafeProperties -Resource $_ -PropertyNames @("enabled", "severity", "scopes", "criteria", "windowSize", "evaluationFrequency")
}
} | Out-Null
if ($IncludeDiagnosticSettings) {
Invoke-DiscoveryStep -Name "diagnostic-settings" -Directory $ObservabilityRoot -ScriptBlock {
Get-DiagnosticSettings -Resources $Resources
} | Out-Null
}
}
if ($IncludeEntra) {
Write-Section "Optional Entra ID discovery"
Invoke-DiscoveryStep -Name "entra-summary" -Directory (Join-Path $OutputPath "entra") -ScriptBlock {
Get-EntraSummary
} | Out-Null
}
if (-not $NoArchive) {
$ArchivePath = "{0}.zip" -f $OutputPath
Compress-Archive -Path $OutputPath -DestinationPath $ArchivePath -Force
Write-Host ("Archive created: {0}" -f $ArchivePath)
}
Write-Host ("Discovery output folder: {0}" -f $OutputPath)
Write-Host "Review output for sensitive resource names or metadata before sharing."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment