AzureSaveMoney.psm1

# Contributors:
# Chad Schultz
#
# PowerShell module to List on and delete unused Azure resources and save money.
#
# This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code.


# Class to hold alert resource groups and names as script has to get RGs from different command let than alert.
Class MyAlert
{
[String]$RG
[String]$Name
}
function global:Get-AzSMUnusedNICs {
    <#
        .SYNOPSIS
            Lists unused NICs in a subscription.
         
        .DESCRIPTION
 
        .PARAMETER SubscriptionID
 
        .OUTPUTS
 
        .EXAMPLE
            Get-AzSMUnusedNICs -Subscription 00000000-0000-0000-0000-000000000000
        .NOTES
 
        .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null|Out-Null
    Write-Debug "Subscription: $SubscriptionID"

    $nics=Get-AzureRmNetworkInterface|Where-Object{!$_.VirtualMachine}
    #$nics|fl Name,ResourceGroupName,Location
    Return $nics
}
function global:Get-AzSMUnusedNSGs {

    <#
        .SYNOPSIS
            Lists unused NSGs in a subscription.
         
        .DESCRIPTION
 
        .PARAMETER SubscriptionID
 
        .OUTPUTS
 
        .EXAMPLE
 
        .NOTES
 
        .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription: $SubscriptionID"

    $nsg=Get-AzureRmNetworkSecurityGroup|Where-Object{!$_.NetworkInterfaces}
    #$nsg|fl Name,ResourceGroupName,Location
    if($nsg.count -gt 0) {
        Return $nsg
    } else {
        Return "No unused network security groups"
    }
}
function global:Get-AzSMUnusedPIPs {

    <#
        .SYNOPSIS
            Lists unused Public IPs in a subscription.
         
        .DESCRIPTION
 
        .PARAMETER SubscriptionID
 
        .OUTPUTS
 
        .EXAMPLE
 
        .NOTES
 
        .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription: $SubscriptionID"

    $pip=Get-AzureRmPublicIpAddress|Where-Object{!$_.IpConfiguration}
    if($pip.count -gt 0) {
        Return $pip
    } else {
        Return "No unused public IP addresses"
    }
    
}
function global:Get-AzSMDisabledAlerts {

    <#
    .SYNOPSIS
        Lists disabled "classic" alerts in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription: $SubscriptionID"

    $alerts = New-Object System.Collections.ArrayList
    $rgs=Get-AzureRmResourceGroup
    foreach ($r in $rgs)
    {
        $a=Get-AzureRmAlertRule -ResourceGroupName $r.ResourceGroupName -WarningAction Ignore|Where-Object{$_.IsEnabled -eq $false}
    
        if ($a.IsEnabled -eq $false){
        $al=New-Object MyAlert
        $al.RG=$r.ResourceGroupName
        $al.Name=$a.Name
        $alerts.Add($al)|Out-Null
        #Write-Host ($al.RG+"-"+$al.Name ) -ForegroundColor DarkYellow
        }
    }
    Return $alerts

}
function global:Get-AzSMDisabledLogAlerts{

    <#
    .SYNOPSIS
        List disabled Activity Log alerts in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription: $SubscriptionID"

    $logalerts = New-Object System.Collections.ArrayList
    $rg=Get-AzureRmResourceGroup
    foreach ($r in $rg)
    {
        $a=Get-AzureRmActivityLogAlert -ResourceGroupName $r.ResourceGroupName -WarningAction Ignore -ErrorAction Ignore|Where-Object{$_.Enabled -eq $false}

        if ($a.Enabled -eq $false){
            $al=New-Object MyAlert
            $al.RG=$r.ResourceGroupName
            $al.Name=$a.Name
            $logalerts.Add($al)|Out-Null
            #Write-Host ($al.RG+"-"+$al.Name ) -ForegroundColor DarkYellow
        }
    }
    
    Return $logalerts
}
function global:Get-AzSMEmptyResourceGroups {

    <#
    .SYNOPSIS
        Lists empty resource groups in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription: $SubscriptionID"

    $emptyrgs = New-Object System.Collections.ArrayList
    $rgs=Get-AzureRmResourceGroup

    $rgs|ForEach-Object {
        $rgd=Get-AzureRMResource -ResourceGroupNameEquals $_.ResourceGroupName -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -ErrorVariable $rgerr
        if (!$rgd -and $rgerr -eq $null) {
            $emptyrgs.add($_)|Out-Null
            #Write-Host $_.ResourceGroupName
        }
    }
    Return $emptyrgs
}
function global:Get-AzSMUnusedAlertActionGroups {

    <#
    .SYNOPSIS
        Lists unused Alert Action Groups in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription: $SubscriptionID"

    $actiongroups = New-Object System.Collections.ArrayList
    $rgs=Get-AzureRmResourceGroup
    
    foreach ($rg in $rgs) {
        $ala=Get-AzureRmActivityLogAlert -ResourceGroupName $rg.ResourceGroupName -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -ErrorVariable $alerr
        if ($ala -and $alerr.count -lt 1) {
            $a=$ala.Actions.ActionGroups[0].ActionGroupId.Split("/")
            $actiongroups.add($a.GetValue($a.Count -1))|Out-Null
        }
    }

    $actiongroups2 = New-Object System.Collections.ArrayList
    $ags=Get-AzureRmResource -ResourceType microsoft.insights/actionGroups

    foreach ($g in $ags) {
        $actiongroups2.add($g.ResourceName)|Out-Null
    }

    $unusedactiongroups=$actiongroups2|Where-Object{$actiongroups -notcontains $_}

    Return $unusedactiongroups
}
function global:Get-AzSMUnusedRouteTables {

    <#
    .SYNOPSIS
        List unused route tables in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription: $SubscriptionID"

    $routelist = New-Object System.Collections.ArrayList
    $routes=Get-AzureRmRouteTable
    $routes|ForEach-Object {
        if ($route.Subnets.Count -eq 0) {
            $routelist.add($_)|Out-Null
        }
    }

    Return $routelist
}
function global:Get-AzSMVNetsWithoutSubnets {

    <#
    .SYNOPSIS
        List VNets without any subnets defined in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription: $SubscriptionID"

    $emptysubnets=New-Object System.Collections.ArrayList
    $vnets=Get-AzureRmVirtualNetwork
    foreach ($vnet in $vnets) {
        if ($vnet.Subnets.Count -eq 0) {
            $emptysubnets.add($vnet)|Out-Null
            #Write-Host "VNet without subnets defined found: " + $vnet.Name
        }
    }

    Return $emptysubnets
}
function global:Get-AzSMOldDeployments{

    <#
    .SYNOPSIS
        List deployments older than 365 days in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .PARAMETER Days
        Set to the number of days to scan back for old deployments.
        Default is 365 days old.
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID,
        [Parameter(Mandatory=$false)][int] $Days = 365
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription: $SubscriptionID"

    $rgd=Get-AzureRMResourceGroup|Get-AzureRmResourceGroupDeployment|Where-Object{$_.Timestamp -lt (Get-Date).AddDays(-$Days)}
    
    Return $rgd
}
function global:Get-AzSMUnusedDisks {

    <#
    .SYNOPSIS
        List unused managed disks in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription: $SubscriptionID"

    $disks = Get-AzureRmDisk|Where-Object{$_.ManagedBy.Length -lt 1}

    Return $disks
}
function global:Get-AzSMEmptyAADGroups {

    <#
    .SYNOPSIS
        List empty AAD groups in a tenant.
         
    .DESCRIPTION
 
    .PARAMETER TenantID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="TenantID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $TenantID
    )

    Connect-AzureAD -TenantId $TenantID
    Write-Debug "Tenant ID: $TenantID"

    $emptygroups=New-Object System.Collections.ArrayList
    Get-AzureADGroup|ForEach-Object {
        $aadgmem=Get-AzureADGroupMember -ObjectId $_.ObjectId
        if($aadgmem.Count -lt 1) {
            $emptygroups.add($_)|Out-Null
        }
    }
    Return $emptygroups
}
function global:Get-AzSMDisabledLogicApps {

    <#
    .SYNOPSIS
        List disabled Logic Apps in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription ID: $SubscriptionID"

    $disabledlapps = New-Object System.Collections.ArrayList
    $lapps=Get-AzureRMResource -ResourceType "Microsoft.Logic/workflows"
    $lapps|ForEach-Object {
        $lapp=Get-AzureRmLogicApp -ResourceGroupName $_.ResourceGroupName -Name $_.Name|Where-Object{$_.State -eq "Disabled"}
        $disabledlapps.add($lapp)|Out-Null
    }

    Return $disabledlapps
}
function global:Get-AzSMOldSnapshots{

    <#
    .SYNOPSIS
        List snapshots older than 365 days in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .PARAMETER Days
        Set to the number of days to scan back for old deployments.
        Default is 365 days old.
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID,
        [Parameter(Mandatory=$false)][int] $Days = 365
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription ID: $SubscriptionID"

    $snap=Get-AzureRmSnapshot|Where-Object{$_.TimeCreated -lt (Get-Date).AddDays(-$Days)}
    
    Return $snap
}
function global:Get-AzSMIlbNoBackendPool {

    <#
    .SYNOPSIS
        List Internal load balancers that have no backend pool in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription ID: $SubscriptionID"

    $ilbsnopool=Get-AzureRmLoadBalancer|Where-Object{$_.BackendAddressPools.Count -lt 1}

    Return $ilbsnopool
}
function global:Get-AzSMDisabledTrafficManagerProfiles {

    <#
    .SYNOPSIS
        List disabled TrafficManager Profiles in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription ID: $SubscriptionID"

    $dtmpro=Get-AzureRmTrafficManagerProfile|Where-Object{$_.ProfileStatus -eq "Disabled"}

    Return $dtmpro
}
function global:Get-AzSMTrafficManagerProfilesWithNoEndpoints {

    <#
    .SYNOPSIS
        List TrafficManager Profiles without endpoints in a subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID
    )

    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null
    Write-Debug "Subscription ID: $SubscriptionID"

    $dtmpro=Get-AzureRmTrafficManagerProfile|Where-Object{$_.Endpoints.Count -lt 1}

    Return $dtmpro
}
function global:Get-AzSMAllResources {

    <#
    .SYNOPSIS
        List all unused resources that this module implements in a tenant and subscription.
         
    .DESCRIPTION
 
    .PARAMETER SubscriptionID
 
    .PARAMETER TenantID
 
    .PARAMETER Days
 
    .OUTPUTS
 
    .EXAMPLE
 
    .NOTES
 
    .LINK
 
    #>


    [CmdletBinding(
        DefaultParameterSetName="SubscriptionID",
        SupportsShouldProcess=$false,
        ConfirmImpact='Low'
    )]

    param(
        [Parameter(Mandatory=$true)][string] $SubscriptionID,
        [Parameter(Mandatory=$true)][string] $TenantID,
        [Parameter(Mandatory=$false)][int] $Days = 365
    )

    Connect-AzureAD -TenantId $TenantID|Out-Null
    Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null

    Write-Debug "Tenant ID: $TenantID"
    Write-Debug "Subscription ID: $SubscriptionID"
    Write-Debug "Days: $Days"
    
    Write-Host "Ununsed NICs:" -ForegroundColor Cyan
    Get-AzSMUnusedNICs -Subscription $SubscriptionID
    
    Write-Host "Ununsed NSGs:" -ForegroundColor Cyan
    Get-AzSMUnusedNSGs -Subscription $SubscriptionID
    
    Write-Host "Ununsed PIPs:" -ForegroundColor Cyan
    Get-AzSMUnusedPIPs -Subscription $SubscriptionID
    
    Write-Host "Disabled Alerts(Classic):" -ForegroundColor Cyan
    Get-AzSMDisabledAlerts -Subscription $SubscriptionID
    
    Write-Host "Disabled Log Alerts:" -ForegroundColor Cyan
    Get-AzSMDisabledLogAlerts -Subscription $SubscriptionID
    
    Write-Host "Empty Resource Groups:" -ForegroundColor Cyan
    Get-AzSMEmptyResourceGroups -Subscription $SubscriptionID
    
    Write-Host "Ununsed Alert Groups:" -ForegroundColor Cyan
    Get-AzSMUnusedAlertActionGroups -Subscription $SubscriptionID
    
    Write-Host "Ununsed Route Tables:" -ForegroundColor Cyan
    Get-AzSMUnusedRouteTables -Subscription $SubscriptionID
    
    Write-Host "VNets without Subnets:" -ForegroundColor Cyan
    Get-AzSMVNetsWithoutSubnets -Subscription $SubscriptionID
    
    Write-Host "Old Deployments older than $Days days:" -ForegroundColor Cyan
    Get-AzSMOldDeployments -Subscription $SubscriptionID
    
    Write-Host "Ununsed Disks:" -ForegroundColor Cyan
    Get-AzSMUnusedDisks -Subscription $SubscriptionID
    
    Write-Host "Empty AAD Groups:" -ForegroundColor Cyan
    Get-AzSMEmptyAADGroups -Subscription -TenantId $TenantID
    
    Write-Host "Disabled Logic Apps:" -ForegroundColor Cyan
    Get-AzSMDisabledLogicApps -Subscription $SubscriptionID
    
    Write-Host "Old Snapshots older than $Days days:" -ForegroundColor Cyan
    Get-AzSMOldSnapshots -Subscription $SubscriptionID
    
    Write-Host "ILBs with no backend pools:" -ForegroundColor Cyan
    Get-AzSMIlbNoBackendPool -Subscription $SubscriptionID

    Write-Host "Disabled TrafficManager Profiles:" -ForegroundColor Cyan
    Get-AzSMDisabledTrafficManagerProfiles -Subscription $SubscriptionID
    
    Write-Host "TrafficManager Profiles With No Endpoints:" -ForegroundColor Cyan
    Get-AzSMTrafficManagerProfilesWithNoEndpoints -Subscription $SubscriptionID

}