Register-SelfDestructiveAzResource.ps1


<#PSScriptInfo
 
.VERSION 1.0.0.8
 
.GUID ba60be59-a510-4ca2-a15d-34cbb83ed089
 
.AUTHOR Mahmood Abushaireh
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS Azure 'Azure Automation'
 
.LICENSEURI
 
.PROJECTURI http://mabushaireh.info/posts/about-register-selfdestructiveazresource
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES Az
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
[1.0.0.6] 2020-06-24 Rename TicketNumber to RefNumber and make it [optional]
                2020-06-24 Make subscription ID [optinoal] if not passed default subscription is assumed
[1.0.0.7] 2020-06-29 Add help Message to the params
                2020-06-29 Add validation for -DeleteAfterDays, should positive integer and a maximum of 30 days.
                2020-06-29 Add validation for DeleteAfterTimeOfDay, should be positive and from 0 to 23 hours
[1.0.0.8] 2020-07-02 Add -Force to Expand-Archive to save over existing runbook
                2021-01-04 Add Resource group support
 
#>


<#
.Synopsis
    Schedule a cleanup job for the provided Azure Resources.
.DESCRIPTION
    Automatically delete Azure Resource at a given time date
        
.EXAMPLE
    Register-SelfDestructiveResource --ResourceId "Resource Id" -RefNumber "Ref Number"
    #Note: Refnumber is not to link the resource to an internal record, could be a workitem id if you are working on a project
     
.EXAMPLE
    Register-SelfDestructiveResource -ResourceName "Resource Name" -ResrourceGroup "Resource Group" -DeleteAfterDays 3 DeleteAfterTimeOfDay 19
.LINK
    http://mabushaireh.info/posts/about-register-selfdestructiveazresource
#>



#Requires -RunAsAdministrator
[CmdletBinding(
    DefaultParameterSetName = 'ResourceID',
    HelpUri = 'http://mabushaireh.info/posts/about-register-selfdestructiveazresource'
)]
#TODO: add Support for resource group cleanup
param 
(
    
    [Parameter(Mandatory = $true, ParameterSetName = "ResourceID", HelpMessage = "Resource Id for the resource you want the tool to manage. Resource Id format is /subscriptions/{guid}/resourceGroups/{resource-group-name}/{resource-provider-namespace}/{resource-type}/{resource-name}." )]
    [string] $ResourceId,

    [Parameter(Mandatory = $true, ParameterSetName = "ResourceName", HelpMessage = "Resource Name")]
    [string] $ResourceName,

    [Parameter(Mandatory = $true, ParameterSetName = "ResourceName", HelpMessage = "Resource Group where the Resource provided in the [-ResourceName] param exists. ")]
    [Parameter(Mandatory = $true, ParameterSetName = "ResourceGroup", HelpMessage = "Resource Group required to be destroyed.")]
    [string] $ResourceGroup,

    [Parameter(Mandatory = $false, ParameterSetName = "ResourceID", HelpMessage = "[OPTIONAL] Referce numbed used for tracking resouces created for specific recored. Refernace Id could be workitem id or task id from your DevOps tool!")]
    [Parameter(ParameterSetName = "ResourceName")]
    [Parameter(ParameterSetName = "ResourceGroup")]
    [string] $RefNumber,
    
    [Parameter(Mandatory = $false, ParameterSetName = "ResourceID", HelpMessage = "[OPTIONAL] if you want to manage a resource in a subscription other than the default subsciption!")] 
    [Parameter(ParameterSetName = "ResourceName")]
    [Parameter(ParameterSetName = "ResourceGroup")]
    [string] $SubscriptionId,

    [Parameter(Mandatory = $false, ParameterSetName = "ResourceID", HelpMessage = "Number of Days before the resource is deleted. Default value is 0")] 
    [Parameter(ParameterSetName = "ResourceName")]
    [Parameter(ParameterSetName = "ResourceGroup")]
    [ValidateRange(0, 30)]
    [int] $DeleteAfterDays = 0,

    [Parameter(Mandatory = $false, ParameterSetName = "ResourceID", HelpMessage = "Local time in Days (24 Hours format) when the delete operaion starts. Default is 19:00 or 7pm.")]  
    [Parameter(ParameterSetName = "ResourceName")]
    [Parameter(ParameterSetName = "ResourceGroup")]
    [ValidateRange(0, 23)]
    [int] $DeleteAfterTimeOfDay = 19,

    [Parameter(Mandatory = $false, ParameterSetName = "ResourceID", HelpMessage = "Resource Group where the resources required for the tool will be created, by default its SelfDistructiveTool-rg.")]  
    [Parameter(ParameterSetName = "ResourceName")]
    [Parameter(ParameterSetName = "ResourceGroup")]
    [string] $ToolResourceGroup = "SelfDistructiveTool-rg"
)

$ScriptVersion = '1.0.0.8'

function New-RunAsAccount {
    Param (
        [Parameter(Mandatory = $true)]
        [String] $ResourceGroup,

        [Parameter(Mandatory = $true)]
        [String] $AutomationAccountName,

        [Parameter(Mandatory = $true)]
        [String] $ApplicationDisplayName,

        [Parameter(Mandatory = $true)]
        [String] $SubscriptionId,

        [Parameter(Mandatory = $true)]
        [String] $SelfSignedCertPlainPassword,

        [Parameter(Mandatory = $false)]
        [string] $EnterpriseCertPathForRunAsAccount,

        [Parameter(Mandatory = $false)]
        [String] $EnterpriseCertPlainPasswordForRunAsAccount,

        [Parameter(Mandatory = $false)]
        [String] $EnterpriseCertPathForClassicRunAsAccount,

        [Parameter(Mandatory = $false)]
        [int] $SelfSignedCertNoOfMonthsUntilExpired = 12
    )

    function CreateSelfSignedCertificate([string] $certificateName, [string] $selfSignedCertPlainPassword,
        [string] $certPath, [string] $certPathCer, [string] $selfSignedCertNoOfMonthsUntilExpired ) {
        $Cert = New-SelfSignedCertificate -DnsName $certificateName -CertStoreLocation cert:\LocalMachine\My `
            -KeyExportPolicy Exportable -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" `
            -NotAfter (Get-Date).AddMonths($selfSignedCertNoOfMonthsUntilExpired) -HashAlgorithm SHA256

        $CertPassword = ConvertTo-SecureString $selfSignedCertPlainPassword -AsPlainText -Force
        Export-PfxCertificate -Cert ("Cert:\localmachine\my\" + $Cert.Thumbprint) -FilePath $certPath -Password $CertPassword -Force | Write-Verbose
        Export-Certificate -Cert ("Cert:\localmachine\my\" + $Cert.Thumbprint) -FilePath $certPathCer -Type CERT | Write-Verbose
    }

    function CreateServicePrincipal([System.Security.Cryptography.X509Certificates.X509Certificate2] $PfxCert, [string] $applicationDisplayName) {
        $keyValue = [System.Convert]::ToBase64String($PfxCert.GetRawCertData())
        $keyId = (New-Guid).Guid

        # Create an Azure AD application, AD App Credential, AD ServicePrincipal

        # Requires Application Developer Role, but works with Application administrator or GLOBAL ADMIN
        $Application = New-AzADApplication -DisplayName $ApplicationDisplayName -HomePage ("http://" + $applicationDisplayName) -IdentifierUris ("http://" + $keyId)
        # Requires Application administrator or GLOBAL ADMIN
        $ApplicationCredential = New-AzADAppCredential -ApplicationId $Application.ApplicationId -CertValue $keyValue -StartDate $PfxCert.NotBefore -EndDate $PfxCert.NotAfter
        # Requires Application administrator or GLOBAL ADMIN
        $ServicePrincipal = New-AzADServicePrincipal -ApplicationId $Application.ApplicationId
        $GetServicePrincipal = Get-AzADServicePrincipal -ObjectId $ServicePrincipal.Id

        # Sleep here for a few seconds to allow the service principal application to become active (ordinarily takes a few seconds)
        Start-Sleep -s 15
        # Requires User Access Administrator or Owner.
        $NewRole = New-AzRoleAssignment -RoleDefinitionName Contributor -ServicePrincipalName $Application.ApplicationId -ErrorAction SilentlyContinue
        $Retries = 0;
        While ($null -eq $NewRole -and $Retries -le 6) {
            Start-Sleep -s 10
            New-AzRoleAssignment -RoleDefinitionName Contributor -ServicePrincipalName $Application.ApplicationId | Write-Verbose -ErrorAction SilentlyContinue
            $NewRole = Get-AzRoleAssignment -ServicePrincipalName $Application.ApplicationId -ErrorAction SilentlyContinue
            $Retries++;
        }
        return $Application.ApplicationId.ToString();
    }

    function CreateAutomationCertificateAsset ([string] $resourceGroup, [string] $automationAccountName, [string] $certifcateAssetName, [string] $certPath, [string] $certPlainPassword, [Boolean] $Exportable) {
        $CertPassword = ConvertTo-SecureString $certPlainPassword -AsPlainText -Force
        Remove-AzAutomationCertificate -ResourceGroupName $resourceGroup -AutomationAccountName $automationAccountName -Name $certifcateAssetName -ErrorAction SilentlyContinue
        New-AzAutomationCertificate -ResourceGroupName $resourceGroup -AutomationAccountName $automationAccountName -Path $certPath -Name $certifcateAssetName -Password $CertPassword -Exportable:$Exportable  | write-verbose
    }

    function CreateAutomationConnectionAsset ([string] $resourceGroup, [string] $automationAccountName, [string] $connectionAssetName, [string] $connectionTypeName, [System.Collections.Hashtable] $connectionFieldValues ) {
        Remove-AzAutomationConnection -ResourceGroupName $resourceGroup -AutomationAccountName $automationAccountName -Name $connectionAssetName -Force -ErrorAction SilentlyContinue
        New-AzAutomationConnection -ResourceGroupName $ResourceGroup -AutomationAccountName $automationAccountName -Name $connectionAssetName -ConnectionTypeName $connectionTypeName -ConnectionFieldValues $connectionFieldValues
    }

    # To use the new Az modules to create your Run As accounts, please uncomment the following lines and ensure you comment out the previous 8 lines that import the AzureRM modules to avoid any issues. To learn about about using Az modules in your Automation account see https://docs.microsoft.com/azure/automation/az-modules.

    Import-Module Az.Automation
    Enable-AzureRmAlias


    $Subscription = Get-AzSubscription -SubscriptionId $SubscriptionId | Set-AzContext

    # Create a Run As account by using a service principal
    $CertifcateAssetName = "AzureRunAsCertificate"
    $ConnectionAssetName = "AzureRunAsConnection"
    $ConnectionTypeName = "AzureServicePrincipal"

    if ($EnterpriseCertPathForRunAsAccount -and $EnterpriseCertPlainPasswordForRunAsAccount) {
        $PfxCertPathForRunAsAccount = $EnterpriseCertPathForRunAsAccount
        $PfxCertPlainPasswordForRunAsAccount = $EnterpriseCertPlainPasswordForRunAsAccount
    }
    else {
        $CertificateName = $AutomationAccountName + $CertifcateAssetName
        $PfxCertPathForRunAsAccount = Join-Path $env:TEMP ($CertificateName + ".pfx")
        $PfxCertPlainPasswordForRunAsAccount = $SelfSignedCertPlainPassword
        $CerCertPathForRunAsAccount = Join-Path $env:TEMP ($CertificateName + ".cer")
        CreateSelfSignedCertificate $CertificateName $PfxCertPlainPasswordForRunAsAccount $PfxCertPathForRunAsAccount $CerCertPathForRunAsAccount $SelfSignedCertNoOfMonthsUntilExpired
    }

    # Create a service principal
    $PfxCert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($PfxCertPathForRunAsAccount, $PfxCertPlainPasswordForRunAsAccount)
    $ApplicationId = CreateServicePrincipal $PfxCert $ApplicationDisplayName

    # Create the Automation certificate asset
    CreateAutomationCertificateAsset $ResourceGroup $AutomationAccountName $CertifcateAssetName $PfxCertPathForRunAsAccount $PfxCertPlainPasswordForRunAsAccount $true

    # Populate the ConnectionFieldValues
    $SubscriptionInfo = Get-AzSubscription -SubscriptionId $SubscriptionId
    $TenantID = $SubscriptionInfo | Select-Object TenantId -First 1
    $Thumbprint = $PfxCert.Thumbprint
    $ConnectionFieldValues = @{"ApplicationId" = $ApplicationId; "TenantId" = $TenantID.TenantId; "CertificateThumbprint" = $Thumbprint; "SubscriptionId" = $SubscriptionId }

    # Create an Automation connection asset named AzureRunAsConnection in the Automation account. This connection uses the service principal.
    CreateAutomationConnectionAsset $ResourceGroup $AutomationAccountName $ConnectionAssetName $ConnectionTypeName $ConnectionFieldValues
}

Import-Module Az



# Write the script version
Write-Host ("Script version: {0}" -f $ScriptVersion)

# Write the command line that was used when the script was called
Write-Host ("Command line: {0}" -f $MyInvocation.Line)

# Get the current time in UTC
Write-Host ("Current UTC time: {0}" -f [System.DateTime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss'))


#### Constants DONT CHANGE
$runbookName = "Delete-AzureResource"
$automationAccountName = "SelfDistructiveTool-aa"


$DeleteAfter = (Get-Date ($deleteAfterTimeOfDay.ToString() + ":00:00")).AddDays($DeleteAfterDays)

if ($DeleteAfter.Subtract((Get-Date)).Hours -lt 2) {
    #Delete Next Day!!!
    $DeleteAfter = $DeleteAfter.AddDays(1)
}

# this is might be needed in the feature.
#$moduleContentUrl = "https://www.powershellgallery.com/api/v2/package/AzureRM.HDInsight/1.0.4"
#

#TODO: Convert this to a param with default value
$defaultLocation = "West Europe"
$ToolTags = @{"CreatedWith" = "Register-SelfDestructiveAzResource.ps1"; "VersionInfo" = "$ScriptVersion"; }


Write-Verbose "subscriptionId : $SubscriptionId"
Write-Verbose "deleteAfterDays : $DeleteAfterDays"
Write-Verbose "deleteAfterTimeOfDay : $deleteAfterTimeOfDay"
Write-Verbose "DeleteAfter : $DeleteAfter"


if ($PSBoundParameters.ContainsKey("SubscriptionId")) {
    if ($SubscriptionId -ne "" ) {
        Write-Information -Message "Selecting Subscription $subscriptionId"
        Get-AzSubscription -SubscriptionId $SubscriptionId | Set-AzContext
    }
    else {
        Write-Information "Using default subscription"    
    }
}
else {
    Write-Information "Using default subscription"
}


Write-Verbose -Message 'Get Resource'

if ($PSBoundParameters.ContainsKey("ResourceId")) {
    $resource = Get-AzResource -ResourceId $ResourceId 
} 
elseif ($PSBoundParameters.ContainsKey("ResourceName")) {
     
    $resource = Get-AzResource -ResourceGroupName $ResourceGroup -Name $ResourceName
    $ResourceId = $resource.ResourceId
}
else {
    # this is resource Group
    $rg = Get-AzResourceGroup -ResourceGroupName $ResourceGroup
}

if (!$resource -and !$rg) {
    if ($PSBoundParameters.ContainsKey("ResourceId")) {
        Throw "Resource with id [$ResourceId] doesnt exist!"
    }
    elseif ($PSBoundParameters.ContainsKey("ResourceName")){
        Throw "Resource with name [$ResourceName] doesnt exist under resource group [$ResourceGroup]!"
    }
    else {
        Throw "Resource Group with name [$ResourceGroup] doesnt exist!"
    }
    
    exit 1
}

Write-Verbose -Message 'Set resourcer TAGs'

$resourceTags = @{"RefNumber" = " $RefNumber"; "DeleteAfter" = "$DeleteAfter"; "ManagedBy" = "Register-SelfDestructiveAzResource.ps1"; "VersionInfo" = "$ScriptVersion"; }


if ($resource){
    Set-AzResource -ResourceId $ResourceId -Tag $resourceTags -Force
} else {
    Set-AzResourceGroup -Name $ResourceGroup -Tag $resourceTags
}


Write-Verbose "Check if Tool Resource Group Exists account exists"
$rg = Get-AzResourceGroup -Name $ToolResourceGroup  -ErrorAction SilentlyContinue

if (!$rg) {
    Write-Verbose "Resource Group [$ToolResourceGroup] doent exist, creating it!"
    New-AzResourceGroup -Name $ToolResourceGroup -Location $defaultLocation -Tag $ToolTags
}

Write-Verbose "Check if automation account exists"
$aa = Get-AzAutomationAccount -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName
if (!$aa) { 
    Write-Verbose "Automation Account required for cleanup doesnt not exist!"

    Write-Verbose "Creating automation account, this required for the cleanup"
    New-AzAutomationAccount -ResourceGroupName $ToolResourceGroup -Name $automationAccountName -Location $defaultLocation -Tag $ToolTags

    #TODO: take this outside to enable retries and check if RunAs Is created then skip the step
    Write-Verbose "Creating automation account, Run As Account"
    New-RunAsAccount -ResourceGroup $ToolResourceGroup -AutomationAccountName $automationAccountName -ApplicationDisplayName "$automationAccountName-runas" -SubscriptionId $subscriptionId -SelfSignedCertPlainPassword "P@ssw0rd1"


    #Write-Verbose "Import AzureRm.HdiInsight Module to Automation Account"
    #New-AzAutomationModule -Name AzureRM.HDInsight -ResourceGroupName $AutomationAccountResourceGroup -AutomationAccountName $automationAccountName -ContentLink $ModuleContentUrl
}

Write-Verbose "Check if runbook for cleanup exists"
$runbook = Get-AzAutomationRunbook -Name $runbookName -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName

if (!$runbook) { 
    Write-Verbose "Create Runbook for $runbookName"
    New-AzAutomationRunbook -Name $runbookName -Type PowerShell -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName -Tags $ToolTags

    Write-Verbose "Import local script to automation account"

    #Downlaod Script Locally
    $url = "https://psg-prod-eastus.azureedge.net/packages/delete-azureresource.1.0.0.1.nupkg"
    $output = "$env:TEMP\Delete-AzureResource.zip"
    Invoke-WebRequest -Uri $url -OutFile $output

    Expand-Archive -LiteralPath $output -DestinationPath "$env:TEMP\Delete-AzureResource" -Force
    $runbookPath = "$env:TEMP\Delete-AzureResource\Delete-AzureResource.ps1"

    Import-AzAutomationRunbook -Name $runbookName -Path $runbookPath -Type PowerShell -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName -Force
    
    #TODO: Clean up temps when finish
    Write-Verbose "Publish Runbook"
    Publish-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $ToolResourceGroup -Name $runbookName
}


$postfix = ""
if ($PSBoundParameters.ContainsKey("RefNumber")) {
    $postfix = "-" + $RefNumber.Substring($RefNumber.Length - 3)
}
if ($resource){
    $scheduleName = "Cleanup-Resource-" + $resource.ResourceName + $postfix
}
else {
    $scheduleName = "Cleanup-ResourceGroup-" + $rg.ResourceGroupName + $postfix
}

Write-Verbose "Check if scheduleName [$scheduleName] exists"
$schedule = Get-AzAutomationSchedule -AutomationAccountName $automationAccountName -ResourceGroupName $ToolResourceGroup -Name $scheduleName

if (!$schedule) {

    Write-Verbose "Create Schedule"
    if ($resource){
        $params = @{"ResourceId" = "$ResourceId"; }
    } 
    else {
        $params = @{"ResourceGroup" = "$ResourceGroup"; }
    }
    
    $timezone = ([System.TimeZoneInfo]::local).Id
    Write-Verbose "Your Timezone: $timezone"


    New-AzAutomationSchedule -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName -Name $scheduleName -StartTime $DeleteAfter -OneTime -TimeZone $timezone
    Write-Verbose "Register Schedule"
    Register-AzAutomationScheduledRunbook -Name $runbookName -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName -ScheduleName $scheduleName -Parameters $params
}