src/public/Get-AzViz.ps1
<#
.SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER ResourceGroups Target resource groups .PARAMETER ShowVisualization Launches visualization image .PARAMETER LabelVerbosity Level of information to included in vizualization .PARAMETER CategoryDepth Level of Azure Resource Sub-category to be included in vizualization -CategoryDepth 'level1' only allow resource catergores like: Microsoft.EventGrid/topics and Microsoft.ServiceBus/namespaces -CategoryDepth 'level2' only allow resource catergores like: Microsoft.ServiceBus/namespaces/AuthorizationRules and Microsoft.ServiceBus/namespaces/networkRuleSets .PARAMETER OutputFormat Output format of the vizualization, i.e, .png or .svg .PARAMETER Theme Changes the color theme, i.e 'light', 'dark' or 'neon'. Default is 'light'. .PARAMETER Direction Direction in which resource groups are plotted on the visualization .PARAMETER OutputFilePath Output file path .EXAMPLE An example Get-AzViz -ResourceGroups demo-2 -LabelVerbosity 2 -CategoryDepth 2 -Theme light -Verbose -ShowGraph -OutputFormat png .NOTES Project URL: https://github.com/PrateekKumarSingh/azviz Author: https://twitter.com/singhprateik https://www.linkedin.com/in/prateeksingh1590 #> function Get-AzViz { [alias("AzViz")] [CmdletBinding()] param ( # Names of target resource groups [Parameter(ParameterSetName = 'AzLogin', Mandatory = $true, Position = 0)] [string[]] $ResourceGroup, # # File paths to target ARM templates # [Parameter(ParameterSetName = 'FilePath', Mandatory = $true, Position = 0)] # [System.IO.Path[]] $Path, # # URLs to target ARM templates # [Parameter(ParameterSetName = 'Url', Mandatory = $true, Position = 0)] # [uri[]] $Url, # Launches visualization image [Parameter(ParameterSetName = 'AzLogin')] # [Parameter(ParameterSetName = 'FilePath')] # [Parameter(ParameterSetName = 'Url')] [switch] $ShowVisualization, # Level of information to included in vizualization [Parameter(ParameterSetName = 'AzLogin')] # [Parameter(ParameterSetName = 'FilePath')] # [Parameter(ParameterSetName = 'Url')] [ValidateSet(1, 2, 3)] [int] $LabelVerbosity = 1, # Level of Azure Resource Sub-category to be included in vizualization [Parameter(ParameterSetName = 'AzLogin')] # [Parameter(ParameterSetName = 'FilePath')] # [Parameter(ParameterSetName = 'Url')] [ValidateSet(1, 2, 3)] [int] $CategoryDepth = 1, # Output format of the vizualization [Parameter(ParameterSetName = 'AzLogin')] # [Parameter(ParameterSetName = 'FilePath')] # [Parameter(ParameterSetName = 'Url')] [ValidateSet('png', 'svg')] [string] $OutputFormat = 'png', # Changes the color theme, i.e light or dark [Parameter(ParameterSetName = 'AzLogin')] # [Parameter(ParameterSetName = 'FilePath')] # [Parameter(ParameterSetName = 'Url')] [ValidateSet('light', 'dark', 'neon')] [string] $Theme = 'light', # Direction in which resource groups are plotted on the visualization [Parameter(ParameterSetName = 'AzLogin')] # [Parameter(ParameterSetName = 'FilePath')] # [Parameter(ParameterSetName = 'Url')] [ValidateSet('left-to-right', 'top-to-bottom')] [string] $Direction = 'top-to-bottom', # Output file path [Parameter(ParameterSetName = 'AzLogin')] # [Parameter(ParameterSetName = 'FilePath')] # [Parameter(ParameterSetName = 'Url')] [ValidateScript({Test-Path -Path $_ -IsValid})] [string] $OutputFilePath = "$env:TEMP\output.$OutputFormat" ) #region defaults $ErrorActionPreference = 'stop' switch ($Theme) { 'light' { $GraphColor = 'White' $SubGraphColor = 'Black' $GraphFontColor = 'Black' $EdgeColor = 'Black' $EdgeFontColor = 'Black' $NodeColor = 'Black' $NodeFontColor = 'Black' break } 'dark' { $GraphColor = 'Black' $SubGraphColor = 'White' $GraphFontColor = 'White' $EdgeColor = 'White' $EdgeFontColor = 'White' $NodeColor = 'White' $NodeFontColor = 'White' break } 'neon' { $GraphColor = 'Black' $SubGraphColor = 'x11green' $GraphFontColor = 'x11green' $EdgeColor = 'x11green' $EdgeFontColor = 'x11green' $NodeColor = 'x11green' $NodeFontColor = 'x11green' break } } if ($PSBoundParameters.ContainsKey('ResourceGroup')) { $TargetType = 'Azure Resource Group' } elseif ($PSBoundParameters.ContainsKey('Path')) { $TargetType = 'File' } elseif ($PSBoundParameters.ContainsKey('URL')) { $TargetType = 'URL' } switch ($Direction) { 'left-to-right' {$rankdir = "LR"} 'top-to-bottom' {$rankdir = "TB"} } $rank = @{ "Microsoft.Network/publicIPAddresses" = 1 "Microsoft.Network/loadBalancers" = 2 "Microsoft.Network/virtualNetworks" = 3 "Microsoft.Network/networkSecurityGroups" = 4 "Microsoft.Network/networkInterfaces" = 5 "Microsoft.Compute/virtualMachines" = 6 } Write-Verbose "Configuring Defaults..." Write-Verbose " [+] Target Type : $TargetType" Write-Verbose " [+] Output Format : $OutputFormat" Write-Verbose " [+] Output File Path : $OutputFilePath" Write-Verbose " [+] Label Verbosity : $LabelVerbosity" Write-Verbose " [+] Category Depth : $CategoryDepth" Write-Verbose " [+] Sub-graph Direction : $Direction" Write-Verbose " [+] Theme : $Theme" Write-Verbose " [+] Launch Visualization : $ShowVisualization" #endregion defaults try { switch ($TargetType) { 'Azure Resource Group' { $targets = $ResourceGroup } 'File' { $targets = $path } 'Url' { $targets = $url } } Write-Verbose "Target ${TargetType}s: " $targets.ForEach( { Write-Verbose " > '$_'" } ) #region graph-generation Write-Verbose "Starting to generate Azure visualization..." $Counter = 0 $subgraphs = foreach ($target in $targets) { #region obtaining-arm-template switch ($TargetType) { 'Azure Resource Group' { if (!(Test-AzLogin)) { break } Write-Verbose " [+] Exporting ARM template of Azure Resource group: `"$target`"" $template = (Export-AzResourceGroup -ResourceGroupName $target -SkipAllParameterization -Force -Path $env:TEMP\armtemplate.json).Path } 'File' { Write-Verbose " [+] Accessing ARM template from local file: `"$target`"" $template = $target } 'Url' { Write-Verbose " [+] Downloading ARM template from URL: `"$target`"" # $target = 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-linux/azuredeploy.json' $template = "$env:TEMP\armtemplate.json" Invoke-WebRequest -Uri $target -OutFile $template -Verbose:$false # todo test-path the downloaded file } } Write-Verbose " [+] Processing the ARM template to extract resources" # $arm = ConvertFrom-ArmTemplate -Path $template $arm = Get-Content -Path $template | ConvertFrom-Json $resources = $arm.Resources if ($resources) { Write-Verbose " [+] Total resources found: $($resources.count)" Write-Verbose " [+] Cleaning up temporary ARM template file at: $template" Remove-Item $template -Force } else { Write-Verbose " [+] Total resources/sub-resources found: $($resources.count)" Write-Verbose " [-] Skipping ${TargetType}: `"$target`" as no resources were found." break } #endregion obtaining-arm-template Write-Verbose " [+] Plotting sub-graph for ${TargetType}: `"$target`"" #region parsing-arm-template-and-finding-resource-dependencies $data = @() $Counter = $Counter + 1 # $excluded_types = @("scheduledqueryrules","containers","solutions","modules","savedSearches") $data += $resources | Where-Object { $_.type.tostring().split("/").count -le $($CategoryDepth + 1) } | ForEach-Object { $dependson = $null if ($_.dependson) { $dependson = $_.DependsOn #| ForEach-Object { $_.ToString().split("parameters('")[1].split("')")[0]} foreach ($dependency in $dependson) { $r = $rank["$($_.type.ToString())"] [PSCustomObject]@{ fromcateg = $_.type.ToString() #.split('/')[-1] from = $_.name.ToString() #.split('/')[-1] #.split("parameters('")[1].split("')")[0] to = $dependency.tostring().replace("[resourceId(", "").replace(")]", "").Split(",")[1].replace("'", "").trim() # -join '/' #.split('/')[-1] tocateg = $dependency.tostring().replace("[resourceId(", "").replace(")]", "").Split(",")[0].replace("'", "").trim().Split("/")[0..1] -join '/' #.split('/')[-1] isdependent = $true rank = if ($r) { $r }else { 9999 } } } } else { $r = $rank["$($_.type.ToString())"] [PSCustomObject]@{ fromcateg = $_.type.ToString() #.split('/')[-1] from = $_.name.ToString() #.split("parameters('")[1].split("')")[0] to = '' tocateg = '' isdependent = $false rank = if ($r) { $r }else { 9999 } } } } | Sort-Object Rank #endregion parsing-arm-template-and-finding-resource-associations #region plotting-edges-to-nodes $nodes_and_edges = $data | # Where-Object to | Tee-Object -Variable pipe_var | ForEach-Object { $from = $_.from $fromcateg = $_.fromcateg $to = $_.to $tocateg = $_.tocateg if ($_.isdependent) { Edge -From "$fromcateg$from".ToUpper() ` -to "$tocateg$to".ToUpper() ` -Attributes @{ arrowhead = 'normal'; style = 'dotted'; label = 'dependsOn' penwidth = "1" fontname = "Courier New" } Write-Verbose " > Creating Edge: $from -> $to" if ($LabelVerbosity -eq 1) { Get-ImageNode -Name "$fromcateg$from".ToUpper() -Rows $from -Type $fromcateg Get-ImageNode -Name "$tocateg$to".ToUpper() -Rows $to -Type $tocateg Write-Verbose " > Creating Node: $from" Write-Verbose " > Creating Node: $to" } elseif ($LabelVerbosity -eq 2) { Get-ImageNode -Name "$fromcateg$from".ToUpper() -Rows ($from, $fromcateg) -Type $fromcateg Get-ImageNode -Name "$tocateg$to".ToUpper() -Rows ($to, $toCateg) -Type $tocateg Write-Verbose " > Creating Node: $from" Write-Verbose " > Creating Node: $to" } } else { if ($LabelVerbosity -eq 1) { Get-ImageNode -Name "$fromcateg$from".ToUpper() -Rows $from -Type $fromcateg Write-Verbose " > Creating Node: $from" } elseif ($LabelVerbosity -eq 2) { Get-ImageNode -Name "$fromcateg$from".ToUpper() -Rows ($from, $fromcateg) -Type $fromcateg Write-Verbose " > Creating Node: $to" } } } # Write-Verbose " [+] Total resources filtered: $($pipe_var.count)" if (!$nodes_and_edges) { Write-Warning " [-] No resources found.. re-run the command and try increasing the category depth using -CategoryDepth 2 or -CategoryDepth 3 cmdlet parameters." -Verbose } else { $SubGraphName = "Resource Group: $target" SubGraph "$($TargetType.Replace(' ',''))$Counter" @{label = $SubGraphName; labelloc = 'b'; penwidth = "1"; fontname = "Courier New" ; color = $SubGraphColor; style = 'rounded' } { $nodes_and_edges } } #endregion plotting-edges-to-nodes #region ranking-nodes # foreach($item in $data | Group-Object rank){ # $nodes = foreach($group in $item.Group){ # $from = $group.from # $fromcateg = $group.fromcateg # $to = $group.to # $tocateg = $group.tocateg # "`"$fromcateg$from`"".ToUpper() # } # "{rank = `"same`"; $($nodes -join '; ')}" # } #endregion ranking-nodes #endregion plotting-all-remaining-nodes } if ($subgraphs) { $Subscription = (Get-AzContext).Subscription $GraphName = "Subscription: {0} ({1})" -f $Subscription.name, $Subscription.Id $graph = Graph 'Visualization' @{label= $GraphName; rankdir = $rankdir; overlap = 'false'; splines = 'true' ; color = $GraphColor; bgcolor = $GraphColor; penwidth = "1"; fontname = "Courier New" ; fontcolor = $GraphFontColor } { edge @{color = $EdgeColor; fontcolor = $EdgeFontColor } node @{color = $NodeColor ; fontcolor = $NodeFontColor } $subgraphs } } if ($graph) { @" strict $graph "@ | Export-PSGraph -ShowGraph:$ShowVisualization -OutputFormat $OutputFormat -DestinationPath $OutputFilePath -OutVariable output | Out-Null Write-Verbose "Graph Exported to path: $($output.fullname)" Write-Verbose "Finished Azure visualization." } #endregion graph-generation } catch { $_ } } Export-ModuleMember Get-AzViz |